From d95375693173f6f68107e0596bbc7d9c5da61082 Mon Sep 17 00:00:00 2001 From: wuyiping <42107997+wuyiping0628@users.noreply.github.com> Date: Tue, 26 May 2026 17:35:30 +0800 Subject: [PATCH] fix: supplement development document specifications (#4218) * fix: supplement development document specifications * fix: change skill name * fix: change skill name * fix: add design.skill.md --- .agents/skills/SKILL.md | 27 +-- .agents/skills/common.skill.md | 186 ++++++++++++++++++++ .agents/skills/design.skill.md | 261 +++++++++++++++++++++++++++++ .agents/skills/directive.skill.md | 162 ++++++++++++++++++ .agents/skills/hooks.skill.md | 155 +++++++++++++++++ .agents/skills/renderless.skill.md | 2 +- .agents/skills/utils.skill.md | 122 ++++++++++++++ 7 files changed, 901 insertions(+), 14 deletions(-) create mode 100644 .agents/skills/common.skill.md create mode 100644 .agents/skills/design.skill.md create mode 100644 .agents/skills/directive.skill.md create mode 100644 .agents/skills/hooks.skill.md create mode 100644 .agents/skills/utils.skill.md diff --git a/.agents/skills/SKILL.md b/.agents/skills/SKILL.md index b3009897d..c7fe4d36f 100644 --- a/.agents/skills/SKILL.md +++ b/.agents/skills/SKILL.md @@ -22,16 +22,17 @@ description: tinyvue组件库开发规范 ## 组件库的架构 -| 模块名称 | 代码目录 | 参考规范 | 说明 | -| --------------- | ----------------------------------------------------------------------------- | ----------------------- | ------------------------------------ | -| 模板层 | `packages/vue` | `./vue.skill.md` | 每个组件的模板,必须在相应的组件目录 | -| 无渲染逻辑层 | `packages/renderless` | `./renderless.skill.md` | 每个组件的逻辑,必须在相应的组件目录 | -| 样式层 | `packages/theme` | `./theme.skill.md` | 每个组件的样式,必须在相应的组件目录 | -| 适配Vue2/Vue3层 | `packages/vue-common` | - | 禁止修改适配层的代码 | -| 图标库 | `packages/vue-icon` | `./icon.skill.md` | 图标库 | -| 国际化层 | `packages/vue-locale` | `./locale.skill.md` | 组件的国际化 | -| 公用hooks | `packages/vue-hooks` | `./hooks.skill.md` | 跨组件可以复用的hooks函数 | -| 公用指令 | `packages/vue-directive` | `./directive.skill.md` | 跨组件可以复用的指令 | -| utils | `packages/utils` | `./utils.skill.md` | 公用函数 | -| 组件测试 | `examples\sites\demos\pc\app\**\*.spec.ts` 或 `packages\vue\src\**\__tests__` | `./test.skill.md` | e2e测试和单元测试 | -| 文档开发 | `examples\sites\demos\pc\app\**` | `./docs.skill.md` | 每一个组件的api和demo的开发 | +| 模块名称 | 代码目录 | 参考规范 | 说明 | +| --------------- | ----------------------------------------------------------------------------- | -------------------------- | -------------------------------------------- | +| 模板层 | `packages/vue` | `./vue.skill.md` | 每个组件的模板,必须在相应的组件目录 | +| 无渲染逻辑层 | `packages/renderless` | `./renderless.skill.md` | 每个组件的逻辑,必须在相应的组件目录 | +| 样式层 | `packages/theme` | `./theme.skill.md` | 每个组件的样式,必须在相应的组件目录 | +| 设计规范层 | `packages/design` | `./design.skill.md` | 交互规范、默认 props、图标与 renderless 扩展 | +| 适配Vue2/Vue3层 | `packages/vue-common` | `./common.skill.md` | 非必要不修改适配层代码 | +| 图标库 | `packages/vue-icon` | `./icon.skill.md` | 图标库 | +| 国际化层 | `packages/vue-locale` | `./i18n-workflow.skill.md` | 组件的国际化 | +| 公用hooks | `packages/vue-hooks` | `./hooks.skill.md` | 跨组件可以复用的hooks函数 | +| 公用指令 | `packages/vue-directive` | `./directive.skill.md` | 跨组件可以复用的指令 | +| utils | `packages/utils` | `./utils.skill.md` | 公用函数 | +| 组件测试 | `examples/sites/demos/pc/app/**/*.spec.ts` 或 `packages/vue/src/**/__tests__` | `./testing-guide.skill.md` | e2e测试和单元测试 | +| 文档开发 | `examples/sites/demos/pc/app/**` | `./vue.skill.md` | 每一个组件的 api 和 demo 的开发 | diff --git a/.agents/skills/common.skill.md b/.agents/skills/common.skill.md new file mode 100644 index 000000000..ed6e82f16 --- /dev/null +++ b/.agents/skills/common.skill.md @@ -0,0 +1,186 @@ +# @opentiny/vue-common 适配层开发规范 + +## 适用场景 + +- 理解 TinyVue 如何同时支持 Vue 2.6 / 2.7 / Vue 3 +- 编写或调试组件 `setup`、`$setup`、跨端模板选择 +- 使用 `defineComponent`、`hooks`、`directive`、`svg`、`mergeClass` 等适配 API +- 排查「仅在某一 Vue 版本下复现」的问题 + +## 核心原则 + +### 1. 包定位 + +`packages/vue-common` 发布为 `@opentiny/vue-common`,是整个组件库的 **Vue 版本适配与运行时胶水层**: + +- 通过 `virtual:common/adapter/vue` 在构建时指向 `adapter/vue2` | `vue2.7` | `vue3` +- 向 renderless 提供统一的 `hooks` 对象(即对应 Vue 版本的 API) +- 提供 `$setup` / `setup`,连接模板与 renderless +- 提供主题/模式解析、设计规范注入、图标 `svg` 包装等 + +**修改约束**:表结构变更、新 Vue 版本支持需充分评估;日常组件开发**优先改 renderless / vue 模板**,避免随意改动适配层行为。`SKILL.md` 中亦提示:非必要不修改适配层。 + +### 2. 目录结构 + +```text +packages/vue-common/src/ +├── index.ts # 对外主入口:$setup、setup、$prefix、svg 等 +├── adapter/ +│ ├── index.ts # 导出当前 Vue 版本 adapter +│ ├── vue2/ +│ ├── vue2.7/ +│ └── vue3/ +├── breakpoint.ts # useBreakpoint 响应式断点 +├── csscls.ts # 类名序列化、去重 +├── usedefer.ts # useDefer +└── generateIcon.ts # 渐变图标 id 处理 +``` + +### 3. 组件开发中最常用的 API + +| API | 用途 | +| ---------------------------------- | ----------------------------------------------------------------- | +| `$prefix` | 组件名前缀 `'Tiny'`,如 `TinyButton` | +| `$props` / `props` | 框架保留 prop:`tiny_mode`、`tiny_renderless`、`tiny_template` 等 | +| `defineComponent` | 定义组件(跨版本) | +| `$setup` | 跨端父组件:根据 `tiny_mode` 选择 `pc` / `mobile-first` 模板 | +| `setup` | 子模板中连接 renderless,返回模板绑定对象 | +| `hooks` | 当前 Vue 版本的 API 集合,可传给 renderless 第二参数 | +| `isVue2` / `isVue3` | 版本判断(renderless 中应尽量避免,优先用 utils/vm) | +| `directive` | 统一 Vue2/3 指令钩子名 | +| `svg` | 包装 `@opentiny/vue-theme` 的 svg 为图标组件 | +| `mergeClass` | mobile-first 下合并 Tailwind 类名 | +| `filterAttrs`(模板中 `a`) | 过滤 `$attrs` 绑定 | +| `Teleport` / `KeepAlive` | 须从此包导入以保证兼容 | +| `useBreakpoint` / `useDefer` | 布局与渲染优化 | +| `useInstanceSlots` / `useRelation` | 已注入 `isVue2` 的 hooks 封装 | + +### 4. 双层组件:$setup + 子模板 setup + +**跨端组件**(`pc.vue` + `mobile-first.vue`): + +```typescript +// packages/vue/src/alert/src/index.ts +import { $props, $prefix, $setup, defineComponent } from '@opentiny/vue-common' +import template from 'virtual-template?pc|mobile-first' + +export default defineComponent({ + name: $prefix + 'Alert', + props: alertProps, + setup(props, context) { + return $setup({ props, context, template }) + } +}) +``` + +`$setup` 会: + +1. `resolveMode` 解析 `tiny_mode`(`pc` | `mobile` | `mobile-first`) +2. 通过 `virtual-template` 插件加载对应模板 +3. `renderComponent` 渲染子组件,并合并设计规范 `designConfig` 中的默认 props + +**具体模板**(`pc.vue`): + +```typescript +import { setup, defineComponent, props } from '@opentiny/vue-common' +import { renderless, api } from '@opentiny/vue-renderless/alert/vue' +import type { IAlertApi } from '@opentiny/vue-renderless/types/alert.type' + +export default defineComponent({ + props: [...props, 'type', 'size' /* 其它组件 props */], + setup(props, context) { + return setup({ props, context, renderless, api }) as unknown as IAlertApi + } +}) +``` + +### 5. setup 与 renderless 的协作 + +`setup` 内部流程概要: + +1. 选择 `props.tiny_renderless` 或入参 `renderless` +2. 构造 `utils`(含 `vm`、`emit`、`t`、`designConfig`、`mode`、`mergeClass` 等) +3. 调用 `render(props, hooks, utils, extendOptions)` 得到 `sdk` +4. 按 `api` 数组将 `sdk` 上的方法/状态暴露给模板(`attrs`) +5. 双层组件默认 `mono: false`,将 api 同步到父组件 ref;单层组件传 `mono: true` + +模板中常用简写: + +- `t('ui.xxx')` — 国际化 +- `a($attrs, filters, include)` — `filterAttrs` +- `m(...)` — `mergeClass` +- `f` / `d` / `dp` — 过滤器与实例属性定义(见 renderless.skill.md) + +### 6. 模式与主题解析 + +- **模式** `resolveMode`:`tiny_mode` prop > inject `TinyMode` > 全局 config > 默认 `'pc'` +- **主题** `resolveTheme`:`'tiny'` | `'saas'` +- 根组件可设 `tiny_mode_root` 以 `provide('TinyMode', mode)` + +函数式组件(Modal、Loading、Notify)依赖全局 `tiny_mode`,需在应用级配置。 + +### 7. 设计规范 designConfig + +适配层负责注入与合并 `designConfig`,详细约定见 [design.skill.md](./design.skill.md)。 + +```typescript +import { provideDesignConfig } from '@opentiny/vue-common' + +provideDesignConfig({ + components: { + Button: { props: { round: true }, api: [], renderless: fn } + } +}) +``` + +`setup` 会合并 `designConfig.renderless` 与组件级 props 默认值。 + +### 8. 图标 svg 工厂 + +```typescript +import { svg } from '@opentiny/vue-common' +import IconX from '@opentiny/vue-theme/svgs/icon-x.svg' + +export default () => svg({ name: 'IconX', component: IconX, filledComponent: IconX })() +``` + +详见 [icon.skill.md](./icon.skill.md)。 + +### 9. 版本判断与 hooks 使用 + +```typescript +// ❌ renderless 中 +if (process.env.VUE_VERSION === '3') { ... } + +// ✅ 模板/极少数适配代码 +import { isVue2, hooks } from '@opentiny/vue-common' + +// ✅ renderless 接收第二参数 +export const renderless = (props, { computed, reactive, watch }, utils) => { ... } +``` + +### 10. 禁止事项 + +- ❌ 组件模板中不要 `import from 'vue'`(使用 `@opentiny/vue-common` 的 `defineComponent`、`hooks`、`Teleport` 等) +- ❌ renderless 中不要依赖适配层实现细节(仅使用文档化的 `utils` / `vm` 字段) +- ❌ 不要随意修改 `adapter/` 下生命周期映射逻辑 +- ❌ 不要在 `.vue` 中写复杂业务逻辑,应下沉 renderless + +## 与其它规范的关系 + +| 模块 | 规范文件 | +| ---------- | -------------------------------------------- | +| 模板层 | [vue.skill.md](./vue.skill.md) | +| 逻辑层 | [renderless.skill.md](./renderless.skill.md) | +| 样式 | [theme.skill.md](./theme.skill.md) | +| 设计规范 | [design.skill.md](./design.skill.md) | +| 工具函数 | [utils.skill.md](./utils.skill.md) | +| 组合式逻辑 | [hooks.skill.md](./hooks.skill.md) | +| 指令 | [directive.skill.md](./directive.skill.md) | + +## 参考资源 + +- [主入口 setup / $setup](../../packages/vue-common/src/index.ts) +- [Vue3 适配器](../../packages/vue-common/src/adapter/vue3/index.ts) +- [Alert 跨端入口](../../packages/vue/src/alert/src/index.ts) +- [Alert pc 模板 setup](../../packages/vue/src/alert/src/pc.vue) diff --git a/.agents/skills/design.skill.md b/.agents/skills/design.skill.md new file mode 100644 index 000000000..af6191242 --- /dev/null +++ b/.agents/skills/design.skill.md @@ -0,0 +1,261 @@ +# TinyVue 设计规范(Design)开发指南 + +## 适用场景 + +- 为 Aurora / SaaS / 企业自定义规范配置组件默认行为 +- 通过 `design` 覆盖图标、默认 props、renderless 扩展逻辑 +- 使用 `TinyConfigProvider` 在应用级注入交互规范 +- 在 renderless 中读取 `designConfig` 实现规范差异 + +## 与 Theme 的区别 + +| 维度 | Design(本规范) | Theme(见 [theme.skill.md](./theme.skill.md)) | +| ---------- | -------------------------------------------- | ---------------------------------------------- | +| 职责 | 交互行为、默认 props、图标、renderless 扩展 | 视觉样式、CSS 变量、Less | +| 代码位置 | `packages/design/*`、`designConfig` | `packages/theme`、`packages/theme-saas` | +| 注入方式 | `TinyConfigProvider` / `provideDesignConfig` | 引入 Less、`theme` prop、ThemeTool | +| 运行时对象 | `utils.designConfig`(renderless 第三参数) | `tiny_theme`、`--tv-*` CSS 变量 | + +二者可组合使用:SaaS 场景常见 `tiny_theme="saas"` + `@opentiny/vue-design-saas`。 + +## 架构概览 + +```text +packages/design/ +├── aurora/ # Aurora 规范(@opentiny/vue-design-aurora) +│ ├── index.ts # 导出 { name, version, components } +│ └── src//index.ts +├── saas/ # SaaS 规范(@opentiny/vue-design-saas) +│ └── src//index.ts +└── smb/ # SMB 示例规范(文档 demo 用) + +packages/vue/src/config-provider/ # TinyConfigProvider +packages/vue-common/src/index.ts # provideDesignConfig、getDesignConfig、setup 合并逻辑 +packages/vue-saas-common/ # 预置 customDesignConfig.designConfig = designSaasConfig +``` + +全局配置结构(`DesignConfig`): + +```typescript +{ + name?: string // 规范名称,如 'saas' + version?: string // 规范版本 + components?: { + [ComponentName: string]: IComponentDesignConfig // 键名不含 Tiny 前缀,如 Button、Select、Alert + } +} +``` + +单组件配置(内部类型 `IComponentDesignConfig`,定义于 `packages/renderless/types/shared.type.ts`,未对外 export)常用字段: + +| 字段 | 说明 | +| ------------ | ---------------------------------------------------------------------------------------- | +| `props` | 默认 props;用户未传时才覆盖(仅在跨端父组件 **`$setup`** 中合并到 `customDesignProps`) | +| `icons` | 图标映射,如 `{ warning: iconWarning() }` | +| `state` | 规范级状态默认值,renderless 内通过 `designConfig.state` 读取 | +| `renderless` | 扩展函数,在组件 renderless 执行后合并进 `sdk` | +| `api` | 扩展暴露给模板的 api 名称列表(与组件 `api` 数组合并) | +| 其它 | 组件自定义字段,如 `baseOpts`、`messageType`、`showText` 等 | + +## 应用侧使用 + +### 1. ConfigProvider(推荐) + +```vue + + + +``` + +`TinyConfigProvider` 内部调用 `provideDesignConfig(design)`,子树组件通过 `inject` 获取。 + +### 2. 内置规范包(SaaS / Aurora) + +SaaS 工程使用 `@opentiny/vue-saas-common` 时已注入: + +```typescript +// packages/vue-saas-common/src/index.ts +import { customDesignConfig } from '@opentiny/vue-common' +import designSaasConfig from '@opentiny/vue-design-saas' + +customDesignConfig.designConfig = designSaasConfig +``` + +Aurora 对应 `@opentiny/vue-design-aurora`,在应用入口赋值 `customDesignConfig.designConfig`(写法同 SaaS)。 + +> **注意**:`@opentiny/vue-saas-common` 启动时已写入 `customDesignConfig.designConfig`,会**优先于**子树 `TinyConfigProvider` 的 `inject` 配置。应用级临时覆盖需改 `customDesignConfig`,或勿使用 `vue-saas-common` 的全局注入。普通 `@opentiny/vue` 场景下 `customDesignConfig.designConfig` 默认为 `null`,以 `ConfigProvider` 的 `provide` 为准。 + +### 3. 编程式注入 + +须在组件 `setup` 中调用(需 Vue 上下文): + +```typescript +import { provideDesignConfig } from '@opentiny/vue-common' + +export default { + setup() { + provideDesignConfig({ + components: { + Form: { props: { hideRequiredAsterisk: true } } + } + }) + } +} +``` + +`TinyConfigProvider` 传入的 `design` 为响应式 ref,`getDesignConfig` 会通过 `.value` 解包后读取。 + +## 运行时解析流程 + +1. `getDesignConfig()`:**优先** `customDesignConfig.designConfig`,否则 `inject(design.configKey, {})`;若结果为 ref/computed 则取 `.value` +2. 按当前组件名去掉 `Tiny` 前缀(`getComponentName().replace('Tiny', '')`),取 `globalDesignConfig.components[ComponentName]` +3. **`$setup`**:合并 `designConfig.props` 为默认 props(不覆盖用户已传属性) +4. **`setup`**: + - 将 `designConfig`、`globalDesignConfig` 放入 renderless 第三参数 `utils` + - 执行组件 `renderless` 得到 `sdk` + - 若存在 `designConfig.renderless`,将其返回值 `Object.assign` 到 `sdk` + - 若 `designConfig.api` 存在,与组件 `api` 数组合并后暴露给模板 + +组件名映射示例:`TinySelect` → `Select`,`TinyAlert` → `Alert`。 + +## 在 renderless 中使用 designConfig + +### 1. 从 utils 解构(推荐) + +```typescript +export const renderless = ( + props: IAlertProps, + hooks: ISharedRenderlessParamHooks, + { designConfig, t, emit }: IAlertRenderlessParamUtils +): IAlertApi => { + // ... +} + +// index.ts 纯函数 +export const computedGetIcon = + ({ constants, props, designConfig }: Pick) => + () => { + const designIcon = designConfig?.icons?.[props.type] + return props.icon || designIcon || constants.ICON_MAP[props.type] + } +``` + +### 2. 读取 state / props 级配置 + +```typescript +// select/vue.ts 示例 +autoHideDownIcon: (() => { + if (designConfig?.state && 'autoHideDownIcon' in designConfig.state) { + return designConfig.state.autoHideDownIcon + } + return true +})(), + designConfig // 挂到 state,供模板 state.designConfig?.icons 使用 +``` + +### 3. designConfig.renderless 扩展 + +规范包可覆盖或增补 api 方法,**不得**破坏原有 `api.state` 引用: + +```typescript +// packages/design/saas/src/select/index.ts +// 签名:(props, hooks, utils, sdk) => Partial +renderless: (props, hooks, utils, api) => { + const state = api.state + return { + toggleCheckAll: (filtered) => { + /* 规范定制逻辑 */ + }, + computedShowTagText: () => state.isDisabled || state.isDisplayOnly + } +} +``` + +适配层实际调用:`Object.assign(sdk, designConfig.renderless(props, hooks, utils, sdk))`,第三参为完整 `utils`(含 `emit`、`designConfig`、`vm` 等),第四参为组件 renderless 已返回的 `sdk`。 + +## 新增 / 修改设计规范 + +### 在 packages/design 中增加组件配置 + +1. 在 `packages/design/saas/src//index.ts`(或 `aurora`)新增默认导出对象 +2. 在对应 `index.ts` 的 `components` 中注册(键名与组件名一致,PascalCase,无 `Tiny` 前缀) +3. 图标优先使用对应主题的 icon 包(SaaS 用 `@opentiny/vue-icon-saas`) +4. 在 renderless 中增加对 `designConfig` 的可选读取,并提供合理默认值(无 design 时行为不变) + +示例(SaaS Alert 仅换图标): + +```typescript +// packages/design/saas/src/alert/index.ts +import { iconWarning } from '@opentiny/vue-icon-saas' + +export default { + icons: { + warning: iconWarning() + } +} +``` + +### 在组件 renderless 中支持新 design 字段 + +1. 在 `index.ts` 纯函数中增加 `designConfig` 参数类型(`Pick<..., 'designConfig'>`) +2. 使用可选链与 `in` 判断,避免假设 design 一定存在 +3. 模板需访问时,将 `designConfig` 挂到 `state`(参考 Select) + +## mobile-first 与 twMerge + +SaaS 多端模式依赖 `customDesignConfig.twMerge`(通常 `tailwind-merge`): + +```typescript +import { customDesignConfig } from '@opentiny/vue-common' +import { twMerge } from 'tailwind-merge' + +customDesignConfig.twMerge = twMerge +``` + +`mergeClass`(模板中的 `m(...)`)会经此函数合并 Tailwind 类名。 + +## 禁止事项 + +- ❌ 不要用 design 配置颜色/尺寸等纯样式(应走 theme / CSS 变量) +- ❌ 不要在 design 的 `components` 键名中带 `Tiny` 前缀 +- ❌ 不要在 `designConfig.renderless` 中替换整个 `api` 或 `state` 对象 +- ❌ 不要在 renderless 中 `import` 规范包;规范由应用或 `vue-saas-common` 注入 +- ❌ 不要假设 `designConfig` 一定存在,必须提供默认行为 + +## 参考资源 + +- [ConfigProvider 组件](../../packages/vue/src/config-provider/src/index.vue) +- [适配层 design 逻辑](../../packages/vue-common/src/index.ts) +- [类型定义](../../packages/renderless/types/shared.type.ts) +- [SaaS Select 规范示例](../../packages/design/saas/src/select/index.ts) +- [文档 demo](../../examples/sites/demos/pc/app/config-provider/base-composition-api.vue) +- [主题规范](./theme.skill.md) +- [适配层规范](./common.skill.md) diff --git a/.agents/skills/directive.skill.md b/.agents/skills/directive.skill.md new file mode 100644 index 000000000..e0b46c765 --- /dev/null +++ b/.agents/skills/directive.skill.md @@ -0,0 +1,162 @@ +# @opentiny/vue-directive 开发规范 + +## 适用场景 + +- 模板层需要声明式 DOM 行为(点击外部关闭、无限滚动、文本高亮、自动 Tooltip 等) +- 逻辑与具体组件解耦,可在多个 `pc.vue` / `mobile-first.vue` 中复用 +- 不适合放入 renderless 的、强依赖 DOM 或全局监听的交互 + +## 核心原则 + +### 1. 包定位 + +`packages/vue-directive` 发布为 `@opentiny/vue-directive`。 + +- 指令实现放在 `src/.ts` +- 在 `index.ts` 统一导出 +- 可依赖 `@opentiny/utils`、`@opentiny/vue-common`(如 `auto-tip` 使用 Tooltip 与 `hooks`) +- **业务逻辑仍应优先下沉 renderless**;指令只负责 DOM 绑定与事件桥接 + +### 2. 内置指令一览 + +| 导出 | 指令名 | 说明 | +| ------------------- | ---------------------- | ----------------------------------------------------------- | +| `Clickoutside` | `v-clickoutside` | 点击元素外部触发回调,支持 `.mousedown` / `.mouseup` 修饰符 | +| `AutoTip` | `v-auto-tip` | 文本溢出时自动展示 Tooltip | +| `InfiniteScroll` | `v-infinite-scroll` | 滚动到底加载更多 | +| `HighlightQuery` | `v-highlight-query` | 高亮匹配关键字 | +| `ObserveVisibility` | `v-observe-visibility` | 元素可见性监听 | +| `RepeatClick` | `v-repeat-click` | 长按/重复点击 | + +文档示例:[自定义指令](https://opentiny.design/tiny-vue/zh-CN/smb-theme/components/directives-auto-tip) + +### 3. 在组件模板中注册(必须) + +指令在 **vue 模板层** 注册,不在 renderless 中注册。 + +**Vue 3 写法**(推荐通过 `directive` 适配函数兼容 Vue 2 生命周期名): + +```vue + + + +``` + +**仅含 Vue 2 钩子(`bind`/`inserted`/`unbind`)的指令**,应通过 `directive({ ... })` 包装。部分指令(如 `ObserveVisibility`、`HighlightQuery`)已在实现内同时声明 Vue 2 / Vue 3 钩子,可直接注册:`directives: { ObserveVisibility }`。 + +`directive` 辅助函数位于适配层,用于统一 Vue 2 / Vue 3 指令钩子名: + +```typescript +// packages/vue-common/src/adapter/vue3/index.ts +mapping(content, 'bind', 'beforeMount') +mapping(content, 'inserted', 'mounted') +mapping(content, 'update', 'updated') +mapping(content, 'unbind', 'unmounted') +``` + +### 4. 指令实现规范 + +#### 指令钩子写法 + +**推荐(新指令)**:实现 `bind` / `update` / `unbind`,由 `directive()` 映射为 Vue 3 的 `beforeMount` / `updated` / `unmounted`: + +**特例 `RepeatClick`**:导出为函数而非对象,需手动包装: + +```typescript +import { RepeatClick } from '@opentiny/vue-directive' + +directives: { + repeatClick: { + bind: RepeatClick + } // 模板中使用 v-repeat-click +} +``` + +**特例 `HighlightQuery` / `ObserveVisibility`**:实现内已同时声明 Vue 2 / Vue 3 钩子,可直接 `directives: { HighlightQuery }`,无需再经 `directive()` 转换。 + +新指令推荐模板: + +```typescript +import { on, isServer } from '@opentiny/utils' + +export default { + bind(el, binding, vnode) { + // 初始化 + }, + update(el, binding, vnode) { + // 更新 binding + }, + unbind(el) { + // 清理监听、移除 DOM 副作用 + } +} +``` + +#### 全局监听 + +若需 `document` 级监听(如 `Clickoutside`),在模块顶层用 `isServer` 守卫,维护共享 `nodeList`,在 `unbind` 中务必移除引用,防止泄漏。 + +#### Shadow DOM + +点击外部判断需使用 `event.composedPath()`,参考 `clickoutside.ts`。 + +### 5. 典型用法示例 + +**Clickoutside** + +```html + +
+ +
+
+``` + +**AutoTip** + +```html +
长文本...
+
+``` + +绑定值为 `false` / 空时禁用。模板内文字节点需用标签包裹,且避免指令节点直接包含裸文本节点(详见 `highlight-query.ts` 注释)。 + +**InfiniteScroll** + +```html +
+``` + +### 6. 新增指令流程 + +1. 在 `packages/vue-directive/src/` 新增实现,默认导出指令对象 +2. 在 `packages/vue-directive/index.ts` 具名导出 +3. 在使用的 `pc.vue` / `mobile-first.vue` 中 `import` 并通过 `directives: directive({ ... })` 注册 +4. 优先使用 `@opentiny/utils` 的 `on`/`off`、`throttle`、`getScrollContainer` 等 +5. 若需组件方法,通过 `binding.expression` 或 `binding.value` 与 `vnode.context` 通信(Vue 3 项目注意与 `setup` 返回方法的兼容方式,保持与现有指令一致) +6. 在 `examples/sites/demos` 补充演示(若对用户可见) + +### 7. 禁止事项 + +- ❌ 不得在 renderless 的 `index.ts` 中注册或使用 `v-*` 指令 +- ❌ 不得在指令中编写组件级业务状态机(应通过 binding 调用 renderless 暴露的方法) +- ❌ 不得跳过 `unbind` 清理 +- ❌ 新增指令时避免直接 `import from 'vue'`,DOM 工具走 `@opentiny/utils`,组件/渲染走 `@opentiny/vue-common` + +## 参考资源 + +- [Clickoutside](../../packages/vue-directive/src/clickoutside.ts) +- [AutoTip](../../packages/vue-directive/src/auto-tip.ts) +- [select pc 模板注册示例](../../packages/vue/src/select/src/pc.vue) diff --git a/.agents/skills/hooks.skill.md b/.agents/skills/hooks.skill.md new file mode 100644 index 000000000..a5eac6801 --- /dev/null +++ b/.agents/skills/hooks.skill.md @@ -0,0 +1,155 @@ +# @opentiny/vue-hooks 开发规范 + +## 适用场景 + +- 多个组件共享的组合式逻辑(Popper、弹层、父子关系、窗口尺寸等) +- renderless 的 `vue.ts` 中需要 DOM/生命周期相关能力,但逻辑仍希望可测试、可复用 +- 不宜放入 `@opentiny/utils` 的、需要 Vue 生命周期或响应式 API 的逻辑 + +## 核心原则 + +### 1. 包定位 + +`packages/vue-hooks` 发布为 `@opentiny/vue-hooks`。 + +- **可以**依赖 `@opentiny/utils` +- **不得**直接 `import from 'vue'`——Vue API 由调用方从 `hooks` 解构后传入(柯里化或直接传参,见下文) +- renderless 与 vue-common 通过 renderless 第二参数或合并后的 `hooks` 对象实例化 + +### 2. 现有 Hooks 一览 + +| 导出 | 文件 | 用途 | +| ------------------ | --------------------- | -------------------------------------------- | +| `useEventListener` | `useEventListener.ts` | 绑定/自动清理 DOM 事件,支持 target 为 ref | +| `useWindowSize` | `useWindowSize.ts` | 窗口宽高响应式 | +| `useRect` | `useRect.ts` | 元素尺寸与位置 | +| `useTouch` | `useTouch.ts` | 触摸手势 | +| `useUserAgent` | `useUserAgent.ts` | UA / 浏览器能力判断 | +| `useInstanceSlots` | `useInstanceSlots.ts` | 插槽访问(vue-common 已预绑定 `isVue2`) | +| `useRelation` | `useRelation.ts` | 父子组件关系树(Tabs、Form 等) | +| `useFloating` | `use-floating.ts` | 浮层定位 | +| `useLazyShow` | `use-lazy-show.ts` | 延迟展示 | +| `userPopper` | `vue-popper.ts` | Popper 弹层(注意导出名拼写为 `userPopper`) | +| `usePopup` | `vue-popup.ts` | Popup 弹层管理 | + +### 3. 两种调用形态 + +#### 形态 A:柯里化(多数 hooks) + +第一个参数为 Vue 运行时 API 集合,返回可在 renderless 中调用的函数。适用于 `useEventListener`、`useRelation`、`useRect` 等。 + +```typescript +// packages/vue-hooks/src/useEventListener.ts +import { on, off, isServer } from '@opentiny/utils' + +export const useEventListener = + ({ unref, isRef, watch, nextTick, onMounted, onUnmounted, onActivated, onDeactivated }) => + (type, listener, options = {}) => { + if (isServer) return + // ... 实现 + } +``` + +在 renderless 的 `vue.ts` 中使用: + +```typescript +import { useEventListener } from '@opentiny/vue-hooks' + +export const renderless = (props, hooks, utils) => { + const addListener = useEventListener(hooks) + addListener('scroll', onScroll, { target: scrollRef }) + // ... +} +``` + +在 vue-common 中预绑定 Vue2 差异的示例: + +```typescript +import { useRelation as createUseRelation } from '@opentiny/vue-hooks' +import hooks from './adapter' + +export const useRelation = createUseRelation({ ...hooks, isVue2 }) +``` + +#### 形态 B:直接工厂(`userPopper`、`usePopup`) + +接收合并了 Vue API 与业务上下文的对象,**不是**柯里化: + +```typescript +import { userPopper } from '@opentiny/vue-hooks' + +export const renderless = ( + props, + { watch, reactive, onBeforeUnmount, onDeactivated, nextTick, toRefs }, + { vm, slots, emit } +) => { + const popper = userPopper({ + emit, + nextTick, + onBeforeUnmount, + onDeactivated, + props, + reactive, + vm, + slots, + toRefs, + watch + }) + const state = initState({ reactive, popper }) + // 返回 updatePopper、destroyPopper、doDestroy 及 popper 相关 state 字段 +} +``` + +`usePopup` 用法见 `dialog-box/vue.ts`,返回 `{ open, close, PopupManager, ...toRefs(state) }`。 + +### 4. useRelation 使用要点 + +用于「父收集子、子注册到父」的场景。典型写法(参考 `tabs-mf/vue.ts`): + +```typescript +import { useRelation } from '@opentiny/vue-hooks' + +Object.assign(api, { useRelation: useRelation(hooks) }) + +api.useRelation({ + relationKey: `tabs-${state.tabsId}`, + relationContainer: () => vm.$el.querySelector('[data-tag=tiny-tabs-hidden]'), + childrenKey: 'childTabs', + onChange: () => api.onRelationChange() +}) +// 返回 { children, index, delivery } +``` + +- `relationKey` 必须在父子树中一致 +- 子组件通过 inject 注册,在 `onUnmounted` 时自动 `unlink` +- 也可从 `@opentiny/vue-common` 导入已绑定 `isVue2` 的 `useRelation` + +### 5. userPopper / usePopup + +Popper、Dialog、Select 等浮层在 renderless `vue.ts` 中按**形态 B**调用,将 `popper` / `usePopup` 传入 `initState` 或 `initApi`,勿 `Object.assign` 到整个 `api`。 + +内部依赖 `@opentiny/utils` 的 `PopupManager`、`PopperJS`、`on`/`off` 等,勿在 renderless 重复实现定位逻辑。 + +### 6. 新增 Hook 流程 + +1. 在 `packages/vue-hooks/src/` 新建实现文件 +2. 在 `packages/vue-hooks/index.ts` 导出 +3. 仅使用 `@opentiny/utils` 处理 DOM/工具,Vue API 一律从参数解构 +4. 在 renderless 或 vue-common 中接入并补充类型(`packages/vue-hooks/types/shared.type.ts` 若需扩展) +5. 避免与 utils 中「待改造成 hooks」的模块重复(如 `touch`) + +### 7. 禁止事项 + +- ❌ 不得在 vue-hooks 中 `import { ref } from 'vue'` +- ❌ 不得在 hook 内写组件模板或 JSX +- ❌ 不得被 `packages/utils` 反向依赖 +- ❌ renderless 的 `index.ts` 纯函数文件中不宜直接调用 vue-hooks(应在 `vue.ts` 中组装) + +## 参考资源 + +- [入口导出](../../packages/vue-hooks/index.ts) +- [useEventListener](../../packages/vue-hooks/src/useEventListener.ts) +- [useRelation](../../packages/vue-hooks/src/useRelation.ts) +- [sticky 组件中的组合使用](../../packages/renderless/src/sticky/vue.ts) +- [tabs-mf 中 useRelation](../../packages/renderless/src/tabs-mf/vue.ts) +- [select-dropdown 中 userPopper](../../packages/renderless/src/select-dropdown/vue.ts) diff --git a/.agents/skills/renderless.skill.md b/.agents/skills/renderless.skill.md index 42a902c79..ef0859e7d 100644 --- a/.agents/skills/renderless.skill.md +++ b/.agents/skills/renderless.skill.md @@ -240,7 +240,7 @@ TypeScript 类型安全的开发规范 - 优先使用 TypeScript - 禁止使用 `any`,需要类型逃逸时加注释说明 -- 类型定义放在 `packages/vue-common/src/types/` 或组件目录下 +- 类型定义放在 `packages/renderless/types/` 或组件目录下 ### 开发规范 diff --git a/.agents/skills/utils.skill.md b/.agents/skills/utils.skill.md new file mode 100644 index 000000000..6b44bfe66 --- /dev/null +++ b/.agents/skills/utils.skill.md @@ -0,0 +1,122 @@ +# @opentiny/utils 开发规范 + +## 适用场景 + +- 在 renderless 层或 vue-hooks 中编写与框架无关的纯逻辑 +- 复用日期、字符串、DOM、校验等通用能力 +- 新增跨组件工具函数 + +## 核心原则 + +### 1. 包定位 + +`packages/utils` 发布为 `@opentiny/utils`,**不依赖 Vue**,可被以下模块引用: + +- `packages/renderless`(无渲染逻辑层,主要消费方) +- `packages/vue-hooks` +- `packages/vue-directive` +- `packages/vue-common`(适配层少量使用) + +renderless 层约定:**除 `@opentiny/utils` 与 `@opentiny/vue-hooks` 外,不得依赖其它第三方包**。 + +### 2. 目录结构 + +```text +packages/utils/src/ +├── index.ts # 统一导出入口 +├── array/ # 数组操作 +├── bigInt/ # 大数/精度计算 +├── calendar/ # 日历相关 +├── crypt/ # sha256 等 +├── date/ # 日期格式化、时区 +├── date-util/ # 日期工具(与 date 部分能力重叠,待整理) +├── debounce/ # 防抖 +├── throttle/ # 节流 +├── dom/ # DOM 操作、样式、滚动 +├── decimal/ # 小数精度 +├── event/ # 事件派发 +├── form/ # 表单常量(待迁移至组件内部) +├── function/ # noop、callInterceptor +├── globalConfig/ # isServer、browserInfo、globalConfig +├── logger/ # 日志 +├── nanoid/ # 唯一 ID +├── object/ # 对象拷贝、合并、相等 +├── string/ # 字符串格式化、驼峰/连字符 +├── type/ # 类型判断 +├── validate/ # 校验器 Validator +├── xss/ # XSS 过滤 +├── tree-model/ # 树形数据结构 +├── popper/ # Popper 定位 +├── popup-manager/ # 弹层层级管理 +└── ... # 其它模块见 src/index.ts +``` + +新增工具时:在对应子目录实现,并在 `src/index.ts` 中导出。 + +### 3. 模块编写规范 + +1. **纯函数**:不持有组件状态,不 import Vue API +2. **SSR 安全**:涉及 `window`/`document` 时,使用 `isServer`(来自 `globalConfig`)做守卫 +3. **单职责**:一个文件聚焦一类能力,避免在 utils 中写组件专属常量(`common/`、`form/` 中部分常量标注为「待移除」,新代码勿再扩展) +4. **测试**:在模块目录下补充 `__tests__` 或 `__test__`,在 `packages/utils` 目录执行 `pnpm test`,或在仓库根目录执行 `pnpm --filter @opentiny/utils test` + +### 4. 常用 API 分类 + +| 分类 | 代表导出 | 典型用途 | +| --------- | --------------------------------------------------------------------------------------------------- | -------------------------------------- | +| 类型判断 | `isObject`, `isFunction`, `isDate`, `typeOf` | 参数校验 | +| 字符串 | `camelize`, `hyphenate`, `formatString`, `guid` | 命名转换、展示格式化 | +| 日期 | `formatDate`, `toDateStr`, `limitTimeRange`;`calendar` 的 `parseDate`;`date-util` 的 `parseDate1` | 日期/时间组件(注意两套 parse 勿混用) | +| 对象/数组 | `extend`, `merge`, `isEqual`, `find`, `unique` | 数据处理 | +| DOM | `on`, `off`, `addClass`, `getScrollParent` | 事件与布局 | +| 性能 | `debounce`, `throttle`, `fastdom` | 高频回调、布局批处理 | +| 安全 | `xss`, `sha256` | 内容过滤、摘要 | +| ID | `nanoid`(`nanoid.api.nanoid(size)`) | 无障碍 id、唯一 key | + +### 5. 在 renderless 中的引用方式 + +```typescript +// ✅ 正确:从 @opentiny/utils 按需导入 +import { debounce } from '@opentiny/utils' +import { nanoid } from '@opentiny/utils' + +// 使用 nanoid +const id = `tiny-alert-title-${nanoid.api.nanoid(8)}` + +// ❌ 错误:在 renderless 中 import 'vue' +// ❌ 错误:在 utils 中 import '@opentiny/vue-common' +``` + +### 6. nanoid 使用说明 + +`nanoid` 以命名空间导出,推荐通过 `nanoid.api` 访问: + +```typescript +import { nanoid } from '@opentiny/utils' + +nanoid.api.nanoid(8) // 默认长度 +nanoid.api.customAlphabet('abc', 10) // 自定义字母表 +nanoid.random() // 0~1 随机数(SSR 下返回 0) +``` + +### 7. 禁止事项 + +- ❌ 不得在 utils 中引入 Vue、`@opentiny/vue-common`、`@opentiny/vue-hooks` +- ❌ 不得将仅某一组件使用的常量长期放在 `common/`、`form/`(应下沉到对应 renderless 或组件) +- ❌ 不得重复造轮子:新增前先检索 `src/index.ts` 是否已有同类方法 +- ❌ 注意 `date` 与 `date-util` 存在部分重名导出(如 `toDate` / `toDate1`),优先使用语义清晰的现有 API,避免再增加别名 + +## 构建与发布 + +```bash +# 在 packages/utils 目录 +pnpm build # vite 构建 +pnpm test # vitest +pnpm pub # 发布(维护者) +``` + +## 参考资源 + +- [统一导出](../../packages/utils/src/index.ts) +- [debounce 示例](../../packages/utils/src/debounce/index.ts) +- [renderless 中的引用](../../packages/renderless/src/alert/vue.ts)