news 2026/10/1 20:16:24

HTML注册登录实现指南:从localStorage到真实API对接

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HTML注册登录实现指南:从localStorage到真实API对接

简介:这是一份面向前端初学者的HTML注册登录演示工程,以简单直观的方式展示用户信息填写、单选多选、下拉框选择以及用户名与密码正则校验等常见表单交互。无论是文本输入、性别或爱好选择,还是通过下拉框完成职业或城市选择,页面都提供了完整的前端实现。凡是存在未通过校验或空白项,页面都会阻止注册提交,并在校验成功后跳转到注册成功界面,稍后再自动返回注册页,完整演示了前端表单从验证到跳转的闭环流程。压缩包共4个文件,含2个HTML页面和2张JPG图片,分别对应表单页面、成功反馈页面及背景视觉素材,整体仅559KB,结构简单,适合直接下载学习。目前已有8690人学习或下载,受到不少初学者关注。通过阅读代码可以理解表单元素布局、事件处理与正则校验的配合方式,也可基于现有逻辑扩展手机号、邮箱等更多校验项,或调整成功页停留时长,是个易上手、可改造的入门练习。

1. 只做一个 HTML 注册登录:先看清边界,再动手

搜索"html实现用户简单的注册登录"的人,往往不是产品经理,而是被课程设计、毕设或交付 demo 逼到跟前的开发者。真正动手会发现这个"简单"里藏着三个要决策的点:用户数据存哪、页面刷新后怎么记住登录状态、以及后面到底接不接后端。

这篇文章按我平时做演示页面的顺序来。先用一个 HTML 文件配合 localStorage 跑通注册、登录、下线,把最小闭环完整放到浏览器里;然后把表单校验和按钮反馈做扎实;再讲清楚静态 HTML 怎么对接真实登录接口;最后写五个我在这个需求上实际踩过的坑。读完后你能拿到一份可以直接跑的注册登录页面,同时知道它离生产环境还差多远。

适合人群很明确:前端刚入门的新手、被课程设计催着交 demo 的学生,以及需要快速给后端接口配一个测试页面的开发。下面所有代码都以可复现为准,你直接建一个 HTML 文件也能跑。

2. 用 localStorage 跑通注册、登录、下线:完整代码与参数说明

在没有后端的情况下,浏览器 localStorage 是存用户数据最常用的方案。它对同源页面开放,按 key-value 存储,JSON 序列化后可以放数组和对象,刷新页面不会丢。登录态我习惯放 sessionStorage,好处是关闭标签页后会话自然失效,不会出现"关掉浏览器再打开还是登录状态"的奇怪体验。

2.1 页面结构:两个 form 切换,还是同屏展示

常见做法是同一个容器里放登录、注册两个 form,上面用 tab 按钮切换,而不是做两个 HTML 页面。后者在真实项目里更常见,但 demo 阶段用 tab 切换可以让评审一眼看到全部功能。

<!doctype html> <html lang="zh-cn"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>注册登录演示</title> </head> <body> <div id="status-bar" class="hidden"> 当前用户:<span id="current-user"></span> <button type="button" id="logout-btn">下线</button> </div> <div class="auth-box"> <button type="button" class="tab active">const USERS_KEY = 'auth_demo_users'; const SESSION_KEY = 'auth_demo_session'; function getUsers() { const raw = localStorage.getItem(USERS_KEY); try { return JSON.parse(raw) || []; } catch (e) { return []; } }

JSON.parse(raw)必须放进 try/catch。因为 localStorage 内容可能被用户手抖改坏,或上一版本写入的数据不是标准 JSON。一旦解析失败,直接返回空数组,不要让页面白屏。|| []处理的是"key 存在但值为 null"的情况。

注册表单的提交事件是整个 demo 的核心:

document.getElementById('register-form').addEventListener('submit', function (e) { e.preventDefault(); const msg = document.getElementById('register-message'); const username = document.getElementById('reg-username').value.trim(); const password = document.getElementById('reg-password').value; const confirm = document.getElementById('reg-confirm').value; if (username.length < 3) { msg.textContent = '用户名至少 3 个字符'; return; } if (password.length < 6) { msg.textContent = '密码至少 6 位'; return; } if (password !== confirm) { msg.textContent = '两次密码不一致'; return; } const users = getUsers(); if (users.some(u => u.username === username)) { msg.textContent = '用户名已存在'; return; } users.push({ username: username, password: password, createdAt: Date.now() }); localStorage.setItem(USERS_KEY, JSON.stringify(users)); msg.textContent = '注册成功,请切到登录页'; });

