Full Code of tsai996/lowflow-design for AI

main 8c4b11c9a68a cached
80 files
150.7 KB
45.1k tokens
42 symbols
1 requests
Download .txt
Repository: tsai996/lowflow-design
Branch: main
Commit: 8c4b11c9a68a
Files: 80
Total size: 150.7 KB

Directory structure:
gitextract_5dzaix2j/

├── .eslintrc-auto-import.json
├── .eslintrc.cjs
├── .github/
│   └── workflows/
│       └── gh-pages.yml
├── .gitignore
├── .prettierrc.json
├── .vscode/
│   └── extensions.json
├── LICENSE
├── README.en.md
├── README.md
├── env.d.ts
├── index.html
├── package.json
├── public/
│   └── CNAME
├── src/
│   ├── App.vue
│   ├── api/
│   │   ├── index.ts
│   │   └── modules/
│   │       ├── model.ts
│   │       ├── role.ts
│   │       └── user.ts
│   ├── components/
│   │   ├── AdvancedFilter/
│   │   │   ├── Operator.vue
│   │   │   ├── Trigger.vue
│   │   │   ├── index.vue
│   │   │   └── type.ts
│   │   ├── Render/
│   │   │   ├── index.tsx
│   │   │   └── type.ts
│   │   ├── RoleSelector/
│   │   │   ├── RolePicker.vue
│   │   │   ├── RoleTag.vue
│   │   │   └── index.vue
│   │   ├── SvgIcon/
│   │   │   ├── index.scss
│   │   │   └── index.tsx
│   │   └── UserSelector/
│   │       ├── UserPicker.vue
│   │       ├── UserTag.vue
│   │       └── index.vue
│   ├── hooks/
│   │   └── useDraggableScroll.ts
│   ├── main.ts
│   ├── mock/
│   │   ├── index.ts
│   │   ├── role.ts
│   │   └── user.ts
│   ├── mockProdServer.ts
│   ├── router/
│   │   └── index.ts
│   ├── stores/
│   │   └── counter.ts
│   ├── styles/
│   │   ├── el-segmented.scss
│   │   ├── element/
│   │   │   ├── dark.scss
│   │   │   └── index.scss
│   │   └── index.scss
│   ├── typings/
│   │   ├── auto-imports.d.ts
│   │   ├── components.d.ts
│   │   └── index.d.ts
│   └── views/
│       ├── flowDesign/
│       │   ├── index.vue
│       │   ├── nodes/
│       │   │   ├── Add.vue
│       │   │   ├── ApprovalNode.vue
│       │   │   ├── CcNode.vue
│       │   │   ├── ConditionNode.vue
│       │   │   ├── EndNode.vue
│       │   │   ├── ExclusiveNode.vue
│       │   │   ├── GatewayNode.vue
│       │   │   ├── Node.vue
│       │   │   ├── NotifyNode.vue
│       │   │   ├── ServiceNode.vue
│       │   │   ├── StartNode.vue
│       │   │   ├── TimerNode.vue
│       │   │   ├── TreeNode.vue
│       │   │   └── type.ts
│       │   └── panels/
│       │       ├── ApprovalPanel.vue
│       │       ├── AssigneePanel.vue
│       │       ├── CcPanel.vue
│       │       ├── ConditionPanel.vue
│       │       ├── EndPanel.vue
│       │       ├── ExecutionListeners.vue
│       │       ├── NotifyPanel.vue
│       │       ├── ServicePanel.vue
│       │       ├── StartPanel.vue
│       │       ├── TaskListeners.vue
│       │       ├── TimerPanel.vue
│       │       └── index.vue
│       └── home/
│           └── index.vue
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
├── unocss.config.ts
└── vite.config.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .eslintrc-auto-import.json
================================================
{
  "globals": {
    "Component": true,
    "ComponentPublicInstance": true,
    "ComputedRef": true,
    "EffectScope": true,
    "ExtractDefaultPropTypes": true,
    "ExtractPropTypes": true,
    "ExtractPublicPropTypes": true,
    "InjectionKey": true,
    "PropType": true,
    "Ref": true,
    "VNode": true,
    "WritableComputedRef": true,
    "computed": true,
    "createApp": true,
    "customRef": true,
    "defineAsyncComponent": true,
    "defineComponent": true,
    "effectScope": true,
    "getCurrentInstance": true,
    "getCurrentScope": true,
    "h": true,
    "inject": true,
    "isProxy": true,
    "isReactive": true,
    "isReadonly": true,
    "isRef": true,
    "markRaw": true,
    "nextTick": true,
    "onActivated": true,
    "onBeforeMount": true,
    "onBeforeRouteLeave": true,
    "onBeforeRouteUpdate": true,
    "onBeforeUnmount": true,
    "onBeforeUpdate": true,
    "onDeactivated": true,
    "onErrorCaptured": true,
    "onMounted": true,
    "onRenderTracked": true,
    "onRenderTriggered": true,
    "onScopeDispose": true,
    "onServerPrefetch": true,
    "onUnmounted": true,
    "onUpdated": true,
    "provide": true,
    "reactive": true,
    "readonly": true,
    "ref": true,
    "resolveComponent": true,
    "shallowReactive": true,
    "shallowReadonly": true,
    "shallowRef": true,
    "toRaw": true,
    "toRef": true,
    "toRefs": true,
    "toValue": true,
    "triggerRef": true,
    "unref": true,
    "useAttrs": true,
    "useCssModule": true,
    "useCssVars": true,
    "useLink": true,
    "useRoute": true,
    "useRouter": true,
    "useSlots": true,
    "watch": true,
    "watchEffect": true,
    "watchPostEffect": true,
    "watchSyncEffect": true,
    "ElCheckbox": true,
    "ElInput": true,
    "ElInputNumber": true,
    "ElRadio": true,
    "ElSelect": true,
    "ElDivider": true
  }
}


================================================
FILE: .eslintrc.cjs
================================================
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')

module.exports = {
  root: true,
  'extends': [
    'plugin:vue/vue3-essential',
    'eslint:recommended',
    '@vue/eslint-config-typescript',
    '@vue/eslint-config-prettier/skip-formatting'
  ],
  parserOptions: {
    ecmaVersion: 'latest'
  },
  rules: {
    'vue/no-mutating-props': ['error', {
      'shallowOnly': true
    }],
    'vue/multi-word-component-names': 'off'
  }
}


================================================
FILE: .github/workflows/gh-pages.yml
================================================
name: GitHub Pages
# 触发脚本的条件,develop分支push代码的时候
on:
  push:
    branches:
      - main
# 要执行的任务
jobs:
  # 任务名称
  build_and_deploy:
    # runs-on 指定job任务运行所需要的虚拟机环境(必填)
    runs-on: ubuntu-latest
    # 任务步骤
    steps:
      - name: 迁出代码
        # 使用action库  actions/checkout获取源码
        uses: actions/checkout@v3 # 使用的工具
      # 使用 pnpm
      - name: 使用 pnpm
        uses: pnpm/action-setup@v2
        with:
          version: 8.5.0
      # 安装node
      - name: 安装node.js
        # 使用action库 actions/setup-node 安装node
        uses: actions/setup-node@v3
        with:
          node-version: 16.20.0
          cache: 'pnpm'
      # 安装
      - name: 安装依赖
        run: pnpm install
      # 打包
      - name: 打包
        run: pnpm build:test
      # 部署
      - name: 部署到gh-pages分支
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./dist
          # 指定推送分支,默认为:gh-pages
          publish_branch: gh-pages


================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
.DS_Store
dist
dist-ssr
coverage
*.local

/cypress/videos/
/cypress/screenshots/

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

*.tsbuildinfo


================================================
FILE: .prettierrc.json
================================================
{
  "$schema": "https://json.schemastore.org/prettierrc",
  "semi": false,
  "tabWidth": 2,
  "singleQuote": true,
  "printWidth": 100,
  "trailingComma": "none"
}

================================================
FILE: .vscode/extensions.json
================================================
{
  "recommendations": [
    "Vue.volar",
    "dbaeumer.vscode-eslint",
    "esbenp.prettier-vscode"
  ]
}


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2023 Victor Tsai

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.en.md
================================================
<div align="center">
  <h1>lowflow-design</h1>
  <p>Low-code Workflow Designer</p>
  <p>
    <a href="./README.md">中文</a> | <a href="./README.en.md">English</a>
  </p>
</div>

## Overview
`lowflow-design` is a workflow designer built with `Vue 3`, `Vite`, `TypeScript`, and `Element Plus`. It is designed for low-code/no-code platforms that need visual workflow configuration.

You can create workflows visually and convert workflow JSON to BPMN XML with the backend converter project:
- Backend converter: [GitHub](https://github.com/tsai996/lowflow-design-converter) | [Gitee](https://gitee.com/cai_xiao_feng/lowflow-design-converter)
- Traditional `bpmn-js` workflow designer: [GitHub](https://github.com/tsai996/vue-bpmn-designer) | [Gitee](https://gitee.com/cai_xiao_feng/vue-bpmn-designer)

## Live Demo
- Preview: <https://tsai996.github.io/lowflow-design/>
- Full demo: <https://demo.lowflow.vip/>

## Screenshots
<p>
  <img alt="workflow designer" src="public/flow.png" style="display: inline-block"/>
  <img alt="property panel" src="public/penal.png" style="display: inline-block"/>
</p>

## Features
- Approval node: supports single user, multiple users, roles, departments, initiator, manager, and custom approvers.
- CC node: supports single user, multiple users, roles, departments, initiator, manager, and custom CC recipients.
- Conditional branches: supports condition groups and combination logic.
- Timer wait: supports second, minute, hour, day, week, month, and custom durations.
- Notification node: supports in-app, email, WeCom, DingTalk, Feishu, SMS, and more.

## Tech Stack
- Vue 3
- TypeScript
- Vite
- Element Plus
- Pinia
- Vue Router
- UnoCSS

## Quick Start
### 1. Install dependencies
```bash
npm install
```

### 2. Start development server
```bash
npm run dev
```

### 3. Build
```bash
npm run build
```

### 4. Preview production build
```bash
npm run preview
```

## Scripts
```bash
npm run dev         # run dev server
npm run build       # production build (with type check)
npm run build:dev   # build in development mode
npm run build:test  # build in test mode
npm run preview     # preview production build
npm run type-check  # run type checking
npm run lint        # run ESLint (with --fix)
npm run format      # format src with Prettier
```

## Project Structure
```text
.
|-- public/
|-- src/
|   |-- api/
|   |-- assets/
|   |-- components/
|   |-- hooks/
|   |-- mock/
|   |-- router/
|   |-- stores/
|   |-- styles/
|   |-- typings/
|   |-- views/
|   |   |-- flowDesign/
|   |   |   |-- nodes/
|   |   |   |-- panels/
|   |   |   `-- index.vue
|   |-- App.vue
|   `-- main.ts
|-- package.json
|-- vite.config.ts
`-- README.md
```

## Repositories
| Platform | Frontend | Backend Converter |
| --- | --- | --- |
| GitHub | <https://github.com/tsai996/lowflow-design> | <https://github.com/tsai996/lowflow-design-converter> |
| Gitee | <https://gitee.com/cai_xiao_feng/lowflow-design> | <https://gitee.com/cai_xiao_feng/lowflow-design-converter> |

## Community and Support
### Community Groups
<p>
  <img alt="WeChat" src="public/wx.jpg" width="240" height="400" style="display: inline-block"/>
  <img alt="QQ Group" src="public/qq_qun.jpg" width="240" height="400" style="display: inline-block"/>
</p>

### Sponsor
If this project helps you, sponsorship is welcome.

<p>
  <img alt="WeChat Pay" src="public/wxpay.png" height="240" width="240" style="display: inline-block"/>
  <img alt="Alipay" src="public/alipay.png" height="240" width="240" style="display: inline-block"/>
</p>

## Recommended Reading
*In-depth Flowable Workflow Engine: Core Principles and Advanced Practice*:
<https://item.jd.com/14804836.html>

![flowable](public/flowable.jpg)


================================================
FILE: README.md
================================================
<div align="center">
  <h1>lowflow-design</h1>
  <p>低代码流程设计器</p>
  <p>
    <a href="./README.md">中文</a> | <a href="./README.en.md">English</a>
  </p>
</div>

## 项目简介
`lowflow-design` 是一个基于 `Vue 3`、`Vite`、`TypeScript`、`Element Plus` 的流程设计器,适用于低代码/无代码平台中的流程配置场景。

项目支持通过可视化方式快速搭建流程,并可配合后端转换项目将流程 JSON 转换为 BPMN XML:
- 后端转换器:[GitHub](https://github.com/tsai996/lowflow-design-converter) | [Gitee](https://gitee.com/cai_xiao_feng/lowflow-design-converter)
- 传统 `bpmn-js` 流程设计器:[GitHub](https://github.com/tsai996/vue-bpmn-designer) | [Gitee](https://gitee.com/cai_xiao_feng/vue-bpmn-designer)

## 在线体验
- 预览地址:<https://tsai996.github.io/lowflow-design/>
- 成品示例:<https://demo.lowflow.vip/>

## 效果预览
<p>
  <img alt="流程设计器" src="public/flow.png" style="display: inline-block"/>
  <img alt="属性面板" src="public/penal.png" style="display: inline-block"/>
</p>

## 功能特性
- 审批节点:支持单人、多人、角色、部门、发起人、上级领导、自定义审批人等。
- 抄送节点:支持单人、多人、角色、部门、发起人、上级领导、自定义抄送人等。
- 条件分支:支持条件组及组合逻辑。
- 计时等待:支持秒、分、时、天、周、月及自定义时长。
- 消息通知:支持站内信、邮件、企业微信、钉钉、飞书、短信等通知方式。

## 技术栈
- Vue 3
- TypeScript
- Vite
- Element Plus
- Pinia
- Vue Router
- UnoCSS

## 快速开始
### 1. 安装依赖
```bash
npm install
```

### 2. 启动开发环境
```bash
npm run dev
```

### 3. 构建
```bash
npm run build
```

### 4. 预览构建产物
```bash
npm run preview
```

## 常用脚本
```bash
npm run dev         # 本地开发
npm run build       # 生产构建(含类型检查)
npm run build:dev   # development 模式构建
npm run build:test  # test 模式构建
npm run preview     # 预览构建产物
npm run type-check  # 类型检查
npm run lint        # ESLint(自动修复)
npm run format      # Prettier 格式化(src)
```

## 项目结构
```text
.
|-- public/
|-- src/
|   |-- api/
|   |-- assets/
|   |-- components/
|   |-- hooks/
|   |-- mock/
|   |-- router/
|   |-- stores/
|   |-- styles/
|   |-- typings/
|   |-- views/
|   |   |-- flowDesign/
|   |   |   |-- nodes/
|   |   |   |-- panels/
|   |   |   `-- index.vue
|   |-- App.vue
|   `-- main.ts
|-- package.json
|-- vite.config.ts
`-- README.md
```

## 源码仓库
| 平台 | 前端 | 后端转换器 |
| --- | --- | --- |
| GitHub | <https://github.com/tsai996/lowflow-design> | <https://github.com/tsai996/lowflow-design-converter> |
| Gitee | <https://gitee.com/cai_xiao_feng/lowflow-design> | <https://gitee.com/cai_xiao_feng/lowflow-design-converter> |

## 交流与支持
### 交流群
<p>
  <img alt="微信" src="public/wx.jpg" width="240" height="400" style="display: inline-block"/>
  <img alt="QQ群" src="public/qq_qun.jpg" width="240" height="400" style="display: inline-block"/>
</p>

### 赞助
如果这个项目对你有帮助,欢迎赞助支持。

<p>
  <img alt="微信赞助" src="public/wxpay.png" height="240" width="240" style="display: inline-block"/>
  <img alt="支付宝赞助" src="public/alipay.png" height="240" width="240" style="display: inline-block"/>
</p>

## 推荐阅读
推荐搭配阅读《深入 Flowable 流程引擎:核心原理与高阶实战》:
<https://item.jd.com/14804836.html>

![flowable](public/flowable.jpg)


================================================
FILE: env.d.ts
================================================
/// <reference types="vite/client" />

interface ImportMetaEnv {
    readonly VITE_API_URL: string;
    readonly VITE_PUBLIC_PATH: string;
}

interface ImportMeta {
    readonly env: ImportMetaEnv
}


================================================
FILE: index.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <link rel="icon" href="/favicon.ico">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>


================================================
FILE: package.json
================================================
{
  "name": "lowflow-design",
  "version": "0.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "run-p type-check \"build-only {@}\" --",
    "build:dev": "vite build --mode development",
    "build:test": "vite build --mode test",
    "preview": "vite preview",
    "build-only": "vite build",
    "type-check": "vue-tsc --build --force",
    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
    "format": "prettier --write src/"
  },
  "dependencies": {
    "@element-plus/icons-vue": "^2.3.1",
    "@vueuse/core": "^10.9.0",
    "axios": "^1.6.8",
    "element-plus": "^2.7.2",
    "file-saver": "^2.0.5",
    "lodash-es": "^4.17.21",
    "pinia": "^2.1.7",
    "vue": "^3.4.21",
    "vue-router": "^4.3.0"
  },
  "devDependencies": {
    "@rushstack/eslint-patch": "^1.8.0",
    "@tsconfig/node20": "^20.1.4",
    "@types/file-saver": "^2.0.7",
    "@types/lodash-es": "^4.17.12",
    "@types/node": "^20.12.5",
    "@vitejs/plugin-vue": "^5.0.4",
    "@vitejs/plugin-vue-jsx": "^3.1.0",
    "@vue/eslint-config-prettier": "^9.0.0",
    "@vue/eslint-config-typescript": "^13.0.0",
    "@vue/tsconfig": "^0.5.1",
    "eslint": "^8.57.0",
    "eslint-plugin-vue": "^9.23.0",
    "mockjs": "^1.1.0",
    "npm-run-all2": "^6.1.2",
    "prettier": "^3.2.5",
    "sass": "^1.77.0",
    "typescript": "~5.4.0",
    "unocss": "^0.59.4",
    "unplugin-auto-import": "^0.17.5",
    "unplugin-vue-components": "^0.27.0",
    "vite": "^5.2.8",
    "vite-plugin-mock": "^2.9.6",
    "vite-plugin-svg-icons": "^2.0.1",
    "vite-plugin-vue-setup-extend": "^0.4.0",
    "vue-tsc": "^2.0.11"
  }
}


================================================
FILE: public/CNAME
================================================
vite-starter.element-plus.org


================================================
FILE: src/App.vue
================================================
<script setup lang="ts">
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import { ElNotification } from 'element-plus'

onMounted(() => {
  setTimeout(() => {
    ElNotification({
      type: 'info',
      position: 'bottom-left',
      duration: 10000,
      title: '求职信息',
      message: h('div', [
        h('p', '96年失业(全栈开发)求收留'),
        h('p', '地点:杭州'),
        h('p', '微信:xfcai1216')
      ])
    })
  }, 2000)
})
</script>

<template>
  <el-config-provider namespace="el" :locale="zhCn">
    <router-view></router-view>
  </el-config-provider>
</template>

<style scoped>
#app {
  color: var(--el-text-color-primary);
}
</style>


================================================
FILE: src/api/index.ts
================================================
import axios, {
  type AxiosInstance,
  AxiosError,
  type AxiosRequestConfig,
  type InternalAxiosRequestConfig,
  type AxiosResponse
} from 'axios'
import { ElNotification } from 'element-plus'

export interface Result {
  code: number
  success: boolean
  message: string
}

export interface ResultData<T = any> extends Result {
  data: T
}

/**
 * axios配置
 */
const config = {
  baseURL: import.meta.env.VITE_API_URL,
  timeout: 8000
}

class RequestHttp {
  service: AxiosInstance

  /**
   * 请求构造函数
   * @param config
   */
  public constructor(config: AxiosRequestConfig) {
    this.service = axios.create(config)
    this.service.interceptors.request.use(
      (config: InternalAxiosRequestConfig) => {
        return config
      },
      (error: AxiosError) => {
        return Promise.reject(error)
      }
    )
    this.service.interceptors.response.use(
      (response: AxiosResponse) => {
        const { data } = response
        return data
      },
      (error: AxiosError) => {
        const { response, message } = error
        const data = response?.data as ResultData
        const errMsg = data ? data.message : message
        ElNotification.error(errMsg || '未知错误')
        return Promise.reject(response?.data || error)
      }
    )
  }

  /**
   * get请求
   * @param url
   * @param params
   * @param config
   */
  get<T>(url: string, params?: object, config = {}): Promise<ResultData<T>> {
    return this.service.get(url, { params, ...config })
  }

  /**
   * post请求
   * @param url
   * @param data
   * @param config
   */
  post<T>(url: string, data?: object, config = {}): Promise<ResultData<T>> {
    return this.service.post(url, data, config)
  }

  /**
   * request请求
   * @param config
   */
  request<T>(config: AxiosRequestConfig): Promise<ResultData<T>> {
    return this.service.request(config)
  }

  /**
   * 下载文件
   * @param url
   * @param data
   * @param config
   */
  download(url: string, data?: object, config = {}): Promise<BlobPart> {
    return this.service.post(url, data, { ...config, responseType: 'blob' })
  }
}

export default new RequestHttp(config)


================================================
FILE: src/api/modules/model.ts
================================================
import http from '@/api'
import FileSaver from 'file-saver'

export const downloadXml = async (data: object) => {
  const res = await http.download('https://demo.lowflow.vip/api/model/download', data)
  FileSaver.saveAs(
    new Blob([res], { type: 'application/octet-stream;charset=utf-8' }),
    '测试流程.bpmn20.xml'
  )
}


================================================
FILE: src/api/modules/role.ts
================================================
import http from '@/api/index'

export interface Role {
  id: string
  name: string
}

/**
 * 获取角色信息
 * @param id
 */
export const getById = (id: string) => {
  return http.get<Role>(`/role/info`, { id: id })
}

/**
 * 查询角色列表
 */
export const getList = (roleIds?: string[]) => {
  const params = roleIds ? { roleIds: roleIds } : {}
  return http.post<Role[]>('/role/list', params)
}


================================================
FILE: src/api/modules/user.ts
================================================
import http from '@/api/index'

export interface User {
  id: string
  username: string
  name: string
  avatar: string
}

/**
 * 获取用户信息
 * @param username
 */
export const getByUsername = (username: string) => {
  return http.get<User>(`/user/info`, { username: username })
}

/**
 * 查询用户列表
 */
export const getList = (userIds?: string[]) => {
  const params = userIds ? { userIds: userIds } : {}
  return http.post<User[]>('/user/list', params)
}


================================================
FILE: src/components/AdvancedFilter/Operator.vue
================================================
<script setup lang="ts">
import { useVModel } from '@vueuse/core'

const $props = defineProps<{
  modelValue: string
}>()
const operatorOptions = [
  {
    value: 'eq',
    label: '等于'
  },
  {
    value: 'ne',
    label: '不等于'
  },
  {
    label: '包含',
    value: 'in'
  },
  {
    label: '不包含',
    value: 'ni'
  }
]
const $emits = defineEmits<{
  (e: 'update:modelValue', modelValue: any): void
}>()
const data = useVModel($props, 'modelValue', $emits)
</script>

<template>
  <el-select class="operator-container" v-model="data" filterable placeholder="筛选符">
    <el-option
      v-for="item in operatorOptions"
      :key="item.value"
      :label="item.label"
      :value="item.value"
    />
  </el-select>
</template>

<style scoped lang="scss">
.operator-container {
  width: 100%;
  flex-shrink: 0;
}
</style>


================================================
FILE: src/components/AdvancedFilter/Trigger.vue
================================================
<script setup lang="ts">
import type { Field } from '@/components/Render/type'
import type { FilterRules } from '@/components/AdvancedFilter/type'
import { useVModel } from '@vueuse/core'

