AI 生成的前端代码在本地正常但生产环境出现布局抖动、内容溢出、CLS 等五类视觉 bug,文章给出具体修复代码。
如果你在前端工作中使用 Cursor、Claude 或 Copilot,应该已经有过这种体验。
你让模型生成一个仪表盘卡片或一个模态框组件,它在两秒内吐出了五十行整洁的 TypeScript 和 Tailwind 类。编译通过,没有报错。在本地浏览器中用示例数据查看,效果看起来可以直接上线。
然后你把它部署到 staging 环境或在手机上打开,各种细微的 UI bug 就冒出来了。
按钮加载时布局跳动。长用户名溢出容器。禁用按钮上的 tooltip 不生效。小屏幕上文字被截断。
AI 模型很擅长语法,但往往会跳过防御性 CSS 模式。以下是 AI 编码助手在几乎每次提示时都会引入的五种视觉 bug,以及防止这些 bug 的具体代码修复方法。
当你让 AI 给按钮添加加载状态时,它几乎总是写出类似这样的代码:
// Typical AI code
<button disabled={isLoading} className="px-4 py-2 bg-blue-600 text-white rounded-md">
{isLoading ? <Spinner className="w-4 h-4" /> : "Save Changes"}
</button>
问题是,16px 的加载动画比文字"Save Changes"窄得多。用户一点击,按钮宽度就收缩了。相邻的输入框和按钮在屏幕上发生位移,导致令人不适的累积布局偏移(Cumulative Layout Shift,CLS)。
保持原始标签在 DOM 中以锁定按钮宽度,并把加载动画放在绝对定位的遮罩层中:
// Production fix
<button disabled={isLoading} className="relative px-4 py-2 bg-blue-600 text-white rounded-md">
<span className={isLoading ? "invisible" : "visible"}>Save Changes</span>
{isLoading && (
<span className="absolute inset-0 flex items-center justify-center">
<Spinner className="w-4 h-4 animate-spin" />
</span>
)}
</button>
这样无论状态如何变化,按钮宽度始终保持固定。
AI 工具喜欢用 flexbox,但很少包含防御性的宽度边界。当用户有一个很长的名字,或者上传了标题长达五十个字符的文件时,布局就会坏掉。
// Typical AI code
<div className="flex items-center gap-3">
<Avatar />
<div className="flex flex-col">
<span className="font-medium truncate">{user.name}</span>
<span className="text-sm text-slate-500 truncate">{user.email}</span>
</div>
</div>
在这里添加 truncate 没有任何作用,因为 flex 子元素默认是 min-width: auto。如果容器被挤压,文字拒绝收缩,而是把父元素推出视口。
给包含文字的 flex 子元素添加 min-w-0:
// Production fix
<div className="flex items-center gap-3">
<Avatar />
<div className="flex flex-col min-w-0">
<span className="font-medium truncate">{user.name}</span>
<span className="text-sm text-slate-500 truncate">{user.email}</span>
</div>
</div>
就这么一个工具类,告诉浏览器这个 flex 子元素允许收缩到低于内容宽度,使文字截断按预期工作。
当某个操作被阻止时,通常需要用 tooltip 解释原因(例如"你需要管理员权限才能删除此项目")。
AI 模型几乎总是直接禁用按钮:
// Typical AI code
<Tooltip content="Admin permissions required">
<button disabled={!isAdmin} className="btn-primary">
Delete Project
</button>
</Tooltip>
当 HTML 按钮具有原生 disabled 属性时,浏览器会停止在该元素上触发所有鼠标事件。鼠标悬停在按钮上不会触发 mouseEnter,tooltip 永远不会渲染。用户点击一个死的按钮,却完全不知道它为什么不能点击。
改用 aria-disabled 并手动管理交互样式:
// Production fix
<Tooltip content={!isAdmin ? "Admin permissions required" : ""}>
<button
aria-disabled={!isAdmin}
onClick={(e) => {
if (!isAdmin) {
e.preventDefault();
return;
}
handleDelete();
}}
className={`btn-primary ${!isAdmin ? "opacity-50 cursor-not-allowed pointer-events-auto" : ""}`}
>
Delete Project
</button>
</Tooltip>
按钮保持键盘和屏幕阅读器可访问,鼠标事件仍然向上冒泡传递给 tooltip,用户得到了清晰的解释。
让 AI 模型生成一个模态框、抽屉或设置面板时,它经常设置一个固定高度:
// Typical AI code
<div className="fixed inset-0 bg-black/50 flex items-center justify-center">
<div className="w-[500px] h-[600px] bg-white rounded-lg p-6 flex flex-col justify-between">
<ModalHeader />
<ModalBody />
<ModalFooter />
</div>
</div>
在桌面显示器上,600px 看起来没问题。在较小的手机屏幕上,或者在打开了浏览器工具栏的笔记本上,模态框的 footer 会被推到屏幕底部边缘之外。用户看不到也点不到提交按钮。
改用动态视口单位并设置内部滚动区域:
// Production fix
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4">
<div className="w-full max-w-lg max-h-[90dvh] bg-white rounded-lg flex flex-col">
<div className="p-6 border-b border-slate-200">
<ModalHeader />
</div>
<div className="p-6 overflow-y-auto flex-1">
<ModalBody />
</div>
<div className="p-6 border-t border-slate-200">
<ModalFooter />
</div>
</div>
</div>
header 和 footer 保持在视口中可见,而长表单内容在 ModalBody 内部干净地滚动。
在生成编辑模态框时,AI 工具经常将表单输入存储在组件状态中,但当目标实体发生变化时不会重置它们。
// Typical AI code
function EditUserModal({ user, isOpen, onClose }) {
const [name, setName] = useState(user.name);
const [role, setRole] = useState(user.role);
if (!isOpen) return null;
return (
<Modal onClose={onClose}>
<input value={name} onChange={(e) => setName(e.target.value)} />
</Modal>
);
}
如果用户编辑了"Alice",关闭了模态框,然后点击编辑"Bob",输入框仍然显示"Alice"。状态在首次挂载时初始化,之后再也没有刷新。
在调用站点按实体 ID 给模态框实例加 key,使 React 在实体变化时销毁并重新挂载fresh状态:
// Production fix at the parent call site
{activeUser && (
<EditUserModal
key={activeUser.id}
user={activeUser}
isOpen={Boolean(activeUser)}
onClose={() => setActiveUser(null)}
/>
)}
使用 key 边界比在子组件里塞一堆 useEffect 重置钩子更简洁、出错概率更低。
一次又一次地逐个提示修复这些边缘情况会占用大量代码审查时间。
一种解决方案是在 .cursorrules 或 AGENTS.md 文件中定义严格的 CSS 规则,这样模型就不会再使用脆弱的模式。另一种方法是维护一套专用的无障碍 UI 原始组件,这些组件开箱即用地处理布局抖动、触摸目标尺寸和视口边界。
我们在 devpreflight.com 为 React 19 和 Next.js 16 整理了一个开源的无障碍扁平 UI 原始组件集合,帮助开发者避免在每个项目中重复构建这些防护措施。