三个关键点:用户名先trim()再去校验和存储,否则用户注册admin,登录时输入admin永远匹配不上;密码不做 trim,因为用户密码可能真的包含首尾空格,虽然少见,但过滤掉反而制造新问题;用some做用户名查重,比 indexOf 直观,找到第一个重复就阻止注册。

users.push的对象里除了用户名和密码,我习惯加一个createdAt时间戳。后面做用户列表、会话过期时间都用得上,现在不存的话,之后只能把旧数据全清掉重来,这是我自己吃过亏的地方。

2.3 登录与下线:sessionStorage 管理会话状态

登录逻辑和注册是对应的。区别在于登录成功时要把当前用户写进 sessionStorage,并立刻更新页面 UI。

function renderBySession() { const raw = sessionStorage.getItem(SESSION_KEY); if (!raw) return; try { const session = JSON.parse(raw); document.getElementById('status-bar').classList.remove('hidden'); document.getElementById('current-user').textContent = session.username; } catch (e) { sessionStorage.removeItem(SESSION_KEY); } } document.getElementById('login-form').addEventListener('submit', function (e) { e.preventDefault(); const msg = document.getElementById('login-message'); const username = document.getElementById('login-username').value.trim(); const password = document.getElementById('login-password').value; const users = getUsers(); const matched = users.find(u => u.username === username && u.password === password); if (!matched) { msg.textContent = '用户名或密码不对'; return; } sessionStorage.setItem(SESSION_KEY, JSON.stringify({ username: matched.username, loginAt: Date.now() })); renderBySession(); msg.textContent = '登录成功'; }); document.getElementById('logout-btn').addEventListener('click', function () { sessionStorage.removeItem(SESSION_KEY); location.reload(); }); renderBySession();

登录态放在 sessionStorage 而不是 localStorage,是因为 sessionStorage 的生命周期是标签页。用户关掉页面再打开,登录态自动消失,这符合大多数 demo 对"下线"的预期。如果需求是"七天免登录",再换 localStorage 并在登录时写过期时间,后面第 6 章会讲。

下线按钮用removeItem加location.reload(),是最省事的做法。刷新后状态栏回退,表单回到初始状态,所有 JS 变量也被清掉,不会出现页面残留"已登录"痕迹的尴尬。

tab 切换也不能少:

document.querySelectorAll('.tab').forEach(function (tab) { tab.addEventListener('click', function () { document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); this.classList.add('active'); document.getElementById('login-form').classList.toggle('hidden', this.dataset.tab !== 'login'); document.getElementById('register-form').classList.toggle('hidden', this.dataset.tab !== 'register'); }); });

到这里,注册、登录、下线三个动作已经闭环。如果只是交课程 demo,这份代码加一点 CSS 就够用了。但如果你把它当成一个正经项目的起点,还需要把表单校验和错误提示做得更细,这就是下一章的内容。

3. 表单校验做扎实:正则、原生属性和输入防呆

第 2 章的校验放在 JS 里提交事件内,够用但不够专业。合格的做法是"前端双层校验":HTML 原生属性拦第一层,JS 正则和关系判断拦第二层。第一层让浏览器给出即时提示,第二层保证逻辑正确。

3.1 HTML5 原生校验:required、minlength、pattern 的参数设计

原生校验可以直接写在 input 标签上,浏览器会接管空值、格式、长度等检查,不通过时表单不触发 submit 事件。

属性作用推荐参数
required必填,空值阻止提交无参数
minlength最小长度6
maxlength最大长度20
pattern正则校验看业务规则
autocomplete控制自动填充new-password

给注册表单加上原生校验后长这样:

