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
================================================
lowflow-design
Low-code Workflow Designer
中文 | English
## 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:
- Full demo:
## Screenshots
## 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 | | |
| Gitee | | |
## Community and Support
### Community Groups
### Sponsor
If this project helps you, sponsorship is welcome.
## Recommended Reading
*In-depth Flowable Workflow Engine: Core Principles and Advanced Practice*:

================================================
FILE: README.md
================================================
## 项目简介
`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)
## 在线体验
- 预览地址:
- 成品示例:
## 效果预览
## 功能特性
- 审批节点:支持单人、多人、角色、部门、发起人、上级领导、自定义审批人等。
- 抄送节点:支持单人、多人、角色、部门、发起人、上级领导、自定义抄送人等。
- 条件分支:支持条件组及组合逻辑。
- 计时等待:支持秒、分、时、天、周、月及自定义时长。
- 消息通知:支持站内信、邮件、企业微信、钉钉、飞书、短信等通知方式。
## 技术栈
- 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 | | |
| Gitee | | |
## 交流与支持
### 交流群
### 赞助
如果这个项目对你有帮助,欢迎赞助支持。
## 推荐阅读
推荐搭配阅读《深入 Flowable 流程引擎:核心原理与高阶实战》:

================================================
FILE: env.d.ts
================================================
///
interface ImportMetaEnv {
readonly VITE_API_URL: string;
readonly VITE_PUBLIC_PATH: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
================================================
FILE: index.html
================================================
Vite App
================================================
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
================================================
================================================
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 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(url: string, params?: object, config = {}): Promise> {
return this.service.get(url, { params, ...config })
}
/**
* post请求
* @param url
* @param data
* @param config
*/
post(url: string, data?: object, config = {}): Promise> {
return this.service.post(url, data, config)
}
/**
* request请求
* @param config
*/
request(config: AxiosRequestConfig): Promise> {
return this.service.request(config)
}
/**
* 下载文件
* @param url
* @param data
* @param config
*/
download(url: string, data?: object, config = {}): Promise {
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/info`, { id: id })
}
/**
* 查询角色列表
*/
export const getList = (roleIds?: string[]) => {
const params = roleIds ? { roleIds: roleIds } : {}
return http.post('/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/info`, { username: username })
}
/**
* 查询用户列表
*/
export const getList = (userIds?: string[]) => {
const params = userIds ? { userIds: userIds } : {}
return http.post('/user/list', params)
}
================================================
FILE: src/components/AdvancedFilter/Operator.vue
================================================
================================================
FILE: src/components/AdvancedFilter/Trigger.vue
================================================
================================================
FILE: src/components/AdvancedFilter/index.vue
================================================
================================================
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,
default: undefined,
required: false
},
field: {
type: Object as PropType,
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 = {}
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 = {}
const slotFunctions: Record = {
ElSelect: (conf: Field) => {
return conf.props.options.map((item: any) => {
return
})
},
ElRadio: (conf: Field) => {
return conf.props.options.map((item: any) => {
return {item.label}
})
},
ElCheckbox: (conf: Field) => {
return conf.props.options.map((item: any) => {
return {item.label}
})
}
}
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
================================================
取消
确认
================================================
FILE: src/components/RoleSelector/RoleTag.vue
================================================
{{ roleInfo.name || id }}
================================================
FILE: src/components/RoleSelector/index.vue
================================================
================================================
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,
required: true
},
prefix: {
type: String as PropType,
default: 'icon'
},
color: {
type: String as PropType
},
size: {
type: Number as PropType
},
className: {
type: String as PropType
}
},
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(() => {
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 (
{h(resolveComponent(this.name.slice(3)))}
)
} else {
return (
)
}
}
return null
}
})
================================================
FILE: src/components/UserSelector/UserPicker.vue
================================================
{{ data.name.charAt(0) }}
{{ data.name }}
取消
确认
================================================
FILE: src/components/UserSelector/UserTag.vue
================================================
{{ (userInfo.name || username).charAt(0) }}
{{ userInfo.name || username }}
================================================
FILE: src/components/UserSelector/index.vue
================================================
{{ placeholder }}
================================================
FILE: src/hooks/useDraggableScroll.ts
================================================
import { ref, onMounted, onBeforeUnmount, type Ref } from 'vue'
export function useDraggableScroll(containerRef: Ref) {
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 = Record
================================================
FILE: src/views/flowDesign/index.vue
================================================
================================================
FILE: src/views/flowDesign/nodes/Add.vue
================================================
审批人
抄送人
互斥分支
计时等待
消息通知
服务节点
================================================
FILE: src/views/flowDesign/nodes/ApprovalNode.vue
================================================
{{ content }}
================================================
FILE: src/views/flowDesign/nodes/CcNode.vue
================================================
{{ content }}
================================================
FILE: src/views/flowDesign/nodes/ConditionNode.vue
================================================
{{ content }}
================================================
FILE: src/views/flowDesign/nodes/EndNode.vue
================================================
================================================
FILE: src/views/flowDesign/nodes/ExclusiveNode.vue
================================================
添加条件
================================================
FILE: src/views/flowDesign/nodes/GatewayNode.vue
================================================
================================================
FILE: src/views/flowDesign/nodes/Node.vue
================================================
================================================
FILE: src/views/flowDesign/nodes/NotifyNode.vue
================================================
{{ content }}
================================================
FILE: src/views/flowDesign/nodes/ServiceNode.vue
================================================
{{ content }}
================================================
FILE: src/views/flowDesign/nodes/StartNode.vue
================================================
发起人
================================================
FILE: src/views/flowDesign/nodes/TimerNode.vue
================================================
{{ content }}
================================================
FILE: src/views/flowDesign/nodes/TreeNode.vue
================================================
================================================
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
================================================
自动拒绝
依次审批(按顺序审批)
会签(需要所有审批人都通过)
或签(其中一名审批人通过即可)
需要 %人员通过
自动通过
指定人员
自动拒绝
转交流程管理员
拒绝
当拒绝任务时,当前任务被终止,并结束整个流程
退回
若审批内容存在问题,当前任务将中止并回退至特定历史任务节点
转交
将当前任务移交给其他人处理,以便他们继续执行所需的操作
委派
将当前任务暂时交由他人处理,待其完成后再交回自己处理
加签
在当前任务上额外添加新人员,以处理相关事项或提供必要的审批或意见
减签
在当前任务中减少处理人员数量,以简化流程或重新分配责任
================================================
FILE: src/views/flowDesign/panels/AssigneePanel.vue
================================================
指定人员
指定角色
发起人自选
发起人自己
直属上级
组织主管
表单内人员
表单内角色
单选
多选
================================================
FILE: src/views/flowDesign/panels/CcPanel.vue
================================================
================================================
FILE: src/views/flowDesign/panels/ConditionPanel.vue
================================================
================================================
FILE: src/views/flowDesign/panels/EndPanel.vue
================================================
================================================
FILE: src/views/flowDesign/panels/ExecutionListeners.vue
================================================
配置
添加监听器
监听器
实现 ExecutionListener 接口
委托表达式:${myExecutionListener}
表达式: ${myExecutionListener.notify(execution)}
java类:${com.example.listener.MyExecutionListener}
================================================
FILE: src/views/flowDesign/panels/NotifyPanel.vue
================================================
消息主题
消息内容
================================================
FILE: src/views/flowDesign/panels/ServicePanel.vue
================================================
执行值
实现 JavaDelegate 接口
类:${com.example.delegate.MyServiceDelegate}
表达式: ${myServiceDelegate.execute(execution)}
委托表达式:${myServiceDelegate}
================================================
FILE: src/views/flowDesign/panels/StartPanel.vue
================================================
================================================
FILE: src/views/flowDesign/panels/TaskListeners.vue
================================================
配置
添加监听器
监听器
委托表达式:${myCreateTaskListener}
表达式: ${myCreateTaskListener.notify(execution)}
java类:${com.example.listener.MyCreateTaskListener}
================================================
FILE: src/views/flowDesign/panels/TimerPanel.vue
================================================
================================================
FILE: src/views/flowDesign/panels/index.vue
================================================
{{ activeData?.name || '节点配置' }}
================================================
FILE: src/views/home/index.vue
================================================
转bpmn
开源地址
Gitee
Github
================================================
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, '')
)
}
}
}
}
})