const $props = defineProps<{
  modelValue: any
  options: Field[]
  filterRules: FilterRules
}>()
const $emits = defineEmits<{
  (e: 'update:modelValue', modelValue: string): void
}>()
const data = useVModel($props, 'modelValue', $emits)
</script>

<template>
  <el-select class="trigger-container" v-model="data" filterable placeholder="选择字段">
    <el-option v-for="item in $props.options" :key="item.id" :label="item.label" :value="item.id" />
  </el-select>
</template>

<style scoped lang="scss"></style>


================================================
FILE: src/components/AdvancedFilter/index.vue
================================================
<script setup lang="ts" name="AdvancedFilter">
import type { FilterRules } from './type'
import type { Field } from '@/components/Render/type'
import { useVModel } from '@vueuse/core'
import Trigger from './Trigger.vue'
import Operator from './Operator.vue'

const $props = defineProps<{
  filterFields: Field[]
  modelValue: FilterRules
}>()
const $emits = defineEmits<{
  (e: 'update:modelValue', modelValue: FilterRules): void
  (e: 'addCondition', index: number): void
  (e: 'delCondition', index: number): void
  (e: 'delGroup'): void
}>()
const filterRules = useVModel($props, 'modelValue', $emits)
/**
 * 添加条件
 */
const addRule = () => {
  filterRules.value.conditions.push({
    field: null,
    operator: 'eq',
    value: null
  })
}
/**
 * 删除条件
 * @param index
 */
const handleDel = (index: number) => {
  filterRules.value.conditions.splice(index, 1)
  if (filterRules.value.conditions.length <= 0) {
    $emits('delGroup')
  }
  $emits('delCondition', index)
}
/**
 * 条件条件组
 */
const addGroup = () => {
  filterRules.value.groups.push({
    operator: 'and',
    conditions: [
      {
        field: null,
        operator: '',
        value: null
      }
    ],
    groups: []
  })
}
/**
 * 删除条件组
 * @param index
 */
const delGroup = (index: number) => {
  filterRules.value.groups.splice(index, 1)
}
</script>

<template>
  <div class="filter-container">
    <div class="logical-operator">
      <div class="logical-operator__line"></div>
      <el-switch
        v-model="filterRules.operator"
        inline-prompt
        style="--el-switch-on-color: #409eff; --el-switch-off-color: #67c23a"
        active-value="and"
        inactive-value="or"
        active-text="且"
        inactive-text="或"
      />
    </div>
    <div class="filter-option-content">
      <el-form :label-width="0" :inline="true" :model="filterRules">
        <el-row
          v-for="(item, index) in filterRules.conditions"
          :key="`${item.field}-${index}`"
          :gutter="5"
          class="filter-item-rule"
        >
          <el-col :xs="24" :sm="7">
            <el-form-item :prop="'conditions.' + index + '.field'" style="width: 100%">
              <trigger
                ref="triggerRef"
                :options="$props.filterFields.filter((e) => e.value !== undefined)"
                :filter-rules="filterRules"
                v-model="item.field"
                @update:model-value="item.value = null"
              />
            </el-form-item>
          </el-col>
          <el-col :xs="24" :sm="5" v-if="item.field">
            <el-form-item :prop="'conditions.' + index + '.operator'" style="width: 100%">
              <operator ref="operatorRef" v-model="item.operator" />
            </el-form-item>
          </el-col>
          <el-col :xs="24" :sm="10" v-if="item.field">
            <el-form-item :prop="'conditions.' + index + '.value'" style="width: 100%">
              <Render
                :field="$props.filterFields.find((e) => e.id === item.field) as Field"
                v-model="item.value"
              />
            </el-form-item>
          </el-col>
          <el-col
            :xs="24"
            :sm="2"
            style="display: flex; align-items: center; flex-direction: row-reverse"
          >
            <el-button plain circle type="danger" icon="Delete" @click="handleDel(index)" />
          </el-col>
        </el-row>
        <AdvancedFilter
          v-for="(item, index) in filterRules.groups"
          :key="index"
          @delGroup="delGroup(index)"
          v-model="filterRules.groups[index]"
          :filterFields="filterFields"
        >
          <el-button @click="delGroup(index)" icon="CircleClose" class="filter-filter-item__add">
            删除条件组
          </el-button>
        </AdvancedFilter>
        <div
          v-if="filterRules.groups.length === 0 && filterRules.conditions.length === 0"
          class="filter-item-rule"
        />
      </el-form>
      <div class="filter-item-rule">
        <el-button @click="addRule" icon="CirclePlus" class="filter-filter-item__add">
          添加条件
        </el-button>
        <el-button @click="addGroup" icon="CirclePlus" class="filter-filter-item__add">
          添加条件组
        </el-button>
        <slot />
      </div>
    </div>
  </div>
</template>

<style scoped lang="scss">
:deep(.el-form-item) {
  margin-right: 0;
  margin-bottom: 0;
}

.filter-container {
  background-color: var(--el-fill-color-blank);
  border-radius: 3px;
  display: flex;

  .logical-operator {
    position: relative;
    display: flex;
    align-items: center;
    overflow: hidden;
    min-width: 60px;
    padding-right: 5px;

    .logical-operator__line {
      position: absolute;
      left: calc(32% - 1px);
      width: 30px;
      border-width: 1px 0 1px 1px;
      border-top-style: solid;
      border-bottom-style: solid;
      border-left-style: solid;
      border-top-color: var(--el-border-color);
      border-bottom-color: var(--el-border-color);
      border-left-color: var(--el-border-color);
      border-image: initial;
      border-right-style: initial;
      border-right-color: initial;
      border-radius: 5px 0 0 5px;
      height: calc(100% - 48px);

      &::before {
        content: '';
        position: absolute;
        top: 0;
        right: 0;
        transform: translateX(100%) translateY(-50%);
        width: 6px;
        height: 6px;
        border: var(--el-border);
        border-radius: 50%;
      }

      &::after {
        content: '';
        position: absolute;
        bottom: 0;
        right: 0;
        transform: translateX(100%) translateY(50%);
        width: 6px;
        height: 6px;
        border: var(--el-border);
        border-radius: 50%;
      }
    }
  }

  .filter-option-content {
    position: relative;
    width: 100%;

    .filter-item-rule {
      display: flex;
      align-items: center;
      min-height: 48px;
    }

    .filter-filter-item__add {
      border-style: dashed;
      width: 100%;
    }
  }
}
</style>


================================================
FILE: src/components/AdvancedFilter/type.ts
================================================
/**
 * 字段筛选结果
 */
export interface Condition {
  // 筛选字段
  field: string | null
  // 条件运算符
  operator: string
  // 筛选值
  value: any | null
}

/**
 * 筛选规则
 */
export interface FilterRules {
  operator: 'or' | 'and'
  conditions: Condition[]
  groups: FilterRules[]
}


================================================
FILE: src/components/Render/index.tsx
================================================
import { cloneDeep } from 'lodash-es'
import type { Field } from './type'
import type { PropType } from 'vue'

export default defineComponent({
  props: {
    modelValue: {
      type: [String, Number, Boolean, Array, Object] as PropType<any>,
      default: undefined,
      required: false
    },
    field: {
      type: Object as PropType<Field>,
      required: true
    }
  },
  emits: ['update:modelValue'],
  components: {
    ElInput: defineAsyncComponent(() => import('element-plus/es').then(({ ElInput }) => ElInput)),
    ElInputNumber: defineAsyncComponent(() =>
      import('element-plus/es').then(({ ElInputNumber }) => ElInputNumber)
    ),
    ElSelect: defineAsyncComponent(() =>
      import('element-plus/es').then(({ ElSelect }) => ElSelect)
    ),
    ElRadio: defineAsyncComponent(() => import('element-plus/es').then(({ ElRadio }) => ElRadio)),
    ElCheckbox: defineAsyncComponent(() =>
      import('element-plus/es').then(({ ElCheckbox }) => ElCheckbox)
    ),
    UserSelector: defineAsyncComponent(() => import('@/components/UserSelector/index.vue')),
    RoleSelector: defineAsyncComponent(() => import('@/components/RoleSelector/index.vue'))
  },
  setup(props, { emit }) {
    /**
     * 构建属性参数
     * @param fieldClone
     */
    const buildProps = (fieldClone: Field) => {
      const dataObject: Record<string, any> = {}
      const _props = fieldClone.props || {}
      Object.keys(_props).forEach((key) => {
        dataObject[key] = _props[key]
      })
      if (props.modelValue !== undefined) {
        dataObject.modelValue = props.modelValue
      } else {
        dataObject.modelValue = fieldClone.value
      }
      dataObject['onUpdate:modelValue'] = (value: any) => {
        emit('update:modelValue', value)
      }
      delete dataObject.options
      return dataObject
    }
    /**
     * 构建插槽
     * @param fieldClone
     */
    const buildSlots = (fieldClone: Field) => {
      const children: Record<string, any> = {}
      const slotFunctions: Record<string, any> = {
        ElSelect: (conf: Field) => {
          return conf.props.options.map((item: any) => {
            return <el-option label={item.label} value={item.value}></el-option>
          })
        },
        ElRadio: (conf: Field) => {
          return conf.props.options.map((item: any) => {
            return <el-radio label={item.value}>{item.label}</el-radio>
          })
        },
        ElCheckbox: (conf: Field) => {
          return conf.props.options.map((item: any) => {
            return <el-checkbox label={item.value}>{item.label}</el-checkbox>
          })
        }
      }
      const slotFunction = slotFunctions[fieldClone.name]
      if (slotFunction) {
        children.default = () => {
          return slotFunction(fieldClone)
        }
      }
      return children
    }
    return {
      buildProps,
      buildSlots
    }
  },
  render() {
    const fieldClone: Field = cloneDeep(this.field)
    const slots = this.buildSlots(fieldClone)
    const props = this.buildProps(fieldClone)
    const eleComponent = resolveComponent(fieldClone.name)
    if (typeof eleComponent === 'string') {
      return h(eleComponent, props, slots)
    }
    return h(eleComponent, props, slots)
  }
})


================================================
FILE: src/components/Render/type.ts
================================================
export interface Field {
  id: string
  type: 'formItem' | 'container'
  label: string
  name: string
  value: any
  readonly?: boolean
  required?: boolean
  hidden: boolean
  props: Recordable
  children?: Field[]
}


================================================
FILE: src/components/RoleSelector/RolePicker.vue
================================================
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import type { TreeNodeData } from 'element-plus/es/components/tree/src/tree.type'
import { type ElTree } from 'element-plus'
import { getList } from '@/api/modules/role'

export type ModelValueType = string | string[] | null | undefined

export interface RoleDropdownProps {
  modelValue: ModelValueType
  multiple?: boolean
}

const treeProps = {
  label: 'name',
  children: 'children',
  isLeaf: 'leaf',
  class: (role: TreeNodeData) => renderClass(role)
}

export interface Role {
  id: string
  name: string
}

const $props = withDefaults(defineProps<RoleDropdownProps>(), {
  multiple: false
})

const $emits = defineEmits<{
  (e: 'update:modelValue', modelValue: ModelValueType): void
}>()

const value = useVModel($props, 'modelValue', $emits)

const roleOptions = ref<Role[]>([])
const roleOrgOptions = ref<Role[]>([])
const orgTreeRef = ref<InstanceType<typeof ElTree>>()
const expandedKeys = ref<string[]>([])

const renderClass = (role: TreeNodeData): string | { [key: string]: boolean } => {
  const val = roleOptions.value.find((e) => e.id === role.id)
  if (val) {
    return 'is-active'
  } else {
    return ''
  }
}

const onNodeClick = (data: Role) => {
  if ($props.multiple) {
    const index = roleOptions.value.findIndex((e) => e.id === data.id)
    if (index === -1) {
      roleOptions.value.push(data)
      roleOptions.value.sort((a, b) => a.id.localeCompare(b.id))
    } else {
      roleOptions.value.splice(index, 1)
    }
  } else {
    const index = roleOptions.value.findIndex((e) => e.id === data.id)
    if (index === -1) {
      roleOptions.value = [data]
    } else {
      roleOptions.value.splice(index, 1)
    }
  }
}
const dialogVisible = ref(false)
const queryForm = reactive({
  name: null
})

watch(
  () => queryForm.name,
  (val) => {
    orgTreeRef.value?.filter(val)
  }
)
const filterNode = (value: string, data: TreeNodeData): boolean => {
  if (!value) return true
  return data.name.includes(value)
}
const open = () => {
  dialogVisible.value = true
}
const onOpen = () => {
  getList().then((res) => {
    if (res.success) {
      roleOrgOptions.value = res.data.map((e) => {
        return {
          id: e.id,
          name: e.name
        } as Role
      })
    }
  })

  let roleIds: string[] = []
  if (Array.isArray(value.value)) {
    roleIds.push(...value.value)
  } else if (value.value) {
    roleIds.push(value.value)
  }
  if (roleIds.length > 0) {
    getList(roleIds).then((res) => {
      if (res.success) {
        roleOptions.value = res.data.map((role) => {
          return {
            id: role.id,
            name: role.name
          } as Role
        })
        roleOptions.value.sort((a, b) => a.id.localeCompare(b.id))
      }
    })
  } else {
    roleOptions.value = []
  }
}
const handelConfirm = () => {
  if ($props.multiple) {
    value.value = roleOptions.value.map((e) => e.id)
  } else {
    if (roleOptions.value.length > 0) {
      value.value = roleOptions.value[0].id
    } else {
      value.value = null
    }
  }
  dialogVisible.value = false
}
defineExpose({
  open
})
</script>

<template>
  <el-dialog
    v-model="dialogVisible"
    @open="onOpen"
    :lock-scroll="false"
    align-center
    draggable
    title="选择角色"
    width="30%"
  >
    <el-card shadow="never" class="org-card">
      <template #header>
        <el-input
          v-model="queryForm.name"
          placeholder="输入关键字进行查询"
          :style="{ width: '100%' }"
          suffix-icon="search"
          clearable
        >
        </el-input>
      </template>
      <el-scrollbar tag="div" class="org-tree">
        <el-tree
          ref="orgTreeRef"
          node-key="id"
          :data="roleOrgOptions"
          :default-expanded-keys="expandedKeys"
          :props="treeProps"
          :filter-node-method="filterNode"
          @node-click="onNodeClick"
        >
          <template #default="{ data }">
            <div class="flex flex-1 flex-items-center flex-justify-between">
              <div class="flex-center">
                <el-icon :size="16">
                  <School />
                </el-icon>
                &nbsp;{{ data.name }}
              </div>
              <el-icon class="is-selected">
                <Check />
              </el-icon>
            </div>
          </template>
        </el-tree>
      </el-scrollbar>
    </el-card>
    <template #footer>
      <el-button @click="dialogVisible = false">取消</el-button>
      <el-button type="primary" @click="handelConfirm">确认</el-button>
    </template>
  </el-dialog>
</template>

<style scoped lang="scss">
:deep {
  .el-tree {
    --el-tree-node-content-height: 40px;

    .el-tree-node__content {
      border-radius: 8px;
      margin: 2px 0 2px 0;
    }

    .is-active {
      color: var(--el-color-primary);

      .is-selected {
        display: block;
      }
    }
  }
}

.el-card {
  background-color: transparent;

  :deep(.el-card__header) {
    padding: 10px !important;
  }

  :deep(.el-card__body) {
    padding: 0 !important;
  }
}

.org-tree {
  height: 270px;
  padding: 5px;
}

.is-selected {
  display: none;
  padding-right: 15px;
}
</style>


================================================
FILE: src/components/RoleSelector/RoleTag.vue
================================================
<script setup lang="ts">
import { getById } from '@/api/modules/role'

export interface RoleTagProps {
  id: string
  type?: 'success' | 'info' | 'warning' | 'danger'
  closable?: boolean
}

const $props = withDefaults(defineProps<RoleTagProps>(), {
  closable: false,
  type: 'info'
})
const $emits = defineEmits<{
  (e: 'close', id: string): void
}>()

export interface RoleInfo {
  id?: string
  name?: string
}

let roleInfo = reactive<RoleInfo>({
  id: undefined,
  name: undefined
})
onMounted(() => {
  if (!$props.id) {
    throw new Error('username is required')
  }
  getById($props.id).then((res) => {
    if (res.success) {
      roleInfo.id = res.data.id
      roleInfo.name = res.data.name
    }
  })
})
const onClose = () => {
  $emits('close', $props.id)
}
</script>
<template>
  <el-tag round :closable="$props.closable" :type="type" effect="light" @close="onClose">
    <div class="flex-center" style="gap: 4px; grid-gap: 4px">
      <span>{{ roleInfo.name || id }}</span>
    </div>
  </el-tag>
</template>

<style scoped lang="scss">
:deep {
  .el-tag__content:only-child {
    margin-right: 4px;
  }
}
</style>


================================================
FILE: src/components/RoleSelector/index.vue
================================================
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import RoleTag from './RoleTag.vue'
import RolePicker, { type ModelValueType } from './RolePicker.vue'
import { useFormDisabled, useFormSize } from 'element-plus'
import type { CSSProperties } from 'vue'

export interface RoleSelectorProps {
  modelValue: ModelValueType
  placeholder?: string
  multiple?: boolean
  disabled?: boolean
  style?: CSSProperties
}

const $props = withDefaults(defineProps<RoleSelectorProps>(), {
  multiple: false,
  disabled: false,
  placeholder: '请选择角色'
})
const $emits = defineEmits<{
  (e: 'update:modelValue', modelValue: ModelValueType): void
}>()
const value = useVModel($props, 'modelValue', $emits)
const valueArr = computed<string[]>(() => {
  if (!value.value) return []
  return Array.isArray(value.value) ? value.value : [value.value]
})
const RolePickerRef = ref<InstanceType<typeof RolePicker>>()
const formDisabled = useFormDisabled()
const formSize = useFormSize()
const disabled = computed<boolean>(() => {
  return formDisabled.value || $props.disabled
})
const openRolePicker = () => {
  RolePickerRef.value?.open()
}
const onClose = (username: string) => {
  if (!value.value) return
  if ($props.multiple && Array.isArray(value.value)) {
    value.value.splice(value.value.indexOf(username), 1)
  } else {
    value.value = null
  }
}
</script>

<template>
  <role-picker ref="RolePickerRef" :multiple="multiple" v-model="value" />
  <div class="role-wrapper">
    <el-button
      class="role-but-item"
      :size="formSize"
      :disabled="disabled"
      @click="openRolePicker"
      circle
    >
      <el-icon>
        <Avatar />
      </el-icon>
    </el-button>
    <RoleTag
      v-for="item in valueArr"
      :closable="!disabled"
      :key="item"
      :id="item"
      @close="onClose"
    />
    <el-text v-show="!value || value.length === 0" class="placeholder">
      {{ placeholder }}
    </el-text>
  </div>
</template>

<style scoped lang="scss">
.el-tag {
  padding: 0 3px;
}

.role-wrapper {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  grid-gap: 7px;
  gap: 7px;

  .placeholder {
    color: var(--el-text-color-placeholder);
  }

  .role-but-item {
    border-style: dashed;

    &:hover {
      border-style: solid;
    }
  }
}
</style>


================================================
FILE: src/components/SvgIcon/index.scss
================================================
.svg-icon {
  width: 1em;
  height: 1em;
  vertical-align: -0.15em;
  fill: currentColor;
  overflow: hidden;
}


================================================
FILE: src/components/SvgIcon/index.tsx
================================================
import './index.scss'
import type { CSSProperties, PropType } from 'vue'

export default defineComponent({
  name: 'SvgIcon',
  props: {
    name: {
      type: String as PropType<string>,
      required: true
    },
    prefix: {
      type: String as PropType<string>,
      default: 'icon'
    },
    color: {
      type: String as PropType<string>
    },
    size: {
      type: Number as PropType<number>
    },
    className: {
      type: String as PropType<string>
    }
  },
  setup(props) {
    const symbolId = computed(() => `#${props.prefix}-${props.name}`)
    const svgClass = computed(() => [
      'svg-icon',
      props.name && props.name.replace('el:', ''),
      props.className
    ])
    const fill = computed(() => (props.color ? props.color : 'currentColor'))
    const style = computed<CSSProperties>(() => {
      const { size } = props
      if (!size) return {}
      return {
        fontSize: `${size}px`
      }
    })
    return {
      symbolId,
      svgClass,
      fill,
      style
    }
  },
  render() {
    const { $attrs, symbolId, svgClass, fill } = this
    if (this.name) {
      if (this.name.startsWith('el:')) {
        return (
          <el-icon class={svgClass} color={this.color} size={this.size} {...$attrs}>
            {h(resolveComponent(this.name.slice(3)))}
          </el-icon>
        )
      } else {
        return (
          <svg class={svgClass} style={this.style} aria-hidden="true" {...$attrs}>
            <use xlinkHref={symbolId} fill={fill}></use>
          </svg>
        )
      }
    }
    return null
  }
})


================================================
FILE: src/components/UserSelector/UserPicker.vue
================================================
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import type { TreeNodeData } from 'element-plus/es/components/tree/src/tree.type'
import { getList } from '@/api/modules/user'
import type { TreeInstance } from 'element-plus'

export type ModelValueType = string | string[] | null | undefined

export interface UserDropdownProps {
  modelValue: ModelValueType
  multiple?: boolean
}

const treeProps = {
  label: 'name',
  children: 'children',
  isLeaf: 'leaf',
  class: (org: TreeNodeData) => renderClass(org)
}

export interface Org {
  id: string
  type: 'user' | 'dept'
  avatar?: string
  name: string
  leaf: boolean
}

const $props = withDefaults(defineProps<UserDropdownProps>(), {
  multiple: false
})

const $emits = defineEmits<{
  (e: 'update:modelValue', modelValue: ModelValueType): void
}>()

const value = useVModel($props, 'modelValue', $emits)

const userOptions = ref<Org[]>([])
const userOrgOptions = ref<Org[]>([])
const orgTreeRef = ref<TreeInstance>()
const expandedKeys = ref<string[]>([])

const renderClass = (org: TreeNodeData): string | { [key: string]: boolean } => {
  const val = userOptions.value.find((e) => e.id === org.id)
  if (val) {
    return 'is-active'
  } else {
    return ''
  }
}

const onNodeClick = (data: Org) => {
  if (data.type !== 'user') return
  if ($props.multiple) {
    const index = userOptions.value.findIndex((e) => e.id === data.id)
    if (index === -1) {
      userOptions.value.push(data)
      userOptions.value.sort((a, b) => a.id.localeCompare(b.id))
    } else {
      userOptions.value.splice(index, 1)
    }
  } else {
    const index = userOptions.value.findIndex((e) => e.id === data.id)
    if (index === -1) {
      userOptions.value = [data]
    } else {
      userOptions.value.splice(index, 1)
    }
  }
}
const dialogVisible = ref(false)
const queryForm = reactive({
  name: null
})

watch(
  () => queryForm.name,
  (val) => {
    orgTreeRef.value?.filter(val)
  }
)
const filterNode = (value: string, data: TreeNodeData): boolean => {
  if (!value) return true
  return data.name.includes(value)
}
const open = () => {
  dialogVisible.value = true
}
const onOpen = () => {
  getList().then((res) => {
    if (res.success) {
      userOrgOptions.value = res.data.map((e) => {
        return {
          id: e.username,
          name: e.name,
          type: 'user',
          leaf: true,
          avatar: e.avatar
        } as Org
      })
    }
  })

  let userIds: string[] = []
  if (Array.isArray(value.value)) {
    userIds.push(...value.value)
  } else if (value.value) {
    userIds.push(value.value)
  }
  if (userIds.length > 0) {
    getList(userIds).then((res) => {
      if (res.success) {
        userOptions.value = res.data.map((user) => {
          return {
            id: user.username,
            name: user.name,
            avatar: user.avatar,
            type: 'user',
            leaf: true
          } as Org
        })
        userOptions.value.sort((a, b) => a.id.localeCompare(b.id))
      }
    })
  } else {
    userOptions.value = []
  }
}
const handelConfirm = () => {
  if ($props.multiple) {
    value.value = userOptions.value.map((e) => e.id)
  } else {
    if (userOptions.value.length > 0) {
      value.value = userOptions.value[0].id
    } else {
      value.value = null
    }
  }
  dialogVisible.value = false
}
defineExpose({
  open
})
</script>

