aiDoc/frontend-backend/component-library.md
本文件是项目「基础 UI 组件库」的唯一规范来源。任何 AI 在新增、修改或使用这套组件时, 都必须遵守这里的约定,确保组件统一接入主题系统、可换肤、暗色可用,并与构建管线保持一致。
本组件库全局可用(g- 前缀,kebab-case),但「是否必须用」按场景区分,不要在业务页面里一刀切地强推:
一句话:配置 / 外壳层强制 reka-ui,业务页面尊重习惯、不强制。
unocss-preset-shadcn
默认 presetWind4 与本项目错配、维护停滞,风险高。shadcn-vue 源码仅作为「设计蓝本」参考。themeStore 换肤与暗色模式。web/src/core/componentLibrary/web/src/core/componentLibrary/index.jsweb/src/core/global.js 的 registerComponentLibrary()reka-ui、class-variance-authority(cva)、clsx、tailwind-merge目录结构(一个组件一个目录,Xxx.vue 放实现、index.js 放出口 / cva 变体):
core/componentLibrary/
├── index.js # 总出口:re-export 各组件 + cn
├── utils.js # cn():clsx + tailwind-merge 合并 class
├── button/ # Button + buttonVariants(cva)
├── dropdown-menu/ # DropdownMenu(:items 便捷模式,trigger=click|hover) + Content/Item 部件
├── select/ # Select(:options 便捷模式) + Trigger/Content/Item 部件
├── switch/ # Switch
├── slider/ # Slider(单值 number 对外,内部包数组,支持 marks)
├── number-field/ # NumberField(数字步进输入)
├── color-picker/ # ColorPicker(Popover 内组合 reka 颜色原语,支持 alpha)
├── page-tab/ # PageTab(页签,button/chrome/slider 三种模式子组件私有)
└── menu/ # Menu(导航菜单,MenuItem/MenuFlyout/HorizontalMenu 等部件私有)
core/global.js 会把 barrel 导出的每个组件以 **g- 前缀(kebab-case)**注册为全局组件,
命名与项目里 el-button 等用法统一,全站直接用、无需 import:
| 组件 | 全局标签 |
|---|---|
| Button | <g-button /> |
| DropdownMenu | <g-dropdown-menu /> |
| Select | <g-select /> |
| Switch | <g-switch /> |
| Slider | <g-slider /> |
| NumberField | <g-number-field /> |
| ColorPicker | <g-color-picker /> |
| PageTab | <g-page-tab /> |
| Menu | <g-menu /> |
注册是自动遍历 barrel 导出实现的(g- + 导出名转 kebab-case),新增组件只要从 index.js
导出即自动获得全局标签,无需再改 global.js。三类非组件导出不会获得全局标签:
cn / buttonVariants 这类函数导出由 typeof 判断跳过;BUTTON_VARIANTS / MENU_THEMES /
PAGE_TAB_MODES 这类枚举数组导出由 Array.isArray 统一跳过;其余「非本库自有组件对象」
(如 reka-ui 的 SelectValue,仅供 granular 模式按需 import)需登记进 global.js 的
NON_GLOBAL_EXPORTS 名单显式排除——新增此类 re-export 时同步登记。
命名三层关系(刻意分层,勿混用):组件内
defineOptions({ name })= devtools 显示名 (六个基础控件为UiXxx;Menu 系为Gva*、PageTab 系为PageTab*)/ barrel 导出Xxx(PascalCase)= 显式 import 名 / 全局标签g-xxx(kebab-case)= 模板里用。
需要按需引入、或使用 Select 的 granular 部件(SelectTrigger / SelectContent / SelectItem)时:
import { Button, Select } from '@/core/componentLibrary'
// 或细到单组件目录
import { Button } from '@/core/componentLibrary/button'
web/src/theme/vars.js:
primary / info / success / warning / error(含 -50~-950 阶梯)container(卡片/浮层底)、layout(布局底)、inverted、base-text(主文本)、border、
muted(弱底)、muted-foreground(弱文本)、control-track(控件未激活轨道:开关关闭态 / 滑块未填充)bg-gray-300 dark:bg-gray-600 这类裸色阶 + 手写 dark: 变体上色,
暗色应交给语义 token 在 CSS 变量层自适应(单个 bg-control-track 即可,无需再写 dark:)。shadow-header / shadow-sider / shadow-tab / shadow-card:style="{ backgroundColor: settings.themeColor }",
颜色一律走 token(bg-primary 自动跟随换肤)。bg-primary/10、text-base-text/60 这类
透明度后缀(CSS 变量 + alpha 在亮/暗下不可靠)。需要弱化时改用语义 token,
如 hover:bg-muted、text-muted-foreground。cn() 合并(clsx 处理条件类 + tailwind-merge 消解冲突原子类),
并把对外可覆盖的 class prop 放在最后参与合并。index.js(如 buttonVariants),variant/size 各成一档。z-[3000]:组件常被放进 el-drawer/对话框里,下拉、Popover 的 Content 需
z-[3000] 才能盖过 Element Plus 浮层。componentLibrary/utils.js,全组件引用、禁止各处手写导致漂移:
FOCUS_RING(focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-300)。FOCUS_RING_WITHIN(focus-within:ring-2 focus-within:ring-primary-300)。<button> 基类需带 appearance-none bg-transparent,再由各 variant 显式底色覆盖。v-model 用 defineModel():组件双向绑定一律走 defineModel,不写 modelValue prop +
defineEmits(['update:modelValue']) 老样板;需变换 / 防抖 / 类型还原时也基于 defineModel 的 ref
(可写 computed 桥接,或在回调里 modelValue.value = ...)。详见 frontend-rules.md「组件写法规范」。validator:如 variant / size / format,可选值集中到组件 index.js 导出
(如 BUTTON_VARIANTS),供 cva 与 prop validator 复用做单一事实源。<svg-icon>:组件内图标用 <svg-icon icon="lucide:xxx" />(在线 Iconify,优先 lucide 集),
不手写裸 <svg><path/></svg>、不用 el-icon / ep: 图标集。详见 frontend-rules.md「图标规范」。.js/.ts(cva 变体) 时,UnoCSS 默认扫描管线不扫 .js/.ts,
这些原子类不会被生成 → 组件丢色。web/uno.config.js 的 content.pipeline.include 加入规则覆盖本目录:
/[\\/]core[\\/]componentLibrary[\\/].*\.[jt]s($|\?)/。core/componentLibrary/ 下才会被扫描;
若把含类名的 .js/.ts 放到别处,记得同步扩展该 include 规则。uno.config.js 需重启 dev server 才会重扫旧的 .js。el-drawer / el-upload / ElMessage(Box) 等);图标走全局 <svg-icon>,不用 el-icon。z-[3000] 盖过 EP 浮层即可。variant = default | destructive | outline | outline-primary | outline-success | secondary | ghost;
size = default | sm | lg | icon;outline-primary / outline-success 是带主色 / 成功色描边的次级按钮
(hover 填充实色、文字转白),调用方用 variant 表达颜色、不要手写 border-/text- 覆盖;
支持 as / asChild(reka Primitive 透传);loading 异步提交时转圈并禁用点击,disabled 经原生属性禁用。:options 模式(本地算当前文案,规避 reka SelectValue 首屏回填时机问题);
option.value 支持 string | number | boolean,回写保留原值类型(内部用 String(value) 映射桥接 reka);
便捷模式仅必填单选,需清空 / 多选时用 granular 部件(g-select-trigger / g-select-content / g-select-item)。DropdownMenu 底座,anatomy 对齐官方文档);默认插槽放触发器
(asChild 合并行为),便捷模式传 :items({ label, value?, danger?, disabled? }),选中把整个 item 从
select 事件抛出;trigger = click | hover(可选值集中导出为 DROPDOWN_MENU_TRIGGERS)。
data-[highlighted]:bg-primary,鼠标悬停与键盘导航同态),
danger 项红色文本、高亮红色实底;面板自带指向触发器的箭头(DropdownMenuArrow,fill-container
随换肤 / 暗色自适应),:arrow="false" 可关;进出场按官方推荐用
--reka-dropdown-menu-content-transform-origin 做缩放淡入淡出(keyframes popper-in/out,transition.scss)。pointer-events:none,触发器收不到指针事件导致开关闪烁死循环)、
移出后延迟 120ms 收起、关闭时阻止 closeAutoFocus 回焦触发器(避免非键盘操作留下 focus ring)。#content 插槽 + granular 部件
(g-dropdown-menu-content / g-dropdown-menu-item / g-dropdown-menu-label / g-dropdown-menu-separator)。control-track、开=primary;纯图形控件,调用方按语义传 aria-label。number(内部包成数组),支持 marks;未填充轨道走 control-track,可传 aria-label。+/- 按 step 增减,手输的值也会吸附到 step 的倍数。ColorArea/ColorSlider/ColorField/ColorSwatchPicker;
alpha 开透明度通道,format=hex|rgb,swatches 传预设色卡;
纯图形触发器可传 title(hover 提示,兼作可访问名兜底)/ ariaLabel;
对外写回防抖 100ms,卸载时 flush 补发最终值。mode = button | chrome | slider(可选值集中导出为 PAGE_TAB_MODES);
原生事件经 attribute fallthrough 透传到根元素,关闭走显式 close 事件;
ButtonTab / ChromeTab / SliderTab 三个模式子组件保持私有、不从 barrel 导出。theme = design | light | group(可选值集中导出为 MENU_THEMES),
orientation = vertical | horizontal,支持 collapsed / v-model:open-keys,选中走 select 事件;
MenuItem / MenuFlyout / HorizontalMenu 等部件保持私有,仅 Menu 获得全局标签。core/componentLibrary/<name>/ 下建 <Name>.vue + index.js,并从总 index.js 导出。index.js 的 cva;class 用 cn() 合并、class prop 可覆盖;
v-model 用 defineModel()(不写 modelValue+emit 老样板);纯图形控件提供 ariaLabel。.js/.ts,确认落在已被 uno.config.js include 覆盖的目录内。z-[3000] 不被遮挡。g-<name>(kebab-case)全局标签,无需改 global.js。