264 lines
12 KiB
Markdown
264 lines
12 KiB
Markdown
# 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 文档中明确记录。
|