<template>
  <el-dialog
    v-model="dialogVisible"
    @open="onOpen"
    :lock-scroll="false"
    align-center
    draggable
    title="选择用户"
    width="30%"
  >
    <el-card shadow="never" class="org-card">
      <template #header>
        <el-input
          v-model="queryForm.name"
          placeholder="输入关键字进行查询"
          :style="{ width: '100%' }"
          suffix-icon="search"
          clearable
        >
        </el-input>
      </template>
      <el-scrollbar tag="div" class="org-tree">
        <el-tree
          ref="orgTreeRef"
          node-key="id"
          :data="userOrgOptions"
          :default-expanded-keys="expandedKeys"
          :props="treeProps"
          :filter-node-method="filterNode"
          @node-click="onNodeClick"
        >
          <template #default="{ data }">
            <div class="flex flex-1 flex-items-center flex-justify-between">
              <div class="flex-center">
                <el-avatar v-if="data.type === 'user'" :size="25" :src="data.avatar">
                  {{ data.name.charAt(0) }}
                </el-avatar>
                <el-icon v-else :size="16">
                  <School />
                </el-icon>
                &nbsp;{{ data.name }}
              </div>
              <el-icon class="is-selected">
                <Check />
              </el-icon>
            </div>
          </template>
        </el-tree>
      </el-scrollbar>
    </el-card>
    <template #footer>
      <el-button @click="dialogVisible = false">取消</el-button>
      <el-button type="primary" @click="handelConfirm">确认</el-button>
    </template>
  </el-dialog>
</template>

<style scoped lang="scss">
:deep {
  .el-tree {
    --el-tree-node-content-height: 40px;

    .el-tree-node__content {
      border-radius: 8px;
      margin: 2px 0 2px 0;
    }

    .is-active {
      color: var(--el-color-primary);

      .is-selected {
        display: block;
      }
    }
  }
}

.el-card {
  background-color: transparent;

  :deep(.el-card__header) {
    padding: 10px !important;
  }

  :deep(.el-card__body) {
    padding: 0 !important;
  }
}

.org-tree {
  height: 270px;
  padding: 5px;
}

.is-selected {
  display: none;
  padding-right: 15px;
}
</style>


================================================
FILE: src/components/UserSelector/UserTag.vue
================================================
<script setup lang="ts">
import { getByUsername } from '@/api/modules/user'
import { componentSizeMap, useFormSize } from 'element-plus'

export interface UserTagProps {
  username: string
  type?: 'success' | 'info' | 'warning' | 'danger'
  closable?: boolean
}

const $props = withDefaults(defineProps<UserTagProps>(), {
  closable: false,
  type: 'info'
})
const $emits = defineEmits<{
  (e: 'close', username: string): void
}>()

export interface UserInfo {
  username?: string
  avatar?: string
  name?: string
}

let userInfo = reactive<UserInfo>({
  username: undefined,
  avatar: undefined,
  name: undefined
})
onMounted(() => {
  if (!$props.username) {
    throw new Error('username is required')
  }
  getByUsername($props.username).then((res) => {
    if (res.success) {
      userInfo.username = res.data.username
      userInfo.avatar = res.data.avatar
      userInfo.name = res.data.name
    }
  })
})
const formSize = useFormSize()
const getComponentSize = computed(() => {
  return componentSizeMap[formSize.value || 'default'] - 12
})
const onClose = () => {
  $emits('close', $props.username)
}
</script>
<template>
  <el-tag round :closable="$props.closable" :type="type" effect="light" @close="onClose">
    <div class="flex-center" style="gap: 4px; grid-gap: 4px">
      <el-avatar :size="getComponentSize" :src="userInfo.avatar">
        {{ (userInfo.name || username).charAt(0) }}
      </el-avatar>
      <span>{{ userInfo.name || username }}</span>
    </div>
  </el-tag>
</template>

<style scoped lang="scss">
:deep {
  .el-tag__content:only-child {
    margin-right: 4px;
  }
}
</style>


================================================
FILE: src/components/UserSelector/index.vue
================================================
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import UserTag from './UserTag.vue'
import UserPicker, { type ModelValueType } from './UserPicker.vue'
import { useFormDisabled, useFormSize } from 'element-plus'
import { type CSSProperties } from 'vue'

export interface UserSelectorProps {
  modelValue: ModelValueType
  placeholder?: string
  multiple?: boolean
  disabled?: boolean
  style?: CSSProperties
}

const $props = withDefaults(defineProps<UserSelectorProps>(), {
  multiple: false,
  disabled: false,
  placeholder: '请选择用户'
})
const $emits = defineEmits<{
  (e: 'update:modelValue', modelValue: ModelValueType): void
}>()
const value = useVModel($props, 'modelValue', $emits)
const valueArr = computed<string[]>(() => {
  if (!value.value) return []
  return Array.isArray(value.value) ? value.value : [value.value]
})
const userPickerRef = ref<InstanceType<typeof UserPicker>>()
const formDisabled = useFormDisabled()
const formSize = useFormSize()
const disabled = computed<boolean>(() => {
  return formDisabled.value || $props.disabled
})
const openUserPicker = () => {
  userPickerRef.value?.open()
}
const onClose = (username: string) => {
  if (!value.value) return
  if ($props.multiple && Array.isArray(value.value)) {
    value.value.splice(value.value.indexOf(username), 1)
  } else {
    value.value = null
  }
}
</script>

<template>
  <user-picker ref="userPickerRef" :multiple="multiple" v-model="value" />
  <div class="user-wrapper">
    <el-button
      class="user-but-item"
      :size="formSize"
      :disabled="disabled"
      @click="openUserPicker"
      circle
    >
      <svg-icon name="add-user" />
    </el-button>
    <user-tag
      v-for="item in valueArr"
      :closable="!disabled"
      :key="item"
      :username="item"
      @close="onClose"
    />
    <el-text v-show="!value || value.length === 0" class="placeholder">
      {{ placeholder }}
    </el-text>
  </div>
</template>

<style scoped lang="scss">
.el-tag {
  padding: 0 3px;
}

.user-wrapper {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  grid-gap: 7px;
  gap: 7px;

  .placeholder {
    color: var(--el-text-color-placeholder);
  }

  .user-but-item {
    border-style: dashed;

    &:hover {
      border-style: solid;
    }
  }
}
</style>


================================================
FILE: src/hooks/useDraggableScroll.ts
================================================
import { ref, onMounted, onBeforeUnmount, type Ref } from 'vue'

export function useDraggableScroll(containerRef: Ref<HTMLElement | null>) {
  const isDragging = ref(false)
  let startX: number, startY: number
  let scrollLeft: number, scrollTop: number

  const onMouseDown = (e: MouseEvent) => {
    if (!containerRef.value) return
    isDragging.value = true
    startX = e.pageX
    startY = e.pageY
    scrollLeft = containerRef.value.scrollLeft
    scrollTop = containerRef.value.scrollTop
    document.addEventListener('mousemove', onMouseMove)
    document.addEventListener('mouseup', onMouseUp)
  }

  const onMouseMove = (e: MouseEvent) => {
    if (!isDragging.value || !containerRef.value) return
    const deltaX = e.pageX - startX
    const deltaY = e.pageY - startY
    containerRef.value.scrollLeft = scrollLeft - deltaX
    containerRef.value.scrollTop = scrollTop - deltaY
  }

  const onMouseUp = () => {
    isDragging.value = false
    document.removeEventListener('mousemove', onMouseMove)
    document.removeEventListener('mouseup', onMouseUp)
  }

  onMounted(() => {
    containerRef.value?.addEventListener('mousedown', onMouseDown)
  })

  onBeforeUnmount(() => {
    containerRef.value?.removeEventListener('mousedown', onMouseDown)
  })

  return {
    isDragging
  }
}


================================================
FILE: src/main.ts
================================================
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import 'virtual:svg-icons-register'
import 'uno.css'
import '@/styles/index.scss'

// If you want to use ElMessage, import it.
import 'element-plus/theme-chalk/src/message.scss'
import 'element-plus/theme-chalk/src/notification.scss'
import 'element-plus/theme-chalk/el-input-number.css'

const app = createApp(App)
import * as Icons from '@element-plus/icons-vue'
for (const [key, component] of Object.entries(Icons)) {
  app.component(key, component)
}
app.use(router).use(createPinia())
app.mount('#app')


================================================
FILE: src/mock/index.ts
================================================
import user from './user'
import role from './role'
import type { MockMethod } from 'vite-plugin-mock'

const mockModules: MockMethod[] = [...user, ...role]
export default mockModules


================================================
FILE: src/mock/role.ts
================================================
import type { MockMethod } from 'vite-plugin-mock'
import type { ResultData } from '@/api'

const roleList = [
  {
    id: '1',
    name: '项目经理'
  },
  {
    id: '2',
    name: '产品经理'
  },
  {
    id: '3',
    name: '高级开发工程师'
  },
  {
    id: '4',
    name: '中级开发工程师'
  },
  {
    id: '5',
    name: '项目总监'
  },
  {
    id: '6',
    name: '产品策划'
  },
  {
    id: '7',
    name: '客服'
  },
  {
    id: '8',
    name: '销售经理'
  }
]

const role = [
  {
    url: '/api/role/info',
    method: 'get',
    response: (req: any) => {
      const id = req.query.id
      return {
        code: 200,
        success: true,
        message: '操作成功',
        data: roleList.find((item) => item.id === id)
      } as ResultData
    }
  },
  {
    url: '/api/role/list',
    method: 'post',
    response: (req: any) => {
      const roleIds = req.body.roleIds
      return {
        code: 200,
        success: true,
        message: '操作成功',
        data: Array.isArray(roleIds)
          ? roleList.filter((item) => roleIds.includes(item.id))
          : roleList
      } as ResultData
    }
  }
] as MockMethod[]

export default role


================================================
FILE: src/mock/user.ts
================================================
import type { MockMethod } from 'vite-plugin-mock'
import type { ResultData } from '@/api'

const userList = [
  {
    id: 1,
    name: '张三',
    username: 'admin',
    avatar: 'https://avatars.githubusercontent.com/u/44080404?v=4'
  },
  {
    id: 2,
    name: '李四',
    username: 'lisi',
    avatar: 'https://avatars.githubusercontent.com/u/44080404?v=4'
  },
  {
    id: 3,
    name: '王五',
    username: 'wangwu',
    avatar: 'https://avatars.githubusercontent.com/u/44080404?v=4'
  },
  {
    id: 4,
    name: '赵六',
    username: 'zhaoliu',
    avatar: 'https://avatars.githubusercontent.com/u/44080404?v=4'
  },
  {
    id: 5,
    name: '孙七',
    username: 'sunqi',
    avatar: 'https://avatars.githubusercontent.com/u/44080404?v=4'
  },
  {
    id: 6,
    name: '周八',
    username: 'zhouba',
    avatar: 'https://avatars.githubusercontent.com/u/44080404?v=4'
  },
  {
    id: 7,
    name: '吴九',
    username: 'wujui',
    avatar: 'https://avatars.githubusercontent.com/u/44080404?v=4'
  },
  {
    id: 8,
    name: '郑十',
    username: 'zhengshi',
    avatar: 'https://avatars.githubusercontent.com/u/44080404?v=4'
  }
]

const user = [
  {
    url: '/api/user/info',
    method: 'get',
    response: (req: any) => {
      const username = req.query.username
      return {
        code: 200,
        success: true,
        message: '操作成功',
        data: userList.find((item) => item.username === username)
      } as ResultData
    }
  },
  {
    url: '/api/user/list',
    method: 'post',
    response: (req: any) => {
      const userIds = req.body.userIds
      return {
        code: 200,
        success: true,
        message: '操作成功',
        data: Array.isArray(userIds)
          ? userList.filter((item) => userIds.includes(item.username))
          : userList
      } as ResultData
    }
  }
] as MockMethod[]

export default user


================================================
FILE: src/mockProdServer.ts
================================================
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'
import mock from './mock'

export function setupProdMockServer() {
  createProdMockServer(mock)
}


================================================
FILE: src/router/index.ts
================================================
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/home/index.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home
    }
  ]
})

export default router


================================================
FILE: src/stores/counter.ts
================================================
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const doubleCount = computed(() => count.value * 2)
  function increment() {
    count.value++
  }

  return { count, doubleCount, increment }
})


================================================
FILE: src/styles/el-segmented.scss
================================================
.el-segmented {
  --el-segmented-radius: var(--el-border-radius-base);
  --el-segmented-padding: 3px;
  --el-segmented-bg: var(--el-fill-color-light);
  --el-segmented-height: 28px;
  --el-segmented-font-size: 14px;
  --el-segmented-item-padding: 12px;
  --el-segmented-color: var(--el-text-color-secondary);
  --el-segmented-active-color: var(--el-text-color-primary);
  --el-segmented-active-bg: var(--el-bg-color-overlay);
  --el-segmented-active-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.08);
  --el-segmented-hover-bg: rgba(0, 0, 0, 0.04);
  --el-segmented-disabled-color: var(--el-text-color-placeholder);

  :deep {
    &.is-block {
      .el-tabs__header {
        display: inline-block;
      }
    }

    .el-tabs__header {
      margin: 0;
      box-sizing: border-box;
      background: var(--el-segmented-bg);
      border-radius: var(--el-segmented-radius);
      padding: var(--el-segmented-padding);
    }

    .el-tabs__nav-scroll,
    .el-tabs__nav-wrap {
      margin: 0;
      overflow: visible;
    }

    .el-tabs__nav-wrap {
      &:after {
        display: none;
      }
    }

    .el-tabs__nav {
      float: none;

      &:not(:has(.is-active)) {
        .el-tabs__active-bar {
          padding: 0;
        }
      }

      .el-tabs__item {
        padding: 0 var(--el-segmented-item-padding);
        color: var(--el-segmented-color);
        height: var(--el-segmented-height);
        line-height: var(--el-segmented-height);
        font-size: var(--el-segmented-font-size);
        border-radius: var(--el-segmented-radius);
        transition:
          color 0.2s,
          background-color 0.2s;
        background: none;
        z-index: 2;

        &:not(.is-disabled) {
          &.is-active {
            color: var(--el-segmented-active-color) !important;
            background: none !important;
          }

          &:hover {
            color: var(--el-segmented-active-color);
            background: var(--el-segmented-hover-bg);
          }
        }
      }
    }

    .el-tabs__active-bar {
      padding: 0 var(--el-segmented-item-padding);
      margin-left: calc(0px - var(--el-segmented-item-padding));
      background: var(--el-segmented-active-bg);
      border-radius: var(--el-segmented-radius);
      box-shadow: var(--el-segmented-active-shadow);
      transform: translate(var(--el-segmented-item-padding));
      box-sizing: content-box;
      height: auto;
      bottom: 0;
      top: 0;
    }
  }
}


================================================
FILE: src/styles/element/dark.scss
================================================
// only scss variables

$--colors: (
  'primary': (
    'base': #589ef8
  )
);

@forward 'element-plus/theme-chalk/src/dark/var.scss' with (
  $colors: $--colors
);


================================================
FILE: src/styles/element/index.scss
================================================
$--colors: (
  'primary': (
    'base': #589ef8
  ),
  'success': (
    'base': #21ba45
  ),
  'warning': (
    'base': #f2711c
  ),
  'danger': (
    'base': #db2828
  ),
  'error': (
    'base': #db2828
  ),
  'info': (
    'base': #42b8dd
  )
);

// we can add this to custom namespace, default is 'el'
@forward 'element-plus/theme-chalk/src/mixins/config.scss' with (
  $namespace: 'el'
);

// You should use them in scss, because we calculate it by sass.
// comment next lines to use default color
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
    // do not use same name, it will override.
    $colors: $--colors,
    // $button-padding-horizontal: ("default": 50px)
  );

// if you want to import all
// @use "element-plus/theme-chalk/src/index.scss" as *;

// You can comment it to hide debug info.
// @debug $--colors;

// custom dark variables
@use './dark.scss';


================================================
FILE: src/styles/index.scss
================================================
// import dark theme
@use 'element-plus/theme-chalk/src/dark/css-vars.scss' as *;

:root {
  .el-segmented {
    --el-segmented-radius: var(--el-border-radius-base);
    --el-segmented-padding: 3px;
    --el-segmented-bg: var(--el-fill-color-light);
    --el-segmented-height: 28px;
    --el-segmented-font-size: 14px;
    --el-segmented-item-padding: 12px;
    --el-segmented-color: var(--el-text-color-secondary);
    --el-segmented-active-color: var(--el-text-color-primary);
    --el-segmented-active-bg: var(--el-bg-color-overlay);
    --el-segmented-active-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.08);
    --el-segmented-hover-bg: rgba(0, 0, 0, 0.04);
    --el-segmented-disabled-color: var(--el-text-color-placeholder);
  }
}
@media (max-width: 1200px) {
  .el-drawer.rtl {
    width: 90% !important;
  }
  .el-dialog {
    width: 90% !important;
  }
  .el-dialog.is-fullscreen {
    width: 100% !important;
  }
}

// 自定义抽屉样式
.el-drawer {
  // 抽屉头部
  .el-drawer__header {
    margin-bottom: 0;
    padding: calc(var(--el-drawer-padding-primary) - 5px) var(--el-drawer-padding-primary)
      calc(var(--el-drawer-padding-primary) - 6px);
    border-bottom: 1px var(--el-border-style) var(--el-border-color);
    justify-content: space-between;
    // 抽屉标题
    .el-drawer__title {
      border-left: 3px solid var(--el-color-primary);
      padding-left: 5px;
    }
  }

  .el-drawer__footer {
    border-top: var(--el-border);
    padding: calc(var(--el-drawer-padding-primary) - 5px);
  }
}

body {
  font-family: Inter, system-ui, Avenir, 'Helvetica Neue', Helvetica, 'PingFang SC',
    'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  margin: 0;
}

a {
  color: var(--el-color-primary);
}

html,
body,
#app {
  width: 100%;
  height: 100%;
  padding: 0;
  margin: 0;
}

code {
  border-radius: 2px;
  padding: 2px 4px;
  background-color: var(--el-color-primary-light-9);
  color: var(--el-color-primary);
}


================================================
FILE: src/typings/auto-imports.d.ts
================================================
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
export {}
declare global {
  const EffectScope: (typeof import('vue'))['EffectScope']
  const ElCheckbox: (typeof import('element-plus/es'))['ElCheckbox']
  const ElDivider: (typeof import('element-plus/es'))['ElDivider']
  const ElInput: (typeof import('element-plus/es'))['ElInput']
  const ElInputNumber: (typeof import('element-plus/es'))['ElInputNumber']
  const ElRadio: (typeof import('element-plus/es'))['ElRadio']
  const ElSelect: (typeof import('element-plus/es'))['ElSelect']
  const computed: (typeof import('vue'))['computed']
  const createApp: (typeof import('vue'))['createApp']
  const customRef: (typeof import('vue'))['customRef']
  const defineAsyncComponent: (typeof import('vue'))['defineAsyncComponent']
  const defineComponent: (typeof import('vue'))['defineComponent']
  const effectScope: (typeof import('vue'))['effectScope']
  const getCurrentInstance: (typeof import('vue'))['getCurrentInstance']
  const getCurrentScope: (typeof import('vue'))['getCurrentScope']
  const h: (typeof import('vue'))['h']
  const inject: (typeof import('vue'))['inject']
  const isProxy: (typeof import('vue'))['isProxy']
  const isReactive: (typeof import('vue'))['isReactive']
  const isReadonly: (typeof import('vue'))['isReadonly']
  const isRef: (typeof import('vue'))['isRef']
  const markRaw: (typeof import('vue'))['markRaw']
  const nextTick: (typeof import('vue'))['nextTick']
  const onActivated: (typeof import('vue'))['onActivated']
  const onBeforeMount: (typeof import('vue'))['onBeforeMount']
  const onBeforeRouteLeave: (typeof import('vue-router'))['onBeforeRouteLeave']
  const onBeforeRouteUpdate: (typeof import('vue-router'))['onBeforeRouteUpdate']
  const onBeforeUnmount: (typeof import('vue'))['onBeforeUnmount']
  const onBeforeUpdate: (typeof import('vue'))['onBeforeUpdate']
  const onDeactivated: (typeof import('vue'))['onDeactivated']
  const onErrorCaptured: (typeof import('vue'))['onErrorCaptured']
  const onMounted: (typeof import('vue'))['onMounted']
  const onRenderTracked: (typeof import('vue'))['onRenderTracked']
  const onRenderTriggered: (typeof import('vue'))['onRenderTriggered']
  const onScopeDispose: (typeof import('vue'))['onScopeDispose']
  const onServerPrefetch: (typeof import('vue'))['onServerPrefetch']
  const onUnmounted: (typeof import('vue'))['onUnmounted']
  const onUpdated: (typeof import('vue'))['onUpdated']
  const provide: (typeof import('vue'))['provide']
  const reactive: (typeof import('vue'))['reactive']
  const readonly: (typeof import('vue'))['readonly']
  const ref: (typeof import('vue'))['ref']
  const resolveComponent: (typeof import('vue'))['resolveComponent']
  const shallowReactive: (typeof import('vue'))['shallowReactive']
  const shallowReadonly: (typeof import('vue'))['shallowReadonly']
  const shallowRef: (typeof import('vue'))['shallowRef']
  const toRaw: (typeof import('vue'))['toRaw']
  const toRef: (typeof import('vue'))['toRef']
  const toRefs: (typeof import('vue'))['toRefs']
  const toValue: (typeof import('vue'))['toValue']
  const triggerRef: (typeof import('vue'))['triggerRef']
  const unref: (typeof import('vue'))['unref']
  const useAttrs: (typeof import('vue'))['useAttrs']
  const useCssModule: (typeof import('vue'))['useCssModule']
  const useCssVars: (typeof import('vue'))['useCssVars']
  const useLink: (typeof import('vue-router'))['useLink']
  const useRoute: (typeof import('vue-router'))['useRoute']
  const useRouter: (typeof import('vue-router'))['useRouter']
  const useSlots: (typeof import('vue'))['useSlots']
  const watch: (typeof import('vue'))['watch']
  const watchEffect: (typeof import('vue'))['watchEffect']
  const watchPostEffect: (typeof import('vue'))['watchPostEffect']
  const watchSyncEffect: (typeof import('vue'))['watchSyncEffect']
}
// for type re-export
declare global {
  // @ts-ignore
  export type {
    Component,
    ComponentPublicInstance,
    ComputedRef,
    ExtractDefaultPropTypes,
    ExtractPropTypes,
    ExtractPublicPropTypes,
    InjectionKey,
    PropType,
    Ref,
    VNode,
    WritableComputedRef
  } from 'vue'
  import('vue')
}


================================================
FILE: src/typings/components.d.ts
================================================
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}