<input type="text" id="reg-username" required pattern="[\u4e00-\u9fa5A-Za-z0-9_]{2,20}" title="用户名2-20位,支持中文、字母、数字、下划线"> <input type="password" id="reg-password" required minlength="6" maxlength="20" pattern="(?=.*\d)(?=.*[A-Za-z])[\S]{6,20}" title="密码6-20位,必须包含字母和数字"> <input type="password" id="reg-confirm" required minlength="6" maxlength="20">

pattern里的[\u4e00-\u9fa5A-Za-z0-9_]意思是一到多个中文字符、英文字母、数字或下划线。HTML 的 pattern 属性在浏览器底层用的是 JS 正则,所以\u4e00写法有效。(?=.*\d)是正向预查,表示密码里至少要有一个数字,(?=.*[A-Za-z])表示至少要有一个字母,[\S]{6,20}表示 6 到 20 位非空白字符。

注意minlength只限制长度,限制不了"密码必须包含字母和数字",所以要和pattern配合。maxlength一定要写,否则 1 万位密码也能被塞进 JSON 并写入 localStorage,直接卡死页面。

3.2 前端校验的边界:确认密码、中英文用户名和空白字符

HTML 原生校验处理不了"两次密码是否一致",因为它看到的只是两个独立的密码框。这个判断必须放到 JS 里,在提交前比较两个值。

常见的校验函数可以统一收敛:

function validateRegister(username, password, confirm) { if (!/^[\u4e00-\u9fa5A-Za-z0-9_]{2,20}$/.test(username)) { return '用户名需为2-20位中文、字母、数字或下划线'; } if (password.length < 6 || password.length > 20) { return '密码长度需在6到20位之间'; } if (!/[A-Za-z]/.test(password) || !/\d/.test(password)) { return '密码必须同时包含字母和数字'; } if (!/\S/.test(password)) { return '密码不能包含空格'; } if (password !== confirm) { return '两次输入的密码不一致'; } return ''; }

用户名是否允许中文,取决于你的业务。课程 demo 通常允许中文,所以正则里用\u4e00-\u9fa5这一段 Unicode 中文字符区间。\S判断的是非空白字符,它会把普通空格、全角空格\u3000都挡在外面。这样注册时把空格封死,比登录时再trim()更省事。

这里有一个边界要讲清楚:前端正则只是体验层,不是安全层。用户完全可以绕过页面直接给后端发请求,所以这些规则在后端接口里必须再校验一遍。前端做得再漂亮,也只是减少后端无效请求和服务端压力。

3.3 给按钮加一个"处理中"状态:体验上的最后一公里

很多人做完注册登录,发现一个隐患:用户连点两次注册按钮,localStorage 里出现了两个相同名字的用户。第二次写入会被some挡住,但按钮没有任何反馈,用户以为没点上。

常见的处理方式是在提交时禁用按钮,并改成"注册中...",等逻辑执行完再恢复:

const btn = this.querySelector('button[type="submit"]'); btn.disabled = true; btn.textContent = '注册中...'; try { // 这里执行第 2.2 节里的注册校验和写入逻辑 } finally { btn.disabled = false; btn.textContent = '注册'; }

finally保证代码无论正常执行还是抛出异常,按钮都会恢复,不会出现"按钮永远灰色"的坑。在模拟接口的场景里,你还可以用setTimeout包一层再执行注册逻辑,模拟 300 毫秒网络延迟,让评审看到状态变化。

如果后面接了真实接口,这里应该配合async/await等接口返回再恢复按钮。注意接口失败时要把错误信息message.textContent显示在表单下方,不能只把按钮恢复,否则用户根本不知道发生了什么。这块代码不多,但对 demo 质感的提升最明显。

4. 从演示到真实接口:fetch 对接注册登录 API 的常见做法

第 3 章做完,你已经有一个能在浏览器里自娱自乐的注册登录系统。但把它部署到服务器上,让不同电脑上的用户都能登录,就必须接后端接口,把用户数据从 localStorage 搬到数据库。

4.1 为什么纯 HTML 还要区分本地存储与后端存储

localStorage 的数据只在当前浏览器的当前源下可见。你在自己电脑上注册的用户,换一台电脑根本不存在,更别提同一个用户在多台设备上共享数据。真实业务的注册登录,永远是"前端 HTML 负责采集和展示,后端接口负责存储和校验"。

所以标题里的"html实现注册登录",更准确的理解是"用 HTML 把注册登录的表单和交互实现出来"。数据层要么用 localStorage 做本地 demo,要么用 fetch 对接远程接口。两种方案不是互斥的,我的习惯是在代码里留一个开关:

const USE_API = false; async function registerUser(username, password) { if (!USE_API) { // 走 localStorage 逻辑 return true; } // 走接口逻辑 const res = await fetch('/api/register', { ... }); return res.ok; }

这样同一个页面既能本地演示,也能在后端就绪后切换到接口模式。比写两套 HTML 好维护得多。

4.2 用 fetch 提交登录:请求体、Content-Type 与错误处理

后端接口最常见的登录协议是 POST 一个 JSON 体,服务端校验后返回登录态。前端用 fetch 发起的请求长这样:

async function apiLogin(username, password) { const response = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ username: username, password: password }) }); const data = await response.json().catch(() => ({})); if (!response.ok) { const message = data.message || `HTTP ${response.status}`; throw new Error(message); } return data; }

