npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

zcw-vue-ui

v1.33.3

Published

本组件库按照前缀进行分组,包含四种类型的组件:

Readme

组件规范,必须绝对遵守此规则。

组件库概述

本组件库按照前缀进行分组,包含四种类型的组件:

  1. 重型第三方整合组件 (Tp 前缀):以 Tp 开头,基于 ECharts、Monaco、Three.js 等重型第三方库进行深度封装与扩展
  2. 自研组件 (Cw 前缀):以 Cw 开头的自主研发组件,实现通用业务需求和自定义功能,具有较高的复用性
  3. 业务特定组件 (Bs 前缀):以 Bs 开头的业务特定组件,面向特定业务场景,复用性较低,通常只在特定页面或业务中使用
  4. 页面级业务组件 (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 分类格式)
  1. 每个组件都需要stories
  2. 需要自定义滚动条的容器必须使用自研组件 CwDiv,与原生 div 用法一致,可直接替换;保持滚动体验与 macOS Chrome 一致
  3. 所有组件都放在src/components目录下
  4. 一个组件都单独生成一个文件夹,包含一个 index.ts、一个 types.ts、一个 stories 文件、组件文件,以及 styles.tstexts.ts 文件(用于样式和文本国际化);参与低代码物料库扫描的 Cw/Tp/Page 组件还须包含 materialMeta.ts(见 3.3 低代码物料 materialMeta.ts
  5. 自研组件(Cw/Tp/Page 开头)必须有自定义逻辑和类型定义
  6. 所有需要双向绑定的组件在 stories 中必须使用 v-model 而不是直接设置 modelValue 属性,需要在 render 函数中通过 ref 创建响应式变量并使用 v-model 绑定
  7. 所有可交互的自研组件必须支持聚焦和键盘操作(强制)
  8. 图标使用规范(强制):所有图标必须使用 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) |

  • 独立 Chathtml.chat-root-viewport-lock 上生效;嵌入 qiankun 不改宿主 html
  • 唯一允许写根字号 px 的文件packages/chat.zengchaowu.com/src/styles/chat-viewport.css
  • 断点 / inject 键:constants/chatLayoutViewport.tsCHAT_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 → 2CW_CHAT_SHELL_MINE_AVATAR_SIZE;16px → 1CW_FILE_ICON_SIZE_COMPOSER_PENDING
  • 非 UI 语义字段(如 UploadFile.size 字节数、并查集 element.size、表单 size="small")不受此限

布局尺寸

  • 优先 Tailwind rem 刻度:h-7gap-2max-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-10z-[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.tssrc/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 + traps
  • bindPopupOverlayLayer:Popup 内自动注册
  • useAppendToBody({ overlayStack: true }):层叠置顶

谁需要注册 Dismiss?

| 类型 | 是否注册 | 接入方式 | | --- | --- | --- | | Dialog / Sheet | 是 | CwDynamicDialogInner / CwDynamicSheetInneruseCwOverlayLayer | | 全屏预览 | 是 | CwDynamicDialogLayer + variant: 'fullscreen' | | Popup 系(含 ContextMenu) | 是 | createCwPopupbindPopupOverlayLayer | | Toast / Hud | 否(maskClosable 时 Hud 临时进栈) | CwDynamicDialogLayer + dismissStack: false;Hud maskClosable: true 时由 Dismiss 栈关层 |

Popup / ContextMenu 挂载

  • modal 档浮层一律挂 document.bodycreateCwPopup / 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](嵌套时跳过仍被子层占用的祖先)。
  • 边界 = panel DOM + traps(如 Popup 的 trigger/bridge)。
  • 禁止在 backdrop 上单独 @click 关层(与栈双关);禁止手写 contains(target) 函数式 hit test。
  • click-outside 在 capture 阶段 判定(Dialog/Sheet 内容区 @click.stop 会挡住 bubble);同一轮 click 仍会继续传到 panel 内菜单项等目标。

命令式浮层(Toast / Hud / 图片预览 / Dialog)

showCwToastshowHudshowCwImageGalleryPreviewshowDynamicDialog 均为 按需 mount:打开时创建 DOM 与 Vue 实例,关闭动画结束后 unmount 并移除容器。每次 visible === true 时须 scheduleBringOverlayHostToFront,保证与同档 modal 层叠顺序正确。

反模式

  • ❌ 仅 overlayStack: true 的全屏层,却不进 Dismiss 栈(多层弹窗必踩坑)

  • ❌ 业务 SFC 里直接 useCwOverlayLayer 或自写 append/dismiss

  • ❌ 为嵌套 Select 单独提高 z-index

  • modal 档CW_OVERLAY_MODAL_Z_CLASSz-[var(--z-index-modal)]),同档内顺序由 DOM 末尾决定

  • toast 档CW_OVERLAY_TOAST_Z_CLASSz-[var(--z-index-toast)]),仅 Toast / Hud 使用,始终盖在 modal 档之上

