普通视图

发现新文章,点击刷新页面。
今天 — 2026年4月1日首页

别再手写代码了!2026 前端 5 个 AI 杀招,直接解放 80% 重复劳动(附工具+步骤)

作者 前端Hardy
2026年4月1日 18:16

你还在手动搭项目、手写组件、熬夜调 Bug 吗?2026 年的前端开发,AI 已经接管 80% 重复工作——从项目初始化、UI 生成、Bug 修复到代码重构,全流程智能化。

今天这篇,不讲虚的,直接带工具、带步骤、带实战指令,照着做,今天就能少加班 50%。


一、AI 一键搭项目:1 分钟搞定 Vue/React 工程(VS Code + Copilot)

以前搭项目:装依赖、配路由、装状态库、调 ESLint……半天没了。 现在用 GitHub Copilot(VS Code 必装),一句话生成完整工程。

工具安装(5 分钟)

  1. 安装 VS Code(最新版)

image.png

  1. 扩展商店搜:GitHub Copilot + GitHub Copilot Chat(安装)

image.png

  1. 点击左下角 Copilot 图标 → 登录 GitHub → 授权成功(图标变绿)

image.png

实战步骤(1 分钟出项目)

  1. 新建空文件夹 → 用 VS Code 打开
  2. 快捷键 Ctrl+Shift+I(Win)/ Cmd+Shift+I(Mac)打开 Copilot Chat

image.png

  1. 直接发指令(复制可用):

    生成一个 Vue3 + Vite + Pinia + VueRouter + Tailwind CSS 项目,包含:

    • 完整目录结构
    • ESLint + Prettier 规范配置
    • 请求封装(axios)
    • 路由守卫
    • 自适应布局基础
    • 自动安装依赖
  2. 等待 30 秒 → AI 自动生成所有文件、安装依赖、写好 README

image.png

效果对比

  • 以前:1 天工作量
  • 现在:1 分钟,零配置、零报错

二、AI 组件工厂:一句话生成生产级 UI(Cursor 编辑器)

前端最耗时:写页面、调样式、做响应式、加交互。 Cursor(AI 原生编辑器) 是前端 UI 生成神器,比 VS Code 更智能,支持跨文件、自动处理样式依赖。

工具安装

  1. 官网下载:www.cursor.so/

image.png

  1. 安装 → 首次启动用 GitHub 登录 → 导入 VS Code 配置

安装

  1. 设置中文:Ctrl+Shift+P → 搜索 Configure Display Language → 选中文

设置中文

实战步骤(生成电商商品卡片)

  1. 新建 ProductCard.vue
  2. 快捷键 Ctrl+K(Win)/ Cmd+K(Mac)打开 AI 指令

打开 AI

  1. 输入(复制可用):

    用 Vue3 + TS + Tailwind CSS 生成电商商品卡片组件,要求:

    • 包含:商品图、标题、原价、现价、折扣标签、加入购物车按钮
    • hover 上浮动效、过渡动画
    • 移动端响应式(375px 适配)
    • 带 TS 类型定义
    • 支持自定义主题色
    • 加注释、符合 ESLint 规范
  2. 回车 → 直接生成完整代码(复制即用)

完整代码

进阶:Figma 转代码

  1. 打开 Figma 设计稿 → 复制链接
  2. Cursor 指令:

    把这个 Figma 设计稿转成 Vue3 代码:[粘贴链接],带响应式、TS 类型、可直接运行


三、AI 自动改 Bug:秒定位+修复,告别熬夜(Copilot Chat)

前端最痛:白屏、样式错乱、报错、兼容问题。 Copilot Chat 能直接读代码+报错,自动定位根因+给修复方案

实战步骤(修复白屏 Bug)

  1. 遇到报错:Uncaught TypeError: Cannot read properties of undefined (reading 'xxx')
  2. 选中报错代码 → 右键 → Copilot → Explain This Error

修改Bug

  1. 或直接在聊天框发:

    分析这段代码和报错,找出根因,给修复代码+解释: 【粘贴报错】 【粘贴代码】

  2. AI 秒回:

  • 错误原因(如:变量未初始化、异步时序问题)
  • 完整修复代码
  • 优化建议(如:加可选链、错误捕获)

image

常见前端 Bug 指令(直接复制)

  • 样式兼容:修复 iOS 微信浏览器样式错乱问题
  • 性能卡顿:分析页面滚动卡顿,优化 FPS,给代码方案
  • 接口报错:修复 axios 跨域+超时+错误重试

四、AI 代码重构:老项目一键升级(文心快码)

维护 jQuery/老 Vue2 项目?手动重构太痛苦。 文心快码(国产 AI,前端重构最强) 能批量升级、补 TS、优化性能。

工具安装(VS Code 插件)

  1. 扩展商店搜:文心快码(Baidu Comate) → 安装

Baidu Comate

  1. 用百度账号登录 → 免费额度够用

实战步骤(jQuery 转 Vue3)

  1. 打开老代码文件

  2. 打开文心快码聊天 → 发指令:

    把这段 jQuery 代码重构成 Vue3 组合式 API + TS,要求:

    • 保留原功能
    • 加类型定义
    • 用 Pinia 管理状态
    • 优化性能、移除冗余
    • 符合团队规范
  3. AI 自动生成新代码 → 对比确认 → 直接替换

进阶:批量重构

分析整个项目,把所有 Vue2 组件升级到 Vue3,统一 TS 规范


五、AI 全链路工程化:从接口到部署一条龙(v0 + Copilot)

不止写代码,接口、类型、测试、部署 AI 全包。 v0(Vercel 出品)+ Copilot 前端全链路最强组合。

1. 接口 + TS 类型自动生成

Copilot 指令:

根据这份接口文档,生成:

  • axios 请求封装
  • TS 接口类型定义
  • Mock 数据
  • API 调用示例

2. UI 生成(v0 最强)

  1. 打开:v0.dev/

image

  1. 输入:生成一个后台管理系统列表页,带筛选、分页、操作按钮,用 React + Tailwind
  2. 10 秒出页面 → 复制代码到项目

3. 自动写测试 + 部署

Copilot 指令:

为这个组件写 Vitest 单元测试,覆盖:渲染、交互、边界情况 再生成 Dockerfile + CI/CD 部署脚本


2026 前端 AI 工具选型表(直接抄)

场景 最佳工具 价格 上手难度
日常编码、补全 GitHub Copilot $19/月
UI 组件、页面生成 Cursor、v0 $20/月(Cursor) ⭐⭐
老项目重构、升级 文心快码 免费额度+付费
Bug 修复、调试 Copilot Chat 含在 Copilot 内
全栈项目、原型 Bolt.new 免费试用 ⭐⭐

最后:AI 不淘汰前端,淘汰不用 AI 的人

2026 年的前端竞争:

  • ❶ 不会 AI:天天手写、加班、被淘汰
  • ❷ 会用 AI:少写 80% 重复代码、早下班、涨薪更快

今天就行动

  1. 装 VS Code + Copilot + Cursor
  2. 把本文指令复制试用
  3. 把重复工作丢给 AI,专注架构、业务、价值

别再手写代码了,AI 时代,拼的是会不会用工具,不是手速!


各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

前端工程师必备的 10 个 AI 万能提示词(Prompt),复制直接用,效率再翻倍!

作者 前端Hardy
2026年4月1日 17:51

你是不是也有这种困扰? 用 Copilot、Cursor 写代码,明明想让 AI 帮你省时间,结果指令发出去,AI 瞎编代码、答非所问,反而更费劲儿?

不是 AI 不好用,是你没找对“说话方式”——前端 AI 高效开发的核心,从来不是“让 AI 写代码”,而是“让 AI 精准懂你的需求”。

很多前端每天用 AI,却不知道:一句好的 Prompt(提示词),能让效率直接翻 3 倍,少写 80% 重复代码、少踩 90% 的坑。

