Compare commits
No commits in common. "dev" and "dev" have entirely different histories.
|
|
@ -1,38 +0,0 @@
|
|||
---
|
||||
name: tinyvue-develop-spec
|
||||
description: tinyvue组件库开发规范
|
||||
---
|
||||
|
||||
## 使用时机
|
||||
|
||||
在当前仓库进行组件的开发,主题开发,国际化开发,测试脚本开发时,必须遵守以下规范。
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 新增 UI 组件
|
||||
- 修改现有组件逻辑
|
||||
- 重构组件架构
|
||||
- 理解 Renderless 模式
|
||||
|
||||
### 重要术语
|
||||
|
||||
1. 跨端模板: tiny vue组件库中,同一个组件可以有2种组件模板, 其中`pc.vue` 面向家用PC浏览器的场景, `mobile-first.vue`是移动优先的浏览器场景(可以兼容用户使用手机或PC浏览器)。
|
||||
|
||||
2. 无渲染逻辑层: tiny vue组件库遵从模板和逻辑分离的原则进行开发。模板层只负责绑定变量和函数,所有的业务逻辑(data 处理、状态计算等)都要放在无渲染逻辑层 `packages/renderless` 目录
|
||||
|
||||
## 组件库的架构
|
||||
|
||||
| 模块名称 | 代码目录 | 参考规范 | 说明 |
|
||||
| --------------- | ----------------------------------------------------------------------------- | -------------------------- | -------------------------------------------- |
|
||||
| 模板层 | `packages/vue` | `./vue.skill.md` | 每个组件的模板,必须在相应的组件目录 |
|
||||
| 无渲染逻辑层 | `packages/renderless` | `./renderless.skill.md` | 每个组件的逻辑,必须在相应的组件目录 |
|
||||
| 样式层 | `packages/theme` | `./theme.skill.md` | 每个组件的样式,必须在相应的组件目录 |
|
||||
| 设计规范层 | `packages/design` | `./design.skill.md` | 交互规范、默认 props、图标与 renderless 扩展 |
|
||||
| 适配Vue2/Vue3层 | `packages/vue-common` | `./common.skill.md` | 非必要不修改适配层代码 |
|
||||
| 图标库 | `packages/vue-icon` | `./icon.skill.md` | 图标库 |
|
||||
| 国际化层 | `packages/vue-locale` | `./i18n-workflow.skill.md` | 组件的国际化 |
|
||||
| 公用hooks | `packages/vue-hooks` | `./hooks.skill.md` | 跨组件可以复用的hooks函数 |
|
||||
| 公用指令 | `packages/vue-directive` | `./directive.skill.md` | 跨组件可以复用的指令 |
|
||||
| utils | `packages/utils` | `./utils.skill.md` | 公用函数 |
|
||||
| 组件测试 | `examples/sites/demos/pc/app/**/*.spec.ts` 或 `packages/vue/src/**/__tests__` | `./testing-guide.skill.md` | e2e测试和单元测试 |
|
||||
| 文档开发 | `examples/sites/demos/pc/app/**` | `./vue.skill.md` | 每一个组件的 api 和 demo 的开发 |
|
||||
|
|
@ -1,186 +0,0 @@
|
|||
# @opentiny/vue-common 适配层开发规范
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 理解 TinyVue 如何同时支持 Vue 2.6 / 2.7 / Vue 3
|
||||
- 编写或调试组件 `setup`、`$setup`、跨端模板选择
|
||||
- 使用 `defineComponent`、`hooks`、`directive`、`svg`、`mergeClass` 等适配 API
|
||||
- 排查「仅在某一 Vue 版本下复现」的问题
|
||||
|
||||
## 核心原则
|
||||
|
||||
### 1. 包定位
|
||||
|
||||
`packages/vue-common` 发布为 `@opentiny/vue-common`,是整个组件库的 **Vue 版本适配与运行时胶水层**:
|
||||
|
||||
- 通过 `virtual:common/adapter/vue` 在构建时指向 `adapter/vue2` | `vue2.7` | `vue3`
|
||||
- 向 renderless 提供统一的 `hooks` 对象(即对应 Vue 版本的 API)
|
||||
- 提供 `$setup` / `setup`,连接模板与 renderless
|
||||
- 提供主题/模式解析、设计规范注入、图标 `svg` 包装等
|
||||
|
||||
**修改约束**:表结构变更、新 Vue 版本支持需充分评估;日常组件开发**优先改 renderless / vue 模板**,避免随意改动适配层行为。`SKILL.md` 中亦提示:非必要不修改适配层。
|
||||
|
||||
### 2. 目录结构
|
||||
|
||||
```text
|
||||
packages/vue-common/src/
|
||||
├── index.ts # 对外主入口:$setup、setup、$prefix、svg 等
|
||||
├── adapter/
|
||||
│ ├── index.ts # 导出当前 Vue 版本 adapter
|
||||
│ ├── vue2/
|
||||
│ ├── vue2.7/
|
||||
│ └── vue3/
|
||||
├── breakpoint.ts # useBreakpoint 响应式断点
|
||||
├── csscls.ts # 类名序列化、去重
|
||||
├── usedefer.ts # useDefer
|
||||
└── generateIcon.ts # 渐变图标 id 处理
|
||||
```
|
||||
|
||||
### 3. 组件开发中最常用的 API
|
||||
|
||||
| API | 用途 |
|
||||
| ---------------------------------- | ----------------------------------------------------------------- |
|
||||
| `$prefix` | 组件名前缀 `'Tiny'`,如 `TinyButton` |
|
||||
| `$props` / `props` | 框架保留 prop:`tiny_mode`、`tiny_renderless`、`tiny_template` 等 |
|
||||
| `defineComponent` | 定义组件(跨版本) |
|
||||
| `$setup` | 跨端父组件:根据 `tiny_mode` 选择 `pc` / `mobile-first` 模板 |
|
||||
| `setup` | 子模板中连接 renderless,返回模板绑定对象 |
|
||||
| `hooks` | 当前 Vue 版本的 API 集合,可传给 renderless 第二参数 |
|
||||
| `isVue2` / `isVue3` | 版本判断(renderless 中应尽量避免,优先用 utils/vm) |
|
||||
| `directive` | 统一 Vue2/3 指令钩子名 |
|
||||
| `svg` | 包装 `@opentiny/vue-theme` 的 svg 为图标组件 |
|
||||
| `mergeClass` | mobile-first 下合并 Tailwind 类名 |
|
||||
| `filterAttrs`(模板中 `a`) | 过滤 `$attrs` 绑定 |
|
||||
| `Teleport` / `KeepAlive` | 须从此包导入以保证兼容 |
|
||||
| `useBreakpoint` / `useDefer` | 布局与渲染优化 |
|
||||
| `useInstanceSlots` / `useRelation` | 已注入 `isVue2` 的 hooks 封装 |
|
||||
|
||||
### 4. 双层组件:$setup + 子模板 setup
|
||||
|
||||
**跨端组件**(`pc.vue` + `mobile-first.vue`):
|
||||
|
||||
```typescript
|
||||
// packages/vue/src/alert/src/index.ts
|
||||
import { $props, $prefix, $setup, defineComponent } from '@opentiny/vue-common'
|
||||
import template from 'virtual-template?pc|mobile-first'
|
||||
|
||||
export default defineComponent({
|
||||
name: $prefix + 'Alert',
|
||||
props: alertProps,
|
||||
setup(props, context) {
|
||||
return $setup({ props, context, template })
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
`$setup` 会:
|
||||
|
||||
1. `resolveMode` 解析 `tiny_mode`(`pc` | `mobile` | `mobile-first`)
|
||||
2. 通过 `virtual-template` 插件加载对应模板
|
||||
3. `renderComponent` 渲染子组件,并合并设计规范 `designConfig` 中的默认 props
|
||||
|
||||
**具体模板**(`pc.vue`):
|
||||
|
||||
```typescript
|
||||
import { setup, defineComponent, props } from '@opentiny/vue-common'
|
||||
import { renderless, api } from '@opentiny/vue-renderless/alert/vue'
|
||||
import type { IAlertApi } from '@opentiny/vue-renderless/types/alert.type'
|
||||
|
||||
export default defineComponent({
|
||||
props: [...props, 'type', 'size' /* 其它组件 props */],
|
||||
setup(props, context) {
|
||||
return setup({ props, context, renderless, api }) as unknown as IAlertApi
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 5. setup 与 renderless 的协作
|
||||
|
||||
`setup` 内部流程概要:
|
||||
|
||||
1. 选择 `props.tiny_renderless` 或入参 `renderless`
|
||||
2. 构造 `utils`(含 `vm`、`emit`、`t`、`designConfig`、`mode`、`mergeClass` 等)
|
||||
3. 调用 `render(props, hooks, utils, extendOptions)` 得到 `sdk`
|
||||
4. 按 `api` 数组将 `sdk` 上的方法/状态暴露给模板(`attrs`)
|
||||
5. 双层组件默认 `mono: false`,将 api 同步到父组件 ref;单层组件传 `mono: true`
|
||||
|
||||
模板中常用简写:
|
||||
|
||||
- `t('ui.xxx')` — 国际化
|
||||
- `a($attrs, filters, include)` — `filterAttrs`
|
||||
- `m(...)` — `mergeClass`
|
||||
- `f` / `d` / `dp` — 过滤器与实例属性定义(见 renderless.skill.md)
|
||||
|
||||
### 6. 模式与主题解析
|
||||
|
||||
- **模式** `resolveMode`:`tiny_mode` prop > inject `TinyMode` > 全局 config > 默认 `'pc'`
|
||||
- **主题** `resolveTheme`:`'tiny'` | `'saas'`
|
||||
- 根组件可设 `tiny_mode_root` 以 `provide('TinyMode', mode)`
|
||||
|
||||
函数式组件(Modal、Loading、Notify)依赖全局 `tiny_mode`,需在应用级配置。
|
||||
|
||||
### 7. 设计规范 designConfig
|
||||
|
||||
适配层负责注入与合并 `designConfig`,详细约定见 [design.skill.md](./design.skill.md)。
|
||||
|
||||
```typescript
|
||||
import { provideDesignConfig } from '@opentiny/vue-common'
|
||||
|
||||
provideDesignConfig({
|
||||
components: {
|
||||
Button: { props: { round: true }, api: [], renderless: fn }
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
`setup` 会合并 `designConfig.renderless` 与组件级 props 默认值。
|
||||
|
||||
### 8. 图标 svg 工厂
|
||||
|
||||
```typescript
|
||||
import { svg } from '@opentiny/vue-common'
|
||||
import IconX from '@opentiny/vue-theme/svgs/icon-x.svg'
|
||||
|
||||
export default () => svg({ name: 'IconX', component: IconX, filledComponent: IconX })()
|
||||
```
|
||||
|
||||
详见 [icon.skill.md](./icon.skill.md)。
|
||||
|
||||
### 9. 版本判断与 hooks 使用
|
||||
|
||||
```typescript
|
||||
// ❌ renderless 中
|
||||
if (process.env.VUE_VERSION === '3') { ... }
|
||||
|
||||
// ✅ 模板/极少数适配代码
|
||||
import { isVue2, hooks } from '@opentiny/vue-common'
|
||||
|
||||
// ✅ renderless 接收第二参数
|
||||
export const renderless = (props, { computed, reactive, watch }, utils) => { ... }
|
||||
```
|
||||
|
||||
### 10. 禁止事项
|
||||
|
||||
- ❌ 组件模板中不要 `import from 'vue'`(使用 `@opentiny/vue-common` 的 `defineComponent`、`hooks`、`Teleport` 等)
|
||||
- ❌ renderless 中不要依赖适配层实现细节(仅使用文档化的 `utils` / `vm` 字段)
|
||||
- ❌ 不要随意修改 `adapter/` 下生命周期映射逻辑
|
||||
- ❌ 不要在 `.vue` 中写复杂业务逻辑,应下沉 renderless
|
||||
|
||||
## 与其它规范的关系
|
||||
|
||||
| 模块 | 规范文件 |
|
||||
| ---------- | -------------------------------------------- |
|
||||
| 模板层 | [vue.skill.md](./vue.skill.md) |
|
||||
| 逻辑层 | [renderless.skill.md](./renderless.skill.md) |
|
||||
| 样式 | [theme.skill.md](./theme.skill.md) |
|
||||
| 设计规范 | [design.skill.md](./design.skill.md) |
|
||||
| 工具函数 | [utils.skill.md](./utils.skill.md) |
|
||||
| 组合式逻辑 | [hooks.skill.md](./hooks.skill.md) |
|
||||
| 指令 | [directive.skill.md](./directive.skill.md) |
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [主入口 setup / $setup](../../packages/vue-common/src/index.ts)
|
||||
- [Vue3 适配器](../../packages/vue-common/src/adapter/vue3/index.ts)
|
||||
- [Alert 跨端入口](../../packages/vue/src/alert/src/index.ts)
|
||||
- [Alert pc 模板 setup](../../packages/vue/src/alert/src/pc.vue)
|
||||
|
|
@ -1,261 +0,0 @@
|
|||
# TinyVue 设计规范(Design)开发指南
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 为 Aurora / SaaS / 企业自定义规范配置组件默认行为
|
||||
- 通过 `design` 覆盖图标、默认 props、renderless 扩展逻辑
|
||||
- 使用 `TinyConfigProvider` 在应用级注入交互规范
|
||||
- 在 renderless 中读取 `designConfig` 实现规范差异
|
||||
|
||||
## 与 Theme 的区别
|
||||
|
||||
| 维度 | Design(本规范) | Theme(见 [theme.skill.md](./theme.skill.md)) |
|
||||
| ---------- | -------------------------------------------- | ---------------------------------------------- |
|
||||
| 职责 | 交互行为、默认 props、图标、renderless 扩展 | 视觉样式、CSS 变量、Less |
|
||||
| 代码位置 | `packages/design/*`、`designConfig` | `packages/theme`、`packages/theme-saas` |
|
||||
| 注入方式 | `TinyConfigProvider` / `provideDesignConfig` | 引入 Less、`theme` prop、ThemeTool |
|
||||
| 运行时对象 | `utils.designConfig`(renderless 第三参数) | `tiny_theme`、`--tv-*` CSS 变量 |
|
||||
|
||||
二者可组合使用:SaaS 场景常见 `tiny_theme="saas"` + `@opentiny/vue-design-saas`。
|
||||
|
||||
## 架构概览
|
||||
|
||||
```text
|
||||
packages/design/
|
||||
├── aurora/ # Aurora 规范(@opentiny/vue-design-aurora)
|
||||
│ ├── index.ts # 导出 { name, version, components }
|
||||
│ └── src/<Component>/index.ts
|
||||
├── saas/ # SaaS 规范(@opentiny/vue-design-saas)
|
||||
│ └── src/<Component>/index.ts
|
||||
└── smb/ # SMB 示例规范(文档 demo 用)
|
||||
|
||||
packages/vue/src/config-provider/ # TinyConfigProvider
|
||||
packages/vue-common/src/index.ts # provideDesignConfig、getDesignConfig、setup 合并逻辑
|
||||
packages/vue-saas-common/ # 预置 customDesignConfig.designConfig = designSaasConfig
|
||||
```
|
||||
|
||||
全局配置结构(`DesignConfig`):
|
||||
|
||||
```typescript
|
||||
{
|
||||
name?: string // 规范名称,如 'saas'
|
||||
version?: string // 规范版本
|
||||
components?: {
|
||||
[ComponentName: string]: IComponentDesignConfig // 键名不含 Tiny 前缀,如 Button、Select、Alert
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
单组件配置(内部类型 `IComponentDesignConfig`,定义于 `packages/renderless/types/shared.type.ts`,未对外 export)常用字段:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| ------------ | ---------------------------------------------------------------------------------------- |
|
||||
| `props` | 默认 props;用户未传时才覆盖(仅在跨端父组件 **`$setup`** 中合并到 `customDesignProps`) |
|
||||
| `icons` | 图标映射,如 `{ warning: iconWarning() }` |
|
||||
| `state` | 规范级状态默认值,renderless 内通过 `designConfig.state` 读取 |
|
||||
| `renderless` | 扩展函数,在组件 renderless 执行后合并进 `sdk` |
|
||||
| `api` | 扩展暴露给模板的 api 名称列表(与组件 `api` 数组合并) |
|
||||
| 其它 | 组件自定义字段,如 `baseOpts`、`messageType`、`showText` 等 |
|
||||
|
||||
## 应用侧使用
|
||||
|
||||
### 1. ConfigProvider(推荐)
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<tiny-config-provider :design="design">
|
||||
<tiny-alert type="warning" />
|
||||
<tiny-button>按钮</tiny-button>
|
||||
</tiny-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { TinyConfigProvider, TinyAlert, TinyButton } from '@opentiny/vue'
|
||||
import { iconWarningTriangle } from '@opentiny/vue-icon'
|
||||
|
||||
const design = {
|
||||
name: 'x-design',
|
||||
version: '1.0.0',
|
||||
components: {
|
||||
Alert: {
|
||||
icons: { warning: iconWarningTriangle() },
|
||||
props: { center: true },
|
||||
renderless: (props, hooks, utils, api) => ({
|
||||
handleClose() {
|
||||
api.state.show = false
|
||||
utils.emit('close')
|
||||
}
|
||||
})
|
||||
},
|
||||
Button: {
|
||||
props: { round: true, resetTime: 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
`TinyConfigProvider` 内部调用 `provideDesignConfig(design)`,子树组件通过 `inject` 获取。
|
||||
|
||||
### 2. 内置规范包(SaaS / Aurora)
|
||||
|
||||
SaaS 工程使用 `@opentiny/vue-saas-common` 时已注入:
|
||||
|
||||
```typescript
|
||||
// packages/vue-saas-common/src/index.ts
|
||||
import { customDesignConfig } from '@opentiny/vue-common'
|
||||
import designSaasConfig from '@opentiny/vue-design-saas'
|
||||
|
||||
customDesignConfig.designConfig = designSaasConfig
|
||||
```
|
||||
|
||||
Aurora 对应 `@opentiny/vue-design-aurora`,在应用入口赋值 `customDesignConfig.designConfig`(写法同 SaaS)。
|
||||
|
||||
> **注意**:`@opentiny/vue-saas-common` 启动时已写入 `customDesignConfig.designConfig`,会**优先于**子树 `TinyConfigProvider` 的 `inject` 配置。应用级临时覆盖需改 `customDesignConfig`,或勿使用 `vue-saas-common` 的全局注入。普通 `@opentiny/vue` 场景下 `customDesignConfig.designConfig` 默认为 `null`,以 `ConfigProvider` 的 `provide` 为准。
|
||||
|
||||
### 3. 编程式注入
|
||||
|
||||
须在组件 `setup` 中调用(需 Vue 上下文):
|
||||
|
||||
```typescript
|
||||
import { provideDesignConfig } from '@opentiny/vue-common'
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
provideDesignConfig({
|
||||
components: {
|
||||
Form: { props: { hideRequiredAsterisk: true } }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`TinyConfigProvider` 传入的 `design` 为响应式 ref,`getDesignConfig` 会通过 `.value` 解包后读取。
|
||||
|
||||
## 运行时解析流程
|
||||
|
||||
1. `getDesignConfig()`:**优先** `customDesignConfig.designConfig`,否则 `inject(design.configKey, {})`;若结果为 ref/computed 则取 `.value`
|
||||
2. 按当前组件名去掉 `Tiny` 前缀(`getComponentName().replace('Tiny', '')`),取 `globalDesignConfig.components[ComponentName]`
|
||||
3. **`$setup`**:合并 `designConfig.props` 为默认 props(不覆盖用户已传属性)
|
||||
4. **`setup`**:
|
||||
- 将 `designConfig`、`globalDesignConfig` 放入 renderless 第三参数 `utils`
|
||||
- 执行组件 `renderless` 得到 `sdk`
|
||||
- 若存在 `designConfig.renderless`,将其返回值 `Object.assign` 到 `sdk`
|
||||
- 若 `designConfig.api` 存在,与组件 `api` 数组合并后暴露给模板
|
||||
|
||||
组件名映射示例:`TinySelect` → `Select`,`TinyAlert` → `Alert`。
|
||||
|
||||
## 在 renderless 中使用 designConfig
|
||||
|
||||
### 1. 从 utils 解构(推荐)
|
||||
|
||||
```typescript
|
||||
export const renderless = (
|
||||
props: IAlertProps,
|
||||
hooks: ISharedRenderlessParamHooks,
|
||||
{ designConfig, t, emit }: IAlertRenderlessParamUtils
|
||||
): IAlertApi => {
|
||||
// ...
|
||||
}
|
||||
|
||||
// index.ts 纯函数
|
||||
export const computedGetIcon =
|
||||
({ constants, props, designConfig }: Pick<IAlertRenderlessParams, 'constants' | 'props' | 'designConfig'>) =>
|
||||
() => {
|
||||
const designIcon = designConfig?.icons?.[props.type]
|
||||
return props.icon || designIcon || constants.ICON_MAP[props.type]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 读取 state / props 级配置
|
||||
|
||||
```typescript
|
||||
// select/vue.ts 示例
|
||||
autoHideDownIcon: (() => {
|
||||
if (designConfig?.state && 'autoHideDownIcon' in designConfig.state) {
|
||||
return designConfig.state.autoHideDownIcon
|
||||
}
|
||||
return true
|
||||
})(),
|
||||
designConfig // 挂到 state,供模板 state.designConfig?.icons 使用
|
||||
```
|
||||
|
||||
### 3. designConfig.renderless 扩展
|
||||
|
||||
规范包可覆盖或增补 api 方法,**不得**破坏原有 `api.state` 引用:
|
||||
|
||||
```typescript
|
||||
// packages/design/saas/src/select/index.ts
|
||||
// 签名:(props, hooks, utils, sdk) => Partial<api>
|
||||
renderless: (props, hooks, utils, api) => {
|
||||
const state = api.state
|
||||
return {
|
||||
toggleCheckAll: (filtered) => {
|
||||
/* 规范定制逻辑 */
|
||||
},
|
||||
computedShowTagText: () => state.isDisabled || state.isDisplayOnly
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
适配层实际调用:`Object.assign(sdk, designConfig.renderless(props, hooks, utils, sdk))`,第三参为完整 `utils`(含 `emit`、`designConfig`、`vm` 等),第四参为组件 renderless 已返回的 `sdk`。
|
||||
|
||||
## 新增 / 修改设计规范
|
||||
|
||||
### 在 packages/design 中增加组件配置
|
||||
|
||||
1. 在 `packages/design/saas/src/<component>/index.ts`(或 `aurora`)新增默认导出对象
|
||||
2. 在对应 `index.ts` 的 `components` 中注册(键名与组件名一致,PascalCase,无 `Tiny` 前缀)
|
||||
3. 图标优先使用对应主题的 icon 包(SaaS 用 `@opentiny/vue-icon-saas`)
|
||||
4. 在 renderless 中增加对 `designConfig` 的可选读取,并提供合理默认值(无 design 时行为不变)
|
||||
|
||||
示例(SaaS Alert 仅换图标):
|
||||
|
||||
```typescript
|
||||
// packages/design/saas/src/alert/index.ts
|
||||
import { iconWarning } from '@opentiny/vue-icon-saas'
|
||||
|
||||
export default {
|
||||
icons: {
|
||||
warning: iconWarning()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 在组件 renderless 中支持新 design 字段
|
||||
|
||||
1. 在 `index.ts` 纯函数中增加 `designConfig` 参数类型(`Pick<..., 'designConfig'>`)
|
||||
2. 使用可选链与 `in` 判断,避免假设 design 一定存在
|
||||
3. 模板需访问时,将 `designConfig` 挂到 `state`(参考 Select)
|
||||
|
||||
## mobile-first 与 twMerge
|
||||
|
||||
SaaS 多端模式依赖 `customDesignConfig.twMerge`(通常 `tailwind-merge`):
|
||||
|
||||
```typescript
|
||||
import { customDesignConfig } from '@opentiny/vue-common'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
customDesignConfig.twMerge = twMerge
|
||||
```
|
||||
|
||||
`mergeClass`(模板中的 `m(...)`)会经此函数合并 Tailwind 类名。
|
||||
|
||||
## 禁止事项
|
||||
|
||||
- ❌ 不要用 design 配置颜色/尺寸等纯样式(应走 theme / CSS 变量)
|
||||
- ❌ 不要在 design 的 `components` 键名中带 `Tiny` 前缀
|
||||
- ❌ 不要在 `designConfig.renderless` 中替换整个 `api` 或 `state` 对象
|
||||
- ❌ 不要在 renderless 中 `import` 规范包;规范由应用或 `vue-saas-common` 注入
|
||||
- ❌ 不要假设 `designConfig` 一定存在,必须提供默认行为
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [ConfigProvider 组件](../../packages/vue/src/config-provider/src/index.vue)
|
||||
- [适配层 design 逻辑](../../packages/vue-common/src/index.ts)
|
||||
- [类型定义](../../packages/renderless/types/shared.type.ts)
|
||||
- [SaaS Select 规范示例](../../packages/design/saas/src/select/index.ts)
|
||||
- [文档 demo](../../examples/sites/demos/pc/app/config-provider/base-composition-api.vue)
|
||||
- [主题规范](./theme.skill.md)
|
||||
- [适配层规范](./common.skill.md)
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
# @opentiny/vue-directive 开发规范
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 模板层需要声明式 DOM 行为(点击外部关闭、无限滚动、文本高亮、自动 Tooltip 等)
|
||||
- 逻辑与具体组件解耦,可在多个 `pc.vue` / `mobile-first.vue` 中复用
|
||||
- 不适合放入 renderless 的、强依赖 DOM 或全局监听的交互
|
||||
|
||||
## 核心原则
|
||||
|
||||
### 1. 包定位
|
||||
|
||||
`packages/vue-directive` 发布为 `@opentiny/vue-directive`。
|
||||
|
||||
- 指令实现放在 `src/<directive-name>.ts`
|
||||
- 在 `index.ts` 统一导出
|
||||
- 可依赖 `@opentiny/utils`、`@opentiny/vue-common`(如 `auto-tip` 使用 Tooltip 与 `hooks`)
|
||||
- **业务逻辑仍应优先下沉 renderless**;指令只负责 DOM 绑定与事件桥接
|
||||
|
||||
### 2. 内置指令一览
|
||||
|
||||
| 导出 | 指令名 | 说明 |
|
||||
| ------------------- | ---------------------- | ----------------------------------------------------------- |
|
||||
| `Clickoutside` | `v-clickoutside` | 点击元素外部触发回调,支持 `.mousedown` / `.mouseup` 修饰符 |
|
||||
| `AutoTip` | `v-auto-tip` | 文本溢出时自动展示 Tooltip |
|
||||
| `InfiniteScroll` | `v-infinite-scroll` | 滚动到底加载更多 |
|
||||
| `HighlightQuery` | `v-highlight-query` | 高亮匹配关键字 |
|
||||
| `ObserveVisibility` | `v-observe-visibility` | 元素可见性监听 |
|
||||
| `RepeatClick` | `v-repeat-click` | 长按/重复点击 |
|
||||
|
||||
文档示例:[自定义指令](https://opentiny.design/tiny-vue/zh-CN/smb-theme/components/directives-auto-tip)
|
||||
|
||||
### 3. 在组件模板中注册(必须)
|
||||
|
||||
指令在 **vue 模板层** 注册,不在 renderless 中注册。
|
||||
|
||||
**Vue 3 写法**(推荐通过 `directive` 适配函数兼容 Vue 2 生命周期名):
|
||||
|
||||
```vue
|
||||
<script lang="ts">
|
||||
import { setup, directive, defineComponent } from '@opentiny/vue-common'
|
||||
import { Clickoutside, AutoTip } from '@opentiny/vue-directive'
|
||||
|
||||
export default defineComponent({
|
||||
directives: directive({ Clickoutside, AutoTip }),
|
||||
setup(props, context) {
|
||||
return setup({ props, context, renderless, api })
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-clickoutside="handleClose" v-auto-tip>...</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
**仅含 Vue 2 钩子(`bind`/`inserted`/`unbind`)的指令**,应通过 `directive({ ... })` 包装。部分指令(如 `ObserveVisibility`、`HighlightQuery`)已在实现内同时声明 Vue 2 / Vue 3 钩子,可直接注册:`directives: { ObserveVisibility }`。
|
||||
|
||||
`directive` 辅助函数位于适配层,用于统一 Vue 2 / Vue 3 指令钩子名:
|
||||
|
||||
```typescript
|
||||
// packages/vue-common/src/adapter/vue3/index.ts
|
||||
mapping(content, 'bind', 'beforeMount')
|
||||
mapping(content, 'inserted', 'mounted')
|
||||
mapping(content, 'update', 'updated')
|
||||
mapping(content, 'unbind', 'unmounted')
|
||||
```
|
||||
|
||||
### 4. 指令实现规范
|
||||
|
||||
#### 指令钩子写法
|
||||
|
||||
**推荐(新指令)**:实现 `bind` / `update` / `unbind`,由 `directive()` 映射为 Vue 3 的 `beforeMount` / `updated` / `unmounted`:
|
||||
|
||||
**特例 `RepeatClick`**:导出为函数而非对象,需手动包装:
|
||||
|
||||
```typescript
|
||||
import { RepeatClick } from '@opentiny/vue-directive'
|
||||
|
||||
directives: {
|
||||
repeatClick: {
|
||||
bind: RepeatClick
|
||||
} // 模板中使用 v-repeat-click
|
||||
}
|
||||
```
|
||||
|
||||
**特例 `HighlightQuery` / `ObserveVisibility`**:实现内已同时声明 Vue 2 / Vue 3 钩子,可直接 `directives: { HighlightQuery }`,无需再经 `directive()` 转换。
|
||||
|
||||
新指令推荐模板:
|
||||
|
||||
```typescript
|
||||
import { on, isServer } from '@opentiny/utils'
|
||||
|
||||
export default {
|
||||
bind(el, binding, vnode) {
|
||||
// 初始化
|
||||
},
|
||||
update(el, binding, vnode) {
|
||||
// 更新 binding
|
||||
},
|
||||
unbind(el) {
|
||||
// 清理监听、移除 DOM 副作用
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 全局监听
|
||||
|
||||
若需 `document` 级监听(如 `Clickoutside`),在模块顶层用 `isServer` 守卫,维护共享 `nodeList`,在 `unbind` 中务必移除引用,防止泄漏。
|
||||
|
||||
#### Shadow DOM
|
||||
|
||||
点击外部判断需使用 `event.composedPath()`,参考 `clickoutside.ts`。
|
||||
|
||||
### 5. 典型用法示例
|
||||
|
||||
**Clickoutside**
|
||||
|
||||
```html
|
||||
<!-- 默认:外部按下并松开才触发 -->
|
||||
<div v-clickoutside="handleClose"></div>
|
||||
<!-- 修饰符 -->
|
||||
<div v-clickoutside.mousedown="handleClose"></div>
|
||||
<div v-clickoutside.mouseup="handleClose"></div>
|
||||
```
|
||||
|
||||
**AutoTip**
|
||||
|
||||
```html
|
||||
<div v-auto-tip>长文本...</div>
|
||||
<div v-auto-tip="{ content: '自定义', placement: 'top', effect: 'dark' }"></div>
|
||||
```
|
||||
|
||||
绑定值为 `false` / 空时禁用。模板内文字节点需用标签包裹,且避免指令节点直接包含裸文本节点(详见 `highlight-query.ts` 注释)。
|
||||
|
||||
**InfiniteScroll**
|
||||
|
||||
```html
|
||||
<div v-infinite-scroll="loadMore" :infinite-scroll-disabled="loading"></div>
|
||||
```
|
||||
|
||||
### 6. 新增指令流程
|
||||
|
||||
1. 在 `packages/vue-directive/src/` 新增实现,默认导出指令对象
|
||||
2. 在 `packages/vue-directive/index.ts` 具名导出
|
||||
3. 在使用的 `pc.vue` / `mobile-first.vue` 中 `import` 并通过 `directives: directive({ ... })` 注册
|
||||
4. 优先使用 `@opentiny/utils` 的 `on`/`off`、`throttle`、`getScrollContainer` 等
|
||||
5. 若需组件方法,通过 `binding.expression` 或 `binding.value` 与 `vnode.context` 通信(Vue 3 项目注意与 `setup` 返回方法的兼容方式,保持与现有指令一致)
|
||||
6. 在 `examples/sites/demos` 补充演示(若对用户可见)
|
||||
|
||||
### 7. 禁止事项
|
||||
|
||||
- ❌ 不得在 renderless 的 `index.ts` 中注册或使用 `v-*` 指令
|
||||
- ❌ 不得在指令中编写组件级业务状态机(应通过 binding 调用 renderless 暴露的方法)
|
||||
- ❌ 不得跳过 `unbind` 清理
|
||||
- ❌ 新增指令时避免直接 `import from 'vue'`,DOM 工具走 `@opentiny/utils`,组件/渲染走 `@opentiny/vue-common`
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [Clickoutside](../../packages/vue-directive/src/clickoutside.ts)
|
||||
- [AutoTip](../../packages/vue-directive/src/auto-tip.ts)
|
||||
- [select pc 模板注册示例](../../packages/vue/src/select/src/pc.vue)
|
||||
|
|
@ -1,155 +0,0 @@
|
|||
# @opentiny/vue-hooks 开发规范
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 多个组件共享的组合式逻辑(Popper、弹层、父子关系、窗口尺寸等)
|
||||
- renderless 的 `vue.ts` 中需要 DOM/生命周期相关能力,但逻辑仍希望可测试、可复用
|
||||
- 不宜放入 `@opentiny/utils` 的、需要 Vue 生命周期或响应式 API 的逻辑
|
||||
|
||||
## 核心原则
|
||||
|
||||
### 1. 包定位
|
||||
|
||||
`packages/vue-hooks` 发布为 `@opentiny/vue-hooks`。
|
||||
|
||||
- **可以**依赖 `@opentiny/utils`
|
||||
- **不得**直接 `import from 'vue'`——Vue API 由调用方从 `hooks` 解构后传入(柯里化或直接传参,见下文)
|
||||
- renderless 与 vue-common 通过 renderless 第二参数或合并后的 `hooks` 对象实例化
|
||||
|
||||
### 2. 现有 Hooks 一览
|
||||
|
||||
| 导出 | 文件 | 用途 |
|
||||
| ------------------ | --------------------- | -------------------------------------------- |
|
||||
| `useEventListener` | `useEventListener.ts` | 绑定/自动清理 DOM 事件,支持 target 为 ref |
|
||||
| `useWindowSize` | `useWindowSize.ts` | 窗口宽高响应式 |
|
||||
| `useRect` | `useRect.ts` | 元素尺寸与位置 |
|
||||
| `useTouch` | `useTouch.ts` | 触摸手势 |
|
||||
| `useUserAgent` | `useUserAgent.ts` | UA / 浏览器能力判断 |
|
||||
| `useInstanceSlots` | `useInstanceSlots.ts` | 插槽访问(vue-common 已预绑定 `isVue2`) |
|
||||
| `useRelation` | `useRelation.ts` | 父子组件关系树(Tabs、Form 等) |
|
||||
| `useFloating` | `use-floating.ts` | 浮层定位 |
|
||||
| `useLazyShow` | `use-lazy-show.ts` | 延迟展示 |
|
||||
| `userPopper` | `vue-popper.ts` | Popper 弹层(注意导出名拼写为 `userPopper`) |
|
||||
| `usePopup` | `vue-popup.ts` | Popup 弹层管理 |
|
||||
|
||||
### 3. 两种调用形态
|
||||
|
||||
#### 形态 A:柯里化(多数 hooks)
|
||||
|
||||
第一个参数为 Vue 运行时 API 集合,返回可在 renderless 中调用的函数。适用于 `useEventListener`、`useRelation`、`useRect` 等。
|
||||
|
||||
```typescript
|
||||
// packages/vue-hooks/src/useEventListener.ts
|
||||
import { on, off, isServer } from '@opentiny/utils'
|
||||
|
||||
export const useEventListener =
|
||||
({ unref, isRef, watch, nextTick, onMounted, onUnmounted, onActivated, onDeactivated }) =>
|
||||
(type, listener, options = {}) => {
|
||||
if (isServer) return
|
||||
// ... 实现
|
||||
}
|
||||
```
|
||||
|
||||
在 renderless 的 `vue.ts` 中使用:
|
||||
|
||||
```typescript
|
||||
import { useEventListener } from '@opentiny/vue-hooks'
|
||||
|
||||
export const renderless = (props, hooks, utils) => {
|
||||
const addListener = useEventListener(hooks)
|
||||
addListener('scroll', onScroll, { target: scrollRef })
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
在 vue-common 中预绑定 Vue2 差异的示例:
|
||||
|
||||
```typescript
|
||||
import { useRelation as createUseRelation } from '@opentiny/vue-hooks'
|
||||
import hooks from './adapter'
|
||||
|
||||
export const useRelation = createUseRelation({ ...hooks, isVue2 })
|
||||
```
|
||||
|
||||
#### 形态 B:直接工厂(`userPopper`、`usePopup`)
|
||||
|
||||
接收合并了 Vue API 与业务上下文的对象,**不是**柯里化:
|
||||
|
||||
```typescript
|
||||
import { userPopper } from '@opentiny/vue-hooks'
|
||||
|
||||
export const renderless = (
|
||||
props,
|
||||
{ watch, reactive, onBeforeUnmount, onDeactivated, nextTick, toRefs },
|
||||
{ vm, slots, emit }
|
||||
) => {
|
||||
const popper = userPopper({
|
||||
emit,
|
||||
nextTick,
|
||||
onBeforeUnmount,
|
||||
onDeactivated,
|
||||
props,
|
||||
reactive,
|
||||
vm,
|
||||
slots,
|
||||
toRefs,
|
||||
watch
|
||||
})
|
||||
const state = initState({ reactive, popper })
|
||||
// 返回 updatePopper、destroyPopper、doDestroy 及 popper 相关 state 字段
|
||||
}
|
||||
```
|
||||
|
||||
`usePopup` 用法见 `dialog-box/vue.ts`,返回 `{ open, close, PopupManager, ...toRefs(state) }`。
|
||||
|
||||
### 4. useRelation 使用要点
|
||||
|
||||
用于「父收集子、子注册到父」的场景。典型写法(参考 `tabs-mf/vue.ts`):
|
||||
|
||||
```typescript
|
||||
import { useRelation } from '@opentiny/vue-hooks'
|
||||
|
||||
Object.assign(api, { useRelation: useRelation(hooks) })
|
||||
|
||||
api.useRelation({
|
||||
relationKey: `tabs-${state.tabsId}`,
|
||||
relationContainer: () => vm.$el.querySelector('[data-tag=tiny-tabs-hidden]'),
|
||||
childrenKey: 'childTabs',
|
||||
onChange: () => api.onRelationChange()
|
||||
})
|
||||
// 返回 { children, index, delivery }
|
||||
```
|
||||
|
||||
- `relationKey` 必须在父子树中一致
|
||||
- 子组件通过 inject 注册,在 `onUnmounted` 时自动 `unlink`
|
||||
- 也可从 `@opentiny/vue-common` 导入已绑定 `isVue2` 的 `useRelation`
|
||||
|
||||
### 5. userPopper / usePopup
|
||||
|
||||
Popper、Dialog、Select 等浮层在 renderless `vue.ts` 中按**形态 B**调用,将 `popper` / `usePopup` 传入 `initState` 或 `initApi`,勿 `Object.assign` 到整个 `api`。
|
||||
|
||||
内部依赖 `@opentiny/utils` 的 `PopupManager`、`PopperJS`、`on`/`off` 等,勿在 renderless 重复实现定位逻辑。
|
||||
|
||||
### 6. 新增 Hook 流程
|
||||
|
||||
1. 在 `packages/vue-hooks/src/` 新建实现文件
|
||||
2. 在 `packages/vue-hooks/index.ts` 导出
|
||||
3. 仅使用 `@opentiny/utils` 处理 DOM/工具,Vue API 一律从参数解构
|
||||
4. 在 renderless 或 vue-common 中接入并补充类型(`packages/vue-hooks/types/shared.type.ts` 若需扩展)
|
||||
5. 避免与 utils 中「待改造成 hooks」的模块重复(如 `touch`)
|
||||
|
||||
### 7. 禁止事项
|
||||
|
||||
- ❌ 不得在 vue-hooks 中 `import { ref } from 'vue'`
|
||||
- ❌ 不得在 hook 内写组件模板或 JSX
|
||||
- ❌ 不得被 `packages/utils` 反向依赖
|
||||
- ❌ renderless 的 `index.ts` 纯函数文件中不宜直接调用 vue-hooks(应在 `vue.ts` 中组装)
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [入口导出](../../packages/vue-hooks/index.ts)
|
||||
- [useEventListener](../../packages/vue-hooks/src/useEventListener.ts)
|
||||
- [useRelation](../../packages/vue-hooks/src/useRelation.ts)
|
||||
- [sticky 组件中的组合使用](../../packages/renderless/src/sticky/vue.ts)
|
||||
- [tabs-mf 中 useRelation](../../packages/renderless/src/tabs-mf/vue.ts)
|
||||
- [select-dropdown 中 userPopper](../../packages/renderless/src/select-dropdown/vue.ts)
|
||||
|
|
@ -1,484 +0,0 @@
|
|||
# TinyVue 国际化工作流
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 为新组件添加多语言支持
|
||||
- 翻译现有组件的文案
|
||||
- 新增支持的语言包
|
||||
- 统一术语翻译
|
||||
|
||||
## 核心原则
|
||||
|
||||
### 1. 语言文件结构
|
||||
|
||||
TinyVue 使用基于 key-value 的多语言系统:
|
||||
|
||||
```
|
||||
packages/vue-locale/src/lang/
|
||||
├── zh-CN.ts # 简体中文
|
||||
├── en-US.ts # 英文(美国)
|
||||
├── ja-JP.ts # 日文
|
||||
└── ...
|
||||
```
|
||||
|
||||
每个语言文件结构:
|
||||
|
||||
```typescript
|
||||
// packages/vue-locale/src/lang/zh-CN.ts
|
||||
|
||||
export default {
|
||||
ui: {
|
||||
// 组件名称小写
|
||||
button: {
|
||||
confirm: '确定',
|
||||
cancel: '取消'
|
||||
},
|
||||
input: {
|
||||
placeholder: '请输入',
|
||||
clear: '清空',
|
||||
more: '更多',
|
||||
detail: '详情',
|
||||
close: '关闭'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 命名规范
|
||||
|
||||
**Key 命名规则:**
|
||||
|
||||
- 使用 `camelCase`
|
||||
- 按组件分组:`ui.<component>.<key>`
|
||||
- 保持语义清晰,避免缩写
|
||||
|
||||
**示例:**
|
||||
|
||||
```typescript
|
||||
// ✅ 正确
|
||||
ui.datePicker.confirm
|
||||
ui.grid.selectAll
|
||||
ui.input.clear
|
||||
|
||||
// ❌ 错误
|
||||
ui.dp.ok
|
||||
ui.g.sel_all
|
||||
ui.input.clr
|
||||
```
|
||||
|
||||
### 3. 翻译一致性
|
||||
|
||||
同一概念在不同组件中使用相同翻译:
|
||||
|
||||
| 中文 | 英文 | 使用场景 |
|
||||
| -------- | --------- | ---------------- |
|
||||
| 确定 | Confirm | 对话框、确认操作 |
|
||||
| 取消 | Cancel | 对话框、取消操作 |
|
||||
| 保存 | Save | 表单提交 |
|
||||
| 删除 | Delete | 删除操作 |
|
||||
| 编辑 | Edit | 编辑操作 |
|
||||
| 搜索 | Search | 搜索功能 |
|
||||
| 重置 | Reset | 重置表单 |
|
||||
| 加载更多 | Load More | 分页加载 |
|
||||
|
||||
## 标准流程
|
||||
|
||||
### 步骤 1:在组件中使用国际化
|
||||
|
||||
#### Renderless 层
|
||||
|
||||
```typescript
|
||||
// packages/renderless/src/my-component/index.ts
|
||||
|
||||
import { t } from '@opentiny/vue-locale'
|
||||
|
||||
export const api = ({ state, props }) => {
|
||||
return {
|
||||
getPlaceholder: () => {
|
||||
return props.placeholder || t('ui.myComponent.placeholder')
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Vue 视图层
|
||||
|
||||
```vue
|
||||
<!-- packages/vue/src/my-component/src/mobile-first.vue -->
|
||||
|
||||
<script lang="ts">
|
||||
import { useLocale } from '@opentiny/vue-locale'
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const { t } = useLocale()
|
||||
|
||||
return {
|
||||
t
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input :placeholder="t('ui.myComponent.placeholder')" />
|
||||
<button>{{ t('ui.myComponent.confirm') }}</button>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 步骤 2:添加语言包条目
|
||||
|
||||
#### 中文(zh-CN.ts)
|
||||
|
||||
```typescript
|
||||
// packages/vue-locale/src/lang/zh-CN.ts
|
||||
|
||||
export default {
|
||||
ui: {
|
||||
myComponent: {
|
||||
placeholder: '请输入内容',
|
||||
confirm: '确定',
|
||||
cancel: '取消',
|
||||
clear: '清空',
|
||||
noData: '暂无数据',
|
||||
loading: '加载中...',
|
||||
error: '加载失败,请重试'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 英文(en-US.ts)
|
||||
|
||||
```typescript
|
||||
// packages/vue-locale/src/lang/en-US.ts
|
||||
|
||||
export default {
|
||||
ui: {
|
||||
myComponent: {
|
||||
placeholder: 'Please enter content',
|
||||
confirm: 'Confirm',
|
||||
cancel: 'Cancel',
|
||||
clear: 'Clear',
|
||||
noData: 'No data available',
|
||||
loading: 'Loading...',
|
||||
error: 'Failed to load, please try again'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤 3:验证翻译完整性
|
||||
|
||||
确保所有语言包包含相同的 key:
|
||||
|
||||
```bash
|
||||
# 运行检查脚本(如果有的话)
|
||||
pnpm check:i18n
|
||||
|
||||
# 或手动对比
|
||||
diff packages/vue-locale/src/lang/zh-CN.ts packages/vue-locale/src/lang/en-US.ts
|
||||
```
|
||||
|
||||
## 代码示例
|
||||
|
||||
### 动态文本插值
|
||||
|
||||
```typescript
|
||||
// 带参数的翻译
|
||||
t('ui.pagination.total', { total: 100 })
|
||||
|
||||
// 语言文件中定义
|
||||
pagination: {
|
||||
total: '共 {total} 条' // zh-CN
|
||||
total: 'Total {total} items' // en-US
|
||||
}
|
||||
```
|
||||
|
||||
### 条件翻译
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<span>{{
|
||||
state.count === 0 ? t('ui.myComponent.noItems') : t('ui.myComponent.itemsCount', { count: state.count })
|
||||
}}</span>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 完整组件示例
|
||||
|
||||
```typescript
|
||||
// packages/renderless/src/pagination/index.ts
|
||||
|
||||
import { t } from '@opentiny/vue-locale'
|
||||
|
||||
export const api = ({ state, props }) => {
|
||||
return {
|
||||
getTotalText: () => {
|
||||
return t('ui.pagination.total', { total: state.total })
|
||||
},
|
||||
|
||||
getJumpText: () => {
|
||||
return t('ui.pagination.jumpTo')
|
||||
},
|
||||
|
||||
getPageText: (page: number) => {
|
||||
return t('ui.pagination.page', { page })
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```vue
|
||||
<!-- packages/vue/src/pagination/src/mobile-first.vue -->
|
||||
|
||||
<template>
|
||||
<div class="tiny-pagination">
|
||||
<span>{{ getTotalText() }}</span>
|
||||
<button @click="prev">{{ t('ui.pagination.prev') }}</button>
|
||||
<button @click="next">{{ t('ui.pagination.next') }}</button>
|
||||
<span>{{ getJumpText() }} <input v-model="jumpPage" /></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { renderless, api } from '@opentiny/vue-renderless/pagination/vue'
|
||||
import { props, setup, defineComponent } from '@opentiny/vue-common'
|
||||
import { useLocale } from '@opentiny/vue-locale'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TinyPagination',
|
||||
props: [...props, 'total', 'pageSize'],
|
||||
emits: ['update:currentPage', 'change'],
|
||||
setup(props, context) {
|
||||
const { t } = useLocale()
|
||||
const renderlessResult = setup({ props, context, renderless, api })
|
||||
|
||||
return {
|
||||
...renderlessResult,
|
||||
t
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
## 常见陷阱
|
||||
|
||||
### ❌ 错误做法
|
||||
|
||||
1. **硬编码文本**
|
||||
|
||||
```vue
|
||||
<!-- ❌ 错误 -->
|
||||
<button>确定</button>
|
||||
|
||||
<!-- ✅ 正确 -->
|
||||
<button>{{ t('ui.button.confirm') }}</button>
|
||||
```
|
||||
|
||||
2. **拼接翻译字符串**
|
||||
|
||||
```typescript
|
||||
// ❌ 错误
|
||||
const text = t('ui.msg.prefix') + ' ' + value + ' ' + t('ui.msg.suffix')
|
||||
|
||||
// ✅ 正确
|
||||
const text = t('ui.msg.complete', { value })
|
||||
// 语言文件:complete: '{value} 已成功处理'
|
||||
```
|
||||
|
||||
3. **遗漏某些语言包**
|
||||
|
||||
```typescript
|
||||
// ❌ 错误 - 只添加了中文
|
||||
// zh-CN.ts
|
||||
myComponent: {
|
||||
label: '标签'
|
||||
}
|
||||
|
||||
// en-US.ts - 忘记添加
|
||||
// myComponent: { label: 'Label' } ← 缺失
|
||||
|
||||
// ✅ 正确 - 同时更新所有语言包
|
||||
```
|
||||
|
||||
4. **翻译不一致**
|
||||
|
||||
```typescript
|
||||
// ❌ 错误 - 不同组件用不同翻译
|
||||
// button.ts: confirm: '确定'
|
||||
// dialog.ts: ok: '确认'
|
||||
|
||||
// ✅ 正确 - 统一使用
|
||||
// button.ts: confirm: '确定'
|
||||
// dialog.ts: confirm: '确定'
|
||||
```
|
||||
|
||||
### ✅ 最佳实践
|
||||
|
||||
1. **使用常量管理 Key**
|
||||
|
||||
```typescript
|
||||
// packages/vue-locale/src/keys.ts
|
||||
|
||||
export const I18N_KEYS = {
|
||||
BUTTON_CONFIRM: 'ui.button.confirm',
|
||||
BUTTON_CANCEL: 'ui.button.cancel',
|
||||
INPUT_PLACEHOLDER: 'ui.input.placeholder'
|
||||
} as const
|
||||
|
||||
// 使用时
|
||||
t(I18N_KEYS.BUTTON_CONFIRM)
|
||||
```
|
||||
|
||||
2. **提供默认值**
|
||||
|
||||
```typescript
|
||||
// 如果翻译缺失,使用默认值
|
||||
t('ui.myComponent.label', { default: 'Default Label' })
|
||||
```
|
||||
|
||||
3. **懒加载语言包**
|
||||
|
||||
```typescript
|
||||
// 按需加载,减小初始包体积
|
||||
const loadLocale = async (lang: string) => {
|
||||
return import(`@/lang/${lang}.ts`)
|
||||
}
|
||||
```
|
||||
|
||||
4. **记录翻译上下文**
|
||||
|
||||
```typescript
|
||||
// 添加注释说明使用场景
|
||||
myComponent: {
|
||||
// 用于下拉框的空状态提示
|
||||
noData: '暂无数据',
|
||||
|
||||
// 用于加载状态的简短提示
|
||||
loading: '加载中...'
|
||||
}
|
||||
```
|
||||
|
||||
## 翻译质量检查清单
|
||||
|
||||
添加新翻译时,确认:
|
||||
|
||||
### 准确性
|
||||
|
||||
- [ ] 翻译准确表达原意
|
||||
- [ ] 符合目标语言的语法习惯
|
||||
- [ ] 专业术语使用行业标准译法
|
||||
- [ ] 没有机器翻译的生硬感
|
||||
|
||||
### 一致性
|
||||
|
||||
- [ ] 与现有翻译风格一致
|
||||
- [ ] 相同概念使用相同译文
|
||||
- [ ] 标点符号使用规范统一
|
||||
- [ ] 大小写遵循目标语言规范
|
||||
|
||||
### 完整性
|
||||
|
||||
- [ ] 所有语言包都已更新
|
||||
- [ ] 没有遗漏任何 key
|
||||
- [ ] 参数占位符格式正确
|
||||
- [ ] 特殊字符已转义
|
||||
|
||||
### 技术正确性
|
||||
|
||||
- [ ] Key 命名符合规范
|
||||
- [ ] 没有硬编码文本
|
||||
- [ ] 插值参数使用正确
|
||||
- [ ] 类型定义已更新(如需要)
|
||||
|
||||
## 常用术语对照表
|
||||
|
||||
### 通用操作
|
||||
|
||||
| 中文 | 英文 | 备注 |
|
||||
| ---- | --------------- | ---------- |
|
||||
| 确定 | Confirm | 确认操作 |
|
||||
| 取消 | Cancel | 取消操作 |
|
||||
| 保存 | Save | 保存数据 |
|
||||
| 删除 | Delete | 删除项目 |
|
||||
| 编辑 | Edit | 编辑内容 |
|
||||
| 新建 | New / Create | 创建新项目 |
|
||||
| 修改 | Modify / Update | 更新现有项 |
|
||||
| 查询 | Search / Query | 搜索功能 |
|
||||
| 重置 | Reset | 恢复默认 |
|
||||
| 提交 | Submit | 提交表单 |
|
||||
|
||||
### 状态提示
|
||||
|
||||
| 中文 | 英文 | 备注 |
|
||||
| ------ | ----------- | -------- |
|
||||
| 成功 | Success | 操作成功 |
|
||||
| 失败 | Failed | 操作失败 |
|
||||
| 警告 | Warning | 警告信息 |
|
||||
| 错误 | Error | 错误信息 |
|
||||
| 加载中 | Loading | 加载状态 |
|
||||
| 已完成 | Completed | 完成状态 |
|
||||
| 进行中 | In Progress | 进行状态 |
|
||||
|
||||
### 数据相关
|
||||
|
||||
| 中文 | 英文 | 备注 |
|
||||
| -------- | ------------- | --------- |
|
||||
| 暂无数据 | No Data | 空状态 |
|
||||
| 加载更多 | Load More | 分页加载 |
|
||||
| 全部 | All | 全选/全部 |
|
||||
| 当前页 | Current Page | 分页信息 |
|
||||
| 共 X 条 | Total X Items | 总数统计 |
|
||||
| 第 X 页 | Page X | 页码显示 |
|
||||
|
||||
## 新增语言支持
|
||||
|
||||
如需添加新语言(如法语):
|
||||
|
||||
### 步骤 1:创建语言文件
|
||||
|
||||
```typescript
|
||||
// packages/vue-locale/src/lang/fr-FR.ts
|
||||
|
||||
export default {
|
||||
ui: {
|
||||
button: {
|
||||
confirm: 'Confirmer',
|
||||
cancel: 'Annuler'
|
||||
}
|
||||
// ... 复制其他组件的翻译
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤 2:注册语言
|
||||
|
||||
```typescript
|
||||
// packages/vue-locale/src/index.ts
|
||||
|
||||
import frFR from './lang/fr-FR'
|
||||
|
||||
export const locales = {
|
||||
'zh-CN': zhCN,
|
||||
'en-US': enUS,
|
||||
'fr-FR': frFR // 新增
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤 3:更新类型定义
|
||||
|
||||
```typescript
|
||||
// packages/vue-locale/src/types.ts
|
||||
|
||||
export type Locale = 'zh-CN' | 'en-US' | 'fr-FR' // 添加新语言
|
||||
```
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [Vue I18n 官方文档](https://vue-i18n.intlify.dev/)
|
||||
- [语言包目录](../../packages/vue-locale/src/lang/)
|
||||
- [dialog-box 翻译示例](../../packages/vue-locale/src/lang/zh-CN.ts)
|
||||
- [modal.js 翻译规范](../../AGENTS.md#代码注释规范)
|
||||
- [API 文档翻译规范](../../CONTRIBUTING.md)
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
# ICON 开发规范
|
||||
|
||||
## 核心原理
|
||||
|
||||
所有的图标原始文件在 `packages/theme/src/svgs` 目录中,每个svg文件对应一个图标,图标名字为小写连字符的格式。
|
||||
|
||||
图标库的位置在 `packages\vue-icon`, 它依赖 `@opentiny/vue-theme`包,通过`vite-svg-loader`插件,引用svg文件转为标准的Vue组件。
|
||||
|
||||
图标库中的每一个图标都是一个函数,函数中调用适配层的 `svg`函数包装组件一下,返回一个标准的Vue组件。
|
||||
|
||||
```typescript
|
||||
import { svg } from '@opentiny/vue-common'
|
||||
import Acceptance from '@opentiny/vue-theme/svgs/acceptance.svg'
|
||||
|
||||
export default () => svg({ name: 'IconAcceptance', component: Acceptance, filledComponent: Acceptance })()
|
||||
```
|
||||
|
||||
`svg` 函数的路径为: `packages\vue-common\src\index.ts`
|
||||
|
||||
## 图标库的开发步骤
|
||||
|
||||
1. 设计师提供原始的svg图标
|
||||
|
||||
将图标放到 `packages/theme/src/svgs` 目录中,修改文件名为小写连字符的格式,该文件名即为最终的图标名称。
|
||||
|
||||
2. 自动同步图标脚本文件
|
||||
|
||||
在 `internals\automate` 文件夹中打开终端,然后执行脚本,会将 `packages/theme/src/svgs` 目录中的所有图标同步到`packages\vue-icon`中去。
|
||||
|
||||
```bash
|
||||
pnpm run build-svgs
|
||||
```
|
||||
|
||||
3. 发布图标库
|
||||
|
||||
在 `packages\vue-icon`目录中发布即可。
|
||||
|
|
@ -1,274 +0,0 @@
|
|||
# 无渲染逻辑层开发规范
|
||||
|
||||
无渲染逻辑层中,是将一个组件的内部变量和内部方法整合在一起,返回给模板的使用。该模块不应该依赖第三方包,只能依赖 `@opentiny/utils` ,`@opentiny/vue-hooks` 这两个包。
|
||||
|
||||
约定它的必须有2个文件来开发一个组件的逻辑层:
|
||||
|
||||
### Renderless 架构(必须遵守)
|
||||
|
||||
1. 无渲染逻辑层的入口: `packages/renderless/src/<component-name>/vue.ts`
|
||||
|
||||
它主要是导出一个 `renderless`的函数变量供适配层调用。 在每一个组件的 `setup`生命周期中,会调用且只调用一次 `renderless`的函数。 适配层会给`renderless`传入相应的参数, `renderless`的函数返回一个全新的上下文对象 {state,api} 。
|
||||
|
||||
在`renderless`函数中,除了定义 state,api之外,还要处理 `watch`, `computed`等变化监听,也常常要处理 onMounted, onUnmounted 的生命周期需要处理的任务以及组件初始化的准备工作。
|
||||
|
||||
renderless 接收四个参数:
|
||||
|
||||
第一个参数: props, 组件初始化时,Vue运行时生成的props对象
|
||||
第二个参数: 传入Vue2包 或 Vue3包的导出对象,也称其为hooks对象
|
||||
第三个参数: 适配层生成的组件上下文对象。由于要兼容Vue2/3, 所以使用组件内部的对象时,需要使用该对象。
|
||||
第四个参数: 这个是可选参数,如果模板中传入 `extendOptions`属性时, 从这个参数中接收。
|
||||
|
||||
一个完整的rendereless示例如下:
|
||||
|
||||
```typescript
|
||||
import type {
|
||||
IAlertApi,
|
||||
IAlertProps,
|
||||
IAlertState,
|
||||
ISharedRenderlessParamHooks,
|
||||
IAlertRenderlessParamUtils
|
||||
} from '@/types'
|
||||
import {
|
||||
computedGetIcon,
|
||||
computedGetTitle,
|
||||
computedStyle,
|
||||
computedClass,
|
||||
handleClose,
|
||||
handleHeaderClick,
|
||||
watchAutoHide,
|
||||
handlerTargetNode
|
||||
} from './index'
|
||||
import { nanoid } from '@opentiny/utils'
|
||||
|
||||
export const api = ['handleClose', 'state', 'handleHeaderClick']
|
||||
|
||||
const initState = ({ api, computed, constants, reactive }): IAlertState => {
|
||||
return reactive({
|
||||
show: true,
|
||||
contentVisible: false,
|
||||
contentDescribeHeight: 0,
|
||||
contentDefaultHeight: 0,
|
||||
contentMaxHeight: constants.CONTENT_MAXHEUGHT,
|
||||
scrollStatus: false,
|
||||
getIcon: computed(() => api.computedGetIcon()),
|
||||
getTitle: computed(() => api.computedGetTitle()),
|
||||
alertClass: computed(() => api.computedClass()),
|
||||
alertStyle: computed(() => api.computedStyle()),
|
||||
titleId: `tiny-alert-title-${nanoid.api.nanoid(8)}`,
|
||||
contentId: `tiny-alert-description-${nanoid.api.nanoid(8)}`
|
||||
})
|
||||
}
|
||||
|
||||
const initApi = ({ api, state, constants, props, designConfig, t, emit, vm, parent, nextTick, mode }): void => {
|
||||
Object.assign(api, {
|
||||
state,
|
||||
computedGetIcon: computedGetIcon({ constants, props, designConfig }),
|
||||
computedGetTitle: computedGetTitle({ constants, props, t }),
|
||||
computedClass: computedClass({ props, mode }),
|
||||
computedStyle: computedStyle({ props, mode }),
|
||||
handleClose: handleClose({ emit, state }),
|
||||
handleHeaderClick: handleHeaderClick({ state, props, vm }),
|
||||
watchAutoHide: watchAutoHide({ api, props }),
|
||||
handlerTargetNode: handlerTargetNode({ props, parent, vm, nextTick })
|
||||
})
|
||||
}
|
||||
|
||||
const initWatcher = ({ watch, props, api }) => {
|
||||
watch(() => props.autoHide, api.watchAutoHide, { immediate: true })
|
||||
watch(() => props.target, api.handlerTargetNode, { immediate: true })
|
||||
}
|
||||
|
||||
export const renderless = (
|
||||
props: IAlertProps,
|
||||
{ computed, reactive, watch }: ISharedRenderlessParamHooks,
|
||||
{ t, emit, constants, vm, designConfig, parent, nextTick, mode }: IAlertRenderlessParamUtils
|
||||
): IAlertApi => {
|
||||
const api = {} as IAlertApi
|
||||
const state: IAlertState = initState({ api, computed, constants, reactive })
|
||||
initApi({ api, state, constants, props, designConfig, t, emit, vm, parent, nextTick, mode })
|
||||
initWatcher({ watch, props, api })
|
||||
|
||||
return api
|
||||
}
|
||||
```
|
||||
|
||||
#### 适配层生成的组件上下文对象
|
||||
|
||||
renderless函数的第三个参数是适配层生成的组件上下文对象, 它是适配层为了兼容Vue2,Vue3,在组件初始化时,构造了一组相同的上下文对象,来抹平跨框架的差异。
|
||||
|
||||
这个参数是在 `..\packages\vue-common\src\index.ts` 的setup函数中传入的,它包含以下值:
|
||||
|
||||
```typescript
|
||||
// 适配层构造一个组件的vm变量,将Vue实例上的某些值代理出来。
|
||||
const vm = {
|
||||
$attrs: { get: () => $attrs },
|
||||
$children: { get: () => generateChildren(instance.subTree) },
|
||||
$constants: { get: () => instance.props._constants },
|
||||
$emit: { get: () => emit },
|
||||
$el: { get: () => instance.vnode.el },
|
||||
$listeners: { get: () => $listeners },
|
||||
$mode: { get: () => instance._tiny_mode },
|
||||
$nextTick: { get: () => hooks.nextTick },
|
||||
$off: { get: () => $emitter.off },
|
||||
$on: { get: () => $emitter.on },
|
||||
$once: { get: () => $emitter.once },
|
||||
$options: { get: () => ({ componentName: instance.type.componentName }) },
|
||||
$parent: {
|
||||
get: () => instance.parent && createVm({}, getRealParent(instance))
|
||||
},
|
||||
$refs: { get: () => instance.refs },
|
||||
$renderless: { get: () => instance.props.tiny_renderless },
|
||||
$scopedSlots: { get: () => instance.slots },
|
||||
$set: { get: () => $set },
|
||||
$slots: { get: () => instance.slots },
|
||||
$template: { get: () => instance.props.tiny_template }
|
||||
}
|
||||
|
||||
// 该值为传递给renderless第3个参数,所有属性都可以从中解构出来。
|
||||
const utils = {
|
||||
$prefix,
|
||||
t,
|
||||
designConfig,
|
||||
globalDesignConfig,
|
||||
useBreakpoint,
|
||||
mergeClass,
|
||||
framework: 'vue3',
|
||||
vm,
|
||||
emit,
|
||||
emitter,
|
||||
route,
|
||||
router,
|
||||
dispatch,
|
||||
broadcast,
|
||||
parentHandler,
|
||||
childrenHandler,
|
||||
i18n,
|
||||
refs,
|
||||
slots: instance?.slots,
|
||||
scopedSlots: instance?.slots,
|
||||
attrs: context.attrs,
|
||||
parent: parentVm,
|
||||
nextTick: hooks.nextTick,
|
||||
constants: instance?.props._constants,
|
||||
mode,
|
||||
isPCMode: mode === 'pc',
|
||||
isMobileMode: mode === 'mobile',
|
||||
service: root?.$getService ? root?.$getService(vm) : root?.$service,
|
||||
getService: () => root?.$getService(vm),
|
||||
setParentAttribute,
|
||||
defineInstanceProperties,
|
||||
defineParentInstanceProperties
|
||||
}
|
||||
```
|
||||
|
||||
我们观察到,有些属性在`utils`下存在,在`vm`下也存在。建议使用 `vm`下的相应值,比如 `vm.$refs, vm.$slots` 。
|
||||
|
||||
2. 辅助方法与函数
|
||||
|
||||
- **业务逻辑** → `packages/renderless/src/<component-name>/index.ts`
|
||||
1. 它们都是纯函数,不依赖任何 UI 框架,不依赖其它上下文
|
||||
2. 在`index.ts`文件中, 禁止直接导入 Vue API(ref、reactive、watch 等)
|
||||
3. 每一个函数,通常都是要接收{state, api, props} 等传入的变量,生成一个全新的函数变量。这个函数只与当前组件产生互操作。
|
||||
4. 所有的函数不要互相调用,通常应该是通过 api 变量来调用其它函数。
|
||||
|
||||
以下是一个简单的纯函数示例:
|
||||
|
||||
```typescript
|
||||
export const watchAutoHide =
|
||||
({ api, props }: Pick<IAlertRenderlessParams, 'api' | 'props'>) =>
|
||||
(newVal: boolean) => {
|
||||
if (props.autoHide && newVal) {
|
||||
const timer = setTimeout(() => {
|
||||
api.handleClose()
|
||||
clearTimeout(timer)
|
||||
}, ALERT_TIMEOUT)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. 类型声明
|
||||
|
||||
- **类型声明** → `packages/renderless/types/<component-name>.type.ts`
|
||||
|
||||
在 `types` 文件夹中,为每一个组件添加一个声明文件,这样在逻辑层代码开发,以及模板绑定属性和方法时,才会有正确的类型提示。
|
||||
|
||||
以下是一个类型声明的示例:
|
||||
|
||||
```typescript
|
||||
import type { ExtractPropTypes, CSSProperties } from 'vue'
|
||||
import type { alertProps, $constants } from '@/alert/src'
|
||||
import type { ISharedRenderlessFunctionParams, ISharedRenderlessParamUtils } from './shared.type'
|
||||
|
||||
export interface IAlertState {
|
||||
show: boolean
|
||||
getIcon: string
|
||||
getTitle: string
|
||||
contentVisible: boolean
|
||||
contentDescribeHeight: number
|
||||
contentDefaultHeight: number
|
||||
contentMaxHeight: number
|
||||
scrollStatus: boolean
|
||||
titleId: string
|
||||
contentId: string
|
||||
}
|
||||
|
||||
export type IAlertProps = ExtractPropTypes<typeof alertProps>
|
||||
|
||||
export type IAlertConstants = typeof $constants
|
||||
|
||||
export type IAlertRenderlessParams = ISharedRenderlessFunctionParams<IAlertConstants> & {
|
||||
api: IAlertApi
|
||||
state: IAlertState
|
||||
props: IAlertProps
|
||||
}
|
||||
|
||||
export interface IAlertApi {
|
||||
state: IAlertState
|
||||
computedGetIcon: () => string
|
||||
computedGetTitle: () => string
|
||||
handleClose: () => void
|
||||
handleHeaderClick: () => void
|
||||
watchAutoHide: (value: boolean) => void
|
||||
computedStyle: () => CSSProperties
|
||||
}
|
||||
|
||||
export type IAlertRenderlessParamUtils = ISharedRenderlessParamUtils<IAlertConstants>
|
||||
```
|
||||
|
||||
TypeScript 类型安全的开发规范
|
||||
|
||||
- 优先使用 TypeScript
|
||||
- 禁止使用 `any`,需要类型逃逸时加注释说明
|
||||
- 类型定义放在 `packages/renderless/types/` 或组件目录下
|
||||
|
||||
### 开发规范
|
||||
|
||||
- 一份 renderless 逻辑同时服务 Vue 2 和 Vue 3
|
||||
- 如需判断框架版本,使用 `@opentiny/vue-common` 中的工具函数, 不得在 renderless 层写 `if (vue3)` 这样的判断
|
||||
- 应该尽量避免直接操作dom
|
||||
- 尽量编写相应的类型声明, 包含 props, state, api下的属性和方法的签名。
|
||||
- 不要引用 'vue' 包,应该从renderless的第2个参数中,获取vue包下的变量。
|
||||
|
||||
**在 renderless 层使用 Vue API**
|
||||
|
||||
```typescript
|
||||
// ❌ 错误
|
||||
import { ref } from 'vue'
|
||||
const count = ref(0)
|
||||
|
||||
// ✅ 正确方式 1
|
||||
import { hooks } from '@opentiny/vue-common'
|
||||
const state = hooks.ref({ count: 0 })
|
||||
|
||||
// ✅ 正确方式 2
|
||||
export const renderless = (props, { ref }) => {
|
||||
const state = hooks.ref({ count: 0 })
|
||||
}
|
||||
```
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [vue.ts](../../packages/renderless/src/button/vue.ts)
|
||||
- [index.ts](../../packages/renderless/src/button/index.ts)
|
||||
- [button.type.ts](../../packages/renderless/types/button.type.ts)
|
||||
|
|
@ -1,522 +0,0 @@
|
|||
# TinyVue 测试编写指南
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 为新组件编写测试
|
||||
- 为现有组件补充测试用例
|
||||
- 修复 Bug 时添加回归测试
|
||||
- 重构代码后验证功能
|
||||
|
||||
## 核心原则
|
||||
|
||||
### 1. 测试金字塔
|
||||
|
||||
```
|
||||
/\
|
||||
/ \ E2E 测试(少量,覆盖关键流程)
|
||||
/----\
|
||||
/ \ 集成测试(适量,测试组件交互)
|
||||
/--------\
|
||||
/ \ 单元测试(大量,测试单个函数/组件)
|
||||
/------------\
|
||||
```
|
||||
|
||||
- **单元测试**:Vitest,测试 renderless 层的纯函数
|
||||
- **E2E 测试**:Playwright,测试完整用户流程
|
||||
- **比例建议**:70% 单元 + 30% E2E
|
||||
|
||||
### 2. 测试命名规范
|
||||
|
||||
```typescript
|
||||
// 格式:should + 预期行为 + when + 条件
|
||||
it('should emit change event when value is updated', () => {})
|
||||
it('should not submit form when validation fails', () => {})
|
||||
it('should display error message when input is invalid', () => {})
|
||||
```
|
||||
|
||||
### 3. AAA 模式
|
||||
|
||||
每个测试用例遵循 **Arrange-Act-Assert**:
|
||||
|
||||
```typescript
|
||||
it('should calculate total price correctly', () => {
|
||||
// Arrange - 准备数据
|
||||
const items = [
|
||||
{ price: 100, quantity: 2 },
|
||||
{ price: 50, quantity: 1 }
|
||||
]
|
||||
|
||||
// Act - 执行操作
|
||||
const total = calculateTotal(items)
|
||||
|
||||
// Assert - 验证结果
|
||||
expect(total).toBe(250)
|
||||
})
|
||||
```
|
||||
|
||||
## 标准流程
|
||||
|
||||
### 单元测试(Vitest)
|
||||
|
||||
#### 步骤 1:创建测试文件
|
||||
|
||||
```typescript
|
||||
// packages/renderless/src/my-component/__tests__/index.spec.ts
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { api, renderless } from '../index'
|
||||
|
||||
describe('MyComponent Renderless', () => {
|
||||
// 测试用例
|
||||
})
|
||||
```
|
||||
|
||||
#### 步骤 2:编写基础测试
|
||||
|
||||
```typescript
|
||||
describe('api', () => {
|
||||
it('should return correct methods', () => {
|
||||
const state = { value: '' }
|
||||
const props = { disabled: false }
|
||||
const emit = vi.fn()
|
||||
|
||||
const result = api({ state, props, emit })
|
||||
|
||||
expect(result).toHaveProperty('handleClick')
|
||||
expect(result).toHaveProperty('updateValue')
|
||||
})
|
||||
|
||||
it('should emit click event with current value', () => {
|
||||
const state = { value: 'test' }
|
||||
const props = { disabled: false }
|
||||
const emit = vi.fn()
|
||||
|
||||
const { handleClick } = api({ state, props, emit })
|
||||
handleClick()
|
||||
|
||||
expect(emit).toHaveBeenCalledWith('click', 'test')
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
#### 步骤 3:边界情况测试
|
||||
|
||||
```typescript
|
||||
describe('edge cases', () => {
|
||||
it('should not emit when disabled', () => {
|
||||
const state = { value: 'test' }
|
||||
const props = { disabled: true }
|
||||
const emit = vi.fn()
|
||||
|
||||
const { handleClick } = api({ state, props, emit })
|
||||
handleClick()
|
||||
|
||||
expect(emit).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle empty value', () => {
|
||||
const state = { value: '' }
|
||||
const props = { disabled: false }
|
||||
const emit = vi.fn()
|
||||
|
||||
const { updateValue } = api({ state, props, emit })
|
||||
updateValue('')
|
||||
|
||||
expect(state.value).toBe('')
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### E2E 测试(Playwright)
|
||||
|
||||
#### 步骤 1:创建测试文件
|
||||
|
||||
```typescript
|
||||
// examples/sites/demos/my-component/test/my-component.spec.ts
|
||||
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('MyComponent E2E', () => {
|
||||
// 测试用例
|
||||
})
|
||||
```
|
||||
|
||||
#### 步骤 2:编写交互测试
|
||||
|
||||
```typescript
|
||||
test('should click button and show result', async ({ page }) => {
|
||||
// 导航到示例页面
|
||||
await page.goto('/my-component/basic')
|
||||
|
||||
// 找到按钮并点击
|
||||
const button = page.getByRole('button', { name: '点击我' })
|
||||
await button.click()
|
||||
|
||||
// 验证结果显示
|
||||
const result = page.getByText('操作成功')
|
||||
await expect(result).toBeVisible()
|
||||
})
|
||||
```
|
||||
|
||||
#### 步骤 3:视觉回归测试
|
||||
|
||||
```typescript
|
||||
test('should render correctly', async ({ page }) => {
|
||||
await page.goto('/my-component/basic')
|
||||
|
||||
// 截取整个页面
|
||||
await expect(page).toHaveScreenshot('my-component-basic.png', {
|
||||
maxDiffPixels: 100
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## 代码示例
|
||||
|
||||
### Vitest 完整示例
|
||||
|
||||
```typescript
|
||||
// packages/renderless/src/input/__tests__/index.spec.ts
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { api, renderless } from '../index'
|
||||
|
||||
describe('Input Renderless', () => {
|
||||
let mockState: any
|
||||
let mockProps: any
|
||||
let mockEmit: any
|
||||
|
||||
beforeEach(() => {
|
||||
mockState = {
|
||||
value: '',
|
||||
hovering: false,
|
||||
focused: false
|
||||
}
|
||||
|
||||
mockProps = {
|
||||
modelValue: '',
|
||||
disabled: false,
|
||||
readonly: false,
|
||||
placeholder: '请输入'
|
||||
}
|
||||
|
||||
mockEmit = vi.fn()
|
||||
})
|
||||
|
||||
describe('handleInput', () => {
|
||||
it('should update state value', () => {
|
||||
const { handleInput } = api({
|
||||
state: mockState,
|
||||
props: mockProps,
|
||||
emit: mockEmit
|
||||
})
|
||||
|
||||
handleInput('new value')
|
||||
|
||||
expect(mockState.value).toBe('new value')
|
||||
})
|
||||
|
||||
it('should emit update:modelValue event', () => {
|
||||
const { handleInput } = api({
|
||||
state: mockState,
|
||||
props: mockProps,
|
||||
emit: mockEmit
|
||||
})
|
||||
|
||||
handleInput('test')
|
||||
|
||||
expect(mockEmit).toHaveBeenCalledWith('update:modelValue', 'test')
|
||||
})
|
||||
|
||||
it('should not emit when value unchanged', () => {
|
||||
mockState.value = 'existing'
|
||||
mockProps.modelValue = 'existing'
|
||||
|
||||
const { handleInput } = api({
|
||||
state: mockState,
|
||||
props: mockProps,
|
||||
emit: mockEmit
|
||||
})
|
||||
|
||||
handleInput('existing')
|
||||
|
||||
expect(mockEmit).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleFocus', () => {
|
||||
it('should set focused to true', () => {
|
||||
const { handleFocus } = api({
|
||||
state: mockState,
|
||||
props: mockProps,
|
||||
emit: mockEmit
|
||||
})
|
||||
|
||||
handleFocus()
|
||||
|
||||
expect(mockState.focused).toBe(true)
|
||||
})
|
||||
|
||||
it('should emit focus event', () => {
|
||||
const { handleFocus } = api({
|
||||
state: mockState,
|
||||
props: mockProps,
|
||||
emit: mockEmit
|
||||
})
|
||||
|
||||
handleFocus()
|
||||
|
||||
expect(mockEmit).toHaveBeenCalledWith('focus')
|
||||
})
|
||||
})
|
||||
|
||||
describe('clear', () => {
|
||||
it('should clear value and emit events', () => {
|
||||
mockState.value = 'some text'
|
||||
|
||||
const { clear } = api({
|
||||
state: mockState,
|
||||
props: mockProps,
|
||||
emit: mockEmit
|
||||
})
|
||||
|
||||
clear()
|
||||
|
||||
expect(mockState.value).toBe('')
|
||||
expect(mockEmit).toHaveBeenCalledWith('update:modelValue', '')
|
||||
expect(mockEmit).toHaveBeenCalledWith('clear')
|
||||
})
|
||||
|
||||
it('should not clear when disabled', () => {
|
||||
mockProps.disabled = true
|
||||
mockState.value = 'some text'
|
||||
|
||||
const { clear } = api({
|
||||
state: mockState,
|
||||
props: mockProps,
|
||||
emit: mockEmit
|
||||
})
|
||||
|
||||
clear()
|
||||
|
||||
expect(mockState.value).toBe('some text')
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Playwright 完整示例
|
||||
|
||||
```typescript
|
||||
// examples/sites/demos/input/test/input.spec.ts
|
||||
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('Input Component', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/input/basic')
|
||||
})
|
||||
|
||||
test('should render input correctly', async ({ page }) => {
|
||||
const input = page.getByRole('textbox')
|
||||
await expect(input).toBeVisible()
|
||||
await expect(input).toHaveAttribute('placeholder', '请输入')
|
||||
})
|
||||
|
||||
test('should handle user input', async ({ page }) => {
|
||||
const input = page.getByRole('textbox')
|
||||
await input.fill('Hello World')
|
||||
|
||||
await expect(input).toHaveValue('Hello World')
|
||||
})
|
||||
|
||||
test('should clear input when clear button clicked', async ({ page }) => {
|
||||
const input = page.getByRole('textbox')
|
||||
await input.fill('Test')
|
||||
|
||||
const clearButton = page.getByRole('button', { name: 'clear' })
|
||||
await clearButton.click()
|
||||
|
||||
await expect(input).toHaveValue('')
|
||||
})
|
||||
|
||||
test('should not accept input when disabled', async ({ page }) => {
|
||||
await page.goto('/input/disabled')
|
||||
|
||||
const input = page.getByRole('textbox')
|
||||
await expect(input).toBeDisabled()
|
||||
|
||||
await input.fill('Should not work')
|
||||
await expect(input).toHaveValue('')
|
||||
})
|
||||
|
||||
test('should show word limit', async ({ page }) => {
|
||||
await page.goto('/input/word-limit')
|
||||
|
||||
const input = page.getByRole('textbox')
|
||||
await input.fill('12345')
|
||||
|
||||
const wordLimit = page.getByText('5/10')
|
||||
await expect(wordLimit).toBeVisible()
|
||||
})
|
||||
|
||||
test('should validate required field', async ({ page }) => {
|
||||
await page.goto('/input/validation')
|
||||
|
||||
const submitButton = page.getByRole('button', { name: '提交' })
|
||||
await submitButton.click()
|
||||
|
||||
const errorMsg = page.getByText('此字段为必填项')
|
||||
await expect(errorMsg).toBeVisible()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## 常见陷阱
|
||||
|
||||
### ❌ 错误做法
|
||||
|
||||
1. **测试实现细节而非行为**
|
||||
|
||||
```typescript
|
||||
// ❌ 错误 - 测试内部状态
|
||||
it('should set state.hovering to true', () => {
|
||||
expect(state.hovering).toBe(true)
|
||||
})
|
||||
|
||||
// ✅ 正确 - 测试可见行为
|
||||
it('should show tooltip on hover', async () => {
|
||||
await element.hover()
|
||||
await expect(tooltip).toBeVisible()
|
||||
})
|
||||
```
|
||||
|
||||
2. **测试之间相互依赖**
|
||||
|
||||
```typescript
|
||||
// ❌ 错误
|
||||
it('step 1: login', () => {})
|
||||
it('step 2: navigate', () => {}) // 依赖 step 1
|
||||
|
||||
// ✅ 正确 - 每个测试独立
|
||||
it('should navigate after login', async () => {
|
||||
await login()
|
||||
await navigate()
|
||||
// 验证
|
||||
})
|
||||
```
|
||||
|
||||
3. **使用硬编码等待时间**
|
||||
|
||||
```typescript
|
||||
// ❌ 错误
|
||||
await page.waitForTimeout(5000)
|
||||
|
||||
// ✅ 正确
|
||||
await expect(element).toBeVisible({ timeout: 5000 })
|
||||
```
|
||||
|
||||
4. **忽略异步操作**
|
||||
|
||||
```typescript
|
||||
// ❌ 错误
|
||||
button.click()
|
||||
expect(result).toBe('done')
|
||||
|
||||
// ✅ 正确
|
||||
await button.click()
|
||||
await expect(result).toHaveText('done')
|
||||
```
|
||||
|
||||
### ✅ 最佳实践
|
||||
|
||||
1. **使用 beforeEach 重置状态**
|
||||
|
||||
```typescript
|
||||
beforeEach(() => {
|
||||
mockState = { value: '' }
|
||||
mockEmit = vi.fn()
|
||||
})
|
||||
```
|
||||
|
||||
2. **测试失败时提供清晰信息**
|
||||
|
||||
```typescript
|
||||
expect(result).toBe(expected)
|
||||
// 失败时会显示:Expected "expected" but received "actual"
|
||||
```
|
||||
|
||||
3. **Mock 外部依赖**
|
||||
|
||||
```typescript
|
||||
vi.mock('@opentiny/utils', () => ({
|
||||
debounce: vi.fn((fn) => fn)
|
||||
}))
|
||||
```
|
||||
|
||||
4. **覆盖率目标**
|
||||
|
||||
- 行覆盖率:≥ 80%
|
||||
- 分支覆盖率:≥ 75%
|
||||
- 函数覆盖率:≥ 85%
|
||||
|
||||
## 检查清单
|
||||
|
||||
编写测试前,确认:
|
||||
|
||||
### 单元测试
|
||||
|
||||
- [ ] 测试了所有公共 API
|
||||
- [ ] 覆盖了正常流程和异常流程
|
||||
- [ ] 测试了边界值(空值、null、undefined)
|
||||
- [ ] Mock 了所有外部依赖
|
||||
- [ ] 每个测试用例独立运行
|
||||
- [ ] 测试名称清晰描述预期行为
|
||||
|
||||
### E2E 测试
|
||||
|
||||
- [ ] 覆盖了主要用户操作流程
|
||||
- [ ] 测试了不同浏览器尺寸(响应式)
|
||||
- [ ] 验证了无障碍访问(键盘导航、屏幕阅读器)
|
||||
- [ ] 包含了视觉回归测试(可选)
|
||||
- [ ] 测试数据可重复使用
|
||||
- [ ] 没有硬编码等待时间
|
||||
|
||||
### 通用
|
||||
|
||||
- [ ] 测试文件命名符合规范(\*.spec.ts)
|
||||
- [ ] 使用了 AAA 模式组织代码
|
||||
- [ ] 没有测试私有实现细节
|
||||
- [ ] 添加了必要的注释说明复杂逻辑
|
||||
- [ ] 运行 `pnpm test:unit` 全部通过
|
||||
- [ ] 运行 `pnpm test:e2e` 全部通过
|
||||
|
||||
## 运行测试
|
||||
|
||||
```bash
|
||||
# 运行所有单元测试
|
||||
pnpm test:unit
|
||||
|
||||
# 运行 Vue 3 单元测试
|
||||
pnpm test:unit3
|
||||
|
||||
# 运行 Vue 2 单元测试
|
||||
pnpm test:unit2
|
||||
|
||||
# 运行所有 E2E 测试
|
||||
pnpm test:e2e
|
||||
|
||||
# 运行特定组件的 E2E 测试
|
||||
pnpm test:e2e --grep "input"
|
||||
|
||||
# 生成覆盖率报告
|
||||
pnpm test:unit --coverage
|
||||
```
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [Vitest 官方文档](https://vitest.dev/)
|
||||
- [Playwright 官方文档](https://playwright.dev/)
|
||||
- [Input 组件单元测试](../../packages/renderless/src/input/__tests__/)
|
||||
- [Input 组件 E2E 测试](../../examples/sites/demos/input/test/)
|
||||
- [AGENTS.md 测试要求](../../AGENTS.md#测试要求)
|
||||
|
|
@ -1,370 +0,0 @@
|
|||
# TinyVue 主题定制指南
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 为新组件编写样式
|
||||
- 修改现有组件的视觉效果
|
||||
- 创建自定义主题
|
||||
- 调整响应式断点
|
||||
|
||||
## 核心原则
|
||||
|
||||
### 1. 主题架构
|
||||
|
||||
TinyVue 支持多主题系统:
|
||||
|
||||
```
|
||||
packages/
|
||||
├── theme/
|
||||
│ └── src/
|
||||
│ ├── base / # 公共变量和混入
|
||||
│ │ ├── vars.less # 全局的CSS 变量定义
|
||||
│ │ └── reset.less # rest 样式定义
|
||||
│ │ └── transition.less # 全局动画 样式定义
|
||||
│ │ └── aurora-theme.less # vars.less 中的一个变体, aurora风格
|
||||
│ │ └── dark-theme.less # vars.less 中的一个变体, 暗黑风格
|
||||
│ │ └── motion-theme.less # vars.less 中的一个变体, motion 风格
|
||||
│ │ └── old-theme.less # vars.less 中的一个变体, 原来的主题风格
|
||||
│ ├── svgs / # 所有图标的原始 svg 文件
|
||||
│ └── <component>/ # 各组件样式
|
||||
│ └── vars.less # 组件级的 CSS 变量定义
|
||||
│ └── index.less # 组件的 CSS 样式
|
||||
└── theme-saas/ # SAAS 主题
|
||||
└── src/
|
||||
└── ... # 类似结构,不同变量值
|
||||
```
|
||||
|
||||
### 2. CSS 变量系统
|
||||
|
||||
Tinyvue的样式系统设计了一套多级CSS 变量系统,`base CSS 变量` 和 `common CSS 变量` 的定义都在 `packages/theme/src/base/vars.less`中;`组件的CSS 变量`的定义在各个组件的文件夹下面。
|
||||
|
||||
1. `base CSS 变量` 定义一套规范的颜色值和数字值, 以 --tv-base-\* 打头。
|
||||
2. `common CSS 变量` 定义一套通用的CSS 变量值,有明确的使用场景约束, 更好的名称可读性。所有的值必须从`base CSS 变量`里面选取, 以 --tv-小写字母 打头, 比如 --tv-color-\* 。
|
||||
3. `组件的CSS 变量` 定义在组件级别的CSS 变量值,方便用户定制组件样式。 它必须使用 `common CSS 变量` ,并且不允许使用 `base CSS 变量`的值。 以 --tv-组件名首字母大写 打头,比如: --tv-Button-\*。 每一个组件级CSS 变量的上面必须是对应的注释,写明它的使用的位置。
|
||||
|
||||
```less
|
||||
// base CSS 变量
|
||||
:root {
|
||||
--tv-base-color-brand: #191919;
|
||||
--tv-base-color-brand-1: #f0f7ff;
|
||||
--tv-base-color-brand-2: #deecff;
|
||||
--tv-base-color-brand-3: #b3d6ff;
|
||||
--tv-base-color-brand-4: #7eb7fc;
|
||||
--tv-base-color-brand-5: #4191fa;
|
||||
--tv-base-color-brand-6: #1476ff;
|
||||
--tv-base-color-brand-7: #0f5ed4;
|
||||
--tv-base-color-brand-8: #0845a6;
|
||||
--tv-base-color-brand-9: #022e7a;
|
||||
--tv-base-color-brand-10: #001a4a;
|
||||
--tv-base-color-brand-11: #3d6899;
|
||||
--tv-base-color-brand-12: #7fa6d4;
|
||||
--tv-base-color-brand-13: #b6d4f2;
|
||||
}
|
||||
// common CSS 变量
|
||||
:root {
|
||||
--tv-color-success-text: var(--tv-base-color-success-6); // #5cb300 成功-文本色 tag的light、plain类型
|
||||
--tv-color-success-text-primary: var(--tv-base-color-common-11); // #191919 常规一级文本色(非主题色)
|
||||
--tv-color-success-bg: var(--tv-base-color-success-6); //#5cb300 成功-背景色(深) tag的dark类型/tooltip/badge
|
||||
--tv-color-success-bg-light: var(--tv-base-color-success-14); // #e6f2d5 成功-背景色(浅)
|
||||
--tv-color-success-bg-1: var(--tv-base-color-success-14); // #e6f2d5 tag的light类型
|
||||
--tv-color-success-border: var(--tv-base-color-success-6); // #5cb300 成功-边框色(深)
|
||||
--tv-color-success-border-light: var(--tv-base-color-success-14); // #e6f2d5 成功-边框色(浅) 型
|
||||
--tv-color-success-border-1: var(--tv-base-color-success-14); // #e6f2d5 tag的light类型
|
||||
--tv-color-success-icon: var(--tv-base-color-success-6); // #5cb300 成功-图标色
|
||||
}
|
||||
|
||||
// 组件级 CSS 变量
|
||||
.inject-Button-vars() {
|
||||
// 默认时按钮字重
|
||||
--tv-Button-font-weight: var(--tv-font-weight-regular, 400);
|
||||
// 默认时按钮边框宽度
|
||||
--tv-Button-border-width: var(--tv-border-width, 1px);
|
||||
// 按钮的文本行高
|
||||
--tv-Button-line-height: var(--tv-line-height-number, 1.5);
|
||||
}
|
||||
```
|
||||
|
||||
**重要约束**
|
||||
|
||||
目前组件库已经稳定,在开发组件时,尽量复用已经存在的 `common CSS 变量`,不要添加新变量。
|
||||
|
||||
### 3. BEM 命名规范
|
||||
|
||||
在模板中,为节点添加类名时,使用 **Block\_\_Element--Modifier** 命名的规则:
|
||||
|
||||
```less
|
||||
// Block: 组件名
|
||||
.tiny-button {
|
||||
// Element: 组成部分(双下划线)
|
||||
&__icon {
|
||||
margin-right: @spacing-xs;
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
// Modifier: 状态变体(双横线)
|
||||
&--primary {
|
||||
background-color: @color-brand;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
opacity: @opacity-disabled;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```less
|
||||
@button-prefix-cls: ~'@{css-prefix}button';
|
||||
|
||||
.@{button-prefix-cls} {
|
||||
.inject-Button-vars();
|
||||
|
||||
&.@{button-prefix-cls}--large {
|
||||
.size-mixin(-large);
|
||||
}
|
||||
&.@{button-prefix-cls}--medium {
|
||||
.size-mixin(-medium);
|
||||
}
|
||||
&.@{button-prefix-cls}--small {
|
||||
.size-mixin(-small);
|
||||
}
|
||||
&.@{button-prefix-cls}--mini {
|
||||
.size-mixin(-mini);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 组件的样式开发规范
|
||||
|
||||
一个组件通常都有三个样式文件:
|
||||
|
||||
1. vars.less: 组件级 CSS 变量。 通过分析组件中,哪些地方的样式需要适配不同主题,以及它们的值在 `common CSS 变量`中存在的,就需要找出来定义为组件级变量。 每一个组件级变量上面必须写注释指示该变量的使用场景。示例如下:
|
||||
|
||||
```less
|
||||
.inject-Button-vars() {
|
||||
// 默认时按钮字重
|
||||
--tv-Button-font-weight: var(--tv-font-weight-regular, 400);
|
||||
// 默认时按钮边框宽度
|
||||
--tv-Button-border-width: var(--tv-border-width, 1px);
|
||||
// 按钮的文本行高
|
||||
--tv-Button-line-height: var(--tv-line-height-number, 1.5);
|
||||
// 默认时按钮圆角
|
||||
--tv-Button-border-radius: var(--tv-border-radius-md, 6px); // 默认还原为6px
|
||||
// 大圆角时按钮圆角
|
||||
--tv-Button-border-radius-round: var(--tv-border-radius-round, 999px);
|
||||
// 圆形时按钮圆角
|
||||
--tv-Button-border-radius-circle: var(--tv-border-radius-round, 999px);
|
||||
}
|
||||
```
|
||||
|
||||
2. index.less: 组件的样式编写。每个组件的根节点,
|
||||
|
||||
首先要注入一下自己的组件级 CSS 变量,以便这些变量生效。如果组件有多个根节点,或者有弹出层等场景,需要给所有的根节点或弹出层的根节点添加这个注入。
|
||||
|
||||
其次,尽量使用 `less`的`嵌套结构`和`父选择器 &` 的能力进行编写。
|
||||
|
||||
最后,需要使用组件级的CSS变量,避免使用 `common CSS 变量` 和 `base CSS 变量`。
|
||||
|
||||
示例如下:
|
||||
|
||||
```less
|
||||
@alert-prefix-cls: ~'@{css-prefix}alert';
|
||||
|
||||
.@{alert-prefix-cls} {
|
||||
.inject-Alert-vars();
|
||||
|
||||
position: relative;
|
||||
display: flex;
|
||||
border: none;
|
||||
border-radius: var(--tv-Alert-border-radius);
|
||||
padding: var(--tv-Alert-padding-y) var(--tv-Alert-padding-x);
|
||||
margin: var(--tv-Alert-margin-y) var(--tv-Alert-margin-x);
|
||||
line-height: 1.5;
|
||||
|
||||
&.is-center {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/** alert-icon 场景 */
|
||||
.@{alert-prefix-cls}__icon:not(.@{alert-prefix-cls}__close) {
|
||||
font-size: var(--tv-Alert-icon-size);
|
||||
margin-right: var(--tv-Alert-icon-margin-right);
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. 组件的响应式设计
|
||||
|
||||
组件在不同的屏幕尺寸时,有不同的规范要求时,就需要添加 `responsive.less` 样式文件,编写在指定的屏幕大小时应该呈现的样式。开发规范同 `index.ts` 一致。示例如下:
|
||||
|
||||
```less
|
||||
@import '../custom.less';
|
||||
@import './vars.less';
|
||||
@alert-prefix-cls: ~'@{css-prefix}alert';
|
||||
|
||||
@media screen and (max-width: 1280px) {
|
||||
.@{alert-prefix-cls} {
|
||||
.inject-Alert-responsive-vars();
|
||||
|
||||
.@{alert-prefix-cls}__content {
|
||||
.@{alert-prefix-cls}__title {
|
||||
font-size: var(--tv-Alert-title-responsive-font-size);
|
||||
}
|
||||
|
||||
.@{alert-prefix-cls}__description {
|
||||
color: var(--tv-Alert-title-responsive-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5 移动优先的多端模板的开发规范
|
||||
|
||||
组件的多端模板 `mobile-first.vue`文件,不使用传统的 `BEM`样式规范,而是使用 `tailwind css`进行开发,`tailwind css`的配置文件在`..\packages\theme-saas\tailwind.config.js` 中,其中有定制的颜色值,断点值,布局等配置信息。
|
||||
|
||||
多端模板不需要引入任何的 css 文件,在模板中,只需要添加tailwind的类名即可,示例如下:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div
|
||||
data-tag="tiny-alert"
|
||||
v-if="state.show"
|
||||
:class="
|
||||
m(
|
||||
'min-h-min flex py-2 sm:py-3 px-4 my-2 rounded box-border font-light sm:font-normal text-color-text-primary',
|
||||
{ 'bg-color-info-primary-subtler': type === 'info' || !type },
|
||||
{ 'bg-color-error-subtler': type === 'error' },
|
||||
{ 'bg-color-warning-subtler': type === 'warning' },
|
||||
{ 'bg-color-success-subtler': type === 'success' },
|
||||
{ 'text-center': center },
|
||||
customClass
|
||||
)
|
||||
"
|
||||
>
|
||||
<span
|
||||
v-else-if="closeText && closable"
|
||||
data-tag="tiny-alert-close-text"
|
||||
@click="handleClose"
|
||||
class="leading-6 text-sm cursor-pointer"
|
||||
>{{ closeText }}</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
上面的 `m函数`是适配层注入的tailwind merge函数,用于合并类名。 如果元素上的类名太长,也可以将类名抽取为变量,集中编写到 `token.ts`文件中。
|
||||
|
||||
```typescript token.ts
|
||||
export const classes = {
|
||||
'button': 'inline-block.....',
|
||||
'size-default': 'h-10 text-sm sm:h-7'
|
||||
// ....
|
||||
}
|
||||
```
|
||||
|
||||
### Tailwind CSS 集成(Mobile First)
|
||||
|
||||
```vue
|
||||
<!-- 使用 Tailwind 工具类 -->
|
||||
<template>
|
||||
<div
|
||||
:class="
|
||||
m(
|
||||
'flex items-center justify-between',
|
||||
'px-4 py-2 sm:px-6 sm:py-3',
|
||||
'bg-white dark:bg-gray-800',
|
||||
'border border-gray-200 rounded-lg',
|
||||
'hover:border-blue-500 transition-colors'
|
||||
)
|
||||
"
|
||||
>
|
||||
<!-- 内容 -->
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## 主题定制检查清单
|
||||
|
||||
编写样式时,确认:
|
||||
|
||||
### 变量使用
|
||||
|
||||
- [ ] 尽量使用common CSS 变量
|
||||
- [ ] 所有间距使用间距变量
|
||||
- [ ] 所有字体大小使用字体变量
|
||||
- [ ] 没有硬编码的十六进制颜色值
|
||||
|
||||
### BEM 命名
|
||||
|
||||
- [ ] 类名遵循 BEM 规范
|
||||
- [ ] Block 名称与组件名一致
|
||||
- [ ] Element 使用双下划线 `__`
|
||||
- [ ] Modifier 使用双横线 `--`
|
||||
|
||||
### 响应式
|
||||
|
||||
- [ ] 考虑移动端优先设计
|
||||
- [ ] 测试不同屏幕尺寸
|
||||
- [ ] 使用相对单位(rem、em、%)
|
||||
- [ ] 避免固定宽度,使用 max-width
|
||||
|
||||
### 无障碍
|
||||
|
||||
- [ ] 颜色对比度符合 WCAG AA 标准
|
||||
- [ ] 焦点状态清晰可见
|
||||
- [ ] 禁用状态明确标识
|
||||
- [ ] 支持键盘导航样式
|
||||
|
||||
### 性能
|
||||
|
||||
- [ ] 避免深层嵌套(不超过 3 层)
|
||||
- [ ] 合理使用 CSS 过渡
|
||||
- [ ] 避免使用 `!important`
|
||||
- [ ] 合并重复的样式规则
|
||||
|
||||
### 兼容性
|
||||
|
||||
- [ ] 深色模式适配(如需要)
|
||||
- [ ] 浏览器兼容性检查
|
||||
|
||||
## 调试技巧
|
||||
|
||||
### 查看 CSS 变量
|
||||
|
||||
```javascript
|
||||
// 在浏览器控制台执行
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--tv-color-brand')
|
||||
```
|
||||
|
||||
### 临时覆盖样式
|
||||
|
||||
```vue
|
||||
<style scoped>
|
||||
/* 仅用于调试,不要提交 */
|
||||
.tiny-my-component {
|
||||
border: 1px solid red !important;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
### 使用浏览器 DevTools
|
||||
|
||||
1. 打开 Elements 面板
|
||||
2. 查看 Computed 样式
|
||||
3. 检查 CSS 变量值
|
||||
4. 实时修改测试效果
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [Less 官方文档](https://lesscss.org/)
|
||||
- [BEM 命名规范](https://getbem.com/)
|
||||
- [WCAG 无障碍指南](https://www.w3.org/WAI/WCAG21/quickref/)
|
||||
- [Button 组件级变量示例](../../packages/theme/src/button/vars.less)
|
||||
- [Button 组件样式示例](../../packages/theme/src/button/index.less)
|
||||
- [Button 组件响应样式的示例](../../packages/theme/src/button/responsive.less)
|
||||
- [Tailwind CSS 文档](https://tailwindcss.com/)
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
# @opentiny/utils 开发规范
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 在 renderless 层或 vue-hooks 中编写与框架无关的纯逻辑
|
||||
- 复用日期、字符串、DOM、校验等通用能力
|
||||
- 新增跨组件工具函数
|
||||
|
||||
## 核心原则
|
||||
|
||||
### 1. 包定位
|
||||
|
||||
`packages/utils` 发布为 `@opentiny/utils`,**不依赖 Vue**,可被以下模块引用:
|
||||
|
||||
- `packages/renderless`(无渲染逻辑层,主要消费方)
|
||||
- `packages/vue-hooks`
|
||||
- `packages/vue-directive`
|
||||
- `packages/vue-common`(适配层少量使用)
|
||||
|
||||
renderless 层约定:**除 `@opentiny/utils` 与 `@opentiny/vue-hooks` 外,不得依赖其它第三方包**。
|
||||
|
||||
### 2. 目录结构
|
||||
|
||||
```text
|
||||
packages/utils/src/
|
||||
├── index.ts # 统一导出入口
|
||||
├── array/ # 数组操作
|
||||
├── bigInt/ # 大数/精度计算
|
||||
├── calendar/ # 日历相关
|
||||
├── crypt/ # sha256 等
|
||||
├── date/ # 日期格式化、时区
|
||||
├── date-util/ # 日期工具(与 date 部分能力重叠,待整理)
|
||||
├── debounce/ # 防抖
|
||||
├── throttle/ # 节流
|
||||
├── dom/ # DOM 操作、样式、滚动
|
||||
├── decimal/ # 小数精度
|
||||
├── event/ # 事件派发
|
||||
├── form/ # 表单常量(待迁移至组件内部)
|
||||
├── function/ # noop、callInterceptor
|
||||
├── globalConfig/ # isServer、browserInfo、globalConfig
|
||||
├── logger/ # 日志
|
||||
├── nanoid/ # 唯一 ID
|
||||
├── object/ # 对象拷贝、合并、相等
|
||||
├── string/ # 字符串格式化、驼峰/连字符
|
||||
├── type/ # 类型判断
|
||||
├── validate/ # 校验器 Validator
|
||||
├── xss/ # XSS 过滤
|
||||
├── tree-model/ # 树形数据结构
|
||||
├── popper/ # Popper 定位
|
||||
├── popup-manager/ # 弹层层级管理
|
||||
└── ... # 其它模块见 src/index.ts
|
||||
```
|
||||
|
||||
新增工具时:在对应子目录实现,并在 `src/index.ts` 中导出。
|
||||
|
||||
### 3. 模块编写规范
|
||||
|
||||
1. **纯函数**:不持有组件状态,不 import Vue API
|
||||
2. **SSR 安全**:涉及 `window`/`document` 时,使用 `isServer`(来自 `globalConfig`)做守卫
|
||||
3. **单职责**:一个文件聚焦一类能力,避免在 utils 中写组件专属常量(`common/`、`form/` 中部分常量标注为「待移除」,新代码勿再扩展)
|
||||
4. **测试**:在模块目录下补充 `__tests__` 或 `__test__`,在 `packages/utils` 目录执行 `pnpm test`,或在仓库根目录执行 `pnpm --filter @opentiny/utils test`
|
||||
|
||||
### 4. 常用 API 分类
|
||||
|
||||
| 分类 | 代表导出 | 典型用途 |
|
||||
| --------- | --------------------------------------------------------------------------------------------------- | -------------------------------------- |
|
||||
| 类型判断 | `isObject`, `isFunction`, `isDate`, `typeOf` | 参数校验 |
|
||||
| 字符串 | `camelize`, `hyphenate`, `formatString`, `guid` | 命名转换、展示格式化 |
|
||||
| 日期 | `formatDate`, `toDateStr`, `limitTimeRange`;`calendar` 的 `parseDate`;`date-util` 的 `parseDate1` | 日期/时间组件(注意两套 parse 勿混用) |
|
||||
| 对象/数组 | `extend`, `merge`, `isEqual`, `find`, `unique` | 数据处理 |
|
||||
| DOM | `on`, `off`, `addClass`, `getScrollParent` | 事件与布局 |
|
||||
| 性能 | `debounce`, `throttle`, `fastdom` | 高频回调、布局批处理 |
|
||||
| 安全 | `xss`, `sha256` | 内容过滤、摘要 |
|
||||
| ID | `nanoid`(`nanoid.api.nanoid(size)`) | 无障碍 id、唯一 key |
|
||||
|
||||
### 5. 在 renderless 中的引用方式
|
||||
|
||||
```typescript
|
||||
// ✅ 正确:从 @opentiny/utils 按需导入
|
||||
import { debounce } from '@opentiny/utils'
|
||||
import { nanoid } from '@opentiny/utils'
|
||||
|
||||
// 使用 nanoid
|
||||
const id = `tiny-alert-title-${nanoid.api.nanoid(8)}`
|
||||
|
||||
// ❌ 错误:在 renderless 中 import 'vue'
|
||||
// ❌ 错误:在 utils 中 import '@opentiny/vue-common'
|
||||
```
|
||||
|
||||
### 6. nanoid 使用说明
|
||||
|
||||
`nanoid` 以命名空间导出,推荐通过 `nanoid.api` 访问:
|
||||
|
||||
```typescript
|
||||
import { nanoid } from '@opentiny/utils'
|
||||
|
||||
nanoid.api.nanoid(8) // 默认长度
|
||||
nanoid.api.customAlphabet('abc', 10) // 自定义字母表
|
||||
nanoid.random() // 0~1 随机数(SSR 下返回 0)
|
||||
```
|
||||
|
||||
### 7. 禁止事项
|
||||
|
||||
- ❌ 不得在 utils 中引入 Vue、`@opentiny/vue-common`、`@opentiny/vue-hooks`
|
||||
- ❌ 不得将仅某一组件使用的常量长期放在 `common/`、`form/`(应下沉到对应 renderless 或组件)
|
||||
- ❌ 不得重复造轮子:新增前先检索 `src/index.ts` 是否已有同类方法
|
||||
- ❌ 注意 `date` 与 `date-util` 存在部分重名导出(如 `toDate` / `toDate1`),优先使用语义清晰的现有 API,避免再增加别名
|
||||
|
||||
## 构建与发布
|
||||
|
||||
```bash
|
||||
# 在 packages/utils 目录
|
||||
pnpm build # vite 构建
|
||||
pnpm test # vitest
|
||||
pnpm pub # 发布(维护者)
|
||||
```
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [统一导出](../../packages/utils/src/index.ts)
|
||||
- [debounce 示例](../../packages/utils/src/debounce/index.ts)
|
||||
- [renderless 中的引用](../../packages/renderless/src/alert/vue.ts)
|
||||
|
|
@ -1,253 +0,0 @@
|
|||
# TinyVue 组件开发规范
|
||||
|
||||
## 核心原则
|
||||
|
||||
在 `packages/vue/src` 目录中,每个组件有一个独立的文件夹进行隔离。
|
||||
|
||||
每一个组件的结构为:
|
||||
|
||||
1. index.ts: 整个组件的对外导出对象
|
||||
2. src/index.ts: 整合 pc/mobile-first 模板为一个统一的组件。通常要把pc/mobile-first模板的全量属性定义在这里并导出给2个模板使用
|
||||
3. src/pc.vue: pc 浏览器下的模板,需要引入外部的css样式文件。
|
||||
4. src/mobile-first.vue: 移动优先的浏览器模板, 使用tailwind进行内联类名。
|
||||
|
||||
## 详细解释每个文件的编写规范
|
||||
|
||||
1. index.ts
|
||||
|
||||
该文件是整个组件的对外导出对象,负责给组件添加install方法和veresion属性。 示例如下:
|
||||
|
||||
```typescript
|
||||
import Alert from './src/index'
|
||||
import { version } from './package.json'
|
||||
|
||||
Alert.install = function (Vue) {
|
||||
Vue.component(Alert.name, Alert)
|
||||
}
|
||||
|
||||
Alert.version = version
|
||||
|
||||
if (process.env.BUILD_TARGET === 'runtime') {
|
||||
if (typeof window !== 'undefined' && window.Vue) {
|
||||
Alert.install(window.Vue)
|
||||
}
|
||||
}
|
||||
|
||||
export default Alert
|
||||
```
|
||||
|
||||
2. src/index.ts
|
||||
|
||||
整合 pc/mobile-first 模板为一个统一的组件。示例如下:
|
||||
|
||||
```typescript
|
||||
import { $props, $prefix, $setup, defineComponent } from '@opentiny/vue-common'
|
||||
import template from 'virtual-template?pc|mobile-first'
|
||||
|
||||
export const alertProps = {
|
||||
// ......
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: $prefix + 'Alert',
|
||||
props: alertProps,
|
||||
setup(props, context) {
|
||||
return $setup({ props, context, template })
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
同时引入2个模板的语法为 `import template from 'virtual-template?pc|mobile-first'` , 它是非标准的TS用法,有专门的`vite`插件会将其编译为两个模板的引入。 `template`是一个函数,它需要传入 `$setup`函数。
|
||||
|
||||
3. src/pc.vue 和 src/mobile-first.vue 的模板共同规范
|
||||
|
||||
这2个文件是组件的**视图模板**,
|
||||
|
||||
- 负责 UI 渲染和用户交互
|
||||
- 通过适配层的 `setup` 调用 renderless 层的函数
|
||||
- 可以包含2个跨端模板(如 `mobile-first.vue`、`pc.vue`)
|
||||
- 禁止内联样式,需要从外部引入。比如:import '@opentiny/vue-theme/alert/index.less'
|
||||
- 必须引入类型定义文件。 比如: import type { IAlertApi } from '@opentiny/vue-renderless/types/alert.type'
|
||||
|
||||
一个标准的模板文件写法如下:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- 模板内容 -->
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { renderless, api } from '@opentiny/vue-renderless/alert/vue'
|
||||
import { props, setup, defineComponent } from '@opentiny/vue-common'
|
||||
import type { IAlertApi } from '@opentiny/vue-renderless/types/alert.type'
|
||||
import '@opentiny/vue-theme/alert/index.less'
|
||||
|
||||
export default defineComponent({
|
||||
props: [...props /** 其它属性名 */],
|
||||
setup(props, context) {
|
||||
return setup({ props, context, renderless, api }) as unknown as IAlertApi
|
||||
}
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
**适配层 setup** 函数作用:
|
||||
|
||||
1. 纽带作用
|
||||
|
||||
我们从 '@opentiny/vue-common' 引入 `setup`函数, 它是联系`无渲染逻辑层` 和 `模板`的纽带。 将`无渲染逻辑层`的组件逻辑函数传入`setup`后,
|
||||
setup内部会调用该函数,并传入组件实例的 porps, vue官方包对象,以及适配层构造的vm上下文对象,这样`无渲染逻辑层`的组件逻辑函数就会获得组件的所有令牌。 最后它会返回一个 {state,api} 对象,以便模板的绑定。
|
||||
|
||||
2. 处理模板选择
|
||||
|
||||
如果组件是跨端组件,那么它本质上是一个双层组件,父组件是用来选择使用 pc.vue ,mobile-first.vue 哪个模板, 子组件就是真实的组件。 `setup`函数内部负责向父组件同步状态和属性。此时 setup可选择性传入 `mono:false` 的属性,来告诉setup函数来同步这个状态和属性。
|
||||
|
||||
如果组件是一个单一组件,没有通用 `$setup` 来选择模板,那么在调用setup时, 必须传入 `mono:true` 来指示,它是一个单一组件。示例如下:
|
||||
|
||||
```typescript
|
||||
setup(props, context) {
|
||||
return setup({ props, context, renderless, api, mono:true }) as unknown as IAlertApi
|
||||
}
|
||||
```
|
||||
|
||||
3. 向`无渲染逻辑层`的组件逻辑函数传入额外的数据
|
||||
|
||||
`无渲染逻辑层`的设计上,它不能依赖第三方的库,如果之它需要引入其它对象,需要在 `模板层` 引入它们,并通过`setup` 的`extendOptions`属性 传入 `无渲染逻辑层`的组件逻辑函数中,示例如下:
|
||||
|
||||
```typescript
|
||||
import FluentEditor from '@opentiny/fluent-editor'
|
||||
|
||||
setup(props, context) {
|
||||
return setup({ props, context, renderless, api,
|
||||
extendOptions: {
|
||||
FluentEditor
|
||||
}})
|
||||
}
|
||||
```
|
||||
|
||||
## 模板规范
|
||||
|
||||
1. 必须使用 vue2, vue3同时兼容的模板语法。
|
||||
2. 必须是单根节点
|
||||
3. 不允许 v-if / v-for在同一个节点上, 建议使用 <template> 来使用 v-if / v-for
|
||||
4. 不允许使用 `id` 等属性
|
||||
5. 允许使用 Teleport 组件, 但必须从 '@opentiny/vue-common' 包中导入。
|
||||
6. 模板中可以使用 `a函数`,比如: ` v-bind="a($attrs, ['class', 'style', 'title', 'id'], true)"` , `a 函数`的意思是从$attrs上过滤出一些属性绑定到元素上。 最后一个参数为true的话,表示这些属性要保留下来,如果为false,则表示这些属性要过滤掉,其它的属性才保留下来。
|
||||
7. 模板中可以使用 `t函数`, 它是用来加载国际化内容。 比如: `{{ t('ui.base.cancel') }}`
|
||||
8. 模板中建议增加 `aria-*`的 无障碍信息,尤其是表单元素和图标元素等。
|
||||
|
||||
### mobile-first.vue的规范
|
||||
|
||||
1. 它使用 `tailwind css`进行模板开发,不依赖外部的样式库。
|
||||
2. 如果模板中的类名过长,可以将类名转换为变量,约定这些变量存放在 `tokens.ts` 文件中, 示例如下
|
||||
|
||||
```typescript token.ts
|
||||
export const classes = {
|
||||
'button': 'inline-block.....',
|
||||
'size-default': 'h-10 text-sm sm:h-7'
|
||||
// ....
|
||||
}
|
||||
```
|
||||
|
||||
3. mobile-first模板中,由于不能使用类名表示节点的作用,建议给关键的dom元素,增加 `data-tag`属性来表示dom的作用。根结点统一要增加 data-tag, 示例如下:
|
||||
|
||||
```html
|
||||
<button data-tag="tiny-button"></button>
|
||||
```
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [最终导出示例](../../packages/vue/src/button/index.ts)
|
||||
- [整合模板为一个统一的组件示例](../../packages/vue/src/button/src/index.ts)
|
||||
- [Button pc模板示例](../../packages/vue/src/button/src/pc.vue)
|
||||
- [Button mobile-first模板示例](../../packages/vue/src/button/src/mobile-first.vue)
|
||||
|
||||
## 多模式介绍
|
||||
|
||||
TinyVue 提供了 PC 和 Mobile 组件库,两套组件库对外是同一份依赖`@opentiny/vue`,同名组件通过`tiny_mode`切换组件模式。
|
||||
针对 SaaS 业务场景,TinyVue 提供了基于`tailwind`实现的多端组件,
|
||||
在 TinyVue 基础上新增`多端模式`,支持业务切换同名组件,同名组件默认情况下是`桌面模式`即`PC 组件`。
|
||||
|
||||
### 模式分类
|
||||
|
||||
AUI 组件库提供了三种组件模式:`桌面模式`、`多端模式(移动优先)`
|
||||
|
||||
| 模式 | 模式介绍 | 模式配置 |
|
||||
| -------- | -------- | ------------ |
|
||||
| 桌面模式 | 纯 PC | pc |
|
||||
| 多端模式 | 多端一致 | mobile-first |
|
||||
|
||||
### 模式切换
|
||||
|
||||
AUI 组件模式设置优先级
|
||||
|
||||
`单组件切换` > `模式透传` > `全局切换` > `组件默认模式`
|
||||
|
||||
#### 单组件切换
|
||||
|
||||
可在组件标签上配置`tiny_mode`属性,指定组件模式,就会选择对应模板渲染:
|
||||
|
||||
- 桌面模式:`pc`
|
||||
- 多端模式:`mobile-first`
|
||||
|
||||
参考示例如下:
|
||||
|
||||
```html
|
||||
<tiny-button tiny_mode="mobile-first">默认按钮</tiny-button>
|
||||
```
|
||||
|
||||
#### 全局切换
|
||||
|
||||
通过在 Vue 的原型上全局设置`tiny_mode`,可以指定所有同名组件的默认模式。
|
||||
在项目入口 `src/main.js` 文件中导入 Vue 依赖后,增加如下配置:
|
||||
|
||||
```js
|
||||
// Vue 2.0
|
||||
Vue.prototype.tiny_mode = { value: 'mobile-first' }
|
||||
|
||||
// Vue 3.0
|
||||
app.config.globalProperties.tiny_mode = { value: 'mobile-first' }
|
||||
```
|
||||
|
||||
#### 模式透传
|
||||
|
||||
如果想在页面部分区域切换模式,如卡片级控制,
|
||||
可以在外层 AUI 组件上添加`tiny_mode_root`属性,透传当前 AUI 组件的`tiny_mode`配置到所有子级 AUI 组件上,
|
||||
参考 demo 示例如下:
|
||||
|
||||
```html
|
||||
<template>
|
||||
<div>
|
||||
<tiny-layout>
|
||||
<tiny-row tiny_mode="pc" tiny_mode_root>
|
||||
<tiny-button>PC 按钮</tiny-button>
|
||||
<tiny-button type="primary" native-type="submit">主要按钮</tiny-button>
|
||||
<tiny-button type="success">成功按钮</tiny-button>
|
||||
<tiny-button type="info">信息按钮</tiny-button>
|
||||
<tiny-button type="warning">警告按钮</tiny-button>
|
||||
<tiny-button type="danger">危险按钮</tiny-button>
|
||||
</tiny-row>
|
||||
<tiny-row tiny_mode="mobile-first" tiny_mode_root>
|
||||
<tiny-button>多端按钮</tiny-button>
|
||||
<tiny-button type="primary" native-type="submit">主要按钮</tiny-button>
|
||||
<tiny-button type="success">成功按钮</tiny-button>
|
||||
<tiny-button type="info">信息按钮</tiny-button>
|
||||
<tiny-button type="warning">警告按钮</tiny-button>
|
||||
<tiny-button type="danger">危险按钮</tiny-button>
|
||||
</tiny-row>
|
||||
</tiny-layout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Button, Layout, Row } from '@opentiny/vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TinyButton: Button,
|
||||
TinyLayout: Layout,
|
||||
TinyRow: Row
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
34
AGENTS.md
34
AGENTS.md
|
|
@ -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(单元)+ Playwright(E2E)
|
||||
|
||||
## E2E 测试触发
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
`
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -10,10 +10,8 @@ export default {
|
|||
type: 'string',
|
||||
defaultValue: '',
|
||||
desc: {
|
||||
'zh-CN':
|
||||
'限制文件类型(thumbnail-mode 模式下此参数无效),支持<code> 后缀名(.pdf),MIME类型(image/png),MIME类型通配符(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
|
|
@ -32,7 +32,7 @@ export default {
|
|||
'en-US': ''
|
||||
},
|
||||
mode: ['pc', 'mobile-first'],
|
||||
pcDemo: 'before-link-open'
|
||||
pcDemo: ''
|
||||
},
|
||||
{
|
||||
name: 'data-type',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}`
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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: ''
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
`
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 不显示 title,large 显示 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')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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('消息')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
`
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ const value = ref('')
|
|||
|
||||
<style scoped>
|
||||
.demo-date-picker-wrap {
|
||||
/* 输入框收窄到 240px,比面板默认宽度 360px 窄,宽度差让 left/center/right 对齐效果清晰可见 */
|
||||
width: 240px;
|
||||
width: 360px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ export default {
|
|||
|
||||
<style scoped>
|
||||
.demo-date-picker-wrap {
|
||||
/* 输入框收窄到 240px,比面板默认宽度 360px 窄,宽度差让 left/center/right 对齐效果清晰可见 */
|
||||
width: 240px;
|
||||
width: 360px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
})
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
@ -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>
|
||||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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('调职补偿')
|
||||
})
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
||||
// ==================== 示例1:drawLink 完全自定义绘制 ====================
|
||||
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)
|
||||
|
||||
// ==================== 示例2:styleLink + 外部悬停检测 ====================
|
||||
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的 styleLink,只查询示例2自己的 canvas
|
||||
chartConfig2.styleLink = (ctx, afterLink) => {
|
||||
const link = afterLink.raw
|
||||
const key = `${link.from}-${link.to}`
|
||||
|
||||
// 关键:只查询示例2 wrapper 内的 canvas,避免和示例1冲突
|
||||
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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 // createItem存在,items数组长度大于此值时显示列表,默认值是1。items数组长度等于1,不显示,所以设置成0。
|
||||
|
||||
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>
|
||||
|
|
@ -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 // createItem存在,items数组长度大于此值时显示列表,默认值是1。items数组长度等于1,不显示,所以设置成0。
|
||||
|
||||
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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 // createItem存在,items数组长度大于此值时显示列表,默认值是1。items数组长度等于1,不显示,所以设置成0。
|
||||
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>
|
||||
|
|
@ -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 // createItem存在,items数组长度大于此值时显示列表,默认值是1。items数组长度等于1,不显示,所以设置成0。
|
||||
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>
|
||||
|
|
@ -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 // createItem存在,items数组长度大于此值时显示列表,默认值是1。items数组长度等于1,不显示,所以设置成0。
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -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('王五')
|
||||
})
|
||||
|
|
@ -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 // createItem存在,items数组长度大于此值时显示列表,默认值是1。items数组长度等于1,不显示,所以设置成0。
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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()
|
||||
})
|
||||
|
|
@ -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>
|
||||
|
|
@ -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']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ import { TinyGrid, TinyGridColumn, TinyGridToolbar, TinyModal } from '@opentiny/
|
|||
|
||||
const toolbarButtons = ref([
|
||||
{
|
||||
code: 'clearSelected',
|
||||
code: ' clearSelected',
|
||||
name: '手动清除单元格选中状态'
|
||||
}
|
||||
])
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ export default {
|
|||
return {
|
||||
toolbarButtons: [
|
||||
{
|
||||
code: 'clearSelected',
|
||||
code: ' clearSelected',
|
||||
name: '手动清除单元格选中状态'
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue