跳到正文
码上手 coding.chengyu.dev

做界面:令牌、组件与截图自查

先定设计令牌再写组件,用假数据做出响应式、支持暗色模式的书单界面,让 Claude Code 自己截图检查,并避开千篇一律的 AI 味审美。

约 35 分钟 进阶 Claude CodeAstroTailwind CSS 核实于 2026-10-25

学完这一课,你能

  • 能用 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 平实的短句

怎么让智能体不犯这些?三招:

  1. 给气质,不给形容词。别说「简洁优雅」,说「像图书馆的索引卡和旧书扉页:纸色底、衬线标题、一条细线分隔、赭红色只用于链接和选中状态」。
  2. 给禁止清单。把上表右边的反面写进提示词。禁令比期望更好执行。
  3. 让它先给方案再动手。在计划模式下让智能体写 3 个视觉方向的文字描述,你选一个,再让它实现。比做完再推翻便宜得多。

让 AI 自己截图检查

官方最佳实践说得很明白:界面类任务,给智能体一个能看见结果的办法,它才能自己迭代,否则你就是那个「人肉截图工具」。有三种常见途径,选你手边有的:

  • Claude Code 桌面应用的浏览器面板:智能体启动开发服务器,在面板里截图、检查 DOM、点击、填表。开发服务器的启动方式记在项目的 .claude/launch.json(详见官方 Preview 一节)。
  • Claude in Chrome 扩展:用 claude --chrome 启动或在会话里运行 /chrome,智能体可以打开 localhost 页面操作、读取控制台报错。它共用你浏览器的登录状态,所以别让它去碰无关的网站。
  • 其他浏览器类 MCP 服务器:能力类似,装法见 MCP:连接外部工具。

不管用哪种,检查流程是固定的:

claude — ~/projects/my-shelf
> 启动 dev 服务器,用 375x812 和 1280x800 两种宽度,各截浅色和深色共 4 张图
⏺ npm run dev → http://localhost:4321
⏺ 截图 4 张。对照检查清单:
✗ 375px:书名「《把时间当作朋友:运用心智获取更多自由与效率》」撑出卡片,出现横向滚动
✗ 深色:次要文字 ink-soft 对比度偏低
✓ 键盘 Tab 可见焦点环
⏺ 已修复前两项,重新截图确认。
✓ 4 张图均无横向滚动,正文对比度达标

把检查清单也写出来,比让它「看看有什么问题」可靠:无横向滚动、正文对比度不低于 4.5:1(WCAG AA 的常用基准)、所有可点击元素有焦点样式、空状态有提示而不是白板、超长文本不撑破布局。

用令牌与假数据做出书单界面,并自行截图验收 Claude Code

读取 SPEC.md 和 CLAUDE.md,完成书单项目的界面,本阶段不接后端,所有数据来自 src/data/sample-books.ts。

设计约束:

  • 气质:图书馆索引卡和旧书扉页。纸色底、标题用衬线体、细线分隔;只有一种强调色(赭红),仅用于链接、选中和焦点。
  • 禁止:渐变(含渐变文字)、emoji 图标、大面积阴影、居中大标题加三张特性卡的营销页布局、「✨」式文案。
  • 在 src/styles/global.css 里按语义命名定义令牌(paper、ink、line、accent 等),明暗两套,用 @theme inline 引用;组件里不得写具体色值,也尽量不写 dark:。
  • 字体不引入网页字体,用系统字体栈。

任务:

  1. 先建 src/data/sample-books.ts,至少 8 本书,必须包含:超长中文书名、英文书名加长作者名、无评分无摘要无标签的书、6 个标签的书、很长的短评;
  2. 组件:BaseLayout、BookCard、Rating、TagList、ThemeToggle;页面:/(书架,按标签筛选可以先用查询参数)、/book/[id](详情)、/admin(表单,先不提交);
  3. 主题脚本放在 head 最前,localStorage 访问必须 try/catch;
  4. 标签输入框回车添加时要判断 isComposing;
  5. 完成后启动开发服务器,用浏览器工具在 375x812 与 1280x800 下各截浅色、深色共 4 张图,按这份清单逐项检查:无横向滚动、正文对比度不低于 4.5:1、可点击元素有可见焦点、空状态有提示、超长文本不破坏布局。发现问题就修,修完重新截图,直到全部通过。

最后用表格汇报每项检查的结果,并附上你看过的截图对应的页面和宽度。

小测

为什么推荐先定义语义令牌(如 paper、ink),而不是在组件里直接写具体颜色或 gray-900 这样的名字?

小测

让智能体自查界面时,下面哪种说法最可靠?

动手任务

动手清单

0/6

下一课:加后端。界面有了,我们给它接上 D1 数据库和 Workers AI。

参考来源

工具更新很快,若与官方文档不一致,以官方为准。

学完了吗?

标记完成会记录在本机浏览器,计入你的学习进度和连续天数。

Esc

输入关键词开始搜索,例如「CLAUDE.md」「MCP」「部署」