Compare commits
22 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
35cdc71987 | |
|
|
99aae2deea | |
|
|
bda0d05200 | |
|
|
235d22f756 | |
|
|
f74b34c655 | |
|
|
675527e389 | |
|
|
e698434275 | |
|
|
fcd5b71476 | |
|
|
2fce9eede4 | |
|
|
4e618e10b5 | |
|
|
1a42fe5fc0 | |
|
|
974d0ae479 | |
|
|
213ccf6f35 | |
|
|
f7ea2fcec4 | |
|
|
fbfac72b8c | |
|
|
9b54d24c8b | |
|
|
a320ef2449 | |
|
|
96a9e618be | |
|
|
713431b730 | |
|
|
0c322dce8c | |
|
|
466c9c87b0 | |
|
|
0c7c6cd16c |
|
|
@ -1,38 +0,0 @@
|
|||
---
|
||||
name: tinyvue-develop-spec
|
||||
description: tinyvue组件库开发规范
|
||||
---
|
||||
|
||||
## 使用时机
|
||||
|
||||
在当前仓库进行组件的开发,主题开发,国际化开发,测试脚本开发时,必须遵守以下规范。
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 新增 UI 组件
|
||||
- 修改现有组件逻辑
|
||||
- 重构组件架构
|
||||
- 理解 Renderless 模式
|
||||
|
||||
### 重要术语
|
||||
|
||||
1. 跨端模板: tiny vue组件库中,同一个组件可以有2种组件模板, 其中`pc.vue` 面向家用PC浏览器的场景, `mobile-first.vue`是移动优先的浏览器场景(可以兼容用户使用手机或PC浏览器)。
|
||||
|
||||
2. 无渲染逻辑层: tiny vue组件库遵从模板和逻辑分离的原则进行开发。模板层只负责绑定变量和函数,所有的业务逻辑(data 处理、状态计算等)都要放在无渲染逻辑层 `packages/renderless` 目录
|
||||
|
||||
## 组件库的架构
|
||||
|
||||
| 模块名称 | 代码目录 | 参考规范 | 说明 |
|
||||
| --------------- | ----------------------------------------------------------------------------- | -------------------------- | -------------------------------------------- |
|
||||
| 模板层 | `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 的开发 |
|
||||
|
|
@ -1,186 +0,0 @@
|
|||
# @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)
|
||||
|
|
@ -1,261 +0,0 @@
|
|||
# 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)
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
# @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)
|
||||
|
|
@ -1,155 +0,0 @@
|
|||
# @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)
|
||||
|
|
@ -1,484 +0,0 @@
|
|||
# TinyVue 国际化工作流
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 为新组件添加多语言支持
|
||||
- 翻译现有组件的文案
|
||||
- 新增支持的语言包
|
||||
- 统一术语翻译
|
||||
|
||||
## 核心原则
|
||||
|
||||
### 1. 语言文件结构
|
||||
|
||||
TinyVue 使用基于 key-value 的多语言系统:
|
||||
|
||||
```
|
||||
packages/vue-locale/src/lang/
|
||||
├── zh-CN.ts # 简体中文
|
||||
├── en-US.ts # 英文(美国)
|
||||
├── ja-JP.ts # 日文
|
||||
└── ...
|
||||
```
|
||||
|
||||
每个语言文件结构:
|
||||
|
||||
```typescript
|
||||
// packages/vue-locale/src/lang/zh-CN.ts
|
||||
|
||||
export default {
|
||||
ui: {
|
||||
// 组件名称小写
|
||||
button: {
|
||||
confirm: '确定',
|
||||
cancel: '取消'
|
||||
},
|
||||
input: {
|
||||
placeholder: '请输入',
|
||||
clear: '清空',
|
||||
more: '更多',
|
||||
detail: '详情',
|
||||
close: '关闭'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 命名规范
|
||||
|
||||
**Key 命名规则:**
|
||||
|
||||
- 使用 `camelCase`
|
||||
- 按组件分组:`ui.<component>.<key>`
|
||||
- 保持语义清晰,避免缩写
|
||||
|
||||
**示例:**
|
||||
|
||||
```typescript
|
||||
// ✅ 正确
|
||||
ui.datePicker.confirm
|
||||
ui.grid.selectAll
|
||||
ui.input.clear
|
||||
|
||||
// ❌ 错误
|
||||
ui.dp.ok
|
||||
ui.g.sel_all
|
||||
ui.input.clr
|
||||
```
|
||||
|
||||
### 3. 翻译一致性
|
||||
|
||||
同一概念在不同组件中使用相同翻译:
|
||||
|
||||
| 中文 | 英文 | 使用场景 |
|
||||
| -------- | --------- | ---------------- |
|
||||
| 确定 | Confirm | 对话框、确认操作 |
|
||||
| 取消 | Cancel | 对话框、取消操作 |
|
||||
| 保存 | Save | 表单提交 |
|
||||
| 删除 | Delete | 删除操作 |
|
||||
| 编辑 | Edit | 编辑操作 |
|
||||
| 搜索 | Search | 搜索功能 |
|
||||
| 重置 | Reset | 重置表单 |
|
||||
| 加载更多 | Load More | 分页加载 |
|
||||
|
||||
## 标准流程
|
||||
|
||||
### 步骤 1:在组件中使用国际化
|
||||
|
||||
#### Renderless 层
|
||||
|
||||
```typescript
|
||||
// packages/renderless/src/my-component/index.ts
|
||||
|
||||
import { t } from '@opentiny/vue-locale'
|
||||
|
||||
export const api = ({ state, props }) => {
|
||||
return {
|
||||
getPlaceholder: () => {
|
||||
return props.placeholder || t('ui.myComponent.placeholder')
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Vue 视图层
|
||||
|
||||
```vue
|
||||
<!-- packages/vue/src/my-component/src/mobile-first.vue -->
|
||||
|
||||
<script lang="ts">
|
||||
import { useLocale } from '@opentiny/vue-locale'
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const { t } = useLocale()
|
||||
|
||||
return {
|
||||
t
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input :placeholder="t('ui.myComponent.placeholder')" />
|
||||
<button>{{ t('ui.myComponent.confirm') }}</button>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 步骤 2:添加语言包条目
|
||||
|
||||
#### 中文(zh-CN.ts)
|
||||
|
||||
```typescript
|
||||
// packages/vue-locale/src/lang/zh-CN.ts
|
||||
|
||||
export default {
|
||||
ui: {
|
||||
myComponent: {
|
||||
placeholder: '请输入内容',
|
||||
confirm: '确定',
|
||||
cancel: '取消',
|
||||
clear: '清空',
|
||||
noData: '暂无数据',
|
||||
loading: '加载中...',
|
||||
error: '加载失败,请重试'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 英文(en-US.ts)
|
||||
|
||||
```typescript
|
||||
// packages/vue-locale/src/lang/en-US.ts
|
||||
|
||||
export default {
|
||||
ui: {
|
||||
myComponent: {
|
||||
placeholder: 'Please enter content',
|
||||
confirm: 'Confirm',
|
||||
cancel: 'Cancel',
|
||||
clear: 'Clear',
|
||||
noData: 'No data available',
|
||||
loading: 'Loading...',
|
||||
error: 'Failed to load, please try again'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤 3:验证翻译完整性
|
||||
|
||||
确保所有语言包包含相同的 key:
|
||||
|
||||
```bash
|
||||
# 运行检查脚本(如果有的话)
|
||||
pnpm check:i18n
|
||||
|
||||
# 或手动对比
|
||||
diff packages/vue-locale/src/lang/zh-CN.ts packages/vue-locale/src/lang/en-US.ts
|
||||
```
|
||||
|
||||
## 代码示例
|
||||
|
||||
### 动态文本插值
|
||||
|
||||
```typescript
|
||||
// 带参数的翻译
|
||||
t('ui.pagination.total', { total: 100 })
|
||||
|
||||
// 语言文件中定义
|
||||
pagination: {
|
||||
total: '共 {total} 条' // zh-CN
|
||||
total: 'Total {total} items' // en-US
|
||||
}
|
||||
```
|
||||
|
||||
### 条件翻译
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<span>{{
|
||||
state.count === 0 ? t('ui.myComponent.noItems') : t('ui.myComponent.itemsCount', { count: state.count })
|
||||
}}</span>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 完整组件示例
|
||||
|
||||
```typescript
|
||||
// packages/renderless/src/pagination/index.ts
|
||||
|
||||
import { t } from '@opentiny/vue-locale'
|
||||
|
||||
export const api = ({ state, props }) => {
|
||||
return {
|
||||
getTotalText: () => {
|
||||
return t('ui.pagination.total', { total: state.total })
|
||||
},
|
||||
|
||||
getJumpText: () => {
|
||||
return t('ui.pagination.jumpTo')
|
||||
},
|
||||
|
||||
getPageText: (page: number) => {
|
||||
return t('ui.pagination.page', { page })
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```vue
|
||||
<!-- packages/vue/src/pagination/src/mobile-first.vue -->
|
||||
|
||||
<template>
|
||||
<div class="tiny-pagination">
|
||||
<span>{{ getTotalText() }}</span>
|
||||
<button @click="prev">{{ t('ui.pagination.prev') }}</button>
|
||||
<button @click="next">{{ t('ui.pagination.next') }}</button>
|
||||
<span>{{ getJumpText() }} <input v-model="jumpPage" /></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { renderless, api } from '@opentiny/vue-renderless/pagination/vue'
|
||||
import { props, setup, defineComponent } from '@opentiny/vue-common'
|
||||
import { useLocale } from '@opentiny/vue-locale'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TinyPagination',
|
||||
props: [...props, 'total', 'pageSize'],
|
||||
emits: ['update:currentPage', 'change'],
|
||||
setup(props, context) {
|
||||
const { t } = useLocale()
|
||||
const renderlessResult = setup({ props, context, renderless, api })
|
||||
|
||||
return {
|
||||
...renderlessResult,
|
||||
t
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
## 常见陷阱
|
||||
|
||||
### ❌ 错误做法
|
||||
|
||||
1. **硬编码文本**
|
||||
|
||||
```vue
|
||||
<!-- ❌ 错误 -->
|
||||
<button>确定</button>
|
||||
|
||||
<!-- ✅ 正确 -->
|
||||
<button>{{ t('ui.button.confirm') }}</button>
|
||||
```
|
||||
|
||||
2. **拼接翻译字符串**
|
||||
|
||||
```typescript
|
||||
// ❌ 错误
|
||||
const text = t('ui.msg.prefix') + ' ' + value + ' ' + t('ui.msg.suffix')
|
||||
|
||||
// ✅ 正确
|
||||
const text = t('ui.msg.complete', { value })
|
||||
// 语言文件:complete: '{value} 已成功处理'
|
||||
```
|
||||
|
||||
3. **遗漏某些语言包**
|
||||
|
||||
```typescript
|
||||
// ❌ 错误 - 只添加了中文
|
||||
// zh-CN.ts
|
||||
myComponent: {
|
||||
label: '标签'
|
||||
}
|
||||
|
||||
// en-US.ts - 忘记添加
|
||||
// myComponent: { label: 'Label' } ← 缺失
|
||||
|
||||
// ✅ 正确 - 同时更新所有语言包
|
||||
```
|
||||
|
||||
4. **翻译不一致**
|
||||
|
||||
```typescript
|
||||
// ❌ 错误 - 不同组件用不同翻译
|
||||
// button.ts: confirm: '确定'
|
||||
// dialog.ts: ok: '确认'
|
||||
|
||||
// ✅ 正确 - 统一使用
|
||||
// button.ts: confirm: '确定'
|
||||
// dialog.ts: confirm: '确定'
|
||||
```
|
||||
|
||||
### ✅ 最佳实践
|
||||
|
||||
1. **使用常量管理 Key**
|
||||
|
||||
```typescript
|
||||
// packages/vue-locale/src/keys.ts
|
||||
|
||||
export const I18N_KEYS = {
|
||||
BUTTON_CONFIRM: 'ui.button.confirm',
|
||||
BUTTON_CANCEL: 'ui.button.cancel',
|
||||
INPUT_PLACEHOLDER: 'ui.input.placeholder'
|
||||
} as const
|
||||
|
||||
// 使用时
|
||||
t(I18N_KEYS.BUTTON_CONFIRM)
|
||||
```
|
||||
|
||||
2. **提供默认值**
|
||||
|
||||
```typescript
|
||||
// 如果翻译缺失,使用默认值
|
||||
t('ui.myComponent.label', { default: 'Default Label' })
|
||||
```
|
||||
|
||||
3. **懒加载语言包**
|
||||
|
||||
```typescript
|
||||
// 按需加载,减小初始包体积
|
||||
const loadLocale = async (lang: string) => {
|
||||
return import(`@/lang/${lang}.ts`)
|
||||
}
|
||||
```
|
||||
|
||||
4. **记录翻译上下文**
|
||||
|
||||
```typescript
|
||||
// 添加注释说明使用场景
|
||||
myComponent: {
|
||||
// 用于下拉框的空状态提示
|
||||
noData: '暂无数据',
|
||||
|
||||
// 用于加载状态的简短提示
|
||||
loading: '加载中...'
|
||||
}
|
||||
```
|
||||
|
||||
## 翻译质量检查清单
|
||||
|
||||
添加新翻译时,确认:
|
||||
|
||||
### 准确性
|
||||
|
||||
- [ ] 翻译准确表达原意
|
||||
- [ ] 符合目标语言的语法习惯
|
||||
- [ ] 专业术语使用行业标准译法
|
||||
- [ ] 没有机器翻译的生硬感
|
||||
|
||||
### 一致性
|
||||
|
||||
- [ ] 与现有翻译风格一致
|
||||
- [ ] 相同概念使用相同译文
|
||||
- [ ] 标点符号使用规范统一
|
||||
- [ ] 大小写遵循目标语言规范
|
||||
|
||||
### 完整性
|
||||
|
||||
- [ ] 所有语言包都已更新
|
||||
- [ ] 没有遗漏任何 key
|
||||
- [ ] 参数占位符格式正确
|
||||
- [ ] 特殊字符已转义
|
||||
|
||||
### 技术正确性
|
||||
|
||||
- [ ] Key 命名符合规范
|
||||
- [ ] 没有硬编码文本
|
||||
- [ ] 插值参数使用正确
|
||||
- [ ] 类型定义已更新(如需要)
|
||||
|
||||
## 常用术语对照表
|
||||
|
||||
### 通用操作
|
||||
|
||||
| 中文 | 英文 | 备注 |
|
||||
| ---- | --------------- | ---------- |
|
||||
| 确定 | Confirm | 确认操作 |
|
||||
| 取消 | Cancel | 取消操作 |
|
||||
| 保存 | Save | 保存数据 |
|
||||
| 删除 | Delete | 删除项目 |
|
||||
| 编辑 | Edit | 编辑内容 |
|
||||
| 新建 | New / Create | 创建新项目 |
|
||||
| 修改 | Modify / Update | 更新现有项 |
|
||||
| 查询 | Search / Query | 搜索功能 |
|
||||
| 重置 | Reset | 恢复默认 |
|
||||
| 提交 | Submit | 提交表单 |
|
||||
|
||||
### 状态提示
|
||||
|
||||
| 中文 | 英文 | 备注 |
|
||||
| ------ | ----------- | -------- |
|
||||
| 成功 | Success | 操作成功 |
|
||||
| 失败 | Failed | 操作失败 |
|
||||
| 警告 | Warning | 警告信息 |
|
||||
| 错误 | Error | 错误信息 |
|
||||
| 加载中 | Loading | 加载状态 |
|
||||
| 已完成 | Completed | 完成状态 |
|
||||
| 进行中 | In Progress | 进行状态 |
|
||||
|
||||
### 数据相关
|
||||
|
||||
| 中文 | 英文 | 备注 |
|
||||
| -------- | ------------- | --------- |
|
||||
| 暂无数据 | No Data | 空状态 |
|
||||
| 加载更多 | Load More | 分页加载 |
|
||||
| 全部 | All | 全选/全部 |
|
||||
| 当前页 | Current Page | 分页信息 |
|
||||
| 共 X 条 | Total X Items | 总数统计 |
|
||||
| 第 X 页 | Page X | 页码显示 |
|
||||
|
||||
## 新增语言支持
|
||||
|
||||
如需添加新语言(如法语):
|
||||
|
||||
### 步骤 1:创建语言文件
|
||||
|
||||
```typescript
|
||||
// packages/vue-locale/src/lang/fr-FR.ts
|
||||
|
||||
export default {
|
||||
ui: {
|
||||
button: {
|
||||
confirm: 'Confirmer',
|
||||
cancel: 'Annuler'
|
||||
}
|
||||
// ... 复制其他组件的翻译
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤 2:注册语言
|
||||
|
||||
```typescript
|
||||
// packages/vue-locale/src/index.ts
|
||||
|
||||
import frFR from './lang/fr-FR'
|
||||
|
||||
export const locales = {
|
||||
'zh-CN': zhCN,
|
||||
'en-US': enUS,
|
||||
'fr-FR': frFR // 新增
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤 3:更新类型定义
|
||||
|
||||
```typescript
|
||||
// packages/vue-locale/src/types.ts
|
||||
|
||||
export type Locale = 'zh-CN' | 'en-US' | 'fr-FR' // 添加新语言
|
||||
```
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [Vue I18n 官方文档](https://vue-i18n.intlify.dev/)
|
||||
- [语言包目录](../../packages/vue-locale/src/lang/)
|
||||
- [dialog-box 翻译示例](../../packages/vue-locale/src/lang/zh-CN.ts)
|
||||
- [modal.js 翻译规范](../../AGENTS.md#代码注释规范)
|
||||
- [API 文档翻译规范](../../CONTRIBUTING.md)
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
# ICON 开发规范
|
||||
|
||||
## 核心原理
|
||||
|
||||
所有的图标原始文件在 `packages/theme/src/svgs` 目录中,每个svg文件对应一个图标,图标名字为小写连字符的格式。
|
||||
|
||||
图标库的位置在 `packages\vue-icon`, 它依赖 `@opentiny/vue-theme`包,通过`vite-svg-loader`插件,引用svg文件转为标准的Vue组件。
|
||||
|
||||
图标库中的每一个图标都是一个函数,函数中调用适配层的 `svg`函数包装组件一下,返回一个标准的Vue组件。
|
||||
|
||||
```typescript
|
||||
import { svg } from '@opentiny/vue-common'
|
||||
import Acceptance from '@opentiny/vue-theme/svgs/acceptance.svg'
|
||||
|
||||
export default () => svg({ name: 'IconAcceptance', component: Acceptance, filledComponent: Acceptance })()
|
||||
```
|
||||
|
||||
`svg` 函数的路径为: `packages\vue-common\src\index.ts`
|
||||
|
||||
## 图标库的开发步骤
|
||||
|
||||
1. 设计师提供原始的svg图标
|
||||
|
||||
将图标放到 `packages/theme/src/svgs` 目录中,修改文件名为小写连字符的格式,该文件名即为最终的图标名称。
|
||||
|
||||
2. 自动同步图标脚本文件
|
||||
|
||||
在 `internals\automate` 文件夹中打开终端,然后执行脚本,会将 `packages/theme/src/svgs` 目录中的所有图标同步到`packages\vue-icon`中去。
|
||||
|
||||
```bash
|
||||
pnpm run build-svgs
|
||||
```
|
||||
|
||||
3. 发布图标库
|
||||
|
||||
在 `packages\vue-icon`目录中发布即可。
|
||||
|
|
@ -1,274 +0,0 @@
|
|||
# 无渲染逻辑层开发规范
|
||||
|
||||
无渲染逻辑层中,是将一个组件的内部变量和内部方法整合在一起,返回给模板的使用。该模块不应该依赖第三方包,只能依赖 `@opentiny/utils` ,`@opentiny/vue-hooks` 这两个包。
|
||||
|
||||
约定它的必须有2个文件来开发一个组件的逻辑层:
|
||||
|
||||
### Renderless 架构(必须遵守)
|
||||
|
||||
1. 无渲染逻辑层的入口: `packages/renderless/src/<component-name>/vue.ts`
|
||||
|
||||
它主要是导出一个 `renderless`的函数变量供适配层调用。 在每一个组件的 `setup`生命周期中,会调用且只调用一次 `renderless`的函数。 适配层会给`renderless`传入相应的参数, `renderless`的函数返回一个全新的上下文对象 {state,api} 。
|
||||
|
||||
在`renderless`函数中,除了定义 state,api之外,还要处理 `watch`, `computed`等变化监听,也常常要处理 onMounted, onUnmounted 的生命周期需要处理的任务以及组件初始化的准备工作。
|
||||
|
||||
renderless 接收四个参数:
|
||||
|
||||
第一个参数: props, 组件初始化时,Vue运行时生成的props对象
|
||||
第二个参数: 传入Vue2包 或 Vue3包的导出对象,也称其为hooks对象
|
||||
第三个参数: 适配层生成的组件上下文对象。由于要兼容Vue2/3, 所以使用组件内部的对象时,需要使用该对象。
|
||||
第四个参数: 这个是可选参数,如果模板中传入 `extendOptions`属性时, 从这个参数中接收。
|
||||
|
||||
一个完整的rendereless示例如下:
|
||||
|
||||
```typescript
|
||||
import type {
|
||||
IAlertApi,
|
||||
IAlertProps,
|
||||
IAlertState,
|
||||
ISharedRenderlessParamHooks,
|
||||
IAlertRenderlessParamUtils
|
||||
} from '@/types'
|
||||
import {
|
||||
computedGetIcon,
|
||||
computedGetTitle,
|
||||
computedStyle,
|
||||
computedClass,
|
||||
handleClose,
|
||||
handleHeaderClick,
|
||||
watchAutoHide,
|
||||
handlerTargetNode
|
||||
} from './index'
|
||||
import { nanoid } from '@opentiny/utils'
|
||||
|
||||
export const api = ['handleClose', 'state', 'handleHeaderClick']
|
||||
|
||||
const initState = ({ api, computed, constants, reactive }): IAlertState => {
|
||||
return reactive({
|
||||
show: true,
|
||||
contentVisible: false,
|
||||
contentDescribeHeight: 0,
|
||||
contentDefaultHeight: 0,
|
||||
contentMaxHeight: constants.CONTENT_MAXHEUGHT,
|
||||
scrollStatus: false,
|
||||
getIcon: computed(() => api.computedGetIcon()),
|
||||
getTitle: computed(() => api.computedGetTitle()),
|
||||
alertClass: computed(() => api.computedClass()),
|
||||
alertStyle: computed(() => api.computedStyle()),
|
||||
titleId: `tiny-alert-title-${nanoid.api.nanoid(8)}`,
|
||||
contentId: `tiny-alert-description-${nanoid.api.nanoid(8)}`
|
||||
})
|
||||
}
|
||||
|
||||
const initApi = ({ api, state, constants, props, designConfig, t, emit, vm, parent, nextTick, mode }): void => {
|
||||
Object.assign(api, {
|
||||
state,
|
||||
computedGetIcon: computedGetIcon({ constants, props, designConfig }),
|
||||
computedGetTitle: computedGetTitle({ constants, props, t }),
|
||||
computedClass: computedClass({ props, mode }),
|
||||
computedStyle: computedStyle({ props, mode }),
|
||||
handleClose: handleClose({ emit, state }),
|
||||
handleHeaderClick: handleHeaderClick({ state, props, vm }),
|
||||
watchAutoHide: watchAutoHide({ api, props }),
|
||||
handlerTargetNode: handlerTargetNode({ props, parent, vm, nextTick })
|
||||
})
|
||||
}
|
||||
|
||||
const initWatcher = ({ watch, props, api }) => {
|
||||
watch(() => props.autoHide, api.watchAutoHide, { immediate: true })
|
||||
watch(() => props.target, api.handlerTargetNode, { immediate: true })
|
||||
}
|
||||
|
||||
export const renderless = (
|
||||
props: IAlertProps,
|
||||
{ computed, reactive, watch }: ISharedRenderlessParamHooks,
|
||||
{ t, emit, constants, vm, designConfig, parent, nextTick, mode }: IAlertRenderlessParamUtils
|
||||
): IAlertApi => {
|
||||
const api = {} as IAlertApi
|
||||
const state: IAlertState = initState({ api, computed, constants, reactive })
|
||||
initApi({ api, state, constants, props, designConfig, t, emit, vm, parent, nextTick, mode })
|
||||
initWatcher({ watch, props, api })
|
||||
|
||||
return api
|
||||
}
|
||||
```
|
||||
|
||||
#### 适配层生成的组件上下文对象
|
||||
|
||||
renderless函数的第三个参数是适配层生成的组件上下文对象, 它是适配层为了兼容Vue2,Vue3,在组件初始化时,构造了一组相同的上下文对象,来抹平跨框架的差异。
|
||||
|
||||
这个参数是在 `..\packages\vue-common\src\index.ts` 的setup函数中传入的,它包含以下值:
|
||||
|
||||
```typescript
|
||||
// 适配层构造一个组件的vm变量,将Vue实例上的某些值代理出来。
|
||||
const vm = {
|
||||
$attrs: { get: () => $attrs },
|
||||
$children: { get: () => generateChildren(instance.subTree) },
|
||||
$constants: { get: () => instance.props._constants },
|
||||
$emit: { get: () => emit },
|
||||
$el: { get: () => instance.vnode.el },
|
||||
$listeners: { get: () => $listeners },
|
||||
$mode: { get: () => instance._tiny_mode },
|
||||
$nextTick: { get: () => hooks.nextTick },
|
||||
$off: { get: () => $emitter.off },
|
||||
$on: { get: () => $emitter.on },
|
||||
$once: { get: () => $emitter.once },
|
||||
$options: { get: () => ({ componentName: instance.type.componentName }) },
|
||||
$parent: {
|
||||
get: () => instance.parent && createVm({}, getRealParent(instance))
|
||||
},
|
||||
$refs: { get: () => instance.refs },
|
||||
$renderless: { get: () => instance.props.tiny_renderless },
|
||||
$scopedSlots: { get: () => instance.slots },
|
||||
$set: { get: () => $set },
|
||||
$slots: { get: () => instance.slots },
|
||||
$template: { get: () => instance.props.tiny_template }
|
||||
}
|
||||
|
||||
// 该值为传递给renderless第3个参数,所有属性都可以从中解构出来。
|
||||
const utils = {
|
||||
$prefix,
|
||||
t,
|
||||
designConfig,
|
||||
globalDesignConfig,
|
||||
useBreakpoint,
|
||||
mergeClass,
|
||||
framework: 'vue3',
|
||||
vm,
|
||||
emit,
|
||||
emitter,
|
||||
route,
|
||||
router,
|
||||
dispatch,
|
||||
broadcast,
|
||||
parentHandler,
|
||||
childrenHandler,
|
||||
i18n,
|
||||
refs,
|
||||
slots: instance?.slots,
|
||||
scopedSlots: instance?.slots,
|
||||
attrs: context.attrs,
|
||||
parent: parentVm,
|
||||
nextTick: hooks.nextTick,
|
||||
constants: instance?.props._constants,
|
||||
mode,
|
||||
isPCMode: mode === 'pc',
|
||||
isMobileMode: mode === 'mobile',
|
||||
service: root?.$getService ? root?.$getService(vm) : root?.$service,
|
||||
getService: () => root?.$getService(vm),
|
||||
setParentAttribute,
|
||||
defineInstanceProperties,
|
||||
defineParentInstanceProperties
|
||||
}
|
||||
```
|
||||
|
||||
我们观察到,有些属性在`utils`下存在,在`vm`下也存在。建议使用 `vm`下的相应值,比如 `vm.$refs, vm.$slots` 。
|
||||
|
||||
2. 辅助方法与函数
|
||||
|
||||
- **业务逻辑** → `packages/renderless/src/<component-name>/index.ts`
|
||||
1. 它们都是纯函数,不依赖任何 UI 框架,不依赖其它上下文
|
||||
2. 在`index.ts`文件中, 禁止直接导入 Vue API(ref、reactive、watch 等)
|
||||
3. 每一个函数,通常都是要接收{state, api, props} 等传入的变量,生成一个全新的函数变量。这个函数只与当前组件产生互操作。
|
||||
4. 所有的函数不要互相调用,通常应该是通过 api 变量来调用其它函数。
|
||||
|
||||
以下是一个简单的纯函数示例:
|
||||
|
||||
```typescript
|
||||
export const watchAutoHide =
|
||||
({ api, props }: Pick<IAlertRenderlessParams, 'api' | 'props'>) =>
|
||||
(newVal: boolean) => {
|
||||
if (props.autoHide && newVal) {
|
||||
const timer = setTimeout(() => {
|
||||
api.handleClose()
|
||||
clearTimeout(timer)
|
||||
}, ALERT_TIMEOUT)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. 类型声明
|
||||
|
||||
- **类型声明** → `packages/renderless/types/<component-name>.type.ts`
|
||||
|
||||
在 `types` 文件夹中,为每一个组件添加一个声明文件,这样在逻辑层代码开发,以及模板绑定属性和方法时,才会有正确的类型提示。
|
||||
|
||||
以下是一个类型声明的示例:
|
||||
|
||||
```typescript
|
||||
import type { ExtractPropTypes, CSSProperties } from 'vue'
|
||||
import type { alertProps, $constants } from '@/alert/src'
|
||||
import type { ISharedRenderlessFunctionParams, ISharedRenderlessParamUtils } from './shared.type'
|
||||
|
||||
export interface IAlertState {
|
||||
show: boolean
|
||||
getIcon: string
|
||||
getTitle: string
|
||||
contentVisible: boolean
|
||||
contentDescribeHeight: number
|
||||
contentDefaultHeight: number
|
||||
contentMaxHeight: number
|
||||
scrollStatus: boolean
|
||||
titleId: string
|
||||
contentId: string
|
||||
}
|
||||
|
||||
export type IAlertProps = ExtractPropTypes<typeof alertProps>
|
||||
|
||||
export type IAlertConstants = typeof $constants
|
||||
|
||||
export type IAlertRenderlessParams = ISharedRenderlessFunctionParams<IAlertConstants> & {
|
||||
api: IAlertApi
|
||||
state: IAlertState
|
||||
props: IAlertProps
|
||||
}
|
||||
|
||||
export interface IAlertApi {
|
||||
state: IAlertState
|
||||
computedGetIcon: () => string
|
||||
computedGetTitle: () => string
|
||||
handleClose: () => void
|
||||
handleHeaderClick: () => void
|
||||
watchAutoHide: (value: boolean) => void
|
||||
computedStyle: () => CSSProperties
|
||||
}
|
||||
|
||||
export type IAlertRenderlessParamUtils = ISharedRenderlessParamUtils<IAlertConstants>
|
||||
```
|
||||
|
||||
TypeScript 类型安全的开发规范
|
||||
|
||||
- 优先使用 TypeScript
|
||||
- 禁止使用 `any`,需要类型逃逸时加注释说明
|
||||
- 类型定义放在 `packages/renderless/types/` 或组件目录下
|
||||
|
||||
### 开发规范
|
||||
|
||||
- 一份 renderless 逻辑同时服务 Vue 2 和 Vue 3
|
||||
- 如需判断框架版本,使用 `@opentiny/vue-common` 中的工具函数, 不得在 renderless 层写 `if (vue3)` 这样的判断
|
||||
- 应该尽量避免直接操作dom
|
||||
- 尽量编写相应的类型声明, 包含 props, state, api下的属性和方法的签名。
|
||||
- 不要引用 'vue' 包,应该从renderless的第2个参数中,获取vue包下的变量。
|
||||
|
||||
**在 renderless 层使用 Vue API**
|
||||
|
||||
```typescript
|
||||
// ❌ 错误
|
||||
import { ref } from 'vue'
|
||||
const count = ref(0)
|
||||
|
||||
// ✅ 正确方式 1
|
||||
import { hooks } from '@opentiny/vue-common'
|
||||
const state = hooks.ref({ count: 0 })
|
||||
|
||||
// ✅ 正确方式 2
|
||||
export const renderless = (props, { ref }) => {
|
||||
const state = hooks.ref({ count: 0 })
|
||||
}
|
||||
```
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [vue.ts](../../packages/renderless/src/button/vue.ts)
|
||||
- [index.ts](../../packages/renderless/src/button/index.ts)
|
||||
- [button.type.ts](../../packages/renderless/types/button.type.ts)
|
||||
|
|
@ -1,522 +0,0 @@
|
|||
# TinyVue 测试编写指南
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 为新组件编写测试
|
||||
- 为现有组件补充测试用例
|
||||
- 修复 Bug 时添加回归测试
|
||||
- 重构代码后验证功能
|
||||
|
||||
## 核心原则
|
||||
|
||||
### 1. 测试金字塔
|
||||
|
||||
```
|
||||
/\
|
||||
/ \ E2E 测试(少量,覆盖关键流程)
|
||||
/----\
|
||||
/ \ 集成测试(适量,测试组件交互)
|
||||
/--------\
|
||||
/ \ 单元测试(大量,测试单个函数/组件)
|
||||
/------------\
|
||||
```
|
||||
|
||||
- **单元测试**:Vitest,测试 renderless 层的纯函数
|
||||
- **E2E 测试**:Playwright,测试完整用户流程
|
||||
- **比例建议**:70% 单元 + 30% E2E
|
||||
|
||||
### 2. 测试命名规范
|
||||
|
||||
```typescript
|
||||
// 格式:should + 预期行为 + when + 条件
|
||||
it('should emit change event when value is updated', () => {})
|
||||
it('should not submit form when validation fails', () => {})
|
||||
it('should display error message when input is invalid', () => {})
|
||||
```
|
||||
|
||||
### 3. AAA 模式
|
||||
|
||||
每个测试用例遵循 **Arrange-Act-Assert**:
|
||||
|
||||
```typescript
|
||||
it('should calculate total price correctly', () => {
|
||||
// Arrange - 准备数据
|
||||
const items = [
|
||||
{ price: 100, quantity: 2 },
|
||||
{ price: 50, quantity: 1 }
|
||||
]
|
||||
|
||||
// Act - 执行操作
|
||||
const total = calculateTotal(items)
|
||||
|
||||
// Assert - 验证结果
|
||||
expect(total).toBe(250)
|
||||
})
|
||||
```
|
||||
|
||||
## 标准流程
|
||||
|
||||
### 单元测试(Vitest)
|
||||
|
||||
#### 步骤 1:创建测试文件
|
||||
|
||||
```typescript
|
||||
// packages/renderless/src/my-component/__tests__/index.spec.ts
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { api, renderless } from '../index'
|
||||
|
||||
describe('MyComponent Renderless', () => {
|
||||
// 测试用例
|
||||
})
|
||||
```
|
||||
|
||||
#### 步骤 2:编写基础测试
|
||||
|
||||
```typescript
|
||||
describe('api', () => {
|
||||
it('should return correct methods', () => {
|
||||
const state = { value: '' }
|
||||
const props = { disabled: false }
|
||||
const emit = vi.fn()
|
||||
|
||||
const result = api({ state, props, emit })
|
||||
|
||||
expect(result).toHaveProperty('handleClick')
|
||||
expect(result).toHaveProperty('updateValue')
|
||||
})
|
||||
|
||||
it('should emit click event with current value', () => {
|
||||
const state = { value: 'test' }
|
||||
const props = { disabled: false }
|
||||
const emit = vi.fn()
|
||||
|
||||
const { handleClick } = api({ state, props, emit })
|
||||
handleClick()
|
||||
|
||||
expect(emit).toHaveBeenCalledWith('click', 'test')
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
#### 步骤 3:边界情况测试
|
||||
|
||||
```typescript
|
||||
describe('edge cases', () => {
|
||||
it('should not emit when disabled', () => {
|
||||
const state = { value: 'test' }
|
||||
const props = { disabled: true }
|
||||
const emit = vi.fn()
|
||||
|
||||
const { handleClick } = api({ state, props, emit })
|
||||
handleClick()
|
||||
|
||||
expect(emit).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle empty value', () => {
|
||||
const state = { value: '' }
|
||||
const props = { disabled: false }
|
||||
const emit = vi.fn()
|
||||
|
||||
const { updateValue } = api({ state, props, emit })
|
||||
updateValue('')
|
||||
|
||||
expect(state.value).toBe('')
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### E2E 测试(Playwright)
|
||||
|
||||
#### 步骤 1:创建测试文件
|
||||
|
||||
```typescript
|
||||
// examples/sites/demos/my-component/test/my-component.spec.ts
|
||||
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('MyComponent E2E', () => {
|
||||
// 测试用例
|
||||
})
|
||||
```
|
||||
|
||||
#### 步骤 2:编写交互测试
|
||||
|
||||
```typescript
|
||||
test('should click button and show result', async ({ page }) => {
|
||||
// 导航到示例页面
|
||||
await page.goto('/my-component/basic')
|
||||
|
||||
// 找到按钮并点击
|
||||
const button = page.getByRole('button', { name: '点击我' })
|
||||
await button.click()
|
||||
|
||||
// 验证结果显示
|
||||
const result = page.getByText('操作成功')
|
||||
await expect(result).toBeVisible()
|
||||
})
|
||||
```
|
||||
|
||||
#### 步骤 3:视觉回归测试
|
||||
|
||||
```typescript
|
||||
test('should render correctly', async ({ page }) => {
|
||||
await page.goto('/my-component/basic')
|
||||
|
||||
// 截取整个页面
|
||||
await expect(page).toHaveScreenshot('my-component-basic.png', {
|
||||
maxDiffPixels: 100
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## 代码示例
|
||||
|
||||
### Vitest 完整示例
|
||||
|
||||
```typescript
|
||||
// packages/renderless/src/input/__tests__/index.spec.ts
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { api, renderless } from '../index'
|
||||
|
||||
describe('Input Renderless', () => {
|
||||
let mockState: any
|
||||
let mockProps: any
|
||||
let mockEmit: any
|
||||
|
||||
beforeEach(() => {
|
||||
mockState = {
|
||||
value: '',
|
||||
hovering: false,
|
||||
focused: false
|
||||
}
|
||||
|
||||
mockProps = {
|
||||
modelValue: '',
|
||||
disabled: false,
|
||||
readonly: false,
|
||||
placeholder: '请输入'
|
||||
}
|
||||
|
||||
mockEmit = vi.fn()
|
||||
})
|
||||
|
||||
describe('handleInput', () => {
|
||||
it('should update state value', () => {
|
||||
const { handleInput } = api({
|
||||
state: mockState,
|
||||
props: mockProps,
|
||||
emit: mockEmit
|
||||
})
|
||||
|
||||
handleInput('new value')
|
||||
|
||||
expect(mockState.value).toBe('new value')
|
||||
})
|
||||
|
||||
it('should emit update:modelValue event', () => {
|
||||
const { handleInput } = api({
|
||||
state: mockState,
|
||||
props: mockProps,
|
||||
emit: mockEmit
|
||||
})
|
||||
|
||||
handleInput('test')
|
||||
|
||||
expect(mockEmit).toHaveBeenCalledWith('update:modelValue', 'test')
|
||||
})
|
||||
|
||||
it('should not emit when value unchanged', () => {
|
||||
mockState.value = 'existing'
|
||||
mockProps.modelValue = 'existing'
|
||||
|
||||
const { handleInput } = api({
|
||||
state: mockState,
|
||||
props: mockProps,
|
||||
emit: mockEmit
|
||||
})
|
||||
|
||||
handleInput('existing')
|
||||
|
||||
expect(mockEmit).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleFocus', () => {
|
||||
it('should set focused to true', () => {
|
||||
const { handleFocus } = api({
|
||||
state: mockState,
|
||||
props: mockProps,
|
||||
emit: mockEmit
|
||||
})
|
||||
|
||||
handleFocus()
|
||||
|
||||
expect(mockState.focused).toBe(true)
|
||||
})
|
||||
|
||||
it('should emit focus event', () => {
|
||||
const { handleFocus } = api({
|
||||
state: mockState,
|
||||
props: mockProps,
|
||||
emit: mockEmit
|
||||
})
|
||||
|
||||
handleFocus()
|
||||
|
||||
expect(mockEmit).toHaveBeenCalledWith('focus')
|
||||
})
|
||||
})
|
||||
|
||||
describe('clear', () => {
|
||||
it('should clear value and emit events', () => {
|
||||
mockState.value = 'some text'
|
||||
|
||||
const { clear } = api({
|
||||
state: mockState,
|
||||
props: mockProps,
|
||||
emit: mockEmit
|
||||
})
|
||||
|
||||
clear()
|
||||
|
||||
expect(mockState.value).toBe('')
|
||||
expect(mockEmit).toHaveBeenCalledWith('update:modelValue', '')
|
||||
expect(mockEmit).toHaveBeenCalledWith('clear')
|
||||
})
|
||||
|
||||
it('should not clear when disabled', () => {
|
||||
mockProps.disabled = true
|
||||
mockState.value = 'some text'
|
||||
|
||||
const { clear } = api({
|
||||
state: mockState,
|
||||
props: mockProps,
|
||||
emit: mockEmit
|
||||
})
|
||||
|
||||
clear()
|
||||
|
||||
expect(mockState.value).toBe('some text')
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Playwright 完整示例
|
||||
|
||||
```typescript
|
||||
// examples/sites/demos/input/test/input.spec.ts
|
||||
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('Input Component', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/input/basic')
|
||||
})
|
||||
|
||||
test('should render input correctly', async ({ page }) => {
|
||||
const input = page.getByRole('textbox')
|
||||
await expect(input).toBeVisible()
|
||||
await expect(input).toHaveAttribute('placeholder', '请输入')
|
||||
})
|
||||
|
||||
test('should handle user input', async ({ page }) => {
|
||||
const input = page.getByRole('textbox')
|
||||
await input.fill('Hello World')
|
||||
|
||||
await expect(input).toHaveValue('Hello World')
|
||||
})
|
||||
|
||||
test('should clear input when clear button clicked', async ({ page }) => {
|
||||
const input = page.getByRole('textbox')
|
||||
await input.fill('Test')
|
||||
|
||||
const clearButton = page.getByRole('button', { name: 'clear' })
|
||||
await clearButton.click()
|
||||
|
||||
await expect(input).toHaveValue('')
|
||||
})
|
||||
|
||||
test('should not accept input when disabled', async ({ page }) => {
|
||||
await page.goto('/input/disabled')
|
||||
|
||||
const input = page.getByRole('textbox')
|
||||
await expect(input).toBeDisabled()
|
||||
|
||||
await input.fill('Should not work')
|
||||
await expect(input).toHaveValue('')
|
||||
})
|
||||
|
||||
test('should show word limit', async ({ page }) => {
|
||||
await page.goto('/input/word-limit')
|
||||
|
||||
const input = page.getByRole('textbox')
|
||||
await input.fill('12345')
|
||||
|
||||
const wordLimit = page.getByText('5/10')
|
||||
await expect(wordLimit).toBeVisible()
|
||||
})
|
||||
|
||||
test('should validate required field', async ({ page }) => {
|
||||
await page.goto('/input/validation')
|
||||
|
||||
const submitButton = page.getByRole('button', { name: '提交' })
|
||||
await submitButton.click()
|
||||
|
||||
const errorMsg = page.getByText('此字段为必填项')
|
||||
await expect(errorMsg).toBeVisible()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## 常见陷阱
|
||||
|
||||
### ❌ 错误做法
|
||||
|
||||
1. **测试实现细节而非行为**
|
||||
|
||||
```typescript
|
||||
// ❌ 错误 - 测试内部状态
|
||||
it('should set state.hovering to true', () => {
|
||||
expect(state.hovering).toBe(true)
|
||||
})
|
||||
|
||||
// ✅ 正确 - 测试可见行为
|
||||
it('should show tooltip on hover', async () => {
|
||||
await element.hover()
|
||||
await expect(tooltip).toBeVisible()
|
||||
})
|
||||
```
|
||||
|
||||
2. **测试之间相互依赖**
|
||||
|
||||
```typescript
|
||||
// ❌ 错误
|
||||
it('step 1: login', () => {})
|
||||
it('step 2: navigate', () => {}) // 依赖 step 1
|
||||
|
||||
// ✅ 正确 - 每个测试独立
|
||||
it('should navigate after login', async () => {
|
||||
await login()
|
||||
await navigate()
|
||||
// 验证
|
||||
})
|
||||
```
|
||||
|
||||
3. **使用硬编码等待时间**
|
||||
|
||||
```typescript
|
||||
// ❌ 错误
|
||||
await page.waitForTimeout(5000)
|
||||
|
||||
// ✅ 正确
|
||||
await expect(element).toBeVisible({ timeout: 5000 })
|
||||
```
|
||||
|
||||
4. **忽略异步操作**
|
||||
|
||||
```typescript
|
||||
// ❌ 错误
|
||||
button.click()
|
||||
expect(result).toBe('done')
|
||||
|
||||
// ✅ 正确
|
||||
await button.click()
|
||||
await expect(result).toHaveText('done')
|
||||
```
|
||||
|
||||
### ✅ 最佳实践
|
||||
|
||||
1. **使用 beforeEach 重置状态**
|
||||
|
||||
```typescript
|
||||
beforeEach(() => {
|
||||
mockState = { value: '' }
|
||||
mockEmit = vi.fn()
|
||||
})
|
||||
```
|
||||
|
||||
2. **测试失败时提供清晰信息**
|
||||
|
||||
```typescript
|
||||
expect(result).toBe(expected)
|
||||
// 失败时会显示:Expected "expected" but received "actual"
|
||||
```
|
||||
|
||||
3. **Mock 外部依赖**
|
||||
|
||||
```typescript
|
||||
vi.mock('@opentiny/utils', () => ({
|
||||
debounce: vi.fn((fn) => fn)
|
||||
}))
|
||||
```
|
||||
|
||||
4. **覆盖率目标**
|
||||
|
||||
- 行覆盖率:≥ 80%
|
||||
- 分支覆盖率:≥ 75%
|
||||
- 函数覆盖率:≥ 85%
|
||||
|
||||
## 检查清单
|
||||
|
||||
编写测试前,确认:
|
||||
|
||||
### 单元测试
|
||||
|
||||
- [ ] 测试了所有公共 API
|
||||
- [ ] 覆盖了正常流程和异常流程
|
||||
- [ ] 测试了边界值(空值、null、undefined)
|
||||
- [ ] Mock 了所有外部依赖
|
||||
- [ ] 每个测试用例独立运行
|
||||
- [ ] 测试名称清晰描述预期行为
|
||||
|
||||
### E2E 测试
|
||||
|
||||
- [ ] 覆盖了主要用户操作流程
|
||||
- [ ] 测试了不同浏览器尺寸(响应式)
|
||||
- [ ] 验证了无障碍访问(键盘导航、屏幕阅读器)
|
||||
- [ ] 包含了视觉回归测试(可选)
|
||||
- [ ] 测试数据可重复使用
|
||||
- [ ] 没有硬编码等待时间
|
||||
|
||||
### 通用
|
||||
|
||||
- [ ] 测试文件命名符合规范(\*.spec.ts)
|
||||
- [ ] 使用了 AAA 模式组织代码
|
||||
- [ ] 没有测试私有实现细节
|
||||
- [ ] 添加了必要的注释说明复杂逻辑
|
||||
- [ ] 运行 `pnpm test:unit` 全部通过
|
||||
- [ ] 运行 `pnpm test:e2e` 全部通过
|
||||
|
||||
## 运行测试
|
||||
|
||||
```bash
|
||||
# 运行所有单元测试
|
||||
pnpm test:unit
|
||||
|
||||
# 运行 Vue 3 单元测试
|
||||
pnpm test:unit3
|
||||
|
||||
# 运行 Vue 2 单元测试
|
||||
pnpm test:unit2
|
||||
|
||||
# 运行所有 E2E 测试
|
||||
pnpm test:e2e
|
||||
|
||||
# 运行特定组件的 E2E 测试
|
||||
pnpm test:e2e --grep "input"
|
||||
|
||||
# 生成覆盖率报告
|
||||
pnpm test:unit --coverage
|
||||
```
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [Vitest 官方文档](https://vitest.dev/)
|
||||
- [Playwright 官方文档](https://playwright.dev/)
|
||||
- [Input 组件单元测试](../../packages/renderless/src/input/__tests__/)
|
||||
- [Input 组件 E2E 测试](../../examples/sites/demos/input/test/)
|
||||
- [AGENTS.md 测试要求](../../AGENTS.md#测试要求)
|
||||
|
|
@ -1,370 +0,0 @@
|
|||
# TinyVue 主题定制指南
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 为新组件编写样式
|
||||
- 修改现有组件的视觉效果
|
||||
- 创建自定义主题
|
||||
- 调整响应式断点
|
||||
|
||||
## 核心原则
|
||||
|
||||
### 1. 主题架构
|
||||
|
||||
TinyVue 支持多主题系统:
|
||||
|
||||
```
|
||||
packages/
|
||||
├── theme/
|
||||
│ └── src/
|
||||
│ ├── base / # 公共变量和混入
|
||||
│ │ ├── vars.less # 全局的CSS 变量定义
|
||||
│ │ └── reset.less # rest 样式定义
|
||||
│ │ └── transition.less # 全局动画 样式定义
|
||||
│ │ └── aurora-theme.less # vars.less 中的一个变体, aurora风格
|
||||
│ │ └── dark-theme.less # vars.less 中的一个变体, 暗黑风格
|
||||
│ │ └── motion-theme.less # vars.less 中的一个变体, motion 风格
|
||||
│ │ └── old-theme.less # vars.less 中的一个变体, 原来的主题风格
|
||||
│ ├── svgs / # 所有图标的原始 svg 文件
|
||||
│ └── <component>/ # 各组件样式
|
||||
│ └── vars.less # 组件级的 CSS 变量定义
|
||||
│ └── index.less # 组件的 CSS 样式
|
||||
└── theme-saas/ # SAAS 主题
|
||||
└── src/
|
||||
└── ... # 类似结构,不同变量值
|
||||
```
|
||||
|
||||
### 2. CSS 变量系统
|
||||
|
||||
Tinyvue的样式系统设计了一套多级CSS 变量系统,`base CSS 变量` 和 `common CSS 变量` 的定义都在 `packages/theme/src/base/vars.less`中;`组件的CSS 变量`的定义在各个组件的文件夹下面。
|
||||
|
||||
1. `base CSS 变量` 定义一套规范的颜色值和数字值, 以 --tv-base-\* 打头。
|
||||
2. `common CSS 变量` 定义一套通用的CSS 变量值,有明确的使用场景约束, 更好的名称可读性。所有的值必须从`base CSS 变量`里面选取, 以 --tv-小写字母 打头, 比如 --tv-color-\* 。
|
||||
3. `组件的CSS 变量` 定义在组件级别的CSS 变量值,方便用户定制组件样式。 它必须使用 `common CSS 变量` ,并且不允许使用 `base CSS 变量`的值。 以 --tv-组件名首字母大写 打头,比如: --tv-Button-\*。 每一个组件级CSS 变量的上面必须是对应的注释,写明它的使用的位置。
|
||||
|
||||
```less
|
||||
// base CSS 变量
|
||||
:root {
|
||||
--tv-base-color-brand: #191919;
|
||||
--tv-base-color-brand-1: #f0f7ff;
|
||||
--tv-base-color-brand-2: #deecff;
|
||||
--tv-base-color-brand-3: #b3d6ff;
|
||||
--tv-base-color-brand-4: #7eb7fc;
|
||||
--tv-base-color-brand-5: #4191fa;
|
||||
--tv-base-color-brand-6: #1476ff;
|
||||
--tv-base-color-brand-7: #0f5ed4;
|
||||
--tv-base-color-brand-8: #0845a6;
|
||||
--tv-base-color-brand-9: #022e7a;
|
||||
--tv-base-color-brand-10: #001a4a;
|
||||
--tv-base-color-brand-11: #3d6899;
|
||||
--tv-base-color-brand-12: #7fa6d4;
|
||||
--tv-base-color-brand-13: #b6d4f2;
|
||||
}
|
||||
// common CSS 变量
|
||||
:root {
|
||||
--tv-color-success-text: var(--tv-base-color-success-6); // #5cb300 成功-文本色 tag的light、plain类型
|
||||
--tv-color-success-text-primary: var(--tv-base-color-common-11); // #191919 常规一级文本色(非主题色)
|
||||
--tv-color-success-bg: var(--tv-base-color-success-6); //#5cb300 成功-背景色(深) tag的dark类型/tooltip/badge
|
||||
--tv-color-success-bg-light: var(--tv-base-color-success-14); // #e6f2d5 成功-背景色(浅)
|
||||
--tv-color-success-bg-1: var(--tv-base-color-success-14); // #e6f2d5 tag的light类型
|
||||
--tv-color-success-border: var(--tv-base-color-success-6); // #5cb300 成功-边框色(深)
|
||||
--tv-color-success-border-light: var(--tv-base-color-success-14); // #e6f2d5 成功-边框色(浅) 型
|
||||
--tv-color-success-border-1: var(--tv-base-color-success-14); // #e6f2d5 tag的light类型
|
||||
--tv-color-success-icon: var(--tv-base-color-success-6); // #5cb300 成功-图标色
|
||||
}
|
||||
|
||||
// 组件级 CSS 变量
|
||||
.inject-Button-vars() {
|
||||
// 默认时按钮字重
|
||||
--tv-Button-font-weight: var(--tv-font-weight-regular, 400);
|
||||
// 默认时按钮边框宽度
|
||||
--tv-Button-border-width: var(--tv-border-width, 1px);
|
||||
// 按钮的文本行高
|
||||
--tv-Button-line-height: var(--tv-line-height-number, 1.5);
|
||||
}
|
||||
```
|
||||
|
||||
**重要约束**
|
||||
|
||||
目前组件库已经稳定,在开发组件时,尽量复用已经存在的 `common CSS 变量`,不要添加新变量。
|
||||
|
||||
### 3. BEM 命名规范
|
||||
|
||||
在模板中,为节点添加类名时,使用 **Block\_\_Element--Modifier** 命名的规则:
|
||||
|
||||
```less
|
||||
// Block: 组件名
|
||||
.tiny-button {
|
||||
// Element: 组成部分(双下划线)
|
||||
&__icon {
|
||||
margin-right: @spacing-xs;
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
// Modifier: 状态变体(双横线)
|
||||
&--primary {
|
||||
background-color: @color-brand;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
opacity: @opacity-disabled;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```less
|
||||
@button-prefix-cls: ~'@{css-prefix}button';
|
||||
|
||||
.@{button-prefix-cls} {
|
||||
.inject-Button-vars();
|
||||
|
||||
&.@{button-prefix-cls}--large {
|
||||
.size-mixin(-large);
|
||||
}
|
||||
&.@{button-prefix-cls}--medium {
|
||||
.size-mixin(-medium);
|
||||
}
|
||||
&.@{button-prefix-cls}--small {
|
||||
.size-mixin(-small);
|
||||
}
|
||||
&.@{button-prefix-cls}--mini {
|
||||
.size-mixin(-mini);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 组件的样式开发规范
|
||||
|
||||
一个组件通常都有三个样式文件:
|
||||
|
||||
1. vars.less: 组件级 CSS 变量。 通过分析组件中,哪些地方的样式需要适配不同主题,以及它们的值在 `common CSS 变量`中存在的,就需要找出来定义为组件级变量。 每一个组件级变量上面必须写注释指示该变量的使用场景。示例如下:
|
||||
|
||||
```less
|
||||
.inject-Button-vars() {
|
||||
// 默认时按钮字重
|
||||
--tv-Button-font-weight: var(--tv-font-weight-regular, 400);
|
||||
// 默认时按钮边框宽度
|
||||
--tv-Button-border-width: var(--tv-border-width, 1px);
|
||||
// 按钮的文本行高
|
||||
--tv-Button-line-height: var(--tv-line-height-number, 1.5);
|
||||
// 默认时按钮圆角
|
||||
--tv-Button-border-radius: var(--tv-border-radius-md, 6px); // 默认还原为6px
|
||||
// 大圆角时按钮圆角
|
||||
--tv-Button-border-radius-round: var(--tv-border-radius-round, 999px);
|
||||
// 圆形时按钮圆角
|
||||
--tv-Button-border-radius-circle: var(--tv-border-radius-round, 999px);
|
||||
}
|
||||
```
|
||||
|
||||
2. index.less: 组件的样式编写。每个组件的根节点,
|
||||
|
||||
首先要注入一下自己的组件级 CSS 变量,以便这些变量生效。如果组件有多个根节点,或者有弹出层等场景,需要给所有的根节点或弹出层的根节点添加这个注入。
|
||||
|
||||
其次,尽量使用 `less`的`嵌套结构`和`父选择器 &` 的能力进行编写。
|
||||
|
||||
最后,需要使用组件级的CSS变量,避免使用 `common CSS 变量` 和 `base CSS 变量`。
|
||||
|
||||
示例如下:
|
||||
|
||||
```less
|
||||
@alert-prefix-cls: ~'@{css-prefix}alert';
|
||||
|
||||
.@{alert-prefix-cls} {
|
||||
.inject-Alert-vars();
|
||||
|
||||
position: relative;
|
||||
display: flex;
|
||||
border: none;
|
||||
border-radius: var(--tv-Alert-border-radius);
|
||||
padding: var(--tv-Alert-padding-y) var(--tv-Alert-padding-x);
|
||||
margin: var(--tv-Alert-margin-y) var(--tv-Alert-margin-x);
|
||||
line-height: 1.5;
|
||||
|
||||
&.is-center {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/** alert-icon 场景 */
|
||||
.@{alert-prefix-cls}__icon:not(.@{alert-prefix-cls}__close) {
|
||||
font-size: var(--tv-Alert-icon-size);
|
||||
margin-right: var(--tv-Alert-icon-margin-right);
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. 组件的响应式设计
|
||||
|
||||
组件在不同的屏幕尺寸时,有不同的规范要求时,就需要添加 `responsive.less` 样式文件,编写在指定的屏幕大小时应该呈现的样式。开发规范同 `index.ts` 一致。示例如下:
|
||||
|
||||
```less
|
||||
@import '../custom.less';
|
||||
@import './vars.less';
|
||||
@alert-prefix-cls: ~'@{css-prefix}alert';
|
||||
|
||||
@media screen and (max-width: 1280px) {
|
||||
.@{alert-prefix-cls} {
|
||||
.inject-Alert-responsive-vars();
|
||||
|
||||
.@{alert-prefix-cls}__content {
|
||||
.@{alert-prefix-cls}__title {
|
||||
font-size: var(--tv-Alert-title-responsive-font-size);
|
||||
}
|
||||
|
||||
.@{alert-prefix-cls}__description {
|
||||
color: var(--tv-Alert-title-responsive-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5 移动优先的多端模板的开发规范
|
||||
|
||||
组件的多端模板 `mobile-first.vue`文件,不使用传统的 `BEM`样式规范,而是使用 `tailwind css`进行开发,`tailwind css`的配置文件在`..\packages\theme-saas\tailwind.config.js` 中,其中有定制的颜色值,断点值,布局等配置信息。
|
||||
|
||||
多端模板不需要引入任何的 css 文件,在模板中,只需要添加tailwind的类名即可,示例如下:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div
|
||||
data-tag="tiny-alert"
|
||||
v-if="state.show"
|
||||
:class="
|
||||
m(
|
||||
'min-h-min flex py-2 sm:py-3 px-4 my-2 rounded box-border font-light sm:font-normal text-color-text-primary',
|
||||
{ 'bg-color-info-primary-subtler': type === 'info' || !type },
|
||||
{ 'bg-color-error-subtler': type === 'error' },
|
||||
{ 'bg-color-warning-subtler': type === 'warning' },
|
||||
{ 'bg-color-success-subtler': type === 'success' },
|
||||
{ 'text-center': center },
|
||||
customClass
|
||||
)
|
||||
"
|
||||
>
|
||||
<span
|
||||
v-else-if="closeText && closable"
|
||||
data-tag="tiny-alert-close-text"
|
||||
@click="handleClose"
|
||||
class="leading-6 text-sm cursor-pointer"
|
||||
>{{ closeText }}</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
上面的 `m函数`是适配层注入的tailwind merge函数,用于合并类名。 如果元素上的类名太长,也可以将类名抽取为变量,集中编写到 `token.ts`文件中。
|
||||
|
||||
```typescript token.ts
|
||||
export const classes = {
|
||||
'button': 'inline-block.....',
|
||||
'size-default': 'h-10 text-sm sm:h-7'
|
||||
// ....
|
||||
}
|
||||
```
|
||||
|
||||
### Tailwind CSS 集成(Mobile First)
|
||||
|
||||
```vue
|
||||
<!-- 使用 Tailwind 工具类 -->
|
||||
<template>
|
||||
<div
|
||||
:class="
|
||||
m(
|
||||
'flex items-center justify-between',
|
||||
'px-4 py-2 sm:px-6 sm:py-3',
|
||||
'bg-white dark:bg-gray-800',
|
||||
'border border-gray-200 rounded-lg',
|
||||
'hover:border-blue-500 transition-colors'
|
||||
)
|
||||
"
|
||||
>
|
||||
<!-- 内容 -->
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## 主题定制检查清单
|
||||
|
||||
编写样式时,确认:
|
||||
|
||||
### 变量使用
|
||||
|
||||
- [ ] 尽量使用common CSS 变量
|
||||
- [ ] 所有间距使用间距变量
|
||||
- [ ] 所有字体大小使用字体变量
|
||||
- [ ] 没有硬编码的十六进制颜色值
|
||||
|
||||
### BEM 命名
|
||||
|
||||
- [ ] 类名遵循 BEM 规范
|
||||
- [ ] Block 名称与组件名一致
|
||||
- [ ] Element 使用双下划线 `__`
|
||||
- [ ] Modifier 使用双横线 `--`
|
||||
|
||||
### 响应式
|
||||
|
||||
- [ ] 考虑移动端优先设计
|
||||
- [ ] 测试不同屏幕尺寸
|
||||
- [ ] 使用相对单位(rem、em、%)
|
||||
- [ ] 避免固定宽度,使用 max-width
|
||||
|
||||
### 无障碍
|
||||
|
||||
- [ ] 颜色对比度符合 WCAG AA 标准
|
||||
- [ ] 焦点状态清晰可见
|
||||
- [ ] 禁用状态明确标识
|
||||
- [ ] 支持键盘导航样式
|
||||
|
||||
### 性能
|
||||
|
||||
- [ ] 避免深层嵌套(不超过 3 层)
|
||||
- [ ] 合理使用 CSS 过渡
|
||||
- [ ] 避免使用 `!important`
|
||||
- [ ] 合并重复的样式规则
|
||||
|
||||
### 兼容性
|
||||
|
||||
- [ ] 深色模式适配(如需要)
|
||||
- [ ] 浏览器兼容性检查
|
||||
|
||||
## 调试技巧
|
||||
|
||||
### 查看 CSS 变量
|
||||
|
||||
```javascript
|
||||
// 在浏览器控制台执行
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--tv-color-brand')
|
||||
```
|
||||
|
||||
### 临时覆盖样式
|
||||
|
||||
```vue
|
||||
<style scoped>
|
||||
/* 仅用于调试,不要提交 */
|
||||
.tiny-my-component {
|
||||
border: 1px solid red !important;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
### 使用浏览器 DevTools
|
||||
|
||||
1. 打开 Elements 面板
|
||||
2. 查看 Computed 样式
|
||||
3. 检查 CSS 变量值
|
||||
4. 实时修改测试效果
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [Less 官方文档](https://lesscss.org/)
|
||||
- [BEM 命名规范](https://getbem.com/)
|
||||
- [WCAG 无障碍指南](https://www.w3.org/WAI/WCAG21/quickref/)
|
||||
- [Button 组件级变量示例](../../packages/theme/src/button/vars.less)
|
||||
- [Button 组件样式示例](../../packages/theme/src/button/index.less)
|
||||
- [Button 组件响应样式的示例](../../packages/theme/src/button/responsive.less)
|
||||
- [Tailwind CSS 文档](https://tailwindcss.com/)
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
# @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)
|
||||
|
|
@ -1,253 +0,0 @@
|
|||
# TinyVue 组件开发规范
|
||||
|
||||
## 核心原则
|
||||
|
||||
在 `packages/vue/src` 目录中,每个组件有一个独立的文件夹进行隔离。
|
||||
|
||||
每一个组件的结构为:
|
||||
|
||||
1. index.ts: 整个组件的对外导出对象
|
||||
2. src/index.ts: 整合 pc/mobile-first 模板为一个统一的组件。通常要把pc/mobile-first模板的全量属性定义在这里并导出给2个模板使用
|
||||
3. src/pc.vue: pc 浏览器下的模板,需要引入外部的css样式文件。
|
||||
4. src/mobile-first.vue: 移动优先的浏览器模板, 使用tailwind进行内联类名。
|
||||
|
||||
## 详细解释每个文件的编写规范
|
||||
|
||||
1. index.ts
|
||||
|
||||
该文件是整个组件的对外导出对象,负责给组件添加install方法和veresion属性。 示例如下:
|
||||
|
||||
```typescript
|
||||
import Alert from './src/index'
|
||||
import { version } from './package.json'
|
||||
|
||||
Alert.install = function (Vue) {
|
||||
Vue.component(Alert.name, Alert)
|
||||
}
|
||||
|
||||
Alert.version = version
|
||||
|
||||
if (process.env.BUILD_TARGET === 'runtime') {
|
||||
if (typeof window !== 'undefined' && window.Vue) {
|
||||
Alert.install(window.Vue)
|
||||
}
|
||||
}
|
||||
|
||||
export default Alert
|
||||
```
|
||||
|
||||
2. src/index.ts
|
||||
|
||||
整合 pc/mobile-first 模板为一个统一的组件。示例如下:
|
||||
|
||||
```typescript
|
||||
import { $props, $prefix, $setup, defineComponent } from '@opentiny/vue-common'
|
||||
import template from 'virtual-template?pc|mobile-first'
|
||||
|
||||
export const alertProps = {
|
||||
// ......
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: $prefix + 'Alert',
|
||||
props: alertProps,
|
||||
setup(props, context) {
|
||||
return $setup({ props, context, template })
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
同时引入2个模板的语法为 `import template from 'virtual-template?pc|mobile-first'` , 它是非标准的TS用法,有专门的`vite`插件会将其编译为两个模板的引入。 `template`是一个函数,它需要传入 `$setup`函数。
|
||||
|
||||
3. src/pc.vue 和 src/mobile-first.vue 的模板共同规范
|
||||
|
||||
这2个文件是组件的**视图模板**,
|
||||
|
||||
- 负责 UI 渲染和用户交互
|
||||
- 通过适配层的 `setup` 调用 renderless 层的函数
|
||||
- 可以包含2个跨端模板(如 `mobile-first.vue`、`pc.vue`)
|
||||
- 禁止内联样式,需要从外部引入。比如:import '@opentiny/vue-theme/alert/index.less'
|
||||
- 必须引入类型定义文件。 比如: import type { IAlertApi } from '@opentiny/vue-renderless/types/alert.type'
|
||||
|
||||
一个标准的模板文件写法如下:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- 模板内容 -->
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { renderless, api } from '@opentiny/vue-renderless/alert/vue'
|
||||
import { props, setup, defineComponent } from '@opentiny/vue-common'
|
||||
import type { IAlertApi } from '@opentiny/vue-renderless/types/alert.type'
|
||||
import '@opentiny/vue-theme/alert/index.less'
|
||||
|
||||
export default defineComponent({
|
||||
props: [...props /** 其它属性名 */],
|
||||
setup(props, context) {
|
||||
return setup({ props, context, renderless, api }) as unknown as IAlertApi
|
||||
}
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
**适配层 setup** 函数作用:
|
||||
|
||||
1. 纽带作用
|
||||
|
||||
我们从 '@opentiny/vue-common' 引入 `setup`函数, 它是联系`无渲染逻辑层` 和 `模板`的纽带。 将`无渲染逻辑层`的组件逻辑函数传入`setup`后,
|
||||
setup内部会调用该函数,并传入组件实例的 porps, vue官方包对象,以及适配层构造的vm上下文对象,这样`无渲染逻辑层`的组件逻辑函数就会获得组件的所有令牌。 最后它会返回一个 {state,api} 对象,以便模板的绑定。
|
||||
|
||||
2. 处理模板选择
|
||||
|
||||
如果组件是跨端组件,那么它本质上是一个双层组件,父组件是用来选择使用 pc.vue ,mobile-first.vue 哪个模板, 子组件就是真实的组件。 `setup`函数内部负责向父组件同步状态和属性。此时 setup可选择性传入 `mono:false` 的属性,来告诉setup函数来同步这个状态和属性。
|
||||
|
||||
如果组件是一个单一组件,没有通用 `$setup` 来选择模板,那么在调用setup时, 必须传入 `mono:true` 来指示,它是一个单一组件。示例如下:
|
||||
|
||||
```typescript
|
||||
setup(props, context) {
|
||||
return setup({ props, context, renderless, api, mono:true }) as unknown as IAlertApi
|
||||
}
|
||||
```
|
||||
|
||||
3. 向`无渲染逻辑层`的组件逻辑函数传入额外的数据
|
||||
|
||||
`无渲染逻辑层`的设计上,它不能依赖第三方的库,如果之它需要引入其它对象,需要在 `模板层` 引入它们,并通过`setup` 的`extendOptions`属性 传入 `无渲染逻辑层`的组件逻辑函数中,示例如下:
|
||||
|
||||
```typescript
|
||||
import FluentEditor from '@opentiny/fluent-editor'
|
||||
|
||||
setup(props, context) {
|
||||
return setup({ props, context, renderless, api,
|
||||
extendOptions: {
|
||||
FluentEditor
|
||||
}})
|
||||
}
|
||||
```
|
||||
|
||||
## 模板规范
|
||||
|
||||
1. 必须使用 vue2, vue3同时兼容的模板语法。
|
||||
2. 必须是单根节点
|
||||
3. 不允许 v-if / v-for在同一个节点上, 建议使用 <template> 来使用 v-if / v-for
|
||||
4. 不允许使用 `id` 等属性
|
||||
5. 允许使用 Teleport 组件, 但必须从 '@opentiny/vue-common' 包中导入。
|
||||
6. 模板中可以使用 `a函数`,比如: ` v-bind="a($attrs, ['class', 'style', 'title', 'id'], true)"` , `a 函数`的意思是从$attrs上过滤出一些属性绑定到元素上。 最后一个参数为true的话,表示这些属性要保留下来,如果为false,则表示这些属性要过滤掉,其它的属性才保留下来。
|
||||
7. 模板中可以使用 `t函数`, 它是用来加载国际化内容。 比如: `{{ t('ui.base.cancel') }}`
|
||||
8. 模板中建议增加 `aria-*`的 无障碍信息,尤其是表单元素和图标元素等。
|
||||
|
||||
### mobile-first.vue的规范
|
||||
|
||||
1. 它使用 `tailwind css`进行模板开发,不依赖外部的样式库。
|
||||
2. 如果模板中的类名过长,可以将类名转换为变量,约定这些变量存放在 `tokens.ts` 文件中, 示例如下
|
||||
|
||||
```typescript token.ts
|
||||
export const classes = {
|
||||
'button': 'inline-block.....',
|
||||
'size-default': 'h-10 text-sm sm:h-7'
|
||||
// ....
|
||||
}
|
||||
```
|
||||
|
||||
3. mobile-first模板中,由于不能使用类名表示节点的作用,建议给关键的dom元素,增加 `data-tag`属性来表示dom的作用。根结点统一要增加 data-tag, 示例如下:
|
||||
|
||||
```html
|
||||
<button data-tag="tiny-button"></button>
|
||||
```
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [最终导出示例](../../packages/vue/src/button/index.ts)
|
||||
- [整合模板为一个统一的组件示例](../../packages/vue/src/button/src/index.ts)
|
||||
- [Button pc模板示例](../../packages/vue/src/button/src/pc.vue)
|
||||
- [Button mobile-first模板示例](../../packages/vue/src/button/src/mobile-first.vue)
|
||||
|
||||
## 多模式介绍
|
||||
|
||||
TinyVue 提供了 PC 和 Mobile 组件库,两套组件库对外是同一份依赖`@opentiny/vue`,同名组件通过`tiny_mode`切换组件模式。
|
||||
针对 SaaS 业务场景,TinyVue 提供了基于`tailwind`实现的多端组件,
|
||||
在 TinyVue 基础上新增`多端模式`,支持业务切换同名组件,同名组件默认情况下是`桌面模式`即`PC 组件`。
|
||||
|
||||
### 模式分类
|
||||
|
||||
AUI 组件库提供了三种组件模式:`桌面模式`、`多端模式(移动优先)`
|
||||
|
||||
| 模式 | 模式介绍 | 模式配置 |
|
||||
| -------- | -------- | ------------ |
|
||||
| 桌面模式 | 纯 PC | pc |
|
||||
| 多端模式 | 多端一致 | mobile-first |
|
||||
|
||||
### 模式切换
|
||||
|
||||
AUI 组件模式设置优先级
|
||||
|
||||
`单组件切换` > `模式透传` > `全局切换` > `组件默认模式`
|
||||
|
||||
#### 单组件切换
|
||||
|
||||
可在组件标签上配置`tiny_mode`属性,指定组件模式,就会选择对应模板渲染:
|
||||
|
||||
- 桌面模式:`pc`
|
||||
- 多端模式:`mobile-first`
|
||||
|
||||
参考示例如下:
|
||||
|
||||
```html
|
||||
<tiny-button tiny_mode="mobile-first">默认按钮</tiny-button>
|
||||
```
|
||||
|
||||
#### 全局切换
|
||||
|
||||
通过在 Vue 的原型上全局设置`tiny_mode`,可以指定所有同名组件的默认模式。
|
||||
在项目入口 `src/main.js` 文件中导入 Vue 依赖后,增加如下配置:
|
||||
|
||||
```js
|
||||
// Vue 2.0
|
||||
Vue.prototype.tiny_mode = { value: 'mobile-first' }
|
||||
|
||||
// Vue 3.0
|
||||
app.config.globalProperties.tiny_mode = { value: 'mobile-first' }
|
||||
```
|
||||
|
||||
#### 模式透传
|
||||
|
||||
如果想在页面部分区域切换模式,如卡片级控制,
|
||||
可以在外层 AUI 组件上添加`tiny_mode_root`属性,透传当前 AUI 组件的`tiny_mode`配置到所有子级 AUI 组件上,
|
||||
参考 demo 示例如下:
|
||||
|
||||
```html
|
||||
<template>
|
||||
<div>
|
||||
<tiny-layout>
|
||||
<tiny-row tiny_mode="pc" tiny_mode_root>
|
||||
<tiny-button>PC 按钮</tiny-button>
|
||||
<tiny-button type="primary" native-type="submit">主要按钮</tiny-button>
|
||||
<tiny-button type="success">成功按钮</tiny-button>
|
||||
<tiny-button type="info">信息按钮</tiny-button>
|
||||
<tiny-button type="warning">警告按钮</tiny-button>
|
||||
<tiny-button type="danger">危险按钮</tiny-button>
|
||||
</tiny-row>
|
||||
<tiny-row tiny_mode="mobile-first" tiny_mode_root>
|
||||
<tiny-button>多端按钮</tiny-button>
|
||||
<tiny-button type="primary" native-type="submit">主要按钮</tiny-button>
|
||||
<tiny-button type="success">成功按钮</tiny-button>
|
||||
<tiny-button type="info">信息按钮</tiny-button>
|
||||
<tiny-button type="warning">警告按钮</tiny-button>
|
||||
<tiny-button type="danger">危险按钮</tiny-button>
|
||||
</tiny-row>
|
||||
</tiny-layout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Button, Layout, Row } from '@opentiny/vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TinyButton: Button,
|
||||
TinyLayout: Layout,
|
||||
TinyRow: Row
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
|
@ -627,114 +627,6 @@
|
|||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "dufu1991",
|
||||
"name": "dufu1991",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/24769077?v=4",
|
||||
"profile": "https://github.com/dufu1991",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "hu-qi",
|
||||
"name": "huqi",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/17986122?v=4",
|
||||
"profile": "https://github.com/hu-qi",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "tsinghua-lau",
|
||||
"name": "Mao Tao",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/43164478?v=4",
|
||||
"profile": "https://github.com/tsinghua-lau",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Darkingtail",
|
||||
"name": "Darkingtail",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/51188676?v=4",
|
||||
"profile": "https://github.com/Darkingtail",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "hashiqi12138",
|
||||
"name": "hashiqi",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/52161363?v=4",
|
||||
"profile": "https://github.com/hashiqi12138",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "discreted66",
|
||||
"name": "liukun",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/190872652?v=4",
|
||||
"profile": "https://github.com/discreted66",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "lcy0620",
|
||||
"name": "lcy0620",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/188683944?v=4",
|
||||
"profile": "https://github.com/lcy0620",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "sakurajiajia",
|
||||
"name": "木斯佳",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/37933037?v=4",
|
||||
"profile": "https://github.com/sakurajiajia",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "552847957",
|
||||
"name": "552847957",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/8729901?v=4",
|
||||
"profile": "https://github.com/552847957",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Lingchen111",
|
||||
"name": "Lingchen111",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/123021749?v=4",
|
||||
"profile": "https://github.com/Lingchen111",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "gausszhou",
|
||||
"name": "Gauss Zhou",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/46297500?v=4",
|
||||
"profile": "https://gausszhou.github.io/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "liangguanhui0117",
|
||||
"name": "LiangGuanhui",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/28801619?v=4",
|
||||
"profile": "https://github.com/liangguanhui0117",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 8,
|
||||
|
|
|
|||
|
|
@ -50,8 +50,6 @@ module.exports = {
|
|||
'@typescript-eslint/no-use-before-define': 'off',
|
||||
'@typescript-eslint/restrict-template-expressions': 'off',
|
||||
'@typescript-eslint/no-invalid-this': 'off',
|
||||
'vue/no-deprecated-dollar-scopedslots-api': 'off',
|
||||
'@typescript-eslint/lines-between-class-members': 'off',
|
||||
'@typescript-eslint/no-this-alias': 'off'
|
||||
'vue/no-deprecated-dollar-scopedslots-api': 'off'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ labels:
|
|||
- label: "enhancement"
|
||||
sync: true
|
||||
matcher:
|
||||
title: "^(feat|✨).*: ?.*"
|
||||
title: "^feat.*: ?.*"
|
||||
commit: "^feat.*: ?.*"
|
||||
- label: "bug"
|
||||
sync: true
|
||||
matcher:
|
||||
title: "^(fix|🐛).*: ?.*"
|
||||
title: "^fix.*: ?.*"
|
||||
commit: "^fix.*: ?.*"
|
||||
- label: "documentation"
|
||||
sync: true
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ jobs:
|
|||
build:
|
||||
runs-on: windows-latest
|
||||
outputs:
|
||||
publishVersion: ${{ steps.parseTag.outputs.publishVersion }}
|
||||
branchVersion: ${{ steps.parseTag.outputs.branchVersion }}
|
||||
steps:
|
||||
- name: Parse Tag
|
||||
|
|
@ -17,7 +18,9 @@ jobs:
|
|||
with:
|
||||
script: |
|
||||
const tag = `${{ github.ref_name }}`
|
||||
const branchVersion = tag.slice(1).split('.').slice(0, 2).join('.') + '.0'
|
||||
const publishVersion = tag.slice(3)
|
||||
const branchVersion = tag.slice(1)
|
||||
core.setOutput('publishVersion', publishVersion)
|
||||
core.setOutput('branchVersion', branchVersion)
|
||||
|
||||
- name: CheckOut Code
|
||||
|
|
@ -33,48 +36,42 @@ jobs:
|
|||
with:
|
||||
node-version: 20.10.0
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
${{ runner.os }}-pnpm-store-
|
||||
- name: Install dependencies
|
||||
run: pnpm i --no-frozen-lockfile
|
||||
|
||||
- name: Run Build Components
|
||||
run: pnpm build:ui
|
||||
|
||||
run: pnpm build:ui -t ${{ steps.parseTag.outputs.publishVersion }}
|
||||
|
||||
- name: Run Build Sass Common
|
||||
run: pnpm build:ui saas-common -d saas
|
||||
run: pnpm build:ui saas-common -t ${{ steps.parseTag.outputs.publishVersion }} -d saas
|
||||
|
||||
- name: Run Build Theme
|
||||
run: pnpm build:theme
|
||||
|
||||
- name: Run Build Renderless
|
||||
run: pnpm build:renderless
|
||||
|
||||
|
||||
- name: Run Build ThemeSaas
|
||||
run: pnpm build:themeSaas
|
||||
|
||||
- name: Run Build Utils
|
||||
run: pnpm build:utils
|
||||
|
||||
- name: Run Build Hooks
|
||||
run: pnpm build:hooks
|
||||
|
||||
- name: Run Build Runtime
|
||||
run: pnpm build:runtime
|
||||
|
||||
- name: Run Build ThemeMobile
|
||||
run: pnpm build:themeMobile
|
||||
|
||||
- name: Publish Vue3 And Vue2 components
|
||||
run: pnpm pub:all && pnpm pub:runtime && pnpm pub:utils && pnpm pub:hooks
|
||||
run: pnpm pub:all
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,72 +0,0 @@
|
|||
name: Auto Build Main Module Docs
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [dev]
|
||||
paths:
|
||||
- 'examples/sites/demos/pc/webdoc/**'
|
||||
push:
|
||||
branches: [dev]
|
||||
paths:
|
||||
- 'examples/sites/demos/pc/webdoc/**'
|
||||
|
||||
jobs:
|
||||
verify-main-build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: webfactory/ssh-agent@v0.8.0
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.SUBMODULE_SSH_KEY_PRIVATE }}
|
||||
- name: Checkout submodule repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.PAT_TOKEN }}
|
||||
|
||||
- name: Checkout main repository
|
||||
run: |
|
||||
# 克隆主仓库
|
||||
git clone https://github.com/opentiny/docs.git docs
|
||||
cd docs
|
||||
|
||||
# 更新子模块到PR分支的版本
|
||||
git submodule sync --recursive
|
||||
git submodule update --init --recursive
|
||||
|
||||
|
||||
# 获取PR分支的最新提交
|
||||
cd ./tiny-vue
|
||||
BRANCH_NAME="${{ github.head_ref }}"
|
||||
git fetch origin $BRANCH_NAME
|
||||
git checkout $BRANCH_NAME
|
||||
|
||||
# 回到主仓库根目录
|
||||
cd ../
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Run main repo build
|
||||
id: build
|
||||
working-directory: ./docs
|
||||
run: |
|
||||
pnpm i
|
||||
# 如果使用VitePress
|
||||
pnpm build
|
||||
|
||||
- name: Update PR status
|
||||
if: always()
|
||||
run: |
|
||||
# 检查构建是否成功
|
||||
if [ ${{ steps.build.outcome }} == 'success' ]; then
|
||||
echo "✅ 主仓库构建成功"
|
||||
else
|
||||
echo "❌ 主仓库构建失败"
|
||||
exit 1
|
||||
fi
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
name: Auto Deploy Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [dev]
|
||||
paths:
|
||||
- 'examples/sites/demos/pc/webdoc/**'
|
||||
|
||||
jobs:
|
||||
notify:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger main repo workflow
|
||||
run: |
|
||||
curl -X POST \
|
||||
-H "Authorization: token ${{ secrets.PAT_TOKEN }}" \
|
||||
-H "Accept: application/vnd.github.v3+json" \
|
||||
https://api.github.com/repos/opentiny/docs/dispatches \
|
||||
-d '{"event_type":"submodule-updated"}'
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
name: Auto Deploy Site
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [dev]
|
||||
paths:
|
||||
- 'examples/sites/**'
|
||||
- '!examples/sites/**/*.spec.ts'
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow one concurrent deployment
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# Build job
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9
|
||||
- name: Install deps
|
||||
run: pnpm i --no-frozen-lockfile
|
||||
- name: Build site
|
||||
run: export NODE_OPTIONS="--max-old-space-size=8192" && pnpm build:site
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5
|
||||
- name: Build with Jekyll
|
||||
uses: actions/jekyll-build-pages@v1
|
||||
with:
|
||||
source: ./examples/sites/dist
|
||||
destination: ./_site
|
||||
- name: Repair build artifact
|
||||
run: |
|
||||
sudo chmod -R 777 ./_site
|
||||
cp ./examples/sites/dist/assets/_commonjsHelpers*.js ./_site/assets
|
||||
cp ./_site/index.html ./_site/404.html
|
||||
sed -i 's/\/static/\/tiny-vue\/static/' ./_site/static/js/design-common.js
|
||||
sh ./examples/sites/dist/cp-component-md.sh
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
|
||||
# Deployment job
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
|
|
@ -3,13 +3,10 @@ name: Auto Label (Pull Request)
|
|||
on:
|
||||
pull_request_target:
|
||||
types: [opened, edited]
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
label:
|
||||
|
|
|
|||
|
|
@ -1,96 +0,0 @@
|
|||
name: Deploy to Huawei OBS SaaS
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [dev]
|
||||
paths:
|
||||
- 'examples/sites/**'
|
||||
- '!examples/sites/**/*.spec.ts'
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 输入您将要发布的版本号(默认使用 examples/sites/package.json 中的版本号),例如:`3.x.x`。
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
# Build job
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.ver.outputs.value }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9
|
||||
- name: Install deps
|
||||
run: pnpm i --no-frozen-lockfile
|
||||
- name: Get version
|
||||
id: ver
|
||||
run: |
|
||||
# 优先用手动输入的版本号
|
||||
if [ -n "${{ inputs.version }}" ]; then
|
||||
VERSION="${{ inputs.version }}"
|
||||
else
|
||||
VERSION="$(node -p "require('./examples/sites/package.json').version")"
|
||||
fi
|
||||
echo "Home version: $VERSION"
|
||||
echo "value=$VERSION" >> $GITHUB_OUTPUT
|
||||
- name: Build site
|
||||
run: |
|
||||
export NODE_OPTIONS="--max-old-space-size=8192"
|
||||
pnpm -F @opentiny/vue-docs build:saas
|
||||
env:
|
||||
VITE_CONTEXT: /tiny-vue-saas/
|
||||
VITE_PLAYGROUND_URL: /vue-saas-playground
|
||||
VITE_APP_BUILD_BASE_URL: //res-static.opentiny.design/tiny-vue-saas/${{ steps.ver.outputs.value }}/
|
||||
- name: Copy files
|
||||
run: |
|
||||
cp ./examples/sites/dist/index.html ./examples/sites/dist/404.html
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist-artifact
|
||||
path: ./examples/sites/dist/
|
||||
# Deploy job
|
||||
deploy:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download build artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: dist-artifact
|
||||
path: ./examples/sites/dist/
|
||||
- name: Show version
|
||||
run: |
|
||||
echo "Deploying version: ${{ needs.build.outputs.version }}"
|
||||
# ===== 下载 obsutil =====
|
||||
- name: Install obsutil
|
||||
run: |
|
||||
curl -o obsutil.tar.gz https://obs-community.obs.cn-north-1.myhuaweicloud.com/obsutil/current/obsutil_linux_amd64.tar.gz
|
||||
tar -xzf obsutil.tar.gz
|
||||
chmod +x obsutil_linux_amd64_*/obsutil
|
||||
sudo mv obsutil_linux_amd64_*/obsutil /usr/local/bin/obsutil
|
||||
|
||||
# ===== 配置并上传 =====
|
||||
- name: Upload to OBS
|
||||
run: |
|
||||
# 一次性配置 AK/SK/endpoint
|
||||
obsutil config -i=${{ secrets.HUAWEI_CLOUD_AK }} \
|
||||
-k=${{ secrets.HUAWEI_CLOUD_SK }} \
|
||||
-e=${{ secrets.HUAWEI_CLOUD_ENDPOINT }}
|
||||
|
||||
# 把本地 dist/ 目录整站同步到桶根目录
|
||||
echo "needs.build.outputs.version: ${{ needs.build.outputs.version }}"
|
||||
mv examples/sites/dist ${{ needs.build.outputs.version }}
|
||||
obsutil cp ${{ needs.build.outputs.version }} obs://${{ secrets.HUAWEI_CLOUD_BUCKET }}/tiny-vue-saas/ -r -f
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
name: Deploy to Huawei OBS
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
paths:
|
||||
- 'examples/sites/**'
|
||||
- '!examples/sites/**/*.spec.ts'
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 输入您将要发布的版本号(默认使用 examples/sites/package.json 中的版本号),例如:`3.x.x`。
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
# Build job
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.ver.outputs.value }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9
|
||||
- name: Install deps
|
||||
run: pnpm i --no-frozen-lockfile
|
||||
- name: Get version
|
||||
id: ver
|
||||
run: |
|
||||
# 优先用手动输入的版本号
|
||||
if [ -n "${{ inputs.version }}" ]; then
|
||||
VERSION="${{ inputs.version }}"
|
||||
else
|
||||
VERSION="$(node -p "require('./examples/sites/package.json').version")"
|
||||
fi
|
||||
echo "Home version: $VERSION"
|
||||
echo "value=$VERSION" >> $GITHUB_OUTPUT
|
||||
- name: Build site
|
||||
run: |
|
||||
export NODE_OPTIONS="--max-old-space-size=8192"
|
||||
pnpm build:site
|
||||
env:
|
||||
VITE_APP_BUILD_BASE_URL: //res-static.opentiny.design/tiny-vue-web-doc/${{ steps.ver.outputs.value }}/
|
||||
VITE_PLAYGROUND_URL: /vue-playground
|
||||
- name: Copy files
|
||||
run: |
|
||||
cp ./examples/sites/dist/index.html ./examples/sites/dist/404.html
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist-artifact
|
||||
path: ./examples/sites/dist/
|
||||
# Deploy job
|
||||
deploy:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download build artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: dist-artifact
|
||||
path: ./examples/sites/dist/
|
||||
- name: Show version
|
||||
run: |
|
||||
echo "Deploying version: ${{ needs.build.outputs.version }}"
|
||||
# ===== 下载 obsutil =====
|
||||
- name: Install obsutil
|
||||
run: |
|
||||
curl -o obsutil.tar.gz https://obs-community.obs.cn-north-1.myhuaweicloud.com/obsutil/current/obsutil_linux_amd64.tar.gz
|
||||
tar -xzf obsutil.tar.gz
|
||||
chmod +x obsutil_linux_amd64_*/obsutil
|
||||
sudo mv obsutil_linux_amd64_*/obsutil /usr/local/bin/obsutil
|
||||
|
||||
# ===== 配置并上传 =====
|
||||
- name: Upload to OBS
|
||||
run: |
|
||||
# 一次性配置 AK/SK/endpoint
|
||||
obsutil config -i=${{ secrets.HUAWEI_CLOUD_AK }} \
|
||||
-k=${{ secrets.HUAWEI_CLOUD_SK }} \
|
||||
-e=${{ secrets.HUAWEI_CLOUD_ENDPOINT }}
|
||||
|
||||
# 把本地 dist/ 目录整站同步到桶根目录
|
||||
echo "needs.build.outputs.version: ${{ needs.build.outputs.version }}"
|
||||
mv examples/sites/dist ${{ needs.build.outputs.version }}
|
||||
obsutil cp ${{ needs.build.outputs.version }} obs://${{ secrets.HUAWEI_CLOUD_BUCKET }}/tiny-vue-web-doc/ -r -f
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
name: Dispatch Alpha All Publish
|
||||
run-name: Dispatch All Publish Alpha--${{ inputs.version }}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: |
|
||||
输入您将要发布的版本号,
|
||||
例如: `3.xx.xx`.
|
||||
required: true
|
||||
type: string
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: CheckOut Code
|
||||
uses: actions/checkout@master
|
||||
with:
|
||||
ref: ${{ github.ref_name }}
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20.10.0
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
- name: Install dependencies
|
||||
run: pnpm i --no-frozen-lockfile
|
||||
|
||||
- name: Run Build Components
|
||||
run: pnpm build:ui
|
||||
|
||||
- name: Run Build Sass Common
|
||||
run: pnpm build:ui saas-common -d saas
|
||||
|
||||
- name: Run Build Theme
|
||||
run: pnpm build:theme
|
||||
|
||||
- name: Run Build Renderless
|
||||
run: pnpm build:renderless
|
||||
|
||||
- name: Run Build ThemeSaas
|
||||
run: pnpm build:themeSaas
|
||||
|
||||
- name: Run Build Utils
|
||||
run: pnpm build:utils
|
||||
|
||||
- name: Run Build Hooks
|
||||
run: pnpm build:hooks
|
||||
|
||||
- name: Run Build Runtime
|
||||
run: pnpm build:runtime
|
||||
|
||||
- name: Run Release alpha
|
||||
run: pnpm release:alpha
|
||||
|
||||
- name: Publish Vue3 And Vue2 components
|
||||
run: pnpm pub:all && pnpm pub:runtime && pnpm pub:utils && pnpm pub:hooks && pnpm pub:site
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_OPENTINY_VUE_TOKEN }}
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
name: Dispatch Alpha Renderless Theme Runtime
|
||||
run-name: Dispatch Alpha Renderless Theme Runtime--${{ inputs.components }}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
components:
|
||||
description: |
|
||||
输入需要打包的模块名称,多个以英文逗号分隔,
|
||||
例如: `theme,renderless,runtime,docs,utils,hooks`
|
||||
required: true
|
||||
type: string
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Parse Components
|
||||
id: parseComponents
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const branchName = `${{ github.ref_name }}`
|
||||
const moduleName = `${{ inputs.components }}`
|
||||
const validModuleNames = ['theme', 'renderless', 'runtime', 'docs','utils','hooks']
|
||||
|
||||
if (!validModuleNames.some(name => moduleName.includes(name))) {
|
||||
throw new Error('请输入正确的包名称')
|
||||
}
|
||||
|
||||
- name: CheckOut Code
|
||||
uses: actions/checkout@master
|
||||
with:
|
||||
ref: ${{ github.ref_name }}
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20.10.0
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm i --no-frozen-lockfile
|
||||
|
||||
- name: Run Build Theme
|
||||
if: contains(inputs.components, 'theme') == true
|
||||
run: pnpm build:theme
|
||||
|
||||
- name: Run Build Renderless
|
||||
if: contains(inputs.components, 'renderless') == true
|
||||
run: pnpm build:renderless
|
||||
|
||||
- name: Run Build ThemeSaas
|
||||
if: contains(inputs.components, 'theme-saas') == true
|
||||
run: pnpm build:themeSaas
|
||||
|
||||
- name: Run Build Runtime
|
||||
if: contains(inputs.components, 'runtime') == true
|
||||
run: pnpm build:runtime
|
||||
|
||||
- name: Run Build Utils
|
||||
if: contains(inputs.components, 'utils') == true
|
||||
run: pnpm build:utils
|
||||
|
||||
- name: Run Build Hooks
|
||||
if: contains(inputs.components, 'hooks') == true
|
||||
run: pnpm build:hooks
|
||||
|
||||
- name: Run Release alpha
|
||||
run: pnpm release:alpha -u
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
pnpm pub:all
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_OPENTINY_VUE_TOKEN }}
|
||||
|
||||
- name: Publish Utils
|
||||
if: contains(inputs.components, 'utils') == true
|
||||
run: |
|
||||
pnpm pub:utils
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_OPENTINY_VUE_TOKEN }}
|
||||
|
||||
- name: Publish Runtime
|
||||
if: contains(inputs.components, 'runtime') == true
|
||||
run: |
|
||||
pnpm pub:runtime
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_OPENTINY_VUE_TOKEN }}
|
||||
|
||||
- name: Publish Hooks
|
||||
if: contains(inputs.components, 'hooks') == true
|
||||
run: |
|
||||
pnpm pub:hooks
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_OPENTINY_VUE_TOKEN }}
|
||||
|
||||
- name: Publish Docs
|
||||
if: contains(inputs.components, 'docs') == true
|
||||
run: |
|
||||
pnpm pub:site
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_OPENTINY_VUE_TOKEN }}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
name: Dispatch Renderless Theme Runtime
|
||||
run-name: Dispatch Renderless Theme Runtime--${{ inputs.components }}
|
||||
name: Dispatch Renderless Theme
|
||||
run-name: Dispatch Renderless Theme--${{ inputs.components }}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
|
@ -7,7 +7,7 @@ on:
|
|||
components:
|
||||
description: |
|
||||
输入需要打包的模块名称,多个以英文逗号分隔,
|
||||
例如: `theme,renderless,runtime,docs,utils,hooks`
|
||||
例如: `theme,renderless,runtime`
|
||||
required: true
|
||||
type: string
|
||||
concurrency:
|
||||
|
|
@ -25,9 +25,8 @@ jobs:
|
|||
script: |
|
||||
const branchName = `${{ github.ref_name }}`
|
||||
const moduleName = `${{ inputs.components }}`
|
||||
const validModuleNames = ['theme', 'renderless', 'runtime', 'docs','utils','hooks']
|
||||
|
||||
if (!validModuleNames.some(name => moduleName.includes(name))) {
|
||||
if (!moduleName.includes('theme') && !moduleName.includes('renderless')) {
|
||||
throw new Error('请输入正确的包名称')
|
||||
}
|
||||
|
||||
|
|
@ -77,48 +76,16 @@ jobs:
|
|||
if: contains(inputs.components, 'theme-saas') == true
|
||||
run: pnpm build:themeSaas
|
||||
|
||||
- name: Run Build ThemeMobile
|
||||
if: contains(inputs.components, 'theme-mobile') == true
|
||||
run: pnpm build:themeMobile
|
||||
|
||||
- name: Run Build Runtime
|
||||
if: contains(inputs.components, 'runtime') == true
|
||||
run: pnpm build:runtime
|
||||
|
||||
- name: Run Build Utils
|
||||
if: contains(inputs.components, 'utils') == true
|
||||
run: pnpm build:utils
|
||||
|
||||
- name: Run Build Hooks
|
||||
if: contains(inputs.components, 'hooks') == true
|
||||
run: pnpm build:hooks
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
pnpm pub:all
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Publish Utils
|
||||
if: contains(inputs.components, 'utils') == true
|
||||
run: |
|
||||
pnpm pub:utils
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Publish Runtime
|
||||
if: contains(inputs.components, 'runtime') == true
|
||||
run: |
|
||||
pnpm pub:runtime
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Publish Hooks
|
||||
if: contains(inputs.components, 'hooks') == true
|
||||
run: |
|
||||
pnpm pub:hooks
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Publish Docs
|
||||
if: contains(inputs.components, 'docs') == true
|
||||
run: |
|
||||
pnpm pub:site
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
|
|
|||
|
|
@ -1,78 +0,0 @@
|
|||
name: Dispatch Alpha Ui Publish
|
||||
run-name: Dispatch Alpha Ui--${{ inputs.components }}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
components:
|
||||
description: |
|
||||
输入需要打包的组件名称,多个以空格分隔,
|
||||
例如: `input alert`.
|
||||
required: false
|
||||
type: string
|
||||
updateVersion:
|
||||
description: '是否自动升级 patch 版本号'
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: CheckOut Code
|
||||
uses: actions/checkout@master
|
||||
with:
|
||||
ref: ${{ github.ref_name }}
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20.10.0
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm i --no-frozen-lockfile
|
||||
|
||||
- name: Run Build Components
|
||||
if: inputs.components != ''
|
||||
run: pnpm build:ui ${{ inputs.components }}
|
||||
|
||||
- name: Run Build All Components
|
||||
if: inputs.components == ''
|
||||
run: pnpm build:ui
|
||||
|
||||
- name: Run Release alpha
|
||||
shell: bash
|
||||
run: |
|
||||
# 根据工作流输入决定是否自动升级 patch 版本号(使用 bash 语法)
|
||||
if [ "${{ inputs.updateVersion }}" = "true" ]; then
|
||||
pnpm release:alpha -u # 自动升级 patch 版本
|
||||
else
|
||||
pnpm release:alpha # 不升级版本
|
||||
fi
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
pnpm pub:all
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_OPENTINY_VUE_TOKEN }}
|
||||
|
|
@ -1,14 +1,7 @@
|
|||
name: E2E Test ALL
|
||||
run-name: E2E Test All origin--${{ inputs.origin }}
|
||||
run-name: E2E Test All
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
origin:
|
||||
description: |
|
||||
输入需要测试的域名地址,包括微服务路径,如果不输入则采用本地启动服务测试,
|
||||
例如: `https://opentiny.github.io/tiny-vue-web-doc`.
|
||||
required: false
|
||||
type: string
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.sha }}
|
||||
cancel-in-progress: true
|
||||
|
|
@ -44,32 +37,23 @@ jobs:
|
|||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm i --no-frozen-lockfile
|
||||
|
||||
- name: dev start
|
||||
if: contains(inputs.origin, 'http') == false
|
||||
run: pnpm site & sleep 5
|
||||
|
||||
- name: Release E2E Config
|
||||
if: contains(inputs.origin, 'http') == true
|
||||
run: pnpm release:e2eConfig -o ${{ inputs.origin }} & sleep 5
|
||||
|
||||
- name: update playwright
|
||||
run: pnpm recursive update @playwright/test
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: pnpm install:browser --with-deps chromium
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: pnpm test:e2e3 --shard=${{ matrix.shard }} --reporter=blob --pass-with-no-tests
|
||||
run: pnpm test:e2e3 --shard=${{ matrix.shard }} --reporter=blob
|
||||
|
||||
- name: Upload blob report to GitHub Actions Artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: all-blob-reports-${{ strategy.job-index }}
|
||||
name: all-blob-reports
|
||||
path: examples/vue3/blob-report
|
||||
retention-days: 0.5
|
||||
|
||||
|
|
@ -89,17 +73,16 @@ jobs:
|
|||
npm install -g @playwright/test
|
||||
|
||||
- name: Download blob reports from GitHub Actions Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
pattern: all-blob-reports-*
|
||||
merge-multiple: true
|
||||
name: all-blob-reports
|
||||
path: all-blob-reports
|
||||
|
||||
- name: Merge into HTML Report
|
||||
run: playwright merge-reports --reporter html ./all-blob-reports
|
||||
|
||||
- name: Upload HTML report
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: html-report--attempt-${{ github.run_attempt }}
|
||||
path: playwright-report
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
name: E2E Test Dispatch
|
||||
run-name: E2E Test Dispatch--${{ inputs.testDemos }}--origin--${{ inputs.origin }}
|
||||
run-name: E2E Test Dispatch--${{ inputs.testDemos }}--
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
|
|
@ -9,12 +9,6 @@ on:
|
|||
such as `input, alert`.
|
||||
required: true
|
||||
type: string
|
||||
origin:
|
||||
description: |
|
||||
输入需要测试的域名地址,包括微服务路径,如果不输入则采用本地启动服务测试,
|
||||
例如: `https://opentiny.github.io/tiny-vue-web-doc`.
|
||||
required: false
|
||||
type: string
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.sha }}
|
||||
cancel-in-progress: true
|
||||
|
|
@ -50,13 +44,8 @@ jobs:
|
|||
run: pnpm i --no-frozen-lockfile
|
||||
|
||||
- name: dev start
|
||||
if: contains(inputs.origin, 'http') == false
|
||||
run: pnpm site & sleep 5
|
||||
|
||||
- name: Release E2E Config
|
||||
if: contains(inputs.origin, 'http') == true
|
||||
run: pnpm release:e2eConfig -o ${{ inputs.origin }} & sleep 5
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: pnpm install:browser --with-deps chromium
|
||||
|
||||
|
|
@ -64,4 +53,4 @@ jobs:
|
|||
run: |
|
||||
testDemos="${{ inputs.testDemos }}"
|
||||
components=${testDemos//,/' '}
|
||||
pnpm test:e2e3 $components --reporter=line --pass-with-no-tests
|
||||
pnpm test:e2e3 $components --reporter=line
|
||||
|
|
|
|||
|
|
@ -4,327 +4,106 @@ on:
|
|||
pull_request:
|
||||
types: [opened, reopened, synchronize, edited]
|
||||
|
||||
# 并发控制,确保相同PR的工作流不会同时运行多个实例
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.number || github.sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
detect-changed-files:
|
||||
name: Detect Changed Files
|
||||
parse-components:
|
||||
name: Parse Affected Components
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
# 输出三个变量供后续任务使用
|
||||
changed_components: ${{ steps.find-changed-components.outputs.changed_components }} # 自动检测到的变更组件
|
||||
manual_components: ${{ steps.parse-title.outputs.manual_components }} # 从PR标题手动指定的组件
|
||||
testclis: ${{ steps.parse-test-cli.outputs.testClis }} # 测试命令列表
|
||||
testComponents: ${{ steps.parseTitle.outputs.testComponents }}
|
||||
testclis: ${{ steps.parsetestCli.outputs.testClis }}
|
||||
steps:
|
||||
# 检出代码,需要完整历史以便比较变更
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # 获取完整的git历史,用于检测文件变更
|
||||
|
||||
# 使用tj-actions/changed-files获取PR中所有变更的文件
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v41
|
||||
|
||||
# 根据变更的文件自动识别受影响的组件
|
||||
- name: Find changed components
|
||||
id: find-changed-components
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const changedFiles = '${{ steps.changed-files.outputs.all_changed_files }}'.split(' ').filter(file => file.trim() !== '');
|
||||
console.log('变更的文件列表:', changedFiles);
|
||||
|
||||
// 增强组件路径匹配规则,确保能正确识别各种路径格式
|
||||
const componentPathRules = [
|
||||
// 匹配 packages/renderless/src/{组件名} 路径,支持更深层次的子目录
|
||||
{
|
||||
pattern: /^packages\/renderless\/src\/([^/]+)\//,
|
||||
group: 1,
|
||||
type: 'renderless'
|
||||
},
|
||||
// 匹配 packages/vue/src/{组件名} 路径,支持更深层次的子目录
|
||||
{
|
||||
pattern: /^packages\/vue\/src\/([^/]+)\//,
|
||||
group: 1,
|
||||
type: 'vue'
|
||||
},
|
||||
// 匹配示例目录下的组件路径 - 只匹配目录,不匹配package.json等文件
|
||||
{
|
||||
pattern: /^examples\/sites\/demos\/pc\/app\/([^/]+)\//,
|
||||
group: 1,
|
||||
type: 'demo'
|
||||
},
|
||||
// 匹配 packages/theme/src/{组件名} 样式路径
|
||||
{
|
||||
pattern: /^packages\/theme\/src\/([^/]+)\//,
|
||||
group: 1,
|
||||
type: 'theme'
|
||||
}
|
||||
];
|
||||
|
||||
// 定义子组件到父组件的映射关系
|
||||
const subComponentMapping = {
|
||||
'calendar-bar': 'calendar',
|
||||
'carousel-item': 'carousel',
|
||||
'cascader-menu': 'cascader',
|
||||
'cascader-node': 'cascader',
|
||||
'cascader-panel': 'cascader',
|
||||
'checkbox-button': 'checkbox',
|
||||
'checkbox-group': 'checkbox',
|
||||
'col': 'layout',
|
||||
'collapse-item': 'collapse',
|
||||
'date-range': 'date-picker',
|
||||
'date-table': 'date-picker',
|
||||
'dropdown-item': 'dropdown',
|
||||
'dropdown-menu': 'dropdown',
|
||||
'form-item': 'form',
|
||||
'grid-toolbar': 'grid',
|
||||
'image-viewer': 'image',
|
||||
'month-range': 'date-picker',
|
||||
'month-table': 'date-picker',
|
||||
'option': 'select',
|
||||
'option-group': 'select',
|
||||
'pager-item': 'pager',
|
||||
'picker': 'date-picker',
|
||||
'quarter-panel': 'date-picker',
|
||||
'radio-button': 'radio',
|
||||
'radio-group': 'radio',
|
||||
'row': 'layout',
|
||||
'select-dropdown': 'select',
|
||||
'skeleton-item': 'skeleton',
|
||||
'svgs': 'icon',
|
||||
'tab-item': 'tabs',
|
||||
'textarea': 'input',
|
||||
'time-panel': 'time-picker',
|
||||
'time-range': 'time-picker',
|
||||
'time-spinner': 'time-picker',
|
||||
'timeline-item': 'timeline',
|
||||
'transfer-panel': 'transfer',
|
||||
'tree-node': 'tree',
|
||||
'upload': 'file-upload',
|
||||
'upload-dragger': 'file-upload',
|
||||
'upload-list': 'file-upload',
|
||||
'year-range': 'date-picker',
|
||||
'year-table': 'date-picker'
|
||||
};
|
||||
|
||||
// 从文件路径中提取组件名称
|
||||
const components = new Set();
|
||||
const detectedComponents = {}; // 用于记录检测到的组件和相关文件
|
||||
const unmatchedFiles = []; // 记录未匹配的文件
|
||||
|
||||
changedFiles.forEach(file => {
|
||||
// 跳过工作流文件等非组件相关文件
|
||||
if (file.startsWith('.github/') ||
|
||||
file.includes('README') ||
|
||||
file.includes('.md') ||
|
||||
file.includes('theme-saas') || // 跳过theme-saas相关文件
|
||||
file.includes('theme/src/common/')) { // 跳过组件通用样式文件
|
||||
console.log(`跳过非测试相关文件: ${file}`);
|
||||
return;
|
||||
}
|
||||
|
||||
let matched = false;
|
||||
|
||||
for (const rule of componentPathRules) {
|
||||
const match = file.match(rule.pattern);
|
||||
if (match && match[rule.group]) {
|
||||
matched = true;
|
||||
let componentName = match[rule.group];
|
||||
|
||||
// 检查是否是需要映射的子组件
|
||||
if (subComponentMapping[componentName]) {
|
||||
console.log(`检测到子组件 ${componentName},映射到父组件 ${subComponentMapping[componentName]}`);
|
||||
componentName = subComponentMapping[componentName];
|
||||
}
|
||||
|
||||
// 记录找到的组件
|
||||
components.add(componentName);
|
||||
|
||||
// 记录组件来源和相关文件,用于调试
|
||||
if (!detectedComponents[componentName]) {
|
||||
detectedComponents[componentName] = {
|
||||
type: rule.type,
|
||||
files: []
|
||||
};
|
||||
}
|
||||
detectedComponents[componentName].files.push(file);
|
||||
console.log(`成功匹配组件 ${componentName}: ${file} (类型: ${rule.type})`);
|
||||
break; // 一个文件只匹配一个组件
|
||||
}
|
||||
}
|
||||
|
||||
// 检查theme目录中的特殊路径格式
|
||||
if (!matched && file.includes('/theme/src/') && !file.includes('/common/')) {
|
||||
// 尝试从theme路径中提取组件名
|
||||
const themePath = file.split('/');
|
||||
// 常见的组件样式文件命名模式
|
||||
const possibleComponentNames = [];
|
||||
|
||||
for (let i = 0; i < themePath.length; i++) {
|
||||
// 跳过明显不是组件名的路径部分
|
||||
if (['src', 'lib', 'theme', 'packages'].includes(themePath[i])) {
|
||||
continue;
|
||||
}
|
||||
// 检查是否像是组件名(小写字母,可能包含连字符)
|
||||
if (/^[a-z][a-z0-9\-]*$/.test(themePath[i])) {
|
||||
possibleComponentNames.push(themePath[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (possibleComponentNames.length > 0) {
|
||||
// 使用最后一个可能的组件名(通常最具体)
|
||||
let componentName = possibleComponentNames[possibleComponentNames.length - 1];
|
||||
|
||||
// 检查是否是需要映射的子组件
|
||||
if (subComponentMapping[componentName]) {
|
||||
console.log(`检测到子组件 ${componentName},映射到父组件 ${subComponentMapping[componentName]}`);
|
||||
componentName = subComponentMapping[componentName];
|
||||
}
|
||||
|
||||
components.add(componentName);
|
||||
|
||||
if (!detectedComponents[componentName]) {
|
||||
detectedComponents[componentName] = {
|
||||
type: 'theme',
|
||||
files: []
|
||||
};
|
||||
}
|
||||
detectedComponents[componentName].files.push(file);
|
||||
console.log(`通过主题样式路径分析匹配到组件 ${componentName}: ${file}`);
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 记录未匹配到的文件,用于调试
|
||||
if (!matched) {
|
||||
unmatchedFiles.push(file);
|
||||
console.log(`未能匹配组件: ${file}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 打印详细的检测结果,方便调试
|
||||
console.log('检测结果详情:', JSON.stringify(detectedComponents, null, 2));
|
||||
console.log('未匹配文件列表:', unmatchedFiles);
|
||||
|
||||
// 构建E2E测试的组件过滤器表达式
|
||||
if (components.size > 0) {
|
||||
// 构建符合E2E测试命令需要的过滤表达式格式
|
||||
const componentFilters = Array.from(components)
|
||||
.map(comp => `"\\/app\\/${comp}\\/"`)
|
||||
.join(' ');
|
||||
|
||||
console.log(`检测到变更的组件: ${Array.from(components).join(', ')}`);
|
||||
core.setOutput('changed_components', componentFilters);
|
||||
} else {
|
||||
console.log('没有检测到变更的组件');
|
||||
core.setOutput('changed_components', '');
|
||||
}
|
||||
|
||||
# 从PR标题中解析手动指定的组件列表
|
||||
- name: Parse Title for Manual Components
|
||||
id: parse-title
|
||||
- name: Parse Title
|
||||
id: parseTitle
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const prTitle = context.payload.pull_request.title
|
||||
// 匹配PR标题中的 [component1, component2] 格式
|
||||
const regex = /\[(.*?)\]/
|
||||
const matches = prTitle.match(regex)
|
||||
if (matches && matches.length > 1 && matches[1]) {
|
||||
// 处理和格式化手动指定的组件列表
|
||||
let components = matches[1]
|
||||
.split(',')
|
||||
.map((c) => c.trim())
|
||||
.filter((c) => /^[a-z\-\/]+$/.test(c)) // 确保组件名符合规范
|
||||
.map((c) => `"\\/app\\/${c}\\/"`)
|
||||
components = [...new Set(components)].join(' ') // 去重并转为字符串
|
||||
core.setOutput('manual_components', components)
|
||||
.filter((c) => /^[a-z\-\/]+$/.test(c))
|
||||
.map((c) => `"\\/${c}\\/"`)
|
||||
components = [...new Set(components)].slice(0, 3).join(' ')
|
||||
core.setOutput('testComponents', components)
|
||||
} else {
|
||||
core.setOutput('manual_components', '')
|
||||
}
|
||||
const warningString =`**[e2e-test-warn]**
|
||||
The component to be tested is missing.
|
||||
|
||||
# 当没有检测到任何组件时,生成警告提示
|
||||
- name: Generate warning if no components detected
|
||||
id: warning
|
||||
if: ${{ steps.find-changed-components.outputs.changed_components == '' && steps.parse-title.outputs.manual_components == '' }}
|
||||
The title of the Pull request should look like "fix(vue-renderless): [action-menu, alert] fix xxx bug".
|
||||
|
||||
Please make sure you've read our [contributing guide](https://github.com/opentiny/tiny-vue/blob/dev/CONTRIBUTING.md)
|
||||
`
|
||||
core.setOutput('tip', warningString)
|
||||
core.warning(warningString)
|
||||
}
|
||||
- name: generate user-tip.txt
|
||||
if: ${{ steps.parseTitle.outputs.tip }}
|
||||
run: |
|
||||
cat << EOF > user-tip.txt
|
||||
**[e2e-test-warn]**
|
||||
没有检测到要测试的组件。
|
||||
|
||||
系统会自动检测PR中变更的组件文件,或者您可以在PR标题中使用[component1, component2]格式手动指定要测试的组件。
|
||||
例如: "fix(vue-renderless): [action-menu, alert] fix xxx bug"
|
||||
|
||||
请确保您已阅读我们的[贡献指南](https://github.com/opentiny/tiny-vue/blob/dev/CONTRIBUTING.md)
|
||||
${{ steps.parseTitle.outputs.tip }}
|
||||
EOF
|
||||
echo "warning=true" >> $GITHUB_OUTPUT
|
||||
|
||||
# 上传警告信息作为工作流制品
|
||||
- name: Upload warning
|
||||
if: ${{ steps.warning.outputs.warning }}
|
||||
- name: Upload User Tip
|
||||
if: ${{ steps.parseTitle.outputs.tip }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: user-tip
|
||||
path: user-tip.txt
|
||||
retention-days: 1 # 保留1天
|
||||
retention-days: 1
|
||||
- name: Save PR number
|
||||
if: ${{ steps.parseTitle.outputs.tip }}
|
||||
run: echo ${{ github.event.number }} > ./pr-id.txt
|
||||
|
||||
# 解析测试命令配置
|
||||
- name: Upload PR number
|
||||
if: ${{ steps.parseTitle.outputs.tip }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: pr
|
||||
path: ./pr-id.txt
|
||||
- name: Parse Test Cli
|
||||
id: parse-test-cli
|
||||
id: parsetestCli
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
// 从GitHub变量中获取测试命令列表,如果未配置则使用默认命令
|
||||
const testClis = '${{ vars.PLAYWRIGHT_CLIS }}' ? '${{ vars.PLAYWRIGHT_CLIS }}'.split(',') : ['pnpm test:e2e3']
|
||||
core.setOutput('testClis', JSON.stringify(testClis))
|
||||
core.setOutput('testclis', JSON.stringify(testClis))
|
||||
|
||||
# PR测试任务,运行实际的E2E测试
|
||||
pr-test:
|
||||
name: PR E2E Test
|
||||
needs: detect-changed-files # 依赖前一个任务的输出
|
||||
runs-on: ubuntu-latest
|
||||
# 只有当检测到变更组件或手动指定组件时才运行测试
|
||||
if: ${{ needs.detect-changed-files.outputs.changed_components != '' || needs.detect-changed-files.outputs.manual_components != '' }}
|
||||
if: ${{ needs.parse-components.outputs.testComponents }}
|
||||
strategy:
|
||||
matrix:
|
||||
testcli: ${{ fromJson(needs.detect-changed-files.outputs.testclis) }} # 使用矩阵策略运行多个测试命令
|
||||
env:
|
||||
# 合并自动检测和手动指定的组件列表
|
||||
TEST_COMPONENTS: ${{ needs.detect-changed-files.outputs.changed_components }} ${{ needs.detect-changed-files.outputs.manual_components }}
|
||||
steps:
|
||||
# 检出代码
|
||||
- uses: actions/checkout@v3
|
||||
testcli: ${{ fromJson(needs.parse-components.outputs.testclis) }}
|
||||
|
||||
# 设置pnpm
|
||||
name: PR E2E Test
|
||||
needs: parse-components
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TEST_COMPONENTS: ${{ needs.parse-components.outputs.testComponents }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
|
||||
# 设置Node.js环境
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
# 缓存Playwright浏览器安装,加速工作流
|
||||
- name: Cache Playwright Installation
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
# 获取pnpm缓存目录
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
# 设置pnpm缓存
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
|
|
@ -333,19 +112,11 @@ jobs:
|
|||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
# 安装依赖
|
||||
- name: Install dependencies
|
||||
run: pnpm i --no-frozen-lockfile
|
||||
|
||||
# 安装Playwright浏览器
|
||||
- name: Install Playwright browsers
|
||||
run: pnpm install:browser --with-deps chromium
|
||||
|
||||
# 显示要测试的组件列表,便于调试
|
||||
- name: Show detected components
|
||||
run: |
|
||||
echo "Testing components: $TEST_COMPONENTS"
|
||||
|
||||
# 运行E2E测试
|
||||
- name: E2E Test
|
||||
run: ${{ matrix.testcli }} ${{ env.TEST_COMPONENTS }} --retries=1 --workers=2 --pass-with-no-tests # 带重试和并行工作进程,无测试时不报错
|
||||
run: ${{ matrix.testcli }} ${{ env.TEST_COMPONENTS }} --retries=1 --workers=2
|
||||
|
|
|
|||
|
|
@ -1,131 +0,0 @@
|
|||
name: Unit Test PR
|
||||
run-name: Unit Test PR--${{ github.event.pull_request.title }}
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize, edited]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.number || github.sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
parse-components:
|
||||
name: Parse Affected Components
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
testComponents: ${{ steps.parseTitle.outputs.testComponents }}
|
||||
utilsModified: ${{ steps.check-utils-changes.outputs.modified }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Parse Title
|
||||
id: parseTitle
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const prTitle = context.payload.pull_request.title
|
||||
const regex = /\[(.*?)\]/
|
||||
const matches = prTitle.match(regex)
|
||||
if (matches && matches.length > 1 && matches[1]) {
|
||||
let components = matches[1]
|
||||
.split(',')
|
||||
.map((c) => c.trim())
|
||||
.filter((c) => /^[a-z\-\/]+$/.test(c))
|
||||
.map((c) => `${c}`)
|
||||
components = [...new Set(components)].slice(0, 3).join(' ')
|
||||
core.setOutput('testComponents', components)
|
||||
} else {
|
||||
const warningString =`**[unit-test-warn]**
|
||||
The component to be tested is missing.
|
||||
|
||||
The title of the Pull request should look like "fix(vue-renderless): [action-menu, alert] fix xxx bug".
|
||||
|
||||
Please make sure you've read our [contributing guide](https://github.com/opentiny/tiny-vue/blob/dev/CONTRIBUTING.md)
|
||||
`
|
||||
core.setOutput('tip', warningString)
|
||||
core.warning(warningString)
|
||||
}
|
||||
- name: Check Utils Changes
|
||||
id: check-utils-changes
|
||||
run: |
|
||||
# 从远程仓库获取目标分支的最新代码
|
||||
git fetch origin ${{ github.base_ref }}
|
||||
|
||||
# 获取当前PR分支相对于目标分支的所有变更文件列表
|
||||
CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}..HEAD)
|
||||
|
||||
# 使用grep检查变更文件列表中是否包含utils包的改动
|
||||
# ^packages/utils/ 表示以packages/utils/开头的文件路径
|
||||
if echo "$CHANGED_FILES" | grep -q "^packages/utils/"; then
|
||||
# 如果检测到utils包有改动,设置modified输出变量为true
|
||||
echo "modified=true" >> $GITHUB_OUTPUT
|
||||
echo "Utils package has been modified, utils tests will be executed"
|
||||
else
|
||||
# 如果没有检测到utils包的改动,设置modified输出变量为false
|
||||
echo "modified=false" >> $GITHUB_OUTPUT
|
||||
echo "No changes detected in utils package"
|
||||
fi
|
||||
- name: generate user-tip.txt
|
||||
if: ${{ steps.parseTitle.outputs.tip }}
|
||||
run: |
|
||||
cat << EOF > user-tip.txt
|
||||
${{ steps.parseTitle.outputs.tip }}
|
||||
EOF
|
||||
- name: Upload User Tip
|
||||
if: ${{ steps.parseTitle.outputs.tip }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: user-tip
|
||||
path: user-tip.txt
|
||||
retention-days: 1
|
||||
- name: Save PR number
|
||||
if: ${{ steps.parseTitle.outputs.tip }}
|
||||
run: echo ${{ github.event.number }} > ./pr-id.txt
|
||||
|
||||
- name: Upload PR number
|
||||
if: ${{ steps.parseTitle.outputs.tip }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: pr
|
||||
path: ./pr-id.txt
|
||||
|
||||
pr-test:
|
||||
name: PR Unit Test
|
||||
needs: parse-components
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TEST_COMPONENTS: ${{ needs.parse-components.outputs.testComponents }}
|
||||
UTILS_MODIFIED: ${{ needs.parse-components.outputs.utilsModified }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm i --no-frozen-lockfile
|
||||
|
||||
- name: Unit Test for Components
|
||||
if: ${{ env.TEST_COMPONENTS }}
|
||||
run: pnpm test:unit3 ${{ env.TEST_COMPONENTS }}
|
||||
|
||||
- name: Utils Unit Test
|
||||
if: ${{ env.UTILS_MODIFIED == 'true' }}
|
||||
run: pnpm test:utils
|
||||
|
|
@ -4,11 +4,19 @@ dist/
|
|||
dist2/
|
||||
dist2.7/
|
||||
dist3/
|
||||
dist-react/
|
||||
allDist/
|
||||
packages/**/runtime
|
||||
coverage/
|
||||
test-results
|
||||
|
||||
/packages/vue/index.ts
|
||||
/packages/vue/pc.ts
|
||||
/packages/vue/mobile.ts
|
||||
/packages/vue/mobile-first.ts
|
||||
/packages/vue/app.ts
|
||||
/packages/vue-icon-saas/src
|
||||
/packages/vue-icon-saas/index.ts
|
||||
|
||||
/packages/vue-runtime/pc.ts
|
||||
/packages/vue-runtime/mobile.ts
|
||||
|
|
@ -16,6 +24,12 @@ test-results
|
|||
/packages/vue-runtime/all.ts
|
||||
/packages/vue-runtime/simple.ts
|
||||
|
||||
/packages/react/index.ts
|
||||
/packages/react/pc.ts
|
||||
/packages/react/mobile.ts
|
||||
/packages/react/app.ts
|
||||
/packages/react/mobile-first.ts
|
||||
|
||||
/examples/**/playwright-report
|
||||
/examples/**/test-results
|
||||
vite.config.ts.timestamp*
|
||||
|
|
@ -44,10 +58,14 @@ yarn-error.log*
|
|||
tgzs
|
||||
*.tgz
|
||||
|
||||
# theme 的汇总文件是自动生成的
|
||||
packages/theme/src/old-theme-index.less
|
||||
packages/theme/src/index.less
|
||||
packages/theme/src/token.js
|
||||
|
||||
packages/theme/scripts/theme.json
|
||||
packages/theme/scripts/theme-result.txt
|
||||
packages/theme/scripts/themeExcel.xlsx
|
||||
|
||||
packages/theme/src/theme/*-theme/component.js
|
||||
packages/theme/src/aurora-theme
|
||||
packages/theme/src/smb-theme
|
||||
|
||||
pnpm-lock.yaml
|
||||
gulp/bundle.json
|
||||
|
|
@ -61,9 +79,4 @@ test-results
|
|||
examples/sites/public/tiny-vue*.js
|
||||
examples/sites/public/tiny-vue*.mjs
|
||||
examples/sites/public/tailwind.css
|
||||
examples/sites/public/index.css
|
||||
|
||||
|
||||
# next 屏蔽
|
||||
next/cloud-icons/categorys.ts
|
||||
next/scene-theme/docs/.vitepress/cache
|
||||
examples/sites/public/index.css
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
# 兼容node25
|
||||
pnpm exec commitlint --edit
|
||||
|
||||
npx --no-install commitlint --edit
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
# 兼容node25
|
||||
pnpm exec lint-staged
|
||||
npx lint-staged
|
||||
|
|
|
|||
|
|
@ -80,15 +80,6 @@
|
|||
"backgroundColor": "transparent",
|
||||
"bold": false,
|
||||
"italic": false
|
||||
},
|
||||
{
|
||||
"tag": "--",
|
||||
"color": "#FFFFFF",
|
||||
"strikethrough": false,
|
||||
"underline": false,
|
||||
"backgroundColor": "#5CB300",
|
||||
"bold": false,
|
||||
"italic": false
|
||||
}
|
||||
],
|
||||
"vue.codeActions.enabled": false
|
||||
|
|
|
|||
206
AGENTS.md
206
AGENTS.md
|
|
@ -1,206 +0,0 @@
|
|||
# AGENTS.md
|
||||
|
||||
> 面向 AI 编码代理(Copilot、Cursor、Aider、Claude Code 等)的项目指南。
|
||||
> 这是一份机器可读的文档,帮助 AI 助手理解本仓库的结构、规范和约束。
|
||||
|
||||
## 项目概述
|
||||
|
||||
**TinyVue** 是 OpenTiny 开源生态的核心组件库,具备以下特征:
|
||||
|
||||
- 同时支持 **Vue 2.6 / Vue 2.7 / Vue 3**(通过分包发布,一套源码跨框架复用)
|
||||
- 支持 **PC 端**与**移动端**两种终端形态
|
||||
- 采用 **Renderless(无渲染层)** 与 **视图层**分离架构:业务逻辑写在 `packages/renderless`,视图写在 `packages/vue`
|
||||
- 支持主题切换:Aurora / SAAS 主题,样式代码在 `packages/theme` 与 `packages/theme-saas`
|
||||
- Monorepo 结构,使用 **pnpm Workspaces** 管理
|
||||
|
||||
## 关键目录结构
|
||||
|
||||
```text
|
||||
tiny-vue/
|
||||
├── packages/
|
||||
│ ├── vue/ # Vue 组件视图层(.vue 文件)
|
||||
│ ├── renderless/ # 无渲染逻辑层(纯 JS/TS,框架无关)
|
||||
│ ├── vue-common/ # 跨框架公共适配层
|
||||
│ ├── vue-hooks/ # 自定义 Vue Hooks
|
||||
│ ├── vue-locale/ # 国际化
|
||||
│ ├── vue-icon/ # 图标组件
|
||||
│ ├── utils/ # 通用工具函数
|
||||
│ ├── theme/ # Aurora 主题(Less)
|
||||
│ └── theme-saas/ # SAAS 主题(Less)
|
||||
├── examples/
|
||||
│ ├── vue3/ # Vue 3 开发示例与单元/E2E 测试
|
||||
│ ├── vue2/ # Vue 2 开发示例
|
||||
│ └── sites/ # 官方文档站点
|
||||
├── internals/
|
||||
│ └── cli/ # 构建脚本与 CLI 工具
|
||||
└── .github/ # CI/CD 工作流配置
|
||||
```
|
||||
|
||||
## 环境要求
|
||||
|
||||
| 工具 | 版本要求 |
|
||||
| ---- | -------------------------------------- |
|
||||
| Node | `>= 18` |
|
||||
| pnpm | `>= 9.5`(必须,禁止使用 npm 或 yarn) |
|
||||
|
||||
## 核心命令
|
||||
|
||||
```bash
|
||||
# 安装依赖(只能用 pnpm)
|
||||
pnpm i
|
||||
|
||||
# 启动 Vue 3 开发示例(默认)
|
||||
pnpm dev # → http://127.0.0.1:7130/
|
||||
|
||||
# 启动 Vue 2 开发示例
|
||||
pnpm dev2
|
||||
|
||||
# 运行单元测试
|
||||
pnpm test:unit # Vue 3
|
||||
pnpm test:unit2 # Vue 2
|
||||
|
||||
# 运行 E2E 测试
|
||||
pnpm test:e2e # Vue 3(Chromium)
|
||||
pnpm test:e2e2 # Vue 2(Chromium)
|
||||
|
||||
# 格式化代码
|
||||
pnpm format
|
||||
|
||||
# ESLint 检查与修复
|
||||
pnpm lint
|
||||
|
||||
# 构建组件库
|
||||
pnpm build:ui
|
||||
```
|
||||
|
||||
## 架构约定
|
||||
|
||||
### Renderless 模式(重要)
|
||||
|
||||
TinyVue 使用 **Renderless 无渲染架构**。修改或新增组件时,必须遵守:
|
||||
|
||||
- **业务逻辑** → 写在 `packages/renderless/<component-name>/index.ts`,不依赖任何 UI 框架
|
||||
- **视图模板** → 写在 `packages/vue/<component-name>/index.vue`,通过 `setup` 调用 renderless 层
|
||||
- 不得在 `.vue` 文件中直接写业务逻辑(data 处理、状态计算等)
|
||||
|
||||
### 跨版本兼容
|
||||
|
||||
- 一份 renderless 逻辑同时服务 Vue 2 和 Vue 3
|
||||
- 若需判断框架版本,使用 `packages/vue-common` 中的适配工具,不要直接写 `if (vue3)`
|
||||
- 不得在 renderless 层导入任何 Vue 特定 API(`ref`、`reactive` 等)
|
||||
|
||||
## 代码规范
|
||||
|
||||
### 格式化
|
||||
|
||||
使用项目内置的 Prettier 配置(`.prettierrc`),主要规则:
|
||||
|
||||
```json
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2
|
||||
}
|
||||
```
|
||||
|
||||
- 禁止提交未格式化的代码
|
||||
- 代码提交前会经过 `lint-staged` 自动检查
|
||||
|
||||
### TypeScript
|
||||
|
||||
- 优先使用 TypeScript 编写新代码
|
||||
- 禁止使用 `any`,需要类型逃逸时应加注释说明原因
|
||||
- 类型定义放在 `packages/vue-common/src/types.ts` 或组件自身目录下
|
||||
|
||||
### 组件命名
|
||||
|
||||
- 组件文件夹:`kebab-case`(如 `action-menu`)
|
||||
- Vue 组件名:`PascalCase`(如 `TinyActionMenu`)
|
||||
- 组件 Props:`camelCase`
|
||||
- CSS 类名:`BEM` 命名,前缀 `tiny-`(如 `tiny-button__icon`)
|
||||
|
||||
## Commit 规范
|
||||
|
||||
严格遵循 [Conventional Commits](https://www.conventionalcommits.org/zh-hans/v1.0.0/) 规范:
|
||||
|
||||
```text
|
||||
<type>(<scope>): <description>
|
||||
```
|
||||
|
||||
**允许的 type:**
|
||||
|
||||
| type | 用途 |
|
||||
| ---------- | ---------------------- |
|
||||
| `feat` | 新功能 |
|
||||
| `fix` | 缺陷修复 |
|
||||
| `docs` | 文档变更 |
|
||||
| `style` | 代码格式(不影响逻辑) |
|
||||
| `refactor` | 重构(无新功能/修复) |
|
||||
| `perf` | 性能优化 |
|
||||
| `test` | 测试用例 |
|
||||
| `chore` | 构建/工具链变更 |
|
||||
| `ci` | CI/CD 配置 |
|
||||
| `revert` | 回滚提交 |
|
||||
|
||||
**scope 规范:**
|
||||
|
||||
- 包名:`vue-renderless`、`theme`、`utils` 等
|
||||
- 组件名(小写中划线):`button`、`action-menu`、`select` 等
|
||||
- 目录名:`internals/cli`、`examples/sites` 等
|
||||
|
||||
**示例:**
|
||||
|
||||
```text
|
||||
feat(vue/button): 新增 loading 动画插槽支持
|
||||
fix(vue-renderless/select): 修复过滤后重复选中问题
|
||||
docs(alert): 补充无障碍使用说明
|
||||
test(button): 新增 E2E 测试用例
|
||||
```
|
||||
|
||||
## 禁止事项(Do NOT)
|
||||
|
||||
- ❌ 不得使用 `npm` 或 `yarn`,只能用 `pnpm`
|
||||
- ❌ 不得在 renderless 层引入 Vue 框架相关 API
|
||||
- ❌ 不得在 `.vue` 文件中写业务逻辑,应下沉到 renderless 层
|
||||
- ❌ 不得硬编码颜色值,必须使用 CSS 变量(`packages/theme` 中的 Less 变量)
|
||||
- ❌ 不得修改 `pnpm-lock.yaml`(除非有意更新依赖版本)
|
||||
- ❌ 不得跳过 ESLint / Prettier 检查(`--no-verify`)
|
||||
- ❌ 不得修改 `packages/dist2/**` 和 `packages/dist3/**` 下的文件(构建产物,由 CI 生成)
|
||||
- ❌ commit message 不符合 Conventional Commits 规范的 PR 不会被合并
|
||||
|
||||
## 测试要求
|
||||
|
||||
- 修复 Bug 时:必须同步补充能复现该 Bug 的测试用例
|
||||
- 新增 Feature 时:需补充对应的单元测试,优先考虑同步 E2E 测试
|
||||
- 单元测试位置:`examples/vue3/src/**/__tests__/`
|
||||
- E2E 测试位置:`examples/sites/demos/{pc,mobile-first}/app/<component-name>/`
|
||||
- 测试框架:Vitest(单元)+ Playwright(E2E)
|
||||
|
||||
## E2E 测试触发
|
||||
|
||||
在 PR 标题中添加 `[componentName1, componentName2]` 可手动触发对应组件的 E2E 测试:
|
||||
|
||||
```text
|
||||
fix(vue-renderless/select): [select, cascader] 修复级联过滤问题
|
||||
```
|
||||
|
||||
## Pull Request 检查清单
|
||||
|
||||
PR 提交前,请确认以下各项:
|
||||
|
||||
- [ ] commit message 符合 Conventional Commits 规范
|
||||
- [ ] 新增或修改的代码已格式化(`pnpm format`)
|
||||
- [ ] ESLint 无报错(`pnpm lint`)
|
||||
- [ ] 单元测试已补充并通过(`pnpm test:unit`)
|
||||
- [ ] 视图变更已在 Vue 2 / Vue 3 两个环境验证
|
||||
- [ ] 无破坏性变更;若有,已在 PR 描述中说明
|
||||
|
||||
## 参考文档
|
||||
|
||||
- 官方文档:[https://opentiny.design/tiny-vue](https://opentiny.design/tiny-vue)
|
||||
- 贡献指南:[CONTRIBUTING.md](./CONTRIBUTING.md)
|
||||
- 贡献指南(中文):[CONTRIBUTING.zh-CN.md](./CONTRIBUTING.zh-CN.md)
|
||||
- GitHub Issues:[https://github.com/opentiny/tiny-vue/issues](https://github.com/opentiny/tiny-vue/issues)
|
||||
- GitHub Discussions:[https://github.com/opentiny/tiny-vue/discussions](https://github.com/opentiny/tiny-vue/discussions)
|
||||
|
|
@ -57,15 +57,17 @@ The commit message should be in the form of a `type(scope): description of the m
|
|||
|
||||
1. The specification of the title is the same as the commit information, which is entered in the form of `type(scope): description information`.
|
||||
|
||||
2. Trigger component **e2e test**: CI will automatically trigger the test cases of the corresponding components according to the files modified by the PR. If you need to manually trigger the test of a specific component, you can add the component name in the format of `[componentName1, componentName2]` in the Pull Request title, and the system will execute the test cases of these specified components.
|
||||
2. **e2e test** of trigger components: Adding '[componentName1, componentName2]' to the Pull Request title will execute test cases for componentName1, componentName2, and declare this structure when changes to code affect components.
|
||||
|
||||
- Note: The `github action` under this project will use the component name declared by `[componentName1, componentName2]` to match the path name under the `examples/sites/demos` directory to identify the test e2e cases to be executed. (Because running all test cases is too time-consuming)
|
||||
- Note: The `github action` in this project identifies test e2e use cases to be executed by matching the component names declared by `[componentName1, componentName2]` with the path names in the `examples/sites/demos` directory. (Because running the full test case is too time consuming)
|
||||
|
||||
- When modifying a sub-component of a component, such as the `col` component, which has no corresponding examples and test cases, the `layout` component should be tested, because the `col` component is a sub-component of the `layout` component. The PR title can be: `fix(col): [layout] xxxxxxxxxxxxxx`
|
||||
|
||||
3. Example title:
|
||||
|
||||
- Added alert component documentation: `docs(alert): xxxxxxxxxxxxxxx`, `docs(site):xxxxxxxxxxxxxxx`
|
||||
- Added alert component test cases: `test(alert): xxxxxxxxxxxxxx`
|
||||
- Fixed the defect of alert component @opentiny/vue-renderless (manually trigger e2e test cases): `fix(vue-renderless/alert): [alert] xxxxxxxxxxxxxxx`
|
||||
- Supplement alert component document: `docs(alert): [alert] xxxxxxxxxxxxxxx`, `docs(site): [alert] xxxxxxxxxxxxxxx`
|
||||
- Supplement alert component test case: `test(alert): [alert] xxxxxxxxxxxxxx`
|
||||
- Fixed bug in alert component @opentiny/vue-renderless: `fix(vue-renderless/alert): [alert] xxxxxxxxxxxxxx`
|
||||
|
||||
#### Pull Request Description
|
||||
|
||||
|
|
|
|||
|
|
@ -57,15 +57,17 @@ commit 信息要以 `type(scope): 描述信息` 的形式填写,例如 `fix(vu
|
|||
|
||||
1. 标题的规范与 commit 信息一样,以`type(scope): 描述信息` 的形式填写。
|
||||
|
||||
2. 触发组件的 **e2e 测试**: CI 会自动根据 PR 修改的文件来触发相应组件的测试用例。如果需要手动触发特定组件的测试,可以在 Pull Request 标题中添加 `[componentName1, componentName2]` 格式的组件名称,系统将会执行这些指定组件的测试用例。
|
||||
2. 触发组件的 **e2e 测试**: 在 Pull Request 标题里添加 `[componentName1, componentName2]`,将会执行 componentName1,componentName2 的测试用例,当改动的代码会影响组件时,需要声明这个结构。
|
||||
|
||||
- 注:本项目下的 `github action` 会用`[componentName1, componentName2]`声明的组件名匹配 `examples/sites/demos` 目录下的路径名称,识别要执行的测试e2e用例。(因为全量跑测试用例太耗费时间)
|
||||
|
||||
- 当修改了某个组件的子组件,比如`col`组件,它本身没有对应的示例以及测试用例,这时应该要测试的是`layout`组件,因为`col`组件是`layout`组件的子组件,PR 标题可以这样: `fix(col): [layout] xxxxxxxxxxxxxx`
|
||||
|
||||
3. 标题示例:
|
||||
|
||||
- 补充 alert 组件文档: `docs(alert): xxxxxxxxxxxxxxx`, `docs(site):xxxxxxxxxxxxxxx`
|
||||
- 补充 alert 组件测试用例: `test(alert): xxxxxxxxxxxxxx`
|
||||
- 修复 alert 组件 @opentiny/vue-renderless 下的缺陷(手动触发 e2e 测试用例): `fix(vue-renderless/alert): [alert] xxxxxxxxxxxxxx`
|
||||
- 补充 alert 组件文档: `docs(alert): [alert] xxxxxxxxxxxxxxx`, `docs(site): [alert] xxxxxxxxxxxxxxx`
|
||||
- 补充 alert 组件测试用例: `test(alert): [alert] xxxxxxxxxxxxxx`
|
||||
- 修复 alert 组件 @opentiny/vue-renderless 下的缺陷: `fix(vue-renderless/alert): [alert] xxxxxxxxxxxxxx`
|
||||
|
||||
#### Pull Request 的描述
|
||||
|
||||
|
|
@ -143,4 +145,3 @@ pnpm dev:vue2
|
|||
```
|
||||
/examples/sites/src/data/contributorMap.js
|
||||
```
|
||||
|
||||
|
|
|
|||
109
PACKAGES.md
109
PACKAGES.md
|
|
@ -1,109 +0,0 @@
|
|||
# TinyVue 仓库结构说明
|
||||
|
||||
## packages/
|
||||
|
||||
### 1. 核心组件与运行时
|
||||
|
||||
├── vue/ # Vue核心组件库
|
||||
│ └── src/ # 源码目录
|
||||
│ ├── alert/ # alert组件源码目录
|
||||
│ ├── button/ # button组件源码目录
|
||||
│ └── ... # 其他组件
|
||||
│
|
||||
├── vue-runtime/ # Vue运行时支持
|
||||
│
|
||||
├── vue-common/ # Vue2/Vue3通用基础组件库
|
||||
│ └── src/
|
||||
│ ├── adapter/ # Vue2/Vue3适配层
|
||||
│ │ ├── vue2/ # Vue2版本适配器
|
||||
│ │ └── vue3/ # Vue3版本适配器
|
||||
│
|
||||
├── renderless/ # 无渲染组件逻辑
|
||||
│ └── src/
|
||||
│ ├── alert/ # Alert组件逻辑层
|
||||
│ ├── button/ # Button组件逻辑层
|
||||
│ └── ... # 其他组件逻辑层
|
||||
│
|
||||
├── vue-hooks/ # Vue Hooks集合
|
||||
│ └── src/ # Hooks源码
|
||||
|
||||
### 2. 主题与样式
|
||||
|
||||
├── theme/ # 主题系统
|
||||
│ └── src/
|
||||
│ ├── alert/ # Alert组件样式
|
||||
│ ├── button/ # Button组件样式
|
||||
│ └── ... # 其他组件样式
|
||||
│
|
||||
├── theme-saas/ # SaaS主题包
|
||||
│ └── src/
|
||||
│ ├── alert/ # Alert组件样式
|
||||
│ ├── button/ # Button组件样式
|
||||
│ └── ... # 其他组件样式
|
||||
|
||||
### 3. 图标相关
|
||||
|
||||
├── vue-icon/ # Vue图标组件库
|
||||
│ └── src/ # 源码目录
|
||||
│
|
||||
├── vue-icon-saas/ # SaaS图标组件
|
||||
│ └── src/ # 源码目录
|
||||
│
|
||||
├── vue-icon-multicolor/ # 多色图标支持
|
||||
│ └── src/ # 源码目录
|
||||
|
||||
### 5. 工具与指令
|
||||
|
||||
├── utils/ # 工具函数库
|
||||
│ └── src/
|
||||
│ ├── dom/ # DOM相关工具函数
|
||||
│ ├── is/ # 类型判断工具函数
|
||||
│ └── ... # 其他工具函数
|
||||
│
|
||||
├── vue-directive/ # Vue指令集合
|
||||
│ └── src/ # 指令源码
|
||||
|
||||
### 6. 国际化
|
||||
|
||||
├── vue-locale/ # 国际化支持
|
||||
│ └── src/
|
||||
│ ├── vue2/ # Vue2版本国际化适配器
|
||||
│ ├── vue3/ # Vue3版本国际化适配器
|
||||
│ └── lang/ # 语言包资源
|
||||
|
||||
### 7. 设计规范
|
||||
|
||||
├── design/ # 设计规范和资源
|
||||
├── aurora/ # Aurora设计规范
|
||||
├── saas/ # SaaS设计规范
|
||||
└── smb/ # SMB设计规范
|
||||
|
||||
---
|
||||
|
||||
## examples/ # 示例代码
|
||||
|
||||
├── sites/ # 本地官网
|
||||
├── vue3/ # Vue3本地开发工程
|
||||
├── vue2/ # Vue2本地开发工程
|
||||
├── docs/ # 本地开发模板包含 pc 和 mobile-first 模式
|
||||
└── public/ # 公共资源
|
||||
|
||||
---
|
||||
|
||||
## internals/ # 内部工具和配置
|
||||
|
||||
├── cli/ # 构建脚本
|
||||
├── automate/ # 内部自动化相关脚本
|
||||
├── playwright-config/ # playwright测试配置
|
||||
├── unplugin-virtual-template/ # vite插件解析虚拟模块
|
||||
└── vue-test-utils/ # vitest测试工具,提供 mountPcMode 和 mountMobilefirstMode
|
||||
|
||||
---
|
||||
|
||||
### 主要目录说明
|
||||
|
||||
1. **packages/**: 核心源码包,分为组件、主题、图标、SaaS、工具、国际化等子模块,便于维护和按需引入。
|
||||
2. **examples/**: 示例代码,包含多版本Vue的示例、文档和演示站点,用于测试和展示。
|
||||
3. **internals/**: 内部工具,包含构建工具、配置和内部开发支持。
|
||||
|
||||
---
|
||||
52
README.md
52
README.md
|
|
@ -6,8 +6,6 @@
|
|||
|
||||
<p align="center">An enterprise-class UI component library, support both Vue.js 2 and Vue.js 3, as well as PC and mobile.</p>
|
||||
|
||||
[](https://deepwiki.com/opentiny/tiny-vue)
|
||||
|
||||
English | [简体中文](README.zh-CN.md)
|
||||
|
||||
🌈 Features:
|
||||
|
|
@ -20,39 +18,6 @@ English | [简体中文](README.zh-CN.md)
|
|||
- 📊 Components support configuration development, can support low-code platform.
|
||||
- 💡 Use a cross-end and cross-framework architecture, flexible and portable.
|
||||
|
||||
## Build with AI
|
||||
|
||||
`tiny-vue-skill` is a set of skill tools designed for AI coding assistants, enabling them to deeply understand the usage, APIs, and best practices of the TinyVue component library, thereby helping you develop applications with TinyVue more efficiently.
|
||||
|
||||
You can install `tiny-vue-skill` globally using the following command:
|
||||
|
||||
```bash
|
||||
npx skills add opentiny/agent-skills -g --skill tiny-vue-skill --agent cursor
|
||||
```
|
||||
|
||||

|
||||
|
||||
The `--agent` parameters corresponding to some other AI tools:
|
||||
|
||||
| Agent | `--agent` |
|
||||
| -------------- | ---------------- |
|
||||
| Amp | `amp` |
|
||||
| Antigravity | `antigravity` |
|
||||
| Claude Code | `claude-code` |
|
||||
| Clawdbot | `clawdbot` |
|
||||
| Codex | `codex` |
|
||||
| Cursor | `cursor` |
|
||||
| Droid | `droid` |
|
||||
| Gemini CLI | `gemini-cli` |
|
||||
| GitHub Copilot | `github-copilot` |
|
||||
| Goose | `goose` |
|
||||
| Kilo Code | `kilo` |
|
||||
| Kiro CLI | `kiro-cli` |
|
||||
| OpenCode | `opencode` |
|
||||
| Roo Code | `roo` |
|
||||
| Trae | `trae` |
|
||||
| Windsurf | `windsurf` |
|
||||
|
||||
## 🛠️ Usage
|
||||
|
||||
### 1. Installation
|
||||
|
|
@ -205,23 +170,6 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|||
<td align="center" valign="top" width="12.5%"><a href="http://simonhe.me"><img src="https://avatars.githubusercontent.com/u/57086651?v=4?s=100" width="100px;" alt="Simon He"/><br /><sub><b>Simon He</b></sub></a><br /><a href="https://github.com/opentiny/tiny-vue/commits?author=Simon-He95" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="12.5%"><a href="https://study.linlove.cn/"><img src="https://avatars.githubusercontent.com/u/44194929?v=4?s=100" width="100px;" alt="zhangpaopao"/><br /><sub><b>zhangpaopao</b></sub></a><br /><a href="#tool-zhangpaopao0609" title="Tools">🔧</a></td>
|
||||
<td align="center" valign="top" width="12.5%"><a href="https://github.com/Nowitzki41"><img src="https://avatars.githubusercontent.com/u/22088253?v=4?s=100" width="100px;" alt="Nowitzki41"/><br /><sub><b>Nowitzki41</b></sub></a><br /><a href="https://github.com/opentiny/tiny-vue/commits?author=Nowitzki41" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="12.5%"><a href="https://github.com/dufu1991"><img src="https://avatars.githubusercontent.com/u/24769077?v=4?s=100" width="100px;" alt="dufu1991"/><br /><sub><b>dufu1991</b></sub></a><br /><a href="https://github.com/opentiny/tiny-vue/commits?author=dufu1991" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="12.5%"><a href="https://github.com/hu-qi"><img src="https://avatars.githubusercontent.com/u/17986122?v=4?s=100" width="100px;" alt="huqi"/><br /><sub><b>huqi</b></sub></a><br /><a href="https://github.com/opentiny/tiny-vue/commits?author=hu-qi" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="12.5%"><a href="https://github.com/tsinghua-lau"><img src="https://avatars.githubusercontent.com/u/43164478?v=4?s=100" width="100px;" alt="Mao Tao"/><br /><sub><b>Mao Tao</b></sub></a><br /><a href="https://github.com/opentiny/tiny-vue/commits?author=tsinghua-lau" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="12.5%"><a href="https://github.com/Darkingtail"><img src="https://avatars.githubusercontent.com/u/51188676?v=4?s=100" width="100px;" alt="Darkingtail"/><br /><sub><b>Darkingtail</b></sub></a><br /><a href="https://github.com/opentiny/tiny-vue/issues?q=author%3ADarkingtail" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="12.5%"><a href="https://github.com/hashiqi12138"><img src="https://avatars.githubusercontent.com/u/52161363?v=4?s=100" width="100px;" alt="hashiqi"/><br /><sub><b>hashiqi</b></sub></a><br /><a href="https://github.com/opentiny/tiny-vue/issues?q=author%3Ahashiqi12138" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="12.5%"><a href="https://github.com/discreted66"><img src="https://avatars.githubusercontent.com/u/190872652?v=4?s=100" width="100px;" alt="liukun"/><br /><sub><b>liukun</b></sub></a><br /><a href="https://github.com/opentiny/tiny-vue/commits?author=discreted66" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="12.5%"><a href="https://github.com/lcy0620"><img src="https://avatars.githubusercontent.com/u/188683944?v=4?s=100" width="100px;" alt="lcy0620"/><br /><sub><b>lcy0620</b></sub></a><br /><a href="https://github.com/opentiny/tiny-vue/commits?author=lcy0620" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="12.5%"><a href="https://github.com/sakurajiajia"><img src="https://avatars.githubusercontent.com/u/37933037?v=4?s=100" width="100px;" alt="木斯佳"/><br /><sub><b>木斯佳</b></sub></a><br /><a href="https://github.com/opentiny/tiny-vue/commits?author=sakurajiajia" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="12.5%"><a href="https://github.com/552847957"><img src="https://avatars.githubusercontent.com/u/8729901?v=4?s=100" width="100px;" alt="552847957"/><br /><sub><b>552847957</b></sub></a><br /><a href="https://github.com/opentiny/tiny-vue/commits?author=552847957" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="12.5%"><a href="https://github.com/Lingchen111"><img src="https://avatars.githubusercontent.com/u/123021749?v=4?s=100" width="100px;" alt="Lingchen111"/><br /><sub><b>Lingchen111</b></sub></a><br /><a href="https://github.com/opentiny/tiny-vue/commits?author=Lingchen111" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="12.5%"><a href="https://gausszhou.github.io/"><img src="https://avatars.githubusercontent.com/u/46297500?v=4?s=100" width="100px;" alt="Gauss Zhou"/><br /><sub><b>Gauss Zhou</b></sub></a><br /><a href="https://github.com/opentiny/tiny-vue/commits?author=gausszhou" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="12.5%"><a href="https://github.com/liangguanhui0117"><img src="https://avatars.githubusercontent.com/u/28801619?v=4?s=100" width="100px;" alt="LiangGuanhui"/><br /><sub><b>LiangGuanhui</b></sub></a><br /><a href="https://github.com/opentiny/tiny-vue/commits?author=liangguanhui0117" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="12.5%"><a href="https://github.com/ourcx"><img src="https://avatars.githubusercontent.com/u/173872687?v=4&size=100" width="100px;" alt="ourcx"/><br /><sub><b>ourcx</b></sub></a><br /><a href="https://github.com/opentiny/tiny-vue/commits?author=ourcx/" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@
|
|||
|
||||
<p align="center">一套跨端、跨框架的企业级 UI 组件库,支持 Vue 2 和 Vue 3,支持 PC 端和移动端。</p>
|
||||
|
||||
[](https://deepwiki.com/opentiny/tiny-vue)
|
||||
|
||||
[English](README.md) | 简体中文
|
||||
|
||||
🌈 特性:
|
||||
|
|
@ -20,39 +18,6 @@
|
|||
- 📊 组件内部支持配置式开发,可支持低代码平台可视化组件配置
|
||||
- 💡 采用模板、样式、逻辑分离的跨端、跨框架架构,保障灵活性和可移植性
|
||||
|
||||
## AI 编程
|
||||
|
||||
`tiny-vue-skill` 是一套面向 AI 编码助手的技能工具集,让 AI 助手深度理解 TinyVue 组件库的用法、API 和最佳实践,从而帮助您更高效地使用 TinyVue 开发应用。
|
||||
|
||||
你可以通过以下命令,全局安装 `tiny-vue-skill`:
|
||||
|
||||
```bash
|
||||
npx skills add opentiny/agent-skills -g --skill tiny-vue-skill --agent cursor
|
||||
```
|
||||
|
||||

|
||||
|
||||
部分其他 AI 工具对应的 `--agent` 参数:
|
||||
|
||||
| Agent | `--agent` |
|
||||
| -------------- | ---------------- |
|
||||
| Amp | `amp` |
|
||||
| Antigravity | `antigravity` |
|
||||
| Claude Code | `claude-code` |
|
||||
| Clawdbot | `clawdbot` |
|
||||
| Codex | `codex` |
|
||||
| Cursor | `cursor` |
|
||||
| Droid | `droid` |
|
||||
| Gemini CLI | `gemini-cli` |
|
||||
| GitHub Copilot | `github-copilot` |
|
||||
| Goose | `goose` |
|
||||
| Kilo Code | `kilo` |
|
||||
| Kiro CLI | `kiro-cli` |
|
||||
| OpenCode | `opencode` |
|
||||
| Roo Code | `roo` |
|
||||
| Trae | `trae` |
|
||||
| Windsurf | `windsurf` |
|
||||
|
||||
## 🛠️ 如何使用
|
||||
|
||||
### 1. 安装
|
||||
|
|
@ -191,7 +156,6 @@ pnpm dev2
|
|||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="12.5%"><a href="https://github.com/James-9696"><img src="https://avatars.githubusercontent.com/u/72028410?v=4?s=100" width="100px;" alt="James"/><br /><sub><b>James</b></sub></a><br /><a href="https://github.com/opentiny/tiny-vue/commits?author=James-9696" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="12.5%"><a href="https://github.com/ourcx"><img src="https://avatars.githubusercontent.com/u/173872687?v=4&size=100" width="100px;" alt="ourcx"/><br /><sub><b>ourcx</b></sub></a><br /><a href="https://github.com/opentiny/tiny-vue/commits?author=ourcx/" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -1,6 +1 @@
|
|||
module.exports = {
|
||||
extends: ['@commitlint/config-conventional'],
|
||||
rules: {
|
||||
'header-max-length': [1, 'always', 150]
|
||||
}
|
||||
}
|
||||
module.exports = { extends: ['@commitlint/config-conventional'] }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
package-lock.json
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# Blocks
|
||||
|
||||
使用 TinyVue 组件搭建出来的区块。
|
||||
|
||||
## 本启启动
|
||||
|
||||
```shell
|
||||
pnpm i
|
||||
pnpm -F blocks dev
|
||||
```
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
export {}
|
||||
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
RouterLink: (typeof import('vue-router'))['RouterLink']
|
||||
RouterView: (typeof import('vue-router'))['RouterView']
|
||||
TinyActionSheet: (typeof import('@opentiny/vue'))['ActionSheet']
|
||||
TinyAlert: (typeof import('@opentiny/vue'))['Alert']
|
||||
TinyBaseSelect: (typeof import('@opentiny/vue'))['BaseSelect']
|
||||
TinyButton: (typeof import('@opentiny/vue'))['Button']
|
||||
TinyCascaderSelect: (typeof import('@opentiny/vue'))['CascaderSelect']
|
||||
TinyCell: (typeof import('@opentiny/vue'))['Cell']
|
||||
TinyCheckbox: (typeof import('@opentiny/vue'))['Checkbox']
|
||||
TinyCheckboxGroup: (typeof import('@opentiny/vue'))['CheckboxGroup']
|
||||
TinyCol: (typeof import('@opentiny/vue'))['Col']
|
||||
TinyCustomSwitch: (typeof import('@opentiny/vue'))['CustomSwitch']
|
||||
TinyDialogBox: (typeof import('@opentiny/vue'))['DialogBox']
|
||||
TinyDropdown: (typeof import('@opentiny/vue'))['Dropdown']
|
||||
TinyDropdownItem: (typeof import('@opentiny/vue'))['DropdownItem']
|
||||
TinyDropdownMenu: (typeof import('@opentiny/vue'))['DropdownMenu']
|
||||
TinyException: (typeof import('@opentiny/vue'))['Exception']
|
||||
TinyFilterBox: (typeof import('@opentiny/vue'))['FilterBox']
|
||||
TinyGrid: (typeof import('@opentiny/vue'))['Grid']
|
||||
TinyGridColumn: (typeof import('@opentiny/vue'))['GridColumn']
|
||||
TinyInput: (typeof import('@opentiny/vue'))['Input']
|
||||
TinyLayout: (typeof import('@opentiny/vue'))['Layout']
|
||||
TinyModal: (typeof import('@opentiny/vue'))['Modal']
|
||||
TinyOption: (typeof import('@opentiny/vue'))['Option']
|
||||
TinyOptionGroup: (typeof import('@opentiny/vue'))['OptionGroup']
|
||||
TinyPager: (typeof import('@opentiny/vue'))['Pager']
|
||||
TinyPopover: (typeof import('@opentiny/vue'))['Popover']
|
||||
TinyRadio: (typeof import('@opentiny/vue'))['Radio']
|
||||
TinyRadioGroup: (typeof import('@opentiny/vue'))['RadioGroup']
|
||||
TinyRecycleScroller: (typeof import('@opentiny/vue'))['RecycleScroller']
|
||||
TinyRow: (typeof import('@opentiny/vue'))['Row']
|
||||
TinySearch: (typeof import('@opentiny/vue'))['Search']
|
||||
TinySelect: (typeof import('@opentiny/vue'))['Select']
|
||||
TinySelectedBox: (typeof import('@opentiny/vue'))['SelectedBox']
|
||||
TinySplit: (typeof import('@opentiny/vue'))['Split']
|
||||
TinyTabItem: (typeof import('@opentiny/vue'))['TabItem']
|
||||
TinyTabs: (typeof import('@opentiny/vue'))['Tabs']
|
||||
TinyTagGroup: (typeof import('@opentiny/vue'))['TagGroup']
|
||||
TinyTimePickerMobile: (typeof import('@opentiny/vue'))['TimePickerMobile']
|
||||
TinyTooltip: (typeof import('@opentiny/vue'))['Tooltip']
|
||||
TinyTree: (typeof import('@opentiny/vue'))['Tree']
|
||||
TinyUserHead: (typeof import('@opentiny/vue'))['UserHead']
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>TinyVue 区块</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "blocks",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@opentiny/vue": "~3.18.0",
|
||||
"vue": "^3.3.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@opentiny/unplugin-tiny-vue": "~0.0.2",
|
||||
"@vitejs/plugin-vue": "^4.5.2",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.0.8",
|
||||
"vue-tsc": "^1.8.25"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import BlockDemo from './blocks/BlockDemo.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BlockDemo />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<tiny-button>确定</tiny-button>
|
||||
<tiny-alert description="描述信息"></tiny-alert>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { createApp } from 'vue'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import autoImportPlugin from '@opentiny/unplugin-tiny-vue'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue(), autoImportPlugin('vite')],
|
||||
resolve: {
|
||||
alias: [
|
||||
// XDesign 主题
|
||||
{
|
||||
find: /\@opentiny\/vue-theme\/(?!(smb))/,
|
||||
replacement: '@opentiny/vue-theme/smb-theme/'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
|
@ -19,6 +19,7 @@
|
|||
</div>
|
||||
<div class="fi-1 of-auto">
|
||||
<tiny-pc v-if="modeState.mode === 'pc'" :show-fixed-menu="state.showFixedMenu"></tiny-pc>
|
||||
<tiny-mobile v-else-if="modeState.mode === 'mobile'" :show-fixed-menu="state.showFixedMenu"></tiny-mobile>
|
||||
<tiny-mobile-first v-else :show-fixed-menu="state.showFixedMenu"></tiny-mobile-first>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -29,6 +30,7 @@ import './style.css'
|
|||
import { ButtonGroup, Link } from '@opentiny/vue'
|
||||
import { iconEditorMenuRight, iconEditorMenuLeft } from '@opentiny/vue-icon'
|
||||
import TinyPc from './pc.vue'
|
||||
import TinyMobile from './mobile.vue'
|
||||
import TinyMobileFirst from './mobile-first.vue'
|
||||
import { hooks } from '@opentiny/vue-common'
|
||||
import { useModeCtx } from './uses'
|
||||
|
|
@ -36,6 +38,7 @@ import { useModeCtx } from './uses'
|
|||
export default {
|
||||
components: {
|
||||
TinyPc,
|
||||
TinyMobile,
|
||||
TinyMobileFirst,
|
||||
TinyButtonGroup: ButtonGroup,
|
||||
TinyLink: Link,
|
||||
|
|
@ -47,6 +50,7 @@ export default {
|
|||
const state = hooks.reactive({
|
||||
groupData: [
|
||||
{ text: 'PC', value: 'pc' },
|
||||
{ text: 'Mobile', value: 'mobile' },
|
||||
{ text: 'Mobile-first', value: 'mobile-first' }
|
||||
],
|
||||
vueVersion: hooks.version,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,189 @@
|
|||
<template>
|
||||
<div class="wp100 hp100 f-r of-hidden">
|
||||
<div class="w230 pt20 of-auto sm-hidden b-r bg-white" :class="{ 'fixed-menu': showFixedMenu }">
|
||||
<tiny-tree-menu
|
||||
class="!w213"
|
||||
:data="menuData"
|
||||
:filter-node-method="fn.searchMenu"
|
||||
@current-change="fn.clickMenu"
|
||||
></tiny-tree-menu>
|
||||
</div>
|
||||
<div class="fi-1 f-c px20 pb30 f-c pr200 of-auto">
|
||||
<!-- 标题 -->
|
||||
<div class="py20">
|
||||
<component :is="state.currMd"></component>
|
||||
</div>
|
||||
<div id="preview" class="bg-white">
|
||||
<div class="mb20 py10 pl16 child<code>p4 child<code>bg-lightless">
|
||||
<div class="mr20 fw-bold">
|
||||
{{ state.currDemo?.name['zh-CN'] }}( <span class="allselect">{{ state.currDemo?.codeFiles[0] }}</span
|
||||
>):
|
||||
</div>
|
||||
<div v-html="state.currDemo?.desc['zh-CN']"></div>
|
||||
</div>
|
||||
<!-- 预览 -->
|
||||
<div class="rel px20" :id="state.currDemo?.demoId">
|
||||
<div class="phone-container" @dblclick="fn.openInVscode(state.currDemo)">
|
||||
<div class="mobile-view-container">
|
||||
<component :is="state.comp"></component>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- API表格 -->
|
||||
<div v-if="state.currApi?.length" class="mt20 f24 fw-bold">组件API</div>
|
||||
<div v-for="(oneGroup, idx) in state.currApi" :key="idx">
|
||||
<div class="mt20 f-r f-pos-start fw-bold">
|
||||
<div :id="oneGroup.name" class="f18">
|
||||
{{ oneGroup.name }}
|
||||
</div>
|
||||
<div class="ml12 b-a-primary c-primary px8 py4">
|
||||
{{ oneGroup.type }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="(oneApiArr, key) in oneGroup" :key="key">
|
||||
<div v-if="key !== 'name' && key !== 'type' && oneApiArr.length > 0">
|
||||
<div class="f18 py28">
|
||||
{{ key }}
|
||||
</div>
|
||||
<table class="api-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="20%">名称</th>
|
||||
<th width="15%">类型</th>
|
||||
<th width="20%">默认值</th>
|
||||
<th width="55%">说明</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in oneApiArr" :key="row.name">
|
||||
<td>
|
||||
<a v-if="row.demoId" class="c-primary h:c-error cur-hand" @click="fn.selectDemo(row.demoId)">{{
|
||||
row.name
|
||||
}}</a>
|
||||
<span v-else>{{ row.name }}</span>
|
||||
</td>
|
||||
<td>{{ row.type }}</td>
|
||||
<td v-html="typeof row.defaultValue === 'string' ? row.defaultValue || '--' : row.defaultValue"></td>
|
||||
<td v-html="row.desc['zh-CN']"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右边浮动所有的demos -->
|
||||
<tiny-floatbar v-if="state.demos?.length > 0" class="!top120 !z1 !right25">
|
||||
<div class="f12 ofy-auto">
|
||||
<div
|
||||
v-for="demo in state.demos"
|
||||
:key="demo.demoId"
|
||||
@click="fn.selectDemo(demo.demoId)"
|
||||
class="w130 px10 py4 bg-light f-r f-pos-between"
|
||||
:class="{ 'c-error': state.currDemo === demo }"
|
||||
>
|
||||
<div class="link-primary h:c-error h:td-under ellipsis">
|
||||
{{ demo.name['zh-CN'] }}
|
||||
<Icon-star-icon v-if="state.currDemo === demo" style="fill: #ee343f" />
|
||||
</div>
|
||||
<IconOpeninVscode @click.stop="fn.openInVscode(demo)" class="f18 cur-hand" />
|
||||
</div>
|
||||
</div>
|
||||
</tiny-floatbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { hooks } from '@opentiny/vue-common'
|
||||
import { Floatbar, TreeMenu, Button, Tooltip } from '@opentiny/vue'
|
||||
import { iconStarActive, iconSelect } from '@opentiny/vue-icon'
|
||||
import { menuData, demos, demoStr, demoVue, mds } from './resourceMobile.js'
|
||||
import { useModeCtx } from './uses'
|
||||
import { getDemosConfig, getApisConfig } from './utils/componentsDoc'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
showFixedMenu: Boolean
|
||||
},
|
||||
components: {
|
||||
TinyFloatbar: Floatbar,
|
||||
TinyTreeMenu: TreeMenu,
|
||||
TinyButton: Button,
|
||||
TinyTooltip: Tooltip,
|
||||
IconStarIcon: iconStarActive(),
|
||||
IconOpeninVscode: iconSelect()
|
||||
},
|
||||
setup() {
|
||||
const { state: modeState, fn: modeFn } = useModeCtx()
|
||||
const state = hooks.reactive({
|
||||
demos: [], // 组件的所有示例
|
||||
currDemo: null, // 选中的demo
|
||||
currApi: [], // 当前path下的api
|
||||
comp: null, // 当前示例的组件实例
|
||||
currDemoSrc: '',
|
||||
currMd: hooks.computed(() => mds[`${modeState.pathName}.cn.md`])
|
||||
})
|
||||
const fn = {
|
||||
// 菜单搜索:忽略大小写
|
||||
searchMenu: (value, data) => {
|
||||
if (!value) return true
|
||||
return data.label.toLowerCase().includes(value.toLowerCase())
|
||||
},
|
||||
// 点击菜单:如果是二级菜单,根据path 刷新整个页面内容
|
||||
clickMenu: async (menu) => {
|
||||
if (menu.nameCn && menu.key !== state.key) {
|
||||
modeState.pathName = menu.key
|
||||
await _switchPath()
|
||||
}
|
||||
},
|
||||
// 点击示例
|
||||
selectDemo: async (demoId) => {
|
||||
const demo = state.demos.find((d) => d.demoId === demoId)
|
||||
if (state.currDemo !== demo) {
|
||||
state.currDemo = demo
|
||||
await _switchDemo()
|
||||
}
|
||||
},
|
||||
openInVscode: (demo) => {
|
||||
fetch(`/__open-in-editor?file=../sites/demos/mobile/app/${modeState.pathName}/${demo.codeFiles[0]}`)
|
||||
}
|
||||
}
|
||||
|
||||
hooks.onMounted(() => {
|
||||
_switchPath()
|
||||
})
|
||||
|
||||
// 以下私有方法,无须传递给vue模板的。
|
||||
async function _switchPath() {
|
||||
const demosModule = demos[`../../sites/demos/mobile/app/${modeState.pathName}/webdoc/${modeState.pathName}.js`]
|
||||
const demosConfig = await getDemosConfig(demosModule)
|
||||
state.demos = demosConfig.demos
|
||||
state.currDemo = state.demos.find((d) => d.demoId === modeState.demoId) || state.demos?.[0]
|
||||
state.currApi = (await getApisConfig(modeState.pathName, 'mobile')).apis
|
||||
await _switchDemo()
|
||||
}
|
||||
async function _switchDemo() {
|
||||
modeState.demoId = state.currDemo.demoId
|
||||
const path = `../../sites/demos/mobile/app/${modeState.pathName}/${state.currDemo?.codeFiles[0]}`
|
||||
|
||||
// 查找源码 查找组件
|
||||
state.currDemoSrc = await demoStr[path]()
|
||||
const comp = await demoVue[path]()
|
||||
|
||||
state.comp = hooks.markRaw(comp.default)
|
||||
|
||||
modeFn.cacheCtx()
|
||||
modeFn.pushToUrl()
|
||||
}
|
||||
|
||||
return {
|
||||
menuData,
|
||||
state,
|
||||
fn,
|
||||
modeState,
|
||||
modeFn
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -96,14 +96,8 @@
|
|||
</div>
|
||||
</div>
|
||||
</tiny-floatbar>
|
||||
<!-- 切换主题 暂时先屏蔽,等后续其他主题验收完成再放出此功能 -->
|
||||
<tiny-dropdown
|
||||
v-if="false"
|
||||
class="!fixed bottom20 right140"
|
||||
:show-icon="false"
|
||||
@item-click="changeTheme"
|
||||
:disabled="isSaasMode"
|
||||
>
|
||||
<!-- 切换主题 -->
|
||||
<tiny-dropdown class="!fixed bottom20 right140" :show-icon="false" @item-click="changeTheme" :disabled="isSaasMode">
|
||||
<span title="切换主题">
|
||||
<SvgTheme></SvgTheme>
|
||||
</span>
|
||||
|
|
@ -157,6 +151,8 @@ import {
|
|||
} from '@opentiny/vue'
|
||||
import { iconStarActive, iconSelect } from '@opentiny/vue-icon'
|
||||
import Loading from '@opentiny/vue-loading'
|
||||
import designSmbConfig from '@opentiny/vue-design-smb'
|
||||
import designAuroraConfig from '@opentiny/vue-design-aurora'
|
||||
import designSaasConfig from '@opentiny/vue-design-saas'
|
||||
import { menuData, demoStr, demoVue, mds, demos } from './resourcePc.js'
|
||||
import { useTheme, useModeCtx } from './uses'
|
||||
|
|
@ -198,7 +194,7 @@ export default {
|
|||
currMd: hooks.computed(() => mds[`${modeState.pathName}.cn.md`]),
|
||||
demoLoading: false
|
||||
})
|
||||
// hui chart 新增图表类型,新增图表的 api 和原有图表的api 区分开。
|
||||
// hui chart 新增图表类型,新增图表的 api 和原有图表的api 区分开。
|
||||
const huiNewChart = ['chart-process']
|
||||
const fn = {
|
||||
// 菜单搜索:忽略大小写
|
||||
|
|
@ -249,11 +245,9 @@ export default {
|
|||
await _switchDemo()
|
||||
}
|
||||
async function _switchDemo() {
|
||||
if (!state.currDemo) {
|
||||
return
|
||||
}
|
||||
modeState.demoId = state.currDemo.demoId
|
||||
const path = `../../sites/demos/pc/app/${getPath(modeState.pathName)}/${state.currDemo?.codeFiles[0]}`
|
||||
|
||||
// 查找源码 查找组件
|
||||
state.currDemoSrc = await demoStr[path]()
|
||||
const comp = await demoVue[path]()
|
||||
|
|
@ -265,8 +259,8 @@ export default {
|
|||
}
|
||||
|
||||
const designConfigMap = {
|
||||
'tiny-smb-theme': {},
|
||||
'tiny-aurora-theme': {}
|
||||
'tiny-smb-theme': designSmbConfig,
|
||||
'tiny-aurora-theme': designAuroraConfig
|
||||
}
|
||||
|
||||
const lastThemeKey = localStorage.getItem('tinyThemeToolkey')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
// eager = true,可以校验vue,json等文件错误。
|
||||
// eager = false, 可以懒加载,优化速度
|
||||
|
||||
// demo源码
|
||||
// 同web-doc的菜单资源
|
||||
import { cmpMenus } from '../../sites/demos/mobile/menus.js'
|
||||
|
||||
export const demoStr = import.meta.glob('../../sites/demos/mobile/app/**/*.vue', { eager: false, as: 'raw' })
|
||||
export const demoVue = import.meta.glob('../../sites/demos/mobile/app/**/*.vue', { eager: false })
|
||||
|
||||
// demos配置
|
||||
export const demos = import.meta.glob('../../sites/demos/mobile/app/*/webdoc/*.js', { eager: false })
|
||||
|
||||
// 组件的md
|
||||
const allMD = import.meta.glob('../../sites/demos/mobile/app/*/webdoc/*.cn.md', { eager: true })
|
||||
export const mds = {}
|
||||
for (const path in allMD) {
|
||||
let key = path.split('/').slice(-1)[0]
|
||||
mds[key] = allMD[path].default
|
||||
}
|
||||
|
||||
const menuData = cmpMenus.slice(0)
|
||||
function processMenu(menu, isTop) {
|
||||
menu.id = menu.key
|
||||
menu.label = isTop ? menu.label : `${menu.nameCn} ${menu.name}`
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
menu.children.forEach((m) => processMenu(m, false))
|
||||
}
|
||||
}
|
||||
menuData.forEach((m) => processMenu(m, true))
|
||||
|
||||
export { menuData }
|
||||
|
|
@ -1,10 +1,6 @@
|
|||
import { cmpMenus } from '../../sites/demos/mobile-first/menus.js'
|
||||
|
||||
export const demoStr = import.meta.glob('../../sites/demos/mobile-first/app/**/*.vue', {
|
||||
eager: false,
|
||||
query: '?raw',
|
||||
import: 'default'
|
||||
})
|
||||
export const demoStr = import.meta.glob('../../sites/demos/mobile-first/app/**/*.vue', { eager: false, as: 'raw' })
|
||||
export const demoVue = import.meta.glob('../../sites/demos/mobile-first/app/**/*.vue', { eager: false })
|
||||
|
||||
// demos配置
|
||||
|
|
|
|||
|
|
@ -5,11 +5,7 @@
|
|||
// 同web-doc的菜单资源
|
||||
import { cmpMenus } from '../../sites/demos/pc/menus.js'
|
||||
|
||||
export const demoStr = import.meta.glob('../../sites/demos/pc/app/**/*.vue', {
|
||||
eager: false,
|
||||
query: '?raw',
|
||||
import: 'default'
|
||||
})
|
||||
export const demoStr = import.meta.glob('../../sites/demos/pc/app/**/*.vue', { eager: false, as: 'raw' })
|
||||
export const demoVue = import.meta.glob('../../sites/demos/pc/app/**/*.vue', { eager: false })
|
||||
|
||||
// demos配置
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import TinyThemeTool from '@opentiny/vue-theme/theme-tool'
|
||||
import { tinyAuroraTheme, tinySmbTheme, tinyInfinityTheme } from '@opentiny/vue-theme/theme'
|
||||
import { hooks } from '@opentiny/vue-common'
|
||||
import { Notify } from '@opentiny/vue'
|
||||
|
||||
|
|
@ -20,9 +21,9 @@ export function useTheme({ readCacheImmediate = true } = {}) {
|
|||
const lastThemeKey = localStorage.getItem('tinyThemeToolkey')
|
||||
|
||||
const THEME_MAP = {
|
||||
'tiny-aurora-theme': null,
|
||||
'tiny-smb-theme': null,
|
||||
'tiny-infinity-theme': null
|
||||
'tiny-aurora-theme': tinyAuroraTheme,
|
||||
'tiny-smb-theme': tinySmbTheme,
|
||||
'tiny-infinity-theme': tinyInfinityTheme
|
||||
}
|
||||
|
||||
const changeTheme = ({ vm }) => {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,12 @@
|
|||
export const apis = import.meta.glob('../../../sites/demos/apis/*.js', { eager: false })
|
||||
|
||||
export const getPath = (path) => {
|
||||
if (path === 'grid-select') {
|
||||
return 'grid-select'
|
||||
} else if (path.startsWith('grid-')) {
|
||||
if (path.startsWith('grid-')) {
|
||||
return 'grid'
|
||||
} else if (path.startsWith('chart-')) {
|
||||
return 'chart'
|
||||
} else {
|
||||
return path
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
export const getApisConfig = async (component, mode) => {
|
||||
|
|
|
|||
|
|
@ -3,22 +3,24 @@
|
|||
"type": "module",
|
||||
"version": "1.0.8",
|
||||
"description": "",
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"keywords": [],
|
||||
"files": [
|
||||
"resources"
|
||||
],
|
||||
"dependencies": {
|
||||
"@opentiny/vue": "workspace:~",
|
||||
"@opentiny/vue-common": "workspace:~",
|
||||
"@opentiny/vue-design-aurora": "workspace:~",
|
||||
"@opentiny/vue-design-saas": "workspace:~",
|
||||
"@opentiny/vue-icon": "workspace:~",
|
||||
"@opentiny/vue-theme-mobile": "workspace:~",
|
||||
"@opentiny/vue-theme-saas": "workspace:~",
|
||||
"@opentiny/vue-design-aurora": "workspace:~",
|
||||
"@opentiny/vue-design-smb": "workspace:~",
|
||||
"@opentiny/vue-design-saas": "workspace:~",
|
||||
"sortablejs": "1.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "catalog:"
|
||||
}
|
||||
}
|
||||
"@playwright/test": "~1.42.0"
|
||||
},
|
||||
"files": [
|
||||
"resources"
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Opentiny openinula 组件调试</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"name": "@opentiny/openinula-docs",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@opentiny/openinula": "workspace:~",
|
||||
"openinula": "^0.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.14",
|
||||
"@types/react-dom": "^18.2.6",
|
||||
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
||||
"@typescript-eslint/parser": "^6.12.0",
|
||||
"@vitejs/plugin-react": "^4.0.1",
|
||||
"eslint": "^8.44.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"postcss": "^8.4.16",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.3.8",
|
||||
"vite-plugin-svgr": "^3.2.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -0,0 +1,12 @@
|
|||
import { Alert } from '@opentiny/openinula'
|
||||
|
||||
// 在这里导入组件,进行 api 调试
|
||||
function App() {
|
||||
return (
|
||||
<div className="app">
|
||||
<Alert description="默认提示组件" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
.app {
|
||||
margin: 10px;
|
||||
width: 500px;
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import Inula from 'openinula'
|
||||
import App from './App.tsx'
|
||||
import './main.css'
|
||||
|
||||
Inula.render(<App />, document.getElementById('root'))
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import svgr from 'vite-plugin-svgr'
|
||||
|
||||
const alias = {
|
||||
react: 'openinula',
|
||||
'react-dom': 'openinula',
|
||||
'react/jsx-dev-runtime': 'openinula/jsx-dev-runtime'
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [svgr(), react({ include: /\.(mdx|js|jsx|ts|tsx)$/ })],
|
||||
resolve: {
|
||||
alias
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Opentiny React 组件调试</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"name": "@opentiny/react-docs",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@opentiny/react": "workspace:~",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.14",
|
||||
"@types/react-dom": "^18.2.6",
|
||||
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
||||
"@typescript-eslint/parser": "^6.12.0",
|
||||
"@vitejs/plugin-react": "^4.0.1",
|
||||
"autoprefixer": "^10.4.12",
|
||||
"eslint": "^8.44.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"postcss": "^8.4.16",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.3.8",
|
||||
"vite-plugin-react": "^4.0.1",
|
||||
"vite-plugin-svgr": "^3.2.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -0,0 +1,18 @@
|
|||
import { Button, Alert, Switch, Badge } from '@opentiny/react'
|
||||
|
||||
// 在这里导入组件,进行 api 调试
|
||||
function App() {
|
||||
|
||||
return (
|
||||
<div
|
||||
className='app'
|
||||
>
|
||||
<Button>点击按钮</Button>
|
||||
<Alert description='默认提示组件'/>
|
||||
<Switch/>
|
||||
<Badge value={10}>待办</Badge>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.app {
|
||||
margin: 10px;
|
||||
width: 500px;
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
// import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
import './main.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<App />
|
||||
)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
'../../packages/react/src/**/*.{css,less,vue,js,jsx,ts,tsx}',
|
||||
'!../../packages/react/src/**/node_modules',
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import svgr from "vite-plugin-svgr";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react({ include: /\.(mdx|js|jsx|ts|tsx)$/ }), svgr()
|
||||
],
|
||||
})
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
module.exports = {
|
||||
printWidth: 160, // 一行120字符数,如果超过会进行换行
|
||||
tabWidth: 2, // tab等2个空格
|
||||
useTabs: false, // 用空格缩进行
|
||||
semi: true, // 行尾使用分号
|
||||
singleQuote: true, // 字符串使用单引号
|
||||
quoteProps: 'as-needed', // 仅在需要时在对象属性添加引号
|
||||
jsxSingleQuote: false, // 在JSX中使用双引号
|
||||
trailingComma: 'es5', // 使用尾逗号(对象、数组等)
|
||||
bracketSpacing: true, // 对象的括号间增加空格
|
||||
jsxBracketSameLine: false, // 将多行JSX元素的>放在最后一行的末尾
|
||||
arrowParens: 'avoid', // 在唯一的arrow函数参数周围省略括号
|
||||
vueIndentScriptAndStyle: false, // 不缩进Vue文件中的<script>和<style>标记内的代码
|
||||
endOfLine: 'lf', // 仅限换行(\n)
|
||||
};
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
*.js
|
||||
*.ts
|
||||
*.png
|
||||
*.eot
|
||||
*.ttf
|
||||
*.woff
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
module.exports = {
|
||||
extends: "stylelint-config-standard",//stylelint-config-airbnb
|
||||
rules: {
|
||||
"string-quotes": "single",
|
||||
"property-no-unknown": true,
|
||||
"selector-pseudo-class-no-unknown": true,
|
||||
"at-rule-empty-line-before": "always",
|
||||
"block-no-empty": true,
|
||||
"indentation": 4 // http://cui.ulanqab.huawei.com/#/articalDetail?id=b76da810d8ed8
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
# react 组件文档开发说明
|
||||
|
||||
## 命令
|
||||
|
||||
### 启动
|
||||
|
||||
```bash
|
||||
pnpm start
|
||||
```
|
||||
|
||||
### 打包
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
### 编译 react.jsx 为 react.js
|
||||
|
||||
```bash
|
||||
pnpm build:react2
|
||||
```
|
||||
|
||||
此 vue 项目,vue自身需要 jsx,所以使用 react 的 jsx,只能将其编译为 .js 才能使用
|
||||
|
||||
## 开发说明
|
||||
|
||||
### 目录说明
|
||||
|
||||
主要是 demos 底下的文件是用来配置文档展示那些组件,以及写组件示例的
|
||||
|
||||
```b
|
||||
demos
|
||||
/app
|
||||
/alert
|
||||
/webdoc
|
||||
/alert.json 配置 alert 组件的文档展示
|
||||
/base.vue base 示例文件,导入 base.jsx,注册 web comp
|
||||
/base.jsx base 写 react 示例
|
||||
/webdoc
|
||||
/introduce.md 组件库的介绍文档
|
||||
/menu.js 组件库的左侧菜单配置
|
||||
/config.js demo 配置
|
||||
/overviewimage 组件总览的图标,在这里添加图标
|
||||
```
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
// generated by unplugin-vue-components
|
||||
// We suggest you to commit this file into source control
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
import '@vue/runtime-core'
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
export interface GlobalComponents {
|
||||
NAnchor: typeof import('naive-ui')['NAnchor']
|
||||
NAnchorLink: typeof import('naive-ui')['NAnchorLink']
|
||||
NCard: typeof import('naive-ui')['NCard']
|
||||
NCode: typeof import('naive-ui')['NCode']
|
||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||
NDataTable: typeof import('naive-ui')['NDataTable']
|
||||
NInput: typeof import('naive-ui')['NInput']
|
||||
NLayout: typeof import('naive-ui')['NLayout']
|
||||
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
|
||||
NMenu: typeof import('naive-ui')['NMenu']
|
||||
NSpace: typeof import('naive-ui')['NSpace']
|
||||
NSpin: typeof import('naive-ui')['NSpin']
|
||||
NTabPane: typeof import('naive-ui')['NTabPane']
|
||||
NTabs: typeof import('naive-ui')['NTabs']
|
||||
NTooltip: typeof import('naive-ui')['NTooltip']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { Alert as TinyAlert } from '@pe-3/react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
function App(props) {
|
||||
return (<div>
|
||||
{props.children}
|
||||
</div>)
|
||||
}
|
||||
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback() {
|
||||
ReactDOM.createRoot(this).render(
|
||||
<App>
|
||||
<TinyAlert description="type 为默认值 success"></TinyAlert>
|
||||
<TinyAlert type="error" description="type 为 error"></TinyAlert>
|
||||
<TinyAlert type="info" description="type 为 info"></TinyAlert>
|
||||
<TinyAlert type="warning" description="type 为 warning"></TinyAlert>
|
||||
</App>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<template>
|
||||
<div>
|
||||
<alert-base-demo></alert-base-demo>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AlertBase from '@/webcomps/alert/base.js'
|
||||
customElements.define(
|
||||
'alert-base-demo',
|
||||
AlertBase
|
||||
)
|
||||
export default {
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import { Alert as TinyAlert } from '@pe-3/react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
function App(props) {
|
||||
return (<div>
|
||||
{props.children}
|
||||
</div>)
|
||||
}
|
||||
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback() {
|
||||
ReactDOM.createRoot(this).render(
|
||||
<App>
|
||||
<TinyAlert center description="文字居中"></TinyAlert>
|
||||
</App>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
<alert-center-demo></alert-center-demo>
|
||||
</template>
|
||||
|
||||
<script lang="jsx">
|
||||
import AlertCenter from "@/webcomps/alert/center.js";
|
||||
customElements.define("alert-center-demo", AlertCenter);
|
||||
export default {};
|
||||
</script>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { Alert as TinyAlert } from '@pe-3/react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
function App(props) {
|
||||
return (<div>
|
||||
{props.children}
|
||||
</div>)
|
||||
}
|
||||
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback() {
|
||||
ReactDOM.createRoot(this).render(
|
||||
<App>
|
||||
<TinyAlert size="normal" description="size 为 normal"></TinyAlert>
|
||||
<TinyAlert size="large" title="size 为 large"></TinyAlert>
|
||||
</App>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<template>
|
||||
<div>
|
||||
<alert-size-demo></alert-size-demo>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AlertSize from "@/webcomps/alert/size.js";
|
||||
customElements.define("alert-size-demo", AlertSize);
|
||||
export default {};
|
||||
</script>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { Alert as TinyAlert } from '@pe-3/react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
function App(props) {
|
||||
return (<div>
|
||||
{props.children}
|
||||
</div>)
|
||||
}
|
||||
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback() {
|
||||
ReactDOM.createRoot(this).render(
|
||||
<App>
|
||||
<TinyAlert size="large" title="通过属性设置自定义 title"></TinyAlert>
|
||||
<br/>
|
||||
<TinyAlert
|
||||
size="large"
|
||||
slots={{
|
||||
title: () => '通过 slot 设置自定义 title'
|
||||
}}
|
||||
>
|
||||
</TinyAlert>
|
||||
</App>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<template>
|
||||
<div>
|
||||
<alert-title-demo></alert-title-demo>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="jsx">
|
||||
import AlertTitle from "@/webcomps/alert/title.js";
|
||||
customElements.define("alert-title-demo", AlertTitle);
|
||||
export default {};
|
||||
</script>
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { Alert as TinyAlert } from '@pe-3/react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
function App(props) {
|
||||
return (<div>
|
||||
{props.children}
|
||||
</div>)
|
||||
}
|
||||
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback() {
|
||||
ReactDOM.createRoot(this).render(
|
||||
<App>
|
||||
<TinyAlert description="type 为默认值 success"></TinyAlert>
|
||||
<TinyAlert type="info" description="type 为 info"></TinyAlert>
|
||||
<TinyAlert type="error" description="type 为 error"></TinyAlert>
|
||||
<TinyAlert type="success" description="type 为 success"></TinyAlert>
|
||||
<TinyAlert type="warning" description="type 为 warning"></TinyAlert>
|
||||
</App>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<template>
|
||||
<div>
|
||||
<alert-type-demo></alert-type-demo>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="jsx">
|
||||
import AlertType from "@/webcomps/alert/type.js";
|
||||
customElements.define("alert-type-demo", AlertType);
|
||||
export default {};
|
||||
</script>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
title: Alert 警告
|
||||
---
|
||||
|
||||
# Alert 警告
|
||||
|
||||
<div>Alert 警告,提供 warning、error、info、success 四种类型显示不同类别的信息。</div>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
title: Alert
|
||||
---
|
||||
|
||||
# Alert
|
||||
|
||||
<div>Alert alarms, including warning, error, info, and success.</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue