zcw-vue-ui
v1.33.3
Published
本组件库按照前缀进行分组,包含四种类型的组件:
Readme
组件规范,必须绝对遵守此规则。
组件库概述
本组件库按照前缀进行分组,包含四种类型的组件:
- 重型第三方整合组件 (Tp 前缀):以
Tp开头,基于 ECharts、Monaco、Three.js 等重型第三方库进行深度封装与扩展 - 自研组件 (Cw 前缀):以
Cw开头的自主研发组件,实现通用业务需求和自定义功能,具有较高的复用性 - 业务特定组件 (Bs 前缀):以
Bs开头的业务特定组件,面向特定业务场景,复用性较低,通常只在特定页面或业务中使用 - 页面级业务组件 (Page 前缀):以
Page开头,面向具体业务场景的页面级组件,可能包含多个基础组件的组合与业务逻辑
基本规范
重要说明:除非特别注明,所有针对
Cw自研组件的规范同样适用于Tp重型第三方整合组件、Bs业务特定组件和Page页面级业务组件,包括但不限于:
- Tailwind CSS 样式规范(只能使用 Tailwind CSS,禁止自定义 CSS)
- Design Tokens 色值使用规范(必须使用 tokens 变量,禁止 hardcode 色值;
pnpm run check:tokens强制检查Cw*)- 样式和文本国际化规范(通过 styles 和 texts props 传入)
- Focus 和键盘支持规范(可交互组件必须支持聚焦和键盘操作)
- 纯函数抽离规范(复杂逻辑必须抽离到 functions 目录)
- 组件导出格式规范(index.ts 导出格式)
- 低代码物料 materialMeta.ts 规范(Cw/Tp/Page 参与 Admin 物料库扫描时强制)
- Stories 文件格式规范(Storybook 分类格式)
- 每个组件都需要stories
- 需要自定义滚动条的容器必须使用自研组件
CwDiv,与原生div用法一致,可直接替换;保持滚动体验与 macOS Chrome 一致 - 所有组件都放在src/components目录下
- 一个组件都单独生成一个文件夹,包含一个
index.ts、一个types.ts、一个stories文件、组件文件,以及styles.ts和texts.ts文件(用于样式和文本国际化);参与低代码物料库扫描的 Cw/Tp/Page 组件还须包含materialMeta.ts(见 3.3 低代码物料 materialMeta.ts) - 自研组件(Cw/Tp/Page 开头)必须有自定义逻辑和类型定义
- 所有需要双向绑定的组件在 stories 中必须使用 v-model 而不是直接设置 modelValue 属性,需要在 render 函数中通过 ref 创建响应式变量并使用 v-model 绑定
- 所有可交互的自研组件必须支持聚焦和键盘操作(强制)
- 图标使用规范(强制):所有图标必须使用
CwIcon组件,禁止直接内嵌 SVG 代码、SVG 组件或其他第三方图标组件
rem / token 尺寸规范(Cw 自研组件 + 消费方,强制)
vue-ui 组件与 Chat 业务层(packages/chat.zengchaowu.com/src)的 UI 尺寸须随 根字号 同比缩放,禁止把视觉尺寸写死在 px。Chat 视口与移动端交互区分见 packages/chat.zengchaowu.com/README.md。
设计稿 rem 换算
- rem 数值按 16px 设计稿定义(如 14px →
0.875rem/text-sm,20px 图标 →size: 1.25) - 工具函数:
designPxToRem(n)、resolveDesignCssSize()(src/utils/cwRemSize.ts) - 实际渲染像素 =
rem × 当前根字号;Chat 独立打开时根字号会变,故必须用 rem/token,勿写 px
Chat 根字号(仅独立打开)
| 视口 | 根字号 | 配置位置 |
|------|--------|----------|
| 宽屏 ≥768px | 6px | chat-viewport.css → --chat-root-font-size 默认值 |
| 窄屏 ≤767px | 30px | 同上,@media (max-width: 767px) |
- 仅 独立 Chat 在
html.chat-root-viewport-lock上生效;嵌入 qiankun 不改宿主 html - 唯一允许写根字号 px 的文件:
packages/chat.zengchaowu.com/src/styles/chat-viewport.css - 断点 / inject 键:
constants/chatLayoutViewport.ts(CHAT_LAYOUT_BELOW_MD_MEDIA等);勿在 TS 再重复定义 6/30
字号(Tailwind / styles.ts)
| 设计稿 | 用法 |
|--------|------|
| 12px | text-xs |
| 14px | text-sm |
| 16px | text-base |
| 10、12、14、16、18px 等 | [font-size:var(--font-size-*)](见 tokens.css) |
// ❌ Tailwind v3 会把 [font-size:var(--font-size-*)] 编译成 color,字号不生效
'... [font-size:var(--font-size-caption)] ...'
// ✅
'... text-sm ...'
'... [font-size:var(--font-size-caption)] ...' // 12px @16禁止 text-[Npx]。新增语义字号先在 tokens.css 增加 --font-size-*。
图标 / 头像(CwIcon、CwAvatar、CwFileIcon)
<!-- ❌ 把设计稿 px 当 rem 直填(20px 应写 1.25,不是 20) -->
:styles="{ size: 20 }"
<!-- ✅ rem(设计稿 20px → 1.25) -->
<CwFileIcon :size="2" />
:styles="{ size: 1.25 }"
:styles="{ size: 0 }" <!-- 尺寸交给 em / Tailwind -->size:边长(rem,相对当前根字号);size: 0表示不注入 inline 宽高- 换算:设计稿 px ÷ 16(
designPxToRemNum(n)/designPxToRem(n)) - 勿把设计稿 px 整数当 rem 传入(迁移后常见 bug):
- ❌
mineAvatarSize: 32、:size="16"、avatarStyles(32)→ 窄屏会变成 32rem - ✅ 32px →
2或CW_CHAT_SHELL_MINE_AVATAR_SIZE;16px →1或CW_FILE_ICON_SIZE_COMPOSER_PENDING
- ❌
- 非 UI 语义字段(如
UploadFile.size字节数、并查集element.size、表单size="small")不受此限
布局尺寸
- 优先 Tailwind rem 刻度:
h-7、gap-2、max-w-sm - 语义尺寸:
min-h-[var(--size-touch-target)]等--size-*token - 无 token 时用 rem 字面量:
w-[6.75rem],勿w-[108px]
允许保留 px
- DOM 实测:
getBoundingClientRect、选区/拖拽、弹层定位、裁剪框坐标 - Safe area:仅用
var(--app-safe-area-*)/var(--app-chrome-safe-*);bootstrapAppSafeArea()+html[data-app-chrome] - Storybook(
*.stories.*)、TpHeroScene chat-viewport.css根字号与@media断点--radius-*:圆角 token 一般为 px,不随字号缩放
校验
cd packages/vue-ui && pnpm run check:no-px # prebuild 会跑
cd packages/chat.zengchaowu.com && pnpm run check:no-px # type-check 会跑层叠与 z-index 规范(Cw 自研组件,强制)
目标:默认不写 z-index,视觉上下关系尽量交给 DOM 结构(同一父级下,后出现的兄弟节点后绘制,自然盖在先出现的节点之上)。
语义 token(tokens.css)
禁止在组件内写裸数字、z-10、z-[100]、内联 zIndex: 120 / Z_INDEX_TOKEN.* 等;必须使用下表语义 token(Tailwind:z-[var(--z-index-*)]):
| Token | 值 | 用途 |
| --- | --- | --- |
| --z-index-base | 0 | 同层垫底(空状态、选中 scrim 底) |
| --z-index-above | 1 | 局部浮于兄弟(NavBar 槽位、StickyTabs、行内关闭钮) |
| --z-index-above-high | 2 | 高于 above(如标题栏控件区) |
| --z-index-sticky | 10 | 吸顶表头、行内 loading 遮罩、Chrome 标签激活态 |
| --z-index-float | 20 | Split 分隔条、多选勾选、表格固定列 body |
| --z-index-fixed-column-header | 30 | 表格固定列表头 |
| --z-index-fixed-column-shadow | 40 | 表格固定列阴影 pseudo |
| --z-index-scrollbar | 45 | 滚动视口内自绘滚动条(CwDiv 等),高于表格固定列栈 |
| --z-index-scene-overlay | 50 | 全屏场景层(Tp 可视化,同尺度) |
| --z-index-modal | 100 | Dialog / Sheet / Popup / ContextMenu / 全屏预览(modal 档;同档顺序靠 DOM) |
| --z-index-toast | 200 | Toast / Hud(全局反馈层,高于 modal 档) |
CI:pnpm run check:tokens 会扫描 Cw* 组件中的违规写法。
浮层栈(Overlay Stack)
实现见 src/constants/cwOverlayStack.ts、src/constants/overlayBodyScrollLock.ts。
浮层有两套独立能力,不要混为一谈:
| 能力 | 作用 | 典型 API |
| --- | --- | --- |
| 层叠(视觉顺序) | 谁盖在谁上面 | modal 档:useAppendToBody({ overlayStack: true })、scheduleBringOverlayHostToFront;Toast/Hud 固定 toast 档 z-index |
| Dismiss 栈 | click-outside / Escape 一次只关栈顶一层 | useCwOverlayLayer(Dialog/Sheet 内)、bindPopupOverlayLayer(Popup 内) |
只做层叠、不进 Dismiss 栈的浮层,其 click 会「穿透」到下层已注册的 Dialog/Sheet。凡参与 click-outside/Escape 关层的浮层,必须走三壳之一;业务勿直接散写 useCwOverlayLayer。
三壳 + variant(唯一入口)
| 壳 | variant / 用途 | Dismiss |
| --- | --- | --- |
| CwDynamicDialog | modal(默认)/ fullscreen(图库预览)/ toast / hud | modal、fullscreen:是;toast、hud:否 |
| CwDynamicSheet | 底滑/侧滑 Sheet | 是 |
| CwPopup | Select/Dropdown/Tooltip/ContextMenu 等锚点浮层 | 是(bindPopupOverlayLayer) |
- 声明式单层:
CwDynamicDialogLayer(预览、Toast、Hud) - 命令式:
showDynamicDialog/showDynamicSheet/createCwPopup - ContextMenu =
CwPopup+virtualReference+dismissGraceMs+closeOnScroll(无箭头) - 全屏预览 =
CwDynamicDialogLayer+variant: 'fullscreen' - Toast / Hud =
CwDynamicDialogLayer+variant: 'toast' | 'hud'
底层机制(仅三壳内部使用)
useCwOverlayLayer:Dialog/Sheet/Layer 内 panel + trapsbindPopupOverlayLayer:Popup 内自动注册useAppendToBody({ overlayStack: true }):层叠置顶
谁需要注册 Dismiss?
| 类型 | 是否注册 | 接入方式 |
| --- | --- | --- |
| Dialog / Sheet | 是 | CwDynamicDialogInner / CwDynamicSheetInner 内 useCwOverlayLayer |
| 全屏预览 | 是 | CwDynamicDialogLayer + variant: 'fullscreen' |
| Popup 系(含 ContextMenu) | 是 | createCwPopup → bindPopupOverlayLayer |
| Toast / Hud | 否(maskClosable 时 Hud 临时进栈) | CwDynamicDialogLayer + dismissStack: false;Hud maskClosable: true 时由 Dismiss 栈关层 |
Popup / ContextMenu 挂载
- modal 档浮层一律挂
document.body(createCwPopup/showCwContextMenu/ Dialog ephemeral mount)。 - 同档层叠靠 DOM 顺序 +
scheduleBringOverlayHostToFront;Dismiss 走全局栈。
Dismiss 规则摘要
- 层根:
data-cw-overlay-layer+data-cw-overlay-layer-id;可见时data-cw-overlay-active。 - 栈顶 = 文档序最后一个
[data-cw-overlay-layer][data-cw-overlay-active](嵌套时跳过仍被子层占用的祖先)。 - 边界 =
panelDOM +traps(如 Popup 的 trigger/bridge)。 - 禁止在 backdrop 上单独
@click关层(与栈双关);禁止手写contains(target)函数式 hit test。 - click-outside 在 capture 阶段 判定(Dialog/Sheet 内容区
@click.stop会挡住 bubble);同一轮 click 仍会继续传到 panel 内菜单项等目标。
命令式浮层(Toast / Hud / 图片预览 / Dialog)
showCwToast、showHud、showCwImageGalleryPreview、showDynamicDialog 均为 按需 mount:打开时创建 DOM 与 Vue 实例,关闭动画结束后 unmount 并移除容器。每次 visible === true 时须 scheduleBringOverlayHostToFront,保证与同档 modal 层叠顺序正确。
反模式
❌ 仅
overlayStack: true的全屏层,却不进 Dismiss 栈(多层弹窗必踩坑)❌ 业务 SFC 里直接
useCwOverlayLayer或自写 append/dismiss❌ 为嵌套 Select 单独提高 z-index
modal 档:
CW_OVERLAY_MODAL_Z_CLASS(z-[var(--z-index-modal)]),同档内顺序由 DOM 末尾决定toast 档:
CW_OVERLAY_TOAST_Z_CLASS(z-[var(--z-index-toast)]),仅 Toast / Hud 使用,始终盖在 modal 档之上
何时允许使用 z-index
仅当组件(或子树根)脱离常规文档流、且必须压过页面其它区域时,才在该浮层根节点使用语义 z-index token,例如:
- modal 档(
--z-index-modal):CwPopup、CwDynamicDialog(modal/fullscreen)、CwDynamicSheet、CwContextMenu、CwImageGalleryPreview - toast 档(
--z-index-toast):CwToast、CwHud(须始终可见于任意 Dialog/Sheet 之上) - 固定在视口的辅助浮层:如
CwMessageInput的@提及面板等
浮层内部(遮罩、标题、正文、按钮区等)禁止再为「谁在上」而叠加 z-index:用 DOM 顺序;装饰层用 pointer-events-none。
窄幅例外(非全屏弹层)
在滚动视口内部,仅靠 DOM 无法满足 sticky / 固定列 / 伪元素阴影时,使用 --z-index-sticky ~ --z-index-scrollbar,例如 CwStickyTabs、CwDataTable;CwDiv 自绘滚动条统一用 --z-index-scrollbar,须高于固定列 body/header/shadow 以便拖拽。
动态栈深(如 CwStack):同一父级下用 DOM 顺序 叠层,禁止按层递增写内联 zIndex 数字。
上述例外不得滥用到普通布局组件。
右键菜单(showCwContextMenu)
- 仅命令式 API:
showCwContextMenu({ x, y, items, onSelect, onClose });每次打开 ephemeral mount(body + 全屏透明 scrim + Popup 面板)。 - 不要在
v-for里为每一行各挂一个菜单组件;列表在contextmenu里解析行数据后调用showCwContextMenu。 - 全屏预览等组件只 emit
onContextMenu({ clientX, clientY, index, item }),由业务层拼菜单项并调用showCwContextMenu。 - body 滚动锁:Dialog / Sheet / Hud(mask) 共用
acquireOverlayBodyScrollLock/releaseOverlayBodyScrollLock。
Focus 和键盘支持规范(强制)
所有可交互的自研组件(Cw/Tp/Page 前缀)必须支持聚焦和键盘操作,确保无障碍性和用户体验。
组件分类
根据组件功能,分为三种类型:
1. 直接交互组件(需要完整的 focus 和键盘支持)
- 用户可以与其直接交互
- 示例:
CwButton、CwInput、CwCheckbox、CwRadio、CwSlider、CwSwitch - 必须遵循完整的 focus 和键盘支持规范
2. 容器组件(不需要直接 focus,但需要键盘支持)
- 包装器组件,通过插槽渲染内容
- 示例:
CwPopup、CwDynamicDialog - 特点:
- 不需要 tabindex 和 focus 管理(由内部元素管理)
- 必须支持键盘操作(如 ESC 关闭)
- 焦点由 trigger 或其他内部可聚焦元素管理
3. 展示组件(无交互性)
- 纯展示组件,不需要键盘支持
- 示例:
CwAvatar、CwCard、CwBadge
组件聚焦类型(重要)
对于"直接交互组件",有两种聚焦方式:
类型1:整个容器聚焦(适用于无标签或标签为组件整体的组件)
- 示例:
CwButton、CwSlider - 聚焦样式:
rootFocused
类型2:内部元素聚焦(适用于带标签文字的组件或开关)
- 示例:
CwCheckbox、CwRadio、CwSwitch - 聚焦样式:
checkboxFocused、radioFocused、trackFocused - 原因:焦点应该显示在图标/开关按钮上,而不是整个容器(包括文字),更符合用户预期
1. 聚焦支持(强制)
所有可聚焦的组件必须:
添加 tabindex 属性(必须使用 computed 函数处理 disabled 状态):
<!-- 类型1:整个容器可聚焦(适用于组件本身就是一个整体的情况,如按钮、开关) --> <div :tabindex="tabindex" @focus="handleFocus" @blur="handleBlur" > <!-- 类型2:内部元素可聚焦,外部容器不聚焦(适用于带标签的组件,如 checkbox、radio) --> <div :class="styles.root"> <!-- 外层容器不聚焦 --> <div :class="[styles.checkbox, { [styles.checkboxFocused]: isFocusVisible && !disabled }]" :tabindex="tabindex" @focus="handleFocus" @blur="handleBlur" >// 必须使用 computed 函数定义 tabindex const tabindex = computed(() => (props.disabled ? undefined : 0))重要:
- 必须使用 computed 函数:
const tabindex = computed(() => (props.disabled ? undefined : 0)) - 统一使用
undefined:当disabled为true时,tabindex必须为undefined - 变量名统一:使用小写
tabindex(不是tabIndex) - 对于带标签的组件(如 Checkbox、Radio),焦点应该应用在图标元素本身,而不是整个容器(包括文字)
- 必须使用 computed 函数:
添加 outline-none 样式(移除浏览器默认聚焦样式):
// styles.ts root: 'outline-none focus:outline-none',添加自定义聚焦样式(必须从
focusStyles.ts引用,禁止手写 ring 类):// styles.ts import { INTERACTIVE_FOCUS_CLASS, FORM_CONTROL_FOCUS_CLASS } from '../../utils/focusStyles' // 交互控件(Button / Checkbox / Tab 等) rootFocused: INTERACTIVE_FOCUS_CLASS, // 表单控件 wrapper(Input / Select / Textarea / NumberInput 等) inputWrapperFocus: FORM_CONTROL_FOCUS_CLASS,添加聚焦状态管理(使用 useFocusVisible):
import { useFocusVisible } from '../../utils/useFocusVisible' const { isFocusVisible, handleFocus: handleFocusVisibleFocus, handleBlur: handleFocusVisibleBlur } = useFocusVisible() const handleFocus = (event: FocusEvent) => { if (props.disabled) return handleFocusVisibleFocus(event) } const handleBlur = () => { handleFocusVisibleBlur() }重要:
- 必须使用
useFocusVisiblecomposable:这确保了只在键盘导航(Tab 键)时显示 focus 样式,鼠标点击时不显示 - 符合成熟组件库的行为:与 Material-UI、Ant Design 等组件库保持一致
- handleFocus 必须接收 FocusEvent:用于判断是否是键盘导航导致的聚焦
- 必须使用
应用聚焦样式(必须排除 disabled 状态):
<!-- 类型1:聚焦样式应用在根容器 --> <div :class="[styles.root, { [styles.rootFocused]: isFocusVisible && !disabled }]"> <!-- 类型2:聚焦样式应用在内部可聚焦元素(适用于带标签的组件) --> <div :class="styles.root"> <div :class="[styles.checkbox, { [styles.checkboxFocused]: isFocusVisible && !disabled }]">重要:
- 聚焦样式只在
isFocusVisible && !disabled时应用(配合useFocusVisible,鼠标点击不显示) - 对于带标签的组件(如 Checkbox、Radio),聚焦样式应该应用在图标元素上(如
checkboxFocused),而不是整个容器(root) - 验证逻辑:如果组件有标签文字,但整个容器显示了聚焦环,则需要将焦点调整到内部元素
- 聚焦样式只在
handleFocus 必须检查 disabled 状态:
const handleFocus = (event: FocusEvent) => { if (props.disabled) return handleFocusVisibleFocus(event) }重要:
handleFocus函数必须首先检查disabled状态,如果已禁用则直接返回handleFocus必须接收FocusEvent参数,并传递给handleFocusVisibleFocus
2. 键盘支持(强制)
所有需要键盘操作的组件必须:
监听键盘事件:
<div @keydown="handleKeyDown">实现键盘事件处理函数(必须首先检查 disabled 状态):
const handleKeyDown = (event: KeyboardEvent) => { // 重要:disabled 状态下不允许键盘操作 if (props.disabled) return switch (event.key) { case 'Enter': case ' ': // Space event.preventDefault() // 执行操作 break // 根据组件功能实现相应的键盘支持 } }重要:
handleKeyDown函数必须首先检查props.disabled,如果已禁用则直接返回,不执行任何键盘操作。必须阻止默认行为:
event.preventDefault()
3. ARIA 属性支持(强制)
所有组件必须提供完整的 ARIA 支持:
添加 role 属性:
:role="'button'" // 或 'slider'、'combobox' 等添加 aria 属性:
:aria-disabled="disabled" :aria-label="label" :aria-describedby="descriptionId"
4. 组件分类规范
不同组件类型的键盘支持要求:
按钮类组件(Button):
Enter或Space:触发点击- 示例:
CwButton、CwIconButton
输入类组件(Input、Select、Slider):
ArrowLeft/ArrowRight:左右导航ArrowUp/ArrowDown:上下导航Home/End:跳到开始/结束Escape:关闭/取消- 示例:
CwSlider、CwSelect、CwInput
列表类组件(List、Menu):
ArrowUp/ArrowDown:导航Enter:选择Home/End:跳到开始/结束- 示例:
CwDropdown、CwMenu
5. 聚焦视觉样式统一规范(强制)
禁止手写 ring-2 ring-[var(--brand-color-focus)] ring-offset-1 等旧样式;禁止使用已废弃的 --brand-color-focus 作为聚焦环颜色(该 token 为浅色品牌色,不适合聚焦)。
统一入口:src/utils/focusStyles.ts(组件 styles.ts 中 import 使用,勿复制字符串)
| 场景 | 常量 | 实际效果 |
|------|------|----------|
| 带 border 的表单控件 wrapper | FORM_CONTROL_FOCUS_CLASS | !border-[var(--focus-ring)](须 ! 覆盖常态 border-color,勿再加 ring) |
| 无独立边框的交互控件 | INTERACTIVE_FOCUS_CLASS | ring-1 ring-[var(--focus-ring)] ring-offset-0 |
| 菜单项等原生 focus-visible | FOCUS_VISIBLE_INTERACTIVE_CLASS | 同上,带 focus-visible: 前缀 |
| 原生 <input> / <select> 的 :focus | NATIVE_FOCUS_CLASS | focus:outline-none focus:!border-[var(--focus-ring)] |
| 交互控件 + 额外圆角等 | withInteractiveFocus('rounded-lg') | 在统一 ring 后拼接 utility |
Design Token(src/assets/tokens/tokens.css):
--focus-ring:键盘聚焦色,默认var(--brand-color-6);暗色主题见theme-dark.css- 表单控件不要再加
ring:inputWrapper等常有overflow-hidden,ring+ring-offset会与border叠成「双线」或白边
适用组件(示例):
- 表单:
CwInput、CwNumberInput、CwTextarea、CwSearchBar、CwSelect(内嵌CwInput) - 交互:
CwButton、CwCheckbox、CwRadio、CwSwitch、CwSlider、CwTabbar、CwTag - 浮层项:
CwDropdown菜单项、CwSegment选项
表格内 Checkbox:多选列须 fitContent: true;CwDataTable 对 fitContent 列使用 cellContentFit / overflow-visible,避免外圈被 overflow-hidden 裁切。
选中态 ≠ 聚焦态:如图节点 nodeActive、LowcodeEngine 画布选中等业务高亮,不使用上述聚焦常量。
状态样式选择写法(强制)
- 所有状态样式的选择必须使用统一三元嵌套写法,禁止使用
|| ''作为回退:- 根容器:
disabled ? styles.rootDisabled : (readonly ? styles.rootReadonly : styles.rootNormal) - 控件容器:
disabled ? styles.inputWrapperDisabled : (readonly ? styles.inputWrapperReadonly : styles.inputWrapperNormal) - 输入/控件本体:
disabled ? styles.inputDisabled : (readonly ? styles.inputReadonly : styles.inputNormal)
- 根容器:
- 聚焦样式为独立类,仅在
isFocusVisible && !disabled为真时叠加,例如:{ [styles.inputWrapperFocus]: isFocusVisible && !disabled } - 禁止在 class 表达式中使用空字符串兜底(如
styles.xxx || ''),所有状态类都必须在styles中事先定义好
6. 示例代码
完整的聚焦和键盘支持示例:
<template>
<div
:class="[
styles.root,
disabled ? styles.rootDisabled : styles.rootNormal,
{ [styles.rootFocused]: isFocusVisible && !disabled },
]"
@keydown="handleKeyDown"
@focus="handleFocus"
@blur="handleBlur"
:tabindex="tabindex"
:role="'button'"
:aria-disabled="disabled ? 'true' : 'false'"
:aria-label="texts.button"
>
{{ texts.button }}
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { CwButtonProps } from './types'
import { defaultStyles } from './styles'
import { useFocusVisible } from '../../utils/useFocusVisible'
const { isFocusVisible, handleFocus: handleFocusVisibleFocus, handleBlur: handleFocusVisibleBlur } =
useFocusVisible()
const tabindex = computed(() => (props.disabled ? undefined : 0))
const handleFocus = (event: FocusEvent) => {
if (props.disabled) return
handleFocusVisibleFocus(event)
}
const handleBlur = () => {
handleFocusVisibleBlur()
}
const handleKeyDown = (event: KeyboardEvent) => {
if (props.disabled) return
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault()
emit('click')
}
}
</script>// styles.ts
import { INTERACTIVE_FOCUS_CLASS } from '../../utils/focusStyles'
export const defaultStyles: CwButtonStyles = {
root: 'outline-none focus:outline-none',
rootNormal: 'cursor-pointer',
rootDisabled: 'cursor-not-allowed text-[var(--text-color-disabled)] bg-[var(--bg-color-component-disabled)] border-[var(--component-border)]',
rootFocused: INTERACTIVE_FOCUS_CLASS,
}7. Disabled / Readonly 实现清单(强制)
行为规则(所有 Data Input 必须遵守)
- Readonly:可聚焦并显示聚焦环,但不得以任何方式改变值(鼠标、键盘、拖拽、清除、打开弹层、滚轮、步进等全部拦截)
- Disabled:不可聚焦(tabindex 为 undefined),不响应任何交互,不显示聚焦环
Props(统一)
- 必须同时提供:
disabled?: boolean、readonly?: boolean,默认均为false
- 必须同时提供:
交互拦截(统一入口)
- 一切可能改值的入口函数首行添加:
if (props.disabled || props.readonly) return
- 一切可能改值的入口函数首行添加:
ARIA(统一)
:aria-disabled="disabled ? 'true' : 'false'":aria-readonly="readonly ? 'true' : 'false'"(适用于 checkbox/radio/switch/slider/combobox/input 等角色)
TabIndex(统一)
- 必须使用 computed 函数:
const tabindex = computed(() => (props.disabled ? undefined : 0)) - Disabled:
tabindex置undefined(统一使用undefined) - Readonly:允许聚焦(保留键盘导航/可读性),但不得改值
- 必须使用 computed 函数:
样式类名(统一)
- 根容器:
rootDisabled、rootReadonly - 控件元素:
inputDisabled/inputReadonly、checkboxDisabled/checkboxReadonly、radioDisabled/radioReadonly、switchDisabled/switchReadonly、sliderDisabled/sliderReadonly、optionDisabled - 选项元素(Select/Cascader/AutoComplete):
option(基础)+optionNormal(正常状态)+optionSelected(选中状态)+optionDisabled(禁用状态),必须使用多层三元表达式确保状态互斥
- 根容器:
颜色与对比度:
- 禁用态必须使用
text-[var(--text-color-disabled)]、border-[var(--component-border)]、bg-[var(--bg-color-component-disabled)]等 tokens - 禁用态完全禁止使用透明度(
opacity-*),必须通过 token 实现禁用视觉效果 - 只读态保持可读(避免误用禁用色),且禁止通过降低透明度体现只读(例如
opacity-70) - 所有颜色必须使用 token,禁止使用硬编码色值(如
#ffffff、rgb()、rgba()等) - 状态选择表达式(强制):
disabled ? disabledClass : (readonly ? readonlyClass : normalClass),禁止使用|| ''
- 禁用态必须使用
Hover 在移动端的规范(强制)
- 移动端默认禁止 hover 视觉效果(包含
hover:bg-*、hover:text-*、hover:border-*、hover:shadow-*等) - 仅允许在“明确支持 hover 的设备”上生效,必须使用媒体查询门控:
@media (hover: hover) and (pointer: fine)(推荐)
- Tailwind 写法建议(任选其一):
- 使用任意变体:
[@media(hover:hover)_and_(pointer:fine)]:hover:bg-[var(--gray-color-1)] - 或在组件中按设备能力切换 class(例如通过
matchMedia('(hover: hover) and (pointer: fine)'))
- 使用任意变体:
- 禁止在通用 class 中直接写裸
hover:*并默认作用于移动端 - JS 行为与 Tooltip / 弹出层(强制):
- 依赖
mouseenter/mouseleave的 hover 型触发(如CwPopup/CwTooltip的trigger: 'hover')必须与上述门控一致:仅在matchMedia('(hover: hover) and (pointer: fine)').matches为真时绑定或响应该类事件。 - 原因:触摸环境第一次点击常会合成鼠标事件,易误触「悬停才出现」的浮层;统一门控后,触摸为主设备上不会因误合成
mouseenter弹出说明层。 - 实现约定:弹出层事件在
usePopupEvents中已按该媒体查询处理;若新增同类组件,须复用finePointerHoverMatches()(CwPopup/utils/finePointerHoverMedia.ts)或等价逻辑,禁止在无门控下对通用触发区域绑定纯 hover 打开浮层。 - 移动端需要显式展示说明:使用
trigger: 'click'/manual、或独立文案/UI,不得依赖未门控的 hover 浮层作为唯一说明手段。
- 依赖
- 移动端默认禁止 hover 视觉效果(包含
样式分离规范(强制)
- 基础样式与状态样式必须分离:每个元素必须明确区分基础样式(所有状态共享的布局/尺寸)和状态样式(根据状态切换的颜色/cursor/opacity 等)
- 基础样式:包含所有状态共享的样式,如尺寸(
w-4 h-4)、布局(flex items-center)、边框宽度(border-2)、圆角(rounded)等,不包含任何状态相关的样式(颜色、cursor、opacity) - 状态样式:根据组件状态(Normal/Checked/Disabled/Readonly)互斥切换,每个元素在同一时刻只能应用一个状态样式类,禁止在同一元素上同时存在多个互斥的状态类
- 状态样式组合方式:
- 根容器:
root(基础)+rootNormal/rootDisabled/rootReadonly(状态,互斥) - 控件元素:
checkbox(基础)+checkboxNormal/checkboxChecked/checkboxDisabled/checkboxReadonly(状态,互斥) - 选项元素:
option(基础,布局尺寸)+optionNormal/optionSelected/optionDisabled(状态,互斥),使用多层三元表达式:disabled ? optionDisabled : (selected ? optionSelected : optionNormal) - 子元素:
icon(基础,尺寸布局)+iconNormal/iconDisabled(状态,颜色);label(基础,字体大小)+labelNormal/labelDisabled(状态,颜色)
- 根容器:
- 禁止在同一样式类中混用基础样式和状态样式:如
checkbox: 'w-4 border-2 cursor-pointer'是错误的(cursor-pointer应放在checkboxNormal状态样式中) - 完整示例(CwCheckbox):
// styles.ts - 正确示例 import { INTERACTIVE_FOCUS_CLASS } from '../../utils/focusStyles' export const defaultStyles: CwCheckboxStyles = { // 根容器:基础样式 + 状态样式 root: 'inline-flex items-center gap-2 select-none', // 基础:布局、间距 rootNormal: 'cursor-pointer', // 状态:正常指针 rootDisabled: 'cursor-not-allowed', // 状态:禁用指针 rootReadonly: 'cursor-default', // 状态:只读指针 // 控件元素:基础样式 + 状态样式 checkbox: 'relative inline-flex items-center justify-center w-4 h-4 border-2 rounded appearance-none outline-none transition-colors', // 基础:布局、尺寸、边框结构 checkboxNormal: 'border-[var(--gray-color-6)] bg-[var(--white)] cursor-pointer', // 状态:未选中 checkboxChecked: 'border-[var(--brand-color-6)] bg-[var(--white)] cursor-pointer', // 状态:选中 checkboxDisabled: 'border-[var(--component-border)] bg-[var(--bg-color-component-disabled)] cursor-not-allowed', // 状态:禁用 checkboxReadonly: 'border-[var(--gray-color-6)] bg-[var(--white)] cursor-default', // 状态:只读(禁止降低透明度) checkboxFocused: INTERACTIVE_FOCUS_CLASS, // 独立:聚焦环(可叠加),见 focusStyles.ts // 子元素:基础样式(尺寸)+ 状态样式(颜色) icon: 'w-3 h-3', // 基础:尺寸 iconNormal: 'text-[var(--brand-color-6)]', // 状态:正常颜色 iconDisabled: 'text-[var(--text-color-disabled)]', // 状态:禁用颜色 label: 'text-sm select-none', // 基础:字体大小、选择禁止 labelNormal: 'text-[var(--text-color-primary)]', // 状态:正常颜色 labelDisabled: 'text-[var(--text-color-disabled)]' // 状态:禁用颜色 }<!-- CwCheckbox.vue - 正确示例 --> <template> <!-- 根容器:基础样式 + 互斥状态样式 --> <div :class="[styles.root, disabled ? styles.rootDisabled : (readonly ? styles.rootReadonly : styles.rootNormal)]"> <!-- 控件:基础样式 + 互斥状态样式 + 独立聚焦样式 --> <div :class="[ styles.checkbox, disabled ? styles.checkboxDisabled : (readonly ? styles.checkboxReadonly : (isChecked ? styles.checkboxChecked : styles.checkboxNormal)), { [styles.checkboxFocused]: isFocusVisible && !disabled } ]"> <!-- 图标:基础样式(尺寸)+ 互斥状态样式(颜色) --> <svg :class="[styles.icon, disabled ? styles.iconDisabled : styles.iconNormal]">...</svg> </div> <!-- 标签:基础样式(字体大小)+ 互斥状态样式(颜色) --> <span :class="[styles.label, { [styles.labelNormal]: !disabled }, { [styles.labelDisabled]: disabled }]">...</span> </div> </template> - 选项元素示例(CwSelect/CwCascader/CwAutoComplete):
// styles.ts - 正确示例 export const defaultStyles: CwSelectStyles = { // 选项:基础样式(布局尺寸)+ 状态样式(颜色、cursor) option: 'px-3 py-2 text-sm whitespace-nowrap overflow-hidden text-ellipsis transition-colors duration-200', // 基础:布局、尺寸、过渡 optionNormal: 'text-[var(--text-color-primary)] cursor-pointer', // 状态:正常 optionSelected: 'text-[var(--brand-color-6)] bg-[var(--brand-color-1)] font-medium', // 状态:选中 optionDisabled: 'text-[var(--text-color-disabled)] cursor-not-allowed pointer-events-none', // 状态:禁用(必须使用 token,禁止使用透明度) optionHover: 'hover:bg-[var(--gray-color-1)]', // 独立:悬停(可叠加) optionFocus: 'bg-[var(--gray-color-1)]', // 独立:聚焦(可叠加) }<!-- CwSelect.vue - 正确示例 --> <template> <div v-for="(option, index) in options" :key="option.value" :class="[ styles.option, // 使用多层三元表达式确保状态互斥:disabled > selected > normal option.disabled ? styles.optionDisabled : (option.value === modelValue ? styles.optionSelected : styles.optionNormal), // 独立样式可叠加 { [styles.optionHover]: option.value !== modelValue && !option.disabled, [styles.optionFocus]: (hasKeyboardInteracted && index === selectedIndex && !option.disabled) } ]" > {{ option.label }} </div> </template>
插槽状态传递(统一)
- 对外暴露插槽的输入类组件,必须通过 slot props 传递
disabled与readonly,以便插槽内容按状态自行切换样式 - 示例:
- CwInput:
<slot name="icon" :disabled="disabled" :readonly="readonly" />、<slot name="suffix" ... />、<slot name="clear" ... /> - CwCheckbox/CwRadio:默认插槽
<slot :disabled="disabled" :readonly="readonly" />
- CwInput:
- 对外暴露插槽的输入类组件,必须通过 slot props 传递
清除按钮规范(强制)
- 清除按钮不可聚焦:清除按钮不得设置
tabindex属性,禁止通过 Tab 键聚焦 - 清除按钮交互方式:
- 只能通过鼠标点击清除
- 清除操作通过键盘快捷键处理:通过各自的键盘 hook(如
useSelectKeyboard、useAutoCompleteKeyboard、useCascaderKeyboard)中的Backspace/Delete键处理
- 清除按钮实现:
- 清除按钮只设置
role="button"和@click.stop="handleClear" - 不设置
tabindex属性 - 不设置
@keydown事件处理 - 设置
aria-label和aria-disabled属性(根据 disabled/readonly 状态)
- 清除按钮只设置
- 适用组件:所有带清除功能的组件(CwInput、CwSelect、CwAutoComplete、CwCascader 等)
- 清除按钮不可聚焦:清除按钮不得设置
组件要点
- CwSelect:Readonly 不打开弹层、不清除、不改值;触发器可聚焦;内置
CwInput只读 - CwSlider:Readonly 可聚焦但拖拽与键盘增减无效
- CwCheckbox/CwRadio/CwSwitch:Readonly 不允许切换;支持
aria-readonly
- CwSelect:Readonly 不打开弹层、不清除、不改值;触发器可聚焦;内置
验证清单
- Props 存在且默认
false - 改值入口均有
if (props.disabled || props.readonly) return - Disabled 不可聚焦;Readonly 可聚焦但不改值
- ARIA:
aria-disabled/aria-readonly同步 - 样式:
rootDisabled与rootReadonly以及子元素*Disabled/*Readonly已生效,禁用态不显示 focus/hover/active 高亮
- Props 存在且默认
自定义组件命名规范(Cw/Tp/Page 前缀组件)
Cw/Tp/Page 组件分类规范
随着自定义组件数量的增加,Cw/Tp/Page 组件必须按核心能力进行分类管理,便于维护和使用。所有 Cw/Tp/Page 组件必须按照以下分类规则进行组织:
分类原则
- 能力导向分类:按组件的核心能力和功能进行分类
- 业务场景优先:考虑组件的典型使用场景和业务需求
- 技术能力相关:考虑组件提供的技术能力和实现方式
- 平台无关:不按平台(Mobile/PC)分类,按能力分类
分类规则
1. Form & Input(表单和输入)
- 用户数据输入、表单处理、选择器相关
- 如:输入框、选择器、表单组件、复选框、单选框、滑块、开关等
2. Display(展示)
- 内容展示、媒体播放、信息呈现、文件管理、内容创作相关
- 如:头像、徽章、单元格、分割线、图标、图片、图片预览、加载中、富文本、轮播、标签、视频播放器、文件上传、图片裁剪、富文本编辑器等
3. Layout & Navigation(布局和导航)
- 页面布局、空间组织、导航结构相关
- 如:响应式布局、栅格系统、栈容器、虚拟滚动、瀑布流、导航栏、标签栏、标签页、树形列表等
4. Interaction(交互)
- 用户交互、手势操作、交互反馈相关
- 如:按钮、倒计时按钮、无限循环滚动、无限滚动等
5. Overlay & Dialog(覆盖层和对话框)
- 弹窗、对话框、提示框等覆盖层组件
- 如:弹出层、工具提示、确认对话框、动态对话框、动态底部弹层、选择器弹层、图片裁剪弹层等
6. Visualization(可视化)
- 数据图表、可视化、流程图、图形处理、动画效果、3D渲染相关
- 如:图表组件、流程图、算法可视化、数据结构可视化、3D场景、动画组件、粒子效果、GLSL着色器等
7. Business(业务)
- 特定业务场景、业务功能相关
- 如:聊天组件、低代码引擎、计算网格、信息流等
8. Map & Location(地图位置)
- 地图展示、地理位置相关
- 如:地图组件、位置服务等
分类实施要求
- 入口文件分类:在
src/index.ts中按分类组织导出,先导出所有 Cw 组件,再导出所有 Tp 组件,最后导出所有 Page 组件 - Storybook分类:
Cw组件使用1. Custom Components/{Category}/{ComponentName}(数字前缀用于控制排序)Bs组件使用2. Business Specific Components/{Category}/{ComponentName}(数字前缀用于控制排序)Page组件使用3. Pages/{ComponentName}(数字前缀用于控制排序)Tp组件使用4. Third Party Components/{Category}/{ComponentName}(数字前缀用于控制排序)
- 新增组件:必须确定合适的分类归属
- 分类调整:当组件功能发生变化时,及时调整分类
- 命名一致性:分类名称使用英文,保持简洁明了
- 规范一致性:Cw、Tp、Page 组件必须遵守完全相同的规范要求(Tailwind CSS、tokens 使用、样式和文本国际化等)
自研组件命名规范(Cw/Tp/Page,强制)
规则 1:Mobile 后缀(可选)
- 仅面向移动端交互、布局明显不同于 PC 的 Cw/Tp/Page 组件,可在名称末尾加
Mobile(如CwPhoneLoginMobile) - 纯响应式组件(同一套 Cw 组件适配宽窄视口)不要加
Mobile后缀
规则 2:Tp 前缀使用规则
- 当组件在自定义逻辑中深度集成 ECharts、Monaco Editor、Three.js、Mapbox、PDF.js 等重型第三方库并提供超出简单包装的功能时,必须使用
Tp前缀 Tp组件必须遵守本文所有自研组件规范(包括 Tailwind CSS、tokens 使用等),同时在文档中明确列出所依赖的重型第三方库
规则 3:Page 前缀使用规则
- 当组件是面向具体业务场景的页面级组件,可能包含多个基础组件的组合与业务逻辑时,必须使用
Page前缀 Page组件必须遵守本文所有自研组件规范(包括 Tailwind CSS、tokens 使用、样式和文本国际化等),与Cw和Tp组件规范完全一致
// ✅ 正确示例
CwPhoneLoginMobile // 移动端专用流程
CwChat // 纯自研,响应式
TpChartJs // 深度整合 Chart.js
PageLogin // 页面级业务组件
// ❌ 错误示例
CwChatMobile // 同一套 Cw 响应式组件不应加 Mobile违规检查:
- Tp / Page 组件必须遵守与 Cw 相同的规范(Tailwind、tokens、styles/texts 等)
1. 组件代码格式(强制)
1.1 自研组件格式(Cw/Tp/Page 开头)
重要:所有 Cw/Tp/Page 开头的自研组件都必须只使用 Tailwind CSS,不允许使用任何自定义 CSS 样式!
自研组件可以根据业务需求自由编写,不受第三方库透传限制,但必须严格遵守以下样式规范:
根容器 BEM 类名(强制,仅 Cw 组件):
- 命名:组件
CwFooBar对应块名cw-foo-bar(Cw前缀改为cw-,其余 PascalCase 转 kebab-case)。 - 只加在根容器:块名 class 仅写在组件最外层原生 DOM 节点(
div/span/nav等)的class或styles.root中,用于在 DOM 里定位组件;可不写任何配套 CSS,与 Tailwind 并存。 - 禁止子元素 BEM:除根块名外,不要再写
cw-foo-bar__toolbar、cw-foo-bar--active等 Element/Modifier;子区域样式只用 Tailwind(styles.ts各字段)或data-*钩子。 - 根节点是其他 Cw 组件时不加:若模板根是
<CwPopup>、<CwCard>、<CwTooltip>等,不要为当前组件再包一层只为加 BEM;由该子组件自己的根 BEM 承担定位。 - 编辑器 / 协议钩子例外:ProseMirror、拖拽、单元测试等必须在 DOM 上留名的 class(如
cw-emoji、cw-file-ref、data-cw-*)保留,但不算「块名」,也不扩展到__子元素 BEM。 CwDiv自绘滚动条:根节点必须有cw-div;内部cw-div__viewport、cw-div__scrollbar等为滚动实现与querySelector钩子,允许保留(不视为装饰性 BEM)。
样式规范(强制):
- 只能使用 Tailwind CSS 类名:所有样式都必须通过 Tailwind CSS 类名实现
- 禁止自定义 CSS:不允许在
<style>标签中编写任何自定义样式 - 禁止 SFC
<style>:不允许在.vue中使用<style>(pnpm run check:no-vue-style强制)- 装饰与布局:Tailwind +
styles.ts - 纯功能性 CSS(容器查询、
:deep穿透、动画 keyframes):放在src/assets/components/*.css,在组件<script>中 side-effectimport
- 装饰与布局:Tailwind +
- HTML 元素使用限制:为避免 Tailwind @layer 样式优先级问题,Cw/Tp/Page 组件中禁止使用以下浏览器原生元素:
- ❌ 禁止:
<button>、<a>、<form>、<label>、<select>等(使用 div/span 替代) - ✅ 允许:
<input>、<textarea>等输入控件(无法替代的功能性元素) - ✅ 推荐:主要使用
<div>和<span>构建组件 - 💡 提示:需要点击交互的 div 元素必须添加
cursor-pointer类名 - 💡 使用 div 替代 button 时的键盘支持:当使用
<div>替代<button>时,必须添加:role="button"属性(用于可访问性):tabindex="tabindex"属性(使用 computed 函数,使元素可聚焦)@keydown.enter.prevent和@keydown.space.prevent事件处理器(支持键盘操作)- 示例:
- ❌ 禁止:
<div
role="button"
:tabindex="tabindex"
@click="handleClick"
@keydown.enter.prevent="handleClick"
@keydown.space.prevent="handleClick"
>
按钮文本
</div>// 必须使用 computed 函数定义 tabindex
const tabindex = computed(() => (props.disabled ? undefined : 0))- 强制使用 Design Tokens 色值:
- ❌ 严禁 hardcode 色值:禁止
#hex、rgb()/rgba()、bg-blue-500、text-gray-600等 - ❌ 严禁
var(--token, #fallback):禁止为 token 写 hex fallback,只写var(--token) - ✅ 唯一可写 hex 的位置:
src/assets/tokens/**(含tokens.css与各 token 源文件) - ✅ 组件内只用语义 token:
bg-[var(--brand-color-6)]、text-[var(--text-color-primary)]、border-[var(--component-border)]等;浅色在:root、深色在theme-dark.css仅写与浅色不同的变量(其余继承:root,pnpm run check:theme-parity校验子集),通过setAppTheme('dark'|'light')或data-theme在应用根切换 - ✅ 导入 tokens:应用入口须引入
zcw-vue-ui/dist/tokens.css(或包内src/assets/tokens/tokens.css) - 🔍 CI / 本地检查(默认扫描
Cw*):pnpm run check:tokens # 必须通过后再提 PR(默认扫描 Cw*) pnpm run check:tokens -- --scope=all # 含 Tp/Bs/Page pnpm run check:tokens -- --fix # 仅自动去掉 var(--x, #hex) 形式的 fallback - ⚠️ Canvas / 运行时拼色:若必须用
rgba()拼 alpha,在上一行加// design-token-check: ignore并说明原因;优先从tokenSolidColors等 token 源读取 hex - 💡 参考:
src/assets/tokens/tokens.css、脚本scripts/check-design-token-colors.mjs
- ❌ 严禁 hardcode 色值:禁止
<template>
<!-- 只能使用布局相关的class -->
<div class="flex flex-col p-4 space-y-2">
<h3 :class="styles.title">{{ texts.title }}</h3>
<p :class="styles.content">{{ texts.content }}</p>
<!-- ✅ 正确:使用 div 替代 button,样式通过 styles 传入 -->
<div
:class="styles.button"
@click="handleAction"
>
{{ texts.buttonText }}
</div>
<!-- ✅ 允许:输入控件可以使用原生元素,样式通过 styles 传入 -->
<input
type="text"
:class="styles.input"
:placeholder="texts.inputPlaceholder"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import type { CwComponentProps, CwComponentEmits } from './types'
import { defaultStyles } from './styles'
import { defaultTexts } from './texts'
defineOptions({
name: 'CwComponentName'
})
const props = defineProps<CwComponentProps & {
styles?: Record<string, string>
texts?: Record<string, string>
}>()
const emit = defineEmits<CwComponentEmits>()
// 合并默认样式和传入的样式
const styles = computed(() => ({ ...defaultStyles, ...props.styles }))
// 合并默认文本和传入的文本
const texts = computed(() => ({ ...defaultTexts, ...props.texts }))
</script>
<!-- 注意:Cw/Tp/Page 组件不应包含 <style> 标签 -->Cw/Tp/Page 组件样式设计原则:
- 布局优先:组件内部只能使用布局相关的class
- 样式外部化:所有外观样式通过
styles对象传入 - 文本国际化:所有文本通过
texts对象传入 - 默认值管理:提供默认的
styles.ts和texts.ts文件 - 简单合并:使用
{ ...defaultStyles, ...props.styles }方式合并样式和文本
自研组件要求:
- 必须使用
<script setup lang="ts">进行逻辑编写 - 必须使用
defineOptions({ name: 'CwComponentName' })定义组件名称(Cw/Tp/Page 组件都遵循此规范) - 可以自定义 Props 和 Emits 类型
- 可以包含自定义业务逻辑
- 可以使用任何 Vue 3 Composition API
- 组件名必须以
Cw、Tp或Page开头(Tp 需满足"重型第三方整合"规则,Page 需满足"页面级业务组件"规则) - 必须只使用 Tailwind CSS 进行样式设计(Cw/Tp/Page 组件统一要求)
- 禁止使用自定义 CSS 样式(Cw/Tp/Page 组件统一要求)
- 强制使用 tokens 色值:禁止使用
gray-500、blue-600等 hardcode 色值,必须使用var(--brand-color-6)等 tokens 变量(Cw/Tp/Page 组件统一要求)
1.4 自研组件纯函数抽离规范(Cw/Tp/Page,强制)
适用场景:
当组件逻辑较为复杂(超过 300 行代码或包含大量计算逻辑)时,必须将纯函数抽离到独立的 functions 目录中。
纯函数定义:
- 纯函数:不依赖 Vue 响应式状态(
ref、computed、props等),只依赖传入参数的函数 - 非纯函数:需要访问组件状态、
props、emit等的函数,必须保留在组件中
抽离规范:
创建 functions 目录:
- 在组件目录下创建
functions目录 - 每个纯函数放在独立的
.ts文件中 - 使用
functions/index.ts统一导出所有函数
- 在组件目录下创建
纯函数命名规范:
- 使用动词开头,清晰表达函数功能
- 例如:
calculateImageBounds、adjustCropBoxSize、generateCropHandles
类型定义:
- 纯函数必须包含完整的 TypeScript 类型定义
- 输入参数和返回值必须有明确的类型
- 相关类型定义可以放在函数文件中,或统一放在
functions/types.ts
必须抽离的纯函数类型:
- ✅ 计算函数:坐标计算、尺寸计算、边界检查等
- ✅ 数据处理函数:数据转换、格式化、验证等
- ✅ 工具函数:事件坐标提取、URL 创建/撤销等
- ✅ 配置生成函数:生成配置对象、样式对象等
- ❌ 事件处理函数:需要访问
props、emit、响应式状态的函数 - ❌ 生命周期钩子:
onMounted、onUnmounted、watch等
目录结构示例:
CwImageCropper/ ├── functions/ │ ├── eventCoordinates.ts # 事件坐标提取 │ ├── imageBounds.ts # 图片边界计算 │ ├── cropBoxBounds.ts # 裁剪框边界限制 │ ├── cropBoxAdjust.ts # 裁剪框尺寸调整 │ ├── cropBoxMove.ts # 裁剪框移动 │ ├── cropBoxReset.ts # 初始裁剪框计算 │ ├── cropCoordinates.ts # 裁剪坐标计算 │ ├── cropHandles.ts # 裁剪手柄配置生成 │ ├── canvasCrop.ts # Canvas 裁剪逻辑 │ ├── fileUtils.ts # 文件工具函数 │ └── index.ts # 统一导出 ├── CwImageCropper.vue ├── types.ts ├── styles.ts └── index.ts函数导出和使用:
// functions/index.ts export * from './eventCoordinates' export * from './imageBounds' // ... 其他函数导出 // CwImageCropper.vue import { getEventCoordinates, calculateImageBounds, adjustCropBoxSize, // ... 其他函数导入 type CropBox, type ImageBounds } from './functions'组件代码要求:
- 组件代码应专注于 Vue 响应式逻辑和事件处理
- 所有纯计算逻辑都应通过导入的纯函数实现
- 组件代码应保持简洁,避免超过 500 行(不包括模板)
示例:
// functions/cropBoxBounds.ts - 纯函数
export interface CropBox {
x: number
y: number
width: number
height: number
}
export function clampCropBoxToBounds(cropBox: CropBox, bounds: ImageBounds): CropBox {
// 纯函数逻辑,不依赖任何 Vue 响应式状态
const result = { ...cropBox }
// ... 计算逻辑
return result
}
// CwImageCropper.vue - 组件中使用
import { clampCropBoxToBounds, type CropBox } from './functions'
const cropBox = ref<CropBox>({ x: 0, y: 0, width: 200, height: 200 })
const updateCropBox = () => {
const bounds = getImageBounds()
if (bounds) {
cropBox.value = clampCropBoxToBounds(cropBox.value, bounds)
}
}好处:
- ✅ 提高代码可测试性:纯函数易于单独测试
- ✅ 提高代码可维护性:逻辑分离,职责清晰
- ✅ 提高代码可复用性:纯函数可在其他组件中复用
- ✅ 降低组件复杂度:组件代码更简洁,易于理解
1.5 自研组件样式和文本规范(Cw/Tp/Page,强制)
样式规范:
- 内部样式限制:组件内部只能有布局相关的class(如
flex、grid、space-x-4、p-4、w-full等) - 外观样式禁止:禁止在组件内部使用外观样式(如
bg-blue-500、text-red-600、border-gray-300、rounded-lg等) - 必须使用 tokens:禁止使用任何 hardcode 色值,所有颜色必须使用
bg-[var(--brand-color-6)]、text-[var(--text-color-primary)]等 tokens 变量 - 样式统一管理:所有样式相关的都放在
styles对象中,通过 props 传入 - 默认样式文件:默认styles对象应该是一个单独的文件(如
styles.ts),组件导入并使用 - 样式覆盖:外部可以通过传入自定义styles对象来覆盖默认样式
- Props 类型必须是 Partial(强制):
- 所有组件的
styles和textsProps 类型必须是Partial<组件Styles>和Partial<组件Texts> - 原因:组件内部必须对
props.styles与defaultStyles进行浅层合并({ ...defaultStyles, ...props.styles }) - 类型定义示例:
export interface CwComponentProps { styles?: Partial<CwComponentStyles> texts?: Partial<CwComponentTexts> } - 组件内部实现:
const styles = computed(() => ({ ...defaultStyles, ...props.styles })) const texts = computed(() => ({ ...defaultTexts, ...props.texts })) - 好处:
- 外部只需传入需要覆盖的部分样式,不需要提供所有样式
- 类型安全,支持智能提示
- 符合组件设计原则:默认值 + 可选覆盖
- 所有组件的
文本国际化规范:
- 内部文本禁止:组件内部不能有硬编码文本,因为所有组件都需要做国际化
- 文本统一管理:文本也应该有一个类似styles的对象(如
texts或labels),通过 props 传入 - 默认文本文件:默认文本对象也应该是一个单独的文件(如
texts.ts或defaultTexts.ts) - 文本覆盖:外部可以通过传入自定义文本对象来覆盖默认文本
- 禁止硬编码文本:组件内不允许有任何硬编码的中文或英文文本,所有文本都必须通过
texts或props传入 - 简单合并:使用
{ ...defaultTexts, ...props.texts }方式合并,undefined 值会自动被忽略 - Props 类型必须是 Partial(强制):与样式规范相同,
textsProps 类型也必须是Partial<组件Texts>
插槽容器规范:
- 禁止插槽容器内置样式:插槽容器(包裹 slot 的 div)不应该有硬编码的样式类
- 样式外部化:插槽容器的样式应该通过外层容器传入,或者由插槽内容自行控制
- 结构纯净:插槽容器只负责提供结构框架,不添加任何装饰性样式
子组件处理:
- 独立子组件:可以提供独立的子组件供外部导入使用(如 CwCardTitle、CwCardContent 等)
- 子组件样式完整:这些子组件应该包含完整的样式(包括 padding、margin 等),使其可以独立使用
- 子组件独立导出:子组件应该在 index.ts 中独立导出,使外部可以直接使用
- 不作为默认内容:这些子组件不应该直接写在主组件的 slots 中作为默认内容,而是由外部用户根据需要选择是否使用
- Story 展示:每个子组件都应该有独立的 Story 来展示其样式和用法
父组件中使用子组件的样式和文本配置规范(强制):
⚠️ 核心原则(绝对禁止违反):
- ❌ 严禁在父组件的 styles 中直接定义子组件的样式:所有对 Cw/Tp/Page 子组件样式的覆盖必须单独成对象,不能直接写在父组件的 styles 中
- ✅ 必须使用独立的子对象:子组件的样式配置必须作为父组件 props 的独立子对象(如
inputStyles、buttonStyles) - ✅ 必须导入子组件类型:父组件的
types.ts中必须导入子组件的 styles 和 texts 类型,并使用Partial<子组件Styles>和Partial<子组件Texts>作为类型约束 - ✅ 支持部分覆盖:由于所有 Cw/Tp/Page 组件内部都会浅合并传入的 styles 和自带的 defaultStyles,所以覆盖时可以只覆盖需要修改的部分样式属性
详细规范:
子组件配置分离(强制):
- 当父组件内部使用了 Cw/Tp/Page 子组件时,所有对子组件样式的覆盖必须作为父组件 props 的独立子对象
- 禁止:在父组件的 styles 中直接定义子组件的样式(如
inputWrapper、button等子组件样式) - 必须:创建独立的子对象(如
inputStyles、buttonStyles)来配置子组件样式
命名规范(强制):
- 子组件的样式配置使用
{子组件简写}Styles格式(如inputStyles、buttonStyles、iconStyles) - 子组件的文本配置使用
{子组件简写}Texts格式(如inputTexts、buttonTexts、iconTexts) - 命名必须清晰明确,能够一眼看出是哪个子组件的配置
- 子组件的样式配置使用
类型定义(强制):
- 必须导入子组件类型:在父组件的
types.ts中,必须从子组件导入对应的 styles 和 texts 类型 - 使用
Partial<子组件Styles>和Partial<子组件Texts>作为类型约束 - 类型定义必须是
Partial类型,因为只需要覆盖部分样式,不需要提供所有样式
- 必须导入子组件类型:在父组件的
传递规则(强制):
- 父组件通过 props 接收子组件的配置,然后传递给子组件的
:styles和:texts属性 - 传递时直接传递子对象(如
:styles="styles.inputStyles"),而不是整个 styles 对象
- 父组件通过 props 接收子组件的配置,然后传递给子组件的
默认值处理:
- 子对象是可选的(
?),如果不需要自定义子组件配置,可以不传入该子对象,子组件使用其默认配置 - 由于使用
Partial类型,可以只覆盖需要修改的部分样式属性,其他样式会使用子组件的默认值
- 子对象是可选的(
示例:CwSelect 使用 CwInput
✅ 正确示例:
// CwSelect/types.ts
// ⚠️ 必须导入子组件的类型
import type { CwInputStyles, CwInputTexts } from '../CwInput/types'
export interface CwSelectStyles {
root: string
popup: string
option: string
// ... 其他父组件自身的样式
/** CwInput 样式配置 - 必须作为独立子对象 */
inputStyles?: Partial<CwInputStyles>
}
export interface CwSelectTexts {
placeholder: string
empty: string
// ... 其他父组件自身的文本
/** CwInput 文本配置 - 必须作为独立子对象 */
inputTexts?: Partial<CwInputTexts>
}❌ 错误示例(禁止):
// ❌ 错误:在父组件的 styles 中直接定义子组件的样式
export interface CwSelectStyles {
root: string
popup: string
// ❌ 禁止:不能直接定义子组件的样式属性
inputWrapper: string // 这是 CwInput 的样式,不能写在这里
input: string // 这是 CwInput 的样式,不能写在这里
inputFocused: string // 这是 CwInput 的样式,不能写在这里
}<!-- CwSelect.vue -->
<template>
<CwInput
:model-value="displayValue"
:placeholder="placeholder"
readonly
:styles="styles.inputStyles"
:texts="texts.inputTexts"
/>
</template>
<script setup lang="ts">
import CwInput from '../CwInput/CwInput.vue'
import type { CwSelectProps } from './types'
const props = defineProps<CwSelectProps>()
const styles = computed(() => props.styles || defaultStyles)
const texts = computed(() => props.texts || defaultTexts)
</script>使用方式:
<!-- 使用默认配置 -->
<CwSelect :options="options" />
<!-- ✅ 正确:自定义子组件配置 - 使用独立的子对象 -->
<CwSelect
:options="options"
:styles="{
// 可以只覆盖部分样式,其他样式会使用 CwInput 的默认值
inputStyles: {
inputWrapper: 'border-2 border-[var(--brand-color-6)]',
// 只覆盖 inputWrapper,其他样式(如 input、inputFocused 等)使用默认值
}
}"
:texts="{
inputTexts: {
placeholder: 'Custom placeholder'
}
}"
/>
<!-- ❌ 错误:禁止在父组件的 styles 中直接定义子组件样式 -->
<CwSelect
:options="options"
:styles="{
// ❌ 禁止:不能直接定义子组件的样式属性
inputWrapper: 'border-2 border-[var(--brand-color-6)]', // 错误
input: 'custom-input-style', // 错误
}"
/>规范要点(强制遵守):
- ✅ 严禁在父组件 styles 中直接定义子组件样式:所有对 Cw/Tp/Page 子组件样式的覆盖必须作为独立的子对象(如
inputStyles、buttonStyles) - ✅ 必须导入子组件类型:父组件的
types.ts中必须导入子组件的 styles 和 texts 类型,确保类型安全 - ✅ 必须使用 Partial 类型:子组件配置必须是
Partial<子组件Styles>类型,因为只需要覆盖部分样式 - ✅ 支持部分覆盖:由于所有 Cw/Tp/Page 组件内部都会浅合并传入的 styles 和自带的 defaultStyles,可以只覆盖需要修改的部分样式属性
- ✅ 父组件不直接操作子组件的样式:父组件通过子对象的 props 传递配置,保持组件解耦
- ✅ 子组件保持独立性:子组件不知道自己被其他组件使用,保持可复用性
- ✅ 配置类型安全:使用
Partial<子组件类型>确保类型安全和智能提示 - ✅ 向后兼容:子对象是可选的(
?),不传则使用子组件默认配置
函数式 API 组件的特殊说明:
- Dialog 和 Sheet 类型组件(如
CwSelectSheet、CwImageCropperSheet、CwConfirmDialog等)允许将子组件样式配置作为 props 的直接属性,而不是必须在styles对象中 - 这是为了方便函数式 API 的使用,保持 API 的简洁性
- 示例:
// ✅ 函数式 API 组件允许这样做 export interface CwSelectSheetProps { options?: CwSelectSheetOption[] styles?: Partial<CwSelectSheetStyles> // 子组件样式可以作为 props 的直接属性 sheetStyles?: Partial<CwDynamicSheetInnerStyles> } export interface CwImageCropperSheetProps { styles?: Partial<CwImageCropperSheetStyles> // 子组件样式可以作为 props 的直接属性 cropperStyles?: Partial<CwImageCropperStyles> sheetStyles?: Partial<CwDynamicSheetInnerStyles> } - 普通组件(非函数式 API)仍然必须将子组件样式配置放在
styles对象中(如styles.inputStyles、styles.buttonStyles)
示例:父组件包含子组件配置的完整示例
✅ 正确示例:CwSelect 的 styles.ts 和 texts.ts
// CwSelect/styles.ts
import type { CwSelectStyles } from './types'
import type { CwInputStyles } from '../CwInput/types'
export const defaultStyles: CwSelectStyles = {
// 父组件自身的样式
root: 'relative',
popup: 'absolute z-10 bg-[var(--white)] border border-[var(--component-border)] rounded-md shadow-lg',
option: 'px-3 py-2 text-sm cursor-pointer hover:bg-[var(--gray-color-1)]',
// ⚠️ 子组件配置必须作为独立子对象(可选)
// 如果需要为子组件提供默认样式覆盖,可以在这里定义
// 如果不需要,可以不定义这个属性,子组件会使用自己的默认样式
inputStyles: {
// 只覆盖需要的部分样式,其他样式使用 CwInput 的默认值
inputWrapper: 'border-2 border-[var(--component-border)]',
// 可以只覆盖部分样式属性
} as Partial<CwInputStyles>,
}
// CwSelect/texts.ts
import type { CwSelectTexts } from './types'
import type { CwInputTexts } from '../CwInput/types'
export const defaultTexts: CwSelectTexts = {
// 父组件自身的文本
placeholder: '请选择',
empty: '暂无数据',
// ⚠️ 子组件配置必须作为独立子对象(可选)
inputTexts: {
placeholder: '请选择',
} as Partial<CwInputTexts>,
}❌ 错误示例(禁止):
// ❌ 错误:在父组件的 styles 中直接定义子组件的样式
export const defaultStyles = {
root: 'relative',
popup: 'absolute z-10',
// ❌ 禁止:不能直接定义子组件的样式属性
inputWrapper: 'border-2 border-[var(--component-border)]', // 错误:这是 CwInput 的样式
input: 'px-3 py-2', // 错误:这是 CwInput 的样式
inputWrapperFocus: 'ring-2', // 错误:这是 CwInput 的样式,应走 focusStyles.ts
}简单组件示例(无子组件):
// styles.ts - 单独的默认样式文件(无子组件时)
import { NATIVE_FOCUS_CLASS } from '../../utils/focusStyles'
export const defaultStyles = {
title: 'text-lg font-semibold text-[var(--text-color-primary)] mb-2',
content: 'text-sm text-[var(--text-color-secondary)]',
button: 'mt-4 px-4 py-2 bg-[var(--brand-color-6)] text-[var(--white)] rounded-md hover:bg-[var(--brand-color-hover)] transition-colors duration-200 cursor-pointer inline-block text-center',
input: `mt-2 w-full px-3 py-2 border border-[var(--component-border)] rounded-md ${NATIVE_FOCUS_CLASS}` // NATIVE_FOCUS_CLASS 来自 focusStyles.ts
}
// texts.ts - 单独的默认文本文件(无子组件时)
export const defaultTexts = {
title: '组件标题',
content: '组件内容',
buttonText: '操作按钮',
inputPlaceholder: '输入内容'
}<!-- CwButton组件 -->
<template>
<div class="flex items-center justify-center"> <!-- 布局class -->
<div
:class="styles.button"
@click="handleClick"
>
{{ texts.submit }}
</div>
</div>
</template>
<script setup>
import { defaultStyles } from './styles'
import { defaultTexts } from './texts'
const props = defineProps({
styles: {
type: Object,
default: () => defaultStyles
},
texts: {
type: Object,
default: () => defaultTexts
}
})
</script>使用方式:
<!-- 使用默认样式和文本 -->
<CwComponent />
<!-- 自定义样式和文本(必须使用 tokens) -->
<CwComponent
:styles="{
button: 'px-6 py-3 bg-[var(--error-color-6)] text-[var(--white)] rounded-lg hover:bg-[var(--error-color-hover)]',
title: 'text-xl font-bold text-[var(--text-color-brand)]'
}"
:texts="{
buttonText: 'Submit',
title: 'Custom Title'
}"
/>⚠️ 重要提醒:
- 自定义样式时也必须使用 tokens 变量,禁止使用
bg-red-500、text-blue-600等 hardcode 色值 - 所有颜色必须从
src/assets/tokens/tokens.css中定义的变量中选择 - 可选的 tokens:
--brand-color-*、--gray-color-*、--text-color-*、--error-color-*、--warning-color-*、--success-color-*等
可跟随状态旋转的图标样式规范(强制):
当组件需要使用可跟随状态旋转的图标(如箭头图标,根据弹层打开/关闭状态旋转)时,必须遵循以下规范:
样式定义规范:
- 基础样式:将所有静态样式(颜色、过渡效果等)整合到
{icon}IconStyles.root的默认值中 - 状态样式:使用独立的字符串属性(如
arrowOpen)定义状态样式(如rotate-180) - 必须包含过渡效果:在
{icon}IconStyles.root中必须包含transition-transform duration-200等过渡样式,确保旋转动画流畅
- 基础样式:将所有静态样式(颜色、过渡效果等)整合到
类型定义规范:
export interface CwComponentStyles { // ... 其他样式 /** 状态样式(如旋转角度) */ arrowOpen: string /** CwIcon 样式配置(箭头图标) */ arrowIconStyles?: Partial<CwIconStyles> }默认样式定义:
export const defaultStyles: CwComponentStyles = { // ... 其他样式 // 状态样式:定义旋转角度等状态变化 arrowOpen: 'rotate-180', // 图标样式:基础样式包含颜色、过渡效果等,不包含状态样式 arrowIconStyles: { root: 'inline-flex items-center justify-center text-current text-[var(--text-color-secondary)] transition-transform duration-200', size: 16 } as Partial<CwIconStyles>, }组件实现规范:
- 使用
computed动态合并基础样式和状态样式 - 根据组件状态(如
popupVisible)决定是否应用状态样式 - 只合并状态样式,不重复合并基础样式(因为基础样式已经在
arrowIconStyles.root中)
// 箭头图标样式 const arrowIconStyle = computed(() => { const baseStyles = styles.value const iconStyle = baseStyles.arrowIconStyles || {} return { ...iconStyle, root: [ iconStyle.root || '', popupVisible.value ? baseStyles.arrowOpen : '' ].filter(Boolean).join(' ') } })- 使用
模板使用:
<CwIcon :svg="arrowDownIconSvg" :styles="arrowIconStyle" />规范要点:
- ✅ 基础样式整合:所有静态样式(颜色、过渡等)必须整合到
arrowIconStyles.root默认值中 - ✅ 状态样式独立:状态样式(如
rotate-180)作为独立字符串属性,在 computed 中动态合并 - ✅ 避免重复:不在 computed 中重复合并已在默认值中的基础样式
- ✅ 过渡效果必须:必须包含
transition-transform等过渡样式,确保状态切换动画流畅 - ✅ 类型安全:使用
Partial<CwIconStyles>类型,支持部分覆盖
- ✅ 基础样式整合:所有静态样式(颜色、过渡等)必须整合到
完整示例:CwSelect 箭头图标
// CwSelect/types.ts
export interface CwSelectStyles {
// ... 其他样式
arrowOpen: string
/** CwIcon 样式配置(箭头图标) */
arrowIconStyles?: Partial<CwIconStyles>
}
// CwSelect/styles.ts
export const defaultStyles: CwSelectStyles = {
// ... 其他样式
arrowOpen: 'rotate-180',
arrowIconStyles: {
root: 'inline-flex items-center justify-center text-current text-[var(--text-color-secondary)] transition-transform duration-200',
size: 16
} as Partial<CwIconStyles>,
}
// CwSelect/CwSelect.vue
const popupVisible = ref(false)
// 箭头图标样式
const arrowIconStyle = computed(() => {
const baseStyles = styles.value
const iconStyle = baseStyles.arrowIconStyles || {}
return {
...iconStyle,
root: [
iconStyle.root || '',
popupVisible.value ? baseStyles.arrowOpen : ''
].filter(Boolean).join(' ')
}
})<!-- 模板中使用 -->
<CwIcon
:svg="arrowDownIconSvg"
:styles="arrowIconStyle"
/>2. 入口文件格式(强制)
本组件库采用分离式入口文件结构,按组件类型分别导出:
2.1 主入口文件 src/index.ts(自研组件)
主入口文件 src/index.ts 专门导出自研组件(Cw/Tp 前缀)、业务特定组件(Bs 前缀)和页面组件(Page 前缀),按以下顺序组织:
- 先导出所有 Cw 组件(按功能分类)
- 再导出所有 Bs 组件(按功能分类)
- 再导出所有 Tp 组件(按功能分类)
- 最后导出所有 Page 组件
// ==================== Cw 组件(自研组件) ====================
// ==================== Data Visualization(数据可视化) ====================
export * from "./components/CwGraph";
export * from "./components/CwHashMap";
export * from "./components/CwUnionFind";
// ==================== 3D & Graphics(3D和图形) ====================
export * from "./components/CwGlsl";
export * from "./components/CwParticleBackground";
// ... 其他 Cw 组件分类
// ==================== Bs 组件(业务特定组件) ====================
// ==================== Display(展示) ====================
export * from "./components/BsAccountBalanceCard";
export * from "./components/BsBenefitCard";
// ==================== Interaction(交互) ====================
export * from "./components/BsFunctionGridItem";
// ==================== Business(业务) ====================
export * from "./components/BsPromotionBanner";
// ==================== Tp 组件(重型第三方库整合) ====================
// ==================== Content Creation(内容创作) ====================
export * from "./components/TpCherryMarkdown";
export * from "./components/TpCodeEditor";
export * from "./components/TpFabricCanvas";
export * from "./components/TpMonacoEditor";
export * from "./components/TpSkiaCanvas";
// ==================== Data Visualization(数据可视化) ====================
export * from "./components/TpChartJs";
export * from "./components/TpVueFlow";
// ==================== 3D & Graphics(3D和图形) ====================
export * from "./components/TpGsap";
export * from "./components/TpHeroScene";
export * from "./components/TpThreeJs";
// ... 其他 Tp 组件分类
// ==================== Page 组件(页面级业务组件) ====================
export * from "./components/PageChangePassword";
export * from "./components/PageChangePhone";
export * from "./components/PageContactInfoMobile";
export * from "./components/PageDeactivateAccountMobile";分类组织要求:
- 导出顺序:先导出所有 Cw 组件,再导出所有 Bs 组件,再导出所有 Tp 组件,最后导出所有 Page 组件
- 按功能分类进行分组
- 每个分类使用注释分隔
- 分类内按字母顺序排列
- 新增组件时按分类归属插入
样式入口(构建产物):
- 组件 JS 入口:
src/index.ts(Cw/Tp/Page)、src/style.ts(tokens/tailwind/iconfont)
3. 组件 index.ts 导出格式(强制)
3.1 自研组件导出格式(Cw/Tp/Page 开头)
单组件导出:
export { default as CwComponentName } from './CwComponentName.vue'
export * from'./types'多组件导出(包含子组件):
export { default as CwMainComponent } from './CwMainComponent.vue'
export { default as CwSubComponent1 } from './CwSubComponent1.vue'
export { default as CwSubComponent2 } from './CwSubComponent2.vue'
export * from'./types'强制要求:
- 必须使用
export { default as ... }格式 - 必须使用
export * from'./types'导出类型 - 必须导出所有子组件(如 CwDropdownItem、CwMenuItem、CwTabPane、TpChartLegend 等)
- 不允许使用其他导出方式
- 不允许添加额外的导入或注释
- 重要:如果组件有子组件,必须在 index.ts 中导出所有子组件
3.2 Dialog 和 Sheet 类型组件的函数式 API 规范(强制)
适用范围:
- Dialog 类型组件:如
CwConfirmDialog、CwDynamicDialog等 - Sheet 类型组件:如
CwSelectSheet、CwImageCropperSheet等
强制要求:
- 只支持函数式 API:Dialog 和 Sheet 类型组件必须只通过函数式 API 使用,禁止直接作为组件使用
- 函数命名规范:函数式 API 必须使用
show{ComponentName}格式命名- 例如:
showConfirmDialog、showSelectSheet、showImageCropperSheet
- 例如:
- 导出规范:
- 在
index.ts中必须导出函数式 API:export { showXxx } from './showXxx' - 禁止导出组件本身:
index.ts中不得导出组件(如export { default as CwSelectSheet } from './CwSelectSheet.vue'),只允许导出函数式 API 和类型 - 函数式 API 必须返回
Promise<Result>,Result 类型必须明确 - 正确的
index.ts格式:// ✅ 正确:只导出函数式 API 和类型 export { showSelectSheet } from './showSelectSheet' export * from './types' // ❌ 错误:禁止导出组件 export { default as CwSelectSheet } from './CwSelectSheet.vue'
- 在
- 实现要求:
- 函数式 API 必须使用
createApp创建独立的 Vue 应用实例 - 必须正确处理组件的生命周期(创建、挂载、卸载)
- 必须等待关闭动画完成后再清理组件(延迟时间与动画时间一致,通常为 300ms)
- 必须正确处理 Promise 的 resolve 和 reject
- 函数式 API 必须使用
- 子组件样式配置规范(函数式 API 组件特殊规则):
- 允许作为 props 的直接属性:函数式 API 组件允许将子组件样式配置作为 props 的直接属性(如
sheetStyles、cropperStyles),而不是必须在styles对象中 - 类型定义示例:
export interface CwSelectSheetProps { options?: CwSelectSheetOption[] styles?: Partial<CwSelectSheetStyles> // ✅ 允许:子组件样式作为 props 的直接属性 sheetStyles?: Partial<CwDynamicSheetInnerStyles> } export interface CwImageCropperSheetProps { styles?: Partial<CwImageCropperSheetStyles> // ✅ 允许:子组件样式作为 props 的直接属性 cropperStyles?: Partial<CwImageCropperStyles> sheetStyles?: Partial<CwDynamicSheetInnerStyles> } - 使用示例:
// ✅ 正确:使用独立的 props 传递子组件样式 await showSelectSheet({ options, sheetStyles: { body: 'custom-body-style' } }) await showImageCropperSheet({ cropperStyles: { container: 'custom-container-style' }, sheetStyles: { body: 'custom-body-style' } }) - 原因:函数式 API 组件使用独立的 props 更直观和简洁,符合函数式调用的习惯
- 注意:普通组件(非函数式 API)仍然必须将子组件样式配置放在
styles对象中
- 允许作为 props 的直接属性:函数式 API 组件允许将子组件样式配置作为 props 的直接属性(如
- 使用示例:
// ✅ 正确:使用函数式 API import { showSelectSheet } from '@/components/CwSelectSheet' const result = await showSelectSheet({ options, title: '选择' }) // ❌ 错误:直接作为组件使用(禁止) import { CwSelectSheet } from '@/components/CwSelectSheet' <CwSelectSheet :options="options" />
3.3 低代码物料 materialMeta.ts(Cw/Tp/Page,强制)
适用范围:
- 在
src/index.ts中导出、且会被 Admin 低代码设计器 物料插件(vite-plugin-component-materials)扫描的 Cw / Tp / Page 组件 - 类型定义:
src/component-material/types.ts(CwComponentMaterialMeta)
文件位置:
- 与
types.ts同级:src/components/CwFoo/materialMeta.ts - 不要写进
.vue的defineOptions,不要在设计器/admin 侧维护容器白名单
必须导出:
import type { CwComponentMaterialMeta } from '../../component-material/types'
export const materialMeta: CwComponentMaterialMeta = {
type: 'component' | 'container',
preview?: { … },
}type 含义(强制二选一):
| 值 | 含义 | 典型示例 |
|----|------|----------|
| 'component' | 叶子组件,画布上不可编排子节点 | CwButton、CwInput、CwTag |
| 'container' | 容器,设计器可向 slot 拖入子组件 | CwFlex、CwCard、CwSplit、CwDiv |
判定原则:能否在低代码画布中接受子节点,与「是否有 default slot」无关。例如 CwButton 虽有 default slot 承载文案,仍属 'component'。
preview(设计器画布展示,按需):
| 字段 | 类型 | 说明 |
|------|------|------|
| minWidth | number | 画布预览最小宽度(px) |
| minHeight | number | 画布预览最小高度(px);容器若含 h-full 子节点,设计器会同步设 height 以免坍缩 |
| slots | string[] | 设计器需挂接/编排内容的 slot 名 |
示例:
// 叶子:文案走 default slot(配合物料 defaultSlotContent,非本文件职责)
export const materialMeta: CwComponentMaterialMeta = {
type: 'component',
preview: { slots: ['default'] },
}
// 容器:默认 slot
export const materialMeta: CwComponentMaterialMeta = {
type: 'container',
preview: { minWidth: 100, minHeight: 160, slots: ['default'] },
}
// 容器:命名 slot(如 CwSplit)
export const materialMeta: CwComponentMaterialMeta = {
type: 'container',
preview: { minWidth: 100, minHeight: 160, slots: ['first', 'second'] },
}与设计器的关系:
- 物料扫描读取
materialMeta.ts→ 写入ComponentMaterialMeta.materialType/preview - 设计器 仅根据
preview设置画布尺寸、挂接 slot,不在 admin 侧写死h-[12rem]等侵入式壳 - 复杂组件可在 Admin
PRESET_MATERIAL_CATALOG做 Preset 包装(简化 props),但type/preview仍以 vue-ui 声明为准
维护命令:
# 为新导出组件补 materialMeta.ts(已存在则跳过)
pnpm --filter zcw-vue-ui run sync:material-meta
# 全量按脚本规则重写(慎用,会覆盖手写 preview)
pnpm --filter zcw-vue-ui run sync:material-meta -- --force新增 Cw/Tp/Page 组件并加入 src/index.ts 导出后,必须补充或运行上述命令生成 materialMeta.ts,否则物料库可能无法正确识别容器/预览尺寸。
4. AI 开发约束(强制)
在使用 AI 辅助开发时,必须严格遵守以下规则:
- 代码格式不可变更:AI 不得修改已确定的组件代码格式
- 导出格式统一:所有
index.ts文件必须使用统一的导出格式 - 自研组件灵活性:Cw/Tp/Page 开头的自研组件必须有自定义逻辑和类型定义
- 目录结构固定:每个组件必须包含
index.ts、types.ts、stories文件和组件文件;参与低代码物料扫描的 Cw/Tp/Page 还须包含materialMeta.ts(见 3.4) - 命名规范:组件名必须以
Tp(重型第三方整合)、Cw(自研)、Bs(业务特定)或Page(页面级)开头 - 子组件导出规范:必须导出所有子组件,确保用户可以正常使用完整的组件功能
- 避免混合导出:严格禁止同时使用命名导出和默认导出,避免构建警告
- 强制使用 tokens 色值:严禁使用 hardcode 色值(如
gray-500、blue-600),必须使用src/assets/tokens/tokens.css中定义的 CSS 变量 - Dialog 和 Sheet 组件规范:Dialog 和 Sheet 类型组件必须只提供函数式 API(
showXxx),禁止直接作为组件使用。函数式 API 必须返回Promise<Result>,并正确处理生命周期和动画清理。禁止在index.ts中导出组件本身,禁止在 Stories 中直接使用组件,只能使用函数式 API
5. 父子组件结构规范(强制)
当组件库中存在父子组件关系时,必须按照以下规范进行组织:
组织原则:
- 子组件归属:将子组件的
.vue文件放置在父组件目录下 - 类型统一:将子组件的类型定义统一到父组件的
types.ts文件中 - 导出集中:在父组件的
index.ts中集中导出父组件和子组件 - Stories 规范:子组件不应该有独立的
.stories.ts文件,只在父组件的 stories 中展示
6. types.ts 文件格式(强制)
6.1 自研组件类型格式(Cw/Tp/Page 开头)
基本格式:
export interface CwComponentProps {
// 自定义属性
title?: string
disabled?: boolean
// ... 其他属性
}
export interface CwComponentEmits {
// 自定义事件
change: [value: any]
click: [event: MouseEvent]
// ... 其他事件
}自研组件类型要求:
- 可以自定义任何类型和接口
- 接口命名必须以
Cw、Tp或Page开头,后跟组件名和Props或Emits - 可以定义复杂的类型结构
- 可以导入和使用第三方类型
- styles 和 texts Props 必须是 Partial 类型(强制):
- 所有组件的
styles和textsProps 类型必须定义为Partial<组件Styles>和Partial<组件Texts> - 原因:组件内部需要对
props.styles与defaultStyles进行浅层合并 - 示例:
export interface CwComponentProps { styles?: Partial<CwComponentStyles> texts?: Partial<CwComponentTexts> // ... 其他属性 }
- 所有组件的
types.ts 文件格式规则
- 自研组件使用
export interface导出自定义类型 - 接口/类型名称必须以
Cw、Tp、Bs或Page开头,后跟组件名称和Props或Emits - 强制要求:所有自研组件的
styles和textsProps 类型必须是Partial<组件Styles>和Partial<组件Texts>
7. stories 文件格式(强制)
7.1 自研组件 Stories 特殊要求
自研组件(Cw/Tp/Page 开头)的 stories 文件必须按照分类组织:
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import CwComponent from './CwComponent.vue'
const meta: Meta<typeof CwComponent> = {
title: 'Custom Components/{Category}/CwComponent', // 使用分类路径
component: CwComponent,
parameters: {
layout: 'centered',
docs: {
description: {
component: '自研组件的详细描述,包括业务场景和使用方法。'
}
}
},
argTypes: {
// 可以定义复杂的 argTypes
onCustomEvent: { action: 'custom-event' }
}
}
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
title: '默认标题'
}
}
export const BusinessScenario: Story = {
args: {
title: '业务场景示例',
data: mockBusinessData
}
}自研组件 Stories 要求:
- 使用
Custom Components/{Category}/{ComponentName}格式 - 可以包含复杂的业务场景示例
- 可以使用 mock 数据展示真实使用场景
- 可以定义详细的组件描述和使用说明
- 分类路径必须与入口文件分类保持一致
7.2 自研组件子组件 Stories 规范(Cw/Tp/Page,强制)
当 Cw/Tp/Page 组件包含子组件时,必须严格遵守以下规范:
类型定义规范:
// 在主组件的 stories 文件中定义子组件类型
type ConversationCellStory = StoryObj<typeof CwChatConversationCell>
type MessageTextStory = StoryObj<typeof CwChatMessageText>
type MessageImageStory = StoryObj<typeof CwChatMessageImage>
// ... 其他子组件类型子组件 Story 定义规范:
// 子组件 story 必须使用其专用类型,而不是通用的 StoryObj
export const ConversationCell: ConversationCellStory = {
args: {
// 必须使用子组件自己的 props,不能使用主组件的 props
name: '张三',
avatar: 'https://example.com/avatar.jpg',
description: '你好,最近怎么样?',
time: '5分钟前',
showRedDot: true
},
render: (args) => ({
components: { CwChatConversationCell },
setup() {
const handleClick = (event: MouseEvent) => {
console.log('Conversation cell clicked:', event)
}
return { args, handleClick }
},
template: `
<div class="w-full p-4">
<CwChatConversationCell
v-bind="args"
@click="handleClick"
/>
</div>
`
})
}类型定义格式:
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import CwMainComponent from './CwMainComponent.vue'
import CwSubComponent1 from './CwSubComponent1.vue'
import CwSubComponent2 from './CwSubComponent2.vue'
const meta: Meta<typeof CwMainComponent> = {
title: 'Custom Components/{Category}/CwMainComponent',
component: CwMainComponent,
// ...
}
export default meta
type Story = StoryObj<typeof meta>
// 为每个子组件定义独立的 Story 类型
type CwSubComponent1Story = StoryObj<typeof CwSubComponent1>
type CwSubComponent2Story = StoryObj<typeof CwSubComponent2>
export const Default: Story = { /* ... */ }
// 子组件 story 必须使用其专用的类型
export const SubComponent1: CwSubComponent1Story = {
render: () => ({
components: { CwSubComponent1 },
setup() {
return {};
},
template: `<CwSubComponent1>Content</CwSubComponent1>`
}),
}
export const SubComponent2: CwSubComponent2Story = {
render: () => ({
components: { CwSubComponent2 },
setup() {
return {};
},
template: `<CwSubComponent2>Content</CwSubComponent2>`
}),
}强制要求:
- 类型定义:必须在 stories 文件中为每个子组件定义独立的 Story 类型,如
type CwSubComponent1Story = StoryObj<typeof CwSubComponent1> - 类型使用:每个子组件 story 必须使用其专用的类型定义,而不是主组件的
Story类型 - 类型安全:不允许使用通用的
StoryObj或主组件的Story类型来定义子组件 story - 独立导出:子组件必须从 index.ts 独立导出,供外部直接使用
- 导入声明:必须在文件顶部正确导入所有子组件
- Props 独立性:子组件 story 的
args必须只包含该子组件自己的 props - 事件处理:子组件的事件处理函数必须符合该子组件的 emits 定义
8. 违规处理
任何违反上述规则的代码都必须立即修正,不允许例外情况。AI 在开发过程中必须:
- 你不需要启动服务器来进行验证,因为我会一直启动一个预览服务,你只需要告诉我你修改完成即可
- 严格检查代码格式是否符合规范
- 确保所有导出格式统一
- 验证 types.ts 文件中的类型定义正确性
- 自研组件确保类型定义完整和准确
- 验证 stories 文件中的 Meta 泛型和响应式要求
- 验证组件功能正常
- 保持代码风格一致性
- 严格检查 tokens 使用:确保所有 Cw/Tp/Page 组件使用 tokens 色值,严禁
gray-500、blue-600等 hardcode 色值 - 严格检查 Tailwind CSS 使用:确保所有 Cw/Tp/Page 组件只使用 Tailwind CSS;功能性 CSS 仅允许
src/assets/components/*.css
