重构版

This commit is contained in:
caixiaofeng 2024-05-07 22:35:44 +08:00
parent edc39d8b42
commit 70bac4ac8c
134 changed files with 9058 additions and 5416 deletions

6
.env
View File

@ -1,8 +1,2 @@
# title
VITE_GLOB_APP_TITLE=lowflow-design
# 本地运行端口号
VITE_PORT=8848
# 启动时自动打开浏览器
VITE_OPEN=true

View File

@ -1,6 +1,3 @@
# 本地环境
VITE_USER_NODE_ENV=development
# 公共基础路径
VITE_PUBLIC_PATH=/

View File

@ -1,6 +1,3 @@
# 本地环境
VITE_USER_NODE_ENV=development
# 公共基础路径
VITE_PUBLIC_PATH=/

View File

@ -0,0 +1,78 @@
{
"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
}
}

21
.eslintrc.cjs Normal file
View File

@ -0,0 +1,21 @@
/* 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'
}
}

27
.gitignore vendored
View File

@ -1,13 +1,30 @@
.vite-ssg-temp
# 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
# lock
yarn.lock
package-lock.json
/cypress/videos/
/cypress/screenshots/
*.log
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

2
.npmrc
View File

@ -1,2 +0,0 @@
shamefully-hoist=true
strict-peer-dependencies=false

8
.prettierrc.json Normal file
View File

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"tabWidth": 2,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none"
}

View File

@ -1,4 +1,7 @@
{
"recommendations": ["Vue.volar"]
"recommendations": [
"Vue.volar",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}

21
LICENSE
View File

@ -1,21 +0,0 @@
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.

View File

@ -5,7 +5,8 @@
## 介绍
lowflow-design是一个基于`Vue3``Vite``TypeScript``Element-Plus`等技术栈开发的,适用于低代码或无代码开发平台的流程设计器。
让普通人也能通过简单配置快速搭建流程。
让普通人也能通过简单配置快速搭建流程。 <br />
并提供了将json转xml的后端代码[lowflow-design-converter](https://github.com/tsai996/lowflow-design-converter)。
## 在线预览
https://tsai996.github.io/lowflow-design/
### 成品案例
@ -23,8 +24,9 @@ https://www.666cxf.com/
## 扫码加入群聊,如果失效加微信拉入群聊(备注:加群)
<p>
<img alt="微信群" src="public/wxq.png" width="240" style="display: inline-block"/>
<img alt="微信" src="public/wx.png" width="240" style="display: inline-block"/>
<img alt="QQ群" src="public/qq_qun.jpg" width="240" height="400" style="display: inline-block"/>
<img alt="微信群" src="public/wx_qun.jpg" width="240" height="400" style="display: inline-block"/>
<img alt="微信" src="public/wx.jpg" width="240" style="display: inline-block"/>
</p>
## 赞助

10
env.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string;
readonly VITE_PUBLIC_PATH: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

View File

@ -1,23 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>lowflow-design</title>
<!-- element css cdn, if you use custom theme, remove it. -->
<!-- <link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/element-plus/dist/index.css"
/> -->
<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>
<script>
window.setInterval(function(){
new Function('debugger')();
}, 100);
</script>
</body>
</html>

View File

@ -1,11 +0,0 @@
import user from "./user";
import role from "./role";
const mockModules = [
...user,
...role,
];
export default mockModules

View File

@ -1,68 +0,0 @@
import {MockMethod} from "vite-plugin-mock";
import {ResultData} from "../src/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

View File

@ -1,84 +0,0 @@
import {MockMethod} from "vite-plugin-mock";
import {ResultData} from "../src/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

View File

@ -1,38 +1,56 @@
{
"name": "lowflow-design",
"version": "0.0.0",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"build:dev": "vite build --mode development",
"build:test": "vite build --mode test",
"mock": "vite --mode mock --force",
"generate": "vite-ssg build",
"preview": "vite preview",
"typecheck": "vue-tsc --noEmit"
"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": {
"axios": "^1.5.1",
"element-plus": "^2.3.12",
"@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",
"vue": "^3.3.4"
"lodash-es": "^4.17.21",
"pinia": "^2.1.7",
"vue": "^3.4.21",
"vue-router": "^4.3.0"
},
"devDependencies": {
"@iconify-json/ep": "^1.1.12",
"@types/file-saver": "^2.0.5",
"@types/node": "^20.6.0",
"@vitejs/plugin-vue": "^4.3.4",
"@vitejs/plugin-vue-jsx": "^3.0.1",
"@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",
"sass": "^1.66.1",
"typescript": "^5.2.2",
"unocss": "^0.55.7",
"unplugin-vue-components": "^0.25.2",
"vite": "^4.4.9",
"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-devtools": "^7.0.25",
"vite-plugin-vue-setup-extend": "^0.4.0",
"vite-ssg": "^0.23.1",
"vue-tsc": "^1.8.11"
},
"license": "MIT"
"vue-tsc": "^2.0.11"
}
}

7733
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 267 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

View File

@ -1 +0,0 @@
<svg id="图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 44 44"><defs><style>.cls-1{fill:#409eff;fill-rule:evenodd;}</style></defs><title>element plus-logo-small 副本</title><path id="element_plus-logo-small" data-name="element plus-logo-small" class="cls-1" d="M37.41,32.37c0,1.57-.83,1.93-.83,1.93L21.51,43A1.69,1.69,0,0,1,20,43S5.2,34.4,4.66,34a1.29,1.29,0,0,1-.55-1V15.24c0-.78,1-1.33,1-1.33L19.86,5.36a2,2,0,0,1,1.79,0l14.46,8.41a2.06,2.06,0,0,1,1.25,2.06V32.37Zm-5.9-17L21.35,9.5a1.59,1.59,0,0,0-1.41,0L8.33,16.15s-.77.46-.76,1.08,0,13.92,0,13.92A1,1,0,0,0,8,31.9c.43.3,12,7,12,7a1.31,1.31,0,0,0,1.19,0C21.91,38.5,33,32.11,33,32.11s.65-.28.65-1.51V27.13l-13,7.9V32a3.05,3.05,0,0,1,1-2.07L33.2,23a2.44,2.44,0,0,0,.55-1.46V18.43L20.64,26.35v-3.2a2.22,2.22,0,0,1,.83-1.79ZM41.07,4.22a.39.39,0,0,0-.37-.42H38V1.06c0-.16-.26-.22-.53-.22L36,1.08c-.18,0-.31.12-.31.23V3.8H33a.4.4,0,0,0-.36.37v2h3V9c0,.16.26.27.54.23l1.51-.25c.18,0,.29-.13.29-.23V6.14h3Z"/></svg>

Before

Width:  |  Height:  |  Size: 995 B

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -1 +0,0 @@
<svg id="图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 44 44"><defs><style>.cls-1{fill:#409eff;fill-rule:evenodd;}</style></defs><title>element plus-logo-small 副本</title><path id="element_plus-logo-small" data-name="element plus-logo-small" class="cls-1" d="M37.41,32.37c0,1.57-.83,1.93-.83,1.93L21.51,43A1.69,1.69,0,0,1,20,43S5.2,34.4,4.66,34a1.29,1.29,0,0,1-.55-1V15.24c0-.78,1-1.33,1-1.33L19.86,5.36a2,2,0,0,1,1.79,0l14.46,8.41a2.06,2.06,0,0,1,1.25,2.06V32.37Zm-5.9-17L21.35,9.5a1.59,1.59,0,0,0-1.41,0L8.33,16.15s-.77.46-.76,1.08,0,13.92,0,13.92A1,1,0,0,0,8,31.9c.43.3,12,7,12,7a1.31,1.31,0,0,0,1.19,0C21.91,38.5,33,32.11,33,32.11s.65-.28.65-1.51V27.13l-13,7.9V32a3.05,3.05,0,0,1,1-2.07L33.2,23a2.44,2.44,0,0,0,.55-1.46V18.43L20.64,26.35v-3.2a2.22,2.22,0,0,1,.83-1.79ZM41.07,4.22a.39.39,0,0,0-.37-.42H38V1.06c0-.16-.26-.22-.53-.22L36,1.08c-.18,0-.31.12-.31.23V3.8H33a.4.4,0,0,0-.36.37v2h3V9c0,.16.26.27.54.23l1.51-.25c.18,0,.29-.13.29-.23V6.14h3Z"/></svg>

Before

Width:  |  Height:  |  Size: 995 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 132 KiB

BIN
public/qq_qun.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

BIN
public/wx.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

BIN
public/wx_qun.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

View File

@ -1,124 +1,15 @@
<script setup lang="ts">
import zhCn from 'element-plus/es/locale/lang/zh-cn'
</script>
<template>
<el-config-provider namespace="el" :locale="zhCn">
<FlowDesign :process="process" :fields="fields"/>
<router-view></router-view>
</el-config-provider>
</template>
<script setup lang="ts">
import FlowDesign from '~/views/flowDesign/index.vue'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import {FlowNode} from "~/views/flowDesign/nodes/Node/index";
import {ref} from "vue";
import {Field} from "~/components/Render/interface";
import {StartNode} from "~/views/flowDesign/nodes/Start/index";
import {EndNode} from "~/views/flowDesign/nodes/End/index";
//
const process = ref<FlowNode>({
id: 'root',
pid: null,
type: 'start',
name: '发起人',
formProperties: [],
child: {
id: 'end',
pid: 'root',
type: 'end',
name: '结束',
child: null
} as EndNode
} as StartNode)
//
const fields = ref<Field[]>([
{
id: 'field_da2w55',
title: "请假人",
name: "UserSelection",
value: null,
props: {
multiple: false,
disabled: false,
placeholder: "请选择用户",
style: {
width: "100%"
}
}
},
{
id: 'field_fa2w40',
title: "请假天数",
name: "Number",
value: null,
props: {
disabled: false,
placeholder: "请假天数",
style: {
width: "100%"
},
min: 0,
max: 100,
step: 1,
precision: 0,
}
},
{
id: 'field_d42t45',
title: "请假事由",
name: "Select",
value: null,
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',
title: "请假原因",
name: "Input",
value: null,
props: {
type: 'textarea',
placeholder: "请输入请假原因",
autosize: {
minRows: 3,
maxRows: 3
},
disabled: false,
style: {
width: "100%"
}
}
}
])
</script>
<style>
<style scoped>
#app {
color: var(--el-text-color-primary);
}
</style>

View File

@ -1,158 +1,99 @@
import axios, {
AxiosInstance,
AxiosError,
AxiosRequestConfig,
InternalAxiosRequestConfig,
AxiosResponse
type AxiosInstance,
AxiosError,
type AxiosRequestConfig,
type InternalAxiosRequestConfig,
type AxiosResponse
} from 'axios'
import {ElNotification} from "element-plus";
import { ElNotification } from 'element-plus'
export interface Result {
code: number;
success: boolean;
message: string;
code: number
success: boolean
message: string
}
export interface ResultData<T = any> extends Result {
data: T;
data: T
}
export interface ResultPage<T = any> extends ResultData {
data: {
rows: T[];
total: number;
}
}
/**
* axios配置
*/
const config = {
// 请求地址
baseURL: import.meta.env.VITE_API_URL as string,
// 设置超时时间
timeout: 8000,
baseURL: import.meta.env.VITE_API_URL,
timeout: 8000
}
class RequestHttp {
service: AxiosInstance
service: AxiosInstance
/**
*
* @param config
*/
public constructor(config: AxiosRequestConfig) {
// 创建axios实例
this.service = axios.create(config)
// 请求拦截
this.service.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
// config.headers['Authorization'] = "Bearer 87779e5a-3342-4df6-865d-d8828800d6fb"
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)
}
)
}
/**
*
* @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
*/
get<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {
return this.service.get(url, {params, ..._object})
}
/**
* 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
*/
post<T>(url: string, data?: object): Promise<ResultData<T>> {
return this.service.post(url, data)
}
/**
* post请求
* @param url
* @param data
* @param config
*/
post<T>(url: string, data?: object, config = {}): Promise<ResultData<T>> {
return this.service.post(url, data, config)
}
/**
* put请求
* @param url
* @param data
*/
put<T>(url: string, data?: object): Promise<ResultData<T>> {
return this.service.put(url, data)
}
/**
* request请求
* @param config
*/
request<T>(config: AxiosRequestConfig): Promise<ResultData<T>> {
return this.service.request(config)
}
/**
* delete请求
* @param url
* @param params
*/
delete<T>(url: string, params?: any): Promise<ResultData<T>> {
return this.service.delete(url, params)
}
/**
* patch请求
* @param url
* @param data
*/
patch<T>(url: string, data?: object): Promise<ResultData<T>> {
return this.service.patch(url, data)
}
/**
* head请求
* @param url
*/
head<T>(url: string): Promise<ResultData<T>> {
return this.service.head(url)
}
/**
* options请求
* @param url
* @param _object
*/
options<T>(url: string): Promise<ResultData<T>> {
return this.service.options(url)
}
/**
* 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' })
}
/**
*
* @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)

View File

@ -1,10 +1,10 @@
import http from '~/api/index'
import FileSaver from "file-saver";
import http from '@/api'
import FileSaver from 'file-saver'
export const downloadXml = async (data: object) => {
const res = await http.download('https://www.666cxf.com/api/model/download', data)
FileSaver.saveAs(
new Blob([res], { type: 'application/octet-stream;charset=utf-8' }),
'测试流程.bpmn20.xml'
)
const res = await http.download('https://www.666cxf.com/api/model/download', data)
FileSaver.saveAs(
new Blob([res], { type: 'application/octet-stream;charset=utf-8' }),
'测试流程.bpmn20.xml'
)
}

View File

@ -1,8 +1,8 @@
import http from '~/api/index'
import http from '@/api/index'
export interface Role {
id: string,
name: string
id: string
name: string
}
/**
@ -10,13 +10,13 @@ export interface Role {
* @param id
*/
export const getById = (id: string) => {
return http.get<Role>(`/role/info`,{id:id})
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)
const params = roleIds ? { roleIds: roleIds } : {}
return http.post<Role[]>('/role/list', params)
}