/* prettier-ignore */
declare module 'vue' {
  export interface GlobalComponents {
    AdvancedFilter: typeof import('./../components/AdvancedFilter/index.vue')['default']
    ElAvatar: typeof import('element-plus/es')['ElAvatar']
    ElBadge: typeof import('element-plus/es')['ElBadge']
    ElButton: typeof import('element-plus/es')['ElButton']
    ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
    ElCard: typeof import('element-plus/es')['ElCard']
    ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
    ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
    ElCol: typeof import('element-plus/es')['ElCol']
    ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
    ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
    ElDialog: typeof import('element-plus/es')['ElDialog']
    ElDrawer: typeof import('element-plus/es')['ElDrawer']
    ElDropdown: typeof import('element-plus/es')['ElDropdown']
    ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
    ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
    ElForm: typeof import('element-plus/es')['ElForm']
    ElFormItem: typeof import('element-plus/es')['ElFormItem']
    ElIcon: typeof import('element-plus/es')['ElIcon']
    ElInput: typeof import('element-plus/es')['ElInput']
    ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
    ElLink: typeof import('element-plus/es')['ElLink']
    ElOption: typeof import('element-plus/es')['ElOption']
    ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
    ElPopover: typeof import('element-plus/es')['ElPopover']
    ElRadio: typeof import('element-plus/es')['ElRadio']
    ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
    ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
    ElRow: typeof import('element-plus/es')['ElRow']
    ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
    ElSelect: typeof import('element-plus/es')['ElSelect']
    ElSpace: typeof import('element-plus/es')['ElSpace']
    ElSwitch: typeof import('element-plus/es')['ElSwitch']
    ElTable: typeof import('element-plus/es')['ElTable']
    ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
    ElTabPane: typeof import('element-plus/es')['ElTabPane']
    ElTabs: typeof import('element-plus/es')['ElTabs']
    ElTag: typeof import('element-plus/es')['ElTag']
    ElText: typeof import('element-plus/es')['ElText']
    ElTooltip: typeof import('element-plus/es')['ElTooltip']
    ElTree: typeof import('element-plus/es')['ElTree']
    Operator: typeof import('./../components/AdvancedFilter/Operator.vue')['default']
    Render: typeof import('./../components/Render/index.tsx')['default']
    RolePicker: typeof import('./../components/RoleSelector/RolePicker.vue')['default']
    RoleSelector: typeof import('./../components/RoleSelector/index.vue')['default']
    RoleTag: typeof import('./../components/RoleSelector/RoleTag.vue')['default']
    RouterLink: typeof import('vue-router')['RouterLink']
    RouterView: typeof import('vue-router')['RouterView']
    SvgIcon: typeof import('./../components/SvgIcon/index.tsx')['default']
    Trigger: typeof import('./../components/AdvancedFilter/Trigger.vue')['default']
    UserPicker: typeof import('./../components/UserSelector/UserPicker.vue')['default']
    UserSelector: typeof import('./../components/UserSelector/index.vue')['default']
    UserTag: typeof import('./../components/UserSelector/UserTag.vue')['default']
  }
}


================================================
FILE: src/typings/index.d.ts
================================================
type Recordable<T = any> = Record<string, T>


================================================
FILE: src/views/flowDesign/index.vue
================================================
<script setup lang="ts">
import TreeNode from './nodes/TreeNode.vue'
import Panel from './panels/index.vue'
import type { ErrorInfo, FlowNode, ServiceNode, TimerNode } from './nodes/type'
import type {
  ApprovalNode,
  BranchNode,
  CcNode,
  NotifyNode,
  ConditionNode,
  ExclusiveNode,
  NodeType
} from './nodes/type'
import type { FilterRules } from '@/components/AdvancedFilter/type'
import type { Field } from '@/components/Render/type'
import { useDraggableScroll } from '@/hooks/useDraggableScroll'

const props = withDefaults(
  defineProps<{
    process: FlowNode
    fields: Field[]
    readOnly?: boolean
    defaultZoom?: number
    bgColor?: string
  }>(),
  {
    readOnly: false,
    defaultZoom: 100,
    bgColor: 'var(--el-bg-color-page)'
  }
)

const flatFields = computed(() => {
  const all: Field[] = []
  const loop = (children: Field[]) => {
    children.forEach((field) => {
      if (field.type === 'formItem') {
        all.push(field)
      }
      if (Array.isArray(field.children)) {
        loop(field.children)
      }
    })
  }
  loop(props.fields)
  return all
})
const getScale = computed(() => zoom.value / 100)
const zoom = ref(props.defaultZoom)
const readOnly = computed(() => props.readOnly)
const activeData = ref<FlowNode>({
  id: '',
  name: '',
  type: 'start'
})
const penalVisible = ref(false)
const nodesError = ref<Recordable<ErrorInfo[]>>({})

const designerContainerRef = ref<HTMLElement | null>(null)
useDraggableScroll(designerContainerRef)
provide('flowDesign', {
  readOnly: readOnly,
  fields: flatFields,
  nodesError: nodesError
})
const openPenal = (node: FlowNode) => {
  activeData.value = node
  penalVisible.value = true
}
const nextId = (): string => {
  let id = `node_${Math.random().toString(36).substring(2, 7)}`
  const findId = (node: FlowNode, id: string): boolean => {
    if (node.id === id) {
      return true
    }
    if (node.next) {
      return findId(node.next, id)
    }
    if ('branches' in node) {
      const branchNode = node as BranchNode
      if (branchNode.branches && branchNode.branches.length > 0) {
        return branchNode.branches.some((item) => {
          return findId(item, id)
        })
      }
    }
    return false
  }
  if (findId(props.process, id)) {
    return nextId()
  }
  return id
}
const addExclusive = (node: FlowNode) => {
  const next = node.next
  const id = nextId()
  const exclusiveNode = {
    id: id,
    pid: node.id,
    type: 'exclusive',
    name: '独占网关',
    next: next,
    branches: []
  } as ExclusiveNode
  if (next) {
    next.pid = id
  }
  addCondition(exclusiveNode)
  addCondition(exclusiveNode)
  node.next = exclusiveNode
  if (exclusiveNode.branches.length > 0) {
    const condition = exclusiveNode.branches[exclusiveNode.branches.length - 1] as ConditionNode
    condition.def = true
    condition.name = '默认条件'
  }
}
const addCondition = (node: FlowNode) => {
  const exclusive = node as ExclusiveNode
  exclusive.branches.splice(exclusive.branches.length - 1, 0, {
    id: nextId(),
    pid: exclusive.id,
    type: 'condition',
    def: false,
    name: `条件${exclusive.branches.length + 1}`,
    conditions: {
      operator: 'and',
      conditions: [],
      groups: []
    } as FilterRules,
    next: undefined
  })
}
const addCc = (node: FlowNode) => {
  const next = node.next
  const id = nextId()
  node.next = {
    id: id,
    pid: node.id,
    type: 'cc',
    name: '抄送人',
    next: next,
    assigneeType: 'user',
    formUser: '',
    formRole: '',
    users: [],
    roles: [],
    leader: 1,
    orgLeader: 1,
    choice: false,
    self: false,
    formProperties: []
  } as CcNode
  if (next) {
    next.pid = id
  }
}
const addTimer = (node: FlowNode) => {
  const next = node.next
  const id = nextId()
  node.next = {
    id: id,
    pid: node.id,
    name: '计时等待',
    type: 'timer',
    next: next,
    waitType: 'duration',
    unit: 'PT%sS',
    duration: 0,
    timeDate: undefined
  } as TimerNode
  if (next) {
    next.pid = id
  }
}

const addNotify = (node: FlowNode) => {
  const next = node.next
  const id = nextId()
  node.next = {
    id: id,
    pid: node.id,
    name: '消息通知',
    type: 'notify',
    next: next,
    assigneeType: 'user',
    formUser: '',
    formRole: '',
    users: [],
    roles: [],
    leader: 1,
    orgLeader: 1,
    choice: false,
    self: false,
    types: ['site'],
    subject: '',
    content: ''
  } as NotifyNode
  if (next) {
    next.pid = id
  }
}
const addService = (node: FlowNode) => {
  const next = node.next
  const id = nextId()
  node.next = {
    id: id,
    pid: node.id,
    type: 'service',
    name: '服务节点',
    next: next,
    implementationType: '',
    implementation: ''
  } as ServiceNode
  if (next) {
    next.pid = id
  }
}
const addApproval = (node: FlowNode) => {
  const next = node.next
  const id = nextId()
  node.next = {
    id: id,
    pid: node.id,
    type: 'approval',
    name: '审批人',
    executionListeners: [],
    next: next,
    // 属性
    assigneeType: 'user',
    formUser: '',
    formRole: '',
    users: [],
    roles: [],
    leader: 1,
    orgLeader: 1,
    choice: false,
    self: false,
    multi: 'sequential',
    multiPercent: 100,
    nobody: 'pass',
    nobodyUsers: [],
    formProperties: [],
    operations: {
      complete: true,
      refuse: true,
      back: true,
      transfer: true,
      delegate: true,
      addMulti: false,
      minusMulti: false
    }
  } as ApprovalNode
  if (next) {
    next.pid = id
  }
}
const addNode = (type: NodeType, node: FlowNode) => {
  const addMap: Recordable<(node: FlowNode) => void> = {
    exclusive: addExclusive,
    condition: addCondition,
    cc: addCc,
    timer: addTimer,
    notify: addNotify,
    service: addService,
    approval: addApproval
  }
  const fun = addMap[type]
  fun && fun(node)
}
const delNode = (del: FlowNode) => {
  delete nodesError.value[del.id]
  delNodeNext(props.process, del)
}
const delNodeNext = (next: FlowNode, del: FlowNode) => {
  delete nodesError.value[del.id]
  if (next.id === del.pid) {
    if ('branches' in next && next.next?.id !== del.id) {
      const branchNode = next as BranchNode
      const index = branchNode.branches.findIndex((item) => item.id === del.id)
      if (index !== -1) {
        if (branchNode.branches.length <= 2) {
          delError(branchNode)
          delNode(branchNode)
        } else {
          delError(del)
          branchNode.branches.splice(index, 1)
        }
      }
    } else {
      if (del.next && del.next.pid) {
        del.next.pid = next.id
      }
      next.next = del.next
    }
  } else {
    if (next.next) {
      delNodeNext(next.next, del)
    }
    if ('branches' in next) {
      const nextBranch = next as BranchNode
      if (nextBranch.branches && nextBranch.branches.length > 0) {
        nextBranch.branches.forEach((item) => {
          delNodeNext(item, del)
        })
      }
    }
  }
}
const delError = (node: FlowNode) => {
  delete nodesError.value[node.id]
  if (node.next) {
    delError(node.next)
  }
  if ('branches' in node) {
    const branchNode = node as BranchNode
    if (branchNode.branches && branchNode.branches.length > 0) {
      branchNode.branches.forEach((item) => {
        delError(item)
      })
    }
  }
}
const validate = () => {
  return new Promise((resolve, reject) => {
    const errors = Object.values(nodesError.value).flat()
    if (errors.length > 0) {
      reject(errors)
    } else {
      resolve(true)
    }
  })
}
defineExpose({
  validate
})
</script>

<template>
  <div class="designer-container cursor-default active:cursor-grabbing" ref="designerContainerRef">
    <div class="tool">
      <slot></slot>
    </div>
    <!--放大/缩小-->
    <div class="zoom">
      <el-tooltip content="放大" placement="bottom-start">
        <el-button icon="plus" @click="zoom += 10" :disabled="zoom >= 170" circle></el-button>
      </el-tooltip>
      <span>{{ zoom }}%</span>
      <el-tooltip content="缩小" placement="bottom-start">
        <el-button icon="minus" @click="zoom -= 10" circle :disabled="zoom <= 50"></el-button>
      </el-tooltip>
    </div>
    <!--流程树-->
    <div class="node-container">
      <TreeNode :node="process" @addNode="addNode" @delNode="delNode" @activeNode="openPenal" />
    </div>
    <!--属性面板-->
    <Panel v-model="penalVisible" :active-data="activeData" />
  </div>
</template>

<style scoped lang="scss">
.designer-container {
  --flow-bg-color: v-bind(bgColor);
  position: relative;
  display: flex;
  flex-direction: row;
  height: 100%;
  width: 100%;
  overflow: auto;
  background-color: var(--flow-bg-color);

  .zoom {
    position: fixed;
    z-index: 999;
    top: 30px;
    right: 40px;

    span {
      margin: 0 10px;
    }
  }

  .tool {
    position: fixed;
    z-index: 999;
    top: 5px;
    left: 5px;
    display: flex;
    gap: 5px;
  }

  .node-container {
    margin: 0 auto;
    transform: scale(v-bind(getScale));
    transform-origin: 50% 0 0;
    display: flex;
    align-items: center;
    flex-direction: column;
  }
}
</style>


================================================
FILE: src/views/flowDesign/nodes/Add.vue
================================================
<script setup lang="ts">
import type { PopoverInstance } from 'element-plus'
import type { NodeType } from './type'
import type { Ref } from 'vue'

const { readOnly } = inject<{
  readOnly?: Ref<boolean>
}>('flowDesign', { readOnly: ref(false) })
const popoverRef = ref<PopoverInstance>()
const $emits = defineEmits<{
  (e: 'addNode', type: NodeType): void
}>()
const addApprovalNode = () => {
  $emits('addNode', 'approval')
  popoverRef.value?.hide()
}
const addCcNode = () => {
  $emits('addNode', 'cc')
  popoverRef.value?.hide()
}
const addExclusiveNode = () => {
  $emits('addNode', 'exclusive')
  popoverRef.value?.hide()
}
const addTimerNode = () => {
  $emits('addNode', 'timer')
  popoverRef.value?.hide()
}
const addNotifyNode = () => {
  $emits('addNode', 'notify')
  popoverRef.value?.hide()
}
const addServiceNode = () => {
  $emits('addNode', 'service')
  popoverRef.value?.hide()
}
</script>

<template>
  <div class="add-but">
    <el-popover
      placement="bottom-start"
      ref="popoverRef"
      trigger="click"
      title="添加节点"
      :width="336"
    >
      <el-space wrap>
        <div class="node-select" @click="addApprovalNode">
          <svg-icon name="el:Stamp" />
          <el-text>审批人</el-text>
        </div>
        <div class="node-select" @click="addCcNode">
          <svg-icon name="el:Promotion" />
          <el-text>抄送人</el-text>
        </div>
        <div class="node-select" @click="addExclusiveNode">
          <svg-icon name="el:Share" />
          <el-text>互斥分支</el-text>
        </div>
        <div class="node-select" @click="addTimerNode">
          <svg-icon name="el:Timer" />
          <el-text>计时等待</el-text>
        </div>
        <div class="node-select" @click="addNotifyNode">
          <svg-icon name="el:BellFilled" />
          <el-text>消息通知</el-text>
        </div>
        <div class="node-select" @click="addServiceNode">
          <svg-icon name="el:Tools" />
          <el-text>服务节点</el-text>
        </div>
      </el-space>
      <template #reference>
        <el-button
          v-show="!readOnly"
          icon="Plus"
          type="primary"
          style="z-index: 1"
          circle
        ></el-button>
      </template>
    </el-popover>
  </div>
</template>

<style scoped lang="scss">
.node-select {
  cursor: pointer;
  display: flex;
  padding: 8px;
  width: 135px;
  border-radius: 10px;
  position: relative;
  background-color: var(--el-fill-color-light);

  &:hover {
    background-color: var(--el-color-primary-light-9);
    box-shadow: var(--el-box-shadow-light);
    color: var(--el-color-primary);
  }

  .svg-icon {
    font-size: 25px;
    padding: 5px;
    border-radius: 50%;
    color: var(--el-color-white);

    &.Stamp {
      background-color: #ff943e;
    }

    &.Promotion {
      background-color: #3296fa;
    }

    &.Share {
      background-color: #45cf9b;
    }

    &.Timer {
      background-color: #e872b7;
    }

    &.BellFilled {
      background-color: #95d475;
    }

    &.Tools {
      background-color: #ffc107;
    }
  }

  .el-text {
    margin-left: 10px;
  }
}

.add-but {
  display: flex;
  justify-content: center;
  width: 100%;
  padding: 20px 0 32px;
  position: relative;

  &:before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    margin: auto;
    width: 1px;
    height: 100%;
    background-color: var(--el-border-color);
  }
}
</style>


================================================
FILE: src/views/flowDesign/nodes/ApprovalNode.vue
================================================
<script setup lang="ts">
import Node from './Node.vue'
import type { ApprovalNode } from './type'
import type { Ref } from 'vue'
import type { Field } from '@/components/Render/type'
import { getById } from '@/api/modules/role'
import type { ErrorInfo } from './type'
import { getByUsername } from '@/api/modules/user'

const { fields, nodesError } = inject<{
  fields: Ref<Field[]>
  nodesError: Ref<Recordable<ErrorInfo[]>>
}>('flowDesign', { fields: ref([]), nodesError: ref({}) })
const props = defineProps<{
  node: ApprovalNode
}>()
const content = ref<string>('')
watchEffect(() => {
  const errors: ErrorInfo[] = []
  const {
    id,
    name,
    assigneeType,
    nobody,
    nobodyUsers,
    choice,
    formUser,
    formRole,
    leader,
    orgLeader,
    users,
    roles
  } = props.node
  if (assigneeType === 'user') {
    if (users.length > 0) {
      const all = users.map((user) => getByUsername(user))
      Promise.all(all).then((users) => {
        content.value = users.map((user) => user.data.name).join('、')
      })
    } else {
      errors.push({ id: id, name: name, message: '未指定人员' })
      content.value = '未指定人员'
    }
  } else if (assigneeType === 'choice') {
    content.value = `发起人自选(${choice ? '多选' : '单选'})`
  } else if (assigneeType === 'self') {
    content.value = '发起人自己'
  } else if (assigneeType === 'leader') {
    content.value = leader === 1 ? '直属上级' : `${leader}级上级`
  } else if (assigneeType === 'orgLeader') {
    content.value = orgLeader === 1 ? '直属主管' : `${orgLeader}级主管`
  } else if (assigneeType === 'formUser') {
    if (!formUser) {
      errors.push({ id: id, name: name, message: '未指定表单内人员' })
    }
    const title = fields.value.find((e) => e.id === formUser)?.label || formUser || '?'
    content.value = `表单内(${title})人员`
  } else if (assigneeType === 'formRole') {
    if (!formRole) {
      errors.push({ id: id, name: name, message: '未指定表单内角色' })
    }
    const title = fields.value.find((e) => e.id === formRole)?.label || formRole || '?'
    content.value = `表单内(${title})角色`
  } else if (assigneeType === 'role') {
    if (roles.length > 0) {
      const all = roles.map((id) => getById(id))
      Promise.all(all).then((roles) => {
        content.value = roles.map((res) => res.data.name).join('、')
      })
    } else {
      errors.push({ id: id, name: name, message: '未指定角色' })
      content.value = '未指定角色'
    }
  } else if (assigneeType === 'autoRefuse') {
    content.value = '系统自动拒绝'
  } else {
    errors.push({ id: id, name: name, message: '未知错误' })
    content.value = name
  }
  if (nobody === 'assign') {
    if (!nobodyUsers || nobodyUsers.length === 0) {
      errors.push({ id: id, name: name, message: '未指定审批人为空时的处理人' })
    }
  }

  // 记录错误
  if (errors.length > 0) {
    nodesError.value[id] = errors
  } else {
    delete nodesError.value[id]
  }
})
</script>

<template>
  <Node
    v-bind="$attrs"
    icon="el:Stamp"
    color="linear-gradient(89.96deg, #FA6F32 .05%, #FB9337 79.83%)"
    :node="node"
  >
    <el-text>{{ content }}</el-text>
  </Node>
</template>

<style scoped lang="scss"></style>


================================================
FILE: src/views/flowDesign/nodes/CcNode.vue
================================================
<script setup lang="ts">
import Node from './Node.vue'
import type { CcNode } from './type'
import type { Ref } from 'vue'
import type { ErrorInfo } from './type'
import type { Field } from '@/components/Render/type'
import { getById } from '@/api/modules/role'
import { getByUsername } from '@/api/modules/user'

const props = defineProps<{
  node: CcNode
}>()
const { fields, nodesError } = inject<{
  fields: Ref<Field[]>
  nodesError: Ref<Recordable<ErrorInfo[]>>
}>('flowDesign', { fields: ref([]), nodesError: ref({}) })
const content = ref<string>('')
watchEffect(() => {
  const errors: ErrorInfo[] = []
  const { id, assigneeType, name, users, roles, leader, choice, formUser, formRole, orgLeader } =
    props.node
  if (assigneeType === 'user') {
    if (users.length > 0) {
      const all = users.map((user) => getByUsername(user))
      Promise.all(all).then((users) => {
        content.value = users.map((user) => user.data.name).join('、')
      })
    } else {
      errors.push({ id: id, name: name, message: '未指定人员' })
      content.value = '未指定人员'
    }
  } else if (assigneeType === 'choice') {
    content.value = `发起人自选(${choice ? '多选' : '单选'})`
  } else if (assigneeType === 'self') {
    content.value = '发起人自己'
  } else if (assigneeType === 'leader') {
    content.value = leader === 1 ? '直属上级' : `${leader}级上级`
  } else if (assigneeType === 'orgLeader') {
    content.value = orgLeader === 1 ? '直属主管' : `${orgLeader}级主管`
  } else if (assigneeType === 'formUser') {
    if (!formUser) {
      errors.push({ id: id, name: name, message: '未指定表单内人员' })
    }
    const title = fields.value.find((e) => e.id === formUser)?.label || formUser || '?'
    content.value = `表单内(${title})人员`
  } else if (assigneeType === 'formRole') {
    if (!formRole) {
      errors.push({ id: id, name: name, message: '未指定表单内角色' })
    }
    const title = fields.value.find((e) => e.id === formRole)?.label || formRole || '?'
    content.value = `表单内(${title})角色`
  } else if (assigneeType === 'role') {
    if (roles.length > 0) {
      const all = roles.map((id) => getById(id))
      Promise.all(all).then((roles) => {
        content.value = roles.map((res) => res.data.name).join('、')
      })
    } else {
      errors.push({ id: id, name: name, message: '未指定角色' })
      content.value = '未指定角色'
    }
  } else {
    errors.push({ id: id, name: name, message: '未知错误' })
    content.value = name
  }

  // 记录错误
  if (errors.length > 0) {
    nodesError.value[id] = errors
  } else {
    delete nodesError.value[id]
  }
})
</script>

<template>
  <Node v-bind="$attrs" icon="el:Promotion" color="rgb(50, 150, 250)" :node="node">
    <el-text>{{ content }}</el-text>
  </Node>
</template>

<style scoped lang="scss"></style>


================================================
FILE: src/views/flowDesign/nodes/ConditionNode.vue
================================================
<script setup lang="ts">
import type { ConditionNode } from './type'
import type { Ref } from 'vue'
import type { ErrorInfo } from './type'
import Node from './Node.vue'

const props = defineProps<{
  node: ConditionNode
}>()
const { nodesError } = inject<{
  nodesError: Ref<Recordable<ErrorInfo[]>>
}>('flowDesign', { nodesError: ref({}) })
const content = ref<string>('')
watchEffect(() => {
  const errors: ErrorInfo[] = []
  const { id, name, def, conditions, next } = props.node
  if (def) {
    content.value = '不满足其他条件,进入此分支'
  } else if (conditions.conditions.length > 0 || (conditions.groups?.length || 0) > 0) {
    const count = conditions.conditions.length + (conditions.groups?.length || 0)
    content.value = `已设置(${count})个条件`
    if (!next) {
      errors.push({ id: id, name: name, message: '分支下节点为空' })
    }
  } else {
    errors.push({ id: id, name: name, message: '未设置条件' })
    content.value = '未设置条件'
  }
  // 记录错误
  if (errors.length > 0) {
    nodesError.value[id] = errors
  } else {
    delete nodesError.value[id]
  }
})
</script>

