Compare commits

..

No commits in common. "dev" and "dev" have entirely different histories.
dev ... dev

594 changed files with 3819 additions and 14179 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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`目录中发布即可。

View File

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

View File

@ -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#测试要求)

View File

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

View File

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

View File

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

View File

@ -38,10 +38,10 @@ tiny-vue/
## 环境要求
| 工具 | 版本要求 |
| ---- | -------------------------------------- |
| Node | `>= 18` |
| pnpm | `>= 9.5`(必须,禁止使用 npm 或 yarn |
| 工具 | 版本要求 |
|------|------------------------------------------|
| Node | `>= 18` |
| pnpm | `>= 9.5`(必须,禁止使用 npm 或 yarn |
## 核心命令
@ -131,18 +131,18 @@ TinyVue 使用 **Renderless 无渲染架构**。修改或新增组件时,必
**允许的 type**
| type | 用途 |
| ---------- | ---------------------- |
| `feat` | 新功能 |
| `fix` | 缺陷修复 |
| `docs` | 文档变更 |
| `style` | 代码格式(不影响逻辑) |
| `refactor` | 重构(无新功能/修复) |
| `perf` | 性能优化 |
| `test` | 测试用例 |
| `chore` | 构建/工具链变更 |
| `ci` | CI/CD 配置 |
| `revert` | 回滚提交 |
| type | 用途 |
|-------------|--------------------------|
| `feat` | 新功能 |
| `fix` | 缺陷修复 |
| `docs` | 文档变更 |
| `style` | 代码格式(不影响逻辑) |
| `refactor` | 重构(无新功能/修复) |
| `perf` | 性能优化 |
| `test` | 测试用例 |
| `chore` | 构建/工具链变更 |
| `ci` | CI/CD 配置 |
| `revert` | 回滚提交 |
**scope 规范:**
@ -175,7 +175,7 @@ test(button): 新增 E2E 测试用例
- 修复 Bug 时:必须同步补充能复现该 Bug 的测试用例
- 新增 Feature 时:需补充对应的单元测试,优先考虑同步 E2E 测试
- 单元测试位置:`examples/vue3/src/**/__tests__/`
- E2E 测试位置:`examples/sites/demos/{pc,mobile-first}/app/<component-name>/`
- E2E 测试位置:`examples/sites/demos/<component-name>/`
- 测试框架Vitest单元+ PlaywrightE2E
## E2E 测试触发

View File

@ -119,7 +119,7 @@ export default {
'zh-CN': '是否显示标题,在 size 为 large 时有效',
'en-US': 'Whether to show title Only valid when size is large'
},
mode: ['pc'],
mode: ['pc', 'mobile-first'],
pcDemo: 'title',
meta: {
stable: '3.21.0'
@ -138,8 +138,7 @@ export default {
},
{
name: 'size',
typeAnchorName: 'ISize',
type: 'ISize',
type: "'normal' | 'large'",
defaultValue: "'normal'",
desc: {
'zh-CN': '警告的尺寸大小',
@ -147,7 +146,7 @@ export default {
},
mode: ['pc', 'mobile-first'],
pcDemo: 'size',
mfDemo: 'size'
mfDemo: ''
},
{
name: 'title',
@ -280,13 +279,6 @@ export default {
type: 'type',
code: `
type IType = 'success' | 'warning' | 'info' | 'error' | 'simple'
`
},
{
name: 'ISize',
type: 'type',
code: `
type ISize = 'small' | 'medium' | 'normal' | 'large'
`
}
]

View File

@ -51,21 +51,6 @@ export default {
},
mode: ['mobile-first'],
mfDemo: ''
},
{
name: 'size',
type: 'String',
defaultValue: '',
desc: {
'zh-CN': '控制折叠面板的尺寸,可选值为 "" | "medium"',
'en-US': 'adjust the size of the fold panel. Available options: "" | "medium"'
},
mode: ['pc', 'mobile-first'],
mfDemo: '',
pcDemo: 'size.vue',
meta: {
stable: '3.31.0'
}
}
],
events: [

View File

@ -10,11 +10,11 @@ export default {
type: 'boolean',
defaultValue: 'false',
desc: {
'zh-CN': 'drawer 本身是否插入到 body',
'zh-CN': 'drawer 本身是否插入到 body',
'en-US': 'Whether the drawer itself is inserted into the body'
},
mode: ['pc', 'mobile-first'],
pcDemo: 'drawer-to-body',
pcDemo: '',
mfDemo: '',
meta: {
stable: '3.30.0'
@ -42,21 +42,6 @@ export default {
mode: ['mobile-first'],
mfDemo: ''
},
{
name: 'destroy-on-close',
type: 'boolean',
defaultValue: 'false',
desc: {
'zh-CN': '关闭时销毁抽屉内的元素,而非隐藏',
'en-US': 'Destroy elements inside the drawer when closing, instead of hiding them'
},
mode: ['pc'],
pcDemo: 'destroy-on-close',
mfDemo: '',
meta: {
stable: '3.31.0'
}
},
{
name: 'dragable',
type: 'boolean',

View File

@ -10,10 +10,8 @@ export default {
type: 'string',
defaultValue: '',
desc: {
'zh-CN':
'限制文件类型thumbnail-mode 模式下此参数无效),支持<code> 后缀名(.pdfMIME类型image/pngMIME类型通配符image/*</code>, 多个格式以逗号分隔',
'en-US':
'Restrict the types of files. This parameter is invalid in thumbnail-mode mode, support <code>suffix (.pdf), MIME type (image/png), MIME type wildcard (image/*)</code>, separated by commas'
'zh-CN': '限制文件类型thumbnail-mode 模式下此参数无效)',
'en-US': 'Restrict the types of files. This parameter is invalid in thumbnail-mode mode'
},
mode: ['pc', 'mobile-first'],
pcDemo: 'accept-file-image',

File diff suppressed because it is too large Load Diff

View File

@ -32,7 +32,7 @@ export default {
'en-US': ''
},
mode: ['pc', 'mobile-first'],
pcDemo: 'before-link-open'
pcDemo: ''
},
{
name: 'data-type',

View File

@ -2954,7 +2954,7 @@ export default {
},
{
name: 'type',
type: "'index' | 'selection' | 'radio' | 'expand' | 'operation'",
type: "'index' | 'selection' | 'radio' | 'expand'",
defaultValue: '',
desc: {
'zh-CN': '设置内置列的类型',
@ -2963,20 +2963,6 @@ export default {
mode: ['pc', 'mobile-first'],
pcDemo: 'grid-serial-column#serial-column-default-serial-column'
},
{
name: 'operation-config',
typeAnchorName: 'IOperationConfig',
type: 'IOperationConfig',
defaultValue: '',
desc: {
'zh-CN': '当 <code>type="operation"</code> 时有效,通过 <code>operationConfig</code> 配置操作列',
'en-US':
'Effective when <code>type="operation"</code> is specified. Configures the operation column through <code>operationConfig</code>'
},
mode: ['pc', 'mobile-first'],
pcDemo: 'grid-operation-column#operation-column',
mfDemo: 'operation-column'
},
{
name: 'width',
type: 'number | string',
@ -2988,7 +2974,7 @@ export default {
'Set the column width. The value can be pixel, percentage, or auto. If the value is auto, the column width automatically adapts.; column width; The optional value of this property is integer/px/%'
},
mode: ['pc', 'mobile-first'],
pcDemo: 'operation-column'
pcDemo: 'grid-size#size-fixed-column-width'
}
],
events: [],
@ -4226,8 +4212,6 @@ interface IFilterConfig {
defaultFilter?: boolean
// 设置在过滤面板中显示输入筛选true 使用默认 input或传入 IInputFilterConfig 配置
inputFilter?: boolean | IInputFilterConfig
// 重置输入时的回调
onResetInputFilter?: (ref: any) => void
// 设置枚举选项的静态数据源,也可为函数 (params) => Promise<Array<{label,value,checked?}>>
values?: Array<{ [key: string]: any }> | (params: { property: string; filter: IFilterConfig }) => Promise<Array<{ [key: string]: any }>>
// 设置枚举数据的显示值属性字段,默认 'label'
@ -4261,6 +4245,8 @@ interface IInputFilterConfig {
relations?: IRelationFilterItem[]
// 默认选中的 relation 值
relation?: string
// 重置输入时的回调
onResetInputFilter?: (ref: any) => void
}
// 关系选项项
@ -4376,21 +4362,6 @@ interface ICustomConfig {
visible?: boolean
// 列宽
width?: number | string
}`
},
{
name: 'IOperationConfig',
type: 'type',
code: `
interface IOperationConfig {
// 操作列的按钮配置
buttons: Array<{name:string, icon:Icon, click:()=>void, hidden:(row)=> boolean, class?:string, disabled?:boolean| (row)=> boolean}>
// 最多显示按钮数默认值为3
max?: number
// 自定义操作列渲染函数, 优先级高
render?: ({h, buttons, params}) => VNode
// 禁用时需要添加的class
disabledClass?:string
}`
}
]

View File

@ -10,16 +10,12 @@ export default {
type: 'Function',
defaultValue: '',
desc: {
'zh-CN': '设置关闭前的回调函数(仅点击关闭按钮或遮罩区域时被调用),如果回调函数返回 false 则阻止窗口关闭',
'zh-CN': '可以配置一个拦截弹窗关闭的方法。如果方法返回 false 值,则拦截弹窗关闭;否则不拦截',
'en-US':
'Set the callback function before closing (only called when clicking the close button or mask area). If the callback function returns false, the modal will not be closed'
'Configure a method to intercept modal closing. If the method returns false, the modal closing is intercepted; otherwise it is not intercepted'
},
mode: ['pc', 'mobile-first'],
mfDemo: '',
pcDemo: 'before-close',
meta: {
stable: '3.31.0'
}
mode: ['mobile-first'],
mfDemo: ''
},
{
name: 'cancel-btn-props',

View File

@ -409,22 +409,6 @@ export default {
mode: ['pc', 'mobile-first'],
pcDemo: 'string-mode',
mfDemo: ''
},
{
name: 'parse-input',
type: '(value:string)=>string',
defaultValue: '',
desc: {
'zh-CN': '自定义输入值解析函数,优先级较高。当输入字符以及粘贴时,会调用该函数进行解析。',
'en-US':
'custom input value parsing function with high priority.This function is called for parsing when input characters are entered or pasted'
},
mode: ['pc', 'mobile-first'],
pcDemo: 'parse-input',
mfDemo: '',
meta: {
stable: '3.31.0'
}
}
],
events: [

View File

@ -1,5 +1,5 @@
export default {
mode: ['pc', 'mobile-first'],
mode: ['mobile-first'],
apis: [
{
name: 'slider-button',
@ -13,7 +13,7 @@ export default {
'zh-CN': '设置滑块项禁用态',
'en-US': ''
},
mode: ['pc', 'mobile-first'],
mode: ['mobile-first'],
mfDemo: ''
},
{
@ -27,6 +27,17 @@ export default {
mode: ['mobile-first'],
mfDemo: ''
},
{
name: 'disabled',
type: 'Boolean',
defaultValue: '',
desc: {
'zh-CN': '设置滑块项禁用态',
'en-US': ''
},
mode: ['mobile-first'],
mfDemo: ''
},
{
name: 'label',
type: 'Number / String',
@ -35,7 +46,7 @@ export default {
'zh-CN': '设置 Button 的内容',
'en-US': ''
},
mode: ['pc', 'mobile-first'],
mode: ['mobile-first'],
mfDemo: ''
},
{
@ -46,7 +57,7 @@ export default {
'zh-CN': '设置 Button 的内容',
'en-US': ''
},
mode: ['pc', 'mobile-first'],
mode: ['mobile-first'],
mfDemo: ''
}
],
@ -61,7 +72,7 @@ export default {
'zh-CN': '组件默认插槽',
'en-US': ''
},
mode: ['pc', 'mobile-first'],
mode: ['mobile-first'],
mfDemo: ''
}
]

View File

@ -147,21 +147,6 @@ export default {
},
mode: ['mobile-first'],
mfDemo: ''
},
{
name: 'display-only',
type: 'boolean',
defaultValue: 'false',
desc: {
'zh-CN': '设置开关为只读状态',
'en-US': 'Set the switch to read-only status'
},
meta: {
stable: '3.31.0'
},
mode: ['pc', 'mobile-first'],
pcDemo: 'display-only',
mfDemo: 'display-only'
}
],
events: [

View File

@ -16,21 +16,6 @@ export default {
mode: ['pc'],
pcDemo: 'delete'
},
{
name: 'round',
type: 'boolean',
defaultValue: '',
desc: {
'zh-CN': '是否设置圆角标签',
'en-US': 'Whether to set the rounded corner label'
},
mode: ['pc', 'mobile-first'],
meta: {
stable: '3.31.0'
},
pcDemo: 'rounded',
mfDemo: 'rounded'
},
{
name: 'closable',
type: 'boolean',
@ -261,7 +246,7 @@ type ISize = 'medium' | 'small' | ''
name: 'IType',
type: 'type',
code: `
type IType = 'success' | 'info' | 'warning' | 'danger' | 'alerting' | 'error'
type IType = 'success' | 'info' | 'warning' | 'danger'
`
}
]

View File

@ -1,11 +1,7 @@
<template>
<div>
<tiny-alert size="small" title="size 为 small" description="size 为 small"></tiny-alert>
<tiny-alert size="medium" title="size 为 medium" description="size 为 medium"></tiny-alert>
<tiny-alert size="normal" description="size 为 normal"></tiny-alert>
<tiny-alert size="large" title="size 为 large">
<!-- <span>自定义内容</span> -->
</tiny-alert>
<tiny-alert size="large" title="size 为 large"></tiny-alert>
</div>
</template>

View File

@ -18,14 +18,13 @@ export default {
{
demoId: 'size',
name: {
'zh-CN': '尺寸',
'en-US': 'Size'
'zh-CN': '尺寸',
'en-US': 'Large size'
},
desc: {
'zh-CN':
'<p>通过 <code>size</code> 设置不同的尺寸模式,可选值: <code>small</code> 、<code>medium</code> 、<code>normal</code> 、<code>large</code>。</p>',
'zh-CN': '<p>通过 <code>size</code> 属性设置不同的尺寸可选值nomal、large默认值nomal 。</p>',
'en-US':
'<p>Set different size modes through<code>size</code>, with optional values:<code>small</code> 、<code>medium</code> 、<code>normal</code> 、<code>large</code>.</p>'
'<p>Use the <code>size</code> attribute to set different sizes. The options are nomal and large. The default value is nomal.</p>'
},
codeFiles: ['size.vue']
},

View File

@ -41,7 +41,7 @@
<div>场景 5自定义图标 + 自定义样式</div>
<br />
<tiny-base-select
v-model="value5"
v-model="value4"
multiple
:dropdown-icon="iconPopup"
:drop-style="{ width: '200px', 'min-width': '200px' }"
@ -85,7 +85,6 @@ export default {
value2: ['选项 1', '选项 2'],
value3: ['选项 1', '选项 2'],
value4: [],
value5: [],
iconPopup: iconPopup()
}
}

View File

@ -1,39 +0,0 @@
<template>
<div>
<tiny-button @click="openDrawer" type="primary"> 抽屉组件AppendToBody </tiny-button>
<tiny-drawer
v-if="visible"
title="标题"
:visible="visible"
:append-to-body="true"
@update:visible="visible = $event"
@confirm="confirm"
>
<div>内容区域</div>
</tiny-drawer>
</div>
</template>
<script>
import { TinyDrawer, TinyButton } from '@opentiny/vue'
export default {
components: {
TinyDrawer,
TinyButton
},
data() {
return {
visible: false
}
},
methods: {
openDrawer() {
this.visible = true
},
confirm() {
this.visible = false
}
}
}
</script>

View File