今天这篇,不搞虚的,直接给大家整理了 10 个前端专属 AI 万能提示词,覆盖前端开发全场景——组件开发、Bug 修复、代码重构、样式优化、工程化配置,全部复制就能用,不用自己琢磨,新手也能轻松上手。

不管你用的是 Copilot、Cursor、文心快码,还是 Claude Code,这些 Prompt 都通用,今天用,今天就能省时间、少加班!

先划重点:前端 AI Prompt 万能公式(记牢更省心)

所有好用的前端 Prompt,都离不开这 4 个核心要素,记下来,以后自己也能自定义:

明确场景 + 技术栈 + 具体需求 + 输出要求

举个反例:“帮我写个按钮组件”(模糊,AI 易瞎编) 举个正例:“用 Vue3 + TS + Tailwind CSS 写一个按钮组件,包含默认/禁用/高亮三种状态,hover 有过渡动画,带类型定义和注释,符合 ESLint 规范”(精准,AI 直接出可用代码)

下面这 10 个 Prompt,全部按照这个公式编写,复制粘贴,替换括号里的内容,就能直接用!

一、组件开发类(最常用,每天都能用到)

前端每天都要写组件,这 2 个 Prompt,覆盖 80% 的组件开发场景,不用再手动写样式、写逻辑。

Prompt 1:基础组件生成(复制即用)

用【Vue3/React】+【TS】+【Tailwind CSS/Element Plus/Ant Design】生成【组件名称,如:登录表单/商品卡片/分页组件】,要求:
1. 包含【具体功能,如:表单校验/分页切换/hover 动效】;
2. 支持【自定义属性,如:自定义颜色/尺寸/回调函数】;
3. 带完整 TS 类型定义、详细注释,符合 ESLint 规范;
4. 适配移动端响应式,兼容主流浏览器;
5. 输出完整可运行代码,复制就能直接导入项目。

示例替换:用 Vue3 + TS + Element Plus 生成登录表单,要求:1. 包含账号密码校验、记住密码、忘记密码功能;2. 支持自定义提交按钮文本;3. 带完整 TS 类型定义、详细注释,符合 ESLint 规范;4. 适配移动端响应式,兼容主流浏览器;5. 输出完整可运行代码,复制就能直接导入项目。

Prompt 2:复杂组件封装(复制即用)

帮我封装一个【复杂组件名称,如:树形表格/弹窗表单/下拉搜索选择器】,技术栈【Vue3/React + TS】,要求:
1. 核心功能:【详细描述功能,如:树形表格支持勾选、展开/折叠、搜索筛选;弹窗表单支持表单联动、提交校验】;
2. 性能优化:【如:懒加载、防抖节流、避免重复渲染】;
3. 可扩展性:支持插槽、自定义事件、Props 传参,方便后续二次开发;
4. 附带使用示例、TS 类型说明、常见问题备注;
5. 代码结构清晰,分模块编写,便于维护。

二、Bug 修复类(前端救星,告别熬夜改 Bug)

遇到 Bug 不用慌,不用再翻 Stack Overflow、不用瞎试代码,这 2 个 Prompt,让 AI 秒定位、秒修复,还能告诉你问题根源。

Prompt 3:报错快速修复(复制即用)

帮我分析以下前端报错和对应代码,要求:
1. 报错信息:【粘贴完整报错信息,如:Uncaught TypeError: Cannot read properties of undefined (reading 'value')】;
2. 对应代码:【粘贴报错相关的完整代码片段】;
3. 请找出报错根因,给出详细解释,然后提供完整的修复代码;
4. 补充优化建议,避免以后再出现类似问题;
5. 修复后的代码要符合项目技术栈【Vue3/React + TS】规范,可直接替换使用。

Prompt 4:兼容性/Bug 排查(复制即用)

我遇到一个前端问题:【详细描述问题,如:iOS 微信浏览器样式错乱、页面滚动卡顿、接口请求跨域失败、组件渲染异常】;
项目技术栈:【Vue3/React + TS + 具体框架/工具】;
请帮我:
1. 分析可能的问题原因,列出所有可能性;
2. 给出每一种原因的解决方案和完整代码;
3. 提供预防措施,避免后续出现类似兼容性/性能问题;
4. 方案要简单易操作,不用复杂配置,直接能落地。

三、代码重构类(老项目救星,提升代码质量)

维护老项目、接手烂代码,手动重构太费时间?这 2 个 Prompt,让 AI 帮你优化代码、升级版本,不用自己逐行修改。

Prompt 5:代码优化/重构(复制即用)

帮我重构以下前端代码,项目技术栈【Vue3/React + TS】,要求:
1. 原始代码:【粘贴需要重构的代码片段】;
2. 重构目标:优化代码结构、移除冗余代码、修复潜在 Bug、提升代码可读性和可维护性;
3. 保留原有的所有功能,不改变业务逻辑;
4. 加入 TS 类型定义(如果没有),补充必要注释,符合 ESLint 规范;
5. 给出重构前后的对比说明,解释优化的原因和好处。

Prompt 6:版本升级迁移(复制即用)

帮我将【旧版本技术,如:Vue2 组件/Vue3 旧语法/jQuery 代码】迁移到【新版本技术,如:Vue3 组合式 API/TS/React 函数组件】,要求:
1. 原始代码:【粘贴需要迁移的代码片段/文件】;
2. 迁移要求:完全保留原业务功能,兼容原有项目配置,不引入新的依赖;
3. 遵循新版本的最佳实践,如:Vue3 组合式 API 规范、React Hooks 规范;
4. 补充迁移说明,列出需要注意的细节和可能出现的问题及解决方案;
5. 输出完整的迁移后代码,可直接替换使用。

四、样式/交互类(告别调样式的痛苦)

调样式、做交互,最费时间还容易出错?这 2 个 Prompt,让 AI 帮你写样式、做动效,不用再反复调试。

Prompt 7:样式快速生成/优化(复制即用)

帮我写/优化【元素/组件】的样式,技术栈【Tailwind CSS/CSS3/SCSS】,要求:
1. 样式需求:【详细描述,如:居中显示、圆角、阴影、hover 动效、响应式适配(375px/768px/1200px)、深色模式兼容】;
2. 样式规范:符合项目设计规范,避免样式冲突,代码简洁可复用;
3. 优化要求:减少冗余样式,提升样式加载速度,兼容主流浏览器;
4. 输出完整的样式代码,可直接复制到项目中使用,并给出使用说明。

Prompt 8:交互效果实现(复制即用)

帮我实现【交互效果,如:下拉菜单动画、弹窗淡入淡出、滚动加载、拖拽排序、表单联动】,技术栈【Vue3/React + JS/TS】,要求:
1. 交互细节:【详细描述,如:弹窗点击遮罩关闭、下拉菜单hover展开、拖拽时显示提示、滚动加载到底部自动请求数据】;
2. 性能要求:避免卡顿、防抖节流处理,不影响页面其他功能;
3. 兼容性:适配移动端和PC端,兼容主流浏览器;
4. 输出完整的代码(HTML/CSS/JS/TS),复制就能用,附带使用说明和注意事项。

五、工程化/工具类(提升全链路效率)

除了写代码,工程化配置、接口请求、测试用例也能让 AI 帮你做,这 2 个 Prompt,覆盖前端全链路开发。

Prompt 9:接口请求/类型生成(复制即用)

根据以下接口文档,生成【Vue3/React】项目的接口请求代码,要求:
1. 接口信息:【粘贴接口文档,包含请求地址、请求方式、参数、返回值】;
2. 技术栈:【Axios + TS】;
3. 输出内容:
   - 完整的接口请求函数封装(包含请求拦截、响应拦截、错误处理);
   - 所有接口参数和返回值的 TS 类型定义;
   - Mock 数据生成(用于本地调试);
   - 接口调用示例;
4. 代码符合项目规范,可直接导入项目使用。

Prompt 10:测试用例/工程化配置(复制即用)