View File

@ -1,10 +1,10 @@
import http from '~/api/index'
import http from '@/api/index'
export interface User {
id: string,
username: string,
name: string,
avatar: string
id: string
username: string
name: string
avatar: string
}
/**
@ -12,13 +12,13 @@ export interface User {
* @param username
*/
export const getByUsername = (username: string) => {
return http.get<User>(`/user/info`,{username:username})
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)
const params = userIds ? { userIds: userIds } : {}
return http.post<User[]>('/user/list', params)
}

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1707920696842" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3610" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M918.8 770.9H804.7V656.8c0-13.8-11.2-25-25-25s-25 11.2-25 25v114.1h-114c-13.8 0-25 11.2-25 25s11.2 25 25 25h114.1V935c0 13.8 11.2 25 25 25s25-11.2 25-25V820.9h114.1c13.8 0 25-11.2 25-25s-11.3-25-25.1-25zM487.4 483.1c116.5 0 211.4-94.3 211.4-210.1S603.9 62.8 487.4 62.8C370.8 62.8 276 157.1 276 273s94.8 210.1 211.4 210.1z m0-370.3c89 0 161.4 71.8 161.4 160.1S576.4 433 487.4 433 326 361.2 326 272.9s72.4-160.1 161.4-160.1z" fill="currentColor" p-id="3611"></path><path d="M560.2 909.6c-25.7-0.5-47.1-0.7-67.5-0.7-40.6 0-79 0.9-116.2 1.9-106.7 2.6-207.5 5.1-236.9-23.6-4.1-4-8.8-10.4-8.8-23.8 0-14.6 5.4-56.4 17.1-99.6 14.2-52.4 29.7-78.2 39.6-85.1 187.9-114.6 188-114.6 308.7-114.6 92.8 0 112.1 0 181.2 39.9 12 6.9 27.2 2.8 34.1-9.2 6.9-12 2.8-27.2-9.2-34.1-80.7-46.6-109.8-46.6-206.2-46.6-64.6 0-100.2 0-144.6 16.4-42.7 15.5-92.1 45.7-190.5 105.7-0.3 0.2-0.5 0.3-0.8 0.5-24.1 15.9-44.5 54.3-60.7 114-12.1 45.3-18.7 91.8-18.7 112.6 0 24.1 8 44.1 23.9 59.6 33.3 32.5 95.6 39.3 180.6 39.3 28.4 0 59.3-0.8 92.4-1.6 36.9-0.9 75-1.8 114.9-1.8 20 0 41.2 0.2 66.5 0.7h0.5c13.6 0 24.7-10.9 25-24.5 0.4-13.7-10.6-25.1-24.4-25.4z" fill="currentColor" p-id="3612"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1707920525706" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3101" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M917.5 819.5H639.3c-13.8 0-25-11.2-25-25s11.2-25 25-25h278.1c13.8 0 25 11.2 25 25s-11.1 25-24.9 25zM486 481.7c-116.5 0-211.4-94.3-211.4-210.1 0-115.9 94.8-210.1 211.4-210.1 116.5 0 211.4 94.3 211.4 210.1S602.6 481.7 486 481.7z m0-370.2c-89 0-161.4 71.8-161.4 160.1S397 431.7 486 431.7s161.4-71.8 161.4-160.1S575 111.5 486 111.5z" fill="currentColor" p-id="3102"></path><path d="M284 960.9c-85 0-147.3-6.8-180.6-39.3-15.9-15.6-24-35.6-24-59.7 0-20.8 6.6-67.4 18.9-112.7 16.1-59.7 36.5-98.1 60.7-114 0.2-0.2 0.5-0.3 0.8-0.5 98.4-60 147.8-90.1 190.6-105.9 44.5-16.4 80.1-16.4 144.6-16.4 96.4 0 125.4 0 206.2 46.6 12 6.9 16.1 22.2 9.2 34.1-6.9 12-22.2 16.1-34.1 9.2-69.1-39.9-88.4-39.9-181.2-39.9-120.7 0-120.8 0-308.7 114.6-9.9 6.9-25.5 32.7-39.6 85.1-11.7 43.2-17.1 85-17.1 99.6 0 13.5 4.7 19.8 8.8 23.8 29.4 28.7 130.2 26.2 236.9 23.6 37.2-0.9 75.6-1.9 116.2-1.9 20.4 0 41.9 0.2 67.5 0.7 13.8 0.3 24.8 11.7 24.5 25.5-0.3 13.6-11.4 24.5-25 24.5h-0.5c-25.3-0.5-46.4-0.7-66.5-0.7-39.9 0-78.1 0.9-114.9 1.8-33.5 1.1-64.4 1.9-92.7 1.9z" fill="currentColor" p-id="3103"></path></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

1
src/assets/logo.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

Before

Width:  |  Height:  |  Size: 497 B

View File

