简介:面向Web前端初学者的HTML+CSS+JavaScript综合练习项目,完整模拟QQ登录窗口的界面风格与基础交互逻辑,适合想通过仿写真实产品界面来巩固前端基础技能的开发者。整个压缩包共11个文件,包含1个HTML结构页面、1个CSS样式表、1个JavaScript脚本文件,以及6张PNG与2张GIF界面素材,整体仅有55KB,结构紧凑,下载解压后可直接用浏览器运行查看实际效果。目前已有859人学习下载。QQ登录窗的视觉还原度较高,覆盖了输入框聚焦过渡、按钮悬停变色、用户头像切换、回车快捷登录、表单空值校验以及模拟登录成功或失败的提示反馈等交互细节;CSS部分涉及flex布局和过渡动画的使用,JavaScript部分则展示了事件监听、DOM操作与简单验证逻辑。在此基础上,还可继续扩展响应式适配、localStorage记住用户名等进阶能力,是一份适合自学、练手或课堂演示的完整前端小项目。
1. 模拟QQ登陆窗口:一个前端练手题,值得认真做一次
很多人学前端到表单和布局这一环,会突然发现书上的知识点全散了——flex 会用了,事件绑定了,表单验证也看过,但连一个像样的登录框都凑不出来。「html+css+javascript模拟QQ登陆窗口效果」这种练手题,就是为了把这些散点串成一条线:用纯 html 搭建语义化结构,用 css 还原界面质感,用 javascript 把验证、明文切换、回车提交这些交互做闭环。它不依赖框架,不碰后端,适合准备前端基础面试的人、刚学完三件套想练手的新手,也适合拿来当组件库里的第一个登录面板。真正难的地方不是把窗口画得像,而是让交互经得起边界情况折腾——这一篇就把这条线完整走一遍。
2. 搭 HTML 骨架:先拆清楚登录窗口有哪些板块
写任何页面都别一上来就写样式。先用 html 把结构立住,结构错了后面全是返工。QQ 登录窗口看似简单,拆开看其实是五个板块:顶部品牌区、账号输入区、密码输入区、辅助选项区(记住密码、找回密码)、提交按钮和底部注册入口。把这五个板块用清晰的标签包起来,CSS 再往后接手就顺了。
2.1 从整体到局部:登录窗口的板块划分与语义化标签
我一般会先把doctype、charset和viewport写好,这三行决定了页面在浏览器里以标准模式渲染,不会踩到怪异模式的坑。理想情况下,登录窗口应该是一个独立的组件,但在一个练手页面里,用body做背景、内部放一个div容器就足够了。结构上要注意:账号和密码必须用<form>包裹,因为后面提交事件、回车触发登录都要依赖表单的默认行为,用div包一堆输入框会让自己多写一堆事件代码。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>QQ 登录</title> <link rel="stylesheet" href="style.css"> </head> <body> <div class="login-wrapper"> <div class="login-panel"> <div class="login-header"> <img src="logo.png" alt="QQ logo" class="logo"> <h1>QQ 登录</h1> <p>登录后即可畅享沟通体验</p> </div> <form id="loginForm" autocomplete="off"> <div class="field-group"> <label for="account">账号</label> <input type="text" id="account" name="account" placeholder="手机号 / QQ号 / 邮箱" autocomplete="username"> </div> <div class="field-group password-group"> <label for="password">密码</label> <input type="password" id="password" name="password" placeholder="请输入密码" autocomplete="current-password"> <span class="toggle-pwd" id="togglePwd">显示</span> </div> <div class="option-row"> <label class="checkbox-label"> <input type="checkbox" id="remember"> 记住密码 </label> <label class="checkbox-label"> <input type="checkbox" id="autoLogin"> 自动登录 </label> <a href="#" class="forget-link">找回密码</a> </div> <button type="submit" class="login-btn" id="loginBtn">登 录</button> </form> <div class="login-footer"> <a href="#">注册账号</a> <span>多账号登录</span> </div> </div> </div> <script src="script.js"></script> </body> </html>这段结构里有几个关键决定:label的for属性必须指向输入框的id,这样点击文字时输入框能自动聚焦,这是可访问性最基本的一条,很多教程会漏掉;button明确写成type="submit",后面 JavaScript 拦截提交事件才有意义;autocomplete的username和current-password是给浏览器看的提示,告诉它这两个输入框分别应该填什么类型的值,但外层的autocomplete="off"又会拦住浏览器自作主张的填充——这个组合能让测试时不被浏览器的历史记录干扰。
2.2 表单控件的细节:placeholder 与 label 的分工
placeholder 和 label 的分工,是新手最容易做歪的地方。常见错误是干脆不写label,只用 placeholder 当提示文字。这在视觉上没问题,但一旦输入框聚焦、用户开始打字,提示文字消失,用户就忘了这里该填什么;而且屏幕阅读器读不到 placeholder,对无障碍是负分。正确做法是label负责描述字段身份,placeholder只给一个输入示例。代码里我把 placeholder 写成「手机号 / QQ号 / 邮箱」就是告诉你这里可以接受三种账号格式,而label的「账号」才是字段名称。
另一个容易忽略的点是type的选择。密码框必须用type="password",浏览器才会用圆点掩盖输入内容;但后面 JavaScript 做明文切换时,需要把它动态改成type="text",这个细节我会在第 4 章展开。至于邮箱和 QQ 号的输入框,我刻意用了type="text"而不是type="email",因为这里要兼容 QQ 号和手机号两种格式,type="email"会直接触发浏览器原生的邮箱格式校验,反而碍事。表单控件的 type 选型要跟着业务规则走,不是越语义化越好。
3. CSS 还原 QQ 风格:布局、配色、圆角与阴影
结构搭完,进入视觉还原阶段。QQ 登录窗口的核心视觉特征是蓝色渐变背景、白色圆角卡片、圆润的输入框和渐变的登录按钮。CSS 这部分要解决三件事:窗口居中、卡片质感、输入框和按钮的状态反馈。三者缺一个,页面就会显得「像网页模板」而不是「像产品」。
3.1 让登录窗口垂直水平居中:flex 与绝对定位怎么选
居中方案是前端面试的高频考点,也是这里第一个分岔路。我推荐用 flex 方案:给body设置display: flex配合justify-content: center和align-items: center,就能让登录卡片在视口正中间。另一种常见方案是给卡片设position: absolute加top: 50%; left: 50%,再用transform: translate(-50%, -50%)拉回自身一半尺寸。两者都能实现居中,但 flex 方案不需要知道卡片宽高,也天然支持响应式,我一般优先用 flex。
* { margin: 0; padding: 0; box-sizing: border-box; } body { height: 100vh; background: linear-gradient(135deg, #12aaee 0%, #0d85d0 100%); display: flex; justify-content: center; align-items: center; font-family: "Microsoft YaHei", "PingFang SC", sans-serif; } .login-panel { width: 440px; background: #fff; border-radius: 12px; padding: 36px 40px 24px; box-shadow: 0 12px 32px rgba(0, 0, 0, 0.18); }box-sizing: border-box这一行是全局盒模型的保险丝——不加它,padding 会把卡片实际宽度撑到 520px 以上,和设定的 440px 对不上。box-shadow的参数依次是水平偏移 0、垂直偏移 12px、模糊半径 32px、颜色 rgba(0,0,0,0.18),这样阴影只向下扩散,模拟卡片悬浮在背景上的质感。背景渐变的方向 135deg 是从左上到右下,这个角度在登录页里最自然,不会显得生硬。如果觉得渐变层次不够,可以把两个色号调近一些,比如#0aa8f0到#0d85d0,对比太强反而会有廉价感。
3.2 输入框和按钮的质感:边框、聚焦光环与渐变按钮
输入框是用户接触最多的元素,行高、字号、边框圆角这些参数直接决定手感。我习惯统一让输入框高度为 40px,边框用#d9d9d9的浅灰,聚焦时边框变成品牌蓝#12aaee,同时加一个 2px 的淡蓝色光晕。这个光晕是用box-shadow: 0 0 0 2px rgba(18, 170, 238, 0.2)实现的——注意这里0 0 0 2px的第三个 0 是模糊半径,必须为 0,否则光晕会糊成一片而不是清晰的环。
.field-group { margin-bottom: 18px; } .field-group label { display: block; font-size: 14px; color: #333; margin-bottom: 6px; } .field-group input { width: 100%; height: 40px; border: 1px solid #d9d9d9; border-radius: 6px; padding: 0 12px; font-size: 14px; transition: border-color 0.2s, box-shadow 0.2s; outline: none; } .field-group input:focus { border-color: #12aaee; box-shadow: 0 0 0 2px rgba(18, 170, 238, 0.2); } .login-btn { width: 100%; height: 42px; background: linear-gradient(135deg, #2cc0ff 0%, #0aa8f0 100%); border: none; border-radius: 6px; color: #fff; font-size: 16px; letter-spacing: 6px; cursor: pointer; transition: background 0.2s, transform 0.1s; } .login-btn:hover { background: linear-gradient(135deg, #1ab6fb 0%, #0795d4 100%); } .login-btn:active { transform: translateY(1px); }注意输入框的outline: none是写在默认样式里的,然后将焦点样式完全交给:focus自定义,这样既去掉了浏览器默认的黑框,又不会丢失焦点可辨识度。按钮的letter-spacing: 6px可以让「登 录」两个字在按钮内居中分布,视觉上更接近 QQ 的风格。transition只对border-color和box-shadow做动画,不对width或margin做,因为这些属性动画会触发重排,掉帧明显。按钮的 active 状态用translateY(1px)做出按下去的物理位移,交互反馈就藏在这 1px 里。
3.3 密码框右侧的「显示」按钮:绝对定位的正确打开方式
密码框右侧那个「显示 / 隐藏」切换按钮,很多新手会用 flex 把输入框和按钮放在同一行,但这样输入框宽度会被按钮挤占,而且对齐要额外处理。更省心的做法是给外层.password-group设position: relative,给切换按钮设position: absolute并定位到右上角,完全脱离文档流,不影响输入框的宽度计算。
.password-group { position: relative; } .toggle-pwd { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); font-size: 12px; color: #12aaee; cursor: pointer; user-select: none; background: #fff; padding: 0 2px; } .option-row { display: flex; align-items: center; justify-content: space-between; margin: 14px 0 20px; } .checkbox-label { display: flex; align-items: center; gap: 4px; font-size: 13px; color: #666; cursor: pointer; } .forget-link { font-size: 13px; color: #12aaee; text-decoration: none; } .forget-link:hover { text-decoration: underline; }top: 50%配合transform: translateY(-50%)是垂直居中的经典组合,这里之所以不用flex,是因为切换按钮不需要参与输入框的高度计算。user-select: none防止按钮文字被鼠标选中,否则用户快速连续点击时会出现文本选中造成的蓝底闪烁。option-row用justify-content: space-between把记住密码、自动登录、找回密码三组内容推到两端,中间自然留白——如果后续要加更多选项,这种弹性布局比写死margin更抗扩展。
4. JavaScript 交互逻辑:验证、明文切换与回车提交
HTML 和 CSS 做的是静态呈现,用户能看见登录窗口,但只有 JavaScript 介入,这个窗口才真正「活」过来。这一章要做的三件事是登录窗口最常见、也最容易被面试官追问的交互:表单验证、密码明文切换、回车提交。另外我会补一个 localStorage 的「记住账号」,让项目具备一点真实感。
4.1 表单验证:空值、格式与提交拦截
验证逻辑必须挂在submit事件上,而不是登录按钮的click事件。原因在于:用户可能在输入框里按回车触发提交,如果只监听按钮点击,回车路径就漏掉了;而监听form的submit,无论点击按钮还是回车,都会走同一条验证通道。先用event.preventDefault()拦下浏览器的默认提交行为,再做业务校验。
const form = document.getElementById('loginForm'); const accountInput = document.getElementById('account'); const passwordInput = document.getElementById('password'); function showError(input, message) { const fieldGroup = input.closest('.field-group'); let errorTip = fieldGroup.querySelector('.error-tip'); if (!errorTip) { errorTip = document.createElement('div'); errorTip.className = 'error-tip'; fieldGroup.appendChild(errorTip); } errorTip.textContent = message; input.classList.add('input-error'); } function clearError(input) { const fieldGroup = input.closest('.field-group'); const errorTip = fieldGroup.querySelector('.error-tip'); if (errorTip) { errorTip.remove(); } input.classList.remove('input-error'); } form.addEventListener('submit', function (event) { event.preventDefault(); const account = accountInput.value.trim(); const password = passwordInput.value; clearError(accountInput); clearError(passwordInput); if (!account) { showError(accountInput, '账号不能为空'); return; } const isQQ = /^[1-9]\d{4,10}$/.test(account); const isPhone = /^1\d{10}$/.test(account); const isEmail = /^[\w.+-]+@[\w-]+\.[\w.-]+$/.test(account); if (!isQQ && !isPhone && !isEmail) { showError(accountInput, '请输入正确的QQ号、手机号或邮箱'); return; } if (password.length < 6) { showError(passwordInput, '密码长度至少为6位'); return; } simulateLogin(account, password); });几个容易被忽略的参数细节:accountInput.value.trim()把首尾空格去掉,否则用户复制账号时不小心带了个空格,验证会误判;QQ 号的正则^[1-9]\d{4,10}$要求首位不为 0 且总长度 5 到 11 位;手机号用^1\d{10}$已经能覆盖绝大多数国内号码,如果还要更严格可以在第二位再加限制,但作为练手项目没必要;邮箱正则里的[\w.+-]+涵盖了点号和加减号,比只写\w+更贴合真实邮箱规则。showError在输入框下方动态插入一个.error-tip节点,比预先写死一个隐藏标签更干净——没有错误时不占任何布局空间,出现错误时才生成。
对应的 CSS 需要在样式表里补两段:错误文字的红色样式,以及输入框的错误边框状态。
.error-tip { font-size: 12px; color: #e04b4b; margin-top: 4px; } .input-error { border-color: #e04b4b !important; } .input-error:focus { box-shadow: 0 0 0 2px rgba(224, 75, 75, 0.2) !important; }!important在这里是有意为之,因为.field-group input:focus的边框优先级和.input-error同级,而后者的样式写在前面会被后者覆盖,加!important保证错误状态永远压过聚焦状态。用户修正输入后,下次提交时clearError会移除错误节点和样式,不会出现错误提示一直残留的尴尬。
4.2 密码可见性切换与回车提交的底层逻辑
密码可见性切换是登录窗口里最典型的 JavaScript 事件应用。点击「显示」按钮,把密码框的type从password改成text,点击「隐藏」再改回来——就这么简单。但有两个边界需要注意:切换后要把焦点还给输入框,否则用户键盘操作会断掉;切换按钮的文案要同步更新,视觉状态和实际状态不能脱节。
const togglePwd = document.getElementById('togglePwd'); togglePwd.addEventListener('click', function () { const isPassword = passwordInput.type === 'password'; passwordInput.type = isPassword ? 'text' : 'password'; togglePwd.textContent = isPassword ? '隐藏' : '显示'; // 切换 type 后光标会丢,重新聚焦并把光标放到末尾 passwordInput.focus(); const len = passwordInput.value.length; passwordInput.setSelectionRange(len, len); });setSelectionRange(len, len)是关键中的关键。直接改type会把光标位置重置到开头,用户密码长一点就非常难受,这句代码把光标挪回末尾。这个小细节是很多教程不会写的,但实际用起来体验差异巨大。另外注意这里用的是click事件而不是change或focus,因为切换按钮只有鼠标点击一个交互入口,语义最准确。
至于回车提交,实际上不需要专门写监听keydown的代码——只要按钮是type="submit"且包在<form>里,浏览器原生就会在输入框内按回车时触发form的submit事件。前面所有验证逻辑都挂在submit上,回车路径自然就走通了。很多新人会额外写一个keydown监听器,不仅冗余,还容易和原生行为叠加导致提交两次,这是典型的「画蛇添足」。
4.3 用 localStorage 做「记住账号」与模拟登录
登录窗口的前端模拟需要一个收尾动作:提交成功后假装登录。真实项目里这一步会发 HTTP 请求,但在纯前端练手里,用setTimeout模拟一个 1.2 秒的网络延迟就够了。同时「记住密码」这个勾选项要做成一个可验证的效果——我用 localStorage 记住账号,页面下次打开时自动回填。
const rememberCheck = document.getElementById('remember'); const loginBtn = document.getElementById('loginBtn'); window.addEventListener('DOMContentLoaded', function () { const savedAccount = localStorage.getItem('qq_login_account'); if (savedAccount) { accountInput.value = savedAccount; rememberCheck.checked = true; } }); form.addEventListener('submit', function (event) { event.preventDefault(); // 精简版:实际代码里前面的验证逻辑不变,这里省略 const account = accountInput.value.trim(); const password = passwordInput.value; if (rememberCheck.checked) { localStorage.setItem('qq_login_account', account); } else { localStorage.removeItem('qq_login_account'); } simulateLogin(account, password); }); function simulateLogin(account, password) { loginBtn.disabled = true; loginBtn.textContent = '登录中…'; setTimeout(function () { loginBtn.disabled = false; loginBtn.textContent = '登 录'; alert('登录成功(模拟),欢迎回来:' + account); }, 1200); }这里要特别强调一个安全边界:项目名是「记住密码」,但 localStorage 绝对不应该存密码。localStorage 是明文存储,任何在浏览器里执行 JavaScript 的脚本都能读取它,把密码放进去等于把钥匙放在门口地毯下面。所以我实现的是「记住账号」——页面刷新后账号自动回填,密码仍然要手动输入。这是很多练手项目的通病,面试官如果追问「密码怎么处理」,你要能答出这一层。
DOMContentLoaded事件保证脚本在 DOM 解析完成后才执行,避免出现「脚本在页面底部所以没问题」这种隐式依赖。把脚本放在</body>前确实能让 DOM 先就绪,但如果哪天你把<script>挪到了<head>里,有没有这个监听就是挂与不挂的区别。养成习惯,用DOMContentLoaded包裹初始化逻辑,比依赖标签位置更可靠。登录按钮的disabled属性在模拟请求期间拦截重复点击,防止用户手快连点导致多个计时器同时跑,这也是表单交互的基本素养。
5. 七个常见避坑点:从样式塌陷到交互失效
这部分内容是我自己写这类页面时踩过、也帮别人排查过的真实翻车现场。每一条都按「现象 → 原因 → 解决」的顺序写,照着排查能省很多时间。
5.1 script 放在 head 里报错:DOM 还没存在就拿元素
现象:浏览器控制台报Cannot read properties of null,getElementById拿到的是null。原因:<script>放在<head>中时,脚本执行时body还没解析,登录窗口的 DOM 节点还没创建。解决:把<script>放到</body>前是最简单的方案;如果因为某些原因必须放 head,就等DOMContentLoaded事件触发后再执行。这个坑的本质是 JavaScript 的执行时机和 DOM 解析时机的竞争关系,不是代码写错了,而是位置放错了。经验法则:操作 DOM 的代码必须在目标节点存在之后执行,要么靠标签位置,要么靠事件,别赌浏览器速度。
5.2 回车刷新页面:form 的默认提交行为没有拦
现象:在密码框里按回车,页面整个刷新,输入的内容全没了。原因:<form>的默认提交行为是 GET 请求并刷新页面,如果你给按钮写了click监听却发现回车不触发,或者触发了却没拦住,说明事件绑错了对象。解决:监听form的submit事件,并在逻辑开头调用event.preventDefault()。注意preventDefault()必须在所有业务代码之前,否则验证还没跑,表单已经提交了。另外检查按钮是不是type="button"——如果是,回车时不会触发 submit,要改成type="submit"才行。我排查过不少类似案例,最后发现是按钮的 type 写错了。
5.3 localStorage 里存了密码:记住密码的语义被做歪了
现象:勾选「记住密码」后,页面刷新密码框自动填入了一串明文,或者更糟——打开浏览器开发者工具 Application 面板能看到密码原文。原因:把passwordInput.value直接存进了 localStorage。解决:要么存账号不存密码,要么用真实场景里的做法——把账号密码交给后端,由后端返回一个 token,前端只存 token。纯前端练手项目没有后端,所以正确做法是只存账号并回填账号。这一点面试里常作为追问出现,答不上来会显得安全意识薄弱。
5.4 CSS 样式完全没生效:三种样式引入方式的坑
现象:HTML 结构正常,但页面没有任何样式,背景白板一块。原因:<link rel="stylesheet" href="style.css">的文件路径写错了,或者文件本身是空的;还有一种情况是把<style>标签写进了<body>里——浏览器虽然能解析,但这是不规范的。解决:link标签必须放在<head>里,href用相对路径时确认style.css和index.html在同一目录下。如果用的是内联样式<style>标签,必须放在<head>里;如果想在 HTML 标签上直接写style="",那是内联样式,优先级最高,但一般只用于临时调试。三种引入方式优先级从高到低是内联、嵌入式<style>、外部<link>,排查样式问题时先分清是哪一层在打架。
5.5 页面在窄屏下塌陷:固定宽度与视口的冲突
现象:把浏览器窗口拖窄到 360px,登录卡片右侧被截断,背景露出白边。原因:.login-panel设置了固定width: 440px,而小屏幕视口不足 440px,再加上没有min-width和相关保护,卡片就溢出到视口外。解决:在媒体查询里让卡片宽度自适应,比如屏幕小于 480px 时width: calc(100% - 32px),配合max-width: 440px限住上限。这个坑在 QQ 登录窗口这种固定设计稿练习里特别容易被忽略——因为桌面端预览永远正常,一旦拿到手机上测就原形毕露。顺手给.login-wrapper加一句padding: 16px,能避免极端窄屏下卡片贴边。
5.6 切换密码可见性后光标跳回开头
现象:密码输入到一半点「显示」,输入框的光标跳到文本开头,继续输入的内容全跑前面去了。原因:修改input.type会触发输入框状态重置,光标位置被还原到起始位置。解决:切换后调用focus()再加setSelectionRange(len, len)把光标挪回末尾。这个坑极其隐蔽,因为功能看起来是正常的——明文能显示,切换能点,只有实际操作才会发现体验割裂。我在 4.2 里已经写了完整解法,这里再次单列是为了强调:凡是改变输入框type的交互,都必须处理光标。
5.7 按钮上的涟漪动画点一次没反应
现象:给按钮加了涟漪效果的 JavaScript,点击后没有出现水波扩散,控制台报错。原因:通常是把事件绑定写在了DOMContentLoaded之外,脚本执行时按钮还不存在;或者按钮上同时绑着submit事件,点击后页面闪一下,涟漪动画根本来不及展示。解决:涟漪监听放在DOMContentLoaded内,且按钮若为type="submit",需要在连续的点击事件里用event.preventDefault()拦截一下?——不,submit 的默认行为已经被表单验证拦住了,所以这里只需保证事件绑定顺序在 DOM 就绪之后即可。另外涟漪元素生成后要记得在动画结束后移除,否则每次点击都往按钮里塞一个span,几百次后 DOM 节点数量爆炸,页面越来越卡。
6. 继续加码:把登录窗口升级成完整的登录面板
如果上面的内容你已经全部跑通,下一步可以考虑把「登录窗口」升级为「登录面板」。我推荐三个方向:一是给登录按钮加一个涟漪光圈扩散效果,点击时从鼠标位置向外扩散一圈淡淡的白色波纹,这个小动效成本低、视觉提升明显,面试时也能当作一个展示点;二是增加登录方式切换——在「密码登录」和「扫码登录」两个 tab 之间切换,这需要准备一张二维码占位图,并在切换时用 JavaScript 控制两个面板的显示隐藏;三是引入 CSS 变量统一管理主题色,把#12aaee这类色值抽成变量,后续换肤只需要改一行。
涟漪光圈扩散的效果用 CSS 动画加 JavaScript 事件就能实现。核心思路是:在按钮的click事件里获取鼠标相对按钮的位置,创建一个绝对定位的圆形span,做一次从scale(0)到scale(4)、透明度降为 0 的扩散动画,动画结束后移除节点。
const btn = document.getElementById('loginBtn'); btn.addEventListener('click', function (e) { const circle = document.createElement('span'); const rect = btn.getBoundingClientRect(); const size = Math.max(rect.width, rect.height); circle.style.width = size + 'px'; circle.style.height = size + 'px'; circle.style.left = (e.clientX - rect.left - size / 2) + 'px'; circle.style.top = (e.clientY - rect.top - size / 2) + 'px'; circle.className = 'ripple'; btn.appendChild(circle); setTimeout(function () { circle.remove(); }, 600); });这里的坐标计算逻辑是:e.clientX和e.clientY是鼠标在视口的位置,rect是按钮相对视口的位置,相减得到鼠标相对按钮的偏移,再减去圆的一半尺寸,让圆心正好落在鼠标点击点上。圆的大小取按钮宽高的最大值,保证扩散后能覆盖整个按钮。配套的 CSS 需要一个position: relative; overflow: hidden的按钮容器,.ripple用绝对定位加transform: scale(0)初始状态,动画到scale(4)结束,同时透明度归零。overflow: hidden把扩散范围限制在按钮内部,不会溢到页面上。
做这类效果我有一个习惯:每次写完交互先开控制台看报错,再开 Application 面板检查 DOM 节点数量,最后把浏览器窗口拖到手机宽度跑一遍。涟漪动画最容易翻车的点就是忘记移除节点,或者overflow: hidden没加上导致水波溢出按钮边界。扫码登录的切换同理,两个面板的显示隐藏可以用hidden属性和一个标记变量控制,切过去时记得把另一个面板里的输入框清空,避免下次切回来时残留上次的输入状态。
这个练手项目做到这里,你已经把 HTML 语义化、CSS 布局与动画、JavaScript 事件与存储全部串了起来。我最初做这个项目时也踩过不少坑,最大的教训是:别急着追求视觉完美,先把流程闭环打通——结构能跑通、验证能拦住、点击有反馈、刷新有记忆,再回头打磨细节。希望帮到你。
本文还有配套的精品资源,点击获取