Files
workspace/code/fms/.codebuddy/plans/fms-vue-tabs_bc485ecf.md
2026-08-16 22:01:32 +08:00

8.7 KiB
Raw Permalink Blame History

name, overview, design, todos
name overview design todos
fms-vue-tabs 在 fms-vue 组件库中新增 Tabs 标签页组件:line 线型 + top/left/right 三种标签方向,同时支持 items 数组与 TabPane 插槽双数据源,含键盘方向键导航、懒渲染缓存、禁用态与完整 ARIA,并在演示站 App.vue 中新增展示。
architecture styleKeywords fontSystem colorSystem
framework
vue
克制简洁
清晰分层
主色指示条
fontFamily heading subheading body
PingFang SC
size weight
14px 600
size weight
14px 500
size weight
14px 400
primary background text functional
#0f172b
#ffffff
#f5f6f8
#1f2329
#86909c
#c9cdd4
#e5e6eb
id content status
create-tab-pane-and-normalize 参考 [skill:antdv-next] 核对 API 语义,创建 tab-pane.vue 并实现 tabs.vue 双数据源归一与激活管理 completed
id content status dependencies
implement-tabs-interaction 在 tabs.vue 完成模板:tablist/方向键导航/懒渲染缓存/ARIA 关联 completed
create-tab-pane-and-normalize
id content status dependencies
write-tabs-styles 用 [skill:ui-ux-pro-max] 校验后编写 index.scss:top/left/right 布局、指示条与各状态样式 completed
create-tab-pane-and-normalize
id content status dependencies
add-demo-and-check App.vue 新增 Tabs 导航项与演示 section,做静态检查并汇报 completed
implement-tabs-interaction
write-tabs-styles

产品概述

为 fms-vue 自研 UI 组件库新增 Tabs 标签页组件,遵循 fms-vue/开发规范.md 约束并借鉴 参考组件库/antdv-next 的 Tabs 设计语义。包含 Tabs(根)与 TabPane(子声明)两个组件,用于在同一区域内分组展示内容、切换视图。

核心功能

  • 双数据源:支持 items 数组([{ key, label, disabled?, content? }])与 TabPane 插槽两种声明方式,与 Select 的 options/插槽模式一致,二选一使用。
  • line 线型指示:激活标签底部(top)或侧边(left/right)显示主色指示条,标签栏有浅色分隔线。
  • 侧边/垂直方向:placement 支持 top(默认,横排在上)、left、right(纵向排列在左/右,内容区自适应剩余宽度)。
  • 禁用态:支持单个 TabPane 禁用与 Tabs 整组禁用,反映到原生属性与视觉(降透明 + not-allowed)。
  • 键盘与无障碍:tablist/tab/tabpanel 语义,方向键循环切换激活(top 用左右键,left/right 用上下键),roving tabindex、aria-selected/aria-controls/aria-labelledby,:focus-visible 焦点样式。
  • 懒渲染 + 缓存:非激活面板不渲染,首次激活后缓存(切换不销毁),符合性能优先规范。
  • 自定义标签:#label 插槽支持自定义标题内容(TabPane 与 items 两种模式均可用)。

技术栈

  • Vue 3.5 <script setup> + Sass(项目现有技术栈,不引入新依赖)
  • 样式复用 src/theme/tokens.css 的 --fms-* token,图标按需从 @lucide/vue 导入(本组件无需图标)
  • 参照 radio-group/radio 的协作组件写法与 select 的"数组 + 插槽"双数据源模式

实现思路

Tabs 根组件通过 slots.default() 遍历插槽 vnode(展平 Fragment、过滤注释),识别 type.name === 'TabPane' 的子组件并提取其 props 与子插槽(default 内容、label 自定义标题)——此机制参考 antdv-next 的 useLegacyItems。归一化后的 panes 统一管理激活态、渲染与键盘导航;items 数组模式 normalize 为同构数据结构,两条路径输出一致的渲染结果。

关键技术决策

  • vnode 解析而非 provide/inject:TabPane 是纯数据声明组件(自身渲染空),激活状态必须由根组件统一管理,根组件负责渲染 tab 头列表与内容区;这与 antd 的 TabPane 语义一致,优于 Radio 的 provide/inject 协作模式(radio 每个子项自己渲染视觉)。
  • 指示条不加 JS 测量:line 模式指示条用激活 tab 自身的 border-bottom/border-left(2px 主色)实现,标签栏底部/侧边用 1px --fms-border。不做滑动跟随动画,避免布局读取与高频重排,符合"过渡限低成本属性、不主动构建"的规范。
  • 懒渲染 + 缓存:renderedKeys Set 记录已激活 pane,模板 v-if(首次激活才渲染)+ v-show(激活显示)组合,首屏只渲染激活项且切换不重建内容。
  • 激活回退:modelValue 未传或指向不存在/禁用的 pane 时,回退到第一个可用 pane,且不写回 modelValue(保持受控性)。