何时允许使用 z-index

仅当组件(或子树根)脱离常规文档流、且必须压过页面其它区域时,才在该浮层根节点使用语义 z-index token,例如:

  • modal 档--z-index-modal):CwPopupCwDynamicDialog(modal/fullscreen)、CwDynamicSheetCwContextMenuCwImageGalleryPreview
  • toast 档--z-index-toast):CwToastCwHud(须始终可见于任意 Dialog/Sheet 之上)
  • 固定在视口的辅助浮层:如 CwMessageInput@ 提及面板等

浮层内部(遮罩、标题、正文、按钮区等)禁止再为「谁在上」而叠加 z-index:用 DOM 顺序;装饰层用 pointer-events-none

窄幅例外(非全屏弹层)

滚动视口内部,仅靠 DOM 无法满足 sticky / 固定列 / 伪元素阴影时,使用 --z-index-sticky ~ --z-index-scrollbar,例如 CwStickyTabsCwDataTableCwDiv 自绘滚动条统一用 --z-index-scrollbar,须高于固定列 body/header/shadow 以便拖拽。

动态栈深(如 CwStack):同一父级下用 DOM 顺序 叠层,禁止按层递增写内联 zIndex 数字。

上述例外不得滥用到普通布局组件。

右键菜单(showCwContextMenu)

  • 仅命令式 APIshowCwContextMenu({ 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 和键盘支持)

  • 用户可以与其直接交互
  • 示例:CwButtonCwInputCwCheckboxCwRadioCwSliderCwSwitch
  • 必须遵循完整的 focus 和键盘支持规范

2. 容器组件(不需要直接 focus,但需要键盘支持)

  • 包装器组件,通过插槽渲染内容
  • 示例:CwPopupCwDynamicDialog
  • 特点:
    • 不需要 tabindex 和 focus 管理(由内部元素管理)
    • 必须支持键盘操作(如 ESC 关闭)
    • 焦点由 trigger 或其他内部可聚焦元素管理

3. 展示组件(无交互性)

  • 纯展示组件,不需要键盘支持
  • 示例:CwAvatarCwCardCwBadge

组件聚焦类型(重要)

对于"直接交互组件",有两种聚焦方式:

类型1:整个容器聚焦(适用于无标签或标签为组件整体的组件)

  • 示例:CwButtonCwSlider
  • 聚焦样式:rootFocused

类型2:内部元素聚焦(适用于带标签文字的组件或开关)

  • 示例:CwCheckboxCwRadioCwSwitch
  • 聚焦样式:checkboxFocusedradioFocusedtrackFocused
  • 原因:焦点应该显示在图标/开关按钮上,而不是整个容器(包括文字),更符合用户预期

1. 聚焦支持(强制)

所有可聚焦的组件必须:

  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:当 disabledtrue 时,tabindex 必须为 undefined
    • 变量名统一:使用小写 tabindex(不是 tabIndex
    • 对于带标签的组件(如 Checkbox、Radio),焦点应该应用在图标元素本身,而不是整个容器(包括文字)
  2. 添加 outline-none 样式(移除浏览器默认聚焦样式):

    // styles.ts
    root: 'outline-none focus:outline-none',
  3. 添加自定义聚焦样式(必须从 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,
  4. 添加聚焦状态管理(使用 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()
    }

    重要

    • 必须使用 useFocusVisible composable:这确保了只在键盘导航(Tab 键)时显示 focus 样式,鼠标点击时不显示
    • 符合成熟组件库的行为:与 Material-UI、Ant Design 等组件库保持一致
    • handleFocus 必须接收 FocusEvent:用于判断是否是键盘导航导致的聚焦
  5. 应用聚焦样式(必须排除 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)
    • 验证逻辑:如果组件有标签文字,但整个容器显示了聚焦环,则需要将焦点调整到内部元素
  6. handleFocus 必须检查 disabled 状态

    const handleFocus = (event: FocusEvent) => {
      if (props.disabled) return
      handleFocusVisibleFocus(event)
    }

    重要

    • handleFocus 函数必须首先检查 disabled 状态,如果已禁用则直接返回
    • handleFocus 必须接收 FocusEvent 参数,并传递给 handleFocusVisibleFocus

2. 键盘支持(强制)

所有需要键盘操作的组件必须:

  1. 监听键盘事件

    <div @keydown="handleKeyDown">
  2. 实现键盘事件处理函数(必须首先检查 disabled 状态)

    const handleKeyDown = (event: KeyboardEvent) => {
      // 重要:disabled 状态下不允许键盘操作
      if (props.disabled) return
         
      switch (event.key) {
        case 'Enter':
        case ' ': // Space
          event.preventDefault()
          // 执行操作
          break
        // 根据组件功能实现相应的键盘支持
      }
    }

    重要handleKeyDown 函数必须首先检查 props.disabled,如果已禁用则直接返回,不执行任何键盘操作。

  3. 必须阻止默认行为

    event.preventDefault()

3. ARIA 属性支持(强制)

所有组件必须提供完整的 ARIA 支持:

  1. 添加 role 属性

    :role="'button'"  // 或 'slider'、'combobox' 等
  2. 添加 aria 属性

    :aria-disabled="disabled"
    :aria-label="label"
    :aria-describedby="descriptionId"

4. 组件分类规范

不同组件类型的键盘支持要求:

按钮类组件(Button)

  • EnterSpace:触发点击
  • 示例:CwButtonCwIconButton

输入类组件(Input、Select、Slider)

  • ArrowLeft/ArrowRight:左右导航
  • ArrowUp/ArrowDown:上下导航
  • Home/End:跳到开始/结束
  • Escape:关闭/取消
  • 示例:CwSliderCwSelectCwInput

列表类组件(List、Menu)

  • ArrowUp/ArrowDown:导航
  • Enter:选择
  • Home/End:跳到开始/结束
  • 示例:CwDropdownCwMenu

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 Tokensrc/assets/tokens/tokens.css):

  • --focus-ring:键盘聚焦色,默认 var(--brand-color-6);暗色主题见 theme-dark.css
  • 表单控件不要再加 ringinputWrapper 等常有 overflow-hiddenring + ring-offset 会与 border 叠成「双线」或白边

适用组件(示例)

  • 表单:CwInputCwNumberInputCwTextareaCwSearchBarCwSelect(内嵌 CwInput
  • 交互:CwButtonCwCheckboxCwRadioCwSwitchCwSliderCwTabbarCwTag
  • 浮层项:CwDropdown 菜单项、CwSegment 选项

表格内 Checkbox:多选列须 fitContent: trueCwDataTable 对 fitContent 列使用 cellContentFit / overflow-visible,避免外圈被 overflow-hidden 裁切。

选中态 ≠ 聚焦态:如图节点 nodeActiveLowcodeEngine 画布选中等业务高亮,不使用上述聚焦常量。

状态样式选择写法(强制)

  • 所有状态样式的选择必须使用统一三元嵌套写法,禁止使用 || '' 作为回退:
    • 根容器: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?: booleanreadonly?: 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:tabindexundefined(统一使用 undefined
    • Readonly:允许聚焦(保留键盘导航/可读性),但不得改值
  • 样式类名(统一)

    • 根容器:rootDisabledrootReadonly
    • 控件元素:inputDisabled/inputReadonlycheckboxDisabled/checkboxReadonlyradioDisabled/radioReadonlyswitchDisabled/switchReadonlysliderDisabled/sliderReadonlyoptionDisabled
    • 选项元素(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,禁止使用硬编码色值(如 #ffffffrgb()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 / mouseleavehover 型触发(如 CwPopup / CwTooltiptrigger: 'hover')必须与上述门控一致:仅在 matchMedia('(hover: hover) and (pointer: fine)').matches 为真时绑定或响应该类事件。
      • 原因:触摸环境第一次点击常会合成鼠标事件,易误触「悬停才出现」的浮层;统一门控后,触摸为主设备上不会因误合成 mouseenter 弹出说明层。
      • 实现约定:弹出层事件在 usePopupEvents 中已按该媒体查询处理;若新增同类组件,须复用 finePointerHoverMatches()CwPopup/utils/finePointerHoverMedia.ts)或等价逻辑,禁止在无门控下对通用触发区域绑定纯 hover 打开浮层。
      • 移动端需要显式展示说明:使用 trigger: 'click' / manual、或独立文案/UI,不得依赖未门控的 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 传递 disabledreadonly,以便插槽内容按状态自行切换样式
    • 示例:
      • CwInput:<slot name="icon" :disabled="disabled" :readonly="readonly" /><slot name="suffix" ... /><slot name="clear" ... />
      • CwCheckbox/CwRadio:默认插槽 <slot :disabled="disabled" :readonly="readonly" />
  • 清除按钮规范(强制)

    • 清除按钮不可聚焦:清除按钮不得设置 tabindex 属性,禁止通过 Tab 键聚焦
    • 清除按钮交互方式
      • 只能通过鼠标点击清除
      • 清除操作通过键盘快捷键处理:通过各自的键盘 hook(如 useSelectKeyboarduseAutoCompleteKeyboarduseCascaderKeyboard)中的 Backspace/Delete 键处理
    • 清除按钮实现
      • 清除按钮只设置 role="button"@click.stop="handleClear"
      • 不设置 tabindex 属性
      • 不设置 @keydown 事件处理
      • 设置 aria-labelaria-disabled 属性(根据 disabled/readonly 状态)
    • 适用组件:所有带清除功能的组件(CwInput、CwSelect、CwAutoComplete、CwCascader 等)
  • 组件要点

    • CwSelect:Readonly 不打开弹层、不清除、不改值;触发器可聚焦;内置 CwInput 只读
    • CwSlider:Readonly 可聚焦但拖拽与键盘增减无效
    • CwCheckbox/CwRadio/CwSwitch:Readonly 不允许切换;支持 aria-readonly
  • 验证清单

    • Props 存在且默认 false
    • 改值入口均有 if (props.disabled || props.readonly) return
    • Disabled 不可聚焦;Readonly 可聚焦但不改值
    • ARIA:aria-disabled/aria-readonly 同步
    • 样式:rootDisabledrootReadonly 以及子元素 *Disabled/*Readonly 已生效,禁用态不显示 focus/hover/active 高亮

自定义组件命名规范(Cw/Tp/Page 前缀组件)

Cw/Tp/Page 组件分类规范

随着自定义组件数量的增加,Cw/Tp/Page 组件必须按核心能力进行分类管理,便于维护和使用。所有 Cw/Tp/Page 组件必须按照以下分类规则进行组织:

分类原则

  1. 能力导向分类:按组件的核心能力和功能进行分类
  2. 业务场景优先:考虑组件的典型使用场景和业务需求
  3. 技术能力相关:考虑组件提供的技术能力和实现方式
  4. 平台无关:不按平台(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(地图位置)

  • 地图展示、地理位置相关
  • 如:地图组件、位置服务等

分类实施要求

  1. 入口文件分类:在 src/index.ts 中按分类组织导出,先导出所有 Cw 组件,再导出所有 Tp 组件,最后导出所有 Page 组件
  2. 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} (数字前缀用于控制排序)
  3. 新增组件:必须确定合适的分类归属
  4. 分类调整:当组件功能发生变化时,及时调整分类
  5. 命名一致性:分类名称使用英文,保持简洁明了
  6. 规范一致性: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 使用、样式和文本国际化等),与 CwTp 组件规范完全一致
// ✅ 正确示例
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 组件):

  1. 命名:组件 CwFooBar 对应块名 cw-foo-barCw 前缀改为 cw-,其余 PascalCase 转 kebab-case)。
  2. 只加在根容器:块名 class 写在组件最外层原生 DOM 节点(div / span / nav 等)的 classstyles.root 中,用于在 DOM 里定位组件;可不写任何配套 CSS,与 Tailwind 并存。
  3. 禁止子元素 BEM:除根块名外,不要再写 cw-foo-bar__toolbarcw-foo-bar--active 等 Element/Modifier;子区域样式只用 Tailwind(styles.ts 各字段)或 data-* 钩子。
  4. 根节点是其他 Cw 组件时不加:若模板根是 <CwPopup><CwCard><CwTooltip> 等,不要为当前组件再包一层只为加 BEM;由该子组件自己的根 BEM 承担定位。
  5. 编辑器 / 协议钩子例外:ProseMirror、拖拽、单元测试等必须在 DOM 上留名的 class(如 cw-emojicw-file-refdata-cw-*)保留,但不算「块名」,也不扩展到 __ 子元素 BEM。
  6. CwDiv 自绘滚动条:根节点必须有 cw-div;内部 cw-div__viewportcw-div__scrollbar 等为滚动实现与 querySelector 钩子,允许保留(不视为装饰性 BEM)。

样式规范(强制):

  1. 只能使用 Tailwind CSS 类名:所有样式都必须通过 Tailwind CSS 类名实现
  2. 禁止自定义 CSS:不允许在 <style> 标签中编写任何自定义样式
  3. 禁止 SFC <style>:不允许在 .vue 中使用 <style>pnpm run check:no-vue-style 强制)
    • 装饰与布局:Tailwind + styles.ts
    • 纯功能性 CSS(容器查询、:deep 穿透、动画 keyframes):放在 src/assets/components/*.css,在组件 <script> 中 side-effect import
  4. 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))
  1. 强制使用 Design Tokens 色值
    • 严禁 hardcode 色值:禁止 #hexrgb() / rgba()bg-blue-500text-gray-600
    • 严禁 var(--token, #fallback):禁止为 token 写 hex fallback,只写 var(--token)
    • 唯一可写 hex 的位置src/assets/tokens/**(含 tokens.css 与各 token 源文件)
    • 组件内只用语义 tokenbg-[var(--brand-color-6)]text-[var(--text-color-primary)]border-[var(--component-border)] 等;浅色在 :root、深色在 theme-dark.css 仅写与浅色不同的变量(其余继承 :rootpnpm 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
<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 组件样式设计原则:

  1. 布局优先:组件内部只能使用布局相关的class
  2. 样式外部化:所有外观样式通过 styles 对象传入
  3. 文本国际化:所有文本通过 texts 对象传入
  4. 默认值管理:提供默认的 styles.tstexts.ts 文件
  5. 简单合并:使用 { ...defaultStyles, ...props.styles } 方式合并样式和文本

自研组件要求:

  • 必须使用 <script setup lang="ts"> 进行逻辑编写
  • 必须使用 defineOptions({ name: 'CwComponentName' }) 定义组件名称(Cw/Tp/Page 组件都遵循此规范)
  • 可以自定义 Props 和 Emits 类型
  • 可以包含自定义业务逻辑
  • 可以使用任何 Vue 3 Composition API
  • 组件名必须以 CwTpPage 开头(Tp 需满足"重型第三方整合"规则,Page 需满足"页面级业务组件"规则)
  • 必须只使用 Tailwind CSS 进行样式设计(Cw/Tp/Page 组件统一要求)
  • 禁止使用自定义 CSS 样式(Cw/Tp/Page 组件统一要求)
  • 强制使用 tokens 色值:禁止使用 gray-500blue-600 等 hardcode 色值,必须使用 var(--brand-color-6) 等 tokens 变量(Cw/Tp/Page 组件统一要求)

1.4 自研组件纯函数抽离规范(Cw/Tp/Page,强制)

适用场景: 当组件逻辑较为复杂(超过 300 行代码或包含大量计算逻辑)时,必须将纯函数抽离到独立的 functions 目录中。

纯函数定义:

  • 纯函数:不依赖 Vue 响应式状态(refcomputedprops 等),只依赖传入参数的函数
  • 非纯函数:需要访问组件状态、propsemit 等的函数,必须保留在组件中

抽离规范:

  1. 创建 functions 目录

    • 在组件目录下创建 functions 目录
    • 每个纯函数放在独立的 .ts 文件中
    • 使用 functions/index.ts 统一导出所有函数
  2. 纯函数命名规范

    • 使用动词开头,清晰表达函数功能
    • 例如:calculateImageBoundsadjustCropBoxSizegenerateCropHandles
  3. 类型定义

    • 纯函数必须包含完整的 TypeScript 类型定义
    • 输入参数和返回值必须有明确的类型
    • 相关类型定义可以放在函数文件中,或统一放在 functions/types.ts
  4. 必须抽离的纯函数类型

    • ✅ 计算函数:坐标计算、尺寸计算、边界检查等
    • ✅ 数据处理函数:数据转换、格式化、验证等
    • ✅ 工具函数:事件坐标提取、URL 创建/撤销等
    • ✅ 配置生成函数:生成配置对象、样式对象等
    • ❌ 事件处理函数:需要访问 propsemit、响应式状态的函数
    • ❌ 生命周期钩子:onMountedonUnmountedwatch
  5. 目录结构示例

    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
  6. 函数导出和使用

    // functions/index.ts
    export * from './eventCoordinates'
    export * from './imageBounds'
    // ... 其他函数导出
       
    // CwImageCropper.vue
    import {
      getEventCoordinates,
      calculateImageBounds,
      adjustCropBoxSize,
      // ... 其他函数导入
      type CropBox,
      type ImageBounds
    } from './functions'
  7. 组件代码要求

    • 组件代码应专注于 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,强制)

样式规范:

  1. 内部样式限制:组件内部只能有布局相关的class(如 flexgridspace-x-4p-4w-full 等)
  2. 外观样式禁止:禁止在组件内部使用外观样式(如 bg-blue-500text-red-600border-gray-300rounded-lg 等)
  3. 必须使用 tokens禁止使用任何 hardcode 色值,所有颜色必须使用 bg-[var(--brand-color-6)]text-[var(--text-color-primary)] 等 tokens 变量
  4. 样式统一管理:所有样式相关的都放在 styles 对象中,通过 props 传入
  5. 默认样式文件:默认styles对象应该是一个单独的文件(如 styles.ts),组件导入并使用
  6. 样式覆盖:外部可以通过传入自定义styles对象来覆盖默认样式
  7. Props 类型必须是 Partial(强制)
    • 所有组件的 stylestexts Props 类型必须是 Partial<组件Styles>Partial<组件Texts>
    • 原因:组件内部必须对 props.stylesdefaultStyles 进行浅层合并({ ...defaultStyles, ...props.styles }
    • 类型定义示例
      export interface CwComponentProps {
        styles?: Partial<CwComponentStyles>
        texts?: Partial<CwComponentTexts>
      }
    • 组件内部实现
      const styles = computed(() => ({ ...defaultStyles, ...props.styles }))
      const texts = computed(() => ({ ...defaultTexts, ...props.texts }))
    • 好处
      • 外部只需传入需要覆盖的部分样式,不需要提供所有样式
      • 类型安全,支持智能提示
      • 符合组件设计原则:默认值 + 可选覆盖

文本国际化规范:

  1. 内部文本禁止:组件内部不能有硬编码文本,因为所有组件都需要做国际化
  2. 文本统一管理:文本也应该有一个类似styles的对象(如 textslabels),通过 props 传入
  3. 默认文本文件:默认文本对象也应该是一个单独的文件(如 texts.tsdefaultTexts.ts
  4. 文本覆盖:外部可以通过传入自定义文本对象来覆盖默认文本
  5. 禁止硬编码文本:组件内不允许有任何硬编码的中文或英文文本,所有文本都必须通过 textsprops 传入
  6. 简单合并:使用 { ...defaultTexts, ...props.texts } 方式合并,undefined 值会自动被忽略
  7. Props 类型必须是 Partial(强制):与样式规范相同,texts Props 类型也必须是 Partial<组件Texts>

插槽容器规范:

  1. 禁止插槽容器内置样式:插槽容器(包裹 slot 的 div)不应该有硬编码的样式类
  2. 样式外部化:插槽容器的样式应该通过外层容器传入,或者由插槽内容自行控制
  3. 结构纯净:插槽容器只负责提供结构框架,不添加任何装饰性样式

子组件处理:

  1. 独立子组件:可以提供独立的子组件供外部导入使用(如 CwCardTitle、CwCardContent 等)
  2. 子组件样式完整:这些子组件应该包含完整的样式(包括 padding、margin 等),使其可以独立使用
  3. 子组件独立导出:子组件应该在 index.ts 中独立导出,使外部可以直接使用
  4. 不作为默认内容:这些子组件不应该直接写在主组件的 slots 中作为默认内容,而是由外部用户根据需要选择是否使用
  5. Story 展示:每个子组件都应该有独立的 Story 来展示其样式和用法

父组件中使用子组件的样式和文本配置规范(强制):

⚠️ 核心原则(绝对禁止违反):

  • 严禁在父组件的 styles 中直接定义子组件的样式:所有对 Cw/Tp/Page 子组件样式的覆盖必须单独成对象,不能直接写在父组件的 styles 中
  • 必须使用独立的子对象:子组件的样式配置必须作为父组件 props 的独立子对象(如 inputStylesbuttonStyles
  • 必须导入子组件类型:父组件的 types.ts 中必须导入子组件的 styles 和 texts 类型,并使用 Partial<子组件Styles>Partial<子组件Texts> 作为类型约束
  • 支持部分覆盖:由于所有 Cw/Tp/Page 组件内部都会浅合并传入的 styles 和自带的 defaultStyles,所以覆盖时可以只覆盖需要修改的部分样式属性

详细规范:

  1. 子组件配置分离(强制)

    • 当父组件内部使用了 Cw/Tp/Page 子组件时,所有对子组件样式的覆盖必须作为父组件 props 的独立子对象
    • 禁止:在父组件的 styles 中直接定义子组件的样式(如 inputWrapperbutton 等子组件样式)
    • 必须:创建独立的子对象(如 inputStylesbuttonStyles)来配置子组件样式
  2. 命名规范(强制)

    • 子组件的样式配置使用 {子组件简写}Styles 格式(如 inputStylesbuttonStylesiconStyles
    • 子组件的文本配置使用 {子组件简写}Texts 格式(如 inputTextsbuttonTextsiconTexts
    • 命名必须清晰明确,能够一眼看出是哪个子组件的配置
  3. 类型定义(强制)

    • 必须导入子组件类型:在父组件的 types.ts 中,必须从子组件导入对应的 styles 和 texts 类型
    • 使用 Partial<子组件Styles>Partial<子组件Texts> 作为类型约束
    • 类型定义必须是 Partial 类型,因为只需要覆盖部分样式,不需要提供所有样式
  4. 传递规则(强制)

    • 父组件通过 props 接收子组件的配置,然后传递给子组件的 :styles:texts 属性
    • 传递时直接传递子对象(如 :styles="styles.inputStyles"),而不是整个 styles 对象
  5. 默认值处理

    • 子对象是可选的(?),如果不需要自定义子组件配置,可以不传入该子对象,子组件使用其默认配置
    • 由于使用 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 子组件样式的覆盖必须作为独立的子对象(如 inputStylesbuttonStyles
  • 必须导入子组件类型:父组件的 types.ts 中必须导入子组件的 styles 和 texts 类型,确保类型安全
  • 必须使用 Partial 类型:子组件配置必须是 Partial<子组件Styles> 类型,因为只需要覆盖部分样式
  • 支持部分覆盖:由于所有 Cw/Tp/Page 组件内部都会浅合并传入的 styles 和自带的 defaultStyles,可以只覆盖需要修改的部分样式属性
  • 父组件不直接操作子组件的样式:父组件通过子对象的 props 传递配置,保持组件解耦
  • 子组件保持独立性:子组件不知道自己被其他组件使用,保持可复用性
  • 配置类型安全:使用 Partial<子组件类型> 确保类型安全和智能提示
  • 向后兼容:子对象是可选的(?),不传则使用子组件默认配置

函数式 API 组件的特殊说明:

  • Dialog 和 Sheet 类型组件(如 CwSelectSheetCwImageCropperSheetCwConfirmDialog 等)允许将子组件样式配置作为 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.inputStylesstyles.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-500text-blue-600 等 hardcode 色值
  • 所有颜色必须从 src/assets/tokens/tokens.css 中定义的变量中选择
  • 可选的 tokens:--brand-color-*--gray-color-*--text-color-*--error-color-*--warning-color-*--success-color-*

可跟随状态旋转的图标样式规范(强制):

当组件需要使用可跟随状态旋转的图标(如箭头图标,根据弹层打开/关闭状态旋转)时,必须遵循以下规范:

  1. 样式定义规范

    • 基础样式:将所有静态样式(颜色、过渡效果等)整合到 {icon}IconStyles.root 的默认值中
    • 状态样式:使用独立的字符串属性(如 arrowOpen)定义状态样式(如 rotate-180
    • 必须包含过渡效果:在 {icon}IconStyles.root 中必须包含 transition-transform duration-200 等过渡样式,确保旋转动画流畅
  2. 类型定义规范

    export interface CwComponentStyles {
      // ... 其他样式
      /** 状态样式(如旋转角度) */
      arrowOpen: string
      /** CwIcon 样式配置(箭头图标) */
      arrowIconStyles?: Partial<CwIconStyles>
    }
  3. 默认样式定义

    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>,
    }
  4. 组件实现规范

    • 使用 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(' ')
      }
    })
  5. 模板使用

    <CwIcon
      :svg="arrowDownIconSvg"
      :styles="arrowIconStyle"
    />
  6. 规范要点

    • 基础样式整合:所有静态样式(颜色、过渡等)必须整合到 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 前缀),按以下顺序组织:

  1. 先导出所有 Cw 组件(按功能分类)
  2. 再导出所有 Bs 组件(按功能分类)
  3. 再导出所有 Tp 组件(按功能分类)
  4. 最后导出所有 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 类型组件:如 CwConfirmDialogCwDynamicDialog
  • Sheet 类型组件:如 CwSelectSheetCwImageCropperSheet

