重构版
6
.env
@ -1,8 +1,2 @@
|
|||||||
# title
|
# title
|
||||||
VITE_GLOB_APP_TITLE=lowflow-design
|
VITE_GLOB_APP_TITLE=lowflow-design
|
||||||
|
|
||||||
# 本地运行端口号
|
|
||||||
VITE_PORT=8848
|
|
||||||
|
|
||||||
# 启动时自动打开浏览器
|
|
||||||
VITE_OPEN=true
|
|
||||||
|
|||||||
@ -1,6 +1,3 @@
|
|||||||
# 本地环境
|
|
||||||
VITE_USER_NODE_ENV=development
|
|
||||||
|
|
||||||
# 公共基础路径
|
# 公共基础路径
|
||||||
VITE_PUBLIC_PATH=/
|
VITE_PUBLIC_PATH=/
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,3 @@
|
|||||||
# 本地环境
|
|
||||||
VITE_USER_NODE_ENV=development
|
|
||||||
|
|
||||||
# 公共基础路径
|
# 公共基础路径
|
||||||
VITE_PUBLIC_PATH=/
|
VITE_PUBLIC_PATH=/
|
||||||
|
|
||||||
|
|||||||
78
.eslintrc-auto-import.json
Normal 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
@ -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
@ -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
|
node_modules
|
||||||
.DS_Store
|
.DS_Store
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
|
coverage
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
# lock
|
/cypress/videos/
|
||||||
yarn.lock
|
/cypress/screenshots/
|
||||||
package-lock.json
|
|
||||||
|
|
||||||
*.log
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
*.tsbuildinfo
|
||||||
|
|||||||
8
.prettierrc.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/prettierrc",
|
||||||
|
"semi": false,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100,
|
||||||
|
"trailingComma": "none"
|
||||||
|
}
|
||||||
7
.vscode/extensions.json
vendored
@ -1,4 +1,7 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
"recommendations": ["Vue.volar"]
|
"recommendations": [
|
||||||
|
"Vue.volar",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"esbenp.prettier-vscode"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
21
LICENSE
@ -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.
|
|
||||||
@ -5,7 +5,8 @@
|
|||||||
|
|
||||||
## 介绍
|
## 介绍
|
||||||
lowflow-design是一个基于`Vue3`,`Vite`,`TypeScript`,`Element-Plus`等技术栈开发的,适用于低代码或无代码开发平台的流程设计器。
|
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/
|
https://tsai996.github.io/lowflow-design/
|
||||||
### 成品案例
|
### 成品案例
|
||||||
@ -23,8 +24,9 @@ https://www.666cxf.com/
|
|||||||
|
|
||||||
## 扫码加入群聊,如果失效加微信拉入群聊(备注:加群)
|
## 扫码加入群聊,如果失效加微信拉入群聊(备注:加群)
|
||||||
<p>
|
<p>
|
||||||
<img alt="微信群" src="public/wxq.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.png" width="240" 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>
|
</p>
|
||||||
|
|
||||||
## 赞助
|
## 赞助
|
||||||
|
|||||||
10
env.d.ts
vendored
Normal 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
|
||||||
|
}
|
||||||
18
index.html
@ -1,23 +1,13 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/favicon.svg" />
|
<link rel="icon" href="/favicon.ico">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>lowflow-design</title>
|
<title>Vite App</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"
|
|
||||||
/> -->
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
<script>
|
|
||||||
window.setInterval(function(){
|
|
||||||
new Function('debugger')();
|
|
||||||
}, 100);
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -1,11 +0,0 @@
|
|||||||
import user from "./user";
|
|
||||||
import role from "./role";
|
|
||||||
|
|
||||||
const mockModules = [
|
|
||||||
...user,
|
|
||||||
...role,
|
|
||||||
];
|
|
||||||
export default mockModules
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
68
mock/role.ts
@ -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
|
|
||||||
84
mock/user.ts
@ -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
|
|
||||||
60
package.json
@ -1,38 +1,56 @@
|
|||||||
{
|
{
|
||||||
"name": "lowflow-design",
|
"name": "lowflow-design",
|
||||||
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
"build": "run-p type-check \"build-only {@}\" --",
|
||||||
"build:dev": "vite build --mode development",
|
"build:dev": "vite build --mode development",
|
||||||
"build:test": "vite build --mode test",
|
"build:test": "vite build --mode test",
|
||||||
"mock": "vite --mode mock --force",
|
|
||||||
"generate": "vite-ssg build",
|
|
||||||
"preview": "vite preview",
|
"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": {
|
"dependencies": {
|
||||||
"axios": "^1.5.1",
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
"element-plus": "^2.3.12",
|
"@vueuse/core": "^10.9.0",
|
||||||
|
"axios": "^1.6.8",
|
||||||
|
"element-plus": "^2.7.2",
|
||||||
"file-saver": "^2.0.5",
|
"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": {
|
"devDependencies": {
|
||||||
"@iconify-json/ep": "^1.1.12",
|
"@rushstack/eslint-patch": "^1.8.0",
|
||||||
"@types/file-saver": "^2.0.5",
|
"@tsconfig/node20": "^20.1.4",
|
||||||
"@types/node": "^20.6.0",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@vitejs/plugin-vue": "^4.3.4",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@vitejs/plugin-vue-jsx": "^3.0.1",
|
"@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",
|
"mockjs": "^1.1.0",
|
||||||
"sass": "^1.66.1",
|
"npm-run-all2": "^6.1.2",
|
||||||
"typescript": "^5.2.2",
|
"prettier": "^3.2.5",
|
||||||
"unocss": "^0.55.7",
|
"sass": "^1.77.0",
|
||||||
"unplugin-vue-components": "^0.25.2",
|
"typescript": "~5.4.0",
|
||||||
"vite": "^4.4.9",
|
"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-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-plugin-vue-setup-extend": "^0.4.0",
|
||||||
"vite-ssg": "^0.23.1",
|
"vue-tsc": "^2.0.11"
|
||||||
"vue-tsc": "^1.8.11"
|
}
|
||||||
},
|
|
||||||
"license": "MIT"
|
|
||||||
}
|
}
|
||||||
|
|||||||
7683
pnpm-lock.yaml
generated
|
Before Width: | Height: | Size: 267 KiB |
BIN
public/bpmn.png
|
Before Width: | Height: | Size: 27 KiB |
BIN
public/dark.png
|
Before Width: | Height: | Size: 40 KiB |
@ -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
|
After Width: | Height: | Size: 4.2 KiB |
@ -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/flow.png
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 108 KiB |
BIN
public/penal.png
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 132 KiB |
BIN
public/qq_qun.jpg
Normal file
|
After Width: | Height: | Size: 479 KiB |
@ -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
|
After Width: | Height: | Size: 139 KiB |
BIN
public/wx.png
|
Before Width: | Height: | Size: 85 KiB |
BIN
public/wx_qun.jpg
Normal file
|
After Width: | Height: | Size: 278 KiB |
BIN
public/wxq.png
|
Before Width: | Height: | Size: 168 KiB |
127
src/App.vue
@ -1,124 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-config-provider namespace="el" :locale="zhCn">
|
<el-config-provider namespace="el" :locale="zhCn">
|
||||||
<FlowDesign :process="process" :fields="fields"/>
|
<router-view></router-view>
|
||||||
</el-config-provider>
|
</el-config-provider>
|
||||||
</template>
|
</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 scoped>
|
||||||
<style>
|
#app {
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
103
src/api/index.ts
@ -1,38 +1,28 @@
|
|||||||
import axios, {
|
import axios, {
|
||||||
AxiosInstance,
|
type AxiosInstance,
|
||||||
AxiosError,
|
AxiosError,
|
||||||
AxiosRequestConfig,
|
type AxiosRequestConfig,
|
||||||
InternalAxiosRequestConfig,
|
type InternalAxiosRequestConfig,
|
||||||
AxiosResponse
|
type AxiosResponse
|
||||||
} from 'axios'
|
} from 'axios'
|
||||||
import {ElNotification} from "element-plus";
|
import { ElNotification } from 'element-plus'
|
||||||
|
|
||||||
export interface Result {
|
export interface Result {
|
||||||
code: number;
|
code: number
|
||||||
success: boolean;
|
success: boolean
|
||||||
message: string;
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResultData<T = any> extends Result {
|
export interface ResultData<T = any> extends Result {
|
||||||
data: T;
|
data: T
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResultPage<T = any> extends ResultData {
|
|
||||||
data: {
|
|
||||||
rows: T[];
|
|
||||||
total: number;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* axios配置
|
* axios配置
|
||||||
*/
|
*/
|
||||||
const config = {
|
const config = {
|
||||||
// 请求地址
|
baseURL: import.meta.env.VITE_API_URL,
|
||||||
baseURL: import.meta.env.VITE_API_URL as string,
|
timeout: 8000
|
||||||
// 设置超时时间
|
|
||||||
timeout: 8000,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class RequestHttp {
|
class RequestHttp {
|
||||||
@ -43,31 +33,24 @@ class RequestHttp {
|
|||||||
* @param config
|
* @param config
|
||||||
*/
|
*/
|
||||||
public constructor(config: AxiosRequestConfig) {
|
public constructor(config: AxiosRequestConfig) {
|
||||||
// 创建axios实例
|
|
||||||
this.service = axios.create(config)
|
this.service = axios.create(config)
|
||||||
// 请求拦截
|
|
||||||
this.service.interceptors.request.use(
|
this.service.interceptors.request.use(
|
||||||
(config: InternalAxiosRequestConfig) => {
|
(config: InternalAxiosRequestConfig) => {
|
||||||
// config.headers['Authorization'] = "Bearer 87779e5a-3342-4df6-865d-d8828800d6fb"
|
|
||||||
return config
|
return config
|
||||||
},
|
},
|
||||||
(error: AxiosError) => {
|
(error: AxiosError) => {
|
||||||
// 请求错误处理
|
|
||||||
return Promise.reject(error)
|
return Promise.reject(error)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
// 响应拦截
|
|
||||||
this.service.interceptors.response.use(
|
this.service.interceptors.response.use(
|
||||||
(response: AxiosResponse) => {
|
(response: AxiosResponse) => {
|
||||||
const { data } = response
|
const { data } = response
|
||||||
// 响应拦截处理
|
|
||||||
return data
|
return data
|
||||||
},
|
},
|
||||||
(error: AxiosError) => {
|
(error: AxiosError) => {
|
||||||
const { response, message } = error
|
const { response, message } = error
|
||||||
const data = response?.data as ResultData
|
const data = response?.data as ResultData
|
||||||
const errMsg = data ? data.message : message
|
const errMsg = data ? data.message : message
|
||||||
// 响应错误处理
|
|
||||||
ElNotification.error(errMsg || '未知错误')
|
ElNotification.error(errMsg || '未知错误')
|
||||||
return Promise.reject(response?.data || error)
|
return Promise.reject(response?.data || error)
|
||||||
}
|
}
|
||||||
@ -76,69 +59,27 @@ class RequestHttp {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* get请求
|
* get请求
|
||||||
* @param url 请求地址
|
* @param url
|
||||||
* @param params 请求参数
|
* @param params
|
||||||
|
* @param config
|
||||||
*/
|
*/
|
||||||
get<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {
|
get<T>(url: string, params?: object, config = {}): Promise<ResultData<T>> {
|
||||||
return this.service.get(url, {params, ..._object})
|
return this.service.get(url, { params, ...config })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* post请求
|
* post请求
|
||||||
* @param url 请求地址
|
* @param url
|
||||||
* @param data 请求参数
|
* @param data
|
||||||
|
* @param config
|
||||||
*/
|
*/
|
||||||
post<T>(url: string, data?: object): Promise<ResultData<T>> {
|
post<T>(url: string, data?: object, config = {}): Promise<ResultData<T>> {
|
||||||
return this.service.post(url, data)
|
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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请求
|
* request请求
|
||||||
* @param config 请求配置
|
* @param config
|
||||||
*/
|
*/
|
||||||
request<T>(config: AxiosRequestConfig): Promise<ResultData<T>> {
|
request<T>(config: AxiosRequestConfig): Promise<ResultData<T>> {
|
||||||
return this.service.request(config)
|
return this.service.request(config)
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import http from '~/api/index'
|
import http from '@/api'
|
||||||
import FileSaver from "file-saver";
|
import FileSaver from 'file-saver'
|
||||||
|
|
||||||
export const downloadXml = async (data: object) => {
|
export const downloadXml = async (data: object) => {
|
||||||
const res = await http.download('https://www.666cxf.com/api/model/download', data)
|
const res = await http.download('https://www.666cxf.com/api/model/download', data)
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import http from '~/api/index'
|
import http from '@/api/index'
|
||||||
|
|
||||||
export interface Role {
|
export interface Role {
|
||||||
id: string,
|
id: string
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import http from '~/api/index'
|
import http from '@/api/index'
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: string,
|
id: string
|
||||||
username: string,
|
username: string
|
||||||
name: string,
|
name: string
|
||||||
avatar: string
|
avatar: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1
src/assets/icons/add-user.svg
Normal 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 |
1
src/assets/icons/reduce-user.svg
Normal 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
@ -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 |
@ -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 |
@ -6,20 +6,20 @@ const $props = defineProps<{
|
|||||||
}>()
|
}>()
|
||||||
const operatorOptions = [
|
const operatorOptions = [
|
||||||
{
|
{
|
||||||
value: 'equal',
|
value: 'eq',
|
||||||
label: '等于'
|
label: '等于'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'not_equal',
|
value: 'ne',
|
||||||
label: '不等于'
|
label: '不等于'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '包含',
|
label: '包含',
|
||||||
value: 'contains'
|
value: 'in'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '不包含',
|
label: '不包含',
|
||||||
value: 'not_contain'
|
value: 'ni'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
const $emits = defineEmits<{
|
const $emits = defineEmits<{
|
||||||
@ -1,11 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {Field} from "~/components/Render/interface";
|
import type { Field } from '@/components/Render/type'
|
||||||
import {FilterRules} from "~/components/Condition/index";
|
import type { FilterRules } from '@/components/AdvancedFilter/type'
|
||||||
import {useVModel} from "@vueuse/core";
|
import { useVModel } from '@vueuse/core'
|
||||||
|
|
||||||
const $props = defineProps<{
|
const $props = defineProps<{
|
||||||
modelValue: any,
|
modelValue: any
|
||||||
options: Field[],
|
options: Field[]
|
||||||
filterRules: FilterRules
|
filterRules: FilterRules
|
||||||
}>()
|
}>()
|
||||||
const $emits = defineEmits<{
|
const $emits = defineEmits<{
|
||||||
@ -16,15 +16,8 @@ const data = useVModel($props, 'modelValue', $emits)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-select class="trigger-container" v-model="data" filterable placeholder="选择字段">
|
<el-select class="trigger-container" v-model="data" filterable placeholder="选择字段">
|
||||||
<el-option
|
<el-option v-for="item in $props.options" :key="item.id" :label="item.label" :value="item.id" />
|
||||||
v-for="item in $props.options"
|
|
||||||
:key="item.id"
|
|
||||||
:label="item.title"
|
|
||||||
:value="item.id"
|
|
||||||
/>
|
|
||||||
</el-select>
|
</el-select>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss"></style>
|
||||||
|
|
||||||
</style>
|
|
||||||
@ -1,14 +1,12 @@
|
|||||||
<script setup lang="ts" name="ConditionFilter">
|
<script setup lang="ts" name="AdvancedFilter">
|
||||||
import {FilterRules} from "~/components/Condition/index";
|
import type { FilterRules } from './type'
|
||||||
import {Field} from "~/components/Render/interface";
|
import type { Field } from '@/components/Render/type'
|
||||||
import {useVModel} from "@vueuse/core";
|
import { useVModel } from '@vueuse/core'
|
||||||
import {Delete, CirclePlus, CircleClose} from "@element-plus/icons-vue";
|
|
||||||
import Trigger from './Trigger.vue'
|
import Trigger from './Trigger.vue'
|
||||||
import Operator from './Operator.vue'
|
import Operator from './Operator.vue'
|
||||||
import Render from '~/components/Render/index'
|
|
||||||
|
|
||||||
const $props = defineProps<{
|
const $props = defineProps<{
|
||||||
filterFields: Field[],
|
filterFields: Field[]
|
||||||
modelValue: FilterRules
|
modelValue: FilterRules
|
||||||
}>()
|
}>()
|
||||||
const $emits = defineEmits<{
|
const $emits = defineEmits<{
|
||||||
@ -22,13 +20,11 @@ const filterRules = useVModel($props, 'modelValue', $emits)
|
|||||||
* 添加条件
|
* 添加条件
|
||||||
*/
|
*/
|
||||||
const addRule = () => {
|
const addRule = () => {
|
||||||
filterRules.value.conditions.push(
|
filterRules.value.conditions.push({
|
||||||
{
|
|
||||||
field: null,
|
field: null,
|
||||||
operator: 'equal',
|
operator: 'eq',
|
||||||
value: null
|
value: null
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 删除条件
|
* 删除条件
|
||||||
@ -46,12 +42,14 @@ const handleDel = (index: number) => {
|
|||||||
*/
|
*/
|
||||||
const addGroup = () => {
|
const addGroup = () => {
|
||||||
filterRules.value.groups.push({
|
filterRules.value.groups.push({
|
||||||
logicalOperator: 'and',
|
operator: 'and',
|
||||||
conditions: [{
|
conditions: [
|
||||||
|
{
|
||||||
field: null,
|
field: null,
|
||||||
operator: '',
|
operator: '',
|
||||||
value: null
|
value: null
|
||||||
}],
|
}
|
||||||
|
],
|
||||||
groups: []
|
groups: []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -69,9 +67,9 @@ const delGroup = (index: number) => {
|
|||||||
<div class="logical-operator">
|
<div class="logical-operator">
|
||||||
<div class="logical-operator__line"></div>
|
<div class="logical-operator__line"></div>
|
||||||
<el-switch
|
<el-switch
|
||||||
v-model="filterRules.logicalOperator"
|
v-model="filterRules.operator"
|
||||||
inline-prompt
|
inline-prompt
|
||||||
style="--el-switch-on-color: #409EFF; --el-switch-off-color: #67C23A"
|
style="--el-switch-on-color: #409eff; --el-switch-off-color: #67c23a"
|
||||||
active-value="and"
|
active-value="and"
|
||||||
inactive-value="or"
|
inactive-value="or"
|
||||||
active-text="且"
|
active-text="且"
|
||||||
@ -80,13 +78,17 @@ const delGroup = (index: number) => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="filter-option-content">
|
<div class="filter-option-content">
|
||||||
<el-form :label-width="0" :inline="true" :model="filterRules">
|
<el-form :label-width="0" :inline="true" :model="filterRules">
|
||||||
<el-row v-for="(item, index) in filterRules.conditions" :key="`${item.field}-${index}`" :gutter="5"
|
<el-row
|
||||||
class="filter-item-rule">
|
v-for="(item, index) in filterRules.conditions"
|
||||||
|
:key="`${item.field}-${index}`"
|
||||||
|
:gutter="5"
|
||||||
|
class="filter-item-rule"
|
||||||
|
>
|
||||||
<el-col :xs="24" :sm="7">
|
<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
|
<trigger
|
||||||
ref="triggerRef"
|
ref="triggerRef"
|
||||||
:options="$props.filterFields.filter(e=>e.value!==undefined)"
|
:options="$props.filterFields.filter((e) => e.value !== undefined)"
|
||||||
:filter-rules="filterRules"
|
:filter-rules="filterRules"
|
||||||
v-model="item.field"
|
v-model="item.field"
|
||||||
@update:model-value="item.value = null"
|
@update:model-value="item.value = null"
|
||||||
@ -94,49 +96,47 @@ const delGroup = (index: number) => {
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :xs="24" :sm="5" v-if="item.field">
|
<el-col :xs="24" :sm="5" v-if="item.field">
|
||||||
<el-form-item :prop="'conditions.' + index + '.operator'" style="width: 100%;">
|
<el-form-item :prop="'conditions.' + index + '.operator'" style="width: 100%">
|
||||||
<operator
|
<operator ref="operatorRef" v-model="item.operator" />
|
||||||
ref="operatorRef"
|
|
||||||
v-model="item.operator"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :xs="24" :sm="10" v-if="item.field">
|
<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
|
<Render
|
||||||
:field="$props.filterFields.find(e=>e.id===item.field)"
|
:field="$props.filterFields.find((e) => e.id === item.field) as Field"
|
||||||
v-model="item.value"
|
v-model="item.value"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :xs="24" :sm="2" style="display: flex;align-items: center;flex-direction: row-reverse;">
|
<el-col
|
||||||
<el-button
|
:xs="24"
|
||||||
plain circle
|
:sm="2"
|
||||||
type="danger"
|
style="display: flex; align-items: center; flex-direction: row-reverse"
|
||||||
:icon="Delete"
|
>
|
||||||
@click="handleDel(index)"
|
<el-button plain circle type="danger" icon="Delete" @click="handleDel(index)" />
|
||||||
/>
|
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
<ConditionFilter
|
<AdvancedFilter
|
||||||
v-for="(item, index) in filterRules.groups"
|
v-for="(item, index) in filterRules.groups"
|
||||||
:key="index"
|
:key="index"
|
||||||
@delGroup="delGroup(index)"
|
@delGroup="delGroup(index)"
|
||||||
v-model="filterRules.groups[index]"
|
v-model="filterRules.groups[index]"
|
||||||
:filterFields="filterFields"
|
: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>
|
</el-button>
|
||||||
</ConditionFilter>
|
</AdvancedFilter>
|
||||||
<div v-if="filterRules.groups.length===0 && filterRules.conditions.length===0"
|
<div
|
||||||
class="filter-item-rule"/>
|
v-if="filterRules.groups.length === 0 && filterRules.conditions.length === 0"
|
||||||
|
class="filter-item-rule"
|
||||||
|
/>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div class="filter-item-rule">
|
<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>
|
||||||
<el-button @click="addGroup" :icon="CirclePlus" class="filter-filter-item__add">
|
<el-button @click="addGroup" icon="CirclePlus" class="filter-filter-item__add">
|
||||||
添加条件组
|
添加条件组
|
||||||
</el-button>
|
</el-button>
|
||||||
<slot />
|
<slot />
|
||||||
@ -182,7 +182,7 @@ const delGroup = (index: number) => {
|
|||||||
height: calc(100% - 48px);
|
height: calc(100% - 48px);
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: "";
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
@ -194,7 +194,7 @@ const delGroup = (index: number) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: "";
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
20
src/components/AdvancedFilter/type.ts
Normal 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[]
|
||||||
|
}
|
||||||
@ -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[]
|
|
||||||
}
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import {defineAsyncComponent, defineComponent, h, PropType, resolveComponent} from "vue";
|
|
||||||
import { cloneDeep } from 'lodash-es'
|
import { cloneDeep } from 'lodash-es'
|
||||||
import {Field} from "./interface";
|
import type { Field } from './type'
|
||||||
|
import type { PropType } from 'vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
@ -16,13 +16,13 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
emits: ['update:modelValue'],
|
emits: ['update:modelValue'],
|
||||||
components: {
|
components: {
|
||||||
Input: defineAsyncComponent(() => import('element-plus/es').then(({ElInput}) => ElInput)),
|
ElInput: defineAsyncComponent(() => import('element-plus/es').then(({ElInput}) => ElInput)),
|
||||||
Number: defineAsyncComponent(() => import('element-plus/es').then(({ElInputNumber}) => ElInputNumber)),
|
ElInputNumber: defineAsyncComponent(() => import('element-plus/es').then(({ElInputNumber}) => ElInputNumber)),
|
||||||
Select: defineAsyncComponent(() => import('element-plus/es').then(({ElSelect}) => ElSelect)),
|
ElSelect: defineAsyncComponent(() => import('element-plus/es').then(({ElSelect}) => ElSelect)),
|
||||||
Radio: defineAsyncComponent(() => import('element-plus/es').then(({ElRadio}) => ElRadio)),
|
ElRadio: defineAsyncComponent(() => import('element-plus/es').then(({ElRadio}) => ElRadio)),
|
||||||
Checkbox: defineAsyncComponent(() => import('element-plus/es').then(({ElCheckbox}) => ElCheckbox)),
|
ElCheckbox: defineAsyncComponent(() => import('element-plus/es').then(({ElCheckbox}) => ElCheckbox)),
|
||||||
UserSelection: defineAsyncComponent(() => import('~/components/UserSelection/index')),
|
UserSelector: defineAsyncComponent(() => import('@/components/UserSelector/index.vue')),
|
||||||
RoleSelection: defineAsyncComponent(() => import('~/components/RoleSelection/index'))
|
RoleSelector: defineAsyncComponent(() => import('@/components/RoleSelector/index.vue'))
|
||||||
},
|
},
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
/**
|
/**
|
||||||
@ -32,7 +32,7 @@ export default defineComponent({
|
|||||||
const buildProps = (fieldClone: Field) => {
|
const buildProps = (fieldClone: Field) => {
|
||||||
const dataObject: Record<string, any> = {}
|
const dataObject: Record<string, any> = {}
|
||||||
const _props = fieldClone.props || {}
|
const _props = fieldClone.props || {}
|
||||||
Object.keys(_props).forEach(key => {
|
Object.keys(_props).forEach((key) => {
|
||||||
dataObject[key] = _props[key]
|
dataObject[key] = _props[key]
|
||||||
})
|
})
|
||||||
if (props.modelValue !== undefined) {
|
if (props.modelValue !== undefined) {
|
||||||
@ -51,19 +51,19 @@ export default defineComponent({
|
|||||||
* @param fieldClone
|
* @param fieldClone
|
||||||
*/
|
*/
|
||||||
const buildSlots = (fieldClone: Field) => {
|
const buildSlots = (fieldClone: Field) => {
|
||||||
let children: Record<string, any> = {}
|
const children: Record<string, any> = {}
|
||||||
const slotFunctions: Record<string, any> = {
|
const slotFunctions: Record<string, any> = {
|
||||||
Select: (conf: Field) => {
|
ElSelect: (conf: Field) => {
|
||||||
return conf.props.options.map((item: any) => {
|
return conf.props.options.map((item: any) => {
|
||||||
return <el-option label={item.label} value={item.value}></el-option>
|
return <el-option label={item.label} value={item.value}></el-option>
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
Radio: (conf: Field) => {
|
ElRadio: (conf: Field) => {
|
||||||
return conf.props.options.map((item: any) => {
|
return conf.props.options.map((item: any) => {
|
||||||
return <el-radio label={item.value}>{item.label}</el-radio>
|
return <el-radio label={item.value}>{item.label}</el-radio>
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
Checkbox: (conf: Field) => {
|
ElCheckbox: (conf: Field) => {
|
||||||
return conf.props.options.map((item: any) => {
|
return conf.props.options.map((item: any) => {
|
||||||
return <el-checkbox label={item.value}>{item.label}</el-checkbox>
|
return <el-checkbox label={item.value}>{item.label}</el-checkbox>
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
export interface Field {
|
|
||||||
id: string,
|
|
||||||
title: string,
|
|
||||||
name: string,
|
|
||||||
value: any,
|
|
||||||
props: Record<string, any>
|
|
||||||
}
|
|
||||||
12
src/components/Render/type.ts
Normal 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[]
|
||||||
|
}
|
||||||
@ -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
|
|
||||||
@ -1,15 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useVModel } from '@vueuse/core'
|
import { useVModel } from '@vueuse/core'
|
||||||
import {TreeNodeData} from 'element-plus/es/components/tree/src/tree.type'
|
import type { TreeNodeData } from 'element-plus/es/components/tree/src/tree.type'
|
||||||
import { type ElTree } from 'element-plus'
|
import { type ElTree } from 'element-plus'
|
||||||
import {reactive, ref, watch} from "vue";
|
import { getList } from '@/api/modules/role'
|
||||||
import {getList} from "~/api/modules/role";
|
|
||||||
import {School, Check} from "@element-plus/icons-vue";
|
|
||||||
|
|
||||||
export type ModelValueType = string | string[] | null | undefined
|
export type ModelValueType = string | string[] | null | undefined
|
||||||
|
|
||||||
export interface RoleDropdownProps {
|
export interface RoleDropdownProps {
|
||||||
modelValue: ModelValueType,
|
modelValue: ModelValueType
|
||||||
multiple?: boolean
|
multiple?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,7 +39,7 @@ const orgTreeRef = ref<InstanceType<typeof ElTree>>()
|
|||||||
const expandedKeys = ref<string[]>([])
|
const expandedKeys = ref<string[]>([])
|
||||||
|
|
||||||
const renderClass = (role: TreeNodeData): string | { [key: string]: boolean } => {
|
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) {
|
if (val) {
|
||||||
return 'is-active'
|
return 'is-active'
|
||||||
} else {
|
} else {
|
||||||
@ -51,7 +49,7 @@ const renderClass = (role: TreeNodeData): string | { [key: string]: boolean } =>
|
|||||||
|
|
||||||
const onNodeClick = (data: Role) => {
|
const onNodeClick = (data: Role) => {
|
||||||
if ($props.multiple) {
|
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) {
|
if (index === -1) {
|
||||||
roleOptions.value.push(data)
|
roleOptions.value.push(data)
|
||||||
roleOptions.value.sort((a, b) => a.id.localeCompare(b.id))
|
roleOptions.value.sort((a, b) => a.id.localeCompare(b.id))
|
||||||
@ -59,7 +57,7 @@ const onNodeClick = (data: Role) => {
|
|||||||
roleOptions.value.splice(index, 1)
|
roleOptions.value.splice(index, 1)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const index = roleOptions.value.findIndex(e => e.id === data.id)
|
const index = roleOptions.value.findIndex((e) => e.id === data.id)
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
roleOptions.value = [data]
|
roleOptions.value = [data]
|
||||||
} else {
|
} else {
|
||||||
@ -69,12 +67,15 @@ const onNodeClick = (data: Role) => {
|
|||||||
}
|
}
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
const queryForm = reactive({
|
const queryForm = reactive({
|
||||||
name: null,
|
name: null
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(() => queryForm.name, (val) => {
|
watch(
|
||||||
|
() => queryForm.name,
|
||||||
|
(val) => {
|
||||||
orgTreeRef.value?.filter(val)
|
orgTreeRef.value?.filter(val)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
const filterNode = (value: string, data: TreeNodeData): boolean => {
|
const filterNode = (value: string, data: TreeNodeData): boolean => {
|
||||||
if (!value) return true
|
if (!value) return true
|
||||||
return data.name.includes(value)
|
return data.name.includes(value)
|
||||||
@ -83,9 +84,9 @@ const open = () => {
|
|||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
}
|
}
|
||||||
const onOpen = () => {
|
const onOpen = () => {
|
||||||
getList().then(res => {
|
getList().then((res) => {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
roleOrgOptions.value = res.data.map(e => {
|
roleOrgOptions.value = res.data.map((e) => {
|
||||||
return {
|
return {
|
||||||
id: e.id,
|
id: e.id,
|
||||||
name: e.name
|
name: e.name
|
||||||
@ -94,7 +95,6 @@ const onOpen = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
let roleIds: string[] = []
|
let roleIds: string[] = []
|
||||||
if (Array.isArray(value.value)) {
|
if (Array.isArray(value.value)) {
|
||||||
roleIds.push(...value.value)
|
roleIds.push(...value.value)
|
||||||
@ -102,9 +102,9 @@ const onOpen = () => {
|
|||||||
roleIds.push(value.value)
|
roleIds.push(value.value)
|
||||||
}
|
}
|
||||||
if (roleIds.length > 0) {
|
if (roleIds.length > 0) {
|
||||||
getList(roleIds).then(res => {
|
getList(roleIds).then((res) => {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
roleOptions.value = res.data.map(role => {
|
roleOptions.value = res.data.map((role) => {
|
||||||
return {
|
return {
|
||||||
id: role.id,
|
id: role.id,
|
||||||
name: role.name
|
name: role.name
|
||||||
@ -119,7 +119,7 @@ const onOpen = () => {
|
|||||||
}
|
}
|
||||||
const handelConfirm = () => {
|
const handelConfirm = () => {
|
||||||
if ($props.multiple) {
|
if ($props.multiple) {
|
||||||
value.value = roleOptions.value.map(e => e.id)
|
value.value = roleOptions.value.map((e) => e.id)
|
||||||
} else {
|
} else {
|
||||||
if (roleOptions.value.length > 0) {
|
if (roleOptions.value.length > 0) {
|
||||||
value.value = roleOptions.value[0].id
|
value.value = roleOptions.value[0].id
|
||||||
@ -150,7 +150,8 @@ defineExpose({
|
|||||||
placeholder="输入关键字进行查询"
|
placeholder="输入关键字进行查询"
|
||||||
:style="{ width: '100%' }"
|
:style="{ width: '100%' }"
|
||||||
suffix-icon="search"
|
suffix-icon="search"
|
||||||
clearable>
|
clearable
|
||||||
|
>
|
||||||
</el-input>
|
</el-input>
|
||||||
</template>
|
</template>
|
||||||
<el-scrollbar tag="div" class="org-tree">
|
<el-scrollbar tag="div" class="org-tree">
|
||||||
@ -161,7 +162,8 @@ defineExpose({
|
|||||||
:default-expanded-keys="expandedKeys"
|
:default-expanded-keys="expandedKeys"
|
||||||
:props="treeProps"
|
:props="treeProps"
|
||||||
:filter-node-method="filterNode"
|
:filter-node-method="filterNode"
|
||||||
@node-click="onNodeClick">
|
@node-click="onNodeClick"
|
||||||
|
>
|
||||||
<template #default="{ data }">
|
<template #default="{ data }">
|
||||||
<div class="flex flex-1 flex-items-center flex-justify-between">
|
<div class="flex flex-1 flex-items-center flex-justify-between">
|
||||||
<div class="flex-center">
|
<div class="flex-center">
|
||||||
@ -1,10 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {getById} from '~/api/modules/role'
|
import { getById } from '@/api/modules/role'
|
||||||
import {componentSizeMap, useFormSize} from 'element-plus'
|
|
||||||
import {computed, onMounted, reactive} from "vue";
|
|
||||||
|
|
||||||
export interface RoleTagProps {
|
export interface RoleTagProps {
|
||||||
id: string,
|
id: string
|
||||||
type?: 'success' | 'info' | 'warning' | 'danger'
|
type?: 'success' | 'info' | 'warning' | 'danger'
|
||||||
closable?: boolean
|
closable?: boolean
|
||||||
}
|
}
|
||||||
@ -30,34 +28,20 @@ onMounted(() => {
|
|||||||
if (!$props.id) {
|
if (!$props.id) {
|
||||||
throw new Error('username is required')
|
throw new Error('username is required')
|
||||||
}
|
}
|
||||||
getById($props.id).then(res => {
|
getById($props.id).then((res) => {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
console.log(res);
|
|
||||||
roleInfo.id = res.data.id
|
roleInfo.id = res.data.id
|
||||||
roleInfo.name = res.data.name
|
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 = () => {
|
const onClose = () => {
|
||||||
$emits('close', $props.id)
|
$emits('close', $props.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<el-tag round :closable="$props.closable" :type="type" effect="light" @close="onClose">
|
<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>
|
<span>{{ roleInfo.name || id }}</span>
|
||||||
</div>
|
</div>
|
||||||
</el-tag>
|
</el-tag>
|
||||||
@ -1,16 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useVModel } from '@vueuse/core'
|
import { useVModel } from '@vueuse/core'
|
||||||
import RoleTag from './RoleTag.vue'
|
import RoleTag from './RoleTag.vue'
|
||||||
import RolePicker, {ModelValueType} from './RolePicker.vue'
|
import RolePicker, { type ModelValueType } from './RolePicker.vue'
|
||||||
import { useFormDisabled, useFormSize } from 'element-plus'
|
import { useFormDisabled, useFormSize } from 'element-plus'
|
||||||
import {computed, CSSProperties, ref} from 'vue'
|
import type { CSSProperties } from 'vue'
|
||||||
import {Avatar} from "@element-plus/icons-vue";
|
|
||||||
|
|
||||||
export interface RoleSelectorProps {
|
export interface RoleSelectorProps {
|
||||||
modelValue: ModelValueType,
|
modelValue: ModelValueType
|
||||||
placeholder?: string,
|
placeholder?: string
|
||||||
multiple?: boolean,
|
multiple?: boolean
|
||||||
disabled?: boolean,
|
disabled?: boolean
|
||||||
style?: CSSProperties
|
style?: CSSProperties
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,12 +48,24 @@ const onClose = (username: string) => {
|
|||||||
<template>
|
<template>
|
||||||
<role-picker ref="RolePickerRef" :multiple="multiple" v-model="value" />
|
<role-picker ref="RolePickerRef" :multiple="multiple" v-model="value" />
|
||||||
<div class="role-wrapper">
|
<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>
|
<el-icon>
|
||||||
<Avatar />
|
<Avatar />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
</el-button>
|
</el-button>
|
||||||
<RoleTag v-for="item in valueArr" :closable="!disabled" :key="item" :id="item" @close="onClose"/>
|
<RoleTag
|
||||||
|
v-for="item in valueArr"
|
||||||
|
:closable="!disabled"
|
||||||
|
:key="item"
|
||||||
|
:id="item"
|
||||||
|
@close="onClose"
|
||||||
|
/>
|
||||||
<el-text v-show="!value || value.length === 0" class="placeholder">
|
<el-text v-show="!value || value.length === 0" class="placeholder">
|
||||||
{{ placeholder }}
|
{{ placeholder }}
|
||||||
</el-text>
|
</el-text>
|
||||||
@ -1,5 +0,0 @@
|
|||||||
import Segmented from './src/index.vue'
|
|
||||||
|
|
||||||
export default Segmented
|
|
||||||
|
|
||||||
export * from './src/index.vue'
|
|
||||||
7
src/components/SvgIcon/index.scss
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.svg-icon {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
vertical-align: -0.15em;
|
||||||
|
fill: currentColor;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
66
src/components/SvgIcon/index.tsx
Normal 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
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -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
|
|
||||||
@ -1,16 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useVModel } from '@vueuse/core'
|
import { useVModel } from '@vueuse/core'
|
||||||
import {TreeNodeData} from 'element-plus/es/components/tree/src/tree.type'
|
import type { TreeNodeData } from 'element-plus/es/components/tree/src/tree.type'
|
||||||
import {type ElTree} from 'element-plus'
|
import { getList } from '@/api/modules/user'
|
||||||
import {reactive, ref, watch} from "vue";
|
import type { TreeInstance } from 'element-plus'
|
||||||
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";
|
|
||||||
|
|
||||||
export type ModelValueType = string | string[] | null | undefined
|
export type ModelValueType = string | string[] | null | undefined
|
||||||
|
|
||||||
export interface UserDropdownProps {
|
export interface UserDropdownProps {
|
||||||
modelValue: ModelValueType,
|
modelValue: ModelValueType
|
||||||
multiple?: boolean
|
multiple?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,11 +38,11 @@ const value = useVModel($props, 'modelValue', $emits)
|
|||||||
|
|
||||||
const userOptions = ref<Org[]>([])
|
const userOptions = ref<Org[]>([])
|
||||||
const userOrgOptions = ref<Org[]>([])
|
const userOrgOptions = ref<Org[]>([])
|
||||||
const orgTreeRef = ref<InstanceType<typeof ElTree>>()
|
const orgTreeRef = ref<TreeInstance>()
|
||||||
const expandedKeys = ref<string[]>([])
|
const expandedKeys = ref<string[]>([])
|
||||||
|
|
||||||
const renderClass = (org: TreeNodeData): string | { [key: string]: boolean } => {
|
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) {
|
if (val) {
|
||||||
return 'is-active'
|
return 'is-active'
|
||||||
} else {
|
} else {
|
||||||
@ -56,7 +53,7 @@ const renderClass = (org: TreeNodeData): string | { [key: string]: boolean } =>
|
|||||||
const onNodeClick = (data: Org) => {
|
const onNodeClick = (data: Org) => {
|
||||||
if (data.type !== 'user') return
|
if (data.type !== 'user') return
|
||||||
if ($props.multiple) {
|
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) {
|
if (index === -1) {
|
||||||
userOptions.value.push(data)
|
userOptions.value.push(data)
|
||||||
userOptions.value.sort((a, b) => a.id.localeCompare(b.id))
|
userOptions.value.sort((a, b) => a.id.localeCompare(b.id))
|
||||||
@ -64,7 +61,7 @@ const onNodeClick = (data: Org) => {
|
|||||||
userOptions.value.splice(index, 1)
|
userOptions.value.splice(index, 1)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const index = userOptions.value.findIndex(e => e.id === data.id)
|
const index = userOptions.value.findIndex((e) => e.id === data.id)
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
userOptions.value = [data]
|
userOptions.value = [data]
|
||||||
} else {
|
} else {
|
||||||
@ -74,12 +71,15 @@ const onNodeClick = (data: Org) => {
|
|||||||
}
|
}
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
const queryForm = reactive({
|
const queryForm = reactive({
|
||||||
name: null,
|
name: null
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(() => queryForm.name, (val) => {
|
watch(
|
||||||
|
() => queryForm.name,
|
||||||
|
(val) => {
|
||||||
orgTreeRef.value?.filter(val)
|
orgTreeRef.value?.filter(val)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
const filterNode = (value: string, data: TreeNodeData): boolean => {
|
const filterNode = (value: string, data: TreeNodeData): boolean => {
|
||||||
if (!value) return true
|
if (!value) return true
|
||||||
return data.name.includes(value)
|
return data.name.includes(value)
|
||||||
@ -88,9 +88,9 @@ const open = () => {
|
|||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
}
|
}
|
||||||
const onOpen = () => {
|
const onOpen = () => {
|
||||||
getList().then(res => {
|
getList().then((res) => {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
userOrgOptions.value = res.data.map(e => {
|
userOrgOptions.value = res.data.map((e) => {
|
||||||
return {
|
return {
|
||||||
id: e.username,
|
id: e.username,
|
||||||
name: e.name,
|
name: e.name,
|
||||||
@ -102,7 +102,6 @@ const onOpen = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
let userIds: string[] = []
|
let userIds: string[] = []
|
||||||
if (Array.isArray(value.value)) {
|
if (Array.isArray(value.value)) {
|
||||||
userIds.push(...value.value)
|
userIds.push(...value.value)
|
||||||
@ -110,9 +109,9 @@ const onOpen = () => {
|
|||||||
userIds.push(value.value)
|
userIds.push(value.value)
|
||||||
}
|
}
|
||||||
if (userIds.length > 0) {
|
if (userIds.length > 0) {
|
||||||
getList(userIds).then(res => {
|
getList(userIds).then((res) => {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
userOptions.value = res.data.map(user => {
|
userOptions.value = res.data.map((user) => {
|
||||||
return {
|
return {
|
||||||
id: user.username,
|
id: user.username,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
@ -130,7 +129,7 @@ const onOpen = () => {
|
|||||||
}
|
}
|
||||||
const handelConfirm = () => {
|
const handelConfirm = () => {
|
||||||
if ($props.multiple) {
|
if ($props.multiple) {
|
||||||
value.value = userOptions.value.map(e => e.id)
|
value.value = userOptions.value.map((e) => e.id)
|
||||||
} else {
|
} else {
|
||||||
if (userOptions.value.length > 0) {
|
if (userOptions.value.length > 0) {
|
||||||
value.value = userOptions.value[0].id
|
value.value = userOptions.value[0].id
|
||||||
@ -161,7 +160,8 @@ defineExpose({
|
|||||||
placeholder="输入关键字进行查询"
|
placeholder="输入关键字进行查询"
|
||||||
:style="{ width: '100%' }"
|
:style="{ width: '100%' }"
|
||||||
suffix-icon="search"
|
suffix-icon="search"
|
||||||
clearable>
|
clearable
|
||||||
|
>
|
||||||
</el-input>
|
</el-input>
|
||||||
</template>
|
</template>
|
||||||
<el-scrollbar tag="div" class="org-tree">
|
<el-scrollbar tag="div" class="org-tree">
|
||||||
@ -172,7 +172,8 @@ defineExpose({
|
|||||||
:default-expanded-keys="expandedKeys"
|
:default-expanded-keys="expandedKeys"
|
||||||
:props="treeProps"
|
:props="treeProps"
|
||||||
:filter-node-method="filterNode"
|
:filter-node-method="filterNode"
|
||||||
@node-click="onNodeClick">
|
@node-click="onNodeClick"
|
||||||
|
>
|
||||||
<template #default="{ data }">
|
<template #default="{ data }">
|
||||||
<div class="flex flex-1 flex-items-center flex-justify-between">
|
<div class="flex flex-1 flex-items-center flex-justify-between">
|
||||||
<div class="flex-center">
|
<div class="flex-center">
|
||||||
@ -1,10 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {getByUsername} from '~/api/modules/user'
|
import { getByUsername } from '@/api/modules/user'
|
||||||
import { componentSizeMap, useFormSize } from 'element-plus'
|
import { componentSizeMap, useFormSize } from 'element-plus'
|
||||||
import {computed, onMounted, reactive} from "vue";
|
|
||||||
|
|
||||||
export interface UserTagProps {
|
export interface UserTagProps {
|
||||||
username: string,
|
username: string
|
||||||
type?: 'success' | 'info' | 'warning' | 'danger'
|
type?: 'success' | 'info' | 'warning' | 'danger'
|
||||||
closable?: boolean
|
closable?: boolean
|
||||||
}
|
}
|
||||||
@ -32,7 +31,7 @@ onMounted(() => {
|
|||||||
if (!$props.username) {
|
if (!$props.username) {
|
||||||
throw new Error('username is required')
|
throw new Error('username is required')
|
||||||
}
|
}
|
||||||
getByUsername($props.username).then(res => {
|
getByUsername($props.username).then((res) => {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
userInfo.username = res.data.username
|
userInfo.username = res.data.username
|
||||||
userInfo.avatar = res.data.avatar
|
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 formSize = useFormSize()
|
||||||
const getComponentSize = computed(() => {
|
const getComponentSize = computed(() => {
|
||||||
return componentSizeMap[formSize.value || 'default'] - 12
|
return componentSizeMap[formSize.value || 'default'] - 12
|
||||||
@ -55,11 +46,10 @@ const getComponentSize = computed(() => {
|
|||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
$emits('close', $props.username)
|
$emits('close', $props.username)
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<el-tag round :closable="$props.closable" :type="type" effect="light" @close="onClose">
|
<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">
|
<el-avatar :size="getComponentSize" :src="userInfo.avatar">
|
||||||
{{ (userInfo.name || username).charAt(0) }}
|
{{ (userInfo.name || username).charAt(0) }}
|
||||||
</el-avatar>
|
</el-avatar>
|
||||||
@ -1,16 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useVModel } from '@vueuse/core'
|
import { useVModel } from '@vueuse/core'
|
||||||
import UserTag from './UserTag.vue'
|
import UserTag from './UserTag.vue'
|
||||||
import UserPicker, {ModelValueType} from './UserPicker.vue'
|
import UserPicker, { type ModelValueType } from './UserPicker.vue'
|
||||||
import { useFormDisabled, useFormSize } from 'element-plus'
|
import { useFormDisabled, useFormSize } from 'element-plus'
|
||||||
import {computed, CSSProperties, ref} from 'vue'
|
import { type CSSProperties } from 'vue'
|
||||||
import {User} from "@element-plus/icons-vue";
|
|
||||||
|
|
||||||
export interface UserSelectorProps {
|
export interface UserSelectorProps {
|
||||||
modelValue: ModelValueType,
|
modelValue: ModelValueType
|
||||||
placeholder?: string,
|
placeholder?: string
|
||||||
multiple?: boolean,
|
multiple?: boolean
|
||||||
disabled?: boolean,
|
disabled?: boolean
|
||||||
style?: CSSProperties
|
style?: CSSProperties
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,12 +48,22 @@ const onClose = (username: string) => {
|
|||||||
<template>
|
<template>
|
||||||
<user-picker ref="userPickerRef" :multiple="multiple" v-model="value" />
|
<user-picker ref="userPickerRef" :multiple="multiple" v-model="value" />
|
||||||
<div class="user-wrapper">
|
<div class="user-wrapper">
|
||||||
<el-button class="user-but-item" :size="formSize" :disabled="disabled" @click="openUserPicker" circle>
|
<el-button
|
||||||
<el-icon>
|
class="user-but-item"
|
||||||
<User/>
|
:size="formSize"
|
||||||
</el-icon>
|
:disabled="disabled"
|
||||||
|
@click="openUserPicker"
|
||||||
|
circle
|
||||||
|
>
|
||||||
|
<svg-icon name="add-user" />
|
||||||
</el-button>
|
</el-button>
|
||||||
<user-tag v-for="item in valueArr" :closable="!disabled" :key="item" :username="item" @close="onClose"/>
|
<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">
|
<el-text v-show="!value || value.length === 0" class="placeholder">
|
||||||
{{ placeholder }}
|
{{ placeholder }}
|
||||||
</el-text>
|
</el-text>
|
||||||
@ -1,4 +0,0 @@
|
|||||||
import { useDark, useToggle } from "@vueuse/core";
|
|
||||||
|
|
||||||
export const isDark = useDark();
|
|
||||||
export const toggleDark = useToggle(isDark);
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./dark";
|
|
||||||
21
src/env.d.ts
vendored
@ -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
|
|
||||||
}
|
|
||||||
37
src/main.ts
@ -1,23 +1,20 @@
|
|||||||
import { createApp } from "vue";
|
import { createApp } from 'vue'
|
||||||
import App from "./App.vue";
|
import { createPinia } from 'pinia'
|
||||||
|
import App from './App.vue'
|
||||||
// import "~/styles/element/index.scss";
|
import router from './router'
|
||||||
|
import 'virtual:svg-icons-register'
|
||||||
// import ElementPlus from "element-plus";
|
import 'uno.css'
|
||||||
// import all element css, uncommented next line
|
import '@/styles/index.scss'
|
||||||
// import "element-plus/dist/index.css";
|
|
||||||
|
|
||||||
// or use cdn, uncomment cdn link in `index.html`
|
|
||||||
|
|
||||||
import "~/styles/index.scss";
|
|
||||||
import "uno.css";
|
|
||||||
|
|
||||||
// If you want to use ElMessage, import it.
|
// If you want to use ElMessage, import it.
|
||||||
import "element-plus/theme-chalk/src/message.scss";
|
import 'element-plus/theme-chalk/src/message.scss'
|
||||||
import "element-plus/theme-chalk/src/notification.scss";
|
import 'element-plus/theme-chalk/src/notification.scss'
|
||||||
import "element-plus/theme-chalk/el-input-number.css";
|
import 'element-plus/theme-chalk/el-input-number.css'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
const app = createApp(App);
|
import * as Icons from '@element-plus/icons-vue'
|
||||||
// app.use(ElementPlus);
|
for (const [key, component] of Object.entries(Icons)) {
|
||||||
app.mount("#app");
|
app.component(key, component)
|
||||||
|
}
|
||||||
|
app.use(router).use(createPinia())
|
||||||
|
app.mount('#app')
|
||||||
|
|||||||
6
src/mock/index.ts
Normal 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
@ -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
@ -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
|
||||||
@ -1,9 +1,6 @@
|
|||||||
import { createProdMockServer } from "vite-plugin-mock/es/createProdMockServer";
|
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'
|
||||||
import index from "../mock";
|
import mock from './mock'
|
||||||
|
|
||||||
export const mockModules = [...index];
|
|
||||||
|
|
||||||
export function setupProdMockServer() {
|
export function setupProdMockServer() {
|
||||||
createProdMockServer(mockModules);
|
createProdMockServer(mock)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
15
src/router/index.ts
Normal 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
@ -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 }
|
||||||
|
})
|
||||||
@ -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 {
|
||||||
|
--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 {
|
:deep {
|
||||||
&.is-block {
|
&.is-block {
|
||||||
.el-tabs__header {
|
.el-tabs__header {
|
||||||
@ -39,7 +27,8 @@ const data = useVModel(props, 'modelValue', emits)
|
|||||||
padding: var(--el-segmented-padding);
|
padding: var(--el-segmented-padding);
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-tabs__nav-scroll, .el-tabs__nav-wrap {
|
.el-tabs__nav-scroll,
|
||||||
|
.el-tabs__nav-wrap {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
@ -66,7 +55,9 @@ const data = useVModel(props, 'modelValue', emits)
|
|||||||
line-height: var(--el-segmented-height);
|
line-height: var(--el-segmented-height);
|
||||||
font-size: var(--el-segmented-font-size);
|
font-size: var(--el-segmented-font-size);
|
||||||
border-radius: var(--el-segmented-radius);
|
border-radius: var(--el-segmented-radius);
|
||||||
transition: color .2s, background-color .2s;
|
transition:
|
||||||
|
color 0.2s,
|
||||||
|
background-color 0.2s;
|
||||||
background: none;
|
background: none;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|
||||||
@ -84,7 +75,6 @@ const data = useVModel(props, 'modelValue', emits)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 活动栏属性 */
|
|
||||||
.el-tabs__active-bar {
|
.el-tabs__active-bar {
|
||||||
padding: 0 var(--el-segmented-item-padding);
|
padding: 0 var(--el-segmented-item-padding);
|
||||||
margin-left: calc(0px - 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;
|
top: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
@ -1,11 +1,11 @@
|
|||||||
// only scss variables
|
// only scss variables
|
||||||
|
|
||||||
$--colors: (
|
$--colors: (
|
||||||
"primary": (
|
'primary': (
|
||||||
"base": #589ef8,
|
'base': #589ef8
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@forward "element-plus/theme-chalk/src/dark/var.scss" with (
|
@forward 'element-plus/theme-chalk/src/dark/var.scss' with (
|
||||||
$colors: $--colors
|
$colors: $--colors
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,32 +1,32 @@
|
|||||||
$--colors: (
|
$--colors: (
|
||||||
"primary": (
|
'primary': (
|
||||||
"base": #589ef8,
|
'base': #589ef8
|
||||||
),
|
),
|
||||||
"success": (
|
'success': (
|
||||||
"base": #21ba45,
|
'base': #21ba45
|
||||||
),
|
),
|
||||||
"warning": (
|
'warning': (
|
||||||
"base": #f2711c,
|
'base': #f2711c
|
||||||
),
|
),
|
||||||
"danger": (
|
'danger': (
|
||||||
"base": #db2828,
|
'base': #db2828
|
||||||
),
|
),
|
||||||
"error": (
|
'error': (
|
||||||
"base": #db2828,
|
'base': #db2828
|
||||||
),
|
|
||||||
"info": (
|
|
||||||
"base": #42b8dd,
|
|
||||||
),
|
),
|
||||||
|
'info': (
|
||||||
|
'base': #42b8dd
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// we can add this to custom namespace, default is 'el'
|
// we can add this to custom namespace, default is 'el'
|
||||||
@forward "element-plus/theme-chalk/src/mixins/config.scss" with (
|
@forward 'element-plus/theme-chalk/src/mixins/config.scss' with (
|
||||||
$namespace: "el"
|
$namespace: 'el'
|
||||||
);
|
);
|
||||||
|
|
||||||
// You should use them in scss, because we calculate it by sass.
|
// You should use them in scss, because we calculate it by sass.
|
||||||
// comment next lines to use default color
|
// comment next lines to use default color
|
||||||
@forward "element-plus/theme-chalk/src/common/var.scss" with (
|
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
|
||||||
// do not use same name, it will override.
|
// do not use same name, it will override.
|
||||||
$colors: $--colors,
|
$colors: $--colors,
|
||||||
// $button-padding-horizontal: ("default": 50px)
|
// $button-padding-horizontal: ("default": 50px)
|
||||||
@ -39,4 +39,4 @@ $--colors: (
|
|||||||
// @debug $--colors;
|
// @debug $--colors;
|
||||||
|
|
||||||
// custom dark variables
|
// custom dark variables
|
||||||
@use "./dark.scss";
|
@use './dark.scss';
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
// import dark theme
|
// 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 {
|
:root {
|
||||||
.el-segmented {
|
.el-segmented {
|
||||||
@ -12,8 +12,8 @@
|
|||||||
--el-segmented-color: var(--el-text-color-secondary);
|
--el-segmented-color: var(--el-text-color-secondary);
|
||||||
--el-segmented-active-color: var(--el-text-color-primary);
|
--el-segmented-active-color: var(--el-text-color-primary);
|
||||||
--el-segmented-active-bg: var(--el-bg-color-overlay);
|
--el-segmented-active-bg: var(--el-bg-color-overlay);
|
||||||
--el-segmented-active-shadow: 0 1px 3px 0 rgba(0, 0, 0, .08);
|
--el-segmented-active-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.08);
|
||||||
--el-segmented-hover-bg: rgba(0, 0, 0, .04);
|
--el-segmented-hover-bg: rgba(0, 0, 0, 0.04);
|
||||||
--el-segmented-disabled-color: var(--el-text-color-placeholder);
|
--el-segmented-disabled-color: var(--el-text-color-placeholder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -34,7 +34,8 @@
|
|||||||
// 抽屉头部
|
// 抽屉头部
|
||||||
.el-drawer__header {
|
.el-drawer__header {
|
||||||
margin-bottom: 0;
|
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);
|
border-bottom: 1px var(--el-border-style) var(--el-border-color);
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
// 抽屉标题
|
// 抽屉标题
|
||||||
@ -46,13 +47,13 @@
|
|||||||
|
|
||||||
.el-drawer__footer {
|
.el-drawer__footer {
|
||||||
border-top: var(--el-border);
|
border-top: var(--el-border);
|
||||||
padding: calc(var(--el-drawer-padding-primary) - 5px)
|
padding: calc(var(--el-drawer-padding-primary) - 5px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: Inter, system-ui, Avenir, "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
|
font-family: Inter, system-ui, Avenir, 'Helvetica Neue', Helvetica, 'PingFang SC',
|
||||||
"Microsoft YaHei", "微软雅黑", Arial, sans-serif;
|
'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -62,6 +63,15 @@ a {
|
|||||||
color: var(--el-color-primary);
|
color: var(--el-color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#app {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
|
|||||||
77
src/typings/auto-imports.d.ts
vendored
Normal 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')
|
||||||
|
}
|
||||||
@ -1,26 +1,27 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
/* prettier-ignore */
|
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
// Generated by unplugin-vue-components
|
// Generated by unplugin-vue-components
|
||||||
// Read more: https://github.com/vuejs/core/pull/3399
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
export {}
|
export {}
|
||||||
|
|
||||||
|
/* prettier-ignore */
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
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']
|
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||||
ElButton: typeof import('element-plus/es')['ElButton']
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
ElCard: typeof import('element-plus/es')['ElCard']
|
ElCard: typeof import('element-plus/es')['ElCard']
|
||||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||||
ElCol: typeof import('element-plus/es')['ElCol']
|
ElCol: typeof import('element-plus/es')['ElCol']
|
||||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||||
|
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||||
ElDivider: typeof import('element-plus/es')['ElDivider']
|
|
||||||
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||||
ElForm: typeof import('element-plus/es')['ElForm']
|
ElForm: typeof import('element-plus/es')['ElForm']
|
||||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||||
ElInput: typeof import('element-plus/es')['ElInput']
|
ElInput: typeof import('element-plus/es')['ElInput']
|
||||||
|
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||||
ElLink: typeof import('element-plus/es')['ElLink']
|
ElLink: typeof import('element-plus/es')['ElLink']
|
||||||
ElOption: typeof import('element-plus/es')['ElOption']
|
ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
|
ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
|
||||||
@ -31,6 +32,7 @@ declare module 'vue' {
|
|||||||
ElRow: typeof import('element-plus/es')['ElRow']
|
ElRow: typeof import('element-plus/es')['ElRow']
|
||||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||||
|
ElSpace: typeof import('element-plus/es')['ElSpace']
|
||||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||||
ElTable: typeof import('element-plus/es')['ElTable']
|
ElTable: typeof import('element-plus/es')['ElTable']
|
||||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||||
@ -40,9 +42,17 @@ declare module 'vue' {
|
|||||||
ElText: typeof import('element-plus/es')['ElText']
|
ElText: typeof import('element-plus/es')['ElText']
|
||||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||||
ElTree: typeof import('element-plus/es')['ElTree']
|
ElTree: typeof import('element-plus/es')['ElTree']
|
||||||
Interface: typeof import('./components/Render/interface.ts')['default']
|
Operator: typeof import('./../components/AdvancedFilter/Operator.vue')['default']
|
||||||
RoleSelection: typeof import('./components/RoleSelection/index.ts')['default']
|
Render: typeof import('./../components/Render/index.tsx')['default']
|
||||||
Segmented: typeof import('./components/Segmented/index.ts')['default']
|
RolePicker: typeof import('./../components/RoleSelector/RolePicker.vue')['default']
|
||||||
UserSelection: typeof import('./components/UserSelection/index.ts')['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
@ -0,0 +1 @@
|
|||||||
|
type Recordable<T = any> = Record<string, T>
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -1,30 +1,55 @@
|
|||||||
<script setup lang="ts" name="flowDesign">
|
<script setup lang="ts">
|
||||||
import NodeTree from './nodes/index.vue'
|
import TreeNode from './nodes/TreeNode.vue'
|
||||||
import NodePenal from './penal/index.vue'
|
import Panel from './panels/index.vue'
|
||||||
import {FlowNode} from './nodes/Node/index'
|
import type { ErrorInfo, FlowNode, TimerNode } from './nodes/type'
|
||||||
import useNode from './hooks/useNode'
|
import type {
|
||||||
import {computed, onUnmounted, provide, ref} from "vue";
|
ApprovalNode,
|
||||||
import {Plus, Minus, Download, Sunny, Moon, TopRight, TopLeft} from "@element-plus/icons-vue";
|
BranchNode,
|
||||||
import {useVModels} from "@vueuse/core";
|
CcNode,
|
||||||
import {Field} from "~/components/Render/interface";
|
ConditionNode,
|
||||||
import {downloadXml} from "~/api/modules/model";
|
ExclusiveNode,
|
||||||
import {useRefHistory} from '@vueuse/core'
|
NodeType
|
||||||
import {cloneDeep} from "lodash-es";
|
} 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 {
|
const props = defineProps<{
|
||||||
process: FlowNode,
|
process: FlowNode
|
||||||
fields: Field[]
|
fields: Field[]
|
||||||
}
|
readOnly?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
const $props = defineProps<FlowDesignProps>()
|
const isDark = ref(false)
|
||||||
const $emits = defineEmits(['update:process', 'update:fields'])
|
const flatFields = computed(() => {
|
||||||
const {fields} = useVModels($props, $emits)
|
const all: Field[] = []
|
||||||
const process = ref<FlowNode>($props.process)
|
const loop = (children: Field[]) => {
|
||||||
const {undo, redo, canUndo, canRedo} = useRefHistory(process, {deep: true, clone: cloneDeep})
|
children.forEach((field) => {
|
||||||
const nodePenalRef = ref<InstanceType<typeof NodePenal>>()
|
if (field.type === 'formItem') {
|
||||||
const zoom = ref(100)
|
all.push(field)
|
||||||
|
}
|
||||||
|
if (Array.isArray(field.children)) {
|
||||||
|
loop(field.children)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
loop(props.fields)
|
||||||
|
return all
|
||||||
|
})
|
||||||
const getScale = computed(() => zoom.value / 100)
|
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 = () => {
|
const handleToggleDark = () => {
|
||||||
if (isDark.value) {
|
if (isDark.value) {
|
||||||
document.documentElement.classList.add('dark')
|
document.documentElement.classList.add('dark')
|
||||||
@ -33,32 +58,224 @@ const handleToggleDark = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const openPenal = (node: FlowNode) => {
|
const openPenal = (node: FlowNode) => {
|
||||||
nodePenalRef.value?.open(node)
|
activeData.value = node
|
||||||
|
penalVisible.value = true
|
||||||
}
|
}
|
||||||
const {addNode, delNode, validateNodes, addNodeRef} = useNode(process, fields)
|
const nextId = (): string => {
|
||||||
provide('nodeHooks', {
|
let id = `node_${Math.random().toString(36).substring(2, 7)}`
|
||||||
readOnly: false,
|
const findId = (node: FlowNode, id: string): boolean => {
|
||||||
fields: fields,
|
if (node.id === id) {
|
||||||
addNode,
|
return true
|
||||||
delNode,
|
}
|
||||||
addNodeRef,
|
if (node.child) {
|
||||||
openPenal
|
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)
|
||||||
})
|
})
|
||||||
const handleZoom = (e: WheelEvent) => {
|
}
|
||||||
if (e.shiftKey) {
|
}
|
||||||
if (e.deltaY > 0) {
|
return false
|
||||||
if (zoom.value > 50) {
|
}
|
||||||
zoom.value -= 10
|
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 {
|
} else {
|
||||||
if (zoom.value < 170) {
|
if (del.child && del.child.pid) {
|
||||||
zoom.value += 10
|
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 = () => {
|
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 converterBpmn = () => {
|
||||||
const processModel = {
|
const processModel = {
|
||||||
@ -66,45 +283,19 @@ const converterBpmn = () => {
|
|||||||
name: '测试',
|
name: '测试',
|
||||||
icon: {
|
icon: {
|
||||||
name: 'el:HomeFilled',
|
name: 'el:HomeFilled',
|
||||||
color: '#409EFF',
|
color: '#409EFF'
|
||||||
},
|
},
|
||||||
process: process.value,
|
process: props.process,
|
||||||
|
enable: true,
|
||||||
version: 1,
|
version: 1,
|
||||||
sort: 0,
|
sort: 0,
|
||||||
groupId: '',
|
groupId: '',
|
||||||
remark: '',
|
remark: ''
|
||||||
}
|
}
|
||||||
downloadXml(processModel)
|
downloadXml(processModel)
|
||||||
}
|
}
|
||||||
const downloadJson = () => {
|
defineExpose({
|
||||||
const processModel = {
|
validate
|
||||||
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)
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -113,47 +304,47 @@ onUnmounted(() => {
|
|||||||
<div class="tool">
|
<div class="tool">
|
||||||
<el-switch
|
<el-switch
|
||||||
inline-prompt
|
inline-prompt
|
||||||
:active-icon="Sunny"
|
active-icon="Sunny"
|
||||||
:inactive-icon="Moon"
|
inactive-icon="Moon"
|
||||||
@change="handleToggleDark"
|
@change="handleToggleDark"
|
||||||
v-model="isDark"/>
|
v-model="isDark"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!--放大/缩小-->
|
<!--放大/缩小-->
|
||||||
<div class="zoom">
|
<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>
|
<span>{{ zoom }}%</span>
|
||||||
<el-button :icon="Minus" @click="zoom -= 10" circle :disabled="zoom <= 50"></el-button>
|
<el-tooltip content="缩小" placement="bottom-start">
|
||||||
<el-button @click="undo()" :disabled="!canUndo" :icon="TopLeft">撤销</el-button>
|
<el-button icon="minus" @click="zoom -= 10" circle :disabled="zoom <= 50"></el-button>
|
||||||
<el-button @click="redo()" :disabled="!canRedo" :icon="TopRight">重做</el-button>
|
</el-tooltip>
|
||||||
<el-button @click="validate">校验</el-button>
|
<el-button @click="converterBpmn" type="primary" icon="Download">转bpmn</el-button>
|
||||||
<el-button @click="downloadJson" type="primary" :icon="Download">导出json</el-button>
|
|
||||||
<el-button @click="converterBpmn" type="primary" :icon="Download">转bpmn</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
<!--流程树-->
|
<!--流程树-->
|
||||||
<div class="node-container">
|
<div class="node-container">
|
||||||
<NodeTree :node="process"/>
|
<TreeNode :node="process" @addNode="addNode" @delNode="delNode" @activeNode="openPenal" />
|
||||||
</div>
|
</div>
|
||||||
<!--属性面板-->
|
<!--属性面板-->
|
||||||
<NodePenal ref="nodePenalRef"/>
|
<Panel v-model="penalVisible" :active-data="activeData" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.designer-container {
|
.designer-container {
|
||||||
background-color: var(--el-bg-color);
|
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
// overflow: scroll;
|
overflow: auto;
|
||||||
|
background-color: var(--el-bg-color-page);
|
||||||
|
|
||||||
.zoom {
|
.zoom {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
top: 20px;
|
top: 30px;
|
||||||
right: 20px;
|
right: 40px;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
margin: 0 10px;
|
margin: 0 10px;
|
||||||
|
|||||||
124
src/views/flowDesign/nodes/Add.vue
Normal 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>
|
||||||
@ -1,4 +0,0 @@
|
|||||||
import { FlowNode } from '../Node/index'
|
|
||||||
export interface AddNode extends FlowNode {
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
@ -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>
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
106
src/views/flowDesign/nodes/ApprovalNode.vue
Normal 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>
|
||||||
@ -1,5 +0,0 @@
|
|||||||
import {FlowNode} from '../Node/index'
|
|
||||||
|
|
||||||
export interface BranchNode extends FlowNode{
|
|
||||||
children: FlowNode[];
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
@ -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>
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { FlowNode } from '../Node/index'
|
|
||||||
import {FormProperty} from "~/views/flowDesign/index";
|
|
||||||
export interface CcNode extends FlowNode {
|
|
||||||
users: string[]
|
|
||||||
// 表单字段
|
|
||||||
formProperties: FormProperty[]
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
82
src/views/flowDesign/nodes/CcNode.vue
Normal 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>
|
||||||
@ -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>
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
53
src/views/flowDesign/nodes/ConditionNode.vue
Normal 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>
|
||||||
@ -1,5 +0,0 @@
|
|||||||
import {FlowNode} from '../Node/index'
|
|
||||||
|
|
||||||
export interface EndNode extends FlowNode {
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||