前端样式规范
本站的全站设计标准,以站内博客目录页(/blog)为主要视觉与交互基准。
新页面与既有页面改版均按本文设计;尚未对齐的页面不因此成为新的标准。
本文规定设计目标、组件边界与验收要求,源码提供实现,维护计划登记待办。
1. 定位与使用方式
1.1 适用范围
标准覆盖界面中的布局、颜色、排版、形状、控件、交互状态、动效与响应式行为。 博客目录页提供可复用的设计关系:页面底色上摆放卡片,以胶囊组织操作, 用连续伸展的外壳连接收起态与展开态。其他页面按自己的内容与任务组织这些元素。
阅读页、工作台、编辑器与用户选择的装饰款式有各自的内容约束,范围见 §7。 这些差异不改变公共导航、按钮、字段、主题与可访问性的基本要求。
1.2 如何查阅
- 确定整体方向先读 §2;选择视觉尺度查 §3。
- 组织页面查 §4;选择组件与交互语义查 §5。
- 设计状态变化与过渡查 §6;组合典型页面查 §7。
- 实现、复用和验收查 §8;源码入口见附录 A。
同一规则只在一个位置定义。组件章节说明如何应用基础规则,场景章节只规定 该场景需要的组合与尺寸。差异必须写明对象和用途,不能靠章节先后判断哪条生效。
1.3 规范的维护
设计改变时直接修改对应规则,并同步受影响的组件、引用与验收依据。
仅改文档不代表已有页面已经完成适配;实现差异与未完成工作登记在
docs/plan/maintenance-plan.md,不得把旧实现写成通用规则来消解差异。
修复过程与已完成记录查 Git 历史。本文不追加按日期排列的补充说明、故障经过、 已修问题清单或逐页样式台账。数值表只保留做设计决策所需的内容; 完整令牌定义、组件落点与业务权限分别由源码、守卫和架构文档维护。
需要一份能整份转发、不依赖本站源码的对外版本时用 docs/design-philosophy.md:
它讲原则与判据,数值与本文一致 —— 改数值要两处一起改。
2. 整体设计原则
- 胶囊与大圆角。 单行操作、字段、导航和统计标记采用胶囊; 内容卡片采用固定大圆角。形状由内容和用途决定。
- 底色建立层级。 页面、卡片、卡内子区通过主题底色与留白分开。 普通卡片不描边、常态不投影;选中态使用淡底与同色文字,不添加装饰竖条。
- 内容有主次。 标题、正文、辅助信息、数据分别承担不同角色。 用字号、字重、间距和颜色共同建立层级,弱化信息仍须可读。
- 反馈对应操作。 可以点击、当前选中、键盘聚焦、已经展开是不同状态。 非交互内容不借用按钮或可点击卡片的反馈。
- 动效长在组件上。 所有交互状态都要有可见过渡;外壳伸展说明内容来自哪里, 前后两态的部件一一对应,滑块说明选中了哪项,内容入场说明展开的先后关系。 动效不是盖在组件上的一层效果。按钮和卡片的悬停反馈不做垂直位移。
- 文案服务使用。 保留功能名称、必要说明、操作提示与状态反馈。 不主动增加抒情、装饰性或凑气氛的副标题;图标按钮仍须提供可访问名称。
3. 基础视觉标准
3.1 颜色与主题
主题令牌定义于 src/styles-scss/base/_root.scss,由
<html data-theme="light|dark"> 切换。界面使用令牌,不在页面里另建明暗色板。
下面列出核心颜色作为设计参照,完整定义以该文件为准。
| 角色 | 令牌 | 浅色 | 深色 |
|---|---|---|---|
| 品牌主色 | --color-brand-primary |
#2563EB |
#23A5FF |
| 品牌淡底 | --color-brand-secondary |
品牌蓝 10% | 品牌蓝 10% |
| 页面底色 | --color-background-page |
#F8FAFC |
#131517 |
| 卡片底色 | --color-background-card |
#fff |
#181A1D |
| 卡内内容底色 | --color-background-content |
#f4f6f9 |
#21252A |
| 弱背景 | --color-background-subtle |
#f3f4f6 |
#1e2024 |
| 主要文字 | --color-text-primary |
#0f172a |
#f1f5f9 |
| 辅助文字 | --color-text-secondary |
#64748B |
#94A3B8 |
字段一般放在卡片内,使用页面底色;正文中的嵌入块使用内容底色。 同色区域相贴时先调整底色层级或留白,不能直接加描边补救。 未读等特定状态已有专用背景令牌,使用时同时保留文字或图标提示。
功能色与分类强调色分开使用:
| 用途 | 令牌 | 边界 |
|---|---|---|
| 危险、删除;既有点赞状态 | --color-warning-primary / secondary |
这组是红色,名字不表示黄色 |
| 成功;既有评论统计;行情上涨 | --color-success-primary / secondary |
绿色不代替所有“已选中”状态 |
| 投喂交互与鱼干统计 | --color-brand-primary / secondary |
沿用本站品牌蓝 |
| 收藏状态 | --color-star-primary / secondary |
仅用于收藏图标的交互与已收藏状态 |
| 分类与并列内容的区分 | --color-accent-blue / amber / cyan / violet 及各自 -soft |
不承担成功、失败或危险的含义 |
收藏未激活时图标跟随灰色 currentColor,不能预先点亮。
分类色可同时驱动某张功能卡的图标、按钮与悬停光晕,三者保持同一色相;
普通可点击卡片默认使用品牌蓝光晕。
令牌使用边界:
--color-text-tertiary实际是浅淡背景与分隔色,不用于弱化文字。- 分隔线可使用
--color-border;它不授权给普通卡片增加轮廓。 - 现有
--fd-*是兼容别名;新样式优先使用语义明确的--color-*, 不能据此建立另一套设计体系。 - 白字配实底按钮、开关的白色圆钮可以使用固定白色。原创插画、画报、 identicon 和图表的数据着色按其内容定义;界面外壳仍使用主题令牌。
- 使用变量前确认其定义或组件提供方存在。不能假定存在
--color-danger-*、--color-brand-primary-rgb或自行拼出变量名。
3.2 字体与排版
| 角色 | 字体令牌 | 使用方式 |
|---|---|---|
| 界面与普通文字 | --font-body |
Kumbh Sans,中文走系统回退字体 |
| 品牌英文 | --font-brand |
Outfit,仅用于品牌表达 |
| 代码与代码编辑 | --font-mono |
JetBrains Mono |
| 界面数据 | --font-numeric |
Raricy Numeric,数字与数值符号等宽 |
| 书页阅读模式 | --font-reader-serif |
使用该阅读模式的衬线回退栈 |
字体栈集中定义于 src/styles-scss/base/_root.scss,控件继承所在界面的字体。
随站点提供的字体及授权见 public/static/fonts/README.md。
数学内容保留数学排版所需的字体。
顶栏品牌按三级排列:图标 → 英文标准字 → 中文标准字。图标到英文的距离固定为 6px,
必须小于英文到中文的距离(0.5em,约 12px);两段等距会把「图标 + 英文名 + 中文名」
读成三段并列文字,品牌层级随之消失。
界面正文通常为 0.9–1rem,辅助信息为 0.75–0.875rem;
列表标题以博客卡片的桌面 1.3rem / 600、窄屏 1.05rem 为参照。
页级标题使用 600 / 700 字重,按页面体量与视口调整,不能让大标题挤压主要操作。
这些是排版尺度,不代表存在同名 CSS 令牌。
阅读正文与界面文字分开:博客正文采用 1.15rem 字号、2 行高,
段落两端对齐;代码块维持等宽排版并在自身区域内滚动。
链接默认不加下划线,依靠颜色与上下文识别;说明文中的链接可保留下划线,
悬停时不能靠改变字重挤动相邻文字。
点赞数、金额、页码、日期、进度等数据使用 .numeric;
数值与自由文字混排时只包裹数据部分。用户名、标题、文件名与正文里的数字
属于文字内容,不给整块内容套数字字体,也不扫描正文后改写其中的数字。
3.3 圆角与形状
| 对象 | 标准 | 说明 |
|---|---|---|
| 单行按钮、字段、导航、徽章 | 999px,即 $radius-max |
呈胶囊形 |
| 普通卡片、主要内容面板、弹窗 | 30px,即 $radius-card |
固定圆角,不随内容高度增长 |
| 窄屏紧凑列表卡片 | 18px |
以博客目录 ≤768px 的卡片为基准 |
| 多行字段与输入面板 | 20px,即 $radius-field |
不因输入行数变化而变成椭圆 |
| 次级内容块、小标记 | 10px / 5px,即 $radius-large / small |
按内容密度选用 |
| 圆形图标按钮与角标 | 50% |
正方形盒子形成圆形 |
窄屏的 18px 用于收紧后的列表条目,不要求阅读卡、大面板和弹窗一律缩成同一尺寸。 输入框的判据是内部是否仅有一行文字:单行输入与选择字段为胶囊, textarea、正文编辑区与多行组合面板为固定圆角。
有淡色底座的小图标容器一律圆形。 小方块 + 淡色底 + 图标的容器取 50%,
不随底座尺寸或所在页面变化。头像仍按 §3.4 的 8% 方角;卡片、按钮、输入框不适用这条,
它们的圆角由上面的表与用途判据决定。非正方形的底座先把盒子改成正方形
(50% 会把长宽不等的盒子拉成椭圆)。
展开外壳保留收起态的曲率。 高 42px 的栏目触发器收起时为胶囊, 同一外壳展开后保持 21px 固定圆角;不能继续用 999px 把长面板变成椭圆。 嵌套胶囊的内外圆角与间距相配,例如高 48px 的外壳内放高 40px 的按钮, 四周留 4px,对应外圆角 24px、内圆角 20px。
3.4 头像
头像统一为 8% 圆角的正方形,使用百分比随尺寸缩放,不改成圆形头像。
这同时是图标底座圆形规则(§3.3)的例外:带淡色底座的小图标一律 50%,
头像始终是 8% 方角。
内联样式同样使用 borderRadius: '8%'。位图按头像盒子裁剪,
头像框贴图铺满同一盒子,不占布局、不接收点击。
尺寸由所在组件决定:目录作者 20px、阅读页作者 32px、 目录个人面板展开头像 48px、个人主页身份头像 120px。 这些是典型用途,不作为所有页面的固定尺寸清单。
界面头像使用 src/app/components/Avatar.tsx;字符串渲染管线按其 DOM 结构输出。
完整渲染落点与结构约束由 tests/unit/avatar-sites-guard.test.ts 维护。
头像框素材的几何与交付要求见 docs/guide/头像框出图规范.md。
3.5 间距与尺寸
优先沿用 4px 基础尺度:4、8、12、16、20、24、32、40、48px,
分别用于贴边间隙、行内间距、控件内距、内容分组与大区间留白。
现有 SCSS 间距变量在 src/styles-scss/abstracts/_variables.scss;
没有对应变量的尺寸不能自行假定已有令牌。
同一行控件保持等高,文字与图标垂直居中;关系相近的内容间距小于区块之间的间距。 卡片内距要让文字避开圆角,窄屏随内容密度收紧。 博客卡片的非对称内距属于该组件的光学调整,不逐项推广为全站间距变量。
按钮尺寸按用途决定,三档按钮统一表面与状态,不强制复制“创建”按钮的字号和宽度。 主要触屏图标入口采用至少 44px 的点击区域; 紧凑工具栏可缩小可见图标,但要保留足够且不重叠的点击区域。
3.6 阴影、轮廓与遮罩
| 用途 | 令牌或配方 | 显示时机 |
|---|---|---|
| 可点击卡片的反馈 | --shadow-card-brand |
整卡可点时悬停显示 |
| 浮层与固定顶栏 | --shadow-card-brand |
保持可见时显示 |
| 字段与键盘聚焦 | --shadow-focus-brand |
聚焦时显示 |
| 开启态的控件光晕 | --shadow-glow-brand |
开关 / 勾选项处于开启时显示 |
| 普通模态遮罩 | rgba(0,0,0,.5) |
模态面板展开时显示 |
普通卡片常态无阴影,卡内只有局部按钮可点时,不能让整卡悬停发光。
不用黑色 box-shadow;内容分类明确的功能卡可用 §3.1 的同色光晕。
三种光晕的语义各有归属:抬起是“卡片可点 / 浮层”,聚焦只属于字段与键盘焦点,
开启态只属于“这一项现在是开着的”。它们不能互换,也不能拿开启态光晕做装饰性提亮
或替代焦点标识。
右上角 Toast 不投影:它靠状态色淡底与背景模糊浮起来(见 §5.7)。
容器不使用 border、outline 或 inset 阴影模拟装饰轮廓,不加左右竖条表示选中。
结构性边框同样不画:列表与工作台的分栏线、区段横线、输入区顶线、弹窗分页顶线
一律删掉 —— 层级由页面、卡片、内容底色与留白承担,不靠线把区域「框」出来。
通知列表的按日分组也不用贯通横线,分组层级由标题字号、字重与卡片间距建立。
这个限制不取消键盘焦点标识,也不禁止有内容语义的图形:
正文引用块、图表轴线、日期分隔、新消息线、菜单分组线、滚动条结构,
以及 §7.5 的专用内容边界按其用途设计。
聊天气泡(含 data-bubble 用户款式)的边框、底色与圆角属于气泡自己的外观
(§7.4、§7.5),不随这条改变。
遮罩属于背景,不是投影。遮罩的层级由模态范围决定: 博客目录的移动个人面板只遮页面内容,顶栏保持在其上方; 全局弹窗覆盖自己的完整交互范围。不能把某页的 z-index 数字直接当成全站层级。
3.7 图标
优先复用 public/static/img/icons/ 的 SVG mask;已有 React 界面可沿用
已安装的 Lucide 图标与现成内联 SVG,不为单个图标增加另一套图标库。
同一组操作保持相近的视觉重量、尺寸和基线。
图标颜色跟随 currentColor,状态由承载控件提供。箭头随展开方向旋转;
计数图标与数字同行。纯装饰图标退出读屏,单独操作图标的名称由按钮提供。
业务图形(小鱼干、品牌标识等)复用站内素材,不用含义近似的图标替代。
4. 布局与响应式
4.1 容器、页边距与阅读行宽
普通页面的 .container、顶栏 .site-container 与页脚内容共用宽度阶梯:
| 视口 | 容器上限 | 左右内距 |
|---|---|---|
| <992px | 可用宽度的 100% | 16px |
| ≥992px | 960px | 16px |
| ≥1200px | 1140px | 16px |
实现复用 container-padding 与 site-width-ladder。
通栏色带占满外部宽度,其内容另放容器。
页边距由一层外壳负责:组件嵌入页面时不重复提供容器、标题与整页内距。
阅读行宽在容器内部另设上限。现有博客详情外层为 940px,
正文列为 900px;故事普通阅读与互动阅读分别为 820px、720px。
这些上限用于控制阅读密度,不随普通容器扩大。
后台是专用宽工作区:外壳上限 1440px、左右 20px,由 AdminShell 统一承担。
4.2 页面组织
- 页面主标题只出现一次,标题、工具栏与主体内容有明确的上下关系。
- 侧栏加主列使用
min-width: 0或minmax(0, 1fr)允许内容列收缩; 侧栏按自身高度吸顶,不被长列表拉伸。 - 网格与并列卡片按可用空间折列,保持内容顺序和主要操作可达。
- 筛选无结果时保留筛选控件,分页与页脚之间保留足够空间。
- 宽内容只在自己的区域滚动,例如代码、表格、图表与密集操作条; 不能把整个页面撑出横向滚动条。
4.3 固定导航、安全区与层级
顶栏内容基高为 62px,总高度由 --navbar-height 表达,
包含 --safe-top。页面补偿、工作台高度与吸顶位置读取这个变量,
不能各自再抄一份顶栏高度。
普通吸顶工具栏在顶栏下额外留 12px,博客目录的筛选条与个人侧栏使用同一基准。 吸顶背景或渐变使用当前主题底色,不遮挡下拉,也不制造横向滚动。 页面内容、局部吸顶、局部浮层、固定导航、全局模态保持清晰的层级关系。
安全区统一读取 --safe-top / bottom / left / right;
父层已经让出的安全区,子层不能重复叠加。
占满可见高度的页面使用 dvh,可先写 vh 兜底;
软键盘引起的可视区域变化由工作台自己的视口适配处理。
4.4 响应式决策
通用断点为 576、768、992、1200px,定义于
src/styles-scss/abstracts/_breakpoints.scss。
断点用于安排布局变化,不等于所有组件都必须在同一点折叠:
顶栏的 808px、后台与讨论侧栏的 900px 属于按内容容量确定的组件断点。
空间不足时依次考虑收紧留白、换行、折列、收进可展开入口。 隐藏文字后必须保留名称与操作能力;长名字在没有悬停提示的触屏上仍要有读全的路径。 局部工作区的折行按实际容器宽度决定,适合时使用容器查询, 不要用视口宽度猜测一张卡片还剩多少空间。
5. 通用组件标准
5.1 按钮与图标按钮
按钮只有以下三档公共表面,定义在 src/styles-scss/abstracts/_mixins.scss:
| 档位 | 用途 | 空闲 | 悬停 | 按下 |
|---|---|---|---|---|
btn-primary |
当前主要行动 | 品牌淡底、品牌字 | 品牌实底、白字 | 整体压暗 |
btn-secondary |
辅助操作 | 无底色、辅助文字色 | 品牌淡底、品牌字 | 品牌实底、白字 |
btn-tab |
导航、筛选、当前项选择 | 未选中同次级;选中为品牌淡底、品牌字 | 当前项保持选中表面 | 使用淡底,不闪成实底 |
危险操作沿用对应档位的层次,颜色换成危险色,不另造第四档按钮。 状态按钮的激活态见 §6.1。任何档位都不做垂直位移,也不以新增描边表示按下。
优先复用公共按钮类或 mixin;尺寸、字号和图标间距由所在组件补齐。
当前项使用 .is-active 并提供对应 ARIA 状态;兼容旧 .active 不作为新写法。
禁用按钮降低视觉强度并实际禁止操作,加载时保持外壳尺寸和名称稳定。
图标按钮复用次级档的反馈。主要入口采用圆形或胶囊点击区;
密集桌面列表可使用可见盒子 30px、圆角 8px 的紧凑按钮,
触屏仍按 §3.5 保留点击区域。仅图标的控件必须有 aria-label,
title 只作额外提示。
5.2 输入框、搜索框与表单控件
单行输入与选择字段使用胶囊,多行字段使用 20px 固定圆角。
默认表面为页面底色、主要文字色、无边框,复用 field-surface 或公共表单类。
聚焦使用 --shadow-focus-brand,不换底色、不临时增加边框宽度。
搜索框复用 .search-input;内嵌按钮时,按钮距上、右、下各 4px,
弧线与外壳相配,输入文字为按钮保留空间。
只有内嵌按钮的搜索字段需要右侧留白,独立按钮和纯搜索输入不套用这段几何。
字段有持久标签,placeholder 只提供示例;错误与帮助信息靠近字段并关联到控件。 错误使用文字与危险色表达,错误、聚焦和校验状态不能改变字段几何。 触屏单行字段字号至少 1rem;紧凑桌面字号不直接套到触屏输入。
选择字段沿用字段表面,不伪装成主要行动按钮。
原生 select 的选项和分组同时使用主题底色与字色,并配合 color-scheme。
复选框、单选框与开关保留各自语义、标签和键盘操作;
不能因为都是“二选一”就换成胶囊滑块。
5.3 卡片、内容块与统计胶囊
卡片遵循 §3 的底色、圆角和阴影规则。 整卡可点击时使用真实链接或按钮;拉伸链接不能盖住作者链接等局部操作。 卡片中只有一颗按钮可点时,卡片本身不提供点击光晕或手形指针。
卡内的嵌入内容使用内容底色与适当内距,避免同色卡片层层套壳。 渲染器负责正文时,页面必须明确外壳由哪一层承担; 同一内容不能叠加两份卡片底色、内距或页边距。
标签、身份标记与统计胶囊是信息组件。图标和数据靠近排列, 需要时用功能色或分类色区分;没有操作就不显示按钮反馈。 选中、佩戴中或“我”使用淡底与文字信息表示,不用装饰竖条、描边环或 inset 伪边框。 正文引用块的竖线属于内容语义,不套用装饰竖条的限制。
5.4 导航、筛选与分页
同一层导航保持一致的形状、间距与当前项表达,默认使用 btn-tab。
互斥项选择可以使用 aria-pressed;指向当前页面的链接使用 aria-current。
视觉共用不代表 ARIA 角色必须相同。
多选项筛选、栏目层级、可增加的分类列表使用普通导航项、下拉或复选控件。 分页当前页使用品牌淡底与品牌字,前后页使用站内箭头图标; 跳页字段为单行胶囊。切换范围时搜索、筛选和页码的保留规则由业务定义, 必须与当前界面显示一致。
分页标记复用 src/app/components/SitePager.tsx(博客 / 通知 / 对外目录 /
小鱼干流水 / 图床与音频床管理都是它),页码窗口与折叠的算术在 src/lib/page-window.ts。
只渲染窗口内的页码、首尾常驻、缺口折成 …,不把全部页码平铺成一行;
「没有第二份手写分页标记」由 tests/unit/site-pager-guard.test.ts 盯着。
管理端 /admin/* 的 AdminPager 是另一套(不带跳页框),两套并存是刻意的。
5.5 胶囊滑块
.segmented 仅用于同一份内容的两种互斥呈现或排序方式,
例如发布时间 / 更新时间、K 线 / 折线。
执行动作、设置开关、买卖方向、金额或杠杆参数选择不属于这种视图切换;
三种及以上、选项数量可能增加的切换采用普通标签或选择控件。
轨道有固定内距,两个按钮等宽,滑块与选中项对齐; 按钮文字长度不同也不改变分段宽度。 选中状态由滑块与文字共同表达,未选中项悬停不变成选中色, 避免与真正选中项同时发亮。
实现复用 src/styles-scss/components/_segmented.scss,
由容器的 --seg-n 与 --seg-i 驱动位置。
这两个组件变量是实现接口,不授权把该控件扩展成任意数量的通用分段选择。
5.6 下拉与展开面板
收起触发器和展开面板优先使用同一个连续外壳; 外壳沿实际内容高度伸展,内部选项随后淡入。 展开态新增的部件必须能对应到收起态的同一颗控件,或由外壳内部让出; 不能旧控件淡出、新控件淡入,也不能让面板从与触发器无关的位置淡入。 圆角按 §3.3 保持曲率,宽度变化不能超出视口或遮住必要的相邻操作。
普通选择使用现有 ExpandSelect;博客栏目包含父子层级,使用
BlogCategorySelect。两者共用 expand-panel 的展开节奏。
父栏目名称负责选择,箭头负责展开子项,两种操作保持独立的点击区域。
外部点击、焦点离开和 ESC 可收起;关闭后内容立即退出交互和焦点序列, 内容退场先于外壳收缩,不能先收壳把内容切断。 隐藏内容不能仅设透明度。首次内容测量不播放宽度伸缩,避免进入页面就发生一次假展开。 菜单或字段保留符合其实际语义的键盘操作,不能只实现鼠标点选。
5.7 弹窗、浮层与提示
普通弹窗采用主题卡片底色、30px 圆角与品牌浮层阴影,
默认内容宽度以 500px 为参照,窄屏限制在视口内并保留边距。
内容过长时在合适的内部区域滚动,标题和关闭操作保持可达。
内容分两段到达(先占位、后正文)的弹窗,窄屏把上沿钉死:高度变化只向下长,
盒子不跟着上下跳半个高度差;其余弹窗仍是 margin: auto 居中。
模态使用 §3.6 的遮罩,阻止背景交互和穿透滚动; 焦点进入弹窗并在关闭后回到触发入口。非模态下拉不额外遮暗整页。 浮层受父级裁剪或层叠上下文限制时,由浮层组件负责定位与挂载, 不能通过抬高内部 z-index 猜测解决。
Toast 用于短暂结果反馈,不承载唯一的错误详情或必须完成的操作。
右上角的 Toast 是长胶囊形毛玻璃:背景只铺该条提示状态色的 secondary 淡底(10% 透明),
用 backdrop-filter 模糊背后的页面,不投影。
淡底、文字与关闭叉都取同一条提示的状态色(成功绿 / 错误红 / 提示蓝 / 警告橙),
文字直接用状态色、不压深;背景是透明的,所以它在暗色主题下落在深色页面上。
告警区通过状态色、图标和文字共同表达含义,
使用固定圆角与淡底,不加装饰性左侧竖条。
5.8 加载、空状态与错误
加载尽量维持布局与按钮宽度;刷新同一份列表时保留筛选和当前操作上下文。 空状态说明当前范围与下一步,例如调整筛选或创建内容。 错误说明失败的操作与可行的恢复方式,关键错误不只靠 Toast 短暂出现。
这些状态与正常内容使用同一套容器和间距,不额外套入整页卡片。 服务端与客户端的首屏状态要一致,不能先显示可操作内容再在水合时隐藏。
6. 交互状态与动效
6.1 状态表达
状态覆盖是基线,不是可选项。悬停、按下、聚焦、选中、展开、加载、报错、禁用、 数值变化与增删排序,只要状态变了,就必须有可见且连续的反馈;没有动画是待补, 不是克制。瞬时跳变只允许出现在减少动态效果里,或首屏的测量 / 水合修正上。
| 状态 | 视觉与行为要求 |
|---|---|
| 悬停 | 只反馈当前可操作对象,不改变布局 |
| 按下 | 使用按钮档位的颜色反馈,不做垂直位移 |
| 键盘聚焦 | 保留清晰的焦点标识,不能只移除 outline |
| 选中 | 使用持续的淡底、文字、滑块或开启态光晕,并提供 ARIA 状态 |
| 展开 | 触发器、箭头、外壳与面板保持同一状态 |
| 禁用 | 弱化显示并实际阻止操作 |
| 加载 | 明确正在处理,防止重复提交并保持几何稳定 |
| 错误 | 用文字、颜色与关联关系说明,不能只变红 |
点赞、投喂、收藏等状态按钮沿用同一递进:
未激活时灰色无底;未激活悬停或已激活空闲时为功能色淡底与同色图标;
已激活悬停及按下时为功能色实底与白色图标。
开关 / 勾选项也走这条递进(评论输入区「匿名」前面那颗圆形勾选钮即此形态),
但勾选态直接取功能色实底 + 白色图标,并额外常驻一层开启态光晕
(--shadow-glow-brand,见 §3.6)——“打勾 = 已选”要一眼看得出来,淡底留给悬停;
光晕是这条状态唯一多出来的一档。
勾选钮里的 ✔ 是状态图标的承载者:颜色跟随上述递进,但不能代替 ARIA 状态;
原生复选框的语义、标签与键盘操作必须保留。
触屏松手后恢复真实激活状态,悬停配方按需要限定为
(hover: hover) and (pointer: fine),不能残留模拟悬停造成假选中。
6.2 动效节奏
节奏是第二层问题。先确认动效属于这个组件:同壳、同源、对位(§6.3),再谈时长。
按用途定义时长,不使用一条 transition: all 统管所有变化。
形变只保留两条节奏:展开 480ms / cubic-bezier(0.16, 1, 0.3, 1),
收起 220ms / cubic-bezier(0.4, 0, 0.2, 1)。伸缩、宽度、位移、旋转、网格展开、
入场与退场都引用这两条;颜色、底色、阴影、透明度的短反馈不并入,仍走 150–250ms 的 ease。
这两条是同一手势内共用的时间线,不是"全站只允许两个动画";颜色反馈、内容淡入与
错峰仍是受约束的从属节奏。覆盖范围见 §6.1,不因这两条节奏而缩小。
以下节奏来自博客目录与现有共享组件:
| 变化 | 时长 | 缓动 |
|---|---|---|
| 按钮文字、底色 | 250ms | ease |
| 主按钮按下压暗 | 150ms | ease |
| 卡片悬停与字段聚焦光晕 | 200ms | ease |
| 胶囊滑块位置 | 250ms | ease |
| 展开:外壳伸缩、宽度、位移、旋转、网格展开、入场 | 480ms | cubic-bezier(0.16, 1, 0.3, 1) |
| 收起:以上各项反向 | 220ms | cubic-bezier(0.4, 0, 0.2, 1) |
| 展开内容淡入淡出 | 180ms | ease |
展开内容可延后 55ms 开始,连续选项按 28ms 错开。 这套延迟只用于需要说明顺序的展开,不给所有列表逐条排队入场。
expand-panel 与顶栏移动导航不分两套:展开 480ms、收起 220ms(内容先退场,
空面板随后收回);具体编排由顶栏组件持有。顶栏主题切换按钮的旋转、首页日月旋转与天体换色
也引用上面两条,不另写时长与曲线。新组件先使用公共节奏,
需要不同时间时按操作体量说明理由,不能另列无用途的动画常量。
6.3 展开与收起的编排
- 触发后同一外壳开始伸展,箭头同步改变方向;面板从触发器的边界长出, 不另建独立浮层。
- 收起态与展开态都存在的部件一一对应:同一颗按钮换标签、同一颗头像位移、 同一个关闭钮换位置;展开态新增的内容只能由外壳内部让出, 不能从壳外飞入或原地淡入。
- 一次手势只有一条主几何时间线;内容跟着外壳走,进入可见区域后淡入, 轻微位移与外壳保持相同方向。内容不能先于壳出现(会溢出),也不能明显滞后(会空壳)。
- 收起先停止内部操作并退出焦点序列,内容先退场,外壳随后收缩; 不能先收壳把内容切断。
- 收完后再隐藏可视面板;不能第一帧就移除 DOM 或 visibility, 让收起动画消失,也不能为了等待动画而继续接收点击。
整个过程裁剪在外壳圆角内,不叠加回弹、弹簧与和部件无关的并行运动。 凭空淡入的面板、旧控件淡出 / 新控件淡入、壳先长而内容迟到、关闭时内容被壳切断, 都属于把动画盖在组件上,不是本节这套编排。
按实际内容高度伸展可复用 expand-panel 的 0fr → 1fr。
同一时间线里还有按进度线性移动的元素时(例如手机悬浮球的头像),面板高度改用显式 height
过渡:浏览器把 fr 内插成「进度²」,叠上线性位移会让合成轨迹折一下。
组件内部变化不能触发整页位移,内容溢出时的裁剪与滚动在过渡前后保持稳定。
连续操作从当前状态续接,不等待上一段播放完再响应。
6.4 首屏与主题切换
服务端兜底尺寸到客户端测量尺寸的修正不播放用户操作动画。 这条只约束"没有用户操作"的首帧修正,不表示首屏不该有交互反馈:用户一旦操作, 该有的过渡照常播放(§6.1)。收起状态、排序与主题尽量在首帧就正确,避免先展开再收起、 先亮后暗。
手动主题切换由 public/static/js/core/base.js 统一协调文字、底色、
阴影与 SVG 颜色,局部组件不另跑一套主题动画。换色时间线是 480ms、
cubic-bezier(0.16, 1, 0.3, 1),与形变的展开档同一条。
首页日月的旋转、透明度与天体填色走同一条 480ms 时间线,曲线也相同;
主题换色的 WAAPI 动画与日月必须同速同曲线,改一处要改两处。
离开视口的内容直接更新到终态,避免大量不可见元素占用动画开销。
全局颜色过渡只由 src/styles-scss/base/_reset.scss 定义,
形变、位移和缩放限定在对应组件,不能扩大到全局 * 选择器。
6.5 减少动态效果
prefers-reduced-motion: reduce 下关闭或压缩形变、滑移、视差与错开入场,
直接显示最终状态;主题直接换色。减少动态效果不是取消状态反馈,而是把过渡压到 0ms,
终态、内容、焦点、隐藏状态与操作能力都不变。
顶栏展开后长用户名的自动左右往返也属于这一类:减少动态效果时停止移动,但那一格
仍是可手动横向滚动的文本框,读到全名的路径不消失。动画结束事件不能成为完成功能的唯一条件。
7. 典型页面的组合方式
7.1 列表与目录
博客目录是主要参照:页面底色上放列表卡片,桌面配个人侧栏, 上方同一工具条容纳搜索、栏目与排序。 卡片通过标题、摘要、作者、统计和日期表达内容层级,日期保持辅助信息的强度, 详细时间通过可访问的详情入口或提示提供。
目录的基准尺寸是:桌面侧栏 280px,筛选控件统一高 42px;
≤992px 搜索独占首行,栏目和排序放第二行。
移动个人入口为 129×70px 胶囊:左内距 13 + 创建钮 44 + 间距 8 + 头像 44 + 右侧留白 20,
创建钮与头像各为 44px,创建圆钮与胶囊左端圆角同心。
展开后复用同一外壳:头像移到面板左上(left 20 / top 24、48×48),创建钮变成 105px 宽的按钮,
圆图标与自身左端圆角同心,文案右侧保留与图标同档的留白。
创建钮是同一颗按钮展开文字,头像与关闭钮也是同一颗元素位移,不是收起态消失、展开态新建。
收起态到展开态,外壳宽度、面板高度与头像的 top/left/宽高必须共用同一条时间线
(展开 480ms、收起 220ms,见 §6.2),合成轨迹才是直线。
面板高度用显式 height 过渡,不用 grid-template-rows 的 0fr → 1fr:
fr 会被内插成进度²,而头像按进度线性移动,两者叠加会让头像走折线。
卡片按 §3.3 从桌面 30px 收紧到 ≤768px 的 18px 圆角。
通知页的按日分组沿用同一套列表观感:同一天的通知收在同一组, 组标题用字号、字重与次要色建立层级,组间用卡片间距分开,不画贯通分割线。
这组具体几何由目录组件持有,其他列表复用其关系,
不要求每页都有个人侧栏或悬浮入口。
公开列表 /explore 与站内目录有不同的信息组织,
不能把其旧栏目侧栏当成新的全站基准。
7.2 阅读与读者操作
标题、作者、时间和正文共用清楚的阅读外壳, 避免标题卡、正文卡与渲染器卡片重复套壳。 评论与嵌入内容沿用相同的底色层级,长引用、代码与媒体不撑宽页面。
评论区的楼中楼按层深交替使用卡片底色与卡内内容底色: 每条评论独立成卡,同级评论之间用卡片间距分隔,不使用分割线; 头像、正文与操作行共用同一条左竖线(正文不让出头像列), 某条评论的子回复由操作行右端的小箭头整棵折叠。 顶部主输入框不另套外壳,多行输入面板本身使用弱背景色,与评论卡的卡片底色区分开; 就地回复只保留输入框本体(不出现回复预览条,也不套外框),右上角有一颗取消叉。 输入区底条的「匿名」文字不变,变的是它前面那颗圆形勾选钮(样式与状态见 §6.1): 空闲为中性圆底 + 灰色 ✔,勾选后是品牌实底 + 白色 ✔ 与开启态光晕;「匿名」两个字 不随状态变色,也不套底色。
文章详情的读者操作条保留以下契约:
- 返回圆钮、互动胶囊、管理胶囊全视口保持同一行, 整排按实际内容宽度居中,外壳均高 48px。
- 互动底座内距 4px,内部按钮高 40px,只显示图标与同行数字;
点赞和收藏有
aria-pressed,所有操作有可访问名称。 - 管理项向右展开。≤768px 展开管理时互动胶囊收缩并退出交互, 收起后恢复;空间不足只允许操作区滚动,不能撑宽正文。
- 极窄屏仍保证最后一项可达,关闭菜单或取消弹窗后焦点回到相应入口。 数字变化平滑伸缩,不能让操作条突然错位。
- 收藏选择器中名称独占一行,两种创建操作在下一行等宽并排; <360px 才上下堆叠。收藏状态遵循 §3.1 与 §6.1。
这些契约由 tests/e2e/favorite-layout.spec.ts 覆盖,
改布局前先检查其几何断言,不能只看桌面截图。
7.3 表单与管理页面
表单按任务分组,标签、帮助、错误和提交位置保持一致。 主要行动与辅助行动使用既有按钮档位,密集表单通过列数与间距调整密度。
后台共用 AdminShell 的宽工作区和导航,内容组件只承担内容列。
数据列表、筛选条、分页和批量操作复用公共组件规则;
个人主页等自由组合页面保持自己的网格,但卡片表面、按钮和编辑状态遵循同一标准。
7.4 工作台与图表
工作台按任务分配侧栏、主区与操作区,空间不足时折成横条、抽屉或单列, 尽量复用同一份 DOM 和状态。整屏工作台由外壳承担视口高度, 内部各区明确谁滚动,输入区与必要操作不能被固定栏或软键盘盖住。
讨论消息默认采用卡片底色气泡,内距 8px 16px,
圆角为 calc(0.775em + 8px):单行呈胶囊,多行保持相同曲率。
讨论区的结构性边框一律不画:会话侧栏的右分栏线、侧栏头部与脚部横线、
消息主区头部横线、输入区顶线、弹窗分页顶线都删掉,层级只由页面、卡片与内容底色承担。
保留有内容语义的分隔:日期分隔、新消息线、菜单分组线。
聊天气泡(含 data-bubble 用户款式)的边框、底色与圆角仍是气泡自己的外观(§7.5),不随这条改变;
图标淡色底座的圆形规则同样适用于讨论区(§3.3)。
消息操作使用右键或触屏长按菜单;触屏长按 500ms、
移动超过 8px 取消,长按后的点击不重复触发操作。
菜单夹入视口并复用统一浮层皮肤,分组、回复和消息状态由消息组件决定。
实现入口为 src/app/chat/ChatMessageItem.tsx 与
src/app/chat/ChatMessageMenu.tsx。
行情工作台随容器阶梯从三栏收紧为两栏、单列;
自选列表可以从竖列改为横条,具体列宽由
src/styles-scss/pages/_fish-trade.scss 维护。
图表数据图形与界面标签分层:会非等比拉伸的 SVG 只画图形,
坐标文字用同格 HTML 定位,线条保持稳定视觉宽度。
涨跌颜色配合正负号、方向和文字,不能只依赖红绿。
7.5 专用内容与装饰款式
编辑器、媒体查看、图表、原创插画和用户选择的聊天气泡可按内容需要定义专用外观, 但不能修改公共按钮、主题、焦点和页面外壳的规则。
编辑器的编辑边界可以保留自身描边;直角或描边聊天气泡仅作用于选定款式, 普通卡片仍按 §5.3。款式边框改变时从内距中扣除对应尺寸, 保持正文位置与外部几何稳定。头像框按 §3.4,不能改造成 CSS 外环。
Markdown 正文与自由内容有独立的排版规则。新增表情、用户名片等内联引用时, 检查通用正文图片选择器带入的 margin、border、border-radius、background、cursor, 按内联组件用途重置,避免把正文图片的外观套到头像或透明贴图上。 内容引用、匿名与权限行为由服务与组件契约决定,不能因统一样式改变。
8. 实现约定与验收
8.1 样式入口与令牌来源
样式入口为 src/styles-scss/main.scss,由
src/app/layout.tsx 引入并交给 Next.js 编译。
修改 SCSS 源文件即可,开发使用 HMR,正式构建输出到 Next 的产物目录;
不手工提交编译 CSS,也不把入口改成 CSS Module。
| 目录 | 职责 |
|---|---|
abstracts/ |
尺度、断点、mixin 与共享计算 |
base/ |
reset、字体、主题变量与基础排版 |
components/ |
跨页面组件表面与交互 |
layout/ |
容器、导航、页脚与布局外壳 |
pages/ |
页面组合与专用组件尺寸 |
utilities/ |
小范围通用工具类 |
颜色、字体和安全区读取 base/_root.scss;SCSS 尺度读取
abstracts/_variables.scss;断点读取 abstracts/_breakpoints.scss。
使用明暗主题 CSS 变量,不启用已经停用的 theme-map / themeify 方案。
原生控件与滚动条随 color-scheme 切换;自定义滚动条复用根样式,
不在局部页面另建亮暗配方。
8.2 复用与样式边界
优先复用已有组件、公共类和 mixin。
相同视觉用于不同 BEM 前缀时使用参数化 mixin,
例如 rich-composer($p)、rich-markdown($cls)、doc-prose($cls);
按钮、字段与展开面板复用 abstracts/_mixins.scss。
页面拥有宽度、页边距与主标题,组件拥有内部结构和状态。 渲染器、弹窗或子组件嵌入页面时不能再次提供同一外壳。 新页面类名限制在自己的根作用域,不能用同名全局选择器修改另一页。
复用前核对实际 DOM、选择器权重与导入顺序。
带 !important 的文字工具类会覆盖组件颜色,不挂到需要表达交互状态的控件上;
不能用增加权重和 !important 层层覆盖代替修正样式归属。
Bootstrap 风格类由本地样式提供,不代表加载了 Bootstrap。
JS 或 JSX 使用的类必须有样式或明确的行为消费者;
刻意无样式的类由 tests/unit/css-tsx-classes.test.ts 的台账维护,
不在本文重复列名单。已应用数据库迁移中的注释属于历史上下文,
不为调整本文章节编号而改写迁移文件。
8.3 可访问性与体验验收
实现完成后检查:
- 内容与语义:一个页面主标题,标签和错误关联正确; 图标有名称,选中与展开状态可被读屏识别,颜色不是唯一信息。
- 键盘与焦点:操作可用键盘到达,焦点始终可见; 隐藏面板不可聚焦,ESC 与关闭后的焦点恢复符合组件用途。
- 主题与输入方式:明暗主题均可读,原生下拉和浮层颜色配对; 触屏没有悬停残留,长文字与数值输入不会破坏布局。
- 布局:宽屏、窄屏与极窄屏无整页横向溢出; 长标题、长名字、较大数字、空结果和错误状态仍能完整使用。
- 动效:所有交互状态都有连续反馈,没有瞬时换脸; 首屏的测量 / 水合修正不播放动画(那是无操作修正,不是交互反馈); 连续点击从当前状态续接;减少动态效果时直接切换,功能与焦点不依赖动画结束。
- 复用影响:共享组件的各使用场景都要验证,不能只检查当前页面。
8.4 检查方式
文档改动运行登记、引用和渲染守卫:
npm test -- tests/unit/docs-catalog.test.ts tests/unit/docs-xref.test.ts tests/unit/docs-render.test.ts tests/unit/guide-docs.test.ts
样式实现按改动范围选择 CSS 类名守卫、组件测试和现有布局 e2e;
样式检查需要编译后的选择器,不能用扫 SCSS 字面量代替。
scripts/compiled-css.mjs 为守卫即时编译;
npm run css:probe 只服务离线探针,不属于正常构建流程。
几何与交互变化必须通过实际页面验证。
已有 tests/e2e/blog-directory.spec.ts、
tests/e2e/favorite-layout.spec.ts、tests/e2e/page-gutter.spec.ts
及主题相关用例分别覆盖目录、读者操作、页边距与主题。
e2e 读取现有构建产物;修改源码后先构建,或使用 npm run e2e:ci,
避免把旧产物的结果当成本次验证。
附录 A:实现索引
本表提供源码入口,不复制页面尺寸和已完成记录。查看组件时一并检查其对应测试。
| 对象 | 入口 |
|---|---|
| 主题、字体、安全区 | src/styles-scss/base/_root.scss、src/styles-scss/base/_fonts.scss |
| 尺度与公共配方 | src/styles-scss/abstracts/_variables.scss、src/styles-scss/abstracts/_mixins.scss |
| 博客目录卡片与搜索 | src/styles-scss/pages/blog/_menu.scss |
| 目录筛选与个人面板 | src/styles-scss/pages/blog/_directory.scss、src/app/blog/BlogCategorySelect.tsx、src/app/blog/BlogPersonalSidebar.tsx |
| 字段、分段切换、展开选择 | src/styles-scss/components/_form-controls.scss、src/styles-scss/components/_segmented.scss、src/app/components/ExpandSelect.tsx |
| 头像与头像框 | src/app/components/Avatar.tsx、src/styles-scss/components/_avatar.scss |
| 顶栏与主题切换 | src/styles-scss/layout/_header.scss、public/static/js/core/base.js |
| 阅读卡与互动操作 | src/styles-scss/pages/blog/_blog.scss、src/styles-scss/pages/_favorite.scss |
| 评论与讨论输入、正文 | src/styles-scss/components/_composer.scss、src/styles-scss/components/_markdown-body.scss |
| 后台与自由网格 | src/app/components/AdminShell.tsx、src/styles-scss/pages/admin/_common.scss、src/styles-scss/pages/_profile.scss |
| 代码编辑与文档排版 | src/styles-scss/components/_markdown-editor.scss、src/styles-scss/components/_doc-prose.scss |