强制要求:

  1. 只支持函数式 API:Dialog 和 Sheet 类型组件必须只通过函数式 API 使用,禁止直接作为组件使用
  2. 函数命名规范:函数式 API 必须使用 show{ComponentName} 格式命名
    • 例如:showConfirmDialogshowSelectSheetshowImageCropperSheet
  3. 导出规范
    • 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'
  4. 实现要求
    • 函数式 API 必须使用 createApp 创建独立的 Vue 应用实例
    • 必须正确处理组件的生命周期(创建、挂载、卸载)
    • 必须等待关闭动画完成后再清理组件(延迟时间与动画时间一致,通常为 300ms)
    • 必须正确处理 Promise 的 resolve 和 reject
  5. 子组件样式配置规范(函数式 API 组件特殊规则)
    • 允许作为 props 的直接属性:函数式 API 组件允许将子组件样式配置作为 props 的直接属性(如 sheetStylescropperStyles),而不是必须在 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 对象中
  6. 使用示例
    // ✅ 正确:使用函数式 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.tsCwComponentMaterialMeta

文件位置:

  • types.ts 同级:src/components/CwFoo/materialMeta.ts
  • 不要写进 .vuedefineOptions不要在设计器/admin 侧维护容器白名单

必须导出:

import type { CwComponentMaterialMeta } from '../../component-material/types'

