22 KiB
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`。 |
|
|
产品概述
将 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-dtsrollupTypes);@lucide/vue需一并 external - 文档:Docus/Nuxt,
@g3soft/uialias 到源码热更;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()与 SCSSb/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