做界面:令牌、组件与截图自查
先定设计令牌再写组件,用假数据做出响应式、支持暗色模式的书单界面,让 Claude Code 自己截图检查,并避开千篇一律的 AI 味审美。
学完这一课,你能
- 能用 Tailwind 4 的 @theme 和 CSS 变量定义语义化设计令牌
- 能把界面拆成可复用的 Astro 组件,并用带边界情况的假数据驱动
- 能让智能体在浏览器里截图、对照清单自查并修复
- 能写出避免「AI 味」的设计约束,而不是只说「好看一点」
你对智能体说「把界面做好看一点」,它会交出一个看起来很熟悉的页面:居中的大标题、紫蓝渐变、三张圆角阴影卡片,加一个到处都是的 emoji。每一处都不算错,合在一起却像任何一个网站,唯独不像你的。
原因很直白。「好看」是一个没有边界的词,智能体只能取训练数据里的平均值。这一课解决两个问题:怎样把审美变成智能体能执行的约束,怎样让它自己检查成果,而不是靠你一遍遍说「再改改」。
先定令牌,再写组件
设计令牌(design token)是把颜色、字体、间距、圆角这些选择命名后集中放在一处。先定令牌再写组件,有两个好处:改风格只改一个地方;智能体写组件时有现成的词汇可用,不会随手写 #6366f1。
Tailwind 4 把令牌放在 CSS 里,用 @theme 声明。官方的规则是:变量名的前缀决定生成什么工具类,比如 --color-* 会生成 bg-*、text-*、border-*。需要让令牌引用其他变量时,用 @theme inline。
我们做两层:底层是随明暗模式变化的 CSS 变量,上层是 Tailwind 令牌,只引用底层。这样组件里写 bg-paper text-ink 就够了,几乎不用到处写 dark:。
/* src/styles/global.css */
@import "tailwindcss";
/* 手动切换主题:由 html 上的 data-theme 属性驱动 */
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
:root {
--paper: oklch(0.97 0.012 85); /* 页面底色:偏暖的纸色 */
--paper-raised: oklch(0.995 0.004 85); /* 卡片底色 */
--ink: oklch(0.25 0.02 60); /* 正文 */
--ink-soft: oklch(0.45 0.02 60); /* 次要文字 */
--line: oklch(0.88 0.015 80); /* 分隔线 */
--accent: oklch(0.5 0.13 35); /* 唯一强调色:赭红 */
}
:root[data-theme="dark"] {
--paper: oklch(0.2 0.012 60);
--paper-raised: oklch(0.25 0.014 60);
--ink: oklch(0.93 0.01 85);
--ink-soft: oklch(0.72 0.015 80);
--line: oklch(0.35 0.015 60);
--accent: oklch(0.74 0.12 45);
}
@theme inline {
--color-paper: var(--paper);
--color-paper-raised: var(--paper-raised);
--color-ink: var(--ink);
--color-ink-soft: var(--ink-soft);
--color-line: var(--line);
--color-accent: var(--accent);
--font-serif: "Source Han Serif SC", "Noto Serif SC", "Songti SC", serif;
--font-sans: system-ui, -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
--radius-card: 0.375rem;
}
body { background: var(--paper); color: var(--ink); }
几点说明:
- 颜色用
oklch。调明暗时只改第一个数(亮度),不会像十六进制那样「改亮度顺带改了色相」。 - 名字表达用途,不表达颜色。叫
ink而不是gray-900,因为暗色模式下它变成浅色。 - 字体不引入网页字体。中文字体文件很大,用系统字体栈,标题用衬线体,书的气质就有了。
- 强调色只留一个。多一种强调色,界面就多一份混乱。
主题切换需要一小段脚本在页面最早处运行,否则深色用户会先看到一闪而过的白底。把它放进布局组件的 head 里(is:inline 让 Astro 不要打包它):
<script is:inline>
try {
const saved = localStorage.getItem("theme");
const dark = saved ? saved === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
document.documentElement.dataset.theme = dark ? "dark" : "light";
} catch {}
</script>
读取 localStorage 要包 try/catch:隐私模式或被禁用 Cookie 时它会抛错,页面不能因此崩掉。
用假数据驱动组件
现在还没有数据库,所以先写一份假数据 src/data/sample-books.ts。假数据要刻意包含边界情况,因为真实内容总会撞上它们:
- 一个超长书名(30 个字以上),测试换行;
- 一个英文书名加长作者名;
- 一本没有评分、没有摘要、没有标签的书;
- 一本标签有 6 个的书;
- 一条很长的短评。
界面的毛病几乎都出在这些地方,而不是「标准书」上。组件拆分建议如下:
| 组件 | 职责 | 关键 props |
|---|---|---|
BaseLayout.astro |
页头、页脚、主题脚本、<head> |
title、description |
BookCard.astro |
书架上的一张卡片 | book |
Rating.astro |
1–5 星展示(无评分时不渲染) | value |
TagList.astro |
标签列表 | tags |
ThemeToggle.astro |
明暗切换按钮 | 无 |
用令牌写出来的卡片大概是这样,注意没有一个具体颜色值:
---
interface Props {
book: { id: number; title: string; author: string; rating: number | null;
summary: string | null; tags: string[] };
}
const { book } = Astro.props;
---
<a
href={`/book/${book.id}`}
class="block min-w-0 rounded-card border border-line bg-paper-raised p-5
transition-colors hover:border-accent
focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
>
<h3 class="font-serif text-lg leading-snug text-ink break-words">{book.title}</h3>
<p class="mt-1 text-sm text-ink-soft">{book.author}</p>
{book.summary && <p class="mt-3 line-clamp-3 text-sm text-ink">{book.summary}</p>}
</a>
min-w-0 和 break-words 是给超长书名预备的;focus-visible 样式保证键盘用户看得见焦点;{book.summary && ...} 处理摘要为空的情况。这几处都对应上面的边界假数据。
响应式用移动优先:书架网格写成 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3,先保证 375px 宽的手机上舒服,再向大屏扩展。
避开 AI 味:把审美写成约束
「AI 味」不是玄学,它有可识别的特征:
| 常见特征 | 为什么显得廉价 | 换成什么 |
|---|---|---|
| 紫蓝渐变、渐变文字 | 默认配色,没有立场 | 一种底色加一种强调色 |
| 所有卡片都是大圆角加阴影 | 缺少层次,哪里都一样重 | 细边框,圆角收小,少用阴影 |
| emoji 当图标 | 各平台渲染不一致,显得随意 | 用文字,或统一风格的 SVG 图标 |
| 居中大标题加三张特性卡 | 营销站模板,书架不是营销页 | 直接展示书,信息密度高一点 |
| 到处是「✨」「🚀」式文案 | 一眼 AI | 平实的短句 |
怎么让智能体不犯这些?三招:
- 给气质,不给形容词。别说「简洁优雅」,说「像图书馆的索引卡和旧书扉页:纸色底、衬线标题、一条细线分隔、赭红色只用于链接和选中状态」。
- 给禁止清单。把上表右边的反面写进提示词。禁令比期望更好执行。
- 让它先给方案再动手。在计划模式下让智能体写 3 个视觉方向的文字描述,你选一个,再让它实现。比做完再推翻便宜得多。
让 AI 自己截图检查
官方最佳实践说得很明白:界面类任务,给智能体一个能看见结果的办法,它才能自己迭代,否则你就是那个「人肉截图工具」。有三种常见途径,选你手边有的:
- Claude Code 桌面应用的浏览器面板:智能体启动开发服务器,在面板里截图、检查 DOM、点击、填表。开发服务器的启动方式记在项目的
.claude/launch.json(详见官方 Preview 一节)。 - Claude in Chrome 扩展:用
claude --chrome启动或在会话里运行/chrome,智能体可以打开localhost页面操作、读取控制台报错。它共用你浏览器的登录状态,所以别让它去碰无关的网站。 - 其他浏览器类 MCP 服务器:能力类似,装法见 MCP:连接外部工具。
不管用哪种,检查流程是固定的:
> 启动 dev 服务器,用 375x812 和 1280x800 两种宽度,各截浅色和深色共 4 张图⏺ npm run dev → http://localhost:4321⏺ 截图 4 张。对照检查清单: ✗ 375px:书名「《把时间当作朋友:运用心智获取更多自由与效率》」撑出卡片,出现横向滚动 ✗ 深色:次要文字 ink-soft 对比度偏低 ✓ 键盘 Tab 可见焦点环⏺ 已修复前两项,重新截图确认。 ✓ 4 张图均无横向滚动,正文对比度达标 把检查清单也写出来,比让它「看看有什么问题」可靠:无横向滚动、正文对比度不低于 4.5:1(WCAG AA 的常用基准)、所有可点击元素有焦点样式、空状态有提示而不是白板、超长文本不撑破布局。
读取 SPEC.md 和 CLAUDE.md,完成书单项目的界面,本阶段不接后端,所有数据来自 src/data/sample-books.ts。
设计约束:
- 气质:图书馆索引卡和旧书扉页。纸色底、标题用衬线体、细线分隔;只有一种强调色(赭红),仅用于链接、选中和焦点。
- 禁止:渐变(含渐变文字)、emoji 图标、大面积阴影、居中大标题加三张特性卡的营销页布局、「✨」式文案。
- 在 src/styles/global.css 里按语义命名定义令牌(paper、ink、line、accent 等),明暗两套,用
@theme inline引用;组件里不得写具体色值,也尽量不写dark:。 - 字体不引入网页字体,用系统字体栈。
任务:
- 先建 src/data/sample-books.ts,至少 8 本书,必须包含:超长中文书名、英文书名加长作者名、无评分无摘要无标签的书、6 个标签的书、很长的短评;
- 组件:BaseLayout、BookCard、Rating、TagList、ThemeToggle;页面:
/(书架,按标签筛选可以先用查询参数)、/book/[id](详情)、/admin(表单,先不提交); - 主题脚本放在 head 最前,localStorage 访问必须 try/catch;
- 标签输入框回车添加时要判断 isComposing;
- 完成后启动开发服务器,用浏览器工具在 375x812 与 1280x800 下各截浅色、深色共 4 张图,按这份清单逐项检查:无横向滚动、正文对比度不低于 4.5:1、可点击元素有可见焦点、空状态有提示、超长文本不破坏布局。发现问题就修,修完重新截图,直到全部通过。
最后用表格汇报每项检查的结果,并附上你看过的截图对应的页面和宽度。
为什么推荐先定义语义令牌(如 paper、ink),而不是在组件里直接写具体颜色或 gray-900 这样的名字?
让智能体自查界面时,下面哪种说法最可靠?
动手任务
动手清单
0/6下一课:加后端。界面有了,我们给它接上 D1 数据库和 Workers AI。
参考来源
- Theme variables(Tailwind CSS 官方文档)
- Dark mode(Tailwind CSS 官方文档)
- Styling and CSS(Astro 官方文档)
- Use Claude Code with Chrome(Claude Code 官方文档)
- Claude Code Desktop:Preview your app(Claude Code 官方文档)
- Best practices for Claude Code(Anthropic 官方文档)
工具更新很快,若与官方文档不一致,以官方为准。
学完了吗?
标记完成会记录在本机浏览器,计入你的学习进度和连续天数。