export const materialMeta: CwComponentMaterialMeta = {
  type: 'component' | 'container',
  preview?: { … },
}

type 含义(强制二选一):

| 值 | 含义 | 典型示例 | |----|------|----------| | 'component' | 叶子组件,画布上不可编排子节点 | CwButtonCwInputCwTag | | 'container' | 容器,设计器可向 slot 拖入子组件 | CwFlexCwCardCwSplitCwDiv |

判定原则:能否在低代码画布中接受子节点,与「是否有 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 辅助开发时,必须严格遵守以下规则:

  1. 代码格式不可变更:AI 不得修改已确定的组件代码格式
  2. 导出格式统一:所有 index.ts 文件必须使用统一的导出格式
  3. 自研组件灵活性:Cw/Tp/Page 开头的自研组件必须有自定义逻辑和类型定义
  4. 目录结构固定:每个组件必须包含 index.tstypes.tsstories 文件和组件文件;参与低代码物料扫描的 Cw/Tp/Page 还须包含 materialMeta.ts(见 3.4)
  5. 命名规范:组件名必须以 Tp(重型第三方整合)、Cw(自研)、Bs(业务特定)或 Page(页面级)开头
  6. 子组件导出规范:必须导出所有子组件,确保用户可以正常使用完整的组件功能
  7. 避免混合导出:严格禁止同时使用命名导出和默认导出,避免构建警告
  8. 强制使用 tokens 色值严禁使用 hardcode 色值(如 gray-500blue-600),必须使用 src/assets/tokens/tokens.css 中定义的 CSS 变量
  9. Dialog 和 Sheet 组件规范:Dialog 和 Sheet 类型组件必须只提供函数式 API(showXxx),禁止直接作为组件使用。函数式 API 必须返回 Promise<Result>,并正确处理生命周期和动画清理。禁止在 index.ts 中导出组件本身禁止在 Stories 中直接使用组件,只能使用函数式 API