<template>
  <div class="branch-node">
    <Node v-bind="$attrs" icon="el:Share" :node="node" :readOnly="node.def">
      <el-text>{{ content }}</el-text>
      <slot name="append" />
    </Node>
  </div>
</template>

<style scoped lang="scss">
.branch-node {
  :deep(.node-box) {
    margin: 60px 40px 0;
  }
}
</style>


================================================
FILE: src/views/flowDesign/nodes/EndNode.vue
================================================
<script setup lang="ts">
import type { FlowNode } from '@/views/flowDesign/nodes/type'
import type { Ref } from 'vue'

const _inject = inject<{
  readOnly?: Ref<boolean>
}>('flowDesign', { readOnly: ref(false) })
const $emits = defineEmits<{
  (e: 'activeNode', node: FlowNode): void
}>()
const $props = withDefaults(
  defineProps<{
    node: FlowNode
    readOnly?: boolean
  }>(),
  {
    readOnly: false
  }
)
const _readOnly = computed(() => _inject.readOnly?.value || $props.readOnly)
const activeNode = () => {
  if (_readOnly.value) return
  $emits('activeNode', $props.node)
}
</script>

<template>
  <div class="node-box">
    <div class="end-node-circle"></div>
    <div class="end-node" @click="activeNode">
      <el-text>{{ node.name }}</el-text>
    </div>
  </div>
</template>

<style scoped lang="scss">
.node-box {
  position: relative;
  padding-bottom: 50px;

  .end-node-circle {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background-color: var(--el-border-color);
    margin: auto auto 5px;
  }

  .end-node {
    position: relative;
    background: var(--el-border-color-lighter);
    padding: 7px 20px;
    border-radius: 24px;
    cursor: pointer;
    overflow: visible;
    z-index: 10;
    box-shadow: var(--el-box-shadow-light);

    &:hover {
      box-shadow: 0 0 5px 0 var(--el-color-primary);
    }
  }
}
</style>


================================================
FILE: src/views/flowDesign/nodes/ExclusiveNode.vue
================================================
<script setup lang="ts">
import type { ExclusiveNode } from './type'
import GatewayNode from './GatewayNode.vue'

defineProps<{
  node: ExclusiveNode
}>()
</script>

<template>
  <GatewayNode v-bind="$attrs" :node="node">
    <template #default="{ addNode, readOnly }">
      <el-button type="primary" :disabled="readOnly" @click="addNode('condition', node)" plain round
        >添加条件</el-button
      >
    </template>
  </GatewayNode>
</template>

<style scoped lang="scss"></style>


================================================
FILE: src/views/flowDesign/nodes/GatewayNode.vue
================================================
<script setup lang="ts">
import TreeNode from './TreeNode.vue'
import type { BranchNode, FlowNode, NodeType } from './type'
import Add from './Add.vue'
import type { Ref } from 'vue'

const $emits = defineEmits<{
  (e: 'addNode', type: NodeType, node: FlowNode): void
}>()
const props = defineProps<{
  node: BranchNode
}>()
const { readOnly } = inject<{
  readOnly?: Ref<boolean>
}>('flowDesign', { readOnly: ref(false) })
const addNode = (type: NodeType, node?: FlowNode) => {
  $emits('addNode', type, node || props.node)
}
const moveRight = (index: number) => {
  const node = props.node.branches[index]
  props.node.branches.splice(index, 1)
  props.node.branches.splice(index + 1, 0, node)
}
const moveLeft = (index: number) => {
  const node = props.node.branches[index]
  props.node.branches.splice(index, 1)
  props.node.branches.splice(index - 1, 0, node)
}
</script>

<template>
  <div class="gateway-node">
    <div class="add-branch">
      <slot :addNode="addNode" :readOnly="readOnly"></slot>
    </div>
    <div v-for="(item, index) in node.branches" :key="item.id" class="col-box">
      <template v-if="index === 0">
        <div class="top-left-border"></div>
        <div class="bottom-left-border" />
      </template>
      <template v-else-if="node.branches.length === index + 1">
        <div class="top-right-border"></div>
        <div class="bottom-right-border" />
      </template>
      <TreeNode :node="item" v-bind="$attrs" @addNode="addNode" class="col-node">
        <template #append>
          <div
            class="move-left"
            @click.stop="moveLeft(index)"
            v-show="index !== 0 && node.branches.length !== index + 1 && !readOnly"
          >
            <svg-icon name="el:ArrowLeft" />
          </div>
          <div
            class="move-right"
            @click.stop="moveRight(index)"
            v-show="![index + 1, index + 2].includes(node.branches.length) && !readOnly"
          >
            <svg-icon name="el:ArrowRight" />
          </div>
        </template>
      </TreeNode>
    </div>
  </div>
  <Add @addNode="addNode" class="branch-but" />
</template>

<style scoped lang="scss">
.gateway-node {
  display: flex;
  border-top: var(--el-border);
  border-bottom: var(--el-border);
  overflow: visible;
  position: relative;

  .add-branch {
    position: absolute;
    left: 50%;
    top: -15px;
    z-index: 2;
    transform: translateX(-50%);
  }

  .col-box {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    background-color: var(--flow-bg-color);

    &:before {
      content: '';
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      margin: auto;
      width: 1px;
      height: 100%;
      background-color: var(--el-border-color);
    }

    .top-left-border {
      position: absolute;
      left: 0;
      top: -3px;
      height: 3px;
      width: 50%;
      background-color: var(--flow-bg-color);
    }

    .bottom-left-border {
      position: absolute;
      left: 0;
      bottom: -3px;
      height: 3px;
      width: 50%;
      background-color: var(--flow-bg-color);
    }

    .top-right-border {
      position: absolute;
      right: 0;
      top: -3px;
      height: 3px;
      width: 50%;
      background-color: var(--flow-bg-color);
    }

    .bottom-right-border {
      position: absolute;
      right: 0;
      bottom: -3px;
      height: 3px;
      width: 50%;
      background-color: var(--flow-bg-color);
    }
  }
}

.col-node {
  &:hover {
    .move-right,
    .move-left {
      opacity: 1;
    }
  }
}

.move-right,
.move-left {
  position: absolute;
  top: 0;
  height: 100%;
  display: flex;
  align-items: center;
  opacity: 0;

  &:hover {
    background-color: var(--el-color-primary-light-9);
  }
}

.move-left {
  left: 0;
}

.move-right {
  right: 0;
}
</style>


================================================
FILE: src/views/flowDesign/nodes/Node.vue
================================================
<script setup lang="ts">
import type { ErrorInfo, FlowNode, NodeType } from './type'
import { ClickOutside as vClickOutside, type InputInstance } from 'element-plus'
import Add from './Add.vue'
import type { Ref } from 'vue'

const _inject = inject<{
  readOnly?: Ref<boolean>
  nodesError: Ref<Recordable<ErrorInfo[]>>
}>('flowDesign', { readOnly: ref(false), nodesError: ref({}) })
const $emits = defineEmits<{
  (e: 'addNode', type: NodeType, node: FlowNode): void
  (e: 'delNode', node: FlowNode): void
  (e: 'activeNode', node: FlowNode): void
}>()
const $props = withDefaults(
  defineProps<{
    icon?: string
    node: FlowNode
    color?: string
    readOnly?: boolean
    close?: boolean
  }>(),
  {
    readOnly: false,
    close: true
  }
)
const errorInfo = computed<ErrorInfo[] | undefined>(() => _inject.nodesError.value[$props.node.id])
const _readOnly = computed(() => _inject.readOnly?.value || $props.readOnly)
const showInput = ref(false)
const inputRef = ref<InputInstance>()
const onShowInput = () => {
  if (_readOnly.value) return
  showInput.value = true
  nextTick(() => {
    inputRef.value?.focus()
  })
}
const onClickOutside = () => {
  if (showInput.value) {
    showInput.value = false
  }
}
const activeNode = () => {
  if (_readOnly.value) return
  $emits('activeNode', $props.node)
}
const addNode = (type: NodeType) => {
  $emits('addNode', type, $props.node)
}
const delNode = () => {
  $emits('delNode', $props.node)
}
</script>

<template>
  <div class="node-box">
    <div @click="activeNode" :class="['node', { 'error-node': errorInfo?.length && !_readOnly }]">
      <!--头部-->
      <div class="node-header">
        <!--删除按钮-->
        <span @click.stop>
          <el-popconfirm
            title="您确定要删除该节点吗?"
            width="200"
            :hide-after="0"
            placement="right-start"
            @confirm="delNode"
          >
            <template #reference>
              <el-button
                class="node-close"
                v-show="close && !_readOnly"
                plain
                circle
                icon="CircleClose"
                size="small"
                type="danger"
              />
            </template>
          </el-popconfirm>
        </span>
        <div class="head">
          <div @click.stop v-if="showInput">
            <el-input
              ref="inputRef"
              v-click-outside="onClickOutside"
              @blur="onClickOutside"
              maxlength="30"
              v-model="node.name"
            />
          </div>
          <el-text tag="b" truncated v-else @click.stop="onShowInput">
            {{ node.name }}
            <el-icon>
              <EditPen />
            </el-icon>
          </el-text>
          <slot name="icon">
            <svg-icon :size="30" color="node-icon" v-if="icon" :name="icon" />
          </slot>
        </div>
        <!--错误提示-->
        <el-tooltip placement="top-start">
          <template #content>
            <div v-for="err in errorInfo" :key="err.id">
              {{ err.message }}
            </div>
          </template>
          <el-icon class="warn-icon" :size="20" v-show="errorInfo?.length && !_readOnly">
            <WarnTriangleFilled @click.stop />
          </el-icon>
        </el-tooltip>
      </div>
      <!--插槽内容-->
      <div class="node-content">
        <slot></slot>
      </div>
    </div>
    <Add @add-node="addNode" />
  </div>
</template>

<style scoped lang="scss">
.node-box {
  position: relative;

  /* &:before {
    content: '';
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    margin: auto;
    width: 1px;
    height: 100%;
    background-color: var(--el-border-color);
  }*/

  &:after {
    content: '';
    position: absolute;
    top: -12px;
    left: 50%;
    transform: translate(-50%);
    border-style: solid;
    width: 0;
    border-width: 8px 6px 4px;
    border-color: var(--el-border-color) transparent transparent;
    background-color: var(--flow-bg-color);
  }

  .warn-icon {
    cursor: pointer;
    position: absolute;
    right: -30px;
    top: 50%;
    transform: translateY(-50%);
    color: var(--el-color-error);
  }

  .node {
    border-radius: 7px;
    cursor: pointer;
    position: relative;
    // overflow: visible;
    min-height: 90px;
    width: 230px;
    z-index: 10;
    color: var(--el-text-color-primary);
    border: 1px solid var(--el-border-color-light);
    background-color: var(--el-bg-color-overlay);
    box-shadow: var(--el-box-shadow-light);

    &.error-node {
      box-shadow: 0 0 5px 0 var(--el-color-error-light-3);
    }

    .node-header {
      padding: 2px 7px;
      border-radius: 7px 7px 0 0;
      background: v-bind(color);
      box-sizing: border-box;
      border-bottom: 1px solid var(--el-border-color-light);

      .head {
        display: flex;
        align-items: center;
        justify-content: space-between;

        :deep(.el-input__wrapper) {
          background-color: var(--el-bg-color-overlay);
        }
      }

      .node-close {
        position: absolute;
        top: -10px;
        right: -10px;
        z-index: 10;
        display: none;
      }
    }

    .node-content {
      position: relative;
      padding: 20px;
    }

    &:hover {
      &:not(.error-node) {
        box-shadow: 0 0 5px 0 var(--el-color-primary);
      }

      .node-close {
        display: block;
      }
    }
  }
}
</style>


================================================
FILE: src/views/flowDesign/nodes/NotifyNode.vue
================================================
<script setup lang="ts">
import Node from './Node.vue'
import type { NotifyNode } from './type'
import type { Ref } from 'vue'
import type { ErrorInfo } from './type'
import { getById } from '@/api/modules/role'
import type { Field } from '@/components/Render/type'
import { getByUsername } from '@/api/modules/user'

const props = defineProps<{
  node: NotifyNode
}>()
const { fields, nodesError } = inject<{
  fields: Ref<Field[]>
  nodesError: Ref<Recordable<ErrorInfo[]>>
}>('flowDesign', { fields: ref([]), nodesError: ref({}) })
const content = ref<string>('')
watchEffect(() => {
  const errors: ErrorInfo[] = []
  const {
    id,
    assigneeType,
    name,
    users,
    roles,
    leader,
    choice,
    formUser,
    formRole,
    orgLeader,
    subject,
    types
  } = props.node
  if (assigneeType === 'user') {
    if (users.length > 0) {
      const all = users.map((user) => getByUsername(user))
      Promise.all(all).then((users) => {
        content.value = users.map((user) => user.data.name).join('、')
      })
    } else {
      errors.push({ id: id, name: name, message: '未指定人员' })
      content.value = '未指定人员'
    }
  } else if (assigneeType === 'choice') {
    content.value = `发起人自选(${choice ? '多选' : '单选'})`
  } else if (assigneeType === 'self') {
    content.value = '发起人自己'
  } else if (assigneeType === 'leader') {
    content.value = leader === 1 ? '直属上级' : `${leader}级上级`
  } else if (assigneeType === 'orgLeader') {
    content.value = orgLeader === 1 ? '直属主管' : `${orgLeader}级主管`
  } else if (assigneeType === 'formUser') {
    if (!formUser) {
      errors.push({ id: id, name: name, message: '未指定表单内人员' })
    }
    const title = fields.value.find((e) => e.id === formUser)?.label || formUser || '?'
    content.value = `表单内(${title})人员`
  } else if (assigneeType === 'formRole') {
    if (!formRole) {
      errors.push({ id: id, name: name, message: '未指定表单内角色' })
    }
    const title = fields.value.find((e) => e.id === formRole)?.label || formRole || '?'
    content.value = `表单内(${title})角色`
  } else if (assigneeType === 'role') {
    if (roles.length > 0) {
      const all = roles.map((id) => getById(id))
      Promise.all(all).then((roles) => {
        content.value = roles.map((res) => res.data.name).join('、')
      })
    } else {
      errors.push({ id: id, name: name, message: '未指定角色' })
      content.value = '未指定角色'
    }
  } else {
    errors.push({ id: id, name: name, message: '未知错误' })
    content.value = name
  }

  if (types.length === 0) {
    errors.push({ id: id, name: name, message: '未指定通知类型' })
  }
  if (!subject) {
    errors.push({ id: id, name: name, message: '消息主题为空' })
  }
  if (!props.node.content) {
    errors.push({ id: id, name: name, message: '消息内容为空' })
  }

  // 记录错误
  if (errors.length > 0) {
    nodesError.value[id] = errors
  } else {
    delete nodesError.value[id]
  }
})
</script>

<template>
  <Node v-bind="$attrs" icon="el:BellFilled" color="#95d475" :node="node">
    <el-text>{{ content }}</el-text>
  </Node>
</template>

<style scoped lang="scss"></style>


================================================
FILE: src/views/flowDesign/nodes/ServiceNode.vue
================================================
<script setup lang="ts">
import Node from './Node.vue'
import type { ErrorInfo, ServiceNode } from './type'
import type { Ref } from 'vue'

const props = defineProps<{
  node: ServiceNode
}>()
const { nodesError } = inject<{
  nodesError: Ref<Recordable<ErrorInfo[]>>
}>('flowDesign', { nodesError: ref({}) })
const content = ref<string>('')
watchEffect(() => {
  const errors: ErrorInfo[] = []
  const { id, name, implementationType, implementation } = props.node
  if (!implementationType) {
    errors.push({ id: id, name: name, message: '执行类型为空' })
    content.value = '执行类型为空'
  } else if (!implementation) {
    errors.push({ id: id, name: name, message: '执行值为空' })
    content.value = '执行值为空'
  } else {
    content.value = `执行服务`
  }
  // 记录错误
  if (errors.length > 0) {
    nodesError.value[id] = errors
  } else {
    delete nodesError.value[id]
  }
})
</script>

<template>
  <Node v-bind="$attrs" icon="el:Tools" color="#ffc107" :node="node">
    <el-text>{{ content }}</el-text>
  </Node>
</template>

<style scoped lang="scss"></style>


================================================
FILE: src/views/flowDesign/nodes/StartNode.vue
================================================
<script setup lang="ts">
import Node from './Node.vue'
import type { StartNode } from './type'
import type { ErrorInfo } from './type'
import type { Ref } from 'vue'

const props = defineProps<{
  node: StartNode
}>()
const { nodesError } = inject<{
  nodesError: Ref<Recordable<ErrorInfo[]>>
}>('flowDesign', { nodesError: ref({}) })
watchEffect(() => {
  const errors: ErrorInfo[] = []
  const { id, name, next } = props.node
  if (next?.type === 'end') {
    errors.push({ id: id, name: name, message: '发起下节点为空' })
  }
  // 记录错误
  if (errors.length > 0) {
    nodesError.value[id] = errors
  } else {
    delete nodesError.value[id]
  }
})
</script>

<template>
  <div class="start-node">
    <Node v-bind="$attrs" icon="el:List" :close="false" color="#8c7cf3" :node="node">
      <el-text>发起人</el-text>
    </Node>
  </div>
</template>

<style scoped lang="scss">
.start-node {
  padding-top: 50px;
  :deep(.node-box) {
    &:after {
      display: none;
    }
  }
}
</style>


================================================
FILE: src/views/flowDesign/nodes/TimerNode.vue
================================================
<script setup lang="ts">
import Node from './Node.vue'
import type { TimerNode } from './type'
import type { Ref } from 'vue'
import type { ErrorInfo } from './type'

const props = defineProps<{
  node: TimerNode
}>()
const { nodesError } = inject<{
  nodesError: Ref<Recordable<ErrorInfo[]>>
}>('flowDesign', { nodesError: ref({}) })
const content = ref<string>('')
const unitMap: Recordable = {
  'PT%sS': '秒',
  'PT%sM': '分钟',
  'PT%sH': '小时',
  'P%sD': '天',
  'P%sW': '周',
  'P%sM': '月'
}
watchEffect(() => {
  const errors: ErrorInfo[] = []
  const { id, name, waitType, unit, duration, timeDate } = props.node
  if (waitType === 'date') {
    content.value = `等至 ${timeDate || '?'}`
    if (!timeDate) {
      errors.push({ id: id, name: name, message: '未设置等待时间' })
    }
  } else if (waitType === 'duration') {
    content.value = `等待 ${duration} ${unitMap[unit]}`
    if (duration <= 0) {
      errors.push({ id: id, name: name, message: '未设置等待时长' })
    }
  }
  // 记录错误
  if (errors.length > 0) {
    nodesError.value[id] = errors
  } else {
    delete nodesError.value[id]
  }
})
</script>

<template>
  <Node v-bind="$attrs" icon="el:Timer" color="#E872B7" :node="node">
    <el-text>{{ content }}</el-text>
  </Node>
</template>

<style scoped lang="scss"></style>


================================================
FILE: src/views/flowDesign/nodes/TreeNode.vue
================================================
<script setup lang="ts" name="TreeNode">
import type { FlowNode } from './type'
import type { Component } from 'vue'
import Start from './StartNode.vue'
import End from './EndNode.vue'
import Approval from './ApprovalNode.vue'
import Cc from './CcNode.vue'
import Timer from './TimerNode.vue'
import Notify from './NotifyNode.vue'
import Service from './ServiceNode.vue'
import Exclusive from './ExclusiveNode.vue'
import Condition from './ConditionNode.vue'

defineProps<{
  node: FlowNode
}>()
const nodes: Recordable<Component> = {
  start: Start,
  approval: Approval,
  cc: Cc,
  timer: Timer,
  notify: Notify,
  service: Service,
  exclusive: Exclusive,
  condition: Condition,
  end: End
}
</script>

<template>
  <slot />
  <component :is="nodes[node.type]" :node="node" v-bind="$attrs">
    <template v-for="(value, name) in $slots" #[name]="scope">
      <slot :name="name" v-bind="scope || {}"></slot>
    </template>
  </component>
  <TreeNode v-if="node.next" :node="node.next" v-bind="$attrs" />
</template>

<style scoped lang="scss"></style>


================================================
FILE: src/views/flowDesign/nodes/type.ts
================================================
import type { FilterRules } from '@/components/AdvancedFilter/type'

export type NodeType =
  | 'start'
  | 'approval'
  | 'cc'
  | 'exclusive'
  | 'timer'
  | 'notify'
  | 'service'
  | 'condition'
  | 'end'

export interface FlowNode {
  id: string
  pid?: string
  name: string
  type: NodeType
  executionListeners?: NodeListener[]
  next?: FlowNode
}

export interface NodeListener {
  event: string
  implementationType: 'class' | 'expression' | 'delegateExpression'
  implementation: string
}

export interface StartNode extends FlowNode {
  formProperties: FormProperty[]
}

export interface EndNode extends FlowNode {}

export interface AssigneeNode extends FlowNode {
  // 审批方式
  assigneeType:
    | 'user'
    | 'role'
    | 'choice'
    | 'self'
    | 'leader'
    | 'orgLeader'
    | 'formUser'
    | 'formRole'
    | 'autoRefuse'
  // 审批人
  users: string[]
  // 审批角色
  roles: string[]
  // 表单内人员
  formUser: string
  // 表单内角色
  formRole: string
  // 主管
  leader: number
  // 组织主管
  orgLeader: number
  // 自选:true-多选,false-单选
  choice: boolean
  // 发起人自己
  self: boolean
}

export interface CcNode extends AssigneeNode {
  formProperties: FormProperty[]
}

export interface NotifyNode extends AssigneeNode {
  types: ('site' | 'email' | 'sms' | 'wechat' | 'dingtalk' | 'feishu')[]
  subject: string
  content: string
}

export interface ApprovalNode extends AssigneeNode {
  // 多人审批方式
  multi: 'sequential' | 'joint' | 'single'
  // 审批人为空时处理方式:reject-拒绝,pass-通过,admin-管理员,assign-指定人员
  nobody: 'refuse' | 'pass' | 'admin' | 'assign'
  // 多人审批通过比例
  multiPercent: number
  // 审批人为空时,指定人员
  nobodyUsers: string[]
  // 表单字段
  formProperties: FormProperty[]
  // 操作权限
  operations: OperationPermissions
  // 任务监听器
  taskListeners?: NodeListener[]
}

export interface ServiceNode extends FlowNode {
  implementationType: string
  implementation: string
}

export interface TimerNode extends FlowNode {
  waitType: 'duration' | 'date'
  unit: 'PT%sS' | 'PT%sM' | 'PT%sH' | 'P%sD' | 'P%sW' | 'P%sM'
  duration: number
  timeDate?: string
}

export interface ConditionNode extends FlowNode {
  def: boolean
  conditions: FilterRules
}

export interface BranchNode extends FlowNode {
  branches: FlowNode[]
}

export interface ExclusiveNode extends BranchNode {
  branches: ConditionNode[]
}

export interface ErrorInfo {
  id: string
  name: string
  message: string
}

export interface FormProperty {
  // 字段ID
  id: string
  // 字段名称
  name: string
  // 只读
  readonly: boolean
  // 必填
  required: boolean
  // 隐藏
  hidden: boolean
}

export interface OperationPermissions {
  // 同意
  complete: boolean
  // 拒绝
  refuse: boolean
  // 回退
  back: boolean
  // 转交
  transfer: boolean
  // 委派
  delegate: boolean
  // 加签
  addMulti: boolean
  // 减签
  minusMulti: boolean
}


================================================
FILE: src/views/flowDesign/panels/ApprovalPanel.vue
================================================
<script setup lang="ts">
import type { ApprovalNode, FormProperty } from '../nodes/type'
import type { Field } from '@/components/Render/type'
import UserSelector from '@/components/UserSelector/index.vue'
import AssigneePanel from './AssigneePanel.vue'
import type { Ref } from 'vue'
import TaskListeners from './TaskListeners.vue'

