This commit is contained in:
oneao committed 2026-10-08 22:38:34 +08:00
1 parent f572dce2f6
commit e99a9fb274
356 files changed
+28877 -1055

No files matched your search

@@ -0,0 +1,334 @@
---
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