帮我生成【组件/函数】的测试用例,或【工程化配置文件】,要求:
1. 目标:【如:为登录组件写单元测试、生成 ESLint 配置、生成 Vitest 配置、生成 Dockerfile】;
2. 技术栈:【Vitest/Jest/ESLint/Docker】;
3. 具体要求:【如:测试用例覆盖渲染、交互、边界情况;配置文件适配 Vue3/React + TS 项目,包含常用配置】;
4. 输出完整的代码/配置文件,可直接复制到项目中使用,并给出配置说明和使用方法。

关键提醒:这 3 个小技巧,让 Prompt 效果再翻倍

  1. 越具体,AI 越精准:不要说“帮我写个表单”,要明确技术栈、功能、样式,甚至是兼容要求,避免 AI 瞎编;
  2. 分场景使用:不同的 AI 工具(Copilot/Cursor)适配性略有差异,但以上 Prompt 全部通用,复制后可根据工具微调;
  3. 善用追问:如果 AI 输出不符合预期,直接追问“修改一下,让组件支持自定义颜色”“修复这个代码里的语法错误”,不用重新发指令。

写在最后:AI 提效,Prompt 是关键

2026 年的前端开发,拼的不是手速,是“用 AI 的能力”——同样是用 AI,会写 Prompt 的人,每天能多省 1-2 小时,少加很多班;不会写的人,反而被 AI 拖累。

以上 10 个 Prompt,覆盖了前端开发的全场景,不管你是新手还是资深前端,复制就能用,不用自己琢磨、不用记复杂语法。

建议你 收藏本文,转发给身边还在瞎用 AI、天天加班的前端同事,一起省时间、提效率、早下班。


各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

昨天 — 2026年3月31日首页

NW.js v0.109.1 最新稳定版发布:被遗忘的桌面开发神器?启动快 3 倍,内存省 70%!

作者 前端Hardy
2026年3月30日 13:48

你的 Electron 应用启动要 5 秒?内存占用 400MB?
而用 NW.js v0.109.1(2026 年 3 月 21 日发布的最新稳定版),相同功能应用启动仅需 1.6 秒,内存占用仅 120MB——而且直接访问 Node.js API,无需 IPC 通信,代码更简洁。

如果你厌倦了:

  • Electron 的庞大体积和高内存开销
  • 主进程/渲染进程之间繁琐的 ipcRenderer 通信
  • 打包后动辄 150MB+ 的安装包
  • 启动时"白屏转圈"的糟糕体验

那么,NW.js v0.109.1 的发布,可能正在悄悄夺回桌面开发的王座


一、Electron 的统治与代价(2026 年现状)

Electron 仍是桌面应用主流,但代价日益凸显:

  • 资源消耗巨大:每个窗口独立 Chromium 实例,内存轻松超 300MB
  • 架构复杂:主进程(Node)与渲染进程(Browser)需 IPC 通信
  • 启动慢:冷启动常超 4 秒(需先启 Node 主进程)
  • 打包臃肿:简单应用最终体积 120MB+(含 Chromium)

关键事实:NW.js 诞生于 2011 年(原名 node-webkit),比 Electron(2013 年)更早,但因生态推广较少被掩盖。


二、NW.js v0.109.1 是什么?为什么它能快 3 倍、省 70% 内存?

NW.js v0.109.1 是当前最新稳定版(2026 年 3 月 21 日发布),基于 Chromium 146 + Node.js v25.6.1

能力 Electron 33 NW.js v0.109.1
启动时间(简单应用) 4.2–5.8 秒 1.4–1.9 秒
内存占用(空应用) 320–450 MB 90–130 MB
最终打包体积 120–180 MB 45–70 MB
Node.js 访问方式 需 IPC 通信 直接 require()
多窗口管理 复杂(BrowserWindow 原生 <webview>window.open()
安全模型 默认开启(限制多) 可配置(开发更灵活)

核心优势

  • 单进程融合:Node.js 与 DOM 运行在同一上下文(require('fs')<script> 直接可用)
  • 无 IPC 开销:读文件、调系统 API 不再需要 send/on 回调地狱
  • Chromium 更新快:紧跟上游(v0.109.1 已支持 Chromium 146 新特性)

版本说明:NW.js 项目长期采用 0.x.x 版本号体系(v0.109.1 是当前稳定版,并非测试版)。


三、真实迁移:从 Electron 到 NW.js

1. 无需改写核心逻辑

<!-- NW.js 直接可用 Node.js -->
<script>
  const fs = require('fs'); // 无需 preload
  document.getElementById('btn').onclick = () => {
    fs.readFile('/data.json', 'utf8', (err, data) => {
      console.log(data);
    });
  };
</script>

2. 项目结构极简

my-app/
├── index.html   # 仅需此文件
└── package.json # 10 行配置
{
  "name": "my-app",
  "main": "index.html",
  "window": {
    "width": 800,
    "height": 600
  }
}

3. 启动命令(仅 1 行)

npx nw .  # 无需主进程脚本

对比 Electron:需 main.js + preload.js + IPC 通信,代码量增加 50%+。


四、实测:NW.js v0.109.1 vs Electron 33(实验室环境)

测试声明:以下数据为实验室环境(M3 MacBook Pro,16GB RAM,macOS 15)下简单应用(窗口+文件读取)的测试结果,实际表现因项目复杂度、系统环境而异。

指标 Electron 33 NW.js v0.109.1
项目初始化时间 3 分钟(含 IPC 配置) 30 秒(仅 HTML + package.json)
冷启动时间 4.7 秒 1.6 秒(快 3 倍)
内存峰值 385 MB 118 MB(省 70%)
打包体积(macOS) 142 MB 58 MB
代码行数(核心逻辑) 42 行(IPC 通信) 12 行(直接调用)

测试方法:使用 Activity Monitor 测量内存,手动计时冷启动(从点击应用到窗口完全渲染)。


五、它为什么没被广泛采用?(客观分析)

  1. 历史包袱:2011-2013 年 NW.js 有安全漏洞记录,导致部分开发者转向 Electron
  2. 生态差距:Electron 插件生态更丰富,社区资源更多
  3. 版本认知:长期 0.x 版本号让部分开发者误以为是测试版
  4. Mac App Store 上架:因直接暴露 Node,需额外签名处理

v0.109.1 改进

  • 基于 Chromium 146,安全性大幅提升
  • 官方文档已更新签名流程指南

六、5 分钟上手 NW.js v0.109.1

# 1. 创建项目
mkdir my-nw-app && cd my-nw-app

# 2. 创建 package.json
echo '{
  "name": "hello-nw",
  "main": "index.html"
}' > package.json

# 3. 创建 index.html(见下文)
# 4. 安装 NW.js CLI
npm install -g nw

# 5. 运行!
nw .

index.html 示例

<!DOCTYPE html>
<html>
<head>
  <title>NW.js Demo</title>
</head>
<body>
  <button id="readFile">读取本地文件</button>
  <script>
    // 直接使用 Node.js!
    document.getElementById('readFile').onclick = () => {
      const fs = require('fs');
      const data = fs.readFileSync('/etc/hosts', 'utf8');
      alert(data.substring(0, 100));
    };
  </script>
</body>
</html>

无需任何配置,点开即用!


七、谁在用 NW.js?(确认案例)

项目 说明
Adobe Brackets 经典开源编辑器(2012-2021),已归档但仍具参考价值
Intel XDK Intel 的跨平台开发工具(已停止维护)
各类企业内部工具 因轻量、易维护被部分团队采用

GitHub 数据:NW.js 仓库 Star 数约 39.5k(2026 年 3 月),活跃度稳定 。


结语:简单,才是终极的复杂

NW.js v0.109.1 的回归,不是"怀旧",而是对开发本质的回归
为什么我们要为"读一个文件"写 10 行 IPC 代码?为什么工具不能像写网页一样自然?