实现注意

  • 键盘仅遍历 enabled panes 循环移动,禁用项跳过;方向键按 placement 映射(top: Left/Right,left/right: Up/Down,Home/End 定位首尾)。
  • 切换时先更新内部激活态再 emit update:modelValue 与 change;点击触发 tabClick(key, event)。
  • id 用 useId() 生成,tab 与 tabpanel 通过 aria-controls/aria-labelledby 关联。
  • 卸载无需清理(无全局监听器、无定时器)。

架构设计

Tabs(根组件)
 ├─ 数据源归一(items 数组 / TabPane 插槽 vnode 解析)→ mergedPanes
 ├─ 激活管理(modelValue 回退、select、emit)
 ├─ 渲染:nav 列表(role=tablist)+ 内容区(role=tabpanel,v-if/v-show 懒渲染)
 ├─ 键盘导航(方向键循环 + roving tabindex)
 └─ 布局:placement top(纵向堆叠)/ left|right(flex 行)

目录结构

fms-vue/src/components/ui/tabs/
├── tabs.vue        # [NEW] Tabs 根组件。props: modelValue/placement/disabled/items;
│                   # emits: update:modelValue/change/tabClick;实现 vnode 解析、激活管理、
│                   # 懒渲染缓存、键盘导航与 ARIA 完整模板。
├── tab-pane.vue    # [NEW] TabPane 数据声明组件。props: tabKey/label/disabled;
│                   # 插槽: default(面板内容)/ label(自定义标题);
│                   # defineOptions({ name: 'TabPane' }) 供根组件识别;自身渲染空节点。
└── index.scss      # [NEW] 样式:top/left/right 三种布局、line 指示条、hover/disabled/focus 态、
                    # prefers-reduced-motion;全部复用 --fms-* token,tabs.vue 内 @use 引入。

fms-vue/src/App.vue # [MODIFY] 通用分组新增导航项 { key: 'tabs', label: 'Tabs 标签页' },
                    # 新增演示 section(基础用法/items 数组/侧边方向/禁用/自定义标签/懒渲染)。

关键数据结构(伪类型)

// 归一化后的 pane(items 数组与 TabPane 插槽统一输出)
{ key: String, label: String|VNode, disabled: Boolean,
  content: () => VNode[],  // 内容渲染函数(懒渲染时按需调用)
  labelVNode: () => VNode[] // 自定义标题渲染函数,无则 undefined
}

设计风格

与现有组件库一致的克制现代风格:干净的分层、克制的交互反馈、清晰的状态表达。

视觉要点

  • 布局:top 模式标签横排在上、内容在下,标签栏底部 1px --fms-border 分隔;left/right 模式为 flex 行,标签纵向排列于左/右,内容区 flex: 1 1 auto 自适应。垂直方向标签栏加 --fms-border 侧边分隔,激活标签文字主色并配 --fms-secondary-hover 浅背景增强选中识别。
  • 指示条(line):top 模式激活标签自身 2px 主色下边框,left/right 为 2px 主色侧边框,无滑动动画,稳定且零布局读取。
  • 状态反馈:hover 文字变主色;禁用用 --fms-disabled-text + not-allowed 光标并降透明;选中不单靠颜色,同时用指示条与文字粗细双重表达。
  • 间距:标签 padding 约 0 16px、高度对齐 --fms-control-height(32px),标签间距紧凑;内容区 padding 16px 0 与标签栏对齐。
  • 焦点::focus-visible 主色 outline 2px + 2px offset。
  • 动画:仅 hover 文字颜色过渡(低成本属性),prefers-reduced-motion 下关闭。

Agent 扩展

Skill

  • antdv-next
  • 用途:核对 antdv-next Tabs/TabPane 的 API 语义(tabKey 解析顺序、键盘交互、ARIA 细节),确保借鉴准确。
  • 预期产出:tabs.vue 的 props/事件/键盘映射与参考库一致,无遗漏交互。
  • ui-ux-pro-max
  • 用途:辅助校验 Tabs 视觉细节(间距、指示条、禁用/焦点状态表达),与组件库整体风格对齐。
  • 预期产出:index.scss 的视觉规格(间距、指示条、状态色)符合现代组件库实践。