fix: supplement development document specifications (#4218)

* fix: supplement development document specifications

* fix: change skill name

* fix: change skill name

* fix: add design.skill.md
This commit is contained in:
wuyiping 2026-05-26 17:35:30 +08:00 committed by GitHub
parent c17736357c
commit d953756931
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 901 additions and 14 deletions

View File

@ -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 的开发 |

View File

@ -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)

View File

@ -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/<Component>/index.ts
├── saas/ # SaaS 规范(@opentiny/vue-design-saas
│ └── src/<Component>/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
<template>
<tiny-config-provider :design="design">
<tiny-alert type="warning" />
<tiny-button>按钮</tiny-button>
</tiny-config-provider>
</template>
<script setup>
import { TinyConfigProvider, TinyAlert, TinyButton } from '@opentiny/vue'
import { iconWarningTriangle } from '@opentiny/vue-icon'
const design = {
name: 'x-design',
version: '1.0.0',
components: {
Alert: {
icons: { warning: iconWarningTriangle() },
props: { center: true },
renderless: (props, hooks, utils, api) => ({
handleClose() {
api.state.show = false
utils.emit('close')
}
})
},
Button: {
props: { round: true, resetTime: 0 }
}
}
}
</script>
```
`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<IAlertRenderlessParams, 'constants' | 'props' | 'designConfig'>) =>
() => {
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<api>
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/<component>/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)

View File

@ -0,0 +1,162 @@
# @opentiny/vue-directive 开发规范
## 适用场景
- 模板层需要声明式 DOM 行为(点击外部关闭、无限滚动、文本高亮、自动 Tooltip 等)
- 逻辑与具体组件解耦,可在多个 `pc.vue` / `mobile-first.vue` 中复用
- 不适合放入 renderless 的、强依赖 DOM 或全局监听的交互
## 核心原则
### 1. 包定位
`packages/vue-directive` 发布为 `@opentiny/vue-directive`
- 指令实现放在 `src/<directive-name>.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
<script lang="ts">
import { setup, directive, defineComponent } from '@opentiny/vue-common'
import { Clickoutside, AutoTip } from '@opentiny/vue-directive'
export default defineComponent({
directives: directive({ Clickoutside, AutoTip }),
setup(props, context) {
return setup({ props, context, renderless, api })
}
})
</script>
<template>
<div v-clickoutside="handleClose" v-auto-tip>...</div>
</template>
```
**仅含 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
<!-- 默认:外部按下并松开才触发 -->
<div v-clickoutside="handleClose"></div>
<!-- 修饰符 -->
<div v-clickoutside.mousedown="handleClose"></div>
<div v-clickoutside.mouseup="handleClose"></div>
```
**AutoTip**
```html
<div v-auto-tip>长文本...</div>
<div v-auto-tip="{ content: '自定义', placement: 'top', effect: 'dark' }"></div>
```
绑定值为 `false` / 空时禁用。模板内文字节点需用标签包裹,且避免指令节点直接包含裸文本节点(详见 `highlight-query.ts` 注释)。
**InfiniteScroll**
```html
<div v-infinite-scroll="loadMore" :infinite-scroll-disabled="loading"></div>
```
### 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)

View File

@ -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)

View File

@ -240,7 +240,7 @@ TypeScript 类型安全的开发规范
- 优先使用 TypeScript
- 禁止使用 `any`,需要类型逃逸时加注释说明
- 类型定义放在 `packages/vue-common/src/types/` 或组件目录下
- 类型定义放在 `packages/renderless/types/` 或组件目录下
### 开发规范

View File

@ -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)