const { fields } = inject<{ fields: Ref<Field[]>; admin: string[] }>('flowDesign', {
  fields: ref([]),
  admin: []
})
const props = defineProps<{
  activeData: ApprovalNode
}>()
const spacer = h(ElDivider)
const activeName = ref('properties')
const allReadonly = computed({
  get() {
    return props.activeData.formProperties.every((e) => e.readonly)
  },
  set(val) {
    props.activeData.formProperties.forEach((e) => (e.readonly = val))
    if (val) {
      allHidden.value = false
      allRequired.value = false
    }
  }
})
const allHidden = computed({
  get() {
    return props.activeData.formProperties.every((e) => e.hidden)
  },
  set(val) {
    props.activeData.formProperties.forEach((e) => (e.hidden = val))
    if (val) {
      allRequired.value = false
      allReadonly.value = false
    }
  }
})
const allRequired = computed({
  get() {
    return props.activeData.formProperties.every((e) => e.required)
  },
  set(val) {
    props.activeData.formProperties.forEach((e) => (e.required = val))
    if (val) {
      allReadonly.value = false
      allHidden.value = false
    }
  }
})

const changeReadonly = (row: FormProperty) => {
  if (row.readonly) {
    row.required = false
    row.hidden = false
  }
}
const changeRequired = (row: FormProperty) => {
  if (row.required) {
    row.readonly = false
    row.hidden = false
  }
}
const changeHidden = (row: FormProperty) => {
  if (row.hidden) {
    row.readonly = false
    row.required = false
  }
}

watchEffect(() => {
  const formProperties = props.activeData.formProperties
  props.activeData.formProperties = fields.value.map((field) => ({
    id: field.id,
    name: field.label,
    readonly: field.readonly || false,
    hidden: field.hidden,
    required: field.required || false
  }))
  props.activeData.formProperties.forEach((item) => {
    const properties = formProperties.find((f) => f.id === item.id)
    if (properties) {
      item.readonly = properties.readonly
      item.hidden = properties.hidden
      item.required = properties.required
    }
  })
})
</script>

<template>
  <el-tabs v-model="activeName" stretch class="el-segmented">
    <el-tab-pane label="审批人" name="properties">
      <el-form label-position="top" label-width="90px">
        <AssigneePanel :active-data="activeData" :fields="fields" type="审批">
          <el-col :span="8">
            <el-radio value="autoRefuse">自动拒绝</el-radio>
          </el-col>
        </AssigneePanel>
        <el-form-item prop="method" label="多人审批方式">
          <el-radio-group v-model="activeData.multi" class="flex-col important-items-start">
            <el-radio value="sequential">依次审批(按顺序审批)</el-radio>
            <el-radio value="joint">会签(需要所有审批人都通过)</el-radio>
            <el-radio value="single">或签(其中一名审批人通过即可)</el-radio>
          </el-radio-group>
          <el-text v-if="activeData.multi === 'joint'">
            需要 <el-input-number v-model="activeData.multiPercent" :min="1" :max="100" /> %人员通过
          </el-text>
        </el-form-item>
        <el-form-item prop="nobody" label="审批人为空">
          <el-radio-group v-model="activeData.nobody" class="w-full">
            <el-row>
              <el-col :span="12">
                <el-radio value="pass">自动通过</el-radio>
              </el-col>
              <el-col :span="12">
                <el-radio value="assign">指定人员</el-radio>
              </el-col>
              <el-col :span="12">
                <el-radio value="reject">自动拒绝</el-radio>
              </el-col>
              <el-col :span="12">
                <el-radio value="admin">转交流程管理员</el-radio>
              </el-col>
            </el-row>
          </el-radio-group>
          <user-selector
            v-if="activeData.nobody === 'assign'"
            multiple
            v-model="activeData.nobodyUsers"
            placeholder="指定人员"
          />
        </el-form-item>
        <el-form-item prop="taskListeners" label="任务监听器">
          <TaskListeners :node="activeData" />
        </el-form-item>
      </el-form>
    </el-tab-pane>
    <el-tab-pane label="表单权限" name="formPermissions">
      <el-table :data="activeData.formProperties">
        <el-table-column prop="name" label="字段" />
        <el-table-column prop="readonly">
          <template #header>
            <el-checkbox v-model="allReadonly" label="只读" />
          </template>
          <template #default="{ row }">
            <el-checkbox v-model="row.readonly" @change="changeReadonly(row)" />
          </template>
        </el-table-column>
        <el-table-column prop="required">
          <template #header>
            <el-checkbox v-model="allRequired" label="必填" />
          </template>
          <template #default="{ row }">
            <el-checkbox v-model="row.required" @change="changeRequired(row)" />
          </template>
        </el-table-column>
        <el-table-column prop="hidden">
          <template #header>
            <el-checkbox v-model="allHidden" label="隐藏" />
          </template>
          <template #default="{ row }">
            <el-checkbox v-model="row.hidden" @change="changeHidden(row)" />
          </template>
        </el-table-column>
      </el-table>
    </el-tab-pane>
    <el-tab-pane label="操作权限" name="operationPermissions">
      <el-space fill :size="0" direction="horizontal" :spacer="spacer">
        <div class="opt-item">
          <el-icon :size="32" class="opt-item__icon">
            <CircleCheck />
          </el-icon>
          <div class="opt-item__content">
            <el-text tag="b"> 同意</el-text>
            <div class="opt-item__second">审批通过,流转到下一个节点</div>
          </div>
          <el-switch
            v-model="activeData.operations.complete"
            inline-prompt
            active-text="开"
            inactive-text="关"
          />
        </div>
        <div class="opt-item">
          <el-icon :size="32" class="opt-item__icon">
            <CircleClose />
          </el-icon>
          <div class="opt-item__content">
            <el-text tag="b"> 拒绝</el-text>
            <div class="opt-item__second">当拒绝任务时,当前任务被终止,并结束整个流程</div>
          </div>
          <el-switch
            v-model="activeData.operations.refuse"
            inline-prompt
            active-text="开"
            inactive-text="关"
          />
        </div>
        <div class="opt-item">
          <el-icon :size="32" class="opt-item__icon">
            <Back />
          </el-icon>
          <div class="opt-item__content">
            <el-text tag="b"> 退回</el-text>
            <div class="opt-item__second">
              若审批内容存在问题,当前任务将中止并回退至特定历史任务节点
            </div>
          </div>
          <el-switch
            v-model="activeData.operations.back"
            inline-prompt
            active-text="开"
            inactive-text="关"
          />
        </div>
        <div class="opt-item">
          <el-icon :size="32" class="opt-item__icon">
            <Switch />
          </el-icon>
          <div class="opt-item__content">
            <el-text tag="b"> 转交</el-text>
            <div class="opt-item__second">
              将当前任务移交给其他人处理,以便他们继续执行所需的操作
            </div>
          </div>
          <el-switch
            v-model="activeData.operations.transfer"
            inline-prompt
            active-text="开"
            inactive-text="关"
          />
        </div>
        <div class="opt-item">
          <el-icon :size="32" class="opt-item__icon">
            <UserFilled />
          </el-icon>
          <div class="opt-item__content">
            <el-text tag="b"> 委派</el-text>
            <div class="opt-item__second">将当前任务暂时交由他人处理,待其完成后再交回自己处理</div>
          </div>
          <el-switch
            v-model="activeData.operations.delegate"
            inline-prompt
            active-text="开"
            inactive-text="关"
          />
        </div>
        <div class="opt-item">
          <svg-icon class-name="opt-item__icon" name="add-user" :size="32" />
          <div class="opt-item__content">
            <el-text tag="b"> 加签</el-text>
            <div class="opt-item__second">
              在当前任务上额外添加新人员,以处理相关事项或提供必要的审批或意见
            </div>
          </div>
          <el-switch
            v-model="activeData.operations.addMulti"
            inline-prompt
            active-text="开"
            inactive-text="关"
          />
        </div>
        <div class="opt-item">
          <svg-icon class-name="opt-item__icon" name="reduce-user" :size="32" />
          <div class="opt-item__content">
            <el-text tag="b"> 减签</el-text>
            <div class="opt-item__second">
              在当前任务中减少处理人员数量,以简化流程或重新分配责任
            </div>
          </div>
          <el-switch
            v-model="activeData.operations.minusMulti"
            inline-prompt
            active-text="开"
            inactive-text="关"
          />
        </div>
      </el-space>
    </el-tab-pane>
  </el-tabs>
</template>

<style scoped lang="scss">
@import '@/styles/el-segmented.scss';

.opt-item {
  display: flex;
  align-items: center;

  .opt-item__icon {
    background: var(--el-color-primary);
    color: var(--el-color-white);
    border-radius: 7px;
    padding: 3px;
  }

  .opt-item__content {
    box-sizing: border-box;
    flex: 1;
    margin-left: 20px;
    font-size: 14px;

    .opt-item__second {
      margin-top: 3px;
      font-size: 12px;
      color: var(--el-text-color-placeholder);
    }
  }
}
</style>


================================================
FILE: src/views/flowDesign/panels/AssigneePanel.vue
================================================
<script setup lang="ts">
import type { AssigneeNode } from '../nodes/type'
import type { Field } from '@/components/Render/type'

defineProps<{
  activeData: AssigneeNode
  fields: Field[]
  type: '审批' | '抄送' | '办理' | '通知'
}>()
</script>

<template>
  <el-form label-position="top" label-width="90px">
    <el-form-item prop="assigneeType" :label="`${type}对象`">
      <el-radio-group v-model="activeData.assigneeType">
        <el-row>
          <el-col :span="8">
            <el-radio value="user">指定人员</el-radio>
          </el-col>
          <el-col :span="8">
            <el-radio value="role">指定角色</el-radio>
          </el-col>
          <el-col :span="8">
            <el-radio value="choice">发起人自选</el-radio>
          </el-col>
          <el-col :span="8">
            <el-radio value="self">发起人自己</el-radio>
          </el-col>
          <el-col :span="8">
            <el-radio value="leader">直属上级</el-radio>
          </el-col>
          <el-col :span="8">
            <el-radio value="orgLeader">组织主管</el-radio>
          </el-col>
          <el-col :span="8">
            <el-radio value="formUser">表单内人员</el-radio>
          </el-col>
          <el-col :span="8">
            <el-radio value="formRole">表单内角色</el-radio>
          </el-col>
          <slot></slot>
        </el-row>
      </el-radio-group>
    </el-form-item>
    <el-form-item prop="users" label="指定人员" v-if="activeData.assigneeType === 'user'">
      <user-selector v-model="activeData.users" multiple :placeholder="`请选择${type}人`" />
    </el-form-item>
    <el-form-item prop="choice" label="发起人自选择" v-if="activeData.assigneeType === 'choice'">
      <el-radio-group v-model="activeData.choice">
        <el-radio-button :value="false">单选</el-radio-button>
        <el-radio-button :value="true">多选</el-radio-button>
      </el-radio-group>
    </el-form-item>
    <el-form-item prop="leader" label="多级上级" v-if="activeData.assigneeType === 'leader'">
      <el-select v-model="activeData.leader" style="width: 220px" placeholder="请选择多级上级">
        <el-option label="直属上级" :value="1"></el-option>
        <el-option label="2级上级" :value="2"></el-option>
        <el-option label="3级上级" :value="3"></el-option>
        <el-option label="4级上级" :value="4"></el-option>
        <el-option label="5级上级" :value="5"></el-option>
        <el-option label="6级上级" :value="6"></el-option>
        <el-option label="7级上级" :value="7"></el-option>
        <el-option label="8级上级" :value="8"></el-option>
        <el-option label="9级上级" :value="9"></el-option>
        <el-option label="10级上级" :value="10"></el-option>
        <el-option label="11级上级" :value="11"></el-option>
      </el-select>
    </el-form-item>
    <el-form-item prop="orgLeader" label="组织主管" v-if="activeData.assigneeType === 'orgLeader'">
      <el-select v-model="activeData.orgLeader" style="width: 220px" placeholder="请选择组织主管">
        <el-option label="直属主管" :value="1"></el-option>
        <el-option label="2级主管" :value="2"></el-option>
        <el-option label="3级主管" :value="3"></el-option>
        <el-option label="4级主管" :value="4"></el-option>
        <el-option label="5级主管" :value="5"></el-option>
        <el-option label="6级主管" :value="6"></el-option>
        <el-option label="7级主管" :value="7"></el-option>
        <el-option label="8级主管" :value="8"></el-option>
        <el-option label="9级主管" :value="9"></el-option>
        <el-option label="10级主管" :value="10"></el-option>
        <el-option label="11级主管" :value="11"></el-option>
      </el-select>
    </el-form-item>
    <el-form-item prop="roles" label="指定角色" v-if="activeData.assigneeType === 'role'">
      <RoleSelector
        v-model="activeData.roles"
        style="width: 220px"
        collapse-tags
        :max-collapse-tags="1"
        multiple
        clearable
        placeholder="请选择角色"
      />
    </el-form-item>
    <el-form-item prop="formUser" label="表单内人员" v-if="activeData.assigneeType === 'formUser'">
      <el-select placeholder="选择表单内人员" style="width: 220px" v-model="activeData.formUser">
        <el-option
          v-for="item in fields.filter((e) => e.name === 'UserSelector')"
          :key="item.id"
          :label="item.label"
          :value="item.id"
        />
      </el-select>
    </el-form-item>
    <el-form-item prop="formRole" label="表单内角色" v-if="activeData.assigneeType === 'formRole'">
      <el-select placeholder="选择表单内角色" style="width: 220px" v-model="activeData.formRole">
        <el-option
          v-for="item in fields.filter((e) => e.name === 'RoleSelector')"
          :key="item.id"
          :label="item.label"
          :value="item.id"
        />
      </el-select>
    </el-form-item>
  </el-form>
</template>

<style scoped lang="scss"></style>


================================================
FILE: src/views/flowDesign/panels/CcPanel.vue
================================================
<script setup lang="ts">
import type { CcNode } from '../nodes/type'
import type { Ref } from 'vue'
import type { Field } from '@/components/Render/type'
import type { FormProperty } from '../nodes/type'
import AssigneePanel from './AssigneePanel.vue'

const { fields } = inject<{ fields: Ref<Field[]> }>('flowDesign', { fields: ref([]) })
const props = defineProps<{
  activeData: CcNode
}>()
const activeName = ref('properties')
const allReadonly = computed({
  get() {
    return props.activeData.formProperties.every((e) => e.readonly)
  },
  set(val) {
    props.activeData.formProperties.forEach((e) => (e.readonly = val))
    if (val) {
      allHidden.value = false
    }
  }
})
const allHidden = computed({
  get() {
    return props.activeData.formProperties.every((e) => e.hidden)
  },
  set(val) {
    props.activeData.formProperties.forEach((e) => (e.hidden = val))
    if (val) {
      allReadonly.value = false
    }
  }
})

const changeReadonly = (row: FormProperty) => {
  if (row.readonly) {
    row.hidden = false
  }
}
const changeHidden = (row: FormProperty) => {
  if (row.hidden) {
    row.readonly = false
  }
}

watchEffect(() => {
  const formProperties = props.activeData.formProperties
  props.activeData.formProperties = fields.value.map((field) => ({
    id: field.id,
    name: field.label,
    readonly: field.readonly || false,
    hidden: field.hidden,
    required: field.required || false
  }))
  props.activeData.formProperties.forEach((item) => {
    const properties = formProperties.find((f) => f.id === item.id)
    if (properties) {
      item.readonly = properties.readonly
      item.hidden = properties.hidden
      item.required = properties.required
    }
  })
})
</script>

<template>
  <el-tabs v-model="activeName" stretch class="el-segmented">
    <el-tab-pane label="抄送人" name="properties">
      <el-form label-position="top" label-width="90px">
        <AssigneePanel :active-data="activeData" :fields="fields" type="抄送" />
      </el-form>
    </el-tab-pane>
    <el-tab-pane label="表单权限" name="formPermissions">
      <el-table :data="activeData.formProperties">
        <el-table-column prop="name" label="字段" />
        <el-table-column prop="readonly">
          <template #header>
            <el-checkbox v-model="allReadonly" label="只读" />
          </template>
          <template #default="{ row }">
            <el-checkbox v-model="row.readonly" @change="changeReadonly(row)" />
          </template>
        </el-table-column>
        <el-table-column prop="hidden">
          <template #header>
            <el-checkbox v-model="allHidden" label="隐藏" />
          </template>
          <template #default="{ row }">
            <el-checkbox v-model="row.hidden" @change="changeHidden(row)" />
          </template>
        </el-table-column>
      </el-table>
    </el-tab-pane>
  </el-tabs>
</template>

<style scoped lang="scss">
@import '@/styles/el-segmented.scss';
</style>


================================================
FILE: src/views/flowDesign/panels/ConditionPanel.vue
================================================
<script setup lang="ts">
import type { ConditionNode } from '../nodes/type'
import type { Ref } from 'vue'
import type { Field } from '@/components/Render/type'

const { fields } = inject<{ fields: Ref<Field[]> }>('flowDesign', { fields: ref([]) })
defineProps<{
  activeData: ConditionNode
}>()
const initialFormFields = ref<Field[]>([
  {
    id: 'initiator',
    name: 'UserSelector',
    type: 'formItem',
    label: '发起人',
    value: null,
    readonly: false,
    required: true,
    hidden: false,
    props: {
      key: undefined,
      multiple: false,
      placeholder: '请选择发起人',
      class: [],
      style: {
        width: '100%'
      }
    }
  }
])
</script>

<template>
  <AdvancedFilter
    v-model="activeData.conditions"
    :filter-fields="[...initialFormFields, ...fields]"
  />
</template>

<style scoped lang="scss"></style>


================================================
FILE: src/views/flowDesign/panels/EndPanel.vue
================================================
<script setup lang="ts">
import type { EndNode } from '../nodes/type'
import ExecutionListeners from './ExecutionListeners.vue'

defineProps<{
  activeData: EndNode
}>()
</script>

<template>
  <el-form label-position="top" label-width="90px">
    <el-form-item prop="executionListeners" label="执行监听器">
      <ExecutionListeners :node="activeData" />
    </el-form-item>
  </el-form>
</template>

<style scoped lang="scss"></style>


================================================
FILE: src/views/flowDesign/panels/ExecutionListeners.vue
================================================
<script setup lang="ts">
import type { FlowNode } from '@/views/flowDesign/nodes/type'

const props = defineProps<{
  node: FlowNode
}>()
const drawer = ref(false)
const addListener = () => {
  if (!props.node.executionListeners) {
    props.node.executionListeners = []
  }
  props.node.executionListeners?.push({
    event: 'start',
    implementationType: 'delegateExpression',
    implementation: ''
  })
}
const delListener = (index: number) => {
  props.node.executionListeners?.splice(index, 1)
}
</script>

<template>
  <div>
    <slot>
      <el-badge :value="node.executionListeners?.length || 0" class="item" type="primary">
        <el-button icon="Setting" @click="drawer = true"> 配置</el-button>
      </el-badge>
    </slot>
    <el-drawer v-model="drawer" :lock-scroll="false" title="执行监听器">
      <div class="flex-col">
        <el-button @click="addListener" type="primary" icon="Plus">添加监听器</el-button>
        <div v-for="(item, index) in node.executionListeners" :key="index" class="listener-box">
          <el-button
            class="listener-close"
            @click="delListener(index)"
            plain
            circle
            icon="CircleClose"
            size="small"
            type="danger"
          />
          <el-form-item label="事件" :prop="`executionListeners.${index}.event`">
            <el-radio-group v-model="item.event">
              <el-radio-button label="开始" value="start" />
              <el-radio-button label="结束" value="end" />
              <el-radio-button label="迁移" value="take" />
            </el-radio-group>
          </el-form-item>
          <el-form-item label="类型" :prop="`executionListeners.${index}.implementationType`">
            <el-radio-group v-model="item.implementationType">
              <el-radio-button label="委托表达式" value="delegateExpression" />
              <el-radio-button label="java类" value="class" />
              <el-radio-button label="表达式" value="expression" />
            </el-radio-group>
          </el-form-item>
          <el-form-item label="监听器" :prop="`executionListeners.${index}.implementation`">
            <template #label>
              <div class="flex-items-center gap3px">
                <span>监听器</span>
                <el-tooltip placement="top-start">
                  <template #content>
                    实现 ExecutionListener 接口 <br />
                    委托表达式:${myExecutionListener} <br />
                    表达式: ${myExecutionListener.notify(execution)} <br />
                    java类:${com.example.listener.MyExecutionListener}
                  </template>
                  <el-icon>
                    <QuestionFilled />
                  </el-icon>
                </el-tooltip>
              </div>
            </template>
            <el-input v-model="item.implementation" placeholder="请输入监听器" clearable>
            </el-input>
          </el-form-item>
        </div>
      </div>
    </el-drawer>
  </div>
</template>

<style scoped lang="scss">
.listener-box {
  border: 1px dashed var(--el-border-color);
  border-radius: var(--el-border-radius-base);
  margin-top: 10px;
  padding: 7px;
  position: relative;

  &:hover {
    border-color: var(--el-color-primary);

    .listener-close {
      display: block;
    }
  }

  .listener-close {
    position: absolute;
    top: -7px;
    right: -7px;
    z-index: 1;
    display: none;
  }
}
</style>


================================================
FILE: src/views/flowDesign/panels/NotifyPanel.vue
================================================
<script setup lang="ts">
import type { NotifyNode } from '../nodes/type'
import type { Ref } from 'vue'
import type { Field } from '@/components/Render/type'
import AssigneePanel from './AssigneePanel.vue'

const { fields } = inject<{ fields: Ref<Field[]> }>('flowDesign', { fields: ref([]) })
defineProps<{
  activeData: NotifyNode
}>()
</script>

<template>
  <el-form label-position="top" label-width="90px">
    <AssigneePanel :active-data="activeData" :fields="fields" type="通知" />
    <el-form-item prop="types" label="通知类型">
      <el-checkbox-group v-model="activeData.types">
        <el-checkbox label="站内" value="site" />
        <el-checkbox label="邮件" value="email" />
        <el-checkbox label="短信" value="sms" />
        <el-checkbox label="企业微信" value="wechat" />
        <el-checkbox label="钉钉" value="dingtalk" />
        <el-checkbox label="飞书" value="feishu" />
      </el-checkbox-group>
    </el-form-item>
    <el-form-item prop="subject" label="消息主题">
      <template #label>
        <div class="flex-items-center gap3px">
          <el-tooltip content="可以使用 ${字段名} 字段名填充内容" placement="top">
            <el-icon>
              <QuestionFilled />
            </el-icon>
          </el-tooltip>
          <span>消息主题</span>
        </div>
      </template>
      <el-input
        v-model="activeData.subject"
        :maxlength="255"
        clearable
        placeholder="请输入消息主题"
      />
    </el-form-item>
    <el-form-item prop="content" label="消息内容">
      <template #label>
        <div class="flex-items-center gap3px">
          <el-tooltip content="可以使用 ${字段名} 字段名填充内容" placement="top">
            <el-icon>
              <QuestionFilled />
            </el-icon>
          </el-tooltip>
          <span>消息内容</span>
        </div>
      </template>
      <el-input
        v-model="activeData.content"
        :autosize="{ minRows: 6, maxRows: 8 }"
        type="textarea"
        :maxlength="1000"
        show-word-limit
        placeholder="请输入消息内容"
      >
      </el-input>
    </el-form-item>
  </el-form>
</template>

<style scoped lang="scss"></style>


================================================
FILE: src/views/flowDesign/panels/ServicePanel.vue
================================================
<script setup lang="ts">
import type { ServiceNode } from '../nodes/type'

