Files
dcalc/frontend-ui/docs/ui-migration-standard.md
T
2026-08-05 19:01:52 +08:00

264 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# DCalc UI 统一迁移与实现标准
本文档用于在其他 Vue 项目中复用当前 `frontend-ui` 的组件库迁移方法。目标不是机械复制页面,而是在保持业务功能的前提下,统一交互底座、视觉语言、主题能力、响应式策略和验收流程。
## 1. 标准目标
- 以现有业务实现为功能基线,允许调整业务代码以适配新的组件组合方式。
- 以 `design` 的组件结构、状态、动效、素材和交互为视觉事实来源。
- 使用 Reka UI 承担无障碍、键盘、焦点、受控状态和 Portal 等交互语义。
- 使用 Tailwind CSS 处理通用布局,关闭 Preflight,避免迁移期全局样式突变。
- 使用 CSS Variables 建立 DCalc Token 和语义 Token,组件只消费语义 Token。
- 使用 CSS/SCSS 实现游戏质感、像素素材、复杂渐变、边框和状态效果。
- 参考 shadcn-vue 的目录、组合、表单和 variants 规范,不要求引入整套默认视觉。
- 使用 Oxc 统一格式化和静态检查。
## 2. 事实来源与优先级
发生冲突时按以下顺序判断:
1. 业务功能、数据边界和用户可完成的操作不能丢失。
2. `design` 中的组件结构、状态矩阵、动效和真实素材决定视觉结果。
3. Reka UI 的可访问性与交互语义决定组件的基础 DOM 和状态模型。
4. 组件 API、目录和组合方式遵循本标准。
5. 仅在不影响以上约束时使用通用 Tailwind 工具类简化布局。
不要以静态截图代替业务与交互核对。设计中未展示的状态仍需覆盖 hover、pressed、focus-visible、disabled、open/closed、checked/unchecked 和 reduced-motion。
## 3. 推荐技术栈
| 层级 | 标准职责 |
| ------------------------ | ----------------------------------------------------------- |
| Vue 3 + TypeScript | 使用 Composition API、`<script setup>` 和类型化 Props/Emits |
| Vue Router + Pinia | 保留现有路由和业务状态边界 |
| Reka UI | Primitive、焦点管理、键盘交互、受控状态、Portal |
| Tailwind CSS | 栅格、Flex、尺寸约束和简单响应式布局 |
| CSS Variables | Primitive Token、Semantic Token、主题映射 |
| CSS/SCSS | 游戏纹理、分层渐变、图片边框、复杂动效 |
| class-variance-authority | Button、Badge 等离散 variants |
| clsx + tailwind-merge | 条件 class 合并,统一由 `cn()` 暴露 |
| oxlint + oxfmt | Lint、格式化和 CI 检查 |
## 4. 推荐目录
```text
src/
├── components/
│ ├── ui/ # 无业务语义的基础组件
│ │ └── button/
│ │ ├── UiButton.vue
│ │ ├── buttonVariants.ts
│ │ └── index.ts
│ ├── domain/ # 领域组合组件
│ └── hooks/ # Dialog 等跨组件编排
├── composables/
│ └── useTheme.ts
├── lib/
│ └── utils.ts # cn()
├── pages/
│ └── ComponentCatalog/ # 全组件状态与交互目录
└── style/
├── tokens/primitives.css
├── themes/dcalc.css
├── themes/<skin>.css
├── components/
├── tailwind.css
└── index.scss
```
基础组件不得直接请求接口、读取业务 Store 或绑定具体页面路由。业务行为放在页面、领域组件、Store 或 composable 中。
## 5. Token 与换肤标准
Token 分成两层:
1. Primitive Token 保存 DCalc 的原始色值、品质颜色和基础素材色。
2. Semantic Token 表达组件用途,如 `--ui-background`、`--ui-primary`、`--ui-border`、`--ui-ring`。
组件只使用 `--ui-*` 语义变量;主题文件在 `[data-theme='<name>']` 下完成映射。新增皮肤时:
1. 新建 `style/themes/<name>.css`。
2. 完整实现现有 `--ui-*` 契约。
3. 在 Tailwind 入口中导入主题文件。
4. 在 `useTheme.ts` 注册主题名。
5. 使用组件目录页逐状态验证,不修改组件内部代码。
禁止在组件中以 `data-theme` 分支写特定皮肤逻辑。确需固定的源素材颜色时,保留物理色值,并在代码注释或 QA 文档中注明它属于素材还原而非主题语义。
## 6. 单组件迁移流程
### 6.1 盘点功能
- 找出旧组件的所有调用点、Props、Emits、插槽、v-model、快捷键和禁用条件。
- 沿调用链确认业务副作用,避免只复制组件外观。
- 记录必须保持的功能和允许调整的旧 API。
### 6.2 拆解 design
- 对照组件结构、真实尺寸、边框层、渐变、素材、文本和间距。
- 建立完整状态矩阵:rest、hover、pressed、focus、disabled、selected/open。
- 动效需记录帧数、间隔、缓动、焦点条件和 reduced-motion 行为。
- 优先复用原始 PNG、字体、图标和 rarity frame,不用近似图标替代。
- 先核对 Design 的真实实现方式:源组件使用 CSS 渐变、伪元素或 `clip-path` 构造的
控制图标继续使用 CSS;只有被 Design 明确作为像素素材引用的图标才引入图片。
### 6.3 选择 Reka Primitive
- 先使用 Reka 已有 Primitive,再包装视觉层。
- 使用 Reka 提供的 `data-state`、`data-disabled`、ARIA 和键盘模型。
- 不重复实现已有的焦点陷阱、Escape 关闭、方向键选择和焦点恢复。
- 只有 design 明确增加的行为才在包装层补充,例如 NumberField 快速步进和 Tabs 移动标记。
### 6.4 实现包装组件
标准模式:
```ts
const props = defineProps<RekaProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<RekaEmits>()
const delegatedProps = computed(() => {
const { class: _class, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
```
- 从转发对象中排除视觉包装专属 Props。
- 通过 `cn(baseClass, props.class)` 合并调用方 class。
- variants 较少且稳定时使用 CVA;动态几何或连续数值使用 computed/CSS Variable。
- 保留清晰的 `Ui<Component>` 命名和目录级 `index.ts` 导出。
### 6.5 迁移业务调用
- 将旧组件 API 映射到新组件的受控状态和事件。
- 允许调整页面或业务组件,使其符合 Reka 的组合方式。
- 每次迁移一个可验证的业务切片,旧实现只在调用点迁完后删除。
- 不在通用组件中加入仅为兼容单个旧页面的隐式逻辑。
## 7. 关键实现规则
### 7.1 Portal、Teleport 与层级
Reka 的 Select、Tooltip、DropdownMenu、Popover 等通常经过 Portal 和 Popper 包装层。Vue scoped CSS 可能只落到包装节点,而没有落到实际 Content 节点。
- 对 Portal Content 使用全局唯一的 BEM class,并采用非 scoped 样式或 `:global()`。
- 同时给 Content 和 `[data-reka-popper-content-wrapper]:has(> .<content-class>)` 设置明确层级。
- 配置 `collisionPadding`,验证视口四边的避让。
- 不能只看 DOM 中存在 listbox/tooltip;必须检查背景、边框、尺寸和最上层命中元素。
推荐层级:普通浮层 80,Tooltip 90,Dialog/Overlay 按页面统一规划,禁止无上限叠加 z-index。
### 7.2 Slider 坐标一致性
`SliderRoot` 的宽度就是 Reka 计算 Range 和 Thumb 的坐标系。数值标签、单位和标题不得放进这个坐标容器并参与其宽度。
```text
.slider-shell
├── SliderRoot.slider-control
│ ├── SliderTrack
│ │ └── SliderRange
│ └── SliderThumb
└── .slider-value
```
验收时检查 `rangeWidth / trackWidth` 是否等于当前百分比,并确认 Thumb 中心与 Range 终点一致。
### 7.3 Tabs 动态标记
- 标记归 TabsList 所有,不绑定在某个 Trigger 的伪元素上。
- 根据 active Trigger 与 List 的实际矩形计算中心位置。
- 监听 `data-state`、容器 Resize 和字体加载完成事件。
- 初次定位不播放位移动画,后续选择变化才动画。
- 在 `prefers-reduced-motion: reduce` 下移除过渡。
### 7.4 Dialog 标题动效
- 焦点态使用 design 提供的完整帧序列,不用单个渐变假装动画。
- 使用确定的数字顺序加载帧,避免字符串排序造成 `10` 排在 `2` 前。
- 仅在 focused 状态推进和展示动效;unfocused 状态使用静态标题栏。
- 组件卸载时清理计时器,reduced-motion 下隐藏或静态化动效。
- 可拖动窗口仅以标题栏空白区域作为手柄,关闭按钮和标题栏操作控件不得触发拖动。
- 使用 Pointer Events 同时覆盖鼠标和触摸;拖动范围限制在当前视口安全边距内。
- 每次重新打开或视口尺寸变化时恢复几何居中,避免桌面偏移泄漏到移动端。
- 标题使用对称网格独立居中,长 Description 放入内容区,不参与标题栏宽度计算。
### 7.5 NumberField 扩展操作
- 基础加减由 Reka NumberField 处理。
- 快速步进作为包装层显式按钮,遵守 min/max 边界并发出标准 `update:modelValue`。
- 所有图形按钮必须有可读的 `aria-label`。
### 7.6 页面滚动契约
- 从应用根到页面滚动容器的 Flex/Grid 链路必须包含 `min-height: 0`。
- 页面只能有一个主要纵向滚动所有者。
- 若 `html/body` 被旧应用设为 `overflow:hidden`,路由视图容器必须明确 `height:100%`、`min-height:0` 和 `overflow-y:auto`。
- Portal 不得渲染到会裁剪内容的局部 `overflow:hidden` 容器。
## 8. 响应式标准
区分“必须像素对齐的组件原子”和“必须流式变化的页面布局”:
- 保留 px:1px 边框、位图原始尺寸、30px 控件高度、像素箭头、帧动画和精细纹理。
- 使用流式单位:页面 gutter、列宽、面板间距、内容最大宽度和字号层级。
- 推荐 `clamp()`、`minmax(0, 1fr)`、`auto-fit`、Flex wrap 和少量语义断点。
- 宽表格和完整状态矩阵允许组件内部横向滚动,但页面本身不得产生横向溢出。
- 至少验证 1920、1280、768、390 CSS px;同时检查 200% 缩放与键盘访问。
不要为了“去除 px”而破坏游戏素材的像素精度。响应式的重点是容器、组合与内容流,而不是把所有物理尺寸换成 rem。
## 9. 组件目录页要求
每引入一个组件,都必须同步加入 `/components` 或等价目录页,并展示:
- 默认、hover、pressed、focus、disabled 等状态。
- 受控值变化和可见的结果反馈。
- 键盘操作、Escape、焦点恢复和 Portal 展示。
- 主题切换后的同一状态。
- 窄屏组合和必要的内部滚动。
- 真实业务素材,而非临时占位图。
目录页既是文档,也是运行态回归面。静态状态展示不能替代真实可交互实例。
## 10. 验证流程
### 10.1 静态检查
```sh
pnpm run format:check
pnpm run lint
pnpm run type-check
pnpm run build-only
git diff --check
```
### 10.2 浏览器检查
逐个组件验证:
1. 默认视觉和 design 对比。
2. 鼠标 hover/pressed 和键盘 focus-visible。
3. v-model、禁用态和边界值。
4. Select、Tooltip、DropdownMenu、Dialog 的层级、避让和关闭行为。
5. Slider、Tabs 等动态几何的实际端点。
6. 页面顶部到底部可滚动,且无意外横向滚动。
7. 主题切换后组件无需修改即可工作。
8. 浏览器控制台无新增 error/warning。
构建通过不是浏览器交互通过。涉及滚动、焦点、Portal、拖拽、动效或响应式的问题必须保留运行态证据。
## 11. 完成定义
一个组件只有同时满足以下条件才算迁移完成:
- 旧调用点所需功能已覆盖,业务链路可完成。
- design 的结构、状态、动效和素材已经逐项核对。
- Reka 的键盘、ARIA、焦点和受控状态没有被视觉包装破坏。
- 组件只消费语义 Token,并通过至少两个主题验证。
- 组件目录页包含真实交互和状态矩阵。
- 桌面、平板、手机宽度无页面级溢出。
- format、lint、type-check、build 和浏览器 QA 均通过。
- 尚未迁移或有意保留的差异在 QA 文档中明确记录。