官网:nwjs.io
GitHub:github.com/nwjs/nw.js
最新版本:v0.109.1(2026-03-21 发布,Chromium 146 + Node.js v25.6.1)

你愿意用 NW.js v0.109.1 重构一个 Electron 项目吗?评论区投票!


各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

昨天以前首页

别再手动写 loading 了!封装一个自动防重提交的 Hook

作者 前端Hardy
2026年3月24日 18:05

每次提交表单都要写 loading = truedisabled = true.finally(() => loading = false)
你不是在写业务,你是在重复造轮子。

在日常开发中,我们无数次面对这样的场景:

  • 用户点击“提交订单”
  • 点击“发送验证码”
  • 点击“保存设置”

而为了防止重复点击,你不得不:

  1. 定义一个 loading 状态;
  2. 在点击时设为 true
  3. 禁用按钮;
  4. 发起请求;
  5. 成功或失败后,再设回 false

一段逻辑,复制粘贴十次。

更糟的是——一旦忘记写 .finally,按钮就永远禁用;一旦并发请求没处理好,照样重复提交。

今天,我们就用 一个自定义 Hook,彻底终结这种体力劳动。


手动管理 loading 的三大痛点

1. 代码冗余

const [submitting, setSubmitting] = useState(false);

const handleSubmit = async () => {
  if (submitting) return;
  setSubmitting(true);
  try {
    await submitForm();
  } finally {
    setSubmitting(false); // 忘记这行?按钮就废了
  }
};

每个按钮都要写一遍,毫无意义。

2. 无法天然防重

即使你写了 if (submitting) return,如果用户快速双击,在 setSubmitting(true) 异步更新前,两次点击仍可能触发两次请求。

3. 状态分散,难以维护

多个按钮?多个表单?每个都要独立管理状态,逻辑割裂。


解法:封装一个 useSubmitLock Hook

我们要实现的效果:

const [handleSubmit, isSubmitting] = useSubmitLock(async (formData) => {
  await api.submitOrder(formData);
  message.success('下单成功!');
});

return (
  <button disabled={isSubmitting} onClick={() => handleSubmit(data)}>
    {isSubmitting ? '提交中...' : '立即下单'}
  </button>
);

一行调用,自动加锁、自动解锁、自动防重、自动透传参数!


实现原理:Promise 锁 + 状态同步

// React + TypeScript 版本(JS 可轻松转写)
import { useState, useCallback } from 'react';

type AsyncFunction<T extends any[], R> = (...args: T) => Promise<R>;

export const useSubmitLock = <T extends any[], R>(
  asyncFn: AsyncFunction<T, R>
) => {
  const [isLocked, setIsLocked] = useState(false);

  const wrappedFn = useCallback(
    async (...args: T): Promise<R | undefined> => {
      if (isLocked) {
        console.warn('操作正在进行中,请勿重复提交');
        return; // 直接拦截,不执行函数
      }

      setIsLocked(true);
      try {
        const result = await asyncFn(...args);
        return result;
      } finally {
        setIsLocked(false); // 无论成功失败,一定解锁
      }
    },
    [isLocked, asyncFn]
  );

  return [wrappedFn, isLocked] as const;
};

关键设计亮点:

特性 说明
闭包锁 isLockedtrue 时,直接 return,不执行原函数
自动 finally 解锁 即使接口报错、用户中断,也不会卡死
泛型支持 完美透传参数和返回值类型
无副作用 不依赖全局状态,每个调用独立隔离

使用场景全覆盖

场景 1:表单提交

const [submitForm, submitting] = useSubmitLock(api.createPost);

场景 2:发送验证码

const [sendCode, sending] = useSubmitLock(phoneApi.sendSmsCode);
// 按钮文案可结合倒计时:{sending ? '发送中...' : '获取验证码'}

场景 3:删除确认操作

const [confirmDelete, deleting] = useSubmitLock(api.deleteUser);
// 防止用户狂点“确定”导致多次删除

场景 4:组合多个异步操作

const [handlePay, paying] = useSubmitLock(async (orderId) => {
  await api.createPayment(orderId);
  await trackEvent('pay_clicked');
  window.location.href = '/payment';
});

注意事项 & 进阶建议

1. 不要用于需要“取消”的操作

此 Hook 适用于“提交即不可逆”的场景。如果是上传、下载等可取消任务,应使用 AbortController

2. 与防重 Token 不冲突

useSubmitLock前端体验层防护,后端仍需配合 Token 或幂等设计做最终校验。

3. Vue 用户怎么办?

同样可封装为 Composable:

// Vue 3 + Composition API
import { ref } from 'vue';

export function useSubmitLock(asyncFn) {
  const isLocked = ref(false);
  
  const wrappedFn = async (...args) => {
    if (isLocked.value) return;
    isLocked.value = true;
    try {
      return await asyncFn(...args);
    } finally {
      isLocked.value = false;
    }
  };

  return { execute: wrappedFn, isLocked };
}

使用:

const { execute: submit, isLocked } = useSubmitLock(api.submit);

更进一步:自动绑定到按钮?

你可以再封装一个 <SubmitButton> 组件:

const SubmitButton = ({ onClick, children, ...props }) => {
  const [handler, loading] = useSubmitLock(onClick);
  return (
    <button
      disabled={loading}
      onClick={handler}
      {...props}
    >
      {loading ? '处理中...' : children}
    </button>
  );
};

// 使用
<SubmitButton onClick={submitOrder}>提交订单</SubmitButton>

从此,防重提交,零成本集成。


结语

优秀的工程师,不是写更多代码,而是让重复的事不再发生

一个小小的 useSubmitLock,背后是对用户体验的尊重,对代码洁癖的坚持,更是对“DRY 原则”的践行。

下次当你又要写第 101 次 loading = true 时,停下来问问自己:
“这事,能不能一次解决?”

把这个 Hook 加到你的工具库里,团队效率提升 10%。

欢迎收藏、转发,拯救还在手写 loading 的同事!


各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

前端如何实现“无感刷新”Token?90% 的人都做错了

作者 前端Hardy
2026年3月24日 18:04

刷新 Token 不是“过期就重新登录”,而是让用户毫无感知地继续使用
可惜,大多数项目还在用 401 跳登录 粗暴处理——这根本不是用户体验,这是放弃治疗。

在现代 Web 应用中,用户登录后通常会获得一对 Token:

  • Access Token(短期有效,如 15 分钟)
  • Refresh Token(长期有效,如 7 天)

当 Access Token 过期时,理想状态是:前端自动用 Refresh Token 换取新 Token,并重试原请求——整个过程用户无感,页面不跳转、操作不中断。

但现实呢?

“Token 过期 → 弹出登录框 → 用户骂一句‘怎么又登出了’ → 关掉页面走人。”

今天,我们就来彻底搞懂:如何真正实现“无感刷新”Token?为什么 90% 的实现都有致命缺陷?


错误做法一:在每个接口里手动判断 401

// 千万别这么写!
fetch('/api/user')
  .then(res => {
    if (res.status === 401) {
      // 重新登录 or 刷新 token?
      window.location.href = '/login';
    }
  });

问题在哪?

  • 每个接口都要重复写逻辑;
  • 如果多个请求同时 401,会触发多次刷新,甚至多次跳登录;
  • 完全无法做到“无感”

错误做法二:全局拦截 401 后直接刷新 Token 并重试一次

这是目前最“主流”的错误方案:

// 伪代码:看似聪明,实则危险
axios.interceptors.response.use(
  res => res,
  async (error) => {
    if (error.response.status === 401) {
      const newToken = await refreshToken(); // 获取新 token
      saveToken(newToken);
      
      // 用新 token 重试原请求
      return axios(error.config);
    }
  }
);

表面看没问题,但隐藏三大坑:

坑 1:并发请求雪崩

当页面刚加载,10 个接口同时发起,而此时 Token 已过期 ——
→ 10 个请求全部返回 401 → 触发 10 次 refreshToken() → 后端收到 10 个刷新请求!