method: 'POST'是登录接口的铁律,不要用 GET 把用户名密码拼在 URL 上。Content-Type: application/json告诉后端你发的是 JSON 而不是表单格式,没有这个头,很多后端框架会把整个 body 解析成空对象。

credentials: 'include'表示跨域请求也要携带 Cookie。如果登录成功依赖服务端种 Cookie 来维持会话,这一行必须写。注意同源请求不写也默认带上,但一旦前端页面和后端接口不同端口,不带这行就会遇到"明明返回了 Set-Cookie,浏览器就是不存"的怪问题。

response.json().catch(() => ({}))是防呆写法。后端如果返回的不是 JSON 而是 502 页面,直接await response.json()会抛异常,导致真正的错误信息被吞掉。

4.3 401 未登录与登录态失效:前端要统一处理的场景

真实项目里,请求接口经常遇到{"code":401,"message":"未登录,请登录!"}这种返回。它出现在两种场景:一是你压根没登录就请求了需要登录的接口,二是登录状态过期。

前端不能只在登录页里处理 401,而是要在所有需要登录态的接口请求里统一处理。常见做法是封装一个请求函数:

async function request(url, options = {}) { const response = await fetch(url, { headers: { 'Content-Type': 'application/json' }, credentials: 'include', ...options }); if (response.status === 401) { sessionStorage.removeItem(SESSION_KEY); location.href = 'login.html'; const data = await response.json().catch(() => ({})); throw new Error(data.message || '未登录,请登录!'); } return response; }

这里的关键是:401 时清理本地登录态并跳转登录页。很多翻车现场是接口返回 401,前端却只把错误信息 alert 出来,用户点掉弹窗后又在一个"假登录"状态下操作,接着又收到 401,形成死循环。

如果后端用 token 而不是 Cookie,登录成功时返回的data.token要放进 sessionStorage,之后每次请求加Authorization: Bearer <token>头。这和 401 处理是同一套逻辑,只是把凭证从 Cookie 换成了显式 token。

5. 注册登录避坑记录:5 个让我翻车又爬出来的细节

这一章是我在这个标题上最有体会的部分。看似简单的注册登录,坑全在细节里。下面每条都按"现象、原因、解决"来写。

5.1 localStorage 读取返回值 null/解析异常:不能想当然

现象:页面第一次打开时,localStorage.getItem(USERS_KEY)返回 null,直接用null.length或null.map直接抛错,注册按钮点一下页面就没反应。

原因:本地存储没有数据时,getItem返回的就是 null。另外如果之前有人用手改过 localStorage,里面存了不是 JSON 格式的内容,JSON.parse也会抛异常。

解决:读取函数里必须同时做 null 判断和异常捕获。第 2 章的getUsers已经演示了标准写法:JSON.parse(raw) || []加try/catch。任何从 localStorage 读取的数据都要当成"不可信数据"处理,因为你控制不了用户的浏览器环境。

5.2 浏览器自动填充密码带来的假"已登录"状态

现象:刷新登录页面,用户名和密码被浏览器自动填上,用户没输入任何东西直接点登录,结果真的登录成功了。这本身没问题,但到了注册页面,浏览器会把你自己的密码填到"确认密码"框里,导致注册反复提示"两次密码不一致"。

