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

12 KiB
Raw Blame History

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. 推荐目录

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 实现包装组件

标准模式:

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 浏览器检查

逐个组件验证:

  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 文档中明确记录。