后果:

  • 后端可能拒绝重复刷新(安全策略);
  • Refresh Token 被提前消耗,后续真失效;
  • 用户反而被踢下线。

坑 2:Refresh Token 泄露风险

如果前端把 Refresh Token 存在 localStorage,一旦 XSS 攻击成功,攻击者可长期盗用账号。

安全最佳实践:Refresh Token 应仅存于 HttpOnly Cookie,前端不可读!

但上述方案要求前端“拿到新 token”,这就逼你把 Refresh Token 暴露给 JS —— 安全与功能不可兼得?

坑 3:无限重试死循环

如果 refreshToken() 本身也返回 401(比如 Refresh Token 也过期了),
→ 重试原请求 → 又 401 → 再刷新 → 再 401 → ……
浏览器卡死,内存飙升。


正确方式:用“锁机制 + 队列 + 安全存储”三位一体

要实现真正的无感刷新,必须同时解决:

  1. 并发控制(只刷一次)
  2. 安全存储(Refresh Token 不暴露给 JS)
  3. 失败兜底(Refresh 失败时优雅降级)

第一步:后端配合 —— Refresh Token 存 HttpOnly Cookie

HTTP/1.1 200 OK
Set-Cookie: refreshToken=abc123; HttpOnly; Secure; SameSite=Strict; Path=/auth

前端永远拿不到 refreshToken,但每次请求会自动携带。

第二步:前端实现“单例刷新锁 + 请求队列”

let isRefreshing = false;
let refreshPromise = null;
const failedQueue = [];

// 重试队列中的请求
const processQueue = (error, token = null) => {
  failedQueue.forEach(({ resolve, reject }) => {
    if (error) {
      reject(error);
    } else {
      resolve(token);
    }
  });
  failedQueue.length = 0;
};

axios.interceptors.response.use(
  response => response,
  async (error) => {
    const originalRequest = error.config;

    if (error.response?.status === 401 && !originalRequest._retry) {
      if (isRefreshing) {
        // 已在刷新中,将请求加入队列,等待新 token
        return new Promise((resolve, reject) => {
          failedQueue.push({ resolve, reject });
        }).then(token => {
          originalRequest.headers['Authorization'] = `Bearer ${token}`;
          return axios(originalRequest);
        });
      }

      originalRequest._retry = true;
      isRefreshing = true;

      try {
        // 调用刷新接口(后端从 Cookie 读 refreshToken)
        const { data } = await axios.post('/auth/refresh');
        const newAccessToken = data.accessToken;

        // 通知所有排队的请求
        processQueue(null, newAccessToken);

        // 重试当前请求
        originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;
        return axios(originalRequest);
      } catch (refreshError) {
        // 刷新失败:清空本地身份,跳转登录
        clearAuth();
        processQueue(refreshError, null);
        window.location.href = '/login';
        return Promise.reject(refreshError);
      } finally {
        isRefreshing = false;
        refreshPromise = null;
      }
    }

    return Promise.reject(error);
  }
);

关键设计解析:

机制 作用
isRefreshing 确保同一时间只发起一次刷新
failedQueue 队列 缓存所有因 401 失败的请求,等新 token 到手后批量重试
_retry 标记 防止重试后的请求再次进入刷新逻辑
HttpOnly Cookie 保护 Refresh Token 不被 XSS 窃取

安全补充:前端 Token 存储建议

Token 类型 推荐存储方式 原因
Access Token 内存(JS 变量)或 sessionStorage 短期有效,避免持久化泄露
Refresh Token HttpOnly Cookie 前端不可读,防 XSS

切勿将任何 Token 存入 localStorage!这是 XSS 攻击的黄金目标。


如何测试你的刷新逻辑?

  1. 手动将 Access Token 设为过期;
  2. 快速点击多个按钮,触发并发请求;
  3. 观察 Network 面板:
    • 是否只调用了一次 /auth/refresh
    • 所有原请求是否最终成功?
  4. 模拟 Refresh Token 失效,是否跳转登录?

结语

“无感刷新 Token”不是炫技,而是对用户体验和系统安全的基本尊重。
那些让用户频繁重新登录的产品,不是技术做不到,而是没把用户当回事

真正的专业,藏在细节里:
一个锁、一个队列、一个 HttpOnly Cookie —— 就是 10% 正确方案 与 90% 错误实现的分水岭。

你的项目还在用“401 就跳登录”吗?是时候升级了。

欢迎转发给那个总说“Token 过期就让用户重新登录”的同事。


各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

Wails v3 正式发布:用 Go 写桌面应用,体积仅 12MB,性能飙升 40%!

作者 前端Hardy
2026年3月24日 18:03

一个 12MB 的桌面应用,启动不到 0.5 秒,内存占用仅 70MB——
前端仍是 Vue/React,后端是纯 Go,无需 Node.js,不嵌 Chromium,双击即运行。

如果你曾因 Electron 的臃肿而却步,又觉得 Tauri 的 Rust 门槛太高,那么 Wails v3 的正式发布,或许正是 Go 开发者和前端工程师共同等待的“理想平衡点”。


一、桌面开发的新选择:Go 的优雅回归

过去几年,桌面应用框架基本被两大阵营主导:

  • Electron:简单但笨重;
  • Tauri:轻量但需 Rust。

Wails 自 2019 年诞生以来,一直坚持一条独特路径:

用 Go 构建高性能后端,用 Web 技术构建现代 UI,最终编译为单文件原生应用。

如今,随着 Wails v3 在 2025 年底正式 GA(General Availability),它不仅完成架构重构,更带来:

  • 全新 WebEngine Core 渲染引擎
  • 二进制通信协议(吞吐量提升 3 倍)
  • 多窗口原生支持
  • Bazel 多平台构建系统
  • 企业级插件生态

最重要的是——前端开发者几乎无需改变习惯


二、v3 为何能比 v2 再小 30%?性能提升从何而来?

Wails v3 的核心突破,在于彻底重构底层架构:

组件 Wails v2 Wails v3
渲染引擎 系统 WebView(WebView2 / WebKit) WebEngine Core(轻量 Blink 内核)
通信层 JSON over IPC Protocol Buffers 二进制协议
内存占用 ≈120MB ≈70MB(降低 40%)
启动时间 0.8–1.2s 0.4–0.6s
构建系统 Go build + Makefile Bazel 多平台构建(增量编译提速 60%)
原生集成 基础 API WinUI 3 / SwiftUI / GTK 4 深度支持

关键升级解析:

WebEngine Core:告别 WebView2 依赖

v3 不再依赖用户是否安装 WebView2(Windows 常见痛点),而是内置 精简版 Blink 引擎,移除冗余模块,基础应用启动内存从 120MB 降至 70MB

二进制通信:消息吞吐量达 6000 条/秒

从前端调用 Go 方法,不再经过 JSON 序列化,而是通过 Protobuf 编码的二进制流,高频交互场景(如实时图表、日志流)性能提升 300%

插件系统:wails plugin install 即可扩展

官方已上线插件市场,支持数据库连接、AI 推理、OAuth 登录等,社区可自由贡献。


三、前端开发者会被 Go 劝退吗?

完全不会!Wails 的设计哲学始终是:Go 只做它最擅长的事——系统交互与高性能计算

比如,从前端保存一个文件:

// frontend/src/App.vue (Vue 3 + TypeScript)
import { saveFile } from '@/wailsjs/go/main/App';

const handleSave = async () => {
  await saveFile('Hello from Wails v3!');
  alert('Saved!');
};

而后端只需定义一个公开方法:

// backend/app.go
package main

import "os"

type App struct{}

// 自动暴露为前端可调用函数
func (a *App) SaveFile(content string) error {
    return os.WriteFile("output.txt", []byte(content), 0644)
}

Wails 自动生成类型安全的 TypeScript SDK,无需手动写桥接代码,也无需 REST API 或 WebSocket。


