Theme Development / 主题开发指南
本文说明 Zeno 的外观主题、服务器卡片主题和相关设置如何分层,以及新增或修改主题时必须遵守的代码、兼容性、测试和视觉验收规则。
1. 先区分三类“主题”
Zeno 当前有三层互相独立、可以组合的外观能力:
| 层级 | 设置字段 | 当前值 | 作用范围 |
|---|---|---|---|
| 明暗模式 | theme | system、light、dark | 全站颜色模式和 color-scheme |
| 外观预设 | appearance_preset | default、gaussian_blur | 卡片透明度、模糊、圆角、边框、阴影、背景遮罩和主题色 |
| 服务器卡片主题 | server_card_theme | classic、capsule | 首页 ServerCard 内部的信息编排和局部样式 |
不要用服务器卡片主题复制整套明暗配色,也不要用外观预设改变数据语义或卡片 DOM 结构。三层应当正交:任何服务器卡片主题都必须能与浅色、深色、两种外观预设以及有/无背景图组合使用。
当前兼容性基线:
classic是默认值和安全回退值,必须保留现有信息密度、交互和升级前行为。capsule只改变服务器卡片内部组织,继续复用同一批数据、格式化函数、指标组件、状态颜色和页面布局。- 旧数据库没有
server_card_theme、旧 API 响应省略该字段或存量值非法时,Controller 和 Web 都必须回退到classic。 - Admin API 收到未知值时必须返回校验错误,不能静默保存。
2. 设计边界
2.1 必须复用的能力
主题只负责视觉和布局,不得复制或改写以下业务逻辑:
- 节点在线、警告、离线和无数据状态语义;
- CPU、内存、磁盘、流量、网络速率、账单、到期日、延迟和丢包计算;
- 币种换算、单位格式化、计费周期和流量周期;
- 卡片点击、键盘访问、预加载、路由和可访问性标签;
UsageBar、Metric、ServerFlag、图标和既有格式化函数;- 全站颜色、圆角、阴影、模糊和背景相关 CSS Token。
优先通过主题作用域 class、CSS Token 和少量条件布局完成变体。除非两个主题确实有不同的语义结构,否则不要复制整份 ServerCard。
2.2 视觉语言
- 保留 Zeno 已有的轻量、干净、透明背景风格,不把主题做成另一套独立设计系统。
- 同一层级只能使用一种分组语言。服务器卡片上下区域统一使用分割线体系时,不得把上方资源项做成独立有边框小卡片、下方继续使用分割线。这是新主题和后续主题修订的验收标准;已有样式不应成为继续复制混合语言的理由。
- 优先复用
--foreground、--muted、--border、--divider、--divider-strong、--blue、资源状态色和--radius-*;不要在主题选择器内散落固定色值。 - 绿色、橙色、红色是状态语义色,不是装饰色。主题不能改变同一状态的含义。
- 数字使用稳定的单行布局和
tabular-nums;长服务器名、容量、账单和负载值不能挤出卡片。 - hover 只在
@media (hover: hover) and (pointer: fine)下启用;同时尊重prefers-reduced-motion。
2.3 响应式与可访问性
每个主题必须保持:
< 768px单列、768px–1023px两列、>= 1024px三列的首页卡片布局;- 卡片自身在窄容器内无横向滚动;
- 卡片的
role、tabIndex、Enter/Space 行为和focus-visible不退化; - 状态和资源信息不能只依靠颜色表达;
- 装饰层使用
pointer-events: none,不能遮挡卡片点击或后台控件。
3. 代码地图
Controller
| 文件 | 职责 |
|---|---|
internal/controller/api/admin_settings_types.go | SiteSettings、PATCH DTO、默认值、允许值校验 |
internal/controller/api/settings_store.go | server_card_theme 设置键 |
internal/controller/api/settings_binding.go | 从通用 settings 表读取并校验 |
internal/controller/api/settings_update_binding.go | 将 PATCH 字段绑定到持久化更新 |
internal/controller/api/admin_settings_test.go | 默认值、PATCH/Public 回读、非法存量回退和非法输入测试 |
设置保存在通用键值表中。只增加一个新的 server_card_theme 枚举值通常不需要数据库 migration;如果主题需要新的持久化字段,则属于设置/API 变更,必须同步 DTO、默认值、读写绑定、校验、测试和 API 文档。
Web
| 文件 | 职责 |
|---|---|
web/src/types.ts | ServerCardTheme、AppearancePreset 和 AdminSettings 类型 |
web/src/api/apiTypes.ts | Public/Admin API snake_case DTO |
web/src/api/publicNormalizers.ts | API 响应归一化和 classic 安全回退 |
web/src/api/adminNormalizers.ts | Admin PATCH camelCase → snake_case |
web/src/lib/appearance.ts | 外观预设、默认设置、CSS Token 和明暗模式应用 |
web/src/components/admin/AdminSettingsSection.tsx | 后台主题选择器和保存草稿 |
web/src/App.tsx | 把生效设置传给首页卡片并应用全站外观 |
web/src/components/ServerCard.tsx | 卡片结构、主题作用域和共享指标 |
web/src/styles.css | 全局 Token、基础卡片样式和主题作用域规则 |
web/src/components/ServerCard.test.tsx | 主题 DOM、信息完整性、状态和格式测试 |
web/src/components/admin/AdminDashboard.test.tsx | 选择器、输入校验和保存流程 |
web/src/api/publicClient.test.ts | Public 设置归一化和非法值回退 |
web/src/api/adminAuthSettingsClient.test.ts | Admin 设置读取、PATCH 映射和回读 |
4. 新增服务器卡片主题
以下示例把新主题 ID 写作 compact;实际名称应使用稳定、全小写的 ASCII snake_case 或单词,不要使用展示文案作为持久化值。
第一步:扩展后端契约
- 在
validServerCardTheme中允许新值。 - 保持默认值为
classic,不要因为新增主题改变旧实例的展示。 - 为 Admin PATCH 成功、非法值拒绝、Public 回读和非法存量回退补测试。
- 确认 Public/Admin Settings 都返回
server_card_theme,但不暴露任何凭据。
第二步:扩展前端类型和回退
- 在
ServerCardTheme联合类型中加入新值。 - 更新 Public normalizer 的允许值集合;未知值仍回退到
classic。 - 更新 Admin PATCH DTO 和测试 fixture。
- 在后台选择器中加入中文展示名,持久化值保持稳定。
- 保存仍必须带
expected_revision;主题开发不能绕过设置的乐观并发控制。
建议把允许值集中成一个只读列表并复用于类型守卫、normalizer 和选择器,避免每增加一个主题就漏改某一处分支。
第三步:实现卡片变体
当前第二种主题通过 is-capsule class 和局部条件渲染实现。加入第三种及更多主题前,应先把单个 capsule 布尔分支整理为可扩展的主题作用域,例如:
<article className="kulin-node-card" data-card-theme={serverCardTheme}>
...
</article>CSS 仅在主题作用域内覆盖需要改变的部分:
.kulin-node-card[data-card-theme='compact'] .node-usage-grid {
/* 只写 compact 与基础卡片不同的规则 */
}实现时遵守:
- 基础
.kulin-node-card仍是所有主题的共同来源; - DOM 差异尽量限制在卡片内部,页面网格、头部、底部健康区和路由不复制;
- 不用
nth-child猜测业务含义,优先使用稳定的语义 class; - 每项资源和指标保持原有顺序、标签、单位和数据来源;
- 如果隐藏一个旧区域,必须证明同一信息已在新区域完整出现,而不是直接丢失;
- CSS 选择器必须同时在浅色、深色和高斯模糊预设下成立。
第四步:补齐测试
至少覆盖:
classic的 DOM 和文案保持不变;- 新主题获得唯一、可断言的作用域;
- 四个资源项以及底部网络、账单、到期、延迟和丢包信息仍存在且顺序稳定;
- online、warning、offline、no_data 的 class 和文字语义正确;
- 缺失/非法主题在 Controller 与 Web 两层都回退到
classic; - Admin 选择、PATCH payload、成功回读和 revision conflict 正确;
- 长名称、长容量、长账单周期和空数据不产生溢出或伪造值。
不要只断言压缩后的 CSS 字符串。优先断言语义 DOM、class/data attribute、计算样式和真实渲染结果。
第五步:同步文档
新增主题时至少更新:
- 本文的当前值和兼容性基线;
docs/HOME_CARD_SPEC.md的主题说明和结构规格;docs/API.md的 Settings 示例、允许值和回退规则;docs/TECHNICAL_DESIGN.md的公开设置字段;- 如为用户可见能力,中文/英文 README 或 Release notes。
5. 新增外观预设
外观预设只是一组参数默认值,不应复制服务器卡片结构。新增预设时:
- 扩展
AppearancePreset和 Controller 的validAppearancePreset。 - 在
appearancePresets中定义完整参数组,不继承未说明的偶然值。 - 在
appearancePresetOptions和 Admin 设置中提供名称。 - 继续通过
shellStyleForSettings生成 CSS Token,不在组件中拼接大段内联样式。 - 更新数值边界、默认值、API fixture、前后端校验和测试。
- 验证预设能与所有
server_card_theme、明暗模式和背景图组合。
6. 本地验证
主题代码修改后,从仓库根目录执行:
go test -race ./internal/controller/api
go vet ./...
npm --prefix web ci
npm --prefix web test -- --run
npm --prefix web run build
npm --prefix web run check:performance
bash scripts/check-sensitive-files.sh
git diff --check迭代时可以先运行主题相关测试:
npm --prefix web test -- --run \
src/components/ServerCard.test.tsx \
src/components/admin/AdminDashboard.test.tsx \
src/api/publicClient.test.ts \
src/api/adminAuthSettingsClient.test.ts
go test ./internal/controller/api -run 'Settings|ServerCardTheme'最终仍需执行完整前端测试和生产构建。源码测试通过不代表生产静态资源正确;需要检查构建后的 HTML/CSS/JS 确实包含新主题作用域和选择器。
7. 真实视觉验收
使用真实浏览器检查构建产物或候选实例,最低矩阵如下:
| 维度 | 必测值 |
|---|---|
| 卡片主题 | classic、每个新增主题 |
| 明暗模式 | light、dark |
| 外观预设 | default、gaussian_blur |
| 背景 | 无背景、有背景 |
| 视口 | 1440×900、1024×768、390×844 |
| 节点状态 | online、warning、offline、no_data |
| 数据 | 普通值、长名称/长账单/大容量、空值 |
每个组合至少确认:
- 卡片上下分区的分割线和视觉语言一致,没有混用子卡边框;
- 文字、图标、进度条、状态、间距和对齐清楚;
document.documentElement.scrollWidth <= document.documentElement.clientWidth;- 卡片可点击、可用键盘聚焦和打开;
- 后台切换后 PATCH 成功,重新读取 Public Settings 仍是所选主题;
- 刷新页面后主题保持,旧/非法值安全回退;
- 控制台没有错误,页面没有被装饰层遮挡。
截图必须至少包含桌面和手机、浅色和深色。只看源码、单张桌面截图或 CSS fixture 都不能算完成。
8. 发布与回滚
主题设置和代码发布是两件事:
- 设置层回滚:在后台切回
classic,保存后回读 Public Settings 并刷新首页确认。 - 代码层回滚:按正常 Zeno 发布流程保留旧镜像 digest、数据库和部署配置,不能把可变 tag 当作回滚点。
- 候选版本失败时,先恢复固定旧镜像和配置,再确认
/ready、Public Settings、静态资源和首页渲染。 - 部署授权不等于 commit、push、PR、tag、Release 或镜像发布授权。
9. 完成定义
一个主题只有同时满足以下条件才算完成:
- 前后端允许值、默认值、持久化、回退和 revision 行为一致;
- 经典主题没有回归,新主题不复制业务逻辑或丢字段;
- 主题 CSS 使用现有 Token,并保持统一的分割线与分组语言;
- 相关单测、完整前端测试、Go 测试、生产构建和敏感文件检查通过;
- 桌面/手机、浅色/深色、主要状态和长内容完成真实浏览器验收;
- API、卡片规格和本开发指南同步更新;
- 如已部署,运行镜像、readiness、Public Settings 和线上静态资源均已回读确认。