@ -1,25 +1,25 @@
<script setup lang="ts">
import {useVModel} from '@vueuse/core'
import { useVModel } from '@vueuse/core'
const $props = defineProps<{
modelValue: string
}>()
const operatorOptions = [
{
value: 'equal',
value: 'eq',
label: '等于'
},
{
value: 'not_equal',
value: 'ne',
label: '不等于'
},
{
label: '包含',
value: 'contains'
value: 'in'
},
{
label: '不包含',
value: 'not_contain'
value: 'ni'
}
]
const $emits = defineEmits<{
@ -31,10 +31,10 @@ const data = useVModel($props, 'modelValue', $emits)
<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"
v-for="item in operatorOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>

View File

@ -1,11 +1,11 @@
<script setup lang="ts">
import {Field} from "~/components/Render/interface";
import {FilterRules} from "~/components/Condition/index";
import {useVModel} from "@vueuse/core";
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[],
modelValue: any
options: Field[]
filterRules: FilterRules
}>()
const $emits = defineEmits<{
@ -16,15 +16,8 @@ const data = useVModel($props, 'modelValue', $emits)
<template>
<el-select class="trigger-container" v-model="data" filterable placeholder="选择字段">
<el-option
v-for="item in $props.options"
:key="item.id"
:label="item.title"
:value="item.id"
/>
<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>
<style scoped lang="scss"></style>

View File

@ -1,14 +1,12 @@
<script setup lang="ts" name="ConditionFilter">
import {FilterRules} from "~/components/Condition/index";
import {Field} from "~/components/Render/interface";
import {useVModel} from "@vueuse/core";
import {Delete, CirclePlus, CircleClose} from "@element-plus/icons-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'
import Render from '~/components/Render/index'
const $props = defineProps<{
filterFields: Field[],
filterFields: Field[]
modelValue: FilterRules
}>()
const $emits = defineEmits<{
@ -22,13 +20,11 @@ const filterRules = useVModel($props, 'modelValue', $emits)
* 添加条件
*/
const addRule = () => {
filterRules.value.conditions.push(
{
field: null,
operator: 'equal',
value: null
}
)
filterRules.value.conditions.push({
field: null,
operator: 'eq',
value: null
})
}
/**
* 删除条件
@ -46,12 +42,14 @@ const handleDel = (index: number) => {
*/
const addGroup = () => {
filterRules.value.groups.push({
logicalOperator: 'and',
conditions: [{
field: null,
operator: '',
value: null
}],
operator: 'and',
conditions: [
{
field: null,
operator: '',
value: null
}
],
groups: []
})
}
@ -69,77 +67,79 @@ const delGroup = (index: number) => {
<div class="logical-operator">
<div class="logical-operator__line"></div>
<el-switch
v-model="filterRules.logicalOperator"
inline-prompt
style="--el-switch-on-color: #409EFF; --el-switch-off-color: #67C23A"
active-value="and"
inactive-value="or"
active-text="且"
inactive-text="或"
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-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%;">
<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"
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 :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%;">
<el-form-item :prop="'conditions.' + index + '.value'" style="width: 100%">
<Render
:field="$props.filterFields.find(e=>e.id===item.field)"
v-model="item.value"
: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
: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>
<ConditionFilter
v-for="(item,index) in filterRules.groups"
:key="index"
@delGroup="delGroup(index)"
v-model="filterRules.groups[index]"
:filterFields="filterFields"
<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 @click="delGroup(index)" icon="CircleClose" class="filter-filter-item__add">
删除条件组
</el-button>
</ConditionFilter>
<div v-if="filterRules.groups.length===0 && filterRules.conditions.length===0"
class="filter-item-rule"/>
</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 @click="addRule" icon="CirclePlus" class="filter-filter-item__add">
添加条件
</el-button>
<el-button @click="addGroup" :icon="CirclePlus" class="filter-filter-item__add">
<el-button @click="addGroup" icon="CirclePlus" class="filter-filter-item__add">
添加条件组
</el-button>
<slot/>
<slot />
</div>
</div>
</div>
@ -182,7 +182,7 @@ const delGroup = (index: number) => {
height: calc(100% - 48px);
&::before {
content: "";
content: '';
position: absolute;
top: 0;
right: 0;
@ -194,7 +194,7 @@ const delGroup = (index: number) => {
}
&::after {
content: "";
content: '';
position: absolute;
bottom: 0;
right: 0;
@ -223,4 +223,4 @@ const delGroup = (index: number) => {
}
}
}
</style>
</style>

View File

@ -0,0 +1,20 @@
/**
*
*/
export interface Condition {
// 筛选字段
field: string | null
// 条件运算符
operator: string
// 筛选值
value: any | null
}
/**
*
*/
export interface FilterRules {
operator: 'or' | 'and'
conditions: Condition[]
groups: FilterRules[]
}

View File

@ -1,20 +0,0 @@
/**
*
*/
export interface Condition {
// 筛选字段
field: string | null,
// 条件运算符
operator: string,
// 筛选值
value: any | null
}
/**
*
*/
export interface FilterRules {
logicalOperator: 'or' | 'and',
conditions: Condition[]
groups: FilterRules[]
}

View File

@ -1,95 +1,95 @@
import {defineAsyncComponent, defineComponent, h, PropType, resolveComponent} from "vue";
import {cloneDeep} from 'lodash-es'
import {Field} from "./interface";
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
}
props: {
modelValue: {
type: [String, Number, Boolean, Array, Object] as PropType<any>,
default: undefined,
required: false
},
emits: ['update:modelValue'],
components: {
Input: defineAsyncComponent(() => import('element-plus/es').then(({ElInput}) => ElInput)),
Number: defineAsyncComponent(() => import('element-plus/es').then(({ElInputNumber}) => ElInputNumber)),
Select: defineAsyncComponent(() => import('element-plus/es').then(({ElSelect}) => ElSelect)),
Radio: defineAsyncComponent(() => import('element-plus/es').then(({ElRadio}) => ElRadio)),
Checkbox: defineAsyncComponent(() => import('element-plus/es').then(({ElCheckbox}) => ElCheckbox)),
UserSelection: defineAsyncComponent(() => import('~/components/UserSelection/index')),
RoleSelection: defineAsyncComponent(() => import('~/components/RoleSelection/index'))
},
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) => {
let children: Record<string, any> = {}
const slotFunctions: Record<string, any> = {
Select: (conf: Field) => {
return conf.props.options.map((item: any) => {
return <el-option label={item.label} value={item.value}></el-option>
})
},
Radio: (conf: Field) => {
return conf.props.options.map((item: any) => {
return <el-radio label={item.value}>{item.label}</el-radio>
})
},
Checkbox: (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)
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)
}
})

View File

@ -1,7 +0,0 @@
export interface Field {
id: string,
title: string,
name: string,
value: any,
props: Record<string, any>
}

View File

@ -0,0 +1,12 @@
export interface Field {
id: string
type: 'formItem' | 'container'
label: string
name: string
value: any
readonly?: boolean
required?: boolean
hidden: boolean
props: Recordable
children?: Field[]
}

View File

@ -1,12 +0,0 @@
import roleTag from './src/RoleTag.vue'
import rolePicker from './src/RolePicker.vue'
import roleSelector from './src/RoleSelector.vue'
import {withInstall, withNoopInstall} from 'element-plus/es/utils/vue/install'
export const RoleTag = withNoopInstall(roleTag)
export const RolePicker = withNoopInstall(rolePicker)
export const RoleSelector = withInstall(roleSelector, {
roleTag,
rolePicker
})
export default RoleSelector

View File

@ -1,15 +1,13 @@
<script setup lang="ts">
import {useVModel} from '@vueuse/core'
import {TreeNodeData} from 'element-plus/es/components/tree/src/tree.type'
import {type ElTree} from 'element-plus'
import {reactive, ref, watch} from "vue";
import {getList} from "~/api/modules/role";
import {School, Check} from "@element-plus/icons-vue";
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,
modelValue: ModelValueType
multiple?: boolean
}
@ -41,7 +39,7 @@ 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)
const val = roleOptions.value.find((e) => e.id === role.id)
if (val) {
return 'is-active'
} else {
@ -51,7 +49,7 @@ const renderClass = (role: TreeNodeData): string | { [key: string]: boolean } =>
const onNodeClick = (data: Role) => {
if ($props.multiple) {
const index = roleOptions.value.findIndex(e => e.id === data.id)
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))
@ -59,7 +57,7 @@ const onNodeClick = (data: Role) => {
roleOptions.value.splice(index, 1)
}
} else {
const index = roleOptions.value.findIndex(e => e.id === data.id)
const index = roleOptions.value.findIndex((e) => e.id === data.id)
if (index === -1) {
roleOptions.value = [data]
} else {
@ -69,12 +67,15 @@ const onNodeClick = (data: Role) => {
}
const dialogVisible = ref(false)
const queryForm = reactive({
name: null,
name: null
})
watch(() => queryForm.name, (val) => {
orgTreeRef.value?.filter(val)
})
watch(
() => queryForm.name,
(val) => {
orgTreeRef.value?.filter(val)
}
)
const filterNode = (value: string, data: TreeNodeData): boolean => {
if (!value) return true
return data.name.includes(value)
@ -83,9 +84,9 @@ const open = () => {
dialogVisible.value = true
}
const onOpen = () => {
getList().then(res => {
getList().then((res) => {
if (res.success) {
roleOrgOptions.value = res.data.map(e => {
roleOrgOptions.value = res.data.map((e) => {
return {
id: e.id,
name: e.name
@ -94,7 +95,6 @@ const onOpen = () => {
}
})
let roleIds: string[] = []
if (Array.isArray(value.value)) {
roleIds.push(...value.value)
@ -102,9 +102,9 @@ const onOpen = () => {
roleIds.push(value.value)
}
if (roleIds.length > 0) {
getList(roleIds).then(res => {
getList(roleIds).then((res) => {
if (res.success) {
roleOptions.value = res.data.map(role => {
roleOptions.value = res.data.map((role) => {
return {
id: role.id,
name: role.name
@ -119,7 +119,7 @@ const onOpen = () => {
}
const handelConfirm = () => {
if ($props.multiple) {
value.value = roleOptions.value.map(e => e.id)
value.value = roleOptions.value.map((e) => e.id)
} else {
if (roleOptions.value.length > 0) {
value.value = roleOptions.value[0].id
@ -136,42 +136,44 @@ defineExpose({
<template>
<el-dialog
v-model="dialogVisible"
@open="onOpen"
align-center
draggable
title="选择角色"
width="30%"
v-model="dialogVisible"
@open="onOpen"
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>
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}">
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/>
<School />
</el-icon>
&nbsp;{{ data.name }}
</div>
<el-icon class="is-selected">
<Check/>
<Check />
</el-icon>
</div>
</template>
@ -179,7 +181,7 @@ defineExpose({
</el-scrollbar>
</el-card>
<template #footer>
<el-button @click="dialogVisible=false">取消</el-button>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handelConfirm">确认</el-button>
</template>
</el-dialog>

View File

@ -1,10 +1,8 @@
<script setup lang="ts">
import {getById} from '~/api/modules/role'
import {componentSizeMap, useFormSize} from 'element-plus'
import {computed, onMounted, reactive} from "vue";
import { getById } from '@/api/modules/role'
export interface RoleTagProps {
id: string,
id: string
type?: 'success' | 'info' | 'warning' | 'danger'
closable?: boolean
}
@ -30,34 +28,20 @@ onMounted(() => {
if (!$props.id) {
throw new Error('username is required')
}
getById($props.id).then(res => {
getById($props.id).then((res) => {
if (res.success) {
console.log(res);
roleInfo.id = res.data.id
roleInfo.name = res.data.name
}
})
})
const getBaseUrl = computed(() => {
const url = import.meta.env.VITE_API_URL
if (url.startsWith('http')) {
return url
} else {
return window.location.origin + url
}
})
const formSize = useFormSize()
const getComponentSize = computed(() => {
return componentSizeMap[formSize.value || 'default'] - 12
})
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;">
<div class="flex-center" style="gap: 4px; grid-gap: 4px">
<span>{{ roleInfo.name || id }}</span>
</div>
</el-tag>

View File

@ -1,16 +1,15 @@
<script setup lang="ts">
import {useVModel} from '@vueuse/core'
import { useVModel } from '@vueuse/core'
import RoleTag from './RoleTag.vue'
import RolePicker, {ModelValueType} from './RolePicker.vue'
import {useFormDisabled, useFormSize} from 'element-plus'
import {computed, CSSProperties, ref} from 'vue'
import {Avatar} from "@element-plus/icons-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,
modelValue: ModelValueType
placeholder?: string
multiple?: boolean
disabled?: boolean
style?: CSSProperties
}
@ -23,7 +22,7 @@ const $emits = defineEmits<{
(e: 'update:modelValue', modelValue: ModelValueType): void
}>()
const value = useVModel($props, 'modelValue', $emits)
const valueArr = computed<string []>(() => {
const valueArr = computed<string[]>(() => {
if (!value.value) return []
return Array.isArray(value.value) ? value.value : [value.value]
})
@ -47,15 +46,27 @@ const onClose = (username: string) => {
</script>
<template>
<role-picker ref="RolePickerRef" :multiple="multiple" v-model="value"/>
<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-button
class="role-but-item"
:size="formSize"
:disabled="disabled"
@click="openRolePicker"
circle
>
<el-icon>
<Avatar/>
<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">
<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>

View File

@ -1,5 +0,0 @@
import Segmented from './src/index.vue'
export default Segmented
export * from './src/index.vue'

View File

@ -0,0 +1,7 @@
.svg-icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}

View File

@ -0,0 +1,66 @@
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
}
})

View File

@ -1,12 +0,0 @@
import userTag from './src/UserTag.vue'
import userPicker from './src/UserPicker.vue'
import userSelector from './src/UserSelector.vue'
import {withInstall, withNoopInstall} from 'element-plus/es/utils/vue/install'
export const UserTag = withNoopInstall(userTag)
export const UserPicker = withNoopInstall(userPicker)
export const UserSelector = withInstall(userSelector, {
userTag,
userPicker
})
export default UserSelector

View File

@ -1,16 +1,13 @@
<script setup lang="ts">
import {useVModel} from '@vueuse/core'
import {TreeNodeData} from 'element-plus/es/components/tree/src/tree.type'
import {type ElTree} from 'element-plus'
import {reactive, ref, watch} from "vue";
import {getList} from "~/api/modules/user";
import {School, Check} from "@element-plus/icons-vue";
import Node from "element-plus/es/components/tree/src/model/node";
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,
modelValue: ModelValueType
multiple?: boolean
}
@ -41,11 +38,11 @@ const value = useVModel($props, 'modelValue', $emits)
const userOptions = ref<Org[]>([])
const userOrgOptions = ref<Org[]>([])
const orgTreeRef = ref<InstanceType<typeof ElTree>>()
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)
const val = userOptions.value.find((e) => e.id === org.id)
if (val) {
return 'is-active'
} else {
@ -56,7 +53,7 @@ const renderClass = (org: TreeNodeData): string | { [key: string]: boolean } =>
const onNodeClick = (data: Org) => {
if (data.type !== 'user') return
if ($props.multiple) {
const index = userOptions.value.findIndex(e => e.id === data.id)
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))
@ -64,7 +61,7 @@ const onNodeClick = (data: Org) => {
userOptions.value.splice(index, 1)
}
} else {
const index = userOptions.value.findIndex(e => e.id === data.id)
const index = userOptions.value.findIndex((e) => e.id === data.id)
if (index === -1) {
userOptions.value = [data]
} else {
@ -74,12 +71,15 @@ const onNodeClick = (data: Org) => {
}
const dialogVisible = ref(false)
const queryForm = reactive({
name: null,
name: null
})
watch(() => queryForm.name, (val) => {
orgTreeRef.value?.filter(val)
})
watch(
() => queryForm.name,
(val) => {
orgTreeRef.value?.filter(val)
}
)
const filterNode = (value: string, data: TreeNodeData): boolean => {
if (!value) return true
return data.name.includes(value)
@ -88,9 +88,9 @@ const open = () => {
dialogVisible.value = true
}
const onOpen = () => {
getList().then(res => {
getList().then((res) => {
if (res.success) {
userOrgOptions.value = res.data.map(e => {
userOrgOptions.value = res.data.map((e) => {
return {
id: e.username,
name: e.name,
@ -102,7 +102,6 @@ const onOpen = () => {
}
})
let userIds: string[] = []
if (Array.isArray(value.value)) {
userIds.push(...value.value)
@ -110,9 +109,9 @@ const onOpen = () => {
userIds.push(value.value)
}
if (userIds.length > 0) {
getList(userIds).then(res => {
getList(userIds).then((res) => {
if (res.success) {
userOptions.value = res.data.map(user => {
userOptions.value = res.data.map((user) => {
return {
id: user.username,
name: user.name,
@ -130,7 +129,7 @@ const onOpen = () => {
}
const handelConfirm = () => {
if ($props.multiple) {
value.value = userOptions.value.map(e => e.id)
value.value = userOptions.value.map((e) => e.id)
} else {
if (userOptions.value.length > 0) {
value.value = userOptions.value[0].id
@ -147,45 +146,47 @@ defineExpose({
<template>
<el-dialog
v-model="dialogVisible"
@open="onOpen"
align-center
draggable
title="选择用户"
width="30%"
v-model="dialogVisible"
@open="onOpen"
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>
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}">
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/>
<School />
</el-icon>
&nbsp;{{ data.name }}
</div>
<el-icon class="is-selected">
<Check/>
<Check />
</el-icon>
</div>
</template>
@ -193,7 +194,7 @@ defineExpose({
</el-scrollbar>
</el-card>
<template #footer>
<el-button @click="dialogVisible=false">取消</el-button>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handelConfirm">确认</el-button>
</template>
</el-dialog>

View File

@ -1,10 +1,9 @@
<script setup lang="ts">
import {getByUsername} from '~/api/modules/user'
import {componentSizeMap, useFormSize} from 'element-plus'
import {computed, onMounted, reactive} from "vue";
import { getByUsername } from '@/api/modules/user'
import { componentSizeMap, useFormSize } from 'element-plus'
export interface UserTagProps {
username: string,
username: string
type?: 'success' | 'info' | 'warning' | 'danger'
closable?: boolean
}
@ -32,7 +31,7 @@ onMounted(() => {
if (!$props.username) {
throw new Error('username is required')
}
getByUsername($props.username).then(res => {
getByUsername($props.username).then((res) => {
if (res.success) {
userInfo.username = res.data.username
userInfo.avatar = res.data.avatar
@ -40,14 +39,6 @@ onMounted(() => {
}
})
})
const getBaseUrl = computed(() => {
const url = import.meta.env.VITE_API_URL
if (url.startsWith('http')) {
return url
} else {
return window.location.origin + url
}
})
const formSize = useFormSize()
const getComponentSize = computed(() => {
return componentSizeMap[formSize.value || 'default'] - 12
@ -55,11 +46,10 @@ const getComponentSize = computed(() => {
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;">
<div class="flex-center" style="gap: 4px; grid-gap: 4px">
<el-avatar :size="getComponentSize" :src="userInfo.avatar">
{{ (userInfo.name || username).charAt(0) }}
</el-avatar>

View File

@ -1,16 +1,15 @@
<script setup lang="ts">
import {useVModel} from '@vueuse/core'
import { useVModel } from '@vueuse/core'
import UserTag from './UserTag.vue'
import UserPicker, {ModelValueType} from './UserPicker.vue'
import {useFormDisabled, useFormSize} from 'element-plus'
import {computed, CSSProperties, ref} from 'vue'
import {User} from "@element-plus/icons-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,
modelValue: ModelValueType
placeholder?: string
multiple?: boolean
disabled?: boolean
style?: CSSProperties
}
@ -23,7 +22,7 @@ const $emits = defineEmits<{
(e: 'update:modelValue', modelValue: ModelValueType): void
}>()
const value = useVModel($props, 'modelValue', $emits)
const valueArr = computed<string []>(() => {
const valueArr = computed<string[]>(() => {
if (!value.value) return []
return Array.isArray(value.value) ? value.value : [value.value]
})
@ -47,15 +46,25 @@ const onClose = (username: string) => {
</script>
<template>
<user-picker ref="userPickerRef" :multiple="multiple" v-model="value"/>
<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>
<el-icon>
<User/>
</el-icon>
<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">
<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>

View File

@ -1,4 +0,0 @@
import { useDark, useToggle } from "@vueuse/core";
export const isDark = useDark();
export const toggleDark = useToggle(isDark);

View File

@ -1 +0,0 @@
export * from "./dark";

21
src/env.d.ts vendored
View File

@ -1,21 +0,0 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
interface ImportMetaEnv {
readonly VITE_OPEN: boolean;
readonly VITE_PORT: number;
readonly VITE_GLOB_APP_TITLE: string;
readonly VITE_API_URL: string;
readonly VITE_PUBLIC_PATH: string;
readonly VITE_USER_NODE_ENV: string;
}
// 解决import.meta.env类型提示参考https://cn.vitejs.dev/guide/env-and-mode.html#env-files
interface ImportMeta {
readonly env: ImportMetaEnv
}

View File

@ -1,23 +1,20 @@
import { createApp } from "vue";
import App from "./App.vue";
// import "~/styles/element/index.scss";
// import ElementPlus from "element-plus";
// import all element css, uncommented next line
// import "element-plus/dist/index.css";
// or use cdn, uncomment cdn link in `index.html`
import "~/styles/index.scss";
import "uno.css";
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";
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);
// app.use(ElementPlus);
app.mount("#app");
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')

6
src/mock/index.ts Normal file
View File

@ -0,0 +1,6 @@
import user from './user'
import role from './role'
import type { MockMethod } from 'vite-plugin-mock'
const mockModules: MockMethod[] = [...user, ...role]
export default mockModules

70
src/mock/role.ts Normal file
View File

@ -0,0 +1,70 @@
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

86
src/mock/user.ts Normal file
View File

@ -0,0 +1,86 @@
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

View File

@ -1,9 +1,6 @@
import { createProdMockServer } from "vite-plugin-mock/es/createProdMockServer";
import index from "../mock";
export const mockModules = [...index];
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'
import mock from './mock'
export function setupProdMockServer() {
createProdMockServer(mockModules);
createProdMockServer(mock)
}

15
src/router/index.ts Normal file
View File

@ -0,0 +1,15 @@
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

12
src/stores/counter.ts Normal file
View File

@ -0,0 +1,12 @@
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 }
})

View File

@ -1,29 +1,17 @@
<script setup lang="ts">
import {useVModel} from '@vueuse/core'
interface SegmentedProps {
block?: boolean,
modelValue?: string
}
const props = withDefaults(defineProps<SegmentedProps>(), {
block: true
})
const emits = defineEmits<
(e: 'update:modelValue', value: string) => void
>()
const data = useVModel(props, 'modelValue', emits)
</script>
<template>
<el-tabs v-model="data" :class="['el-segmented',{'is-block': block}]">
<slot/>
</el-tabs>
</template>
<style scoped lang="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 {
@ -39,7 +27,8 @@ const data = useVModel(props, 'modelValue', emits)
padding: var(--el-segmented-padding);
}
.el-tabs__nav-scroll, .el-tabs__nav-wrap {
.el-tabs__nav-scroll,
.el-tabs__nav-wrap {
margin: 0;
overflow: visible;
}
@ -66,7 +55,9 @@ const data = useVModel(props, 'modelValue', emits)
line-height: var(--el-segmented-height);
font-size: var(--el-segmented-font-size);
border-radius: var(--el-segmented-radius);
transition: color .2s, background-color .2s;
transition:
color 0.2s,
background-color 0.2s;
background: none;
z-index: 2;
@ -84,7 +75,6 @@ const data = useVModel(props, 'modelValue', emits)
}
}
/* 活动栏属性 */
.el-tabs__active-bar {
padding: 0 var(--el-segmented-item-padding);
margin-left: calc(0px - var(--el-segmented-item-padding));
@ -98,6 +88,4 @@ const data = useVModel(props, 'modelValue', emits)
top: 0;
}
}
}
</style>

View File

@ -1,11 +1,11 @@
// only scss variables
$--colors: (
"primary": (
"base": #589ef8,
),
'primary': (
'base': #589ef8
)
);
@forward "element-plus/theme-chalk/src/dark/var.scss" with (
@forward 'element-plus/theme-chalk/src/dark/var.scss' with (
$colors: $--colors
);

View File

@ -1,36 +1,36 @@
$--colors: (
"primary": (
"base": #589ef8,
'primary': (
'base': #589ef8
),
"success": (
"base": #21ba45,
'success': (
'base': #21ba45
),
"warning": (
"base": #f2711c,
'warning': (
'base': #f2711c
),
"danger": (
"base": #db2828,
'danger': (
'base': #db2828
),
"error": (
"base": #db2828,
),
"info": (
"base": #42b8dd,
'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"
@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)
);
@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 *;
@ -39,4 +39,4 @@ $--colors: (
// @debug $--colors;
// custom dark variables
@use "./dark.scss";
@use './dark.scss';

View File

@ -1,5 +1,5 @@
// import dark theme
@use "element-plus/theme-chalk/src/dark/css-vars.scss" as *;
@use 'element-plus/theme-chalk/src/dark/css-vars.scss' as *;
:root {
.el-segmented {
@ -12,8 +12,8 @@
--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, .08);
--el-segmented-hover-bg: rgba(0, 0, 0, .04);
--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);
}
}
@ -34,7 +34,8 @@
// 抽屉头部
.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);
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;
// 抽屉标题
@ -46,13 +47,13 @@
.el-drawer__footer {
border-top: var(--el-border);
padding: calc(var(--el-drawer-padding-primary) - 5px)
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;
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;
@ -62,6 +63,15 @@ a {
color: var(--el-color-primary);
}
html,
body,
#app {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}
code {
border-radius: 2px;
padding: 2px 4px;

77
src/typings/auto-imports.d.ts vendored Normal file
View File

@ -0,0 +1,77 @@
/* 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')
}

View File

@ -1,26 +1,27 @@
/* eslint-disable */
/* prettier-ignore */
// @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 {
Condition: typeof import('./components/Condition/index.ts')['default']
AdvancedFilter: typeof import('./../components/AdvancedFilter/index.vue')['default']
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
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']
ElDivider: typeof import('element-plus/es')['ElDivider']
ElDrawer: typeof import('element-plus/es')['ElDrawer']
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']
@ -31,6 +32,7 @@ declare module 'vue' {
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']
@ -40,9 +42,17 @@ declare module 'vue' {
ElText: typeof import('element-plus/es')['ElText']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElTree: typeof import('element-plus/es')['ElTree']
Interface: typeof import('./components/Render/interface.ts')['default']
RoleSelection: typeof import('./components/RoleSelection/index.ts')['default']
Segmented: typeof import('./components/Segmented/index.ts')['default']
UserSelection: typeof import('./components/UserSelection/index.ts')['default']
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']
}
}

1
src/typings/index.d.ts vendored Normal file
View File

@ -0,0 +1 @@
type Recordable<T = any> = Record<string, T>

View File

@ -1,255 +0,0 @@
import {ErrorInfo, FlowNode} from '../nodes/Node/index'
import {ExclusiveNode} from '../nodes/Exclusive/index'
import {BranchNode} from '../nodes/Branch/index'
import {ConditionNode, FilterRules} from '../nodes/Condition/index'
import {ApprovalNode} from '../nodes/Approval/index'
import {CcNode} from '../nodes/Cc/index'
import {ref, Ref} from "vue";
import {FormProperty} from "~/views/flowDesign/index";
import {Field} from "~/components/Render/interface";
const useNode = (node: Ref<FlowNode>, fields: Ref<Field[]>) => {
/**
*
*/
const nodeErrors = ref(new Map<string, string>())
/**
* ref
*/
const nodeRefs = ref(new Map<string, any>())
/**
*
*/
const validateNodes = () => {
nodeErrors.value.clear()
nodeRefs.value.forEach((ref, id) => {
const validate = ref?.validate
if (validate) {
const error: ErrorInfo | undefined = validate()
if (error && error.showError) {
nodeErrors.value.set(id, error.message)
}
}
})
return nodeErrors
}
/**
* ref
* @param id
* @param ref
*/
const addNodeRef = (id: string, ref: any) => {
nodeRefs.value.set(id, ref)
}
/**
* id
*/
const generateId = (): 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.child) {
return findId(node.child, id)
}
if ('children' in node) {
const branchNode = node as BranchNode
if (branchNode.children && branchNode.children.length > 0) {
return branchNode.children.some(item => {
return findId(item, id)
})
}
}
return false
}
if (findId(node.value, id)) {
return generateId()
}
return id
}
/**
*
* @param currentNode
*/
const addConnection = (currentNode: FlowNode) => {
const exclusive = currentNode as ExclusiveNode
exclusive.children.unshift({
id: generateId(),
pid: currentNode.id,
type: 'condition',
def: false,
name: `条件${exclusive.children.length + 1}`,
conditions: {
logicalOperator: 'and',
conditions: [],
groups: []
} as FilterRules,
child: null
} as ConditionNode)
}
/**
*
* @param currentNode
*/
const addApproval = (currentNode: FlowNode) => {
const child = currentNode.child
const id = generateId()
currentNode.child = {
id: id,
pid: currentNode.id,
type: 'approval',
name: '审批人',
child: child,
// 属性
assigneeType: 'user',
formUser: '',
formRole: '',
users: [],
roles: [],
leader: 1,
choice: false,
self: false,
multi: 'sequential',
nobody: 'pass',
formProperties: fields.value.map(item => {
return {
id: item.id,
name: item.title,
readable: true,
writeable: false,
hidden: false,
required: false
} as FormProperty
}),
operations: {
complete: true,
refuse: true,
save: true,
transfer: false,
addMulti: false,
minusMulti: false
}
} as ApprovalNode
if (child) {
child.pid = id
}
}
/**
*
* @param currentNode
*/
const addCc = (currentNode: FlowNode) => {
const child = currentNode.child
const id = generateId()
currentNode.child = {
id: id,
pid: currentNode.id,
type: 'cc',
name: '抄送人',
child: child,
users: [],
formProperties: fields.value.map(item => {
return {
id: item.id,
name: item.title,
readable: true,
writeable: false,
hidden: false,
required: false
} as FormProperty
})
} as CcNode
if (child) {
child.pid = id
}
}
/**
*
* @param currentNode
*/
const addExclusive = (currentNode: FlowNode) => {
const child = currentNode.child
const id = generateId()
const exclusiveNode = {
id: id,
pid: currentNode.id,
type: 'exclusive',
name: '独占网关',
child: child,
children: []
} as ExclusiveNode
currentNode.child = exclusiveNode
if (child) {
child.pid = id
}
addConnection(currentNode.child)
addConnection(currentNode.child)
if (exclusiveNode.children.length > 0) {
const condition = exclusiveNode.children[exclusiveNode.children.length - 1] as ConditionNode
condition.def = true
condition.name = '默认条件'
}
}
const addNodes: Record<string, (currentNode: FlowNode) => void> = {
condition: addConnection,
approval: addApproval,
cc: addCc,
exclusive: addExclusive
}
const addNode = (type: string, currentNode: FlowNode) => {
const fun = addNodes[type]
if (fun) {
fun(currentNode)
}
}
const delNode = (del: FlowNode) => {
delNodeNext(node.value, del)
}
const delNodeNext = (next: FlowNode, del: FlowNode) => {
if (next.id === del.pid) {
if ('children' in next && next.child?.id !== del.id) {
const branchNode = next as BranchNode
const index = branchNode.children.findIndex(item => item.id === del.id)
if (index !== -1) {
if (branchNode.children.length <= 2) {
delNode(branchNode)
} else {
branchNode.children.splice(index, 1)
}
}
} else {
if (del.child && del.child.pid) {
del.child.pid = next.id
}
next.child = del.child
}
} else {
if (next.child) {
delNodeNext(next.child, del)
}
if ('children' in next) {
const nextBranch = next as BranchNode
if (nextBranch.children && nextBranch.children.length > 0) {
nextBranch.children.forEach(item => {
delNodeNext(item, del)
})
}
}
}
}
return {
addNode,
delNode,
addNodeRef,
validateNodes,
nodeErrors
}
}
export default useNode

View File

@ -1,35 +0,0 @@
/**
*
*/
export interface OperationPermissions {
// 同意
complete: boolean
// 拒绝
refuse: boolean
// 保存
save: boolean
// 转办
transfer: boolean
// 加签
addMulti: boolean
// 减签
minusMulti: boolean
}
/**
*
*/
export interface FormProperty {
// 字段ID
id: string
// 字段名称
name: string
// 可读
readable: boolean
// 可写
writeable: boolean
// 必填
required: boolean
// 隐藏
hidden: boolean
}

View File

@ -1,30 +1,55 @@
<script setup lang="ts" name="flowDesign">
import NodeTree from './nodes/index.vue'
import NodePenal from './penal/index.vue'
import {FlowNode} from './nodes/Node/index'
import useNode from './hooks/useNode'
import {computed, onUnmounted, provide, ref} from "vue";
import {Plus, Minus, Download, Sunny, Moon, TopRight, TopLeft} from "@element-plus/icons-vue";
import {useVModels} from "@vueuse/core";
import {Field} from "~/components/Render/interface";
import {downloadXml} from "~/api/modules/model";
import {useRefHistory} from '@vueuse/core'
import {cloneDeep} from "lodash-es";
<script setup lang="ts">
import TreeNode from './nodes/TreeNode.vue'
import Panel from './panels/index.vue'
import type { ErrorInfo, FlowNode, TimerNode } from './nodes/type'
import type {
ApprovalNode,
BranchNode,
CcNode,
ConditionNode,
ExclusiveNode,
NodeType
} from './nodes/type'
import type { FilterRules } from '@/components/AdvancedFilter/type'
import type { Field } from '@/components/Render/type'
import { downloadXml } from '@/api/modules/model'
export interface FlowDesignProps {
process: FlowNode,
const props = defineProps<{
process: FlowNode
fields: Field[]
}
readOnly?: boolean
}>()
const $props = defineProps<FlowDesignProps>()
const $emits = defineEmits(['update:process', 'update:fields'])
const {fields} = useVModels($props, $emits)
const process = ref<FlowNode>($props.process)
const {undo, redo, canUndo, canRedo} = useRefHistory(process, {deep: true, clone: cloneDeep})
const nodePenalRef = ref<InstanceType<typeof NodePenal>>()
const zoom = ref(100)
const isDark = ref(false)
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 isDark = ref<boolean>(false)
const zoom = ref(100)
const activeData = ref<FlowNode>({
id: '',
name: '',
type: 'start'
})
const penalVisible = ref(false)
const nodesError = ref<Recordable<ErrorInfo[]>>({})
provide('flowDesign', {
readOnly: props.readOnly || false,
fields: flatFields,
nodesError: nodesError
})
const handleToggleDark = () => {
if (isDark.value) {
document.documentElement.classList.add('dark')
@ -33,32 +58,224 @@ const handleToggleDark = () => {
}
}
const openPenal = (node: FlowNode) => {
nodePenalRef.value?.open(node)
activeData.value = node
penalVisible.value = true
}
const {addNode, delNode, validateNodes, addNodeRef} = useNode(process, fields)
provide('nodeHooks', {
readOnly: false,
fields: fields,
addNode,
delNode,
addNodeRef,
openPenal
})
const handleZoom = (e: WheelEvent) => {
if (e.shiftKey) {
if (e.deltaY > 0) {
if (zoom.value > 50) {
zoom.value -= 10
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.child) {
return findId(node.child, id)
}
if ('children' in node) {
const branchNode = node as BranchNode
if (branchNode.children && branchNode.children.length > 0) {
return branchNode.children.some((item) => {
return findId(item, id)
})
}
}
return false
}
if (findId(props.process, id)) {
return nextId()
}
return id
}
const addExclusive = (node: FlowNode) => {
const child = node.child
const id = nextId()
const exclusiveNode = {
id: id,
pid: node.id,
type: 'exclusive',
name: '独占网关',
child: child,
children: []
} as ExclusiveNode
if (child) {
child.pid = id
}
addCondition(exclusiveNode)
addCondition(exclusiveNode)
node.child = exclusiveNode
if (exclusiveNode.children.length > 0) {
const condition = exclusiveNode.children[exclusiveNode.children.length - 1] as ConditionNode
condition.def = true
condition.name = '默认条件'
}
}
const addCondition = (node: FlowNode) => {
const exclusive = node as ExclusiveNode
exclusive.children.splice(exclusive.children.length - 1, 0, {
id: nextId(),
pid: exclusive.id,
type: 'condition',
def: false,
name: `条件${exclusive.children.length + 1}`,
conditions: {
operator: 'and',
conditions: [],
groups: []
} as FilterRules,
child: undefined
})
}
const addCc = (node: FlowNode) => {
const child = node.child
const id = nextId()
node.child = {
id: id,
pid: node.id,
type: 'cc',
name: '抄送人',
child: child,
assigneeType: 'user',
formUser: '',
formRole: '',
users: [],
roles: [],
leader: 1,
orgLeader: 1,
choice: false,
self: false,
formProperties: []
} as CcNode
if (child) {
child.pid = id
}
}
const addTimer = (node: FlowNode) => {
const child = node.child
const id = nextId()
node.child = {
id: id,
pid: node.id,
name: '计时等待',
type: 'timer',
child: child,
waitType: 'duration',
unit: 'PT%sS',
duration: 0,
timeDate: undefined
} as TimerNode
if (child) {
child.pid = id
}
}
const addApproval = (node: FlowNode) => {
const child = node.child
const id = nextId()
node.child = {
id: id,
pid: node.id,
type: 'approval',
name: '审批人',
child: child,
//
assigneeType: 'user',
formUser: '',
formRole: '',
users: [],
roles: [],
leader: 1,
orgLeader: 1,
choice: false,
self: false,
multi: 'sequential',
nobody: 'pass',
nobodyUsers: [],
formProperties: [],
operations: {
complete: true,
refuse: true,
back: true,
transfer: true,
delegate: true,
addMulti: false,
minusMulti: false
}
} as ApprovalNode
if (child) {
child.pid = id
}
}
const addNode = (type: NodeType, node: FlowNode) => {
const addMap: Recordable<(node: FlowNode) => void> = {
exclusive: addExclusive,
condition: addCondition,
cc: addCc,
timer: addTimer,
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 ('children' in next && next.child?.id !== del.id) {
const branchNode = next as BranchNode
const index = branchNode.children.findIndex((item) => item.id === del.id)
if (index !== -1) {
if (branchNode.children.length <= 2) {
delError(branchNode)
delNode(branchNode)
} else {
delError(del)
branchNode.children.splice(index, 1)
}
}
} else {
if (zoom.value < 170) {
zoom.value += 10
if (del.child && del.child.pid) {
del.child.pid = next.id
}
next.child = del.child
}
} else {
if (next.child) {
delNodeNext(next.child, del)
}
if ('children' in next) {
const nextBranch = next as BranchNode
if (nextBranch.children && nextBranch.children.length > 0) {
nextBranch.children.forEach((item) => {
delNodeNext(item, del)
})
}
}
}
}
const delError = (node: FlowNode) => {
delete nodesError.value[node.id]
if (node.child) {
delError(node.child)
}
if ('children' in node) {
const branchNode = node as BranchNode
if (branchNode.children && branchNode.children.length > 0) {
branchNode.children.forEach((item) => {
delError(item)
})
}
}
}
const validate = () => {
validateNodes()
return new Promise((resolve, reject) => {
const errors = Object.values(nodesError.value).flat()
if (errors.length > 0) {
reject(errors)
} else {
resolve(true)
}
})
}
const converterBpmn = () => {
const processModel = {
@ -66,45 +283,19 @@ const converterBpmn = () => {
name: '测试',
icon: {
name: 'el:HomeFilled',
color: '#409EFF',
color: '#409EFF'
},
process: process.value,
process: props.process,
enable: true,
version: 1,
sort: 0,
groupId: '',
remark: '',
remark: ''
}
downloadXml(processModel)
}
const downloadJson = () => {
const processModel = {
code: 'test',
name: '测试',
icon: {
name: 'el:HomeFilled',
color: '#409EFF',
},
process: process.value,
form: {
fields: fields.value
},
version: 1,
sort: 0,
groupId: '',
remark: '',
}
const blob = new Blob([JSON.stringify(processModel, null, 2)], {type: 'application/json'})
const a = document.createElement('a')
a.download = 'process.json'
a.href = URL.createObjectURL(blob)
a.click()
URL.revokeObjectURL(a.href)
}
// shift/
window.addEventListener('wheel', handleZoom)
//
onUnmounted(() => {
window.removeEventListener('wheel', handleZoom)
defineExpose({
validate
})
</script>
@ -112,48 +303,48 @@ onUnmounted(() => {
<div class="designer-container">
<div class="tool">
<el-switch
inline-prompt
:active-icon="Sunny"
:inactive-icon="Moon"
@change="handleToggleDark"
v-model="isDark"/>
inline-prompt
active-icon="Sunny"
inactive-icon="Moon"
@change="handleToggleDark"
v-model="isDark"
/>
</div>
<!--放大/缩小-->
<div class="zoom">
<el-button :icon="Plus" @click="zoom += 10" :disabled="zoom >= 170" circle></el-button>
<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-button :icon="Minus" @click="zoom -= 10" circle :disabled="zoom <= 50"></el-button>
<el-button @click="undo()" :disabled="!canUndo" :icon="TopLeft">撤销</el-button>
<el-button @click="redo()" :disabled="!canRedo" :icon="TopRight">重做</el-button>
<el-button @click="validate">校验</el-button>
<el-button @click="downloadJson" type="primary" :icon="Download">导出json</el-button>
<el-button @click="converterBpmn" type="primary" :icon="Download">转bpmn</el-button>
<el-tooltip content="缩小" placement="bottom-start">
<el-button icon="minus" @click="zoom -= 10" circle :disabled="zoom <= 50"></el-button>
</el-tooltip>
<el-button @click="converterBpmn" type="primary" icon="Download">转bpmn</el-button>
</div>
<!--流程树-->
<div class="node-container">
<NodeTree :node="process"/>
<TreeNode :node="process" @addNode="addNode" @delNode="delNode" @activeNode="openPenal" />
</div>
<!--属性面板-->
<NodePenal ref="nodePenalRef"/>
<Panel v-model="penalVisible" :active-data="activeData" />
</div>
</template>
<style scoped lang="scss">
.designer-container {
background-color: var(--el-bg-color);
position: relative;
display: flex;
flex-direction: row;
min-height: 100%;
min-width: 100%;
// overflow: scroll;
overflow: auto;
background-color: var(--el-bg-color-page);
.zoom {
position: fixed;
z-index: 999;
top: 20px;
right: 20px;
top: 30px;
right: 40px;
span {
margin: 0 10px;

View File

@ -0,0 +1,124 @@
<script setup lang="ts">
import type { PopoverInstance } from 'element-plus'
import type { NodeType } from './type'
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()
}
</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>
</el-space>
<template #reference>
<el-button 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;
}
}
.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>

View File

@ -1,4 +0,0 @@
import { FlowNode } from '../Node/index'
export interface AddNode extends FlowNode {
}

View File

@ -1,117 +0,0 @@
<script setup lang="ts">
import {type ElPopover} from 'element-plus'
import {ref} from "vue";
import {Stamp,Promotion,Share,Plus} from "@element-plus/icons-vue";
const nodePopoverRef = ref<InstanceType<typeof ElPopover>>()
const $emits = defineEmits<{
(e: 'addNode', type: string): void
}>()
const addApprovalNode = () => {
$emits('addNode', 'approval')
nodePopoverRef.value?.hide()
}
const addCcNode = () => {
$emits('addNode', 'cc')
nodePopoverRef.value?.hide()
}
const addExclusiveNode = () => {
$emits('addNode', 'exclusive')
nodePopoverRef.value?.hide()
}
</script>
<template>
<div class="add-but">
<el-popover placement="bottom-start" ref="nodePopoverRef" trigger="click" title="添加节点" :width="235">
<div class="node-select">
<div @click="addApprovalNode">
<el-icon color="#ff943e">
<Stamp/>
</el-icon>
<span>审批人</span>
</div>
<div @click="addCcNode">
<el-icon color="#3296fa">
<Promotion/>
</el-icon>
<span>抄送人</span>
</div>
<div @click="addExclusiveNode">
<el-icon>
<Share/>
</el-icon>
<span>互斥分支</span>
</div>
</div>
<template #reference>
<el-button
:icon="Plus"
type="primary"
style="z-index: 1"
circle></el-button>
</template>
</el-popover>
</div>
</template>
<style scoped lang="scss">
.node-select {
p {
font-size: 16px;
font-weight: 700;
margin-top: 2px;
margin-bottom: 5px;
}
div {
display: inline-block;
margin: 5px 5px;
cursor: pointer;
padding: 10px 15px;
border: var(--el-border);
border-radius: 10px;
width: 170px;
position: relative;
span {
position: absolute;
left: 65px;
top: 18px;
}
&:hover {
background-color: var(--el-color-primary-light-9);
box-shadow: var(--el-box-shadow-light);
color: var(--el-color-primary);
}
i {
font-size: 25px;
padding: 5px;
border: var(--el-border);
border-radius: 50%;
}
}
}
.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>

View File

@ -1,66 +0,0 @@
<script setup lang="ts">
import {ApprovalNode} from './index'
import {inject, Ref, ref, watchEffect} from "vue";
import {getList, User} from '~/api/modules/user'
import {getList as getRoles, Role} from '~/api/modules/role'
import {Field} from "~/components/Render/interface";
export interface ApprovalContentProps {
node: ApprovalNode
}
const $props = withDefaults(defineProps<ApprovalContentProps>(), {})
const content = ref<string>('')
const {fields} = inject<{
fields: Ref<Field[]>
}>('nodeHooks')!
watchEffect(() => {
const props = $props.node
if (props.assigneeType === 'choice') {
content.value = `发起人自选(${props.choice ? '多选' : '单选'}`
} else if (props.assigneeType === 'self') {
content.value = '发起人自己'
} else if (props.assigneeType === 'leader') {
content.value = props.leader === 1 ? '直属上级' : `${props.leader}级上级`
} else if (props.assigneeType === 'formUser') {
const title = fields.value.find(e => e.id === props.formUser)?.title || props.formUser || '?'
content.value = `表单内(${title})人员`
} else if (props.assigneeType === 'formRole') {
const title = fields.value.find(e => e.id === props.formRole)?.title || props.formRole || '?'
content.value = `表单内(${title})角色`
} else if (props.assigneeType === 'user') {
if (props.users.length > 0) {
getList(props.users).then(res => {
if(res.success){
content.value = res.data.map((item: User) => item.name).join('、')
}
})
} else {
content.value = '未指定人员'
}
} else if (props.assigneeType === 'role') {
if (props.roles.length > 0) {
getRoles(props.roles).then(res => {
if(res.success){
content.value = res.data.map((item: Role) => item.name).join('、')
}
})
} else {
content.value = '未指定角色'
}
} else {
content.value = $props.node.name
}
})
</script>
<template>
<el-text>
{{ content || node.name }}
</el-text>
</template>
<style scoped lang="scss">
</style>

View File

@ -1,29 +0,0 @@
import {FlowNode} from '../Node/index'
import {FormProperty, OperationPermissions} from "~/views/flowDesign/index";
export interface ApprovalNode extends FlowNode {
// 审批方式
assigneeType: 'user' | 'role' | 'choice' | 'self' | 'leader' | 'formUser' | 'formRole'
// 审批人
users: string[]
// 审批角色
roles: string[]
// 表单内人员
formUser: string
// 表单内角色
formRole: string
// 主管
leader: number
// 自选true-多选false-单选
choice: boolean
// 发起人自己
self: boolean
// 多人审批方式
multi: "sequential" | "joint" | "single"
// 审批人为空时处理方式reject-驳回admin-管理员pass-通过
nobody: 'reject' | 'pass'
// 表单字段
formProperties: FormProperty[]
// 操作权限
operations: OperationPermissions
}

View File

@ -1,76 +0,0 @@
<script setup lang="ts">
import Node from '../Node/index.vue'
import Content from './content.vue'
import {ApprovalNode} from './index'
import {ErrorInfo} from '../Node/index'
import {ref} from "vue";
export interface ApprovalProps {
node: ApprovalNode
}
const $props = withDefaults(defineProps<ApprovalProps>(), {})
const errorInfo = ref({
showError: false,
message: ''
})
const validate = (): ErrorInfo | undefined => {
errorInfo.value = {
showError: false,
message: ''
}
if ($props.node.assigneeType === 'user') {
if ($props.node.users.length === 0) {
errorInfo.value = {
showError: true,
message: `节点:[ ${$props.node.name} ] 未指定人员`
}
}
} else if ($props.node.assigneeType === 'role') {
if ($props.node.roles.length === 0) {
errorInfo.value = {
showError: true,
message: `节点:[ ${$props.node.name} ] 未指定角色`
}
}
} else if ($props.node.assigneeType === 'formUser') {
if (!$props.node.formRole) {
errorInfo.value = {
showError: true,
message: `节点:[ ${$props.node.name} ] 未指定表单内人员`
}
}
} else if ($props.node.assigneeType === 'formRole') {
if (!$props.node.formRole) {
errorInfo.value = {
showError: true,
message: `节点:[ ${$props.node.name} ] 未指定表单内角色`
}
}
} else if ($props.node.assigneeType === 'leader') {
if (!$props.node.leader) {
errorInfo.value = {
showError: true,
message: `节点:[ ${$props.node.name} ] 未指定多级上级`
}
}
}
return errorInfo.value
}
defineExpose({
validate
})
</script>
<template>
<Node icon="Stamp"
color="linear-gradient(89.96deg, #FA6F32 .05%, #FB9337 79.83%)"
:error-info="errorInfo"
:node="node">
<Content :node="node"/>
</Node>
</template>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,106 @@
<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>

View File

@ -1,5 +0,0 @@
import {FlowNode} from '../Node/index'
export interface BranchNode extends FlowNode{
children: FlowNode[];
}

View File

@ -1,129 +0,0 @@
<script setup lang="ts">
import NodeTree from '../index.vue'
import AddBut from '../Add/index.vue'
import {BranchNode} from './index'
import {FlowNode} from '../Node/index'
import {inject} from "vue";
export interface BranchProps {
node: BranchNode
}
withDefaults(defineProps<BranchProps>(), {})
const {addNode} = inject<{
addNode: (type: string, node: FlowNode) => void
}>('nodeHooks')!
</script>
<template>
<div class="branch-node">
<!--添加新分支按钮-->
<div class="add-branch">
<slot></slot>
</div>
<!--分支节点-->
<div v-for="(item,index) in node.children" :key="item.id" class="col-box">
<template v-if="node.children.length===(index+1)">
<div :class="`top-right-border`"></div>
<div :class="`bottom-right-border`"/>
</template>
<template v-if="index===0">
<div :class="`top-left-border`"></div>
<div :class="`bottom-left-border`"/>
</template>
<node-tree :node="item"/>
</div>
</div>
<!--添加节点-->
<add-but @add-node="(type:string)=>addNode(type,node)" class="branch-but"/>
</template>
<style scoped lang="scss">
.branch-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(--el-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: -2px;
height: 3px;
width: 50%;
background-color: var(--el-bg-color);
}
.bottom-left-border {
position: absolute;
left: 0;
bottom: -2px;
height: 3px;
width: 50%;
background-color: var(--el-bg-color);
}
.top-right-border {
position: absolute;
right: 0;
top: -2px;
height: 3px;
width: 50%;
background-color: var(--el-bg-color);
}
.bottom-right-border {
position: absolute;
right: 0;
bottom: -2px;
height: 3px;
width: 50%;
background-color: var(--el-bg-color);
}
}
}
.branch-but {
&:before {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
width: 1px;
height: 100%;
background-color: var(--el-border-color);
}
}
</style>

View File

@ -1,35 +0,0 @@
<script setup lang="ts">
import {CcNode} from './index'
import {ref, watchEffect} from "vue";
import {getList, User} from "~/api/modules/user";
export interface CcContentProps {
node: CcNode
}
const $props = withDefaults(defineProps<CcContentProps>(), {})
const content = ref<string>('')
watchEffect(() => {
const props = $props.node
if (props.users.length > 0) {
getList(props.users).then(res => {
if(res.success){
content.value = res.data.map((item: User) => item.name).join('、')
}
})
} else {
content.value = '未指定人员'
}
})
</script>
<template>
<el-text>
{{ content || node.name }}
</el-text>
</template>
<style scoped lang="scss">
</style>

View File

@ -1,7 +0,0 @@
import { FlowNode } from '../Node/index'
import {FormProperty} from "~/views/flowDesign/index";
export interface CcNode extends FlowNode {
users: string[]
// 表单字段
formProperties: FormProperty[]
}

View File

@ -1,47 +0,0 @@
<script setup lang="ts">
import Node from '../Node/index.vue'
import {CcNode} from '../Cc/index'
import Content from './content.vue'
import {ErrorInfo} from '../Node/index'
import {ref} from "vue";
export interface ApprovalProps {
node: CcNode
}
const $props = withDefaults(defineProps<ApprovalProps>(), {})
const errorInfo = ref<ErrorInfo>({
showError: false,
message: ''
})
/**
* 验证节点
*/
const validate = (): ErrorInfo | undefined => {
errorInfo.value = {
showError: false,
message: ''
}
if ($props.node.users.length === 0) {
errorInfo.value = {showError: true, message: `节点:[ ${$props.node.name} ] 未指定抄送人`}
}
return errorInfo.value
}
defineExpose({
validate
})
</script>
<template>
<Node icon="Promotion"
v-bind="$attrs"
color="#3296FA"
:error-info="errorInfo"
:node="node">
<Content :node="node"/>
</Node>
</template>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,82 @@
<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>

View File

@ -1,32 +0,0 @@
<script setup lang="tsx">
import {ConditionNode} from './index'
import {ref, VNode, watchEffect} from "vue";
export interface ConditionContentProps {
node: ConditionNode
}
const $props = withDefaults(defineProps<ConditionContentProps>(), {})
const showContent = ref<VNode>((<span></span>))
watchEffect(() => {
if ($props.node.def) {
return showContent.value = <span>不满足条件时进入默认条件</span>
} else if ($props.node.conditions.conditions.length > 0 || $props.node.conditions.groups.length > 0) {
const count = $props.node.conditions.conditions.length + $props.node.conditions.groups.length
showContent.value = <span>{`已设置(${count})个条件`}</span>
} else {
showContent.value = <span>未设置条件</span>
}
})
</script>
<template>
<el-text>
<showContent></showContent>
</el-text>
</template>
<style scoped lang="scss">
</style>

View File

@ -1,27 +0,0 @@
import { FlowNode } from '../Node/index'
export interface ConditionNode extends FlowNode {
def: boolean
conditions: FilterRules
}
/**
*
*/
export interface FilterRules {
logicalOperator: 'or' | 'and',
conditions: Condition[]
groups: FilterRules[]
}
/**
*
*/
export interface Condition {
// 筛选字段
field: string | null,
// 条件运算符
operator: string,
// 筛选值
value: any | null
}

View File

@ -1,54 +0,0 @@
<script setup lang="ts">
import Node from '../Node/index.vue'
import {ConditionNode} from '../Condition/index'
import Content from './content.vue'
import {ErrorInfo} from '../Node/index'
import {ref} from "vue";
export interface ApprovalProps {
node: ConditionNode
}
const $props = withDefaults(defineProps<ApprovalProps>(), {})
const errorInfo = ref<ErrorInfo>({
showError: false,
message: ''
})
/**
* 验证节点
*/
const validate = (): ErrorInfo | undefined => {
errorInfo.value = {
showError: false,
message: ''
}
if ($props.node.def) {
return undefined
} else if ($props.node.conditions.conditions.length === 0 && $props.node.conditions.groups.length === 0) {
errorInfo.value = {showError: true, message: `节点:[ ${$props.node.name} ] 未设置条件`}
}
return errorInfo.value
}
defineExpose({
validate
})
</script>
<template>
<div class="condition-box">
<Node icon="Share"
:readOnly="node.def"
:error-info="errorInfo"
:node="node">
<Content :node="node"/>
</Node>
</div>
</template>
<style scoped lang="scss">
.condition-box {
:deep(.node-box) {
padding: 30px 50px 0;
}
}
</style>

View File

@ -0,0 +1,53 @@
<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, child } = 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 (!child) {
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>

View File

@ -1,5 +0,0 @@
import {FlowNode} from '../Node/index'
export interface EndNode extends FlowNode {
}

View File

@ -1,37 +0,0 @@
<script setup lang="ts">
</script>
<template>
<div class="node-box">
<el-text class="end-node">
结束
</el-text>
</div>
</template>
<style scoped lang="scss">
.node-box {
padding: 0 50px 50px;
position: relative;
display: flex;
flex-direction: column;
cursor: pointer;
.end-node {
display: flex;
align-items: center;
justify-content: center;
width: 50px;
height: 50px;
border-radius: 50%;
font-size: 17px;
box-shadow: var(--el-box-shadow-light);
background-color: var(--el-color-primary);
&:hover {
box-shadow: 0 0 5px 0 var(--el-color-primary);
}
}
}
</style>

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