defineProps<{
  activeData: ServiceNode
}>()
</script>

<template>
  <el-form label-position="top">
    <el-form-item prop="implementationType" label="执行类型">
      <el-select v-model="activeData.implementationType" placeholder="请选择执行类型">
        <el-option label="类" value="class" />
        <el-option label="表达式" value="expression" />
        <el-option label="委托表达式" value="delegateExpression" />
      </el-select>
    </el-form-item>
    <el-form-item prop="implementation" label="执行值">
      <template #label>
        <div class="flex-items-center gap3px">
          <span>执行值</span>
          <el-tooltip placement="top-start">
            <template #content>
              实现 JavaDelegate 接口 <br />
              类:${com.example.delegate.MyServiceDelegate} <br />
              表达式: ${myServiceDelegate.execute(execution)} <br />
              委托表达式:${myServiceDelegate}
            </template>
            <el-icon>
              <QuestionFilled />
            </el-icon>
          </el-tooltip>
        </div>
      </template>
      <el-input v-model="activeData.implementation" placeholder="请输入执行值" clearable />
    </el-form-item>
  </el-form>
</template>

<style scoped lang="scss"></style>


================================================
FILE: src/views/flowDesign/panels/StartPanel.vue
================================================
<script setup lang="ts">
import type { FormProperty, StartNode } from '../nodes/type'
import type { Field } from '@/components/Render/type'
import type { Ref } from 'vue'
import ExecutionListeners from './ExecutionListeners.vue'

const { fields } = inject<{ fields: Ref<Field[]> }>('flowDesign', { fields: ref([]) })
const props = defineProps<{
  activeData: StartNode
}>()
const activeName = ref('basicSettings')
const allReadonly = computed({
  get() {
    return props.activeData.formProperties.every((e) => e.readonly)
  },
  set(val) {
    props.activeData.formProperties.forEach((e) => (e.readonly = val))
    if (val) {
      allHidden.value = false
      allRequired.value = false
    }
  }
})
const allHidden = computed({
  get() {
    return props.activeData.formProperties.every((e) => e.hidden)
  },
  set(val) {
    props.activeData.formProperties.forEach((e) => (e.hidden = val))
    if (val) {
      allRequired.value = false
      allReadonly.value = false
    }
  }
})
const allRequired = computed({
  get() {
    return props.activeData.formProperties.every((e) => e.required)
  },
  set(val) {
    props.activeData.formProperties.forEach((e) => (e.required = val))
    if (val) {
      allReadonly.value = false
      allHidden.value = false
    }
  }
})

const changeReadonly = (row: FormProperty) => {
  if (row.readonly) {
    row.required = false
    row.hidden = false
  }
}
const changeRequired = (row: FormProperty) => {
  if (row.required) {
    row.readonly = false
    row.hidden = false
  }
}
const changeHidden = (row: FormProperty) => {
  if (row.hidden) {
    row.readonly = false
    row.required = false
  }
}

watchEffect(() => {
  const formProperties = props.activeData.formProperties
  props.activeData.formProperties = fields.value.map((field) => ({
    id: field.id,
    name: field.label,
    readonly: field.readonly || false,
    hidden: field.hidden,
    required: field.required || false
  }))
  props.activeData.formProperties.forEach((item) => {
    const properties = formProperties.find((f) => f.id === item.id)
    if (properties) {
      item.readonly = properties.readonly
      item.hidden = properties.hidden
      item.required = properties.required
    }
  })
})
</script>

<template>
  <el-tabs v-model="activeName" stretch class="el-segmented">
    <el-tab-pane label="基础设置" name="basicSettings">
      <el-form label-position="top" label-width="90px">
        <el-form-item prop="executionListeners" label="执行监听器">
          <ExecutionListeners :node="activeData" />
        </el-form-item>
      </el-form>
    </el-tab-pane>
    <el-tab-pane label="表单权限" name="formPermissions">
      <el-table :data="activeData.formProperties">
        <el-table-column prop="name" label="字段" />
        <el-table-column prop="readonly">
          <template #header>
            <el-checkbox v-model="allReadonly" label="只读" />
          </template>
          <template #default="{ row }">
            <el-checkbox v-model="row.readonly" @change="changeReadonly(row)" />
          </template>
        </el-table-column>
        <el-table-column prop="required">
          <template #header>
            <el-checkbox v-model="allRequired" label="必填" />
          </template>
          <template #default="{ row }">
            <el-checkbox v-model="row.required" @change="changeRequired(row)" />
          </template>
        </el-table-column>
        <el-table-column prop="hidden">
          <template #header>
            <el-checkbox v-model="allHidden" label="隐藏" />
          </template>
          <template #default="{ row }">
            <el-checkbox v-model="row.hidden" @change="changeHidden(row)" />
          </template>
        </el-table-column>
      </el-table>
    </el-tab-pane>
  </el-tabs>
</template>

<style scoped lang="scss">
@import '@/styles/el-segmented.scss';
</style>


================================================
FILE: src/views/flowDesign/panels/TaskListeners.vue
================================================
<script setup lang="ts">
import type { ApprovalNode } from '@/views/flowDesign/nodes/type'

const props = defineProps<{
  node: ApprovalNode
}>()
const drawer = ref(false)
const addListener = () => {
  if (!props.node.taskListeners) {
    props.node.taskListeners = []
  }
  props.node.taskListeners?.push({
    event: 'create',
    implementationType: 'delegateExpression',
    implementation: ''
  })
}
const delListener = (index: number) => {
  props.node.taskListeners?.splice(index, 1)
}
</script>

<template>
  <div>
    <slot>
      <el-badge :value="node.taskListeners?.length || 0" class="item" type="primary">
        <el-button icon="Setting" @click="drawer = true"> 配置</el-button>
      </el-badge>
    </slot>
    <el-drawer v-model="drawer" :lock-scroll="false" title="任务监听器">
      <div class="flex-col">
        <el-button @click="addListener" type="primary" icon="Plus">添加监听器</el-button>
        <div v-for="(item, index) in node.taskListeners" :key="index" class="listener-box">
          <el-button
            class="listener-close"
            @click="delListener(index)"
            plain
            circle
            icon="CircleClose"
            size="small"
            type="danger"
          />
          <el-form-item label="事件" :prop="`taskListeners.${index}.event`">
            <el-radio-group v-model="item.event">
              <el-radio-button label="创建" value="create" />
              <el-radio-button label="指派" value="assignment" />
              <el-radio-button label="完成" value="complete" />
              <el-radio-button label="删除" value="delete" />
            </el-radio-group>
          </el-form-item>
          <el-form-item label="类型" :prop="`taskListeners.${index}.implementationType`">
            <el-radio-group v-model="item.implementationType">
              <el-radio-button label="委托表达式" value="delegateExpression" />
              <el-radio-button label="java类" value="class" />
              <el-radio-button label="表达式" value="expression" />
            </el-radio-group>
          </el-form-item>
          <el-form-item label="监听器" :prop="`taskListeners.${index}.implementation`">
            <template #label>
              <div class="flex-items-center gap3px">
                <span>监听器</span>
                <el-tooltip placement="top-start">
                  <template #content>
                    委托表达式:${myCreateTaskListener} <br />
                    表达式: ${myCreateTaskListener.notify(execution)} <br />
                    java类:${com.example.listener.MyCreateTaskListener}
                  </template>
                  <el-icon>
                    <QuestionFilled />
                  </el-icon>
                </el-tooltip>
              </div>
            </template>
            <el-input v-model="item.implementation" placeholder="请输入监听器" clearable>
            </el-input>
          </el-form-item>
        </div>
      </div>
    </el-drawer>
  </div>
</template>

<style scoped lang="scss">
.listener-box {
  border: 1px dashed var(--el-border-color);
  border-radius: var(--el-border-radius-base);
  margin-top: 10px;
  padding: 7px;
  position: relative;

  &:hover {
    border-color: var(--el-color-primary);

    .listener-close {
      display: block;
    }
  }

  .listener-close {
    position: absolute;
    top: -7px;
    right: -7px;
    z-index: 1;
    display: none;
  }
}
</style>


================================================
FILE: src/views/flowDesign/panels/TimerPanel.vue
================================================
<script setup lang="ts">
import type { TimerNode } from '../nodes/type'
defineProps<{
  activeData: TimerNode
}>()
</script>

<template>
  <el-form label-position="top">
    <el-form-item prop="waitType" label="等待方式">
      <el-radio-group v-model="activeData.waitType">
        <el-radio-button label="固定时长" value="duration" />
        <el-radio-button label="固定时间" value="date" />
      </el-radio-group>
    </el-form-item>
    <el-form-item prop="duration" label="等待时长" v-if="activeData.waitType === 'duration'">
      <el-input
        v-model.number="activeData.duration"
        :min="0"
        :max="9999999"
        type="number"
        style="max-width: 230px"
        class="input-with-select"
      >
        <template #append>
          <el-select v-model="activeData.unit" placeholder="Select" style="width: 80px">
            <el-option label="秒" value="PT%sS"></el-option>
            <el-option label="分钟" value="PT%sM"></el-option>
            <el-option label="小时" value="PT%sH"></el-option>
            <el-option label="天" value="P%sD"></el-option>
            <el-option label="周" value="P%sW"></el-option>
            <el-option label="月" value="P%sM"></el-option>
          </el-select>
        </template>
      </el-input>
    </el-form-item>
    <el-form-item prop="duration" label="指定时间" v-if="activeData.waitType === 'date'">
      <el-date-picker
        v-model="activeData.timeDate"
        type="datetime"
        format="YYYY-MM-DD HH:mm:ss"
        value-format="YYYY-MM-DD HH:mm:ss"
        placeholder="请选择等待时间"
      />
    </el-form-item>
  </el-form>
</template>

<style scoped lang="scss"></style>


================================================
FILE: src/views/flowDesign/panels/index.vue
================================================
<script setup lang="ts">
import { ClickOutside as vClickOutside } from 'element-plus'
import type { Component } from 'vue'
import Start from './StartPanel.vue'
import Approval from './ApprovalPanel.vue'
import Cc from './CcPanel.vue'
import Timer from './TimerPanel.vue'
import Notify from './NotifyPanel.vue'
import Service from './ServicePanel.vue'
import Condition from './ConditionPanel.vue'
import End from './EndPanel.vue'
import type { FlowNode } from '../nodes/type'

defineProps<{
  activeData: FlowNode
}>()
const penalVisible = defineModel<boolean>({ required: true })
const panels: Recordable<Component> = {
  start: Start,
  approval: Approval,
  cc: Cc,
  timer: Timer,
  notify: Notify,
  service: Service,
  condition: Condition,
  end: End
}
const showInput = ref(false)
const onClickOutside = () => {
  if (showInput.value) {
    showInput.value = false
  }
}
</script>

<template>
  <el-drawer v-model="penalVisible" :lock-scroll="false" size="35%">
    <template #header="{ titleId, titleClass }">
      <span :id="titleId" :class="titleClass">
        <el-input
          v-click-outside="onClickOutside"
          @blur="onClickOutside"
          maxlength="30"
          v-model="activeData.name"
          v-show="showInput"
        ></el-input>
        <el-link icon="EditPen" v-show="!showInput" @click="showInput = true">
          {{ activeData?.name || '节点配置' }}
        </el-link>
      </span>
    </template>
    <component :is="panels[activeData.type]" :activeData="activeData" />
  </el-drawer>
</template>

<style scoped lang="scss">
:deep(.el-tabs__content) {
  margin-top: 10px;
}
</style>


================================================
FILE: src/views/home/index.vue
================================================
<script setup lang="ts">
import FlowDesign from '@/views/flowDesign/index.vue'
import type { Field } from '@/components/Render/type'
import type { EndNode, FlowNode, StartNode } from '@/views/flowDesign/nodes/type'
import { downloadXml } from '@/api/modules/model'

// 流程节点
const process = ref<FlowNode>({
  id: 'root',
  pid: undefined,
  type: 'start',
  name: '发起人',
  executionListeners: [],
  formProperties: [],
  next: {
    id: 'end',
    pid: 'root',
    type: 'end',
    name: '流程结束',
    executionListeners: [],
    next: undefined
  } as EndNode
} as StartNode)
// 表单字段
const fields = ref<Field[]>([
  {
    id: 'field_da2w55',
    type: 'formItem',
    label: '请假人',
    name: 'UserSelector',
    value: null,
    readonly: false,
    required: true,
    hidden: false,
    props: {
      multiple: false,
      disabled: false,
      placeholder: '请选择用户',
      style: {
        width: '100%'
      }
    }
  },
  {
    id: 'field_fa2w40',
    type: 'formItem',
    label: '请假天数',
    name: 'ElInputNumber',
    value: null,
    readonly: false,
    required: true,
    hidden: false,
    props: {
      disabled: false,
      placeholder: '请假天数',
      style: {
        width: '100%'
      },
      min: 0,
      max: 100,
      step: 1,
      precision: 0
    }
  },
  {
    id: 'field_d42t45',
    type: 'formItem',
    label: '请假事由',
    name: 'ElSelect',
    value: null,
    readonly: false,
    required: true,
    hidden: false,
    props: {
      disabled: false,
      multiple: false,
      placeholder: '请选择请假事由',
      options: [
        {
          label: '事假',
          value: '事假'
        },
        {
          label: '病假',
          value: '病假'
        },
        {
          label: '婚假',
          value: '婚假'
        },
        {
          label: '产假',
          value: '产假'
        },
        {
          label: '丧假',
          value: '丧假'
        },
        {
          label: '其他',
          value: '其他'
        }
      ],
      style: {
        width: '100%'
      }
    }
  },
  {
    id: 'field_522g58',
    type: 'formItem',
    label: '请假原因',
    name: 'ElInput',
    value: null,
    readonly: false,
    required: true,
    hidden: false,
    props: {
      type: 'textarea',
      placeholder: '请输入请假原因',
      autosize: {
        minRows: 3,
        maxRows: 3
      },
      disabled: false,
      style: {
        width: '100%'
      }
    }
  }
])
// 是否只读
const readOnly = ref(false)
// 是否暗黑模式
const isDark = ref(false)
const converterBpmn = () => {
  const processModel = {
    code: 'test',
    name: '测试',
    icon: {
      name: 'el:HomeFilled',
      color: '#409EFF'
    },
    process: process.value,
    enable: true,
    version: 1,
    sort: 0,
    groupId: '',
    remark: ''
  }
  downloadXml(processModel)
}
const handleToggleDark = () => {
  if (isDark.value) {
    document.documentElement.classList.add('dark')
  } else {
    document.documentElement.classList.remove('dark')
  }
}
const gitee = () => {
  window.open('https://gitee.com/cai_xiao_feng/lowflow-design')
}
const github = () => {
  window.open('https://github.com/tsai996/lowflow-design')
}
</script>

<template>
  <FlowDesign :process="process" :fields="fields" :readOnly="readOnly">
    <el-switch
      inline-prompt
      active-text="正常模式"
      inactive-text="暗黑模式"
      @change="handleToggleDark"
      v-model="isDark"
    />
    <el-switch
      v-model="readOnly"
      active-text="只读模式"
      inactive-text="编辑模式"
      inline-prompt
      :active-value="true"
      :inactive-value="false"
    />
    <el-button-group>
      <el-button @click="converterBpmn" type="primary" icon="Download"> 转bpmn </el-button>
      <!--开源地址-->
      <el-dropdown>
        <el-button type="primary">
          开源地址
          <el-icon class="el-icon--right">
            <arrow-down />
          </el-icon>
        </el-button>
        <template #dropdown>
          <el-dropdown-menu>
            <el-dropdown-item @click.stop="gitee">Gitee</el-dropdown-item>
            <el-dropdown-item @click.stop="github">Github</el-dropdown-item>
          </el-dropdown-menu>
        </template>
      </el-dropdown>
    </el-button-group>
  </FlowDesign>
</template>

<style scoped lang="scss"></style>