原因:浏览器密码管理器会匹配type="password"的输入框,自动填充。它分不清你的表单是登录还是注册。

解决:登录表单的密码框设置autocomplete="current-password",注册表单的密码和确认密码框统一设置autocomplete="new-password"。这比autocomplete="off"更受浏览器尊重。如果还不行,在页面加载时强制清空注册表单的两个密码框值,用 JS 防止填充残留。

5.3 用户名明明一样却登录不上:空白字符和全角字符

现象:注册时输入测试用户,登录时输入测试用户,中间多了个肉眼几乎看不出来的空格或全角空格\u3000,结果用户名或密码不对。

原因:用户在手机输入法里带出中文全角空格,或首尾不小心敲了空格。第 2 章只在注册和登录时trim()还不够,因为trim()只会去掉普通空格,去不掉全角空格。

解决:把所有输入先统一规范化再比较。我习惯写一个函数:

function normalizeUsername(name) { return name.replace(/[\u3000\s]/g, '').toLowerCase(); }

这里把全角空格\u3000和所有空白符删掉,同时转小写。注册时存入规范化后的用户名,登录时也用同样规则处理,两边永远一致。注意如果业务允许用户名区分大小写,就不要toLowerCase(),否则会把Admin和admin当成同一个人。

5.4 两个标签页登录状态不同步:storage 事件的正确姿势

现象:开两个标签页同进登录页,A 标签页登录成功,B 标签页还显示未登录;在 B 标签页下线,A 标签页依然是登录状态。

原因:sessionStorage 是标签页级隔离的,A 写的 sessionStorage 在 B 根本读不到。storage事件可以跨标签页同步 localStorage,但触发storage事件的窗口不包括当前写入的标签页,而且 sessionStorage 不触发这个事件。

解决:如果需求是"多个标签页共享登录状态",就把登录态从 sessionStorage 移到 localStorage,然后监听storage事件:

window.addEventListener('storage', function (e) { if (e.key === SESSION_KEY) { location.reload(); } });

A 标签页写入 localStorage 后,B 标签页会收到事件并刷新页面,自动更新登录 UI。注意storage事件在 A 标签页自身不会触发,所以不要依赖这个事件更新当前标签页的 UI,写入后主动调用renderBySession()就行。

5.5 file:// 协议访问接口报跨域:换个本地服务器就解决

现象:直接在文件管理器里双击 HTML,用 Chrome 打开,控制台报Access to fetch at 'http://localhost:8080/api/login' from origin 'null' has been blocked by CORS policy,或 fetch 直接 404。

原因:当 HTML 是以file://协议打开时,浏览器把它当作一个null源,发送请求时Origin头是null,后端一般不会允许这种来源。即便后端允许,相对路径/api/login也会指向本地文件系统根目录,而不是你的项目目录。

解决:不要用file://预览带接口请求的页面,启动一个本地静态服务器。最简单的两种方式:

# 方式一:VS Code 装 Live Server,右键 Open with Live Server # 方式二:用 Python 自带服务器 python3 -m http.server 8080

然后在浏览器访问http://localhost:8080/login.html。页面和后端接口同源后,credentials和相对路径就都正常了。这个坑极其隐蔽,不写出来能让人卡一下午。

6. 注册登录写完之后:3 个验证技巧和一个进阶方向

代码写完后别急着交付,花十分钟验证一遍,能省掉很多"我这里明明是好的啊"的争议。

6.1 用开发者工具完成一次完整验收

打开浏览器 F12,切到 Network 面板,勾选 Preserve log,然后依次做:注册一个用户、切登录、刷新页面、下线。每一步看三件事:Console 里有没有红色报错,Network 里有没有请求失败,Application 面板里 localStorage 和 sessionStorage 的键值是否符合预期。

特别要验证的是刷新后登录态是否还在。如果下线后刷新又变成登录状态,多半是SESSION_KEY没清干净,或者renderBySession读到了旧缓存。用 Application 面板手动删掉 sessionStorage 里的数据再刷新,能快速复现和排查这类问题。

6.2 把多个 HTML 打包成单文件分发的两种做法