四、实测:v3 vs v2 vs Electron

我们构建一个带聊天室、本地 SQLite 存储、系统通知的桌面应用:

指标 Electron Wails v2 Wails v3
打包体积 148 MB 18.2 MB 12.3 MB
冷启动时间 2.4s 0.9s 0.5s
内存占用(空窗) 295 MB 120 MB 70 MB
消息吞吐量 2000 msg/s 2000 msg/s 6000 msg/s
首屏加载(含历史记录) 1.8s 0.7s 0.3s

更惊人的是:Wails v3 支持热重载 2.0——修改 Go 或 Vue 文件,应用状态保持率高达90% ,开发体验接近纯 Web。


五、多窗口、原生菜单、沙箱……v3 全都有了

Wails v3 终于补齐了企业级应用所需的关键能力:

  • 多窗口支持app.NewWindow() 创建独立窗口,各自管理生命周期;
  • 原生系统菜单
    app.SetNativeMenu(wails.NativeMenu{
        Items: []wails.MenuItem{
            {Title: "Preferences", Action: "showPrefs", Shortcut: "Cmd+,"},
        },
    })
    
  • 自动沙箱隔离:渲染进程与主进程分离,防止 XSS 攻击扩散;
  • UPX 压缩集成:构建时自动压缩二进制,体积再减 35%。

六、5 分钟上手 Wails v3

# 1. 安装 Go 1.21+ 和 Wails CLI
go install github.com/wailsapp/wails/v3/cmd/wails@latest

# 2. 创建 Vue 3 + TypeScript 项目
wails init -n my-app -t vue-ts

# 3. 进入目录并启动开发(支持热重载)
cd my-app
wails dev

# 4. 打包发布(生成单文件可执行程序)
wails build

你会得到一个 12MB 左右的独立程序,无外部依赖,双击即运行。


七、谁在用 Wails v3?

  • AI 初创公司:本地 LLM 客户端(如私有知识库问答工具);
  • 金融科技团队:加密数据处理、合规审计工具;
  • DevOps 工程师:K8s 集群监控面板、日志分析器;
  • 开源社区:多个数据库 GUI 工具(如 DBeaver 轻量替代)正在迁移。

GitHub 上,Wails 主仓库 Star 数已突破 33.4k,v3 发布后月活跃贡献者增长250%


结语:Go + Web,刚刚好

Wails v3 的发布,标志着它从“个人开发者玩具”正式升级为“企业级桌面开发平台”。

它不追求取代 Electron,也不对标 Tauri。
它只是提供一种可能:用最熟悉的前端,搭配最高效的后端,做出最轻量、最安全、最快速的桌面应用

在这个“资源即成本”的时代,12MB 不仅是一个数字,更是对用户设备、网络带宽和开发效率的尊重。

官网:wails.io
GitHub:github.com/wailsapp/wa…
迁移指南:官方提供 wails migrate 工具,支持 v2 → v3 平滑升级

你的团队用过 Go 做桌面应用吗?评论区聊聊体验!


各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

纯 HTML/CSS/JS 实现的高颜值登录页,还会眨眼睛!少女心爆棚!

作者 前端Hardy
2026年3月24日 17:38

演示效果

演示效果

上周,产品经理说:“我们的登录页太冷了,像银行系统。”

我心想:不就是个输入框 + 按钮?能有多冷?

直到我看到数据——用户平均停留 8 秒,跳出率67%。

那一刻我意识到:在体验经济时代,登录页不是入口,而是第一印象。

于是,我花了 2 小时,用纯 HTML/CSS/JS 写了一个“会呼吸”的登录页:

  • 背景是流动的樱花渐变
  • 四个守护精灵会转头看你
  • 眼球能精准追踪鼠标,还会眨眼
  • 输入用户名时,左边两个“保镖”会 Q 弹靠近

上线三天后,用户停留时长涨到22 秒,注册转化率提升 34%。

今天,我就把这份“有温度的代码”开源出来,并告诉你:前端,也可以很浪漫。


一、为什么登录页值得认真做?

很多人觉得:“登录页只是跳板,做完就行。”

但用户心理是这样的:

  • 第一眼看到页面 → 判断产品调性
  • 如果冰冷、机械、无趣 → “这产品大概也不 care 我”
  • 如果温暖、灵动、有细节 → “他们连登录页都这么用心,功能肯定靠谱”

登录页,是你和用户的第一次约会。

而我们的目标,不是“能用”,而是——让用户多看一眼,再看一眼。


二、核心设计:四个“樱花守护者”

整个页面的灵魂,是左侧那四个圆滚滚的“保镖”。
它们不是静态插图,而是有生命的小精灵

  • 配色柔和:浅粉、薰衣草紫、玫瑰粉、奶白,拒绝刺眼荧光
  • 眼神灵动:双眼中带高光,随鼠标移动,幅度明显但不夸张
  • 微交互反馈:聚焦用户名时,左边两位“凑近偷看”;聚焦密码时,右边两位“紧张张望”
  • 呼吸感动画:背景渐变流动 + 装饰云朵飘过 + 腮红微微闪烁

这一切,只用了 300 行原生代码,零框架、零依赖


三、关键技术点拆解(附核心代码)

1. 眼球追踪:让“看”变得真实

很多人做视线追踪,只动头部。但真正打动人的是眼睛

// 鼠标移动时,计算相对位置
const xPercent = (mouseX / windowWidth) - 0.5;
const yPercent = (mouseY / windowHeight) - 0.5;

// 【关键】眼球移动幅度拉大到 12px(原常见实现仅 3–4px)
allEyes.forEach(eye => {
  const moveX = xPercent * 12; // ← 让眼神“明显在追你”
  const moveY = yPercent * 6;
  eye.style.transform = `translate(${moveX}px, ${moveY}px)`;
});

效果:用户一眼就能感知“它在看我”,产生情感连接。

2. 头部微转:增加层次感

头部转动幅度小、方向交替,避免“集体僵尸舞”:

// 不同保镖朝向微调,制造错落感
const rotateY = xPercent * 10 * (index % 2 === 0 ? 1 : -1);
avatar.style.transform = `rotateY(${rotateY}deg) rotateX(${-yPercent * 8}deg)`;

3. 输入聚焦反馈:Q 弹靠近

当用户输入时,对应保镖“凑近关心”:

usernameInput.addEventListener('focus', () => {
  g1.style.transform = 'scale(1.15) rotateY(12deg)';
  g2.style.transform = 'scale(1.15) rotateY(-12deg)';
});

这种“拟人化”反馈,让用户感觉“有人在陪我”。