================================================
FILE: tsconfig.app.json
================================================
{
  "extends": "@vue/tsconfig/tsconfig.dom.json",
  "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
  "exclude": ["src/**/__tests__/*"],
  "compilerOptions": {
    "composite": true,
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",

    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}


================================================
FILE: tsconfig.json
================================================
{
  "files": [],
  "references": [
    {
      "path": "./tsconfig.node.json"
    },
    {
      "path": "./tsconfig.app.json"
    }
  ]
}


================================================
FILE: tsconfig.node.json
================================================
{
  "extends": "@tsconfig/node20/tsconfig.json",
  "include": [
    "vite.config.*",
    "vitest.config.*",
    "cypress.config.*",
    "nightwatch.conf.*",
    "playwright.config.*"
  ],
  "compilerOptions": {
    "composite": true,
    "noEmit": true,
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",

    "module": "ESNext",
    "moduleResolution": "Bundler",
    "types": ["node"]
  }
}


================================================
FILE: unocss.config.ts
================================================
import {
  defineConfig,
  presetAttributify,
  presetIcons,
  presetUno,
  transformerDirectives,
  transformerVariantGroup,
} from 'unocss'

export default defineConfig({
  presets: [
    presetUno({ dark: 'class' }),
    presetAttributify(),
    presetIcons({
      scale: 1.2,
      warn: true,
    }),
  ],
  // 自定义组合样式
  shortcuts: {
    bgc: 'flex red',
    'flex-col-center': 'flex-center flex-col',
    'flex-col': 'flex flex-col',
    'flex-items-center': 'flex items-center',
    'flex-center': 'flex-items-center justify-center',
    'flex-between': 'flex-items-center justify-between',
    'flex-space': 'flex-items-center flex-justify-between',
    'wh-full': 'w-full h-full',
  },
  transformers: [
    transformerDirectives(),
    transformerVariantGroup(),
  ]
})


================================================
FILE: vite.config.ts
================================================
import {fileURLToPath, URL} from 'node:url'

import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import VueSetupExtend from 'vite-plugin-vue-setup-extend'
import Components from 'unplugin-vue-components/vite'
import AutoImport from 'unplugin-auto-import/vite'
import {viteMockServe} from "vite-plugin-mock";
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import {ElementPlusResolver} from "unplugin-vue-components/resolvers";
import Unocss from 'unocss/vite'
import {resolve} from "path";

// https://vitejs.dev/config/
export default defineConfig({
    base: '/lowflow-design',
    resolve: {
        alias: {
            '@': fileURLToPath(new URL('./src', import.meta.url))
        }
    },
    server: {
        host: '0.0.0.0',
        port: 3200,
        open: true,
        proxy: {
            '/api': {
                target: 'http://localhost:8084',
                changeOrigin: true,
                ws: true,
                rewrite: (path) => path.replace(/^\/api/, ''),
                secure: false
            }
        }
    },
    plugins: [
        vue(),
        vueJsx(),
        Unocss(),
        VueSetupExtend(),
        viteMockServe({
            mockPath: './src/mock',
            localEnabled: true,
            prodEnabled: true,
            injectCode: ` import { setupProdMockServer } from './mockProdServer'; setupProdMockServer(); `,
        }),
        Components({
            extensions: ['vue', 'tsx', 'md'],
            globs: ['src/components/*/*.vue', 'src/components/*/*.tsx'],
            include: [/\.vue$/, /\.vue\?vue/, /\.md$/, /\.[tj]sx?$/],
            resolvers: [
                ElementPlusResolver({
                    importStyle: 'sass',
                }),
            ],
            dts: 'src/typings/components.d.ts'
        }),
        AutoImport({
            imports: ['vue', 'vue-router'],
            resolvers: [ElementPlusResolver()],
            dts: 'src/typings/auto-imports.d.ts',
            eslintrc: {
                enabled: true,
                filepath: './.eslintrc-auto-import.json'
            }
        }),
        // svg 图标
        createSvgIconsPlugin({
            iconDirs: [resolve(process.cwd(), 'src/assets/icons')],
            symbolId: 'icon-[dir]-[name]'
        }),
    ],
    build: {
        rollupOptions: {
            output: {
                // 打包分类
                chunkFileNames: 'assets/js/[name]-[hash].js',
                entryFileNames: 'assets/js/[name]-[hash].js',
                assetFileNames: 'assets/[ext]/[name]-[hash].[ext]',
                // 打包后的文件夹名称生成规则-->解决部分静态服务器无法正常返回_plugin-vue_export-helper文件
                sanitizeFileName(name) {
                    const match = /^[a-z]:/i.exec(name)
                    const driveLetter = match ? match[0] : ''
                    return (
                        driveLetter +
                        // eslint-disable-next-line no-control-regex
                        name.substring(driveLetter.length).replace(/[\x00-\x1F\x7F<>*#"{}|^[\]`;?:&=+$,]/g, '')
                    )
                }
            }
        }
    }
})
Download .txt
gitextract_5dzaix2j/

├── .eslintrc-auto-import.json
├── .eslintrc.cjs
├── .github/
│   └── workflows/
│       └── gh-pages.yml
├── .gitignore
├── .prettierrc.json
├── .vscode/
│   └── extensions.json
├── LICENSE
├── README.en.md
├── README.md
├── env.d.ts
├── index.html
├── package.json
├── public/
│   └── CNAME
├── src/
│   ├── App.vue
│   ├── api/
│   │   ├── index.ts
│   │   └── modules/
│   │       ├── model.ts
│   │       ├── role.ts
│   │       └── user.ts
│   ├── components/
│   │   ├── AdvancedFilter/
│   │   │   ├── Operator.vue
│   │   │   ├── Trigger.vue
│   │   │   ├── index.vue
│   │   │   └── type.ts
│   │   ├── Render/
│   │   │   ├── index.tsx
│   │   │   └── type.ts
│   │   ├── RoleSelector/
│   │   │   ├── RolePicker.vue
│   │   │   ├── RoleTag.vue
│   │   │   └── index.vue
│   │   ├── SvgIcon/
│   │   │   ├── index.scss
│   │   │   └── index.tsx
│   │   └── UserSelector/
│   │       ├── UserPicker.vue
│   │       ├── UserTag.vue
│   │       └── index.vue
│   ├── hooks/
│   │   └── useDraggableScroll.ts
│   ├── main.ts
│   ├── mock/
│   │   ├── index.ts
│   │   ├── role.ts
│   │   └── user.ts
│   ├── mockProdServer.ts
│   ├── router/
│   │   └── index.ts
│   ├── stores/
│   │   └── counter.ts
│   ├── styles/
│   │   ├── el-segmented.scss
│   │   ├── element/
│   │   │   ├── dark.scss
│   │   │   └── index.scss
│   │   └── index.scss
│   ├── typings/
│   │   ├── auto-imports.d.ts
│   │   ├── components.d.ts
│   │   └── index.d.ts
│   └── views/
│       ├── flowDesign/
│       │   ├── index.vue
│       │   ├── nodes/
│       │   │   ├── Add.vue
│       │   │   ├── ApprovalNode.vue
│       │   │   ├── CcNode.vue
│       │   │   ├── ConditionNode.vue
│       │   │   ├── EndNode.vue
│       │   │   ├── ExclusiveNode.vue
│       │   │   ├── GatewayNode.vue
│       │   │   ├── Node.vue
│       │   │   ├── NotifyNode.vue
│       │   │   ├── ServiceNode.vue
│       │   │   ├── StartNode.vue
│       │   │   ├── TimerNode.vue
│       │   │   ├── TreeNode.vue
│       │   │   └── type.ts
│       │   └── panels/
│       │       ├── ApprovalPanel.vue
│       │       ├── AssigneePanel.vue
│       │       ├── CcPanel.vue
│       │       ├── ConditionPanel.vue
│       │       ├── EndPanel.vue
│       │       ├── ExecutionListeners.vue
│       │       ├── NotifyPanel.vue
│       │       ├── ServicePanel.vue
│       │       ├── StartPanel.vue
│       │       ├── TaskListeners.vue
│       │       ├── TimerPanel.vue
│       │       └── index.vue
│       └── home/
│           └── index.vue
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
├── unocss.config.ts
└── vite.config.ts
Download .txt
SYMBOL INDEX (42 symbols across 15 files)

FILE: env.d.ts
  type ImportMetaEnv (line 3) | interface ImportMetaEnv {
  type ImportMeta (line 8) | interface ImportMeta {

FILE: src/api/index.ts
  type Result (line 10) | interface Result {
  type ResultData (line 16) | interface ResultData<T = any> extends Result {
  class RequestHttp (line 28) | class RequestHttp {
    method constructor (line 35) | public constructor(config: AxiosRequestConfig) {
    method get (line 66) | get<T>(url: string, params?: object, config = {}): Promise<ResultData<...
    method post (line 76) | post<T>(url: string, data?: object, config = {}): Promise<ResultData<T...
    method request (line 84) | request<T>(config: AxiosRequestConfig): Promise<ResultData<T>> {
    method download (line 94) | download(url: string, data?: object, config = {}): Promise<BlobPart> {

FILE: src/api/modules/role.ts
  type Role (line 3) | interface Role {

FILE: src/api/modules/user.ts
  type User (line 3) | interface User {

FILE: src/components/AdvancedFilter/type.ts
  type Condition (line 4) | interface Condition {
  type FilterRules (line 16) | interface FilterRules {

FILE: src/components/Render/index.tsx
  method setup (line 33) | setup(props, { emit }) {
  method render (line 91) | render() {

FILE: src/components/Render/type.ts
  type Field (line 1) | interface Field {

FILE: src/components/SvgIcon/index.tsx
  method setup (line 25) | setup(props) {
  method render (line 47) | render() {

FILE: src/hooks/useDraggableScroll.ts
  function useDraggableScroll (line 3) | function useDraggableScroll(containerRef: Ref<HTMLElement | null>) {

FILE: src/mockProdServer.ts
  function setupProdMockServer (line 4) | function setupProdMockServer() {

FILE: src/stores/counter.ts
  function increment (line 7) | function increment() {

FILE: src/typings/components.d.ts
  type GlobalComponents (line 9) | interface GlobalComponents {

FILE: src/typings/index.d.ts
  type Recordable (line 1) | type Recordable<T = any> = Record<string, T>

FILE: src/views/flowDesign/nodes/type.ts
  type NodeType (line 3) | type NodeType =
  type FlowNode (line 14) | interface FlowNode {
  type NodeListener (line 23) | interface NodeListener {
  type StartNode (line 29) | interface StartNode extends FlowNode {
  type EndNode (line 33) | interface EndNode extends FlowNode {}
  type AssigneeNode (line 35) | interface AssigneeNode extends FlowNode {
  type CcNode (line 65) | interface CcNode extends AssigneeNode {
  type NotifyNode (line 69) | interface NotifyNode extends AssigneeNode {
  type ApprovalNode (line 75) | interface ApprovalNode extends AssigneeNode {
  type ServiceNode (line 92) | interface ServiceNode extends FlowNode {
  type TimerNode (line 97) | interface TimerNode extends FlowNode {
  type ConditionNode (line 104) | interface ConditionNode extends FlowNode {
  type BranchNode (line 109) | interface BranchNode extends FlowNode {
  type ExclusiveNode (line 113) | interface ExclusiveNode extends BranchNode {
  type ErrorInfo (line 117) | interface ErrorInfo {
  type FormProperty (line 123) | interface FormProperty {
  type OperationPermissions (line 136) | interface OperationPermissions {

FILE: vite.config.ts
  method sanitizeFileName (line 82) | sanitizeFileName(name) {
Condensed preview — 80 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (172K chars).
[
  {
    "path": ".eslintrc-auto-import.json",
    "chars": 1870,
    "preview": "{\n  \"globals\": {\n    \"Component\": true,\n    \"ComponentPublicInstance\": true,\n    \"ComputedRef\": true,\n    \"EffectScope\":"
  },
  {
    "path": ".eslintrc.cjs",
    "chars": 468,
    "preview": "/* eslint-env node */\nrequire('@rushstack/eslint-patch/modern-module-resolution')\n\nmodule.exports = {\n  root: true,\n  'e"
  },
  {
    "path": ".github/workflows/gh-pages.yml",
    "chars": 982,
    "preview": "name: GitHub Pages\n# 触发脚本的条件,develop分支push代码的时候\non:\n  push:\n    branches:\n      - main\n# 要执行的任务\njobs:\n  # 任务名称\n  build_a"
  },
  {
    "path": ".gitignore",
    "chars": 317,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\n.DS_Stor"
  },
  {
    "path": ".prettierrc.json",
    "chars": 163,
    "preview": "{\n  \"$schema\": \"https://json.schemastore.org/prettierrc\",\n  \"semi\": false,\n  \"tabWidth\": 2,\n  \"singleQuote\": true,\n  \"pr"
  },
  {
    "path": ".vscode/extensions.json",
    "chars": 107,
    "preview": "{\n  \"recommendations\": [\n    \"Vue.volar\",\n    \"dbaeumer.vscode-eslint\",\n    \"esbenp.prettier-vscode\"\n  ]\n}\n"
  },
  {
    "path": "LICENSE",
    "chars": 1068,
    "preview": "MIT License\n\nCopyright (c) 2023 Victor Tsai\n\nPermission is hereby granted, free of charge, to any person obtaining a cop"
  },
  {
    "path": "README.en.md",
    "chars": 3703,
    "preview": "<div align=\"center\">\n  <h1>lowflow-design</h1>\n  <p>Low-code Workflow Designer</p>\n  <p>\n    <a href=\"./README.md\">中文</a"
  },
  {
    "path": "README.md",
    "chars": 2780,
    "preview": "<div align=\"center\">\n  <h1>lowflow-design</h1>\n  <p>低代码流程设计器</p>\n  <p>\n    <a href=\"./README.md\">中文</a> | <a href=\"./REA"
  },
  {
    "path": "env.d.ts",
    "chars": 199,
    "preview": "/// <reference types=\"vite/client\" />\n\ninterface ImportMetaEnv {\n    readonly VITE_API_URL: string;\n    readonly VITE_PU"
  },
  {
    "path": "index.html",
    "chars": 331,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\">\n    <link rel=\"icon\" href=\"/favicon.ico\">\n    <meta"
  },
  {
    "path": "package.json",
    "chars": 1679,
    "preview": "{\n  \"name\": \"lowflow-design\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vi"
  },
  {
    "path": "public/CNAME",
    "chars": 30,
    "preview": "vite-starter.element-plus.org\n"
  },
  {
    "path": "src/App.vue",
    "chars": 642,
    "preview": "<script setup lang=\"ts\">\nimport zhCn from 'element-plus/es/locale/lang/zh-cn'\nimport { ElNotification } from 'element-pl"
  },
  {
    "path": "src/api/index.ts",
    "chars": 2118,
    "preview": "import axios, {\n  type AxiosInstance,\n  AxiosError,\n  type AxiosRequestConfig,\n  type InternalAxiosRequestConfig,\n  type"
  },
  {
    "path": "src/api/modules/model.ts",
    "chars": 322,
    "preview": "import http from '@/api'\nimport FileSaver from 'file-saver'\n\nexport const downloadXml = async (data: object) => {\n  cons"
  },
  {
    "path": "src/api/modules/role.ts",
    "chars": 383,
    "preview": "import http from '@/api/index'\n\nexport interface Role {\n  id: string\n  name: string\n}\n\n/**\n * 获取角色信息\n * @param id\n */\nex"
  },
  {
    "path": "src/api/modules/user.ts",
    "chars": 449,
    "preview": "import http from '@/api/index'\n\nexport interface User {\n  id: string\n  username: string\n  name: string\n  avatar: string\n"
  },
  {
    "path": "src/components/AdvancedFilter/Operator.vue",
    "chars": 820,
    "preview": "<script setup lang=\"ts\">\nimport { useVModel } from '@vueuse/core'\n\nconst $props = defineProps<{\n  modelValue: string\n}>("
  },
  {
    "path": "src/components/AdvancedFilter/Trigger.vue",
    "chars": 698,
    "preview": "<script setup lang=\"ts\">\nimport type { Field } from '@/components/Render/type'\nimport type { FilterRules } from '@/compo"
  },
  {
    "path": "src/components/AdvancedFilter/index.vue",
    "chars": 6029,
    "preview": "<script setup lang=\"ts\" name=\"AdvancedFilter\">\nimport type { FilterRules } from './type'\nimport type { Field } from '@/c"
  },
  {
    "path": "src/components/AdvancedFilter/type.ts",
    "chars": 266,
    "preview": "/**\n * 字段筛选结果\n */\nexport interface Condition {\n  // 筛选字段\n  field: string | null\n  // 条件运算符\n  operator: string\n  // 筛选值\n "
  },
  {
    "path": "src/components/Render/index.tsx",
    "chars": 3245,
    "preview": "import { cloneDeep } from 'lodash-es'\nimport type { Field } from './type'\nimport type { PropType } from 'vue'\n\nexport de"
  },
  {
    "path": "src/components/Render/type.ts",
    "chars": 218,
    "preview": "export interface Field {\n  id: string\n  type: 'formItem' | 'container'\n  label: string\n  name: string\n  value: any\n  rea"
  },
  {
    "path": "src/components/RoleSelector/RolePicker.vue",
    "chars": 5204,
    "preview": "<script setup lang=\"ts\">\nimport { useVModel } from '@vueuse/core'\nimport type { TreeNodeData } from 'element-plus/es/com"
  },
  {
    "path": "src/components/RoleSelector/RoleTag.vue",
    "chars": 1132,
    "preview": "<script setup lang=\"ts\">\nimport { getById } from '@/api/modules/role'\n\nexport interface RoleTagProps {\n  id: string\n  ty"
  },
  {
    "path": "src/components/RoleSelector/index.vue",
    "chars": 2300,
    "preview": "<script setup lang=\"ts\">\nimport { useVModel } from '@vueuse/core'\nimport RoleTag from './RoleTag.vue'\nimport RolePicker,"
  },
  {
    "path": "src/components/SvgIcon/index.scss",
    "chars": 112,
    "preview": ".svg-icon {\n  width: 1em;\n  height: 1em;\n  vertical-align: -0.15em;\n  fill: currentColor;\n  overflow: hidden;\n}\n"
  },
  {
    "path": "src/components/SvgIcon/index.tsx",
    "chars": 1583,
    "preview": "import './index.scss'\nimport type { CSSProperties, PropType } from 'vue'\n\nexport default defineComponent({\n  name: 'SvgI"
  },
  {
    "path": "src/components/UserSelector/UserPicker.vue",
    "chars": 5613,
    "preview": "<script setup lang=\"ts\">\nimport { useVModel } from '@vueuse/core'\nimport type { TreeNodeData } from 'element-plus/es/com"
  },
  {
    "path": "src/components/UserSelector/UserTag.vue",
    "chars": 1617,
    "preview": "<script setup lang=\"ts\">\nimport { getByUsername } from '@/api/modules/user'\nimport { componentSizeMap, useFormSize } fro"
  },
  {
    "path": "src/components/UserSelector/index.vue",
    "chars": 2290,
    "preview": "<script setup lang=\"ts\">\nimport { useVModel } from '@vueuse/core'\nimport UserTag from './UserTag.vue'\nimport UserPicker,"
  },
  {
    "path": "src/hooks/useDraggableScroll.ts",
    "chars": 1299,
    "preview": "import { ref, onMounted, onBeforeUnmount, type Ref } from 'vue'\n\nexport function useDraggableScroll(containerRef: Ref<HT"
  },
  {
    "path": "src/main.ts",
    "chars": 634,
    "preview": "import { createApp } from 'vue'\nimport { createPinia } from 'pinia'\nimport App from './App.vue'\nimport router from './ro"
  },
  {
    "path": "src/mock/index.ts",
    "chars": 184,
    "preview": "import user from './user'\nimport role from './role'\nimport type { MockMethod } from 'vite-plugin-mock'\n\nconst mockModule"
  },
  {
    "path": "src/mock/role.ts",
    "chars": 1119,
    "preview": "import type { MockMethod } from 'vite-plugin-mock'\nimport type { ResultData } from '@/api'\n\nconst roleList = [\n  {\n    i"
  },
  {
    "path": "src/mock/user.ts",
    "chars": 1847,
    "preview": "import type { MockMethod } from 'vite-plugin-mock'\nimport type { ResultData } from '@/api'\n\nconst userList = [\n  {\n    i"
  },
  {
    "path": "src/mockProdServer.ts",
    "chars": 178,
    "preview": "import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'\nimport mock from './mock'\n\nexport functi"
  },
  {
    "path": "src/router/index.ts",
    "chars": 301,
    "preview": "import { createRouter, createWebHistory } from 'vue-router'\nimport Home from '@/views/home/index.vue'\n\nconst router = cr"
  },
  {
    "path": "src/stores/counter.ts",
    "chars": 306,
    "preview": "import { ref, computed } from 'vue'\nimport { defineStore } from 'pinia'\n\nexport const useCounterStore = defineStore('cou"
  },
  {
    "path": "src/styles/el-segmented.scss",
    "chars": 2460,
    "preview": ".el-segmented {\n  --el-segmented-radius: var(--el-border-radius-base);\n  --el-segmented-padding: 3px;\n  --el-segmented-b"
  },
  {
    "path": "src/styles/element/dark.scss",
    "chars": 165,
    "preview": "// only scss variables\n\n$--colors: (\n  'primary': (\n    'base': #589ef8\n  )\n);\n\n@forward 'element-plus/theme-chalk/src/d"
  },
  {
    "path": "src/styles/element/index.scss",
    "chars": 891,
    "preview": "$--colors: (\n  'primary': (\n    'base': #589ef8\n  ),\n  'success': (\n    'base': #21ba45\n  ),\n  'warning': (\n    'base': "
  },
  {
    "path": "src/styles/index.scss",
    "chars": 2009,
    "preview": "// import dark theme\n@use 'element-plus/theme-chalk/src/dark/css-vars.scss' as *;\n\n:root {\n  .el-segmented {\n    --el-se"
  },
  {
    "path": "src/typings/auto-imports.d.ts",
    "chars": 4242,
    "preview": "/* eslint-disable */\n/* prettier-ignore */\n// @ts-nocheck\n// noinspection JSUnusedGlobalSymbols\n// Generated by unplugin"
  },
  {
    "path": "src/typings/components.d.ts",
    "chars": 3709,
    "preview": "/* eslint-disable */\n// @ts-nocheck\n// Generated by unplugin-vue-components\n// Read more: https://github.com/vuejs/core/"
  },
  {
    "path": "src/typings/index.d.ts",
    "chars": 45,
    "preview": "type Recordable<T = any> = Record<string, T>\n"
  },
  {
    "path": "src/views/flowDesign/index.vue",
    "chars": 9066,
    "preview": "<script setup lang=\"ts\">\nimport TreeNode from './nodes/TreeNode.vue'\nimport Panel from './panels/index.vue'\nimport type "
  },
  {
    "path": "src/views/flowDesign/nodes/Add.vue",
    "chars": 3433,
    "preview": "<script setup lang=\"ts\">\nimport type { PopoverInstance } from 'element-plus'\nimport type { NodeType } from './type'\nimpo"
  },
  {
    "path": "src/views/flowDesign/nodes/ApprovalNode.vue",
    "chars": 3098,
    "preview": "<script setup lang=\"ts\">\nimport Node from './Node.vue'\nimport type { ApprovalNode } from './type'\nimport type { Ref } fr"
  },
  {
    "path": "src/views/flowDesign/nodes/CcNode.vue",
    "chars": 2732,
    "preview": "<script setup lang=\"ts\">\nimport Node from './Node.vue'\nimport type { CcNode } from './type'\nimport type { Ref } from 'vu"
  },
  {
    "path": "src/views/flowDesign/nodes/ConditionNode.vue",
    "chars": 1383,
    "preview": "<script setup lang=\"ts\">\nimport type { ConditionNode } from './type'\nimport type { Ref } from 'vue'\nimport type { ErrorI"
  },
  {
    "path": "src/views/flowDesign/nodes/EndNode.vue",
    "chars": 1366,
    "preview": "<script setup lang=\"ts\">\nimport type { FlowNode } from '@/views/flowDesign/nodes/type'\nimport type { Ref } from 'vue'\n\nc"
  },
  {
    "path": "src/views/flowDesign/nodes/ExclusiveNode.vue",
    "chars": 485,
    "preview": "<script setup lang=\"ts\">\nimport type { ExclusiveNode } from './type'\nimport GatewayNode from './GatewayNode.vue'\n\ndefine"
  },
  {
    "path": "src/views/flowDesign/nodes/GatewayNode.vue",
    "chars": 3884,
    "preview": "<script setup lang=\"ts\">\nimport TreeNode from './TreeNode.vue'\nimport type { BranchNode, FlowNode, NodeType } from './ty"
  },
  {
    "path": "src/views/flowDesign/nodes/Node.vue",
    "chars": 5448,
    "preview": "<script setup lang=\"ts\">\nimport type { ErrorInfo, FlowNode, NodeType } from './type'\nimport { ClickOutside as vClickOuts"
  },
  {
    "path": "src/views/flowDesign/nodes/NotifyNode.vue",
    "chars": 3059,
    "preview": "<script setup lang=\"ts\">\nimport Node from './Node.vue'\nimport type { NotifyNode } from './type'\nimport type { Ref } from"
  },
  {
    "path": "src/views/flowDesign/nodes/ServiceNode.vue",
    "chars": 1050,
    "preview": "<script setup lang=\"ts\">\nimport Node from './Node.vue'\nimport type { ErrorInfo, ServiceNode } from './type'\nimport type "
  },
  {
    "path": "src/views/flowDesign/nodes/StartNode.vue",
    "chars": 980,
    "preview": "<script setup lang=\"ts\">\nimport Node from './Node.vue'\nimport type { StartNode } from './type'\nimport type { ErrorInfo }"
  },
  {
    "path": "src/views/flowDesign/nodes/TimerNode.vue",
    "chars": 1277,
    "preview": "<script setup lang=\"ts\">\nimport Node from './Node.vue'\nimport type { TimerNode } from './type'\nimport type { Ref } from "
  },
  {
    "path": "src/views/flowDesign/nodes/TreeNode.vue",
    "chars": 1059,
    "preview": "<script setup lang=\"ts\" name=\"TreeNode\">\nimport type { FlowNode } from './type'\nimport type { Component } from 'vue'\nimp"
  },
  {
    "path": "src/views/flowDesign/nodes/type.ts",
    "chars": 2774,
    "preview": "import type { FilterRules } from '@/components/AdvancedFilter/type'\n\nexport type NodeType =\n  | 'start'\n  | 'approval'\n "
  },
  {
    "path": "src/views/flowDesign/panels/ApprovalPanel.vue",
    "chars": 9745,
    "preview": "<script setup lang=\"ts\">\nimport type { ApprovalNode, FormProperty } from '../nodes/type'\nimport type { Field } from '@/c"
  },
  {
    "path": "src/views/flowDesign/panels/AssigneePanel.vue",
    "chars": 4731,
    "preview": "<script setup lang=\"ts\">\nimport type { AssigneeNode } from '../nodes/type'\nimport type { Field } from '@/components/Rend"
  },
  {
    "path": "src/views/flowDesign/panels/CcPanel.vue",
    "chars": 2947,
    "preview": "<script setup lang=\"ts\">\nimport type { CcNode } from '../nodes/type'\nimport type { Ref } from 'vue'\nimport type { Field "
  },
  {
    "path": "src/views/flowDesign/panels/ConditionPanel.vue",
    "chars": 851,
    "preview": "<script setup lang=\"ts\">\nimport type { ConditionNode } from '../nodes/type'\nimport type { Ref } from 'vue'\nimport type {"
  },
  {
    "path": "src/views/flowDesign/panels/EndPanel.vue",
    "chars": 432,
    "preview": "<script setup lang=\"ts\">\nimport type { EndNode } from '../nodes/type'\nimport ExecutionListeners from './ExecutionListene"
  },
  {
    "path": "src/views/flowDesign/panels/ExecutionListeners.vue",
    "chars": 3398,
    "preview": "<script setup lang=\"ts\">\nimport type { FlowNode } from '@/views/flowDesign/nodes/type'\n\nconst props = defineProps<{\n  no"
  },
  {
    "path": "src/views/flowDesign/panels/NotifyPanel.vue",
    "chars": 2096,
    "preview": "<script setup lang=\"ts\">\nimport type { NotifyNode } from '../nodes/type'\nimport type { Ref } from 'vue'\nimport type { Fi"
  },
  {
    "path": "src/views/flowDesign/panels/ServicePanel.vue",
    "chars": 1280,
    "preview": "<script setup lang=\"ts\">\nimport type { ServiceNode } from '../nodes/type'\n\ndefineProps<{\n  activeData: ServiceNode\n}>()\n"
  },
  {
    "path": "src/views/flowDesign/panels/StartPanel.vue",
    "chars": 3844,
    "preview": "<script setup lang=\"ts\">\nimport type { FormProperty, StartNode } from '../nodes/type'\nimport type { Field } from '@/comp"
  },
  {
    "path": "src/views/flowDesign/panels/TaskListeners.vue",
    "chars": 3386,
    "preview": "<script setup lang=\"ts\">\nimport type { ApprovalNode } from '@/views/flowDesign/nodes/type'\n\nconst props = defineProps<{\n"
  },
  {
    "path": "src/views/flowDesign/panels/TimerPanel.vue",
    "chars": 1641,
    "preview": "<script setup lang=\"ts\">\nimport type { TimerNode } from '../nodes/type'\ndefineProps<{\n  activeData: TimerNode\n}>()\n</scr"
  },
  {
    "path": "src/views/flowDesign/panels/index.vue",
    "chars": 1627,
    "preview": "<script setup lang=\"ts\">\nimport { ClickOutside as vClickOutside } from 'element-plus'\nimport type { Component } from 'vu"
  },
  {
    "path": "src/views/home/index.vue",
    "chars": 4221,
    "preview": "<script setup lang=\"ts\">\nimport FlowDesign from '@/views/flowDesign/index.vue'\nimport type { Field } from '@/components/"
  },
  {
    "path": "tsconfig.app.json",
    "chars": 332,
    "preview": "{\n  \"extends\": \"@vue/tsconfig/tsconfig.dom.json\",\n  \"include\": [\"env.d.ts\", \"src/**/*\", \"src/**/*.vue\"],\n  \"exclude\": [\""
  },
  {
    "path": "tsconfig.json",
    "chars": 139,
    "preview": "{\n  \"files\": [],\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.node.json\"\n    },\n    {\n      \"path\": \"./tsconfig.app"
  },
  {
    "path": "tsconfig.node.json",
    "chars": 414,
    "preview": "{\n  \"extends\": \"@tsconfig/node20/tsconfig.json\",\n  \"include\": [\n    \"vite.config.*\",\n    \"vitest.config.*\",\n    \"cypress"
  },
  {
    "path": "unocss.config.ts",
    "chars": 781,
    "preview": "import {\n  defineConfig,\n  presetAttributify,\n  presetIcons,\n  presetUno,\n  transformerDirectives,\n  transformerVariantG"
  },
  {
    "path": "vite.config.ts",
    "chars": 3180,
    "preview": "import {fileURLToPath, URL} from 'node:url'\n\nimport {defineConfig} from 'vite'\nimport vue from '@vitejs/plugin-vue'\nimpo"
  }
]

About this extraction

This page contains the full source code of the tsai996/lowflow-design GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 80 files (150.7 KB), approximately 45.1k tokens, and a symbol index with 42 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!