重构版
6
.env
@ -1,8 +1,2 @@
|
||||
# title
|
||||
VITE_GLOB_APP_TITLE=lowflow-design
|
||||
|
||||
# 本地运行端口号
|
||||
VITE_PORT=8848
|
||||
|
||||
# 启动时自动打开浏览器
|
||||
VITE_OPEN=true
|
||||
|
||||
@ -1,6 +1,3 @@
|
||||
# 本地环境
|
||||
VITE_USER_NODE_ENV=development
|
||||
|
||||
# 公共基础路径
|
||||
VITE_PUBLIC_PATH=/
|
||||
|
||||
|
||||
@ -1,6 +1,3 @@
|
||||
# 本地环境
|
||||
VITE_USER_NODE_ENV=development
|
||||
|
||||
# 公共基础路径
|
||||
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
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
# lock
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
*.log
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
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`等技术栈开发的,适用于低代码或无代码开发平台的流程设计器。
|
||||
让普通人也能通过简单配置快速搭建流程。
|
||||
让普通人也能通过简单配置快速搭建流程。 <br />
|
||||
并提供了将json转xml的后端代码:[lowflow-design-converter](https://github.com/tsai996/lowflow-design-converter)。
|
||||
## 在线预览
|
||||
https://tsai996.github.io/lowflow-design/
|
||||
### 成品案例
|
||||
@ -23,8 +24,9 @@ https://www.666cxf.com/
|
||||
|
||||
## 扫码加入群聊,如果失效加微信拉入群聊(备注:加群)
|
||||
<p>
|
||||
<img alt="微信群" src="public/wxq.png" width="240" style="display: inline-block"/>
|
||||
<img alt="微信" src="public/wx.png" width="240" style="display: inline-block"/>
|
||||
<img alt="QQ群" src="public/qq_qun.jpg" width="240" height="400" style="display: inline-block"/>
|
||||
<img alt="微信群" src="public/wx_qun.jpg" width="240" height="400" style="display: inline-block"/>
|
||||
<img alt="微信" src="public/wx.jpg" width="240" style="display: inline-block"/>
|
||||
</p>
|
||||
|
||||
## 赞助
|
||||
|
||||
10
env.d.ts
vendored
Normal file
@ -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>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>lowflow-design</title>
|
||||
<!-- element css cdn, if you use custom theme, remove it. -->
|
||||
<!-- <link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/element-plus/dist/index.css"
|
||||
/> -->
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
<script>
|
||||
window.setInterval(function(){
|
||||
new Function('debugger')();
|
||||
}, 100);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -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",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"build:dev": "vite build --mode development",
|
||||
"build:test": "vite build --mode test",
|
||||
"mock": "vite --mode mock --force",
|
||||
"generate": "vite-ssg build",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "vue-tsc --noEmit"
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build --force",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.5.1",
|
||||
"element-plus": "^2.3.12",
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@vueuse/core": "^10.9.0",
|
||||
"axios": "^1.6.8",
|
||||
"element-plus": "^2.7.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"vue": "^3.3.4"
|
||||
"lodash-es": "^4.17.21",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.4.21",
|
||||
"vue-router": "^4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/ep": "^1.1.12",
|
||||
"@types/file-saver": "^2.0.5",
|
||||
"@types/node": "^20.6.0",
|
||||
"@vitejs/plugin-vue": "^4.3.4",
|
||||
"@vitejs/plugin-vue-jsx": "^3.0.1",
|
||||
"@rushstack/eslint-patch": "^1.8.0",
|
||||
"@tsconfig/node20": "^20.1.4",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^20.12.5",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
||||
"@vue/eslint-config-prettier": "^9.0.0",
|
||||
"@vue/eslint-config-typescript": "^13.0.0",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-vue": "^9.23.0",
|
||||
"mockjs": "^1.1.0",
|
||||
"sass": "^1.66.1",
|
||||
"typescript": "^5.2.2",
|
||||
"unocss": "^0.55.7",
|
||||
"unplugin-vue-components": "^0.25.2",
|
||||
"vite": "^4.4.9",
|
||||
"npm-run-all2": "^6.1.2",
|
||||
"prettier": "^3.2.5",
|
||||
"sass": "^1.77.0",
|
||||
"typescript": "~5.4.0",
|
||||
"unocss": "^0.59.4",
|
||||
"unplugin-auto-import": "^0.17.5",
|
||||
"unplugin-vue-components": "^0.27.0",
|
||||
"vite": "^5.2.8",
|
||||
"vite-plugin-mock": "^2.9.6",
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vite-plugin-vue-devtools": "^7.0.25",
|
||||
"vite-plugin-vue-setup-extend": "^0.4.0",
|
||||
"vite-ssg": "^0.23.1",
|
||||
"vue-tsc": "^1.8.11"
|
||||
},
|
||||
"license": "MIT"
|
||||
"vue-tsc": "^2.0.11"
|
||||
}
|
||||
}
|
||||
|
||||
7733
pnpm-lock.yaml
generated
|
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>
|
||||
<el-config-provider namespace="el" :locale="zhCn">
|
||||
<FlowDesign :process="process" :fields="fields"/>
|
||||
<router-view></router-view>
|
||||
</el-config-provider>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import FlowDesign from '~/views/flowDesign/index.vue'
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||
import {FlowNode} from "~/views/flowDesign/nodes/Node/index";
|
||||
import {ref} from "vue";
|
||||
import {Field} from "~/components/Render/interface";
|
||||
import {StartNode} from "~/views/flowDesign/nodes/Start/index";
|
||||
import {EndNode} from "~/views/flowDesign/nodes/End/index";
|
||||
// 流程节点
|
||||
const process = ref<FlowNode>({
|
||||
id: 'root',
|
||||
pid: null,
|
||||
type: 'start',
|
||||
name: '发起人',
|
||||
formProperties: [],
|
||||
child: {
|
||||
id: 'end',
|
||||
pid: 'root',
|
||||
type: 'end',
|
||||
name: '结束',
|
||||
child: null
|
||||
} as EndNode
|
||||
} as StartNode)
|
||||
// 表单字段
|
||||
const fields = ref<Field[]>([
|
||||
{
|
||||
id: 'field_da2w55',
|
||||
title: "请假人",
|
||||
name: "UserSelection",
|
||||
value: null,
|
||||
props: {
|
||||
multiple: false,
|
||||
disabled: false,
|
||||
placeholder: "请选择用户",
|
||||
style: {
|
||||
width: "100%"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'field_fa2w40',
|
||||
title: "请假天数",
|
||||
name: "Number",
|
||||
value: null,
|
||||
props: {
|
||||
disabled: false,
|
||||
placeholder: "请假天数",
|
||||
style: {
|
||||
width: "100%"
|
||||
},
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 1,
|
||||
precision: 0,
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'field_d42t45',
|
||||
title: "请假事由",
|
||||
name: "Select",
|
||||
value: null,
|
||||
props: {
|
||||
disabled: false,
|
||||
multiple: false,
|
||||
placeholder: "请选择请假事由",
|
||||
options: [
|
||||
{
|
||||
label: "事假",
|
||||
value: "事假"
|
||||
},
|
||||
{
|
||||
label: "病假",
|
||||
value: "病假"
|
||||
},
|
||||
{
|
||||
label: "婚假",
|
||||
value: "婚假"
|
||||
},
|
||||
{
|
||||
label: "产假",
|
||||
value: "产假"
|
||||
},
|
||||
{
|
||||
label: "丧假",
|
||||
value: "丧假"
|
||||
},
|
||||
{
|
||||
label: "其他",
|
||||
value: "其他"
|
||||
}
|
||||
],
|
||||
style: {
|
||||
width: "100%"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'field_522g58',
|
||||
title: "请假原因",
|
||||
name: "Input",
|
||||
value: null,
|
||||
props: {
|
||||
type: 'textarea',
|
||||
placeholder: "请输入请假原因",
|
||||
autosize: {
|
||||
minRows: 3,
|
||||
maxRows: 3
|
||||
},
|
||||
disabled: false,
|
||||
style: {
|
||||
width: "100%"
|
||||
}
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
</script>
|
||||
<style>
|
||||
<style scoped>
|
||||
#app {
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
</style>
|
||||
|
||||
209
src/api/index.ts
@ -1,158 +1,99 @@
|
||||
import axios, {
|
||||
AxiosInstance,
|
||||
AxiosError,
|
||||
AxiosRequestConfig,
|
||||
InternalAxiosRequestConfig,
|
||||
AxiosResponse
|
||||
type AxiosInstance,
|
||||
AxiosError,
|
||||
type AxiosRequestConfig,
|
||||
type InternalAxiosRequestConfig,
|
||||
type AxiosResponse
|
||||
} from 'axios'
|
||||
import {ElNotification} from "element-plus";
|
||||
import { ElNotification } from 'element-plus'
|
||||
|
||||
export interface Result {
|
||||
code: number;
|
||||
success: boolean;
|
||||
message: string;
|
||||
code: number
|
||||
success: boolean
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface ResultData<T = any> extends Result {
|
||||
data: T;
|
||||
data: T
|
||||
}
|
||||
|
||||
export interface ResultPage<T = any> extends ResultData {
|
||||
data: {
|
||||
rows: T[];
|
||||
total: number;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* axios配置
|
||||
*/
|
||||
const config = {
|
||||
// 请求地址
|
||||
baseURL: import.meta.env.VITE_API_URL as string,
|
||||
// 设置超时时间
|
||||
timeout: 8000,
|
||||
baseURL: import.meta.env.VITE_API_URL,
|
||||
timeout: 8000
|
||||
}
|
||||
|
||||
class RequestHttp {
|
||||
service: AxiosInstance
|
||||
service: AxiosInstance
|
||||
|
||||
/**
|
||||
* 请求构造函数
|
||||
* @param config
|
||||
*/
|
||||
public constructor(config: AxiosRequestConfig) {
|
||||
// 创建axios实例
|
||||
this.service = axios.create(config)
|
||||
// 请求拦截
|
||||
this.service.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
// config.headers['Authorization'] = "Bearer 87779e5a-3342-4df6-865d-d8828800d6fb"
|
||||
return config
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
// 请求错误处理
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
// 响应拦截
|
||||
this.service.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
const {data} = response
|
||||
// 响应拦截处理
|
||||
return data
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
const {response, message} = error
|
||||
const data = response?.data as ResultData
|
||||
const errMsg = data ? data.message : message
|
||||
// 响应错误处理
|
||||
ElNotification.error(errMsg || '未知错误')
|
||||
return Promise.reject(response?.data || error)
|
||||
}
|
||||
)
|
||||
}
|
||||
/**
|
||||
* 请求构造函数
|
||||
* @param config
|
||||
*/
|
||||
public constructor(config: AxiosRequestConfig) {
|
||||
this.service = axios.create(config)
|
||||
this.service.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
return config
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
this.service.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
const { data } = response
|
||||
return data
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
const { response, message } = error
|
||||
const data = response?.data as ResultData
|
||||
const errMsg = data ? data.message : message
|
||||
ElNotification.error(errMsg || '未知错误')
|
||||
return Promise.reject(response?.data || error)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* get请求
|
||||
* @param url 请求地址
|
||||
* @param params 请求参数
|
||||
*/
|
||||
get<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {
|
||||
return this.service.get(url, {params, ..._object})
|
||||
}
|
||||
/**
|
||||
* get请求
|
||||
* @param url
|
||||
* @param params
|
||||
* @param config
|
||||
*/
|
||||
get<T>(url: string, params?: object, config = {}): Promise<ResultData<T>> {
|
||||
return this.service.get(url, { params, ...config })
|
||||
}
|
||||
|
||||
/**
|
||||
* post请求
|
||||
* @param url 请求地址
|
||||
* @param data 请求参数
|
||||
*/
|
||||
post<T>(url: string, data?: object): Promise<ResultData<T>> {
|
||||
return this.service.post(url, data)
|
||||
}
|
||||
/**
|
||||
* post请求
|
||||
* @param url
|
||||
* @param data
|
||||
* @param config
|
||||
*/
|
||||
post<T>(url: string, data?: object, config = {}): Promise<ResultData<T>> {
|
||||
return this.service.post(url, data, config)
|
||||
}
|
||||
|
||||
/**
|
||||
* put请求
|
||||
* @param url 请求地址
|
||||
* @param data 请求参数
|
||||
*/
|
||||
put<T>(url: string, data?: object): Promise<ResultData<T>> {
|
||||
return this.service.put(url, data)
|
||||
}
|
||||
/**
|
||||
* request请求
|
||||
* @param config
|
||||
*/
|
||||
request<T>(config: AxiosRequestConfig): Promise<ResultData<T>> {
|
||||
return this.service.request(config)
|
||||
}
|
||||
|
||||
/**
|
||||
* delete请求
|
||||
* @param url 请求地址
|
||||
* @param params 请求参数
|
||||
*/
|
||||
delete<T>(url: string, params?: any): Promise<ResultData<T>> {
|
||||
return this.service.delete(url, params)
|
||||
}
|
||||
|
||||
/**
|
||||
* patch请求
|
||||
* @param url 请求地址
|
||||
* @param data 请求参数
|
||||
*/
|
||||
patch<T>(url: string, data?: object): Promise<ResultData<T>> {
|
||||
return this.service.patch(url, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* head请求
|
||||
* @param url 请求地址
|
||||
*/
|
||||
head<T>(url: string): Promise<ResultData<T>> {
|
||||
return this.service.head(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* options请求
|
||||
* @param url 请求地址
|
||||
* @param _object 其他配置
|
||||
*/
|
||||
options<T>(url: string): Promise<ResultData<T>> {
|
||||
return this.service.options(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* request请求
|
||||
* @param config 请求配置
|
||||
*/
|
||||
request<T>(config: AxiosRequestConfig): Promise<ResultData<T>> {
|
||||
return this.service.request(config)
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
* @param url
|
||||
* @param data
|
||||
* @param config
|
||||
*/
|
||||
download(url: string, data?: object, config = {}): Promise<BlobPart> {
|
||||
return this.service.post(url, data, { ...config, responseType: 'blob' })
|
||||
}
|
||||
/**
|
||||
* 下载文件
|
||||
* @param url
|
||||
* @param data
|
||||
* @param config
|
||||
*/
|
||||
download(url: string, data?: object, config = {}): Promise<BlobPart> {
|
||||
return this.service.post(url, data, { ...config, responseType: 'blob' })
|
||||
}
|
||||
}
|
||||
|
||||
export default new RequestHttp(config)
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import http from '~/api/index'
|
||||
import FileSaver from "file-saver";
|
||||
import http from '@/api'
|
||||
import FileSaver from 'file-saver'
|
||||
|
||||
export const downloadXml = async (data: object) => {
|
||||
const res = await http.download('https://www.666cxf.com/api/model/download', data)
|
||||
FileSaver.saveAs(
|
||||
new Blob([res], { type: 'application/octet-stream;charset=utf-8' }),
|
||||
'测试流程.bpmn20.xml'
|
||||
)
|
||||
const res = await http.download('https://www.666cxf.com/api/model/download', data)
|
||||
FileSaver.saveAs(
|
||||
new Blob([res], { type: 'application/octet-stream;charset=utf-8' }),
|
||||
'测试流程.bpmn20.xml'
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import http from '~/api/index'
|
||||
import http from '@/api/index'
|
||||
|
||||
export interface Role {
|
||||
id: string,
|
||||
name: string
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
/**
|
||||
@ -10,13 +10,13 @@ export interface Role {
|
||||
* @param id
|
||||
*/
|
||||
export const getById = (id: string) => {
|
||||
return http.get<Role>(`/role/info`,{id:id})
|
||||
return http.get<Role>(`/role/info`, { id: id })
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询角色列表
|
||||
*/
|
||||
export const getList = (roleIds?: string[]) => {
|
||||
const params = roleIds ? {roleIds: roleIds} : {}
|
||||
return http.post<Role[]>('/role/list', params)
|
||||
const params = roleIds ? { roleIds: roleIds } : {}
|
||||
return http.post<Role[]>('/role/list', params)
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import http from '~/api/index'
|
||||
import http from '@/api/index'
|
||||
|
||||
export interface User {
|
||||
id: string,
|
||||
username: string,
|
||||
name: string,
|
||||
avatar: string
|
||||
id: string
|
||||
username: string
|
||||
name: string
|
||||
avatar: string
|
||||
}
|
||||
|
||||
/**
|
||||
@ -12,13 +12,13 @@ export interface User {
|
||||
* @param username
|
||||
*/
|
||||
export const getByUsername = (username: string) => {
|
||||
return http.get<User>(`/user/info`,{username:username})
|
||||
return http.get<User>(`/user/info`, { username: username })
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户列表
|
||||
*/
|
||||
export const getList = (userIds?: string[]) => {
|
||||
const params = userIds ? {userIds: userIds} : {}
|
||||
return http.post<User[]>('/user/list', params)
|
||||
const params = userIds ? { userIds: userIds } : {}
|
||||
return http.post<User[]>('/user/list', params)
|
||||
}
|
||||
|
||||
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 |
@ -1,25 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import {useVModel} from '@vueuse/core'
|
||||
import { useVModel } from '@vueuse/core'
|
||||
|
||||
const $props = defineProps<{
|
||||
modelValue: string
|
||||
}>()
|
||||
const operatorOptions = [
|
||||
{
|
||||
value: 'equal',
|
||||
value: 'eq',
|
||||
label: '等于'
|
||||
},
|
||||
{
|
||||
value: 'not_equal',
|
||||
value: 'ne',
|
||||
label: '不等于'
|
||||
},
|
||||
{
|
||||
label: '包含',
|
||||
value: 'contains'
|
||||
value: 'in'
|
||||
},
|
||||
{
|
||||
label: '不包含',
|
||||
value: 'not_contain'
|
||||
value: 'ni'
|
||||
}
|
||||
]
|
||||
const $emits = defineEmits<{
|
||||
@ -31,10 +31,10 @@ const data = useVModel($props, 'modelValue', $emits)
|
||||
<template>
|
||||
<el-select class="operator-container" v-model="data" filterable placeholder="筛选符">
|
||||
<el-option
|
||||
v-for="item in operatorOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
v-for="item in operatorOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import {Field} from "~/components/Render/interface";
|
||||
import {FilterRules} from "~/components/Condition/index";
|
||||
import {useVModel} from "@vueuse/core";
|
||||
import type { Field } from '@/components/Render/type'
|
||||
import type { FilterRules } from '@/components/AdvancedFilter/type'
|
||||
import { useVModel } from '@vueuse/core'
|
||||
|
||||
const $props = defineProps<{
|
||||
modelValue: any,
|
||||
options: Field[],
|
||||
modelValue: any
|
||||
options: Field[]
|
||||
filterRules: FilterRules
|
||||
}>()
|
||||
const $emits = defineEmits<{
|
||||
@ -16,15 +16,8 @@ const data = useVModel($props, 'modelValue', $emits)
|
||||
|
||||
<template>
|
||||
<el-select class="trigger-container" v-model="data" filterable placeholder="选择字段">
|
||||
<el-option
|
||||
v-for="item in $props.options"
|
||||
:key="item.id"
|
||||
:label="item.title"
|
||||
:value="item.id"
|
||||
/>
|
||||
<el-option v-for="item in $props.options" :key="item.id" :label="item.label" :value="item.id" />
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
<style scoped lang="scss"></style>
|
||||
@ -1,14 +1,12 @@
|
||||
<script setup lang="ts" name="ConditionFilter">
|
||||
import {FilterRules} from "~/components/Condition/index";
|
||||
import {Field} from "~/components/Render/interface";
|
||||
import {useVModel} from "@vueuse/core";
|
||||
import {Delete, CirclePlus, CircleClose} from "@element-plus/icons-vue";
|
||||
<script setup lang="ts" name="AdvancedFilter">
|
||||
import type { FilterRules } from './type'
|
||||
import type { Field } from '@/components/Render/type'
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import Trigger from './Trigger.vue'
|
||||
import Operator from './Operator.vue'
|
||||
import Render from '~/components/Render/index'
|
||||
|
||||
const $props = defineProps<{
|
||||
filterFields: Field[],
|
||||
filterFields: Field[]
|
||||
modelValue: FilterRules
|
||||
}>()
|
||||
const $emits = defineEmits<{
|
||||
@ -22,13 +20,11 @@ const filterRules = useVModel($props, 'modelValue', $emits)
|
||||
* 添加条件
|
||||
*/
|
||||
const addRule = () => {
|
||||
filterRules.value.conditions.push(
|
||||
{
|
||||
field: null,
|
||||
operator: 'equal',
|
||||
value: null
|
||||
}
|
||||
)
|
||||
filterRules.value.conditions.push({
|
||||
field: null,
|
||||
operator: 'eq',
|
||||
value: null
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 删除条件
|
||||
@ -46,12 +42,14 @@ const handleDel = (index: number) => {
|
||||
*/
|
||||
const addGroup = () => {
|
||||
filterRules.value.groups.push({
|
||||
logicalOperator: 'and',
|
||||
conditions: [{
|
||||
field: null,
|
||||
operator: '',
|
||||
value: null
|
||||
}],
|
||||
operator: 'and',
|
||||
conditions: [
|
||||
{
|
||||
field: null,
|
||||
operator: '',
|
||||
value: null
|
||||
}
|
||||
],
|
||||
groups: []
|
||||
})
|
||||
}
|
||||
@ -69,77 +67,79 @@ const delGroup = (index: number) => {
|
||||
<div class="logical-operator">
|
||||
<div class="logical-operator__line"></div>
|
||||
<el-switch
|
||||
v-model="filterRules.logicalOperator"
|
||||
inline-prompt
|
||||
style="--el-switch-on-color: #409EFF; --el-switch-off-color: #67C23A"
|
||||
active-value="and"
|
||||
inactive-value="or"
|
||||
active-text="且"
|
||||
inactive-text="或"
|
||||
v-model="filterRules.operator"
|
||||
inline-prompt
|
||||
style="--el-switch-on-color: #409eff; --el-switch-off-color: #67c23a"
|
||||
active-value="and"
|
||||
inactive-value="or"
|
||||
active-text="且"
|
||||
inactive-text="或"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-option-content">
|
||||
<el-form :label-width="0" :inline="true" :model="filterRules">
|
||||
<el-row v-for="(item, index) in filterRules.conditions" :key="`${item.field}-${index}`" :gutter="5"
|
||||
class="filter-item-rule">
|
||||
<el-row
|
||||
v-for="(item, index) in filterRules.conditions"
|
||||
:key="`${item.field}-${index}`"
|
||||
:gutter="5"
|
||||
class="filter-item-rule"
|
||||
>
|
||||
<el-col :xs="24" :sm="7">
|
||||
<el-form-item :prop="'conditions.' + index + '.field'" style="width: 100%;">
|
||||
<el-form-item :prop="'conditions.' + index + '.field'" style="width: 100%">
|
||||
<trigger
|
||||
ref="triggerRef"
|
||||
:options="$props.filterFields.filter(e=>e.value!==undefined)"
|
||||
:filter-rules="filterRules"
|
||||
v-model="item.field"
|
||||
@update:model-value="item.value = null"
|
||||
ref="triggerRef"
|
||||
:options="$props.filterFields.filter((e) => e.value !== undefined)"
|
||||
:filter-rules="filterRules"
|
||||
v-model="item.field"
|
||||
@update:model-value="item.value = null"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="5" v-if="item.field">
|
||||
<el-form-item :prop="'conditions.' + index + '.operator'" style="width: 100%;">
|
||||
<operator
|
||||
ref="operatorRef"
|
||||
v-model="item.operator"
|
||||
/>
|
||||
<el-form-item :prop="'conditions.' + index + '.operator'" style="width: 100%">
|
||||
<operator ref="operatorRef" v-model="item.operator" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="10" v-if="item.field">
|
||||
<el-form-item :prop="'conditions.' + index + '.value'" style="width: 100%;">
|
||||
<el-form-item :prop="'conditions.' + index + '.value'" style="width: 100%">
|
||||
<Render
|
||||
:field="$props.filterFields.find(e=>e.id===item.field)"
|
||||
v-model="item.value"
|
||||
:field="$props.filterFields.find((e) => e.id === item.field) as Field"
|
||||
v-model="item.value"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="2" style="display: flex;align-items: center;flex-direction: row-reverse;">
|
||||
<el-button
|
||||
plain circle
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
@click="handleDel(index)"
|
||||
/>
|
||||
<el-col
|
||||
:xs="24"
|
||||
:sm="2"
|
||||
style="display: flex; align-items: center; flex-direction: row-reverse"
|
||||
>
|
||||
<el-button plain circle type="danger" icon="Delete" @click="handleDel(index)" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
<ConditionFilter
|
||||
v-for="(item,index) in filterRules.groups"
|
||||
:key="index"
|
||||
@delGroup="delGroup(index)"
|
||||
v-model="filterRules.groups[index]"
|
||||
:filterFields="filterFields"
|
||||
<AdvancedFilter
|
||||
v-for="(item, index) in filterRules.groups"
|
||||
:key="index"
|
||||
@delGroup="delGroup(index)"
|
||||
v-model="filterRules.groups[index]"
|
||||
:filterFields="filterFields"
|
||||
>
|
||||
<el-button @click="delGroup(index)" :icon="CircleClose" class="filter-filter-item__add">
|
||||
<el-button @click="delGroup(index)" icon="CircleClose" class="filter-filter-item__add">
|
||||
删除条件组
|
||||
</el-button>
|
||||
</ConditionFilter>
|
||||
<div v-if="filterRules.groups.length===0 && filterRules.conditions.length===0"
|
||||
class="filter-item-rule"/>
|
||||
</AdvancedFilter>
|
||||
<div
|
||||
v-if="filterRules.groups.length === 0 && filterRules.conditions.length === 0"
|
||||
class="filter-item-rule"
|
||||
/>
|
||||
</el-form>
|
||||
<div class="filter-item-rule">
|
||||
<el-button @click="addRule" :icon="CirclePlus" class="filter-filter-item__add">
|
||||
<el-button @click="addRule" icon="CirclePlus" class="filter-filter-item__add">
|
||||
添加条件
|
||||
</el-button>
|
||||
<el-button @click="addGroup" :icon="CirclePlus" class="filter-filter-item__add">
|
||||
<el-button @click="addGroup" icon="CirclePlus" class="filter-filter-item__add">
|
||||
添加条件组
|
||||
</el-button>
|
||||
<slot/>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -182,7 +182,7 @@ const delGroup = (index: number) => {
|
||||
height: calc(100% - 48px);
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
@ -194,7 +194,7 @@ const delGroup = (index: number) => {
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
@ -223,4 +223,4 @@ const delGroup = (index: number) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
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,95 +1,95 @@
|
||||
import {defineAsyncComponent, defineComponent, h, PropType, resolveComponent} from "vue";
|
||||
import {cloneDeep} from 'lodash-es'
|
||||
import {Field} from "./interface";
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import type { Field } from './type'
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: [String, Number, Boolean, Array, Object] as PropType<any>,
|
||||
default: undefined,
|
||||
required: false
|
||||
},
|
||||
field: {
|
||||
type: Object as PropType<Field>,
|
||||
required: true
|
||||
}
|
||||
props: {
|
||||
modelValue: {
|
||||
type: [String, Number, Boolean, Array, Object] as PropType<any>,
|
||||
default: undefined,
|
||||
required: false
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
components: {
|
||||
Input: defineAsyncComponent(() => import('element-plus/es').then(({ElInput}) => ElInput)),
|
||||
Number: defineAsyncComponent(() => import('element-plus/es').then(({ElInputNumber}) => ElInputNumber)),
|
||||
Select: defineAsyncComponent(() => import('element-plus/es').then(({ElSelect}) => ElSelect)),
|
||||
Radio: defineAsyncComponent(() => import('element-plus/es').then(({ElRadio}) => ElRadio)),
|
||||
Checkbox: defineAsyncComponent(() => import('element-plus/es').then(({ElCheckbox}) => ElCheckbox)),
|
||||
UserSelection: defineAsyncComponent(() => import('~/components/UserSelection/index')),
|
||||
RoleSelection: defineAsyncComponent(() => import('~/components/RoleSelection/index'))
|
||||
},
|
||||
setup(props, {emit}) {
|
||||
/**
|
||||
* 构建属性参数
|
||||
* @param fieldClone
|
||||
*/
|
||||
const buildProps = (fieldClone: Field) => {
|
||||
const dataObject: Record<string, any> = {}
|
||||
const _props = fieldClone.props || {}
|
||||
Object.keys(_props).forEach(key => {
|
||||
dataObject[key] = _props[key]
|
||||
})
|
||||
if (props.modelValue !== undefined) {
|
||||
dataObject.modelValue = props.modelValue
|
||||
} else {
|
||||
dataObject.modelValue = fieldClone.value
|
||||
}
|
||||
dataObject['onUpdate:modelValue'] = (value: any) => {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
delete dataObject.options
|
||||
return dataObject
|
||||
}
|
||||
/**
|
||||
* 构建插槽
|
||||
* @param fieldClone
|
||||
*/
|
||||
const buildSlots = (fieldClone: Field) => {
|
||||
let children: Record<string, any> = {}
|
||||
const slotFunctions: Record<string, any> = {
|
||||
Select: (conf: Field) => {
|
||||
return conf.props.options.map((item: any) => {
|
||||
return <el-option label={item.label} value={item.value}></el-option>
|
||||
})
|
||||
},
|
||||
Radio: (conf: Field) => {
|
||||
return conf.props.options.map((item: any) => {
|
||||
return <el-radio label={item.value}>{item.label}</el-radio>
|
||||
})
|
||||
},
|
||||
Checkbox: (conf: Field) => {
|
||||
return conf.props.options.map((item: any) => {
|
||||
return <el-checkbox label={item.value}>{item.label}</el-checkbox>
|
||||
})
|
||||
}
|
||||
}
|
||||
const slotFunction = slotFunctions[fieldClone.name]
|
||||
if (slotFunction) {
|
||||
children.default = () => {
|
||||
return slotFunction(fieldClone)
|
||||
}
|
||||
}
|
||||
return children
|
||||
}
|
||||
return {
|
||||
buildProps,
|
||||
buildSlots
|
||||
}
|
||||
},
|
||||
render() {
|
||||
const fieldClone: Field = cloneDeep(this.field)
|
||||
const slots = this.buildSlots(fieldClone)
|
||||
const props = this.buildProps(fieldClone)
|
||||
const eleComponent = resolveComponent(fieldClone.name)
|
||||
if (typeof eleComponent === 'string') {
|
||||
return h(eleComponent, props, slots)
|
||||
}
|
||||
return h(eleComponent, props, slots)
|
||||
field: {
|
||||
type: Object as PropType<Field>,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
components: {
|
||||
ElInput: defineAsyncComponent(() => import('element-plus/es').then(({ElInput}) => ElInput)),
|
||||
ElInputNumber: defineAsyncComponent(() => import('element-plus/es').then(({ElInputNumber}) => ElInputNumber)),
|
||||
ElSelect: defineAsyncComponent(() => import('element-plus/es').then(({ElSelect}) => ElSelect)),
|
||||
ElRadio: defineAsyncComponent(() => import('element-plus/es').then(({ElRadio}) => ElRadio)),
|
||||
ElCheckbox: defineAsyncComponent(() => import('element-plus/es').then(({ElCheckbox}) => ElCheckbox)),
|
||||
UserSelector: defineAsyncComponent(() => import('@/components/UserSelector/index.vue')),
|
||||
RoleSelector: defineAsyncComponent(() => import('@/components/RoleSelector/index.vue'))
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
/**
|
||||
* 构建属性参数
|
||||
* @param fieldClone
|
||||
*/
|
||||
const buildProps = (fieldClone: Field) => {
|
||||
const dataObject: Record<string, any> = {}
|
||||
const _props = fieldClone.props || {}
|
||||
Object.keys(_props).forEach((key) => {
|
||||
dataObject[key] = _props[key]
|
||||
})
|
||||
if (props.modelValue !== undefined) {
|
||||
dataObject.modelValue = props.modelValue
|
||||
} else {
|
||||
dataObject.modelValue = fieldClone.value
|
||||
}
|
||||
dataObject['onUpdate:modelValue'] = (value: any) => {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
delete dataObject.options
|
||||
return dataObject
|
||||
}
|
||||
/**
|
||||
* 构建插槽
|
||||
* @param fieldClone
|
||||
*/
|
||||
const buildSlots = (fieldClone: Field) => {
|
||||
const children: Record<string, any> = {}
|
||||
const slotFunctions: Record<string, any> = {
|
||||
ElSelect: (conf: Field) => {
|
||||
return conf.props.options.map((item: any) => {
|
||||
return <el-option label={item.label} value={item.value}></el-option>
|
||||
})
|
||||
},
|
||||
ElRadio: (conf: Field) => {
|
||||
return conf.props.options.map((item: any) => {
|
||||
return <el-radio label={item.value}>{item.label}</el-radio>
|
||||
})
|
||||
},
|
||||
ElCheckbox: (conf: Field) => {
|
||||
return conf.props.options.map((item: any) => {
|
||||
return <el-checkbox label={item.value}>{item.label}</el-checkbox>
|
||||
})
|
||||
}
|
||||
}
|
||||
const slotFunction = slotFunctions[fieldClone.name]
|
||||
if (slotFunction) {
|
||||
children.default = () => {
|
||||
return slotFunction(fieldClone)
|
||||
}
|
||||
}
|
||||
return children
|
||||
}
|
||||
return {
|
||||
buildProps,
|
||||
buildSlots
|
||||
}
|
||||
},
|
||||
render() {
|
||||
const fieldClone: Field = cloneDeep(this.field)
|
||||
const slots = this.buildSlots(fieldClone)
|
||||
const props = this.buildProps(fieldClone)
|
||||
const eleComponent = resolveComponent(fieldClone.name)
|
||||
if (typeof eleComponent === 'string') {
|
||||
return h(eleComponent, props, slots)
|
||||
}
|
||||
return h(eleComponent, props, slots)
|
||||
}
|
||||
})
|
||||
|
||||
@ -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">
|
||||
import {useVModel} from '@vueuse/core'
|
||||
import {TreeNodeData} from 'element-plus/es/components/tree/src/tree.type'
|
||||
import {type ElTree} from 'element-plus'
|
||||
import {reactive, ref, watch} from "vue";
|
||||
import {getList} from "~/api/modules/role";
|
||||
import {School, Check} from "@element-plus/icons-vue";
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import type { TreeNodeData } from 'element-plus/es/components/tree/src/tree.type'
|
||||
import { type ElTree } from 'element-plus'
|
||||
import { getList } from '@/api/modules/role'
|
||||
|
||||
export type ModelValueType = string | string[] | null | undefined
|
||||
|
||||
export interface RoleDropdownProps {
|
||||
modelValue: ModelValueType,
|
||||
modelValue: ModelValueType
|
||||
multiple?: boolean
|
||||
}
|
||||
|
||||
@ -41,7 +39,7 @@ const orgTreeRef = ref<InstanceType<typeof ElTree>>()
|
||||
const expandedKeys = ref<string[]>([])
|
||||
|
||||
const renderClass = (role: TreeNodeData): string | { [key: string]: boolean } => {
|
||||
const val = roleOptions.value.find(e => e.id === role.id)
|
||||
const val = roleOptions.value.find((e) => e.id === role.id)
|
||||
if (val) {
|
||||
return 'is-active'
|
||||
} else {
|
||||
@ -51,7 +49,7 @@ const renderClass = (role: TreeNodeData): string | { [key: string]: boolean } =>
|
||||
|
||||
const onNodeClick = (data: Role) => {
|
||||
if ($props.multiple) {
|
||||
const index = roleOptions.value.findIndex(e => e.id === data.id)
|
||||
const index = roleOptions.value.findIndex((e) => e.id === data.id)
|
||||
if (index === -1) {
|
||||
roleOptions.value.push(data)
|
||||
roleOptions.value.sort((a, b) => a.id.localeCompare(b.id))
|
||||
@ -59,7 +57,7 @@ const onNodeClick = (data: Role) => {
|
||||
roleOptions.value.splice(index, 1)
|
||||
}
|
||||
} else {
|
||||
const index = roleOptions.value.findIndex(e => e.id === data.id)
|
||||
const index = roleOptions.value.findIndex((e) => e.id === data.id)
|
||||
if (index === -1) {
|
||||
roleOptions.value = [data]
|
||||
} else {
|
||||
@ -69,12 +67,15 @@ const onNodeClick = (data: Role) => {
|
||||
}
|
||||
const dialogVisible = ref(false)
|
||||
const queryForm = reactive({
|
||||
name: null,
|
||||
name: null
|
||||
})
|
||||
|
||||
watch(() => queryForm.name, (val) => {
|
||||
orgTreeRef.value?.filter(val)
|
||||
})
|
||||
watch(
|
||||
() => queryForm.name,
|
||||
(val) => {
|
||||
orgTreeRef.value?.filter(val)
|
||||
}
|
||||
)
|
||||
const filterNode = (value: string, data: TreeNodeData): boolean => {
|
||||
if (!value) return true
|
||||
return data.name.includes(value)
|
||||
@ -83,9 +84,9 @@ const open = () => {
|
||||
dialogVisible.value = true
|
||||
}
|
||||
const onOpen = () => {
|
||||
getList().then(res => {
|
||||
getList().then((res) => {
|
||||
if (res.success) {
|
||||
roleOrgOptions.value = res.data.map(e => {
|
||||
roleOrgOptions.value = res.data.map((e) => {
|
||||
return {
|
||||
id: e.id,
|
||||
name: e.name
|
||||
@ -94,7 +95,6 @@ const onOpen = () => {
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
let roleIds: string[] = []
|
||||
if (Array.isArray(value.value)) {
|
||||
roleIds.push(...value.value)
|
||||
@ -102,9 +102,9 @@ const onOpen = () => {
|
||||
roleIds.push(value.value)
|
||||
}
|
||||
if (roleIds.length > 0) {
|
||||
getList(roleIds).then(res => {
|
||||
getList(roleIds).then((res) => {
|
||||
if (res.success) {
|
||||
roleOptions.value = res.data.map(role => {
|
||||
roleOptions.value = res.data.map((role) => {
|
||||
return {
|
||||
id: role.id,
|
||||
name: role.name
|
||||
@ -119,7 +119,7 @@ const onOpen = () => {
|
||||
}
|
||||
const handelConfirm = () => {
|
||||
if ($props.multiple) {
|
||||
value.value = roleOptions.value.map(e => e.id)
|
||||
value.value = roleOptions.value.map((e) => e.id)
|
||||
} else {
|
||||
if (roleOptions.value.length > 0) {
|
||||
value.value = roleOptions.value[0].id
|
||||
@ -136,42 +136,44 @@ defineExpose({
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
@open="onOpen"
|
||||
align-center
|
||||
draggable
|
||||
title="选择角色"
|
||||
width="30%"
|
||||
v-model="dialogVisible"
|
||||
@open="onOpen"
|
||||
align-center
|
||||
draggable
|
||||
title="选择角色"
|
||||
width="30%"
|
||||
>
|
||||
<el-card shadow="never" class="org-card">
|
||||
<template #header>
|
||||
<el-input
|
||||
v-model="queryForm.name"
|
||||
placeholder="输入关键字进行查询"
|
||||
:style="{width: '100%'}"
|
||||
suffix-icon="search"
|
||||
clearable>
|
||||
v-model="queryForm.name"
|
||||
placeholder="输入关键字进行查询"
|
||||
:style="{ width: '100%' }"
|
||||
suffix-icon="search"
|
||||
clearable
|
||||
>
|
||||
</el-input>
|
||||
</template>
|
||||
<el-scrollbar tag="div" class="org-tree">
|
||||
<el-tree
|
||||
ref="orgTreeRef"
|
||||
node-key="id"
|
||||
:data="roleOrgOptions"
|
||||
:default-expanded-keys="expandedKeys"
|
||||
:props="treeProps"
|
||||
:filter-node-method="filterNode"
|
||||
@node-click="onNodeClick">
|
||||
<template #default="{data}">
|
||||
ref="orgTreeRef"
|
||||
node-key="id"
|
||||
:data="roleOrgOptions"
|
||||
:default-expanded-keys="expandedKeys"
|
||||
:props="treeProps"
|
||||
:filter-node-method="filterNode"
|
||||
@node-click="onNodeClick"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<div class="flex flex-1 flex-items-center flex-justify-between">
|
||||
<div class="flex-center">
|
||||
<el-icon :size="16">
|
||||
<School/>
|
||||
<School />
|
||||
</el-icon>
|
||||
{{ data.name }}
|
||||
</div>
|
||||
<el-icon class="is-selected">
|
||||
<Check/>
|
||||
<Check />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
@ -179,7 +181,7 @@ defineExpose({
|
||||
</el-scrollbar>
|
||||
</el-card>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible=false">取消</el-button>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handelConfirm">确认</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@ -1,10 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import {getById} from '~/api/modules/role'
|
||||
import {componentSizeMap, useFormSize} from 'element-plus'
|
||||
import {computed, onMounted, reactive} from "vue";
|
||||
import { getById } from '@/api/modules/role'
|
||||
|
||||
export interface RoleTagProps {
|
||||
id: string,
|
||||
id: string
|
||||
type?: 'success' | 'info' | 'warning' | 'danger'
|
||||
closable?: boolean
|
||||
}
|
||||
@ -30,34 +28,20 @@ onMounted(() => {
|
||||
if (!$props.id) {
|
||||
throw new Error('username is required')
|
||||
}
|
||||
getById($props.id).then(res => {
|
||||
getById($props.id).then((res) => {
|
||||
if (res.success) {
|
||||
console.log(res);
|
||||
roleInfo.id = res.data.id
|
||||
roleInfo.name = res.data.name
|
||||
}
|
||||
})
|
||||
})
|
||||
const getBaseUrl = computed(() => {
|
||||
const url = import.meta.env.VITE_API_URL
|
||||
if (url.startsWith('http')) {
|
||||
return url
|
||||
} else {
|
||||
return window.location.origin + url
|
||||
}
|
||||
})
|
||||
const formSize = useFormSize()
|
||||
const getComponentSize = computed(() => {
|
||||
return componentSizeMap[formSize.value || 'default'] - 12
|
||||
})
|
||||
const onClose = () => {
|
||||
$emits('close', $props.id)
|
||||
}
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<el-tag round :closable="$props.closable" :type="type" effect="light" @close="onClose">
|
||||
<div class="flex-center" style="gap: 4px;grid-gap: 4px;">
|
||||
<div class="flex-center" style="gap: 4px; grid-gap: 4px">
|
||||
<span>{{ roleInfo.name || id }}</span>
|
||||
</div>
|
||||
</el-tag>
|
||||
@ -1,16 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import {useVModel} from '@vueuse/core'
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import RoleTag from './RoleTag.vue'
|
||||
import RolePicker, {ModelValueType} from './RolePicker.vue'
|
||||
import {useFormDisabled, useFormSize} from 'element-plus'
|
||||
import {computed, CSSProperties, ref} from 'vue'
|
||||
import {Avatar} from "@element-plus/icons-vue";
|
||||
import RolePicker, { type ModelValueType } from './RolePicker.vue'
|
||||
import { useFormDisabled, useFormSize } from 'element-plus'
|
||||
import type { CSSProperties } from 'vue'
|
||||
|
||||
export interface RoleSelectorProps {
|
||||
modelValue: ModelValueType,
|
||||
placeholder?: string,
|
||||
multiple?: boolean,
|
||||
disabled?: boolean,
|
||||
modelValue: ModelValueType
|
||||
placeholder?: string
|
||||
multiple?: boolean
|
||||
disabled?: boolean
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
@ -23,7 +22,7 @@ const $emits = defineEmits<{
|
||||
(e: 'update:modelValue', modelValue: ModelValueType): void
|
||||
}>()
|
||||
const value = useVModel($props, 'modelValue', $emits)
|
||||
const valueArr = computed<string []>(() => {
|
||||
const valueArr = computed<string[]>(() => {
|
||||
if (!value.value) return []
|
||||
return Array.isArray(value.value) ? value.value : [value.value]
|
||||
})
|
||||
@ -47,15 +46,27 @@ const onClose = (username: string) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<role-picker ref="RolePickerRef" :multiple="multiple" v-model="value"/>
|
||||
<role-picker ref="RolePickerRef" :multiple="multiple" v-model="value" />
|
||||
<div class="role-wrapper">
|
||||
<el-button class="role-but-item" :size="formSize" :disabled="disabled" @click="openRolePicker" circle>
|
||||
<el-button
|
||||
class="role-but-item"
|
||||
:size="formSize"
|
||||
:disabled="disabled"
|
||||
@click="openRolePicker"
|
||||
circle
|
||||
>
|
||||
<el-icon>
|
||||
<Avatar/>
|
||||
<Avatar />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<RoleTag v-for="item in valueArr" :closable="!disabled" :key="item" :id="item" @close="onClose"/>
|
||||
<el-text v-show="!value || value.length===0" class="placeholder">
|
||||
<RoleTag
|
||||
v-for="item in valueArr"
|
||||
:closable="!disabled"
|
||||
:key="item"
|
||||
:id="item"
|
||||
@close="onClose"
|
||||
/>
|
||||
<el-text v-show="!value || value.length === 0" class="placeholder">
|
||||
{{ placeholder }}
|
||||
</el-text>
|
||||
</div>
|
||||
@ -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">
|
||||
import {useVModel} from '@vueuse/core'
|
||||
import {TreeNodeData} from 'element-plus/es/components/tree/src/tree.type'
|
||||
import {type ElTree} from 'element-plus'
|
||||
import {reactive, ref, watch} from "vue";
|
||||
import {getList} from "~/api/modules/user";
|
||||
import {School, Check} from "@element-plus/icons-vue";
|
||||
import Node from "element-plus/es/components/tree/src/model/node";
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import type { TreeNodeData } from 'element-plus/es/components/tree/src/tree.type'
|
||||
import { getList } from '@/api/modules/user'
|
||||
import type { TreeInstance } from 'element-plus'
|
||||
|
||||
export type ModelValueType = string | string[] | null | undefined
|
||||
|
||||
export interface UserDropdownProps {
|
||||
modelValue: ModelValueType,
|
||||
modelValue: ModelValueType
|
||||
multiple?: boolean
|
||||
}
|
||||
|
||||
@ -41,11 +38,11 @@ const value = useVModel($props, 'modelValue', $emits)
|
||||
|
||||
const userOptions = ref<Org[]>([])
|
||||
const userOrgOptions = ref<Org[]>([])
|
||||
const orgTreeRef = ref<InstanceType<typeof ElTree>>()
|
||||
const orgTreeRef = ref<TreeInstance>()
|
||||
const expandedKeys = ref<string[]>([])
|
||||
|
||||
const renderClass = (org: TreeNodeData): string | { [key: string]: boolean } => {
|
||||
const val = userOptions.value.find(e => e.id === org.id)
|
||||
const val = userOptions.value.find((e) => e.id === org.id)
|
||||
if (val) {
|
||||
return 'is-active'
|
||||
} else {
|
||||
@ -56,7 +53,7 @@ const renderClass = (org: TreeNodeData): string | { [key: string]: boolean } =>
|
||||
const onNodeClick = (data: Org) => {
|
||||
if (data.type !== 'user') return
|
||||
if ($props.multiple) {
|
||||
const index = userOptions.value.findIndex(e => e.id === data.id)
|
||||
const index = userOptions.value.findIndex((e) => e.id === data.id)
|
||||
if (index === -1) {
|
||||
userOptions.value.push(data)
|
||||
userOptions.value.sort((a, b) => a.id.localeCompare(b.id))
|
||||
@ -64,7 +61,7 @@ const onNodeClick = (data: Org) => {
|
||||
userOptions.value.splice(index, 1)
|
||||
}
|
||||
} else {
|
||||
const index = userOptions.value.findIndex(e => e.id === data.id)
|
||||
const index = userOptions.value.findIndex((e) => e.id === data.id)
|
||||
if (index === -1) {
|
||||
userOptions.value = [data]
|
||||
} else {
|
||||
@ -74,12 +71,15 @@ const onNodeClick = (data: Org) => {
|
||||
}
|
||||
const dialogVisible = ref(false)
|
||||
const queryForm = reactive({
|
||||
name: null,
|
||||
name: null
|
||||
})
|
||||
|
||||
watch(() => queryForm.name, (val) => {
|
||||
orgTreeRef.value?.filter(val)
|
||||
})
|
||||
watch(
|
||||
() => queryForm.name,
|
||||
(val) => {
|
||||
orgTreeRef.value?.filter(val)
|
||||
}
|
||||
)
|
||||
const filterNode = (value: string, data: TreeNodeData): boolean => {
|
||||
if (!value) return true
|
||||
return data.name.includes(value)
|
||||
@ -88,9 +88,9 @@ const open = () => {
|
||||
dialogVisible.value = true
|
||||
}
|
||||
const onOpen = () => {
|
||||
getList().then(res => {
|
||||
getList().then((res) => {
|
||||
if (res.success) {
|
||||
userOrgOptions.value = res.data.map(e => {
|
||||
userOrgOptions.value = res.data.map((e) => {
|
||||
return {
|
||||
id: e.username,
|
||||
name: e.name,
|
||||
@ -102,7 +102,6 @@ const onOpen = () => {
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
let userIds: string[] = []
|
||||
if (Array.isArray(value.value)) {
|
||||
userIds.push(...value.value)
|
||||
@ -110,9 +109,9 @@ const onOpen = () => {
|
||||
userIds.push(value.value)
|
||||
}
|
||||
if (userIds.length > 0) {
|
||||
getList(userIds).then(res => {
|
||||
getList(userIds).then((res) => {
|
||||
if (res.success) {
|
||||
userOptions.value = res.data.map(user => {
|
||||
userOptions.value = res.data.map((user) => {
|
||||
return {
|
||||
id: user.username,
|
||||
name: user.name,
|
||||
@ -130,7 +129,7 @@ const onOpen = () => {
|
||||
}
|
||||
const handelConfirm = () => {
|
||||
if ($props.multiple) {
|
||||
value.value = userOptions.value.map(e => e.id)
|
||||
value.value = userOptions.value.map((e) => e.id)
|
||||
} else {
|
||||
if (userOptions.value.length > 0) {
|
||||
value.value = userOptions.value[0].id
|
||||
@ -147,45 +146,47 @@ defineExpose({
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
@open="onOpen"
|
||||
align-center
|
||||
draggable
|
||||
title="选择用户"
|
||||
width="30%"
|
||||
v-model="dialogVisible"
|
||||
@open="onOpen"
|
||||
align-center
|
||||
draggable
|
||||
title="选择用户"
|
||||
width="30%"
|
||||
>
|
||||
<el-card shadow="never" class="org-card">
|
||||
<template #header>
|
||||
<el-input
|
||||
v-model="queryForm.name"
|
||||
placeholder="输入关键字进行查询"
|
||||
:style="{width: '100%'}"
|
||||
suffix-icon="search"
|
||||
clearable>
|
||||
v-model="queryForm.name"
|
||||
placeholder="输入关键字进行查询"
|
||||
:style="{ width: '100%' }"
|
||||
suffix-icon="search"
|
||||
clearable
|
||||
>
|
||||
</el-input>
|
||||
</template>
|
||||
<el-scrollbar tag="div" class="org-tree">
|
||||
<el-tree
|
||||
ref="orgTreeRef"
|
||||
node-key="id"
|
||||
:data="userOrgOptions"
|
||||
:default-expanded-keys="expandedKeys"
|
||||
:props="treeProps"
|
||||
:filter-node-method="filterNode"
|
||||
@node-click="onNodeClick">
|
||||
<template #default="{data}">
|
||||
ref="orgTreeRef"
|
||||
node-key="id"
|
||||
:data="userOrgOptions"
|
||||
:default-expanded-keys="expandedKeys"
|
||||
:props="treeProps"
|
||||
:filter-node-method="filterNode"
|
||||
@node-click="onNodeClick"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<div class="flex flex-1 flex-items-center flex-justify-between">
|
||||
<div class="flex-center">
|
||||
<el-avatar v-if="data.type === 'user'" :size="25" :src="data.avatar">
|
||||
{{ data.name.charAt(0) }}
|
||||
</el-avatar>
|
||||
<el-icon v-else :size="16">
|
||||
<School/>
|
||||
<School />
|
||||
</el-icon>
|
||||
{{ data.name }}
|
||||
</div>
|
||||
<el-icon class="is-selected">
|
||||
<Check/>
|
||||
<Check />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
@ -193,7 +194,7 @@ defineExpose({
|
||||
</el-scrollbar>
|
||||
</el-card>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible=false">取消</el-button>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handelConfirm">确认</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@ -1,10 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import {getByUsername} from '~/api/modules/user'
|
||||
import {componentSizeMap, useFormSize} from 'element-plus'
|
||||
import {computed, onMounted, reactive} from "vue";
|
||||
import { getByUsername } from '@/api/modules/user'
|
||||
import { componentSizeMap, useFormSize } from 'element-plus'
|
||||
|
||||
export interface UserTagProps {
|
||||
username: string,
|
||||
username: string
|
||||
type?: 'success' | 'info' | 'warning' | 'danger'
|
||||
closable?: boolean
|
||||
}
|
||||
@ -32,7 +31,7 @@ onMounted(() => {
|
||||
if (!$props.username) {
|
||||
throw new Error('username is required')
|
||||
}
|
||||
getByUsername($props.username).then(res => {
|
||||
getByUsername($props.username).then((res) => {
|
||||
if (res.success) {
|
||||
userInfo.username = res.data.username
|
||||
userInfo.avatar = res.data.avatar
|
||||
@ -40,14 +39,6 @@ onMounted(() => {
|
||||
}
|
||||
})
|
||||
})
|
||||
const getBaseUrl = computed(() => {
|
||||
const url = import.meta.env.VITE_API_URL
|
||||
if (url.startsWith('http')) {
|
||||
return url
|
||||
} else {
|
||||
return window.location.origin + url
|
||||
}
|
||||
})
|
||||
const formSize = useFormSize()
|
||||
const getComponentSize = computed(() => {
|
||||
return componentSizeMap[formSize.value || 'default'] - 12
|
||||
@ -55,11 +46,10 @@ const getComponentSize = computed(() => {
|
||||
const onClose = () => {
|
||||
$emits('close', $props.username)
|
||||
}
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<el-tag round :closable="$props.closable" :type="type" effect="light" @close="onClose">
|
||||
<div class="flex-center" style="gap: 4px;grid-gap: 4px;">
|
||||
<div class="flex-center" style="gap: 4px; grid-gap: 4px">
|
||||
<el-avatar :size="getComponentSize" :src="userInfo.avatar">
|
||||
{{ (userInfo.name || username).charAt(0) }}
|
||||
</el-avatar>
|
||||
@ -1,16 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import {useVModel} from '@vueuse/core'
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import UserTag from './UserTag.vue'
|
||||
import UserPicker, {ModelValueType} from './UserPicker.vue'
|
||||
import {useFormDisabled, useFormSize} from 'element-plus'
|
||||
import {computed, CSSProperties, ref} from 'vue'
|
||||
import {User} from "@element-plus/icons-vue";
|
||||
import UserPicker, { type ModelValueType } from './UserPicker.vue'
|
||||
import { useFormDisabled, useFormSize } from 'element-plus'
|
||||
import { type CSSProperties } from 'vue'
|
||||
|
||||
export interface UserSelectorProps {
|
||||
modelValue: ModelValueType,
|
||||
placeholder?: string,
|
||||
multiple?: boolean,
|
||||
disabled?: boolean,
|
||||
modelValue: ModelValueType
|
||||
placeholder?: string
|
||||
multiple?: boolean
|
||||
disabled?: boolean
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
@ -23,7 +22,7 @@ const $emits = defineEmits<{
|
||||
(e: 'update:modelValue', modelValue: ModelValueType): void
|
||||
}>()
|
||||
const value = useVModel($props, 'modelValue', $emits)
|
||||
const valueArr = computed<string []>(() => {
|
||||
const valueArr = computed<string[]>(() => {
|
||||
if (!value.value) return []
|
||||
return Array.isArray(value.value) ? value.value : [value.value]
|
||||
})
|
||||
@ -47,15 +46,25 @@ const onClose = (username: string) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<user-picker ref="userPickerRef" :multiple="multiple" v-model="value"/>
|
||||
<user-picker ref="userPickerRef" :multiple="multiple" v-model="value" />
|
||||
<div class="user-wrapper">
|
||||
<el-button class="user-but-item" :size="formSize" :disabled="disabled" @click="openUserPicker" circle>
|
||||
<el-icon>
|
||||
<User/>
|
||||
</el-icon>
|
||||
<el-button
|
||||
class="user-but-item"
|
||||
:size="formSize"
|
||||
:disabled="disabled"
|
||||
@click="openUserPicker"
|
||||
circle
|
||||
>
|
||||
<svg-icon name="add-user" />
|
||||
</el-button>
|
||||
<user-tag v-for="item in valueArr" :closable="!disabled" :key="item" :username="item" @close="onClose"/>
|
||||
<el-text v-show="!value || value.length===0" class="placeholder">
|
||||
<user-tag
|
||||
v-for="item in valueArr"
|
||||
:closable="!disabled"
|
||||
:key="item"
|
||||
:username="item"
|
||||
@close="onClose"
|
||||
/>
|
||||
<el-text v-show="!value || value.length === 0" class="placeholder">
|
||||
{{ placeholder }}
|
||||
</el-text>
|
||||
</div>
|
||||
@ -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 App from "./App.vue";
|
||||
|
||||
// import "~/styles/element/index.scss";
|
||||
|
||||
// import ElementPlus from "element-plus";
|
||||
// import all element css, uncommented next line
|
||||
// import "element-plus/dist/index.css";
|
||||
|
||||
// or use cdn, uncomment cdn link in `index.html`
|
||||
|
||||
import "~/styles/index.scss";
|
||||
import "uno.css";
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import 'virtual:svg-icons-register'
|
||||
import 'uno.css'
|
||||
import '@/styles/index.scss'
|
||||
|
||||
// If you want to use ElMessage, import it.
|
||||
import "element-plus/theme-chalk/src/message.scss";
|
||||
import "element-plus/theme-chalk/src/notification.scss";
|
||||
import "element-plus/theme-chalk/el-input-number.css";
|
||||
import 'element-plus/theme-chalk/src/message.scss'
|
||||
import 'element-plus/theme-chalk/src/notification.scss'
|
||||
import 'element-plus/theme-chalk/el-input-number.css'
|
||||
|
||||
|
||||
const app = createApp(App);
|
||||
// app.use(ElementPlus);
|
||||
app.mount("#app");
|
||||
const app = createApp(App)
|
||||
import * as Icons from '@element-plus/icons-vue'
|
||||
for (const [key, component] of Object.entries(Icons)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
app.use(router).use(createPinia())
|
||||
app.mount('#app')
|
||||
|
||||
6
src/mock/index.ts
Normal file
@ -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 index from "../mock";
|
||||
|
||||
export const mockModules = [...index];
|
||||
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'
|
||||
import mock from './mock'
|
||||
|
||||
export function setupProdMockServer() {
|
||||
createProdMockServer(mockModules);
|
||||
createProdMockServer(mock)
|
||||
}
|
||||
|
||||
|
||||
15
src/router/index.ts
Normal file
@ -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-radius: var(--el-border-radius-base);
|
||||
--el-segmented-padding: 3px;
|
||||
--el-segmented-bg: var(--el-fill-color-light);
|
||||
--el-segmented-height: 28px;
|
||||
--el-segmented-font-size: 14px;
|
||||
--el-segmented-item-padding: 12px;
|
||||
--el-segmented-color: var(--el-text-color-secondary);
|
||||
--el-segmented-active-color: var(--el-text-color-primary);
|
||||
--el-segmented-active-bg: var(--el-bg-color-overlay);
|
||||
--el-segmented-active-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.08);
|
||||
--el-segmented-hover-bg: rgba(0, 0, 0, 0.04);
|
||||
--el-segmented-disabled-color: var(--el-text-color-placeholder);
|
||||
|
||||
:deep {
|
||||
&.is-block {
|
||||
.el-tabs__header {
|
||||
@ -39,7 +27,8 @@ const data = useVModel(props, 'modelValue', emits)
|
||||
padding: var(--el-segmented-padding);
|
||||
}
|
||||
|
||||
.el-tabs__nav-scroll, .el-tabs__nav-wrap {
|
||||
.el-tabs__nav-scroll,
|
||||
.el-tabs__nav-wrap {
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
@ -66,7 +55,9 @@ const data = useVModel(props, 'modelValue', emits)
|
||||
line-height: var(--el-segmented-height);
|
||||
font-size: var(--el-segmented-font-size);
|
||||
border-radius: var(--el-segmented-radius);
|
||||
transition: color .2s, background-color .2s;
|
||||
transition:
|
||||
color 0.2s,
|
||||
background-color 0.2s;
|
||||
background: none;
|
||||
z-index: 2;
|
||||
|
||||
@ -84,7 +75,6 @@ const data = useVModel(props, 'modelValue', emits)
|
||||
}
|
||||
}
|
||||
|
||||
/* 活动栏属性 */
|
||||
.el-tabs__active-bar {
|
||||
padding: 0 var(--el-segmented-item-padding);
|
||||
margin-left: calc(0px - var(--el-segmented-item-padding));
|
||||
@ -98,6 +88,4 @@ const data = useVModel(props, 'modelValue', emits)
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
@ -1,11 +1,11 @@
|
||||
// only scss variables
|
||||
|
||||
$--colors: (
|
||||
"primary": (
|
||||
"base": #589ef8,
|
||||
),
|
||||
'primary': (
|
||||
'base': #589ef8
|
||||
)
|
||||
);
|
||||
|
||||
@forward "element-plus/theme-chalk/src/dark/var.scss" with (
|
||||
@forward 'element-plus/theme-chalk/src/dark/var.scss' with (
|
||||
$colors: $--colors
|
||||
);
|
||||
|
||||
@ -1,36 +1,36 @@
|
||||
$--colors: (
|
||||
"primary": (
|
||||
"base": #589ef8,
|
||||
'primary': (
|
||||
'base': #589ef8
|
||||
),
|
||||
"success": (
|
||||
"base": #21ba45,
|
||||
'success': (
|
||||
'base': #21ba45
|
||||
),
|
||||
"warning": (
|
||||
"base": #f2711c,
|
||||
'warning': (
|
||||
'base': #f2711c
|
||||
),
|
||||
"danger": (
|
||||
"base": #db2828,
|
||||
'danger': (
|
||||
'base': #db2828
|
||||
),
|
||||
"error": (
|
||||
"base": #db2828,
|
||||
),
|
||||
"info": (
|
||||
"base": #42b8dd,
|
||||
'error': (
|
||||
'base': #db2828
|
||||
),
|
||||
'info': (
|
||||
'base': #42b8dd
|
||||
)
|
||||
);
|
||||
|
||||
// we can add this to custom namespace, default is 'el'
|
||||
@forward "element-plus/theme-chalk/src/mixins/config.scss" with (
|
||||
$namespace: "el"
|
||||
@forward 'element-plus/theme-chalk/src/mixins/config.scss' with (
|
||||
$namespace: 'el'
|
||||
);
|
||||
|
||||
// You should use them in scss, because we calculate it by sass.
|
||||
// comment next lines to use default color
|
||||
@forward "element-plus/theme-chalk/src/common/var.scss" with (
|
||||
// do not use same name, it will override.
|
||||
$colors: $--colors,
|
||||
// $button-padding-horizontal: ("default": 50px)
|
||||
);
|
||||
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
|
||||
// do not use same name, it will override.
|
||||
$colors: $--colors,
|
||||
// $button-padding-horizontal: ("default": 50px)
|
||||
);
|
||||
|
||||
// if you want to import all
|
||||
// @use "element-plus/theme-chalk/src/index.scss" as *;
|
||||
@ -39,4 +39,4 @@ $--colors: (
|
||||
// @debug $--colors;
|
||||
|
||||
// custom dark variables
|
||||
@use "./dark.scss";
|
||||
@use './dark.scss';
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
// import dark theme
|
||||
@use "element-plus/theme-chalk/src/dark/css-vars.scss" as *;
|
||||
@use 'element-plus/theme-chalk/src/dark/css-vars.scss' as *;
|
||||
|
||||
:root {
|
||||
.el-segmented {
|
||||
@ -12,8 +12,8 @@
|
||||
--el-segmented-color: var(--el-text-color-secondary);
|
||||
--el-segmented-active-color: var(--el-text-color-primary);
|
||||
--el-segmented-active-bg: var(--el-bg-color-overlay);
|
||||
--el-segmented-active-shadow: 0 1px 3px 0 rgba(0, 0, 0, .08);
|
||||
--el-segmented-hover-bg: rgba(0, 0, 0, .04);
|
||||
--el-segmented-active-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.08);
|
||||
--el-segmented-hover-bg: rgba(0, 0, 0, 0.04);
|
||||
--el-segmented-disabled-color: var(--el-text-color-placeholder);
|
||||
}
|
||||
}
|
||||
@ -34,7 +34,8 @@
|
||||
// 抽屉头部
|
||||
.el-drawer__header {
|
||||
margin-bottom: 0;
|
||||
padding: calc(var(--el-drawer-padding-primary) - 5px) var(--el-drawer-padding-primary) calc(var(--el-drawer-padding-primary) - 6px);
|
||||
padding: calc(var(--el-drawer-padding-primary) - 5px) var(--el-drawer-padding-primary)
|
||||
calc(var(--el-drawer-padding-primary) - 6px);
|
||||
border-bottom: 1px var(--el-border-style) var(--el-border-color);
|
||||
justify-content: space-between;
|
||||
// 抽屉标题
|
||||
@ -46,13 +47,13 @@
|
||||
|
||||
.el-drawer__footer {
|
||||
border-top: var(--el-border);
|
||||
padding: calc(var(--el-drawer-padding-primary) - 5px)
|
||||
padding: calc(var(--el-drawer-padding-primary) - 5px);
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Inter, system-ui, Avenir, "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
|
||||
"Microsoft YaHei", "微软雅黑", Arial, sans-serif;
|
||||
font-family: Inter, system-ui, Avenir, 'Helvetica Neue', Helvetica, 'PingFang SC',
|
||||
'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
margin: 0;
|
||||
@ -62,6 +63,15 @@ a {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
code {
|
||||
border-radius: 2px;
|
||||
padding: 2px 4px;
|
||||
|
||||
77
src/typings/auto-imports.d.ts
vendored
Normal file
@ -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 */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
Condition: typeof import('./components/Condition/index.ts')['default']
|
||||
AdvancedFilter: typeof import('./../components/AdvancedFilter/index.vue')['default']
|
||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
ElCol: typeof import('element-plus/es')['ElCol']
|
||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElDivider: typeof import('element-plus/es')['ElDivider']
|
||||
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||
ElForm: typeof import('element-plus/es')['ElForm']
|
||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||
ElLink: typeof import('element-plus/es')['ElLink']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
|
||||
@ -31,6 +32,7 @@ declare module 'vue' {
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSpace: typeof import('element-plus/es')['ElSpace']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
@ -40,9 +42,17 @@ declare module 'vue' {
|
||||
ElText: typeof import('element-plus/es')['ElText']
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
ElTree: typeof import('element-plus/es')['ElTree']
|
||||
Interface: typeof import('./components/Render/interface.ts')['default']
|
||||
RoleSelection: typeof import('./components/RoleSelection/index.ts')['default']
|
||||
Segmented: typeof import('./components/Segmented/index.ts')['default']
|
||||
UserSelection: typeof import('./components/UserSelection/index.ts')['default']
|
||||
Operator: typeof import('./../components/AdvancedFilter/Operator.vue')['default']
|
||||
Render: typeof import('./../components/Render/index.tsx')['default']
|
||||
RolePicker: typeof import('./../components/RoleSelector/RolePicker.vue')['default']
|
||||
RoleSelector: typeof import('./../components/RoleSelector/index.vue')['default']
|
||||
RoleTag: typeof import('./../components/RoleSelector/RoleTag.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SvgIcon: typeof import('./../components/SvgIcon/index.tsx')['default']
|
||||
Trigger: typeof import('./../components/AdvancedFilter/Trigger.vue')['default']
|
||||
UserPicker: typeof import('./../components/UserSelector/UserPicker.vue')['default']
|
||||
UserSelector: typeof import('./../components/UserSelector/index.vue')['default']
|
||||
UserTag: typeof import('./../components/UserSelector/UserTag.vue')['default']
|
||||
}
|
||||
}
|
||||
1
src/typings/index.d.ts
vendored
Normal file
@ -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">
|
||||
import NodeTree from './nodes/index.vue'
|
||||
import NodePenal from './penal/index.vue'
|
||||
import {FlowNode} from './nodes/Node/index'
|
||||
import useNode from './hooks/useNode'
|
||||
import {computed, onUnmounted, provide, ref} from "vue";
|
||||
import {Plus, Minus, Download, Sunny, Moon, TopRight, TopLeft} from "@element-plus/icons-vue";
|
||||
import {useVModels} from "@vueuse/core";
|
||||
import {Field} from "~/components/Render/interface";
|
||||
import {downloadXml} from "~/api/modules/model";
|
||||
import {useRefHistory} from '@vueuse/core'
|
||||
import {cloneDeep} from "lodash-es";
|
||||
<script setup lang="ts">
|
||||
import TreeNode from './nodes/TreeNode.vue'
|
||||
import Panel from './panels/index.vue'
|
||||
import type { ErrorInfo, FlowNode, TimerNode } from './nodes/type'
|
||||
import type {
|
||||
ApprovalNode,
|
||||
BranchNode,
|
||||
CcNode,
|
||||
ConditionNode,
|
||||
ExclusiveNode,
|
||||
NodeType
|
||||
} from './nodes/type'
|
||||
import type { FilterRules } from '@/components/AdvancedFilter/type'
|
||||
import type { Field } from '@/components/Render/type'
|
||||
import { downloadXml } from '@/api/modules/model'
|
||||
|
||||
export interface FlowDesignProps {
|
||||
process: FlowNode,
|
||||
const props = defineProps<{
|
||||
process: FlowNode
|
||||
fields: Field[]
|
||||
}
|
||||
readOnly?: boolean
|
||||
}>()
|
||||
|
||||
const $props = defineProps<FlowDesignProps>()
|
||||
const $emits = defineEmits(['update:process', 'update:fields'])
|
||||
const {fields} = useVModels($props, $emits)
|
||||
const process = ref<FlowNode>($props.process)
|
||||
const {undo, redo, canUndo, canRedo} = useRefHistory(process, {deep: true, clone: cloneDeep})
|
||||
const nodePenalRef = ref<InstanceType<typeof NodePenal>>()
|
||||
const zoom = ref(100)
|
||||
const isDark = ref(false)
|
||||
const flatFields = computed(() => {
|
||||
const all: Field[] = []
|
||||
const loop = (children: Field[]) => {
|
||||
children.forEach((field) => {
|
||||
if (field.type === 'formItem') {
|
||||
all.push(field)
|
||||
}
|
||||
if (Array.isArray(field.children)) {
|
||||
loop(field.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
loop(props.fields)
|
||||
return all
|
||||
})
|
||||
const getScale = computed(() => zoom.value / 100)
|
||||
const isDark = ref<boolean>(false)
|
||||
const zoom = ref(100)
|
||||
const activeData = ref<FlowNode>({
|
||||
id: '',
|
||||
name: '',
|
||||
type: 'start'
|
||||
})
|
||||
const penalVisible = ref(false)
|
||||
const nodesError = ref<Recordable<ErrorInfo[]>>({})
|
||||
provide('flowDesign', {
|
||||
readOnly: props.readOnly || false,
|
||||
fields: flatFields,
|
||||
nodesError: nodesError
|
||||
})
|
||||
const handleToggleDark = () => {
|
||||
if (isDark.value) {
|
||||
document.documentElement.classList.add('dark')
|
||||
@ -33,32 +58,224 @@ const handleToggleDark = () => {
|
||||
}
|
||||
}
|
||||
const openPenal = (node: FlowNode) => {
|
||||
nodePenalRef.value?.open(node)
|
||||
activeData.value = node
|
||||
penalVisible.value = true
|
||||
}
|
||||
const {addNode, delNode, validateNodes, addNodeRef} = useNode(process, fields)
|
||||
provide('nodeHooks', {
|
||||
readOnly: false,
|
||||
fields: fields,
|
||||
addNode,
|
||||
delNode,
|
||||
addNodeRef,
|
||||
openPenal
|
||||
})
|
||||
const handleZoom = (e: WheelEvent) => {
|
||||
if (e.shiftKey) {
|
||||
if (e.deltaY > 0) {
|
||||
if (zoom.value > 50) {
|
||||
zoom.value -= 10
|
||||
const nextId = (): string => {
|
||||
let id = `node_${Math.random().toString(36).substring(2, 7)}`
|
||||
const findId = (node: FlowNode, id: string): boolean => {
|
||||
if (node.id === id) {
|
||||
return true
|
||||
}
|
||||
if (node.child) {
|
||||
return findId(node.child, id)
|
||||
}
|
||||
if ('children' in node) {
|
||||
const branchNode = node as BranchNode
|
||||
if (branchNode.children && branchNode.children.length > 0) {
|
||||
return branchNode.children.some((item) => {
|
||||
return findId(item, id)
|
||||
})
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
if (findId(props.process, id)) {
|
||||
return nextId()
|
||||
}
|
||||
return id
|
||||
}
|
||||
const addExclusive = (node: FlowNode) => {
|
||||
const child = node.child
|
||||
const id = nextId()
|
||||
const exclusiveNode = {
|
||||
id: id,
|
||||
pid: node.id,
|
||||
type: 'exclusive',
|
||||
name: '独占网关',
|
||||
child: child,
|
||||
children: []
|
||||
} as ExclusiveNode
|
||||
if (child) {
|
||||
child.pid = id
|
||||
}
|
||||
addCondition(exclusiveNode)
|
||||
addCondition(exclusiveNode)
|
||||
node.child = exclusiveNode
|
||||
if (exclusiveNode.children.length > 0) {
|
||||
const condition = exclusiveNode.children[exclusiveNode.children.length - 1] as ConditionNode
|
||||
condition.def = true
|
||||
condition.name = '默认条件'
|
||||
}
|
||||
}
|
||||
const addCondition = (node: FlowNode) => {
|
||||
const exclusive = node as ExclusiveNode
|
||||
exclusive.children.splice(exclusive.children.length - 1, 0, {
|
||||
id: nextId(),
|
||||
pid: exclusive.id,
|
||||
type: 'condition',
|
||||
def: false,
|
||||
name: `条件${exclusive.children.length + 1}`,
|
||||
conditions: {
|
||||
operator: 'and',
|
||||
conditions: [],
|
||||
groups: []
|
||||
} as FilterRules,
|
||||
child: undefined
|
||||
})
|
||||
}
|
||||
const addCc = (node: FlowNode) => {
|
||||
const child = node.child
|
||||
const id = nextId()
|
||||
node.child = {
|
||||
id: id,
|
||||
pid: node.id,
|
||||
type: 'cc',
|
||||
name: '抄送人',
|
||||
child: child,
|
||||
assigneeType: 'user',
|
||||
formUser: '',
|
||||
formRole: '',
|
||||
users: [],
|
||||
roles: [],
|
||||
leader: 1,
|
||||
orgLeader: 1,
|
||||
choice: false,
|
||||
self: false,
|
||||
formProperties: []
|
||||
} as CcNode
|
||||
if (child) {
|
||||
child.pid = id
|
||||
}
|
||||
}
|
||||
const addTimer = (node: FlowNode) => {
|
||||
const child = node.child
|
||||
const id = nextId()
|
||||
node.child = {
|
||||
id: id,
|
||||
pid: node.id,
|
||||
name: '计时等待',
|
||||
type: 'timer',
|
||||
child: child,
|
||||
waitType: 'duration',
|
||||
unit: 'PT%sS',
|
||||
duration: 0,
|
||||
timeDate: undefined
|
||||
} as TimerNode
|
||||
if (child) {
|
||||
child.pid = id
|
||||
}
|
||||
}
|
||||
const addApproval = (node: FlowNode) => {
|
||||
const child = node.child
|
||||
const id = nextId()
|
||||
node.child = {
|
||||
id: id,
|
||||
pid: node.id,
|
||||
type: 'approval',
|
||||
name: '审批人',
|
||||
child: child,
|
||||
// 属性
|
||||
assigneeType: 'user',
|
||||
formUser: '',
|
||||
formRole: '',
|
||||
users: [],
|
||||
roles: [],
|
||||
leader: 1,
|
||||
orgLeader: 1,
|
||||
choice: false,
|
||||
self: false,
|
||||
multi: 'sequential',
|
||||
nobody: 'pass',
|
||||
nobodyUsers: [],
|
||||
formProperties: [],
|
||||
operations: {
|
||||
complete: true,
|
||||
refuse: true,
|
||||
back: true,
|
||||
transfer: true,
|
||||
delegate: true,
|
||||
addMulti: false,
|
||||
minusMulti: false
|
||||
}
|
||||
} as ApprovalNode
|
||||
if (child) {
|
||||
child.pid = id
|
||||
}
|
||||
}
|
||||
const addNode = (type: NodeType, node: FlowNode) => {
|
||||
const addMap: Recordable<(node: FlowNode) => void> = {
|
||||
exclusive: addExclusive,
|
||||
condition: addCondition,
|
||||
cc: addCc,
|
||||
timer: addTimer,
|
||||
approval: addApproval
|
||||
}
|
||||
const fun = addMap[type]
|
||||
fun && fun(node)
|
||||
}
|
||||
const delNode = (del: FlowNode) => {
|
||||
delete nodesError.value[del.id]
|
||||
delNodeNext(props.process, del)
|
||||
}
|
||||
const delNodeNext = (next: FlowNode, del: FlowNode) => {
|
||||
delete nodesError.value[del.id]
|
||||
if (next.id === del.pid) {
|
||||
if ('children' in next && next.child?.id !== del.id) {
|
||||
const branchNode = next as BranchNode
|
||||
const index = branchNode.children.findIndex((item) => item.id === del.id)
|
||||
if (index !== -1) {
|
||||
if (branchNode.children.length <= 2) {
|
||||
delError(branchNode)
|
||||
delNode(branchNode)
|
||||
} else {
|
||||
delError(del)
|
||||
branchNode.children.splice(index, 1)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (zoom.value < 170) {
|
||||
zoom.value += 10
|
||||
if (del.child && del.child.pid) {
|
||||
del.child.pid = next.id
|
||||
}
|
||||
next.child = del.child
|
||||
}
|
||||
} else {
|
||||
if (next.child) {
|
||||
delNodeNext(next.child, del)
|
||||
}
|
||||
if ('children' in next) {
|
||||
const nextBranch = next as BranchNode
|
||||
if (nextBranch.children && nextBranch.children.length > 0) {
|
||||
nextBranch.children.forEach((item) => {
|
||||
delNodeNext(item, del)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const delError = (node: FlowNode) => {
|
||||
delete nodesError.value[node.id]
|
||||
if (node.child) {
|
||||
delError(node.child)
|
||||
}
|
||||
if ('children' in node) {
|
||||
const branchNode = node as BranchNode
|
||||
if (branchNode.children && branchNode.children.length > 0) {
|
||||
branchNode.children.forEach((item) => {
|
||||
delError(item)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
const validate = () => {
|
||||
validateNodes()
|
||||
return new Promise((resolve, reject) => {
|
||||
const errors = Object.values(nodesError.value).flat()
|
||||
if (errors.length > 0) {
|
||||
reject(errors)
|
||||
} else {
|
||||
resolve(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
const converterBpmn = () => {
|
||||
const processModel = {
|
||||
@ -66,45 +283,19 @@ const converterBpmn = () => {
|
||||
name: '测试',
|
||||
icon: {
|
||||
name: 'el:HomeFilled',
|
||||
color: '#409EFF',
|
||||
color: '#409EFF'
|
||||
},
|
||||
process: process.value,
|
||||
process: props.process,
|
||||
enable: true,
|
||||
version: 1,
|
||||
sort: 0,
|
||||
groupId: '',
|
||||
remark: '',
|
||||
remark: ''
|
||||
}
|
||||
downloadXml(processModel)
|
||||
}
|
||||
const downloadJson = () => {
|
||||
const processModel = {
|
||||
code: 'test',
|
||||
name: '测试',
|
||||
icon: {
|
||||
name: 'el:HomeFilled',
|
||||
color: '#409EFF',
|
||||
},
|
||||
process: process.value,
|
||||
form: {
|
||||
fields: fields.value
|
||||
},
|
||||
version: 1,
|
||||
sort: 0,
|
||||
groupId: '',
|
||||
remark: '',
|
||||
}
|
||||
const blob = new Blob([JSON.stringify(processModel, null, 2)], {type: 'application/json'})
|
||||
const a = document.createElement('a')
|
||||
a.download = 'process.json'
|
||||
a.href = URL.createObjectURL(blob)
|
||||
a.click()
|
||||
URL.revokeObjectURL(a.href)
|
||||
}
|
||||
// 按住shift键滚动鼠标滚轮,可以放大/缩小
|
||||
window.addEventListener('wheel', handleZoom)
|
||||
// 离开页面时,销毁事件监听
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('wheel', handleZoom)
|
||||
defineExpose({
|
||||
validate
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -112,48 +303,48 @@ onUnmounted(() => {
|
||||
<div class="designer-container">
|
||||
<div class="tool">
|
||||
<el-switch
|
||||
inline-prompt
|
||||
:active-icon="Sunny"
|
||||
:inactive-icon="Moon"
|
||||
@change="handleToggleDark"
|
||||
v-model="isDark"/>
|
||||
inline-prompt
|
||||
active-icon="Sunny"
|
||||
inactive-icon="Moon"
|
||||
@change="handleToggleDark"
|
||||
v-model="isDark"
|
||||
/>
|
||||
</div>
|
||||
<!--放大/缩小-->
|
||||
<div class="zoom">
|
||||
<el-button :icon="Plus" @click="zoom += 10" :disabled="zoom >= 170" circle></el-button>
|
||||
<el-tooltip content="放大" placement="bottom-start">
|
||||
<el-button icon="plus" @click="zoom += 10" :disabled="zoom >= 170" circle></el-button>
|
||||
</el-tooltip>
|
||||
<span>{{ zoom }}%</span>
|
||||
<el-button :icon="Minus" @click="zoom -= 10" circle :disabled="zoom <= 50"></el-button>
|
||||
<el-button @click="undo()" :disabled="!canUndo" :icon="TopLeft">撤销</el-button>
|
||||
<el-button @click="redo()" :disabled="!canRedo" :icon="TopRight">重做</el-button>
|
||||
<el-button @click="validate">校验</el-button>
|
||||
<el-button @click="downloadJson" type="primary" :icon="Download">导出json</el-button>
|
||||
<el-button @click="converterBpmn" type="primary" :icon="Download">转bpmn</el-button>
|
||||
<el-tooltip content="缩小" placement="bottom-start">
|
||||
<el-button icon="minus" @click="zoom -= 10" circle :disabled="zoom <= 50"></el-button>
|
||||
</el-tooltip>
|
||||
<el-button @click="converterBpmn" type="primary" icon="Download">转bpmn</el-button>
|
||||
</div>
|
||||
<!--流程树-->
|
||||
<div class="node-container">
|
||||
<NodeTree :node="process"/>
|
||||
<TreeNode :node="process" @addNode="addNode" @delNode="delNode" @activeNode="openPenal" />
|
||||
</div>
|
||||
<!--属性面板-->
|
||||
<NodePenal ref="nodePenalRef"/>
|
||||
<Panel v-model="penalVisible" :active-data="activeData" />
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.designer-container {
|
||||
background-color: var(--el-bg-color);
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-height: 100%;
|
||||
min-width: 100%;
|
||||
// overflow: scroll;
|
||||
overflow: auto;
|
||||
background-color: var(--el-bg-color-page);
|
||||
|
||||
.zoom {
|
||||
position: fixed;
|
||||
z-index: 999;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
top: 30px;
|
||||
right: 40px;
|
||||
|
||||
span {
|
||||
margin: 0 10px;
|
||||
|
||||
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>
|
||||