ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

HTML登录页从结构到交互:CSS布局、JS校验与移动端适配

HTML登录页从结构到交互:CSS布局、JS校验与移动端适配 很多人第一次搜HTML 登录页面心里想的其实特别简单给我一段能跑的代码我把用户名密码框摆上去按钮一点就完事。真动手才发现不是这么回事——输入框在手机上会跳、密码框被浏览器自动填充塞进了邮箱、点一下按钮整页刷新、样式在 Chrome 里好看在 Safari 里歪掉。我这些年带过不少刚入门的朋友也在实际项目里维护过好几套登录页踩的坑基本就那么几类。这篇就按我自己的做法把用 HTML 做一个简单的登录页面这件事从头到尾拆开把结构、样式、交互、排查四个层面的东西讲透。它适合完全没写过页面的新手拿来照着敲第一遍也适合写过几个静态页但一直觉得登录页哪里不对的人对照着检查。全文围绕 HTML 这个核心配合 CSS 和少量原生 JS不引入任何框架和构建工具一个文件双击就能在浏览器里跑起来。1. 先想清楚简单这两个字到底指什么我在帮别人看代码的时候发现一个问题大部分翻车不是因为写得难而是因为一开始就没想清楚边界。简单这个词在登录页这个场景里其实有三层意思——页面元素少、技术栈少、依赖少。但元素少不等于可以省略结构技术栈少不等于可以省略交互依赖少不等于可以省略体验。这三层边界画清楚了后面写起来就顺了。1.1 一个能上线的登录页最小元素集有哪些先把清单列出来照着这个清单打勾缺哪个心里有数。我一般会把它分成三组第一组是必选页面标题h1 或者可视化的品牌名、账号输入框、密码输入框、提交按钮。这四个缺一个页面就不能用。第二组是强烈建议密码明文切换按钮、忘记密码入口、注册入口、错误提示区域。这四样不写也能跑但用户会骂人。尤其是密码明文切换在移动端几乎是刚需手机键盘打错密码的概率比电脑高得多。第三组是看情况记住我、第三方登录、图形验证码占位、页脚版权。这些取决于业务个人练手项目里可以只留一个记住我复选框把样式练到就行。我见过有人一上来就把三组全做结果第一组里的可访问性都没做对。推荐的做法是先把第一组做到键盘能走通、屏幕阅读器能念出来再往第二组加东西。顺序反了返工量会很大。1.2 登陆和登录这两个词顺手说一句搜索框里输入登陆页面的人非常多登录页面也不少。从用词规范上讲登录指的是登入系统这个动作是更准确的写法我在正文里统一用登录。但从做内容的角度两种写法都有人在搜标题和描述里都覆盖一下没坏处正文里不需要刻意重复。这件小事之所以提一嘴是因为我发现很多新手会纠结我写错了会不会被人笑其实完全不用把页面写对更重要。1.3 纯 HTML 加 CSS 加 JS边界在哪些地方这是最需要提前想明白的一点。原生三件套能做的页面结构、样式布局、按钮一点的即时反馈、格式校验非空、长度、邮箱格式、密码明文切换、提交时的按钮状态。原生三件套做不到或者不该做的真正的身份校验。前端写的任何校验都只是帮用户少犯错绕过它成本几乎为零真正判断账号密码对不对必须放在服务端。安全的会话状态维护。登录成功后凭证怎么存、存哪、多久失效这是后端和协议层的事。防暴力尝试。频率限制、验证码触发策略都必须有服务端参与。我特别想强调第一点因为它经常被新手误解成我写了正则就等于做了安全。前端正则的价值是省一次网络请求、让用户立刻知道格式不对仅此而已。把这个定位摆正代码会写得更克制也不会去抄一些看起来很唬人但实际没用的加密片段。1.4 新手最容易走的三个弯路第一个是直接扒大厂登录页的产物代码。那些页面是组件化、构建工具、设计系统层层堆出来的产物里有大量你不需要的东西抄下来只会得到一堆看不懂的类名和一层套一层的 div学不到任何结构思路。第二个是用 div 堆一切。输入框旁边放个 div 当标签点文字不聚焦屏幕阅读器念不出这俩是什么关系。这类问题不会报错但会在真实使用里不断制造小摩擦。第三个是把样式写成行内。写到第五个元素就开始失控改一个颜色要找遍整个文件。哪怕只有几十行 CSS也老老实实单独开一段 style 或者独立文件。2. 骨架搭建结构对了后面的事都好办结构这一层决定了你后面要写多少补丁。我的一般习惯是先把 HTML 结构完整写完一个样式都不加直接用浏览器打开。这时候页面应该是能用的——输入框能打字Tab 键能按顺序走完回车能提交按钮有默认样式。这个状态如果是对的加样式只是让它变好看如果这个状态就是错的加样式只会把错误盖住。2.1 开头的 !doctype html 别省很多人复制代码的时候会把这行丢掉觉得它就是一串没用的声明。实际上浏览器靠它决定用哪种渲染模式。写了它页面进入标准模式盒模型的宽度计算按 CSS 规范来不写它某些情况下会进入一种兼容老页面的模式宽度计算方式不一样你按标准模式算好的布局会莫名其妙溢出几个像素。这类问题最难查因为它不报错只是差一点点。完整的头部我一般这么写!doctype html html langzh-cn head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1 meta namedescription content账号登录页面 title登录 - 我的站点/title /head body !-- 页面内容 -- /body /htmllangzh-cn不写也能显示但写了以后浏览器知道这是中文页面字体选择、断行规则、屏幕阅读器的发音都会更合理。charset必须放在 head 里靠前的位置放在后面的话前面的中文可能变乱码。viewport这行是做移动端适配的前提不写它手机上看到的是缩得很小的桌面版。2.2 为什么一定要用 form 包住输入框这是我认为登录页结构里最重要的一条。用 form 包裹你能白拿四个好处一是回车提交。当 form 里存在一个 type 为 submit 的按钮时光标停在单行输入框里按回车会触发表单提交。用 div 包的话这个行为要自己写键盘事件写完还要处理输入法组合状态纯属给自己找活。二是浏览器的密码管理器能识别。识别出这是个登录表单之后用户点一下就能自动填充这对回头客的体验影响非常大。识别不出来的话用户每次都得手动敲。三是移动端键盘的前往键。手机软键盘右下角的按钮文字会跟着变输入框之间用下一项跳转最后一个输入框显示前往这个体验是浏览器免费给的。四是无障碍。屏幕阅读器会播报表单以及表单里的字段数量用户心里有数。对应的写法大概是这样form classlogin-form idloginForm novalidate h1 classform-title账号登录/h1 !-- 字段 -- /formnovalidate这个属性要不要加取决于你打算用浏览器自带的校验气泡还是自己写提示。我自己的偏好是加novalidate然后自己控制提示的样式和位置因为浏览器自带的那个气泡在不同浏览器里长得完全不一样而且点了提交按钮就消失用户来不及看完。如果你想省事不加它配合required也能用先把功能跑通再说。注意不管用哪种方式都不要把提交按钮写成div加点击事件。这样回车提交会失效键盘用户会卡住。2.3 label、id、name 这三者是怎么配合的这三个东西经常被混着用我把它们的分工说清楚id是这个元素在页面里的唯一编号label通过forxxx指向它点标签文字就等于点输入框。这是纯体验提升成本极低没理由不做。name是提交时这个字段的键名服务端拿到的就是usernamexxxpasswordyyy这样的数据。它和 id 可以同名也可以不同名但都必须有。label本身是可见的文字标签。有些设计喜欢把 label 藏起来只用 placeholder 代替我的建议是不要。placeholder 在用户开始输入之后就消失了用户回头检查填了什么的时候会失去参照而且它的对比度通常偏低视力不好的用户看不清。真要做极简风格就把 label 用屏幕阅读器可见但视觉隐藏的方式留着。正确的写法div classfield label forusername账号/label input idusername nameusername typetext autocompleteusername placeholder手机号或邮箱 required /div这里autocompleteusername是给密码管理器看的提示告诉它这个字段是账号。别小看这个属性写了它之后自动填充的准确率会明显提升。2.4 输入框类型怎么选一张表说清场景推荐 type关键属性说明账号手机或邮箱textautocompleteusername如果确定只收邮箱用 email移动端键盘会带 密码passwordautocompletecurrent-password现在是改密码场景就用 new-password纯手机号telinputmodenumeric用 tel 而不是 numbernumber 会带上加减号和小数点验证码textinputmodenumericmaxlength6不要用 number会丢掉前导零邮箱emailautocompleteemail自带格式提示但别指望它替代正则搜索框search—移动端回车键会显示搜索这张表我自己每次做表单都会翻一遍。最常出错的是验证码用 number用户输入012345提交上去变成12345这个问题排查起来能让人抓狂。2.5 h1 和 section 要不要用我的习惯是登录页本身就是一个独立页面用它自己的 h1 放品牌名或者登录两个字。如果登录框是嵌在别的页面里的弹窗那就用 h2。一个页面只出现一个 h1这个是硬规矩。至于 section、header、footer 这些标签能用就用不能用也不影响功能但用了之后结构在阅读模式下会清晰很多。3. 样式部分从能看到像样的关键几步CSS 这块我不打算写太多花哨的效果重点放在几个真正容易翻车的点上。先说结论登录页的样式难度不在装饰在于居中、输入框状态、自动填充覆盖、移动端适配这四件事。把这四件事做对页面就已经超过大部分网上抄来的模板了。3.1 居中的三种写法我为什么选 flex登录框居中是最基础的诉求常见写法有三种方案写法优点缺点绝对定位 transformposition:absolute; top:50%; left:50%; transform:translate(-50%,-50%)兼容性好元素脱离文档流内容超高时会顶出屏幕且无法滚动Flex容器display:flex; align-items:center; justify-content:center语义干净内容超高自动滚动基本没有Grid容器display:grid; place-items:center代码最短老版本浏览器支持稍晚我现在一律用 flex理由就是那个内容超高自动滚动。手机上键盘弹出来、或者浏览器缩放比例很大的时候登录框会变高绝对定位那套很容易把顶部内容顶到视口外面用户滚都滚不到这是真实会发生的投诉。body { min-height: 100vh; margin: 0; display: flex; align-items: center; justify-content: center; background: #f4f6f9; font-family: system-ui, -apple-system, Segoe UI, Microsoft YaHei, sans-serif; }min-height: 100vh而不是height这样内容变高的时候容器会跟着长不会截断。字体用 system-ui 开头页面会直接使用系统默认字体中文会自动落到雅黑或者苹方视觉上比硬指定一个英文字体再回退要自然得多。3.2 盒模型和宽度这件事一定要统一我建议在样式最开头加这么一段*, *::before, *::after { box-sizing: border-box; }不写它的话你给一个宽度 100% 的输入框再加左右各 16px 的内边距和 1px 的边框它的实际宽度就是 100% 34px直接撑破父容器。这个现象在调试的时候表现为输入框比卡片宽出来一截右边看不到边框很多人会以为是宽度设错了其实是盒模型在算账。加上这行之后内边距和边框都算在宽度里面你写多少就是多少心算压力小很多。3.3 输入框的四种状态缺一个都不完整一个输入框至少有四种状态需要处理默认、悬停、聚焦、禁用。其中聚焦最重要。.field input { width: 100%; height: 44px; padding: 0 14px; border: 1px solid #d8dde6; border-radius: 8px; font-size: 16px; color: #1f2937; background: #fff; transition: border-color .15s, box-shadow .15s; } .field input:hover { border-color: #b9c2cf; } .field input:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37, 99, 235, .15); }这里的box-shadow是替代默认 outline 的一种常见做法视觉上更柔和。但要注意outline: none是有代价的它会去掉键盘用户唯一的视觉定位。所以我一般会配合:focus-visible做一层保护.field input:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }:focus-visible的特点是鼠标点击时不显示键盘 Tab 过来时显示。这样两类用户都照顾到了。高度给 44px 是有依据的这是移动端触摸目标的推荐最小尺寸低于这个值手指点起来容易点偏。还有一个新手经常忽略的点输入框的 font-size 不要小于 16px。手机浏览器有个规则聚焦到字号小于 16px 的输入框时会自动放大整个页面用户输入完再退出去页面还是放大的状态得手动缩回来体验非常糟。十六像素这个值正好卡在触发线上。3.4 覆盖自动填充的黄色背景这个坑几乎每个做登录页的人都踩过。浏览器自动填充之后会用它的默认样式盖掉你的背景色通常是浅黄或者浅蓝跟你的设计完全冲突。而且这个样式优先级很高直接写 background 是盖不掉的。可靠的做法是用一个巨大的内阴影来伪造背景.field input:-webkit-autofill, .field input:-webkit-autofill:hover, .field input:-webkit-autofill:focus { -webkit-text-fill-color: #1f2937; -webkit-box-shadow: 0 0 0 1000px #fff inset; transition: background-color 9999s ease-in-out 0s; }思路是用内阴影铺满整个输入框视觉上看起来就是白底。-webkit-text-fill-color是用来控制填充后的文字颜色的不写的话在某些情况下文字会变成很浅的灰色。最后那行超长的 transition 是拖住背景色变化的时机属于兼容性补丁。注意这段代码只在基于同一内核的浏览器上生效其他内核的浏览器本来就不覆盖背景所以不需要额外处理。如果你发现填充后文字颜色还是不对先检查-webkit-text-fill-color有没有写。3.5 用变量把颜色收一收即使页面只有几十行 CSS我也建议把颜色和圆角抽成变量:root { --brand: #2563eb; --text-main: #1f2937; --text-sub: #6b7280; --border: #d8dde6; --bg: #f4f6f9; --radius: 8px; }好处是改主题的时候只动这几行不用满文件搜索。做深色模式的时候也方便加一段媒体查询把变量换掉就行不用重写所有规则。3.6 响应式只需要一小段登录页的响应式其实很简单核心就一条小屏时卡片占满宽度并留出边距大屏时固定一个合理的最大宽度。.login-card { width: 100%; max-width: 380px; padding: 32px 28px; background: #fff; border-radius: 12px; box-shadow: 0 8px 30px rgba(17, 24, 39, .08); } media (max-width: 420px) { .login-card { padding: 24px 20px; border-radius: 0; box-shadow: none; min-height: 100vh; max-width: none; } }小屏去掉圆角和阴影、撑满整屏是我个人比较喜欢的处理方式看起来更像原生应用也省得卡片边缘出现奇怪的空白。380px 这个最大宽度也是反复试出来的经验值再宽的话输入框显得太空再窄则手机上放不下完整的按钮文字。4. 交互逻辑用最少的 JS 换最大的体验提升我一直觉得登录页的 JS 应该写得极其克制。它不需要框架不需要状态管理通常一百行以内就能把所有该做的事做完。写多了反而容易引入新问题。下面这几件事是我认为值得写的。4.1 密码明文切换这是性价比最高的一段代码。实现思路就是把 input 的 type 在 password 和 text 之间来回切。div classfield label forpassword密码/label div classpw-wrap input idpassword namepassword typepassword autocompletecurrent-password placeholder请输入密码 required button typebutton classpw-toggle>document.querySelectorAll(.pw-toggle).forEach(function (btn) { btn.addEventListener(click, function () { var input document.getElementById(btn.dataset.target); var isPwd input.type password; input.type isPwd ? text : password; btn.textContent isPwd ? 隐藏 : 显示; btn.setAttribute(aria-label, isPwd ? 隐藏密码 : 显示密码); }); });几个细节值得说按钮的type必须写成button。不写的话默认是 submit点一下密码显示会直接触发表单提交页面刷新用户一脸问号。这是我见过最高频的低级错误之一。切换之后光标位置。在某些浏览器里改变 type 会把光标移到开头。如果你很在意这个细节可以在切换后把焦点重新设回去并把选区设到末尾var len input.value.length; input.focus(); input.setSelectionRange(len, len);按钮文字用显示/隐藏而不是图标是我个人的偏好。图标方案要处理尺寸、对齐、可访问性标签成本高不少。如果一定要用图标记得加aria-label否则屏幕阅读器只会念出一个没有意义的符号。4.2 校验时机什么时候提示最合适这是体验设计里很微妙的一点。我总结的经验是三句话输入时不清除、失焦时校验、提交时全量校验。用户正在输入的时候不要弹错误他还没输完弹了就是打扰。光标离开这个字段的时候如果他填了但不合法给出提示这时候他刚好完成了这个字段的思考。点提交的时候把所有字段扫一遍把第一个出错的字段聚焦过去让用户知道该从哪改起。代码大概长这样function validateField(input) { if (!input.value.trim()) { return 这一项不能为空; } if (input.type email !/^[^\s][^\s]\.[^\s]$/.test(input.value)) { return 邮箱格式看起来不太对; } if (input.id password input.value.length 8) { return 密码至少 8 位; } return ; } function showError(input, msg) { var box document.querySelector([data-error-for input.id ]); if (box) box.textContent msg || ; input.setAttribute(aria-invalid, msg ? true : false); }提示元素上加aria-livepolite屏幕阅读器会在提示出现的时候念出来这个属性加一行就有收益很大。密码长度这个规则我要多说一句不同的系统要求不一样从 6 位到 12 位都有。前端提示的规则要和后端完全一致否则会出现前端说可以提交后端说你密码太短的割裂体验。这类不一致在真实项目里非常常见通常是因为两边各自改过而没同步。4.3 提交按钮的防重复点击用户点得比你想象中快。不处理的话一次提交可能发出去两三个请求。做法很简单form.addEventListener(submit, function (e) { e.preventDefault(); if (btn.disabled) return; // 校验通过后 btn.disabled true; btn.textContent 登录中...; // 请求结束后在 finally 里恢复 });注意恢复按钮状态一定要放在finally或者请求失败的分支里。我见过只写在成功回调里的代码结果一旦请求失败按钮就永远卡在登录中的状态用户只能刷新页面。这个 bug 在弱网环境下几乎必现。4.4 回车提交和键盘顺序前面说过form 里有 submit 按钮时回车提交是免费的。但有两个情况下会失效一是按钮被包在了别的元素里导致浏览器认为没有可提交按钮二是用键盘事件自己拦截了默认行为。最后一个输入框上加一个enterkeyhintgo手机键盘右下角会显示前往而不是默认的换行图标这个小细节能提升一点点手感和完成率。5. 排查实录这些坑我基本都踩过下面这些是我在实际调试中反复遇到的每一条都附上我自己的排查顺序。我建议你在遇到问题的第一分钟先翻这张表比在网上乱搜效率高得多。5.1 页面一点提交就刷新控制台什么都没输出九成是因为没有阻止表单的默认提交行为。浏览器对于 form 的 submit 事件默认动作就是把数据提交到 action 指定的地址并跳转。如果你没写 action它会提交到当前地址表现出来就是页面刷新一遍地址栏后面可能多出一串查询参数。排查顺序先确认监听的是submit事件而不是按钮的click事件再确认回调第一行就是e.preventDefault()最后确认按钮的 type 是 submit配合表单提交还是 button不触发表单提交。这三步走完问题基本就定位了。5.2 自动填充把账号填进了密码框或者反过来这是浏览器猜的猜的依据主要是autocomplete属性、name、id以及字段的先后顺序。经验做法账号框写autocompleteusername密码框写autocompletecurrent-password。name用username和password这种通用的词不要自创拼写。如果页面上还有别的输入框把它们和登录表单在结构上分开不要让浏览器把两个不同的场景混在一起分析。注册页的密码框要用new-password这个提示会让浏览器不要拿旧密码来填。改动之后要多测几次因为浏览器会缓存之前判断的结果最好开一个无痕窗口验证。5.3 手机上键盘弹出来挡住按钮这是移动端最常见的问题之一尤其在横屏或者小尺寸手机上。几种应对第一用min-height而不是固定高度做外层容器让内容能被滚动到。第二把整个页面放在一个可滚动的容器里键盘弹出时视口变小内容自动可滚。第三如果页面高度本身很紧可以在输入框聚焦时用 JS 把卡片往上挪一点但这个方案要处理键盘收起的事件比较麻烦我一般优先用前两种。还有一个相关的现象某些浏览器上键盘弹出会把整个页面顶上去收起之后页面没有回滚留出一片空白。这种情况多半是因为把布局绑在了100vh上。稳妥的做法是用100%配合min-height或者用100dvh这类会跟随视口的单位。5.4 输入法打字的时候实时校验一直闪用中文输入法打字每敲一个字母都可能触发input事件如果你的校验是基于input事件实时显示的提示会疯狂闪烁。解决办法是判断组合状态input.addEventListener(input, function (e) { if (e.isComposing) return; // 输入法组合中先不处理 // 正常逻辑 });isComposing在各大主流浏览器上都已经支持了加上这一行中文输入法下的体验会干净很多。这个属性知道的人不算多但在中文场景里几乎是必加项。5.5 常见问题速查表现象可能的直接原因排查动作提交后页面刷新未阻止默认行为回调第一行加 preventDefault点密码显示直接提交按钮没写 typebutton检查按钮 type输入框比卡片宽未设 box-sizing全局加 border-box手机上聚焦时页面放大输入框字号小于 16px字号调到 16px自动填充背景色盖不住优先级不够用内阴影方案覆盖聚焦没有任何视觉反馈写了 outline:none 没补替代样式加 box-shadow 或 focus-visible按钮一直显示登录中状态恢复只在成功分支里挪到 finally中文输入时提示闪烁未判断组合状态加 e.isComposing 判断6. 从能跑到顺手几个低成本的增强代码能跑起来只是及格线。下面这几件事加起来的成本可能不到半小时但对页面完整度的提升很明显。6.1 记住我到底记住的是什么先说清楚一件事前端这个复选框本身不记录任何东西它只是一个标记真正的记住是后端在登录成功后返回一个长效凭证并存在合适的存储位置。前端要做的是把这个标记随着登录请求一起发出去让后端决定凭证的有效期。页面上写起来就是一个复选框加一个 labellabel classremember input typecheckbox nameremember value1 span记住我/span /label这里把 input 包在 label 里面就不需要 for 和 id 了点文字也能勾选。这是比 for/id 更省事的一种写法两种都行。有一点要提醒不要在客户端自己拼接账号密码然后存到本地存储里做自动登录。这种做法在任何场景下都不合适正确的姿势永远是交给服务端下发有时效的凭证。6.2 第三方登录按钮的排版如果页面上要放第三方登录我的建议是别用各家的彩色圆形图标拼一排那套东西的配色和你的主色调几乎一定打架。更省心的做法是用统一的中性色按钮加文字视觉上干净很多div classdividerspan其他方式登录/span/div div classoauth-row button typebutton classoauth-btn手机号登录/button button typebutton classoauth-btn邮箱登录/button /div分界线那个.divider用的是一根横线中间压文字实现方式是伪元素加定位或者用 flex 加两条flex:1的横线。这类布局做一次就能复用一辈子值得花十分钟写明白。oauth-btn一定要写typebutton理由和密码切换按钮一样防止点它的时候把主表单提交出去。6.3 把完整代码拼起来到这里各部分都能对上了。完整的单文件版本我整理成下面这样可以直接存成 .html 打开!doctype html html langzh-cn head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1 title登录 - 我的站点/title style :root { --brand: #2563eb; --text-main: #1f2937; --text-sub: #6b7280; --border: #d8dde6; --radius: 8px; } *, *::before, *::after { box-sizing: border-box; } body { margin: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center; background: #f4f6f9; color: var(--text-main); font-family: system-ui, -apple-system, Segoe UI, Microsoft YaHei, sans-serif; } .login-card { width: 100%; max-width: 380px; padding: 32px 28px; background: #fff; border-radius: 12px; box-shadow: 0 8px 30px rgba(17, 24, 39, .08); } .form-title { margin: 0 0 24px; font-size: 22px; font-weight: 600; text-align: center; } .field { margin-bottom: 18px; } .field label { display: block; margin-bottom: 6px; font-size: 14px; color: var(--text-sub); } .field input[typetext], .field input[typepassword] { width: 100%; height: 44px; padding: 0 14px; font-size: 16px; color: var(--text-main); background: #fff; border: 1px solid var(--border); border-radius: var(--radius); transition: border-color .15s, box-shadow .15s; } .field input:focus { outline: none; border-color: var(--brand); box-shadow: 0 0 0 3px rgba(37, 99, 235, .15); } .field input:-webkit-autofill, .field input:-webkit-autofill:hover, .field input:-webkit-autofill:focus { -webkit-text-fill-color: var(--text-main); -webkit-box-shadow: 0 0 0 1000px #fff inset; transition: background-color 9999s ease-in-out 0s; } .pw-wrap { position: relative; } .pw-toggle { position: absolute; top: 50%; right: 10px; transform: translateY(-50%); padding: 4px 8px; font-size: 13px; color: var(--text-sub); background: transparent; border: 0; cursor: pointer; } .error { min-height: 18px; margin-top: 4px; font-size: 12px; color: #dc2626; } .row-between { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; font-size: 14px; } .remember { display: inline-flex; align-items: center; gap: 6px; color: var(--text-sub); cursor: pointer; } .btn-submit { width: 100%; height: 46px; font-size: 16px; color: #fff; background: var(--brand); border: 0; border-radius: var(--radius); cursor: pointer; transition: opacity .15s; } .btn-submit:hover { opacity: .92; } .btn-submit:disabled { opacity: .6; cursor: not-allowed; } media (max-width: 420px) { .login-card { max-width: none; min-height: 100vh; border-radius: 0; box-shadow: none; padding: 48px 20px; } } /style /head body main classlogin-card h1 classform-title账号登录/h1 form idloginForm novalidate div classfield label forusername账号/label input idusername nameusername typetext autocompleteusername placeholder手机号或邮箱 required div classerror>
返回列表