如果你的页面包含多个 HTML,比如 login.html 和 register.html,要发给别人演示时,常见做法是把 CSS 和 JS 内联到一个 HTML 里,做成单文件。内联时注意保留完整的<!doctype html><html lang="zh-cn">和<meta charset="utf-8">,否则中文可能乱码。

另一个做法是保持多文件结构,把整个文件夹压缩成 zip 发给对方,让对方本地启动静态服务器访问。没有服务器时就让对方双击 index.html,但页面里不能用 fetch 和 ES6 module,因为file://协议下这些能力受限。打包前检查一下所有相对路径,别漏了图标或图片文件,这是最常出现的交付翻车点。

6.3 一个值得做的加强:密码哈希与记住我

localStorage 里存明文密码,只能算是 demo。稍微认真一点,可以用 Web Crypto API 做哈希:

async function hashPassword(password) { const data = new TextEncoder().encode(password); const digest = await crypto.subtle.digest('SHA-256', data); return Array.from(new Uint8Array(digest)) .map(b => b.toString(16).padStart(2, '0')) .join(''); }

注册时把hashPassword(password)的结果存进 localStorage,登录时也先哈希再比对。注意crypto.subtle只在安全上下文里可用,也就是localhost或 HTTPS 页面,file://打开时会直接报错,这也是我经常强调用本地服务器预览的原因之一。

"记住我"的常见做法是登录成功后在 localStorage 存一个带过期时间的标记:

const expires = Date.now() + 7 * 24 * 60 * 60 * 1000; localStorage.setItem('remember_me', JSON.stringify({ username: matched.username, expires: expires }));

页面加载时判断expires是否大于当前时间,没过期就自动恢复登录态。这里要注意别把密码存进去,只存用户名和过期时间,否则记住我功能反而成为安全隐患。

我最初写这类 demo 时,最常翻车的地方就是忘了统一trim和全角空格处理,导致用户注册成功却登不进去,一度以为密码学出了问题。后来所有输入在进入校验前先过一层规范化,翻车明显少了很多。做注册登录,前端大部分功力不在炫技,而在这些细枝末节。希望这些实现和排查思路,能帮你在课程设计或实际项目里少走一段弯路。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 20:16:00

NVMe驱动开发速通:从PCIe枚举到QEMU实战

刚接触NVMe的时候&#xff0c;我干过一件很蠢的事&#xff1a;插上新固态&#xff0c;系统识别了&#xff0c;就以为“驱动这东西不用管”。直到有一次在服务器上看到dmesg里一堆nvme相关日志&#xff0c;又在一个嵌入式项目里被要求把一块NVMe盘从底层跑起来&#xff0c;我才意…

作者头像 李华
网站建设 2026/10/1 20:14:26

工控现货:工业备件的确定性交付体系

1. 项目概述&#xff1a;什么是“工控现货”&#xff1f;它解决的不是库存问题&#xff0c;而是产线停摆的生死时速“工控现货”这个词最近在自动化工程师、设备维护主管、产线调度员的朋友圈里高频出现&#xff0c;但它绝不是电商平台上标着“当天发货”的普通商品标签。我干了…

作者头像 李华
网站建设 2026/10/1 20:13:35

OpenCV RotatedRect全面解析:角度、宽高、顶点顺序与版本陷阱

有一类问题经常在群里被翻来覆去地问&#xff1a;“为什么我用minAreaRect拿到的angle是负数&#xff1f;”“为什么RotatedRect的宽和高跟我在图上看到的不一样&#xff1f;”“为什么boxPoints返回的四个点顺序每次都不一样&#xff1f;”老实说&#xff0c;这些问题我早期也…

作者头像 李华
网站建设 2026/10/1 20:10:35

安全PLC不等于安全功能:从急停回路到完整功能安全的落地方法

去年在一条汽车焊装线上验收急停安全功能&#xff0c;甲方工程师指着柜子里的安全PLC问我&#xff1a;“这套东西SIL3认证都齐了&#xff0c;是不是安全功能就算做完了&#xff1f;”我顺着他手指的方向看了看柜子侧面的接线——急停按钮的常闭触点确实拆了两路进PLC&#xff0…

作者头像 李华