5. 父子组件结构规范(强制)

当组件库中存在父子组件关系时,必须按照以下规范进行组织:

组织原则:

  1. 子组件归属:将子组件的 .vue 文件放置在父组件目录下
  2. 类型统一:将子组件的类型定义统一到父组件的 types.ts 文件中
  3. 导出集中:在父组件的 index.ts 中集中导出父组件和子组件
  4. 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]
  // ... 其他事件
}

自研组件类型要求:

  • 可以自定义任何类型和接口
  • 接口命名必须以 CwTpPage 开头,后跟组件名和 PropsEmits
  • 可以定义复杂的类型结构
  • 可以导入和使用第三方类型
  • styles 和 texts Props 必须是 Partial 类型(强制)
    • 所有组件的 stylestexts Props 类型必须定义为 Partial<组件Styles>Partial<组件Texts>
    • 原因:组件内部需要对 props.stylesdefaultStyles 进行浅层合并
    • 示例:
      export interface CwComponentProps {
        styles?: Partial<CwComponentStyles>
        texts?: Partial<CwComponentTexts>
        // ... 其他属性
      }

types.ts 文件格式规则

  • 自研组件使用 export interface 导出自定义类型
  • 接口/类型名称必须以 CwTpBsPage 开头,后跟组件名称和 PropsEmits
  • 强制要求:所有自研组件的 stylestexts Props 类型必须是 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>`
  }),
}

强制要求:

  1. 类型定义:必须在 stories 文件中为每个子组件定义独立的 Story 类型,如 type CwSubComponent1Story = StoryObj<typeof CwSubComponent1>
  2. 类型使用:每个子组件 story 必须使用其专用的类型定义,而不是主组件的 Story 类型
  3. 类型安全:不允许使用通用的 StoryObj 或主组件的 Story 类型来定义子组件 story
  4. 独立导出:子组件必须从 index.ts 独立导出,供外部直接使用
  5. 导入声明:必须在文件顶部正确导入所有子组件
  6. Props 独立性:子组件 story 的 args 必须只包含该子组件自己的 props
  7. 事件处理:子组件的事件处理函数必须符合该子组件的 emits 定义

8. 违规处理

任何违反上述规则的代码都必须立即修正,不允许例外情况。AI 在开发过程中必须:

  • 你不需要启动服务器来进行验证,因为我会一直启动一个预览服务,你只需要告诉我你修改完成即可
  • 严格检查代码格式是否符合规范
  • 确保所有导出格式统一
  • 验证 types.ts 文件中的类型定义正确性
  • 自研组件确保类型定义完整和准确
  • 验证 stories 文件中的 Meta 泛型和响应式要求
  • 验证组件功能正常
  • 保持代码风格一致性
  • 严格检查 tokens 使用:确保所有 Cw/Tp/Page 组件使用 tokens 色值,严禁 gray-500blue-600 等 hardcode 色值
  • 严格检查 Tailwind CSS 使用:确保所有 Cw/Tp/Page 组件只使用 Tailwind CSS;功能性 CSS 仅允许 src/assets/components/*.css