Files
workspace/code/g3soft-libs/.codebuddy/plans/g3-ui-重写-参照fms-vue_084cfb52.md
T
2026-10-08 22:38:34 +08:00

334 lines
22 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.
---
name: g3-ui-重写-参照fms-vue
overview: 以 fms-vue 的 `src/components/ui` 为原型,全量重写 `packages/ui`(@g3soft/ui)组件库:25 个组件按「基础→表单→浮层→反馈→数据展示」分批实现,全部 TypeScript 化并遵守 g3 约定(非 scoped SCSS + `g3-` BEM + `--g3-*` 令牌 + types.ts + gen:api + 文档示例)。确认语义统一从 fms 的 `ok/onOk/okText` 改为 `confirm/onConfirm/confirmText`。
design:
styleKeywords:
- 中性克制
- shadcn 风格主色派生
- 层次靠面不靠线
- 轻描边
- 微动效反馈
- 三层 z 轴秩序
fontSystem:
fontFamily: PingFang SC
heading:
size: 24px
weight: 600
subheading:
size: 16px
weight: 500
body:
size: 14px
weight: 400
colorSystem:
primary:
- "#18181B"
- "#E5E5E5"
background:
- "#FFFFFF"
- "#141414"
text:
- "#000000"
- "#FFFFFF"
functional:
- "#45B114"
- "#E79A0D"
- "#EC4246"
todos:
- id: infra-setup
content: 扩展 tokens 8 个变量、TS 化 _utils、改造 z-index 分层、加 @lucide/vue 依赖与 external
status: completed
- id: genapi-and-config
content: 改造 gen-api 只解析目录主组件,扩充 locale 并实现 useComponentDefaults
status: completed
dependencies:
- infra-setup
- id: base-components
content: 用 [skill:ui-ux-pro-max] 重写 Button 变体体系,实现 button-group/space/grid/tag/segmented/scrollbar/spin
status: completed
dependencies:
- genapi-and-config
- id: overlay-popover
content: 用 [subagent:code-explorer] 移植浮层工具,实现 useFloating 与 popover/tooltip/dropdown
status: completed
dependencies:
- base-components
- id: form-components
content: 移植 input/checkbox/radio/switch/select 与 form 校验体系
status: completed
dependencies:
- overlay-popover
- id: modal-drawer-feedback
content: 参照 [skill:antdv-next] 实现 modal(confirm 语义+命令式)、drawer、message/notification
status: completed
dependencies:
- overlay-popover
- id: data-display
content: 实现 tree/tabs/pagination/splitter 与 date-picker/range-picker/time-select
status: completed
dependencies:
- overlay-popover
- id: docs-and-verify
content: 补齐示例与文档页,跑 gen:api、tokens/ui 全量构建冒烟
status: completed
dependencies:
- base-components
- form-components
- modal-drawer-feedback
- data-display
---
## 产品概述
将 `packages/ui`(@g3soft/ui)从一个只有 Button + ConfigProvider 的雏形,重写为一套完整的 Vue 3 组件库。实现原型来自 `D:\workspace\code\fms\fms-vue\src\components\ui` 的 25 个组件目录,但必须全部落在 g3soft 自身的技术约定上:TypeScript、非 scoped SCSS + `g3-` BEM、 `--g3-*` 设计令牌、`types.ts` + `gen:api` 生成文档、docs 示例与 md 页面。
## 核心范围
- 全量迁移 fms-vue 的组件能力,按「基础 → 浮层 → 表单 → 覆盖层与反馈 → 数据展示」分批落地,每批可独立构建验证。
- 共规划 28 个组件目录:button、button-group、space、grid、scrollbar、spin、tag、segmented、input、checkbox、radio(+radio-group)、switch、select、form(+form-item/form-group)、date-picker、range-picker、time-select、popover、tooltip、dropdown、modal、drawer、message、notification、tree、tabs、pagination、splitter。
## 确认语义(本次重写的关键变更)
fms 的 `ok` 风格一律改为 `confirm` 风格:
- `okText` → `confirmText`、`okButtonProps` → `confirmButtonProps`、`onOk` → `onConfirm`
- 事件 `ok` → `confirm`(`update:open` / `cancel` / `closed` 保持不变)
- 命令式仍为 `G3Modal.confirm({ ..., onConfirm })`,返回 `{ destroy }`
- `confirmLoading` 语义不变予以保留;「只有点取消按钮才触发 `onCancel`,点遮罩/Esc/右上角 X 为静默关闭」的行为保留
## 其他已确认决策
- Button 改用 fms 变体体系:`primary | secondary | outline | ghost | danger | danger-outline | link | icon`(破坏性变更,现有 `docs/examples/button/*` 与 `button.md` 一并重写)。
- 图标新增 `@lucide/vue` 运行时依赖,照搬 fms 用法。
- 扩展 `@g3soft/tokens`,但只补跨组件共享的少量语义变量,组件私有尺寸一律用 `v()` 兜底不进令牌;**z-index 不进令牌**,继续走 `packages/ui/src/_utils/z-index.ts`,并改造为分层分配。
## 技术栈
- 组件库:Vue 3.5(`<script setup lang="ts">`)+ TypeScript 严格模式 + SCSS(sass,modern-compiler)
- 图标:新增 `@lucide/vue`(`packages/ui` 的 `dependencies`,非 peer)
- 设计令牌:`@g3soft/tokens`(SCSS 编译产物 `dist/index.css`),变量前缀 `--g3-`
- 构建:`vite build`(vue external + `preserveModules` + `vite-plugin-dts` rollupTypes);`@lucide/vue` 需一并 external
- 文档:Docus/Nuxt,`@g3soft/ui` alias 到源码热更;API 由 `scripts/gen-api.mjs` + `vue-docgen-api` 生成
- 运行时零第三方浮层库:定位/焦点/滚动锁/拖拽全部自研(与 fms 一致,保持依赖最小)
## 实现思路
以「移植 fms 实现 + 套 g3 规范外壳」为策略:行为逻辑(受控/非受控、浮层定位、焦点陷阱、滚动锁、拖拽缩放、命令式挂载)直接平移 fms 已验证的做法;表现层(类名、颜色、尺寸、文案)全部改由 `useNamespace()` 与 `v()` 令牌兜底生成;API 层改为类型式 `defineProps<Props>()` + `types.ts`(JSDoc 写在字段上,这是 `vue-docgen-api` 唯一能提取到描述的写法,对象式 props 实测 description 全空)。
## 关键技术决策
### 1. z-index:分层注册表(不新增令牌)
fms 用固定档位 CSS 变量(modal 3000 / popover 3500 / message 4000,注释强调「浮层必须高于 modal,否则弹窗内下拉被盖住」);g3 已有 `_utils/z-index.ts`(base 2000 + 单调 `nextZIndex()`)。单调自增会随开关次数无限膨胀,固定档位又无法表达同档堆叠。结论:**改造 z-index.ts 为「三档 + 档内栈」**。
- 档位:`popup`(浮层)/ `modal`(Modal、Drawer)/ `feedback`(Message、Notification),相对 base 偏移 0 / 1000 / 2000,满足 popup < modal < feedback
- 档内用栈:打开时压入 `base + offset + 栈深`,关闭时把该值出栈并回收,避免无限增长
- 保持向后兼容:`nextZIndex()` 无参仍返回 popup 档;`getBaseZIndex / setBaseZIndex / resetZIndex` 签名不变
- base 来源不变:`ConfigProvider.zIndex`(默认 2000)或 `G3UI.config({ zIndex })`
- 令牌层完全不出现 z-index(层级是运行时行为,不是视觉令牌)
### 2. 令牌:只补 8 个跨组件变量
fms 的 `--fms-*` 有 60+ 个,其中绝大多数是**组件私有尺寸**(switch 宽高、tag 高度、segmented 内边距、tree 缩进、select 浮层最大高度、splitter 命中区、grid gutter…)。这类一律**不进令牌**,直接在组件 SCSS 里 `v('switch-width', 32px)` 带兜底——既能被换肤覆盖,又不污染全局命名空间。
映射结论:
- 已有可复用:`--fms-primary*` → `--g3-color-primary(-hover/-active/-on)`;`--fms-text/-secondary` → `--g3-text-1/-2`;`--fms-border` → `--g3-border-color`;`--fms-card` → `--g3-bg-container`;`--fms-disabled-*` → `--g3-bg-disabled` / `--g3-text-disabled`;`--fms-success/warning/danger` → `--g3-color-success/warning/danger`;`--fms-control-height/radius` → `--g3-control-height(-sm/-lg)` / `--g3-radius(-sm/-lg)`
- 省略(可用现有变量或直接写兜底):`--fms-background`/`--fms-surface`(页面画布,属应用层职责)、`--fms-*-text` 语义色前景(统一用 `--g3-text-inverse`)、`--fms-frame-shadow`/`--fms-shadow-right`(框架级,非组件)、`--fms-font-size`/`--fms-line-height`(沿用 g3 的 `font-size` 档位)、`--fms-control-padding-x`/`--fms-control-size`(用 `--g3-space-*` 派生)、`--fms-ease-out/-in`(保留单档 `--g3-ease`,避免膨胀)、`--fms-offset-sm/-md`(用 `space-1/space-2`)
- **新增(共 8 个,暗色同步覆盖)**:`--g3-color-primary-soft`(主色淡底:选中/hover 底)、`--g3-fill`(次级填充:hover 底、轨道底)、`--g3-fill-hover`(次级填充 hover)、`--g3-mask-bg`(遮罩)、`--g3-shadow-popup`(浮层阴影,区别于 `--g3-shadow`)、`--g3-line-height`、`--g3-duration-fast`、`--g3-duration-slow`(与已有 `--g3-duration` 组成三档动效)
### 3. 浮层基础设施(一次实现,四处复用)
把 fms 的 `utils/{position,focus,scroll,drag}.js` TS 化落到 `packages/ui/src/_utils/`,并在其之上做一个 `useFloating()` composable,供 popover / tooltip / dropdown / select / date-picker / range-picker / time-select 共用:触发器绑定、Teleport 到 `popupContainer`(默认 body)、打开即算坐标、`ResizeObserver` + 捕获阶段 scroll 监听重算、点击外部关闭、Esc 关闭、`followTriggerWidth`、`arrow`、`placement` 12 方位 + flip + shift。
### 4. 全局配置统一(避免两套)
fms 的 `config.js`(`uiConfig` / `setUIConfig`)**不移植**。g3 已有等价且更强的机制:`ConfigProvider.componentDefaults` + `G3UI.config()` / `getGlobalConfig()`。新增 `_composables/useComponentDefaults.ts` 实现「显式 prop > componentDefaults > withDefaults 默认」的回落(需要支持全局覆盖的 prop 其 default 写 `undefined`)。组件树外的命令式实例(Message / Notification / Modal.confirm)统一走 `getGlobalConfig()` 取 namespace / locale / popupContainer。
### 5. gen-api 改造(必须,否则文档被覆盖)
`gen-api.mjs` 的 key 是**目录名**,同一目录下多个 `.vue` 会互相覆盖(button-group、date、dropdown、form、grid、radio、splitter、tabs、tree 都存在子组件)。规则改为:**每个目录只解析其主组件**(优先 `index.ts` 中首個 `export { default as G3Xxx }` 指向的文件),子组件(Separator / Menu / DropdownItem / TreeNode / TabPane / Col / Panel / FormItem / FormGroup / CalendarPanel)作为内部文件不产出文档。
## 实现注意事项(防回归)
- **unwrapped 触发器两个已知陷阱(fms 已验证)**:① `<slot name="trigger"><slot /></slot>` 会产出多层 Fragment,`vnode.type === Symbol(Fragment)` 导致触发器识别失败——必须 `while (vnode.type === Fragment)` 循环解包到真实元素/组件;② `<component :is="vnode">` 会丢失 `on*` 监听与 `dirs`,必须用 `{ render: () => injectedTrigger.value }` 渲染函数包装,且返回值必须是 `cloneVNode` 原始结果(保留内部标记)再在其上追加 `dirs`,不能用对象展开。
- 滚动锁改为**引用计数**:Modal 内嵌 Select、Drawer 叠 Modal 时不能互相提前解锁;解锁一律放到退场动画 `after-leave` 之后,用 `scrollLocked` 标志位保证「after-leave」与「卸载兜底」只解锁一次,并还原焦点到触发元素。
- 遮罩点击判定保留 fms 的 `mousedown` 在面板内标记(防拖选文本误关)。
- 命令式挂载(Modal.confirm)首帧即 open,`<Transition>` 必须加 `appear`,`watch(isOpen, ..., { immediate: true })` 才能在挂载时初始化滚动锁/焦点/拖拽。
- 组件 `<style>` 一律**非 scoped**,类名/keyframes 全由 `useNamespace()` 与 SCSS `b/e/m/em/is` 派生,颜色尺寸一律 `v()` 带兜底。
- 文案不得硬编码,统一走 `useLocale()`——需扩充 `locale/index.ts`(confirm / cancel / placeholder / empty / pagination total / 日期面板 / aria-label 等键)。
- `@lucide/vue` 必须写进 `packages/ui/package.json` 的 `dependencies` 且加入 `vite.config.ts` 的 `rollupOptions.external`,否则会被打进产物。
- 仓库无测试,每批验收:`pnpm --filter @g3soft/tokens build` → `pnpm --filter @g3soft/ui build`(顺带类型检查)→ `pnpm gen:api`,并在 docs 示例页冒烟。
## 架构设计
```mermaid
graph TD
A[应用层 / docs 示例] --> B[组件层 28 个 G3* 组件]
B --> C[_composables: useNamespace / useLocale / useFloating / useComponentDefaults]
B --> D[_utils: position / focus / scroll / drag / z-index / date / global-config / color]
C --> E[config-provider: ConfigProvider + configProviderKey]
C --> F[_utils/global-config: 组件树外兜底]
B --> G[@g3soft/tokens: --g3-* 变量 + SCSS b/e/m/v]
B --> H[@lucide/vue 图标]
I[命令式: G3Modal.confirm / G3Message / G3Notification] --> F
I --> D
```
分层职责:组件层只做渲染与交互;跨组件能力(定位、焦点、滚动锁、层级、拖拽)下沉到 `_utils`;可复用响应式逻辑(命名空间、locale、浮层、默认 props)收敛到 `_composables`;主题与尺寸全部来自 tokens,运行时主题派生由 `ConfigProvider` + `derivePalette()` 负责。
## 目录结构
```
packages/tokens/src/scss/
├── _config.scss # [MODIFY] 补 $fill / $mask 等种子值(若需)
├── theme.scss # [MODIFY] 新增 8 个变量 + 暗色覆盖:color-primary-soft / fill / fill-hover / mask-bg / shadow-popup / line-height / duration-fast / duration-slow
└── _functions.scss # [MODIFY] 无改动(v() 兜底机制沿用)
packages/ui/
├── package.json # [MODIFY] dependencies 增加 @lucide/vue
├── vite.config.ts # [MODIFY] rollupOptions.external 追加 '@lucide/vue'
└── src/
├── index.ts # [MODIFY] re-export 全部新组件与类型,扩充 components 注册常量
├── env.d.ts # [MODIFY] 无实质改动(保留编译期常量声明)
├── _utils/
│ ├── z-index.ts # [MODIFY] 分层:nextZIndex(layer) / releaseZIndex(v),保留原 API 兼容
│ ├── position.ts # [NEW] TS 化 computePosition + PLACEMENTS(12 方位 flip + shift)
│ ├── focus.ts # [NEW] focusFirst / trapFocus
│ ├── scroll.ts # [NEW] lockScroll / unlockScroll(引用计数)
│ ├── drag.ts # [NEW] bindDrag / bindResize(返回 { reset, destroy })
│ ├── date.ts # [NEW] 日期解析/格式化/面板数据(原 date-utils.js TS 化,供三个日期组件共用)
│ ├── global-config.ts# [MODIFY] 维持 GlobalConfig(namespace/zIndex/locale/popupContainer)
│ └── color.ts # [MODIFY] 无改动
├── _composables/
│ ├── useFloating.ts # [NEW] 浮层通用逻辑(定位/外部点击/Esc/重算/Teleport 容器)
│ ├── useComponentDefaults.ts# [NEW] prop > componentDefaults > 内置默认 的回落
│ ├── useNamespace.ts # [MODIFY] 无实质改动
│ └── useLocale.ts # [MODIFY] 无实质改动
├── config-provider/ # [MODIFY] 维持 provide 结构,文档补 componentDefaults 用法
├── locale/index.ts # [MODIFY] 扩充 Locale 键(confirm/cancel/placeholder/empty/pagination/date/aria)
├── button/ # [MODIFY] 重写为 fms 变体体系(primary/secondary/outline/ghost/danger/danger-outline/link/icon)+ loading 光标、hover 去抖、按下缩放
├── button-group/ # [NEW] G3ButtonGroup + 内部 Separator.vue
├── space/ # [NEW] G3Space
├── grid/ # [NEW] G3Row(主组件)+ 内部 Col.vue → G3Col
├── scrollbar/ # [NEW] G3Scrollbar
├── spin/ # [NEW] G3Spin(含 indicator / description 插槽、包裹模式)
├── tag/ # [NEW] G3Tag(icon / closeIcon 插槽)
├── segmented/ # [NEW] G3Segmented
├── input/ # [NEW] G3Input(prefix/suffix 插槽、clearable、事件 update:modelValue/change/blur/focus/clear)
├── checkbox/ # [NEW] G3Checkbox(indeterminate)
├── radio/ # [NEW] G3Radio(主组件)+ 内部 RadioGroup.vue → G3RadioGroup
├── switch/ # [NEW] G3Switch
├── select/ # [NEW] G3Select(浮层复用 useFloating、搜索、footer/load-more)
├── date-picker/ # [NEW] G3DatePicker(主组件)+ 内部 CalendarPanel.vue
├── range-picker/ # [NEW] G3RangePicker
├── time-select/ # [NEW] G3TimeSelect
├── form/ # [NEW] G3Form(主组件)+ 内部 FormItem.vue / FormGroup.vue / validate.ts → G3FormItem / G3FormGroup
├── popover/ # [NEW] G3Popover(unwrapped 模式,含 Fragment 解包处理)
├── tooltip/ # [NEW] G3Tooltip(hover 延迟)
├── dropdown/ # [NEW] G3Dropdown + 内部 DropdownItem.vue / Menu.vue
├── modal/ # [NEW] G3Modal(confirm/cancel 语义)+ 内部 ModalConfirm.vue + confirm.ts(命令式)
├── drawer/ # [NEW] G3Drawer
├── message/ # [NEW] 命令式 G3Message(manager + container + store)
├── notification/ # [NEW] 命令式 G3Notification(同构)
├── tree/ # [NEW] G3Tree(主组件)+ 内部 TreeNode.vue / utils.ts
├── tabs/ # [NEW] G3Tabs(主组件)+ 内部 TabPane.vue
├── pagination/ # [NEW] G3Pagination
└── splitter/ # [NEW] G3Splitter(主组件)+ 内部 Panel.vue
scripts/
└── gen-api.mjs # [MODIFY] 每目录只解析主组件,避免子组件覆盖同名 key
docs/
├── examples/<component>/*.vue # [NEW] 每个组件 2-6 个示例(button/* 需按新变体重写)
├── content/ui/2.general/button.md # [MODIFY] 按新 API 重写
├── content/ui/<分类>/<component>.md # [NEW] 每组件一个文档页(:::demo + ApiTable)
├── content/ui/.navigation.yml # [MODIFY] 补齐分类导航
└── data/api/*.json # [AUTO] pnpm gen:api 生成,不可手写
```
## 关键代码结构
```ts
// packages/ui/src/_utils/z-index.ts —— 分层 + 档内栈,替换单调自增
export type ZIndexLayer = 'popup' | 'modal' | 'feedback'
export function nextZIndex(layer?: ZIndexLayer): number // 无参兼容旧行为(popup 档)
export function releaseZIndex(value: number): void // 关闭时回收,防止无限膨胀
export function getBaseZIndex(): number
export function setBaseZIndex(value: number): void
export function resetZIndex(): void
```
```ts
// packages/ui/src/modal/types.ts —— confirm 语义(原 okText/onOk/emit('ok') 全部重命名)
export interface ModalProps {
open?: boolean; defaultOpen?: boolean
title?: string; width?: number | string
centered?: boolean; mask?: boolean; maskClosable?: boolean; keyboard?: boolean
closable?: boolean; fullscreen?: boolean; resizable?: boolean; footer?: boolean
/** 确认按钮文字(原 okText) */
confirmText?: string
cancelText?: string
/** 确认中:按钮 loading 且阻止一切关闭 */
confirmLoading?: boolean
}
export interface ModalConfirmOptions {
title?: string; content?: string
confirmText?: string; cancelText?: string
/** 透传给确认按钮(原 okButtonProps) */
confirmButtonProps?: Partial<ButtonProps>
/** 返回 Promise 时按钮 loading:resolve 关闭、reject 保持打开(原 onOk) */
onConfirm?: () => void | Promise<unknown>
onCancel?: () => void
}
export interface ModalConfirmHandle { destroy: () => void }
// emits: 'update:open' | 'confirm' | 'cancel' | 'closed'
// G3Modal.confirm(options: ModalConfirmOptions): ModalConfirmHandle
```
```ts
// packages/ui/src/_composables/useFloating.ts —— 浮层通用能力
export interface UseFloatingOptions {
placement?: Placement // 12 方位
offset?: number
arrow?: boolean
followTriggerWidth?: boolean
popupContainer?: PopupContainer
}
export interface UseFloatingReturn {
triggerRef: Ref<HTMLElement | null>
floatingRef: Ref<HTMLElement | null>
floatingStyle: ComputedRef<CSSProperties>
resolvedPlacement: Readonly<Ref<Placement>>
update: () => void
}
```
## 设计风格
延续 g3soft 现有基调:中性克制的 shadcn 风格——主色由单一 primary 派生(hover/active 走主色透明度),层次靠"面"不靠"线",1px 细边框只用于描边类控件。组件视觉以 fms-vue 的成熟交互为准:按钮 8 档变体(primary/secondary/outline/ghost/danger/danger-outline/link/icon),按下 0.97 缩放、hover 去抖(离开加 60ms 延迟防边缘闪烁)、加载态保留原配色并用 wait 光标、禁用态整体降透明度。
## 交互与动效
三档时长(fast/base/slow)+ 单档缓动:跟手操作用 fast,下拉/提示用 base,Modal/Drawer 用 slow;只允许过渡 opacity / transform / color 系,禁止过渡 width/height/top/left。浮层入场错峰、退场不错峰;`prefers-reduced-motion` 下时长归零。
## 布局与响应式
控件三档尺寸(sm 24 / 默认 32 / lg 40),间距走 space-1..6(4px 阶梯),圆角 sm/radius/lg(4/6/8)。浮层(popover/tooltip/dropdown/select/date)统一 12 方位定位 + flip + shift + 8px 视口边距,保证弹窗内下拉可见。
## Agent Extensions
### Skill
- **ui-ux-pro-max**
- Purpose: 为 28 个组件确定统一的视觉规范(变体配色、间距节奏、浮层阴影层级、动效档位)与可访问性要求
- Expected outcome: 产出一份与 `--g3-*` 令牌一致的组件级视觉基线,供各批实现直接套用,避免分批实现出现风格漂移
- **antdv-next**
- Purpose: 校对 Modal.confirm / Form / Select / Tree / Pagination 等组件的 confirm 语义 API 命名与行为(onConfirm 的 Promise loading、静默关闭策略)
- Expected outcome: confirm 风格 API 与主流库心智一致,命令式 `G3Modal.confirm({ onConfirm })` 行为定义无歧义
### SubAgent
- **code-explorer**
- Purpose: 分批深挖 `D:/workspace/code/fms/fms-vue/src/components/ui` 各组件源码(props/emits/slots 全量、定位与焦点实现细节)
- Expected outcome: 每批实现前拿到该批组件的完整原始 API 与实现要点,避免凭猜测造 API