@ -28,18 +28,6 @@ export default {
},
codeFiles: ['placement.vue']
},
{
demoId: 'to-body',
name: {
'zh-CN': '渲染在 body 中',
'en-US': 'Rendering in body'
},
desc: {
'zh-CN': '<p>添加 <code>append-to-body</code> 属性设置是否插入至<code>body</code></p>',
'en-US': 'Add<code>append to body</code>attribute to set whether to insert into<code>body</code>'
},
codeFiles: ['to-body.vue']
},
{
demoId: 'width',
name: {

View File

@ -1,109 +1,121 @@
<template>
<div>
<tiny-async-flowchart
ref="chart"
:fetch="fetchFunc"
@click-node="onClickNode"
@click-link="onClickLink"
@click-blank="onClickBlank"
/>
</div>
<tiny-async-flowchart
ref="chart"
:fetch="fetchFunc"
@click-node="onClickNode"
@click-link="onClickLink"
@click-blank="onClickBlank"
>
</tiny-async-flowchart>
</template>
<script>
import { TinyFlowchart, TinyAsyncFlowchart, TinyModal } from '@opentiny/vue'
import { hooks } from '@opentiny/vue-common'
const { createConfig, Node } = TinyFlowchart
const nodeWrapperSize = 32
export default {
components: { TinyAsyncFlowchart },
data() {
const chartConfig = createConfig()
Object.assign(chartConfig, {
width: 0,
extraWidth: 100,
height: 90,
gap: 60,
padding: 12,
prior: 'vertical',
align: 'left',
status: { 1: 'completed', 2: 'ongoing', 3: 'not-started', 4: 'fail' },
colors: { 1: '#00a874', 2: '#0067d1', 3: '#999', 4: '#eb171f' },
ongoingBackgroundColor: '#f3f8fe',
popoverPlacement: 'bottom',
renderOuter(h, node) {
return h(Node, { props: { node, config: chartConfig } })
const chartData = {
nodes: [
{
name: '0',
info: {
col: 0,
row: 0,
status: 1,
other: { title: '开始', subtitle: '张三', auxi: '2023-01-01' }
},
type: 'dot',
nodeWrapperSize,
showArrow: false,
nodeSize: 'medium'
})
const chartData = {
nodes: [
{
name: '0',
info: { col: 0, row: 0, status: 1, other: { title: '开始', subtitle: '张三', auxi: '2023-01-01' } },
hidden: false
},
{
name: '1',
info: { col: 1, row: 0, status: 1, other: { title: '申请人', subtitle: '张三', auxi: '2023-01-02' } }
},
{
name: '2',
info: {
col: 2,
row: 0,
status: 1,
other: { title: '制单会计', subtitle: '协同:张三、张四', auxi: '2023-01-03' }
}
},
{
name: '3',
info: { col: 3, row: 0, status: 2, other: { title: '应付会计', subtitle: '张四 0035837', auxi: '' } }
},
{
name: '4',
info: {
col: 4,
row: 0,
status: 4,
other: { title: '应付会计', subtitle: '张四 0035837', auxi: '', error: '人员变更,未同步' }
}
}
],
links: [
{ from: '0', to: '1', fromJoint: 'right', toJoint: 'left', info: { status: 2, style: 'solid' } },
{ from: '1', to: '2', fromJoint: 'right', toJoint: 'left', info: { status: 2, style: 'solid' } },
{ from: '2', to: '3', fromJoint: 'right', toJoint: 'left', info: { status: 3, style: 'solid' } },
{ from: '3', to: '4', fromJoint: 'right', toJoint: 'left', info: { status: 3, style: 'solid' } }
]
hidden: false
},
{
name: '1',
info: {
col: 1,
row: 0,
status: 1,
other: { title: '申请人', subtitle: '张三', auxi: '2023-01-02' }
}
},
{
name: '2',
info: {
col: 2,
row: 0,
status: 1,
other: { title: '制单会计', subtitle: '协同:张三、张四、张五、张六、张七', auxi: '2023-01-03' }
}
},
{
name: '3',
info: {
col: 3,
row: 0,
status: 2,
other: { title: '应付会计', subtitle: '张四 0035837', auxi: '' }
}
},
{
name: '4',
info: {
col: 4,
row: 0,
status: 4,
other: { title: '应付会计', subtitle: '张四 0035837', auxi: '', error: '人员变更,未同步' }
}
}
],
links: [
{ from: '0', to: '1', fromJoint: 'right', toJoint: 'left', info: { status: 2, style: 'solid' } },
{ from: '1', to: '2', fromJoint: 'right', toJoint: 'left', info: { status: 2, style: 'solid' } },
{ from: '2', to: '3', fromJoint: 'right', toJoint: 'left', info: { status: 3, style: 'solid' } },
{ from: '3', to: '4', fromJoint: 'right', toJoint: 'left', info: { status: 3, style: 'solid' } }
]
}
return {
chartData: hooks.markRaw(chartData),
chartConfig: hooks.markRaw(chartConfig)
}
const chartConfig = createConfig()
Object.assign(chartConfig, {
width: 0,
extraWidth: 100, //
height: 90,
gap: 60,
padding: 12,
prior: 'vertical',
align: 'left',
status: { 1: 'completed', 2: 'ongoing', 3: 'not-started', 4: 'fail' },
colors: { 1: '#00a874', 2: '#0067d1', 3: '#999', 4: '#eb171f' },
ongoingBackgroundColor: '#f3f8fe',
popoverPlacement: 'bottom',
renderOuter: (h, node) => {
return h(Node, { props: { node, config: chartConfig } })
},
type: 'dot',
nodeWrapperSize,
showArrow: false,
nodeSize: 'medium' /* mini/small/medium */
})
export default {
components: {
TinyAsyncFlowchart
},
methods: {
onClickNode(afterNode, e) {
// console.log(afterNode, e)
TinyModal.message('click-node')
},
onClickLink(afterLink, e) {
// console.log(afterLink, e)
TinyModal.message('click-link')
},
onClickBlank(param, e) {
// console.log(param, e)
TinyModal.message('click-blank')
},
fetchFunc() {
const self = this
return new Promise(function (resolve) {
setTimeout(function () {
resolve({ data: self.chartData, config: self.chartConfig })
return new Promise((resolve) => {
setTimeout(() => {
resolve({ data: chartData, config: chartConfig })
}, 300)
})
}

View File

@ -1,14 +1,13 @@
<template>
<div>
<tiny-flowchart
ref="chart"
:data="chartData"
:config="chartConfig"
@click-node="onClickNode"
@click-link="onClickLink"
@click-blank="onClickBlank"
/>
</div>
<tiny-flowchart
ref="chart"
:data="chartData"
:config="chartConfig"
@click-node="onClickNode"
@click-link="onClickLink"
@click-blank="onClickBlank"
>
</tiny-flowchart>
</template>
<script>
@ -18,85 +17,104 @@ import { hooks } from '@opentiny/vue-common'
const { createConfig, Node, resizeMixin } = TinyFlowchart
const nodeWrapperSize = 32
const chartData = {
nodes: [
{
name: '0',
info: {
col: 0,
row: 0,
status: 1,
other: { title: '开始', subtitle: '张三', auxi: '2023-01-01' }
},
hidden: false
},
{
name: '1',
info: {
col: 1,
row: 0,
status: 1,
other: { title: '申请人', subtitle: '张三', auxi: '2023-01-02' }
}
},
{
name: '2',
info: {
col: 2,
row: 0,
status: 1,
other: { title: '制单会计', subtitle: '协同:张三、张四、张五、张六、张七', auxi: '2023-01-03' }
}
},
{
name: '3',
info: {
col: 3,
row: 0,
status: 2,
other: { title: '应付会计', subtitle: '张四 0035837', auxi: '' }
}
},
{
name: '4',
info: {
col: 4,
row: 0,
status: 4,
other: { title: '应付会计', subtitle: '张四 0035837', auxi: '', error: '人员变更,未同步' }
}
}
],
links: [
{ from: '0', to: '1', fromJoint: 'right', toJoint: 'left', info: { status: 2, style: 'solid' } },
{ from: '1', to: '2', fromJoint: 'right', toJoint: 'left', info: { status: 2, style: 'solid' } },
{ from: '2', to: '3', fromJoint: 'right', toJoint: 'left', info: { status: 3, style: 'solid' } },
{ from: '3', to: '4', fromJoint: 'right', toJoint: 'left', info: { status: 3, style: 'solid' } }
]
}
const chartConfig = createConfig()
Object.assign(chartConfig, {
width: 0,
extraWidth: 100, //
height: 90,
gap: 60,
padding: 12,
prior: 'vertical',
align: 'left',
status: { 1: 'completed', 2: 'ongoing', 3: 'not-started', 4: 'fail' },
colors: { 1: '#00a874', 2: '#0067d1', 3: '#999', 4: '#eb171f' },
ongoingBackgroundColor: '#f3f8fe',
popoverPlacement: 'bottom',
renderOuter: (h, node) => {
return h(Node, {
props: {
node,
config: chartConfig,
cursorPointerFn: (state) => {
//
if (state.statCompleted) {
return false
}
return true
}
}
})
},
type: 'dot',
nodeWrapperSize,
showArrow: false,
nodeSize: 'medium' /* mini/small/medium */
})
export default {
mixins: [resizeMixin({ refName: 'chart', nodeWrapperSize })],
components: { TinyFlowchart },
components: {
TinyFlowchart
},
data() {
const self = this
const chartConfig = createConfig()
Object.assign(chartConfig, {
width: 0,
extraWidth: 100,
height: 90,
gap: 60,
padding: 12,
prior: 'vertical',
align: 'left',
status: { 1: 'completed', 2: 'ongoing', 3: 'not-started', 4: 'fail' },
colors: { 1: '#00a874', 2: '#0067d1', 3: '#999', 4: '#eb171f' },
ongoingBackgroundColor: '#f3f8fe',
popoverPlacement: 'bottom',
renderOuter(h, node) {
return h(Node, {
props: {
node,
config: chartConfig,
cursorPointerFn(state) {
if (state.statCompleted) return false
return true
}
}
})
},
type: 'dot',
nodeWrapperSize,
showArrow: false,
nodeSize: 'medium'
})
const chartData = {
nodes: [
{
name: '0',
info: { col: 0, row: 0, status: 1, other: { title: '开始', subtitle: '张三', auxi: '2023-01-01' } },
hidden: false
},
{
name: '1',
info: { col: 1, row: 0, status: 1, other: { title: '申请人', subtitle: '张三', auxi: '2023-01-02' } }
},
{
name: '2',
info: {
col: 2,
row: 0,
status: 1,
other: { title: '制单会计', subtitle: '协同:张三、张四', auxi: '2023-01-03' }
}
},
{
name: '3',
info: { col: 3, row: 0, status: 2, other: { title: '应付会计', subtitle: '张四 0035837', auxi: '' } }
},
{
name: '4',
info: {
col: 4,
row: 0,
status: 4,
other: { title: '应付会计', subtitle: '张四 0035837', auxi: '', error: '人员变更,未同步' }
}
}
],
links: [
{ from: '0', to: '1', fromJoint: 'right', toJoint: 'left', info: { status: 2, style: 'solid' } },
{ from: '1', to: '2', fromJoint: 'right', toJoint: 'left', info: { status: 2, style: 'solid' } },
{ from: '2', to: '3', fromJoint: 'right', toJoint: 'left', info: { status: 3, style: 'solid' } },
{ from: '3', to: '4', fromJoint: 'right', toJoint: 'left', info: { status: 3, style: 'solid' } }
]
}
return {
chartData: hooks.markRaw(chartData),
chartConfig: hooks.markRaw(chartConfig)
@ -104,12 +122,15 @@ export default {
},
methods: {
onClickNode(afterNode, e) {
// console.log(afterNode, e)
TinyModal.message('click-node')
},
onClickLink(afterLink, e) {
// console.log(afterLink, e)
TinyModal.message('click-link')
},
onClickBlank(param, e) {
// console.log(param, e)
TinyModal.message('click-blank')
}
}

View File

@ -1,194 +1,220 @@
<template>
<div>
<tiny-async-flowchart
ref="chart"
:fetch="fetchFunc"
@click-node="onClickNode"
@click-link="onClickLink"
@click-blank="onClickBlank"
@click-group="onClickGroup"
/>
</div>
<tiny-async-flowchart
ref="chart"
:fetch="fetchFunc"
@click-node="onClickNode"
@click-link="onClickLink"
@click-blank="onClickBlank"
@click-group="onClickGroup"
>
</tiny-async-flowchart>
</template>
<script>
import { TinyFlowchart, TinyAsyncFlowchart, TinyModal } from '@opentiny/vue'
import { hooks } from '@opentiny/vue-common'
const { createConfig, Node } = TinyFlowchart
const nodeWrapperSize = 32
export default {
components: { TinyAsyncFlowchart },
data() {
const chartConfig = createConfig()
Object.assign(chartConfig, {
width: 0,
extraWidth: 200,
height: 0,
gap: 60,
padding: 12,
prior: 'vertical',
align: 'left',
status: { 1: 'completed', 2: 'ongoing', 3: 'not-started', 4: 'fail' },
colors: { 1: '#0067D1', 2: '#0067d1', 3: 'rgba(22,30,38,0.2)', 4: '#eb171f' },
ongoingBackgroundColor: '#f3f8fe',
popoverPlacement: 'right',
type: 'dot',
renderOuter(h, node) {
return h(Node, { props: { node, config: chartConfig, titleClass: 'bg-color-bg-1' } })
const chartData = {
nodes: [
{
name: '0',
info: {
col: 0,
row: 0,
status: 1,
other: { title: '开始', subtitle: '张三', auxi: '2023-01-01' }
},
nodeLayout: 'left-right',
nodeSize: 'medium',
nodeWrapperSize,
titleMaxWidth: 90,
linkEndMinus: 6,
showOnly: '',
linkPath: [
{
filter: { from: '4', to: '5' },
method({ afterLink, afterNodes }) {
const afterNodeCouple = [afterLink.raw.from, afterLink.raw.to].map(function (name) {
return afterNodes.find(function (afterNode) {
return afterNode.raw.name === name
})
})
const f = {
x: afterNodeCouple[0].x + afterNodeCouple[0].width / 2,
y: afterNodeCouple[0].y + afterNodeCouple[0].height
}
const t = {
x: afterNodeCouple[1].x + afterNodeCouple[1].width,
y: afterNodeCouple[1].y + afterNodeCouple[1].height / 2
}
const c = { x: f.x, y: t.y }
return {
path: [f, c, t],
mid: { x: (f.x + c.x) / 2, y: (f.y + c.y) / 2 },
linear: { stops: [0, 1], colors: ['gold', 'blue'] }
}
}
},
{
filter: { from: '2', to: '4' },
method({ afterLink, afterNodes }) {
const afterNodeCouple = [afterLink.raw.from, afterLink.raw.to].map(function (name) {
return afterNodes.find(function (afterNode) {
return afterNode.raw.name === name
})
})
const f = {
x: afterNodeCouple[0].x + afterNodeCouple[0].width,
y: afterNodeCouple[0].y + afterNodeCouple[0].height / 2
}
const t = { x: afterNodeCouple[1].x + afterNodeCouple[1].width / 2, y: afterNodeCouple[1].y }
const c = { x: t.x, y: f.y }
return [f, c, t]
}
}
],
condClass: 'bg-color-bg-1'
})
const chartData = {
nodes: [
{
name: '0',
info: { col: 0, row: 0, status: 1, other: { title: '开始', subtitle: '张三', auxi: '2023-01-01' } },
hidden: false
},
{
name: '1',
info: { col: 0, row: 1, status: 1, other: { title: '申请人', subtitle: '张三', auxi: '2023-01-02' } }
},
{
name: '2',
info: {
col: 0,
row: 2,
status: 1,
other: { title: '制单会计', subtitle: '协同:张三、张四', auxi: '2023-01-03' }
}
},
{
name: '3',
info: { col: 0, row: 3, status: 2, other: { title: '应付会计', subtitle: '张四 0035837', auxi: '' } }
},
{
name: '4',
info: {
col: 1,
row: 3,
status: 4,
other: { title: '应付会计', subtitle: '张四 0035837', auxi: '', error: '人员变更,未同步' }
}
},
{
name: '5',
info: { col: 0, row: 4, status: 3, other: { title: '复核会计', subtitle: '协同:张三、张四', auxi: '' } }
},
{ name: '6', info: { col: 0, row: 5, status: 3, other: { title: '结束' } }, hidden: false }
],
links: [
{ from: '0', to: '1', fromJoint: 'bottom', toJoint: 'top', info: { status: 1, style: 'solid' } },
{ from: '1', to: '2', fromJoint: 'bottom', toJoint: 'top', info: { status: 1, style: 'solid' } },
{ from: '2', to: '3', fromJoint: 'bottom', toJoint: 'top', info: { status: 1, style: 'solid' } },
{
from: '2',
to: '4',
fromJoint: 'bottom',
toJoint: 'top',
linkOffset: 96,
showArrow: false,
info: { status: 1, style: 'solid', other: { title: '条件1' } }
},
{ from: '3', to: '5', fromJoint: 'bottom', toJoint: 'top', info: { status: 3, style: 'solid' } },
{
from: '4',
to: '5',
fromJoint: 'bottom',
toJoint: 'top',
arrowEndMinus: 96,
info: { status: 3, style: 'solid', other: { title: '条件2' } }
},
{ from: '5', to: '6', fromJoint: 'bottom', toJoint: 'top', info: { status: 3, style: 'dashed' } }
],
groups: [
{
nodes: ['2', '3'],
padding: [20, 100],
lineDash: [6, 6],
strokeStyle: '#8d959e',
title: '主账',
titlePosition: 'top-left',
titleClass: 'bg-color-bg-1'
}
]
hidden: false
},
{
name: '1',
info: {
col: 0,
row: 1,
status: 1,
other: { title: '申请人', subtitle: '张三', auxi: '2023-01-02' }
}
},
{
name: '2',
info: {
col: 0,
row: 2,
status: 1,
other: { title: '制单会计', subtitle: '协同:张三、张四、张五、张六、张七', auxi: '2023-01-03' }
}
},
{
name: '3',
info: {
col: 0,
row: 3,
status: 2,
other: { title: '应付会计', subtitle: '张四 0035837', auxi: '' }
}
},
{
name: '4',
info: {
col: 1,
row: 3,
status: 4,
other: { title: '应付会计', subtitle: '张四 0035837', auxi: '', error: '人员变更,未同步' }
}
},
{
name: '5',
info: {
col: 0,
row: 4,
status: 3,
other: { title: '复核会计', subtitle: '协同:张三、张四', auxi: '' }
}
},
{
name: '6',
info: { col: 0, row: 5, status: 3, other: { title: '结束' } },
hidden: false
}
return {
chartData: hooks.markRaw(chartData),
chartConfig: hooks.markRaw(chartConfig)
],
links: [
{ from: '0', to: '1', fromJoint: 'bottom', toJoint: 'top', info: { status: 1, style: 'solid' } },
{ from: '1', to: '2', fromJoint: 'bottom', toJoint: 'top', info: { status: 1, style: 'solid' } },
{ from: '2', to: '3', fromJoint: 'bottom', toJoint: 'top', info: { status: 1, style: 'solid' } },
{
from: '2',
to: '4',
fromJoint: 'bottom',
toJoint: 'top',
linkOffset: 96,
showArrow: false,
info: { status: 1, style: 'solid', other: { title: '条件1' } }
},
{ from: '3', to: '5', fromJoint: 'bottom', toJoint: 'top', info: { status: 3, style: 'solid' } },
{
from: '4',
to: '5',
fromJoint: 'bottom',
toJoint: 'top',
arrowEndMinus: 96,
// showArrow: false,
info: { status: 3, style: 'solid', other: { title: '条件2' } }
},
{ from: '5', to: '6', fromJoint: 'bottom', toJoint: 'top', info: { status: 3, style: 'dashed' } }
],
groups: [
{
nodes: ['2', '3'],
padding: [20, 100], // fillStyle: '#f5f6f8',
lineDash: [6, 6],
strokeStyle: '#8d959e',
title: '主账',
titlePosition: 'top-left' /* top/top-left */,
titleClass: 'bg-color-bg-1'
}
]
}
const chartConfig = createConfig()
Object.assign(chartConfig, {
width: 0,
extraWidth: 200, //
height: 0,
gap: 60,
padding: 12,
prior: 'vertical',
align: 'left',
status: { 1: 'completed', 2: 'ongoing', 3: 'not-started', 4: 'fail' },
colors: { 1: '#0067D1', 2: '#0067d1', 3: 'rgba(22,30,38,0.2)', 4: '#eb171f' },
ongoingBackgroundColor: '#f3f8fe',
popoverPlacement: 'right',
type: 'dot',
renderOuter: (h, node) => {
return h(Node, { props: { node, config: chartConfig, titleClass: 'bg-color-bg-1' } })
},
nodeLayout: 'left-right' /* up-down/left-right */,
nodeSize: 'medium' /* mini/small/medium */,
nodeWrapperSize,
titleMaxWidth: 90,
linkEndMinus: 6,
showOnly: '' /* icon/title */,
linkPath: [
{
filter: { from: '4', to: '5' },
method: ({ afterLink, afterNodes }) => {
const afterNodeCouple = [afterLink.raw.from, afterLink.raw.to].map((name) =>
afterNodes.find((afterNode) => afterNode.raw.name === name)
)
const { x: x0, y: y0, width: w0, height: h0 } = afterNodeCouple[0]
const { x: x1, y: y1, width: w1, height: h1 } = afterNodeCouple[1]
//
const f = { x: x0 + w0 / 2, y: y0 + h0 }
//
const t = { x: x1 + w1, y: y1 + h1 / 2 }
//
const c = { x: f.x, y: t.y }
// // a. 线
// return [f, c, t]
// b. 线线
return {
path: [f, c, t],
mid: { x: (f.x + c.x) / 2, y: (f.y + c.y) / 2 },
linear: { stops: [0, 1], colors: ['gold', 'blue'] }
}
}
},
{
filter: { from: '2', to: '4' },
method: ({ afterLink, afterNodes }) => {
const afterNodeCouple = [afterLink.raw.from, afterLink.raw.to].map((name) =>
afterNodes.find((afterNode) => afterNode.raw.name === name)
)
const { x: x0, y: y0, width: w0, height: h0 } = afterNodeCouple[0]
const { x: x1, y: y1, width: w1, height: h1 } = afterNodeCouple[1]
//
const f = { x: x0 + w0, y: y0 + h0 / 2 }
//
const t = { x: x1 + w1 / 2, y: y1 }
//
const c = { x: t.x, y: f.y }
// a. 线
return [f, c, t]
// // b. 线
// return { path: [f, c, t], mid: c }
}
}
],
condClass: 'bg-color-bg-1'
})
export default {
components: {
TinyAsyncFlowchart
},
methods: {
onClickNode(afterNode, e) {
// console.log(afterNode, e)
TinyModal.message('click-node')
},
onClickLink(afterLink, e) {
// console.log(afterLink, e)
TinyModal.message('click-link')
},
onClickBlank(param, e) {
// console.log(param, e)
TinyModal.message('click-blank')
},
onClickGroup(afterGroup, e) {
// console.log(afterGroup, e)
TinyModal.message('click-group')
},
fetchFunc() {
const self = this
return new Promise(function (resolve) {
setTimeout(function () {
resolve({ data: self.chartData, config: self.chartConfig })
return new Promise((resolve) => {
setTimeout(() => {
resolve({ data: chartData, config: chartConfig })
}, 300)
})
}

View File

@ -1,15 +1,14 @@
<template>
<div>
<tiny-flowchart
ref="chart"
:data="chartData"
:config="chartConfig"
@click-node="onClickNode"
@click-link="onClickLink"
@click-blank="onClickBlank"
@click-group="onClickGroup"
/>
</div>
<tiny-flowchart
ref="chart"
:data="chartData"
:config="chartConfig"
@click-node="onClickNode"
@click-link="onClickLink"
@click-blank="onClickBlank"
@click-group="onClickGroup"
>
</tiny-flowchart>
</template>
<script>
@ -19,155 +18,186 @@ import { hooks } from '@opentiny/vue-common'
const { createConfig, Node, resizeMixin } = TinyFlowchart
const nodeWrapperSize = 32
const chartData = {
nodes: [
{
name: '0',
info: {
col: 0,
row: 0,
status: 1,
other: { title: '开始', subtitle: '张三', auxi: '2023-01-01' }
},
hidden: false
},
{
name: '1',
info: {
col: 0,
row: 1,
status: 1,
other: { title: '申请人', subtitle: '张三', auxi: '2023-01-02' }
}
},
{
name: '2',
info: {
col: 0,
row: 2,
status: 1,
other: { title: '制单会计', subtitle: '协同:张三、张四、张五、张六、张七', auxi: '2023-01-03' }
}
},
{
name: '3',
info: {
col: 0,
row: 3,
status: 2,
other: { title: '应付会计', subtitle: '张四 0035837', auxi: '' }
}
},
{
name: '4',
info: {
col: 1,
row: 3,
status: 4,
other: { title: '应付会计', subtitle: '张四 0035837', auxi: '', error: '人员变更,未同步' }
}
},
{
name: '5',
info: {
col: 0,
row: 4,
status: 3,
other: { title: '复核会计', subtitle: '协同:张三、张四', auxi: '' }
}
},
{
name: '6',
info: { col: 0, row: 5, status: 3, other: { title: '结束' } },
hidden: false
}
],
links: [
{ from: '0', to: '1', fromJoint: 'bottom', toJoint: 'top', info: { status: 1, style: 'solid' } },
{ from: '1', to: '2', fromJoint: 'bottom', toJoint: 'top', info: { status: 1, style: 'solid' } },
{ from: '2', to: '3', fromJoint: 'bottom', toJoint: 'top', info: { status: 1, style: 'solid' } },
{
from: '2',
to: '4',
fromJoint: 'bottom',
toJoint: 'top',
linkOffset: 96,
showArrow: false,
info: { status: 1, style: 'solid', other: { title: '条件1' } }
},
{ from: '3', to: '5', fromJoint: 'bottom', toJoint: 'top', info: { status: 3, style: 'solid' } },
{
from: '4',
to: '5',
fromJoint: 'bottom',
toJoint: 'top',
arrowEndMinus: 96,
// showArrow: false,
info: { status: 3, style: 'solid', other: { title: '条件2' } }
},
{ from: '5', to: '6', fromJoint: 'bottom', toJoint: 'top', info: { status: 3, style: 'dashed' } }
],
groups: [
{
nodes: ['2', '3'],
padding: [20, 100], // fillStyle: '#f5f6f8',
lineDash: [6, 6],
strokeStyle: '#8d959e',
title: '主账',
titlePosition: 'top-left' /* top/top-left */,
titleClass: 'bg-color-bg-1'
}
]
}
const chartConfig = createConfig()
Object.assign(chartConfig, {
width: 0,
extraWidth: 200, //
height: 0,
gap: 60,
padding: 12,
prior: 'vertical',
align: 'left',
status: { 1: 'completed', 2: 'ongoing', 3: 'not-started', 4: 'fail' },
colors: { 1: '#0067D1', 2: '#0067d1', 3: 'rgba(22,30,38,0.2)', 4: '#eb171f' },
ongoingBackgroundColor: '#f3f8fe',
popoverPlacement: 'right',
type: 'dot',
renderOuter: (h, node) => {
return h(Node, { props: { node, config: chartConfig, titleClass: 'bg-color-bg-1' } })
},
nodeLayout: 'left-right' /* up-down/left-right */,
nodeSize: 'medium' /* mini/small/medium */,
nodeWrapperSize,
titleMaxWidth: 90,
linkEndMinus: 6,
showOnly: '' /* icon/title */,
linkPath: [
{
filter: { from: '4', to: '5' },
method: ({ afterLink, afterNodes }) => {
const afterNodeCouple = [afterLink.raw.from, afterLink.raw.to].map((name) =>
afterNodes.find((afterNode) => afterNode.raw.name === name)
)
const { x: x0, y: y0, width: w0, height: h0 } = afterNodeCouple[0]
const { x: x1, y: y1, width: w1, height: h1 } = afterNodeCouple[1]
//
const f = { x: x0 + w0 / 2, y: y0 + h0 }
//
const t = { x: x1 + w1, y: y1 + h1 / 2 }
//
const c = { x: f.x, y: t.y }
// // a. 线
// return [f, c, t]
// b. 线线
return {
path: [f, c, t],
mid: { x: (f.x + c.x) / 2, y: (f.y + c.y) / 2 },
linear: { stops: [0, 1], colors: ['gold', 'blue'] }
}
}
},
{
filter: { from: '2', to: '4' },
method: ({ afterLink, afterNodes }) => {
const afterNodeCouple = [afterLink.raw.from, afterLink.raw.to].map((name) =>
afterNodes.find((afterNode) => afterNode.raw.name === name)
)
const { x: x0, y: y0, width: w0, height: h0 } = afterNodeCouple[0]
const { x: x1, y: y1, width: w1, height: h1 } = afterNodeCouple[1]
//
const f = { x: x0 + w0, y: y0 + h0 / 2 }
//
const t = { x: x1 + w1 / 2, y: y1 }
//
const c = { x: t.x, y: f.y }
// a. 线
return [f, c, t]
// // b. 线
// return { path: [f, c, t], mid: c }
}
}
],
condClass: 'bg-color-bg-1'
})
export default {
mixins: [resizeMixin({ refName: 'chart', nodeWrapperSize })],
components: { TinyFlowchart },
components: {
TinyFlowchart
},
data() {
const chartConfig = createConfig()
Object.assign(chartConfig, {
width: 0,
extraWidth: 200,
height: 0,
gap: 60,
padding: 12,
prior: 'vertical',
align: 'left',
status: { 1: 'completed', 2: 'ongoing', 3: 'not-started', 4: 'fail' },
colors: { 1: '#0067D1', 2: '#0067d1', 3: 'rgba(22,30,38,0.2)', 4: '#eb171f' },
ongoingBackgroundColor: '#f3f8fe',
popoverPlacement: 'right',
type: 'dot',
renderOuter(h, node) {
return h(Node, { props: { node, config: chartConfig, titleClass: 'bg-color-bg-1' } })
},
nodeLayout: 'left-right',
nodeSize: 'medium',
nodeWrapperSize,
titleMaxWidth: 90,
linkEndMinus: 6,
showOnly: '',
linkPath: [
{
filter: { from: '4', to: '5' },
method({ afterLink, afterNodes }) {
const afterNodeCouple = [afterLink.raw.from, afterLink.raw.to].map(function (name) {
return afterNodes.find(function (afterNode) {
return afterNode.raw.name === name
})
})
const f = {
x: afterNodeCouple[0].x + afterNodeCouple[0].width / 2,
y: afterNodeCouple[0].y + afterNodeCouple[0].height
}
const t = {
x: afterNodeCouple[1].x + afterNodeCouple[1].width,
y: afterNodeCouple[1].y + afterNodeCouple[1].height / 2
}
const c = { x: f.x, y: t.y }
return {
path: [f, c, t],
mid: { x: (f.x + c.x) / 2, y: (f.y + c.y) / 2 },
linear: { stops: [0, 1], colors: ['gold', 'blue'] }
}
}
},
{
filter: { from: '2', to: '4' },
method({ afterLink, afterNodes }) {
const afterNodeCouple = [afterLink.raw.from, afterLink.raw.to].map(function (name) {
return afterNodes.find(function (afterNode) {
return afterNode.raw.name === name
})
})
const f = {
x: afterNodeCouple[0].x + afterNodeCouple[0].width,
y: afterNodeCouple[0].y + afterNodeCouple[0].height / 2
}
const t = { x: afterNodeCouple[1].x + afterNodeCouple[1].width / 2, y: afterNodeCouple[1].y }
const c = { x: t.x, y: f.y }
return [f, c, t]
}
}
],
condClass: 'bg-color-bg-1'
})
const chartData = {
nodes: [
{
name: '0',
info: { col: 0, row: 0, status: 1, other: { title: '开始', subtitle: '张三', auxi: '2023-01-01' } },
hidden: false
},
{
name: '1',
info: { col: 0, row: 1, status: 1, other: { title: '申请人', subtitle: '张三', auxi: '2023-01-02' } }
},
{
name: '2',
info: {
col: 0,
row: 2,
status: 1,
other: { title: '制单会计', subtitle: '协同:张三、张四', auxi: '2023-01-03' }
}
},
{
name: '3',
info: { col: 0, row: 3, status: 2, other: { title: '应付会计', subtitle: '张四 0035837', auxi: '' } }
},
{
name: '4',
info: {
col: 1,
row: 3,
status: 4,
other: { title: '应付会计', subtitle: '张四 0035837', auxi: '', error: '人员变更,未同步' }
}
},
{
name: '5',
info: { col: 0, row: 4, status: 3, other: { title: '复核会计', subtitle: '协同:张三、张四', auxi: '' } }
},
{ name: '6', info: { col: 0, row: 5, status: 3, other: { title: '结束' } }, hidden: false }
],
links: [
{ from: '0', to: '1', fromJoint: 'bottom', toJoint: 'top', info: { status: 1, style: 'solid' } },
{ from: '1', to: '2', fromJoint: 'bottom', toJoint: 'top', info: { status: 1, style: 'solid' } },
{ from: '2', to: '3', fromJoint: 'bottom', toJoint: 'top', info: { status: 1, style: 'solid' } },
{
from: '2',
to: '4',
fromJoint: 'bottom',
toJoint: 'top',
linkOffset: 96,
showArrow: false,
info: { status: 1, style: 'solid', other: { title: '条件1' } }
},
{ from: '3', to: '5', fromJoint: 'bottom', toJoint: 'top', info: { status: 3, style: 'solid' } },
{
from: '4',
to: '5',
fromJoint: 'bottom',
toJoint: 'top',
arrowEndMinus: 96,
info: { status: 3, style: 'solid', other: { title: '条件2' } }
},
{ from: '5', to: '6', fromJoint: 'bottom', toJoint: 'top', info: { status: 3, style: 'dashed' } }
],
groups: [
{
nodes: ['2', '3'],
padding: [20, 100],
lineDash: [6, 6],
strokeStyle: '#8d959e',
title: '主账',
titlePosition: 'top-left',
titleClass: 'bg-color-bg-1'
}
]
}
return {
chartData: hooks.markRaw(chartData),
chartConfig: hooks.markRaw(chartConfig)
@ -175,15 +205,19 @@ export default {
},
methods: {
onClickNode(afterNode, e) {
// console.log(afterNode, e)
TinyModal.message('click-node')
},
onClickLink(afterLink, e) {
// console.log(afterLink, e)
TinyModal.message('click-link')
},
onClickBlank(param, e) {
// console.log(param, e)
TinyModal.message('click-blank')
},
onClickGroup(afterGroup, e) {
// console.log(afterGroup, e)
TinyModal.message('click-group')
}
}

View File

@ -1,15 +1,14 @@
<template>
<div>
<tiny-flowchart
ref="chart"
class="text-xs"
:data="chartData"
:config="chartConfig"
@click-node="onClickNode"
@click-link="onClickLink"
@click-blank="onClickBlank"
/>
</div>
<tiny-flowchart
ref="chart"
class="text-xs"
:data="chartData"
:config="chartConfig"
@click-node="onClickNode"
@click-link="onClickLink"
@click-blank="onClickBlank"
>
</tiny-flowchart>
</template>
<script>
@ -19,188 +18,313 @@ import { hooks } from '@opentiny/vue-common'
const { createConfig, resizeMixin } = TinyFlowchart
const nodeWrapperSize = 130
const chartData = {
nodes: [
{
name: '0',
info: {
row: 0,
col: 1,
width: 130,
height: 56,
shape: 'rectangle',
status: 1,
other: { main: '', auxi: '' }
}
},
{
name: '1',
info: {
row: 0,
col: 2,
width: 130,
height: 56,
shape: 'rectangle',
status: 1,
other: { main: '', auxi: '' }
}
},
{
name: '2',
info: {
row: 1,
col: 0,
width: 130,
height: 56,
shape: 'rectangle',
status: 1,
other: { main: '', auxi: '' }
}
},
{
name: '3',
info: {
row: 1,
col: 3,
width: 130,
height: 56,
shape: 'rectangle',
status: 1,
other: { main: '', auxi: '' }
}
},
{
name: '4',
info: {
row: 1,
col: 4,
width: 130,
height: 56,
shape: 'rectangle',
status: 1,
other: { main: '', auxi: '' }
}
},
{
name: '5',
info: {
row: 2,
col: 1.5,
width: 130,
height: 56,
shape: 'rectangle',
status: 1,
other: { main: '', auxi: '' }
}
}
],
links: [
{
from: '2',
to: '0',
fromJoint: 'right',
toJoint: 'left',
showArrow: false,
info: { status: 3, style: 'solid', other: { title: '条件1' } }
},
{
from: '0',
to: '1',
fromJoint: 'right',
toJoint: 'left',
showArrow: false,
info: { status: 3, style: 'solid', other: { title: '条件2' } }
},
{
from: '1',
to: '3',
fromJoint: 'right',
toJoint: 'left',
showArrow: false,
info: { status: 3, style: 'solid', other: { title: '条件3' } }
},
{
from: '3',
to: '4',
fromJoint: 'right',
toJoint: 'left',
showArrow: false,
info: { status: 3, style: 'solid', other: { title: '条件4' } }
},
{
from: '2',
to: '5',
fromJoint: 'right',
toJoint: 'left',
showArrow: false,
info: { status: 3, style: 'solid', other: { title: '条件5' } }
},
{
from: '5',
to: '3',
fromJoint: 'right',
toJoint: 'left',
showArrow: false,
info: { status: 3, style: 'solid', other: { title: '条件6' } }
}
]
}
const chartConfig = createConfig()
Object.assign(chartConfig, {
width: 0,
extraWidth: 100, //
height: 240,
gap: 24,
padding: 12,
prior: 'vertical',
align: 'center',
status: { 1: 'completed', 2: 'ongoing', 3: 'not-started', 4: 'fail' },
colors: { 1: '#00a874', 2: '#0067d1', 3: '#999', 4: '#eb171f' },
ongoingBackgroundColor: '#f3f8fe',
popoverPlacement: 'bottom',
nodeWrapperSize,
type: 'dot',
lineWidth: 20,
hoverHit: 10,
autoAdjust: false,
layout: ({ afterNodes, graphHeight, graphWidth }) => {
const colSize = graphWidth / 5
const rowSize = graphHeight / 3
return afterNodes.map((afterNode) => ({
x: ~~((afterNode.col + 0.5) * colSize) - 70,
y: ~~((afterNode.row + 0.5) * rowSize) - 28
}))
},
linkPath: [
{
filter: { from: '2', to: '0' },
method: ({ afterLink, afterNodes }) => {
const afterNodeCouple = [afterLink.raw.from, afterLink.raw.to].map((name) =>
afterNodes.find((afterNode) => afterNode.raw.name === name)
)
const { x: x0, y: y0, width: w0, height: h0 } = afterNodeCouple[0]
const { x: x1, y: y1, width: w1, height: h1 } = afterNodeCouple[1]
//
const f = { x: x0 + w0, y: y0 + h0 / 2 - 10 }
//
const t = { x: x1, y: y1 + h1 / 2 }
const c0 = { x: f.x + 30, y: f.y }
const c1 = { x: c0.x, y: t.y }
return {
path: [f, c0, c1, t],
mid: { x: (c1.x + t.x) / 2, y: (c1.y + t.y) / 2 },
linear: { stops: [0, 1], colors: ['#4facfe', '#00f2fe'] }
}
}
},
{
filter: { from: '2', to: '5' },
method: ({ afterLink, afterNodes }) => {
const afterNodeCouple = [afterLink.raw.from, afterLink.raw.to].map((name) =>
afterNodes.find((afterNode) => afterNode.raw.name === name)
)
const { x: x0, y: y0, width: w0, height: h0 } = afterNodeCouple[0]
const { x: x1, y: y1, width: w1, height: h1 } = afterNodeCouple[1]
//
const f = { x: x0 + w0, y: y0 + h0 / 2 + 10 }
//
const t = { x: x1, y: y1 + h1 / 2 }
const c0 = { x: f.x + 30, y: f.y }
const c1 = { x: c0.x, y: t.y }
return {
path: [f, c0, c1, t],
mid: { x: (c1.x + t.x) / 2, y: (c1.y + t.y) / 2 },
linear: { stops: [0, 1], colors: ['#4facfe', '#00f2fe'] }
}
}
},
{
filter: { from: '0', to: '1' },
method: ({ afterLink, afterNodes }) => {
const afterNodeCouple = [afterLink.raw.from, afterLink.raw.to].map((name) =>
afterNodes.find((afterNode) => afterNode.raw.name === name)
)
const { x: x0, y: y0, width: w0, height: h0 } = afterNodeCouple[0]
const { x: x1, y: y1, width: w1, height: h1 } = afterNodeCouple[1]
//
const f = { x: x0 + w0, y: y0 + h0 / 2 }
//
const t = { x: x1, y: y1 + h1 / 2 }
return {
path: [f, t],
mid: { x: (f.x + t.x) / 2, y: (f.y + t.y) / 2 },
linear: { stops: [0, 1], colors: ['#4facfe', '#00f2fe'] }
}
}
},
{
filter: { from: '1', to: '3' },
method: ({ afterLink, afterNodes }) => {
const afterNodeCouple = [afterLink.raw.from, afterLink.raw.to].map((name) =>
afterNodes.find((afterNode) => afterNode.raw.name === name)
)
const { x: x0, y: y0, width: w0, height: h0 } = afterNodeCouple[0]
const { x: x1, y: y1, width: w1, height: h1 } = afterNodeCouple[1]
//
const f = { x: x0 + w0, y: y0 + h0 / 2 }
//
const t = { x: x1, y: y1 + h1 / 2 - 10 }
const c1 = { x: t.x - 30, y: t.y }
const c0 = { x: c1.x, y: f.y }
return {
path: [f, c0, c1, t],
mid: { x: (c1.x + t.x) / 2, y: (c1.y + t.y) / 2 },
linear: { stops: [0, 1], colors: ['#4facfe', '#00f2fe'] }
}
}
},
{
filter: { from: '5', to: '3' },
method: ({ afterLink, afterNodes }) => {
const afterNodeCouple = [afterLink.raw.from, afterLink.raw.to].map((name) =>
afterNodes.find((afterNode) => afterNode.raw.name === name)
)
const { x: x0, y: y0, width: w0, height: h0 } = afterNodeCouple[0]
const { x: x1, y: y1, width: w1, height: h1 } = afterNodeCouple[1]
//
const f = { x: x0 + w0, y: y0 + h0 / 2 }
//
const t = { x: x1, y: y1 + h1 / 2 + 10 }
const c1 = { x: t.x - 30, y: t.y }
const c0 = { x: c1.x, y: f.y }
return {
path: [f, c0, c1, t],
mid: { x: (c1.x + t.x) / 2, y: (c1.y + t.y) / 2 },
linear: { stops: [0, 1], colors: ['#4facfe', '#00f2fe'] }
}
}
},
{
filter: { from: '3', to: '4' },
method: ({ afterLink, afterNodes }) => {
const afterNodeCouple = [afterLink.raw.from, afterLink.raw.to].map((name) =>
afterNodes.find((afterNode) => afterNode.raw.name === name)
)
const { x: x0, y: y0, width: w0, height: h0 } = afterNodeCouple[0]
const { x: x1, y: y1, width: w1, height: h1 } = afterNodeCouple[1]
//
const f = { x: x0 + w0, y: y0 + h0 / 2 }
//
const t = { x: x1, y: y1 + h1 / 2 }
return {
path: [f, t],
mid: { x: (f.x + t.x) / 2, y: (f.y + t.y) / 2 },
linear: { stops: [0, 1], colors: ['#4facfe', '#00f2fe'] }
}
}
}
],
renderOuter: (h, node) => {
return h(
'div',
{
style: { width: '100%', height: '100%', background: 'white', border: '1px solid #4facfe', borderRadius: '4px' }
},
`自定义区域-${node.name}`
)
},
renderCond: (h, afterLink, cfg) => {
const { raw: link } = afterLink
// link.info.other.title
return h('div', `${link.from}-${link.to}`)
}
})
export default {
mixins: [resizeMixin({ refName: 'chart', nodeWrapperSize })],
components: { TinyFlowchart },
components: {
TinyFlowchart
},
data() {
const chartConfig = createConfig()
Object.assign(chartConfig, {
width: 0,
extraWidth: 100,
height: 240,
gap: 24,
padding: 12,
prior: 'vertical',
align: 'center',
status: { 1: 'completed', 2: 'ongoing', 3: 'not-started', 4: 'fail' },
colors: { 1: '#00a874', 2: '#0067d1', 3: '#999', 4: '#eb171f' },
ongoingBackgroundColor: '#f3f8fe',
popoverPlacement: 'bottom',
nodeWrapperSize,
type: 'dot',
lineWidth: 20,
hoverHit: 10,
autoAdjust: false,
layout({ afterNodes, graphHeight, graphWidth }) {
const colSize = graphWidth / 5
const rowSize = graphHeight / 3
return afterNodes.map(function (afterNode) {
return {
x: ~~((afterNode.col + 0.5) * colSize) - 70,
y: ~~((afterNode.row + 0.5) * rowSize) - 28
}
})
},
linkPath: [
{
filter: { from: '2', to: '0' },
method({ afterLink, afterNodes }) {
const afterNodeCouple = [afterLink.raw.from, afterLink.raw.to].map(function (name) {
return afterNodes.find(function (afterNode) {
return afterNode.raw.name === name
})
})
const f = {
x: afterNodeCouple[0].x + afterNodeCouple[0].width,
y: afterNodeCouple[0].y + afterNodeCouple[0].height / 2 - 10
}
const t = { x: afterNodeCouple[1].x, y: afterNodeCouple[1].y + afterNodeCouple[1].height / 2 }
const c0 = { x: f.x + 30, y: f.y }
const c1 = { x: c0.x, y: t.y }
return {
path: [f, c0, c1, t],
mid: { x: (c1.x + t.x) / 2, y: (c1.y + t.y) / 2 },
linear: { stops: [0, 1], colors: ['#4facfe', '#00f2fe'] }
}
}
},
{
filter: { from: '2', to: '5' },
method({ afterLink, afterNodes }) {
const afterNodeCouple = [afterLink.raw.from, afterLink.raw.to].map(function (name) {
return afterNodes.find(function (afterNode) {
return afterNode.raw.name === name
})
})
const f = {
x: afterNodeCouple[0].x + afterNodeCouple[0].width,
y: afterNodeCouple[0].y + afterNodeCouple[0].height / 2 + 10
}
const t = { x: afterNodeCouple[1].x, y: afterNodeCouple[1].y + afterNodeCouple[1].height / 2 }
const c0 = { x: f.x + 30, y: f.y }
const c1 = { x: c0.x, y: t.y }
return {
path: [f, c0, c1, t],
mid: { x: (c1.x + t.x) / 2, y: (c1.y + t.y) / 2 },
linear: { stops: [0, 1], colors: ['#4facfe', '#00f2fe'] }
}
}
}
],
renderOuter(h, node) {
return h(
'div',
{
style: {
width: '100%',
height: '100%',
background: 'white',
border: '1px solid #4facfe',
borderRadius: '4px'
}
},
'自定义区域-' + node.name
)
}
})
const chartData = {
nodes: [
{
name: '0',
info: { row: 0, col: 1, width: 130, height: 56, shape: 'rectangle', status: 1, other: { main: '', auxi: '' } }
},
{
name: '1',
info: { row: 0, col: 2, width: 130, height: 56, shape: 'rectangle', status: 1, other: { main: '', auxi: '' } }
},
{
name: '2',
info: { row: 1, col: 0, width: 130, height: 56, shape: 'rectangle', status: 1, other: { main: '', auxi: '' } }
},
{
name: '3',
info: { row: 1, col: 3, width: 130, height: 56, shape: 'rectangle', status: 1, other: { main: '', auxi: '' } }
},
{
name: '4',
info: { row: 1, col: 4, width: 130, height: 56, shape: 'rectangle', status: 1, other: { main: '', auxi: '' } }
},
{
name: '5',
info: {
row: 2,
col: 1.5,
width: 130,
height: 56,
shape: 'rectangle',
status: 1,
other: { main: '', auxi: '' }
}
}
],
links: [
{
from: '2',
to: '0',
fromJoint: 'right',
toJoint: 'left',
showArrow: false,
info: { status: 3, style: 'solid', other: { title: '条件1' } }
},
{
from: '0',
to: '1',
fromJoint: 'right',
toJoint: 'left',
showArrow: false,
info: { status: 3, style: 'solid', other: { title: '条件2' } }
},
{
from: '1',
to: '3',
fromJoint: 'right',
toJoint: 'left',
showArrow: false,
info: { status: 3, style: 'solid', other: { title: '条件3' } }
},
{
from: '3',
to: '4',
fromJoint: 'right',
toJoint: 'left',
showArrow: false,
info: { status: 3, style: 'solid', other: { title: '条件4' } }
},
{
from: '2',
to: '5',
fromJoint: 'right',
toJoint: 'left',
showArrow: false,
info: { status: 3, style: 'solid', other: { title: '条件5' } }
},
{
from: '5',
to: '3',
fromJoint: 'right',
toJoint: 'left',
showArrow: false,
info: { status: 3, style: 'solid', other: { title: '条件6' } }
}
]
}
return {
chartData: hooks.markRaw(chartData),
chartConfig: hooks.markRaw(chartConfig)
@ -208,12 +332,15 @@ export default {
},
methods: {
onClickNode(afterNode, e) {
// console.log(afterNode, e)
TinyModal.message('click-node')
},
onClickLink(afterLink, e) {
// console.log(afterLink, e)
TinyModal.message('click-link')
},
onClickBlank(param, e) {
// console.log(param, e)
TinyModal.message('click-blank')
}
}

View File

@ -1,15 +1,14 @@
<template>
<div>
<tiny-flowchart
ref="chart"
class="text-xs"
:data="chartData"
:config="chartConfig"
@click-node="onClickNode"
@click-link="onClickLink"
@click-blank="onClickBlank"
/>
</div>
<tiny-flowchart
ref="chart"
class="text-xs"
:data="chartData"
:config="chartConfig"
@click-node="onClickNode"
@click-link="onClickLink"
@click-blank="onClickBlank"
>
</tiny-flowchart>
</template>
<script>
@ -19,121 +18,142 @@ import { hooks } from '@opentiny/vue-common'
const { createConfig, resizeMixin } = TinyFlowchart
const nodeWrapperSize = 130
const chartData = {
nodes: [
{
name: '0',
info: {
row: 0,
col: 0,
width: 130,
height: 56,
shape: 'rectangle',
status: 1,
other: { main: '', auxi: '' }
}
},
{
name: '1',
info: {
row: 0,
col: 1,
width: 130,
height: 56,
shape: 'rectangle',
status: 1,
other: { main: '', auxi: '' }
}
},
{
name: '2',
info: {
row: 0,
col: 2,
width: 130,
height: 56,
shape: 'rectangle',
status: 1,
other: { main: '', auxi: '' }
}
}
],
links: [
{
from: '0',
to: '1',
fromJoint: 'right',
toJoint: 'left',
showArrow: false,
info: { status: 3, style: 'solid', other: { title: '条件1' } }
},
{
from: '1',
to: '2',
fromJoint: 'right',
toJoint: 'left',
showArrow: false,
info: { status: 3, style: 'solid', other: { title: '条件2' } }
}
]
}
const chartConfig = createConfig()
Object.assign(chartConfig, {
width: 0,
extraWidth: 100, //
height: 0,
gap: 24,
padding: 12,
prior: 'vertical',
align: 'center',
status: { 1: 'completed', 2: 'ongoing', 3: 'not-started', 4: 'fail' },
colors: { 1: '#00a874', 2: '#0067d1', 3: '#999', 4: '#eb171f' },
ongoingBackgroundColor: '#f3f8fe',
popoverPlacement: 'bottom',
nodeWrapperSize,
type: 'dot',
lineWidth: 20,
hoverHit: 10,
linkPath: [
{
filter: { from: '0', to: '1' },
method: ({ afterLink, afterNodes }) => {
const afterNodeCouple = [afterLink.raw.from, afterLink.raw.to].map((name) =>
afterNodes.find((afterNode) => afterNode.raw.name === name)
)
const { x: x0, y: y0, width: w0, height: h0 } = afterNodeCouple[0]
const { x: x1, y: y1, width: w1, height: h1 } = afterNodeCouple[1]
//
const f = { x: x0 + w0, y: y0 + h0 / 2 }
//
const t = { x: x1, y: y1 + h1 / 2 }
return {
path: [f, t],
mid: { x: (f.x + t.x) / 2, y: (f.y + t.y) / 2 },
linear: { stops: [0, 1], colors: ['#4facfe', '#00f2fe'] }
}
}
},
{
filter: { from: '1', to: '2' },
method: ({ afterLink, afterNodes }) => {
const afterNodeCouple = [afterLink.raw.from, afterLink.raw.to].map((name) =>
afterNodes.find((afterNode) => afterNode.raw.name === name)
)
const { x: x0, y: y0, width: w0, height: h0 } = afterNodeCouple[0]
const { x: x1, y: y1, width: w1, height: h1 } = afterNodeCouple[1]
//
const f = { x: x0 + w0, y: y0 + h0 / 2 }
//
const t = { x: x1, y: y1 + h1 / 2 }
return {
path: [f, t],
mid: { x: (f.x + t.x) / 2, y: (f.y + t.y) / 2 },
linear: { stops: [0, 1], colors: ['#4facfe', '#00f2fe'] } // '#43e97b','#38f9d7'
}
}
}
],
renderOuter: (h, node) => {
return h(
'div',
{
style: { width: '100%', height: '100%', background: 'white', border: '1px solid #4facfe', borderRadius: '4px' }
},
`自定义区域-${node.name}`
)
}
})
export default {
mixins: [resizeMixin({ refName: 'chart', nodeWrapperSize })],
components: { TinyFlowchart },
components: {
TinyFlowchart
},
data() {
const chartConfig = createConfig()
Object.assign(chartConfig, {
width: 0,
extraWidth: 100,
height: 0,
gap: 24,
padding: 12,
prior: 'vertical',
align: 'center',
status: { 1: 'completed', 2: 'ongoing', 3: 'not-started', 4: 'fail' },
colors: { 1: '#00a874', 2: '#0067d1', 3: '#999', 4: '#eb171f' },
ongoingBackgroundColor: '#f3f8fe',
popoverPlacement: 'bottom',
nodeWrapperSize,
type: 'dot',
lineWidth: 20,
hoverHit: 10,
linkPath: [
{
filter: { from: '0', to: '1' },
method({ afterLink, afterNodes }) {
const afterNodeCouple = [afterLink.raw.from, afterLink.raw.to].map(function (name) {
return afterNodes.find(function (afterNode) {
return afterNode.raw.name === name
})
})
const f = {
x: afterNodeCouple[0].x + afterNodeCouple[0].width,
y: afterNodeCouple[0].y + afterNodeCouple[0].height / 2
}
const t = { x: afterNodeCouple[1].x, y: afterNodeCouple[1].y + afterNodeCouple[1].height / 2 }
return {
path: [f, t],
mid: { x: (f.x + t.x) / 2, y: (f.y + t.y) / 2 },
linear: { stops: [0, 1], colors: ['#4facfe', '#00f2fe'] }
}
}
},
{
filter: { from: '1', to: '2' },
method({ afterLink, afterNodes }) {
const afterNodeCouple = [afterLink.raw.from, afterLink.raw.to].map(function (name) {
return afterNodes.find(function (afterNode) {
return afterNode.raw.name === name
})
})
const f = {
x: afterNodeCouple[0].x + afterNodeCouple[0].width,
y: afterNodeCouple[0].y + afterNodeCouple[0].height / 2
}
const t = { x: afterNodeCouple[1].x, y: afterNodeCouple[1].y + afterNodeCouple[1].height / 2 }
return {
path: [f, t],
mid: { x: (f.x + t.x) / 2, y: (f.y + t.y) / 2 },
linear: { stops: [0, 1], colors: ['#4facfe', '#00f2fe'] }
}
}
}
],
renderOuter(h, node) {
return h(
'div',
{
style: {
width: '100%',
height: '100%',
background: 'white',
border: '1px solid #4facfe',
borderRadius: '4px'
}
},
'自定义区域-' + node.name
)
}
})
const chartData = {
nodes: [
{
name: '0',
info: { row: 0, col: 0, width: 130, height: 56, shape: 'rectangle', status: 1, other: { main: '', auxi: '' } }
},
{
name: '1',
info: { row: 0, col: 1, width: 130, height: 56, shape: 'rectangle', status: 1, other: { main: '', auxi: '' } }
},
{
name: '2',
info: { row: 0, col: 2, width: 130, height: 56, shape: 'rectangle', status: 1, other: { main: '', auxi: '' } }
}
],
links: [
{
from: '0',
to: '1',
fromJoint: 'right',
toJoint: 'left',
showArrow: false,
info: { status: 3, style: 'solid', other: { title: '条件1' } }
},
{
from: '1',
to: '2',
fromJoint: 'right',
toJoint: 'left',
showArrow: false,
info: { status: 3, style: 'solid', other: { title: '条件2' } }
}
]
}
return {
chartData: hooks.markRaw(chartData),
chartConfig: hooks.markRaw(chartConfig)
@ -141,12 +161,15 @@ export default {
},
methods: {
onClickNode(afterNode, e) {
// console.log(afterNode, e)
TinyModal.message('click-node')
},
onClickLink(afterLink, e) {
// console.log(afterLink, e)
TinyModal.message('click-link')
},
onClickBlank(param, e) {
// console.log(param, e)
TinyModal.message('click-blank')
}
}

View File

@ -1,13 +1,14 @@
<template>
<div>
<tiny-flowchart
:data="chartData"
:config="chartConfig"
@click-node="onClickNode"
@click-link="onClickLink"
@click-blank="onClickBlank"
/>
</div>
<tiny-flowchart
ref="chart"
class="text-xs"
:data="chartData"
:config="chartConfig"
@click-node="onClickNode"
@click-link="onClickLink"
@click-blank="onClickBlank"
>
</tiny-flowchart>
</template>
<script>
@ -16,76 +17,81 @@ import { hooks } from '@opentiny/vue-common'
const { createConfig } = TinyFlowchart
export default {
components: { TinyFlowchart },
data() {
const chartConfig = createConfig()
chartConfig.width = 0
chartConfig.height = 0
chartConfig.gap = 24
chartConfig.padding = 12
chartConfig.prior = 'vertical'
chartConfig.align = 'center'
chartConfig.status = { 1: 'completed', 2: 'ongoing', 3: 'not-started', 4: 'ongoing-fail' }
chartConfig.colors = { 1: '#00a874', 2: '#0067d1', 3: '#999', 4: '#eb171f' }
chartConfig.ongoingBackgroundColor = '#f3f8fe'
chartConfig.popoverPlacement = 'bottom'
const chartData = {
nodes: [
{
name: '0',
info: { col: 0, row: 0, width: 46, shape: 'circle', status: 2, other: { main: '开始' } }
},
{
name: '1',
info: {
col: 1,
row: 0,
width: 130,
height: 56,
shape: 'rectangle',
status: 3,
other: { main: '申请人', auxi: '张三' }
}
},
{
name: '2',
info: {
col: 2,
row: 0,
width: 130,
height: 56,
shape: 'rectangle',
status: 3,
other: { main: '制单会计', auxi: '协同:张三、张四' }
}
},
{
name: '3',
info: {
col: 3,
row: 0,
width: 130,
height: 56,
shape: 'rectangle',
status: 3,
other: { main: '应付会计', auxi: '张四 0035837' }
}
},
{
name: '4',
info: { col: 4, row: 0, width: 46, shape: 'circle', status: 3, other: { main: '结束' } }
}
],
links: [
{ from: '0', to: '1', fromJoint: 'right', toJoint: 'left', info: { status: 3, style: 'solid' } },
{ from: '1', to: '2', fromJoint: 'right', toJoint: 'left', info: { status: 3, style: 'solid' } },
{ from: '2', to: '3', fromJoint: 'right', toJoint: 'left', info: { status: 3, style: 'solid' } },
{ from: '3', to: '4', fromJoint: 'right', toJoint: 'left', info: { status: 3, style: 'solid' } }
]
const chartData = {
nodes: [
{
name: '0',
info: { col: 0, row: 0, width: 40, shape: 'circle', status: 2, other: { main: '开始' } }
},
{
name: '1',
info: {
col: 1,
row: 0,
width: 130,
height: 56,
shape: 'rectangle',
status: 3,
other: { main: '申请人', auxi: '张三' }
}
},
{
name: '2',
info: {
col: 2,
row: 0,
width: 130,
height: 56,
shape: 'rectangle',
status: 3,
other: { main: '制单会计', auxi: '协同:张三、张四、张五、张六、张七' }
}
},
{
name: '3',
info: {
col: 3,
row: 0,
width: 130,
height: 56,
shape: 'rectangle',
status: 3,
other: { main: '应付会计', auxi: '张四 0035837' }
}
},
{
name: '4',
info: { col: 4, row: 0, width: 40, shape: 'circle', status: 3, other: { main: '结束' } }
}
],
links: [
{ from: '0', to: '1', fromJoint: 'right', toJoint: 'left', info: { status: 3, style: 'solid' } },
{ from: '1', to: '2', fromJoint: 'right', toJoint: 'left', info: { status: 3, style: 'solid' } },
{ from: '2', to: '3', fromJoint: 'right', toJoint: 'left', info: { status: 3, style: 'solid' } },
{ from: '3', to: '4', fromJoint: 'right', toJoint: 'left', info: { status: 3, style: 'solid' } }
]
}
const chartConfig = createConfig()
Object.assign(chartConfig, {
width: 0,
height: 0,
gap: 24,
padding: 12,
prior: 'vertical',
align: 'center',
status: { 1: 'completed', 2: 'ongoing', 3: 'not-started', 4: 'ongoing-fail' },
colors: { 1: '#00a874', 2: '#0067d1', 3: '#999', 4: '#eb171f' },
ongoingBackgroundColor: '#f3f8fe',
popoverPlacement: 'bottom'
})
export default {
components: {
TinyFlowchart
},
data() {
return {
chartData: hooks.markRaw(chartData),
chartConfig: hooks.markRaw(chartConfig)
@ -93,12 +99,15 @@ export default {
},
methods: {
onClickNode(afterNode, e) {
// console.log(afterNode, e)
TinyModal.message('click-node')
},
onClickLink(afterLink, e) {
// console.log(afterLink, e)
TinyModal.message('click-link')
},
onClickBlank(param, e) {
// console.log(param, e)
TinyModal.message('click-blank')
}
}

View File

@ -1,129 +1,127 @@
<template>
<div>
<div class="mb-4">
<tiny-button-group :data="sizeList" v-model="activeSize"></tiny-button-group>
</div>
<div class="demo-flowchart">
<span>切换节点尺寸 </span>
<tiny-button-group :data="groupData" v-model="chartConfig.nodeSize" @change="changeNodeSize"></tiny-button-group>
<tiny-async-flowchart
ref="chart"
:fetch="fetchFunc"
@click-node="onClickNode"
@click-link="onClickLink"
@click-blank="onClickBlank"
/>
>
</tiny-async-flowchart>
</div>
</template>
<script>
import { TinyFlowchart, TinyAsyncFlowchart, TinyButtonGroup, TinyModal } from '@opentiny/vue'
import { hooks } from '@opentiny/vue-common'
const { createConfig, Node } = TinyFlowchart
const nodeWrapperSize = 32
export default {
components: { TinyAsyncFlowchart, TinyButtonGroup },
data() {
const self = this
const chartConfig = createConfig()
Object.assign(chartConfig, {
width: 0,
extraWidth: 100,
height: 90,
gap: 60,
padding: 12,
prior: 'vertical',
align: 'left',
status: { 1: 'completed', 2: 'ongoing', 3: 'not-started', 4: 'fail' },
colors: { 1: '#00a874', 2: '#0067d1', 3: '#999', 4: '#eb171f' },
ongoingBackgroundColor: '#f3f8fe',
popoverPlacement: 'bottom',
renderOuter(h, node) {
return h(Node, { props: { node, config: chartConfig } })
},
type: 'dot',
nodeWrapperSize,
showArrow: false,
nodeSize: 'medium'
})
const chartData = {
nodes: [
{
name: '0',
info: { col: 0, row: 0, status: 1, other: { title: '开始', subtitle: '张三', auxi: '2023-01-01' } },
hidden: false
},
{
name: '1',
info: { col: 1, row: 0, status: 1, other: { title: '申请人', subtitle: '张三', auxi: '2023-01-02' } }
},
{
name: '2',
info: {
col: 2,
row: 0,
status: 1,
other: { title: '制单会计', subtitle: '协同:张三、张四', auxi: '2023-01-03' }
}
},
{
name: '3',
info: { col: 3, row: 0, status: 2, other: { title: '应付会计', subtitle: '张四 0035837', auxi: '' } }
},
{
name: '4',
info: {
col: 4,
row: 0,
status: 4,
other: { title: '应付会计', subtitle: '张四 0035837', auxi: '', error: '人员变更,未同步' }
}
}
],
links: [
{ from: '0', to: '1', fromJoint: 'right', toJoint: 'left', info: { status: 2, style: 'solid' } },
{ from: '1', to: '2', fromJoint: 'right', toJoint: 'left', info: { status: 2, style: 'solid' } },
{ from: '2', to: '3', fromJoint: 'right', toJoint: 'left', info: { status: 3, style: 'solid' } },
{ from: '3', to: '4', fromJoint: 'right', toJoint: 'left', info: { status: 3, style: 'solid' } }
]
const chartData = {
nodes: [
{
name: '0',
info: { col: 0, row: 0, status: 1, other: { title: '开始', subtitle: '张三', auxi: '2023-01-01' } },
hidden: false
},
{
name: '1',
info: { col: 1, row: 0, status: 1, other: { title: '申请人', subtitle: '张三', auxi: '2023-01-02' } }
},
{
name: '2',
info: {
col: 2,
row: 0,
status: 1,
other: { title: '制单会计', subtitle: '协同:张三、张四、张五、张六、张七', auxi: '2023-01-03' }
}
},
{
name: '3',
info: { col: 3, row: 0, status: 2, other: { title: '应付会计', subtitle: '张四 0035837', auxi: '' } }
},
{
name: '4',
info: {
col: 4,
row: 0,
status: 4,
other: { title: '应付会计', subtitle: '张四 0035837', auxi: '', error: '人员变更,未同步' }
}
}
],
links: [
{ from: '0', to: '1', fromJoint: 'right', toJoint: 'left', info: { status: 2, style: 'solid' } },
{ from: '1', to: '2', fromJoint: 'right', toJoint: 'left', info: { status: 2, style: 'solid' } },
{ from: '2', to: '3', fromJoint: 'right', toJoint: 'left', info: { status: 3, style: 'solid' } },
{ from: '3', to: '4', fromJoint: 'right', toJoint: 'left', info: { status: 3, style: 'solid' } }
]
}
return {
activeSize: 'medium',
sizeList: [
{ text: 'mini', value: 'mini' },
{ text: 'small', value: 'small' },
{ text: 'medium', value: 'medium' }
],
chartData: hooks.markRaw(chartData),
chartConfig: hooks.markRaw(chartConfig)
}
const chartConfig = createConfig()
Object.assign(chartConfig, {
width: 0,
extraWidth: 100, //
height: 90,
gap: 60,
padding: 12,
prior: 'vertical',
align: 'left',
status: { 1: 'completed', 2: 'ongoing', 3: 'not-started', 4: 'fail' },
colors: { 1: '#00a874', 2: '#0067d1', 3: '#999', 4: '#eb171f' },
ongoingBackgroundColor: '#f3f8fe',
popoverPlacement: 'bottom',
renderOuter: (h, node) => {
return h(Node, { props: { node, config: chartConfig } })
},
watch: {
activeSize(newVal) {
this.chartConfig.nodeSize = newVal
this.$refs.chart && this.$refs.chart.refresh()
type: 'dot',
nodeWrapperSize,
showArrow: false,
nodeSize: 'medium' /* mini/small/medium */
})
export default {
components: {
TinyAsyncFlowchart,
TinyButtonGroup
},
data() {
return {
chartConfig,
groupData: [
{ text: 'medium默认', value: 'medium' },
{ text: 'small', value: 'small' },
{ text: 'mini', value: 'mini' }
]
}
},
methods: {
onClickNode(afterNode, e) {
// console.log(afterNode, e)
TinyModal.message('click-node')
},
onClickLink(afterLink, e) {
// console.log(afterLink, e)
TinyModal.message('click-link')
},
onClickBlank(param, e) {
// console.log(param, e)
TinyModal.message('click-blank')
},
fetchFunc() {
const self = this
return new Promise(function (resolve) {
setTimeout(function () {
self.chartConfig.nodeSize = self.activeSize
resolve({ data: self.chartData, config: self.chartConfig })
return new Promise((resolve) => {
setTimeout(() => {
resolve({ data: chartData, config: chartConfig })
}, 300)
})
},
changeNodeSize() {
this.$refs.chart.refresh()
}
}
}

View File

@ -3,92 +3,92 @@ export default {
owner: '',
demos: [
{
'demoId': 'basic-usage',
'name': { 'zh-CN': '基本用法', 'en-US': 'Basic Usage' },
'desc': {
'zh-CN':
'Flowchart 流程图最基础的用法,展示节点与连线的基本渲染。通过 <code>createConfig</code> 创建配置与数据,并使用 <code>hooks.markRaw</code> 避免 Vue 对 Canvas 对象进行响应式代理。',
'en-US':
'The most basic usage of Flowchart, demonstrating the basic rendering of nodes and links. Use <code>createConfig</code> to create configuration and data, and use <code>hooks.markRaw</code> to prevent Vue from making Canvas objects reactive.'
demoId: 'basic-usage',
name: {
'zh-CN': '垂直图形',
'en-US': 'Vertical Graphics'
},
'codeFiles': ['basic-usage.vue']
desc: {
'zh-CN': '<p>节点设置在不同的列data.nodes[i].info.row,就可以创建垂直图形</p>',
'en-US':
'<p>Nodes are set in different columns data.nodes[i].info.row, and vertical graphics can be created.</p>'
},
codeFiles: ['basic-usage.vue']
},
{
'demoId': 'horizon',
'name': { 'zh-CN': '横向流程图', 'en-US': 'Horizontal Flowchart' },
'desc': {
'zh-CN':
'标准模式下的横向流程图示例。节点使用矩形形状,通过 <code>fromJoint</code> 和 <code>toJoint</code> 控制连线在节点的连接位置,支持 <code>click-node</code>、<code>click-link</code>、<code>click-blank</code> 事件。',
'en-US':
'A horizontal flowchart example in standard mode. Nodes use rectangular shapes, and the connection positions of links on nodes are controlled via <code>fromJoint</code> and <code>toJoint</code>. Supports <code>click-node</code>, <code>click-link</code>, and <code>click-blank</code> events.'
demoId: 'horizon',
name: {
'zh-CN': '水平图形',
'en-US': 'Horizontal Graph'
},
'codeFiles': ['horizon.vue']
desc: {
'zh-CN': '<p>节点设置在不同的列data.nodes[i].info.col,就可以创建水平图形</p>',
'en-US':
'<p>Nodes are set in different columns data.nodes[i].info.col, and horizontal graphics can be created.</p>'
},
codeFiles: ['horizon.vue']
},
{
'demoId': 'dot-horizon',
'name': { 'zh-CN': 'Dot 点模式横向', 'en-US': 'Dot Mode Horizontal' },
'desc': {
'zh-CN':
'Dot 点模式下的横向流程图。设置 <code>type: "dot"</code> 开启点模式,节点以圆形图标展示,支持 <code>renderOuter</code> 自定义节点渲染,使用 <code>resizeMixin</code> 实现自适应宽度。',
'en-US':
'Horizontal flowchart in dot mode. Set <code>type: "dot"</code> to enable dot mode, where nodes are displayed as circular icons. Supports <code>renderOuter</code> for custom node rendering and uses <code>resizeMixin</code> for adaptive width.'
demoId: 'dot-vertical',
name: {
'zh-CN': '点模式-垂直图形',
'en-US': 'Point Mode - Vertical Graph'
},
'codeFiles': ['dot-horizon.vue']
desc: {
'zh-CN': '<p></p>',
'en-US': '<p></p>'
},
codeFiles: ['dot-vertical.vue']
},
{
'demoId': 'dot-vertical',
'name': { 'zh-CN': 'Dot 点模式纵向', 'en-US': 'Dot Mode Vertical' },
'desc': {
'zh-CN':
'Dot 点模式下的纵向流程图。设置 <code>nodeLayout: "left-right"</code> 使标签显示在节点右侧,支持 <code>linkPath</code> 自定义连线路径、<code>groups</code> 分组以及 <code>click-group</code> 事件。',
'en-US':
'Vertical flowchart in dot mode. Set <code>nodeLayout: "left-right"</code> to display labels on the right side of nodes. Supports <code>linkPath</code> for custom link paths, <code>groups</code> for grouping, and <code>click-group</code> events.'
demoId: 'dot-horizon',
name: {
'zh-CN': '点模式-水平图形',
'en-US': 'Point Mode - Horizontal Graph'
},
'codeFiles': ['dot-vertical.vue']
desc: {
'zh-CN': '<p></p>',
'en-US': '<p></p>'
},
codeFiles: ['dot-horizon.vue']
},
{
'demoId': 'dot-horizon-async',
'name': { 'zh-CN': 'Dot 点模式横向异步', 'en-US': 'Dot Mode Horizontal Async' },
'desc': {
'zh-CN':
'使用 <code>TinyAsyncFlowchart</code> 组件实现异步加载的横向 Dot 点模式流程图。通过 <code>fetch</code> 属性传入数据加载函数,模拟异步请求后渲染图形。',
'en-US':
'Use the <code>TinyAsyncFlowchart</code> component to implement an asynchronously loaded horizontal dot mode flowchart. Data is loaded via the <code>fetch</code> prop, simulating an async request before rendering the chart.'
demoId: 'dot-horizon-async',
name: {
'zh-CN': '点模式-水平图形-异步',
'en-US': 'Point Mode - Horizontal Graph - Asynchronous'
},
'codeFiles': ['dot-horizon-async.vue']
desc: {
'zh-CN': '<p></p>',
'en-US': '<p></p>'
},
codeFiles: ['dot-horizon-async.vue']
},
{
'demoId': 'dot-vertical-async',
'name': { 'zh-CN': 'Dot 点模式纵向异步', 'en-US': 'Dot Mode Vertical Async' },
'desc': {
'zh-CN':
'使用 <code>TinyAsyncFlowchart</code> 组件实现异步加载的纵向 Dot 点模式流程图。支持分组、自定义路径和条件连线,展示异步数据加载后的完整交互。',
'en-US':
'Use the <code>TinyAsyncFlowchart</code> component to implement an asynchronously loaded vertical dot mode flowchart. Supports grouping, custom paths, and conditional links, demonstrating full interaction after async data loading.'
demoId: 'dot-vertical-async',
name: {
'zh-CN': '点模式-垂直图形-异步',
'en-US': 'events'
},
'codeFiles': ['dot-vertical-async.vue']
desc: {
'zh-CN': '<p><p>',
'en-US': '<p>Point Mode - Vertical Graph - Asynchronous</p>'
},
codeFiles: ['dot-vertical-async.vue']
},
{
'demoId': 'holistic',
'name': { 'zh-CN': '整体自定义渲染', 'en-US': 'Holistic Custom Rendering' },
'desc': {
'zh-CN':
'通过 <code>renderOuter</code> 完全自定义节点外观,通过 <code>linkPath</code> 自定义连线路径和渐变颜色。展示如何对流程图进行整体视觉定制。',
'en-US':
'Completely customize the node appearance via <code>renderOuter</code> and custom link paths and gradient colors via <code>linkPath</code>. Demonstrates how to perform overall visual customization of the flowchart.'
demoId: 'node-size',
name: {
'zh-CN': '节点尺寸',
'en-US': 'Node Size'
},
'codeFiles': ['holistic.vue']
},
{
'demoId': 'holistic-fork',
'name': { 'zh-CN': '整体自定义渲染-分叉', 'en-US': 'Holistic Custom Rendering - Fork' },
'desc': {
desc: {
'zh-CN':
'在整体自定义渲染的基础上,展示分叉布局流程图。通过 <code>layout</code> 自定义节点位置,配合 <code>linkPath</code> 实现复杂的分叉连线路径。',
"<p>使用属性 <code>config.nodeSize</code> 可以设置节点尺寸,可选值为 <code>'mini'</code> , <code>'small'</code> , <code>'medium'</code>。<p>",
'en-US':
'Based on holistic custom rendering, demonstrates a fork layout flowchart. Uses <code>layout</code> to customize node positions and <code>linkPath</code> to implement complex forked link paths.'
"<p>Using the attribute <code>config.nodeSize</code>, you can set the node size to <code>'mini'</code>, <code>'small'</code>, or <code>'medium'</code>.</p>"
},
'codeFiles': ['holistic-fork.vue']
codeFiles: ['node-size.vue']
},
{
demoId: 'link-path',
@ -103,15 +103,28 @@ export default {
codeFiles: ['link-path.vue']
},
{
'demoId': 'node-size',
'name': { 'zh-CN': '节点尺寸', 'en-US': 'Node Size' },
'desc': {
'zh-CN':
'通过 <code>nodeSize</code> 配置切换节点尺寸mini / small / medium配合 <code>TinyButtonGroup</code> 实现动态切换,并调用 <code>refresh()</code> 方法刷新图形。',
'en-US':
'Switch node sizes (mini / small / medium) via the <code>nodeSize</code> configuration, combined with <code>TinyButtonGroup</code> for dynamic switching, and call the <code>refresh()</code> method to refresh the chart.'
demoId: 'holistic',
name: {
'zh-CN': '全景图',
'en-US': 'Panoramic view'
},
'codeFiles': ['node-size.vue']
desc: {
'zh-CN': '<p></p>',
'en-US': '<p></p>'
},
codeFiles: ['holistic.vue']
},
{
demoId: 'holistic-fork',
name: {
'zh-CN': '全景图-分叉',
'en-US': 'Panorama - Fork'
},
desc: {
'zh-CN': '<p></p>',
'en-US': '<p></p>'
},
codeFiles: ['holistic-fork.vue']
}
]
}

View File

@ -1,38 +0,0 @@
<template>
<div>
<tiny-fluent-editor v-model="content" :before-link-open="handleBeforeLinkOpen"></tiny-fluent-editor>
<div class="fluent-editor-demo__before-link-open__tip">
点击编辑器中的链接会弹出确认框点击确定将打开链接点击取消将拦截跳转
</div>
</div>
</template>
<script>
import TinyFluentEditor from '@opentiny/vue-fluent-editor'
import { TinyModal } from '@opentiny/vue'
export default {
components: {
TinyFluentEditor
},
data() {
return {
content:
'{"ops":[{"insert":"点击访问 "},{"attributes":{"link":"https://opentiny.design"},"insert":"OpenTiny 官网"},{"insert":" 了解更多。"}]}'
}
},
methods: {
handleBeforeLinkOpen({ url }) {
return TinyModal.confirm(`即将打开链接:${url},是否允许跳转?`).then((res) => res === 'confirm')
}
}
}
</script>
<style scoped>
.fluent-editor-demo__before-link-open__tip {
margin: 16px 0;
color: #999;
font-size: 14px;
}
</style>

View File

@ -46,8 +46,7 @@ export default {
'en-US': ''
},
desc: {
'zh-CN':
'通过 <code>options</code> 设置编辑器的配置项,支持的配置项和 Quill 的相同,可参考 <a href="https://quilljs.com/docs/configuration#options" target="_blank">Quill</a> 文档。',
'zh-CN': '通过 <code>options</code> 设置编辑器的配置项,支持的配置项和 Quill 的相同,可参考 <a href="https://quilljs.com/docs/configuration#options" target="_blank">Quill</a> 文档。',
'en-US': ''
},
codeFiles: ['options.vue']
@ -65,18 +64,5 @@ export default {
},
codeFiles: ['data-switch.vue']
},
{
demoId: 'before-link-open',
name: {
'zh-CN': '超链接跳转拦截',
'en-US': ''
},
desc: {
'zh-CN':
'<p>通过 <code>before-link-open</code> 拦截富文本中超链接的跳转。该属性接收一个回调函数,返回 <code>false</code>(或 Promise resolve false可拦截跳转返回 <code>true</code> 继续跳转。</p>',
'en-US': ''
},
codeFiles: ['before-link-open.vue']
}
]
}

View File

@ -1,110 +1,70 @@
<template>
<div>
<h2>标签式 + 函数调用</h2>
<div class="content">
<tiny-modal
v-model="mainVisible"
:width="600"
:height="300"
:before-close="handleBeforeClose1"
title="Num.1标签式加函数弹窗"
message="NO.1标签式加函数内容"
show-footer
>
</tiny-modal>
<tiny-button @click="mainVisible = true"> 标签式 + 函数式弹窗</tiny-button>
</div>
<h2>点击确认按钮 + 拦截弹窗</h2>
<div class="content">
<tiny-modal
v-model="mainVisible1"
:width="600"
:height="300"
:before-close="handleBeforeClose1"
title="Num.1确认按钮加拦截弹窗"
message="点击确认按钮,出现拦截弹窗"
show-footer
>
<template #footer>
<tiny-button type="primary" @click="onCancelClose1">取消</tiny-button>
<tiny-button style="margin-left: 12px" @click="onConfirmClose1">确定</tiny-button>
</template>
</tiny-modal>
<tiny-button @click="mainVisible1 = true"> 其他</tiny-button>
</div>
<div class="tiny-demo">
<tiny-radio v-model="value" label="alert" text="alert"></tiny-radio>
<tiny-radio v-model="value" label="confirm" text="confirm"></tiny-radio>
<tiny-radio v-model="value" label="message" text="message"></tiny-radio>
<div style="height: 16px"></div>
<tiny-button @click="handleClick">点击打开 Modal 弹窗</tiny-button>
</div>
</template>
<script lang="jsx">
import { Button, Modal } from '@opentiny/vue'
import { TinyRadio, TinyModal, TinyButton } from '@opentiny/vue'
export default {
components: {
TinyButton: Button,
TinyModal: Modal
TinyRadio,
TinyButton
},
data() {
return {
mainVisible: false,
mainVisible1: false,
confirmVisible: false,
pendingDone: null
value: 'alert'
}
},
methods: {
handleBeforeClose(type, instance, done) {
this.confirmVisible = true
this.pendingDone = done
return false //
},
beforeClose(type) {
if (this.value === 'alert') {
/* alert close,confirm,esc,mask
这里允许 confirm 关闭 */
return !~['close', 'esc', 'mask'].indexOf(type)
}
onConfirmClose() {
this.confirmVisible = false
this.pendingDone && this.pendingDone() // done()
this.pendingDone = null
},
if (this.value === 'confirm') {
/* confirm close,confirm,cancel,esc,mask
这里允许 confirm cancel 关闭 */
return !~['close', 'esc', 'mask'].indexOf(type)
}
// / ->
onCancelClose() {
this.confirmVisible = false
this.pendingDone = null // done()
if (this.value === 'message') {
/* message show
这里允许 show 关闭 */
return type === 'show'
}
},
handleClick() {
let method
handleBeforeClose1(type, instance, done) {
Modal.confirm({
title: '关闭前确认Num.2',
message: '确认弹窗关闭Num.1',
events: {
confirm: () => done && done(), // ->
cancel: () => {} // ->
}
switch (this.value) {
case 'message':
method = TinyModal.message
break
case 'confirm':
method = TinyModal.confirm
break
case 'alert':
method = TinyModal.alert
break
}
method({
status: 'info',
title: '普通提示框',
escClosable: true,
maskClosable: true,
beforeClose: this.beforeClose,
message: (h) => [<div>文本信息文本信息文本信息</div>]
})
return false //
},
//
onConfirmClose1() {
// beforeClose
this.handleBeforeClose1('confirm', null, () => {
this.mainVisible1 = false
})
},
onCancelClose1() {
this.mainVisible1 = false
}
}
}
</script>
<style scoped>
h2 {
font-size: 16px;
font-weight: bold;
margin: 20px 0 12px;
}
.content {
margin: 8px;
}
</style>

View File

@ -325,8 +325,8 @@ export default {
},
desc: {
'zh-CN':
'<p>通过 `before-close` 属性设置关闭前的回调函数仅点击关闭按钮或遮罩区域时被调用。函数入参有type弹窗类型、instance弹窗实例、done回调函数。</p>',
'en-US': ''
'<p>通过 `before-close` 属性可以配置一个拦截弹窗关闭的方法。如果方法返回 false 值,则拦截弹窗关闭;否则不拦截<br> 可以通过该拦截方法传入的参数获取关闭的操作类型<br>confirm 弹窗有以下关闭类型:<br> - confirm点击确认时关闭<br>- cancel点击取消时关闭<br> - close点击关闭按钮时关闭<br>- mask: 点击遮罩时关闭<br>- esc通过按钮 esc 时关闭<br>alert 弹窗比 confirm 弹窗少了 `confirm` 类型<br> message 弹窗只有 `show` 一种关闭类型<p>',
'en-US': '<p>bbutton click</p>'
},
codeFiles: ['before-close.vue']
}

View File

@ -1,41 +0,0 @@
<template>
<div class="switch-demo">
<div class="demo-item">
<p>开关1只显示文字</p>
<br />
<tiny-switch v-model="val1" display-only></tiny-switch>
<br />
<br />
<p>开关2可交互设置val1的值</p>
<br />
<tiny-switch v-model="val1"></tiny-switch>
</div>
</div>
</template>
<script>
import { TinySwitch } from '@opentiny/vue'
export default {
components: {
TinySwitch
},
data() {
return {
val1: true
}
}
}
</script>
<style scoped>
.demo-item {
padding: 15px;
}
.demo-title {
font-size: 14px;
font-weight: 500;
margin-bottom: 10px;
}
</style>

View File

@ -99,20 +99,6 @@ export default {
'en-US': '<p>bbutton click</p>'
},
codeFiles: ['custom-true-false-value.vue']
},
{
demoId: 'display-only',
name: {
'zh-CN': '只读状态',
'en-US': 'Display Only'
},
desc: {
'zh-CN':
'<p>`<code>display-only</code>属性表示开关为只读状态,默认值为 <code>false</code>。当设置 <code>display-only</code> 为 <code>true</code> 时,开关为只读状态,无法进行交互操作。</p>',
'en-US':
'<p><code>display-only</code> property indicates that the switch is in read-only mode, with a default value of <code>false</code>. When set to <code>true</code>, the switch is in read-only mode and cannot be interacted with.</p>'
},
codeFiles: ['display-only.vue']
}
]
}

View File

@ -1,24 +0,0 @@
<template>
<div class="tiny-tag-demo">
<tiny-tag type="success" effect="light" round> 圆角按钮1 </tiny-tag>
<tiny-tag type="danger" effect="dark" round> 圆角按钮2 </tiny-tag>
<tiny-tag type="warning" effect="plain" round> 圆角按钮3 </tiny-tag>
</div>
</template>
<script lang="jsx">
import { TinyTag } from '@opentiny/vue'
export default {
components: {
TinyTag
}
}
</script>
<style scoped>
.tiny-tag-demo .tiny-tag {
margin-right: 10px;
margin-bottom: 10px;
}
</style>

View File

@ -151,18 +151,6 @@ export default {
'en-US': '<p>Set custom content by binding the `value` property</p>'
},
codeFiles: ['content.vue']
},
{
demoId: 'rounded',
name: {
'zh-CN': '设置圆角按钮',
'en-US': 'Set rounded corner button'
},
desc: {
'zh-CN': '<p>通过设置<code>round</code>属性设置圆角按钮</p>',
'en-US': '<p>Set the round button by setting the<code>round</code>attribute</p>'
},
codeFiles: ['rounded.vue']
}
]
}

View File

@ -4,14 +4,16 @@ test('文字居中', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('alert#center')
// 用文本"文字居中"精确过滤,限定在当前 demo
const alert = page.locator('.tiny-alert').filter({ hasText: '文字居中' })
// 等待网络空闲,确保页面完全加载
await page.waitForLoadState('networkidle')
// 验证基础类
const alert = page.locator('.tiny-alert')
// 首先验证基础类存在
await expect(alert).toHaveClass(/tiny-alert--info/)
await expect(alert).toHaveClass(/tiny-alert--normal/)
// 验证居中类
// 然后验证居中类
await expect(alert).toHaveClass(/is-center/)
await expect(alert).toHaveCSS('justify-content', 'center')
})

View File

@ -1,7 +1,5 @@
<template>
<div>
<tiny-alert size="small" title="size 为 small" description="size 为 small"></tiny-alert>
<tiny-alert size="medium" title="size 为 medium" description="size 为 medium"></tiny-alert>
<tiny-alert size="normal" description="size 为 normal"></tiny-alert>
<tiny-alert size="large" title="size 为 large" description="size 为 large"></tiny-alert>
</div>

View File

@ -4,37 +4,6 @@ test('尺寸', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('alert#size')
// 用文本过滤,精确锁定 size demo 里的 4 个 alert
const smallAlert = page.locator('.tiny-alert').filter({ hasText: 'size 为 small' })
const mediumAlert = page.locator('.tiny-alert').filter({ hasText: 'size 为 medium' })
const normalAlert = page.locator('.tiny-alert').filter({ hasText: 'size 为 normal' })
const largeAlert = page.locator('.tiny-alert').filter({ hasText: 'size 为 large' })
// 1. 验证各尺寸类名
await expect(smallAlert).toHaveClass(/tiny-alert--small/)
await expect(mediumAlert).toHaveClass(/tiny-alert--medium/)
await expect(normalAlert).toHaveClass(/tiny-alert--normal/)
const largeAlert = page.locator('.tiny-alert').nth(1)
await expect(largeAlert).toHaveClass(/tiny-alert--large/)
// 2. 验证 small / medium 的 padding
await expect(smallAlert).toHaveCSS('padding-top', '2px')
await expect(smallAlert).toHaveCSS('padding-bottom', '2px')
await expect(mediumAlert).toHaveCSS('padding-top', '4px')
await expect(mediumAlert).toHaveCSS('padding-bottom', '4px')
// 3. 验证 small / medium / normal 不显示 titlelarge 显示 title
await expect(smallAlert.locator('.tiny-alert__title')).toHaveCount(0)
await expect(mediumAlert.locator('.tiny-alert__title')).toHaveCount(0)
await expect(normalAlert.locator('.tiny-alert__title')).toHaveCount(0)
await expect(largeAlert.locator('.tiny-alert__title')).toHaveCount(1)
// 4. 验证 small / medium 的 close 按钮样式
const smallClose = smallAlert.locator('.tiny-alert__close')
const mediumClose = mediumAlert.locator('.tiny-alert__close')
await expect(smallClose).toHaveCSS('top', '4px')
await expect(smallClose).toHaveCSS('transform', 'none')
await expect(smallClose).toHaveCSS('margin-top', '1px')
await expect(mediumClose).toHaveCSS('top', '6px')
await expect(mediumClose).toHaveCSS('transform', 'none')
await expect(mediumClose).toHaveCSS('margin-top', '1px')
})

View File

@ -1,7 +1,5 @@
<template>
<div>
<tiny-alert size="small" title="size 为 small" description="size 为 small"></tiny-alert>
<tiny-alert size="medium" title="size 为 medium" description="size 为 medium"></tiny-alert>
<tiny-alert size="normal" description="size 为 normal"></tiny-alert>
<tiny-alert size="large" title="size 为 large" description="size 为 large"></tiny-alert>
</div>
@ -9,7 +7,6 @@
<script>
import { TinyAlert } from '@opentiny/vue'
export default {
components: {
TinyAlert

View File

@ -4,28 +4,8 @@ test('测试 Alert 自定义交互操作', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('alert#slot-default')
// 第1个slot 自定义内容(有 opration但里面是 span 文本)
const alert1 = page.locator('.tiny-alert').filter({ hasText: 'slot 自定义内容' })
await expect(alert1.locator('.tiny-alert__opration')).toHaveCount(1)
await expect(alert1.locator('.tiny-alert__opration')).toHaveText('自定义内容')
// 第2个slot 自定义交互操作(有 opration里面是链接
const alert2 = page.locator('.tiny-alert').filter({ hasText: 'slot 自定义交互操作' })
const opration = alert2.locator('.tiny-alert__opration')
// size 为 large 时可通过插槽自定义操作
const alert = page.locator('.tiny-alert--large').first()
const opration = alert.locator('.tiny-alert__opration')
await expect(opration).toHaveCount(1)
await expect(opration.locator('a')).toHaveCount(2)
await expect(opration.locator('a').nth(0)).toHaveText('确定')
await expect(opration.locator('a').nth(1)).toHaveText('取消')
// 第3个成功有 opration + 描述)
const alert3 = page.locator('.tiny-alert').filter({ hasText: '成功' })
await expect(alert3.locator('.tiny-alert__opration')).toHaveCount(1)
// 第4个错误有 opration 但为空)
const alert4 = page.locator('.tiny-alert').filter({ hasText: '错误' })
await expect(alert4.locator('.tiny-alert__opration')).toHaveCount(1)
// 第5个警告有 opration
const alert5 = page.locator('.tiny-alert').filter({ hasText: '警告' })
await expect(alert5.locator('.tiny-alert__opration')).toHaveCount(1)
})

View File

@ -4,15 +4,12 @@ test('测试 Alert 自定义标题', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('alert#title')
const alert = page.locator('.tiny-alert').filter({ hasText: '通过属性设置自定义 title' })
// size 为 large 时可设置自定义标题
const alert = page.locator('.tiny-alert--large').first()
const title = alert.locator('.tiny-alert__title')
await expect(title).toHaveCount(1)
await expect(title).toHaveText('通过属性设置自定义 title')
const alert1 = page
.locator('.tiny-alert')
.filter({ hasText: /^描述内容$/ })
.first()
const alert1 = page.locator('.tiny-alert--large').nth(2)
await expect(alert1).not.toHaveText('消息')
})

View File

@ -26,22 +26,22 @@ export default {
},
desc: {
'zh-CN': `
通过 <code>size</code> <code>small</code> <code>medium</code> <code>normal</code> <code>large</code> <br>
通过 <code>size</code> <code>normal</code> <code>large</code> <br>
<div class="tip custom-block">
<p class="custom-block-title"> 尺寸模式区别 </p>
<ul>
<li> <code>small</code> <code>medium</code> <code>normal</code> </li>
<li> <code>large</code> </li>
<li> normal 模式下不会显示标题和交互操作的区域相当于简单模式</li>
<li> large 模式下显示全部元素相当于完整模式</li>
</ul>
</div>
`,
'en-US': `
Use <code>size</code> to set different size modes, optional values: <code>small</code>, <code>medium</code>, <code>normal</code>, <code>large</code>. <br>
Use <code>size</code> to set different size modes, optional values: <code>normal</code>, <code>large</code>. <br>
<div class="tip custom-block">
<p class="custom-block-title"> Size pattern difference </p>
<ul>
<li> <code>small</code> <code>medium</code> <code>normal</code> mode, the header and interactive areas are not displayed, which is equivalent to simple mode. </li>
<li> <code>large</code> mode, all elements are displayed, equivalent to the full mode. </li>
<li> normal mode, the header and interactive areas are not displayed, which is equivalent to simple mode. </li>
<li> large mode, all elements are displayed, equivalent to the full mode. </li>
</ul>
</div>
`

View File

@ -41,7 +41,7 @@
<div>场景 5自定义图标 + 自定义样式</div>
<br />
<tiny-base-select
v-model="value5"
v-model="value4"
multiple
:dropdown-icon="tinyIconPopup"
:drop-style="{ width: '200px', 'min-width': '200px' }"
@ -81,7 +81,6 @@ const value1 = ref(['选项 1', '选项 2'])
const value2 = ref(['选项 1', '选项 2'])
const value3 = ref([])
const value4 = ref([])
const value5 = ref([])
const tinyIconPopup = iconPopup()
</script>

View File

@ -41,7 +41,7 @@
<div>场景 5自定义图标 + 自定义样式</div>
<br />
<tiny-base-select
v-model="value5"
v-model="value4"
multiple
:dropdown-icon="iconPopup"
:drop-style="{ width: '200px', 'min-width': '200px' }"
@ -85,7 +85,6 @@ export default {
value2: ['选项 1', '选项 2'],
value3: ['选项 1', '选项 2'],
value4: [],
value5: [],
iconPopup: iconPopup()
}
}

View File

@ -1,34 +0,0 @@
<template>
<tiny-collapse class="demo-collapse-wrap" v-model="activeNames" size="medium">
<tiny-collapse-item title="一致性 Consistency" name="1">
<div>与现实生活一致与现实生活的流程逻辑保持一致遵循用户习惯的语言和概念</div>
<div>在界面中一致所有的元素和结构需保持一致比如设计样式图标和文本元素的位置等</div>
</tiny-collapse-item>
<tiny-collapse-item title="反馈 Feedback" name="2">
<div>控制反馈通过界面样式和交互动效让用户可以清晰的感知自己的操作</div>
<div>页面反馈操作后通过页面元素的变化清晰地展现当前状态</div>
</tiny-collapse-item>
<tiny-collapse-item title="效率 Efficiency" name="3">
<div>简化流程设计简洁直观的操作流程</div>
<div>清晰明确语言表达清晰且表意明确让用户快速理解进而作出决策</div>
<div>帮助用户识别界面简单直白让用户快速识别而非回忆减少用户记忆负担</div>
</tiny-collapse-item>
<tiny-collapse-item title="可控 Controllability" name="4">
<div>用户决策根据场景可给予用户操作建议或安全提示但不能代替用户进行决策</div>
<div>结果可控用户可以自由的进行操作包括撤销回退和终止当前操作等</div>
</tiny-collapse-item>
</tiny-collapse>
</template>
<script setup>
import { ref } from 'vue'
import { TinyCollapse, TinyCollapseItem } from '@opentiny/vue'
const activeNames = ref(['1', '3'])
</script>
<style scoped lang="less">
.demo-collapse-wrap ::v-deep .tiny-collapse-item__content > * {
line-height: 1.6;
}
</style>

View File

@ -1,43 +0,0 @@
<template>
<tiny-collapse class="demo-collapse-wrap" v-model="activeNames" size="medium">
<tiny-collapse-item title="一致性 Consistency" name="1">
<div>与现实生活一致与现实生活的流程逻辑保持一致遵循用户习惯的语言和概念</div>
<div>在界面中一致所有的元素和结构需保持一致比如设计样式图标和文本元素的位置等</div>
</tiny-collapse-item>
<tiny-collapse-item title="反馈 Feedback" name="2">
<div>控制反馈通过界面样式和交互动效让用户可以清晰的感知自己的操作</div>
<div>页面反馈操作后通过页面元素的变化清晰地展现当前状态</div>
</tiny-collapse-item>
<tiny-collapse-item title="效率 Efficiency" name="3">
<div>简化流程设计简洁直观的操作流程</div>
<div>清晰明确语言表达清晰且表意明确让用户快速理解进而作出决策</div>
<div>帮助用户识别界面简单直白让用户快速识别而非回忆减少用户记忆负担</div>
</tiny-collapse-item>
<tiny-collapse-item title="可控 Controllability" name="4">
<div>用户决策根据场景可给予用户操作建议或安全提示但不能代替用户进行决策</div>
<div>结果可控用户可以自由的进行操作包括撤销回退和终止当前操作等</div>
</tiny-collapse-item>
</tiny-collapse>
</template>
<script>
import { TinyCollapse, TinyCollapseItem } from '@opentiny/vue'
export default {
components: {
TinyCollapse,
TinyCollapseItem
},
data() {
return {
activeNames: ['1', '3']
}
}
}
</script>
<style scoped lang="less">
.demo-collapse-wrap ::v-deep .tiny-collapse-item__content > * {
line-height: 1.6;
}
</style>

View File

@ -46,19 +46,6 @@ export default {
},
codeFiles: ['disable.vue']
},
{
demoId: 'size',
name: {
'zh-CN': '面板大小',
'en-US': 'Custom Panel Size'
},
desc: {
'zh-CN': '通过 <code>size</code> 属性可以指定折叠面板的尺寸,可选值为 "" | "medium"。',
'en-US':
'by <code>size</code> prop can be used to specify the size of the collapse panel. The optional values are "" | "medium".'
},
codeFiles: ['size.vue']
},
{
demoId: 'title',
name: {

View File

@ -10,25 +10,3 @@ test('基本用法', async ({ page }) => {
await page.locator('.tiny-color-select-panel__inner__color-select').click()
await page.getByRole('button', { name: '确定' }).click()
})
test('在 hex 输入框内拖选文本、鼠标移出面板后松开,面板不应误关闭', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('color-picker#basic-usage')
await page.locator('.tiny-color-picker__inner').click()
const panel = page.locator('.tiny-color-select-panel')
await expect(panel).toBeVisible()
const input = panel.locator('.tiny-color-select-panel__tools-hex1 input')
const box = await input.boundingBox()
expect(box).not.toBeNull()
// 模拟用户拖选 hex 输入框文本:在输入框内按下,拖出面板后松开
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2)
await page.mouse.down()
await page.mouse.move(box.x - 300, box.y + box.height / 2, { steps: 10 })
await page.mouse.up()
// mousedown 发生在面板内部,拖选不应被误判为「点击外部」而关闭面板
await expect(panel).toBeVisible()
})

View File

@ -3,8 +3,8 @@ import { test, expect } from '@playwright/test'
test('渐变', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('color-picker#color-mode')
await page.locator('.tiny-color-picker .tiny-color-picker__inner').click()
await page.locator('.tiny-color-picker > .tiny-color-picker__inner').click()
await page.getByRole('button', { name: '确定' }).click()
await expect(page.locator('body')).toContainText('linear-gradient(90deg, #66ADFF 21%,#00FEF1 100%)')
await page.locator('.tiny-notify__icon-close .tiny-svg').click()
await page.locator('.tiny-notify__icon-close > .tiny-svg').click()
})

View File

@ -31,7 +31,6 @@ const value = ref('')
<style scoped>
.demo-date-picker-wrap {
/* 输入框收窄到 240px比面板默认宽度 360px 窄,宽度差让 left/center/right 对齐效果清晰可见 */
width: 240px;
width: 360px;
}
</style>

View File

@ -39,7 +39,6 @@ export default {
<style scoped>
.demo-date-picker-wrap {
/* 输入框收窄到 240px比面板默认宽度 360px 窄,宽度差让 left/center/right 对齐效果清晰可见 */
width: 240px;
width: 360px;
}
</style>

View File

@ -11,8 +11,7 @@ test('dialogSelect 设置多选状态', async ({ page }) => {
const trs = await page.locator('.tiny-grid table tbody tr').all()
for (let i = 0; i < trs.length; i++) {
const classes = await trs[i].getAttribute('class')
// 第1行2行都选被中
if (i === 0 || i === 1) {
if (i === 1) {
expect(classes?.includes('row__selected')).toBeTruthy()
} else {
expect(classes?.includes('row__selected')).toBeFalsy()

View File

@ -1,23 +0,0 @@
<template>
<div>
<div>
<tiny-radio v-model="value" :label="true">关闭时销毁</tiny-radio>
<tiny-radio v-model="value" :label="false">关闭时不销毁</tiny-radio>
</div>
<br />
<tiny-button @click="boxVisibility = true" type="primary">点击打开抽屉</tiny-button>
<tiny-drawer title="标题" v-model:visible="boxVisibility" :destroy-on-close="value">
<div>内容区域</div>
</tiny-drawer>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { TinyButton, TinyDrawer, TinyRadio } from '@opentiny/vue'
const value = ref(true)
const boxVisibility = ref(false)
</script>

View File

@ -1,25 +0,0 @@
import { test, expect } from '@playwright/test'
test('关闭时销毁主体元素', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('drawer#destroy-on-close')
const demo = page.locator('#destroy-on-close')
const drawer = demo.locator('.tiny-drawer__main')
// 关闭时不销毁
await demo.locator('label').filter({ hasText: '关闭时不销毁' }).click()
await demo.getByRole('button', { name: '点击打开抽屉' }).click()
await expect(drawer).toBeVisible()
await page.click('.tiny-drawer__mask')
await expect(drawer).toBeHidden()
// 关闭时销毁
await demo.locator('label').filter({ hasText: '关闭时销毁' }).click()
await demo.getByRole('button', { name: '点击打开抽屉' }).click()
await expect(drawer).toBeVisible()
await page.click('.tiny-drawer__mask')
await expect(drawer).toHaveCount(0)
})

View File

@ -1,33 +0,0 @@
<template>
<div>
<div>
<tiny-radio v-model="value" :label="true">关闭时销毁</tiny-radio>
<tiny-radio v-model="value" :label="false">关闭时不销毁</tiny-radio>
</div>
<br />
<tiny-button @click="boxVisibility = true" type="primary">点击打开抽屉</tiny-button>
<tiny-drawer title="标题" v-model:visible="boxVisibility" :destroy-on-close="value">
<div>内容区域</div>
</tiny-drawer>
</div>
</template>
<script>
import { TinyButton, TinyDrawer, TinyRadio } from '@opentiny/vue'
export default {
components: {
TinyButton,
TinyDrawer,
TinyRadio
},
data() {
return {
value: true,
boxVisibility: false
}
}
}
</script>

View File

@ -1,30 +0,0 @@
<template>
<div>
<tiny-button @click="openDrawer" type="primary"> 抽屉组件AppendToBody </tiny-button>
<tiny-drawer
v-if="visible"
title="标题"
:visible="visible"
:append-to-body="true"
@update:visible="visible = $event"
@confirm="confirm"
>
<div>内容区域</div>
</tiny-drawer>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { TinyDrawer, TinyButton } from '@opentiny/vue'
const visible = ref(false)
function openDrawer() {
visible.value = true
}
function confirm() {
visible.value = false
}
</script>

View File

@ -1,23 +0,0 @@
import { test, expect } from '@playwright/test'
test.describe('挂载节点', () => {
test('挂载节点', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('drawer#drawer-to-body')
const demo = page.locator('#drawer-to-body')
await expect(page.locator('body > .tiny-drawer')).toHaveCount(0)
const openBtn = demo.getByRole('button', { name: /抽屉组件AppendToBody/ })
await openBtn.click()
const drawer = page.locator('body > .tiny-drawer')
// drawer 被挂载到 document.body 下
const isAppendToBody = await drawer.evaluate((el) => el.parentElement === document.body)
expect(isAppendToBody).toBe(true)
// demo 容器内部不应该包含 drawer 的根 DOM
await expect(demo.locator('.tiny-drawer')).toHaveCount(0)
})
})

View File

@ -1,39 +0,0 @@
<template>
<div>
<tiny-button @click="openDrawer" type="primary"> 抽屉组件AppendToBody </tiny-button>
<tiny-drawer
v-if="visible"
title="标题"
:visible="visible"
:append-to-body="true"
@update:visible="visible = $event"
@confirm="confirm"
>
<div>内容区域</div>
</tiny-drawer>
</div>
</template>
<script>
import { TinyDrawer, TinyButton } from '@opentiny/vue'
export default {
components: {
TinyDrawer,
TinyButton
},
data() {
return {
visible: false
}
},
methods: {
openDrawer() {
this.visible = true
},
confirm() {
this.visible = false
}
}
}
</script>

View File

@ -16,20 +16,6 @@ export default {
},
codeFiles: ['basic-usage.vue']
},
{
demoId: 'drawer-to-body',
name: {
'zh-CN': '挂载节点',
'en-US': 'Mount node'
},
desc: {
'zh-CN':
'<code>append-to-body</code> 属性可以将抽屉挂载到 body 元素上。默认值为 <code>false</code>,即挂载在当前组件内。',
'en-US':
'The <code>append-to-body</code> attribute can mount the drawer to the body element. The default value is <code>false</code>, meaning it is mounted within the current component.'
},
codeFiles: ['drawer-to-body.vue']
},
{
demoId: 'close-on-press-escape',
name: {
@ -146,20 +132,6 @@ export default {
},
codeFiles: ['mask-closable.vue']
},
{
demoId: 'destroy-on-close',
name: {
'zh-CN': '关闭时销毁主体元素',
'en-US': 'Destroy on Close'
},
desc: {
'zh-CN':
'<p>可通过<code>destroy-on-close</code>属性设置<code>true</code>在关闭抽屉时销毁<code>drawer</code>抽屉内的所有元素,默认值为<code>false</code>。</p>',
'en-US':
'<p>By setting the<code>:destroy-on-close</code>attribute to<code>true</code>, all elements in the<code>drawer</code>drawer are destroyed when the pop-up window is closed, with the default value being<code>false</code>.</p>'
},
codeFiles: ['destroy-on-close.vue']
},
{
demoId: 'show-close',
name: {

View File

@ -1,25 +1,78 @@
<template>
<div>
<tiny-flowchart :data="data" :config="config" />
<div class="tiny-demo">
<tiny-flowchart
ref="chart"
:data="chartDataRaw"
:config="chartConfigRaw"
@click-node="onClickNode"
@click-link="onClickLink"
@click-blank="onClickBlank"
>
</tiny-flowchart>
</div>
</template>
<script setup>
import { TinyFlowchart } from '@opentiny/vue'
import { reactive } from 'vue'
import { TinyModal, TinyFlowchart } from '@opentiny/vue'
import { hooks } from '@opentiny/vue-common'
const { createNode, createLink, createConfig } = TinyFlowchart
const config = createConfig()
config.width = 960
config.height = 260
const data = reactive({
const chartData = {
nodes: [
createNode('1', 1, '提交申请', '2024-01-01', null, 1, 1),
createNode('2', 2, '主管审批', '2024-01-02', null, 1, 3),
createNode('3', 3, '流程结束', '', null, 1, 5)
createNode('1', 1, '基础信息', '2018.08.02', [], 1, 0),
createNode('2', 1, '调职补偿', '2018.08.02', [], 0, 2),
createNode('3', 1, '汇总调职补偿', '', [], 1, 4),
createNode('4', 3, '启动精算', '', [], 4, 5),
createNode('5', 3, '复核精算', '', [], 4, 6),
createNode('6', 3, '审核精算', '', [], 4, 7),
createNode('7', 1, '调职补偿', '2018.08.02', [], 2, 1),
createNode('8', 1, '复核', '2018.08.02', [], 2, 2),
createNode('9', 2, '审批', '2018.08.02', [], 2, 3),
createNode('10', 1, '复核', '2018.08.02', [], 4, 2),
createNode('11', 2, '审批', '2018.08.02', [], 4, 3),
createNode('12', 3, '运算调职兑现率', '', [], 4, 4),
createNode('13', 1, '复核', '2018.08.02', [], 6, 2),
createNode('14', 4, '审批审批审批审批审批 0123456789asdfghjkl', '2018.08.02', [], 6, 3)
],
links: [createLink('1', '2', '0 r2', 2, 'solid'), createLink('2', '3', '0 r2', 3, 'solid')]
})
links: [
createLink('1', '2', '0 r0.5 t1 c r1.5', 1),
createLink('2', '3', '0 r1.5 c b1 r0.5', 3),
createLink('3', '4', '0 r0.5 c b3 r0.5', 3),
createLink('4', '5', '', 3),
createLink('5', '6', '', 3),
createLink('1', '7', 'r0.5 b1 c r0.5', 1),
createLink('7', '8', '', 1),
createLink('8', '9', '', 1),
createLink('9', '3', '0 r0.5 c t1', 3),
createLink('10', '11', '', 1),
createLink('11', '12', '', 3),
createLink('12', '4', '0 r0.5', 3),
createLink('13', '14', '', 1),
createLink('14', '4', '0 r1.5 c t2', 3, 'dash')
]
}
const chartConfig = createConfig()
chartConfig.headUrl = `${import.meta.env.VITE_APP_BUILD_BASE_URL}static/images/mountain.png`
chartConfig.checkItemStatus = (item) => ~['已转审', '已同意'].indexOf(item.status)
chartConfig.adjustPos = (afterNode) => afterNode.raw.name === '2' && (afterNode.y += 1)
// 使 markRaw Vue
const chartDataRaw = hooks.markRaw(chartData)
const chartConfigRaw = hooks.markRaw(chartConfig)
function onClickNode(_afterNode, _e) {
TinyModal.message('click-node')
}
function onClickLink(_afterLink, _e) {
TinyModal.message('click-link')
}
function onClickBlank(_param, _e) {
TinyModal.message('click-blank')
}
</script>

View File

@ -0,0 +1,17 @@
import { test, expect } from '@playwright/test'
test('基本用法', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('flowchart#basic-usage')
const preview = page.locator('#basic-usage')
const flowchart = preview.locator('.tiny-flow-chart')
const nodes = flowchart.locator('.tiny-flow-chart__node-icon-wrapper')
await expect(flowchart).toBeVisible()
await expect(flowchart).toHaveCSS('width', '1024px')
await expect(flowchart.locator('.tiny-flow-chart__canvas')).toBeVisible()
await expect(nodes).toHaveCount(14)
await expect(nodes.first()).toContainText('基础信息')
await expect(nodes.first()).toContainText('2018.08.02')
await expect(nodes.nth(1)).toContainText('调职补偿')
})

View File

@ -1,13 +1,70 @@
<template>
<div>
<tiny-flowchart :data="data" :config="config" />
<div class="tiny-demo">
<tiny-flowchart
ref="chart"
:data="chartData"
:config="chartConfig"
@click-node="onClickNode"
@click-link="onClickLink"
@click-blank="onClickBlank"
>
</tiny-flowchart>
</div>
</template>
<script>
import { TinyFlowchart } from '@opentiny/vue'
import { TinyModal, TinyFlowchart } from '@opentiny/vue'
import { hooks } from '@opentiny/vue-common'
const { createNode, createLink, createConfig } = TinyFlowchart
const { createNode, createLink, createItem, createConfig } = TinyFlowchart
const handlers = [
createItem('WX100001', '张三', '转审人', '已转审', '很好', '2018-08-20 12:00', ''),
createItem('WX100002', '李四', '主管', '已转审', '非常好', '2018-08-20 12:00', ''),
createItem('WX100003', '王五', '主管', '处理中', '', '', '')
]
const chartData = {
nodes: [
createNode('1', 1, '基础信息', '2018.08.02', [], 1, 0),
createNode('2', 1, '调职补偿', '2018.08.02', handlers, 0, 2),
createNode('3', 1, '汇总调职补偿', '', [], 1, 4),
createNode('4', 3, '启动精算', '', [], 4, 5),
createNode('5', 3, '复核精算', '', [], 4, 6),
createNode('6', 3, '审核精算', '', [], 4, 7),
createNode('7', 1, '调职补偿', '2018.08.02', [], 2, 1),
createNode('8', 1, '复核', '2018.08.02', [], 2, 2),
createNode('9', 2, '审批', '2018.08.02', [], 2, 3),
createNode('10', 1, '复核', '2018.08.02', [], 4, 2),
createNode('11', 2, '审批', '2018.08.02', [], 4, 3),
createNode('12', 3, '运算调职兑现率', '', [], 4, 4),
createNode('13', 1, '复核', '2018.08.02', [], 6, 2),
createNode('14', 4, '审批审批审批审批审批 0123456789asdfghjkl', '2018.08.02', [], 6, 3)
],
links: [
createLink('1', '2', '0 r0.5 t1 c r1.5', 1),
createLink('2', '3', '0 r1.5 c b1 r0.5', 3),
createLink('3', '4', '0 r0.5 c b3 r0.5', 3),
createLink('4', '5', '', 3),
createLink('5', '6', '', 3),
createLink('1', '7', 'r0.5 b1 c r0.5', 1),
createLink('7', '8', '', 1),
createLink('8', '9', '', 1),
createLink('9', '3', '0 r0.5 c t1', 3),
createLink('10', '11', '', 1),
createLink('11', '12', '', 3),
createLink('12', '4', '0 r0.5', 3),
createLink('13', '14', '', 1),
createLink('14', '4', '0 r1.5 c t2', 3, 'dash')
]
}
const chartConfig = createConfig()
chartConfig.headUrl = `${import.meta.env.VITE_APP_BUILD_BASE_URL}static/images/mountain.png`
chartConfig.checkItemStatus = (item) => ~['已转审', '已同意'].indexOf(item.status)
chartConfig.adjustPos = (afterNode) => afterNode.raw.name === '2' && (afterNode.y += 1)
export default {
components: {
@ -15,20 +72,20 @@ export default {
},
data() {
return {
config: createConfig(),
data: {
nodes: [
createNode('1', 1, '提交申请', '2024-01-01', null, 1, 1),
createNode('2', 2, '主管审批', '2024-01-02', null, 1, 3),
createNode('3', 3, '流程结束', '', null, 1, 5)
],
links: [createLink('1', '2', '0 r2', 2, 'solid'), createLink('2', '3', '0 r2', 3, 'solid')]
}
chartData: hooks.markRaw(chartData),
chartConfig: hooks.markRaw(chartConfig)
}
},
created() {
this.config.width = 960
this.config.height = 260
methods: {
onClickNode(_afterNode, _e) {
TinyModal.message('click-node')
},
onClickLink(_afterLink, _e) {
TinyModal.message('click-link')
},
onClickBlank(_param, _e) {
TinyModal.message('click-blank')
}
}
}
</script>

View File

@ -1,190 +0,0 @@
<template>
<div>
<p>示例1卡片式分支流程</p>
<tiny-flowchart :data="chartData1" :config="chartConfig1" @click-node="onClickNode1" @click-link="onClickLink1">
<template #icon="{ afterNode }">
<div class="icon-anchor">
<div v-if="afterNode.raw.name === '0'" class="node-circle node-start">开始</div>
<div v-else-if="afterNode.raw.name === '6'" class="node-circle node-end"></div>
<div
v-else
class="node-card"
:class="{
'card-blue': afterNode.raw.info.status === 2,
'card-red': afterNode.raw.info.status === 4,
'card-gray': afterNode.raw.info.status === 3
}"
>
<div class="card-title" :title="afterNode.raw.info.label">{{ afterNode.raw.info.label }}</div>
<div class="card-sub" :title="afterNode.raw.info.date">{{ afterNode.raw.info.date }}</div>
</div>
</div>
</template>
<template #label>
<span style="display: none"></span>
</template>
</tiny-flowchart>
<p>示例2标准网格路径流程</p>
<tiny-flowchart :data="chartData2" :config="chartConfig2" />
</div>
</template>
<script setup>
import { TinyFlowchart } from '@opentiny/vue'
import { hooks } from '@opentiny/vue-common'
const { createNode, createLink, createConfig } = TinyFlowchart
// ==================== 1 ====================
const config1 = createConfig()
config1.width = 860
config1.height = 480
config1.rows = 6
config1.cols = 8
config1.labelSpacing = 0
config1.labelHeight = 1
config1.styleLink = (ctx, afterLink) => {
const colors = { 1: '#52c41a', 2: '#1890ff', 3: '#bfbfbf', 4: '#ff4d4f' }
ctx.strokeStyle = colors[afterLink.raw.info.status] || '#999'
ctx.lineWidth = 2
if (afterLink.raw.info.style !== 'solid') {
ctx.setLineDash([4, 4])
}
}
const data1 = {
nodes: [
createNode('0', 1, '开始', '', null, 0, 3),
createNode('1', 1, '申请人', '张三', null, 1, 3),
createNode('2', 1, '制单会计', '协同:张三、张四、张五', null, 2, 3),
createNode('3', 2, '应付会计', '张四 0035837', null, 3, 1),
createNode('4', 4, '应付会计', '张四 0035837', null, 3, 5),
createNode('5', 3, '复核会计', '自定义内容-5', null, 4, 3),
createNode('6', 1, '结束', '', null, 5, 3)
],
links: [
createLink('0', '1', '', 1, 'solid'),
createLink('1', '2', '', 1, 'solid'),
createLink('2', '3', '0 b1 l2', 1, 'solid'),
createLink('2', '4', '0 b1 r2', 1, 'solid'),
createLink('3', '5', '0 b1 r2', 3, 'solid'),
createLink('4', '5', '0 b1 l2', 3, 'solid'),
createLink('5', '6', '', 3, 'dash')
]
}
const chartData1 = hooks.markRaw(data1)
const chartConfig1 = hooks.markRaw(config1)
const onClickNode1 = (afterNode) => {
console.log('示例1点击节点:', afterNode.raw.info.label)
}
const onClickLink1 = (afterLink) => {
console.log('示例1点击连线:', afterLink.raw.from, '→', afterLink.raw.to)
}
// ==================== 2 ====================
const config2 = createConfig()
config2.width = 960
config2.height = 470
config2.rows = 10
config2.cols = 10
const data2 = {
nodes: [
createNode('start', 1, '提交报销', '2024-06-01', null, 2, 2),
createNode('check', 2, '金额审核', '2024-06-02', null, 4, 2),
createNode('pass', 1, '直接通过', '2024-06-03', null, 6, 1),
createNode('extra', 2, '额外审批', '2024-06-03', null, 6, 3),
createNode('end', 1, '流程结束', '2024-06-04', null, 8, 2)
],
links: [
createLink('start', 'check', '0 b2', 1, 'solid'),
createLink('check', 'pass', '0 b1 c l1 c b1', 1, 'solid'),
createLink('check', 'extra', '0 b1 c r1 c b1', 2, 'dashed'),
createLink('pass', 'end', '0 b1 c r1 c b1', 1, 'solid'),
createLink('extra', 'end', '0 b1 c l1 c b1', 2, 'solid')
]
}
const chartData2 = hooks.markRaw(data2)
const chartConfig2 = hooks.markRaw(config2)
</script>
<style scoped>
/* 示例1样式 */
.icon-anchor {
position: relative;
width: 1px;
height: 1px;
}
.node-circle {
position: absolute;
left: -12px;
top: -24px;
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
}
.node-start {
border: 2px solid #52c41a;
background: #fff;
color: #333;
}
.node-end {
background: #52c41a;
color: #fff;
font-size: 22px;
font-weight: bold;
}
.node-card {
position: absolute;
left: -70px;
top: -28px;
width: 160px;
padding: 10px 12px;
border-radius: 6px;
border: 1px solid #d9d9d9;
background: #fff;
text-align: center;
box-sizing: border-box;
cursor: pointer;
}
.card-blue {
border-color: #1890ff;
background: #e6f7ff;
}
.card-red {
border-color: #ff4d4f;
background: #fff1f0;
}
.card-gray {
border-color: #bfbfbf;
background: #f5f5f5;
}
.card-title {
font-size: 14px;
font-weight: 500;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.5;
}
.card-sub {
font-size: 12px;
color: #666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.5;
margin-top: 4px;
}
</style>

View File

@ -1,196 +0,0 @@
<template>
<div>
<p>示例1卡片式分支流程</p>
<tiny-flowchart :data="chartData1" :config="chartConfig1" @click-node="onClickNode1" @click-link="onClickLink1">
<template #icon="{ afterNode }">
<div class="icon-anchor">
<div v-if="afterNode.raw.name === '0'" class="node-circle node-start">开始</div>
<div v-else-if="afterNode.raw.name === '6'" class="node-circle node-end"></div>
<div
v-else
class="node-card"
:class="{
'card-blue': afterNode.raw.info.status === 2,
'card-red': afterNode.raw.info.status === 4,
'card-gray': afterNode.raw.info.status === 3
}"
>
<div class="card-title" :title="afterNode.raw.info.label">{{ afterNode.raw.info.label }}</div>
<div class="card-sub" :title="afterNode.raw.info.date">{{ afterNode.raw.info.date }}</div>
</div>
</div>
</template>
<template #label>
<span style="display: none"></span>
</template>
</tiny-flowchart>
<p>示例2标准网格路径流程</p>
<tiny-flowchart :data="chartData2" :config="chartConfig2" />
</div>
</template>
<script>
import { TinyFlowchart } from '@opentiny/vue'
import { hooks } from '@opentiny/vue-common'
const { createNode, createLink, createConfig } = TinyFlowchart
export default {
components: { TinyFlowchart },
data() {
// ==================== 1 ====================
const config1 = createConfig()
config1.width = 860
config1.height = 480
config1.rows = 6
config1.cols = 8
config1.labelSpacing = 0
config1.labelHeight = 1
config1.styleLink = (ctx, afterLink) => {
const colors = { 1: '#52c41a', 2: '#1890ff', 3: '#bfbfbf', 4: '#ff4d4f' }
ctx.strokeStyle = colors[afterLink.raw.info.status] || '#999'
ctx.lineWidth = 2
if (afterLink.raw.info.style !== 'solid') {
ctx.setLineDash([4, 4])
}
}
const data1 = {
nodes: [
createNode('0', 1, '开始', '', null, 0, 3),
createNode('1', 1, '申请人', '张三', null, 1, 3),
createNode('2', 1, '制单会计', '协同:张三、张四、张五', null, 2, 3),
createNode('3', 2, '应付会计', '张四 0035837', null, 3, 1),
createNode('4', 4, '应付会计', '张四 0035837', null, 3, 5),
createNode('5', 3, '复核会计', '自定义内容-5', null, 4, 3),
createNode('6', 1, '结束', '', null, 5, 3)
],
links: [
createLink('0', '1', '', 1, 'solid'),
createLink('1', '2', '', 1, 'solid'),
createLink('2', '3', '0 b1 l2', 1, 'solid'),
createLink('2', '4', '0 b1 r2', 1, 'solid'),
createLink('3', '5', '0 b1 r2', 3, 'solid'),
createLink('4', '5', '0 b1 l2', 3, 'solid'),
createLink('5', '6', '', 3, 'dash')
]
}
// ==================== 2 ====================
const config2 = createConfig()
config2.width = 960
config2.height = 470
config2.rows = 10
config2.cols = 10
const data2 = {
nodes: [
createNode('start', 1, '提交报销', '2024-06-01', null, 2, 2),
createNode('check', 2, '金额审核', '2024-06-02', null, 4, 2),
createNode('pass', 1, '直接通过', '2024-06-03', null, 6, 1),
createNode('extra', 2, '额外审批', '2024-06-03', null, 6, 3),
createNode('end', 1, '流程结束', '2024-06-04', null, 8, 2)
],
links: [
createLink('start', 'check', '0 b2', 1, 'solid'),
createLink('check', 'pass', '0 b1 c l1 c b1', 1, 'solid'),
createLink('check', 'extra', '0 b1 c r1 c b1', 2, 'dashed'),
createLink('pass', 'end', '0 b1 c r1 c b1', 1, 'solid'),
createLink('extra', 'end', '0 b1 c l1 c b1', 2, 'solid')
]
}
return {
chartData1: hooks.markRaw(data1),
chartConfig1: hooks.markRaw(config1),
chartData2: hooks.markRaw(data2),
chartConfig2: hooks.markRaw(config2)
}
},
methods: {
onClickNode1(afterNode) {
console.log('示例1点击节点:', afterNode.raw.info.label)
},
onClickLink1(afterLink) {
console.log('示例1点击连线:', afterLink.raw.from, '→', afterLink.raw.to)
}
}
}
</script>
<style scoped>
/* 示例1样式 */
.icon-anchor {
position: relative;
width: 1px;
height: 1px;
}
.node-circle {
position: absolute;
left: -12px;
top: -24px;
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
}
.node-start {
border: 2px solid #52c41a;
background: #fff;
color: #333;
}
.node-end {
background: #52c41a;
color: #fff;
font-size: 22px;
font-weight: bold;
}
.node-card {
position: absolute;
left: -70px;
top: -28px;
width: 160px;
padding: 10px 12px;
border-radius: 6px;
border: 1px solid #d9d9d9;
background: #fff;
text-align: center;
box-sizing: border-box;
cursor: pointer;
}
.card-blue {
border-color: #1890ff;
background: #e6f7ff;
}
.card-red {
border-color: #ff4d4f;
background: #fff1f0;
}
.card-gray {
border-color: #bfbfbf;
background: #f5f5f5;
}
.card-title {
font-size: 14px;
font-weight: 500;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.5;
}
.card-sub {
font-size: 12px;
color: #666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.5;
margin-top: 4px;
}
</style>

View File

@ -1,172 +0,0 @@
<template>
<div>
<p>示例1使用 drawLink 完全自定义绘制</p>
<tiny-flowchart :data="data1" :config="config1" />
<p>示例2包含悬停时连线样式的效果</p>
<div class="example2-wrapper">
<tiny-flowchart :key="refreshKey2" :data="data2" :config="config2" />
</div>
</div>
</template>
<script setup>
import { TinyFlowchart } from '@opentiny/vue'
import { hooks } from '@opentiny/vue-common'
import { ref, onMounted, onUnmounted } from 'vue'
const { createNode, createLink, createConfig } = TinyFlowchart
// ==================== 1drawLink ====================
const chartConfig1 = createConfig()
chartConfig1.width = 960
chartConfig1.height = 260
chartConfig1.rows = 4
chartConfig1.cols = 8
// 使 drawLink styleLink
chartConfig1.drawLink = ({ ctx, afterLink }) => {
const link = afterLink.raw
const colorMap = { 1: '#12cff8', 2: '#1ff9ff', 3: '#1a1a1a', 4: '#ff4d4f' }
ctx.strokeStyle = colorMap[link.info.status] || '#999'
ctx.lineWidth = link.info.status === 2 ? 3 : 2
if (link.info.style !== 'solid') {
ctx.setLineDash([4, 4])
} else {
ctx.setLineDash([])
}
afterLink.p.forEach((p) => {
const parts = p.split(',')
const cmd = parts[0]
const coords = parts.slice(1).map(Number)
if (cmd === 'm') {
ctx.moveTo(coords[0], coords[1])
} else if (cmd === 'l') {
ctx.lineTo(coords[0], coords[1])
} else if (cmd === 'a') {
ctx.arcTo(coords[0], coords[1], coords[2], coords[3], coords[4])
}
})
}
const chartData1 = {
nodes: [
createNode('a', 1, '已通过', '2024-06-01', [], 1, 1),
createNode('b', 2, '进行中', '2024-06-02', [], 1, 3),
createNode('c', 3, '待处理', '', [], 1, 5),
createNode('d', 4, '已失败', '2024-06-03', [], 1, 7)
],
links: [
createLink('a', 'b', '0 r2', 1, 'solid'),
createLink('b', 'c', '0 r2', 2, 'solid'),
createLink('c', 'd', '0 r2', 3, 'dash')
]
}
const data1 = hooks.markRaw(chartData1)
const config1 = hooks.markRaw(chartConfig1)
// ==================== 2styleLink + ====================
const refreshKey2 = ref(0)
const mousePos2 = ref({ x: -1, y: -1 })
const chartConfig2 = createConfig()
chartConfig2.width = 960
chartConfig2.height = 260
chartConfig2.rows = 4
chartConfig2.cols = 8
// 2 styleLink2 canvas
chartConfig2.styleLink = (ctx, afterLink) => {
const link = afterLink.raw
const key = `${link.from}-${link.to}`
// 2 wrapper canvas1
const canvas = document.querySelector('.example2-wrapper .tiny-flow-chart__canvas')
if (!canvas) {
const colorMap = { 1: '#52c41a', 2: '#1890ff', 3: '#d9d9d9', 4: '#ff4d4f' }
ctx.strokeStyle = colorMap[link.info.status] || '#999'
ctx.lineWidth = link.info.status === 2 ? 3 : 2
if (link.info.style !== 'solid') {
ctx.setLineDash([4, 4])
} else {
ctx.setLineDash([])
}
return
}
const rect = canvas.getBoundingClientRect()
const mx = mousePos2.value.x - rect.left
const my = mousePos2.value.y - rect.top
const nodeY = 97.5
const nodeX = { a: 180, b: 420, c: 660, d: 900 }
let isHover = false
if (Math.abs(my - nodeY) <= 25) {
if (key === 'a-b' && mx >= nodeX.a && mx <= nodeX.b) isHover = true
else if (key === 'b-c' && mx >= nodeX.b && mx <= nodeX.c) isHover = true
else if (key === 'c-d' && mx >= nodeX.c && mx <= nodeX.d) isHover = true
}
if (isHover) {
ctx.setLineDash([])
ctx.strokeStyle = '#faacff'
ctx.lineWidth = 8
ctx.shadowColor = 'rgba(250, 173, 20, 0.6)'
ctx.shadowBlur = 20
} else {
const colorMap = { 1: '#52c41a', 2: '#1890ff', 3: '#d9d9d9', 4: '#ff4d4f' }
ctx.strokeStyle = colorMap[link.info.status] || '#999'
ctx.lineWidth = link.info.status === 2 ? 3 : 2
if (link.info.style !== 'solid') {
ctx.setLineDash([4, 4])
} else {
ctx.setLineDash([])
}
}
}
const chartData2 = {
nodes: [
createNode('a', 1, '已通过', '2024-06-01', [], 1, 1),
createNode('b', 2, '进行中', '2024-06-02', [], 1, 3),
createNode('c', 3, '待处理', '', [], 1, 5),
createNode('d', 4, '已失败', '2024-06-03', [], 1, 7)
],
links: [
createLink('a', 'b', '0 r2', 1, 'solid'),
createLink('b', 'c', '0 r2', 2, 'solid'),
createLink('c', 'd', '0 r2', 3, 'dash')
]
}
const data2 = hooks.markRaw(chartData2)
const config2 = hooks.markRaw(chartConfig2)
// 2
const onMouseMove2 = (e) => {
mousePos2.value = { x: e.clientX, y: e.clientY }
}
let timer2 = null
const startHoverLoop2 = () => {
timer2 = setInterval(() => {
refreshKey2.value++
}, 80)
}
onMounted(() => {
document.addEventListener('mousemove', onMouseMove2)
startHoverLoop2()
})
onUnmounted(() => {
document.removeEventListener('mousemove', onMouseMove2)
clearInterval(timer2)
})
</script>

View File

@ -1,171 +0,0 @@
<template>
<div>
<p>示例1使用 drawLink 完全自定义绘制</p>
<tiny-flowchart :data="data1" :config="config1" />
<p>示例2包含悬停时连线样式的效果</p>
<div class="example2-wrapper">
<tiny-flowchart :key="refreshKey2" :data="data2" :config="config2" />
</div>
</div>
</template>
<script>
import { TinyFlowchart } from '@opentiny/vue'
import { hooks } from '@opentiny/vue-common'
const { createNode, createLink, createConfig } = TinyFlowchart
export default {
components: { TinyFlowchart },
data() {
// 1
const config1 = createConfig()
config1.width = 960
config1.height = 260
config1.rows = 4
config1.cols = 8
config1.drawLink = ({ ctx, afterLink }) => {
const link = afterLink.raw
const colorMap = { 1: '#12cff8', 2: '#1ff9ff', 3: '#1a1a1a', 4: '#ff4d4f' }
ctx.strokeStyle = colorMap[link.info.status] || '#999'
ctx.lineWidth = link.info.status === 2 ? 3 : 2
if (link.info.style !== 'solid') {
ctx.setLineDash([4, 4])
} else {
ctx.setLineDash([])
}
afterLink.p.forEach((p) => {
const parts = p.split(',')
const cmd = parts[0]
const coords = parts.slice(1).map(Number)
if (cmd === 'm') {
ctx.moveTo(coords[0], coords[1])
} else if (cmd === 'l') {
ctx.lineTo(coords[0], coords[1])
} else if (cmd === 'a') {
ctx.arcTo(coords[0], coords[1], coords[2], coords[3], coords[4])
}
})
}
const data1 = {
nodes: [
createNode('a', 1, '已通过', '2024-06-01', [], 1, 1),
createNode('b', 2, '进行中', '2024-06-02', [], 1, 3),
createNode('c', 3, '待处理', '', [], 1, 5),
createNode('d', 4, '已失败', '2024-06-03', [], 1, 7)
],
links: [
createLink('a', 'b', '0 r2', 1, 'solid'),
createLink('b', 'c', '0 r2', 2, 'solid'),
createLink('c', 'd', '0 r2', 3, 'dash')
]
}
// 2
const config2 = createConfig()
config2.width = 960
config2.height = 260
config2.rows = 4
config2.cols = 8
const self = this
config2.styleLink = function (ctx, afterLink) {
const link = afterLink.raw
const key = `${link.from}-${link.to}`
const canvas = document.querySelector('.example2-wrapper .tiny-flow-chart__canvas')
if (!canvas) {
const colorMap = { 1: '#52c41a', 2: '#1890ff', 3: '#d9d9d9', 4: '#ff4d4f' }
ctx.strokeStyle = colorMap[link.info.status] || '#999'
ctx.lineWidth = link.info.status === 2 ? 3 : 2
if (link.info.style !== 'solid') {
ctx.setLineDash([4, 4])
} else {
ctx.setLineDash([])
}
return
}
const rect = canvas.getBoundingClientRect()
const mx = self.mousePos2.x - rect.left
const my = self.mousePos2.y - rect.top
const nodeY = 97.5
const nodeX = { a: 180, b: 420, c: 660, d: 900 }
let isHover = false
if (Math.abs(my - nodeY) <= 25) {
if (key === 'a-b' && mx >= nodeX.a && mx <= nodeX.b) isHover = true
else if (key === 'b-c' && mx >= nodeX.b && mx <= nodeX.c) isHover = true
else if (key === 'c-d' && mx >= nodeX.c && mx <= nodeX.d) isHover = true
}
if (isHover) {
ctx.setLineDash([])
ctx.strokeStyle = '#faacff'
ctx.lineWidth = 8
ctx.shadowColor = 'rgba(250, 173, 20, 0.6)'
ctx.shadowBlur = 20
} else {
const colorMap = { 1: '#52c41a', 2: '#1890ff', 3: '#d9d9d9', 4: '#ff4d4f' }
ctx.strokeStyle = colorMap[link.info.status] || '#999'
ctx.lineWidth = link.info.status === 2 ? 3 : 2
if (link.info.style !== 'solid') {
ctx.setLineDash([4, 4])
} else {
ctx.setLineDash([])
}
}
}
const data2 = {
nodes: [
createNode('a', 1, '已通过', '2024-06-01', [], 1, 1),
createNode('b', 2, '进行中', '2024-06-02', [], 1, 3),
createNode('c', 3, '待处理', '', [], 1, 5),
createNode('d', 4, '已失败', '2024-06-03', [], 1, 7)
],
links: [
createLink('a', 'b', '0 r2', 1, 'solid'),
createLink('b', 'c', '0 r2', 2, 'solid'),
createLink('c', 'd', '0 r2', 3, 'dash')
]
}
return {
refreshKey2: 0,
mousePos2: { x: -1, y: -1 },
data1: hooks.markRaw(data1),
config1: hooks.markRaw(config1),
data2: hooks.markRaw(data2),
config2: hooks.markRaw(config2),
timer2: null
}
},
mounted() {
document.addEventListener('mousemove', this.onMouseMove2)
this.startHoverLoop2()
},
beforeUnmount() {
document.removeEventListener('mousemove', this.onMouseMove2)
clearInterval(this.timer2)
},
methods: {
onMouseMove2(e) {
this.mousePos2 = { x: e.clientX, y: e.clientY }
},
startHoverLoop2() {
this.timer2 = setInterval(() => {
this.refreshKey2++
}, 80)
}
}
}
</script>

View File

@ -1,45 +0,0 @@
<template>
<div>
<tiny-flowchart :data="data" :config="config" />
</div>
</template>
<script setup>
import { TinyFlowchart } from '@opentiny/vue'
import { reactive } from 'vue'
const { createNode, createLink, createConfig } = TinyFlowchart
const config = createConfig()
config.colors = { 1: '#52c41a', 2: '#1890ff', 3: '#d9d9d9', 4: '#ff4d4f' }
config.background = '#f6ffed'
config.font = '14px "Microsoft YaHei"'
config.radius = 8
config.thin = false
config.iconWrapperSize = 32
config.iconSize = 26
config.iconSvgSize = 18
config.labelWidth = 100
config.labelHeight = 70
config.labelSpacing = 12
config.labelDateColor = '#666'
config.listWidth = 70
config.listIconSize = 24
config.headSize = 24
config.width = 860
config.height = 260
const data = reactive({
nodes: [
createNode('req', 1, '需求分析', '2024-06-01', null, 1, 1),
createNode('dev', 2, '开发中', '2024-06-05', null, 1, 3),
createNode('test', 3, '测试中', '', null, 1, 5),
createNode('release', 3, '待发布', '', null, 1, 7)
],
links: [
createLink('req', 'dev', '0 r2', 1, 'solid'),
createLink('dev', 'test', '0 r2', 2, 'solid'),
createLink('test', 'release', '0 r2', 3, 'dashed')
]
})
</script>

View File

@ -1,54 +0,0 @@
<template>
<div>
<tiny-flowchart :data="data" :config="config" />
</div>
</template>
<script>
import { TinyFlowchart } from '@opentiny/vue'
const { createNode, createLink, createConfig } = TinyFlowchart
export default {
components: { TinyFlowchart },
data() {
const config = createConfig()
config.colors = { 1: '#52c41a', 2: '#1890ff', 3: '#d9d9d9', 4: '#ff4d4f' }
config.background = '#f6ffed'
config.font = '14px "Microsoft YaHei"'
config.radius = 8
config.thin = false
config.iconWrapperSize = 32
config.iconSize = 26
config.iconSvgSize = 18
config.labelWidth = 100
config.labelHeight = 70
config.labelSpacing = 12
config.labelDateColor = '#666'
config.listWidth = 70
config.listIconSize = 24
config.headSize = 24
return {
config,
data: {
nodes: [
createNode('req', 1, '需求分析', '2024-06-01', null, 1, 1),
createNode('dev', 2, '开发中', '2024-06-05', null, 1, 3),
createNode('test', 3, '测试中', '', null, 1, 5),
createNode('release', 3, '待发布', '', null, 1, 7)
],
links: [
createLink('req', 'dev', '0 r2', 1, 'solid'),
createLink('dev', 'test', '0 r2', 2, 'solid'),
createLink('test', 'release', '0 r2', 3, 'dashed')
]
}
}
},
created() {
this.config.width = 860
this.config.height = 260
}
}
</script>

View File

@ -1,183 +0,0 @@
<template>
<div>
<div class="toolbar">
<button @click="approve">审批通过</button>
<button @click="reject">审批驳回</button>
<button @click="reset">重置流程</button>
</div>
<tiny-flowchart :data="data" :config="config" :key="chartKey">
<template #content="{ node, config: cfg }">
<div
v-if="node.info.items && node.info.items.length >= cfg.listThreshold"
class="dynamic-person-list"
:style="{ width: cfg.listWidth + 'px' }"
>
<div v-for="item in node.info.items" :key="item.key" class="dynamic-person-item">
<div class="dynamic-person-name">{{ item.name }}</div>
<div class="dynamic-person-comment" :style="{ color: cfg.colors[item.status] }">{{ item.comment }}</div>
</div>
</div>
</template>
</tiny-flowchart>
</div>
</template>
<script setup>
import { TinyFlowchart } from '@opentiny/vue'
import { reactive, ref } from 'vue'
const { createNode, createLink, createItem, createConfig } = TinyFlowchart
const config = createConfig()
config.width = 960
config.height = 260
config.rows = 4
config.cols = 8
config.delay = 15
config.listThreshold = 0 // createItemitems,1items10
const currentStep = ref(0)
const chartKey = ref(0)
const initialNodes = [
createNode(
'apply',
2,
'提交申请',
'2024-06-01',
[createItem('1', '张三', '申请人', 1, '已提交', '2024-06-01')],
1,
1
),
createNode('dept', 3, '部门审批', '', [createItem('2', '李四', '经理', 3, '待审批', '')], 1, 3),
createNode('finance', 3, '财务审批', '', [createItem('3', '王五', '财务', 3, '待审批', '')], 1, 5),
createNode('done', 3, '流程完成', '', null, 1, 7)
]
const initialLinks = [
createLink('apply', 'dept', '0 r2', 3, 'solid'),
createLink('dept', 'finance', '0 r2', 3, 'solid'),
createLink('finance', 'done', '0 r2', 3, 'solid')
]
const data = reactive({
nodes: JSON.parse(JSON.stringify(initialNodes)),
links: JSON.parse(JSON.stringify(initialLinks))
})
const approve = () => {
if (currentStep.value >= data.nodes.length - 1) return
const curr = data.nodes[currentStep.value]
curr.info.status = 1
if (curr.info.items) {
curr.info.items.forEach((it) => {
it.status = 1
it.comment = '已通过'
it.date = new Date().toISOString().split('T')[0]
})
}
if (currentStep.value < data.links.length) {
// links Vue 线
const links = [...data.links]
links[currentStep.value] = { ...links[currentStep.value], info: { ...links[currentStep.value].info, status: 1 } }
data.links = links
chartKey.value++
}
currentStep.value++
if (currentStep.value < data.nodes.length) {
const next = data.nodes[currentStep.value]
next.info.status = 2
if (next.info.items) {
next.info.items.forEach((it) => {
it.status = 2
it.comment = '审批中'
})
}
}
}
const reject = () => {
if (currentStep.value >= data.nodes.length - 1) return
const curr = data.nodes[currentStep.value]
curr.info.status = 4
if (curr.info.items) {
curr.info.items.forEach((it) => {
it.status = 4
it.comment = '已驳回'
})
}
if (currentStep.value < data.links.length) {
// links Vue 线
const links = [...data.links]
links[currentStep.value] = { ...links[currentStep.value], info: { ...links[currentStep.value].info, status: 4 } }
data.links = links
chartKey.value++
}
}
const reset = () => {
currentStep.value = 0
// 使 Object.assign Vue 3 reactive
Object.assign(data, {
nodes: JSON.parse(JSON.stringify(initialNodes)),
links: JSON.parse(JSON.stringify(initialLinks))
})
chartKey.value++
}
</script>
<style scoped>
.toolbar {
margin-bottom: 16px;
display: flex;
gap: 8px;
}
.toolbar button {
padding: 6px 16px;
border: 1px solid #d9d9d9;
background: #fff;
border-radius: 4px;
cursor: pointer;
}
.toolbar button:hover {
border-color: #1890ff;
color: #1890ff;
}
.dynamic-person-list {
position: absolute;
bottom: 10px;
left: 60%;
transform: translateX(-50%);
min-width: 80px;
background: #fff;
border-radius: 8px;
padding: 8px 0;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
border: 1px solid #f0f0f0;
z-index: 10;
}
.dynamic-person-item {
padding: 6px 16px;
text-align: center;
transition: background 0.2s;
}
.dynamic-person-item:hover {
background: #fafafa;
}
.dynamic-person-item:not(:last-child) {
border-bottom: 1px solid #f5f5f5;
}
.dynamic-person-name {
font-size: 13px;
font-weight: 600;
color: #262626;
}
.dynamic-person-comment {
display: inline-block;
margin-top: 4px;
padding: 1px 8px;
border-radius: 10px;
font-size: 10px;
font-weight: 500;
}
</style>

View File

@ -1,194 +0,0 @@
<template>
<div>
<div class="toolbar">
<button @click="approve">审批通过</button>
<button @click="reject">审批驳回</button>
<button @click="reset">重置流程</button>
</div>
<tiny-flowchart :data="data" :config="config" :key="chartKey">
<template #content="{ node, config: cfg }">
<div
v-if="node.info.items && node.info.items.length >= cfg.listThreshold"
class="dynamic-person-list"
:style="{ width: cfg.listWidth + 'px' }"
>
<div v-for="item in node.info.items" :key="item.key" class="dynamic-person-item">
<div class="dynamic-person-name">{{ item.name }}</div>
<div class="dynamic-person-comment" :style="{ color: cfg.colors[item.status] }">{{ item.comment }}</div>
</div>
</div>
</template>
</tiny-flowchart>
</div>
</template>
<script>
import { TinyFlowchart } from '@opentiny/vue'
const { createNode, createLink, createItem, createConfig } = TinyFlowchart
export default {
components: { TinyFlowchart },
data() {
const config = createConfig()
config.width = 960
config.height = 260
config.rows = 4
config.cols = 8
config.delay = 15
config.listThreshold = 0 // createItemitems,1items10
this.initialNodes = [
createNode(
'apply',
2,
'提交申请',
'2024-06-01',
[createItem('1', '张三', '申请人', 1, '已提交', '2024-06-01')],
1,
1
),
createNode('dept', 3, '部门审批', '', [createItem('2', '李四', '经理', 3, '待审批', '')], 1, 3),
createNode('finance', 3, '财务审批', '', [createItem('3', '王五', '财务', 3, '待审批', '')], 1, 5),
createNode('done', 3, '流程完成', '', null, 1, 7)
]
this.initialLinks = [
createLink('apply', 'dept', '0 r2', 3, 'solid'),
createLink('dept', 'finance', '0 r2', 3, 'solid'),
createLink('finance', 'done', '0 r2', 3, 'solid')
]
return {
config,
currentStep: 0,
chartKey: 0, //
data: {
nodes: JSON.parse(JSON.stringify(this.initialNodes)),
links: JSON.parse(JSON.stringify(this.initialLinks))
}
}
},
methods: {
approve() {
if (this.currentStep >= this.data.nodes.length - 1) return
const curr = this.data.nodes[this.currentStep]
curr.info.status = 1
if (curr.info.items) {
curr.info.items.forEach((it) => {
it.status = 1
it.comment = '已通过'
it.date = new Date().toISOString().split('T')[0]
})
}
if (this.currentStep < this.data.links.length) {
// links Vue 线
const links = [...this.data.links]
links[this.currentStep] = {
...links[this.currentStep],
info: { ...links[this.currentStep].info, status: 1 }
}
this.data.links = links
this.chartKey++ //
}
this.currentStep++
if (this.currentStep < this.data.nodes.length) {
const next = this.data.nodes[this.currentStep]
next.info.status = 2
if (next.info.items) {
next.info.items.forEach((it) => {
it.status = 2
it.comment = '审批中'
})
}
}
},
reject() {
if (this.currentStep >= this.data.nodes.length - 1) return
const curr = this.data.nodes[this.currentStep]
curr.info.status = 4
if (curr.info.items) {
curr.info.items.forEach((it) => {
it.status = 4
it.comment = '已驳回'
})
}
if (this.currentStep < this.data.links.length) {
// links Vue 线
const links = [...this.data.links]
links[this.currentStep] = {
...links[this.currentStep],
info: { ...links[this.currentStep].info, status: 4 }
}
this.data.links = links
this.chartKey++ //
}
},
reset() {
this.currentStep = 0
// data tiny-flowchart
this.data = {
nodes: JSON.parse(JSON.stringify(this.initialNodes)),
links: JSON.parse(JSON.stringify(this.initialLinks))
}
this.chartKey++ //
}
}
}
</script>
<style scoped>
.toolbar {
margin-bottom: 16px;
display: flex;
gap: 8px;
}
.toolbar button {
padding: 6px 16px;
border: 1px solid #d9d9d9;
background: #fff;
border-radius: 4px;
cursor: pointer;
}
.toolbar button:hover {
border-color: #1890ff;
color: #1890ff;
}
.dynamic-person-list {
position: absolute;
bottom: 10px;
left: 60%;
transform: translateX(-50%);
min-width: 80px;
background: #fff;
border-radius: 8px;
padding: 8px 0;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
border: 1px solid #f0f0f0;
z-index: 10;
}
.dynamic-person-item {
padding: 6px 16px;
text-align: center;
transition: background 0.2s;
}
.dynamic-person-item:hover {
background: #fafafa;
}
.dynamic-person-item:not(:last-child) {
border-bottom: 1px solid #f5f5f5;
}
.dynamic-person-name {
font-size: 13px;
font-weight: 600;
color: #262626;
}
.dynamic-person-comment {
display: inline-block;
margin-top: 4px;
padding: 1px 8px;
border-radius: 10px;
font-size: 10px;
font-weight: 500;
}
</style>

View File

@ -1,81 +0,0 @@
<template>
<div>
<div class="log-panel">
<div v-for="(log, i) in logs" :key="i" class="log-item">{{ log }}</div>
<div v-if="logs.length === 0" class="log-empty">点击节点连线或空白区域查看事件</div>
</div>
<tiny-flowchart
:data="data"
:config="config"
@click-node="handleNodeClick"
@click-link="handleLinkClick"
@click-blank="handleBlankClick"
/>
</div>
</template>
<script setup>
import { TinyFlowchart, Modal } from '@opentiny/vue'
import { reactive, ref } from 'vue'
const { createNode, createLink, createConfig } = TinyFlowchart
const config = createConfig()
config.width = 960
config.height = 260
const logs = ref([])
const handleNodeClick = (param, e) => {
Modal.message({
status: 'info',
message: `点击节点: ${param.raw.name} (${param.raw.info.label}) - 状态: ${param.raw.info.status}`
})
}
const handleLinkClick = (param, e) => {
Modal.message({
status: 'info',
message: `点击连线: ${param.raw.from}${param.raw.to} - 样式: ${param.raw.info.style}`
})
}
const handleBlankClick = (param, e) => {
Modal.message({
status: 'info',
message: `点击空白: 坐标 (${Math.round(e.x)}, ${Math.round(e.y)})`
})
}
const data = reactive({
nodes: [
createNode('a', 1, '步骤 A', '2024-06-01', null, 1, 1),
createNode('b', 2, '步骤 B', '2024-06-02', null, 1, 3),
createNode('c', 3, '步骤 C', '', null, 1, 5)
],
links: [createLink('a', 'b', '0 r2', 1, 'solid'), createLink('b', 'c', '0 r2', 2, 'solid')]
})
</script>
<style scoped>
.log-panel {
margin-bottom: 16px;
padding: 12px;
background: #f6f8fa;
border-radius: 6px;
max-height: 120px;
overflow-y: auto;
}
.log-item {
font-size: 12px;
color: #333;
padding: 2px 0;
border-bottom: 1px solid #eee;
}
.log-empty {
font-size: 12px;
color: #999;
text-align: center;
padding: 8px 0;
}
</style>

View File

@ -1,86 +0,0 @@
<template>
<div>
<div class="log-panel">
<div v-for="(log, i) in logs" :key="i" class="log-item">{{ log }}</div>
<div v-if="logs.length === 0" class="log-empty">点击节点连线或空白区域查看事件</div>
</div>
<tiny-flowchart
:data="data"
:config="config"
@click-node="handleNodeClick"
@click-link="handleLinkClick"
@click-blank="handleBlankClick"
/>
</div>
</template>
<script>
import { TinyFlowchart, Modal } from '@opentiny/vue'
const { createNode, createLink, createConfig } = TinyFlowchart
export default {
components: { TinyFlowchart },
data() {
return {
config: createConfig(),
logs: [],
data: {
nodes: [
createNode('a', 1, '步骤 A', '2024-06-01', null, 1, 1),
createNode('b', 2, '步骤 B', '2024-06-02', null, 1, 3),
createNode('c', 3, '步骤 C', '', null, 1, 5)
],
links: [createLink('a', 'b', '0 r2', 1, 'solid'), createLink('b', 'c', '0 r2', 2, 'solid')]
}
}
},
methods: {
handleNodeClick(param, e) {
Modal.message({
status: 'info',
message: `点击节点: ${param.raw.name} (${param.raw.info.label}) - 状态: ${param.raw.info.status}`
})
},
handleLinkClick(param, e) {
Modal.message({
status: 'info',
message: `点击连线: ${param.raw.from}${param.raw.to} - 样式: ${param.raw.info.style}`
})
},
handleBlankClick(param, e) {
Modal.message({
status: 'info',
message: `点击空白: 坐标 (${Math.round(e.x)}, ${Math.round(e.y)})`
})
}
},
created() {
this.config.width = 960
this.config.height = 260
}
}
</script>
<style scoped>
.log-panel {
margin-bottom: 16px;
padding: 12px;
background: #f6f8fa;
border-radius: 6px;
max-height: 120px;
overflow-y: auto;
}
.log-item {
font-size: 12px;
color: #333;
padding: 2px 0;
border-bottom: 1px solid #eee;
}
.log-empty {
font-size: 12px;
color: #999;
text-align: center;
padding: 8px 0;
}
</style>

View File

@ -1,78 +0,0 @@
<template>
<div class="tiny-demo">
<tiny-flowchart
ref="chart"
:data="chartDataRaw"
:config="chartConfigRaw"
@click-node="onClickNode"
@click-link="onClickLink"
@click-blank="onClickBlank"
>
</tiny-flowchart>
</div>
</template>
<script setup>
import { TinyModal, TinyFlowchart } from '@opentiny/vue'
import { hooks } from '@opentiny/vue-common'
const { createNode, createLink, createConfig } = TinyFlowchart
const chartData = {
nodes: [
createNode('1', 1, '基础信息', '2018.08.02', [], 1, 0),
createNode('2', 1, '调职补偿', '2018.08.02', [], 0, 2),
createNode('3', 1, '汇总调职补偿', '', [], 1, 4),
createNode('4', 3, '启动精算', '', [], 4, 5),
createNode('5', 3, '复核精算', '', [], 4, 6),
createNode('6', 3, '审核精算', '', [], 4, 7),
createNode('7', 1, '调职补偿', '2018.08.02', [], 2, 1),
createNode('8', 1, '复核', '2018.08.02', [], 2, 2),
createNode('9', 2, '审批', '2018.08.02', [], 2, 3),
createNode('10', 1, '复核', '2018.08.02', [], 4, 2),
createNode('11', 2, '审批', '2018.08.02', [], 4, 3),
createNode('12', 3, '运算调职兑现率', '', [], 4, 4),
createNode('13', 1, '复核', '2018.08.02', [], 6, 2),
createNode('14', 4, '审批审批审批审批审批 0123456789asdfghjkl', '2018.08.02', [], 6, 3)
],
links: [
createLink('1', '2', '0 r0.5 t1 c r1.5', 1),
createLink('2', '3', '0 r1.5 c b1 r0.5', 3),
createLink('3', '4', '0 r0.5 c b3 r0.5', 3),
createLink('4', '5', '', 3),
createLink('5', '6', '', 3),
createLink('1', '7', 'r0.5 b1 c r0.5', 1),
createLink('7', '8', '', 1),
createLink('8', '9', '', 1),
createLink('9', '3', '0 r0.5 c t1', 3),
createLink('10', '11', '', 1),
createLink('11', '12', '', 3),
createLink('12', '4', '0 r0.5', 3),
createLink('13', '14', '', 1),
createLink('14', '4', '0 r1.5 c t2', 3, 'dash')
]
}
const chartConfig = createConfig()
chartConfig.headUrl = `${import.meta.env.VITE_APP_BUILD_BASE_URL}static/images/mountain.png`
chartConfig.checkItemStatus = (item) => ~['已转审', '已同意'].indexOf(item.status)
chartConfig.adjustPos = (afterNode) => afterNode.raw.name === '2' && (afterNode.y += 1)
// 使 markRaw Vue
const chartDataRaw = hooks.markRaw(chartData)
const chartConfigRaw = hooks.markRaw(chartConfig)
function onClickNode(_afterNode, _e) {
TinyModal.message('click-node')
}
function onClickLink(_afterLink, _e) {
TinyModal.message('click-link')
}
function onClickBlank(_param, _e) {
TinyModal.message('click-blank')
}
</script>

View File

@ -1,91 +0,0 @@
<template>
<div class="tiny-demo">
<tiny-flowchart
ref="chart"
:data="chartData"
:config="chartConfig"
@click-node="onClickNode"
@click-link="onClickLink"
@click-blank="onClickBlank"
>
</tiny-flowchart>
</div>
</template>
<script>
import { TinyModal, TinyFlowchart } from '@opentiny/vue'
import { hooks } from '@opentiny/vue-common'
const { createNode, createLink, createItem, createConfig } = TinyFlowchart
const handlers = [
createItem('WX100001', '张三', '转审人', '已转审', '很好', '2018-08-20 12:00', ''),
createItem('WX100002', '李四', '主管', '已转审', '非常好', '2018-08-20 12:00', ''),
createItem('WX100003', '王五', '主管', '处理中', '', '', '')
]
const chartData = {
nodes: [
createNode('1', 1, '基础信息', '2018.08.02', [], 1, 0),
createNode('2', 1, '调职补偿', '2018.08.02', handlers, 0, 2),
createNode('3', 1, '汇总调职补偿', '', [], 1, 4),
createNode('4', 3, '启动精算', '', [], 4, 5),
createNode('5', 3, '复核精算', '', [], 4, 6),
createNode('6', 3, '审核精算', '', [], 4, 7),
createNode('7', 1, '调职补偿', '2018.08.02', [], 2, 1),
createNode('8', 1, '复核', '2018.08.02', [], 2, 2),
createNode('9', 2, '审批', '2018.08.02', [], 2, 3),
createNode('10', 1, '复核', '2018.08.02', [], 4, 2),
createNode('11', 2, '审批', '2018.08.02', [], 4, 3),
createNode('12', 3, '运算调职兑现率', '', [], 4, 4),
createNode('13', 1, '复核', '2018.08.02', [], 6, 2),
createNode('14', 4, '审批审批审批审批审批 0123456789asdfghjkl', '2018.08.02', [], 6, 3)
],
links: [
createLink('1', '2', '0 r0.5 t1 c r1.5', 1),
createLink('2', '3', '0 r1.5 c b1 r0.5', 3),
createLink('3', '4', '0 r0.5 c b3 r0.5', 3),
createLink('4', '5', '', 3),
createLink('5', '6', '', 3),
createLink('1', '7', 'r0.5 b1 c r0.5', 1),
createLink('7', '8', '', 1),
createLink('8', '9', '', 1),
createLink('9', '3', '0 r0.5 c t1', 3),
createLink('10', '11', '', 1),
createLink('11', '12', '', 3),
createLink('12', '4', '0 r0.5', 3),
createLink('13', '14', '', 1),
createLink('14', '4', '0 r1.5 c t2', 3, 'dash')
]
}
const chartConfig = createConfig()
chartConfig.headUrl = `${import.meta.env.VITE_APP_BUILD_BASE_URL}static/images/mountain.png`
chartConfig.checkItemStatus = (item) => ~['已转审', '已同意'].indexOf(item.status)
chartConfig.adjustPos = (afterNode) => afterNode.raw.name === '2' && (afterNode.y += 1)
export default {
components: {
TinyFlowchart
},
data() {
return {
chartData: hooks.markRaw(chartData),
chartConfig: hooks.markRaw(chartConfig)
}
},
methods: {
onClickNode(_afterNode, _e) {
TinyModal.message('click-node')
},
onClickLink(_afterLink, _e) {
TinyModal.message('click-link')
},
onClickBlank(_param, _e) {
TinyModal.message('click-blank')
}
}
}
</script>

View File

@ -1,197 +0,0 @@
<template>
<div>
<tiny-flowchart :data="data" :config="config">
<template #content="{ node, config: cfg }">
<div
v-if="node.info.items && node.info.items.length > cfg.listThreshold"
class="person-list"
:style="{ width: cfg.listWidth + 'px' }"
>
<div v-for="item in node.info.items" :key="item.key" class="person-item">
<div class="person-avatar">
<img
v-if="cfg.headUrl"
:src="cfg.headUrl"
:style="{ width: cfg.headSize + 'px', height: cfg.headSize + 'px' }"
/>
<div
v-else
class="avatar-placeholder"
:style="{ width: cfg.headSize + 'px', height: cfg.headSize + 'px' }"
>
{{ item.name.charAt(0) }}
</div>
<!-- 状态指示点 -->
<span class="status-dot" :style="{ background: cfg.colors[item.status] }"></span>
</div>
<div class="person-info">
<div class="person-name">{{ item.name }}</div>
<div class="person-role">{{ item.role }}</div>
<div class="person-comment" :style="{ color: cfg.colors[item.status] }">{{ item.comment }}</div>
</div>
</div>
</div>
</template>
</tiny-flowchart>
</div>
</template>
<script setup>
import { TinyFlowchart } from '@opentiny/vue'
import { reactive } from 'vue'
const { createNode, createLink, createItem, createConfig } = TinyFlowchart
const config = createConfig()
config.width = 860
config.height = 360
config.rows = 4
config.cols = 8
config.listWidth = 120 //
config.listThreshold = 0 // createItemitems,1items10
config.listBorderColor = '#e8e8e8'
config.listIconColor = '#bfbfbf'
config.listIconSize = 22
config.headSize = 28 //
const data = reactive({
nodes: [
createNode(
'apply',
1,
'提交申请',
'2024-06-01',
[
createItem('1', '张三', '申请人', 1, '已提交', '2024-06-01'),
createItem('2', '李四', '协助人', 1, '已确认', '2024-06-01')
],
1,
1
),
createNode(
'dept',
2,
'部门审批',
'2024-06-02',
[createItem('3', '王五', '部门经理', 2, '审批中', '2024-06-02')],
1,
3
),
createNode(
'finance',
3,
'财务审批',
'',
[createItem('4', '赵六', '财务主管', 3, '待审批', ''), createItem('5', '孙七', '出纳', 3, '待处理', '')],
1,
5
),
createNode('done', 3, '流程结束', '', null, 1, 7)
],
links: [
createLink('apply', 'dept', '0 r2', 1, 'solid'),
createLink('dept', 'finance', '0 r2', 2, 'solid'),
createLink('finance', 'done', '0 r2', 3, 'solid')
]
})
</script>
<style scoped>
/* 人员列表容器 */
.person-list {
position: absolute;
bottom: 10px;
left: 48%;
transform: translateX(-45%);
min-width: 100px;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 8px;
padding: 8px;
background: #ffffff;
z-index: 10;
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.05),
0 1px 3px rgba(0, 0, 0, 0.04);
transition: box-shadow 0.3s ease;
}
/* 单个人员项 */
.person-item {
display: flex;
align-items: flex-start;
padding: 8px 6px;
border-radius: 6px;
transition: all 0.25s ease;
position: relative;
}
.person-item:not(:last-child) {
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
}
.person-item:hover {
background: rgba(24, 144, 255, 0.04);
}
/* 头像区域 */
.person-avatar {
margin-right: 10px;
flex-shrink: 0;
position: relative;
}
.avatar-placeholder {
border-radius: 50%;
background: linear-gradient(135deg, #e6f7ff 0%, #f0f9ff 100%);
color: #1890ff;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
box-shadow: inset 0 0 0 1px rgba(24, 144, 255, 0.15);
}
/* 状态指示圆点 */
.status-dot {
position: absolute;
bottom: -1px;
right: -1px;
width: 8px;
height: 8px;
border-radius: 50%;
border: 2px solid #ffffff;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
/* 信息区域 */
.person-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.person-name {
font-size: 13px;
font-weight: 600;
color: #1f1f1f;
line-height: 1.4;
letter-spacing: 0.2px;
}
.person-role {
font-size: 11px;
color: #8c8c8c;
line-height: 1.3;
font-weight: 400;
}
.person-comment {
font-size: 11px;
line-height: 1.3;
margin-top: 2px;
font-weight: 500;
}
</style>

View File

@ -1,204 +0,0 @@
<template>
<div>
<tiny-flowchart :data="data" :config="config">
<template #content="{ node, config: cfg }">
<div
v-if="node.info.items && node.info.items.length >= cfg.listThreshold"
class="person-list"
:style="{ width: cfg.listWidth + 'px' }"
>
<div v-for="item in node.info.items" :key="item.key" class="person-item">
<div class="person-avatar">
<img
v-if="cfg.headUrl"
:src="cfg.headUrl"
:style="{ width: cfg.headSize + 'px', height: cfg.headSize + 'px' }"
/>
<div
v-else
class="avatar-placeholder"
:style="{ width: cfg.headSize + 'px', height: cfg.headSize + 'px' }"
>
{{ item.name.charAt(0) }}
</div>
<!-- 状态指示点 -->
<span class="status-dot" :style="{ background: cfg.colors[item.status] }"></span>
</div>
<div class="person-info">
<div class="person-name">{{ item.name }}</div>
<div class="person-role">{{ item.role }}</div>
<div class="person-comment" :style="{ color: cfg.colors[item.status] }">{{ item.comment }}</div>
</div>
</div>
</div>
</template>
</tiny-flowchart>
</div>
</template>
<script>
import { TinyFlowchart } from '@opentiny/vue'
const { createNode, createLink, createItem, createConfig } = TinyFlowchart
export default {
components: { TinyFlowchart },
data() {
const config = createConfig()
config.width = 860
config.height = 360
config.rows = 4
config.cols = 8
config.listWidth = 120 //
config.listThreshold = 0 // createItemitems,1items10
config.listBorderColor = '#e8e8e8'
config.listIconColor = '#bfbfbf'
config.listIconSize = 22
config.headSize = 28 //
return {
config,
data: {
nodes: [
createNode(
'apply',
1,
'提交申请',
'2024-06-01',
[
createItem('1', '张三', '申请人', 1, '已提交', '2024-06-01'),
createItem('2', '李四', '协助人', 1, '已确认', '2024-06-01')
],
1,
1
),
createNode(
'dept',
2,
'部门审批',
'2024-06-02',
[createItem('3', '王五', '部门经理', 2, '审批中', '2024-06-02')],
1,
3
),
createNode(
'finance',
3,
'财务审批',
'',
[createItem('4', '赵六', '财务主管', 3, '待审批', ''), createItem('5', '孙七', '出纳', 3, '待处理', '')],
1,
5
),
createNode('done', 3, '流程结束', '', null, 1, 7)
],
links: [
createLink('apply', 'dept', '0 r2', 1, 'solid'),
createLink('dept', 'finance', '0 r2', 2, 'solid'),
createLink('finance', 'done', '0 r2', 3, 'solid')
]
}
}
}
}
</script>
<style scoped>
/* 人员列表容器 */
.person-list {
position: absolute;
bottom: 10px;
left: 48%;
transform: translateX(-45%);
min-width: 100px;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 8px;
padding: 8px;
background: #ffffff;
z-index: 10;
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.05),
0 1px 3px rgba(0, 0, 0, 0.04);
transition: box-shadow 0.3s ease;
}
/* 单个人员项 */
.person-item {
display: flex;
align-items: flex-start;
padding: 8px 6px;
border-radius: 6px;
transition: all 0.25s ease;
position: relative;
}
.person-item:not(:last-child) {
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
}
.person-item:hover {
background: rgba(24, 144, 255, 0.04);
}
/* 头像区域 */
.person-avatar {
margin-right: 10px;
flex-shrink: 0;
position: relative;
}
.avatar-placeholder {
border-radius: 50%;
background: linear-gradient(135deg, #e6f7ff 0%, #f0f9ff 100%);
color: #1890ff;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
box-shadow: inset 0 0 0 1px rgba(24, 144, 255, 0.15);
}
/* 状态指示圆点 */
.status-dot {
position: absolute;
bottom: -1px;
right: -1px;
width: 8px;
height: 8px;
border-radius: 50%;
border: 2px solid #ffffff;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
/* 信息区域 */
.person-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.person-name {
font-size: 13px;
font-weight: 600;
color: #1f1f1f;
line-height: 1.4;
letter-spacing: 0.2px;
}
.person-role {
font-size: 11px;
color: #8c8c8c;
line-height: 1.3;
font-weight: 400;
}
.person-comment {
font-size: 11px;
line-height: 1.3;
margin-top: 2px;
font-weight: 500;
}
</style>

View File

@ -1,134 +1,182 @@
<template>
<div>
<tiny-flowchart :data="data" :config="config">
<template #icon="{ node, config: cfg }">
<div
class="custom-icon"
:style="{
width: cfg.iconSize + 'px',
height: cfg.iconSize + 'px',
borderRadius: '50%',
background: cfg.colors[node.info.status] || '#999',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#fff',
fontSize: '12px'
}"
<div class="tiny-demo">
<tiny-flowchart
ref="chart"
:data="chartDataRaw"
:config="chartConfigRaw"
@click-node="onClickNode"
@click-link="onClickLink"
@click-blank="onClickBlank"
>
<!-- content 插槽下拉形式展示收起时显示紧凑视图点击展开显示表单列表 -->
<template #content="params">
<tiny-popover
placement="bottom-start"
trigger="manual"
width="220"
popper-class="flowchart-content-popover"
:visible-arrow="false"
:model-value="params.dropdowns[params.node.name]"
@update:model-value="params.dropdowns[params.node.name] = $event"
>
{{ node.info.status === 1 ? '✓' : node.info.status === 2 ? '◉' : '○' }}
</div>
</template>
<template #label="{ node }">
<div class="custom-label">
<div class="label-title">{{ node.info.label }}</div>
<div v-if="node.info.date" class="label-date">{{ node.info.date }}</div>
</div>
</template>
<template #content="{ node, config: cfg }">
<div class="custom-content">
<div
v-for="item in getNodeItems(node.name)"
:key="item.key"
class="person-tag"
:class="{ done: item.status === 1, doing: item.status === 2 }"
>
{{ item.name }} - {{ item.comment }}
</div>
</div>
<template #default>
<div class="flowchart-content-slot">
<div v-for="(item, i) in params.node.info.items" :key="item.key || i" class="content-item">
<span class="item-name">{{ item.name }}</span>
<span class="item-role">{{ item.role }}</span>
<span class="item-status">{{ item.status }}</span>
</div>
</div>
</template>
<template #reference>
<div
class="flowchart-content-trigger"
:style="{ borderColor: params.config.listBorderColor }"
@click.stop="params.dropdowns[params.node.name] = !params.dropdowns[params.node.name]"
>
<span class="trigger-text">处理人({{ params.node.info.items.length }})</span>
<component :is="params.dropdowns[params.node.name] ? IconUp : IconDown" class="trigger-icon" />
</div>
</template>
</tiny-popover>
</template>
</tiny-flowchart>
</div>
</template>
<script setup>
import { TinyFlowchart } from '@opentiny/vue'
import { reactive } from 'vue'
import { TinyModal, TinyPopover, TinyFlowchart } from '@opentiny/vue'
import { iconChevronDown, iconChevronUp } from '@opentiny/vue-icon'
import { hooks } from '@opentiny/vue-common'
const { createNode, createLink, createItem, createConfig } = TinyFlowchart
const config = createConfig()
config.width = 960
config.height = 260 //
config.rows = 4 //
config.cols = 8 //
config.listWidth = 80
config.listThreshold = 0 // createItemitems,1items10
config.listBorderColor = '#e8e8e8'
config.listIconColor = '#bfbfbf'
config.listIconSize = 22
config.headSize = 22
const IconDown = iconChevronDown()
const IconUp = iconChevronUp()
function getNodeItems(nodeName) {
const node = data.nodes.find((n) => n.name === nodeName)
return node?.info?.items || []
const handlers = [
createItem('WX100001', '张三', '转审人', '已转审', '很好', '2018-08-20 12:00', ''),
createItem('WX100002', '李四', '主管', '已转审', '非常好', '2018-08-20 12:00', ''),
createItem('WX100003', '王五', '主管', '处理中', '', '', '')
]
const chartData = {
nodes: [
createNode('1', 1, '基础信息', '2018.08.02', [], 1, 0),
createNode('2', 1, '调职补偿', '2018.08.02', handlers, 0, 2),
createNode('3', 1, '汇总调职补偿', '', [], 1, 4),
createNode('4', 3, '启动精算', '', [], 4, 5),
createNode('5', 3, '复核精算', '', [], 4, 6),
createNode('6', 3, '审核精算', '', [], 4, 7),
createNode('7', 1, '调职补偿', '2018.08.02', [], 2, 1),
createNode('8', 1, '复核', '2018.08.02', [], 2, 2),
createNode('9', 2, '审批', '2018.08.02', [], 2, 3),
createNode('10', 1, '复核', '2018.08.02', [], 4, 2),
createNode('11', 2, '审批', '2018.08.02', [], 4, 3),
createNode('12', 3, '运算调职兑现率', '', [], 4, 4),
createNode('13', 1, '复核', '2018.08.02', [], 6, 2),
createNode('14', 4, '审批审批审批审批审批 0123456789asdfghjkl', '2018.08.02', [], 6, 3)
],
links: [
createLink('1', '2', '0 r0.5 t1 c r1.5', 1),
createLink('2', '3', '0 r1.5 c b1 r0.5', 3),
createLink('3', '4', '0 r0.5 c b3 r0.5', 3),
createLink('4', '5', '', 3),
createLink('5', '6', '', 3),
createLink('1', '7', 'r0.5 b1 c r0.5', 1),
createLink('7', '8', '', 1),
createLink('8', '9', '', 1),
createLink('9', '3', '0 r0.5 c t1', 3),
createLink('10', '11', '', 1),
createLink('11', '12', '', 3),
createLink('12', '4', '0 r0.5', 3),
createLink('13', '14', '', 1),
createLink('14', '4', '0 r1.5 c t2', 3, 'dash')
]
}
const data = reactive({
nodes: [
createNode(
'step1',
1,
'提交申请',
'2024-06-01',
[createItem('1', '张三', '申请人', 1, '已提交', '2024-06-01')],
1,
1
),
createNode(
'step2',
2,
'主管审批',
'2024-06-02',
[createItem('2', '李四', '审批人', 2, '审批中', '2024-06-02')],
1,
3
),
createNode('step3', 3, '流程结束', '', null, 1, 5)
],
links: [createLink('step1', 'step2', '0 r2', 1, 'solid'), createLink('step2', 'step3', '0 r2', 2, 'solid')]
})
const chartConfig = createConfig()
chartConfig.headUrl = `${import.meta.env.VITE_APP_BUILD_BASE_URL}static/images/mountain.png`
chartConfig.checkItemStatus = (item) => ~['已转审', '已同意'].indexOf(item.status)
chartConfig.adjustPos = (afterNode) => afterNode.raw.name === '2' && (afterNode.y += 1)
// content listWidth 62px
chartConfig.listWidth = 150
const chartDataRaw = hooks.markRaw(chartData)
const chartConfigRaw = hooks.markRaw(chartConfig)
function onClickNode(_afterNode, _e) {
TinyModal.message('click-node')
}
function onClickLink(_afterLink, _e) {
TinyModal.message('click-link')
}
function onClickBlank(_param, _e) {
TinyModal.message('click-blank')
}
</script>
<style scoped>
.custom-label {
text-align: center;
/* 覆盖 content 插槽容器的固定高度(默认 24px),否则下拉触发区会被挤压 */
:deep(.tiny-flow-chart__node-item) {
min-height: 24px !important;
height: auto !important;
}
.label-title {
font-size: 13px;
font-weight: 600;
color: #333;
/* 下拉触发区:收起时显示的紧凑视图 */
.flowchart-content-trigger {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
width: 100%;
height: 100%;
min-height: 22px;
padding: 0 4px;
border: 1px solid #d9d9d9;
border-radius: 3px;
font-size: 12px;
cursor: pointer;
}
.label-date {
font-size: 11px;
color: #999;
margin-top: 2px;
}
.custom-content {
position: absolute;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
z-index: 10;
}
.person-tag {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
margin: 2px;
background: #f5f5f5;
color: #666;
.flowchart-content-trigger .trigger-text {
white-space: nowrap;
}
.person-tag.done {
background: #f6ffed;
color: #52c41a;
.flowchart-content-trigger .trigger-icon {
flex-shrink: 0;
}
.person-tag.doing {
background: #e6f7ff;
/* content 插槽:下拉展开后的表单列表 */
.flowchart-content-slot {
padding: 4px 8px;
font-size: 12px;
}
.flowchart-content-slot .content-item {
display: flex;
gap: 8px;
padding: 4px 0;
border-bottom: 1px dashed #e8e8e8;
}
.flowchart-content-slot .content-item:last-child {
border-bottom: none;
}
.flowchart-content-slot .item-name {
min-width: 40px;
}
.flowchart-content-slot .item-role {
min-width: 50px;
color: #666;
}
.flowchart-content-slot .item-status {
color: #1890ff;
}
/* 下拉弹层样式 */
:deep(.flowchart-content-popover.tiny-popper) {
margin-top: 2px;
padding: 0;
}
</style>

View File

@ -0,0 +1,24 @@
import { test, expect } from '@playwright/test'
test('插槽定制', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('flowchart#slots')
const preview = page.locator('#slots')
const flowchart = preview.locator('.tiny-flow-chart')
const nodes = flowchart.locator('.tiny-flow-chart__node-icon-wrapper')
await expect(flowchart).toBeVisible()
await expect(flowchart.locator('.tiny-flow-chart__canvas')).toBeVisible()
await expect(nodes).toHaveCount(14)
// content 插槽:下拉形式,点击「处理人(3)」展开后显示表单列表(弹层可能 teleport 到 body
const trigger = flowchart.locator('.flowchart-content-trigger')
await expect(trigger).toBeVisible()
await expect(trigger).toContainText('处理人(3)')
await trigger.click()
const contentSlot = page.locator('.flowchart-content-slot')
await expect(contentSlot).toBeVisible()
await expect(contentSlot).toContainText('张三')
await expect(contentSlot).toContainText('李四')
await expect(contentSlot).toContainText('王五')
})

View File

@ -1,142 +1,193 @@
<template>
<div>
<tiny-flowchart :data="data" :config="config">
<template #icon="{ node, config: cfg }">
<div
class="custom-icon"
:style="{
width: cfg.iconSize + 'px',
height: cfg.iconSize + 'px',
borderRadius: '50%',
background: cfg.colors[node.info.status] || '#999',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#fff',
fontSize: '12px'
}"
<div class="tiny-demo">
<tiny-flowchart
ref="chart"
:data="chartData"
:config="chartConfig"
@click-node="onClickNode"
@click-link="onClickLink"
@click-blank="onClickBlank"
>
<!-- content 插槽下拉形式展示收起时显示紧凑视图点击展开显示表单列表 -->
<template #content="params">
<tiny-popover
placement="bottom-start"
trigger="manual"
width="220"
popper-class="flowchart-content-popover"
:visible-arrow="false"
:model-value="params.dropdowns[params.node.name]"
@update:model-value="params.dropdowns[params.node.name] = $event"
>
{{ node.info.status === 1 ? '✓' : node.info.status === 2 ? '◉' : '○' }}
</div>
</template>
<template #label="{ node }">
<div class="custom-label">
<div class="label-title">{{ node.info.label }}</div>
<div v-if="node.info.date" class="label-date">{{ node.info.date }}</div>
</div>
</template>
<template #content="{ node, config: cfg }">
<div class="custom-content">
<div
v-for="item in getNodeItems(node.name)"
:key="item.key"
class="person-tag"
:class="{ done: item.status === 1, doing: item.status === 2 }"
>
{{ item.name }} - {{ item.comment }}
</div>
</div>
<template #default>
<div class="flowchart-content-slot">
<div v-for="(item, i) in params.node.info.items" :key="item.key || i" class="content-item">
<span class="item-name">{{ item.name }}</span>
<span class="item-role">{{ item.role }}</span>
<span class="item-status">{{ item.status }}</span>
</div>
</div>
</template>
<template #reference>
<div
class="flowchart-content-trigger"
:style="{ borderColor: params.config.listBorderColor }"
@click.stop="params.dropdowns[params.node.name] = !params.dropdowns[params.node.name]"
>
<span class="trigger-text">处理人({{ params.node.info.items.length }})</span>
<component :is="params.dropdowns[params.node.name] ? IconUp : IconDown" class="trigger-icon" />
</div>
</template>
</tiny-popover>
</template>
</tiny-flowchart>
</div>
</template>
<script>
import { TinyFlowchart } from '@opentiny/vue'
import { TinyModal, TinyPopover, TinyFlowchart } from '@opentiny/vue'
import { iconChevronDown, iconChevronUp } from '@opentiny/vue-icon'
import { hooks } from '@opentiny/vue-common'
const { createNode, createLink, createItem, createConfig } = TinyFlowchart
export default {
components: { TinyFlowchart },
data() {
const config = createConfig()
config.width = 960
config.height = 260 //
config.rows = 4 //
config.cols = 8 //
config.listWidth = 80
config.listThreshold = 0 // createItemitems,1items10
config.listBorderColor = '#e8e8e8'
config.listIconColor = '#bfbfbf'
config.listIconSize = 22
config.headSize = 22
const IconDown = iconChevronDown()
const IconUp = iconChevronUp()
const handlers = [
createItem('WX100001', '张三', '转审人', '已转审', '很好', '2018-08-20 12:00', ''),
createItem('WX100002', '李四', '主管', '已转审', '非常好', '2018-08-20 12:00', ''),
createItem('WX100003', '王五', '主管', '处理中', '', '', '')
]
const chartData = {
nodes: [
createNode('1', 1, '基础信息', '2018.08.02', [], 1, 0),
createNode('2', 1, '调职补偿', '2018.08.02', handlers, 0, 2),
createNode('3', 1, '汇总调职补偿', '', [], 1, 4),
createNode('4', 3, '启动精算', '', [], 4, 5),
createNode('5', 3, '复核精算', '', [], 4, 6),
createNode('6', 3, '审核精算', '', [], 4, 7),
createNode('7', 1, '调职补偿', '2018.08.02', [], 2, 1),
createNode('8', 1, '复核', '2018.08.02', [], 2, 2),
createNode('9', 2, '审批', '2018.08.02', [], 2, 3),
createNode('10', 1, '复核', '2018.08.02', [], 4, 2),
createNode('11', 2, '审批', '2018.08.02', [], 4, 3),
createNode('12', 3, '运算调职兑现率', '', [], 4, 4),
createNode('13', 1, '复核', '2018.08.02', [], 6, 2),
createNode('14', 4, '审批审批审批审批审批 0123456789asdfghjkl', '2018.08.02', [], 6, 3)
],
links: [
createLink('1', '2', '0 r0.5 t1 c r1.5', 1),
createLink('2', '3', '0 r1.5 c b1 r0.5', 3),
createLink('3', '4', '0 r0.5 c b3 r0.5', 3),
createLink('4', '5', '', 3),
createLink('5', '6', '', 3),
createLink('1', '7', 'r0.5 b1 c r0.5', 1),
createLink('7', '8', '', 1),
createLink('8', '9', '', 1),
createLink('9', '3', '0 r0.5 c t1', 3),
createLink('10', '11', '', 1),
createLink('11', '12', '', 3),
createLink('12', '4', '0 r0.5', 3),
createLink('13', '14', '', 1),
createLink('14', '4', '0 r1.5 c t2', 3, 'dash')
]
}
const chartConfig = createConfig()
chartConfig.headUrl = `${import.meta.env.VITE_APP_BUILD_BASE_URL}static/images/mountain.png`
chartConfig.checkItemStatus = (item) => ~['已转审', '已同意'].indexOf(item.status)
chartConfig.adjustPos = (afterNode) => afterNode.raw.name === '2' && (afterNode.y += 1)
// content listWidth 62px
chartConfig.listWidth = 150
export default {
components: {
TinyFlowchart,
TinyPopover,
IconDown,
IconUp
},
data() {
return {
config,
data: {
nodes: [
createNode(
'step1',
1,
'提交申请',
'2024-06-01',
[createItem('1', '张三', '申请人', 1, '已提交', '2024-06-01')],
1,
1
),
createNode(
'step2',
2,
'主管审批',
'2024-06-02',
[createItem('2', '李四', '审批人', 2, '审批中', '2024-06-02')],
1,
3
),
createNode('step3', 3, '流程结束', '', null, 1, 5)
],
links: [createLink('step1', 'step2', '0 r2', 1, 'solid'), createLink('step2', 'step3', '0 r2', 2, 'solid')]
}
chartData: hooks.markRaw(chartData),
chartConfig: hooks.markRaw(chartConfig)
}
},
methods: {
getNodeItems(nodeName) {
const node = this.data.nodes.find((n) => n.name === nodeName)
return node?.info?.items || []
onClickNode(_afterNode, _e) {
TinyModal.message('click-node')
},
onClickLink(_afterLink, _e) {
TinyModal.message('click-link')
},
onClickBlank(_param, _e) {
TinyModal.message('click-blank')
}
}
}
</script>
<style scoped>
.custom-label {
text-align: center;
/* 覆盖 content 插槽容器的固定高度(默认 24px),否则下拉触发区会被挤压 */
:deep(.tiny-flow-chart__node-item) {
min-height: 24px !important;
height: auto !important;
}
.label-title {
font-size: 13px;
font-weight: 600;
color: #333;
/* 下拉触发区:收起时显示的紧凑视图 */
.flowchart-content-trigger {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
width: 100%;
height: 100%;
min-height: 22px;
padding: 0 4px;
border: 1px solid #d9d9d9;
border-radius: 3px;
font-size: 12px;
cursor: pointer;
}
.label-date {
font-size: 11px;
color: #999;
margin-top: 2px;
}
.custom-content {
position: absolute;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
z-index: 10;
}
.person-tag {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
margin: 2px;
background: #f5f5f5;
color: #666;
.flowchart-content-trigger .trigger-text {
white-space: nowrap;
}
.person-tag.done {
background: #f6ffed;
color: #52c41a;
.flowchart-content-trigger .trigger-icon {
flex-shrink: 0;
}
.person-tag.doing {
background: #e6f7ff;
/* content 插槽:下拉展开后的表单列表 */
.flowchart-content-slot {
padding: 4px 8px;
font-size: 12px;
}
.flowchart-content-slot .content-item {
display: flex;
gap: 8px;
padding: 4px 0;
border-bottom: 1px dashed #e8e8e8;
}
.flowchart-content-slot .content-item:last-child {
border-bottom: none;
}
.flowchart-content-slot .item-name {
min-width: 40px;
}
.flowchart-content-slot .item-role {
min-width: 50px;
color: #666;
}
.flowchart-content-slot .item-status {
color: #1890ff;
}
/* 下拉弹层样式 */
:deep(.flowchart-content-popover.tiny-popper) {
margin-top: 2px;
padding: 0;
}
</style>

View File

@ -12,54 +12,12 @@ export default {
},
desc: {
'zh-CN':
"\n<p>节点支持 <code>icon</code> | <code>label</code> | <code>content</code> 插槽定制内容,示例提供了 <code>content</code> 插槽的默认实现。节点使用 <code>row</code> | <code>col</code> 属性进行行列配置。连线使用 <code>p</code> 属性进行相对路径配置。流程图的其它设置通过 <code>config</code> 进行配置。组件预置了 <code>createItem</code> | <code>createNode</code> | <code>createLink</code> | <code>createConfig</code> 静态方法,以便于快速构建选项。流程图的行高列宽由 <code>config</code> 的属性 <code>width</code> | <code>height</code> | <code>cols</code> | <code>rows</code> 确定,节点的位置由流程图的行高列宽,以及节点的 <code>row</code> | <code>col</code> 位置确定。</p>\n<p>连线相对路径配置详细介绍:</p>\n<ul>\n<li>'0' 表示从起始节点 1 的位置开始</li>\n<li>'r2' 表示向右画两列宽度的连线</li>\n<li>'c' 表示画一个圆角</li>\n<li>'b2' 表示向下画两行高度的连线</li>\n<li>'l1' 表示向左画一列宽度的连线</li>\n<li>'t1' 向上画一行高度的连线</li>\n</ul>\n<p>连线配置参数举例:</p>\n<pre><code>const link = { from: '1', to: '2', p: '0 r2 c b2 c l1 c t1', status: 1 }</code></pre>\n",
"\n<p>节点支持 <code>icon</code> | <code>label</code> | <code>content</code> 插槽定制内容,示例提供了 <code>content</code> 插槽的默认实现。节点使用 <code>row</code> | <code>col</code> 属性进行行列配置。连线使用 <code>p</code> 属性进行相对路径配置。流程图的其它设置通过 <code>config</code> 进行配置。组件预置了 <code>createItem</code> | <code>createNode</code> | <code>createLink</code> | <code>createConfig</code> 静态方法,以便于快速构建选项。流程图的行高列宽由 <code>config</code> 的属性 <code>width</code> | <code>height</code> | <code>cols</code> | <code>rows</code> 确定,节点的位置由流程图的行高列宽,以及节点的 <code>row</code> | <code>col</code> 位置确定。</p>\n <p>连线相对路径配置详细介绍:</p>\n <ul>\n <li>'0' 表示从起始节点 1 的位置开始</li>\n <li>'r2' 表示向右画两列宽度的连线</li>\n <li>'c' 表示画一个圆角</li>\n <li>'b2' 表示向下画两行高度的连线</li>\n <li>'c' 再画一个圆角</li>\n <li>'l1' 表示向左画一列宽度的连线</li>\n <li>'c' 再画一个圆角</li>\n <li>'t1' 向上画一行高度的连线</li>\n </ul>\n <p>连线配置参数举例:</p>\n <span>const link = { from: '1', to: '2', p: '0 r2 c b2 c l1 c t1', status: 1 }</span>\n ",
'en-US':
'\n<p>The node supports custom content through the <code>icon</code> | <code>label</code> | <code>content</code> slots, with a default implementation provided for the <code>content</code> slot. Nodes can be configured for row and column placement using the <code>row</code> | <code>col</code> attributes, while connections between nodes can be configured using the <code>p</code> attribute for relative path configuration. Other settings for the flowchart can be configured through the <code>config</code> attribute. The component also provides static methods <code>createItem</code> | <code>createNode</code> | <code>createLink</code> | <code>createConfig</code> for quick option building. The row height and column width of the flowchart are determined by the <code>width</code> | <code>height</code> | <code>cols</code> | <code>rows</code> attributes of the <code>config</code>, while the position of each node is determined by the row height, column width, and the <code>row</code> | <code>col</code> position of the node.</p>\n'
"\n<p>The node supports custom content through the <code>icon</code> | <code>label</code> | <code>content</code> slots, with a default implementation provided for the <code>content</code> slot. Nodes can be configured for row and column placement using the <code>row</code> | <code>col</code> attributes, while connections between nodes can be configured using the <code>p</code> attribute for relative path configuration. Other settings for the flowchart can be configured through the <code>config</code> attribute. The component also provides static methods <code>createItem</code> | <code>createNode</code> | <code>createLink</code> | <code>createConfig</code> for quick option building. The row height and column width of the flowchart are determined by the <code>width</code> | <code>height</code> | <code>cols</code> | <code>rows</code> attributes of the <code>config</code>, while the position of each node is determined by the row height, column width, and the <code>row</code> | <code>col</code> position of the node.</p>\n <p>Detailed introduction to configuring relative paths for connections:</p>\n <ul>\n <li>'0' represents starting from node 1 position.</li>\n <li>'r2' represents drawing a line two columns wide to the right.</li>\n <li>'c' represents drawing a circle.</li>\n <li>'b2' represents a downward line of two height units.</li>\n <li>'c' represents drawing a circle again.</li>\n <li>'l1' represents drawing a line one columns wide to the left.</li>\n <li>'t1' represents a upward line of two height units.</li>\n </ul>\n <p>Example of connection configuration parameters:</p>\n <span>const link = { from: '1', to: '2', p: '0 r2 c b2 c l1 c t1', status: 1 }</span>\n "
},
codeFiles: ['basic-usage.vue']
},
{
demoId: 'custom-style',
name: {
'zh-CN': '自定义样式',
'en-US': 'Custom Style'
},
desc: {
'zh-CN':
'\n<p>通过 <code>config</code> 对象的属性可以自定义流程图的样式,包括:</p>\n<ul>\n<li><code>colors</code>:自定义状态颜色映射</li>\n<li><code>background</code>:画布背景色</li>\n<li><code>font</code>Canvas 字体</li>\n<li><code>radius</code>:圆角半径</li>\n<li><code>thin</code>:是否使用细线模式</li>\n<li><code>iconWrapperSize</code> / <code>iconSize</code> / <code>iconSvgSize</code>:图标尺寸</li>\n<li><code>labelWidth</code> / <code>labelHeight</code> / <code>labelSpacing</code> / <code>labelDateColor</code>:标签样式</li>\n<li><code>listWidth</code> / <code>listIconSize</code> / <code>headSize</code>:人员列表样式</li>\n</ul>\n',
'en-US':
'\n<p>Customize the flowchart style through the <code>config</code> object properties, including <code>colors</code>, <code>background</code>, <code>font</code>, <code>radius</code>, <code>thin</code>, <code>icon*</code>, <code>label*</code>, and <code>list*</code> properties.</p>\n'
},
codeFiles: ['custom-style.vue']
},
{
demoId: 'person-list',
name: {
'zh-CN': '人员列表',
'en-US': 'Person List'
},
desc: {
'zh-CN':
'\n<p>通过 <code>createItem</code> 静态方法创建人员列表项,传入节点的 <code>items</code> 属性中。当 <code>items</code> 数量大于 <code>config.listThreshold</code>(默认 1节点下方会显示人员列表。可以通过 <code>config.listWidth</code>、<code>config.listBorderColor</code>、<code>config.listIconColor</code>、<code>config.listIconSize</code>、<code>config.headSize</code>、<code>config.headUrl</code> 等属性自定义人员列表的样式。</p>\n',
'en-US':
"\n<p>Use the <code>createItem</code> static method to create personnel list items and pass them into the node's <code>items</code> property. When the number of <code>items</code> exceeds <code>config.listThreshold</code> (default 1), a personnel list is displayed below the node. Customize the list style through <code>config.listWidth</code>, <code>config.listBorderColor</code>, <code>config.listIconColor</code>, <code>config.listIconSize</code>, <code>config.headSize</code>, and <code>config.headUrl</code>.</p>\n"
},
codeFiles: ['person-list.vue']
},
{
demoId: 'branch-flow',
name: {
'zh-CN': '分支流程',
'en-US': 'Branch Flow'
},
desc: {
'zh-CN':
'\n<p>展示条件分支流程,使用 <code>row</code> 和 <code>col</code> 控制节点在网格中的位置。连线使用 <code>p</code> 属性控制走向,支持 <code>solid</code>(实线)和 <code>dashed</code>(虚线)两种样式。通过调整 <code>config.width</code>、<code>config.height</code>、<code>config.rows</code>、<code>config.cols</code> 可以扩展画布以容纳更多节点。</p>\n',
'en-US':
'\n<p>Demonstrate conditional branch flows using <code>row</code> and <code>col</code> to control node positions in the grid. Links use the <code>p</code> property to control direction, supporting <code>solid</code> and <code>dashed</code> styles. Adjust <code>config.width</code>, <code>config.height</code>, <code>config.rows</code>, and <code>config.cols</code> to expand the canvas.</p>\n'
},
codeFiles: ['branch-flow.vue']
},
{
demoId: 'slots',
name: {
@ -67,149 +25,75 @@ export default {
'en-US': 'Slots Usage'
},
desc: {
'zh-CN':
'\n<p>节点支持 <code>icon</code>、<code>label</code>、<code>content</code> 三个插槽自定义内容。插槽参数包含 <code>afterNode</code>、<code>node</code>、<code>config</code>、<code>allItem</code>、<code>dropdowns</code>、<code>showPop</code> 等上下文信息。</p>\n',
'en-US':
'\n<p>Nodes support three slots for customization: <code>icon</code>, <code>label</code>, and <code>content</code>. Slot parameters include <code>afterNode</code>, <code>node</code>, <code>config</code>, <code>allItem</code>, <code>dropdowns</code>, and <code>showPop</code>.</p>\n'
'zh-CN': '节点支持 content 插槽自定义内容。',
'en-US': 'The node supports content slots for custom content.'
},
codeFiles: ['slots.vue']
},
{
demoId: 'event-handling',
name: {
'zh-CN': '事件处理',
'en-US': 'Event Handling'
},
desc: {
'zh-CN':
'\n<p>流程图支持 <code>click-node</code>、<code>click-link</code>、<code>click-blank</code> 三种点击事件。事件回调参数包含节点/连线/空白区域的详细信息,可用于实现交互逻辑。</p>\n',
'en-US':
'\n<p>The flowchart supports three click events: <code>click-node</code>, <code>click-link</code>, and <code>click-blank</code>. Event callback parameters contain detailed information about nodes, links, or blank areas.</p>\n'
},
codeFiles: ['event-handling.vue']
},
{
demoId: 'dynamic-update',
name: {
'zh-CN': '动态更新',
'en-US': 'Dynamic Update'
},
desc: {
'zh-CN':
'\n<p>演示如何动态更新流程图的状态。通过修改 <code>data.nodes[i].info.status</code> 和 <code>data.links[i].info.status</code> 可以实时刷新流程图。配合 <code>config.delay</code> 属性可以控制 Canvas 绘制的动画延迟效果。</p>\n',
'en-US':
'\n<p>Demonstrate how to dynamically update flowchart status. Modify <code>data.nodes[i].info.status</code> and <code>data.links[i].info.status</code> to refresh the flowchart in real-time. Use <code>config.delay</code> to control Canvas drawing animation delay.</p>\n'
},
codeFiles: ['dynamic-update.vue']
},
{
demoId: 'custom-link-style',
name: {
'zh-CN': '自定义连线样式',
'en-US': 'Custom Link Style'
},
desc: {
'zh-CN':
'<p>通过 <code>config.styleLink</code> 可以自定义连线的样式。这个函数接收连线数据对象,返回 Canvas 样式对象(如 <code>strokeStyle</code>、<code>lineWidth</code>、<code>shadowColor</code> 等)。<code>config.drawLink</code> 则提供更底层的自定义 Canvas 绘制能力,与<code>config.styleLink</code>互斥。</p>',
'en-US':
'You can customize the style of connections via <code>config.styleLink</code>. This function accepts a connection data object and returns a Canvas style object (e.g., <code>strokeStyle</code>, <code>lineWidth</code>, <code>shadowColor</code>, etc.). <code>config.drawLink</code> provides more granular control over Canvas rendering, mutually exclusive with <code>config.styleLink</code>'
},
codeFiles: ['custom-link-style.vue']
},
{
demoId: 'other',
name: {
'zh-CN': '其他用法',
'en-US': 'Other Usage'
},
desc: {
'zh-CN':
"<p>连线相对路径配置详细介绍:</p>\n <ul>\n <li>'0' 表示从起始节点 1 的位置开始</li>\n <li>'r2' 表示向右画两列宽度的连线</li>\n <li>'c' 表示画一个圆角</li>\n <li>'b2' 表示向下画两行高度的连线</li>\n <li>'c' 再画一个圆角</li>\n <li>'l1' 表示向左画一列宽度的连线</li>\n <li>'c' 再画一个圆角</li>\n <li>'t1' 向上画一行高度的连线</li>\n </ul>\n <p>连线配置参数举例:</p>\n <span>const link = { from: '1', to: '2', p: '0 r2 c b2 c l1 c t1', status: 1 }</span>\n ",
'en-US':
"<p>Detailed introduction to configuring relative paths for connections:</p>\n <ul>\n <li>'0' represents starting from node 1 position.</li>\n <li>'r2' represents drawing a line two columns wide to the right.</li>\n <li>'c' represents drawing a circle.</li>\n <li>'b2' represents a downward line of two height units.</li>\n <li>'c' represents drawing a circle again.</li>\n <li>'l1' represents drawing a line one column wide to the left.</li>\n <li>'t1' represents an upward line of one height unit.</li>\n </ul>\n <p>Example of connection configuration parameters:</p>\n <span>const link = { from: '1', to: '2', p: '0 r2 c b2 c l1 c t1', status: 1 }</span>\n "
},
codeFiles: ['other.vue']
}
],
features: [
{
id: 'slots',
name: '插槽定制',
support: { value: true },
support: {
value: true
},
description: '节点支持 icon、label、content 插槽定制内容。',
cloud: { value: false },
cloud: {
value: false
},
apis: ['icon-slot', 'label-slot', 'content-slot'],
demos: ['slots']
demos: ['basic-usage']
},
{
id: 'node-position',
name: '节点位置',
support: { value: true },
support: {
value: true
},
description: '节点使用 row、col 属性进行行列配置。',
cloud: { value: false },
cloud: {
value: false
},
apis: ['row', 'col'],
demos: ['basic-usage', 'branch-flow']
demos: ['basic-usage']
},
{
id: 'link-path',
name: '连线路径',
support: { value: true },
support: {
value: true
},
description: '连线使用 p 属性进行相对路径配置。',
cloud: { value: false },
cloud: {
value: false
},
apis: ['p'],
demos: ['basic-usage', 'branch-flow']
},
{
id: 'link-style',
name: '连线样式',
support: { value: true },
description: '支持 solid、dashed 两种连线样式,可通过 styleLink 自定义。',
cloud: { value: false },
apis: ['style', 'styleLink', 'styleHoverLink'],
demos: ['branch-flow', 'custom-link-style']
demos: ['basic-usage']
},
{
id: 'config',
name: '流程图配置',
support: { value: true },
description: '流程图的其它设置通过 config 进行配置,包括 width、height、cols、rows、colors、background 等属性。',
cloud: { value: false },
support: {
value: true
},
description: '流程图的其它设置通过 config 进行配置,包括 width、height、cols、rows 等属性。',
cloud: {
value: false
},
apis: ['config'],
demos: ['basic-usage', 'custom-style']
},
{
id: 'person-list',
name: '人员列表',
support: { value: true },
description: '节点支持通过 items 属性显示人员列表,可通过 createItem 快速创建。',
cloud: { value: false },
apis: ['items', 'createItem', 'listWidth', 'listThreshold'],
demos: ['person-list']
},
{
id: 'events',
name: '事件交互',
support: { value: true },
description: '支持 click-node、click-link、click-blank 事件。',
cloud: { value: false },
apis: ['click-node', 'click-link', 'click-blank'],
demos: ['event-handling']
},
{
id: 'dynamic-update',
name: '动态更新',
support: { value: true },
description: '支持动态修改节点和连线状态,实时刷新流程图。',
cloud: { value: false },
apis: ['status'],
demos: ['dynamic-update']
demos: ['basic-usage']
},
{
id: 'static-methods',
name: '静态方法',
support: { value: true },
support: {
value: true
},
description: '组件预置了 createItem、createNode、createLink、createConfig 静态方法,以便于快速构建选项。',
cloud: { value: false },
cloud: {
value: false
},
apis: ['createItem', 'createNode', 'createLink', 'createConfig'],
demos: ['basic-usage']
}

View File

@ -1,27 +0,0 @@
<template>
<tiny-fluent-editor v-model="content" :before-link-open="handleBeforeLinkOpen"></tiny-fluent-editor>
<div class="fluent-editor-demo__before-link-open__tip">
点击编辑器中的链接会弹出确认框点击确定将打开链接点击取消将拦截跳转
</div>
</template>
<script setup>
import { ref } from 'vue'
import { TinyFluentEditor, TinyModal } from '@opentiny/vue'
const content = ref(
'{"ops":[{"insert":"点击访问 "},{"attributes":{"link":"https://opentiny.design"},"insert":"OpenTiny 官网"},{"insert":" 了解更多。"}]}'
)
const handleBeforeLinkOpen = ({ url }) => {
return TinyModal.confirm(`即将打开链接:${url},是否允许跳转?`).then((res) => res === 'confirm')
}
</script>
<style scoped>
.fluent-editor-demo__before-link-open__tip {
margin: 16px 0;
color: #999;
font-size: 14px;
}
</style>

View File

@ -1,25 +0,0 @@
import { test, expect } from '@playwright/test'
test('超链接跳转拦截', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('fluent-editor#before-link-open')
const demo = page.locator('#before-link-open')
const confirmModal = page.locator('.tiny-modal').filter({ hasText: '是否允许跳转' })
// 点击编辑器中的超链接,弹出确认框
await demo.getByText('OpenTiny 官网').click()
await expect(confirmModal).toBeVisible()
// 点击取消,拦截跳转,确认框关闭
await confirmModal.getByRole('button', { name: '取消' }).click()
await expect(confirmModal).toBeHidden()
// 再次点击链接,点击确定放行跳转
await demo.getByText('OpenTiny 官网').click()
await expect(confirmModal).toBeVisible()
const popupPromise = page.waitForEvent('popup', { timeout: 5000 })
await confirmModal.getByRole('button', { name: '确定' }).click()
const popup = await popupPromise
await expect(popup).toBeTruthy()
})

View File

@ -1,35 +0,0 @@
<template>
<tiny-fluent-editor v-model="content" :before-link-open="handleBeforeLinkOpen"></tiny-fluent-editor>
<div class="fluent-editor-demo__before-link-open__tip">
点击编辑器中的链接会弹出确认框点击确定将打开链接点击取消将拦截跳转
</div>
</template>
<script>
import { TinyFluentEditor, TinyModal } from '@opentiny/vue'
export default {
components: {
TinyFluentEditor
},
data() {
return {
content:
'{"ops":[{"insert":"点击访问 "},{"attributes":{"link":"https://opentiny.design"},"insert":"OpenTiny 官网"},{"insert":" 了解更多。"}]}'
}
},
methods: {
handleBeforeLinkOpen({ url }) {
return TinyModal.confirm(`即将打开链接:${url},是否允许跳转?`).then((res) => res === 'confirm')
}
}
}
</script>
<style scoped>
.fluent-editor-demo__before-link-open__tip {
margin: 16px 0;
color: #999;
font-size: 14px;
}
</style>

View File

@ -78,19 +78,6 @@ export default {
'en-US': ''
},
codeFiles: ['before-editor-init.vue']
},
{
demoId: 'before-link-open',
name: {
'zh-CN': '超链接跳转拦截',
'en-US': ''
},
desc: {
'zh-CN':
'<p>通过 <code>before-link-open</code> 拦截富文本中超链接的跳转。该属性接收一个回调函数,点击链接时会传入 <code>url</code>、<code>rawUrl</code>、<code>target</code> 等参数。返回 <code>false</code>(或 Promise resolve false可拦截跳转返回 <code>true</code> 或 <code>undefined</code> 继续跳转。<br>本示例使用 <code>TinyModal.confirm</code> 弹出确认框,点击「确定」打开链接,点击「取消」拦截跳转。</p>',
'en-US': ''
},
codeFiles: ['before-link-open.vue']
}
],
features: [
@ -171,19 +158,6 @@ export default {
},
apis: ['before-editor-init'],
demos: ['before-editor-init']
},
{
id: 'before-link-open',
name: '超链接跳转拦截',
support: {
value: true
},
description: '通过 before-link-open 拦截富文本中超链接的跳转支持同步和异步Promise拦截。',
cloud: {
value: false
},
apis: ['before-link-open'],
demos: ['before-link-open']
}
]
}

View File

@ -128,7 +128,7 @@ export default {
'en-US': 'Custom String Length'
},
desc: {
'zh-CN': '<p>通过 <code>rules</code> 的 <code>regular</code> 进行自定义字符串长度</p>',
'zh-CN': '<p>通过 <code>rules</code> 的 <code>regular</code> 进行自定义字符串长度3.28.0版本新增)</p>',
'en-US': '<p>Customize string length using the <code>regular</code> method of <code>rules</code>. </p>'
},
codeFiles: ['custom-validation-string-length.vue']

View File

@ -38,7 +38,7 @@ import { TinyGrid, TinyGridColumn, TinyGridToolbar, TinyModal } from '@opentiny/
const toolbarButtons = ref([
{
code: 'clearSelected',
code: ' clearSelected',
name: '手动清除单元格选中状态'
}
])

View File

@ -45,7 +45,7 @@ export default {
return {
toolbarButtons: [
{
code: 'clearSelected',
code: ' clearSelected',
name: '手动清除单元格选中状态'
}
],

View File

@ -1,68 +0,0 @@
<template>
<div>
<tiny-grid :data="tableData" auto-resize>
<tiny-grid-column type="index" width="60"></tiny-grid-column>
<tiny-grid-column type="operation" title="操作" :operation-config="operationConfig"></tiny-grid-column>
<tiny-grid-column field="name" title="名称"></tiny-grid-column>
<tiny-grid-column field="area" title="所属区域"></tiny-grid-column>
<tiny-grid-column field="address" title="地址"></tiny-grid-column>
<tiny-grid-column field="introduction" title="公司简介" show-overflow></tiny-grid-column>
</tiny-grid>
</div>
</template>
<script setup>
import { TinyGrid, TinyGridColumn, TinyModal } from '@opentiny/vue'
import { IconAreaChart, IconBarChart, IconDotChart, IconLineChart, IconPieChart } from '@opentiny/vue-icon'
import { ref } from 'vue'
function clickHandler(e, { row, buttonConfig }) {
TinyModal.message(`点击按钮 - ${row.name} - ${buttonConfig.name}`)
row.flag = !row.flag
}
function clickHandler2(e, { row, buttonConfig }) {
TinyModal.message(`点击按钮 - ${row.name} - ${buttonConfig.name}`)
}
const operationConfig = ref({
buttons: [
{ name: '操作1', icon: IconAreaChart(), click: clickHandler, hidden: (row) => row.flag === true },
{ name: '操作2', icon: IconBarChart(), click: clickHandler, hidden: (row) => row.flag === false },
{
name: '操作3',
icon: IconDotChart(),
click: clickHandler2,
disabled: false,
class: 'fill-color-icon-active text-color-text-placeholder'
},
{ name: '操作4', icon: IconLineChart(), click: clickHandler2, disabled: () => false },
{ name: '操作5', icon: IconPieChart(), click: clickHandler2, hidden: false }
]
})
const tableData = ref([
{
id: '1',
name: 'GFD科技有限公司',
area: '华东区',
address: '福州',
introduction: '公司技术和研发实力雄厚是国家863项目的参与者并被政府认定为“高新技术企业”。',
flag: true
},
{
id: '2',
name: 'WWWW科技有限公司',
area: '华南区',
address: '深圳福田区',
introduction: '公司技术和研发实力雄厚是国家863项目的参与者并被政府认定为“高新技术企业”。',
flag: true
},
{
id: '3',
name: 'RFV有限责任公司',
area: '华南区',
address: '中山市',
introduction: '公司技术和研发实力雄厚是国家863项目的参与者并被政府认定为“高新技术企业”。',
flag: true
}
])
</script>

Some files were not shown because too many files have changed in this diff Show More