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

22 KiB
Raw Blame History

name, overview, design, todos
name overview design todos
g3-ui-重写-参照fms-vue 以 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`。
styleKeywords fontSystem colorSystem
中性克制
shadcn 风格主色派生
层次靠面不靠线
轻描边
微动效反馈
三层 z 轴秩序
fontFamily heading subheading body
PingFang SC
size weight
24px 600
size weight
16px 500
size weight
14px 400
primary background text functional
#18181B
#E5E5E5
#FFFFFF
#141414
#000000
#FFFFFF
#45B114
#E79A0D
#EC4246
id content status
infra-setup 扩展 tokens 8 个变量、TS 化 _utils、改造 z-index 分层、加 @lucide/vue 依赖与 external completed
id content status dependencies
genapi-and-config 改造 gen-api 只解析目录主组件,扩充 locale 并实现 useComponentDefaults completed
infra-setup
id content status dependencies
base-components 用 [skill:ui-ux-pro-max] 重写 Button 变体体系,实现 button-group/space/grid/tag/segmented/scrollbar/spin completed
genapi-and-config
id content status dependencies
overlay-popover 用 [subagent:code-explorer] 移植浮层工具,实现 useFloating 与 popover/tooltip/dropdown completed
base-components
id content status dependencies
form-components 移植 input/checkbox/radio/switch/select 与 form 校验体系 completed
overlay-popover
id content status dependencies
modal-drawer-feedback 参照 [skill:antdv-next] 实现 modal(confirm 语义+命令式)、drawer、message/notification completed
overlay-popover
id content status dependencies
data-display 实现 tree/tabs/pagination/splitter 与 date-picker/range-picker/time-select completed
overlay-popover
id content status dependencies
docs-and-verify 补齐示例与文档页,跑 gen:api、tokens/ui 全量构建冒烟 completed
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 示例页冒烟。

架构设计

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 生成,不可手写

关键代码结构

// 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
// 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
// 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