4. 视觉氛围:流动的樱花宇宙

  • 背景linear-gradient(135deg, #ffd1dc, #e0bbe4, #d291bc) + animation: gradientFlow
  • 装饰:飘动的 ❤、✿、☁,用 opacity: 0.6 + pointer-events: none 避免干扰
  • 字体Pacifico(手写体标题) + Quicksand(圆润正文),瞬间可爱度拉满

四、为什么它有效?背后的心理学

  • 拟人效应(Anthropomorphism):人类天生对“有眼睛”的物体产生信任
  • 微交互反馈:让用户感到“我的操作被看见了”
  • 色彩心理学:粉色系传递安全、温柔、包容的情绪
  • 动效节奏:慢速流动(15s 渐变)+ 快速响应(眼球追踪),张弛有度

这不是“花里胡哨”,而是用设计语言说“欢迎你”


五、完整代码已开源,复制即用!

我把整个页面打包成一个 单 HTML 文件,无需构建、无需依赖,打开即运行。

5 分钟,让你的登录页从“工具”变成“体验”。

<!DOCTYPE html>
<html lang="zh-CN">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Sakura Login | 樱花守护</title>
  <!-- 引入可爱字体 -->
  <link href="https://fonts.googleapis.com/css2?family=Pacifico&family=Quicksand:wght@400;500;600;700&display=swap"
    rel="stylesheet">
  <style>
    :root {
      /* 提取自您提供的 CSS */
      --bg-start: #ffd1dc;
      --bg-mid: #e0bbe4;
      --bg-end: #d291bc;
      --text-main: #5a3d5c;
      --text-dim: #8a6d8b;
      --accent-pink: #ff69b4;
      --accent-light: #ffb6c1;
      --white-glass: rgba(255, 255, 255, 0.85);

      /* 保镖专属柔和色系 */
      --guard-1: #ffcce0;
      /* 浅粉 */
      --guard-2: #e6c2ff;
      /* 浅紫 */
      --guard-3: #ff99ac;
      /* 玫瑰粉 */
      --guard-4: #fff0f5;
      /* 薰衣草白 */
    }

    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    body {
      /* 核心背景:樱花渐变 */
      background: linear-gradient(135deg, var(--bg-start), var(--bg-mid), var(--bg-end));
      background-size: 200% 200%;
      animation: gradientFlow 15s ease infinite;

      color: var(--text-main);
      min-height: 100vh;
      display: flex;
      justify-content: center;
      align-items: center;
      font-family: 'Quicksand', sans-serif;
      overflow: hidden;
      position: relative;
    }

    @keyframes gradientFlow {
      0% {
        background-position: 0% 50%;
      }

      50% {
        background-position: 100% 50%;
      }

      100% {
        background-position: 0% 50%;
      }
    }

    /* --- 背景装饰 (提取自您的代码) --- */
    .decoration-container {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      pointer-events: none;
      z-index: 0;
      overflow: hidden;
    }

    .heart,
    .flower,
    .cloud {
      position: absolute;
      opacity: 0.6;
    }

    .heart {
      color: rgba(255, 105, 180, 0.4);
      font-size: 24px;
      animation: float 8s infinite ease-in-out;
    }

    .flower {
      color: rgba(255, 215, 0, 0.4);
      font-size: 28px;
      animation: rotate 20s infinite linear;
    }

    .cloud {
      color: rgba(255, 255, 255, 0.7);
      font-size: 50px;
      animation: drift 30s infinite linear;
    }

    @keyframes float {

      0%,
      100% {
        transform: translateY(0) rotate(0deg);
      }

      50% {
        transform: translateY(-20px) rotate(10deg);
      }
    }

    @keyframes rotate {
      0% {
        transform: rotate(0deg);
      }

      100% {
        transform: rotate(360deg);
      }
    }

    @keyframes drift {
      0% {
        transform: translateX(-100px);
      }

      100% {
        transform: translateX(calc(100vw + 100px));
      }
    }

    /* --- 主体容器 --- */
    .container {
      position: relative;
      z-index: 10;
      display: flex;
      width: 900px;
      max-width: 95%;
      background: var(--white-glass);
      backdrop-filter: blur(15px);
      -webkit-backdrop-filter: blur(15px);
      border: 2px solid rgba(255, 255, 255, 0.6);
      border-radius: 30px;
      box-shadow: 0 15px 35px rgba(90, 61, 92, 0.15);
      overflow: hidden;
    }

    /* 左侧保镖区域 */
    .guards-panel {
      flex: 1.2;
      display: grid;
      grid-template-columns: repeat(2, 1fr);
      grid-template-rows: repeat(2, 1fr);
      padding: 30px;
      gap: 20px;
      background: rgba(255, 255, 255, 0.3);
      border-right: 1px solid rgba(255, 255, 255, 0.5);
      position: relative;
    }

    .guard {
      position: relative;
      display: flex;
      justify-content: center;
      align-items: center;
      perspective: 1000px;
      cursor: pointer;
    }

    .guard-avatar {
      width: 90px;
      height: 90px;
      border-radius: 50%;
      display: flex;
      justify-content: center;
      align-items: center;
      position: relative;
      background: #fff;
      border: 3px solid #fff;
      box-shadow: 0 8px 20px rgba(90, 61, 92, 0.1);
      transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
      overflow: hidden;
      will-change: transform;
    }

    /* 保镖配色 */
    .guard-1 .guard-avatar {
      background: var(--guard-1);
      box-shadow: 0 8px 20px rgba(255, 204, 224, 0.6);
    }

    .guard-2 .guard-avatar {
      background: var(--guard-2);
      box-shadow: 0 8px 20px rgba(230, 194, 255, 0.6);
    }

    .guard-3 .guard-avatar {
      background: var(--guard-3);
      box-shadow: 0 8px 20px rgba(255, 153, 172, 0.6);
    }

    .guard-4 .guard-avatar {
      background: var(--guard-4);
      box-shadow: 0 8px 20px rgba(255, 240, 245, 0.6);
    }

    .guard:hover .guard-avatar {
      transform: scale(1.15) !important;
      z-index: 20;
      box-shadow: 0 12px 30px rgba(255, 105, 180, 0.3);
    }

    /* 机械眼结构 (适配可爱风) */
    .visor {
      width: 65%;
      height: 22%;
      background: rgba(255, 255, 255, 0.5);
      border-radius: 12px;
      position: relative;
      display: flex;
      justify-content: space-around;
      align-items: center;
      padding: 0 4px;
      border: 1px solid rgba(255, 255, 255, 0.8);
      box-shadow: inset 0 2px 4px rgba(90, 61, 92, 0.05);
    }

    .eye {
      width: 12px;
      height: 12px;
      border-radius: 50%;
      background: var(--text-main);
      /* 深紫色眼珠 */
      position: relative;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
      transition: transform 0.1s ease-out;
      will-change: transform;
    }

    /* 眼神高光 */
    .eye::after {
      content: '';
      position: absolute;
      width: 4px;
      height: 4px;
      border-radius: 50%;
      background: #fff;
      top: 20%;
      left: 20%;
      opacity: 0.9;
    }

    /* 腮红/状态灯 */
    .blush {
      position: absolute;
      bottom: 18px;
      width: 8px;
      height: 5px;
      border-radius: 50%;
      background: rgba(255, 105, 180, 0.4);
      filter: blur(1px);
      animation: blinkBlush 3s infinite;
    }

    @keyframes blinkBlush {

      0%,
      100% {
        opacity: 0.4;
      }

      50% {
        opacity: 0.8;
      }
    }

    /* 右侧表单区域 */
    .login-panel {
      flex: 1;
      padding: 40px;
      display: flex;
      flex-direction: column;
      justify-content: center;
      position: relative;
      background: rgba(255, 255, 255, 0.4);
    }

    .login-header {
      text-align: center;
      margin-bottom: 30px;
    }

    .login-header h2 {
      font-family: 'Pacifico', cursive;
      font-size: 38px;
      font-weight: 400;
      margin-bottom: 8px;
      background: linear-gradient(90deg, var(--accent-pink), var(--bg-end));
      -webkit-background-clip: text;
      background-clip: text;
      color: transparent;
      letter-spacing: 1px;
      text-shadow: 0 2px 10px rgba(255, 105, 180, 0.2);
    }

    .login-header p {
      font-size: 15px;
      color: var(--text-dim);
      line-height: 1.5;
    }

    .form-group {
      margin-bottom: 20px;
      position: relative;
    }

    .form-group label {
      display: block;
      color: var(--text-main);
      font-size: 13px;
      margin-bottom: 8px;
      font-weight: 600;
      letter-spacing: 0.5px;
      margin-left: 5px;
    }

    .form-group input {
      width: 100%;
      padding: 14px 18px;
      background: rgba(255, 255, 255, 0.7);
      border: 2px solid #ffd1dc;
      border-radius: 15px;
      color: var(--text-main);
      font-size: 15px;
      outline: none;
      transition: all 0.3s;
      font-family: 'Quicksand', sans-serif;
    }

    .form-group input:focus {
      background: #fff;
      border-color: var(--accent-pink);
      box-shadow: 0 0 0 4px rgba(255, 105, 180, 0.15);
      transform: translateY(-2px);
    }

    .form-group input::placeholder {
      color: #c49bb8;
    }

    .actions {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 25px;
      font-size: 13px;
      color: var(--text-dim);
      padding: 0 5px;
    }

    .actions label {
      display: flex;
      align-items: center;
      cursor: pointer;
      color: var(--text-dim);
    }

    .actions input[type="checkbox"] {
      margin-right: 6px;
      accent-color: var(--accent-pink);
      cursor: pointer;
      width: 16px;
      height: 16px;
    }

    .actions a {
      color: var(--accent-pink);
      text-decoration: none;
      font-weight: 600;
      transition: color 0.3s;
    }

    .actions a:hover {
      color: var(--bg-end);
      text-decoration: underline;
    }

    button {
      width: 100%;
      padding: 16px;
      background: linear-gradient(90deg, var(--accent-pink), var(--bg-end));
      color: white;
      border: none;
      border-radius: 18px;
      font-weight: 700;
      font-size: 18px;
      cursor: pointer;
      position: relative;
      overflow: hidden;
      transition: all 0.3s;
      box-shadow: 0 8px 20px rgba(255, 105, 180, 0.4);
      letter-spacing: 1px;
      font-family: 'Quicksand', sans-serif;
    }

    button:hover {
      transform: translateY(-3px);
      box-shadow: 0 12px 25px rgba(255, 105, 180, 0.5);
      filter: brightness(1.05);
    }

    button:active {
      transform: translateY(1px);
    }

    button::after {
      content: '';
      position: absolute;
      top: 0;
      left: -100%;
      width: 100%;
      height: 100%;
      background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
      transition: 0.5s;
    }

    button:hover::after {
      left: 100%;
    }

    /* 响应式 */
    @media (max-width: 768px) {
      .container {
        flex-direction: column;
        width: 90%;
      }

      .guards-panel {
        grid-template-columns: repeat(4, 1fr);
        padding: 20px;
        border-right: none;
        border-bottom: 1px solid rgba(255, 255, 255, 0.5);
      }

      .guard-avatar {
        width: 60px;
        height: 60px;
      }

      .visor {
        width: 60%;
        height: 20%;
      }

      .eye {
        width: 8px;
        height: 8px;
      }

      .login-panel {
        padding: 30px;
      }

      .login-header h2 {
        font-size: 32px;
      }
    }
  </style>
</head>

<body>

  <!-- 背景装饰 -->
  <div class="decoration-container">
    <!-- 动态生成一些装饰物 -->
    <div class="heart" style="top: 10%; left: 10%;"></div>
    <div class="heart" style="top: 20%; right: 15%; animation-delay: -2s;"></div>
    <div class="flower" style="top: 60%; left: 5%; animation-delay: -5s;"></div>
    <div class="flower" style="bottom: 15%; right: 10%;"></div>
    <div class="cloud" style="top: 5%; left: -10%;"></div>
    <div class="cloud" style="top: 40%; right: -5%; animation-delay: -15s;"></div>
  </div>

  <div class="container">
    <!-- 左侧:四个樱花守护精灵 -->
    <div class="guards-panel">
      <div class="guard guard-1" id="g1">
        <div class="guard-avatar">
          <div class="visor">
            <div class="eye" id="e1-1"></div>
            <div class="eye" id="e1-2"></div>
          </div>
          <div class="blush"></div>
        </div>
      </div>
      <div class="guard guard-2" id="g2">
        <div class="guard-avatar">
          <div class="visor">
            <div class="eye" id="e2-1"></div>
            <div class="eye" id="e2-2"></div>
          </div>
          <div class="blush"></div>
        </div>
      </div>
      <div class="guard guard-3" id="g3">
        <div class="guard-avatar">
          <div class="visor">
            <div class="eye" id="e3-1"></div>
            <div class="eye" id="e3-2"></div>
          </div>
          <div class="blush"></div>
        </div>
      </div>
      <div class="guard guard-4" id="g4">
        <div class="guard-avatar">
          <div class="visor">
            <div class="eye" id="e4-1"></div>
            <div class="eye" id="e4-2"></div>
          </div>
          <div class="blush"></div>
        </div>
      </div>
    </div>

    <!-- 右侧:登录表单 -->
    <div class="login-panel">
      <div class="login-header">
        <h2>Welcome Love</h2>
        <p>请输入您的信息,开启梦幻之旅</p>
      </div>

      <form onsubmit="event.preventDefault();">
        <div class="form-group">
          <label for="username">用户名</label>
          <input type="text" id="username" placeholder="Your Name" autocomplete="off">
        </div>

        <div class="form-group">
          <label for="password">密码</label>
          <input type="password" id="password" placeholder="••••••••" autocomplete="off">
        </div>

        <div class="actions">
          <label>
            <input type="checkbox"> 记住我
          </label>
          <a href="#">忘记密码?</a>
        </div>

        <button type="submit">立即登录</button>
      </form>
    </div>
  </div>

  <script>
    const guards = document.querySelectorAll('.guard');
    const allEyes = document.querySelectorAll('.eye');
    const usernameInput = document.getElementById('username');
    const passwordInput = document.getElementById('password');

    // --- 增强的视线追踪逻辑 ---
    document.addEventListener('mousemove', (e) => {
      const mouseX = e.clientX;
      const mouseY = e.clientY;
      const windowWidth = window.innerWidth;
      const windowHeight = window.innerHeight;

      const xPercent = (mouseX / windowWidth) - 0.5;
      const yPercent = (mouseY / windowHeight) - 0.5;

      guards.forEach((guard, index) => {
        const avatar = guard.querySelector('.guard-avatar');

        // 头部转动保持不变 (柔和)
        const rotateY = xPercent * 10 * (index % 2 === 0 ? 1 : -1);
        const rotateX = -yPercent * 8;

        avatar.style.transform = `rotateY(${rotateY}deg) rotateX(${rotateX}deg)`;
      });

      // 【修改点】眼球移动幅度大幅增加:从 4px 改为 12px
      // 现在左右移动非常明显,能一眼看出眼神在跟随
      allEyes.forEach(eye => {
        const moveX = xPercent * 12;  // 之前是 4,现在是 12
        const moveY = yPercent * 6;   // 上下也稍微增加一点,保持自然比例
        eye.style.transform = `translate(${moveX}px, ${moveY}px)`;
      });
    });

    // 输入框焦点交互 (Q弹可爱效果)
    usernameInput.addEventListener('focus', () => {
      const g1 = document.getElementById('g1').querySelector('.guard-avatar');
      const g2 = document.getElementById('g2').querySelector('.guard-avatar');
      g1.style.transform = 'scale(1.15) rotateY(12deg)';
      g2.style.transform = 'scale(1.15) rotateY(-12deg)';
    });

    usernameInput.addEventListener('blur', () => {
      document.getElementById('g1').querySelector('.guard-avatar').style.transform = '';
      document.getElementById('g2').querySelector('.guard-avatar').style.transform = '';
    });

    passwordInput.addEventListener('focus', () => {
      const g3 = document.getElementById('g3').querySelector('.guard-avatar');
      const g4 = document.getElementById('g4').querySelector('.guard-avatar');
      g3.style.transform = 'scale(1.15) rotateY(12deg)';
      g4.style.transform = 'scale(1.15) rotateY(-12deg)';
    });

    passwordInput.addEventListener('blur', () => {
      document.getElementById('g3').querySelector('.guard-avatar').style.transform = '';
      document.getElementById('g4').querySelector('.guard-avatar').style.transform = '';
    });
  </script>
</body>

</html>

结语:前端,不止于逻辑

我们总在讨论性能、架构、工程化,
却忘了——代码也可以传递情感

一个会眨眼的保镖,
一段流动的樱花背景,
一句“Welcome Love”的问候,

可能比十个埋点、百行优化,更能留住一个人。

今天,就给你的登录页,加一点温度吧。


各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

❌
❌