12 KiB
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. 事实来源与优先级
发生冲突时按以下顺序判断:
- 业务功能、数据边界和用户可完成的操作不能丢失。
design中的组件结构、状态矩阵、动效和真实素材决定视觉结果。- Reka UI 的可访问性与交互语义决定组件的基础 DOM 和状态模型。
- 组件 API、目录和组合方式遵循本标准。
- 仅在不影响以上约束时使用通用 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. 推荐目录
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 分成两层:
- Primitive Token 保存 DCalc 的原始色值、品质颜色和基础素材色。
- Semantic Token 表达组件用途,如
--ui-background、--ui-primary、--ui-border、--ui-ring。
组件只使用 --ui-* 语义变量;主题文件在 [data-theme='<name>'] 下完成映射。新增皮肤时:
- 新建
style/themes/<name>.css。 - 完整实现现有
--ui-*契约。 - 在 Tailwind 入口中导入主题文件。
- 在
useTheme.ts注册主题名。 - 使用组件目录页逐状态验证,不修改组件内部代码。
禁止在组件中以 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 实现包装组件
标准模式:
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 的坐标系。数值标签、单位和标题不得放进这个坐标容器并参与其宽度。
.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 静态检查
pnpm run format:check
pnpm run lint
pnpm run type-check
pnpm run build-only
git diff --check
10.2 浏览器检查
逐个组件验证:
- 默认视觉和 design 对比。
- 鼠标 hover/pressed 和键盘 focus-visible。
- v-model、禁用态和边界值。
- Select、Tooltip、DropdownMenu、Dialog 的层级、避让和关闭行为。
- Slider、Tabs 等动态几何的实际端点。
- 页面顶部到底部可滚动,且无意外横向滚动。
- 主题切换后组件无需修改即可工作。
- 浏览器控制台无新增 error/warning。
构建通过不是浏览器交互通过。涉及滚动、焦点、Portal、拖拽、动效或响应式的问题必须保留运行态证据。
11. 完成定义
一个组件只有同时满足以下条件才算迁移完成:
- 旧调用点所需功能已覆盖,业务链路可完成。
- design 的结构、状态、动效和素材已经逐项核对。
- Reka 的键盘、ARIA、焦点和受控状态没有被视觉包装破坏。
- 组件只消费语义 Token,并通过至少两个主题验证。
- 组件目录页包含真实交互和状态矩阵。
- 桌面、平板、手机宽度无页面级溢出。
- format、lint、type-check、build 和浏览器 QA 均通过。
- 尚未迁移或有意保留的差异在 QA 文档中明确记录。