Merge remote-tracking branch 'origin/master'
已删除24个文件
已修改196个文件
已添加69个文件
| | |
| | | # 开发环境:本地只启动前端项目,依赖开发环境(后端、APP) |
| | | NODE_ENV=production |
| | | NODE_ENV=development |
| | | |
| | | VITE_DEV=true |
| | | |
| | | # 请求路径 |
| | | VITE_BASE_URL='http://localhost:48080' |
| | | VITE_BASE_URL='http://localhost' |
| | | |
| | | # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务 |
| | | VITE_UPLOAD_TYPE=server |
| | | # 上传路径 |
| | | VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload' |
| | | |
| | | # 接口地址 |
| | | VITE_API_URL=/admin-api |
| | |
| | | VITE_SOURCEMAP=true |
| | | |
| | | # 打包路径 |
| | | VITE_BASE_PATH=/plat |
| | | VITE_BASE_PATH=/plat/ |
| | | |
| | | # 输出路径 |
| | | VITE_OUT_DIR=dist |
| | | |
| | | # 公共静态文件路径 |
| | | VITE_STATIC_DIR=/ |
| | | VITE_STATIC_DIR=/plat/ |
| | | |
| | | # 商城H5会员端域名iai |
| | | VITE_MALL_H5_DOMAIN='http://' |
| | |
| | | VITE_APP_CAPTCHA_ENABLE=false |
| | | |
| | | # MDK模型上传路径 |
| | | MDK_UPLOAD_URL='http://localhost:48080/admin-api/model//pre/item/upload-model' |
| | | MDK_UPLOAD_URL='http://localhost/admin-api/model/pre/item/upload-model' |
| | |
| | | |
| | | # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务 |
| | | VITE_UPLOAD_TYPE=server |
| | | # 上传路径 |
| | | VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload' |
| | | |
| | | # 接口地址 |
| | | VITE_API_URL=/admin-api |
| | |
| | | VITE_SOURCEMAP=false |
| | | |
| | | # 打包路径 |
| | | VITE_BASE_PATH=/plat |
| | | VITE_BASE_PATH=/plat/ |
| | | |
| | | # 公共静态文件路径 |
| | | VITE_STATIC_DIR=/ |
| | | VITE_STATIC_DIR=/plat/ |
| | | |
| | | # 商城H5会员端域名 |
| | | VITE_MALL_H5_DOMAIN='http://localhost:3000' |
| | |
| | | # 生产环境:只在打包时使用 |
| | | # 测试环境:只在打包时使用 |
| | | NODE_ENV=production |
| | | |
| | | VITE_DEV=false |
| | | |
| | | # 请求路径 |
| | | VITE_BASE_URL='http://localhost:48080' |
| | | VITE_BASE_URL='http://10.88.4.131' |
| | | |
| | | # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务 |
| | | VITE_UPLOAD_TYPE=server |
| | | # 上传路径 |
| | | VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload' |
| | | |
| | | # 接口地址 |
| | | VITE_API_URL=/admin-api |
| | |
| | | VITE_SOURCEMAP=false |
| | | |
| | | # 打包路径 |
| | | VITE_BASE_PATH=/ |
| | | VITE_BASE_PATH=/plat |
| | | |
| | | # 数据采集服务所在服务器,映射截图图片用 |
| | | VITE_VIDEO_CAMERA_DOMAIN='10.88.4.131' |
| | | |
| | | # 输出路径 |
| | | VITE_OUT_DIR=dist-prod |
| | | VITE_OUT_DIR=dist |
| | | |
| | | # 商城H5会员端域名 |
| | | VITE_MALL_H5_DOMAIN='http://' |
| | | # 公共静态文件路径 |
| | | VITE_STATIC_DIR=/plat/ |
| | | |
| | | # 验证码的开关 |
| | | VITE_APP_CAPTCHA_ENABLE=false |
| | |
| | | VITE_DEV=false |
| | | |
| | | # 请求路径 |
| | | VITE_BASE_URL='http://172.16.8.100:48080' |
| | | VITE_BASE_URL='http://172.16.8.100' |
| | | |
| | | # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务 |
| | | VITE_UPLOAD_TYPE=server |
| | | # 上传路径 |
| | | VITE_UPLOAD_URL='http://172.16.8.100:48080/admin-api/infra/file/upload' |
| | | |
| | | # 接口地址 |
| | | VITE_API_URL=/admin-api |
| | |
| | | |
| | | # 公共静态文件路径 |
| | | VITE_STATIC_DIR=/plat/ |
| | | |
| | | # 商城H5会员端域名 |
| | | VITE_MALL_H5_DOMAIN='http://' |
| | | |
| | | # 验证码的开关 |
| | | VITE_APP_CAPTCHA_ENABLE=false |
| | |
| | | <html lang="en"> |
| | | <head> |
| | | <meta charset="UTF-8" /> |
| | | <link rel="icon" href="/favicon.ico" /> |
| | | <link rel="icon" href="/src/assets/imgs/logo.png" /> |
| | | <meta http-equiv="X-UA-Compatible" content="IE=edge" /> |
| | | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| | | <meta |
| | |
| | | <div class="app-loading"> |
| | | <div class="app-loading-wrap"> |
| | | <div class="app-loading-title"> |
| | | <img src="/logo.gif" class="app-loading-logo" alt="Logo" /> |
| | | <img src="/src/assets/imgs/logo.png" class="app-loading-logo" alt="Logo" /> |
| | | <div class="app-loading-title">%VITE_APP_TITLE%</div> |
| | | </div> |
| | | <div class="app-loading-item"> |
| | |
| | | "description": "基于vue3、vite4、element-plus、typesScript", |
| | | "author": "iailab", |
| | | "private": false, |
| | | "main": "dist/iailab-plat-ui.min.js", |
| | | "scripts": { |
| | | "i": "pnpm install", |
| | | "dev": "vite --mode env.local", |
| | | "dev-server": "vite --mode dev", |
| | | "ts:check": "vue-tsc --noEmit", |
| | | "build:local": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build", |
| | | "build:dev": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode dev", |
| | | "build:test": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode test", |
| | | "build:stage": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode stage", |
| | | "build:prod": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode prod", |
| | | "build:local": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build", |
| | | "build:dev": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build --mode dev", |
| | | "build:test": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build --mode test", |
| | | "build:stage": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build --mode stage", |
| | | "build:prod": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build --mode prod", |
| | | "serve:dev": "vite preview --mode dev", |
| | | "serve:prod": "vite preview --mode prod", |
| | | "preview": "pnpm build:local && vite preview", |
| | |
| | | }, |
| | | "dependencies": { |
| | | "@element-plus/icons-vue": "^2.1.0", |
| | | "@form-create/designer": "^3.1.3", |
| | | "@form-create/element-ui": "^3.1.24", |
| | | "@form-create/designer": "^3.2.6", |
| | | "@form-create/element-ui": "^3.2.11", |
| | | "@iconify/iconify": "^3.1.1", |
| | | "@microsoft/fetch-event-source": "^2.0.1", |
| | | "@videojs-player/vue": "^1.0.0", |
| | |
| | | "animate.css": "^4.1.1", |
| | | "axios": "^1.6.8", |
| | | "benz-amr-recorder": "^1.1.5", |
| | | "bpmn-js-token-simulation": "^0.10.0", |
| | | "bpmn-js-token-simulation": "^0.36.0", |
| | | "camunda-bpmn-moddle": "^7.0.1", |
| | | "cropperjs": "^1.6.1", |
| | | "crypto-js": "^4.2.0", |
| | |
| | | "driver.js": "^1.3.1", |
| | | "echarts": "^5.5.0", |
| | | "echarts-wordcloud": "^2.1.0", |
| | | "element-plus": "2.7.0", |
| | | "element-plus": "2.9.1", |
| | | "fast-xml-parser": "^4.3.2", |
| | | "highlight.js": "^11.9.0", |
| | | "jsencrypt": "^3.3.2", |
| | |
| | | "pinia-plugin-persistedstate": "^3.2.1", |
| | | "qrcode": "^1.5.3", |
| | | "qs": "^6.12.0", |
| | | "sortablejs": "^1.15.3", |
| | | "steady-xml": "^0.1.0", |
| | | "url": "^0.11.3", |
| | | "video.js": "^7.21.5", |
| | | "vue": "3.4.21", |
| | | "vue": "3.5.12", |
| | | "vue-dompurify-html": "^4.1.4", |
| | | "vue-i18n": "9.10.2", |
| | | "vue-router": "^4.3.0", |
| | | "vue-router": "4.4.5", |
| | | "vue-types": "^5.1.1", |
| | | "vuedraggable": "^4.1.0", |
| | | "web-storage-cache": "^1.1.1", |
| | | "wujie-vue3": "^1.0.22", |
| | | "xml-js": "^1.6.11" |
| | | "xml-js": "^1.6.11", |
| | | "wujie-vue3": "^1.0.22" |
| | | }, |
| | | "devDependencies": { |
| | | "@commitlint/cli": "^19.0.1", |
| | |
| | | "@vitejs/plugin-vue": "^5.0.4", |
| | | "@vitejs/plugin-vue-jsx": "^3.1.0", |
| | | "autoprefixer": "^10.4.17", |
| | | "bpmn-js": "8.9.0", |
| | | "bpmn-js-properties-panel": "0.46.0", |
| | | "bpmn-js": "^17.9.2", |
| | | "bpmn-js-properties-panel": "5.23.0", |
| | | "consola": "^3.2.3", |
| | | "eslint": "^8.57.0", |
| | | "eslint-config-prettier": "^9.1.0", |
| | |
| | | "stylelint-order": "^6.0.4", |
| | | "terser": "^5.28.1", |
| | | "typescript": "5.3.3", |
| | | "unocss": "^0.58.9", |
| | | "unocss": "^0.58.5", |
| | | "unplugin-auto-import": "^0.16.7", |
| | | "unplugin-element-plus": "^0.8.0", |
| | | "unplugin-vue-components": "^0.25.2", |
| | |
| | | "vite-plugin-progress": "^0.0.7", |
| | | "vite-plugin-purge-icons": "^0.10.0", |
| | | "vite-plugin-svg-icons": "^2.0.1", |
| | | "vite-plugin-top-level-await": "^1.3.1", |
| | | "vite-plugin-top-level-await": "^1.4.4", |
| | | "vue-eslint-parser": "^9.3.2", |
| | | "vue-tsc": "^1.8.27" |
| | | }, |
| | |
| | | "url": "https://xxxx" |
| | | }, |
| | | "homepage": "https://xxxx", |
| | | "web-types": "./web-types.json", |
| | | "engines": { |
| | | "node": ">= 16.0.0", |
| | | "pnpm": ">=8.6.0" |
| | |
| | | return await request.put({ url: `/bpm/category/update`, data }) |
| | | }, |
| | | |
| | | // 批量修改流程分类的排序 |
| | | updateCategorySortBatch: async (ids: number[]) => { |
| | | return await request.put({ |
| | | url: `/bpm/category/update-sort-batch`, |
| | | params: { |
| | | ids: ids.join(',') |
| | | } |
| | | }) |
| | | }, |
| | | |
| | | // 删除流程分类 |
| | | deleteCategory: async (id: number) => { |
| | | return await request.delete({ url: `/bpm/category/delete?id=` + id }) |
| | |
| | | bpmnXml: string |
| | | } |
| | | |
| | | export const getModelPage = async (params) => { |
| | | return await request.get({ url: '/bpm/model/page', params }) |
| | | export const getModelList = async (name: string | undefined) => { |
| | | return await request.get({ url: '/bpm/model/list', params: { name } }) |
| | | } |
| | | |
| | | export const getModel = async (id: number) => { |
| | | export const getModel = async (id: string) => { |
| | | return await request.get({ url: '/bpm/model/get?id=' + id }) |
| | | } |
| | | |
| | |
| | | return await request.put({ url: '/bpm/model/update', data: data }) |
| | | } |
| | | |
| | | // 批量修改流程分类的排序 |
| | | export const updateModelSortBatch = async (ids: number[]) => { |
| | | return await request.put({ |
| | | url: `/bpm/model/update-sort-batch`, |
| | | params: { |
| | | ids: ids.join(',') |
| | | } |
| | | }) |
| | | } |
| | | |
| | | export const updateModelBpmn = async (data: ModelVO) => { |
| | | return await request.put({ url: '/bpm/model/update-bpmn', data: data }) |
| | | } |
| | | |
| | | // 任务状态修改 |
| | | export const updateModelState = async (id: number, state: number) => { |
| | | const data = { |
| | |
| | | import request from '@/config/axios' |
| | | import { ProcessDefinitionVO } from '@/api/bpm/model' |
| | | |
| | | import { NodeType, CandidateStrategy } from '@/components/SimpleProcessDesignerV2/src/consts' |
| | | export type Task = { |
| | | id: string |
| | | name: string |
| | |
| | | createTime: string |
| | | endTime: string |
| | | processDefinition?: ProcessDefinitionVO |
| | | } |
| | | |
| | | // 用户信息 |
| | | export type User = { |
| | | id: number |
| | | nickname: string |
| | | avatar: string |
| | | } |
| | | |
| | | // 审批任务信息 |
| | | export type ApprovalTaskInfo = { |
| | | id: number |
| | | ownerUser: User |
| | | assigneeUser: User |
| | | status: number |
| | | reason: string |
| | | } |
| | | |
| | | // 审批节点信息 |
| | | export type ApprovalNodeInfo = { |
| | | id: number |
| | | name: string |
| | | nodeType: NodeType |
| | | candidateStrategy?: CandidateStrategy |
| | | status: number |
| | | startTime?: Date |
| | | endTime?: Date |
| | | candidateUsers?: User[] |
| | | tasks: ApprovalTaskInfo[] |
| | | } |
| | | |
| | | export const getProcessInstanceMyPage = async (params: any) => { |
| | |
| | | export const getProcessInstanceCopyPage = async (params: any) => { |
| | | return await request.get({ url: '/bpm/process-instance/copy/page', params }) |
| | | } |
| | | |
| | | // 获取审批详情 |
| | | export const getApprovalDetail = async (params: any) => { |
| | | return await request.get({ url: 'bpm/process-instance/get-approval-detail' , params }) |
| | | } |
| | | |
| | | // 获取表单字段权限 |
| | | export const getFormFieldsPermission = async (params: any) => { |
| | | return await request.get({ url: '/bpm/process-instance/get-form-fields-permission', params }) |
| | | } |
| | | |
| | | // 获取流程实例的 BPMN 模型视图 |
| | | export const getProcessInstanceBpmnModelView = async (id: string) => { |
| | | return await request.get({ url: '/bpm/process-instance/get-bpmn-model-view?id=' + id }) |
| | | } |
对比新文件 |
| | |
| | | import request from '@/config/axios' |
| | | |
| | | |
| | | export const updateBpmSimpleModel = async (data) => { |
| | | return await request.post({ |
| | | url: '/bpm/model/simple/update', |
| | | data: data |
| | | }) |
| | | } |
| | | |
| | | export const getBpmSimpleModel = async (id) => { |
| | | return await request.get({ |
| | | url: '/bpm/model/simple/get?id=' + id |
| | | }) |
| | | } |
| | |
| | | import request from '@/config/axios' |
| | | |
| | | export type TaskVO = { |
| | | id: number |
| | | /** |
| | | * 任务状态枚举 |
| | | */ |
| | | export enum TaskStatusEnum { |
| | | /** |
| | | * 未开始 |
| | | */ |
| | | NOT_START = -1, |
| | | |
| | | /** |
| | | * 待审批 |
| | | */ |
| | | WAIT = 0, |
| | | /** |
| | | * 审批中 |
| | | */ |
| | | RUNNING = 1, |
| | | /** |
| | | * 审批通过 |
| | | */ |
| | | APPROVE = 2, |
| | | |
| | | /** |
| | | * 审批不通过 |
| | | */ |
| | | REJECT = 3, |
| | | |
| | | /** |
| | | * 已取消 |
| | | */ |
| | | CANCEL = 4, |
| | | /** |
| | | * 已退回 |
| | | */ |
| | | RETURN = 5, |
| | | /** |
| | | * 审批通过中 |
| | | */ |
| | | APPROVING = 7 |
| | | } |
| | | |
| | | export const getTaskTodoPage = async (params: any) => { |
| | |
| | | }) |
| | | } |
| | | |
| | | // 获取所有可回退的节点 |
| | | // 获取所有可退回的节点 |
| | | export const getTaskListByReturn = async (id: string) => { |
| | | return await request.get({ url: '/bpm/task/list-by-return', params: { id } }) |
| | | } |
| | | |
| | | // 回退 |
| | | // 退回 |
| | | export const returnTask = async (data: any) => { |
| | | return await request.put({ url: '/bpm/task/return', data }) |
| | | } |
| | |
| | | return await request.delete({ url: '/bpm/task/delete-sign', data }) |
| | | } |
| | | |
| | | // 抄送 |
| | | export const copyTask = async (data: any) => { |
| | | return await request.put({ url: '/bpm/task/copy', data }) |
| | | } |
| | | |
| | | // 获取我的待办任务 |
| | | export const myTodoTask = async (processInstanceId: string) => { |
| | | return await request.get({ url: '/bpm/task/my-todo?processInstanceId=' + processInstanceId }) |
| | | } |
| | | |
| | | // 获取减签任务列表 |
| | | export const getChildrenTaskList = async (id: string) => { |
| | | return await request.get({ url: '/bpm/task/list-by-parent-task-id?parentTaskId=' + id }) |
| | |
| | | |
| | | export interface DaPointPageReqVO extends PageParam { |
| | | pointNo?: string, |
| | | pointName?: string |
| | | pointName?: string, |
| | | tagNo?: string, |
| | | collectQuality?: string, |
| | | } |
| | | |
| | | |
| | |
| | | return request.get({ url: '/data/da/point/list', params }) |
| | | } |
| | | |
| | | // 查询DaPoint simpleList |
| | | export const getPointSimpleList = (params: DaPointPageReqVO) => { |
| | | return request.get({ url: '/data/da/point/simple-list', params }) |
| | | } |
| | | |
| | | // 查询DaPoint详情 |
| | | export const getDaPoint = (id: number) => { |
| | | return request.get({ url: `/data/da/point/info/${id}`}) |
| | |
| | | return request.get({ url: '/system/auth/get-permission-info' }) |
| | | } |
| | | |
| | | // 获取用应用户权限信息 |
| | | export const getUserAppInfo = (id: number) => { |
| | | return request.get({ url: '/system/auth/get-app-permission-info?id=' + id }) |
| | | } |
| | | // // 获取用应用户权限信息 |
| | | // export const getUserAppInfo = (id: number) => { |
| | | // return request.get({ url: '/system/auth/get-app-permission-info?id=' + id }) |
| | | // } |
| | | |
| | | //获取登录验证码 |
| | | export const sendSmsCode = (data: SmsCodeVO) => { |
| | |
| | | return request.post({ url: '/model/mpk/api/test', data: params }) |
| | | } |
| | | |
| | | export const list = () => { |
| | | return request.get({ url: '/model/mpk/file/list'}) |
| | | export const list = (params) => { |
| | | return request.get({ url: '/model/mpk/file/list', params}) |
| | | } |
| | | |
| | | export const publish = (params) => { |
| | |
| | | import request from '@/config/axios' |
| | | |
| | | export const getPage = async (params: PageParam) => { |
| | | export const getPage = async (params) => { |
| | | return await request.get({ url: '/model/mpk/project/page', params }) |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | export const packageProject = (params) => { |
| | | return request.download({ url: '/model/mpk/file/packageModel', params }) |
| | | // 超时时间两分钟 |
| | | return request.download({ url: '/model/mpk/file/packageModel', params, timeout: 2 * 60 * 1000 }) |
| | | } |
| | | |
| | | export const list = () => { |
| | | return request.get({ url: '/model/mpk/project/list'}) |
| | | } |
| | | |
| | | export const getProjectModel = async (params: PageParam) => { |
| | | export const getProjectModel = async (params) => { |
| | | return await request.get({ url: '/model/mpk/project/getProjectModel', params }) |
| | | } |
| | |
| | | export interface MmPredictItemPageReqVO extends PageParam { |
| | | itemno?: string, |
| | | itemname?: string, |
| | | itemtypeid?: string, |
| | | modulename?: string, |
| | | } |
| | | |
| | | // 查询MmPredictItem列表 |
| | |
| | | status: number, |
| | | paramList: null, |
| | | settingList: null |
| | | modelOut:null |
| | | } |
| | | |
| | | export interface ModelParamVO { |
| | |
| | | } |
| | | |
| | | // 查询模型参数列表 |
| | | export const getModelParamList = async () => { |
| | | export const getModelParamList = async (id) => { |
| | | |
| | | const dataPointList = ref([] as DataPointApi.DaPointVO) |
| | | dataPointList.value = await DataPointApi.getPointList({}) |
| | |
| | | pointList.push( |
| | | { |
| | | id: item.id, |
| | | name: item.pointName |
| | | name: item.pointName, |
| | | itemNo : item.pointNo |
| | | } |
| | | ) |
| | | }) |
| | |
| | | |
| | | const predictItemList = ref([] as PredictItemApi.MmPredictItemVO) |
| | | predictItemList.value = await PredictItemApi.getMmPredictItemList({ |
| | | status: CommonEnabled.ENABLE, |
| | | itemtypename: 'NormalItem' |
| | | status: CommonEnabled.ENABLE |
| | | }) |
| | | const itemList = [] |
| | | if (predictItemList.value) { |
| | | predictItemList.value.forEach(item => { |
| | | itemList.push( |
| | | const normalItemList = [] |
| | | const predictNormalItemList = predictItemList.value.filter(e => e.itemtypename === 'NormalItem' && e.outPuts && e.outPuts.length > 0); |
| | | if (predictNormalItemList && predictNormalItemList.length > 0) { |
| | | // 过滤掉本身 |
| | | predictNormalItemList.filter(e => e.id !== id).forEach(item => { |
| | | normalItemList.push( |
| | | { |
| | | id: item.id, |
| | | name: item.itemname |
| | | value: item.id, |
| | | label: item.itemname, |
| | | predictlength: item.predictlength, |
| | | moduleid: item.moduleid, |
| | | children: item.outPuts?.map(e => { |
| | | return { |
| | | value: e.id, |
| | | label: e.resultName |
| | | } |
| | | }) |
| | | } |
| | | ) |
| | | }) |
| | |
| | | }) |
| | | } |
| | | |
| | | const predictMergeItemList = predictItemList.value.filter(e => e.itemtypename === 'MergeItem' && e.outPuts && e.outPuts.length > 0); |
| | | const mergeItemList = [] |
| | | if (predictMergeItemList && predictMergeItemList.length > 0) { |
| | | // 过滤掉本身 |
| | | predictMergeItemList.filter(e => e.id !== id).forEach(item => { |
| | | mergeItemList.push( |
| | | { |
| | | id: item.outPuts[0].id, |
| | | name: item.itemname |
| | | } |
| | | ) |
| | | }) |
| | | } |
| | | |
| | | return { |
| | | 'DATAPOINT':pointList, |
| | | 'PREDICTITEM': itemList, |
| | | 'NormalItem': normalItemList, |
| | | 'MergeItem': mergeItemList, |
| | | 'PLAN': planList, |
| | | } |
| | | } |
对比新文件 |
| | |
| | | import request from '@/config/axios' |
| | | |
| | | export interface StScheduleRecordPageReqVO extends PageParam { |
| | | schemeId?: string |
| | | } |
| | | |
| | | // 查询ScheduleRecord列表 |
| | | export const getScheduleRecordPage = (params: StScheduleRecordPageReqVO) => { |
| | | return request.get({ url: '/model/sche/record/page', params }) |
| | | } |
| | | |
| | | // 查询ScheduleRecord详情 |
| | | export const getScheduleRecord = (id: string) => { |
| | | return request.get({ url: '/model/sche/record/get?id=' + id}) |
| | | } |
| | |
| | | scheduleTime: string |
| | | remark: string |
| | | status: number |
| | | mpkprojectid: string |
| | | } |
| | | |
| | | export interface ScheduleSchemePageReqVO extends PageParam { |
| | |
| | | export const deleteScheduleScheme = (id: number) => { |
| | | return request.delete({ url: '/model/sche/scheme/delete?id=' + id }) |
| | | } |
| | | |
| | | // 启用 |
| | | export const enable = (ids) => { |
| | | const data = ids |
| | | return request.put({ url: '/model/sche/scheme/enable', data }) |
| | | } |
| | | |
| | | // 禁用 |
| | | export const disable = (ids) => { |
| | | const data = ids |
| | | return request.put({ url: '/model/sche/scheme/disable', data }) |
| | | } |
| | | |
| | |
| | | id: number |
| | | name: string |
| | | status: number |
| | | icon: string |
| | | labels: string |
| | | description: string |
| | | remark: string |
| | | creator: string |
| | | updater: string |
对比新文件 |
| | |
| | | <svg t="1731390087280" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4297" width="200" height="200"><path d="M639.9 541.7c76.4-44.2 127.9-126.8 127.9-221.5C767.7 179 653.2 64.5 512 64.5S256.3 179 256.3 320.2c0 89.6 46.1 168.4 115.8 214.1C193.5 593 64.5 761.2 64.5 959.5h63.9c0-211.5 172.1-383.6 383.6-383.6 44.9 0 87.8 8.1 127.9 22.4v-56.6zM320.2 320.2c0-105.8 86-191.8 191.8-191.8s191.8 86 191.8 191.8S617.7 512 512 512s-191.8-86-191.8-191.8zM831.6 767.7V639.9h-63.9v127.8H639.9v63.9h127.8v127.9h63.9V831.6h127.9v-63.9z" fill="#5f6266" p-id="4298"></path></svg> |
对比新文件 |
| | |
| | | <?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="1724316565416" class="icon" viewBox="0 0 1300 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1339" xmlns:xlink="http://www.w3.org/1999/xlink" width="253.90625" height="200"><path d="M784.058182 99.258182l10.938182 18.385454-21.294546-2.56-14.196363 16.058182-4.072728-21.061818-19.781818-8.494545 18.734546-10.472728 2.094545-21.294545 15.709091 14.545454 20.945454-4.654545-9.076363 19.549091zM1067.287273 642.443636l-18.501818 10.821819 2.56-21.294546-16.058182-14.196364 21.061818-4.072727 8.494545-19.665454 10.472728 18.734545 21.294545 1.978182-14.661818 15.709091 4.770909 20.945454-19.432727-8.96z" fill="#13C463" p-id="1340"></path><path d="M1067.287273 642.443636l-18.501818 10.821819 2.56-21.294546-16.058182-14.196364 21.061818-4.072727 8.494545-19.665454 10.472728 18.734545 21.294545 1.978182-14.661818 15.709091 4.770909 20.945454-19.432727-8.96zM571.927273 100.072727l-17.454546-12.567272 20.596364-6.167273 6.516364-20.48 12.218181 17.570909 21.410909-0.116364-12.916363 17.105455 6.749091 20.363636-20.247273-6.981818-17.338182 12.683636 0.465455-21.410909zM991.418182 784.407273l-21.178182 3.490909 10.123636-18.967273-9.774545-18.967273 21.061818 3.723637 15.127273-15.243637 2.909091 21.294546 19.2 9.658182-19.316364 9.309091-3.258182 21.178181-14.894545-15.476363zM427.985455 156.741818L407.272727 151.505455l16.872728-13.265455-1.396364-21.410909 17.803636 11.985454 20.014546-7.912727-5.934546 20.596364 13.730909 16.523636-21.410909 0.814546-11.52 18.152727-7.447272-20.247273zM854.225455 896.465455l-20.712728-5.352728 16.872728-13.265454-1.396364-21.294546 17.803636 11.869091 20.014546-7.912727-5.934546 20.712727 13.730909 16.523637-21.527272 0.814545-11.403637 18.036364-7.447272-20.130909zM562.501818 923.694545l10.821818 18.385455-21.294545-2.56-14.196364 16.058182-4.072727-21.061818-19.665455-8.494546 18.734546-10.356363 1.978182-21.41091 15.709091 14.661819 20.945454-4.770909-8.96 19.54909zM242.734545 420.770909l-18.385454 10.938182 2.56-21.294546-16.058182-14.196363 21.061818-4.189091 8.494546-19.665455 10.356363 18.734546 21.410909 2.094545-14.545454 15.709091 4.654545 20.945455-19.549091-9.076364z" fill="#13C463" p-id="1341"></path><path d="M242.734545 420.770909l-18.385454 10.938182 2.56-21.294546-16.058182-14.196363 21.061818-4.189091 8.494546-19.665455 10.356363 18.734546 21.410909 2.094545-14.545454 15.709091 4.654545 20.945455-19.549091-9.076364zM700.858182 943.941818l-17.454546-12.450909 20.48-6.283636 6.516364-20.48 12.334545 17.687272 21.41091-0.116363-12.916364 17.105454 6.632727 20.363637-20.247273-7.098182-17.221818 12.683636 0.465455-21.410909zM303.592727 278.807273l-21.178182 3.490909 10.123637-18.967273-9.890909-18.967273 21.178182 3.723637 15.010909-15.243637 2.909091 21.294546 19.2 9.541818-19.316364 9.425455-3.258182 21.178181-14.778182-15.476363z" fill="#13C463" p-id="1342"></path><path d="M407.272727 90.647273a486.632727 486.632727 0 0 1 504.552728 11.636363l25.018181-14.429091A512 512 0 0 0 139.636364 546.909091l25.018181-14.429091A486.981818 486.981818 0 0 1 407.272727 90.647273zM893.323636 933.352727a486.749091 486.749091 0 0 1-504.669091-11.636363l-24.901818 14.429091A512 512 0 0 0 1161.192727 477.090909l-24.901818 13.963636a486.981818 486.981818 0 0 1-242.967273 442.298182z" fill="#13C463" p-id="1343"></path><path d="M814.545455 795.927273a327.447273 327.447273 0 0 1-258.21091 29.556363l-29.78909 17.105455A353.163636 353.163636 0 0 0 998.865455 570.181818l-29.789091 17.105455A326.865455 326.865455 0 0 1 814.545455 795.927273zM486.865455 228.072727A327.447273 327.447273 0 0 1 744.727273 198.516364l29.789091-17.105455A353.163636 353.163636 0 0 0 302.545455 453.818182l29.78909-17.105455A326.865455 326.865455 0 0 1 486.865455 228.072727zM1288.378182 374.690909a53.294545 53.294545 0 0 1-14.429091 11.636364L229.469091 989.090909a53.876364 53.876364 0 0 1-73.425455-19.665454L7.214545 710.632727a53.527273 53.527273 0 0 1 19.781819-73.309091L1071.476364 34.909091a53.876364 53.876364 0 0 1 73.425454 19.665454l148.829091 258.327273a53.061818 53.061818 0 0 1 5.352727 40.727273 55.272727 55.272727 0 0 1-10.705454 21.061818zM32.232727 665.716364A28.043636 28.043636 0 0 0 29.323636 698.181818l148.829091 257.978182a28.392727 28.392727 0 0 0 38.516364 10.356364l1044.48-601.949091a28.16 28.16 0 0 0 10.356364-38.516364L1122.676364 67.84a28.276364 28.276364 0 0 0-38.4-10.356364L39.68 659.432727a27.810909 27.810909 0 0 0-7.447273 6.283637z" fill="#13C463" p-id="1344"></path><path d="M356.770909 569.250909l22.341818 38.749091-15.476363 8.727273L349.090909 592.64l-153.483636 88.785455 14.778182 25.483636-15.476364 8.96-23.272727-39.912727L256 627.2c-6.283636-4.887273-11.636364-8.843636-16.174545-11.636364L256 602.647273c3.956364 3.141818 9.774545 8.261818 17.338182 15.127272z m-17.338182 199.447273l-49.221818 28.392727 7.563636 13.149091-15.476363 8.96-62.138182-107.52 64.814545-37.469091-12.8-22.574545 15.941819-9.192728 12.8 22.109091 65.396363-37.701818 61.672728 106.821818-15.476364 8.96-7.214546-12.450909-49.92 28.858182 26.065455 45.032727-16.058182 9.192728z m-46.545454-79.825455L244.363636 717.265455l14.778182 25.6 49.221818-28.509091zM267.636364 756.945455l14.778181 25.6 49.221819-28.509091-14.778182-25.483637z m106.938181-80.523637l-14.778181-25.483636-49.92 28.741818 14.778181 25.483636zM346.996364 744.727273l49.803636-28.741818-14.661818-25.483637-49.92 28.741818zM505.832727 609.978182c-4.654545 6.283636-10.123636 13.265455-16.523636 21.061818l35.84 62.021818a18.967273 18.967273 0 0 1-6.749091 29.672727l-19.316364 11.636364-12.450909-13.847273a170.123636 170.123636 0 0 0 17.803637-8.727272 8.494545 8.494545 0 0 0 2.909091-13.614546L477.090909 645.352727l-9.890909 10.472728-10.007273 10.24-12.683636-13.149091c9.309091-8.261818 17.221818-15.941818 23.272727-23.272728l-31.301818-54.341818-25.018182 14.545455-8.843636-15.36 25.018182-14.429091-23.272728-41.076364 15.476364-8.96 23.272727 41.076364L465.454545 538.763636l8.843637 15.36-22.109091 12.567273 28.509091 49.221818c5.469091-6.516364 10.938182-13.498182 16.407273-21.061818z m9.076364-45.730909L572.043636 663.272727a207.825455 207.825455 0 0 0 23.272728-27.461818l11.636363 13.149091a365.381818 365.381818 0 0 1-41.774545 45.498182l-12.567273-12.567273a11.636364 11.636364 0 0 0 1.745455-13.963636L453.818182 493.963636l15.709091-9.076363 36.887272 63.883636 31.301819-18.152727 8.96 15.592727z m129.745454 83.316363a20.596364 20.596364 0 0 1-31.418181-9.774545l-103.098182-178.618182 15.709091-9.192727 38.632727 67.025454a200.261818 200.261818 0 0 0 28.043636-41.076363l16.872728 7.68a303.243636 303.243636 0 0 1-35.723637 49.338182l53.410909 93.090909a9.192727 9.192727 0 0 0 13.963637 4.072727l10.821818-6.283636a14.312727 14.312727 0 0 0 8.029091-11.636364 103.447273 103.447273 0 0 0-15.243637-39.098182l17.338182-3.84c12.567273 25.134545 18.036364 41.658182 16.290909 49.803636A28.392727 28.392727 0 0 1 663.272727 636.741818zM860.276364 521.774545c-7.563636 4.421818-20.829091 11.636364-39.912728 22.574546a179.432727 179.432727 0 0 1-37.352727 16.174545 58.181818 58.181818 0 0 1-33.047273-1.978181 14.312727 14.312727 0 0 0-11.636363-0.581819c-5.352727 3.025455-8.261818 18.385455-8.727273 45.847273l-18.269091-3.956364c1.047273-25.483636 5.003636-42.821818 11.636364-52.014545l-38.865455-67.374545-31.534545 18.152727-8.378182-14.661818 46.545454-26.647273 47.825455 82.850909a55.505455 55.505455 0 0 1 8.494545 1.861818 59.694545 59.694545 0 0 0 25.367273 4.072727 101.701818 101.701818 0 0 0 33.512727-11.636363L849.454545 508.509091l31.418182-18.734546c11.636364-7.214545 19.898182-12.334545 24.087273-15.127272l5.469091 18.152727zM676.072727 413.207273L671.185455 430.545455a279.272727 279.272727 0 0 0-58.181819-13.265455l4.887273-16.64a307.781818 307.781818 0 0 1 58.181818 12.567273zM754.967273 372.363636a261.818182 261.818182 0 0 0 20.247272-38.516363l-98.443636 56.785454-7.796364-13.498182 119.97091-69.46909 6.632727 11.636363a281.134545 281.134545 0 0 1-25.949091 54.807273l5.236364 0.930909L818.734545 349.090909l57.25091 99.025455a18.385455 18.385455 0 0 1-8.843637 27.927272l-18.385454 10.589091-11.636364-11.636363 17.92-9.425455a7.796364 7.796364 0 0 0 3.607273-11.636364L849.454545 437.410909l-37.236363 21.527273 21.992727 38.050909-14.894545 8.610909-21.992728-38.167273L760.203636 488.727273l22.458182 38.749091-15.127273 8.727272L699.461818 418.909091l55.389091-32a306.269091 306.269091 0 0 0-39.330909-1.047273l4.305455-15.127273c13.265455-0.232727 24.901818 0.465455 35.141818 1.629091z m15.825454 49.454546l-11.636363-20.014546-37.003637 21.410909 11.636364 20.014546z m-29.44 34.909091l11.636364 19.549091 37.003636-21.410909-11.636363-19.549091z m81.454546-64.814546l-11.636364-19.898182-37.236364 21.527273 11.636364 19.898182z m-29.556364 34.909091l11.636364 19.432727 37.236363-21.527272-11.636363-19.432728zM1086.370909 391.214545l-19.898182 11.636364-10.589091 6.167273-10.938181 6.050909a186.181818 186.181818 0 0 1-38.749091 16.989091 60.16 60.16 0 0 1-33.978182-1.978182 14.312727 14.312727 0 0 0-11.636364 0c-5.585455 3.258182-8.610909 18.734545-8.96 46.545455l-18.036363-3.723637c0.814545-26.181818 4.770909-43.752727 11.636363-52.945454l-38.865454-67.141819-31.883637 18.385455-8.727272-15.010909 47.243636-27.345455 47.941818 83.2h4.189091a32.465455 32.465455 0 0 1 4.538182 1.163637 71.68 71.68 0 0 0 26.298182 3.490909 112.872727 112.872727 0 0 0 34.210909-13.265455c16.523636-9.192727 31.767273-17.803636 46.545454-25.949091l14.545455-8.727272 14.196363-8.727273c11.636364-6.865455 18.618182-11.636364 22.574546-14.196364l5.352727 18.385455zM896 286.021818l-4.770909 18.385455a296.378182 296.378182 0 0 0-58.181818-14.661818l4.770909-16.872728a311.156364 311.156364 0 0 1 58.181818 13.149091zM1031.098182 384l-12.334546-13.149091c11.636364-5.934545 21.76-11.636364 30.138182-15.941818a9.658182 9.658182 0 0 0 4.189091-14.661818l-54.341818-94.138182-83.781818 48.290909-9.076364-15.709091 83.781818-48.407273-20.712727-35.84 16.174545-9.425454 20.712728 36.072727 32.814545-18.967273 8.610909 15.243637-32.349091 18.850909 56.552728 97.978182a20.247273 20.247273 0 0 1-8.843637 31.185454z m-23.272727-59.345455L1000.727273 340.48a405.876364 405.876364 0 0 0-58.181818-25.6l7.796363-15.127273a393.890909 393.890909 0 0 1 57.716364 24.436364z" fill="#13C463" p-id="1345"></path></svg> |
对比新文件 |
| | |
| | | <svg t="1729561718271" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8640" width="200" height="200"><path d="M908.5952 920.4224H164.7616a31.0784 31.0784 0 0 1-30.976-30.976c0-17.0496 13.9264-30.976 30.976-30.976h743.8336c17.0496 0 31.0272 13.9264 31.0272 30.976a31.0784 31.0784 0 0 1-31.0272 30.976z m0-123.9552H164.7616a31.0784 31.0784 0 0 1-30.976-30.976v-154.9824c0-51.1488 41.8304-92.9792 92.9792-92.9792h198.3488c-6.1952-37.1712-24.7808-72.8064-51.1488-103.8336a216.576 216.576 0 0 1-54.2208-144.128c0-58.88 23.2448-114.688 66.6112-156.4672C429.7728 71.168 485.5296 51.0976 545.9968 52.6848c111.5648 4.608 206.08 100.6592 207.616 212.2752 1.536 55.808-20.1216 110.0288-57.344 151.8592-26.3168 27.904-41.8304 61.952-48.0256 100.7104h198.3488c51.2 0 93.0304 41.8304 93.0304 92.9792v154.9824a31.0784 31.0784 0 0 1-31.0272 30.976z m-712.8064-61.952H877.568v-124.0064a31.0784 31.0784 0 0 0-31.0272-30.976h-232.448a31.0784 31.0784 0 0 1-30.976-31.0272c0-65.024 23.2448-127.0784 66.6624-173.568 27.8528-29.3888 41.8304-68.1472 41.8304-108.4416-1.536-80.5888-68.1984-148.7872-148.7872-151.8592a150.528 150.528 0 0 0-113.152 43.3664 153.6 153.6 0 0 0-48.0256 111.616c0 37.1712 13.9776 74.3424 38.7584 102.2464 44.9536 51.1488 69.7344 113.152 69.7344 176.64a31.0784 31.0784 0 0 1-30.976 31.0272h-232.448a31.0784 31.0784 0 0 0-30.976 30.976v123.9552z" fill="#fff" p-id="8641"></path></svg> |
对比新文件 |
| | |
| | | <svg t="1729178183592" class="icon" viewBox="0 0 1300 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4332" width="200" height="200"><path d="M784.074702 99.196443l10.927871 18.473304-21.302843-2.56935-14.180213 16.066571-4.130475-21.042655-19.676671-8.521137 18.733492-10.440019 2.016452-21.335366 15.708814 14.603017 20.945085-4.683373-9.041512 19.449008zM1067.22363 642.402668l-18.440781 10.92787 2.56935-21.302842-16.099094-14.180213 21.042655-4.130475 8.521137-19.676671 10.440019 18.733492 21.367889 2.016452-14.603017 15.708814 4.683373 20.945085-19.481531-9.041512z" fill="#8a8a8a" p-id="4333"></path><path d="M1067.22363 642.402668l-18.440781 10.92787 2.56935-21.302842-16.099094-14.180213 21.042655-4.130475 8.521137-19.676671 10.440019 18.733492 21.367889 2.016452-14.603017 15.708814 4.683373 20.945085-19.481531-9.041512zM571.924408 100.009528l-17.400031-12.488994 20.52228-6.211974 6.504685-20.457234 12.261331 17.595172 21.432936-0.09757-12.944323 17.074798 6.732349 20.359663-20.262093-7.02506-17.269938 12.716659 0.422804-21.46546zM991.444053 784.43246l-21.172749 3.480006 10.114785-18.928632-9.822074-19.026203 21.107702 3.772717 15.090868-15.253486 2.927109 21.237796 19.156296 9.626933-19.318914 9.366746-3.219819 21.205273-14.863204-15.48115zM428.008258 156.795426l-20.749945-5.333841 16.879657-13.237034-1.365983-21.400413 17.822836 11.936097 19.936859-7.870669-5.88674 20.619851 13.692361 16.521899-21.432936 0.813086-11.513292 18.083024-7.382817-20.132zM854.260251 896.475655l-20.749945-5.333841 16.879657-13.237034-1.365983-21.400413 17.822836 11.96862 19.936859-7.903192-5.854217 20.619851 13.659838 16.554423-21.432936 0.780562-11.513292 18.115547-7.382817-20.164523zM562.460092 923.665237l10.895347 18.440782-21.302843-2.569351-14.180212 16.099095-4.130475-21.042655-19.676672-8.521137 18.733493-10.440019 2.016452-21.36789 15.708814 14.603018 20.945085-4.683373-9.008989 19.48153zM242.787359 420.788058l-18.473305 10.895347 2.569351-21.302843-16.066572-14.180213 21.042656-4.130474 8.521137-19.676672 10.440019 18.733492 21.335366 2.016453-14.603018 15.708813 4.683374 20.945085-19.449008-9.008988z" fill="#8a8a8a" p-id="4334"></path><path d="M242.787359 420.788058l-18.473305 10.895347 2.569351-21.302843-16.066572-14.180213 21.042656-4.130474 8.521137-19.676672 10.440019 18.733492 21.335366 2.016453-14.603018 15.708813 4.683374 20.945085-19.449008-9.008988zM700.814737 943.959854l-17.400032-12.521518 20.522281-6.211974 6.504685-20.42471 12.26133 17.595172 21.432937-0.130094-12.944323 17.107321 6.732349 20.359663-20.262093-7.025059-17.269938 12.684135 0.422804-21.432936zM303.541115 278.823313l-21.140226 3.480006 10.114785-18.928633-9.854597-19.058726 21.107702 3.772717 15.090868-15.220962 2.927109 21.237796 19.156296 9.626933-19.28639 9.366746-3.252342 21.172749-14.863205-15.448626z" fill="#8a8a8a" p-id="4335"></path><path d="M407.648595 90.642782a486.713038 486.713038 0 0 1 504.568397 11.578339l25.010513-14.407877A512.081309 512.081309 0 0 0 139.850723 547.401747l24.977989-14.407877a486.778085 486.778085 0 0 1 242.819883-442.351088zM893.28836 933.422265a486.810608 486.810608 0 0 1-504.568398-11.610863l-25.010513 14.407877a512.081309 512.081309 0 0 0 797.5394-459.621026l-24.97799 14.505447a486.843132 486.843132 0 0 1-242.982499 442.318565z" fill="#8a8a8a" p-id="4336"></path><path d="M814.061299 795.880705a326.665269 326.665269 0 0 1-258.170939 29.563792l-29.791456 17.172368a353.236906 353.236906 0 0 0 472.793013-272.448721l-29.693886 17.172367a326.762839 326.762839 0 0 1-155.136732 208.540194zM486.875655 228.119295a326.795363 326.795363 0 0 1 258.170939-29.563792l29.791456-17.172368a353.236906 353.236906 0 0 0-472.793013 272.448721l29.82398-17.172367a326.762839 326.762839 0 0 1 155.006638-208.540194zM1288.350389 374.73489a53.923837 53.923837 0 0 1-14.34283 12.001143L229.420232 988.712085A53.793743 53.793743 0 0 1 156.112434 968.937843l-148.924757-258.235985a53.76122 53.76122 0 0 1 19.741718-73.437891L1071.516722 35.352962A53.826266 53.826266 0 0 1 1144.82452 55.062157l148.827187 258.268508a53.793743 53.793743 0 0 1-5.398888 61.404225zM32.19819 665.754486a28.360426 28.360426 0 0 0-5.626553 10.73273 28.067715 28.067715 0 0 0 2.699444 21.432936L178.195839 956.188661a28.165285 28.165285 0 0 0 38.442687 10.342449l1044.587328-601.976052a28.132762 28.132762 0 0 0 10.440019-38.442687l-148.924758-258.268509a28.197808 28.197808 0 0 0-38.442687-10.342449L39.711101 659.444942a28.230332 28.230332 0 0 0-7.512911 6.309544z" fill="#8a8a8a" p-id="4337"></path><path d="M498.941845 597.390249l-138.322121 79.877529 38.637827 66.933207q8.000762 13.854979 21.595554 5.98431l114.254788-65.957504a21.172749 21.172749 0 0 0 9.952167-11.123011q2.634397-9.757027-16.91218-47.321582l18.440781-4.130474q20.489757 43.22363 18.148071 56.167953a36.166047 36.166047 0 0 1-16.261712 19.514054l-123.068636 71.031158q-25.17313 14.603017-40.394092-11.77348L317.103383 639.020232l16.066571-9.269176 18.570875 32.133143 122.027886-70.47826-33.596697-58.249452-150.160648 86.707448-9.041511-15.611243 166.454883-96.106718zM691.903319 563.663459c-3.935334 3.837764-9.757027 9.399269-17.497602 16.619469l23.319295 40.394093-15.611244 9.008988-21.237795-36.816516q-31.027346 27.709957-64.754137 54.314118l-12.814229-13.39965 9.171605-7.382818 9.236653-7.122629-79.714912-138.126982-17.627696 10.179832-8.781324-15.155915L601.683341 414.836271l6.960013 12.06619 86.34969-49.858408 8.488614 14.733111q28.197808 65.82741 30.506972 123.39387a274.660314 274.660314 0 0 0 69.339939 27.612387l-3.642623 18.440781a322.177037 322.177037 0 0 1-65.534699-26.40902 220.899095 220.899095 0 0 1-15.38358 72.819946l-18.14807-6.179451a215.272542 215.272542 0 0 0 15.448626-77.340702 312.940384 312.940384 0 0 1-89.374369-86.739971l-8.748801 5.138701-7.2202-12.488995-17.172368 9.919644 71.876767 124.499667q10.570113-10.017215 17.465079-16.61947z m-134.32174-56.948515l40.166428-23.189202-19.969382-34.702493-40.166429 23.189201z m28.067714 48.785135l40.166429-23.189201-19.514055-33.921931-40.166428 23.189201z m48.557472-8.813847l-40.166428 23.189201 21.888264 37.922312q13.334604-10.92787 35.775766-30.767159z m7.2202-117.832365A289.848753 289.848753 0 0 0 715.515325 503.365031a330.437986 330.437986 0 0 0-26.441544-101.92841zM812.760362 400.460918l-4.813467 17.95293a280.482007 280.482007 0 0 0-56.167953-12.781706l5.073654-17.530125a291.637542 291.637542 0 0 1 55.907766 12.358901z m24.360045 28.78323a925.063745 925.063745 0 0 1 10.017214 101.895887l-18.440781 2.016452a812.792886 812.792886 0 0 0-8.878895-101.375512z m-45.923075-86.25212l-4.813467 18.017977a290.922026 290.922026 0 0 0-58.542163-11.513292l5.073655-17.497602a308.972527 308.972527 0 0 1 58.281975 10.992917z m48.459902-17.562649l-9.334223 13.724885A298.792695 298.792695 0 0 0 783.814515 315.477211l9.757027-14.180212a437.635191 437.635191 0 0 1 46.085692 24.13238zM834.355916 269.944418l16.521899-9.529363 35.157821 60.916373 48.199714-27.840051L1003.282579 413.047483q12.716659 22.115928-8.228426 34.214642l-26.018739 15.058345-13.237034-13.009369 25.238177-13.952549c6.992536-4.065428 8.45609-9.561887 4.423186-16.554423l-12.716659-22.018358-80.527997 46.475973L919.762427 491.1037l-16.066572 9.269176-81.926505-141.899698 47.744387-27.579864z m107.750103 73.763125l-14.830682-25.660981-80.56052 46.508496 14.830681 25.726028z m-72.592282 60.330952l14.700587 25.433317 80.560521-46.508496-14.700587-25.433318z m45.532793-166.064603a222.720407 222.720407 0 0 1-2.406733 56.13543l-16.456853 0.878132a242.722312 242.722312 0 0 0 2.081499-55.647578z" fill="#8a8a8a" p-id="4338"></path></svg> |
对比新文件 |
| | |
| | | <svg t="1729585232424" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1602" width="200" height="200"><path d="M925.5 898.9H804.9c-19 0-34.5-15.4-34.5-34.4V761.3c0-19 15.4-34.4 34.5-34.4h34.5V572.2c0-19-15.4-34.4-34.5-34.4H529.2V727h34.5c19 0 34.5 15.4 34.5 34.4v103.2c0 19-15.4 34.4-34.5 34.4H443.1c-19 0-34.5-15.4-34.5-34.4V761.3c0-19 15.4-34.4 34.5-34.4h34.5V537.8H219.1c-19 0-34.5 15.4-34.5 34.4V727h34.5c19 0 34.5 15.4 34.5 34.4v103.2c0 19-15.4 34.4-34.5 34.4H98.5c-19 0-34.5-15.4-34.5-34.4V761.3c0-19 15.4-34.4 34.5-34.4H133V555c0-38 30.9-68.8 68.9-68.8h275.7V297.1h-34.5c-19 0-34.5-15.4-34.5-34.4V159.5c0-19 15.4-34.4 34.5-34.4h120.6c19 0 34.5 15.4 34.5 34.4v103.2c0 19-15.4 34.4-34.5 34.4h-34.5v189.2h292.9c38.1 0 68.9 30.8 68.9 68.8v172h34.5c19 0 34.5 15.4 34.5 34.4v103.2c0 18.8-15.4 34.2-34.5 34.2z m0 0" p-id="1603" fill="#fff"></path></svg> |
对比新文件 |
| | |
| | | <svg t="1729649333541" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1644" width="200" height="200"><path d="M647.888 893.84L491.904 571.52l393.888-393.888-237.904 716.208zM872.32 123.232L459.872 535.68 134.96 380.88 872.32 123.232z m90.72-68.32a23.968 23.968 0 0 0-24.784-5.568L64.08 354.816a23.984 23.984 0 0 0-2.4 44.32l381.392 181.728 187.36 387.088a24.048 24.048 0 0 0 23.152 13.504 24.032 24.032 0 0 0 21.232-16.4L968.96 79.552c2.88-8.672 0.592-18.24-5.92-24.64z" fill="#fff" p-id="1645"></path></svg> |
对比新文件 |
| | |
| | | <?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="1735905505218" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4277" width="200" height="200" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M561.778 454.929h198.117c0.549 0 0.994 0.444 0.994 1.001v97.553a0.998 0.998 0 0 1-0.994 1.001H463.224a1.005 1.005 0 0 1-1.002-1V207.04c0-0.552 0.444-1 1.002-1h97.552c0.553 0 1.002 0.455 1.002 1v247.89zM512 952.706c-247.424 0-448-200.576-448-448 0-247.423 200.576-448 448-448s448 200.577 448 448c0 247.424-200.576 448-448 448z m0-99.555c192.44 0 348.444-156.004 348.444-348.445 0-192.44-156.003-348.444-348.444-348.444-192.44 0-348.444 156.004-348.444 348.444 0 192.441 156.003 348.445 348.444 348.445z" fill="#3296FA" p-id="4278"></path></svg> |
对比新文件 |
| | |
| | | <svg t="1730189225011" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2651" id="mx_n_1730189225011" width="200" height="200"><path d="M793.889347 200.380242c27.648573 20.615681 42.196018 32.710677 63.781037 56.119312 25.313864 27.453234 43.242957 48.52047 64.502857 86.507991 44.537416 79.580127 53.527718 136.949077 53.517684 212.063821 0 64.933675-15.452562 130.459388-40.138263 187.311893-22.076044 50.841799-61.545336 104.359483-101.886297 138.933914-45.506755 39.001681-81.214423 60.462941-137.605337 81.826531-55.699867 21.102023-114.070267 28.641326-181.379458 27.791064-68.274516-0.862973-129.364283-11.040029-180.533878-31.80489-46.159002-18.731189-98.338744-46.827973-141.596418-87.541551-43.946046-41.361142-70.369064-75.958317-93.88139-127.198155-26.157437-57.004361-40.094111-129.065922-39.680686-191.781288 0-36.980719 4.033895-70.902234 12.252873-105.241856 8.532726-35.651474 20.069131-69.572989 38.13135-102.35257 18.856956-34.221214 36.754607-62.067803 58.869452-88.973149 23.248751-28.285434 39.2104-46.417894 64.295476-63.475987 18.297696-12.442861 36.879036-9.295353 47.199252-2.306612 4.403836 2.982273 8.919391 6.577992 12.933218 12.933217 9.572307 15.156208-0.334486 29.769212-6.69038 38.465836-7.148625 9.781026-23.130343 26.023643-38.738775 43.218205-38.192895 42.075603-55.133918 65.965228-74.986303 106.965794-30.772668 63.552249-37.495827 115.718611-38.131349 166.573791-0.668971 53.517684 9.995096 99.647251 27.427813 140.483919 33.916163 80.572211 94.807915 144.44289 175.270414 178.615938 41.108271 17.845472 113.812713 37.319888 181.960793 38.13135 56.193568 0.668971 125.919751-11.321666 166.574459-28.096784 45.935566-18.954626 97.223569-56.862539 127.10383-94.324918 23.013273-28.852721 52.179742-70.910931 64.413884-105.694749 14.863868-42.260239 24.806784-87.661297 24.559934-132.458943 0-54.414105-11.53373-108.417461-36.918505-156.856317-20.16747-38.483228-46.480777-74.607665-84.66899-108.048189-13.377414-11.714352-23.822728-20.067124-38.808348-31.619586-10.191774-7.857065-36.059546-25.027545-28.923632-47.326356 4.970455-15.53217 18.303717-25.294464 31.887843-27.205046 19.456354-2.736092 28.565733 2.427027 43.705885 12.041479l6.179955 4.322891zM510.755379 531.65738c-8.696624-0.668971-10.034566-0.446204-20.738102-6.689711-11.031333-6.434832-17.839451-21.183637-16.514219-35.175166V92.220334c0-18.178619 0.386665-22.815926 8.988295-31.685813 5.351768-5.519011 10.963097-11.381873 26.08987-11.539751 16.055305-0.167243 21.407073 3.846584 27.929542 9.700081 9.70677 8.711341 10.703537 17.56049 10.377078 33.525483v397.5715c-0.509756 15.273947 0.326458 22.967114-11.380535 33.502739-3.884046 3.495374-8.027653 7.693167-20.96087 8.362138l-3.791059 0.000669z m4.453341 0.573308" p-id="2652" fill="#ffffff"></path></svg> |
对比新文件 |
| | |
| | | <svg t="1729585239190" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1755" width="200" height="200"><path d="M901.489435 536.822664v-0.931601l-1.001722-198.240726c-0.100172-19.162936-9.21584-37.474409-25.043042-50.246361-14.024104-11.349507-32.265456-17.60025-51.348255-17.610268l-618.062295-0.18031c-19.142902 0-37.424323 6.280795-51.478478 17.690405-15.827203 12.842072-24.902802 31.2437-24.892785 50.486775v196.798247A114.987635 114.987635 0 1 0 195.295664 536.922836V338.782282c1.15198-1.252152 4.808264-3.596181 10.768509-3.596181l276.725622 0.090155v199.753326a114.987635 114.987635 0 1 0 65.612772 1.412428V335.326342l275.693849 0.080138c6.01033 0 9.626546 2.344029 10.768508 3.596181l1.001722 195.70637a114.987635 114.987635 0 1 0 65.592737 2.113633zM214.979496 645.910158a56.437001 56.437001 0 1 1-56.437001-56.437001 56.507122 56.507122 0 0 1 56.437001 56.437001z m354.689623 0a56.437001 56.437001 0 1 1-56.437001-56.437001 56.507122 56.507122 0 0 1 56.437001 56.437001z m295.507904 56.437001a56.437001 56.437001 0 1 1 56.437001-56.437001 56.507122 56.507122 0 0 1-56.457035 56.437001z" p-id="1756" fill='#fff'></path></svg> |
对比新文件 |
| | |
| | | <?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="1724316570161" class="icon" viewBox="0 0 1185 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1505" xmlns:xlink="http://www.w3.org/1999/xlink" width="231.4453125" height="200"><path d="M414.276535 230.004913l-2.443086-31.647244 26.446614 17.351559 29.437984-11.852598-8.143622 30.31685 20.423559 24.221229-31.623055 1.475527-16.722646 26.801386-11.239811-29.760504-30.663559-7.522772zM581.664252 176.902047l13.884472-28.542992 14.206993 28.220473 31.42148 4.321763-22.350614 22.092599 5.684409 31.123149-28.180157-14.513385-27.897953 14.819779 5.28126-31.066709-22.76989-21.689448zM896.507969 672.735748l17.754708 26.398236-31.494047-2.064126-19.560819 24.705008-7.95011-30.502299-29.575055-11.02211 26.744945-16.771024 1.104629-31.526299 24.414741 20.197795 30.268472-8.619338zM777.030551 801.961323l2.112504 31.647244-26.446614-17.682142-29.413795 11.546205 8.466141-30.308787-20.092976-24.221229 31.606929-1.153008 17.045166-26.793323 10.86085 29.704063 30.647433 7.837229zM609.312252 853.451591l-14.198929 28.518803-14.110236-28.542992-31.405355-4.636221 22.673134-22.084535-5.36189-31.12315 27.833449 14.835906 28.188221-14.803654-5.28126 31.066709 22.76989 22.060346zM298.435528 354.828094l-17.448315-26.390173 31.485984 2.394709 19.875275-24.753386 7.611465 30.865134 29.583118 11.288189-27.011024 16.779087-1.419086 31.526299-24.084158-20.504189-30.518425 8.280693zM962.56 91.53915a43.636913 43.636913 0 0 1 59.375874 15.601889l138.627024 236.753638c12.175118 20.447748 5.12 47.208819-15.609953 59.375874L229.13411 938.185575a43.636913 43.636913 0 0 1-59.375874-15.60189L31.12315 685.773606a43.636913 43.636913 0 0 1 15.601889-59.319433z m25.672567 24.108346a13.594205 13.594205 0 0 0-10.441575 1.548095L61.625449 652.054173a13.586142 13.586142 0 0 0-4.853921 18.83515l138.643149 236.793953a13.586142 13.586142 0 0 0 18.843213 4.837795l915.818834-534.915024a13.957039 13.957039 0 0 0 5.160315-18.778708l-138.602834-236.78589a13.594205 13.594205 0 0 0-8.401638-6.393953z" fill="#F5222D" p-id="1506"></path><path d="M395.981606 172.338394c123.670173-72.349228 271.11811-69.462677 388.394331-5.12l29.623433-17.335433a414.574866 414.574866 0 0 0-112.107842-47.071748 429.991307 429.991307 0 0 0-162.009701-10.498016 412.792945 412.792945 0 0 0-158.80063 54.707401 417.856504 417.856504 0 0 0-125.363402 111.922394A426.282331 426.282331 0 0 0 185.206929 405.004094a417.348535 417.348535 0 0 0-13.529701 120.977134l29.623433-17.335433c1.386835-133.958551 70.688252-263.958173 194.672882-336.307401z m397.666772 679.484472c-123.670173 72.365354-271.110047 69.462677-388.394331 5.128063l-29.623433 17.335433a414.679685 414.679685 0 0 0 112.075591 47.087874 429.991307 429.991307 0 0 0 162.009701 10.498016 412.744567 412.744567 0 0 0 158.808692-54.707402 423.145827 423.145827 0 0 0 209.105638-378.976756l-29.623433 17.335434c-1.072378 133.974677-70.712441 263.95011-194.350362 336.307401h-0.008063z" fill="#F5222D" p-id="1507"></path><path d="M478.377323 313.110173a226.271748 226.271748 0 0 1 109.979212-31.219905l45.668788-26.761071c-58.634079-9.13537-118.316346 2.314079-170.612914 32.735748a258.693039 258.693039 0 0 0-111.91433 132.71685l45.67685-26.761071a230.359685 230.359685 0 0 1 81.097575-80.589606l0.104819-0.120945z m232.568945 397.674835a226.328189 226.328189 0 0 1-109.979213 31.227968l-45.668787 26.753008c58.634079 9.13537 118.316346-2.314079 170.612913-32.735748a258.709165 258.709165 0 0 0 111.914331-132.71685l-45.676851 26.761071a225.215496 225.215496 0 0 1-81.097574 80.597669l-0.104819 0.112882zM188.57726 706.938961l-10.062614-17.424126 109.938897-63.471874 9.578835 16.585574 17.093543-9.869102-18.770645-32.509984-63.689575 36.767244c-4.047622-3.918614-7.804976-7.337323-11.272063-10.24l-16.859717 13.747401c3.249386 2.144756 6.595528 4.458835 9.869103 7.038993l-62.173733 35.896441 19.254426 33.348535 17.093543-9.869102zM317.44 781.142677l-19.060913-33.017953 32.679307-18.867401 4.741039 8.216189 17.093543-9.869103-48.474708-83.959937-49.772851 28.736504-7.933984-13.747401-17.432189 10.062614 7.933984 13.747402-49.264882 28.446236 48.764977 84.459842 17.093543-9.869102-5.031307-8.708032 32.171339-18.585196 19.060913 33.017952 17.432189-10.062614z m-12.505701-97.126803l-32.679307 18.867402-8.321008-14.41663 32.679307-18.867402 8.321008 14.41663z m-50.111496 28.930016l-32.171338 18.577134-8.321008-14.41663 32.171338-18.577134 8.321008 14.41663z m16.932284 29.325102l-32.171339 18.577134-8.127496-14.077984 32.171339-18.577134 8.127496 14.077984z m50.111496-28.930016l-32.679307 18.867402-8.127496-14.077984 32.679307-18.867402 8.127496 14.077984z m95.828661 7.684032c11.062425-6.38589 13.368441-15.537386 6.692284-27.099717l-25.05978-43.411149c3.55578-4.289512 7.014803-8.740283 10.48189-13.199118l-9.482079-16.424315c-3.467087 4.458835-6.92611 8.917669-10.48189 13.199118l-17.803086-30.832882 14.755275-8.51452-9.780409-16.932283-14.747213 8.522582-16.738771-28.994519-17.093544 9.869102 16.738772 28.99452-16.924221 9.772346 9.772347 16.924221 16.932283-9.772347 20.891213 36.202835a299.927181 299.927181 0 0 1-16.690394 15.214866l13.868347 14.344063a572.617575 572.617575 0 0 0 12.497638-12.804031l19.157669 33.179212c2.322142 4.031496 1.475528 7.200252-2.20926 9.328882-3.85411 2.225386-8.167811 4.039559-12.578268 5.692472l13.55389 14.964914 14.247307-8.224252z m111.390236-65.205417c6.369764-3.676724 10.15937-8.329071 11.151118-13.586142 1.225575-5.619906-3.201008-18.706142-13.182992-39.089386l-18.827086 4.160504c7.627591 14.368252 11.368819 23.164976 11.570393 26.615937 0.112882 3.289701-0.959496 5.692472-3.467086 7.143811l-6.539087 3.77348c-3.354205 1.935118-6.095622 1.064315-8.224252-2.628535l-38.702362-67.027654c8.933795-10.07874 17.762772-21.874898 26.390173-35.573921l-18.383622-8.603213a168.443969 168.443969 0 0 1-17.972409 26.914268l-26.801386-46.426709-17.254803 9.965859 77.686929 134.571338c6.966425 12.070299 16.077606 15.077795 27.478677 8.498394l15.077795-8.708031z m-78.501291 45.547842c13.626457-12.779843 25.285543-25.100094 34.783748-37.291339l-12.473449-14.247307a157.808882 157.808882 0 0 1-14.706897 17.875654l-38.412095-66.535811 20.617071-11.900976-9.869102-17.093544-20.617071 11.900977-27.18841-47.087874-17.254803 9.965858 72.94589 126.363212c2.999433 5.192567 2.418898 9.99811-1.564221 14.311811l13.739339 13.739339z m201.663496-113.978457l-65.21348-112.946393c0.137071-7.901732-0.16126-15.771213-0.886929-23.624567l53.78822-31.050583-9.869102-17.093543-144.795213 83.597102 9.869102 17.093543 71.05915-41.024504c1.894803 37.331654-9.45789 76.517795-33.848441 117.856756l20.367118 8.570961c14.860094-26.898142 25.05978-53.344756 30.445859-79.243087l50.990362 88.313953 18.093354-10.449638z m28.728441-76.017889l5.716661-21.850709c-21.157291-7.224441-45.330142-12.707276-72.349228-16.54526l-5.603779 19.318929c29.163843 4.837795 53.385071 11.191433 72.244409 19.07704z m18.738394-105.33493l5.265134-19.13348c-12.739528-4.25726-27.414173-7.627591-43.612725-10.127118l-5.410268 18.101417c17.674079 2.74948 32.380976 6.555213 43.757859 11.159181z m88.934803 67.74526l-15.76315-27.317417 21.786205-12.578268 15.674457 27.148095 16.085669-9.288567-15.674457-27.148095 22.455433-12.965291 4.063748 7.038992c2.031874 3.523528 1.249764 6.426205-2.435023 8.554835l-11.852599 6.176252 12.175118 12.183181 12.570205-7.256693c11.393008-6.579402 13.997354-15.230992 7.998488-25.616126l-42.862866-74.244032-33.848441 19.544693-0.532157-0.145133a202.445606 202.445606 0 0 0 18.738393-38.750741L790.173228 306.87748l-92.676031 53.506016 8.321008 14.41663 31.679496-18.286866-3.85411 13.836094c8.401638-0.16126 16.125984 0.08063 23.261732 0.427339l-37.202646 21.479811 52.538457 90.998929 16.424315-9.482079z m-25.35811-117.856756c-6.724535-0.806299-14.126362-1.233638-21.947465-1.628724l33.517858-19.351181c-3.305827 7.047055-7.143811 13.948976-11.570393 20.979905z m47.571653 16.996788l-22.455433 12.965291-6.095622-10.56252 22.455433-12.965291 6.095622 10.56252z m-38.541102 22.253858l-21.786205 12.578268-6.095622-10.56252 21.786205-12.578268 6.095622 10.56252z m-24.253481 137.570772c-0.330583-19.915591 1.112693-30.582929 4.458835-32.518048 1.846425-1.064315 4.628157-0.886929 8.627402 0.604725 8.304882 2.797858 16.400126 3.265512 24.269606 1.402961 8.006551-2.386646 17.464441-6.506835 28.462362-12.626646 10.812472-6.031118 20.96378-11.66715 30.187843-16.988725l38.379842-22.157102-5.781165-19.673701c-4.329827 2.942992-10.675402 7.055118-19.028662 12.320252-8.708031 5.031307-16.996787 10.038425-25.374236 14.876221-13.07011 7.546961-24.398614 13.868346-34.211275 19.302803-10.07874 5.378016-18.230425 8.296819-24.543748 8.587087-5.28126 0.145134-11.070488-0.983685-17.440252-3.120378l-2.902678-0.774048-36.767244-63.681511-38.379842 22.157102 9.288567 16.085669 22.116787-12.771779 26.511118 45.91874c-4.571717 7.555024-7.014803 20.359055-7.651779 38.605606l19.778519 4.450772z m38.476599-112.938331l-21.786205 12.578268-6.095622-10.56252 21.786205-12.578268 6.095622 10.56252z m38.541102-22.253858l-22.455433 12.965291-6.095622-10.56252 22.455433-12.957228 6.095622 10.56252z m172.241638-43.798173c12.062236-6.966425 14.610142-16.488819 7.740472-28.381733l-39.863433-69.051464 23.302048-13.449071-9.869103-17.093543-23.302047 13.44907-14.513386-25.132346-17.424126 10.062614 14.513386 25.132347-62.681701 36.186708 9.869103 17.093544 62.6817-36.186709 37.34778 64.689386c2.515654 4.354016 1.523906 8.062992-2.838173 10.578645-6.692283 3.870236-14.190866 7.522772-21.955528 11.110804l13.529701 14.537574 23.463307-13.545826z m-130.942992-43.725607l5.386079-20.092976c-12.900787-4.168567-27.389984-7.200252-43.65304-9.433701l-5.321575 18.27074c17.682142 2.74948 32.219717 6.643906 43.596599 11.255937z m80.702488 27.148095l8.466142-17.851465c-10.756031-5.853732-24.825953-12.038047-41.846929-18.302992l-8.740284 16.22274c16.883906 6.789039 30.808693 13.497449 42.121071 19.931717z m-31.219905 99.577952c-0.354772-20.350992 1.064315-31.445669 4.418519-33.380787 2.007685-1.161071 5.128063-1.177197 9.119244 0.32252a42.951559 42.951559 0 0 0 24.938835 1.007874c8.175874-2.483402 18.141732-6.893858 29.639559-13.303937 11.320441-6.321386 21.810394-12.150929 31.365039-17.657953l35.525544-20.520315-5.966614-20.012346c-3.999244 2.74948-10.006173 6.668094-17.859528 11.651023-7.95011 4.805543-15.722835 9.522394-23.439118 13.973166a2406.72252 2406.72252 0 0 1-35.719055 20.181669c-10.586709 5.66022-19.165732 8.603213-25.712882 9.256315-5.28126 0.145134-11.401071-0.790173-17.940158-2.822047l-3.080063-0.685355-36.767244-63.681512-39.041008 22.544126 9.482079 16.424315 22.455433-12.965291 26.511118 45.91874c-4.57978 7.555024-7.256693 20.72189-7.700157 39.299024l19.770457 4.450771z" fill="#F5222D" p-id="1508"></path></svg> |
对比新文件 |
| | |
| | | <?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="1724304256588" class="icon" viewBox="0 0 1300 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1272" xmlns:xlink="http://www.w3.org/1999/xlink" width="253.90625" height="200"><path d="M784.058182 99.258182l10.938182 18.385454-21.294546-2.56-14.196363 16.058182-4.072728-21.061818-19.781818-8.494545 18.734546-10.472728 2.094545-21.294545 15.709091 14.545454 20.945454-4.654545-9.076363 19.549091zM1067.287273 642.443636l-18.501818 10.821819 2.56-21.294546-16.058182-14.196364 21.061818-4.072727 8.494545-19.665454 10.472728 18.734545 21.294545 1.978182-14.661818 15.709091 4.770909 20.945454-19.432727-8.96z" fill="#2196F3" p-id="1273"></path><path d="M1067.287273 642.443636l-18.501818 10.821819 2.56-21.294546-16.058182-14.196364 21.061818-4.072727 8.494545-19.665454 10.472728 18.734545 21.294545 1.978182-14.661818 15.709091 4.770909 20.945454-19.432727-8.96zM571.927273 100.072727l-17.454546-12.567272 20.596364-6.167273 6.516364-20.48 12.218181 17.570909 21.410909-0.116364-12.916363 17.105455 6.749091 20.363636-20.247273-6.981818-17.338182 12.683636 0.465455-21.410909zM991.418182 784.407273l-21.178182 3.490909 10.123636-18.967273-9.774545-18.967273 21.061818 3.723637 15.127273-15.243637 2.909091 21.294546 19.2 9.658182-19.316364 9.309091-3.258182 21.178181-14.894545-15.476363zM427.985455 156.741818L407.272727 151.505455l16.872728-13.265455-1.396364-21.410909 17.803636 11.985454 20.014546-7.912727-5.934546 20.596364 13.730909 16.523636-21.410909 0.814546-11.52 18.152727-7.447272-20.247273zM854.225455 896.465455l-20.712728-5.352728 16.872728-13.265454-1.396364-21.294546 17.803636 11.869091 20.014546-7.912727-5.934546 20.712727 13.730909 16.523637-21.527272 0.814545-11.403637 18.036364-7.447272-20.130909zM562.501818 923.694545l10.821818 18.385455-21.294545-2.56-14.196364 16.058182-4.072727-21.061818-19.665455-8.494546 18.734546-10.356363 1.978182-21.41091 15.709091 14.661819 20.945454-4.770909-8.96 19.54909zM242.734545 420.770909l-18.385454 10.938182 2.56-21.294546-16.058182-14.196363 21.061818-4.189091 8.494546-19.665455 10.356363 18.734546 21.410909 2.094545-14.545454 15.709091 4.654545 20.945455-19.549091-9.076364z" fill="#2196F3" p-id="1274"></path><path d="M242.734545 420.770909l-18.385454 10.938182 2.56-21.294546-16.058182-14.196363 21.061818-4.189091 8.494546-19.665455 10.356363 18.734546 21.410909 2.094545-14.545454 15.709091 4.654545 20.945455-19.549091-9.076364zM700.858182 943.941818l-17.454546-12.450909 20.48-6.283636 6.516364-20.48 12.334545 17.687272 21.41091-0.116363-12.916364 17.105454 6.632727 20.363637-20.247273-7.098182-17.221818 12.683636 0.465455-21.410909zM303.592727 278.807273l-21.178182 3.490909 10.123637-18.967273-9.890909-18.967273 21.178182 3.723637 15.010909-15.243637 2.909091 21.294546 19.2 9.541818-19.316364 9.425455-3.258182 21.178181-14.778182-15.476363z" fill="#2196F3" p-id="1275"></path><path d="M407.272727 90.647273a486.632727 486.632727 0 0 1 504.552728 11.636363l25.018181-14.429091A512 512 0 0 0 139.636364 546.909091l25.018181-14.429091A486.981818 486.981818 0 0 1 407.272727 90.647273zM893.323636 933.352727a486.749091 486.749091 0 0 1-504.669091-11.636363l-24.901818 14.429091A512 512 0 0 0 1161.192727 477.090909l-24.901818 13.963636a486.981818 486.981818 0 0 1-242.967273 442.298182z" fill="#2196F3" p-id="1276"></path><path d="M814.545455 795.927273a327.447273 327.447273 0 0 1-258.21091 29.556363l-29.78909 17.105455A353.163636 353.163636 0 0 0 998.865455 570.181818l-29.789091 17.105455A326.865455 326.865455 0 0 1 814.545455 795.927273zM486.865455 228.072727A327.447273 327.447273 0 0 1 744.727273 198.516364l29.789091-17.105455A353.163636 353.163636 0 0 0 302.545455 453.818182l29.78909-17.105455A326.865455 326.865455 0 0 1 486.865455 228.072727zM1288.378182 374.690909a53.294545 53.294545 0 0 1-14.429091 11.636364L229.469091 989.090909a53.876364 53.876364 0 0 1-73.425455-19.665454L7.214545 710.632727a53.527273 53.527273 0 0 1 19.781819-73.309091L1071.476364 34.909091a53.876364 53.876364 0 0 1 73.425454 19.665454l148.829091 258.327273a53.061818 53.061818 0 0 1 5.352727 40.727273 55.272727 55.272727 0 0 1-10.705454 21.061818zM32.232727 665.716364A28.043636 28.043636 0 0 0 29.323636 698.181818l148.829091 257.978182a28.392727 28.392727 0 0 0 38.516364 10.356364l1044.48-601.949091a28.16 28.16 0 0 0 10.356364-38.516364L1122.676364 67.84a28.276364 28.276364 0 0 0-38.4-10.356364L39.68 659.432727a27.810909 27.810909 0 0 0-7.447273 6.283637z" fill="#2196F3" p-id="1277"></path><path d="M477.090909 500.945455l22.109091 38.283636-15.36 8.843636-13.963636-24.436363-151.272728 87.621818 14.545455 25.134545-15.243636 8.843637-23.272728-39.330909L377.949091 558.545455c-6.050909-4.887273-11.636364-8.843636-15.825455-11.636364l14.894546-12.450909c3.956364 3.141818 9.658182 8.145455 17.105454 14.894545zM459.869091 698.181818l-48.407273 28.043637 7.447273 12.334545-15.36 8.843636-61.207273-106.007272L406.225455 605.090909l-12.683637-21.876364 15.709091-9.076363 12.683636 21.876363L486.4 558.545455l60.509091 104.727272-15.36 8.843637-7.098182-12.218182-49.105454 28.392727L501.294545 733.090909l-15.70909 9.076364z m-45.381818-78.661818l-48.523637 27.461818 14.545455 25.134546 48.523636-28.043637zM388.538182 686.545455l14.545454 25.134545 48.523637-28.043636-14.545455-25.134546z m105.425454-79.476364L479.418182 581.818182 430.545455 609.861818l14.545454 25.134546z m-26.647272 67.490909l49.221818-28.392727-14.545455-25.134546-49.105454 28.392728zM624.058182 541.090909c-4.654545 6.167273-10.123636 13.149091-16.290909 20.829091l34.909091 61.207273a18.734545 18.734545 0 0 1-6.632728 29.207272l-18.734545 10.938182-11.636364-13.614545a174.545455 174.545455 0 0 0 17.454546-8.610909 8.378182 8.378182 0 0 0 2.327272-12.683637l-30.021818-52.363636-9.774545 10.24-9.890909 10.123636-12.450909-12.916363c9.076364-8.145455 16.872727-15.709091 23.272727-22.574546l-30.836364-53.527272-24.785454 14.196363-8.727273-15.010909L546.909091 492.218182l-23.272727-40.378182 15.36-8.843636 23.272727 40.378181 21.643636-12.450909 8.727273 15.127273-21.643636 12.450909L599.156364 546.909091c5.352727-6.4 10.821818-13.381818 16.290909-20.712727z m8.843636-45.032727L689.221818 593.454545a193.745455 193.745455 0 0 0 22.574546-27.112727l11.636363 13.032727a363.985455 363.985455 0 0 1-41.192727 44.8l-12.334545-12.450909a10.821818 10.821818 0 0 0 1.62909-13.730909l-98.90909-171.403636 15.476363-8.96 36.305455 62.952727 30.836363-17.803636 8.029091 15.476363z m128 81.454545a20.130909 20.130909 0 0 1-30.836363-9.541818L628.363636 392.378182l15.36-8.378182 38.050909 66.094545A206.08 206.08 0 0 0 709.818182 409.018182l16.64 7.563636a297.890909 297.890909 0 0 1-34.909091 48.64l52.712727 91.112727a8.843636 8.843636 0 0 0 13.614546 4.072728l10.821818-6.167273a14.429091 14.429091 0 0 0 7.912727-11.636364 102.981818 102.981818 0 0 0-15.010909-38.516363l17.105455-3.723637c12.334545 24.669091 17.687273 41.076364 16.058181 48.989091a28.16 28.16 0 0 1-15.127272 18.152728zM805.236364 288.116364l16.174545-9.309091 23.272727 39.330909 78.429091-45.265455 59.345455 102.749091-16.64 9.076364-7.912727-13.847273L896 407.272727l42.938182 74.472728-16.174546 9.30909-42.938181-74.472727-62.603637 36.072727 8.029091 13.73091-15.825454 9.192727L749.730909 372.363636l78.196364-45.265454z m2.676363 149.061818l62.603637-36.072727-33.745455-58.181819-62.487273 36.072728z m78.778182-45.381818l62.72-36.189091-33.745454-58.181818-62.72 36.072727z" fill="#2196F3" p-id="1278"></path></svg> |
对比新文件 |
| | |
| | | <svg width="22" height="22" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="#FAFAFA" d="M0 0h22v22H0z"/><circle fill="#919BAE" cx="1" cy="1" r="1"/></g></svg> |
对比新文件 |
| | |
| | | <svg t="1729561814171" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1359" width="200" height="200"><path d="M674.496 603.456c120.256 0 218.176 90.752 221.44 203.84l0.064 5.888v125.888c0 11.52-9.92 20.928-22.144 20.928h-44.352a21.568 21.568 0 0 1-22.144-20.928v-125.888c0-67.712-56.512-123.264-128-125.76l-4.928-0.064H349.568c-71.488 0-130.176 53.504-132.864 121.152l-0.064 4.672v125.888c0 11.52-9.92 20.928-22.144 20.928h-44.352A21.568 21.568 0 0 1 128 939.072v-125.888c0-113.92 95.872-206.528 215.36-209.664l6.208-0.064h324.928zM497.216 128c122.368 0 221.568 93.888 221.568 209.728s-99.2 209.792-221.568 209.792c-122.304 0-221.44-93.952-221.44-209.728C275.712 221.952 374.848 128 497.152 128z m0 83.904c-73.408 0-132.864 56.32-132.864 125.888 0 69.504 59.52 125.824 132.864 125.824 73.408 0 132.928-56.32 132.928-125.824 0-69.504-59.52-125.888-132.928-125.888z" fill="#fff" p-id="1360"></path></svg> |
| | |
| | | // 链接列表 |
| | | links: AppLink[] |
| | | } |
| | | |
| | | // APP 链接 |
| | | export interface AppLink { |
| | | // 链接名称 |
| | |
| | | ACTIVITY_COMBINATION, |
| | | // 秒杀活动 |
| | | ACTIVITY_SECKILL, |
| | | // 积分商城活动 |
| | | ACTIVITY_POINT, |
| | | // 文章详情 |
| | | ARTICLE_DETAIL, |
| | | // 优惠券详情 |
| | |
| | | type: APP_LINK_TYPE_ENUM.ACTIVITY_SECKILL |
| | | }, |
| | | { |
| | | name: '积分商城活动', |
| | | path: '/pages/activity/point/list', |
| | | type: APP_LINK_TYPE_ENUM.ACTIVITY_POINT |
| | | }, |
| | | { |
| | | name: '签到中心', |
| | | path: '/pages/app/sign' |
| | | }, |
| | |
| | | defineProps({ |
| | | title: propTypes.string.def(''), |
| | | message: propTypes.string.def(''), |
| | | bodyStyle: propTypes.object.def({ padding: '20px' }) |
| | | bodyStyle: propTypes.object.def({ padding: '10px' }) |
| | | }) |
| | | </script> |
| | | |
| | |
| | | <el-form> |
| | | <el-form-item label="类型"> |
| | | <el-radio-group v-model="cronValue.second.type"> |
| | | <el-radio-button label="0">任意值</el-radio-button> |
| | | <el-radio-button label="1">范围</el-radio-button> |
| | | <el-radio-button label="2">间隔</el-radio-button> |
| | | <el-radio-button label="3">指定</el-radio-button> |
| | | <el-radio-button value="0">任意值</el-radio-button> |
| | | <el-radio-button value="1">范围</el-radio-button> |
| | | <el-radio-button value="2">间隔</el-radio-button> |
| | | <el-radio-button value="3">指定</el-radio-button> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item v-if="cronValue.second.type == '1'" label="范围"> |
| | |
| | | <el-form> |
| | | <el-form-item label="类型"> |
| | | <el-radio-group v-model="cronValue.minute.type"> |
| | | <el-radio-button label="0">任意值</el-radio-button> |
| | | <el-radio-button label="1">范围</el-radio-button> |
| | | <el-radio-button label="2">间隔</el-radio-button> |
| | | <el-radio-button label="3">指定</el-radio-button> |
| | | <el-radio-button value="0">任意值</el-radio-button> |
| | | <el-radio-button value="1">范围</el-radio-button> |
| | | <el-radio-button value="2">间隔</el-radio-button> |
| | | <el-radio-button value="3">指定</el-radio-button> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item v-if="cronValue.minute.type == '1'" label="范围"> |
| | |
| | | <el-form> |
| | | <el-form-item label="类型"> |
| | | <el-radio-group v-model="cronValue.hour.type"> |
| | | <el-radio-button label="0">任意值</el-radio-button> |
| | | <el-radio-button label="1">范围</el-radio-button> |
| | | <el-radio-button label="2">间隔</el-radio-button> |
| | | <el-radio-button label="3">指定</el-radio-button> |
| | | <el-radio-button value="0">任意值</el-radio-button> |
| | | <el-radio-button value="1">范围</el-radio-button> |
| | | <el-radio-button value="2">间隔</el-radio-button> |
| | | <el-radio-button value="3">指定</el-radio-button> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item v-if="cronValue.hour.type == '1'" label="范围"> |
| | |
| | | <el-form> |
| | | <el-form-item label="类型"> |
| | | <el-radio-group v-model="cronValue.day.type"> |
| | | <el-radio-button label="0">任意值</el-radio-button> |
| | | <el-radio-button label="1">范围</el-radio-button> |
| | | <el-radio-button label="2">间隔</el-radio-button> |
| | | <el-radio-button label="3">指定</el-radio-button> |
| | | <el-radio-button label="4">本月最后一天</el-radio-button> |
| | | <el-radio-button label="5">不指定</el-radio-button> |
| | | <el-radio-button value="0">任意值</el-radio-button> |
| | | <el-radio-button value="1">范围</el-radio-button> |
| | | <el-radio-button value="2">间隔</el-radio-button> |
| | | <el-radio-button value="3">指定</el-radio-button> |
| | | <el-radio-button value="4">本月最后一天</el-radio-button> |
| | | <el-radio-button value="5">不指定</el-radio-button> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item v-if="cronValue.day.type == '1'" label="范围"> |
| | |
| | | <el-form> |
| | | <el-form-item label="类型"> |
| | | <el-radio-group v-model="cronValue.month.type"> |
| | | <el-radio-button label="0">任意值</el-radio-button> |
| | | <el-radio-button label="1">范围</el-radio-button> |
| | | <el-radio-button label="2">间隔</el-radio-button> |
| | | <el-radio-button label="3">指定</el-radio-button> |
| | | <el-radio-button value="0">任意值</el-radio-button> |
| | | <el-radio-button value="1">范围</el-radio-button> |
| | | <el-radio-button value="2">间隔</el-radio-button> |
| | | <el-radio-button value="3">指定</el-radio-button> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item v-if="cronValue.month.type == '1'" label="范围"> |
| | |
| | | <el-form> |
| | | <el-form-item label="类型"> |
| | | <el-radio-group v-model="cronValue.week.type"> |
| | | <el-radio-button label="0">任意值</el-radio-button> |
| | | <el-radio-button label="1">范围</el-radio-button> |
| | | <el-radio-button label="2">间隔</el-radio-button> |
| | | <el-radio-button label="3">指定</el-radio-button> |
| | | <el-radio-button label="4">本月最后一周</el-radio-button> |
| | | <el-radio-button label="5">不指定</el-radio-button> |
| | | <el-radio-button value="0">任意值</el-radio-button> |
| | | <el-radio-button value="1">范围</el-radio-button> |
| | | <el-radio-button value="2">间隔</el-radio-button> |
| | | <el-radio-button value="3">指定</el-radio-button> |
| | | <el-radio-button value="4">本月最后一周</el-radio-button> |
| | | <el-radio-button value="5">不指定</el-radio-button> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item v-if="cronValue.week.type == '1'" label="范围"> |
| | |
| | | <el-form> |
| | | <el-form-item label="类型"> |
| | | <el-radio-group v-model="cronValue.year.type"> |
| | | <el-radio-button label="-1">忽略</el-radio-button> |
| | | <el-radio-button label="0">任意值</el-radio-button> |
| | | <el-radio-button label="1">范围</el-radio-button> |
| | | <el-radio-button label="2">间隔</el-radio-button> |
| | | <el-radio-button label="3">指定</el-radio-button> |
| | | <el-radio-button value="-1">忽略</el-radio-button> |
| | | <el-radio-button value="0">任意值</el-radio-button> |
| | | <el-radio-button value="1">范围</el-radio-button> |
| | | <el-radio-button value="2">间隔</el-radio-button> |
| | | <el-radio-button value="3">指定</el-radio-button> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item v-if="cronValue.year.type == '1'" label="范围"> |
| | |
| | | <script lang="tsx"> |
| | | import { defineComponent, PropType, ref } from 'vue' |
| | | import { computed, defineComponent, PropType } from 'vue' |
| | | import { isHexColor } from '@/utils/color' |
| | | import { ElTag } from 'element-plus' |
| | | import { DictDataType, getDictOptions } from '@/utils/dict' |
| | | import { isArray, isBoolean, isNumber, isString } from '@/utils/is' |
| | | |
| | | export default defineComponent({ |
| | | name: 'DictTag', |
| | |
| | | required: true |
| | | }, |
| | | value: { |
| | | type: [String, Number, Boolean] as PropType<string | number | boolean>, |
| | | type: [String, Number, Boolean, Array], |
| | | required: true |
| | | }, |
| | | // 字符串分隔符 只有当 props.value 传入值为字符串时有效 |
| | | separator: { |
| | | type: String as PropType<string>, |
| | | default: ',' |
| | | }, |
| | | // 每个 tag 之间的间隔,默认为 5px,参考的 el-row 的 gutter |
| | | gutter: { |
| | | type: String as PropType<string>, |
| | | default: '5px' |
| | | } |
| | | }, |
| | | setup(props) { |
| | | const dictData = ref<DictDataType>() |
| | | const getDictObj = (dictType: string, value: string) => { |
| | | const dictOptions = getDictOptions(dictType) |
| | | dictOptions.forEach((dict: DictDataType) => { |
| | | if (dict.value === value) { |
| | | if (dict.colorType + '' === 'default') { |
| | | dict.colorType = 'info' |
| | | } |
| | | dictData.value = dict |
| | | } |
| | | }) |
| | | } |
| | | const rederDictTag = () => { |
| | | const valueArr: any = computed(() => { |
| | | // 1. 是 Number 类型和 Boolean 类型的情况 |
| | | if (isNumber(props.value) || isBoolean(props.value)) { |
| | | return [String(props.value)] |
| | | } |
| | | // 2. 是字符串(进一步判断是否有包含分隔符号 -> props.sepSymbol ) |
| | | else if (isString(props.value)) { |
| | | return props.value.split(props.separator) |
| | | } |
| | | // 3. 数组 |
| | | else if (isArray(props.value)) { |
| | | return props.value.map(String) |
| | | } |
| | | return [] |
| | | }) |
| | | const renderDictTag = () => { |
| | | if (!props.type) { |
| | | return null |
| | | } |
| | | // 解决自定义字典标签值为零时标签不渲染的问题 |
| | | if (props.value === undefined || props.value === null) { |
| | | if (props.value === undefined || props.value === null || props.value === '') { |
| | | return null |
| | | } |
| | | getDictObj(props.type, props.value.toString()) |
| | | // 添加标签的文字颜色为白色,解决自定义背景颜色时标签文字看不清的问题 |
| | | const dictOptions = getDictOptions(props.type) |
| | | |
| | | return ( |
| | | <ElTag |
| | | style={dictData.value?.cssClass ? 'color: #fff' : ''} |
| | | type={dictData.value?.colorType} |
| | | color={ |
| | | dictData.value?.cssClass && isHexColor(dictData.value?.cssClass) |
| | | ? dictData.value?.cssClass |
| | | : '' |
| | | } |
| | | disableTransitions={true} |
| | | <div |
| | | class="dict-tag" |
| | | style={{ |
| | | display: 'inline-flex', |
| | | gap: props.gutter, |
| | | justifyContent: 'center', |
| | | alignItems: 'center' |
| | | }} |
| | | > |
| | | {dictData.value?.label} |
| | | </ElTag> |
| | | {dictOptions.map((dict: DictDataType) => { |
| | | if (valueArr.value.includes(dict.value)) { |
| | | if (dict.colorType + '' === 'primary' || dict.colorType + '' === 'default') { |
| | | dict.colorType = '' |
| | | } |
| | | return ( |
| | | // 添加标签的文字颜色为白色,解决自定义背景颜色时标签文字看不清的问题 |
| | | <ElTag |
| | | style={dict?.cssClass ? 'color: #fff' : ''} |
| | | type={dict?.colorType || null} |
| | | color={dict?.cssClass && isHexColor(dict?.cssClass) ? dict?.cssClass : ''} |
| | | disableTransitions={true} |
| | | > |
| | | {dict?.label} |
| | | </ElTag> |
| | | ) |
| | | } |
| | | })} |
| | | </div> |
| | | ) |
| | | } |
| | | return () => rederDictTag() |
| | | return () => renderDictTag() |
| | | } |
| | | }) |
| | | </script> |
| | |
| | | width: 80px; |
| | | height: 25px; |
| | | font-size: 12px; |
| | | color: #6a6a6a; |
| | | line-height: 25px; |
| | | text-align: center; |
| | | background: #fff; |
| | |
| | | <el-form :model="formData" label-width="80px"> |
| | | <el-form-item label="组件背景" prop="bgType"> |
| | | <el-radio-group v-model="formData.bgType"> |
| | | <el-radio label="color">纯色</el-radio> |
| | | <el-radio label="img">图片</el-radio> |
| | | <el-radio value="color">纯色</el-radio> |
| | | <el-radio value="img">图片</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="选择颜色" prop="bgColor" v-if="formData.bgType === 'color'"> |
| | |
| | | autoplay: false, |
| | | interval: 3, |
| | | items: [ |
| | | { type: 'img', imgUrl: 'https://xxxx/banner-01.jpg', videoUrl: '' }, |
| | | { type: 'img', imgUrl: 'https://xxxx/banner-02.jpg', videoUrl: '' } |
| | | { type: 'img', imgUrl: 'https://static.iocoder.cn/mall/banner-01.jpg', videoUrl: '' }, |
| | | { type: 'img', imgUrl: 'https://static.iocoder.cn/mall/banner-02.jpg', videoUrl: '' } |
| | | ] as CarouselItemProperty[], |
| | | style: { |
| | | bgType: 'color', |
| | |
| | | <el-form-item label="样式" prop="type"> |
| | | <el-radio-group v-model="formData.type"> |
| | | <el-tooltip class="item" content="默认" placement="bottom"> |
| | | <el-radio-button label="default"> |
| | | <el-radio-button value="default"> |
| | | <Icon icon="system-uicons:carousel" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <el-tooltip class="item" content="卡片" placement="bottom"> |
| | | <el-radio-button label="card"> |
| | | <el-radio-button value="card"> |
| | | <Icon icon="ic:round-view-carousel" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | |
| | | </el-form-item> |
| | | <el-form-item label="指示器" prop="indicator"> |
| | | <el-radio-group v-model="formData.indicator"> |
| | | <el-radio label="dot">小圆点</el-radio> |
| | | <el-radio label="number">数字</el-radio> |
| | | <el-radio value="dot">小圆点</el-radio> |
| | | <el-radio value="number">数字</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="是否轮播" prop="autoplay"> |
| | |
| | | <template #default="{ element }"> |
| | | <el-form-item label="类型" prop="type" class="m-b-8px!" label-width="40px"> |
| | | <el-radio-group v-model="element.type"> |
| | | <el-radio label="img">图片</el-radio> |
| | | <el-radio label="video">视频</el-radio> |
| | | <el-radio value="img">图片</el-radio> |
| | | <el-radio value="video">视频</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item |
| | |
| | | <el-form-item label="列数" prop="type"> |
| | | <el-radio-group v-model="formData.columns"> |
| | | <el-tooltip class="item" content="一列" placement="bottom"> |
| | | <el-radio-button :label="1"> |
| | | <el-radio-button :value="1"> |
| | | <Icon icon="fluent:text-column-one-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <el-tooltip class="item" content="二列" placement="bottom"> |
| | | <el-radio-button :label="2"> |
| | | <el-radio-button :value="2"> |
| | | <Icon icon="fluent:text-column-two-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <el-tooltip class="item" content="三列" placement="bottom"> |
| | | <el-radio-button :label="3"> |
| | | <el-radio-button :value="3"> |
| | | <Icon icon="fluent:text-column-three-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | |
| | | :key="index" |
| | | :content="item.text" |
| | | > |
| | | <el-radio-button :label="item.type"> |
| | | <el-radio-button :value="item.type"> |
| | | <Icon :icon="item.icon" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | |
| | | <el-form-item label="左右边距" prop="paddingType"> |
| | | <el-radio-group v-model="formData!.paddingType"> |
| | | <el-tooltip content="无边距" placement="top"> |
| | | <el-radio-button label="none"> |
| | | <el-radio-button value="none"> |
| | | <Icon icon="tabler:box-padding" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <el-tooltip content="左右留边" placement="top"> |
| | | <el-radio-button label="horizontal"> |
| | | <el-radio-button value="horizontal"> |
| | | <Icon icon="vaadin:padding" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | |
| | | defineProps<{ property: FloatingActionButtonProperty }>() |
| | | |
| | | // 是否展开 |
| | | const expanded = ref(true) |
| | | const expanded = ref(false) |
| | | // 处理展开/折叠 |
| | | const handleToggleFab = () => { |
| | | expanded.value = !expanded.value |
| | |
| | | <el-card header="按钮配置" class="property-group" shadow="never"> |
| | | <el-form-item label="展开方向" prop="direction"> |
| | | <el-radio-group v-model="formData.direction"> |
| | | <el-radio label="vertical">垂直</el-radio> |
| | | <el-radio label="horizontal">水平</el-radio> |
| | | <el-radio value="vertical">垂直</el-radio> |
| | | <el-radio value="horizontal">水平</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="显示文字" prop="showText"> |
| | |
| | | <el-form label-width="80px" :model="formData" class="m-t-8px"> |
| | | <el-form-item label="每行数量" prop="column"> |
| | | <el-radio-group v-model="formData.column"> |
| | | <el-radio :label="3">3个</el-radio> |
| | | <el-radio :label="4">4个</el-radio> |
| | | <el-radio :value="3">3个</el-radio> |
| | | <el-radio :value="4">4个</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | |
| | |
| | | <el-form label-width="80px" :model="formData" class="m-t-8px"> |
| | | <el-form-item label="布局" prop="layout"> |
| | | <el-radio-group v-model="formData.layout"> |
| | | <el-radio label="iconText">图标+文字</el-radio> |
| | | <el-radio label="icon">仅图标</el-radio> |
| | | <el-radio value="iconText">图标+文字</el-radio> |
| | | <el-radio value="icon">仅图标</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="行数" prop="row"> |
| | | <el-radio-group v-model="formData.row"> |
| | | <el-radio :label="1">1行</el-radio> |
| | | <el-radio :label="2">2行</el-radio> |
| | | <el-radio :value="1">1行</el-radio> |
| | | <el-radio :value="2">2行</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="列数" prop="column"> |
| | | <el-radio-group v-model="formData.column"> |
| | | <el-radio :label="3">3列</el-radio> |
| | | <el-radio :label="4">4列</el-radio> |
| | | <el-radio :label="5">5列</el-radio> |
| | | <el-radio :value="3">3列</el-radio> |
| | | <el-radio :value="4">4列</el-radio> |
| | | <el-radio :value="5">5列</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | |
| | |
| | | <template v-if="selectedHotAreaIndex === cellIndex"> |
| | | <el-form-item label="类型" :prop="`cell[${cellIndex}].type`"> |
| | | <el-radio-group v-model="cell.type"> |
| | | <el-radio label="text">文字</el-radio> |
| | | <el-radio label="image">图片</el-radio> |
| | | <el-radio label="search">搜索框</el-radio> |
| | | <el-radio value="text">文字</el-radio> |
| | | <el-radio value="image">图片</el-radio> |
| | | <el-radio value="search">搜索框</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <!-- 1. 文字 --> |
| | |
| | | <el-form label-width="80px" :model="formData" :rules="rules"> |
| | | <el-form-item label="样式" prop="styleType"> |
| | | <el-radio-group v-model="formData!.styleType"> |
| | | <el-radio label="normal">标准</el-radio> |
| | | <el-radio value="normal">标准</el-radio> |
| | | <el-tooltip |
| | | content="沉侵式头部仅支持微信小程序、APP,建议页面第一个组件为图片展示类组件" |
| | | placement="top" |
| | | > |
| | | <el-radio label="inner">沉浸式</el-radio> |
| | | <el-radio value="inner">沉浸式</el-radio> |
| | | </el-tooltip> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="常驻显示" prop="alwaysShow" v-if="formData.styleType === 'inner'"> |
| | | <el-radio-group v-model="formData!.alwaysShow"> |
| | | <el-radio :label="false">关闭</el-radio> |
| | | <el-radio :value="false">关闭</el-radio> |
| | | <el-tooltip content="常驻显示关闭后,头部小组件将在页面滑动时淡入" placement="top"> |
| | | <el-radio :label="true">开启</el-radio> |
| | | <el-radio :value="true">开启</el-radio> |
| | | </el-tooltip> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="背景类型" prop="bgType"> |
| | | <el-radio-group v-model="formData.bgType"> |
| | | <el-radio label="color">纯色</el-radio> |
| | | <el-radio label="img">图片</el-radio> |
| | | <el-radio value="color">纯色</el-radio> |
| | | <el-radio value="img">图片</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="背景颜色" prop="bgColor" v-if="formData.bgType === 'color'"> |
| | |
| | | name: '公告栏', |
| | | icon: 'ep:bell', |
| | | property: { |
| | | iconUrl: 'http://xxxx/static/images/xinjian.png', |
| | | iconUrl: 'http://mall.yudao.iocoder.cn/static/images/xinjian.png', |
| | | contents: [ |
| | | { |
| | | text: '', |
| | |
| | | <el-form-item label="显示次数" :prop="`list[${index}].showType`"> |
| | | <el-radio-group v-model="element.showType"> |
| | | <el-tooltip content="只显示一次,下次打开时不显示" placement="bottom"> |
| | | <el-radio label="once">一次</el-radio> |
| | | <el-radio value="once">一次</el-radio> |
| | | </el-tooltip> |
| | | <el-tooltip content="每次打开时都会显示" placement="bottom"> |
| | | <el-radio label="always">不限</el-radio> |
| | | <el-radio value="always">不限</el-radio> |
| | | </el-tooltip> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | |
| | | class="text-16px" |
| | | :style="{ color: property.fields.price.color }" |
| | | > |
| | | ¥{{ spu.price }} |
| | | ¥{{ fenToYuan(spu.price as any) }} |
| | | </span> |
| | | <!-- 市场价 --> |
| | | <span |
| | | v-if="property.fields.marketPrice.show && spu.marketPrice" |
| | | class="ml-4px text-10px line-through" |
| | | :style="{ color: property.fields.marketPrice.color }" |
| | | >¥{{ spu.marketPrice }}</span |
| | | > |
| | | >¥{{ fenToYuan(spu.marketPrice) }} |
| | | </span> |
| | | </div> |
| | | <div class="text-12px"> |
| | | <!-- 销量 --> |
| | |
| | | <script setup lang="ts"> |
| | | import { ProductCardProperty } from './config' |
| | | import * as ProductSpuApi from '@/api/mall/product/spu' |
| | | import { fenToYuan } from '../../../../../utils' |
| | | |
| | | /** 商品卡片 */ |
| | | defineOptions({ name: 'ProductCard' }) |
| | |
| | | <el-form-item label="布局" prop="type"> |
| | | <el-radio-group v-model="formData.layoutType"> |
| | | <el-tooltip class="item" content="单列大图" placement="bottom"> |
| | | <el-radio-button label="oneColBigImg"> |
| | | <el-radio-button value="oneColBigImg"> |
| | | <Icon icon="fluent:text-column-one-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <el-tooltip class="item" content="单列小图" placement="bottom"> |
| | | <el-radio-button label="oneColSmallImg"> |
| | | <el-radio-button value="oneColSmallImg"> |
| | | <Icon icon="fluent:text-column-two-left-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <el-tooltip class="item" content="双列" placement="bottom"> |
| | | <el-radio-button label="twoCol"> |
| | | <el-radio-button value="twoCol"> |
| | | <Icon icon="fluent:text-column-two-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | |
| | | <el-card header="按钮" class="property-group" shadow="never"> |
| | | <el-form-item label="按钮类型" prop="btnBuy.type"> |
| | | <el-radio-group v-model="formData.btnBuy.type"> |
| | | <el-radio-button label="text">文字</el-radio-button> |
| | | <el-radio-button label="img">图片</el-radio-button> |
| | | <el-radio-button value="text">文字</el-radio-button> |
| | | <el-radio-button value="img">图片</el-radio-button> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <template v-if="formData.btnBuy.type === 'text'"> |
| | |
| | | class="text-12px" |
| | | :style="{ color: property.fields.price.color }" |
| | | > |
| | | ¥{{ spu.price }} |
| | | ¥{{ fenToYuan(spu.price) }} |
| | | </span> |
| | | </div> |
| | | </div> |
| | |
| | | <script setup lang="ts"> |
| | | import { ProductListProperty } from './config' |
| | | import * as ProductSpuApi from '@/api/mall/product/spu' |
| | | import { fenToYuan } from '@/utils' |
| | | |
| | | /** 商品栏 */ |
| | | defineOptions({ name: 'ProductList' }) |
| | |
| | | <el-form-item label="布局" prop="type"> |
| | | <el-radio-group v-model="formData.layoutType"> |
| | | <el-tooltip class="item" content="双列" placement="bottom"> |
| | | <el-radio-button label="twoCol"> |
| | | <el-radio-button value="twoCol"> |
| | | <Icon icon="fluent:text-column-two-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <el-tooltip class="item" content="三列" placement="bottom"> |
| | | <el-radio-button label="threeCol"> |
| | | <el-radio-button value="threeCol"> |
| | | <Icon icon="fluent:text-column-three-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <el-tooltip class="item" content="水平滑动" placement="bottom"> |
| | | <el-radio-button label="horizSwiper"> |
| | | <el-radio-button value="horizSwiper"> |
| | | <Icon icon="system-uicons:carousel" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | |
| | | /** 拼团属性 */ |
| | | export interface PromotionCombinationProperty { |
| | | // 布局类型:单列 | 三列 |
| | | layoutType: 'oneCol' | 'threeCol' |
| | | layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol' |
| | | // 商品字段 |
| | | fields: { |
| | | // 商品名称 |
| | | name: PromotionCombinationFieldProperty |
| | | // 商品简介 |
| | | introduction: PromotionCombinationFieldProperty |
| | | // 商品价格 |
| | | price: PromotionCombinationFieldProperty |
| | | // 市场价 |
| | | marketPrice: PromotionCombinationFieldProperty |
| | | // 商品销量 |
| | | salesCount: PromotionCombinationFieldProperty |
| | | // 商品库存 |
| | | stock: PromotionCombinationFieldProperty |
| | | } |
| | | // 角标 |
| | | badge: { |
| | | // 是否显示 |
| | | show: boolean |
| | | // 角标图片 |
| | | imgUrl: string |
| | | } |
| | | // 按钮 |
| | | btnBuy: { |
| | | // 类型:文字 | 图片 |
| | | type: 'text' | 'img' |
| | | // 文字 |
| | | text: string |
| | | // 文字按钮:背景渐变起始颜色 |
| | | bgBeginColor: string |
| | | // 文字按钮:背景渐变结束颜色 |
| | | bgEndColor: string |
| | | // 图片按钮:图片地址 |
| | | imgUrl: string |
| | | } |
| | | // 上圆角 |
| | |
| | | // 间距 |
| | | space: number |
| | | // 拼团活动编号 |
| | | activityId: number |
| | | activityIds: number[] |
| | | // 组件样式 |
| | | style: ComponentStyle |
| | | } |
| | |
| | | name: '拼团', |
| | | icon: 'mdi:account-group', |
| | | property: { |
| | | layoutType: 'oneCol', |
| | | layoutType: 'oneColBigImg', |
| | | fields: { |
| | | name: { show: true, color: '#000' }, |
| | | price: { show: true, color: '#ff3000' } |
| | | introduction: { show: true, color: '#999' }, |
| | | price: { show: true, color: '#ff3000' }, |
| | | marketPrice: { show: true, color: '#c4c4c4' }, |
| | | salesCount: { show: true, color: '#c4c4c4' }, |
| | | stock: { show: false, color: '#c4c4c4' } |
| | | }, |
| | | badge: { show: false, imgUrl: '' }, |
| | | btnBuy: { |
| | | type: 'text', |
| | | text: '去拼团', |
| | | bgBeginColor: '#FF6000', |
| | | bgEndColor: '#FE832A', |
| | | imgUrl: '' |
| | | }, |
| | | borderRadiusTop: 8, |
| | | borderRadiusBottom: 8, |
| | | space: 8, |
| | |
| | | <template> |
| | | <el-scrollbar class="z-1 min-h-30px" wrap-class="w-full" ref="containerRef"> |
| | | <!-- 商品网格 --> |
| | | <div :class="`box-content min-h-30px w-full flex flex-row flex-wrap`" ref="containerRef"> |
| | | <div |
| | | class="grid overflow-x-auto" |
| | | class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white" |
| | | :style="{ |
| | | gridGap: `${property.space}px`, |
| | | gridTemplateColumns, |
| | | width: scrollbarWidth |
| | | ...calculateSpace(index), |
| | | ...calculateWidth(), |
| | | borderTopLeftRadius: `${property.borderRadiusTop}px`, |
| | | borderTopRightRadius: `${property.borderRadiusTop}px`, |
| | | borderBottomLeftRadius: `${property.borderRadiusBottom}px`, |
| | | borderBottomRightRadius: `${property.borderRadiusBottom}px` |
| | | }" |
| | | v-for="(spu, index) in spuList" |
| | | :key="index" |
| | | > |
| | | <!-- 商品 --> |
| | | <!-- 角标 --> |
| | | <div v-if="property.badge.show" class="absolute left-0 top-0 z-1 items-center justify-center"> |
| | | <el-image fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" /> |
| | | </div> |
| | | <!-- 商品封面图 --> |
| | | <div |
| | | class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white" |
| | | :style="{ |
| | | borderTopLeftRadius: `${property.borderRadiusTop}px`, |
| | | borderTopRightRadius: `${property.borderRadiusTop}px`, |
| | | borderBottomLeftRadius: `${property.borderRadiusBottom}px`, |
| | | borderBottomRightRadius: `${property.borderRadiusBottom}px` |
| | | }" |
| | | v-for="(spu, index) in spuList" |
| | | :key="index" |
| | | :class="[ |
| | | 'h-140px', |
| | | { |
| | | 'w-full': property.layoutType !== 'oneColSmallImg', |
| | | 'w-140px': property.layoutType === 'oneColSmallImg' |
| | | } |
| | | ]" |
| | | > |
| | | <!-- 角标 --> |
| | | <el-image fit="cover" class="h-full w-full" :src="spu.picUrl" /> |
| | | </div> |
| | | <div |
| | | :class="[ |
| | | ' flex flex-col gap-8px p-8px box-border', |
| | | { |
| | | 'w-full': property.layoutType !== 'oneColSmallImg', |
| | | 'w-[calc(100%-140px-16px)]': property.layoutType === 'oneColSmallImg' |
| | | } |
| | | ]" |
| | | > |
| | | <!-- 商品名称 --> |
| | | <div |
| | | v-if="property.badge.show" |
| | | class="absolute left-0 top-0 z-1 items-center justify-center" |
| | | > |
| | | <el-image fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" /> |
| | | </div> |
| | | <!-- 商品封面图 --> |
| | | <el-image fit="cover" :src="spu.picUrl" :style="{ width: imageSize, height: imageSize }" /> |
| | | <div |
| | | v-if="property.fields.name.show" |
| | | :class="[ |
| | | 'flex flex-col gap-8px p-8px box-border', |
| | | 'text-14px ', |
| | | { |
| | | 'w-[calc(100%-64px)]': columns === 2, |
| | | 'w-full': columns === 3 |
| | | truncate: property.layoutType !== 'oneColSmallImg', |
| | | 'overflow-ellipsis line-clamp-2': property.layoutType === 'oneColSmallImg' |
| | | } |
| | | ]" |
| | | :style="{ color: property.fields.name.color }" |
| | | > |
| | | <!-- 商品名称 --> |
| | | <div |
| | | v-if="property.fields.name.show" |
| | | class="truncate text-12px" |
| | | :style="{ color: property.fields.name.color }" |
| | | {{ spu.name }} |
| | | </div> |
| | | <!-- 商品简介 --> |
| | | <div |
| | | v-if="property.fields.introduction.show" |
| | | class="truncate text-12px" |
| | | :style="{ color: property.fields.introduction.color }" |
| | | > |
| | | {{ spu.introduction }} |
| | | </div> |
| | | <div> |
| | | <!-- 价格 --> |
| | | <span |
| | | v-if="property.fields.price.show" |
| | | class="text-16px" |
| | | :style="{ color: property.fields.price.color }" |
| | | > |
| | | {{ spu.name }} |
| | | </div> |
| | | <div> |
| | | <!-- 商品价格 --> |
| | | <span |
| | | v-if="property.fields.price.show" |
| | | class="text-12px" |
| | | :style="{ color: property.fields.price.color }" |
| | | > |
| | | ¥{{ spu.price }} |
| | | </span> |
| | | </div> |
| | | ¥{{ fenToYuan(spu.price || Infinity) }} |
| | | </span> |
| | | <!-- 市场价 --> |
| | | <span |
| | | v-if="property.fields.marketPrice.show && spu.marketPrice" |
| | | class="ml-4px text-10px line-through" |
| | | :style="{ color: property.fields.marketPrice.color }" |
| | | >¥{{ fenToYuan(spu.marketPrice) }}</span |
| | | > |
| | | </div> |
| | | <div class="text-12px"> |
| | | <!-- 销量 --> |
| | | <span |
| | | v-if="property.fields.salesCount.show" |
| | | :style="{ color: property.fields.salesCount.color }" |
| | | > |
| | | 已售{{ (spu.salesCount || 0) + (spu.virtualSalesCount || 0) }}件 |
| | | </span> |
| | | <!-- 库存 --> |
| | | <span v-if="property.fields.stock.show" :style="{ color: property.fields.stock.color }"> |
| | | 库存{{ spu.stock || 0 }} |
| | | </span> |
| | | </div> |
| | | </div> |
| | | <!-- 购买按钮 --> |
| | | <div class="absolute bottom-8px right-8px"> |
| | | <!-- 文字按钮 --> |
| | | <span |
| | | v-if="property.btnBuy.type === 'text'" |
| | | class="rounded-full p-x-12px p-y-4px text-12px text-white" |
| | | :style="{ |
| | | background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}` |
| | | }" |
| | | > |
| | | {{ property.btnBuy.text }} |
| | | </span> |
| | | <!-- 图片按钮 --> |
| | | <el-image |
| | | v-else |
| | | class="h-28px w-28px rounded-full" |
| | | fit="cover" |
| | | :src="property.btnBuy.imgUrl" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </el-scrollbar> |
| | | </div> |
| | | </template> |
| | | <script setup lang="ts"> |
| | | import { PromotionCombinationProperty } from './config' |
| | | import * as ProductSpuApi from '@/api/mall/product/spu' |
| | | import * as CombinationActivityApi from '@/api/mall/promotion/combination/combinationActivity' |
| | | import { fenToYuan } from '@/utils' |
| | | |
| | | /** 拼团 */ |
| | | /** 拼团卡片 */ |
| | | defineOptions({ name: 'PromotionCombination' }) |
| | | // 定义属性 |
| | | const props = defineProps<{ property: PromotionCombinationProperty }>() |
| | | // 商品列表 |
| | | const spuList = ref<ProductSpuApi.Spu[]>([]) |
| | | const spuIdList = ref<number[]>([]) |
| | | const combinationActivityList = ref<CombinationActivityApi.CombinationActivityVO[]>([]) |
| | | |
| | | watch( |
| | | () => props.property.activityId, |
| | | () => props.property.activityIds, |
| | | async () => { |
| | | if (!props.property.activityId) return |
| | | const activity = await CombinationActivityApi.getCombinationActivity(props.property.activityId) |
| | | if (!activity?.spuId) return |
| | | spuList.value = [await ProductSpuApi.getSpu(activity.spuId)] |
| | | try { |
| | | // 新添加的拼团组件,是没有活动ID的 |
| | | const activityIds = props.property.activityIds |
| | | // 检查活动ID的有效性 |
| | | if (Array.isArray(activityIds) && activityIds.length > 0) { |
| | | // 获取拼团活动详情列表 |
| | | combinationActivityList.value = |
| | | await CombinationActivityApi.getCombinationActivityListByIds(activityIds) |
| | | |
| | | // 获取拼团活动的 SPU 详情列表 |
| | | spuList.value = [] |
| | | spuIdList.value = combinationActivityList.value |
| | | .map((activity) => activity.spuId) |
| | | .filter((spuId): spuId is number => typeof spuId === 'number') |
| | | if (spuIdList.value.length > 0) { |
| | | spuList.value = await ProductSpuApi.getSpuDetailList(spuIdList.value) |
| | | } |
| | | |
| | | // 更新 SPU 的最低价格 |
| | | combinationActivityList.value.forEach((activity) => { |
| | | // 匹配spuId |
| | | const spu = spuList.value.find((spu) => spu.id === activity.spuId) |
| | | if (spu) { |
| | | // 赋值活动价格,哪个最便宜就赋值哪个 |
| | | spu.price = Math.min(activity.combinationPrice || Infinity, spu.price || Infinity) |
| | | } |
| | | }) |
| | | } |
| | | } catch (error) { |
| | | console.error('获取拼团活动细节或 SPU 细节时出错:', error) |
| | | } |
| | | }, |
| | | { |
| | | immediate: true, |
| | | deep: true |
| | | } |
| | | ) |
| | | // 手机宽度 |
| | | const phoneWidth = ref(375) |
| | | |
| | | /** |
| | | * 计算商品的间距 |
| | | * @param index 商品索引 |
| | | */ |
| | | const calculateSpace = (index: number) => { |
| | | // 商品的列数 |
| | | const columns = props.property.layoutType === 'twoCol' ? 2 : 1 |
| | | // 第一列没有左边距 |
| | | const marginLeft = index % columns === 0 ? '0' : props.property.space + 'px' |
| | | // 第一行没有上边距 |
| | | const marginTop = index < columns ? '0' : props.property.space + 'px' |
| | | |
| | | return { marginLeft, marginTop } |
| | | } |
| | | |
| | | // 容器 |
| | | const containerRef = ref() |
| | | // 商品的列数 |
| | | const columns = ref(2) |
| | | // 滚动条宽度 |
| | | const scrollbarWidth = ref('100%') |
| | | // 商品图大小 |
| | | const imageSize = ref('0') |
| | | // 商品网络列数 |
| | | const gridTemplateColumns = ref('') |
| | | // 计算布局参数 |
| | | watch( |
| | | () => [props.property, phoneWidth, spuList.value.length], |
| | | () => { |
| | | // 计算列数 |
| | | columns.value = props.property.layoutType === 'oneCol' ? 1 : 3 |
| | | // 每列的宽度为:(总宽度 - 间距 * (列数 - 1))/ 列数 |
| | | const productWidth = |
| | | (phoneWidth.value - props.property.space * (columns.value - 1)) / columns.value |
| | | // 商品图布局:2列时,左右布局 3列时,上下布局 |
| | | imageSize.value = columns.value === 2 ? '64px' : `${productWidth}px` |
| | | // 指定列数 |
| | | gridTemplateColumns.value = `repeat(${columns.value}, auto)` |
| | | // 不滚动 |
| | | scrollbarWidth.value = '100%' |
| | | }, |
| | | { immediate: true, deep: true } |
| | | ) |
| | | onMounted(() => { |
| | | // 提取手机宽度 |
| | | phoneWidth.value = containerRef.value?.wrapRef?.offsetWidth || 375 |
| | | }) |
| | | // 计算商品的宽度 |
| | | const calculateWidth = () => { |
| | | let width = '100%' |
| | | // 双列时每列的宽度为:(总宽度 - 间距)/ 2 |
| | | if (props.property.layoutType === 'twoCol') { |
| | | width = `${(containerRef.value.offsetWidth - props.property.space) / 2}px` |
| | | } |
| | | return { width } |
| | | } |
| | | </script> |
| | | |
| | | <style scoped lang="scss"></style> |
| | |
| | | <ComponentContainerProperty v-model="formData.style"> |
| | | <el-form label-width="80px" :model="formData"> |
| | | <el-card header="拼团活动" class="property-group" shadow="never"> |
| | | <el-form-item label="拼团活动" prop="activityId"> |
| | | <el-select v-model="formData.activityId"> |
| | | <el-option |
| | | v-for="activity in activityList" |
| | | :key="activity.id" |
| | | :label="activity.name" |
| | | :value="activity.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <CombinationShowcase v-model="formData.activityIds" /> |
| | | </el-card> |
| | | <el-card header="商品样式" class="property-group" shadow="never"> |
| | | <el-form-item label="布局" prop="type"> |
| | | <el-radio-group v-model="formData.layoutType"> |
| | | <el-tooltip class="item" content="单列" placement="bottom"> |
| | | <el-radio-button label="oneCol"> |
| | | <el-tooltip class="item" content="单列大图" placement="bottom"> |
| | | <el-radio-button value="oneColBigImg"> |
| | | <Icon icon="fluent:text-column-one-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <el-tooltip class="item" content="三列" placement="bottom"> |
| | | <el-radio-button label="threeCol"> |
| | | <Icon icon="fluent:text-column-three-24-filled" /> |
| | | <el-tooltip class="item" content="单列小图" placement="bottom"> |
| | | <el-radio-button value="oneColSmallImg"> |
| | | <Icon icon="fluent:text-column-two-left-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <el-tooltip class="item" content="双列" placement="bottom"> |
| | | <el-radio-button value="twoCol"> |
| | | <Icon icon="fluent:text-column-two-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <!--<el-tooltip class="item" content="三列" placement="bottom"> |
| | | <el-radio-button value="threeCol"> |
| | | <Icon icon="fluent:text-column-three-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip>--> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="商品名称" prop="fields.name.show"> |
| | |
| | | <el-checkbox v-model="formData.fields.name.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="商品简介" prop="fields.introduction.show"> |
| | | <div class="flex gap-8px"> |
| | | <ColorInput v-model="formData.fields.introduction.color" /> |
| | | <el-checkbox v-model="formData.fields.introduction.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="商品价格" prop="fields.price.show"> |
| | | <div class="flex gap-8px"> |
| | | <ColorInput v-model="formData.fields.price.color" /> |
| | | <el-checkbox v-model="formData.fields.price.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="市场价" prop="fields.marketPrice.show"> |
| | | <div class="flex gap-8px"> |
| | | <ColorInput v-model="formData.fields.marketPrice.color" /> |
| | | <el-checkbox v-model="formData.fields.marketPrice.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="商品销量" prop="fields.salesCount.show"> |
| | | <div class="flex gap-8px"> |
| | | <ColorInput v-model="formData.fields.salesCount.color" /> |
| | | <el-checkbox v-model="formData.fields.salesCount.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="商品库存" prop="fields.stock.show"> |
| | | <div class="flex gap-8px"> |
| | | <ColorInput v-model="formData.fields.stock.color" /> |
| | | <el-checkbox v-model="formData.fields.stock.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | </el-card> |
| | |
| | | </el-form-item> |
| | | <el-form-item label="角标" prop="badge.imgUrl" v-if="formData.badge.show"> |
| | | <UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px"> |
| | | <template #tip> 建议尺寸:36 * 22 </template> |
| | | <template #tip> 建议尺寸:36 * 22</template> |
| | | </UploadImg> |
| | | </el-form-item> |
| | | </el-card> |
| | | <el-card header="按钮" class="property-group" shadow="never"> |
| | | <el-form-item label="按钮类型" prop="btnBuy.type"> |
| | | <el-radio-group v-model="formData.btnBuy.type"> |
| | | <el-radio-button value="text">文字</el-radio-button> |
| | | <el-radio-button value="img">图片</el-radio-button> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <template v-if="formData.btnBuy.type === 'text'"> |
| | | <el-form-item label="按钮文字" prop="btnBuy.text"> |
| | | <el-input v-model="formData.btnBuy.text" /> |
| | | </el-form-item> |
| | | <el-form-item label="左侧背景" prop="btnBuy.bgBeginColor"> |
| | | <ColorInput v-model="formData.btnBuy.bgBeginColor" /> |
| | | </el-form-item> |
| | | <el-form-item label="右侧背景" prop="btnBuy.bgEndColor"> |
| | | <ColorInput v-model="formData.btnBuy.bgEndColor" /> |
| | | </el-form-item> |
| | | </template> |
| | | <template v-else> |
| | | <el-form-item label="图片" prop="btnBuy.imgUrl"> |
| | | <UploadImg v-model="formData.btnBuy.imgUrl" height="56px" width="56px"> |
| | | <template #tip> 建议尺寸:56 * 56</template> |
| | | </UploadImg> |
| | | </el-form-item> |
| | | </template> |
| | | </el-card> |
| | | <el-card header="商品样式" class="property-group" shadow="never"> |
| | | <el-form-item label="上圆角" prop="borderRadiusTop"> |
| | |
| | | import { usePropertyForm } from '@/components/DiyEditor/util' |
| | | import * as CombinationActivityApi from '@/api/mall/promotion/combination/combinationActivity' |
| | | import { CommonStatusEnum } from '@/utils/constants' |
| | | import CombinationShowcase from '@/views/mall/promotion/combination/components/CombinationShowcase.vue' |
| | | |
| | | // 拼团属性面板 |
| | | defineOptions({ name: 'PromotionCombinationProperty' }) |
| | |
| | | const emit = defineEmits(['update:modelValue']) |
| | | const { formData } = usePropertyForm(props.modelValue, emit) |
| | | // 活动列表 |
| | | const activityList = ref<CombinationActivityApi.CombinationActivityVO>([]) |
| | | const activityList = ref<CombinationActivityApi.CombinationActivityVO[]>([]) |
| | | onMounted(async () => { |
| | | const { list } = await CombinationActivityApi.getCombinationActivityPage({ |
| | | status: CommonStatusEnum.ENABLE |
对比新文件 |
| | |
| | | import {ComponentStyle, DiyComponent} from '@/components/DiyEditor/util' |
| | | |
| | | /** 积分商城属性 */ |
| | | export interface PromotionPointProperty { |
| | | // 布局类型:单列 | 三列 |
| | | layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol' |
| | | // 商品字段 |
| | | fields: { |
| | | // 商品名称 |
| | | name: PromotionPointFieldProperty |
| | | // 商品简介 |
| | | introduction: PromotionPointFieldProperty |
| | | // 商品价格 |
| | | price: PromotionPointFieldProperty |
| | | // 市场价 |
| | | marketPrice: PromotionPointFieldProperty |
| | | // 商品销量 |
| | | salesCount: PromotionPointFieldProperty |
| | | // 商品库存 |
| | | stock: PromotionPointFieldProperty |
| | | } |
| | | // 角标 |
| | | badge: { |
| | | // 是否显示 |
| | | show: boolean |
| | | // 角标图片 |
| | | imgUrl: string |
| | | } |
| | | // 按钮 |
| | | btnBuy: { |
| | | // 类型:文字 | 图片 |
| | | type: 'text' | 'img' |
| | | // 文字 |
| | | text: string |
| | | // 文字按钮:背景渐变起始颜色 |
| | | bgBeginColor: string |
| | | // 文字按钮:背景渐变结束颜色 |
| | | bgEndColor: string |
| | | // 图片按钮:图片地址 |
| | | imgUrl: string |
| | | } |
| | | // 上圆角 |
| | | borderRadiusTop: number |
| | | // 下圆角 |
| | | borderRadiusBottom: number |
| | | // 间距 |
| | | space: number |
| | | // 秒杀活动编号 |
| | | activityIds: number[] |
| | | // 组件样式 |
| | | style: ComponentStyle |
| | | } |
| | | |
| | | // 商品字段 |
| | | export interface PromotionPointFieldProperty { |
| | | // 是否显示 |
| | | show: boolean |
| | | // 颜色 |
| | | color: string |
| | | } |
| | | |
| | | // 定义组件 |
| | | export const component = { |
| | | id: 'PromotionPoint', |
| | | name: '积分商城', |
| | | icon: 'ep:present', |
| | | property: { |
| | | layoutType: 'oneColBigImg', |
| | | fields: { |
| | | name: { show: true, color: '#000' }, |
| | | introduction: { show: true, color: '#999' }, |
| | | price: { show: true, color: '#ff3000' }, |
| | | marketPrice: { show: true, color: '#c4c4c4' }, |
| | | salesCount: { show: true, color: '#c4c4c4' }, |
| | | stock: { show: false, color: '#c4c4c4' } |
| | | }, |
| | | badge: { show: false, imgUrl: '' }, |
| | | btnBuy: { |
| | | type: 'text', |
| | | text: '立即兑换', |
| | | bgBeginColor: '#FF6000', |
| | | bgEndColor: '#FE832A', |
| | | imgUrl: '' |
| | | }, |
| | | borderRadiusTop: 8, |
| | | borderRadiusBottom: 8, |
| | | space: 8, |
| | | style: { |
| | | bgType: 'color', |
| | | bgColor: '', |
| | | marginLeft: 8, |
| | | marginRight: 8, |
| | | marginBottom: 8 |
| | | } as ComponentStyle |
| | | } |
| | | } as DiyComponent<PromotionPointProperty> |
对比新文件 |
| | |
| | | <template> |
| | | <div ref="containerRef" :class="`box-content min-h-30px w-full flex flex-row flex-wrap`"> |
| | | <div |
| | | v-for="(spu, index) in spuList" |
| | | :key="index" |
| | | :style="{ |
| | | ...calculateSpace(index), |
| | | ...calculateWidth(), |
| | | borderTopLeftRadius: `${property.borderRadiusTop}px`, |
| | | borderTopRightRadius: `${property.borderRadiusTop}px`, |
| | | borderBottomLeftRadius: `${property.borderRadiusBottom}px`, |
| | | borderBottomRightRadius: `${property.borderRadiusBottom}px` |
| | | }" |
| | | class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white" |
| | | > |
| | | <!-- 角标 --> |
| | | <div v-if="property.badge.show" class="absolute left-0 top-0 z-1 items-center justify-center"> |
| | | <el-image :src="property.badge.imgUrl" class="h-26px w-38px" fit="cover" /> |
| | | </div> |
| | | <!-- 商品封面图 --> |
| | | <div |
| | | :class="[ |
| | | 'h-140px', |
| | | { |
| | | 'w-full': property.layoutType !== 'oneColSmallImg', |
| | | 'w-140px': property.layoutType === 'oneColSmallImg' |
| | | } |
| | | ]" |
| | | > |
| | | <el-image :src="spu.picUrl" class="h-full w-full" fit="cover" /> |
| | | </div> |
| | | <div |
| | | :class="[ |
| | | ' flex flex-col gap-8px p-8px box-border', |
| | | { |
| | | 'w-full': property.layoutType !== 'oneColSmallImg', |
| | | 'w-[calc(100%-140px-16px)]': property.layoutType === 'oneColSmallImg' |
| | | } |
| | | ]" |
| | | > |
| | | <!-- 商品名称 --> |
| | | <div |
| | | v-if="property.fields.name.show" |
| | | :class="[ |
| | | 'text-14px ', |
| | | { |
| | | truncate: property.layoutType !== 'oneColSmallImg', |
| | | 'overflow-ellipsis line-clamp-2': property.layoutType === 'oneColSmallImg' |
| | | } |
| | | ]" |
| | | :style="{ color: property.fields.name.color }" |
| | | > |
| | | {{ spu.name }} |
| | | </div> |
| | | <!-- 商品简介 --> |
| | | <div |
| | | v-if="property.fields.introduction.show" |
| | | :style="{ color: property.fields.introduction.color }" |
| | | class="truncate text-12px" |
| | | > |
| | | {{ spu.introduction }} |
| | | </div> |
| | | <div> |
| | | <!-- 积分 --> |
| | | <span |
| | | v-if="property.fields.price.show" |
| | | :style="{ color: property.fields.price.color }" |
| | | class="text-16px" |
| | | > |
| | | {{ spu.point }}积分 |
| | | {{ !spu.pointPrice || spu.pointPrice === 0 ? '' : `+${fenToYuan(spu.pointPrice)}元` }} |
| | | </span> |
| | | <!-- 市场价 --> |
| | | <span |
| | | v-if="property.fields.marketPrice.show && spu.marketPrice" |
| | | :style="{ color: property.fields.marketPrice.color }" |
| | | class="ml-4px text-10px line-through" |
| | | > |
| | | ¥{{ fenToYuan(spu.marketPrice) }} |
| | | </span> |
| | | </div> |
| | | <div class="text-12px"> |
| | | <!-- 销量 --> |
| | | <span |
| | | v-if="property.fields.salesCount.show" |
| | | :style="{ color: property.fields.salesCount.color }" |
| | | > |
| | | 已兑{{ (spu.pointTotalStock || 0) - (spu.pointStock || 0) }}件 |
| | | </span> |
| | | <!-- 库存 --> |
| | | <span v-if="property.fields.stock.show" :style="{ color: property.fields.stock.color }"> |
| | | 库存{{ spu.pointTotalStock || 0 }} |
| | | </span> |
| | | </div> |
| | | </div> |
| | | <!-- 购买按钮 --> |
| | | <div class="absolute bottom-8px right-8px"> |
| | | <!-- 文字按钮 --> |
| | | <span |
| | | v-if="property.btnBuy.type === 'text'" |
| | | :style="{ |
| | | background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}` |
| | | }" |
| | | class="rounded-full p-x-12px p-y-4px text-12px text-white" |
| | | > |
| | | {{ property.btnBuy.text }} |
| | | </span> |
| | | <!-- 图片按钮 --> |
| | | <el-image |
| | | v-else |
| | | :src="property.btnBuy.imgUrl" |
| | | class="h-28px w-28px rounded-full" |
| | | fit="cover" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <script lang="ts" setup> |
| | | import { PromotionPointProperty } from './config' |
| | | import * as ProductSpuApi from '@/api/mall/product/spu' |
| | | import { PointActivityApi, PointActivityVO, SpuExtension0 } from '@/api/mall/promotion/point' |
| | | import { fenToYuan } from '@/utils' |
| | | |
| | | /** 积分商城卡片 */ |
| | | defineOptions({ name: 'PromotionPoint' }) |
| | | // 定义属性 |
| | | const props = defineProps<{ property: PromotionPointProperty }>() |
| | | // 商品列表 |
| | | const spuList = ref<SpuExtension0[]>([]) |
| | | const spuIdList = ref<number[]>([]) |
| | | const pointActivityList = ref<PointActivityVO[]>([]) |
| | | |
| | | watch( |
| | | () => props.property.activityIds, |
| | | async () => { |
| | | try { |
| | | // 新添加的积分商城组件,是没有活动ID的 |
| | | const activityIds = props.property.activityIds |
| | | // 检查活动ID的有效性 |
| | | if (Array.isArray(activityIds) && activityIds.length > 0) { |
| | | // 获取积分商城活动详情列表 |
| | | pointActivityList.value = await PointActivityApi.getPointActivityListByIds(activityIds) |
| | | |
| | | // 获取积分商城活动的 SPU 详情列表 |
| | | spuList.value = [] |
| | | spuIdList.value = pointActivityList.value.map((activity) => activity.spuId) |
| | | if (spuIdList.value.length > 0) { |
| | | spuList.value = await ProductSpuApi.getSpuDetailList(spuIdList.value) |
| | | } |
| | | |
| | | // 更新 SPU 的最低兑换积分和所需兑换金额 |
| | | pointActivityList.value.forEach((activity) => { |
| | | // 匹配spuId |
| | | const spu = spuList.value.find((spu) => spu.id === activity.spuId) |
| | | if (spu) { |
| | | spu.pointStock = activity.stock |
| | | spu.pointTotalStock = activity.totalStock |
| | | spu.point = activity.point |
| | | spu.pointPrice = activity.price |
| | | } |
| | | }) |
| | | } |
| | | } catch (error) { |
| | | console.error('获取积分商城活动细节或 SPU 细节时出错:', error) |
| | | } |
| | | }, |
| | | { |
| | | immediate: true, |
| | | deep: true |
| | | } |
| | | ) |
| | | |
| | | /** |
| | | * 计算商品的间距 |
| | | * @param index 商品索引 |
| | | */ |
| | | const calculateSpace = (index: number) => { |
| | | // 商品的列数 |
| | | const columns = props.property.layoutType === 'twoCol' ? 2 : 1 |
| | | // 第一列没有左边距 |
| | | const marginLeft = index % columns === 0 ? '0' : props.property.space + 'px' |
| | | // 第一行没有上边距 |
| | | const marginTop = index < columns ? '0' : props.property.space + 'px' |
| | | |
| | | return { marginLeft, marginTop } |
| | | } |
| | | |
| | | // 容器 |
| | | const containerRef = ref() |
| | | // 计算商品的宽度 |
| | | const calculateWidth = () => { |
| | | let width = '100%' |
| | | // 双列时每列的宽度为:(总宽度 - 间距)/ 2 |
| | | if (props.property.layoutType === 'twoCol') { |
| | | width = `${(containerRef.value.offsetWidth - props.property.space) / 2}px` |
| | | } |
| | | return { width } |
| | | } |
| | | </script> |
| | | |
| | | <style lang="scss" scoped></style> |
对比新文件 |
| | |
| | | <template> |
| | | <ComponentContainerProperty v-model="formData.style"> |
| | | <el-form :model="formData" label-width="80px"> |
| | | <el-card class="property-group" header="积分商城活动" shadow="never"> |
| | | <PointShowcase v-model="formData.activityIds" /> |
| | | </el-card> |
| | | <el-card class="property-group" header="商品样式" shadow="never"> |
| | | <el-form-item label="布局" prop="type"> |
| | | <el-radio-group v-model="formData.layoutType"> |
| | | <el-tooltip class="item" content="单列大图" placement="bottom"> |
| | | <el-radio-button value="oneColBigImg"> |
| | | <Icon icon="fluent:text-column-one-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <el-tooltip class="item" content="单列小图" placement="bottom"> |
| | | <el-radio-button value="oneColSmallImg"> |
| | | <Icon icon="fluent:text-column-two-left-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <el-tooltip class="item" content="双列" placement="bottom"> |
| | | <el-radio-button value="twoCol"> |
| | | <Icon icon="fluent:text-column-two-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <!--<el-tooltip class="item" content="三列" placement="bottom"> |
| | | <el-radio-button value="threeCol"> |
| | | <Icon icon="fluent:text-column-three-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip>--> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="商品名称" prop="fields.name.show"> |
| | | <div class="flex gap-8px"> |
| | | <ColorInput v-model="formData.fields.name.color" /> |
| | | <el-checkbox v-model="formData.fields.name.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="商品简介" prop="fields.introduction.show"> |
| | | <div class="flex gap-8px"> |
| | | <ColorInput v-model="formData.fields.introduction.color" /> |
| | | <el-checkbox v-model="formData.fields.introduction.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="商品价格" prop="fields.price.show"> |
| | | <div class="flex gap-8px"> |
| | | <ColorInput v-model="formData.fields.price.color" /> |
| | | <el-checkbox v-model="formData.fields.price.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="市场价" prop="fields.marketPrice.show"> |
| | | <div class="flex gap-8px"> |
| | | <ColorInput v-model="formData.fields.marketPrice.color" /> |
| | | <el-checkbox v-model="formData.fields.marketPrice.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="商品销量" prop="fields.salesCount.show"> |
| | | <div class="flex gap-8px"> |
| | | <ColorInput v-model="formData.fields.salesCount.color" /> |
| | | <el-checkbox v-model="formData.fields.salesCount.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="商品库存" prop="fields.stock.show"> |
| | | <div class="flex gap-8px"> |
| | | <ColorInput v-model="formData.fields.stock.color" /> |
| | | <el-checkbox v-model="formData.fields.stock.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | </el-card> |
| | | <el-card class="property-group" header="角标" shadow="never"> |
| | | <el-form-item label="角标" prop="badge.show"> |
| | | <el-switch v-model="formData.badge.show" /> |
| | | </el-form-item> |
| | | <el-form-item v-if="formData.badge.show" label="角标" prop="badge.imgUrl"> |
| | | <UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px"> |
| | | <template #tip> 建议尺寸:36 * 22</template> |
| | | </UploadImg> |
| | | </el-form-item> |
| | | </el-card> |
| | | <el-card class="property-group" header="按钮" shadow="never"> |
| | | <el-form-item label="按钮类型" prop="btnBuy.type"> |
| | | <el-radio-group v-model="formData.btnBuy.type"> |
| | | <el-radio-button value="text">文字</el-radio-button> |
| | | <el-radio-button value="img">图片</el-radio-button> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <template v-if="formData.btnBuy.type === 'text'"> |
| | | <el-form-item label="按钮文字" prop="btnBuy.text"> |
| | | <el-input v-model="formData.btnBuy.text" /> |
| | | </el-form-item> |
| | | <el-form-item label="左侧背景" prop="btnBuy.bgBeginColor"> |
| | | <ColorInput v-model="formData.btnBuy.bgBeginColor" /> |
| | | </el-form-item> |
| | | <el-form-item label="右侧背景" prop="btnBuy.bgEndColor"> |
| | | <ColorInput v-model="formData.btnBuy.bgEndColor" /> |
| | | </el-form-item> |
| | | </template> |
| | | <template v-else> |
| | | <el-form-item label="图片" prop="btnBuy.imgUrl"> |
| | | <UploadImg v-model="formData.btnBuy.imgUrl" height="56px" width="56px"> |
| | | <template #tip> 建议尺寸:56 * 56</template> |
| | | </UploadImg> |
| | | </el-form-item> |
| | | </template> |
| | | </el-card> |
| | | <el-card class="property-group" header="商品样式" shadow="never"> |
| | | <el-form-item label="上圆角" prop="borderRadiusTop"> |
| | | <el-slider |
| | | v-model="formData.borderRadiusTop" |
| | | :max="100" |
| | | :min="0" |
| | | :show-input-controls="false" |
| | | input-size="small" |
| | | show-input |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="下圆角" prop="borderRadiusBottom"> |
| | | <el-slider |
| | | v-model="formData.borderRadiusBottom" |
| | | :max="100" |
| | | :min="0" |
| | | :show-input-controls="false" |
| | | input-size="small" |
| | | show-input |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="间隔" prop="space"> |
| | | <el-slider |
| | | v-model="formData.space" |
| | | :max="100" |
| | | :min="0" |
| | | :show-input-controls="false" |
| | | input-size="small" |
| | | show-input |
| | | /> |
| | | </el-form-item> |
| | | </el-card> |
| | | </el-form> |
| | | </ComponentContainerProperty> |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | import { PromotionPointProperty } from './config' |
| | | import { usePropertyForm } from '@/components/DiyEditor/util' |
| | | import PointShowcase from '@/views/mall/promotion/point/components/PointShowcase.vue' |
| | | |
| | | // 秒杀属性面板 |
| | | defineOptions({ name: 'PromotionPointProperty' }) |
| | | |
| | | const props = defineProps<{ modelValue: PromotionPointProperty }>() |
| | | const emit = defineEmits(['update:modelValue']) |
| | | const { formData } = usePropertyForm(props.modelValue, emit) |
| | | </script> |
| | | |
| | | <style lang="scss" scoped></style> |
| | |
| | | /** 秒杀属性 */ |
| | | export interface PromotionSeckillProperty { |
| | | // 布局类型:单列 | 三列 |
| | | layoutType: 'oneCol' | 'threeCol' |
| | | layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol' |
| | | // 商品字段 |
| | | fields: { |
| | | // 商品名称 |
| | | name: PromotionSeckillFieldProperty |
| | | // 商品简介 |
| | | introduction: PromotionSeckillFieldProperty |
| | | // 商品价格 |
| | | price: PromotionSeckillFieldProperty |
| | | // 市场价 |
| | | marketPrice: PromotionSeckillFieldProperty |
| | | // 商品销量 |
| | | salesCount: PromotionSeckillFieldProperty |
| | | // 商品库存 |
| | | stock: PromotionSeckillFieldProperty |
| | | } |
| | | // 角标 |
| | | badge: { |
| | | // 是否显示 |
| | | show: boolean |
| | | // 角标图片 |
| | | imgUrl: string |
| | | } |
| | | // 按钮 |
| | | btnBuy: { |
| | | // 类型:文字 | 图片 |
| | | type: 'text' | 'img' |
| | | // 文字 |
| | | text: string |
| | | // 文字按钮:背景渐变起始颜色 |
| | | bgBeginColor: string |
| | | // 文字按钮:背景渐变结束颜色 |
| | | bgEndColor: string |
| | | // 图片按钮:图片地址 |
| | | imgUrl: string |
| | | } |
| | | // 上圆角 |
| | |
| | | // 间距 |
| | | space: number |
| | | // 秒杀活动编号 |
| | | activityId: number |
| | | activityIds: number[] |
| | | // 组件样式 |
| | | style: ComponentStyle |
| | | } |
| | | |
| | | // 商品字段 |
| | | export interface PromotionSeckillFieldProperty { |
| | | // 是否显示 |
| | |
| | | name: '秒杀', |
| | | icon: 'mdi:calendar-time', |
| | | property: { |
| | | activityId: undefined, |
| | | layoutType: 'oneCol', |
| | | layoutType: 'oneColBigImg', |
| | | fields: { |
| | | name: { show: true, color: '#000' }, |
| | | price: { show: true, color: '#ff3000' } |
| | | introduction: { show: true, color: '#999' }, |
| | | price: { show: true, color: '#ff3000' }, |
| | | marketPrice: { show: true, color: '#c4c4c4' }, |
| | | salesCount: { show: true, color: '#c4c4c4' }, |
| | | stock: { show: false, color: '#c4c4c4' } |
| | | }, |
| | | badge: { show: false, imgUrl: '' }, |
| | | btnBuy: { |
| | | type: 'text', |
| | | text: '立即秒杀', |
| | | bgBeginColor: '#FF6000', |
| | | bgEndColor: '#FE832A', |
| | | imgUrl: '' |
| | | }, |
| | | borderRadiusTop: 8, |
| | | borderRadiusBottom: 8, |
| | | space: 8, |
| | |
| | | <template> |
| | | <el-scrollbar class="z-1 min-h-30px" wrap-class="w-full" ref="containerRef"> |
| | | <!-- 商品网格 --> |
| | | <div :class="`box-content min-h-30px w-full flex flex-row flex-wrap`" ref="containerRef"> |
| | | <div |
| | | class="grid overflow-x-auto" |
| | | class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white" |
| | | :style="{ |
| | | gridGap: `${property.space}px`, |
| | | gridTemplateColumns, |
| | | width: scrollbarWidth |
| | | ...calculateSpace(index), |
| | | ...calculateWidth(), |
| | | borderTopLeftRadius: `${property.borderRadiusTop}px`, |
| | | borderTopRightRadius: `${property.borderRadiusTop}px`, |
| | | borderBottomLeftRadius: `${property.borderRadiusBottom}px`, |
| | | borderBottomRightRadius: `${property.borderRadiusBottom}px` |
| | | }" |
| | | v-for="(spu, index) in spuList" |
| | | :key="index" |
| | | > |
| | | <!-- 商品 --> |
| | | <!-- 角标 --> |
| | | <div v-if="property.badge.show" class="absolute left-0 top-0 z-1 items-center justify-center"> |
| | | <el-image fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" /> |
| | | </div> |
| | | <!-- 商品封面图 --> |
| | | <div |
| | | class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white" |
| | | :style="{ |
| | | borderTopLeftRadius: `${property.borderRadiusTop}px`, |
| | | borderTopRightRadius: `${property.borderRadiusTop}px`, |
| | | borderBottomLeftRadius: `${property.borderRadiusBottom}px`, |
| | | borderBottomRightRadius: `${property.borderRadiusBottom}px` |
| | | }" |
| | | v-for="(spu, index) in spuList" |
| | | :key="index" |
| | | :class="[ |
| | | 'h-140px', |
| | | { |
| | | 'w-full': property.layoutType !== 'oneColSmallImg', |
| | | 'w-140px': property.layoutType === 'oneColSmallImg' |
| | | } |
| | | ]" |
| | | > |
| | | <!-- 角标 --> |
| | | <el-image fit="cover" class="h-full w-full" :src="spu.picUrl" /> |
| | | </div> |
| | | <div |
| | | :class="[ |
| | | ' flex flex-col gap-8px p-8px box-border', |
| | | { |
| | | 'w-full': property.layoutType !== 'oneColSmallImg', |
| | | 'w-[calc(100%-140px-16px)]': property.layoutType === 'oneColSmallImg' |
| | | } |
| | | ]" |
| | | > |
| | | <!-- 商品名称 --> |
| | | <div |
| | | v-if="property.badge.show" |
| | | class="absolute left-0 top-0 z-1 items-center justify-center" |
| | | > |
| | | <el-image fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" /> |
| | | </div> |
| | | <!-- 商品封面图 --> |
| | | <el-image fit="cover" :src="spu.picUrl" :style="{ width: imageSize, height: imageSize }" /> |
| | | <div |
| | | v-if="property.fields.name.show" |
| | | :class="[ |
| | | 'flex flex-col gap-8px p-8px box-border', |
| | | 'text-14px ', |
| | | { |
| | | 'w-[calc(100%-64px)]': columns === 2, |
| | | 'w-full': columns === 3 |
| | | truncate: property.layoutType !== 'oneColSmallImg', |
| | | 'overflow-ellipsis line-clamp-2': property.layoutType === 'oneColSmallImg' |
| | | } |
| | | ]" |
| | | :style="{ color: property.fields.name.color }" |
| | | > |
| | | <!-- 商品名称 --> |
| | | <div |
| | | v-if="property.fields.name.show" |
| | | class="truncate text-12px" |
| | | :style="{ color: property.fields.name.color }" |
| | | {{ spu.name }} |
| | | </div> |
| | | <!-- 商品简介 --> |
| | | <div |
| | | v-if="property.fields.introduction.show" |
| | | class="truncate text-12px" |
| | | :style="{ color: property.fields.introduction.color }" |
| | | > |
| | | {{ spu.introduction }} |
| | | </div> |
| | | <div> |
| | | <!-- 价格 --> |
| | | <span |
| | | v-if="property.fields.price.show" |
| | | class="text-16px" |
| | | :style="{ color: property.fields.price.color }" |
| | | > |
| | | {{ spu.name }} |
| | | </div> |
| | | <div> |
| | | <!-- 商品价格 --> |
| | | <span |
| | | v-if="property.fields.price.show" |
| | | class="text-12px" |
| | | :style="{ color: property.fields.price.color }" |
| | | > |
| | | ¥{{ spu.price }} |
| | | </span> |
| | | </div> |
| | | ¥{{ fenToYuan(spu.price || Infinity) }} |
| | | </span> |
| | | <!-- 市场价 --> |
| | | <span |
| | | v-if="property.fields.marketPrice.show && spu.marketPrice" |
| | | class="ml-4px text-10px line-through" |
| | | :style="{ color: property.fields.marketPrice.color }" |
| | | >¥{{ fenToYuan(spu.marketPrice) }}</span |
| | | > |
| | | </div> |
| | | <div class="text-12px"> |
| | | <!-- 销量 --> |
| | | <span |
| | | v-if="property.fields.salesCount.show" |
| | | :style="{ color: property.fields.salesCount.color }" |
| | | > |
| | | 已售{{ (spu.salesCount || 0) + (spu.virtualSalesCount || 0) }}件 |
| | | </span> |
| | | <!-- 库存 --> |
| | | <span v-if="property.fields.stock.show" :style="{ color: property.fields.stock.color }"> |
| | | 库存{{ spu.stock || 0 }} |
| | | </span> |
| | | </div> |
| | | </div> |
| | | <!-- 购买按钮 --> |
| | | <div class="absolute bottom-8px right-8px"> |
| | | <!-- 文字按钮 --> |
| | | <span |
| | | v-if="property.btnBuy.type === 'text'" |
| | | class="rounded-full p-x-12px p-y-4px text-12px text-white" |
| | | :style="{ |
| | | background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}` |
| | | }" |
| | | > |
| | | {{ property.btnBuy.text }} |
| | | </span> |
| | | <!-- 图片按钮 --> |
| | | <el-image |
| | | v-else |
| | | class="h-28px w-28px rounded-full" |
| | | fit="cover" |
| | | :src="property.btnBuy.imgUrl" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </el-scrollbar> |
| | | </div> |
| | | </template> |
| | | <script setup lang="ts"> |
| | | import { PromotionSeckillProperty } from './config' |
| | | import * as ProductSpuApi from '@/api/mall/product/spu' |
| | | import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity' |
| | | import { fenToYuan } from '@/utils' |
| | | |
| | | /** 秒杀 */ |
| | | /** 秒杀卡片 */ |
| | | defineOptions({ name: 'PromotionSeckill' }) |
| | | // 定义属性 |
| | | const props = defineProps<{ property: PromotionSeckillProperty }>() |
| | | // 商品列表 |
| | | const spuList = ref<ProductSpuApi.Spu[]>([]) |
| | | const spuIdList = ref<number[]>([]) |
| | | const seckillActivityList = ref<SeckillActivityApi.SeckillActivityVO[]>([]) |
| | | |
| | | watch( |
| | | () => props.property.activityId, |
| | | () => props.property.activityIds, |
| | | async () => { |
| | | if (!props.property.activityId) return |
| | | const activity = await SeckillActivityApi.getSeckillActivity(props.property.activityId) |
| | | if (!activity?.spuId) return |
| | | spuList.value = [await ProductSpuApi.getSpu(activity.spuId)] |
| | | try { |
| | | // 新添加的秒杀组件,是没有活动ID的 |
| | | const activityIds = props.property.activityIds |
| | | // 检查活动ID的有效性 |
| | | if (Array.isArray(activityIds) && activityIds.length > 0) { |
| | | // 获取秒杀活动详情列表 |
| | | seckillActivityList.value = |
| | | await SeckillActivityApi.getSeckillActivityListByIds(activityIds) |
| | | |
| | | // 获取秒杀活动的 SPU 详情列表 |
| | | spuList.value = [] |
| | | spuIdList.value = seckillActivityList.value |
| | | .map((activity) => activity.spuId) |
| | | .filter((spuId): spuId is number => typeof spuId === 'number') |
| | | if (spuIdList.value.length > 0) { |
| | | spuList.value = await ProductSpuApi.getSpuDetailList(spuIdList.value) |
| | | } |
| | | |
| | | // 更新 SPU 的最低价格 |
| | | seckillActivityList.value.forEach((activity) => { |
| | | // 匹配spuId |
| | | const spu = spuList.value.find((spu) => spu.id === activity.spuId) |
| | | if (spu) { |
| | | // 赋值活动价格,哪个最便宜就赋值哪个 |
| | | spu.price = Math.min(activity.seckillPrice || Infinity, spu.price || Infinity) |
| | | } |
| | | }) |
| | | } |
| | | } catch (error) { |
| | | console.error('获取秒杀活动细节或 SPU 细节时出错:', error) |
| | | } |
| | | }, |
| | | { |
| | | immediate: true, |
| | | deep: true |
| | | } |
| | | ) |
| | | // 手机宽度 |
| | | const phoneWidth = ref(375) |
| | | |
| | | /** |
| | | * 计算商品的间距 |
| | | * @param index 商品索引 |
| | | */ |
| | | const calculateSpace = (index: number) => { |
| | | // 商品的列数 |
| | | const columns = props.property.layoutType === 'twoCol' ? 2 : 1 |
| | | // 第一列没有左边距 |
| | | const marginLeft = index % columns === 0 ? '0' : props.property.space + 'px' |
| | | // 第一行没有上边距 |
| | | const marginTop = index < columns ? '0' : props.property.space + 'px' |
| | | |
| | | return { marginLeft, marginTop } |
| | | } |
| | | |
| | | // 容器 |
| | | const containerRef = ref() |
| | | // 商品的列数 |
| | | const columns = ref(2) |
| | | // 滚动条宽度 |
| | | const scrollbarWidth = ref('100%') |
| | | // 商品图大小 |
| | | const imageSize = ref('0') |
| | | // 商品网络列数 |
| | | const gridTemplateColumns = ref('') |
| | | // 计算布局参数 |
| | | watch( |
| | | () => [props.property, phoneWidth, spuList.value.length], |
| | | () => { |
| | | // 计算列数 |
| | | columns.value = props.property.layoutType === 'oneCol' ? 1 : 3 |
| | | // 每列的宽度为:(总宽度 - 间距 * (列数 - 1))/ 列数 |
| | | const productWidth = |
| | | (phoneWidth.value - props.property.space * (columns.value - 1)) / columns.value |
| | | // 商品图布局:2列时,左右布局 3列时,上下布局 |
| | | imageSize.value = columns.value === 2 ? '64px' : `${productWidth}px` |
| | | // 指定列数 |
| | | gridTemplateColumns.value = `repeat(${columns.value}, auto)` |
| | | // 不滚动 |
| | | scrollbarWidth.value = '100%' |
| | | }, |
| | | { immediate: true, deep: true } |
| | | ) |
| | | onMounted(() => { |
| | | // 提取手机宽度 |
| | | phoneWidth.value = containerRef.value?.wrapRef?.offsetWidth || 375 |
| | | }) |
| | | // 计算商品的宽度 |
| | | const calculateWidth = () => { |
| | | let width = '100%' |
| | | // 双列时每列的宽度为:(总宽度 - 间距)/ 2 |
| | | if (props.property.layoutType === 'twoCol') { |
| | | width = `${(containerRef.value.offsetWidth - props.property.space) / 2}px` |
| | | } |
| | | return { width } |
| | | } |
| | | </script> |
| | | |
| | | <style scoped lang="scss"></style> |
| | |
| | | <ComponentContainerProperty v-model="formData.style"> |
| | | <el-form label-width="80px" :model="formData"> |
| | | <el-card header="秒杀活动" class="property-group" shadow="never"> |
| | | <el-form-item label="秒杀活动" prop="activityId"> |
| | | <el-select v-model="formData.activityId"> |
| | | <el-option |
| | | v-for="activity in activityList" |
| | | :key="activity.id" |
| | | :label="activity.name" |
| | | :value="activity.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <SeckillShowcase v-model="formData.activityIds" /> |
| | | </el-card> |
| | | <el-card header="商品样式" class="property-group" shadow="never"> |
| | | <el-form-item label="布局" prop="type"> |
| | | <el-radio-group v-model="formData.layoutType"> |
| | | <el-tooltip class="item" content="单列" placement="bottom"> |
| | | <el-radio-button label="oneCol"> |
| | | <el-tooltip class="item" content="单列大图" placement="bottom"> |
| | | <el-radio-button value="oneColBigImg"> |
| | | <Icon icon="fluent:text-column-one-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <el-tooltip class="item" content="三列" placement="bottom"> |
| | | <el-radio-button label="threeCol"> |
| | | <Icon icon="fluent:text-column-three-24-filled" /> |
| | | <el-tooltip class="item" content="单列小图" placement="bottom"> |
| | | <el-radio-button value="oneColSmallImg"> |
| | | <Icon icon="fluent:text-column-two-left-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <el-tooltip class="item" content="双列" placement="bottom"> |
| | | <el-radio-button value="twoCol"> |
| | | <Icon icon="fluent:text-column-two-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <!--<el-tooltip class="item" content="三列" placement="bottom"> |
| | | <el-radio-button value="threeCol"> |
| | | <Icon icon="fluent:text-column-three-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip>--> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="商品名称" prop="fields.name.show"> |
| | |
| | | <el-checkbox v-model="formData.fields.name.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="商品简介" prop="fields.introduction.show"> |
| | | <div class="flex gap-8px"> |
| | | <ColorInput v-model="formData.fields.introduction.color" /> |
| | | <el-checkbox v-model="formData.fields.introduction.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="商品价格" prop="fields.price.show"> |
| | | <div class="flex gap-8px"> |
| | | <ColorInput v-model="formData.fields.price.color" /> |
| | | <el-checkbox v-model="formData.fields.price.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="市场价" prop="fields.marketPrice.show"> |
| | | <div class="flex gap-8px"> |
| | | <ColorInput v-model="formData.fields.marketPrice.color" /> |
| | | <el-checkbox v-model="formData.fields.marketPrice.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="商品销量" prop="fields.salesCount.show"> |
| | | <div class="flex gap-8px"> |
| | | <ColorInput v-model="formData.fields.salesCount.color" /> |
| | | <el-checkbox v-model="formData.fields.salesCount.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="商品库存" prop="fields.stock.show"> |
| | | <div class="flex gap-8px"> |
| | | <ColorInput v-model="formData.fields.stock.color" /> |
| | | <el-checkbox v-model="formData.fields.stock.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | </el-card> |
| | |
| | | </el-form-item> |
| | | <el-form-item label="角标" prop="badge.imgUrl" v-if="formData.badge.show"> |
| | | <UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px"> |
| | | <template #tip> 建议尺寸:36 * 22 </template> |
| | | <template #tip> 建议尺寸:36 * 22</template> |
| | | </UploadImg> |
| | | </el-form-item> |
| | | </el-card> |
| | | <el-card header="按钮" class="property-group" shadow="never"> |
| | | <el-form-item label="按钮类型" prop="btnBuy.type"> |
| | | <el-radio-group v-model="formData.btnBuy.type"> |
| | | <el-radio-button value="text">文字</el-radio-button> |
| | | <el-radio-button value="img">图片</el-radio-button> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <template v-if="formData.btnBuy.type === 'text'"> |
| | | <el-form-item label="按钮文字" prop="btnBuy.text"> |
| | | <el-input v-model="formData.btnBuy.text" /> |
| | | </el-form-item> |
| | | <el-form-item label="左侧背景" prop="btnBuy.bgBeginColor"> |
| | | <ColorInput v-model="formData.btnBuy.bgBeginColor" /> |
| | | </el-form-item> |
| | | <el-form-item label="右侧背景" prop="btnBuy.bgEndColor"> |
| | | <ColorInput v-model="formData.btnBuy.bgEndColor" /> |
| | | </el-form-item> |
| | | </template> |
| | | <template v-else> |
| | | <el-form-item label="图片" prop="btnBuy.imgUrl"> |
| | | <UploadImg v-model="formData.btnBuy.imgUrl" height="56px" width="56px"> |
| | | <template #tip> 建议尺寸:56 * 56</template> |
| | | </UploadImg> |
| | | </el-form-item> |
| | | </template> |
| | | </el-card> |
| | | <el-card header="商品样式" class="property-group" shadow="never"> |
| | | <el-form-item label="上圆角" prop="borderRadiusTop"> |
| | |
| | | import { usePropertyForm } from '@/components/DiyEditor/util' |
| | | import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity' |
| | | import { CommonStatusEnum } from '@/utils/constants' |
| | | import SeckillShowcase from '@/views/mall/promotion/seckill/components/SeckillShowcase.vue' |
| | | |
| | | // 秒杀属性面板 |
| | | defineOptions({ name: 'PromotionSeckillProperty' }) |
| | |
| | | const emit = defineEmits(['update:modelValue']) |
| | | const { formData } = usePropertyForm(props.modelValue, emit) |
| | | // 活动列表 |
| | | const activityList = ref<SeckillActivityApi.SeckillActivityVO>([]) |
| | | const activityList = ref<SeckillActivityApi.SeckillActivityVO[]>([]) |
| | | onMounted(async () => { |
| | | const { list } = await SeckillActivityApi.getSeckillActivityPage({ |
| | | status: CommonStatusEnum.ENABLE |
| | |
| | | <el-form-item label="框体样式"> |
| | | <el-radio-group v-model="formData!.borderRadius"> |
| | | <el-tooltip content="方形" placement="top"> |
| | | <el-radio-button :label="0"> |
| | | <el-radio-button :value="0"> |
| | | <Icon icon="tabler:input-search" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <el-tooltip content="圆形" placement="top"> |
| | | <el-radio-button :label="10"> |
| | | <el-radio-button :value="10"> |
| | | <Icon icon="iconoir:input-search" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | |
| | | <el-form-item label="文本位置" prop="placeholderPosition"> |
| | | <el-radio-group v-model="formData!.placeholderPosition"> |
| | | <el-tooltip content="居左" placement="top"> |
| | | <el-radio-button label="left"> |
| | | <el-radio-button value="left"> |
| | | <Icon icon="ant-design:align-left-outlined" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <el-tooltip content="居中" placement="top"> |
| | | <el-radio-button label="center"> |
| | | <el-radio-button value="center"> |
| | | <Icon icon="ant-design:align-center-outlined" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | |
| | | { |
| | | text: '首页', |
| | | url: '/pages/index/index', |
| | | iconUrl: 'http://xxxx/static/images/1-001.png', |
| | | activeIconUrl: 'http://xxxx/static/images/1-002.png' |
| | | iconUrl: 'http://mall.yudao.iocoder.cn/static/images/1-001.png', |
| | | activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/1-002.png' |
| | | }, |
| | | { |
| | | text: '分类', |
| | | url: '/pages/index/category?id=3', |
| | | iconUrl: 'http://xxxx/static/images/2-001.png', |
| | | activeIconUrl: 'http://xxxx/static/images/2-002.png' |
| | | iconUrl: 'http://mall.yudao.iocoder.cn/static/images/2-001.png', |
| | | activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/2-002.png' |
| | | }, |
| | | { |
| | | text: '购物车', |
| | | url: '/pages/index/cart', |
| | | iconUrl: 'http://xxxx/static/images/3-001.png', |
| | | activeIconUrl: 'http://xxxx/static/images/3-002.png' |
| | | iconUrl: 'http://mall.yudao.iocoder.cn/static/images/3-001.png', |
| | | activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/3-002.png' |
| | | }, |
| | | { |
| | | text: '我的', |
| | | url: '/pages/index/user', |
| | | iconUrl: 'http://xxxx/static/images/4-001.png', |
| | | activeIconUrl: 'http://xxxx/static/images/4-002.png' |
| | | iconUrl: 'http://mall.yudao.iocoder.cn/static/images/4-001.png', |
| | | activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/4-002.png' |
| | | } |
| | | ] |
| | | } |
| | |
| | | </el-form-item> |
| | | <el-form-item label="导航背景"> |
| | | <el-radio-group v-model="formData!.style.bgType"> |
| | | <el-radio-button label="color">纯色</el-radio-button> |
| | | <el-radio-button label="img">图片</el-radio-button> |
| | | <el-radio-button value="color">纯色</el-radio-button> |
| | | <el-radio-button value="img">图片</el-radio-button> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="选择颜色" v-if="formData!.style.bgType === 'color'"> |
| | |
| | | </template> |
| | | |
| | | <script setup lang="ts"> |
| | | import { TabBarProperty, THEME_LIST } from './config' |
| | | import { TabBarProperty, component, THEME_LIST } from './config' |
| | | import { usePropertyForm } from '@/components/DiyEditor/util' |
| | | // 底部导航栏 |
| | | defineOptions({ name: 'TabBarProperty' }) |
| | |
| | | const emit = defineEmits(['update:modelValue']) |
| | | const { formData } = usePropertyForm(props.modelValue, emit) |
| | | |
| | | // 将数据库的值更新到右侧属性栏 |
| | | component.property.items = formData.value.items |
| | | |
| | | // 要的主题 |
| | | const handleThemeChange = () => { |
| | | const theme = THEME_LIST.find((theme) => theme.id === formData.value.theme) |
| | |
| | | <el-form-item label="标题位置" prop="textAlign"> |
| | | <el-radio-group v-model="formData!.textAlign"> |
| | | <el-tooltip content="居左" placement="top"> |
| | | <el-radio-button label="left"> |
| | | <el-radio-button value="left"> |
| | | <Icon icon="ant-design:align-left-outlined" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <el-tooltip content="居中" placement="top"> |
| | | <el-radio-button label="center"> |
| | | <el-radio-button value="center"> |
| | | <Icon icon="ant-design:align-center-outlined" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | |
| | | <template v-if="formData.more.show"> |
| | | <el-form-item label="样式" prop="more.type"> |
| | | <el-radio-group v-model="formData.more.type"> |
| | | <el-radio label="text">文字</el-radio> |
| | | <el-radio label="icon">图标</el-radio> |
| | | <el-radio label="all">文字+图标</el-radio> |
| | | <el-radio value="text">文字</el-radio> |
| | | <el-radio value="icon">图标</el-radio> |
| | | <el-radio value="all">文字+图标</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="更多文字" prop="more.text" v-show="formData.more.type !== 'icon'"> |
| | |
| | | <el-avatar :size="60"> |
| | | <Icon icon="ep:avatar" :size="60" /> |
| | | </el-avatar> |
| | | <span class="text-18px font-bold">工业互联网平台</span> |
| | | <span class="text-18px font-bold">芋道源码</span> |
| | | </div> |
| | | <Icon icon="tdesign:qrcode" :size="20" /> |
| | | </div> |
| | |
| | | class="mb-4px flex flex-col gap-4px border border-gray-2 border-rounded rounded border-solid p-8px" |
| | | > |
| | | <!-- 操作按钮区 --> |
| | | <div class="m--8px m-b-4px flex flex-row items-center justify-between bg-gray-1 p-8px"> |
| | | <div class="m--8px m-b-4px flex flex-row items-center justify-between p-8px" style="background-color: var(--app-content-bg-color);"> |
| | | <el-tooltip content="拖动排序"> |
| | | <Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" /> |
| | | <Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" style="color: #8a909c;" /> |
| | | </el-tooltip> |
| | | <el-tooltip content="删除"> |
| | | <Icon |
| | |
| | | import { isString } from '@/utils/is' |
| | | import { useDesign } from '@/hooks/web/useDesign' |
| | | |
| | | import 'echarts/lib/component/markPoint' |
| | | import 'echarts/lib/component/markLine' |
| | | import 'echarts/lib/component/markArea' |
| | | |
| | | defineOptions({ name: 'EChart' }) |
| | | |
| | | const { getPrefixCls, variables } = useDesign() |
| | |
| | | |
| | | contentEl.value = document.getElementsByClassName(`${variables.namespace}-layout-content`)[0] |
| | | unref(contentEl) && |
| | | (unref(contentEl) as Element).addEventListener('transitionend', contentResizeHandler) |
| | | (unref(contentEl) as Element).addEventListener('transitionend', contentResizeHandler) |
| | | }) |
| | | |
| | | onBeforeUnmount(() => { |
| | | window.removeEventListener('resize', resizeHandler) |
| | | unref(contentEl) && |
| | | (unref(contentEl) as Element).removeEventListener('transitionend', contentResizeHandler) |
| | | (unref(contentEl) as Element).removeEventListener('transitionend', contentResizeHandler) |
| | | }) |
| | | |
| | | onActivated(() => { |
| | |
| | | import { ElMessage } from 'element-plus' |
| | | import { useLocaleStore } from '@/store/modules/locale' |
| | | import { getAccessToken, getTenantId } from '@/utils/auth' |
| | | import { getUploadUrl } from '@/components/UploadFile/src/useUpload' |
| | | |
| | | defineOptions({ name: 'Editor' }) |
| | | |
| | |
| | | scroll: true, |
| | | MENU_CONF: { |
| | | ['uploadImage']: { |
| | | server: import.meta.env.VITE_UPLOAD_URL, |
| | | server: getUploadUrl(), |
| | | // 单个文件的最大体积限制,默认为 2M |
| | | maxFileSize: 5 * 1024 * 1024, |
| | | // 最多可上传几个文件,默认为 100 |
| | |
| | | } |
| | | }, |
| | | ['uploadVideo']: { |
| | | server: import.meta.env.VITE_UPLOAD_URL, |
| | | server: getUploadUrl(), |
| | | // 单个文件的最大体积限制,默认为 10M |
| | | maxFileSize: 10 * 1024 * 1024, |
| | | // 最多可上传几个文件,默认为 100 |
| | |
| | | parseOptions0(data) |
| | | return |
| | | } |
| | | // 情况三:不是 iailab-plat 标准返回 |
| | | // 情况三:不是 yudao-vue-pro 标准返回 |
| | | console.warn( |
| | | `接口[${props.url}] 返回结果不是 iailab-plat 标准返回建议采用自定义解析函数处理` |
| | | `接口[${props.url}] 返回结果不是 yudao-vue-pro 标准返回建议采用自定义解析函数处理` |
| | | ) |
| | | } |
| | | |
| | |
| | | </el-select> |
| | | ) |
| | | } |
| | | // debugger |
| | | return ( |
| | | <el-select |
| | | class="w-1/1" |
| | |
| | | }, |
| | | { |
| | | type: 'select', |
| | | field: 'dictValueType', |
| | | field: 'valueType', |
| | | title: '字典值类型', |
| | | value: 'str', |
| | | options: [ |
| | |
| | | return rule |
| | | }) |
| | | } |
| | | |
| | | /** |
| | | * 解析表单组件的 field, title 等字段(递归,如果组件包含子组件) |
| | | * |
| | | * @param rule 组件的生成规则 https://www.form-create.com/v3/guide/rule |
| | | * @param fields 解析后表单组件字段 |
| | | * @param parentTitle 如果是子表单,子表单的标题,默认为空 |
| | | */ |
| | | export const parseFormFields = ( |
| | | rule: Record<string, any>, |
| | | fields: Array<Record<string, any>> = [], |
| | | parentTitle: string = '' |
| | | ) => { |
| | | const { type, field, $required, title: tempTitle, children } = rule |
| | | if (field && tempTitle) { |
| | | let title = tempTitle |
| | | if (parentTitle) { |
| | | title = `${parentTitle}.${tempTitle}` |
| | | } |
| | | let required = false |
| | | if ($required) { |
| | | required = true |
| | | } |
| | | fields.push({ |
| | | field, |
| | | title, |
| | | type, |
| | | required |
| | | }) |
| | | // TODO 子表单 需要处理子表单字段 |
| | | // if (type === 'group' && rule.props?.rule && Array.isArray(rule.props.rule)) { |
| | | // // 解析子表单的字段 |
| | | // rule.props.rule.forEach((item) => { |
| | | // parseFields(item, fieldsPermission, title) |
| | | // }) |
| | | // } |
| | | } |
| | | if (children && Array.isArray(children)) { |
| | | children.forEach((rule) => { |
| | | parseFormFields(rule, fields) |
| | | }) |
| | | } |
| | | } |
| | |
| | | src: propTypes.string.def('') |
| | | }) |
| | | const loading = ref(true) |
| | | const height = ref('') |
| | | const frameRef = ref<HTMLElement | null>(null) |
| | | const init = () => { |
| | | height.value = document.documentElement.clientHeight - 94.5 + 'px' |
| | | loading.value = false |
| | | nextTick(() => { |
| | | loading.value = true |
| | | if (!frameRef.value) return |
| | | frameRef.value.onload = () => { |
| | | loading.value = false |
| | | } |
| | | }) |
| | | } |
| | | onMounted(() => { |
| | | setTimeout(() => { |
| | | init() |
| | | }, 300) |
| | | init() |
| | | }) |
| | | watch( |
| | | () => props.src, |
| | | () => { |
| | | init() |
| | | } |
| | | ) |
| | | </script> |
| | | <template> |
| | | <div v-loading="loading" :style="'height:' + height"> |
| | | <div |
| | | v-loading="loading" |
| | | class="w-full h-[calc(100vh-var(--top-tool-height)-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-2px)]" |
| | | > |
| | | <iframe |
| | | ref="frameRef" |
| | | :src="props.src" |
| | | frameborder="no" |
| | | frameborder="0" |
| | | scrolling="auto" |
| | | style="width: 100%; height: 100%" |
| | | height="100%" |
| | | width="100%" |
| | | allowfullscreen="true" |
| | | webkitallowfullscreen="true" |
| | | mozallowfullscreen="true" |
| | | ></iframe> |
| | | </div> |
| | | </template> |
| | |
| | | modelValue: { |
| | | require: false, |
| | | type: String |
| | | }, |
| | | clearable: { |
| | | require: false, |
| | | type: Boolean |
| | | } |
| | | }) |
| | | const emit = defineEmits<{ (e: 'update:modelValue', v: string) }>() |
| | |
| | | currentPage.value = page |
| | | } |
| | | |
| | | function clearIcon() { |
| | | icon.value = '' |
| | | emit('update:modelValue', '') |
| | | visible.value = false |
| | | } |
| | | |
| | | watch( |
| | | () => { |
| | | return props.modelValue |
| | |
| | | |
| | | <template> |
| | | <div class="selector"> |
| | | <ElInput v-model="inputValue" @click="visible = !visible"> |
| | | <ElInput v-model="inputValue" @click="visible = !visible" :clearable="props.clearable" @clear="clearIcon"> |
| | | <template #append> |
| | | <ElPopover |
| | | :popper-options="{ |
| | | placement: 'auto' |
| | | }" |
| | | :visible="visible" |
| | | :width="350" |
| | | :width="355" |
| | | popper-class="pure-popper" |
| | | trigger="click" |
| | | > |
| | |
| | | > |
| | | <ElDivider border-style="dashed" class="tab-divider" /> |
| | | <ElScrollbar height="220px"> |
| | | <ul class="ml-2 flex flex-wrap px-2"> |
| | | <ul class="ml-2 flex flex-wrap"> |
| | | <li |
| | | v-for="(item, key) in pageList" |
| | | :key="key" |
| | |
| | | background |
| | | class="h-10 flex items-center justify-center" |
| | | layout="prev, pager, next" |
| | | small |
| | | size="small" |
| | | @current-change="onCurrentChange" |
| | | /> |
| | | </ElPopover> |
| | |
| | | <div v-else class="custom-hover" @click.stop="showTopSearch = !showTopSearch"> |
| | | <Icon icon="ep:search" /> |
| | | <el-select |
| | | @click.stop |
| | | filterable |
| | | :reserve-keyword="false" |
| | | remote |
| | |
| | | |
| | | function handleChange(path) { |
| | | router.push({ path }) |
| | | hiddenSearch() |
| | | hiddenTopSearch() |
| | | } |
| | | |
| | | function hiddenSearch() { |
| | | showSearch.value = false |
| | | } |
| | | |
| | | function hiddenTopSearch() { |
| | |
| | | // 监听 ctrl + k |
| | | function listenKey(event) { |
| | | if ((event.ctrlKey || event.metaKey) && event.key === 'k') { |
| | | // 阻止触发浏览器默认事件 |
| | | event.preventDefault() |
| | | showSearch.value = !showSearch.value |
| | | // 这里可以执行相应的操作(例如打开搜索框等) |
| | | } |
| | |
| | | <template> |
| | | <div class="flex flex-row items-center gap-2"> |
| | | <el-radio-group v-model="shortcutDays" @change="handleShortcutDaysChange"> |
| | | <el-radio-button :label="1">昨天</el-radio-button> |
| | | <el-radio-button :label="7">最近7天</el-radio-button> |
| | | <el-radio-button :label="30">最近30天</el-radio-button> |
| | | <el-radio-button :value="1">昨天</el-radio-button> |
| | | <el-radio-button :value="7">最近7天</el-radio-button> |
| | | <el-radio-button :value="30">最近30天</el-radio-button> |
| | | </el-radio-group> |
| | | <el-date-picker |
| | | v-model="times" |
对比新文件 |
| | |
| | | <template> |
| | | <div class="node-handler-wrapper"> |
| | | <div class="node-handler"> |
| | | <el-popover |
| | | trigger="hover" |
| | | v-model:visible="popoverShow" |
| | | placement="right-start" |
| | | width="auto" |
| | | v-if="!readonly" |
| | | > |
| | | <div class="handler-item-wrapper"> |
| | | <div class="handler-item" @click="addNode(NodeType.USER_TASK_NODE)"> |
| | | <div class="approve handler-item-icon"> |
| | | <span class="iconfont icon-approve icon-size"></span> |
| | | </div> |
| | | <div class="handler-item-text">审批人</div> |
| | | </div> |
| | | <div class="handler-item" @click="addNode(NodeType.COPY_TASK_NODE)"> |
| | | <div class="handler-item-icon copy"> |
| | | <span class="iconfont icon-size icon-copy"></span> |
| | | </div> |
| | | <div class="handler-item-text">抄送</div> |
| | | </div> |
| | | <div class="handler-item" @click="addNode(NodeType.CONDITION_BRANCH_NODE)"> |
| | | <div class="handler-item-icon condition"> |
| | | <span class="iconfont icon-size icon-exclusive"></span> |
| | | </div> |
| | | <div class="handler-item-text">条件分支</div> |
| | | </div> |
| | | <div class="handler-item" @click="addNode(NodeType.PARALLEL_BRANCH_NODE)"> |
| | | <div class="handler-item-icon parallel"> |
| | | <span class="iconfont icon-size icon-parallel"></span> |
| | | </div> |
| | | <div class="handler-item-text">并行分支</div> |
| | | </div> |
| | | <div class="handler-item" @click="addNode(NodeType.INCLUSIVE_BRANCH_NODE)"> |
| | | <div class="handler-item-icon inclusive"> |
| | | <span class="iconfont icon-size icon-inclusive"></span> |
| | | </div> |
| | | <div class="handler-item-text">包容分支</div> |
| | | </div> |
| | | <div class="handler-item" @click="addNode(NodeType.DELAY_TIMER_NODE)"> |
| | | <!-- TODO @芋艿 需要更换一下iconfont的图标 --> |
| | | <div class="handler-item-icon copy"> |
| | | <span class="iconfont icon-size icon-copy"></span> |
| | | </div> |
| | | <div class="handler-item-text">延迟器</div> |
| | | </div> |
| | | </div> |
| | | <template #reference> |
| | | <div class="add-icon"><Icon icon="ep:plus" /></div> |
| | | </template> |
| | | </el-popover> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup lang="ts"> |
| | | import { |
| | | ApproveMethodType, |
| | | AssignEmptyHandlerType, |
| | | AssignStartUserHandlerType, |
| | | NODE_DEFAULT_NAME, |
| | | NodeType, |
| | | RejectHandlerType, |
| | | SimpleFlowNode |
| | | } from './consts' |
| | | import { generateUUID } from '@/utils' |
| | | |
| | | defineOptions({ |
| | | name: 'NodeHandler' |
| | | }) |
| | | |
| | | const message = useMessage() // 消息弹窗 |
| | | |
| | | const popoverShow = ref(false) |
| | | const props = defineProps({ |
| | | childNode: { |
| | | type: Object as () => SimpleFlowNode, |
| | | default: null |
| | | }, |
| | | currentNode: { |
| | | type: Object as () => SimpleFlowNode, |
| | | required: true |
| | | } |
| | | }) |
| | | const emits = defineEmits(['update:childNode']) |
| | | |
| | | const readonly = inject<Boolean>('readonly') // 是否只读 |
| | | |
| | | const addNode = (type: number) => { |
| | | // 校验:条件分支、包容分支后面,不允许直接添加并行分支 |
| | | if ( |
| | | type === NodeType.PARALLEL_BRANCH_NODE && |
| | | [NodeType.CONDITION_BRANCH_NODE, NodeType.INCLUSIVE_BRANCH_NODE].includes( |
| | | props.currentNode?.type |
| | | ) |
| | | ) { |
| | | message.error('条件分支、包容分支后面,不允许直接添加并行分支') |
| | | return |
| | | } |
| | | |
| | | popoverShow.value = false |
| | | if (type === NodeType.USER_TASK_NODE) { |
| | | const id = 'Activity_' + generateUUID() |
| | | const data: SimpleFlowNode = { |
| | | id: id, |
| | | name: NODE_DEFAULT_NAME.get(NodeType.USER_TASK_NODE) as string, |
| | | showText: '', |
| | | type: NodeType.USER_TASK_NODE, |
| | | approveMethod: ApproveMethodType.SEQUENTIAL_APPROVE, |
| | | // 超时处理 |
| | | rejectHandler: { |
| | | type: RejectHandlerType.FINISH_PROCESS |
| | | }, |
| | | timeoutHandler: { |
| | | enable: false |
| | | }, |
| | | assignEmptyHandler: { |
| | | type: AssignEmptyHandlerType.APPROVE |
| | | }, |
| | | assignStartUserHandlerType: AssignStartUserHandlerType.START_USER_AUDIT, |
| | | childNode: props.childNode |
| | | } |
| | | emits('update:childNode', data) |
| | | } |
| | | if (type === NodeType.COPY_TASK_NODE) { |
| | | const data: SimpleFlowNode = { |
| | | id: 'Activity_' + generateUUID(), |
| | | name: NODE_DEFAULT_NAME.get(NodeType.COPY_TASK_NODE) as string, |
| | | showText: '', |
| | | type: NodeType.COPY_TASK_NODE, |
| | | childNode: props.childNode |
| | | } |
| | | emits('update:childNode', data) |
| | | } |
| | | if (type === NodeType.CONDITION_BRANCH_NODE) { |
| | | const data: SimpleFlowNode = { |
| | | name: '条件分支', |
| | | type: NodeType.CONDITION_BRANCH_NODE, |
| | | id: 'GateWay_' + generateUUID(), |
| | | childNode: props.childNode, |
| | | conditionNodes: [ |
| | | { |
| | | id: 'Flow_' + generateUUID(), |
| | | name: '条件1', |
| | | showText: '', |
| | | type: NodeType.CONDITION_NODE, |
| | | childNode: undefined, |
| | | conditionType: 1, |
| | | defaultFlow: false |
| | | }, |
| | | { |
| | | id: 'Flow_' + generateUUID(), |
| | | name: '其它情况', |
| | | showText: '未满足其它条件时,将进入此分支', |
| | | type: NodeType.CONDITION_NODE, |
| | | childNode: undefined, |
| | | conditionType: undefined, |
| | | defaultFlow: true |
| | | } |
| | | ] |
| | | } |
| | | emits('update:childNode', data) |
| | | } |
| | | if (type === NodeType.PARALLEL_BRANCH_NODE) { |
| | | const data: SimpleFlowNode = { |
| | | name: '并行分支', |
| | | type: NodeType.PARALLEL_BRANCH_NODE, |
| | | id: 'GateWay_' + generateUUID(), |
| | | childNode: props.childNode, |
| | | conditionNodes: [ |
| | | { |
| | | id: 'Flow_' + generateUUID(), |
| | | name: '并行1', |
| | | showText: '无需配置条件同时执行', |
| | | type: NodeType.CONDITION_NODE, |
| | | childNode: undefined |
| | | }, |
| | | { |
| | | id: 'Flow_' + generateUUID(), |
| | | name: '并行2', |
| | | showText: '无需配置条件同时执行', |
| | | type: NodeType.CONDITION_NODE, |
| | | childNode: undefined |
| | | } |
| | | ] |
| | | } |
| | | emits('update:childNode', data) |
| | | } |
| | | if (type === NodeType.INCLUSIVE_BRANCH_NODE) { |
| | | const data: SimpleFlowNode = { |
| | | name: '包容分支', |
| | | type: NodeType.INCLUSIVE_BRANCH_NODE, |
| | | id: 'GateWay_' + generateUUID(), |
| | | childNode: props.childNode, |
| | | conditionNodes: [ |
| | | { |
| | | id: 'Flow_' + generateUUID(), |
| | | name: '包容条件1', |
| | | showText: '', |
| | | type: NodeType.CONDITION_NODE, |
| | | childNode: undefined, |
| | | defaultFlow: false |
| | | }, |
| | | { |
| | | id: 'Flow_' + generateUUID(), |
| | | name: '其它情况', |
| | | showText: '未满足其它条件时,将进入此分支', |
| | | type: NodeType.CONDITION_NODE, |
| | | childNode: undefined, |
| | | defaultFlow: true |
| | | } |
| | | ] |
| | | } |
| | | emits('update:childNode', data) |
| | | } |
| | | if (type === NodeType.DELAY_TIMER_NODE) { |
| | | const data: SimpleFlowNode = { |
| | | id: 'Activity_' + generateUUID(), |
| | | name: NODE_DEFAULT_NAME.get(NodeType.DELAY_TIMER_NODE) as string, |
| | | showText: '', |
| | | type: NodeType.DELAY_TIMER_NODE, |
| | | childNode: props.childNode |
| | | } |
| | | emits('update:childNode', data) |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <style lang="scss" scoped></style> |
对比新文件 |
| | |
| | | <template> |
| | | <!-- 发起人节点 --> |
| | | <StartUserNode |
| | | v-if="currentNode && currentNode.type === NodeType.START_USER_NODE" |
| | | :flow-node="currentNode" |
| | | /> |
| | | <!-- 审批节点 --> |
| | | <UserTaskNode |
| | | v-if="currentNode && currentNode.type === NodeType.USER_TASK_NODE" |
| | | :flow-node="currentNode" |
| | | @update:flow-node="handleModelValueUpdate" |
| | | @find:parent-node="findFromParentNode" |
| | | /> |
| | | <!-- 抄送节点 --> |
| | | <CopyTaskNode |
| | | v-if="currentNode && currentNode.type === NodeType.COPY_TASK_NODE" |
| | | :flow-node="currentNode" |
| | | @update:flow-node="handleModelValueUpdate" |
| | | /> |
| | | <!-- 条件节点 --> |
| | | <ExclusiveNode |
| | | v-if="currentNode && currentNode.type === NodeType.CONDITION_BRANCH_NODE" |
| | | :flow-node="currentNode" |
| | | @update:model-value="handleModelValueUpdate" |
| | | @find:parent-node="findFromParentNode" |
| | | /> |
| | | <!-- 并行节点 --> |
| | | <ParallelNode |
| | | v-if="currentNode && currentNode.type === NodeType.PARALLEL_BRANCH_NODE" |
| | | :flow-node="currentNode" |
| | | @update:model-value="handleModelValueUpdate" |
| | | @find:parent-node="findFromParentNode" |
| | | /> |
| | | <!-- 包容分支节点 --> |
| | | <InclusiveNode |
| | | v-if="currentNode && currentNode.type === NodeType.INCLUSIVE_BRANCH_NODE" |
| | | :flow-node="currentNode" |
| | | @update:model-value="handleModelValueUpdate" |
| | | @find:parent-node="findFromParentNode" |
| | | /> |
| | | <!-- 延迟器节点 --> |
| | | <DelayTimerNode |
| | | v-if="currentNode && currentNode.type === NodeType.DELAY_TIMER_NODE" |
| | | :flow-node="currentNode" |
| | | @update:flow-node="handleModelValueUpdate" |
| | | /> |
| | | <!-- 递归显示孩子节点 --> |
| | | <ProcessNodeTree |
| | | v-if="currentNode && currentNode.childNode" |
| | | v-model:flow-node="currentNode.childNode" |
| | | :parent-node="currentNode" |
| | | @find:recursive-find-parent-node="recursiveFindParentNode" |
| | | /> |
| | | |
| | | <!-- 结束节点 --> |
| | | <EndEventNode |
| | | v-if="currentNode && currentNode.type === NodeType.END_EVENT_NODE" |
| | | :flow-node="currentNode" |
| | | /> |
| | | </template> |
| | | <script setup lang="ts"> |
| | | import StartUserNode from './nodes/StartUserNode.vue' |
| | | import EndEventNode from './nodes/EndEventNode.vue' |
| | | import UserTaskNode from './nodes/UserTaskNode.vue' |
| | | import CopyTaskNode from './nodes/CopyTaskNode.vue' |
| | | import ExclusiveNode from './nodes/ExclusiveNode.vue' |
| | | import ParallelNode from './nodes/ParallelNode.vue' |
| | | import InclusiveNode from './nodes/InclusiveNode.vue' |
| | | import DelayTimerNode from './nodes/DelayTimerNode.vue' |
| | | import { SimpleFlowNode, NodeType } from './consts' |
| | | import { useWatchNode } from './node' |
| | | defineOptions({ |
| | | name: 'ProcessNodeTree' |
| | | }) |
| | | const props = defineProps({ |
| | | parentNode: { |
| | | type: Object as () => SimpleFlowNode, |
| | | default: () => null |
| | | }, |
| | | flowNode: { |
| | | type: Object as () => SimpleFlowNode, |
| | | default: () => null |
| | | } |
| | | }) |
| | | const emits = defineEmits<{ |
| | | 'update:flowNode': [node: SimpleFlowNode | undefined] |
| | | 'find:recursiveFindParentNode': [ |
| | | nodeList: SimpleFlowNode[], |
| | | curentNode: SimpleFlowNode, |
| | | nodeType: number |
| | | ] |
| | | }>() |
| | | |
| | | const currentNode = useWatchNode(props) |
| | | |
| | | // 用于删除节点 |
| | | const handleModelValueUpdate = (updateValue) => { |
| | | emits('update:flowNode', updateValue) |
| | | } |
| | | |
| | | const findFromParentNode = (nodeList: SimpleFlowNode[], nodeType: number) => { |
| | | emits('find:recursiveFindParentNode', nodeList, props.parentNode, nodeType) |
| | | } |
| | | |
| | | // 递归从父节点中查询匹配的节点 |
| | | const recursiveFindParentNode = ( |
| | | nodeList: SimpleFlowNode[], |
| | | findNode: SimpleFlowNode, |
| | | nodeType: number |
| | | ) => { |
| | | if (!findNode) { |
| | | return |
| | | } |
| | | if (findNode.type === NodeType.START_USER_NODE) { |
| | | nodeList.push(findNode) |
| | | return |
| | | } |
| | | |
| | | if (findNode.type === nodeType) { |
| | | nodeList.push(findNode) |
| | | } |
| | | emits('find:recursiveFindParentNode', nodeList, props.parentNode, nodeType) |
| | | } |
| | | </script> |
| | | <style lang="scss" scoped></style> |
对比新文件 |
| | |
| | | <template> |
| | | <div v-loading="loading" class="overflow-auto"> |
| | | <SimpleProcessModel |
| | | ref="simpleProcessModelRef" |
| | | v-if="processNodeTree" |
| | | :flow-node="processNodeTree" |
| | | :readonly="false" |
| | | @save="saveSimpleFlowModel" |
| | | /> |
| | | <Dialog v-model="errorDialogVisible" title="保存失败" width="400" :fullscreen="false"> |
| | | <div class="mb-2">以下节点内容不完善,请修改后保存</div> |
| | | <div |
| | | class="mb-3 b-rounded-1 bg-gray-100 p-2 line-height-normal" |
| | | v-for="(item, index) in errorNodes" |
| | | :key="index" |
| | | > |
| | | {{ item.name }} : {{ NODE_DEFAULT_TEXT.get(item.type) }} |
| | | </div> |
| | | <template #footer> |
| | | <el-button type="primary" @click="errorDialogVisible = false">知道了</el-button> |
| | | </template> |
| | | </Dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup lang="ts"> |
| | | import SimpleProcessModel from './SimpleProcessModel.vue' |
| | | import { updateBpmSimpleModel, getBpmSimpleModel } from '@/api/bpm/simple' |
| | | import { SimpleFlowNode, NodeType, NodeId, NODE_DEFAULT_TEXT } from './consts' |
| | | import { getModel } from '@/api/bpm/model' |
| | | import { getForm, FormVO } from '@/api/bpm/form' |
| | | import { handleTree } from '@/utils/tree' |
| | | import * as RoleApi from '@/api/system/role' |
| | | import * as DeptApi from '@/api/system/dept' |
| | | import * as PostApi from '@/api/system/post' |
| | | import * as UserApi from '@/api/system/user' |
| | | import * as UserGroupApi from '@/api/bpm/userGroup' |
| | | |
| | | defineOptions({ |
| | | name: 'SimpleProcessDesigner' |
| | | }) |
| | | |
| | | const emits = defineEmits(['success', 'init-finished']) // 保存成功事件 |
| | | |
| | | const props = defineProps({ |
| | | modelId: { |
| | | type: String, |
| | | required: false |
| | | }, |
| | | modelKey: { |
| | | type: String, |
| | | required: false |
| | | }, |
| | | modelName: { |
| | | type: String, |
| | | required: false |
| | | }, |
| | | // 可发起流程的人员编号 |
| | | startUserIds : { |
| | | type: Array, |
| | | required: false |
| | | }, |
| | | value: { |
| | | type: [String, Object], |
| | | required: false |
| | | } |
| | | }) |
| | | |
| | | const loading = ref(false) |
| | | const formFields = ref<string[]>([]) |
| | | const formType = ref(20) |
| | | const roleOptions = ref<RoleApi.RoleVO[]>([]) // 角色列表 |
| | | const postOptions = ref<PostApi.PostVO[]>([]) // 岗位列表 |
| | | const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表 |
| | | const deptOptions = ref<DeptApi.DeptVO[]>([]) // 部门列表 |
| | | const deptTreeOptions = ref() |
| | | const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表 |
| | | |
| | | // 添加当前值的引用 |
| | | const currentValue = ref<SimpleFlowNode | undefined>() |
| | | |
| | | provide('formFields', formFields) |
| | | provide('formType', formType) |
| | | provide('roleList', roleOptions) |
| | | provide('postList', postOptions) |
| | | provide('userList', userOptions) |
| | | provide('deptList', deptOptions) |
| | | provide('userGroupList', userGroupOptions) |
| | | provide('deptTree', deptTreeOptions) |
| | | provide('startUserIds', props.startUserIds) |
| | | |
| | | const message = useMessage() // 国际化 |
| | | const processNodeTree = ref<SimpleFlowNode | undefined>() |
| | | const errorDialogVisible = ref(false) |
| | | let errorNodes: SimpleFlowNode[] = [] |
| | | |
| | | // 添加更新模型的方法 |
| | | const updateModel = () => { |
| | | if (!processNodeTree.value) { |
| | | processNodeTree.value = { |
| | | name: '发起人', |
| | | type: NodeType.START_USER_NODE, |
| | | id: NodeId.START_USER_NODE_ID, |
| | | childNode: { |
| | | id: NodeId.END_EVENT_NODE_ID, |
| | | name: '结束', |
| | | type: NodeType.END_EVENT_NODE |
| | | } |
| | | } |
| | | // 初始化时也触发一次保存 |
| | | saveSimpleFlowModel(processNodeTree.value) |
| | | } |
| | | } |
| | | |
| | | // 加载流程数据 |
| | | const loadProcessData = async (data: any) => { |
| | | try { |
| | | if (data) { |
| | | const parsedData = typeof data === 'string' ? JSON.parse(data) : data |
| | | processNodeTree.value = parsedData |
| | | currentValue.value = parsedData |
| | | // 确保数据加载后刷新视图 |
| | | await nextTick() |
| | | if (simpleProcessModelRef.value?.refresh) { |
| | | await simpleProcessModelRef.value.refresh() |
| | | } |
| | | } |
| | | } catch (error) { |
| | | console.error('加载流程数据失败:', error) |
| | | } |
| | | } |
| | | |
| | | // 监听属性变化 |
| | | watch( |
| | | () => props.value, |
| | | async (newValue, oldValue) => { |
| | | if (newValue && newValue !== oldValue) { |
| | | await loadProcessData(newValue) |
| | | } |
| | | }, |
| | | { immediate: true, deep: true } |
| | | ) |
| | | |
| | | // 监听流程节点树变化,自动保存 |
| | | watch( |
| | | () => processNodeTree.value, |
| | | async (newValue, oldValue) => { |
| | | if (newValue && oldValue && JSON.stringify(newValue) !== JSON.stringify(oldValue)) { |
| | | await saveSimpleFlowModel(newValue) |
| | | } |
| | | }, |
| | | { deep: true } |
| | | ) |
| | | |
| | | const saveSimpleFlowModel = async (simpleModelNode: SimpleFlowNode) => { |
| | | if (!simpleModelNode) { |
| | | return |
| | | } |
| | | |
| | | // 校验节点 |
| | | errorNodes = [] |
| | | validateNode(simpleModelNode, errorNodes) |
| | | if (errorNodes.length > 0) { |
| | | errorDialogVisible.value = true |
| | | return |
| | | } |
| | | |
| | | try { |
| | | if (props.modelId) { |
| | | // 编辑模式 |
| | | const data = { |
| | | id: props.modelId, |
| | | simpleModel: simpleModelNode |
| | | } |
| | | await updateBpmSimpleModel(data) |
| | | } |
| | | // 无论是编辑还是新建模式,都更新当前值并触发事件 |
| | | currentValue.value = simpleModelNode |
| | | emits('success', simpleModelNode) |
| | | } catch (error) { |
| | | console.error('保存失败:', error) |
| | | } |
| | | } |
| | | |
| | | // 校验节点设置。 暂时以 showText 为空 未节点错误配置 |
| | | const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNode[]) => { |
| | | if (node) { |
| | | const { type, showText, conditionNodes } = node |
| | | if (type == NodeType.END_EVENT_NODE) { |
| | | return |
| | | } |
| | | if (type == NodeType.START_USER_NODE) { |
| | | // 发起人节点暂时不用校验,直接校验孩子节点 |
| | | validateNode(node.childNode, errorNodes) |
| | | } |
| | | |
| | | if ( |
| | | type === NodeType.USER_TASK_NODE || |
| | | type === NodeType.COPY_TASK_NODE || |
| | | type === NodeType.CONDITION_NODE |
| | | ) { |
| | | if (!showText) { |
| | | errorNodes.push(node) |
| | | } |
| | | validateNode(node.childNode, errorNodes) |
| | | } |
| | | |
| | | if ( |
| | | type == NodeType.CONDITION_BRANCH_NODE || |
| | | type == NodeType.PARALLEL_BRANCH_NODE || |
| | | type == NodeType.INCLUSIVE_BRANCH_NODE |
| | | ) { |
| | | // 分支节点 |
| | | // 1. 先校验各个分支 |
| | | conditionNodes?.forEach((item) => { |
| | | validateNode(item, errorNodes) |
| | | }) |
| | | // 2. 校验孩子节点 |
| | | validateNode(node.childNode, errorNodes) |
| | | } |
| | | } |
| | | } |
| | | |
| | | onMounted(async () => { |
| | | try { |
| | | loading.value = true |
| | | // 获取表单字段 |
| | | if (props.modelId) { |
| | | const bpmnModel = await getModel(props.modelId) |
| | | if (bpmnModel) { |
| | | formType.value = bpmnModel.formType |
| | | if (formType.value === 10) { |
| | | const bpmnForm = (await getForm(bpmnModel.formId)) as unknown as FormVO |
| | | formFields.value = bpmnForm?.fields |
| | | } |
| | | } |
| | | } |
| | | // 获得角色列表 |
| | | roleOptions.value = await RoleApi.getSimpleRoleList() |
| | | // 获得岗位列表 |
| | | postOptions.value = await PostApi.getSimplePostList() |
| | | // 获得用户列表 |
| | | userOptions.value = await UserApi.getSimpleUserList() |
| | | // 获得部门列表 |
| | | deptOptions.value = await DeptApi.getSimpleDeptList() |
| | | deptTreeOptions.value = handleTree(deptOptions.value as DeptApi.DeptVO[], 'id') |
| | | // 获取用户组列表 |
| | | userGroupOptions.value = await UserGroupApi.getUserGroupSimpleList() |
| | | |
| | | // 加载流程数据 |
| | | if (props.modelId) { |
| | | // 获取 SIMPLE 设计器模型 |
| | | const result = await getBpmSimpleModel(props.modelId) |
| | | if (result) { |
| | | await loadProcessData(result) |
| | | } else { |
| | | updateModel() |
| | | } |
| | | } else if (props.value) { |
| | | await loadProcessData(props.value) |
| | | } else { |
| | | updateModel() |
| | | } |
| | | } finally { |
| | | loading.value = false |
| | | emits('init-finished') |
| | | } |
| | | }) |
| | | |
| | | const simpleProcessModelRef = ref() |
| | | |
| | | /** 获取当前流程数据 */ |
| | | const getCurrentFlowData = async () => { |
| | | try { |
| | | if (simpleProcessModelRef.value) { |
| | | const data = await simpleProcessModelRef.value.getCurrentFlowData() |
| | | if (data) { |
| | | currentValue.value = data |
| | | return data |
| | | } |
| | | } |
| | | return currentValue.value |
| | | } catch (error) { |
| | | console.error('获取流程数据失败:', error) |
| | | return currentValue.value |
| | | } |
| | | } |
| | | |
| | | // 刷新方法 |
| | | const refresh = async () => { |
| | | try { |
| | | if (currentValue.value) { |
| | | await loadProcessData(currentValue.value) |
| | | } |
| | | } catch (error) { |
| | | console.error('刷新失败:', error) |
| | | } |
| | | } |
| | | |
| | | defineExpose({ |
| | | getCurrentFlowData, |
| | | updateModel, |
| | | loadProcessData, |
| | | refresh |
| | | }) |
| | | </script> |
对比新文件 |
| | |
| | | <template> |
| | | <div class="simple-process-model-container position-relative"> |
| | | <div class="position-absolute top-0px right-0px bg-#fff"> |
| | | <el-row type="flex" justify="end"> |
| | | <el-button-group key="scale-control" size="default"> |
| | | <el-button size="default" :icon="ScaleToOriginal" @click="processReZoom()" /> |
| | | <el-button size="default" :plain="true" :icon="ZoomOut" @click="zoomOut()" /> |
| | | <el-button size="default" class="w-80px"> {{ scaleValue }}% </el-button> |
| | | <el-button size="default" :plain="true" :icon="ZoomIn" @click="zoomIn()" /> |
| | | </el-button-group> |
| | | </el-row> |
| | | </div> |
| | | <div class="simple-process-model" :style="`transform: scale(${scaleValue / 100});`"> |
| | | <ProcessNodeTree v-if="processNodeTree" v-model:flow-node="processNodeTree" /> |
| | | </div> |
| | | </div> |
| | | <Dialog v-model="errorDialogVisible" title="保存失败" width="400" :fullscreen="false"> |
| | | <div class="mb-2">以下节点内容不完善,请修改后保存</div> |
| | | <div |
| | | class="mb-3 b-rounded-1 bg-gray-100 p-2 line-height-normal" |
| | | v-for="(item, index) in errorNodes" |
| | | :key="index" |
| | | > |
| | | {{ item.name }} : {{ NODE_DEFAULT_TEXT.get(item.type) }} |
| | | </div> |
| | | <template #footer> |
| | | <el-button type="primary" @click="errorDialogVisible = false">知道了</el-button> |
| | | </template> |
| | | </Dialog> |
| | | </template> |
| | | |
| | | <script setup lang="ts"> |
| | | import ProcessNodeTree from './ProcessNodeTree.vue' |
| | | import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from './consts' |
| | | import { useWatchNode } from './node' |
| | | import { ZoomOut, ZoomIn, ScaleToOriginal } from '@element-plus/icons-vue' |
| | | |
| | | defineOptions({ |
| | | name: 'SimpleProcessModel' |
| | | }) |
| | | |
| | | const props = defineProps({ |
| | | flowNode: { |
| | | type: Object as () => SimpleFlowNode, |
| | | required: true |
| | | }, |
| | | readonly: { |
| | | type: Boolean, |
| | | required: false, |
| | | default: true |
| | | } |
| | | }) |
| | | |
| | | const emits = defineEmits<{ |
| | | 'save': [node: SimpleFlowNode | undefined] |
| | | }>() |
| | | |
| | | const processNodeTree = useWatchNode(props) |
| | | |
| | | provide('readonly', props.readonly) |
| | | let scaleValue = ref(100) |
| | | const MAX_SCALE_VALUE = 200 |
| | | const MIN_SCALE_VALUE = 50 |
| | | |
| | | // 放大 |
| | | const zoomIn = () => { |
| | | if (scaleValue.value == MAX_SCALE_VALUE) { |
| | | return |
| | | } |
| | | scaleValue.value += 10 |
| | | } |
| | | |
| | | // 缩小 |
| | | const zoomOut = () => { |
| | | if (scaleValue.value == MIN_SCALE_VALUE) { |
| | | return |
| | | } |
| | | scaleValue.value -= 10 |
| | | } |
| | | |
| | | const processReZoom = () => { |
| | | scaleValue.value = 100 |
| | | } |
| | | |
| | | const errorDialogVisible = ref(false) |
| | | let errorNodes: SimpleFlowNode[] = [] |
| | | |
| | | // 校验节点设置。 暂时以 showText 为空 未节点错误配置 |
| | | const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNode[]) => { |
| | | if (node) { |
| | | const { type, showText, conditionNodes } = node |
| | | if (type == NodeType.END_EVENT_NODE) { |
| | | return |
| | | } |
| | | if (type == NodeType.START_USER_NODE) { |
| | | // 发起人节点暂时不用校验,直接校验孩子节点 |
| | | validateNode(node.childNode, errorNodes) |
| | | } |
| | | |
| | | if ( |
| | | type === NodeType.USER_TASK_NODE || |
| | | type === NodeType.COPY_TASK_NODE || |
| | | type === NodeType.CONDITION_NODE |
| | | ) { |
| | | if (!showText) { |
| | | errorNodes.push(node) |
| | | } |
| | | validateNode(node.childNode, errorNodes) |
| | | } |
| | | |
| | | if ( |
| | | type == NodeType.CONDITION_BRANCH_NODE || |
| | | type == NodeType.PARALLEL_BRANCH_NODE || |
| | | type == NodeType.INCLUSIVE_BRANCH_NODE |
| | | ) { |
| | | // 分支节点 |
| | | // 1. 先校验各个分支 |
| | | conditionNodes?.forEach((item) => { |
| | | validateNode(item, errorNodes) |
| | | }) |
| | | // 2. 校验孩子节点 |
| | | validateNode(node.childNode, errorNodes) |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** 获取当前流程数据 */ |
| | | const getCurrentFlowData = async () => { |
| | | try { |
| | | errorNodes = [] |
| | | validateNode(processNodeTree.value, errorNodes) |
| | | if (errorNodes.length > 0) { |
| | | errorDialogVisible.value = true |
| | | return undefined |
| | | } |
| | | return processNodeTree.value |
| | | } catch (error) { |
| | | console.error('获取流程数据失败:', error) |
| | | return undefined |
| | | } |
| | | } |
| | | |
| | | defineExpose({ |
| | | getCurrentFlowData |
| | | }) |
| | | </script> |
| | | |
| | | <style lang="scss" scoped></style> |
对比新文件 |
| | |
| | | <template> |
| | | <SimpleProcessModel :flow-node="simpleModel" :readonly="true" /> |
| | | </template> |
| | | |
| | | <script setup lang="ts"> |
| | | import { useWatchNode } from './node' |
| | | import { SimpleFlowNode } from './consts' |
| | | |
| | | defineOptions({ |
| | | name: 'SimpleProcessViewer' |
| | | }) |
| | | |
| | | const props = defineProps({ |
| | | flowNode: { |
| | | type: Object as () => SimpleFlowNode, |
| | | required: true |
| | | }, |
| | | // 流程任务 |
| | | tasks: { |
| | | type: Array, |
| | | default: () => [] as any[] |
| | | }, |
| | | // 流程实例 |
| | | processInstance: { |
| | | type: Object, |
| | | default: () => undefined |
| | | } |
| | | }) |
| | | const approveTasks = ref<any[]>(props.tasks) |
| | | const currentProcessInstance = ref(props.processInstance) |
| | | const simpleModel = useWatchNode(props) |
| | | watch( |
| | | () => props.tasks, |
| | | (newValue) => { |
| | | approveTasks.value = newValue |
| | | } |
| | | ) |
| | | watch( |
| | | () => props.processInstance, |
| | | (newValue) => { |
| | | currentProcessInstance.value = newValue |
| | | } |
| | | ) |
| | | |
| | | provide('tasks', approveTasks) |
| | | provide('processInstance', currentProcessInstance) |
| | | </script> |
| | | p |
对比新文件 |
| | |
| | | // @ts-ignore |
| | | import { DictDataVO } from '@/api/system/dict/types' |
| | | import { TaskStatusEnum } from '@/api/bpm/task' |
| | | /** |
| | | * 节点类型 |
| | | */ |
| | | export enum NodeType { |
| | | /** |
| | | * 结束节点 |
| | | */ |
| | | END_EVENT_NODE = 1, |
| | | /** |
| | | * 发起人节点 |
| | | */ |
| | | START_USER_NODE = 10, |
| | | /** |
| | | * 审批人节点 |
| | | */ |
| | | USER_TASK_NODE = 11, |
| | | |
| | | /** |
| | | * 抄送人节点 |
| | | */ |
| | | COPY_TASK_NODE = 12, |
| | | |
| | | /** |
| | | * 延迟器节点 |
| | | */ |
| | | DELAY_TIMER_NODE = 14, |
| | | |
| | | /** |
| | | * 条件节点 |
| | | */ |
| | | CONDITION_NODE = 50, |
| | | /** |
| | | * 条件分支节点 (对应排他网关) |
| | | */ |
| | | CONDITION_BRANCH_NODE = 51, |
| | | /** |
| | | * 并行分支节点 (对应并行网关) |
| | | */ |
| | | PARALLEL_BRANCH_NODE = 52, |
| | | |
| | | /** |
| | | * 包容分支节点 (对应包容网关) |
| | | */ |
| | | INCLUSIVE_BRANCH_NODE = 53 |
| | | } |
| | | |
| | | export enum NodeId { |
| | | /** |
| | | * 发起人节点 Id |
| | | */ |
| | | START_USER_NODE_ID = 'StartUserNode', |
| | | |
| | | /** |
| | | * 发起人节点 Id |
| | | */ |
| | | END_EVENT_NODE_ID = 'EndEvent' |
| | | } |
| | | |
| | | /** |
| | | * 节点结构定义 |
| | | */ |
| | | export interface SimpleFlowNode { |
| | | id: string |
| | | type: NodeType |
| | | name: string |
| | | showText?: string |
| | | // 孩子节点 |
| | | childNode?: SimpleFlowNode |
| | | // 条件节点 |
| | | conditionNodes?: SimpleFlowNode[] |
| | | // 审批类型 |
| | | approveType?: ApproveType |
| | | // 候选人策略 |
| | | candidateStrategy?: number |
| | | // 候选人参数 |
| | | candidateParam?: string |
| | | // 多人审批方式 |
| | | approveMethod?: ApproveMethodType |
| | | //通过比例 |
| | | approveRatio?: number |
| | | // 审批按钮设置 |
| | | buttonsSetting?: any[] |
| | | // 表单权限 |
| | | fieldsPermission?: Array<Record<string, any>> |
| | | // 审批任务超时处理 |
| | | timeoutHandler?: TimeoutHandler |
| | | // 审批任务拒绝处理 |
| | | rejectHandler?: RejectHandler |
| | | // 审批人为空的处理 |
| | | assignEmptyHandler?: AssignEmptyHandler |
| | | // 审批节点的审批人与发起人相同时,对应的处理类型 |
| | | assignStartUserHandlerType?: number |
| | | // 条件类型 |
| | | conditionType?: ConditionType |
| | | // 条件表达式 |
| | | conditionExpression?: string |
| | | // 条件组 |
| | | conditionGroups?: ConditionGroup |
| | | // 是否默认的条件 |
| | | defaultFlow?: boolean |
| | | // 活动的状态,用于前端节点状态展示 |
| | | activityStatus?: TaskStatusEnum |
| | | // 延迟设置 |
| | | delaySetting?: DelaySetting |
| | | } |
| | | // 候选人策略枚举 ( 用于审批节点。抄送节点 ) |
| | | export enum CandidateStrategy { |
| | | /** |
| | | * 指定角色 |
| | | */ |
| | | ROLE = 10, |
| | | /** |
| | | * 部门成员 |
| | | */ |
| | | DEPT_MEMBER = 20, |
| | | /** |
| | | * 部门的负责人 |
| | | */ |
| | | DEPT_LEADER = 21, |
| | | /** |
| | | * 连续多级部门的负责人 |
| | | */ |
| | | MULTI_LEVEL_DEPT_LEADER = 23, |
| | | /** |
| | | * 指定岗位 |
| | | */ |
| | | POST = 22, |
| | | /** |
| | | * 指定用户 |
| | | */ |
| | | USER = 30, |
| | | /** |
| | | * 发起人自选 |
| | | */ |
| | | START_USER_SELECT = 35, |
| | | /** |
| | | * 发起人自己 |
| | | */ |
| | | START_USER = 36, |
| | | /** |
| | | * 发起人部门负责人 |
| | | */ |
| | | START_USER_DEPT_LEADER = 37, |
| | | /** |
| | | * 发起人连续多级部门的负责人 |
| | | */ |
| | | START_USER_MULTI_LEVEL_DEPT_LEADER = 38, |
| | | /** |
| | | * 指定用户组 |
| | | */ |
| | | USER_GROUP = 40, |
| | | /** |
| | | * 表单内用户字段 |
| | | */ |
| | | FORM_USER = 50, |
| | | /** |
| | | * 表单内部门负责人 |
| | | */ |
| | | FORM_DEPT_LEADER = 51, |
| | | /** |
| | | * 流程表达式 |
| | | */ |
| | | EXPRESSION = 60 |
| | | } |
| | | |
| | | // 多人审批方式类型枚举 ( 用于审批节点 ) |
| | | export enum ApproveMethodType { |
| | | /** |
| | | * 随机挑选一人审批 |
| | | */ |
| | | RANDOM_SELECT_ONE_APPROVE = 1, |
| | | |
| | | /** |
| | | * 多人会签(按通过比例) |
| | | */ |
| | | APPROVE_BY_RATIO = 2, |
| | | |
| | | /** |
| | | * 多人或签(通过只需一人,拒绝只需一人) |
| | | */ |
| | | ANY_APPROVE = 3, |
| | | /** |
| | | * 多人依次审批 |
| | | */ |
| | | SEQUENTIAL_APPROVE = 4 |
| | | } |
| | | |
| | | /** |
| | | * 审批拒绝结构定义 |
| | | */ |
| | | export type RejectHandler = { |
| | | // 审批拒绝类型 |
| | | type: RejectHandlerType |
| | | // 退回节点 Id |
| | | returnNodeId?: string |
| | | } |
| | | |
| | | /** |
| | | * 审批超时结构定义 |
| | | */ |
| | | export type TimeoutHandler = { |
| | | // 是否开启超时处理 |
| | | enable: boolean |
| | | // 超时执行的动作 |
| | | type?: number |
| | | // 超时时间设置 |
| | | timeDuration?: string |
| | | // 执行动作是自动提醒, 最大提醒次数 |
| | | maxRemindCount?: number |
| | | } |
| | | |
| | | /** |
| | | * 审批人为空的结构定义 |
| | | */ |
| | | export type AssignEmptyHandler = { |
| | | // 审批人为空的处理类型 |
| | | type: AssignEmptyHandlerType |
| | | // 指定用户的编号数组 |
| | | userIds?: number[] |
| | | } |
| | | |
| | | // 审批拒绝类型枚举 |
| | | export enum RejectHandlerType { |
| | | /** |
| | | * 结束流程 |
| | | */ |
| | | FINISH_PROCESS = 1, |
| | | /** |
| | | * 驳回到指定节点 |
| | | */ |
| | | RETURN_USER_TASK = 2 |
| | | } |
| | | // 用户任务超时处理类型枚举 |
| | | export enum TimeoutHandlerType { |
| | | /** |
| | | * 自动提醒 |
| | | */ |
| | | REMINDER = 1, |
| | | /** |
| | | * 自动同意 |
| | | */ |
| | | APPROVE = 2, |
| | | /** |
| | | * 自动拒绝 |
| | | */ |
| | | REJECT = 3 |
| | | } |
| | | // 用户任务的审批人为空时,处理类型枚举 |
| | | export enum AssignEmptyHandlerType { |
| | | /** |
| | | * 自动通过 |
| | | */ |
| | | APPROVE = 1, |
| | | /** |
| | | * 自动拒绝 |
| | | */ |
| | | REJECT = 2, |
| | | /** |
| | | * 指定人员审批 |
| | | */ |
| | | ASSIGN_USER, |
| | | /** |
| | | * 转交给流程管理员 |
| | | */ |
| | | ASSIGN_ADMIN = 4 |
| | | } |
| | | // 用户任务的审批人与发起人相同时,处理类型枚举 |
| | | export enum AssignStartUserHandlerType { |
| | | /** |
| | | * 由发起人对自己审批 |
| | | */ |
| | | START_USER_AUDIT = 1, |
| | | /** |
| | | * 自动跳过【参考飞书】:1)如果当前节点还有其他审批人,则交由其他审批人进行审批;2)如果当前节点没有其他审批人,则该节点自动通过 |
| | | */ |
| | | SKIP = 2, |
| | | /** |
| | | * 转交给部门负责人审批 |
| | | */ |
| | | ASSIGN_DEPT_LEADER = 3 |
| | | } |
| | | |
| | | // 用户任务的审批类型。 【参考飞书】 |
| | | export enum ApproveType { |
| | | /** |
| | | * 人工审批 |
| | | */ |
| | | USER = 1, |
| | | /** |
| | | * 自动通过 |
| | | */ |
| | | AUTO_APPROVE = 2, |
| | | /** |
| | | * 自动拒绝 |
| | | */ |
| | | AUTO_REJECT = 3 |
| | | } |
| | | |
| | | // 时间单位枚举 |
| | | export enum TimeUnitType { |
| | | /** |
| | | * 分钟 |
| | | */ |
| | | MINUTE = 1, |
| | | /** |
| | | * 小时 |
| | | */ |
| | | HOUR = 2, |
| | | /** |
| | | * 天 |
| | | */ |
| | | DAY = 3 |
| | | } |
| | | |
| | | // 条件配置类型 ( 用于条件节点配置 ) |
| | | export enum ConditionType { |
| | | /** |
| | | * 条件表达式 |
| | | */ |
| | | EXPRESSION = 1, |
| | | |
| | | /** |
| | | * 条件规则 |
| | | */ |
| | | RULE = 2 |
| | | } |
| | | /** |
| | | * 表单权限的枚举 |
| | | */ |
| | | export enum FieldPermissionType { |
| | | /** |
| | | * 只读 |
| | | */ |
| | | READ = '1', |
| | | /** |
| | | * 编辑 |
| | | */ |
| | | WRITE = '2', |
| | | /** |
| | | * 隐藏 |
| | | */ |
| | | NONE = '3' |
| | | } |
| | | /** |
| | | * 操作按钮权限结构定义 |
| | | */ |
| | | export type ButtonSetting = { |
| | | id: OperationButtonType |
| | | displayName: string |
| | | enable: boolean |
| | | } |
| | | |
| | | // 操作按钮类型枚举 (用于审批节点) |
| | | export enum OperationButtonType { |
| | | /** |
| | | * 通过 |
| | | */ |
| | | APPROVE = 1, |
| | | /** |
| | | * 拒绝 |
| | | */ |
| | | REJECT = 2, |
| | | /** |
| | | * 转办 |
| | | */ |
| | | TRANSFER = 3, |
| | | /** |
| | | * 委派 |
| | | */ |
| | | DELEGATE = 4, |
| | | /** |
| | | * 加签 |
| | | */ |
| | | ADD_SIGN = 5, |
| | | /** |
| | | * 退回 |
| | | */ |
| | | RETURN = 6, |
| | | /** |
| | | * 抄送 |
| | | */ |
| | | COPY = 7 |
| | | } |
| | | |
| | | /** |
| | | * 条件规则结构定义 |
| | | */ |
| | | export type ConditionRule = { |
| | | type: number |
| | | opName: string |
| | | opCode: string |
| | | leftSide: string |
| | | rightSide: string |
| | | } |
| | | |
| | | /** |
| | | * 条件组结构定义 |
| | | */ |
| | | export type ConditionGroup = { |
| | | // 条件组的逻辑关系是否为且 |
| | | and: boolean |
| | | // 条件数组 |
| | | conditions: Condition[] |
| | | } |
| | | |
| | | /** |
| | | * 条件结构定义 |
| | | */ |
| | | export type Condition = { |
| | | // 条件规则的逻辑关系是否为且 |
| | | and: boolean |
| | | rules: ConditionRule[] |
| | | } |
| | | |
| | | export const NODE_DEFAULT_TEXT = new Map<number, string>() |
| | | NODE_DEFAULT_TEXT.set(NodeType.USER_TASK_NODE, '请配置审批人') |
| | | NODE_DEFAULT_TEXT.set(NodeType.COPY_TASK_NODE, '请配置抄送人') |
| | | NODE_DEFAULT_TEXT.set(NodeType.CONDITION_NODE, '请设置条件') |
| | | NODE_DEFAULT_TEXT.set(NodeType.START_USER_NODE, '请设置发起人') |
| | | NODE_DEFAULT_TEXT.set(NodeType.DELAY_TIMER_NODE, '请设置延迟器') |
| | | |
| | | export const NODE_DEFAULT_NAME = new Map<number, string>() |
| | | NODE_DEFAULT_NAME.set(NodeType.USER_TASK_NODE, '审批人') |
| | | NODE_DEFAULT_NAME.set(NodeType.COPY_TASK_NODE, '抄送人') |
| | | NODE_DEFAULT_NAME.set(NodeType.CONDITION_NODE, '条件') |
| | | NODE_DEFAULT_NAME.set(NodeType.START_USER_NODE, '发起人') |
| | | NODE_DEFAULT_NAME.set(NodeType.DELAY_TIMER_NODE, '延迟器') |
| | | |
| | | // 候选人策略。暂时不从字典中取。 后续可能调整。控制显示顺序 |
| | | export const CANDIDATE_STRATEGY: DictDataVO[] = [ |
| | | { label: '指定成员', value: CandidateStrategy.USER }, |
| | | { label: '指定角色', value: CandidateStrategy.ROLE }, |
| | | { label: '部门成员', value: CandidateStrategy.DEPT_MEMBER }, |
| | | { label: '部门负责人', value: CandidateStrategy.DEPT_LEADER }, |
| | | { label: '连续多级部门负责人', value: CandidateStrategy.MULTI_LEVEL_DEPT_LEADER }, |
| | | { label: '发起人自选', value: CandidateStrategy.START_USER_SELECT }, |
| | | { label: '发起人本人', value: CandidateStrategy.START_USER }, |
| | | { label: '发起人部门负责人', value: CandidateStrategy.START_USER_DEPT_LEADER }, |
| | | { label: '发起人连续部门负责人', value: CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER }, |
| | | { label: '用户组', value: CandidateStrategy.USER_GROUP }, |
| | | { label: '表单内用户字段', value: CandidateStrategy.FORM_USER }, |
| | | { label: '表单内部门负责人', value: CandidateStrategy.FORM_DEPT_LEADER }, |
| | | { label: '流程表达式', value: CandidateStrategy.EXPRESSION } |
| | | ] |
| | | // 审批节点 的审批类型 |
| | | export const APPROVE_TYPE: DictDataVO[] = [ |
| | | { label: '人工审批', value: ApproveType.USER }, |
| | | { label: '自动通过', value: ApproveType.AUTO_APPROVE }, |
| | | { label: '自动拒绝', value: ApproveType.AUTO_REJECT } |
| | | ] |
| | | |
| | | export const APPROVE_METHODS: DictDataVO[] = [ |
| | | { label: '按顺序依次审批', value: ApproveMethodType.SEQUENTIAL_APPROVE }, |
| | | { label: '会签(可同时审批,至少 % 人必须审批通过)', value: ApproveMethodType.APPROVE_BY_RATIO }, |
| | | { label: '或签(可同时审批,有一人通过即可)', value: ApproveMethodType.ANY_APPROVE }, |
| | | { label: '随机挑选一人审批', value: ApproveMethodType.RANDOM_SELECT_ONE_APPROVE } |
| | | ] |
| | | |
| | | export const CONDITION_CONFIG_TYPES: DictDataVO[] = [ |
| | | { label: '条件表达式', value: ConditionType.EXPRESSION }, |
| | | { label: '条件规则', value: ConditionType.RULE } |
| | | ] |
| | | |
| | | // 时间单位类型 |
| | | export const TIME_UNIT_TYPES: DictDataVO[] = [ |
| | | { label: '分钟', value: TimeUnitType.MINUTE }, |
| | | { label: '小时', value: TimeUnitType.HOUR }, |
| | | { label: '天', value: TimeUnitType.DAY } |
| | | ] |
| | | // 超时处理执行动作类型 |
| | | export const TIMEOUT_HANDLER_TYPES: DictDataVO[] = [ |
| | | { label: '自动提醒', value: 1 }, |
| | | { label: '自动同意', value: 2 }, |
| | | { label: '自动拒绝', value: 3 } |
| | | ] |
| | | export const REJECT_HANDLER_TYPES: DictDataVO[] = [ |
| | | { label: '终止流程', value: RejectHandlerType.FINISH_PROCESS }, |
| | | { label: '驳回到指定节点', value: RejectHandlerType.RETURN_USER_TASK } |
| | | // { label: '结束任务', value: RejectHandlerType.FINISH_TASK } |
| | | ] |
| | | export const ASSIGN_EMPTY_HANDLER_TYPES: DictDataVO[] = [ |
| | | { label: '自动通过', value: 1 }, |
| | | { label: '自动拒绝', value: 2 }, |
| | | { label: '指定成员审批', value: 3 }, |
| | | { label: '转交给流程管理员', value: 4 } |
| | | ] |
| | | export const ASSIGN_START_USER_HANDLER_TYPES: DictDataVO[] = [ |
| | | { label: '由发起人对自己审批', value: 1 }, |
| | | { label: '自动跳过', value: 2 }, |
| | | { label: '转交给部门负责人审批', value: 3 } |
| | | ] |
| | | |
| | | // 比较运算符 |
| | | export const COMPARISON_OPERATORS: DictDataVO = [ |
| | | { |
| | | value: '==', |
| | | label: '等于' |
| | | }, |
| | | { |
| | | value: '!=', |
| | | label: '不等于' |
| | | }, |
| | | { |
| | | value: '>', |
| | | label: '大于' |
| | | }, |
| | | { |
| | | value: '>=', |
| | | label: '大于等于' |
| | | }, |
| | | { |
| | | value: '<', |
| | | label: '小于' |
| | | }, |
| | | { |
| | | value: '<=', |
| | | label: '小于等于' |
| | | } |
| | | ] |
| | | // 审批操作按钮名称 |
| | | export const OPERATION_BUTTON_NAME = new Map<number, string>() |
| | | OPERATION_BUTTON_NAME.set(OperationButtonType.APPROVE, '通过') |
| | | OPERATION_BUTTON_NAME.set(OperationButtonType.REJECT, '拒绝') |
| | | OPERATION_BUTTON_NAME.set(OperationButtonType.TRANSFER, '转办') |
| | | OPERATION_BUTTON_NAME.set(OperationButtonType.DELEGATE, '委派') |
| | | OPERATION_BUTTON_NAME.set(OperationButtonType.ADD_SIGN, '加签') |
| | | OPERATION_BUTTON_NAME.set(OperationButtonType.RETURN, '退回') |
| | | OPERATION_BUTTON_NAME.set(OperationButtonType.COPY, '抄送') |
| | | |
| | | // 默认的按钮权限设置 |
| | | export const DEFAULT_BUTTON_SETTING: ButtonSetting[] = [ |
| | | { id: OperationButtonType.APPROVE, displayName: '通过', enable: true }, |
| | | { id: OperationButtonType.REJECT, displayName: '拒绝', enable: true }, |
| | | { id: OperationButtonType.TRANSFER, displayName: '转办', enable: true }, |
| | | { id: OperationButtonType.DELEGATE, displayName: '委派', enable: true }, |
| | | { id: OperationButtonType.ADD_SIGN, displayName: '加签', enable: true }, |
| | | { id: OperationButtonType.RETURN, displayName: '退回', enable: true } |
| | | ] |
| | | |
| | | // 发起人的按钮权限。暂时定死,不可以编辑 |
| | | export const START_USER_BUTTON_SETTING: ButtonSetting[] = [ |
| | | { id: OperationButtonType.APPROVE, displayName: '提交', enable: true }, |
| | | { id: OperationButtonType.REJECT, displayName: '拒绝', enable: false }, |
| | | { id: OperationButtonType.TRANSFER, displayName: '转办', enable: false }, |
| | | { id: OperationButtonType.DELEGATE, displayName: '委派', enable: false }, |
| | | { id: OperationButtonType.ADD_SIGN, displayName: '加签', enable: false }, |
| | | { id: OperationButtonType.RETURN, displayName: '退回', enable: false } |
| | | ] |
| | | |
| | | export const MULTI_LEVEL_DEPT: DictDataVO = [ |
| | | { label: '第 1 级部门', value: 1 }, |
| | | { label: '第 2 级部门', value: 2 }, |
| | | { label: '第 3 级部门', value: 3 }, |
| | | { label: '第 4 级部门', value: 4 }, |
| | | { label: '第 5 级部门', value: 5 }, |
| | | { label: '第 6 级部门', value: 6 }, |
| | | { label: '第 7 级部门', value: 7 }, |
| | | { label: '第 8 级部门', value: 8 }, |
| | | { label: '第 9 级部门', value: 9 }, |
| | | { label: '第 10 级部门', value: 10 }, |
| | | { label: '第 11 级部门', value: 11 }, |
| | | { label: '第 12 级部门', value: 12 }, |
| | | { label: '第 13 级部门', value: 13 }, |
| | | { label: '第 14 级部门', value: 14 }, |
| | | { label: '第 15 级部门', value: 15 } |
| | | ] |
| | | |
| | | /** |
| | | * 流程实例的变量枚举 |
| | | */ |
| | | export enum ProcessVariableEnum { |
| | | /** |
| | | * 发起用户 ID |
| | | */ |
| | | START_USER_ID = 'PROCESS_START_USER_ID' |
| | | } |
| | | |
| | | /** |
| | | * 延迟设置 |
| | | */ |
| | | export type DelaySetting = { |
| | | // 延迟类型 |
| | | delayType: number |
| | | // 延迟时间表达式 |
| | | delayTime: string |
| | | } |
| | | /** |
| | | * 延迟类型 |
| | | */ |
| | | export enum DelayTypeEnum { |
| | | /** |
| | | * 固定时长 |
| | | */ |
| | | FIXED_TIME_DURATION = 1, |
| | | /** |
| | | * 固定日期时间 |
| | | */ |
| | | FIXED_DATE_TIME = 2 |
| | | } |
| | | export const DELAY_TYPE = [ |
| | | { label: '固定时长', value: DelayTypeEnum.FIXED_TIME_DURATION }, |
| | | { label: '固定日期', value: DelayTypeEnum.FIXED_DATE_TIME } |
| | | ] |
对比新文件 |
| | |
| | | import SimpleProcessDesigner from './SimpleProcessDesigner.vue' |
| | | import SimpleProcessViewer from './SimpleProcessViewer.vue' |
| | | import '../theme/simple-process-designer.scss' |
| | | |
| | | export { SimpleProcessDesigner, SimpleProcessViewer} |
对比新文件 |
| | |
| | | import { TaskStatusEnum } from '@/api/bpm/task' |
| | | import * as RoleApi from '@/api/system/role' |
| | | import * as DeptApi from '@/api/system/dept' |
| | | import * as PostApi from '@/api/system/post' |
| | | import * as UserApi from '@/api/system/user' |
| | | import * as UserGroupApi from '@/api/bpm/userGroup' |
| | | import { |
| | | SimpleFlowNode, |
| | | CandidateStrategy, |
| | | NodeType, |
| | | ApproveMethodType, |
| | | RejectHandlerType, |
| | | NODE_DEFAULT_NAME, |
| | | AssignStartUserHandlerType, |
| | | AssignEmptyHandlerType, |
| | | FieldPermissionType |
| | | } from './consts' |
| | | import { parseFormFields } from '@/components/FormCreate/src/utils/index' |
| | | export function useWatchNode(props: { flowNode: SimpleFlowNode }): Ref<SimpleFlowNode> { |
| | | const node = ref<SimpleFlowNode>(props.flowNode) |
| | | watch( |
| | | () => props.flowNode, |
| | | (newValue) => { |
| | | node.value = newValue |
| | | } |
| | | ) |
| | | return node |
| | | } |
| | | |
| | | // 解析 formCreate 所有表单字段, 并返回 |
| | | const parseFormCreateFields = (formFields?: string[]) => { |
| | | const result: Array<Record<string, any>> = [] |
| | | if (formFields) { |
| | | formFields.forEach((fieldStr: string) => { |
| | | parseFormFields(JSON.parse(fieldStr), result) |
| | | }) |
| | | } |
| | | return result |
| | | } |
| | | |
| | | /** |
| | | * @description 表单数据权限配置,用于发起人节点 、审批节点、抄送节点 |
| | | */ |
| | | export function useFormFieldsPermission(defaultPermission: FieldPermissionType) { |
| | | // 字段权限配置. 需要有 field, title, permissioin 属性 |
| | | const fieldsPermissionConfig = ref<Array<Record<string, any>>>([]) |
| | | |
| | | const formType = inject<Ref<number>>('formType') // 表单类型 |
| | | |
| | | const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段 |
| | | |
| | | const getNodeConfigFormFields = (nodeFormFields?: Array<Record<string, string>>) => { |
| | | nodeFormFields = toRaw(nodeFormFields) |
| | | if (!nodeFormFields || nodeFormFields.length === 0) { |
| | | fieldsPermissionConfig.value = getDefaultFieldsPermission(unref(formFields)) |
| | | } else { |
| | | fieldsPermissionConfig.value = mergeFieldsPermission(nodeFormFields, unref(formFields)) |
| | | } |
| | | } |
| | | // 合并已经设置的表单字段权限,当前流程表单字段 (可能新增,或删除了字段) |
| | | const mergeFieldsPermission = ( |
| | | formFieldsPermisson: Array<Record<string, string>>, |
| | | formFields?: string[] |
| | | ) => { |
| | | let mergedFieldsPermission: Array<Record<string, any>> = [] |
| | | if (formFields) { |
| | | mergedFieldsPermission = parseFormCreateFields(formFields).map((item) => { |
| | | const found = formFieldsPermisson.find( |
| | | (fieldPermission) => fieldPermission.field == item.field |
| | | ) |
| | | return { |
| | | field: item.field, |
| | | title: item.title, |
| | | permission: found ? found.permission : defaultPermission |
| | | } |
| | | }) |
| | | } |
| | | return mergedFieldsPermission |
| | | } |
| | | |
| | | // 默认的表单权限: 获取表单的所有字段,设置字段默认权限为只读 |
| | | const getDefaultFieldsPermission = (formFields?: string[]) => { |
| | | let defaultFieldsPermission: Array<Record<string, any>> = [] |
| | | if (formFields) { |
| | | defaultFieldsPermission = parseFormCreateFields(formFields).map((item) => { |
| | | return { |
| | | field: item.field, |
| | | title: item.title, |
| | | permission: defaultPermission |
| | | } |
| | | }) |
| | | } |
| | | return defaultFieldsPermission |
| | | } |
| | | |
| | | // 获取表单的所有字段,作为下拉框选项 |
| | | const formFieldOptions = parseFormCreateFields(unref(formFields)) |
| | | |
| | | return { |
| | | formType, |
| | | fieldsPermissionConfig, |
| | | formFieldOptions, |
| | | getNodeConfigFormFields |
| | | } |
| | | } |
| | | /** |
| | | * @description 获取表单的字段 |
| | | */ |
| | | export function useFormFields() { |
| | | const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段 |
| | | return parseFormCreateFields(unref(formFields)) |
| | | } |
| | | |
| | | export type UserTaskFormType = { |
| | | //candidateParamArray: any[] |
| | | candidateStrategy: CandidateStrategy |
| | | approveMethod: ApproveMethodType |
| | | roleIds?: number[] // 角色 |
| | | deptIds?: number[] // 部门 |
| | | deptLevel?: number // 部门层级 |
| | | userIds?: number[] // 用户 |
| | | userGroups?: number[] // 用户组 |
| | | postIds?: number[] // 岗位 |
| | | expression?: string // 流程表达式 |
| | | formUser?: string // 表单内用户字段 |
| | | formDept?: string // 表单内部门字段 |
| | | approveRatio?: number |
| | | rejectHandlerType?: RejectHandlerType |
| | | returnNodeId?: string |
| | | timeoutHandlerEnable?: boolean |
| | | timeoutHandlerType?: number |
| | | assignEmptyHandlerType?: AssignEmptyHandlerType |
| | | assignEmptyHandlerUserIds?: number[] |
| | | assignStartUserHandlerType?: AssignStartUserHandlerType |
| | | timeDuration?: number |
| | | maxRemindCount?: number |
| | | buttonsSetting: any[] |
| | | } |
| | | |
| | | export type CopyTaskFormType = { |
| | | // candidateParamArray: any[] |
| | | candidateStrategy: CandidateStrategy |
| | | roleIds?: number[] // 角色 |
| | | deptIds?: number[] // 部门 |
| | | deptLevel?: number // 部门层级 |
| | | userIds?: number[] // 用户 |
| | | userGroups?: number[] // 用户组 |
| | | postIds?: number[] // 岗位 |
| | | formUser?: string // 表单内用户字段 |
| | | formDept?: string // 表单内部门字段 |
| | | expression?: string // 流程表达式 |
| | | } |
| | | |
| | | /** |
| | | * @description 节点表单数据。 用于审批节点、抄送节点 |
| | | */ |
| | | export function useNodeForm(nodeType: NodeType) { |
| | | const roleOptions = inject<Ref<RoleApi.RoleVO[]>>('roleList') // 角色列表 |
| | | const postOptions = inject<Ref<PostApi.PostVO[]>>('postList') // 岗位列表 |
| | | const userOptions = inject<Ref<UserApi.UserVO[]>>('userList') // 用户列表 |
| | | const deptOptions = inject<Ref<DeptApi.DeptVO[]>>('deptList') // 部门列表 |
| | | const userGroupOptions = inject<Ref<UserGroupApi.UserGroupVO[]>>('userGroupList') // 用户组列表 |
| | | const deptTreeOptions = inject('deptTree') // 部门树 |
| | | const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段 |
| | | const configForm = ref<UserTaskFormType | CopyTaskFormType>() |
| | | if (nodeType === NodeType.USER_TASK_NODE) { |
| | | configForm.value = { |
| | | candidateStrategy: CandidateStrategy.USER, |
| | | approveMethod: ApproveMethodType.SEQUENTIAL_APPROVE, |
| | | approveRatio: 100, |
| | | rejectHandlerType: RejectHandlerType.FINISH_PROCESS, |
| | | assignStartUserHandlerType: AssignStartUserHandlerType.START_USER_AUDIT, |
| | | returnNodeId: '', |
| | | timeoutHandlerEnable: false, |
| | | timeoutHandlerType: 1, |
| | | timeDuration: 6, // 默认 6小时 |
| | | maxRemindCount: 1, // 默认 提醒 1次 |
| | | buttonsSetting: [] |
| | | } |
| | | } else { |
| | | configForm.value = { |
| | | candidateStrategy: CandidateStrategy.USER |
| | | } |
| | | } |
| | | |
| | | const getShowText = (): string => { |
| | | let showText = '' |
| | | // 指定成员 |
| | | if (configForm.value?.candidateStrategy === CandidateStrategy.USER) { |
| | | if (configForm.value?.userIds!.length > 0) { |
| | | const candidateNames: string[] = [] |
| | | userOptions?.value.forEach((item) => { |
| | | if (configForm.value?.userIds!.includes(item.id)) { |
| | | candidateNames.push(item.nickname) |
| | | } |
| | | }) |
| | | showText = `指定成员:${candidateNames.join(',')}` |
| | | } |
| | | } |
| | | // 指定角色 |
| | | if (configForm.value?.candidateStrategy === CandidateStrategy.ROLE) { |
| | | if (configForm.value.roleIds!.length > 0) { |
| | | const candidateNames: string[] = [] |
| | | roleOptions?.value.forEach((item) => { |
| | | if (configForm.value?.roleIds!.includes(item.id)) { |
| | | candidateNames.push(item.name) |
| | | } |
| | | }) |
| | | showText = `指定角色:${candidateNames.join(',')}` |
| | | } |
| | | } |
| | | // 指定部门 |
| | | if ( |
| | | configForm.value?.candidateStrategy === CandidateStrategy.DEPT_MEMBER || |
| | | configForm.value?.candidateStrategy === CandidateStrategy.DEPT_LEADER || |
| | | configForm.value?.candidateStrategy === CandidateStrategy.MULTI_LEVEL_DEPT_LEADER |
| | | ) { |
| | | if (configForm.value?.deptIds!.length > 0) { |
| | | const candidateNames: string[] = [] |
| | | deptOptions?.value.forEach((item) => { |
| | | if (configForm.value?.deptIds!.includes(item.id!)) { |
| | | candidateNames.push(item.name) |
| | | } |
| | | }) |
| | | if (configForm.value.candidateStrategy === CandidateStrategy.DEPT_MEMBER) { |
| | | showText = `部门成员:${candidateNames.join(',')}` |
| | | } else if (configForm.value.candidateStrategy === CandidateStrategy.DEPT_LEADER) { |
| | | showText = `部门的负责人:${candidateNames.join(',')}` |
| | | } else { |
| | | showText = `多级部门的负责人:${candidateNames.join(',')}` |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 指定岗位 |
| | | if (configForm.value?.candidateStrategy === CandidateStrategy.POST) { |
| | | if (configForm.value.postIds!.length > 0) { |
| | | const candidateNames: string[] = [] |
| | | postOptions?.value.forEach((item) => { |
| | | if (configForm.value?.postIds!.includes(item.id!)) { |
| | | candidateNames.push(item.name) |
| | | } |
| | | }) |
| | | showText = `指定岗位: ${candidateNames.join(',')}` |
| | | } |
| | | } |
| | | // 指定用户组 |
| | | if (configForm.value?.candidateStrategy === CandidateStrategy.USER_GROUP) { |
| | | if (configForm.value?.userGroups!.length > 0) { |
| | | const candidateNames: string[] = [] |
| | | userGroupOptions?.value.forEach((item) => { |
| | | if (configForm.value?.userGroups!.includes(item.id)) { |
| | | candidateNames.push(item.name) |
| | | } |
| | | }) |
| | | showText = `指定用户组: ${candidateNames.join(',')}` |
| | | } |
| | | } |
| | | |
| | | // 表单内用户字段 |
| | | if (configForm.value?.candidateStrategy === CandidateStrategy.FORM_USER) { |
| | | const formFieldOptions = parseFormCreateFields(unref(formFields)) |
| | | const item = formFieldOptions.find((item) => item.field === configForm.value?.formUser) |
| | | showText = `表单用户:${item?.title}` |
| | | } |
| | | |
| | | // 表单内部门负责人 |
| | | if (configForm.value?.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER) { |
| | | showText = `表单内部门负责人` |
| | | } |
| | | |
| | | // 发起人自选 |
| | | if (configForm.value?.candidateStrategy === CandidateStrategy.START_USER_SELECT) { |
| | | showText = `发起人自选` |
| | | } |
| | | // 发起人自己 |
| | | if (configForm.value?.candidateStrategy === CandidateStrategy.START_USER) { |
| | | showText = `发起人自己` |
| | | } |
| | | // 发起人的部门负责人 |
| | | if (configForm.value?.candidateStrategy === CandidateStrategy.START_USER_DEPT_LEADER) { |
| | | showText = `发起人的部门负责人` |
| | | } |
| | | // 发起人的部门负责人 |
| | | if ( |
| | | configForm.value?.candidateStrategy === CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER |
| | | ) { |
| | | showText = `发起人连续部门负责人` |
| | | } |
| | | // 流程表达式 |
| | | if (configForm.value?.candidateStrategy === CandidateStrategy.EXPRESSION) { |
| | | showText = `流程表达式:${configForm.value.expression}` |
| | | } |
| | | return showText |
| | | } |
| | | |
| | | /** |
| | | * 处理候选人参数的赋值 |
| | | */ |
| | | const handleCandidateParam = () => { |
| | | let candidateParam: undefined | string = undefined |
| | | if (!configForm.value) { |
| | | return candidateParam |
| | | } |
| | | switch (configForm.value.candidateStrategy) { |
| | | case CandidateStrategy.USER: |
| | | candidateParam = configForm.value.userIds!.join(',') |
| | | break |
| | | case CandidateStrategy.ROLE: |
| | | candidateParam = configForm.value.roleIds!.join(',') |
| | | break |
| | | case CandidateStrategy.POST: |
| | | candidateParam = configForm.value.postIds!.join(',') |
| | | break |
| | | case CandidateStrategy.USER_GROUP: |
| | | candidateParam = configForm.value.userGroups!.join(',') |
| | | break |
| | | case CandidateStrategy.FORM_USER: |
| | | candidateParam = configForm.value.formUser! |
| | | break |
| | | case CandidateStrategy.EXPRESSION: |
| | | candidateParam = configForm.value.expression! |
| | | break |
| | | case CandidateStrategy.DEPT_MEMBER: |
| | | case CandidateStrategy.DEPT_LEADER: |
| | | candidateParam = configForm.value.deptIds!.join(',') |
| | | break |
| | | // 发起人部门负责人 |
| | | case CandidateStrategy.START_USER_DEPT_LEADER: |
| | | case CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER: |
| | | candidateParam = configForm.value.deptLevel + '' |
| | | break |
| | | // 指定连续多级部门的负责人 |
| | | case CandidateStrategy.MULTI_LEVEL_DEPT_LEADER: { |
| | | // 候选人参数格式: | 分隔 。左边为部门(多个部门用 , 分隔)。 右边为部门层级 |
| | | const deptIds = configForm.value.deptIds!.join(',') |
| | | candidateParam = deptIds.concat('|' + configForm.value.deptLevel + '') |
| | | break |
| | | } |
| | | // 表单内部门的负责人 |
| | | case CandidateStrategy.FORM_DEPT_LEADER: { |
| | | // 候选人参数格式: | 分隔 。左边为表单内部门字段。 右边为部门层级 |
| | | const deptFieldOnForm = configForm.value.formDept! |
| | | candidateParam = deptFieldOnForm.concat('|' + configForm.value.deptLevel + '') |
| | | break |
| | | } |
| | | default: |
| | | break |
| | | } |
| | | return candidateParam |
| | | } |
| | | /** |
| | | * 解析候选人参数 |
| | | */ |
| | | const parseCandidateParam = ( |
| | | candidateStrategy: CandidateStrategy, |
| | | candidateParam: string | undefined |
| | | ) => { |
| | | if (!configForm.value || !candidateParam) { |
| | | return |
| | | } |
| | | switch (candidateStrategy) { |
| | | case CandidateStrategy.USER: { |
| | | configForm.value.userIds = candidateParam.split(',').map((item) => +item) |
| | | break |
| | | } |
| | | case CandidateStrategy.ROLE: |
| | | configForm.value.roleIds = candidateParam.split(',').map((item) => +item) |
| | | break |
| | | case CandidateStrategy.POST: |
| | | configForm.value.postIds = candidateParam.split(',').map((item) => +item) |
| | | break |
| | | case CandidateStrategy.USER_GROUP: |
| | | configForm.value.userGroups = candidateParam.split(',').map((item) => +item) |
| | | break |
| | | case CandidateStrategy.FORM_USER: |
| | | configForm.value.formUser = candidateParam |
| | | break |
| | | case CandidateStrategy.EXPRESSION: |
| | | configForm.value.expression = candidateParam |
| | | break |
| | | case CandidateStrategy.DEPT_MEMBER: |
| | | case CandidateStrategy.DEPT_LEADER: |
| | | configForm.value.deptIds = candidateParam.split(',').map((item) => +item) |
| | | break |
| | | // 发起人部门负责人 |
| | | case CandidateStrategy.START_USER_DEPT_LEADER: |
| | | case CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER: |
| | | configForm.value.deptLevel = +candidateParam |
| | | break |
| | | // 指定连续多级部门的负责人 |
| | | case CandidateStrategy.MULTI_LEVEL_DEPT_LEADER: { |
| | | // 候选人参数格式: | 分隔 。左边为部门(多个部门用 , 分隔)。 右边为部门层级 |
| | | const paramArray = candidateParam.split('|') |
| | | configForm.value.deptIds = paramArray[0].split(',').map((item) => +item) |
| | | configForm.value.deptLevel = +paramArray[1] |
| | | break |
| | | } |
| | | // 表单内的部门负责人 |
| | | case CandidateStrategy.FORM_DEPT_LEADER: { |
| | | // 候选人参数格式: | 分隔 。左边为表单内的部门字段。 右边为部门层级 |
| | | const paramArray = candidateParam.split('|') |
| | | configForm.value.formDept = paramArray[0] |
| | | configForm.value.deptLevel = +paramArray[1] |
| | | break |
| | | } |
| | | default: |
| | | break |
| | | } |
| | | } |
| | | return { |
| | | configForm, |
| | | roleOptions, |
| | | postOptions, |
| | | userOptions, |
| | | userGroupOptions, |
| | | deptTreeOptions, |
| | | handleCandidateParam, |
| | | parseCandidateParam, |
| | | getShowText |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * @description 抽屉配置 |
| | | */ |
| | | export function useDrawer() { |
| | | // 抽屉配置是否可见 |
| | | const settingVisible = ref(false) |
| | | // 关闭配置抽屉 |
| | | const closeDrawer = () => { |
| | | settingVisible.value = false |
| | | } |
| | | // 打开配置抽屉 |
| | | const openDrawer = () => { |
| | | settingVisible.value = true |
| | | } |
| | | return { |
| | | settingVisible, |
| | | closeDrawer, |
| | | openDrawer |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * @description 节点名称配置 |
| | | */ |
| | | export function useNodeName(nodeType: NodeType) { |
| | | // 节点名称 |
| | | const nodeName = ref<string>() |
| | | // 节点名称输入框 |
| | | const showInput = ref(false) |
| | | // 点击节点名称编辑图标 |
| | | const clickIcon = () => { |
| | | showInput.value = true |
| | | } |
| | | // 节点名称输入框失去焦点 |
| | | const blurEvent = () => { |
| | | showInput.value = false |
| | | nodeName.value = nodeName.value || (NODE_DEFAULT_NAME.get(nodeType) as string) |
| | | } |
| | | return { |
| | | nodeName, |
| | | showInput, |
| | | clickIcon, |
| | | blurEvent |
| | | } |
| | | } |
| | | |
| | | export function useNodeName2(node: Ref<SimpleFlowNode>, nodeType: NodeType) { |
| | | // 显示节点名称输入框 |
| | | const showInput = ref(false) |
| | | // 节点名称输入框失去焦点 |
| | | const blurEvent = () => { |
| | | showInput.value = false |
| | | node.value.name = node.value.name || (NODE_DEFAULT_NAME.get(nodeType) as string) |
| | | } |
| | | // 点击节点标题进行输入 |
| | | const clickTitle = () => { |
| | | showInput.value = true |
| | | } |
| | | return { |
| | | showInput, |
| | | clickTitle, |
| | | blurEvent |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * @description 根据节点任务状态,获取节点任务状态样式 |
| | | */ |
| | | export function useTaskStatusClass(taskStatus: TaskStatusEnum | undefined): string { |
| | | if (!taskStatus) { |
| | | return '' |
| | | } |
| | | if (taskStatus === TaskStatusEnum.APPROVE) { |
| | | return 'status-pass' |
| | | } |
| | | if (taskStatus === TaskStatusEnum.RUNNING) { |
| | | return 'status-running' |
| | | } |
| | | if (taskStatus === TaskStatusEnum.REJECT) { |
| | | return 'status-reject' |
| | | } |
| | | if (taskStatus === TaskStatusEnum.CANCEL) { |
| | | return 'status-cancel' |
| | | } |
| | | |
| | | return '' |
| | | } |
对比新文件 |
| | |
| | | <template> |
| | | <el-drawer |
| | | :append-to-body="true" |
| | | v-model="settingVisible" |
| | | :show-close="false" |
| | | :size="588" |
| | | :before-close="handleClose" |
| | | > |
| | | <template #header> |
| | | <div class="config-header"> |
| | | <input |
| | | v-if="showInput" |
| | | type="text" |
| | | class="config-editable-input" |
| | | @blur="blurEvent()" |
| | | v-mountedFocus |
| | | v-model="currentNode.name" |
| | | :placeholder="currentNode.name" |
| | | /> |
| | | <div v-else class="node-name" |
| | | >{{ currentNode.name }} |
| | | <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" |
| | | /></div> |
| | | |
| | | <div class="divide-line"></div> |
| | | </div> |
| | | </template> |
| | | <div> |
| | | <div class="mb-3 font-size-16px" v-if="currentNode.defaultFlow" |
| | | >未满足其它条件时,将进入此分支(该分支不可编辑和删除)</div |
| | | > |
| | | <div v-else> |
| | | <el-form ref="formRef" :model="currentNode" :rules="formRules" label-position="top"> |
| | | <el-form-item label="配置方式" prop="conditionType"> |
| | | <el-radio-group v-model="currentNode.conditionType" @change="changeConditionType"> |
| | | <el-radio |
| | | v-for="(dict, index) in conditionConfigTypes" |
| | | :key="index" |
| | | :value="dict.value" |
| | | :label="dict.value" |
| | | > |
| | | {{ dict.label }} |
| | | </el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | |
| | | <el-form-item |
| | | v-if="currentNode.conditionType === 1" |
| | | label="条件表达式" |
| | | prop="conditionExpression" |
| | | > |
| | | <el-input |
| | | type="textarea" |
| | | v-model="currentNode.conditionExpression" |
| | | clearable |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item v-if="currentNode.conditionType === 2" label="条件规则"> |
| | | <div class="condition-group-tool"> |
| | | <div class="flex items-center"> |
| | | <div class="mr-4">条件组关系</div> |
| | | <el-switch |
| | | v-model="conditionGroups.and" |
| | | inline-prompt |
| | | active-text="且" |
| | | inactive-text="或" |
| | | /> |
| | | </div> |
| | | </div> |
| | | <el-space direction="vertical" :spacer="conditionGroups.and ? '且' : '或'"> |
| | | <el-card |
| | | class="condition-group" |
| | | style="width: 530px" |
| | | v-for="(condition, cIdx) in conditionGroups.conditions" |
| | | :key="cIdx" |
| | | > |
| | | <div class="condition-group-delete" v-if="conditionGroups.conditions.length > 1"> |
| | | <Icon |
| | | color="#0089ff" |
| | | icon="ep:circle-close-filled" |
| | | :size="18" |
| | | @click="deleteConditionGroup(cIdx)" |
| | | /> |
| | | </div> |
| | | <template #header> |
| | | <div class="flex items-center justify-between"> |
| | | <div>条件组</div> |
| | | <div class="flex"> |
| | | <div class="mr-4">规则关系</div> |
| | | <el-switch |
| | | v-model="condition.and" |
| | | inline-prompt |
| | | active-text="且" |
| | | inactive-text="或" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <div class="flex pt-2" v-for="(rule, rIdx) in condition.rules" :key="rIdx"> |
| | | <div class="mr-2"> |
| | | <el-select style="width: 160px" v-model="rule.leftSide"> |
| | | <el-option |
| | | v-for="(item, index) in fieldOptions" |
| | | :key="index" |
| | | :label="item.title" |
| | | :value="item.field" |
| | | :disabled="!item.required" |
| | | /> |
| | | </el-select> |
| | | </div> |
| | | <div class="mr-2"> |
| | | <el-select v-model="rule.opCode" style="width: 100px"> |
| | | <el-option |
| | | v-for="item in COMPARISON_OPERATORS" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | </div> |
| | | <div class="mr-2"> |
| | | <el-input v-model="rule.rightSide" style="width: 160px" /> |
| | | </div> |
| | | <div class="mr-1 flex items-center" v-if="condition.rules.length > 1"> |
| | | <Icon |
| | | icon="ep:delete" |
| | | :size="18" |
| | | @click="deleteConditionRule(condition, rIdx)" |
| | | /> |
| | | </div> |
| | | <div class="flex items-center"> |
| | | <Icon icon="ep:plus" :size="18" @click="addConditionRule(condition, rIdx)" /> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | </el-space> |
| | | <div title="添加条件组" class="mt-4 cursor-pointer"> |
| | | <Icon color="#0089ff" icon="ep:plus" :size="24" @click="addConditionGroup" /> |
| | | </div> |
| | | </el-form-item> |
| | | </el-form> |
| | | </div> |
| | | </div> |
| | | <template #footer> |
| | | <el-divider /> |
| | | <div> |
| | | <el-button type="primary" @click="saveConfig">确 定</el-button> |
| | | <el-button @click="closeDrawer">取 消</el-button> |
| | | </div> |
| | | </template> |
| | | </el-drawer> |
| | | </template> |
| | | <script setup lang="ts"> |
| | | import { |
| | | SimpleFlowNode, |
| | | CONDITION_CONFIG_TYPES, |
| | | ConditionType, |
| | | COMPARISON_OPERATORS, |
| | | ConditionGroup, |
| | | Condition, |
| | | ConditionRule, |
| | | ProcessVariableEnum |
| | | } from '../consts' |
| | | import { getDefaultConditionNodeName } from '../utils' |
| | | import { useFormFields } from '../node' |
| | | import { BpmModelFormType } from '@/utils/constants' |
| | | const message = useMessage() // 消息弹窗 |
| | | defineOptions({ |
| | | name: 'ConditionNodeConfig' |
| | | }) |
| | | const formType = inject<Ref<number>>('formType') // 表单类型 |
| | | const conditionConfigTypes = computed(() => { |
| | | return CONDITION_CONFIG_TYPES.filter((item) => { |
| | | // 业务表单暂时去掉条件规则选项 |
| | | if (formType?.value === BpmModelFormType.CUSTOM && item.value === ConditionType.RULE) { |
| | | return false |
| | | } else { |
| | | return true |
| | | } |
| | | }) |
| | | }) |
| | | |
| | | const props = defineProps({ |
| | | conditionNode: { |
| | | type: Object as () => SimpleFlowNode, |
| | | required: true |
| | | }, |
| | | nodeIndex: { |
| | | type: Number, |
| | | required: true |
| | | } |
| | | }) |
| | | const settingVisible = ref(false) |
| | | const open = () => { |
| | | if (currentNode.value.conditionType === ConditionType.RULE) { |
| | | if (currentNode.value.conditionGroups) { |
| | | conditionGroups.value = currentNode.value.conditionGroups |
| | | } |
| | | } |
| | | settingVisible.value = true |
| | | } |
| | | |
| | | watch( |
| | | () => props.conditionNode, |
| | | (newValue) => { |
| | | currentNode.value = newValue |
| | | } |
| | | ) |
| | | // 显示名称输入框 |
| | | const showInput = ref(false) |
| | | |
| | | const clickIcon = () => { |
| | | showInput.value = true |
| | | } |
| | | // 输入框失去焦点 |
| | | const blurEvent = () => { |
| | | showInput.value = false |
| | | currentNode.value.name = |
| | | currentNode.value.name || |
| | | getDefaultConditionNodeName(props.nodeIndex, currentNode.value?.defaultFlow) |
| | | } |
| | | |
| | | const currentNode = ref<SimpleFlowNode>(props.conditionNode) |
| | | |
| | | defineExpose({ open }) // 提供 open 方法,用于打开弹窗 |
| | | |
| | | // 关闭 |
| | | const closeDrawer = () => { |
| | | settingVisible.value = false |
| | | } |
| | | |
| | | const handleClose = async (done: (cancel?: boolean) => void) => { |
| | | const isSuccess = await saveConfig() |
| | | if (!isSuccess) { |
| | | done(true) // 传入 true 阻止关闭 |
| | | } else { |
| | | done() |
| | | } |
| | | } |
| | | // 表单校验规则 |
| | | const formRules = reactive({ |
| | | conditionType: [{ required: true, message: '配置方式不能为空', trigger: 'blur' }], |
| | | conditionExpression: [{ required: true, message: '条件表达式不能为空', trigger: 'blur' }] |
| | | }) |
| | | const formRef = ref() // 表单 Ref |
| | | |
| | | // 保存配置 |
| | | const saveConfig = async () => { |
| | | if (!currentNode.value.defaultFlow) { |
| | | // 校验表单 |
| | | if (!formRef) return false |
| | | const valid = await formRef.value.validate() |
| | | if (!valid) return false |
| | | const showText = getShowText() |
| | | if (!showText) { |
| | | return false |
| | | } |
| | | currentNode.value.showText = showText |
| | | if (currentNode.value.conditionType === ConditionType.EXPRESSION) { |
| | | currentNode.value.conditionGroups = undefined |
| | | } |
| | | if (currentNode.value.conditionType === ConditionType.RULE) { |
| | | currentNode.value.conditionExpression = undefined |
| | | currentNode.value.conditionGroups = conditionGroups.value |
| | | } |
| | | } |
| | | settingVisible.value = false |
| | | return true |
| | | } |
| | | const getShowText = (): string => { |
| | | let showText = '' |
| | | if (currentNode.value.conditionType === ConditionType.EXPRESSION) { |
| | | if (currentNode.value.conditionExpression) { |
| | | showText = `表达式:${currentNode.value.conditionExpression}` |
| | | } |
| | | } |
| | | if (currentNode.value.conditionType === ConditionType.RULE) { |
| | | // 条件组是否为与关系 |
| | | const groupAnd = conditionGroups.value.and |
| | | let warningMesg: undefined | string = undefined |
| | | const conditionGroup = conditionGroups.value.conditions.map((item) => { |
| | | return ( |
| | | '(' + |
| | | item.rules |
| | | .map((rule) => { |
| | | if (rule.leftSide && rule.rightSide) { |
| | | return ( |
| | | getFieldTitle(rule.leftSide) + ' ' + getOpName(rule.opCode) + ' ' + rule.rightSide |
| | | ) |
| | | } else { |
| | | // 有一条规则不完善。提示错误 |
| | | warningMesg = '请完善条件规则' |
| | | return '' |
| | | } |
| | | }) |
| | | .join(item.and ? ' 且 ' : ' 或 ') + |
| | | ' ) ' |
| | | ) |
| | | }) |
| | | if (warningMesg) { |
| | | message.warning(warningMesg) |
| | | showText = '' |
| | | } else { |
| | | showText = conditionGroup.join(groupAnd ? ' 且 ' : ' 或 ') |
| | | } |
| | | } |
| | | return showText |
| | | } |
| | | |
| | | // 改变条件配置方式 |
| | | const changeConditionType = () => {} |
| | | |
| | | const conditionGroups = ref<ConditionGroup>({ |
| | | and: true, |
| | | conditions: [ |
| | | { |
| | | and: true, |
| | | rules: [ |
| | | { |
| | | type: 1, |
| | | opName: '等于', |
| | | opCode: '==', |
| | | leftSide: '', |
| | | rightSide: '' |
| | | } |
| | | ] |
| | | } |
| | | ] |
| | | }) |
| | | // 添加条件组 |
| | | const addConditionGroup = () => { |
| | | const condition = { |
| | | and: true, |
| | | rules: [ |
| | | { |
| | | type: 1, |
| | | opName: '等于', |
| | | opCode: '==', |
| | | leftSide: '', |
| | | rightSide: '' |
| | | } |
| | | ] |
| | | } |
| | | conditionGroups.value.conditions.push(condition) |
| | | } |
| | | // 删除条件组 |
| | | const deleteConditionGroup = (idx: number) => { |
| | | conditionGroups.value.conditions.splice(idx, 1) |
| | | } |
| | | |
| | | // 添加条件规则 |
| | | const addConditionRule = (condition: Condition, idx: number) => { |
| | | const rule: ConditionRule = { |
| | | type: 1, |
| | | opName: '等于', |
| | | opCode: '==', |
| | | leftSide: '', |
| | | rightSide: '' |
| | | } |
| | | condition.rules.splice(idx + 1, 0, rule) |
| | | } |
| | | |
| | | const deleteConditionRule = (condition: Condition, idx: number) => { |
| | | condition.rules.splice(idx, 1) |
| | | } |
| | | const fieldsInfo = useFormFields() |
| | | |
| | | /** 条件规则可选择的表单字段 */ |
| | | const fieldOptions = computed(() => { |
| | | const fieldsCopy = fieldsInfo.slice() |
| | | // 固定添加发起人 ID 字段 |
| | | fieldsCopy.unshift({ |
| | | field: ProcessVariableEnum.START_USER_ID, |
| | | title: '发起人', |
| | | required: true |
| | | }) |
| | | return fieldsCopy |
| | | }) |
| | | |
| | | /** 获取字段名称 */ |
| | | const getFieldTitle = (field: string) => { |
| | | const item = fieldOptions.value.find((item) => item.field === field) |
| | | return item?.title |
| | | } |
| | | |
| | | /** 获取操作符名称 */ |
| | | const getOpName = (opCode: string): string => { |
| | | const opName = COMPARISON_OPERATORS.find((item: any) => item.value === opCode) |
| | | return opName?.label |
| | | } |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .condition-group-tool { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | width: 500px; |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .condition-group { |
| | | position: relative; |
| | | |
| | | &:hover { |
| | | border-color: #0089ff; |
| | | |
| | | .condition-group-delete { |
| | | opacity: 1; |
| | | } |
| | | } |
| | | |
| | | .condition-group-delete { |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | display: flex; |
| | | cursor: pointer; |
| | | opacity: 0; |
| | | } |
| | | } |
| | | |
| | | ::v-deep(.el-card__header) { |
| | | padding: 8px var(--el-card-padding); |
| | | border-bottom: 1px solid var(--el-card-border-color); |
| | | box-sizing: border-box; |
| | | } |
| | | </style> |
对比新文件 |
| | |
| | | <template> |
| | | <el-drawer |
| | | :append-to-body="true" |
| | | v-model="settingVisible" |
| | | :show-close="false" |
| | | :size="550" |
| | | :before-close="saveConfig" |
| | | > |
| | | <template #header> |
| | | <div class="config-header"> |
| | | <input |
| | | v-if="showInput" |
| | | type="text" |
| | | class="config-editable-input" |
| | | @blur="blurEvent()" |
| | | v-mountedFocus |
| | | v-model="nodeName" |
| | | :placeholder="nodeName" |
| | | /> |
| | | <div v-else class="node-name"> |
| | | {{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" /> |
| | | </div> |
| | | <div class="divide-line"></div> |
| | | </div> |
| | | </template> |
| | | <el-tabs type="border-card" v-model="activeTabName"> |
| | | <el-tab-pane label="抄送人" name="user"> |
| | | <div> |
| | | <el-form ref="formRef" :model="configForm" label-position="top" :rules="formRules"> |
| | | <el-form-item label="抄送人设置" prop="candidateStrategy"> |
| | | <el-radio-group |
| | | v-model="configForm.candidateStrategy" |
| | | @change="changeCandidateStrategy" |
| | | > |
| | | <el-radio |
| | | v-for="(dict, index) in copyUserStrategies" |
| | | :key="index" |
| | | :value="dict.value" |
| | | :label="dict.value" |
| | | > |
| | | {{ dict.label }} |
| | | </el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | |
| | | <el-form-item |
| | | v-if="configForm.candidateStrategy == CandidateStrategy.ROLE" |
| | | label="指定角色" |
| | | prop="roleIds" |
| | | > |
| | | <el-select v-model="configForm.roleIds" clearable multiple style="width: 100%"> |
| | | <el-option |
| | | v-for="item in roleOptions" |
| | | :key="item.id" |
| | | :label="item.name" |
| | | :value="item.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if=" |
| | | configForm.candidateStrategy == CandidateStrategy.DEPT_MEMBER || |
| | | configForm.candidateStrategy == CandidateStrategy.DEPT_LEADER || |
| | | configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER |
| | | " |
| | | label="指定部门" |
| | | prop="deptIds" |
| | | span="24" |
| | | > |
| | | <el-tree-select |
| | | ref="treeRef" |
| | | v-model="configForm.deptIds" |
| | | :data="deptTreeOptions" |
| | | :props="defaultProps" |
| | | empty-text="加载中,请稍后" |
| | | multiple |
| | | node-key="id" |
| | | style="width: 100%" |
| | | show-checkbox |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="configForm.candidateStrategy == CandidateStrategy.POST" |
| | | label="指定岗位" |
| | | prop="postIds" |
| | | span="24" |
| | | > |
| | | <el-select v-model="configForm.postIds" clearable multiple style="width: 100%"> |
| | | <el-option |
| | | v-for="item in postOptions" |
| | | :key="item.id" |
| | | :label="item.name" |
| | | :value="item.id!" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="configForm.candidateStrategy == CandidateStrategy.USER" |
| | | label="指定用户" |
| | | prop="userIds" |
| | | span="24" |
| | | > |
| | | <el-select v-model="configForm.userIds" clearable multiple style="width: 100%"> |
| | | <el-option |
| | | v-for="item in userOptions" |
| | | :key="item.id" |
| | | :label="item.nickname" |
| | | :value="item.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="configForm.candidateStrategy === CandidateStrategy.USER_GROUP" |
| | | label="指定用户组" |
| | | prop="userGroups" |
| | | > |
| | | <el-select v-model="configForm.userGroups" clearable multiple style="width: 100%"> |
| | | <el-option |
| | | v-for="item in userGroupOptions" |
| | | :key="item.id" |
| | | :label="item.name" |
| | | :value="item.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="configForm.candidateStrategy === CandidateStrategy.FORM_USER" |
| | | label="表单内用户字段" |
| | | prop="formUser" |
| | | > |
| | | <el-select v-model="configForm.formUser" clearable style="width: 100%"> |
| | | <el-option |
| | | v-for="(item, idx) in userFieldOnFormOptions" |
| | | :key="idx" |
| | | :label="item.title" |
| | | :value="item.field" |
| | | :disabled ="!item.required" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="configForm.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER" |
| | | label="表单内部门字段" |
| | | prop="formDept" |
| | | > |
| | | <el-select v-model="configForm.formDept" clearable style="width: 100%"> |
| | | <el-option |
| | | v-for="(item, idx) in deptFieldOnFormOptions" |
| | | :key="idx" |
| | | :label="item.title" |
| | | :value="item.field" |
| | | :disabled ="!item.required" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if=" |
| | | configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER || |
| | | configForm.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER || |
| | | configForm.candidateStrategy == |
| | | CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER || |
| | | configForm.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER |
| | | " |
| | | :label="deptLevelLabel!" |
| | | prop="deptLevel" |
| | | span="24" |
| | | > |
| | | <el-select v-model="configForm.deptLevel" clearable> |
| | | <el-option |
| | | v-for="(item, index) in MULTI_LEVEL_DEPT" |
| | | :key="index" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="configForm.candidateStrategy === CandidateStrategy.EXPRESSION" |
| | | label="流程表达式" |
| | | prop="expression" |
| | | > |
| | | <el-input |
| | | type="textarea" |
| | | v-model="configForm.expression" |
| | | clearable |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-form> |
| | | </div> |
| | | </el-tab-pane> |
| | | <el-tab-pane label="表单字段权限" name="fields" v-if="formType === 10"> |
| | | <div class="field-setting-pane"> |
| | | <div class="field-setting-desc">字段权限</div> |
| | | <div class="field-permit-title"> |
| | | <div class="setting-title-label first-title"> 字段名称 </div> |
| | | <div class="other-titles"> |
| | | <span class="setting-title-label">只读</span> |
| | | <span class="setting-title-label">可编辑</span> |
| | | <span class="setting-title-label">隐藏</span> |
| | | </div> |
| | | </div> |
| | | <div |
| | | class="field-setting-item" |
| | | v-for="(item, index) in fieldsPermissionConfig" |
| | | :key="index" |
| | | > |
| | | <div class="field-setting-item-label"> {{ item.title }} </div> |
| | | <el-radio-group class="field-setting-item-group" v-model="item.permission"> |
| | | <div class="item-radio-wrap"> |
| | | <el-radio |
| | | :value="FieldPermissionType.READ" |
| | | size="large" |
| | | :label="FieldPermissionType.WRITE" |
| | | ><span></span |
| | | ></el-radio> |
| | | </div> |
| | | <div class="item-radio-wrap"> |
| | | <el-radio |
| | | :value="FieldPermissionType.WRITE" |
| | | size="large" |
| | | :label="FieldPermissionType.WRITE" |
| | | disabled |
| | | ><span></span |
| | | ></el-radio> |
| | | </div> |
| | | <div class="item-radio-wrap"> |
| | | <el-radio |
| | | :value="FieldPermissionType.NONE" |
| | | size="large" |
| | | :label="FieldPermissionType.NONE" |
| | | ><span></span |
| | | ></el-radio> |
| | | </div> |
| | | </el-radio-group> |
| | | </div> |
| | | </div> |
| | | </el-tab-pane> |
| | | </el-tabs> |
| | | <template #footer> |
| | | <el-divider /> |
| | | <div> |
| | | <el-button type="primary" @click="saveConfig">确 定</el-button> |
| | | <el-button @click="closeDrawer">取 消</el-button> |
| | | </div> |
| | | </template> |
| | | </el-drawer> |
| | | </template> |
| | | <script setup lang="ts"> |
| | | import { |
| | | SimpleFlowNode, |
| | | CandidateStrategy, |
| | | NodeType, |
| | | CANDIDATE_STRATEGY, |
| | | FieldPermissionType, |
| | | MULTI_LEVEL_DEPT |
| | | } from '../consts' |
| | | import { |
| | | useWatchNode, |
| | | useDrawer, |
| | | useNodeName, |
| | | useFormFieldsPermission, |
| | | useNodeForm, |
| | | CopyTaskFormType |
| | | } from '../node' |
| | | import { defaultProps } from '@/utils/tree' |
| | | defineOptions({ |
| | | name: 'CopyTaskNodeConfig' |
| | | }) |
| | | const props = defineProps({ |
| | | flowNode: { |
| | | type: Object as () => SimpleFlowNode, |
| | | required: true |
| | | } |
| | | }) |
| | | const deptLevelLabel = computed(() => { |
| | | let label = '部门负责人来源' |
| | | if (configForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER) { |
| | | label = label + '(指定部门向上)' |
| | | } else { |
| | | label = label + '(发起人部门向上)' |
| | | } |
| | | return label |
| | | }) |
| | | // 抽屉配置 |
| | | const { settingVisible, closeDrawer, openDrawer } = useDrawer() |
| | | // 当前节点 |
| | | const currentNode = useWatchNode(props) |
| | | // 节点名称 |
| | | const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.COPY_TASK_NODE) |
| | | // 激活的 Tab 标签页 |
| | | const activeTabName = ref('user') |
| | | // 表单字段权限配置 |
| | | const { formType, fieldsPermissionConfig, formFieldOptions, getNodeConfigFormFields } = |
| | | useFormFieldsPermission(FieldPermissionType.READ) |
| | | // 表单内用户字段选项, 必须是必填和用户选择器 |
| | | const userFieldOnFormOptions = computed(() => { |
| | | return formFieldOptions.filter((item) => item.type === 'UserSelect') |
| | | }) |
| | | // 表单内部门字段选项, 必须是必填和部门选择器 |
| | | const deptFieldOnFormOptions = computed(() => { |
| | | return formFieldOptions.filter((item) => item.type === 'DeptSelect') |
| | | }) |
| | | // 抄送人表单配置 |
| | | const formRef = ref() // 表单 Ref |
| | | // 表单校验规则 |
| | | const formRules = reactive({ |
| | | candidateStrategy: [{ required: true, message: '抄送人设置不能为空', trigger: 'change' }], |
| | | userIds: [{ required: true, message: '用户不能为空', trigger: 'change' }], |
| | | roleIds: [{ required: true, message: '角色不能为空', trigger: 'change' }], |
| | | deptIds: [{ required: true, message: '部门不能为空', trigger: 'change' }], |
| | | userGroups: [{ required: true, message: '用户组不能为空', trigger: 'change' }], |
| | | postIds: [{ required: true, message: '岗位不能为空', trigger: 'change' }], |
| | | formUser: [{ required: true, message: '表单内用户字段不能为空', trigger: 'change' }], |
| | | formDept: [{ required: true, message: '表单内部门字段不能为空', trigger: 'change' }], |
| | | expression: [{ required: true, message: '流程表达式不能为空', trigger: 'blur' }] |
| | | }) |
| | | |
| | | const { |
| | | configForm: tempConfigForm, |
| | | roleOptions, |
| | | postOptions, |
| | | userOptions, |
| | | userGroupOptions, |
| | | deptTreeOptions, |
| | | getShowText, |
| | | handleCandidateParam, |
| | | parseCandidateParam |
| | | } = useNodeForm(NodeType.COPY_TASK_NODE) |
| | | const configForm = tempConfigForm as Ref<CopyTaskFormType> |
| | | // 抄送人策略, 去掉发起人自选 和 发起人自己 |
| | | const copyUserStrategies = computed(() => { |
| | | return CANDIDATE_STRATEGY.filter((item) => item.value !== CandidateStrategy.START_USER) |
| | | }) |
| | | // 改变抄送人设置策略 |
| | | const changeCandidateStrategy = () => { |
| | | configForm.value.userIds = [] |
| | | configForm.value.deptIds = [] |
| | | configForm.value.roleIds = [] |
| | | configForm.value.postIds = [] |
| | | configForm.value.userGroups = [] |
| | | configForm.value.deptLevel = 1 |
| | | configForm.value.formUser = '' |
| | | } |
| | | // 保存配置 |
| | | const saveConfig = async () => { |
| | | activeTabName.value = 'user' |
| | | if (!formRef) return false |
| | | const valid = await formRef.value.validate() |
| | | if (!valid) return false |
| | | const showText = getShowText() |
| | | if (!showText) return false |
| | | currentNode.value.name = nodeName.value! |
| | | currentNode.value.candidateParam = handleCandidateParam() |
| | | currentNode.value.candidateStrategy = configForm.value.candidateStrategy |
| | | currentNode.value.showText = showText |
| | | currentNode.value.fieldsPermission = fieldsPermissionConfig.value |
| | | settingVisible.value = false |
| | | return true |
| | | } |
| | | // 显示抄送节点配置, 由父组件传过来 |
| | | const showCopyTaskNodeConfig = (node: SimpleFlowNode) => { |
| | | nodeName.value = node.name |
| | | // 抄送人设置 |
| | | configForm.value.candidateStrategy = node.candidateStrategy! |
| | | parseCandidateParam(node.candidateStrategy!, node?.candidateParam) |
| | | // 表单字段权限 |
| | | getNodeConfigFormFields(node.fieldsPermission) |
| | | } |
| | | |
| | | defineExpose({ openDrawer, showCopyTaskNodeConfig }) // 暴露方法给父组件 |
| | | </script> |
| | | |
| | | <style lang="scss" scoped></style> |
对比新文件 |
| | |
| | | <template> |
| | | <el-drawer |
| | | :append-to-body="true" |
| | | v-model="settingVisible" |
| | | :show-close="false" |
| | | :size="550" |
| | | :before-close="saveConfig" |
| | | > |
| | | <template #header> |
| | | <div class="config-header"> |
| | | <input |
| | | v-if="showInput" |
| | | type="text" |
| | | class="config-editable-input" |
| | | @blur="blurEvent()" |
| | | v-mountedFocus |
| | | v-model="nodeName" |
| | | :placeholder="nodeName" |
| | | /> |
| | | <div v-else class="node-name"> |
| | | {{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" /> |
| | | </div> |
| | | <div class="divide-line"></div> |
| | | </div> |
| | | </template> |
| | | <div> |
| | | <el-form ref="formRef" :model="configForm" label-position="top" :rules="formRules"> |
| | | <el-form-item label="延迟时间" prop="delayType"> |
| | | <el-radio-group v-model="configForm.delayType"> |
| | | <el-radio-button |
| | | v-for="item in DELAY_TYPE" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item v-if="configForm.delayType === DelayTypeEnum.FIXED_TIME_DURATION"> |
| | | <el-form-item prop="timeDuration"> |
| | | <el-input-number |
| | | class="mr-2" |
| | | :style="{ width: '100px' }" |
| | | v-model="configForm.timeDuration" |
| | | :min="1" |
| | | controls-position="right" |
| | | /> |
| | | </el-form-item> |
| | | <el-select v-model="configForm.timeUnit" class="mr-2" :style="{ width: '100px' }"> |
| | | <el-option |
| | | v-for="item in TIME_UNIT_TYPES" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | <el-text>后进入下一节点</el-text> |
| | | </el-form-item> |
| | | <el-form-item v-if="configForm.delayType === DelayTypeEnum.FIXED_DATE_TIME" prop="dateTime"> |
| | | <el-date-picker |
| | | class="mr-2" |
| | | v-model="configForm.dateTime" |
| | | type="datetime" |
| | | placeholder="请选择日期和时间" |
| | | value-format="YYYY-MM-DDTHH:mm:ss" |
| | | /> |
| | | <el-text>后进入下一节点</el-text> |
| | | </el-form-item> |
| | | </el-form> |
| | | </div> |
| | | <template #footer> |
| | | <el-divider /> |
| | | <div> |
| | | <el-button type="primary" @click="saveConfig">确 定</el-button> |
| | | <el-button @click="closeDrawer">取 消</el-button> |
| | | </div> |
| | | </template> |
| | | </el-drawer> |
| | | </template> |
| | | <script setup lang="ts"> |
| | | import { |
| | | SimpleFlowNode, |
| | | NodeType, |
| | | TIME_UNIT_TYPES, |
| | | TimeUnitType, |
| | | DelayTypeEnum, |
| | | DELAY_TYPE |
| | | } from '../consts' |
| | | import { useWatchNode, useDrawer, useNodeName } from '../node' |
| | | import { convertTimeUnit } from '../utils' |
| | | defineOptions({ |
| | | name: 'DelayTimerNodeConfig' |
| | | }) |
| | | const props = defineProps({ |
| | | flowNode: { |
| | | type: Object as () => SimpleFlowNode, |
| | | required: true |
| | | } |
| | | }) |
| | | // 抽屉配置 |
| | | const { settingVisible, closeDrawer, openDrawer } = useDrawer() |
| | | // 当前节点 |
| | | const currentNode = useWatchNode(props) |
| | | // 节点名称 |
| | | const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.DELAY_TIMER_NODE) |
| | | // 抄送人表单配置 |
| | | const formRef = ref() // 表单 Ref |
| | | // 表单校验规则 |
| | | const formRules = reactive({ |
| | | delayType: [{ required: true, message: '延迟时间不能为空', trigger: 'change' }], |
| | | timeDuration: [{ required: true, message: '延迟时间不能为空', trigger: 'change' }], |
| | | dateTime: [{ required: true, message: '延迟时间不能为空', trigger: 'change' }] |
| | | }) |
| | | // 配置表单数据 |
| | | const configForm = ref({ |
| | | delayType: DelayTypeEnum.FIXED_TIME_DURATION, |
| | | timeDuration: 1, |
| | | timeUnit: TimeUnitType.HOUR, |
| | | dateTime: '' |
| | | }) |
| | | // 保存配置 |
| | | const saveConfig = async () => { |
| | | if (!formRef) return false |
| | | const valid = await formRef.value.validate() |
| | | if (!valid) return false |
| | | const showText = getShowText() |
| | | if (!showText) return false |
| | | currentNode.value.showText = showText |
| | | if (configForm.value.delayType === DelayTypeEnum.FIXED_TIME_DURATION) { |
| | | currentNode.value.delaySetting = { |
| | | delayType: configForm.value.delayType, |
| | | delayTime: getIsoTimeDuration() |
| | | } |
| | | } |
| | | if (configForm.value.delayType === DelayTypeEnum.FIXED_DATE_TIME) { |
| | | currentNode.value.delaySetting = { |
| | | delayType: configForm.value.delayType, |
| | | delayTime: configForm.value.dateTime |
| | | } |
| | | } |
| | | settingVisible.value = false |
| | | return true |
| | | } |
| | | const getShowText = (): string => { |
| | | let showText = '' |
| | | if (configForm.value.delayType === DelayTypeEnum.FIXED_TIME_DURATION) { |
| | | showText = `延迟${configForm.value.timeDuration}${TIME_UNIT_TYPES.find((item) => item.value === configForm.value.timeUnit).label}` |
| | | } |
| | | if (configForm.value.delayType === DelayTypeEnum.FIXED_DATE_TIME) { |
| | | showText = `延迟至${configForm.value.dateTime.replace('T', ' ')}` |
| | | } |
| | | return showText |
| | | } |
| | | const getIsoTimeDuration = () => { |
| | | let strTimeDuration = 'PT' |
| | | if (configForm.value.timeUnit === TimeUnitType.MINUTE) { |
| | | strTimeDuration += configForm.value.timeDuration + 'M' |
| | | } |
| | | if (configForm.value.timeUnit === TimeUnitType.HOUR) { |
| | | strTimeDuration += configForm.value.timeDuration + 'H' |
| | | } |
| | | if (configForm.value.timeUnit === TimeUnitType.DAY) { |
| | | strTimeDuration += configForm.value.timeDuration + 'D' |
| | | } |
| | | return strTimeDuration |
| | | } |
| | | // 显示延迟器节点配置, 由父组件传过来 |
| | | const showDelayTimerNodeConfig = (node: SimpleFlowNode) => { |
| | | nodeName.value = node.name |
| | | if (node.delaySetting) { |
| | | configForm.value.delayType = node.delaySetting.delayType |
| | | // 固定时长 |
| | | if (configForm.value.delayType === DelayTypeEnum.FIXED_TIME_DURATION) { |
| | | const strTimeDuration = node.delaySetting.delayTime |
| | | let parseTime = strTimeDuration.slice(2, strTimeDuration.length - 1) |
| | | let parseTimeUnit = strTimeDuration.slice(strTimeDuration.length - 1) |
| | | configForm.value.timeDuration = parseInt(parseTime) |
| | | configForm.value.timeUnit = convertTimeUnit(parseTimeUnit) |
| | | } |
| | | // 固定日期时间 |
| | | if (configForm.value.delayType === DelayTypeEnum.FIXED_DATE_TIME) { |
| | | configForm.value.dateTime = node.delaySetting.delayTime |
| | | } |
| | | } |
| | | } |
| | | |
| | | defineExpose({ openDrawer, showDelayTimerNodeConfig }) // 暴露方法给父组件 |
| | | </script> |
| | | |
| | | <style lang="scss" scoped></style> |
对比新文件 |
| | |
| | | <template> |
| | | <el-drawer |
| | | :append-to-body="true" |
| | | v-model="settingVisible" |
| | | :show-close="false" |
| | | :size="550" |
| | | :before-close="saveConfig" |
| | | > |
| | | <template #header> |
| | | <div class="config-header"> |
| | | <input |
| | | v-if="showInput" |
| | | type="text" |
| | | class="config-editable-input" |
| | | @blur="blurEvent()" |
| | | v-mountedFocus |
| | | v-model="nodeName" |
| | | :placeholder="nodeName" |
| | | /> |
| | | <div v-else class="node-name"> |
| | | {{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" /> |
| | | </div> |
| | | <div class="divide-line"></div> |
| | | </div> |
| | | </template> |
| | | <el-tabs type="border-card" v-model="activeTabName"> |
| | | <el-tab-pane label="权限" name="user"> |
| | | <el-text v-if="!startUserIds || startUserIds.length === 0"> 全部成员可以发起流程 </el-text> |
| | | <el-text v-else-if="startUserIds.length == 1"> |
| | | {{ getUserNicknames(startUserIds) }} 可发起流程 |
| | | </el-text> |
| | | <el-text v-else> |
| | | <el-tooltip |
| | | class="box-item" |
| | | effect="dark" |
| | | placement="top" |
| | | :content="getUserNicknames(startUserIds)" |
| | | > |
| | | {{ getUserNicknames(startUserIds.slice(0,2)) }} 等 {{ startUserIds.length }} 人可发起流程 |
| | | </el-tooltip> |
| | | </el-text> |
| | | </el-tab-pane> |
| | | <el-tab-pane label="表单字段权限" name="fields" v-if="formType === 10"> |
| | | <div class="field-setting-pane"> |
| | | <div class="field-setting-desc">字段权限</div> |
| | | <div class="field-permit-title"> |
| | | <div class="setting-title-label first-title"> 字段名称 </div> |
| | | <div class="other-titles"> |
| | | <span class="setting-title-label">只读</span> |
| | | <span class="setting-title-label">可编辑</span> |
| | | <span class="setting-title-label">隐藏</span> |
| | | </div> |
| | | </div> |
| | | <div |
| | | class="field-setting-item" |
| | | v-for="(item, index) in fieldsPermissionConfig" |
| | | :key="index" |
| | | > |
| | | <div class="field-setting-item-label"> {{ item.title }} </div> |
| | | <el-radio-group class="field-setting-item-group" v-model="item.permission"> |
| | | <div class="item-radio-wrap"> |
| | | <el-radio |
| | | :value="FieldPermissionType.READ" |
| | | size="large" |
| | | :label="FieldPermissionType.READ" |
| | | ><span></span |
| | | ></el-radio> |
| | | </div> |
| | | <div class="item-radio-wrap"> |
| | | <el-radio |
| | | :value="FieldPermissionType.WRITE" |
| | | size="large" |
| | | :label="FieldPermissionType.WRITE" |
| | | ><span></span |
| | | ></el-radio> |
| | | </div> |
| | | <div class="item-radio-wrap"> |
| | | <el-radio |
| | | :value="FieldPermissionType.NONE" |
| | | size="large" |
| | | :label="FieldPermissionType.NONE" |
| | | ><span></span |
| | | ></el-radio> |
| | | </div> |
| | | </el-radio-group> |
| | | </div> |
| | | </div> |
| | | </el-tab-pane> |
| | | </el-tabs> |
| | | <template #footer> |
| | | <el-divider /> |
| | | <div> |
| | | <el-button type="primary" @click="saveConfig">确 定</el-button> |
| | | <el-button @click="closeDrawer">取 消</el-button> |
| | | </div> |
| | | </template> |
| | | </el-drawer> |
| | | </template> |
| | | <script setup lang="ts"> |
| | | import { SimpleFlowNode, NodeType, FieldPermissionType, START_USER_BUTTON_SETTING } from '../consts' |
| | | import { useWatchNode, useDrawer, useNodeName, useFormFieldsPermission } from '../node' |
| | | import * as UserApi from '@/api/system/user' |
| | | defineOptions({ |
| | | name: 'StartUserNodeConfig' |
| | | }) |
| | | const props = defineProps({ |
| | | flowNode: { |
| | | type: Object as () => SimpleFlowNode, |
| | | required: true |
| | | } |
| | | }) |
| | | // 可发起流程的用户编号 |
| | | const startUserIds = inject<Ref<any[]>>('startUserIds') |
| | | // 用户列表 |
| | | const userOptions = inject<Ref<UserApi.UserVO[]>>('userList') |
| | | // 抽屉配置 |
| | | const { settingVisible, closeDrawer, openDrawer } = useDrawer() |
| | | // 当前节点 |
| | | const currentNode = useWatchNode(props) |
| | | // 节点名称 |
| | | const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.COPY_TASK_NODE) |
| | | // 激活的 Tab 标签页 |
| | | const activeTabName = ref('user') |
| | | // 表单字段权限配置 |
| | | const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission( |
| | | FieldPermissionType.WRITE |
| | | ) |
| | | const getUserNicknames = (userIds: number[]): string => { |
| | | if (!userIds || userIds.length === 0) { |
| | | return '' |
| | | } |
| | | const nicknames: string[] = [] |
| | | userIds.forEach((userId) => { |
| | | const found = userOptions?.value.find((item) => item.id === userId) |
| | | if (found && found.nickname) { |
| | | nicknames.push(found.nickname) |
| | | } |
| | | }) |
| | | return nicknames.join(',') |
| | | } |
| | | // 保存配置 |
| | | const saveConfig = async () => { |
| | | activeTabName.value = 'user' |
| | | currentNode.value.name = nodeName.value! |
| | | currentNode.value.showText = '已设置' |
| | | // 设置表单权限 |
| | | currentNode.value.fieldsPermission = fieldsPermissionConfig.value |
| | | // 设置发起人的按钮权限 |
| | | currentNode.value.buttonsSetting = START_USER_BUTTON_SETTING |
| | | settingVisible.value = false |
| | | return true |
| | | } |
| | | // 显示发起人节点配置, 由父组件传过来 |
| | | const showStartUserNodeConfig = (node: SimpleFlowNode) => { |
| | | nodeName.value = node.name |
| | | // 表单字段权限 |
| | | getNodeConfigFormFields(node.fieldsPermission) |
| | | } |
| | | |
| | | defineExpose({ openDrawer, showStartUserNodeConfig }) // 暴露方法给父组件 |
| | | </script> |
| | | |
| | | <style lang="scss" scoped></style> |
对比新文件 |
| | |
| | | <template> |
| | | <el-drawer |
| | | :append-to-body="true" |
| | | v-model="settingVisible" |
| | | :show-close="false" |
| | | :size="550" |
| | | :before-close="saveConfig" |
| | | class="justify-start" |
| | | > |
| | | <template #header> |
| | | <div class="config-header"> |
| | | <input |
| | | v-if="showInput" |
| | | type="text" |
| | | class="config-editable-input" |
| | | @blur="blurEvent()" |
| | | v-mountedFocus |
| | | v-model="nodeName" |
| | | :placeholder="nodeName" |
| | | /> |
| | | <div v-else class="node-name"> |
| | | {{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" /> |
| | | </div> |
| | | <div class="divide-line"></div> |
| | | </div> |
| | | </template> |
| | | <div class="flex flex-items-center mb-3"> |
| | | <span class="font-size-16px mr-3">审批类型 :</span> |
| | | <el-radio-group v-model="approveType"> |
| | | <el-radio |
| | | v-for="(item, index) in APPROVE_TYPE" |
| | | :key="index" |
| | | :value="item.value" |
| | | :label="item.value" |
| | | > |
| | | {{ item.label }} |
| | | </el-radio> |
| | | </el-radio-group> |
| | | </div> |
| | | <el-tabs type="border-card" v-model="activeTabName" v-if="approveType === ApproveType.USER"> |
| | | <el-tab-pane label="审批人" name="user"> |
| | | <div> |
| | | <el-form ref="formRef" :model="configForm" label-position="top" :rules="formRules"> |
| | | <el-form-item label="审批人设置" prop="candidateStrategy"> |
| | | <el-radio-group |
| | | v-model="configForm.candidateStrategy" |
| | | @change="changeCandidateStrategy" |
| | | > |
| | | <el-radio |
| | | v-for="(dict, index) in CANDIDATE_STRATEGY" |
| | | :key="index" |
| | | :value="dict.value" |
| | | :label="dict.value" |
| | | > |
| | | {{ dict.label }} |
| | | </el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="configForm.candidateStrategy == CandidateStrategy.ROLE" |
| | | label="指定角色" |
| | | prop="roleIds" |
| | | > |
| | | <el-select v-model="configForm.roleIds" clearable multiple style="width: 100%"> |
| | | <el-option |
| | | v-for="item in roleOptions" |
| | | :key="item.id" |
| | | :label="item.name" |
| | | :value="item.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if=" |
| | | configForm.candidateStrategy == CandidateStrategy.DEPT_MEMBER || |
| | | configForm.candidateStrategy == CandidateStrategy.DEPT_LEADER || |
| | | configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER |
| | | " |
| | | label="指定部门" |
| | | prop="deptIds" |
| | | span="24" |
| | | > |
| | | <el-tree-select |
| | | ref="treeRef" |
| | | v-model="configForm.deptIds" |
| | | :data="deptTreeOptions" |
| | | :props="defaultProps" |
| | | empty-text="加载中,请稍后" |
| | | multiple |
| | | node-key="id" |
| | | :check-strictly="true" |
| | | style="width: 100%" |
| | | show-checkbox |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="configForm.candidateStrategy == CandidateStrategy.POST" |
| | | label="指定岗位" |
| | | prop="postIds" |
| | | span="24" |
| | | > |
| | | <el-select v-model="configForm.postIds" clearable multiple style="width: 100%"> |
| | | <el-option |
| | | v-for="item in postOptions" |
| | | :key="item.id" |
| | | :label="item.name" |
| | | :value="item.id!" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="configForm.candidateStrategy == CandidateStrategy.USER" |
| | | label="指定用户" |
| | | prop="userIds" |
| | | span="24" |
| | | > |
| | | <el-select v-model="configForm.userIds" clearable multiple style="width: 100%"> |
| | | <el-option |
| | | v-for="item in userOptions" |
| | | :key="item.id" |
| | | :label="item.nickname" |
| | | :value="item.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="configForm.candidateStrategy === CandidateStrategy.USER_GROUP" |
| | | label="指定用户组" |
| | | prop="userGroups" |
| | | > |
| | | <el-select v-model="configForm.userGroups" clearable multiple style="width: 100%"> |
| | | <el-option |
| | | v-for="item in userGroupOptions" |
| | | :key="item.id" |
| | | :label="item.name" |
| | | :value="item.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="configForm.candidateStrategy === CandidateStrategy.FORM_USER" |
| | | label="表单内用户字段" |
| | | prop="formUser" |
| | | > |
| | | <el-select v-model="configForm.formUser" clearable style="width: 100%"> |
| | | <el-option |
| | | v-for="(item, idx) in userFieldOnFormOptions" |
| | | :key="idx" |
| | | :label="item.title" |
| | | :value="item.field" |
| | | :disabled ="!item.required" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="configForm.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER" |
| | | label="表单内部门字段" |
| | | prop="formDept" |
| | | > |
| | | <el-select v-model="configForm.formDept" clearable style="width: 100%"> |
| | | <el-option |
| | | v-for="(item, idx) in deptFieldOnFormOptions" |
| | | :key="idx" |
| | | :label="item.title" |
| | | :value="item.field" |
| | | :disabled ="!item.required" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if=" |
| | | configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER || |
| | | configForm.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER || |
| | | configForm.candidateStrategy == |
| | | CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER || |
| | | configForm.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER |
| | | " |
| | | :label="deptLevelLabel!" |
| | | prop="deptLevel" |
| | | span="24" |
| | | > |
| | | <el-select v-model="configForm.deptLevel" clearable> |
| | | <el-option |
| | | v-for="(item, index) in MULTI_LEVEL_DEPT" |
| | | :key="index" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <!-- TODO @jason:后续要支持选择已经存好的表达式 --> |
| | | <el-form-item |
| | | v-if="configForm.candidateStrategy === CandidateStrategy.EXPRESSION" |
| | | label="流程表达式" |
| | | prop="expression" |
| | | > |
| | | <el-input |
| | | type="textarea" |
| | | v-model="configForm.expression" |
| | | clearable |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="多人审批方式" prop="approveMethod"> |
| | | <el-radio-group v-model="configForm.approveMethod" @change="approveMethodChanged"> |
| | | <div class="flex-col"> |
| | | <div |
| | | v-for="(item, index) in APPROVE_METHODS" |
| | | :key="index" |
| | | class="flex items-center" |
| | | > |
| | | <el-radio :value="item.value" :label="item.value"> |
| | | {{ item.label }} |
| | | </el-radio> |
| | | <el-form-item prop="approveRatio"> |
| | | <el-input-number |
| | | v-model="configForm.approveRatio" |
| | | :min="10" |
| | | :max="100" |
| | | :step="10" |
| | | size="small" |
| | | v-if=" |
| | | item.value === ApproveMethodType.APPROVE_BY_RATIO && |
| | | configForm.approveMethod === ApproveMethodType.APPROVE_BY_RATIO |
| | | " |
| | | /> |
| | | </el-form-item> |
| | | </div> |
| | | </div> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | |
| | | <el-divider content-position="left">审批人拒绝时</el-divider> |
| | | <el-form-item prop="rejectHandlerType"> |
| | | <el-radio-group v-model="configForm.rejectHandlerType"> |
| | | <div class="flex-col"> |
| | | <div v-for="(item, index) in REJECT_HANDLER_TYPES" :key="index"> |
| | | <el-radio :key="item.value" :value="item.value" :label="item.label" /> |
| | | </div> |
| | | </div> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="configForm.rejectHandlerType == RejectHandlerType.RETURN_USER_TASK" |
| | | label="驳回节点" |
| | | prop="returnNodeId" |
| | | > |
| | | <el-select v-model="configForm.returnNodeId" clearable style="width: 100%"> |
| | | <el-option |
| | | v-for="item in returnTaskList" |
| | | :key="item.id" |
| | | :label="item.name" |
| | | :value="item.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-divider content-position="left">审批人超时未处理时</el-divider> |
| | | <el-form-item label="启用开关" prop="timeoutHandlerEnable"> |
| | | <el-switch |
| | | v-model="configForm.timeoutHandlerEnable" |
| | | active-text="开启" |
| | | inactive-text="关闭" |
| | | @change="timeoutHandlerChange" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item |
| | | label="执行动作" |
| | | prop="timeoutHandlerType" |
| | | v-if="configForm.timeoutHandlerEnable" |
| | | > |
| | | <el-radio-group |
| | | v-model="configForm.timeoutHandlerType" |
| | | @change="timeoutHandlerTypeChanged" |
| | | > |
| | | <el-radio-button |
| | | v-for="item in TIMEOUT_HANDLER_TYPES" |
| | | :key="item.value" |
| | | :value="item.value" |
| | | :label="item.label" |
| | | /> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="超时时间设置" v-if="configForm.timeoutHandlerEnable"> |
| | | <span class="mr-2">当超过</span> |
| | | <el-form-item prop="timeDuration"> |
| | | <el-input-number |
| | | class="mr-2" |
| | | :style="{ width: '100px' }" |
| | | v-model="configForm.timeDuration" |
| | | :min="1" |
| | | controls-position="right" |
| | | /> |
| | | </el-form-item> |
| | | <el-select |
| | | v-model="timeUnit" |
| | | class="mr-2" |
| | | :style="{ width: '100px' }" |
| | | @change="timeUnitChange" |
| | | > |
| | | <el-option |
| | | v-for="item in TIME_UNIT_TYPES" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | 未处理 |
| | | </el-form-item> |
| | | <el-form-item |
| | | label="最大提醒次数" |
| | | prop="maxRemindCount" |
| | | v-if="configForm.timeoutHandlerEnable && configForm.timeoutHandlerType === 1" |
| | | > |
| | | <el-input-number v-model="configForm.maxRemindCount" :min="1" :max="10" /> |
| | | </el-form-item> |
| | | |
| | | <el-divider content-position="left">审批人为空时</el-divider> |
| | | <el-form-item prop="assignEmptyHandlerType"> |
| | | <el-radio-group v-model="configForm.assignEmptyHandlerType"> |
| | | <div class="flex-col"> |
| | | <div v-for="(item, index) in ASSIGN_EMPTY_HANDLER_TYPES" :key="index"> |
| | | <el-radio :key="item.value" :value="item.value" :label="item.label" /> |
| | | </div> |
| | | </div> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="configForm.assignEmptyHandlerType == AssignEmptyHandlerType.ASSIGN_USER" |
| | | label="指定用户" |
| | | prop="assignEmptyHandlerUserIds" |
| | | span="24" |
| | | > |
| | | <el-select |
| | | v-model="configForm.assignEmptyHandlerUserIds" |
| | | clearable |
| | | multiple |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="item in userOptions" |
| | | :key="item.id" |
| | | :label="item.nickname" |
| | | :value="item.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-divider content-position="left">审批人与提交人为同一人时</el-divider> |
| | | <el-form-item prop="assignStartUserHandlerType"> |
| | | <el-radio-group v-model="configForm.assignStartUserHandlerType"> |
| | | <div class="flex-col"> |
| | | <div v-for="(item, index) in ASSIGN_START_USER_HANDLER_TYPES" :key="index"> |
| | | <el-radio :key="item.value" :value="item.value" :label="item.label" /> |
| | | </div> |
| | | </div> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | </el-form> |
| | | </div> |
| | | </el-tab-pane> |
| | | <el-tab-pane label="操作按钮设置" name="buttons"> |
| | | <div class="button-setting-pane"> |
| | | <div class="button-setting-desc">操作按钮</div> |
| | | <div class="button-setting-title"> |
| | | <div class="button-title-label">操作按钮</div> |
| | | <div class="pl-4 button-title-label">显示名称</div> |
| | | <div class="button-title-label">启用</div> |
| | | </div> |
| | | <div class="button-setting-item" v-for="(item, index) in buttonsSetting" :key="index"> |
| | | <div class="button-setting-item-label"> {{ OPERATION_BUTTON_NAME.get(item.id) }} </div> |
| | | <div class="button-setting-item-label"> |
| | | <input |
| | | type="text" |
| | | class="editable-title-input" |
| | | @blur="btnDisplayNameBlurEvent(index)" |
| | | v-mountedFocus |
| | | v-model="item.displayName" |
| | | :placeholder="item.displayName" |
| | | v-if="btnDisplayNameEdit[index]" |
| | | /> |
| | | <el-button v-else text @click="changeBtnDisplayName(index)" |
| | | >{{ item.displayName }} <Icon icon="ep:edit" |
| | | /></el-button> |
| | | </div> |
| | | <div class="button-setting-item-label"> |
| | | <el-switch v-model="item.enable" /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </el-tab-pane> |
| | | <el-tab-pane label="表单字段权限" name="fields" v-if="formType === 10"> |
| | | <div class="field-setting-pane"> |
| | | <div class="field-setting-desc">字段权限</div> |
| | | <div class="field-permit-title"> |
| | | <div class="setting-title-label first-title"> 字段名称 </div> |
| | | <div class="other-titles"> |
| | | <span class="setting-title-label">只读</span> |
| | | <span class="setting-title-label">可编辑</span> |
| | | <span class="setting-title-label">隐藏</span> |
| | | </div> |
| | | </div> |
| | | <div |
| | | class="field-setting-item" |
| | | v-for="(item, index) in fieldsPermissionConfig" |
| | | :key="index" |
| | | > |
| | | <div class="field-setting-item-label"> {{ item.title }} </div> |
| | | <el-radio-group class="field-setting-item-group" v-model="item.permission"> |
| | | <div class="item-radio-wrap"> |
| | | <el-radio |
| | | :value="FieldPermissionType.READ" |
| | | size="large" |
| | | :label="FieldPermissionType.READ" |
| | | ><span></span |
| | | ></el-radio> |
| | | </div> |
| | | <div class="item-radio-wrap"> |
| | | <el-radio |
| | | :value="FieldPermissionType.WRITE" |
| | | size="large" |
| | | :label="FieldPermissionType.WRITE" |
| | | ><span></span |
| | | ></el-radio> |
| | | </div> |
| | | <div class="item-radio-wrap"> |
| | | <el-radio |
| | | :value="FieldPermissionType.NONE" |
| | | size="large" |
| | | :label="FieldPermissionType.NONE" |
| | | ><span></span |
| | | ></el-radio> |
| | | </div> |
| | | </el-radio-group> |
| | | </div> |
| | | </div> |
| | | </el-tab-pane> |
| | | </el-tabs> |
| | | <template #footer> |
| | | <el-divider /> |
| | | <div> |
| | | <el-button type="primary" @click="saveConfig">确 定</el-button> |
| | | <el-button @click="closeDrawer">取 消</el-button> |
| | | </div> |
| | | </template> |
| | | </el-drawer> |
| | | </template> |
| | | |
| | | <script setup lang="ts"> |
| | | import { |
| | | SimpleFlowNode, |
| | | APPROVE_TYPE, |
| | | ApproveType, |
| | | APPROVE_METHODS, |
| | | CandidateStrategy, |
| | | NodeType, |
| | | ApproveMethodType, |
| | | TimeUnitType, |
| | | RejectHandlerType, |
| | | TIMEOUT_HANDLER_TYPES, |
| | | TIME_UNIT_TYPES, |
| | | REJECT_HANDLER_TYPES, |
| | | DEFAULT_BUTTON_SETTING, |
| | | OPERATION_BUTTON_NAME, |
| | | ButtonSetting, |
| | | MULTI_LEVEL_DEPT, |
| | | CANDIDATE_STRATEGY, |
| | | ASSIGN_START_USER_HANDLER_TYPES, |
| | | TimeoutHandlerType, |
| | | ASSIGN_EMPTY_HANDLER_TYPES, |
| | | AssignEmptyHandlerType, |
| | | FieldPermissionType, |
| | | ProcessVariableEnum |
| | | } from '../consts' |
| | | |
| | | import { |
| | | useWatchNode, |
| | | useNodeName, |
| | | useFormFieldsPermission, |
| | | useNodeForm, |
| | | UserTaskFormType, |
| | | useDrawer |
| | | } from '../node' |
| | | import { defaultProps } from '@/utils/tree' |
| | | import { cloneDeep } from 'lodash-es' |
| | | import { convertTimeUnit, getApproveTypeText } from '../utils' |
| | | defineOptions({ |
| | | name: 'UserTaskNodeConfig' |
| | | }) |
| | | const props = defineProps({ |
| | | flowNode: { |
| | | type: Object as () => SimpleFlowNode, |
| | | required: true |
| | | } |
| | | }) |
| | | const emits = defineEmits<{ |
| | | 'find:returnTaskNodes': [nodeList: SimpleFlowNode[]] |
| | | }>() |
| | | const deptLevelLabel = computed(() => { |
| | | let label = '部门负责人来源' |
| | | if (configForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER) { |
| | | label = label + '(指定部门向上)' |
| | | } else if (configForm.value.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER) { |
| | | label = label + '(表单内部门向上)' |
| | | } else { |
| | | label = label + '(发起人部门向上)' |
| | | } |
| | | return label |
| | | }) |
| | | // 监控节点的变化 |
| | | const currentNode = useWatchNode(props) |
| | | // 抽屉配置 |
| | | const { settingVisible, closeDrawer, openDrawer } = useDrawer() |
| | | // 节点名称配置 |
| | | const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.USER_TASK_NODE) |
| | | // 激活的 Tab 标签页 |
| | | const activeTabName = ref('user') |
| | | // 表单字段权限设置 |
| | | const { formType, fieldsPermissionConfig, formFieldOptions, getNodeConfigFormFields } = |
| | | useFormFieldsPermission(FieldPermissionType.READ) |
| | | // 表单内用户字段选项, 必须是必填和用户选择器 |
| | | const userFieldOnFormOptions = computed(() => { |
| | | // 固定添加发起人 ID 字段 |
| | | formFieldOptions.unshift({ |
| | | field: ProcessVariableEnum.START_USER_ID, |
| | | title: '发起人', |
| | | type: 'UserSelect', |
| | | required: true |
| | | }) |
| | | return formFieldOptions.filter((item) => item.type === 'UserSelect') |
| | | }) |
| | | // 表单内部门字段选项, 必须是必填和部门选择器 |
| | | const deptFieldOnFormOptions = computed(() => { |
| | | return formFieldOptions.filter((item) => item.type === 'DeptSelect') |
| | | }) |
| | | // 操作按钮设置 |
| | | const { buttonsSetting, btnDisplayNameEdit, changeBtnDisplayName, btnDisplayNameBlurEvent } = |
| | | useButtonsSetting() |
| | | const approveType = ref(ApproveType.USER) |
| | | // 审批人表单设置 |
| | | const formRef = ref() // 表单 Ref |
| | | // 表单校验规则 |
| | | const formRules = reactive({ |
| | | candidateStrategy: [{ required: true, message: '审批人设置不能为空', trigger: 'change' }], |
| | | userIds: [{ required: true, message: '用户不能为空', trigger: 'change' }], |
| | | roleIds: [{ required: true, message: '角色不能为空', trigger: 'change' }], |
| | | deptIds: [{ required: true, message: '部门不能为空', trigger: 'change' }], |
| | | userGroups: [{ required: true, message: '用户组不能为空', trigger: 'change' }], |
| | | formUser: [{ required: true, message: '表单内用户字段不能为空', trigger: 'change' }], |
| | | formDept: [{ required: true, message: '表单内部门字段不能为空', trigger: 'change' }], |
| | | postIds: [{ required: true, message: '岗位不能为空', trigger: 'change' }], |
| | | expression: [{ required: true, message: '流程表达式不能为空', trigger: 'blur' }], |
| | | approveMethod: [{ required: true, message: '多人审批方式不能为空', trigger: 'change' }], |
| | | approveRatio: [{ required: true, message: '通过比例不能为空', trigger: 'blur' }], |
| | | returnNodeId: [{ required: true, message: '驳回节点不能为空', trigger: 'change' }], |
| | | timeoutHandlerEnable: [{ required: true }], |
| | | timeoutHandlerType: [{ required: true }], |
| | | timeDuration: [{ required: true, message: '超时时间不能为空', trigger: 'blur' }], |
| | | maxRemindCount: [{ required: true, message: '提醒次数不能为空', trigger: 'blur' }], |
| | | assignEmptyHandlerType: [{ required: true }], |
| | | assignEmptyHandlerUserIds: [{ required: true, message: '用户不能为空', trigger: 'change' }], |
| | | assignStartUserHandlerType: [{ required: true }] |
| | | }) |
| | | |
| | | const { |
| | | configForm: tempConfigForm, |
| | | roleOptions, |
| | | postOptions, |
| | | userOptions, |
| | | userGroupOptions, |
| | | deptTreeOptions, |
| | | handleCandidateParam, |
| | | parseCandidateParam, |
| | | getShowText |
| | | } = useNodeForm(NodeType.USER_TASK_NODE) |
| | | const configForm = tempConfigForm as Ref<UserTaskFormType> |
| | | |
| | | // 改变审批人设置策略 |
| | | const changeCandidateStrategy = () => { |
| | | configForm.value.userIds = [] |
| | | configForm.value.deptIds = [] |
| | | configForm.value.roleIds = [] |
| | | configForm.value.postIds = [] |
| | | configForm.value.userGroups = [] |
| | | configForm.value.deptLevel = 1 |
| | | configForm.value.formUser = '' |
| | | configForm.value.formDept = '' |
| | | configForm.value.approveMethod = ApproveMethodType.SEQUENTIAL_APPROVE |
| | | } |
| | | |
| | | // 审批方式改变 |
| | | const approveMethodChanged = () => { |
| | | configForm.value.rejectHandlerType = RejectHandlerType.FINISH_PROCESS |
| | | if (configForm.value.approveMethod === ApproveMethodType.APPROVE_BY_RATIO) { |
| | | configForm.value.approveRatio = 100 |
| | | } |
| | | formRef.value.clearValidate('approveRatio') |
| | | } |
| | | // 审批拒绝 可退回的节点 |
| | | const returnTaskList = ref<SimpleFlowNode[]>([]) |
| | | // 审批人超时未处理设置 |
| | | const { |
| | | timeoutHandlerChange, |
| | | cTimeoutType, |
| | | timeoutHandlerTypeChanged, |
| | | timeUnit, |
| | | timeUnitChange, |
| | | isoTimeDuration, |
| | | cTimeoutMaxRemindCount |
| | | } = useTimeoutHandler() |
| | | |
| | | // 保存配置 |
| | | const saveConfig = async () => { |
| | | activeTabName.value = 'user' |
| | | // 设置审批节点名称 |
| | | currentNode.value.name = nodeName.value! |
| | | // 设置审批类型 |
| | | currentNode.value.approveType = approveType.value |
| | | // 如果不是人工审批。返回 |
| | | if (approveType.value !== ApproveType.USER) { |
| | | currentNode.value.showText = getApproveTypeText(approveType.value) |
| | | settingVisible.value = false |
| | | return true |
| | | } |
| | | |
| | | if (!formRef) return false |
| | | const valid = await formRef.value.validate() |
| | | if (!valid) return false |
| | | const showText = getShowText() |
| | | if (!showText) return false |
| | | |
| | | currentNode.value.candidateStrategy = configForm.value.candidateStrategy |
| | | // 处理 candidateParam 参数 |
| | | currentNode.value.candidateParam = handleCandidateParam() |
| | | // 设置审批方式 |
| | | currentNode.value.approveMethod = configForm.value.approveMethod |
| | | if (configForm.value.approveMethod === ApproveMethodType.APPROVE_BY_RATIO) { |
| | | currentNode.value.approveRatio = configForm.value.approveRatio |
| | | } |
| | | // 设置拒绝处理 |
| | | currentNode.value.rejectHandler = { |
| | | type: configForm.value.rejectHandlerType!, |
| | | returnNodeId: configForm.value.returnNodeId |
| | | } |
| | | // 设置超时处理 |
| | | currentNode.value.timeoutHandler = { |
| | | enable: configForm.value.timeoutHandlerEnable!, |
| | | type: cTimeoutType.value, |
| | | timeDuration: isoTimeDuration.value, |
| | | maxRemindCount: cTimeoutMaxRemindCount.value |
| | | } |
| | | // 设置审批人为空时 |
| | | currentNode.value.assignEmptyHandler = { |
| | | type: configForm.value.assignEmptyHandlerType!, |
| | | userIds: |
| | | configForm.value.assignEmptyHandlerType === AssignEmptyHandlerType.ASSIGN_USER |
| | | ? configForm.value.assignEmptyHandlerUserIds |
| | | : undefined |
| | | } |
| | | // 设置审批人与发起人相同时 |
| | | currentNode.value.assignStartUserHandlerType = configForm.value.assignStartUserHandlerType |
| | | // 设置表单权限 |
| | | currentNode.value.fieldsPermission = fieldsPermissionConfig.value |
| | | // 设置按钮权限 |
| | | currentNode.value.buttonsSetting = buttonsSetting.value |
| | | |
| | | currentNode.value.showText = showText |
| | | settingVisible.value = false |
| | | return true |
| | | } |
| | | |
| | | // 显示审批节点配置, 由父组件传过来 |
| | | const showUserTaskNodeConfig = (node: SimpleFlowNode) => { |
| | | nodeName.value = node.name |
| | | // 1 审批类型 |
| | | approveType.value = node.approveType ? node.approveType : ApproveType.USER |
| | | // 如果审批类型不是人工审批返回 |
| | | if (approveType.value !== ApproveType.USER) { |
| | | return |
| | | } |
| | | |
| | | //2.1 审批人设置 |
| | | configForm.value.candidateStrategy = node.candidateStrategy! |
| | | // 解析候选人参数 |
| | | parseCandidateParam(node.candidateStrategy!, node?.candidateParam) |
| | | // 2.2 设置审批方式 |
| | | configForm.value.approveMethod = node.approveMethod! |
| | | if (node.approveMethod == ApproveMethodType.APPROVE_BY_RATIO) { |
| | | configForm.value.approveRatio = node.approveRatio! |
| | | } |
| | | // 2.3 设置审批拒绝处理 |
| | | configForm.value.rejectHandlerType = node.rejectHandler!.type |
| | | configForm.value.returnNodeId = node.rejectHandler?.returnNodeId |
| | | const matchNodeList = [] |
| | | emits('find:returnTaskNodes', matchNodeList) |
| | | returnTaskList.value = matchNodeList |
| | | // 2.4 设置审批超时处理 |
| | | configForm.value.timeoutHandlerEnable = node.timeoutHandler!.enable |
| | | if (node.timeoutHandler?.enable && node.timeoutHandler?.timeDuration) { |
| | | const strTimeDuration = node.timeoutHandler.timeDuration |
| | | let parseTime = strTimeDuration.slice(2, strTimeDuration.length - 1) |
| | | let parseTimeUnit = strTimeDuration.slice(strTimeDuration.length - 1) |
| | | configForm.value.timeDuration = parseInt(parseTime) |
| | | timeUnit.value = convertTimeUnit(parseTimeUnit) |
| | | } |
| | | configForm.value.timeoutHandlerType = node.timeoutHandler?.type |
| | | configForm.value.maxRemindCount = node.timeoutHandler?.maxRemindCount |
| | | // 2.5 设置审批人为空时 |
| | | configForm.value.assignEmptyHandlerType = node.assignEmptyHandler?.type |
| | | configForm.value.assignEmptyHandlerUserIds = node.assignEmptyHandler?.userIds |
| | | // 2.6 设置用户任务的审批人与发起人相同时 |
| | | configForm.value.assignStartUserHandlerType = node.assignStartUserHandlerType |
| | | // 3. 操作按钮设置 |
| | | buttonsSetting.value = cloneDeep(node.buttonsSetting) || DEFAULT_BUTTON_SETTING |
| | | // 4. 表单字段权限配置 |
| | | getNodeConfigFormFields(node.fieldsPermission) |
| | | } |
| | | |
| | | defineExpose({ openDrawer, showUserTaskNodeConfig }) // 暴露方法给父组件 |
| | | |
| | | /** |
| | | * @description 操作按钮设置 |
| | | */ |
| | | function useButtonsSetting() { |
| | | const buttonsSetting = ref<ButtonSetting[]>() |
| | | // 操作按钮显示名称可编辑 |
| | | const btnDisplayNameEdit = ref<boolean[]>([]) |
| | | const changeBtnDisplayName = (index: number) => { |
| | | btnDisplayNameEdit.value[index] = true |
| | | } |
| | | const btnDisplayNameBlurEvent = (index: number) => { |
| | | btnDisplayNameEdit.value[index] = false |
| | | const buttonItem = buttonsSetting.value![index] |
| | | buttonItem.displayName = buttonItem.displayName || OPERATION_BUTTON_NAME.get(buttonItem.id)! |
| | | } |
| | | return { |
| | | buttonsSetting, |
| | | btnDisplayNameEdit, |
| | | changeBtnDisplayName, |
| | | btnDisplayNameBlurEvent |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * @description 审批人超时未处理配置 |
| | | */ |
| | | function useTimeoutHandler() { |
| | | // 时间单位 |
| | | const timeUnit = ref(TimeUnitType.HOUR) |
| | | |
| | | // 超时开关改变 |
| | | const timeoutHandlerChange = () => { |
| | | if (configForm.value.timeoutHandlerEnable) { |
| | | timeUnit.value = 2 |
| | | configForm.value.timeDuration = 6 |
| | | configForm.value.timeoutHandlerType = 1 |
| | | configForm.value.maxRemindCount = 1 |
| | | } |
| | | } |
| | | // 超时执行的动作 |
| | | const cTimeoutType = computed(() => { |
| | | if (!configForm.value.timeoutHandlerEnable) { |
| | | return undefined |
| | | } |
| | | return configForm.value.timeoutHandlerType |
| | | }) |
| | | |
| | | // 超时处理动作改变 |
| | | const timeoutHandlerTypeChanged = () => { |
| | | if (configForm.value.timeoutHandlerType === TimeoutHandlerType.REMINDER) { |
| | | configForm.value.maxRemindCount = 1 // 超时提醒次数,默认为1 |
| | | } |
| | | } |
| | | |
| | | // 时间单位改变 |
| | | const timeUnitChange = () => { |
| | | // 分钟,默认是 60 分钟 |
| | | if (timeUnit.value === TimeUnitType.MINUTE) { |
| | | configForm.value.timeDuration = 60 |
| | | } |
| | | // 小时,默认是 6 个小时 |
| | | if (timeUnit.value === TimeUnitType.HOUR) { |
| | | configForm.value.timeDuration = 6 |
| | | } |
| | | // 天, 默认 1天 |
| | | if (timeUnit.value === TimeUnitType.DAY) { |
| | | configForm.value.timeDuration = 1 |
| | | } |
| | | } |
| | | // 超时时间的 ISO 表示 |
| | | const isoTimeDuration = computed(() => { |
| | | if (!configForm.value.timeoutHandlerEnable) { |
| | | return undefined |
| | | } |
| | | let strTimeDuration = 'PT' |
| | | if (timeUnit.value === TimeUnitType.MINUTE) { |
| | | strTimeDuration += configForm.value.timeDuration + 'M' |
| | | } |
| | | if (timeUnit.value === TimeUnitType.HOUR) { |
| | | strTimeDuration += configForm.value.timeDuration + 'H' |
| | | } |
| | | if (timeUnit.value === TimeUnitType.DAY) { |
| | | strTimeDuration += configForm.value.timeDuration + 'D' |
| | | } |
| | | return strTimeDuration |
| | | }) |
| | | |
| | | // 超时最大提醒次数 |
| | | const cTimeoutMaxRemindCount = computed(() => { |
| | | if (!configForm.value.timeoutHandlerEnable) { |
| | | return undefined |
| | | } |
| | | if (configForm.value.timeoutHandlerType !== TimeoutHandlerType.REMINDER) { |
| | | return undefined |
| | | } |
| | | return configForm.value.maxRemindCount |
| | | }) |
| | | |
| | | return { |
| | | timeoutHandlerChange, |
| | | cTimeoutType, |
| | | timeoutHandlerTypeChanged, |
| | | timeUnit, |
| | | timeUnitChange, |
| | | isoTimeDuration, |
| | | cTimeoutMaxRemindCount |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .button-setting-pane { |
| | | display: flex; |
| | | flex-direction: column; |
| | | font-size: 14px; |
| | | |
| | | .button-setting-desc { |
| | | padding-right: 8px; |
| | | margin-bottom: 16px; |
| | | font-size: 16px; |
| | | font-weight: 700; |
| | | } |
| | | |
| | | .button-setting-title { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | height: 45px; |
| | | padding-left: 12px; |
| | | background-color: #f8fafc0a; |
| | | border: 1px solid #1f38581a; |
| | | |
| | | & > :first-child { |
| | | width: 100px !important; |
| | | text-align: left !important; |
| | | } |
| | | |
| | | & > :last-child { |
| | | text-align: center !important; |
| | | } |
| | | |
| | | .button-title-label { |
| | | width: 150px; |
| | | font-size: 13px; |
| | | font-weight: 700; |
| | | color: #000; |
| | | text-align: left; |
| | | } |
| | | } |
| | | |
| | | .button-setting-item { |
| | | align-items: center; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | height: 38px; |
| | | padding-left: 12px; |
| | | border: 1px solid #1f38581a; |
| | | border-top: 0; |
| | | |
| | | & > :first-child { |
| | | width: 100px !important; |
| | | } |
| | | |
| | | & > :last-child { |
| | | text-align: center !important; |
| | | } |
| | | |
| | | .button-setting-item-label { |
| | | width: 150px; |
| | | overflow: hidden; |
| | | text-align: left; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .editable-title-input { |
| | | height: 24px; |
| | | max-width: 130px; |
| | | margin-left: 4px; |
| | | line-height: 24px; |
| | | border: 1px solid #d9d9d9; |
| | | border-radius: 4px; |
| | | transition: all 0.3s; |
| | | |
| | | &:focus { |
| | | border-color: #40a9ff; |
| | | outline: 0; |
| | | box-shadow: 0 0 0 2px rgb(24 144 255 / 20%); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | </style> |
对比新文件 |
| | |
| | | <template> |
| | | <div class="node-wrapper"> |
| | | <div class="node-container"> |
| | | <div |
| | | class="node-box" |
| | | :class="[ |
| | | { 'node-config-error': !currentNode.showText }, |
| | | `${useTaskStatusClass(currentNode?.activityStatus)}` |
| | | ]" |
| | | > |
| | | <div class="node-title-container"> |
| | | <div class="node-title-icon copy-task"><span class="iconfont icon-copy"></span></div> |
| | | <input |
| | | v-if="!readonly && showInput" |
| | | type="text" |
| | | class="editable-title-input" |
| | | @blur="blurEvent()" |
| | | v-mountedFocus |
| | | v-model="currentNode.name" |
| | | :placeholder="currentNode.name" |
| | | /> |
| | | <div v-else class="node-title" @click="clickTitle"> |
| | | {{ currentNode.name }} |
| | | </div> |
| | | </div> |
| | | <div class="node-content" @click="openNodeConfig"> |
| | | <div class="node-text" :title="currentNode.showText" v-if="currentNode.showText"> |
| | | {{ currentNode.showText }} |
| | | </div> |
| | | <div class="node-text" v-else> |
| | | {{ NODE_DEFAULT_TEXT.get(NodeType.COPY_TASK_NODE) }} |
| | | </div> |
| | | <Icon v-if="!readonly" icon="ep:arrow-right-bold" /> |
| | | </div> |
| | | <div v-if="!readonly" class="node-toolbar"> |
| | | <div class="toolbar-icon" |
| | | ><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode" |
| | | /></div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 传递子节点给添加节点组件。会在子节点前面添加节点 --> |
| | | <NodeHandler |
| | | v-if="currentNode" |
| | | v-model:child-node="currentNode.childNode" |
| | | :current-node="currentNode" |
| | | /> |
| | | </div> |
| | | <CopyTaskNodeConfig |
| | | v-if="!readonly && currentNode" |
| | | ref="nodeSetting" |
| | | :flow-node="currentNode" |
| | | /> |
| | | </div> |
| | | </template> |
| | | <script setup lang="ts"> |
| | | import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts' |
| | | import NodeHandler from '../NodeHandler.vue' |
| | | import { useNodeName2, useWatchNode, useTaskStatusClass } from '../node' |
| | | import CopyTaskNodeConfig from '../nodes-config/CopyTaskNodeConfig.vue' |
| | | defineOptions({ |
| | | name: 'CopyTaskNode' |
| | | }) |
| | | const props = defineProps({ |
| | | flowNode: { |
| | | type: Object as () => SimpleFlowNode, |
| | | required: true |
| | | } |
| | | }) |
| | | // 定义事件,更新父组件。 |
| | | const emits = defineEmits<{ |
| | | 'update:flowNode': [node: SimpleFlowNode | undefined] |
| | | }>() |
| | | // 是否只读 |
| | | const readonly = inject<Boolean>('readonly') |
| | | // 监控节点的变化 |
| | | const currentNode = useWatchNode(props) |
| | | // 节点名称编辑 |
| | | const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.COPY_TASK_NODE) |
| | | |
| | | const nodeSetting = ref() |
| | | // 打开节点配置 |
| | | const openNodeConfig = () => { |
| | | if (readonly) { |
| | | return |
| | | } |
| | | nodeSetting.value.showCopyTaskNodeConfig(currentNode.value) |
| | | nodeSetting.value.openDrawer() |
| | | } |
| | | |
| | | // 删除节点。更新当前节点为孩子节点 |
| | | const deleteNode = () => { |
| | | emits('update:flowNode', currentNode.value.childNode) |
| | | } |
| | | </script> |
| | | |
| | | <style lang="scss" scoped></style> |
对比新文件 |
| | |
| | | <template> |
| | | <div class="node-wrapper"> |
| | | <div class="node-container"> |
| | | <div |
| | | class="node-box" |
| | | :class="[ |
| | | { 'node-config-error': !currentNode.showText }, |
| | | `${useTaskStatusClass(currentNode?.activityStatus)}` |
| | | ]" |
| | | > |
| | | <div class="node-title-container"> |
| | | <!-- TODO @芋艿 需要更换图标 --> |
| | | <div class="node-title-icon copy-task"><span class="iconfont icon-copy"></span></div> |
| | | <input |
| | | v-if="!readonly && showInput" |
| | | type="text" |
| | | class="editable-title-input" |
| | | @blur="blurEvent()" |
| | | v-mountedFocus |
| | | v-model="currentNode.name" |
| | | :placeholder="currentNode.name" |
| | | /> |
| | | <div v-else class="node-title" @click="clickTitle"> |
| | | {{ currentNode.name }} |
| | | </div> |
| | | </div> |
| | | <div class="node-content" @click="openNodeConfig"> |
| | | <div class="node-text" :title="currentNode.showText" v-if="currentNode.showText"> |
| | | {{ currentNode.showText }} |
| | | </div> |
| | | <div class="node-text" v-else> |
| | | {{ NODE_DEFAULT_TEXT.get(NodeType.DELAY_TIMER_NODE) }} |
| | | </div> |
| | | <Icon v-if="!readonly" icon="ep:arrow-right-bold" /> |
| | | </div> |
| | | <div v-if="!readonly" class="node-toolbar"> |
| | | <div class="toolbar-icon" |
| | | ><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode" |
| | | /></div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 传递子节点给添加节点组件。会在子节点前面添加节点 --> |
| | | <NodeHandler |
| | | v-if="currentNode" |
| | | v-model:child-node="currentNode.childNode" |
| | | :current-node="currentNode" |
| | | /> |
| | | </div> |
| | | <DelayTimerNodeConfig |
| | | v-if="!readonly && currentNode" |
| | | ref="nodeSetting" |
| | | :flow-node="currentNode" |
| | | /> |
| | | </div> |
| | | </template> |
| | | <script setup lang="ts"> |
| | | import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts' |
| | | import NodeHandler from '../NodeHandler.vue' |
| | | import { useNodeName2, useWatchNode, useTaskStatusClass } from '../node' |
| | | import DelayTimerNodeConfig from '../nodes-config/DelayTimerNodeConfig.vue' |
| | | defineOptions({ |
| | | name: 'DelayTimerNode' |
| | | }) |
| | | const props = defineProps({ |
| | | flowNode: { |
| | | type: Object as () => SimpleFlowNode, |
| | | required: true |
| | | } |
| | | }) |
| | | // 定义事件,更新父组件。 |
| | | const emits = defineEmits<{ |
| | | 'update:flowNode': [node: SimpleFlowNode | undefined] |
| | | }>() |
| | | // 是否只读 |
| | | const readonly = inject<Boolean>('readonly') |
| | | // 监控节点的变化 |
| | | const currentNode = useWatchNode(props) |
| | | // 节点名称编辑 |
| | | const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.DELAY_TIMER_NODE) |
| | | |
| | | const nodeSetting = ref() |
| | | // 打开节点配置 |
| | | const openNodeConfig = () => { |
| | | if (readonly) { |
| | | return |
| | | } |
| | | nodeSetting.value.showDelayTimerNodeConfig(currentNode.value) |
| | | nodeSetting.value.openDrawer() |
| | | } |
| | | |
| | | // 删除节点。更新当前节点为孩子节点 |
| | | const deleteNode = () => { |
| | | emits('update:flowNode', currentNode.value.childNode) |
| | | } |
| | | </script> |
| | | |
| | | <style lang="scss" scoped></style> |
对比新文件 |
| | |
| | | <template> |
| | | <div class="end-node-wrapper"> |
| | | <div class="end-node-box cursor-pointer" :class="`${useTaskStatusClass(currentNode?.activityStatus)}`" @click="nodeClick"> |
| | | <span class="node-fixed-name" title="结束">结束</span> |
| | | </div> |
| | | </div> |
| | | <el-dialog title="审批信息" v-model="dialogVisible" width="1000px" append-to-body> |
| | | <el-row> |
| | | <el-table |
| | | :data="processInstanceInfos" |
| | | size="small" |
| | | border |
| | | header-cell-class-name="table-header-gray" |
| | | > |
| | | <el-table-column |
| | | label="序号" |
| | | header-align="center" |
| | | align="center" |
| | | type="index" |
| | | width="50" |
| | | /> |
| | | <el-table-column |
| | | label="发起人" |
| | | prop="assigneeUser.nickname" |
| | | min-width="100" |
| | | align="center" |
| | | /> |
| | | <el-table-column label="部门" min-width="100" align="center"> |
| | | <template #default="scope"> |
| | | {{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column |
| | | :formatter="dateFormatter" |
| | | align="center" |
| | | label="开始时间" |
| | | prop="createTime" |
| | | min-width="140" |
| | | /> |
| | | <el-table-column |
| | | :formatter="dateFormatter" |
| | | align="center" |
| | | label="结束时间" |
| | | prop="endTime" |
| | | min-width="140" |
| | | /> |
| | | <el-table-column align="center" label="审批状态" prop="status" min-width="90"> |
| | | <template #default="scope"> |
| | | <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.status" /> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column align="center" label="耗时" prop="durationInMillis" width="100"> |
| | | <template #default="scope"> |
| | | {{ formatPast2(scope.row.durationInMillis) }} |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-row> |
| | | </el-dialog> |
| | | </template> |
| | | <script setup lang="ts"> |
| | | import { SimpleFlowNode } from '../consts' |
| | | import { useWatchNode, useTaskStatusClass } from '../node' |
| | | import { dateFormatter, formatPast2 } from '@/utils/formatTime' |
| | | import { DICT_TYPE } from '@/utils/dict' |
| | | defineOptions({ |
| | | name: 'EndEventNode' |
| | | }) |
| | | const props = defineProps({ |
| | | flowNode: { |
| | | type: Object as () => SimpleFlowNode, |
| | | default: () => null |
| | | } |
| | | }) |
| | | // 监控节点变化 |
| | | const currentNode = useWatchNode(props) |
| | | // 是否只读 |
| | | const readonly = inject<Boolean>('readonly') |
| | | const processInstance = inject<Ref<any>>('processInstance') |
| | | // 审批信息的弹窗显示,用于只读模式 |
| | | const dialogVisible = ref(false) // 弹窗可见性 |
| | | const processInstanceInfos = ref<any[]>([]) // 流程的审批信息 |
| | | |
| | | const nodeClick = () => { |
| | | if (readonly) { |
| | | if(processInstance && processInstance.value){ |
| | | processInstanceInfos.value = [ |
| | | { |
| | | assigneeUser: processInstance.value.startUser, |
| | | createTime: processInstance.value.startTime, |
| | | endTime: processInstance.value.endTime, |
| | | status: processInstance.value.status, |
| | | durationInMillis: processInstance.value.durationInMillis |
| | | } |
| | | ] |
| | | dialogVisible.value = true |
| | | } |
| | | } |
| | | } |
| | | </script> |
| | | <style lang="scss" scoped></style> |
对比新文件 |
| | |
| | | <template> |
| | | <div class="branch-node-wrapper"> |
| | | <div class="branch-node-container"> |
| | | <div |
| | | v-if="readonly" |
| | | class="branch-node-readonly" |
| | | :class="`${useTaskStatusClass(currentNode?.activityStatus)}`" |
| | | > |
| | | <span class="iconfont icon-exclusive icon-size condition"></span> |
| | | </div> |
| | | <el-button v-else class="branch-node-add" color="#67c23a" @click="addCondition" plain |
| | | >添加条件</el-button |
| | | > |
| | | |
| | | <div |
| | | class="branch-node-item" |
| | | v-for="(item, index) in currentNode.conditionNodes" |
| | | :key="index" |
| | | > |
| | | <template v-if="index == 0"> |
| | | <div class="branch-line-first-top"> </div> |
| | | <div class="branch-line-first-bottom"></div> |
| | | </template> |
| | | <template v-if="index + 1 == currentNode.conditionNodes?.length"> |
| | | <div class="branch-line-last-top"></div> |
| | | <div class="branch-line-last-bottom"></div> |
| | | </template> |
| | | <div class="node-wrapper"> |
| | | <div class="node-container"> |
| | | <div |
| | | class="node-box" |
| | | :class="[ |
| | | { 'node-config-error': !item.showText }, |
| | | `${useTaskStatusClass(item.activityStatus)}` |
| | | ]" |
| | | > |
| | | <div class="branch-node-title-container"> |
| | | <div v-if="!readonly && showInputs[index]"> |
| | | <input |
| | | type="text" |
| | | class="input-max-width editable-title-input" |
| | | @blur="blurEvent(index)" |
| | | v-mountedFocus |
| | | v-model="item.name" |
| | | /> |
| | | </div> |
| | | <div v-else class="branch-title" @click="clickEvent(index)"> {{ item.name }} </div> |
| | | <div class="branch-priority"> 优先级{{ index + 1 }} </div> |
| | | </div> |
| | | <div class="branch-node-content" @click="conditionNodeConfig(item.id)"> |
| | | <div class="branch-node-text" :title="item.showText" v-if="item.showText"> |
| | | {{ item.showText }} |
| | | </div> |
| | | <div class="branch-node-text" v-else> |
| | | {{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }} |
| | | </div> |
| | | </div> |
| | | <div |
| | | class="node-toolbar" |
| | | v-if="!readonly && index + 1 !== currentNode.conditionNodes?.length" |
| | | > |
| | | <div class="toolbar-icon"> |
| | | <Icon |
| | | color="#0089ff" |
| | | icon="ep:circle-close-filled" |
| | | :size="18" |
| | | @click="deleteCondition(index)" |
| | | /> |
| | | </div> |
| | | </div> |
| | | <div |
| | | class="branch-node-move move-node-left" |
| | | v-if="index != 0 && index + 1 !== currentNode.conditionNodes?.length" |
| | | @click="moveNode(index, -1)" |
| | | > |
| | | <Icon icon="ep:arrow-left" /> |
| | | </div> |
| | | |
| | | <div |
| | | class="branch-node-move move-node-right" |
| | | v-if="currentNode.conditionNodes && index < currentNode.conditionNodes.length - 2" |
| | | @click="moveNode(index, 1)" |
| | | > |
| | | <Icon icon="ep:arrow-right" /> |
| | | </div> |
| | | </div> |
| | | <NodeHandler v-model:child-node="item.childNode" :current-node="item" /> |
| | | </div> |
| | | </div> |
| | | <ConditionNodeConfig :node-index="index" :condition-node="item" :ref="item.id" /> |
| | | <!-- 递归显示子节点 --> |
| | | <ProcessNodeTree |
| | | v-if="item && item.childNode" |
| | | :parent-node="item" |
| | | v-model:flow-node="item.childNode" |
| | | @find:recursive-find-parent-node="recursiveFindParentNode" |
| | | /> |
| | | </div> |
| | | </div> |
| | | <NodeHandler |
| | | v-if="currentNode" |
| | | v-model:child-node="currentNode.childNode" |
| | | :current-node="currentNode" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup lang="ts"> |
| | | import NodeHandler from '../NodeHandler.vue' |
| | | import ProcessNodeTree from '../ProcessNodeTree.vue' |
| | | import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts' |
| | | import { getDefaultConditionNodeName } from '../utils' |
| | | import { useTaskStatusClass } from '../node' |
| | | import { generateUUID } from '@/utils' |
| | | import ConditionNodeConfig from '../nodes-config/ConditionNodeConfig.vue' |
| | | const { proxy } = getCurrentInstance() as any |
| | | defineOptions({ |
| | | name: 'ExclusiveNode' |
| | | }) |
| | | const props = defineProps({ |
| | | flowNode: { |
| | | type: Object as () => SimpleFlowNode, |
| | | required: true |
| | | } |
| | | }) |
| | | // 定义事件,更新父组件 |
| | | const emits = defineEmits<{ |
| | | 'update:modelValue': [node: SimpleFlowNode | undefined] |
| | | 'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: number] |
| | | 'find:recursiveFindParentNode': [ |
| | | nodeList: SimpleFlowNode[], |
| | | curentNode: SimpleFlowNode, |
| | | nodeType: number |
| | | ] |
| | | }>() |
| | | // 是否只读 |
| | | const readonly = inject<Boolean>('readonly') |
| | | const currentNode = ref<SimpleFlowNode>(props.flowNode) |
| | | watch( |
| | | () => props.flowNode, |
| | | (newValue) => { |
| | | currentNode.value = newValue |
| | | } |
| | | ) |
| | | |
| | | const showInputs = ref<boolean[]>([]) |
| | | // 失去焦点 |
| | | const blurEvent = (index: number) => { |
| | | showInputs.value[index] = false |
| | | const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode |
| | | conditionNode.name = |
| | | conditionNode.name || getDefaultConditionNodeName(index, conditionNode.defaultFlow) |
| | | } |
| | | |
| | | // 点击条件名称 |
| | | const clickEvent = (index: number) => { |
| | | showInputs.value[index] = true |
| | | } |
| | | |
| | | const conditionNodeConfig = (nodeId: string) => { |
| | | if (readonly) { |
| | | return |
| | | } |
| | | const conditionNode = proxy.$refs[nodeId][0] |
| | | conditionNode.open() |
| | | } |
| | | |
| | | // 新增条件 |
| | | const addCondition = () => { |
| | | const conditionNodes = currentNode.value.conditionNodes |
| | | if (conditionNodes) { |
| | | const len = conditionNodes.length |
| | | let lastIndex = len - 1 |
| | | const conditionData: SimpleFlowNode = { |
| | | id: 'Flow_' + generateUUID(), |
| | | name: '条件' + len, |
| | | showText: '', |
| | | type: NodeType.CONDITION_NODE, |
| | | childNode: undefined, |
| | | conditionNodes: [], |
| | | conditionType: 1, |
| | | defaultFlow: false |
| | | } |
| | | conditionNodes.splice(lastIndex, 0, conditionData) |
| | | } |
| | | } |
| | | |
| | | // 删除条件 |
| | | const deleteCondition = (index: number) => { |
| | | const conditionNodes = currentNode.value.conditionNodes |
| | | if (conditionNodes) { |
| | | conditionNodes.splice(index, 1) |
| | | if (conditionNodes.length == 1) { |
| | | const childNode = currentNode.value.childNode |
| | | // 更新此节点为后续孩子节点 |
| | | emits('update:modelValue', childNode) |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 移动节点 |
| | | const moveNode = (index: number, to: number) => { |
| | | // -1 :向左 1: 向右 |
| | | if (currentNode.value.conditionNodes) { |
| | | currentNode.value.conditionNodes[index] = currentNode.value.conditionNodes.splice( |
| | | index + to, |
| | | 1, |
| | | currentNode.value.conditionNodes[index] |
| | | )[0] |
| | | } |
| | | } |
| | | // 递归从父节点中查询匹配的节点 |
| | | const recursiveFindParentNode = ( |
| | | nodeList: SimpleFlowNode[], |
| | | node: SimpleFlowNode, |
| | | nodeType: number |
| | | ) => { |
| | | if (!node || node.type === NodeType.START_USER_NODE) { |
| | | return |
| | | } |
| | | if (node.type === nodeType) { |
| | | nodeList.push(node) |
| | | } |
| | | // 条件节点 (NodeType.CONDITION_NODE) 比较特殊。需要调用其父节点条件分支节点(NodeType.EXCLUSIVE_NODE) 继续查找 |
| | | emits('find:parentNode', nodeList, nodeType) |
| | | } |
| | | </script> |
| | | |
| | | <style lang="scss" scoped></style> |
对比新文件 |
| | |
| | | <template> |
| | | <div class="branch-node-wrapper"> |
| | | <div class="branch-node-container"> |
| | | <div |
| | | v-if="readonly" |
| | | class="branch-node-readonly" |
| | | :class="`${useTaskStatusClass(currentNode?.activityStatus)}`" |
| | | > |
| | | <span class="iconfont icon-inclusive icon-size inclusive"></span> |
| | | </div> |
| | | <el-button v-else class="branch-node-add" color="#345da2" @click="addCondition" plain |
| | | >添加条件</el-button |
| | | > |
| | | <div |
| | | class="branch-node-item" |
| | | v-for="(item, index) in currentNode.conditionNodes" |
| | | :key="index" |
| | | > |
| | | <template v-if="index == 0"> |
| | | <div class="branch-line-first-top"> </div> |
| | | <div class="branch-line-first-bottom"></div> |
| | | </template> |
| | | <template v-if="index + 1 == currentNode.conditionNodes?.length"> |
| | | <div class="branch-line-last-top"></div> |
| | | <div class="branch-line-last-bottom"></div> |
| | | </template> |
| | | <div class="node-wrapper"> |
| | | <div class="node-container"> |
| | | <div |
| | | class="node-box" |
| | | :class="[ |
| | | { 'node-config-error': !item.showText }, |
| | | `${useTaskStatusClass(item.activityStatus)}` |
| | | ]" |
| | | > |
| | | <div class="branch-node-title-container"> |
| | | <div v-if="showInputs[index]"> |
| | | <input |
| | | type="text" |
| | | class="editable-title-input" |
| | | @blur="blurEvent(index)" |
| | | v-mountedFocus |
| | | v-model="item.name" |
| | | /> |
| | | </div> |
| | | <div v-else class="branch-title" @click="clickEvent(index)"> {{ item.name }} </div> |
| | | </div> |
| | | <div class="branch-node-content" @click="conditionNodeConfig(item.id)"> |
| | | <div class="branch-node-text" :title="item.showText" v-if="item.showText"> |
| | | {{ item.showText }} |
| | | </div> |
| | | <div class="branch-node-text" v-else> |
| | | {{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }} |
| | | </div> |
| | | </div> |
| | | <div |
| | | class="node-toolbar" |
| | | v-if="!readonly && index + 1 !== currentNode.conditionNodes?.length" |
| | | > |
| | | <div class="toolbar-icon"> |
| | | <Icon |
| | | color="#0089ff" |
| | | icon="ep:circle-close-filled" |
| | | :size="18" |
| | | @click="deleteCondition(index)" |
| | | /> |
| | | </div> |
| | | </div> |
| | | <div |
| | | class="branch-node-move move-node-left" |
| | | v-if="!readonly && index != 0 && index + 1 !== currentNode.conditionNodes?.length" |
| | | @click="moveNode(index, -1)" |
| | | > |
| | | <Icon icon="ep:arrow-left" /> |
| | | </div> |
| | | |
| | | <div |
| | | class="branch-node-move move-node-right" |
| | | v-if=" |
| | | !readonly && |
| | | currentNode.conditionNodes && |
| | | index < currentNode.conditionNodes.length - 2 |
| | | " |
| | | @click="moveNode(index, 1)" |
| | | > |
| | | <Icon icon="ep:arrow-right" /> |
| | | </div> |
| | | </div> |
| | | <NodeHandler v-model:child-node="item.childNode" :current-node="item" /> |
| | | </div> |
| | | </div> |
| | | <ConditionNodeConfig :node-index="index" :condition-node="item" :ref="item.id" /> |
| | | <!-- 递归显示子节点 --> |
| | | <ProcessNodeTree |
| | | v-if="item && item.childNode" |
| | | :parent-node="item" |
| | | v-model:flow-node="item.childNode" |
| | | @find:recursive-find-parent-node="recursiveFindParentNode" |
| | | /> |
| | | </div> |
| | | </div> |
| | | <NodeHandler |
| | | v-if="currentNode" |
| | | v-model:child-node="currentNode.childNode" |
| | | :current-node="currentNode" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup lang="ts"> |
| | | import NodeHandler from '../NodeHandler.vue' |
| | | import ProcessNodeTree from '../ProcessNodeTree.vue' |
| | | import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts' |
| | | import { useTaskStatusClass } from '../node' |
| | | import { getDefaultInclusiveConditionNodeName } from '../utils' |
| | | import { generateUUID } from '@/utils' |
| | | import ConditionNodeConfig from '../nodes-config/ConditionNodeConfig.vue' |
| | | const { proxy } = getCurrentInstance() as any |
| | | defineOptions({ |
| | | name: 'InclusiveNode' |
| | | }) |
| | | const props = defineProps({ |
| | | flowNode: { |
| | | type: Object as () => SimpleFlowNode, |
| | | required: true |
| | | } |
| | | }) |
| | | // 定义事件,更新父组件 |
| | | const emits = defineEmits<{ |
| | | 'update:modelValue': [node: SimpleFlowNode | undefined] |
| | | 'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: number] |
| | | 'find:recursiveFindParentNode': [ |
| | | nodeList: SimpleFlowNode[], |
| | | curentNode: SimpleFlowNode, |
| | | nodeType: number |
| | | ] |
| | | }>() |
| | | // 是否只读 |
| | | const readonly = inject<Boolean>('readonly') |
| | | |
| | | const currentNode = ref<SimpleFlowNode>(props.flowNode) |
| | | |
| | | watch( |
| | | () => props.flowNode, |
| | | (newValue) => { |
| | | currentNode.value = newValue |
| | | } |
| | | ) |
| | | |
| | | const showInputs = ref<boolean[]>([]) |
| | | // 失去焦点 |
| | | const blurEvent = (index: number) => { |
| | | showInputs.value[index] = false |
| | | const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode |
| | | conditionNode.name = |
| | | conditionNode.name || getDefaultInclusiveConditionNodeName(index, conditionNode.defaultFlow) |
| | | } |
| | | |
| | | // 点击条件名称 |
| | | const clickEvent = (index: number) => { |
| | | showInputs.value[index] = true |
| | | } |
| | | |
| | | const conditionNodeConfig = (nodeId: string) => { |
| | | if (readonly) { |
| | | return |
| | | } |
| | | const conditionNode = proxy.$refs[nodeId][0] |
| | | conditionNode.open() |
| | | } |
| | | |
| | | // 新增条件 |
| | | const addCondition = () => { |
| | | const conditionNodes = currentNode.value.conditionNodes |
| | | if (conditionNodes) { |
| | | const len = conditionNodes.length |
| | | let lastIndex = len - 1 |
| | | const conditionData: SimpleFlowNode = { |
| | | id: 'Flow_' + generateUUID(), |
| | | name: '包容条件' + len, |
| | | showText: '', |
| | | type: NodeType.CONDITION_NODE, |
| | | childNode: undefined, |
| | | conditionNodes: [], |
| | | conditionType: 1, |
| | | defaultFlow: false |
| | | } |
| | | conditionNodes.splice(lastIndex, 0, conditionData) |
| | | } |
| | | } |
| | | |
| | | // 删除条件 |
| | | const deleteCondition = (index: number) => { |
| | | const conditionNodes = currentNode.value.conditionNodes |
| | | if (conditionNodes) { |
| | | conditionNodes.splice(index, 1) |
| | | if (conditionNodes.length == 1) { |
| | | const childNode = currentNode.value.childNode |
| | | // 更新此节点为后续孩子节点 |
| | | emits('update:modelValue', childNode) |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 移动节点 |
| | | const moveNode = (index: number, to: number) => { |
| | | // -1 :向左 1: 向右 |
| | | if (currentNode.value.conditionNodes) { |
| | | currentNode.value.conditionNodes[index] = currentNode.value.conditionNodes.splice( |
| | | index + to, |
| | | 1, |
| | | currentNode.value.conditionNodes[index] |
| | | )[0] |
| | | } |
| | | } |
| | | // 递归从父节点中查询匹配的节点 |
| | | const recursiveFindParentNode = ( |
| | | nodeList: SimpleFlowNode[], |
| | | node: SimpleFlowNode, |
| | | nodeType: number |
| | | ) => { |
| | | if (!node || node.type === NodeType.START_USER_NODE) { |
| | | return |
| | | } |
| | | if (node.type === nodeType) { |
| | | nodeList.push(node) |
| | | } |
| | | // 条件节点 (NodeType.CONDITION_NODE) 比较特殊。需要调用其父节点条件分支节点(NodeType.INCLUSIVE_BRANCH_NODE) 继续查找 |
| | | emits('find:parentNode', nodeList, nodeType) |
| | | } |
| | | </script> |
| | | |
| | | <style lang="scss" scoped></style> |
对比新文件 |
| | |
| | | <template> |
| | | <div class="branch-node-wrapper"> |
| | | <div class="branch-node-container"> |
| | | <div |
| | | v-if="readonly" |
| | | class="branch-node-readonly" |
| | | :class="`${useTaskStatusClass(currentNode?.activityStatus)}`" |
| | | > |
| | | <span class="iconfont icon-parallel icon-size parallel"></span> |
| | | </div> |
| | | <el-button v-else class="branch-node-add" color="#626aef" @click="addCondition" plain |
| | | >添加分支</el-button |
| | | > |
| | | <div |
| | | class="branch-node-item" |
| | | v-for="(item, index) in currentNode.conditionNodes" |
| | | :key="index" |
| | | > |
| | | <template v-if="index == 0"> |
| | | <div class="branch-line-first-top"></div> |
| | | <div class="branch-line-first-bottom"></div> |
| | | </template> |
| | | <template v-if="index + 1 == currentNode.conditionNodes?.length"> |
| | | <div class="branch-line-last-top"></div> |
| | | <div class="branch-line-last-bottom"></div> |
| | | </template> |
| | | <div class="node-wrapper"> |
| | | <div class="node-container"> |
| | | <div class="node-box" :class="`${useTaskStatusClass(item.activityStatus)}`"> |
| | | <div class="branch-node-title-container"> |
| | | <div v-if="showInputs[index]"> |
| | | <input |
| | | type="text" |
| | | class="input-max-width editable-title-input" |
| | | @blur="blurEvent(index)" |
| | | v-mountedFocus |
| | | v-model="item.name" |
| | | /> |
| | | </div> |
| | | <div v-else class="branch-title" @click="clickEvent(index)"> {{ item.name }} </div> |
| | | <div class="branch-priority">无优先级</div> |
| | | </div> |
| | | <div class="branch-node-content" @click="conditionNodeConfig(item.id)"> |
| | | <div class="branch-node-text" :title="item.showText" v-if="item.showText"> |
| | | {{ item.showText }} |
| | | </div> |
| | | <div class="branch-node-text" v-else> |
| | | {{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }} |
| | | </div> |
| | | </div> |
| | | <div v-if="!readonly" class="node-toolbar"> |
| | | <div class="toolbar-icon"> |
| | | <Icon |
| | | color="#0089ff" |
| | | icon="ep:circle-close-filled" |
| | | :size="18" |
| | | @click="deleteCondition(index)" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <NodeHandler v-model:child-node="item.childNode" :current-node="item" /> |
| | | </div> |
| | | </div> |
| | | <!-- 递归显示子节点 --> |
| | | <ProcessNodeTree |
| | | v-if="item && item.childNode" |
| | | :parent-node="item" |
| | | v-model:flow-node="item.childNode" |
| | | @find:recursive-find-parent-node="recursiveFindParentNode" |
| | | /> |
| | | </div> |
| | | </div> |
| | | <NodeHandler |
| | | v-if="currentNode" |
| | | v-model:child-node="currentNode.childNode" |
| | | :current-node="currentNode" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup lang="ts"> |
| | | import NodeHandler from '../NodeHandler.vue' |
| | | import ProcessNodeTree from '../ProcessNodeTree.vue' |
| | | import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts' |
| | | import { useTaskStatusClass } from '../node' |
| | | import { generateUUID } from '@/utils' |
| | | const { proxy } = getCurrentInstance() as any |
| | | defineOptions({ |
| | | name: 'ParallelNode' |
| | | }) |
| | | const props = defineProps({ |
| | | flowNode: { |
| | | type: Object as () => SimpleFlowNode, |
| | | required: true |
| | | } |
| | | }) |
| | | // 定义事件,更新父组件 |
| | | const emits = defineEmits<{ |
| | | 'update:modelValue': [node: SimpleFlowNode | undefined] |
| | | 'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: number] |
| | | 'find:recursiveFindParentNode': [ |
| | | nodeList: SimpleFlowNode[], |
| | | curentNode: SimpleFlowNode, |
| | | nodeType: number |
| | | ] |
| | | }>() |
| | | |
| | | const currentNode = ref<SimpleFlowNode>(props.flowNode) |
| | | // 是否只读 |
| | | const readonly = inject<Boolean>('readonly') |
| | | |
| | | watch( |
| | | () => props.flowNode, |
| | | (newValue) => { |
| | | currentNode.value = newValue |
| | | } |
| | | ) |
| | | |
| | | const showInputs = ref<boolean[]>([]) |
| | | // 失去焦点 |
| | | const blurEvent = (index: number) => { |
| | | showInputs.value[index] = false |
| | | const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode |
| | | conditionNode.name = conditionNode.name || `并行${index + 1}` |
| | | } |
| | | |
| | | // 点击条件名称 |
| | | const clickEvent = (index: number) => { |
| | | showInputs.value[index] = true |
| | | } |
| | | |
| | | const conditionNodeConfig = (nodeId: string) => { |
| | | const conditionNode = proxy.$refs[nodeId][0] |
| | | conditionNode.open() |
| | | } |
| | | |
| | | // 新增条件 |
| | | const addCondition = () => { |
| | | const conditionNodes = currentNode.value.conditionNodes |
| | | if (conditionNodes) { |
| | | const len = conditionNodes.length |
| | | let lastIndex = len - 1 |
| | | const conditionData: SimpleFlowNode = { |
| | | id: 'Flow_' + generateUUID(), |
| | | name: '并行' + len, |
| | | showText: '无需配置条件同时执行', |
| | | type: NodeType.CONDITION_NODE, |
| | | childNode: undefined, |
| | | conditionNodes: [] |
| | | } |
| | | conditionNodes.splice(lastIndex, 0, conditionData) |
| | | } |
| | | } |
| | | |
| | | // 删除条件 |
| | | const deleteCondition = (index: number) => { |
| | | const conditionNodes = currentNode.value.conditionNodes |
| | | if (conditionNodes) { |
| | | conditionNodes.splice(index, 1) |
| | | if (conditionNodes.length == 1) { |
| | | const childNode = currentNode.value.childNode |
| | | // 更新此节点为后续孩子节点 |
| | | emits('update:modelValue', childNode) |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 递归从父节点中查询匹配的节点 |
| | | const recursiveFindParentNode = ( |
| | | nodeList: SimpleFlowNode[], |
| | | node: SimpleFlowNode, |
| | | nodeType: number |
| | | ) => { |
| | | if (!node || node.type === NodeType.START_USER_NODE) { |
| | | return |
| | | } |
| | | if (node.type === nodeType) { |
| | | nodeList.push(node) |
| | | } |
| | | // 条件节点 (NodeType.CONDITION_NODE) 比较特殊。需要调用其父节点并行节点(NodeType.PARALLEL_NODE) 继续查找 |
| | | emits('find:parentNode', nodeList, nodeType) |
| | | } |
| | | </script> |
对比新文件 |
| | |
| | | <template> |
| | | <div class="node-wrapper"> |
| | | <div class="node-container"> |
| | | <div |
| | | class="node-box" |
| | | :class="[ |
| | | { 'node-config-error': !currentNode.showText }, |
| | | `${useTaskStatusClass(currentNode?.activityStatus)}` |
| | | ]" |
| | | > |
| | | <div class="node-title-container"> |
| | | <div class="node-title-icon start-user" |
| | | ><span class="iconfont icon-start-user"></span |
| | | ></div> |
| | | <input |
| | | v-if="showInput" |
| | | type="text" |
| | | class="editable-title-input" |
| | | @blur="blurEvent()" |
| | | v-mountedFocus |
| | | v-model="currentNode.name" |
| | | :placeholder="currentNode.name" |
| | | /> |
| | | <div v-else class="node-title" @click="clickTitle"> |
| | | {{ currentNode.name }} |
| | | </div> |
| | | </div> |
| | | <div class="node-content" @click="nodeClick"> |
| | | <div class="node-text" :title="currentNode.showText" v-if="currentNode.showText"> |
| | | {{ currentNode.showText }} |
| | | </div> |
| | | <div class="node-text" v-else> |
| | | {{ NODE_DEFAULT_TEXT.get(NodeType.START_USER_NODE) }} |
| | | </div> |
| | | <Icon icon="ep:arrow-right-bold" v-if="!readonly" /> |
| | | </div> |
| | | </div> |
| | | <!-- 传递子节点给添加节点组件。会在子节点前面添加节点 --> |
| | | <NodeHandler |
| | | v-if="currentNode" |
| | | v-model:child-node="currentNode.childNode" |
| | | :current-node="currentNode" |
| | | /> |
| | | </div> |
| | | </div> |
| | | <StartUserNodeConfig v-if="!readonly && currentNode" ref="nodeSetting" :flow-node="currentNode" /> |
| | | <!-- 审批记录 --> |
| | | <el-dialog |
| | | :title="dialogTitle || '审批记录'" |
| | | v-model="dialogVisible" |
| | | width="1000px" |
| | | append-to-body |
| | | > |
| | | <el-row> |
| | | <el-table :data="selectTasks" size="small" border header-cell-class-name="table-header-gray"> |
| | | <el-table-column |
| | | label="序号" |
| | | header-align="center" |
| | | align="center" |
| | | type="index" |
| | | width="50" |
| | | /> |
| | | <el-table-column label="审批人" min-width="100" align="center"> |
| | | <template #default="scope"> |
| | | {{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }} |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="部门" min-width="100" align="center"> |
| | | <template #default="scope"> |
| | | {{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column |
| | | :formatter="dateFormatter" |
| | | align="center" |
| | | label="开始时间" |
| | | prop="createTime" |
| | | min-width="140" |
| | | /> |
| | | <el-table-column |
| | | :formatter="dateFormatter" |
| | | align="center" |
| | | label="结束时间" |
| | | prop="endTime" |
| | | min-width="140" |
| | | /> |
| | | <el-table-column align="center" label="审批状态" prop="status" min-width="90"> |
| | | <template #default="scope"> |
| | | <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column align="center" label="审批建议" prop="reason" min-width="120" /> |
| | | <el-table-column align="center" label="耗时" prop="durationInMillis" width="100"> |
| | | <template #default="scope"> |
| | | {{ formatPast2(scope.row.durationInMillis) }} |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-row> |
| | | </el-dialog> |
| | | </template> |
| | | <script setup lang="ts"> |
| | | import NodeHandler from '../NodeHandler.vue' |
| | | import { useWatchNode, useNodeName2, useTaskStatusClass } from '../node' |
| | | import { SimpleFlowNode, NODE_DEFAULT_TEXT, NodeType } from '../consts' |
| | | import StartUserNodeConfig from '../nodes-config/StartUserNodeConfig.vue' |
| | | import { dateFormatter, formatPast2 } from '@/utils/formatTime' |
| | | import { DICT_TYPE } from '@/utils/dict' |
| | | defineOptions({ |
| | | name: 'StartEventNode' |
| | | }) |
| | | const props = defineProps({ |
| | | flowNode: { |
| | | type: Object as () => SimpleFlowNode, |
| | | default: () => null |
| | | } |
| | | }) |
| | | const readonly = inject<Boolean>('readonly') // 是否只读 |
| | | const tasks = inject<Ref<any[]>>('tasks') |
| | | // 定义事件,更新父组件。 |
| | | const emits = defineEmits<{ |
| | | 'update:modelValue': [node: SimpleFlowNode | undefined] |
| | | }>() |
| | | // 监控节点变化 |
| | | const currentNode = useWatchNode(props) |
| | | // 节点名称编辑 |
| | | const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.START_USER_NODE) |
| | | |
| | | const nodeSetting = ref() |
| | | // |
| | | const nodeClick = () => { |
| | | if (readonly) { |
| | | // 只读模式,弹窗显示任务信息 |
| | | if (tasks && tasks.value) { |
| | | dialogTitle.value = currentNode.value.name |
| | | selectTasks.value = tasks.value.filter( |
| | | (item: any) => item?.taskDefinitionKey === currentNode.value.id |
| | | ) |
| | | dialogVisible.value = true |
| | | } |
| | | } else { |
| | | // 编辑模式,打开节点配置、把当前节点传递给配置组件 |
| | | nodeSetting.value.showStartUserNodeConfig(currentNode.value) |
| | | nodeSetting.value.openDrawer() |
| | | } |
| | | } |
| | | |
| | | // 任务的弹窗显示,用于只读模式 |
| | | const dialogVisible = ref(false) // 弹窗可见性 |
| | | const dialogTitle = ref<string | undefined>(undefined) // 弹窗标题 |
| | | const selectTasks = ref<any[] | undefined>([]) // 选中的任务数组 |
| | | </script> |
| | | <style lang="scss" scoped></style> |
对比新文件 |
| | |
| | | <template> |
| | | <div class="node-wrapper"> |
| | | <div class="node-container"> |
| | | <div |
| | | class="node-box" |
| | | :class="[ |
| | | { 'node-config-error': !currentNode.showText }, |
| | | `${useTaskStatusClass(currentNode?.activityStatus)}` |
| | | ]" |
| | | > |
| | | <div class="node-title-container"> |
| | | <div class="node-title-icon user-task"><span class="iconfont icon-approve"></span></div> |
| | | <input |
| | | v-if="!readonly && showInput" |
| | | type="text" |
| | | class="editable-title-input" |
| | | @blur="blurEvent()" |
| | | v-mountedFocus |
| | | v-model="currentNode.name" |
| | | :placeholder="currentNode.name" |
| | | /> |
| | | <div v-else class="node-title" @click="clickTitle"> |
| | | {{ currentNode.name }} |
| | | </div> |
| | | </div> |
| | | <div class="node-content" @click="nodeClick"> |
| | | <div class="node-text" :title="currentNode.showText" v-if="currentNode.showText"> |
| | | {{ currentNode.showText }} |
| | | </div> |
| | | <div class="node-text" v-else> |
| | | {{ NODE_DEFAULT_TEXT.get(NodeType.USER_TASK_NODE) }} |
| | | </div> |
| | | <Icon icon="ep:arrow-right-bold" v-if="!readonly" /> |
| | | </div> |
| | | <div v-if="!readonly" class="node-toolbar"> |
| | | <div class="toolbar-icon" |
| | | ><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode" |
| | | /></div> |
| | | </div> |
| | | </div> |
| | | <!-- 传递子节点给添加节点组件。会在子节点前面添加节点 --> |
| | | <NodeHandler |
| | | v-if="currentNode" |
| | | v-model:child-node="currentNode.childNode" |
| | | :current-node="currentNode" |
| | | /> |
| | | </div> |
| | | </div> |
| | | <UserTaskNodeConfig |
| | | v-if="currentNode" |
| | | ref="nodeSetting" |
| | | :flow-node="currentNode" |
| | | @find:return-task-nodes="findReturnTaskNodes" |
| | | /> |
| | | <!-- 审批记录 --> |
| | | <el-dialog |
| | | :title="dialogTitle || '审批记录'" |
| | | v-model="dialogVisible" |
| | | width="1000px" |
| | | append-to-body |
| | | > |
| | | <el-row> |
| | | <el-table :data="selectTasks" size="small" border header-cell-class-name="table-header-gray"> |
| | | <el-table-column |
| | | label="序号" |
| | | header-align="center" |
| | | align="center" |
| | | type="index" |
| | | width="50" |
| | | /> |
| | | <el-table-column label="审批人" min-width="100" align="center"> |
| | | <template #default="scope"> |
| | | {{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }} |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="部门" min-width="100" align="center"> |
| | | <template #default="scope"> |
| | | {{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column |
| | | :formatter="dateFormatter" |
| | | align="center" |
| | | label="开始时间" |
| | | prop="createTime" |
| | | min-width="140" |
| | | /> |
| | | <el-table-column |
| | | :formatter="dateFormatter" |
| | | align="center" |
| | | label="结束时间" |
| | | prop="endTime" |
| | | min-width="140" |
| | | /> |
| | | <el-table-column align="center" label="审批状态" prop="status" min-width="90"> |
| | | <template #default="scope"> |
| | | <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column align="center" label="审批建议" prop="reason" min-width="120" /> |
| | | <el-table-column align="center" label="耗时" prop="durationInMillis" width="100"> |
| | | <template #default="scope"> |
| | | {{ formatPast2(scope.row.durationInMillis) }} |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-row> |
| | | </el-dialog> |
| | | </template> |
| | | <script setup lang="ts"> |
| | | import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts' |
| | | import { useWatchNode, useNodeName2, useTaskStatusClass } from '../node' |
| | | import NodeHandler from '../NodeHandler.vue' |
| | | import UserTaskNodeConfig from '../nodes-config/UserTaskNodeConfig.vue' |
| | | import { dateFormatter, formatPast2 } from '@/utils/formatTime' |
| | | import { DICT_TYPE } from '@/utils/dict' |
| | | defineOptions({ |
| | | name: 'UserTaskNode' |
| | | }) |
| | | const props = defineProps({ |
| | | flowNode: { |
| | | type: Object as () => SimpleFlowNode, |
| | | required: true |
| | | } |
| | | }) |
| | | const emits = defineEmits<{ |
| | | 'update:flowNode': [node: SimpleFlowNode | undefined] |
| | | 'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: NodeType] |
| | | }>() |
| | | |
| | | // 是否只读 |
| | | const readonly = inject<Boolean>('readonly') |
| | | const tasks = inject<Ref<any[]>>('tasks') |
| | | // 监控节点变化 |
| | | const currentNode = useWatchNode(props) |
| | | // 节点名称编辑 |
| | | const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.START_USER_NODE) |
| | | const nodeSetting = ref() |
| | | |
| | | const nodeClick = () => { |
| | | if (readonly) { |
| | | if (tasks && tasks.value) { |
| | | dialogTitle.value = currentNode.value.name |
| | | // 只读模式,弹窗显示任务信息 |
| | | selectTasks.value = tasks.value.filter( |
| | | (item: any) => item?.taskDefinitionKey === currentNode.value.id |
| | | ) |
| | | dialogVisible.value = true |
| | | } |
| | | } else { |
| | | // 编辑模式,打开节点配置、把当前节点传递给配置组件 |
| | | nodeSetting.value.showUserTaskNodeConfig(currentNode.value) |
| | | nodeSetting.value.openDrawer() |
| | | } |
| | | } |
| | | |
| | | const deleteNode = () => { |
| | | emits('update:flowNode', currentNode.value.childNode) |
| | | } |
| | | // 查找可以驳回用户节点 |
| | | const findReturnTaskNodes = ( |
| | | matchNodeList: SimpleFlowNode[] // 匹配的节点 |
| | | ) => { |
| | | // 从父节点查找 |
| | | emits('find:parentNode', matchNodeList, NodeType.USER_TASK_NODE) |
| | | } |
| | | |
| | | // 任务的弹窗显示,用于只读模式 |
| | | const dialogVisible = ref(false) // 弹窗可见性 |
| | | const dialogTitle = ref<string | undefined>(undefined) // 弹窗标题 |
| | | const selectTasks = ref<any[] | undefined>([]) // 选中的任务数组 |
| | | </script> |
| | | <style lang="scss" scoped></style> |
对比新文件 |
| | |
| | | import { TimeUnitType, ApproveType, APPROVE_TYPE } from './consts' |
| | | |
| | | // 获取条件节点默认的名称 |
| | | export const getDefaultConditionNodeName = (index: number, defaultFlow: boolean | undefined): string => { |
| | | if (defaultFlow) { |
| | | return '其它情况' |
| | | } |
| | | return '条件' + (index + 1) |
| | | } |
| | | |
| | | // 获取包容分支条件节点默认的名称 |
| | | export const getDefaultInclusiveConditionNodeName = (index: number, defaultFlow: boolean | undefined): string => { |
| | | if (defaultFlow) { |
| | | return '其它情况' |
| | | } |
| | | return '包容条件' + (index + 1) |
| | | } |
| | | |
| | | export const convertTimeUnit = (strTimeUnit: string) => { |
| | | if (strTimeUnit === 'M') { |
| | | return TimeUnitType.MINUTE |
| | | } |
| | | if (strTimeUnit === 'H') { |
| | | return TimeUnitType.HOUR |
| | | } |
| | | if (strTimeUnit === 'D') { |
| | | return TimeUnitType.DAY |
| | | } |
| | | return TimeUnitType.HOUR |
| | | } |
| | | |
| | | export const getApproveTypeText = (approveType: ApproveType): string => { |
| | | let approveTypeText = '' |
| | | APPROVE_TYPE.forEach((item) => { |
| | | if (item.value === approveType) { |
| | | approveTypeText = item.label |
| | | return |
| | | } |
| | | }) |
| | | return approveTypeText |
| | | } |
对比新文件 |
| | |
| | | // 配置节点头部 |
| | | .config-header { |
| | | display: flex; |
| | | flex-direction: column; |
| | | |
| | | .node-name { |
| | | display: flex; |
| | | height: 24px; |
| | | line-height: 24px; |
| | | font-size: 16px; |
| | | cursor: pointer; |
| | | align-items: center; |
| | | } |
| | | |
| | | .divide-line { |
| | | width: 100%; |
| | | height: 1px; |
| | | margin-top: 16px; |
| | | background: #eee; |
| | | } |
| | | |
| | | .config-editable-input { |
| | | height: 24px; |
| | | max-width: 510px; |
| | | font-size: 16px; |
| | | line-height: 24px; |
| | | border: 1px solid #d9d9d9; |
| | | border-radius: 4px; |
| | | transition: all 0.3s; |
| | | |
| | | &:focus { |
| | | border-color: #40a9ff; |
| | | outline: 0; |
| | | box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 表单字段权限 |
| | | .field-setting-pane { |
| | | display: flex; |
| | | flex-direction: column; |
| | | font-size: 14px; |
| | | |
| | | .field-setting-desc { |
| | | padding-right: 8px; |
| | | margin-bottom: 16px; |
| | | font-size: 16px; |
| | | font-weight: 700; |
| | | } |
| | | |
| | | .field-permit-title { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | height: 45px; |
| | | padding-left: 12px; |
| | | line-height: 45px; |
| | | background-color: #f8fafc0a; |
| | | border: 1px solid #1f38581a; |
| | | |
| | | .first-title { |
| | | text-align: left !important; |
| | | } |
| | | |
| | | .other-titles { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | } |
| | | |
| | | .setting-title-label { |
| | | display: inline-block; |
| | | width: 110px; |
| | | padding: 5px 0; |
| | | font-size: 13px; |
| | | font-weight: 700; |
| | | color: #000; |
| | | text-align: center; |
| | | } |
| | | } |
| | | |
| | | .field-setting-item { |
| | | align-items: center; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | height: 38px; |
| | | padding-left: 12px; |
| | | border: 1px solid #1f38581a; |
| | | border-top: 0; |
| | | |
| | | .field-setting-item-label { |
| | | display: inline-block; |
| | | width: 110px; |
| | | min-height: 16px; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | cursor: text; |
| | | } |
| | | |
| | | .field-setting-item-group { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | |
| | | .item-radio-wrap { |
| | | display: inline-block; |
| | | width: 110px; |
| | | text-align: center; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 节点连线气泡卡片样式 |
| | | .handler-item-wrapper { |
| | | display: flex; |
| | | cursor: pointer; |
| | | |
| | | .handler-item { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | } |
| | | |
| | | .handler-item-icon { |
| | | width: 60px; |
| | | height: 60px; |
| | | background: #fff; |
| | | border: 1px solid #e2e2e2; |
| | | border-radius: 50%; |
| | | user-select: none; |
| | | text-align: center; |
| | | |
| | | &:hover { |
| | | background: #e2e2e2; |
| | | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .icon-size { |
| | | font-size: 25px; |
| | | line-height: 60px; |
| | | } |
| | | } |
| | | |
| | | .approve { |
| | | color: #ff943e; |
| | | } |
| | | .copy { |
| | | color: #3296fa; |
| | | } |
| | | |
| | | .condition { |
| | | color: #67c23a; |
| | | } |
| | | |
| | | .parallel { |
| | | color: #626aef; |
| | | } |
| | | |
| | | .inclusive { |
| | | color: #345da2; |
| | | } |
| | | |
| | | .handler-item-text { |
| | | margin-top: 4px; |
| | | width: 80px; |
| | | text-align: center; |
| | | font-size: 13px; |
| | | } |
| | | } |
| | | // Simple 流程模型样式 |
| | | .simple-process-model-container { |
| | | height: 100%; |
| | | padding-top: 32px; |
| | | background-color: #fafafa; |
| | | overflow-x: auto; |
| | | width: 100%; |
| | | |
| | | .simple-process-model { |
| | | display: flex; |
| | | flex-direction: column; |
| | | justify-content: center; |
| | | align-items: center; |
| | | transform-origin: 50% 0 0; |
| | | min-width: fit-content; |
| | | transform: scale(1); |
| | | transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); |
| | | background: url(@/assets/svgs/bpm/simple-process-bg.svg) 0 0 repeat; |
| | | // 节点容器 定义节点宽度 |
| | | .node-container { |
| | | width: 200px; |
| | | } |
| | | // 节点 |
| | | .node-box { |
| | | position: relative; |
| | | display: flex; |
| | | min-height: 70px; |
| | | padding: 5px 10px 8px; |
| | | cursor: pointer; |
| | | background-color: #fff; |
| | | flex-direction: column; |
| | | border: 2px solid transparent; |
| | | border-radius: 8px; |
| | | box-shadow: 0 1px 4px 0 rgb(10 30 65 / 16%); |
| | | transition: all 0.1s cubic-bezier(0.645, 0.045, 0.355, 1); |
| | | |
| | | &.status-pass { |
| | | background-color: #a9da90; |
| | | border-color: #67c23a; |
| | | } |
| | | |
| | | &.status-pass:hover { |
| | | border-color: #67c23a; |
| | | } |
| | | |
| | | &.status-running { |
| | | background-color: #e7f0fe; |
| | | border-color: #5a9cf8; |
| | | } |
| | | |
| | | &.status-running:hover { |
| | | border-color: #5a9cf8; |
| | | } |
| | | |
| | | &.status-reject { |
| | | background-color: #f6e5e5; |
| | | border-color: #e47470; |
| | | } |
| | | |
| | | &.status-reject:hover { |
| | | border-color: #e47470; |
| | | } |
| | | |
| | | &:hover { |
| | | border-color: #0089ff; |
| | | |
| | | .node-toolbar { |
| | | opacity: 1; |
| | | } |
| | | |
| | | .branch-node-move { |
| | | display: flex; |
| | | } |
| | | } |
| | | |
| | | // 普通节点标题 |
| | | .node-title-container { |
| | | display: flex; |
| | | padding: 4px; |
| | | cursor: pointer; |
| | | border-radius: 4px 4px 0 0; |
| | | align-items: center; |
| | | |
| | | .node-title-icon { |
| | | display: flex; |
| | | align-items: center; |
| | | |
| | | &.user-task { |
| | | color: #ff943e; |
| | | } |
| | | |
| | | &.copy-task { |
| | | color: #3296fa; |
| | | } |
| | | |
| | | &.start-user { |
| | | color: #676565; |
| | | } |
| | | } |
| | | |
| | | .node-title { |
| | | margin-left: 4px; |
| | | overflow: hidden; |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | line-height: 18px; |
| | | color: #1f1f1f; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | |
| | | &:hover { |
| | | border-bottom: 1px dashed #f60; |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 条件节点标题 |
| | | .branch-node-title-container { |
| | | display: flex; |
| | | padding: 4px 0; |
| | | cursor: pointer; |
| | | border-radius: 4px 4px 0 0; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | |
| | | .input-max-width { |
| | | max-width: 115px !important; |
| | | } |
| | | |
| | | .branch-title { |
| | | overflow: hidden; |
| | | font-size: 13px; |
| | | font-weight: 600; |
| | | color: #f60; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | |
| | | &:hover { |
| | | border-bottom: 1px dashed #000; |
| | | } |
| | | } |
| | | |
| | | .branch-priority { |
| | | min-width: 50px; |
| | | font-size: 12px; |
| | | } |
| | | } |
| | | |
| | | .node-content { |
| | | display: flex; |
| | | min-height: 32px; |
| | | padding: 4px 8px; |
| | | margin-top: 4px; |
| | | line-height: 32px; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | color: #111f2c; |
| | | background: rgb(0 0 0 / 3%); |
| | | border-radius: 4px; |
| | | |
| | | .node-text { |
| | | display: -webkit-box; |
| | | overflow: hidden; |
| | | font-size: 14px; |
| | | line-height: 24px; |
| | | text-overflow: ellipsis; |
| | | word-break: break-all; |
| | | -webkit-line-clamp: 2; /* 这将限制文本显示为两行 */ |
| | | -webkit-box-orient: vertical; |
| | | } |
| | | } |
| | | |
| | | //条件节点内容 |
| | | .branch-node-content { |
| | | display: flex; |
| | | min-height: 32px; |
| | | padding: 4px 0; |
| | | margin-top: 4px; |
| | | line-height: 32px; |
| | | align-items: center; |
| | | color: #111f2c; |
| | | border-radius: 4px; |
| | | |
| | | .branch-node-text { |
| | | overflow: hidden; |
| | | font-size: 12px; |
| | | line-height: 24px; |
| | | text-overflow: ellipsis; |
| | | word-break: break-all; |
| | | -webkit-line-clamp: 2; /* 这将限制文本显示为两行 */ |
| | | -webkit-box-orient: vertical; |
| | | } |
| | | } |
| | | |
| | | // 节点操作 :删除 |
| | | .node-toolbar { |
| | | position: absolute; |
| | | top: -20px; |
| | | right: 0; |
| | | display: flex; |
| | | opacity: 0; |
| | | |
| | | .toolbar-icon { |
| | | text-align: center; |
| | | vertical-align: middle; |
| | | } |
| | | } |
| | | |
| | | // 条件节点左右移动 |
| | | .branch-node-move { |
| | | position: absolute; |
| | | display: none; |
| | | width: 10px; |
| | | height: 100%; |
| | | cursor: pointer; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .move-node-left { |
| | | top: 0; |
| | | left: -2px; |
| | | background: rgb(126 134 142 / 8%); |
| | | border-bottom-left-radius: 8px; |
| | | border-top-left-radius: 8px; |
| | | } |
| | | |
| | | .move-node-right { |
| | | top: 0; |
| | | right: -2px; |
| | | background: rgb(126 134 142 / 8%); |
| | | border-top-right-radius: 6px; |
| | | border-bottom-right-radius: 6px; |
| | | } |
| | | } |
| | | |
| | | .node-config-error { |
| | | border-color: #ff5219 !important; |
| | | } |
| | | // 普通节点包装 |
| | | .node-wrapper { |
| | | display: flex; |
| | | flex-direction: column; |
| | | justify-content: center; |
| | | align-items: center; |
| | | } |
| | | // 节点连线处理 |
| | | .node-handler-wrapper { |
| | | position: relative; |
| | | display: flex; |
| | | height: 70px; |
| | | align-items: center; |
| | | user-select: none; |
| | | justify-content: center; |
| | | flex-direction: column; |
| | | |
| | | &::before { |
| | | position: absolute; |
| | | top: 0; |
| | | z-index: 0; |
| | | width: 2px; |
| | | height: 100%; |
| | | margin: auto; |
| | | background-color: #dedede; |
| | | content: ''; |
| | | } |
| | | |
| | | .node-handler { |
| | | .add-icon { |
| | | position: relative; |
| | | top: -5px; |
| | | display: flex; |
| | | width: 25px; |
| | | height: 25px; |
| | | color: #fff; |
| | | cursor: pointer; |
| | | background-color: #0089ff; |
| | | border-radius: 50%; |
| | | align-items: center; |
| | | justify-content: center; |
| | | |
| | | &:hover { |
| | | transform: scale(1.1); |
| | | } |
| | | } |
| | | } |
| | | |
| | | .node-handler-arrow { |
| | | position: absolute; |
| | | bottom: 0; |
| | | left: 50%; |
| | | display: flex; |
| | | transform: translateX(-50%); |
| | | } |
| | | } |
| | | |
| | | // 条件节点包装 |
| | | .branch-node-wrapper { |
| | | position: relative; |
| | | display: flex; |
| | | flex-direction: column; |
| | | justify-content: center; |
| | | align-items: center; |
| | | margin-top: 16px; |
| | | |
| | | .branch-node-container { |
| | | position: relative; |
| | | display: flex; |
| | | min-width: fit-content; |
| | | |
| | | &::before { |
| | | position: absolute; |
| | | left: 50%; |
| | | width: 4px; |
| | | height: 100%; |
| | | background-color: #fafafa; |
| | | content: ''; |
| | | transform: translate(-50%); |
| | | } |
| | | |
| | | .branch-node-add { |
| | | position: absolute; |
| | | top: -18px; |
| | | left: 50%; |
| | | z-index: 1; |
| | | height: 36px; |
| | | padding: 0 10px; |
| | | font-size: 12px; |
| | | line-height: 36px; |
| | | border: 2px solid #dedede; |
| | | border-radius: 18px; |
| | | transform: translateX(-50%); |
| | | transform-origin: center center; |
| | | } |
| | | |
| | | .branch-node-readonly { |
| | | position: absolute; |
| | | top: -18px; |
| | | left: 50%; |
| | | z-index: 1; |
| | | display: flex; |
| | | width: 36px; |
| | | height: 36px; |
| | | background-color: #fff; |
| | | border: 2px solid #dedede; |
| | | border-radius: 50%; |
| | | transform: translateX(-50%); |
| | | align-items: center; |
| | | justify-content: center; |
| | | transform-origin: center center; |
| | | |
| | | &.status-pass { |
| | | background-color: #e9f4e2; |
| | | border-color: #6bb63c; |
| | | } |
| | | |
| | | &.status-pass:hover { |
| | | border-color: #6bb63c; |
| | | } |
| | | |
| | | .icon-size { |
| | | font-size: 22px; |
| | | &.condition { |
| | | color: #67c23a; |
| | | } |
| | | &.parallel { |
| | | color: #626aef; |
| | | } |
| | | &.inclusive { |
| | | color: #345da2; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .branch-node-item { |
| | | position: relative; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | min-width: 280px; |
| | | padding: 40px 40px 0; |
| | | background: transparent; |
| | | border-top: 2px solid #dedede; |
| | | border-bottom: 2px solid #dedede; |
| | | flex-shrink: 0; |
| | | |
| | | &::before { |
| | | position: absolute; |
| | | width: 2px; |
| | | height: 100%; |
| | | margin: auto; |
| | | inset: 0; |
| | | background-color: #dedede; |
| | | content: ''; |
| | | } |
| | | } |
| | | // 覆盖条件节点第一个节点左上角的线 |
| | | .branch-line-first-top { |
| | | position: absolute; |
| | | top: -5px; |
| | | left: -1px; |
| | | width: 50%; |
| | | height: 7px; |
| | | background-color: #fafafa; |
| | | content: ''; |
| | | } |
| | | // 覆盖条件节点第一个节点左下角的线 |
| | | .branch-line-first-bottom { |
| | | position: absolute; |
| | | bottom: -5px; |
| | | left: -1px; |
| | | width: 50%; |
| | | height: 7px; |
| | | background-color: #fafafa; |
| | | content: ''; |
| | | } |
| | | // 覆盖条件节点最后一个节点右上角的线 |
| | | .branch-line-last-top { |
| | | position: absolute; |
| | | top: -5px; |
| | | right: -1px; |
| | | width: 50%; |
| | | height: 7px; |
| | | background-color: #fafafa; |
| | | content: ''; |
| | | } |
| | | // 覆盖条件节点最后一个节点右下角的线 |
| | | .branch-line-last-bottom { |
| | | position: absolute; |
| | | right: -1px; |
| | | bottom: -5px; |
| | | width: 50%; |
| | | height: 7px; |
| | | background-color: #fafafa; |
| | | content: ''; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .node-fixed-name { |
| | | display: inline-block; |
| | | width: auto; |
| | | padding: 0 4px; |
| | | overflow: hidden; |
| | | text-align: center; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | // 开始节点包装 |
| | | .start-node-wrapper { |
| | | position: relative; |
| | | margin-top: 16px; |
| | | |
| | | .start-node-container { |
| | | display: flex; |
| | | flex-direction: column; |
| | | justify-content: center; |
| | | align-items: center; |
| | | |
| | | .start-node-box { |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | width: 90px; |
| | | height: 36px; |
| | | padding: 3px 4px; |
| | | color: #212121; |
| | | cursor: pointer; |
| | | background: #fafafa; |
| | | border-radius: 30px; |
| | | box-shadow: 0 1px 5px 0 rgb(10 30 65 / 8%); |
| | | box-sizing: border-box; |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 结束节点包装 |
| | | .end-node-wrapper { |
| | | margin-bottom: 16px; |
| | | |
| | | .end-node-box { |
| | | display: flex; |
| | | width: 80px; |
| | | height: 36px; |
| | | color: #212121; |
| | | border: 2px solid #fafafa; |
| | | border-radius: 30px; |
| | | box-shadow: 0 1px 5px 0 rgb(10 30 65 / 8%); |
| | | box-sizing: border-box; |
| | | justify-content: center; |
| | | align-items: center; |
| | | |
| | | &.status-pass { |
| | | background-color: #a9da90; |
| | | border-color: #6bb63c; |
| | | } |
| | | |
| | | &.status-pass:hover { |
| | | border-color: #6bb63c; |
| | | } |
| | | |
| | | &.status-reject { |
| | | background-color: #f6e5e5; |
| | | border-color: #e47470; |
| | | } |
| | | |
| | | &.status-reject:hover { |
| | | border-color: #e47470; |
| | | } |
| | | |
| | | &.status-cancel { |
| | | background-color: #eaeaeb; |
| | | border-color: #919398; |
| | | } |
| | | |
| | | &.status-cancel:hover { |
| | | border-color: #919398; |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 可编辑的 title 输入框 |
| | | .editable-title-input { |
| | | height: 20px; |
| | | max-width: 145px; |
| | | margin-left: 4px; |
| | | font-size: 12px; |
| | | line-height: 20px; |
| | | border: 1px solid #d9d9d9; |
| | | border-radius: 4px; |
| | | transition: all 0.3s; |
| | | |
| | | &:focus { |
| | | border-color: #40a9ff; |
| | | outline: 0; |
| | | box-shadow: 0 0 0 2px rgb(24 144 255 / 20%); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // iconfont 样式 |
| | | @font-face { |
| | | font-family: 'iconfont'; /* Project id 4495938 */ |
| | | src: |
| | | url('iconfont.woff2?t=1724339470412') format('woff2'), |
| | | url('iconfont.woff?t=1724339470412') format('woff'), |
| | | url('iconfont.ttf?t=1724339470412') format('truetype'); |
| | | } |
| | | |
| | | .iconfont { |
| | | font-family: 'iconfont' !important; |
| | | font-size: 16px; |
| | | font-style: normal; |
| | | -webkit-font-smoothing: antialiased; |
| | | -moz-osx-font-smoothing: grayscale; |
| | | } |
| | | |
| | | .icon-start-user:before { |
| | | content: '\e679'; |
| | | } |
| | | |
| | | .icon-inclusive:before { |
| | | content: '\e602'; |
| | | } |
| | | |
| | | .icon-copy:before { |
| | | content: '\e7eb'; |
| | | } |
| | | |
| | | .icon-handle:before { |
| | | content: '\e61c'; |
| | | } |
| | | |
| | | .icon-exclusive:before { |
| | | content: '\e717'; |
| | | } |
| | | |
| | | .icon-approve:before { |
| | | content: '\e715'; |
| | | } |
| | | |
| | | .icon-parallel:before { |
| | | content: '\e688'; |
| | | } |
| | |
| | | <template> |
| | | <div class="upload-file"> |
| | | <div v-if="!disabled" class="upload-file"> |
| | | <el-upload |
| | | ref="uploadRef" |
| | | v-model:file-list="fileList" |
| | |
| | | class="upload-file-uploader" |
| | | name="file" |
| | | > |
| | | <el-button v-if="!disabled" type="primary"> |
| | | <el-button type="primary"> |
| | | <Icon icon="ep:upload-filled" /> |
| | | 选取文件 |
| | | </el-button> |
| | | <template v-if="isShowTip && !disabled" #tip> |
| | | <template v-if="isShowTip" #tip> |
| | | <div style="font-size: 8px"> |
| | | 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> |
| | | </div> |
| | |
| | | 格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b> 的文件 |
| | | </div> |
| | | </template> |
| | | <!-- TODO @puhui999:1)表单展示的时候,位置会偏掉,已发微信;2)disable 的时候,应该把【删除】按钮也隐藏掉? --> |
| | | <template #file="row"> |
| | | <div class="flex items-center"> |
| | | <span>{{ row.file.name }}</span> |
| | |
| | | </div> |
| | | </template> |
| | | </el-upload> |
| | | </div> |
| | | |
| | | <!-- 上传操作禁用时 --> |
| | | <div v-if="disabled" class="upload-file"> |
| | | <div v-for="(file, index) in fileList" :key="index" class="flex items-center file-list-item"> |
| | | <span>{{ file.name }}</span> |
| | | <div class="ml-10px"> |
| | | <el-link :href="file.url" :underline="false" download target="_blank" type="primary"> |
| | | 下载 |
| | | </el-link> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <script lang="ts" setup> |
| | |
| | | :deep(.ele-upload-list__item-content-action .el-link) { |
| | | margin-right: 10px; |
| | | } |
| | | |
| | | .file-list-item { |
| | | border: 1px dashed var(--el-border-color-darker); |
| | | border-radius: 8px; |
| | | } |
| | | </style> |
| | |
| | | <template #file="{ file }"> |
| | | <img :src="file.url" class="upload-image" /> |
| | | <div class="upload-handle" @click.stop> |
| | | <div class="handle-icon" @click="handlePictureCardPreview(file)"> |
| | | <div class="handle-icon" @click="imagePreview(file.url!)"> |
| | | <Icon icon="ep:zoom-in" /> |
| | | <span>查看</span> |
| | | </div> |
| | |
| | | <div class="el-upload__tip"> |
| | | <slot name="tip"></slot> |
| | | </div> |
| | | <el-image-viewer |
| | | v-if="imgViewVisible" |
| | | :url-list="[viewImageUrl]" |
| | | @close="imgViewVisible = false" |
| | | /> |
| | | </div> |
| | | </template> |
| | | <script lang="ts" setup> |
| | | import type { UploadFile, UploadProps, UploadUserFile } from 'element-plus' |
| | | import { ElNotification } from 'element-plus' |
| | | import { createImageViewer } from '@/components/ImageViewer' |
| | | |
| | | import { propTypes } from '@/utils/propTypes' |
| | | import { useUpload } from '@/components/UploadFile/src/useUpload' |
| | |
| | | defineOptions({ name: 'UploadImgs' }) |
| | | |
| | | const message = useMessage() // 消息弹窗 |
| | | // 查看图片 |
| | | const imagePreview = (imgUrl: string) => { |
| | | createImageViewer({ |
| | | zIndex: 9999999, |
| | | urlList: [imgUrl] |
| | | }) |
| | | } |
| | | |
| | | type FileTypes = |
| | | | 'image/apng' |
| | |
| | | message: `当前最多只能上传 ${props.limit} 张图片,请移除后上传!`, |
| | | type: 'warning' |
| | | }) |
| | | } |
| | | |
| | | // 图片预览 |
| | | const viewImageUrl = ref('') |
| | | const imgViewVisible = ref(false) |
| | | const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => { |
| | | viewImageUrl.value = uploadFile.url! |
| | | imgViewVisible.value = true |
| | | } |
| | | </script> |
| | | |
| | |
| | | import { UploadRawFile, UploadRequestOptions } from 'element-plus/es/components/upload/src/upload' |
| | | import axios from 'axios' |
| | | |
| | | /** |
| | | * 获得上传 URL |
| | | */ |
| | | export const getUploadUrl = (): string => { |
| | | return import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/infra/file/upload' |
| | | } |
| | | |
| | | export const useUpload = () => { |
| | | // 后端上传地址 |
| | | const uploadUrl = import.meta.env.VITE_UPLOAD_URL |
| | | const uploadUrl = getUploadUrl() |
| | | // 是否使用前端直连上传 |
| | | const isClientUpload = UPLOAD_TYPE.CLIENT === import.meta.env.VITE_UPLOAD_TYPE |
| | | // 重写ElUpload上传方法 |
| | |
| | | // 1.2 获取文件预签名地址 |
| | | const presignedInfo = await FileApi.getFilePresignedUrl(fileName) |
| | | // 1.3 上传文件(不能使用 ElUpload 的 ajaxUpload 方法的原因:其使用的是 FormData 上传,Minio 不支持) |
| | | return axios.put(presignedInfo.uploadUrl, options.file, { |
| | | headers: { |
| | | 'Content-Type': options.file.type, |
| | | } |
| | | }).then(() => { |
| | | // 1.4. 记录文件信息到后端(异步) |
| | | createFile(presignedInfo, fileName, options.file) |
| | | // 通知成功,数据格式保持与后端上传的返回结果一致 |
| | | return { data: presignedInfo.url } |
| | | }) |
| | | return axios |
| | | .put(presignedInfo.uploadUrl, options.file, { |
| | | headers: { |
| | | 'Content-Type': options.file.type |
| | | } |
| | | }) |
| | | .then(() => { |
| | | // 1.4. 记录文件信息到后端(异步) |
| | | createFile(presignedInfo, fileName, options.file) |
| | | // 通知成功,数据格式保持与后端上传的返回结果一致 |
| | | return { data: presignedInfo.url } |
| | | }) |
| | | } else { |
| | | // 模式二:后端上传 |
| | | // 重写 el-upload httpRequest 文件上传成功会走成功的钩子,失败走失败的钩子 |
| | |
| | | enum UPLOAD_TYPE { |
| | | // 客户端直接上传(只支持S3服务) |
| | | CLIENT = 'client', |
| | | // 客户端发送到后端上传 |
| | | SERVER = 'server' |
| | | } |
对比新文件 |
| | |
| | | <template> |
| | | <Dialog v-model="dialogVisible" title="人员选择" width="800"> |
| | | <el-row class="gap2" v-loading="formLoading"> |
| | | <el-col :span="6"> |
| | | <ContentWrap class="h-1/1"> |
| | | <el-tree |
| | | ref="treeRef" |
| | | :data="deptTree" |
| | | :expand-on-click-node="false" |
| | | :props="defaultProps" |
| | | default-expand-all |
| | | highlight-current |
| | | node-key="id" |
| | | @node-click="handleNodeClick" |
| | | /> |
| | | </ContentWrap> |
| | | </el-col> |
| | | <el-col :span="17"> |
| | | <el-transfer |
| | | v-model="selectedUserIdList" |
| | | :titles="['未选', '已选']" |
| | | filterable |
| | | filter-placeholder="搜索成员" |
| | | :data="transferUserList" |
| | | :props="{ label: 'nickname', key: 'id' }" |
| | | /> |
| | | </el-col> |
| | | </el-row> |
| | | <template #footer> |
| | | <el-button |
| | | :disabled="formLoading || !selectedUserIdList?.length" |
| | | type="primary" |
| | | @click="submitForm" |
| | | > |
| | | 确 定 |
| | | </el-button> |
| | | <el-button @click="dialogVisible = false">取 消</el-button> |
| | | </template> |
| | | </Dialog> |
| | | </template> |
| | | <script lang="ts" setup> |
| | | import { defaultProps, handleTree } from '@/utils/tree' |
| | | import * as DeptApi from '@/api/system/dept' |
| | | import * as UserApi from '@/api/system/user' |
| | | |
| | | defineOptions({ name: 'UserSelectForm' }) |
| | | const emit = defineEmits<{ |
| | | confirm: [id: any, userList: any[]] |
| | | }>() |
| | | const { t } = useI18n() // 国际 |
| | | const message = useMessage() // 消息弹窗 |
| | | const deptTree = ref<Tree[]>([]) // 部门树形结构化 |
| | | const deptList = ref<any[]>([]) // 保存扁平化的部门列表数据 |
| | | const userList = ref<UserApi.UserVO[]>([]) // 所有用户列表 |
| | | const filteredUserList = ref<UserApi.UserVO[]>([]) // 当前部门过滤后的用户列表 |
| | | const selectedUserIdList: any = ref([]) // 选中的用户列表 |
| | | const dialogVisible = ref(false) // 弹窗的是否展示 |
| | | const formLoading = ref(false) // 表单的加载中 |
| | | const activityId = ref() |
| | | |
| | | /** 计算属性:合并已选择的用户和当前部门过滤后的用户 */ |
| | | const transferUserList = computed(() => { |
| | | // 1.1 获取所有已选择的用户 |
| | | const selectedUsers = userList.value.filter((user: any) => |
| | | selectedUserIdList.value.includes(user.id) |
| | | ) |
| | | |
| | | // 1.2 获取当前部门过滤后的未选择用户 |
| | | const filteredUnselectedUsers = filteredUserList.value.filter( |
| | | (user: any) => !selectedUserIdList.value.includes(user.id) |
| | | ) |
| | | |
| | | // 2. 合并并去重 |
| | | return [...selectedUsers, ...filteredUnselectedUsers] |
| | | }) |
| | | |
| | | /** 打开弹窗 */ |
| | | const open = async (id: number, selectedList?: any[]) => { |
| | | activityId.value = id |
| | | resetForm() |
| | | |
| | | // 加载部门、用户列表 |
| | | const deptData = await DeptApi.getSimpleDeptList() |
| | | deptList.value = deptData // 保存扁平结构的部门数据 |
| | | deptTree.value = handleTree(deptData) // 转换成树形结构 |
| | | userList.value = await UserApi.getSimpleUserList() |
| | | |
| | | // 初始状态下,过滤列表等于所有用户列表 |
| | | filteredUserList.value = [...userList.value] |
| | | selectedUserIdList.value = selectedList?.map((item: any) => item.id) || [] |
| | | dialogVisible.value = true |
| | | } |
| | | |
| | | /** 获取指定部门及其所有子部门的ID列表 */ |
| | | const getChildDeptIds = (deptId: number, deptList: any[]): number[] => { |
| | | const ids = [deptId] |
| | | const children = deptList.filter((dept) => dept.parentId === deptId) |
| | | children.forEach((child) => { |
| | | ids.push(...getChildDeptIds(child.id, deptList)) |
| | | }) |
| | | return ids |
| | | } |
| | | |
| | | /** 获取部门过滤后的用户列表 */ |
| | | const filterUserList = async (deptId?: number) => { |
| | | formLoading.value = true |
| | | try { |
| | | if (!deptId) { |
| | | // 如果没有选择部门,显示所有用户 |
| | | filteredUserList.value = [...userList.value] |
| | | return |
| | | } |
| | | |
| | | // 直接使用已保存的部门列表数据进行过滤 |
| | | const deptIds = getChildDeptIds(deptId, deptList.value) |
| | | |
| | | // 过滤出这些部门下的用户 |
| | | filteredUserList.value = userList.value.filter((user) => deptIds.includes(user.deptId)) |
| | | } finally { |
| | | formLoading.value = false |
| | | } |
| | | } |
| | | |
| | | /** 提交选择 */ |
| | | const submitForm = async () => { |
| | | try { |
| | | message.success(t('common.updateSuccess')) |
| | | dialogVisible.value = false |
| | | // 从所有用户列表中筛选出已选择的用户 |
| | | const emitUserList = userList.value.filter((user: any) => |
| | | selectedUserIdList.value.includes(user.id) |
| | | ) |
| | | // 发送操作成功的事件 |
| | | emit('confirm', activityId.value, emitUserList) |
| | | } finally { |
| | | } |
| | | } |
| | | |
| | | /** 重置表单 */ |
| | | const resetForm = () => { |
| | | deptTree.value = [] |
| | | deptList.value = [] |
| | | userList.value = [] |
| | | filteredUserList.value = [] |
| | | selectedUserIdList.value = [] |
| | | } |
| | | |
| | | /** 处理部门被点击 */ |
| | | const handleNodeClick = (row: { [key: string]: any }) => { |
| | | filterUserList(row.id) |
| | | } |
| | | |
| | | defineExpose({ open }) // 提供 open 方法,用于打开弹窗 |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | :deep() { |
| | | .el-transfer { |
| | | display: flex; |
| | | } |
| | | .el-transfer__buttons { |
| | | display: flex !important; |
| | | flex-direction: column-reverse; |
| | | justify-content: center; |
| | | gap: 20px; |
| | | .el-transfer__button:nth-child(2) { |
| | | margin: 0; |
| | | } |
| | | } |
| | | } |
| | | </style> |
| | |
| | | <XButton preIcon="ep:refresh" @click="processRestart()" /> |
| | | </el-tooltip> |
| | | </ElButtonGroup> |
| | | <XButton |
| | | preIcon="ep:plus" |
| | | title="保存模型" |
| | | @click="processSave" |
| | | :type="props.headerButtonType" |
| | | :disabled="simulationStatus" |
| | | /> |
| | | </template> |
| | | <!-- 用于打开本地文件--> |
| | | <input |
| | |
| | | ['default', 'primary', 'success', 'warning', 'danger', 'info'].indexOf(value) !== -1 |
| | | } |
| | | }) |
| | | |
| | | // 监听value变化,重新加载流程图 |
| | | watch( |
| | | () => props.value, |
| | | (newValue) => { |
| | | if (newValue && bpmnModeler) { |
| | | createNewDiagram(newValue) |
| | | } |
| | | }, |
| | | { immediate: true } |
| | | ) |
| | | |
| | | // 监听processId和processName变化 |
| | | watch( |
| | | [() => props.processId, () => props.processName], |
| | | ([newId, newName]) => { |
| | | if (newId && newName && !props.value) { |
| | | createNewDiagram(null) |
| | | } |
| | | }, |
| | | { immediate: true } |
| | | ) |
| | | |
| | | provide('configGlobal', props) |
| | | let bpmnModeler: any = null |
| | |
| | | defaultZoom.value = newZoom |
| | | bpmnModeler.get('canvas').zoom(defaultZoom.value) |
| | | } |
| | | // const processZoomTo = (newZoom = 1) => { |
| | | // if (newZoom < 0.2) { |
| | | // throw new Error('[Process Designer Warn ]: The zoom ratio cannot be less than 0.2') |
| | | // } |
| | | // if (newZoom > 4) { |
| | | // throw new Error('[Process Designer Warn ]: The zoom ratio cannot be greater than 4') |
| | | // } |
| | | // defaultZoom = newZoom |
| | | // bpmnModeler.get('canvas').zoom(newZoom) |
| | | // } |
| | | const processReZoom = () => { |
| | | defaultZoom.value = 1 |
| | | bpmnModeler.get('canvas').zoom('fit-viewport', 'auto') |
| | |
| | | } |
| | | const previewProcessJson = () => { |
| | | bpmnModeler.saveXML({ format: true }).then(({ xml }) => { |
| | | // console.log(xml, 'xml') |
| | | |
| | | // const rootNode = parseXmlString(xml) |
| | | // console.log(rootNode, 'rootNoderootNode') |
| | | const rootNodes = new XmlNode(XmlNodeType.Root, parseXmlString(xml)) |
| | | // console.log(rootNodes, 'rootNodesrootNodesrootNodes') |
| | | // console.log(rootNodes.parent.toJsObject(), 'rootNodes.toJSON()') |
| | | // console.log(JSON.stringify(rootNodes.parent.toJsObject()), 'rootNodes.toJSON()') |
| | | // console.log(JSON.stringify(rootNodes.parent.toJSON()), 'rootNodes.toJSON()') |
| | | |
| | | // const parser = new xml2js.XMLParser() |
| | | // let jObj = parser.parse(xml) |
| | | // console.log(jObj, 'jObjjObjjObjjObjjObj') |
| | | // const builder = new xml2js.XMLBuilder(xml) |
| | | // const xmlContent = builder |
| | | // console.log(xmlContent, 'xmlContent') |
| | | // console.log(xml2js, 'convertconvertconvert') |
| | | previewResult.value = rootNodes.parent?.toJSON() as unknown as string |
| | | // previewResult.value = jObj |
| | | // previewResult.value = convert.xml2json(xml, {explicitArray : false},{ spaces: 2 }) |
| | | previewType.value = 'json' |
| | | previewModelVisible.value = true |
| | | }) |
| | | } |
| | | /* ------------------------------------------------ 工业互联网平台 methods ------------------------------------------------------ */ |
| | | const processSave = async () => { |
| | | // console.log(bpmnModeler, 'bpmnModelerbpmnModelerbpmnModelerbpmnModeler') |
| | | const { err, xml } = await bpmnModeler.saveXML() |
| | | // console.log(err, 'errerrerrerrerr') |
| | | // console.log(xml, 'xmlxmlxmlxmlxml') |
| | | // 读取异常时抛出异常 |
| | | if (err) { |
| | | // this.$modal.msgError('保存模型失败,请重试!') |
| | | alert('保存模型失败,请重试!') |
| | | return |
| | | } |
| | | // 触发 save 事件 |
| | | emit('save', xml) |
| | | } |
| | | /** 高亮显示 */ |
| | | // const highlightedCode = (previewType, previewResult) => { |
| | | // console.log(previewType, 'previewType, previewResult') |
| | | // console.log(previewResult, 'previewType, previewResult') |
| | | // console.log(hljs.highlight, 'hljs.highlight') |
| | | // const result = hljs.highlight(previewType, previewResult.value || '', true) |
| | | // return result.value || ' ' |
| | | // } |
| | | onBeforeMount(() => { |
| | | console.log(props, 'propspropspropsprops') |
| | | }) |
| | | |
| | | /* ------------------------------------------------ 芋道源码 methods ------------------------------------------------------ */ |
| | | onMounted(() => { |
| | | initBpmnModeler() |
| | | createNewDiagram(props.value) |
| | | }) |
| | | onBeforeUnmount(() => { |
| | | // this.$once('hook:beforeDestroy', () => { |
| | | // }) |
| | | if (bpmnModeler) bpmnModeler.destroy() |
| | | emit('destroy', bpmnModeler) |
| | | bpmnModeler = null |
| | |
| | | <template> |
| | | <div class="my-process-designer"> |
| | | <div class="my-process-designer__container"> |
| | | <div class="my-process-designer__canvas" style="height: 760px" ref="bpmnCanvas"></div> |
| | | <div class="process-viewer"> |
| | | <div style="height: 100%" ref="processCanvas" v-show="!isLoading"> </div> |
| | | <!-- 自定义箭头样式,用于已完成状态下流程连线箭头 --> |
| | | <defs ref="customDefs"> |
| | | <marker |
| | | id="sequenceflow-end-white-success" |
| | | viewBox="0 0 20 20" |
| | | refX="11" |
| | | refY="10" |
| | | markerWidth="10" |
| | | markerHeight="10" |
| | | orient="auto" |
| | | > |
| | | <path |
| | | class="success-arrow" |
| | | d="M 1 5 L 11 10 L 1 15 Z" |
| | | style="stroke-width: 1px; stroke-linecap: round; stroke-dasharray: 10000, 1" |
| | | /> |
| | | </marker> |
| | | <marker |
| | | id="conditional-flow-marker-white-success" |
| | | viewBox="0 0 20 20" |
| | | refX="-1" |
| | | refY="10" |
| | | markerWidth="10" |
| | | markerHeight="10" |
| | | orient="auto" |
| | | > |
| | | <path |
| | | class="success-conditional" |
| | | d="M 0 10 L 8 6 L 16 10 L 8 14 Z" |
| | | style="stroke-width: 1px; stroke-linecap: round; stroke-dasharray: 10000, 1" |
| | | /> |
| | | </marker> |
| | | </defs> |
| | | |
| | | <!-- 审批记录 --> |
| | | <el-dialog :title="dialogTitle || '审批记录'" v-model="dialogVisible" width="1000px"> |
| | | <el-row> |
| | | <el-table |
| | | :data="selectTasks" |
| | | size="small" |
| | | border |
| | | header-cell-class-name="table-header-gray" |
| | | > |
| | | <el-table-column |
| | | label="序号" |
| | | header-align="center" |
| | | align="center" |
| | | type="index" |
| | | width="50" |
| | | /> |
| | | <el-table-column |
| | | label="审批人" |
| | | min-width="100" |
| | | align="center" |
| | | v-if="selectActivityType === 'bpmn:UserTask'" |
| | | > |
| | | <template #default="scope"> |
| | | {{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column |
| | | label="发起人" |
| | | prop="assigneeUser.nickname" |
| | | min-width="100" |
| | | align="center" |
| | | v-else |
| | | /> |
| | | <el-table-column label="部门" min-width="100" align="center"> |
| | | <template #default="scope"> |
| | | {{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column |
| | | :formatter="dateFormatter" |
| | | align="center" |
| | | label="开始时间" |
| | | prop="createTime" |
| | | min-width="140" |
| | | /> |
| | | <el-table-column |
| | | :formatter="dateFormatter" |
| | | align="center" |
| | | label="结束时间" |
| | | prop="endTime" |
| | | min-width="140" |
| | | /> |
| | | <el-table-column align="center" label="审批状态" prop="status" min-width="90"> |
| | | <template #default="scope"> |
| | | <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column |
| | | align="center" |
| | | label="审批建议" |
| | | prop="reason" |
| | | min-width="120" |
| | | v-if="selectActivityType === 'bpmn:UserTask'" |
| | | /> |
| | | <el-table-column align="center" label="耗时" prop="durationInMillis" width="100"> |
| | | <template #default="scope"> |
| | | {{ formatPast2(scope.row.durationInMillis) }} |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-row> |
| | | </el-dialog> |
| | | |
| | | <!-- Zoom:放大、缩小 --> |
| | | <div style="position: absolute; top: 0; left: 0; width: 100%"> |
| | | <el-row type="flex" justify="end"> |
| | | <el-button-group key="scale-control" size="default"> |
| | | <el-button |
| | | size="default" |
| | | :plain="true" |
| | | :disabled="defaultZoom <= 0.3" |
| | | :icon="ZoomOut" |
| | | @click="processZoomOut()" |
| | | /> |
| | | <el-button size="default" style="width: 90px"> |
| | | {{ Math.floor(defaultZoom * 10 * 10) + '%' }} |
| | | </el-button> |
| | | <el-button |
| | | size="default" |
| | | :plain="true" |
| | | :disabled="defaultZoom >= 3.9" |
| | | :icon="ZoomIn" |
| | | @click="processZoomIn()" |
| | | /> |
| | | <el-button size="default" :icon="ScaleToOriginal" @click="processReZoom()" /> |
| | | </el-button-group> |
| | | </el-row> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | import '../theme/index.scss' |
| | | import BpmnViewer from 'bpmn-js/lib/Viewer' |
| | | import DefaultEmptyXML from './plugins/defaultEmpty' |
| | | import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' |
| | | import { formatDate } from '@/utils/formatTime' |
| | | import { isEmpty } from '@/utils/is' |
| | | |
| | | defineOptions({ name: 'MyProcessViewer' }) |
| | | import MoveCanvasModule from 'diagram-js/lib/navigation/movecanvas' |
| | | import { ZoomOut, ZoomIn, ScaleToOriginal } from '@element-plus/icons-vue' |
| | | import { DICT_TYPE } from '@/utils/dict' |
| | | import { dateFormatter, formatPast2 } from '@/utils/formatTime' |
| | | import { BpmProcessInstanceStatus } from '@/utils/constants' |
| | | |
| | | const props = defineProps({ |
| | | value: { |
| | | // BPMN XML 字符串 |
| | | xml: { |
| | | type: String, |
| | | default: '' |
| | | required: true |
| | | }, |
| | | prefix: { |
| | | // 使用哪个引擎 |
| | | type: String, |
| | | default: 'camunda' |
| | | }, |
| | | activityData: { |
| | | // 活动的数据。传递时,可高亮流程 |
| | | type: Array, |
| | | default: () => [] |
| | | }, |
| | | processInstanceData: { |
| | | // 流程实例的数据。传递时,可展示流程发起人等信息 |
| | | view: { |
| | | type: Object, |
| | | default: () => {} |
| | | }, |
| | | taskData: { |
| | | // 任务实例的数据。传递时,可展示 UserTask 审核相关的信息 |
| | | type: Array, |
| | | default: () => [] |
| | | require: true |
| | | } |
| | | }) |
| | | |
| | | provide('configGlobal', props) |
| | | const processCanvas = ref() |
| | | const bpmnViewer = ref<BpmnViewer | null>(null) |
| | | const customDefs = ref() |
| | | const defaultZoom = ref(1) // 默认缩放比例 |
| | | const isLoading = ref(false) // 是否加载中 |
| | | |
| | | const emit = defineEmits(['destroy']) |
| | | const processInstance = ref<any>({}) // 流程实例 |
| | | const tasks = ref([]) // 流程任务 |
| | | |
| | | let bpmnModeler |
| | | const dialogVisible = ref(false) // 弹窗可见性 |
| | | const dialogTitle = ref<string | undefined>(undefined) // 弹窗标题 |
| | | const selectActivityType = ref<string | undefined>(undefined) // 选中 Task 的活动编号 |
| | | const selectTasks = ref<any[]>([]) // 选中的任务数组 |
| | | |
| | | const xml = ref('') |
| | | const activityLists = ref<any[]>([]) |
| | | const processInstance = ref<any>(undefined) |
| | | const taskList = ref<any[]>([]) |
| | | const bpmnCanvas = ref() |
| | | // const element = ref() |
| | | const elementOverlayIds = ref<any>(null) |
| | | const overlays = ref<any>(null) |
| | | |
| | | const initBpmnModeler = () => { |
| | | if (bpmnModeler) return |
| | | bpmnModeler = new BpmnViewer({ |
| | | container: bpmnCanvas.value, |
| | | bpmnRenderer: {} |
| | | }) |
| | | /** Zoom:恢复 */ |
| | | const processReZoom = () => { |
| | | defaultZoom.value = 1 |
| | | bpmnViewer.value?.get('canvas').zoom('fit-viewport', 'auto') |
| | | } |
| | | |
| | | /* 创建新的流程图 */ |
| | | const createNewDiagram = async (xml) => { |
| | | // 将字符串转换成图显示出来 |
| | | let newId = `Process_${new Date().getTime()}` |
| | | let newName = `业务流程_${new Date().getTime()}` |
| | | let xmlString = xml || DefaultEmptyXML(newId, newName, props.prefix) |
| | | try { |
| | | let { warnings } = await bpmnModeler.importXML(xmlString) |
| | | if (warnings && warnings.length) { |
| | | warnings.forEach((warn) => console.warn(warn)) |
| | | } |
| | | // 高亮流程图 |
| | | await highlightDiagram() |
| | | const canvas = bpmnModeler.get('canvas') |
| | | canvas.zoom('fit-viewport', 'auto') |
| | | } catch (e) { |
| | | console.error(e) |
| | | // console.error(`[Process Designer Warn]: ${e?.message || e}`); |
| | | /** Zoom:放大 */ |
| | | const processZoomIn = (zoomStep = 0.1) => { |
| | | let newZoom = Math.floor(defaultZoom.value * 100 + zoomStep * 100) / 100 |
| | | if (newZoom > 4) { |
| | | throw new Error('[Process Designer Warn ]: The zoom ratio cannot be greater than 4') |
| | | } |
| | | defaultZoom.value = newZoom |
| | | bpmnViewer.value?.get('canvas').zoom(defaultZoom.value) |
| | | } |
| | | |
| | | /* 高亮流程图 */ |
| | | // TODO 芋艿:如果多个 endActivity 的话,目前的逻辑可能有一定的问题。https://www.jdon.com/workflow/multi-events.html |
| | | const highlightDiagram = async () => { |
| | | const activityList = activityLists.value |
| | | if (activityList.length === 0) { |
| | | /** Zoom:缩小 */ |
| | | const processZoomOut = (zoomStep = 0.1) => { |
| | | let newZoom = Math.floor(defaultZoom.value * 100 - zoomStep * 100) / 100 |
| | | if (newZoom < 0.2) { |
| | | throw new Error('[Process Designer Warn ]: The zoom ratio cannot be less than 0.2') |
| | | } |
| | | defaultZoom.value = newZoom |
| | | bpmnViewer.value?.get('canvas').zoom(defaultZoom.value) |
| | | } |
| | | |
| | | /** 流程图预览清空 */ |
| | | const clearViewer = () => { |
| | | if (processCanvas.value) { |
| | | processCanvas.value.innerHTML = '' |
| | | } |
| | | if (bpmnViewer.value) { |
| | | bpmnViewer.value.destroy() |
| | | } |
| | | bpmnViewer.value = null |
| | | } |
| | | |
| | | /** 添加自定义箭头 */ |
| | | // TODO 芋艿:自定义箭头不生效,有点奇怪!!!!相关的 marker-end、marker-start 暂时也注释了!!! |
| | | const addCustomDefs = () => { |
| | | if (!bpmnViewer.value) { |
| | | return |
| | | } |
| | | // 参考自 https://gitee.com/tony2y/RuoYi-flowable/blob/master/ruoyi-ui/src/components/Process/index.vue#L222 实现 |
| | | // 再次基础上,增加不同审批结果的颜色等等 |
| | | let canvas = bpmnModeler.get('canvas') |
| | | let todoActivity: any = activityList.find((m: any) => !m.endTime) // 找到待办的任务 |
| | | let endActivity: any = activityList[activityList.length - 1] // 获得最后一个任务 |
| | | let findProcessTask = false //是否已经高亮了进行中的任务 |
| | | //进行中高亮之后的任务 key 集合,用于过滤掉 taskList 进行中后面的任务,避免进行中后面的数据 Hover 还有数据 |
| | | let removeTaskDefinitionKeyList = [] |
| | | // debugger |
| | | bpmnModeler.getDefinitions().rootElements[0].flowElements?.forEach((n: any) => { |
| | | let activity: any = activityList.find((m: any) => m.key === n.id) // 找到对应的活动 |
| | | if (!activity) { |
| | | return |
| | | const canvas = bpmnViewer.value?.get('canvas') |
| | | const svg = canvas?._svg |
| | | svg.appendChild(customDefs.value) |
| | | } |
| | | |
| | | /** 节点选中 */ |
| | | const onSelectElement = (element: any) => { |
| | | // 清空原选中 |
| | | selectActivityType.value = undefined |
| | | dialogTitle.value = undefined |
| | | if (!element || !processInstance.value?.id) { |
| | | return |
| | | } |
| | | |
| | | // UserTask 的情况 |
| | | const activityType = element.type |
| | | selectActivityType.value = activityType |
| | | if (activityType === 'bpmn:UserTask') { |
| | | dialogTitle.value = element.businessObject ? element.businessObject.name : undefined |
| | | selectTasks.value = tasks.value.filter((item: any) => item?.taskDefinitionKey === element.id) |
| | | dialogVisible.value = true |
| | | } else if (activityType === 'bpmn:EndEvent' || activityType === 'bpmn:StartEvent') { |
| | | dialogTitle.value = '审批信息' |
| | | selectTasks.value = [ |
| | | { |
| | | assigneeUser: processInstance.value.startUser, |
| | | createTime: processInstance.value.startTime, |
| | | endTime: processInstance.value.endTime, |
| | | status: processInstance.value.status, |
| | | durationInMillis: processInstance.value.durationInMillis |
| | | } |
| | | ] |
| | | dialogVisible.value = true |
| | | } |
| | | } |
| | | |
| | | /** 初始化 BPMN 视图 */ |
| | | const importXML = async (xml: string) => { |
| | | // 清空流程图 |
| | | clearViewer() |
| | | |
| | | // 初始化流程图 |
| | | if (xml != null && xml !== '') { |
| | | try { |
| | | bpmnViewer.value = new BpmnViewer({ |
| | | additionalModules: [MoveCanvasModule], |
| | | container: processCanvas.value |
| | | }) |
| | | // 增加点击事件 |
| | | bpmnViewer.value.on('element.click', ({ element }) => { |
| | | onSelectElement(element) |
| | | }) |
| | | |
| | | // 初始化 BPMN 视图 |
| | | isLoading.value = true |
| | | await bpmnViewer.value.importXML(xml) |
| | | // 自定义成功的箭头 |
| | | addCustomDefs() |
| | | } catch (e) { |
| | | clearViewer() |
| | | } finally { |
| | | isLoading.value = false |
| | | // 高亮流程 |
| | | setProcessStatus(props.view) |
| | | } |
| | | if (n.$type === 'bpmn:UserTask') { |
| | | // 用户任务 |
| | | // 处理用户任务的高亮 |
| | | const task: any = taskList.value.find((m: any) => m.id === activity.taskId) // 找到活动对应的 taskId |
| | | if (!task) { |
| | | return |
| | | } |
| | | // 进行中的任务已经高亮过了,则不高亮后面的任务了 |
| | | if (findProcessTask) { |
| | | removeTaskDefinitionKeyList.push(n.id) |
| | | return |
| | | } |
| | | // 高亮任务 |
| | | canvas.addMarker(n.id, getResultCss(task.status)) |
| | | //标记是否高亮了进行中任务 |
| | | if (task.status === 1) { |
| | | findProcessTask = true |
| | | } |
| | | // 如果非通过,就不走后面的线条了 |
| | | if (task.status !== 2) { |
| | | return |
| | | } |
| | | // 处理 outgoing 出线 |
| | | const outgoing = getActivityOutgoing(activity) |
| | | outgoing?.forEach((nn: any) => { |
| | | // debugger |
| | | let targetActivity: any = activityList.find((m: any) => m.key === nn.targetRef.id) |
| | | // 如果目标活动存在,则根据该活动是否结束,进行【bpmn:SequenceFlow】连线的高亮设置 |
| | | if (targetActivity) { |
| | | canvas.addMarker(nn.id, targetActivity.endTime ? 'highlight' : 'highlight-todo') |
| | | } else if (nn.targetRef.$type === 'bpmn:ExclusiveGateway') { |
| | | // TODO 芋艿:这个流程,暂时没走到过 |
| | | canvas.addMarker(nn.id, activity.endTime ? 'highlight' : 'highlight-todo') |
| | | canvas.addMarker(nn.targetRef.id, activity.endTime ? 'highlight' : 'highlight-todo') |
| | | } else if (nn.targetRef.$type === 'bpmn:EndEvent') { |
| | | // TODO 芋艿:这个流程,暂时没走到过 |
| | | if (!todoActivity && endActivity.key === n.id) { |
| | | canvas.addMarker(nn.id, 'highlight') |
| | | canvas.addMarker(nn.targetRef.id, 'highlight') |
| | | } |
| | | if (!activity.endTime) { |
| | | canvas.addMarker(nn.id, 'highlight-todo') |
| | | canvas.addMarker(nn.targetRef.id, 'highlight-todo') |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** 高亮流程 */ |
| | | const setProcessStatus = (view: any) => { |
| | | // 设置相关变量 |
| | | if (!view || !view.processInstance) { |
| | | return |
| | | } |
| | | processInstance.value = view.processInstance |
| | | tasks.value = view.tasks |
| | | if (isLoading.value || !bpmnViewer.value) { |
| | | return |
| | | } |
| | | const { |
| | | unfinishedTaskActivityIds, |
| | | finishedTaskActivityIds, |
| | | finishedSequenceFlowActivityIds, |
| | | rejectedTaskActivityIds |
| | | } = view |
| | | const canvas = bpmnViewer.value.get('canvas') |
| | | const elementRegistry = bpmnViewer.value.get('elementRegistry') |
| | | |
| | | // 已完成节点 |
| | | if (Array.isArray(finishedSequenceFlowActivityIds)) { |
| | | finishedSequenceFlowActivityIds.forEach((item: any) => { |
| | | if (item != null) { |
| | | canvas.addMarker(item, 'success') |
| | | const element = elementRegistry.get(item) |
| | | const conditionExpression = element.businessObject.conditionExpression |
| | | if (conditionExpression) { |
| | | canvas.addMarker(item, 'condition-expression') |
| | | } |
| | | }) |
| | | } else if (n.$type === 'bpmn:ExclusiveGateway') { |
| | | // 排它网关 |
| | | // 设置【bpmn:ExclusiveGateway】排它网关的高亮 |
| | | canvas.addMarker(n.id, getActivityHighlightCss(activity)) |
| | | // 查找需要高亮的连线 |
| | | let matchNN: any = undefined |
| | | let matchActivity: any = undefined |
| | | n.outgoing?.forEach((nn: any) => { |
| | | let targetActivity = activityList.find((m: any) => m.key === nn.targetRef.id) |
| | | if (!targetActivity) { |
| | | return |
| | | } |
| | | // 特殊判断 endEvent 类型的原因,ExclusiveGateway 可能后续连有 2 个路径: |
| | | // 1. 一个是 UserTask => EndEvent |
| | | // 2. 一个是 EndEvent |
| | | // 在选择路径 1 时,其实 EndEvent 可能也存在,导致 1 和 2 都高亮,显然是不正确的。 |
| | | // 所以,在 matchActivity 为 EndEvent 时,需要进行覆盖~~ |
| | | if (!matchActivity || matchActivity.type === 'endEvent') { |
| | | matchNN = nn |
| | | matchActivity = targetActivity |
| | | } |
| | | }) |
| | | if (matchNN && matchActivity) { |
| | | canvas.addMarker(matchNN.id, getActivityHighlightCss(matchActivity)) |
| | | } |
| | | } else if (n.$type === 'bpmn:ParallelGateway') { |
| | | // 并行网关 |
| | | // 设置【bpmn:ParallelGateway】并行网关的高亮 |
| | | canvas.addMarker(n.id, getActivityHighlightCss(activity)) |
| | | n.outgoing?.forEach((nn: any) => { |
| | | // 获得连线是否有指向目标。如果有,则进行高亮 |
| | | const targetActivity = activityList.find((m: any) => m.key === nn.targetRef.id) |
| | | if (targetActivity) { |
| | | canvas.addMarker(nn.id, getActivityHighlightCss(targetActivity)) // 高亮【bpmn:SequenceFlow】连线 |
| | | // 高亮【...】目标。其中 ... 可以是 bpm:UserTask、也可以是其它的。当然,如果是 bpm:UserTask 的话,其实不做高亮也没问题,因为上面有逻辑做了这块。 |
| | | canvas.addMarker(nn.targetRef.id, getActivityHighlightCss(targetActivity)) |
| | | } |
| | | }) |
| | | } else if (n.$type === 'bpmn:StartEvent') { |
| | | // 开始节点 |
| | | canvas.addMarker(n.id, 'highlight') |
| | | n.outgoing?.forEach((nn) => { |
| | | // outgoing 例如说【bpmn:SequenceFlow】连线 |
| | | // 获得连线是否有指向目标。如果有,则进行高亮 |
| | | let targetActivity = activityList.find((m: any) => m.key === nn.targetRef.id) |
| | | if (targetActivity) { |
| | | canvas.addMarker(nn.id, 'highlight') // 高亮【bpmn:SequenceFlow】连线 |
| | | canvas.addMarker(n.id, 'highlight') // 高亮【bpmn:StartEvent】开始节点(自己) |
| | | } |
| | | }) |
| | | } else if (n.$type === 'bpmn:EndEvent') { |
| | | // 结束节点 |
| | | if (!processInstance.value || processInstance.value.status === 1) { |
| | | return |
| | | }) |
| | | } |
| | | if (Array.isArray(finishedTaskActivityIds)) { |
| | | finishedTaskActivityIds.forEach((item: any) => canvas.addMarker(item, 'success')) |
| | | } |
| | | |
| | | // 未完成节点 |
| | | if (Array.isArray(unfinishedTaskActivityIds)) { |
| | | unfinishedTaskActivityIds.forEach((item: any) => canvas.addMarker(item, 'primary')) |
| | | } |
| | | |
| | | // 被拒绝节点 |
| | | if (Array.isArray(rejectedTaskActivityIds)) { |
| | | rejectedTaskActivityIds.forEach((item: any) => { |
| | | if (item != null) { |
| | | canvas.addMarker(item, 'danger') |
| | | } |
| | | canvas.addMarker(n.id, getResultCss(processInstance.value.status)) |
| | | } else if (n.$type === 'bpmn:ServiceTask') { |
| | | //服务任务 |
| | | if (activity.startTime > 0 && activity.endTime === 0) { |
| | | //进入执行,标识进行色 |
| | | canvas.addMarker(n.id, getResultCss(1)) |
| | | } |
| | | if (activity.endTime > 0) { |
| | | // 执行完成,节点标识完成色, 所有outgoing标识完成色。 |
| | | canvas.addMarker(n.id, getResultCss(2)) |
| | | const outgoing = getActivityOutgoing(activity) |
| | | outgoing?.forEach((out) => { |
| | | canvas.addMarker(out.id, getResultCss(2)) |
| | | }) |
| | | } |
| | | } else if (n.$type === 'bpmn:SequenceFlow') { |
| | | let targetActivity = activityList.find((m: any) => m.key === n.targetRef.id) |
| | | if (targetActivity) { |
| | | canvas.addMarker(n.id, getActivityHighlightCss(targetActivity)) |
| | | } |
| | | } |
| | | }) |
| | | if (!isEmpty(removeTaskDefinitionKeyList)) { |
| | | taskList.value = taskList.value.filter( |
| | | (item) => !removeTaskDefinitionKeyList.includes(item.taskDefinitionKey) |
| | | }) |
| | | } |
| | | |
| | | // 特殊:处理 end 节点的高亮。因为 end 在拒绝、取消时,被后端计算成了 finishedTaskActivityIds 里 |
| | | if ( |
| | | [BpmProcessInstanceStatus.CANCEL, BpmProcessInstanceStatus.REJECT].includes( |
| | | processInstance.value.status |
| | | ) |
| | | } |
| | | } |
| | | |
| | | const getActivityHighlightCss = (activity) => { |
| | | return activity.endTime ? 'highlight' : 'highlight-todo' |
| | | } |
| | | |
| | | const getResultCss = (status) => { |
| | | if (status === 1) { |
| | | // 审批中 |
| | | return 'highlight-todo' |
| | | } else if (status === 2) { |
| | | // 已通过 |
| | | return 'highlight' |
| | | } else if (status === 3) { |
| | | // 不通过 |
| | | return 'highlight-reject' |
| | | } else if (status === 4) { |
| | | // 已取消 |
| | | return 'highlight-cancel' |
| | | } else if (status === 5) { |
| | | // 退回 |
| | | return 'highlight-return' |
| | | } else if (status === 6) { |
| | | // 委派 |
| | | return 'highlight-todo' |
| | | } else if (status === 7) { |
| | | // 审批通过中 |
| | | return 'highlight-todo' |
| | | } else if (status === 0) { |
| | | // 待审批 |
| | | return 'highlight-todo' |
| | | } |
| | | return '' |
| | | } |
| | | |
| | | const getActivityOutgoing = (activity) => { |
| | | // 如果有 outgoing,则直接使用它 |
| | | if (activity.outgoing && activity.outgoing.length > 0) { |
| | | return activity.outgoing |
| | | } |
| | | // 如果没有,则遍历获得起点为它的【bpmn:SequenceFlow】节点们。原因是:bpmn-js 的 UserTask 拿不到 outgoing |
| | | const flowElements = bpmnModeler.getDefinitions().rootElements[0].flowElements |
| | | const outgoing: any[] = [] |
| | | flowElements.forEach((item: any) => { |
| | | if (item.$type !== 'bpmn:SequenceFlow') { |
| | | return |
| | | } |
| | | if (item.sourceRef.id === activity.key) { |
| | | outgoing.push(item) |
| | | } |
| | | }) |
| | | return outgoing |
| | | } |
| | | const initModelListeners = () => { |
| | | const EventBus = bpmnModeler.get('eventBus') |
| | | // 注册需要的监听事件 |
| | | EventBus.on('element.hover', function (eventObj) { |
| | | let element = eventObj ? eventObj.element : null |
| | | elementHover(element) |
| | | }) |
| | | EventBus.on('element.out', function (eventObj) { |
| | | let element = eventObj ? eventObj.element : null |
| | | elementOut(element) |
| | | }) |
| | | } |
| | | // 流程图的元素被 hover |
| | | const elementHover = (element) => { |
| | | element.value = element |
| | | !elementOverlayIds.value && (elementOverlayIds.value = {}) |
| | | !overlays.value && (overlays.value = bpmnModeler.get('overlays')) |
| | | // 展示信息 |
| | | // console.log(activityLists.value, 'activityLists.value') |
| | | // console.log(element.value, 'element.value') |
| | | const activity = activityLists.value.find((m) => m.key === element.value.id) |
| | | // console.log(activity, 'activityactivityactivityactivity') |
| | | if (!activity) { |
| | | return |
| | | } |
| | | if (!elementOverlayIds.value[element.value.id] && element.value.type !== 'bpmn:Process') { |
| | | let html = `<div class="element-overlays"> |
| | | <p>Elemet id: ${element.value.id}</p> |
| | | <p>Elemet type: ${element.value.type}</p> |
| | | </div>` // 默认值 |
| | | if (element.value.type === 'bpmn:StartEvent' && processInstance.value) { |
| | | html = `<p>发起人:${processInstance.value.startUser.nickname}</p> |
| | | <p>部门:${processInstance.value.startUser.deptName}</p> |
| | | <p>创建时间:${formatDate(processInstance.value.createTime)}` |
| | | } else if (element.value.type === 'bpmn:UserTask') { |
| | | let task = taskList.value.find((m) => m.id === activity.taskId) // 找到活动对应的 taskId |
| | | if (!task) { |
| | | return |
| | | ) { |
| | | const endNodes = elementRegistry.filter((element: any) => element.type === 'bpmn:EndEvent') |
| | | endNodes.forEach((item: any) => { |
| | | canvas.removeMarker(item.id, 'success') |
| | | if (processInstance.value.status === BpmProcessInstanceStatus.CANCEL) { |
| | | canvas.addMarker(item.id, 'cancel') |
| | | } else { |
| | | canvas.addMarker(item.id, 'danger') |
| | | } |
| | | let optionData = getIntDictOptions(DICT_TYPE.BPM_TASK_STATUS) |
| | | let dataResult = '' |
| | | optionData.forEach((element) => { |
| | | if (element.value == task.status) { |
| | | dataResult = element.label |
| | | } |
| | | }) |
| | | html = `<p>审批人:${task.assigneeUser.nickname}</p> |
| | | <p>部门:${task.assigneeUser.deptName}</p> |
| | | <p>结果:${dataResult}</p> |
| | | <p>创建时间:${formatDate(task.createTime)}</p>` |
| | | // html = `<p>审批人:${task.assigneeUser.nickname}</p> |
| | | // <p>部门:${task.assigneeUser.deptName}</p> |
| | | // <p>结果:${getIntDictOptions( |
| | | // DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT, |
| | | // task.status |
| | | // )}</p> |
| | | // <p>创建时间:${formatDate(task.createTime)}</p>` |
| | | if (task.endTime) { |
| | | html += `<p>结束时间:${formatDate(task.endTime)}</p>` |
| | | } |
| | | if (task.reason) { |
| | | html += `<p>审批建议:${task.reason}</p>` |
| | | } |
| | | } else if (element.value.type === 'bpmn:ServiceTask' && processInstance.value) { |
| | | if (activity.startTime > 0) { |
| | | html = `<p>创建时间:${formatDate(activity.startTime)}</p>` |
| | | } |
| | | if (activity.endTime > 0) { |
| | | html += `<p>结束时间:${formatDate(activity.endTime)}</p>` |
| | | } |
| | | console.log(html) |
| | | } else if (element.value.type === 'bpmn:EndEvent' && processInstance.value) { |
| | | let optionData = getIntDictOptions(DICT_TYPE.BPM_TASK_STATUS) |
| | | let dataResult = '' |
| | | optionData.forEach((element) => { |
| | | if (element.value == processInstance.value.status) { |
| | | dataResult = element.label |
| | | } |
| | | }) |
| | | html = `<p>结果:${dataResult}</p>` |
| | | // html = `<p>结果:${getIntDictOptions( |
| | | // DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT, |
| | | // processInstance.value.status |
| | | // )}</p>` |
| | | if (processInstance.value.endTime) { |
| | | html += `<p>结束时间:${formatDate(processInstance.value.endTime)}</p>` |
| | | } |
| | | } |
| | | // console.log(html, 'html111111111111111') |
| | | elementOverlayIds.value[element.value.id] = toRaw(overlays.value)?.add(element.value, { |
| | | position: { left: 0, bottom: 0 }, |
| | | html: `<div class="element-overlays">${html}</div>` |
| | | }) |
| | | } |
| | | } |
| | | |
| | | // 流程图的元素被 out |
| | | const elementOut = (element) => { |
| | | toRaw(overlays.value).remove({ element }) |
| | | elementOverlayIds.value[element.id] = null |
| | | } |
| | | watch( |
| | | () => props.xml, |
| | | (newXml) => { |
| | | importXML(newXml) |
| | | }, |
| | | { immediate: true } |
| | | ) |
| | | |
| | | watch( |
| | | () => props.view, |
| | | (newView) => { |
| | | setProcessStatus(newView) |
| | | }, |
| | | { immediate: true } |
| | | ) |
| | | |
| | | /** mounted:初始化 */ |
| | | onMounted(() => { |
| | | xml.value = props.value |
| | | activityLists.value = props.activityData |
| | | // 初始化 |
| | | initBpmnModeler() |
| | | createNewDiagram(xml.value) |
| | | // 初始模型的监听器 |
| | | initModelListeners() |
| | | importXML(props.xml) |
| | | setProcessStatus(props.view) |
| | | }) |
| | | |
| | | /** unmount:销毁 */ |
| | | onBeforeUnmount(() => { |
| | | // this.$once('hook:beforeDestroy', () => { |
| | | // }) |
| | | if (bpmnModeler) bpmnModeler.destroy() |
| | | emit('destroy', bpmnModeler) |
| | | bpmnModeler = null |
| | | clearViewer() |
| | | }) |
| | | |
| | | watch( |
| | | () => props.value, |
| | | (newValue) => { |
| | | xml.value = newValue |
| | | createNewDiagram(xml.value) |
| | | } |
| | | ) |
| | | watch( |
| | | () => props.activityData, |
| | | (newActivityData) => { |
| | | activityLists.value = newActivityData |
| | | createNewDiagram(xml.value) |
| | | } |
| | | ) |
| | | watch( |
| | | () => props.processInstanceData, |
| | | (newProcessInstanceData) => { |
| | | processInstance.value = newProcessInstanceData |
| | | createNewDiagram(xml.value) |
| | | } |
| | | ) |
| | | watch( |
| | | () => props.taskData, |
| | | (newTaskListData) => { |
| | | taskList.value = newTaskListData |
| | | createNewDiagram(xml.value) |
| | | } |
| | | ) |
| | | </script> |
| | | |
| | | <style lang="scss"> |
| | | /** 处理中 */ |
| | | .highlight-todo.djs-connection > .djs-visual > path { |
| | | stroke: #1890ff !important; |
| | | stroke-dasharray: 4px !important; |
| | | fill-opacity: 0.2 !important; |
| | | } |
| | | |
| | | .highlight-todo.djs-shape .djs-visual > :nth-child(1) { |
| | | fill: #1890ff !important; |
| | | stroke: #1890ff !important; |
| | | stroke-dasharray: 4px !important; |
| | | fill-opacity: 0.2 !important; |
| | | } |
| | | |
| | | :deep(.highlight-todo.djs-connection > .djs-visual > path) { |
| | | stroke: #1890ff !important; |
| | | stroke-dasharray: 4px !important; |
| | | fill-opacity: 0.2 !important; |
| | | marker-end: url('#sequenceflow-end-_E7DFDF-_E7DFDF-803g1kf6zwzmcig1y2ulm5egr'); |
| | | } |
| | | |
| | | :deep(.highlight-todo.djs-shape .djs-visual > :nth-child(1)) { |
| | | fill: #1890ff !important; |
| | | stroke: #1890ff !important; |
| | | stroke-dasharray: 4px !important; |
| | | fill-opacity: 0.2 !important; |
| | | } |
| | | |
| | | /** 通过 */ |
| | | .highlight.djs-shape .djs-visual > :nth-child(1) { |
| | | fill: green !important; |
| | | stroke: green !important; |
| | | fill-opacity: 0.2 !important; |
| | | } |
| | | |
| | | .highlight.djs-shape .djs-visual > :nth-child(2) { |
| | | fill: green !important; |
| | | } |
| | | |
| | | .highlight.djs-shape .djs-visual > path { |
| | | fill: green !important; |
| | | fill-opacity: 0.2 !important; |
| | | stroke: green !important; |
| | | } |
| | | |
| | | .highlight.djs-connection > .djs-visual > path { |
| | | stroke: green !important; |
| | | } |
| | | |
| | | .highlight:not(.djs-connection) .djs-visual > :nth-child(1) { |
| | | fill: green !important; /* color elements as green */ |
| | | } |
| | | |
| | | :deep(.highlight.djs-shape .djs-visual > :nth-child(1)) { |
| | | fill: green !important; |
| | | stroke: green !important; |
| | | fill-opacity: 0.2 !important; |
| | | } |
| | | |
| | | :deep(.highlight.djs-shape .djs-visual > :nth-child(2)) { |
| | | fill: green !important; |
| | | } |
| | | |
| | | :deep(.highlight.djs-shape .djs-visual > path) { |
| | | fill: green !important; |
| | | fill-opacity: 0.2 !important; |
| | | stroke: green !important; |
| | | } |
| | | |
| | | :deep(.highlight.djs-connection > .djs-visual > path) { |
| | | stroke: green !important; |
| | | } |
| | | |
| | | .djs-element.highlight > .djs-visual > path { |
| | | stroke: green !important; |
| | | } |
| | | |
| | | /** 不通过 */ |
| | | .highlight-reject.djs-shape .djs-visual > :nth-child(1) { |
| | | fill: red !important; |
| | | stroke: red !important; |
| | | fill-opacity: 0.2 !important; |
| | | } |
| | | |
| | | .highlight-reject.djs-shape .djs-visual > :nth-child(2) { |
| | | fill: red !important; |
| | | } |
| | | |
| | | .highlight-reject.djs-shape .djs-visual > path { |
| | | fill: red !important; |
| | | fill-opacity: 0.2 !important; |
| | | stroke: red !important; |
| | | } |
| | | |
| | | .highlight-reject.djs-connection > .djs-visual > path { |
| | | stroke: red !important; |
| | | marker-end: url(#sequenceflow-end-white-success) !important; |
| | | } |
| | | |
| | | .highlight-reject:not(.djs-connection) .djs-visual > :nth-child(1) { |
| | | fill: red !important; /* color elements as green */ |
| | | } |
| | | |
| | | :deep(.highlight-reject.djs-shape .djs-visual > :nth-child(1)) { |
| | | fill: red !important; |
| | | stroke: red !important; |
| | | fill-opacity: 0.2 !important; |
| | | } |
| | | |
| | | :deep(.highlight-reject.djs-shape .djs-visual > :nth-child(2)) { |
| | | fill: red !important; |
| | | } |
| | | |
| | | :deep(.highlight-reject.djs-shape .djs-visual > path) { |
| | | fill: red !important; |
| | | fill-opacity: 0.2 !important; |
| | | stroke: red !important; |
| | | } |
| | | |
| | | :deep(.highlight-reject.djs-connection > .djs-visual > path) { |
| | | stroke: red !important; |
| | | } |
| | | |
| | | /** 已取消 */ |
| | | .highlight-cancel.djs-shape .djs-visual > :nth-child(1) { |
| | | fill: grey !important; |
| | | stroke: grey !important; |
| | | fill-opacity: 0.2 !important; |
| | | } |
| | | |
| | | .highlight-cancel.djs-shape .djs-visual > :nth-child(2) { |
| | | fill: grey !important; |
| | | } |
| | | |
| | | .highlight-cancel.djs-shape .djs-visual > path { |
| | | fill: grey !important; |
| | | fill-opacity: 0.2 !important; |
| | | stroke: grey !important; |
| | | } |
| | | |
| | | .highlight-cancel.djs-connection > .djs-visual > path { |
| | | stroke: grey !important; |
| | | } |
| | | |
| | | .highlight-cancel:not(.djs-connection) .djs-visual > :nth-child(1) { |
| | | fill: grey !important; /* color elements as green */ |
| | | } |
| | | |
| | | :deep(.highlight-cancel.djs-shape .djs-visual > :nth-child(1)) { |
| | | fill: grey !important; |
| | | stroke: grey !important; |
| | | fill-opacity: 0.2 !important; |
| | | } |
| | | |
| | | :deep(.highlight-cancel.djs-shape .djs-visual > :nth-child(2)) { |
| | | fill: grey !important; |
| | | } |
| | | |
| | | :deep(.highlight-cancel.djs-shape .djs-visual > path) { |
| | | fill: grey !important; |
| | | fill-opacity: 0.2 !important; |
| | | stroke: grey !important; |
| | | } |
| | | |
| | | :deep(.highlight-cancel.djs-connection > .djs-visual > path) { |
| | | stroke: grey !important; |
| | | } |
| | | |
| | | /** 回退 */ |
| | | .highlight-return.djs-shape .djs-visual > :nth-child(1) { |
| | | fill: #e6a23c !important; |
| | | stroke: #e6a23c !important; |
| | | fill-opacity: 0.2 !important; |
| | | } |
| | | |
| | | .highlight-return.djs-shape .djs-visual > :nth-child(2) { |
| | | fill: #e6a23c !important; |
| | | } |
| | | |
| | | .highlight-return.djs-shape .djs-visual > path { |
| | | fill: #e6a23c !important; |
| | | fill-opacity: 0.2 !important; |
| | | stroke: #e6a23c !important; |
| | | } |
| | | |
| | | .highlight-return.djs-connection > .djs-visual > path { |
| | | stroke: #e6a23c !important; |
| | | } |
| | | |
| | | .highlight-return:not(.djs-connection) .djs-visual > :nth-child(1) { |
| | | fill: #e6a23c !important; /* color elements as green */ |
| | | } |
| | | |
| | | :deep(.highlight-return.djs-shape .djs-visual > :nth-child(1)) { |
| | | fill: #e6a23c !important; |
| | | stroke: #e6a23c !important; |
| | | fill-opacity: 0.2 !important; |
| | | } |
| | | |
| | | :deep(.highlight-return.djs-shape .djs-visual > :nth-child(2)) { |
| | | fill: #e6a23c !important; |
| | | } |
| | | |
| | | :deep(.highlight-return.djs-shape .djs-visual > path) { |
| | | fill: #e6a23c !important; |
| | | fill-opacity: 0.2 !important; |
| | | stroke: #e6a23c !important; |
| | | } |
| | | |
| | | :deep(.highlight-return.djs-connection > .djs-visual > path) { |
| | | stroke: #e6a23c !important; |
| | | } |
| | | |
| | | .element-overlays { |
| | | width: 200px; |
| | | padding: 8px; |
| | | color: #fafafa; |
| | | background: rgb(0 0 0 / 60%); |
| | | border-radius: 4px; |
| | | box-sizing: border-box; |
| | | } |
| | | </style> |
| | |
| | | "name": "variableMappingDelegateExpression", |
| | | "isAttr": true, |
| | | "type": "String" |
| | | }, |
| | | { |
| | | "name": "calledElementType", |
| | | "isAttr": true, |
| | | "type": "String" |
| | | }, |
| | | { |
| | | "name": "processInstanceName", |
| | | "isAttr": true, |
| | | "type": "String" |
| | | }, |
| | | { |
| | | "name": "inheritBusinessKey", |
| | | "isAttr": true, |
| | | "type": "Boolean" |
| | | }, |
| | | { |
| | | "name": "businessKey", |
| | | "isAttr": true, |
| | | "type": "String" |
| | | }, |
| | | { |
| | | "name": "inheritVariables", |
| | | "isAttr": true, |
| | | "type": "Boolean" |
| | | } |
| | | ] |
| | | }, |
| | |
| | | "isAttr": true |
| | | } |
| | | ] |
| | | }, |
| | | { |
| | | "name": "AssignStartUserHandlerType", |
| | | "superClass": ["Element"], |
| | | "meta": { |
| | | "allowedIn": ["bpmn:StartEvent", "bpmn:UserTask"] |
| | | }, |
| | | "properties": [ |
| | | { |
| | | "name": "value", |
| | | "type": "Integer", |
| | | "isBody": true |
| | | } |
| | | ] |
| | | }, |
| | | { |
| | | "name": "RejectHandlerType", |
| | | "superClass": ["Element"], |
| | | "meta": { |
| | | "allowedIn": ["bpmn:UserTask"] |
| | | }, |
| | | "properties": [ |
| | | { |
| | | "name": "value", |
| | | "type": "Integer", |
| | | "isBody": true |
| | | } |
| | | ] |
| | | }, |
| | | { |
| | | "name": "RejectReturnTaskId", |
| | | "superClass": ["Element"], |
| | | "meta": { |
| | | "allowedIn": ["bpmn:UserTask"] |
| | | }, |
| | | "properties": [ |
| | | { |
| | | "name": "value", |
| | | "type": "String", |
| | | "isBody": true |
| | | } |
| | | ] |
| | | }, |
| | | { |
| | | "name": "AssignEmptyHandlerType", |
| | | "superClass": ["Element"], |
| | | "meta": { |
| | | "allowedIn": ["bpmn:UserTask"] |
| | | }, |
| | | "properties": [ |
| | | { |
| | | "name": "value", |
| | | "type": "Integer", |
| | | "isBody": true |
| | | } |
| | | ] |
| | | }, |
| | | { |
| | | "name": "AssignEmptyUserIds", |
| | | "superClass": ["Element"], |
| | | "meta": { |
| | | "allowedIn": ["bpmn:UserTask"] |
| | | }, |
| | | "properties": [ |
| | | { |
| | | "name": "value", |
| | | "type": "String", |
| | | "isBody": true |
| | | } |
| | | ] |
| | | }, |
| | | { |
| | | "name": "ButtonsSetting", |
| | | "superClass": ["Element"], |
| | | "meta": { |
| | | "allowedIn": ["bpmn:UserTask"] |
| | | }, |
| | | "properties": [ |
| | | { |
| | | "name": "flowable:id", |
| | | "type": "Integer", |
| | | "isAttr": true |
| | | }, |
| | | { |
| | | "name": "flowable:enable", |
| | | "type": "Boolean", |
| | | "isAttr": true |
| | | }, |
| | | { |
| | | "name": "flowable:displayName", |
| | | "type": "String", |
| | | "isAttr": true |
| | | } |
| | | ] |
| | | }, |
| | | { |
| | | "name": "FieldsPermission", |
| | | "superClass": ["Element"], |
| | | "meta": { |
| | | "allowedIn": ["bpmn:UserTask"] |
| | | }, |
| | | "properties": [ |
| | | { |
| | | "name": "flowable:field", |
| | | "type": "String", |
| | | "isAttr": true |
| | | }, |
| | | { |
| | | "name": "flowable:title", |
| | | "type": "String", |
| | | "isAttr": true |
| | | }, |
| | | { |
| | | "name": "flowable:permission", |
| | | "type": "String", |
| | | "isAttr": true |
| | | } |
| | | ] |
| | | }, |
| | | { |
| | | "name": "BoundaryEventType", |
| | | "superClass": ["Element"], |
| | | "meta": { |
| | | "allowedIn": ["bpmn:BoundaryEvent"] |
| | | }, |
| | | "properties": [ |
| | | { |
| | | "name": "value", |
| | | "type": "Integer", |
| | | "isBody": true |
| | | } |
| | | ] |
| | | }, |
| | | { |
| | | "name": "TimeoutHandlerType", |
| | | "superClass": ["Element"], |
| | | "meta": { |
| | | "allowedIn": ["bpmn:BoundaryEvent"] |
| | | }, |
| | | "properties": [ |
| | | { |
| | | "name": "value", |
| | | "type": "Integer", |
| | | "isBody": true |
| | | } |
| | | ] |
| | | }, |
| | | { |
| | | "name": "ApproveType", |
| | | "superClass": ["Element"], |
| | | "meta": { |
| | | "allowedIn": ["bpmn:UserTask"] |
| | | }, |
| | | "properties": [ |
| | | { |
| | | "name": "value", |
| | | "type": "Integer", |
| | | "isBody": true |
| | | } |
| | | ] |
| | | }, |
| | | { |
| | | "name": "ApproveMethod", |
| | | "superClass": ["Element"], |
| | | "meta": { |
| | | "allowedIn": ["bpmn:UserTask"] |
| | | }, |
| | | "properties": [ |
| | | { |
| | | "name": "value", |
| | | "type": "Integer", |
| | | "isBody": true |
| | | } |
| | | ] |
| | | }, |
| | | { |
| | | "name": "CandidateStrategy", |
| | | "superClass": ["Element"], |
| | | "meta": { |
| | | "allowedIn": ["bpmn:UserTask"] |
| | | }, |
| | | "properties": [ |
| | | { |
| | | "name": "value", |
| | | "type": "Integer", |
| | | "isBody": true |
| | | } |
| | | ] |
| | | }, |
| | | { |
| | | "name": "CandidateParam", |
| | | "superClass": ["Element"], |
| | | "meta": { |
| | | "allowedIn": ["bpmn:UserTask"] |
| | | }, |
| | | "properties": [ |
| | | { |
| | | "name": "value", |
| | | "type": "String", |
| | | "isBody": true |
| | | } |
| | | ] |
| | | } |
| | | ], |
| | | "emumerations": [] |
| | |
| | | 'bpmn-icon-user-task', |
| | | translate('Create User Task') |
| | | ), |
| | | 'create.call-activity': createAction( |
| | | 'bpmn:CallActivity', |
| | | 'activity', |
| | | 'bpmn-icon-call-activity', |
| | | translate('Create Call Activity') |
| | | ), |
| | | 'create.service-task': createAction( |
| | | 'bpmn:ServiceTask', |
| | | 'activity', |
| | | 'bpmn-icon-service', |
| | | translate('Create Service Task') |
| | | ), |
| | | 'create.data-object': createAction( |
| | | 'bpmn:DataObjectReference', |
| | | 'data-object', |
| | |
| | | 'bpmn-icon-user-task', |
| | | translate('Create User Task') |
| | | ), |
| | | 'create.service-task': createAction( |
| | | 'bpmn:ServiceTask', |
| | | 'activity', |
| | | 'bpmn-icon-service', |
| | | translate('Create Service Task') |
| | | ), |
| | | 'create.data-object': createAction( |
| | | 'bpmn:DataObjectReference', |
| | | 'data-object', |
| | |
| | | 'Create EndEvent': '创建结束事件', |
| | | 'Create Task': '创建任务', |
| | | 'Create User Task': '创建用户任务', |
| | | 'Create Call Activity': '创建调用活动', |
| | | 'Create Service Task': '创建服务任务', |
| | | 'Create Gateway': '创建网关', |
| | | 'Create DataObjectReference': '创建数据对象', |
| | | 'Create DataStoreReference': '创建数据存储', |
| | |
| | | <template> |
| | | <div class="process-panel__container" :style="{ width: `${width}px` }"> |
| | | <el-collapse v-model="activeTab"> |
| | | <el-collapse v-model="activeTab" v-if="isReady"> |
| | | <el-collapse-item name="base"> |
| | | <!-- class="panel-tab__title" --> |
| | | <template #title> |
| | |
| | | <template #title><Icon icon="ep:list" />表单</template> |
| | | <element-form :id="elementId" :type="elementType" /> |
| | | </el-collapse-item> |
| | | <el-collapse-item name="task" v-if="elementType.indexOf('Task') !== -1" key="task"> |
| | | <template #title><Icon icon="ep:checked" />任务(审批人)</template> |
| | | <el-collapse-item name="task" v-if="isTaskCollapseItemShow(elementType)" key="task"> |
| | | <template #title |
| | | ><Icon icon="ep:checked" />{{ getTaskCollapseItemName(elementType) }}</template |
| | | > |
| | | <element-task :id="elementId" :type="elementType" /> |
| | | </el-collapse-item> |
| | | <el-collapse-item |
| | |
| | | v-if="elementType.indexOf('Task') !== -1" |
| | | key="multiInstance" |
| | | > |
| | | <template #title><Icon icon="ep:help-filled" />多实例(会签配置)</template> |
| | | <element-multi-instance :business-object="elementBusinessObject" :type="elementType" /> |
| | | <template #title><Icon icon="ep:help-filled" />多人审批方式</template> |
| | | <element-multi-instance |
| | | :id="elementId" |
| | | :business-object="elementBusinessObject" |
| | | :type="elementType" |
| | | /> |
| | | </el-collapse-item> |
| | | <el-collapse-item name="listeners" key="listeners"> |
| | | <template #title><Icon icon="ep:bell-filled" />执行监听器</template> |
| | |
| | | <template #title><Icon icon="ep:promotion" />其他</template> |
| | | <element-other-config :id="elementId" /> |
| | | </el-collapse-item> |
| | | <el-collapse-item name="customConfig" key="customConfig"> |
| | | <template #title><Icon icon="ep:tools" />自定义配置</template> |
| | | <element-custom-config |
| | | :id="elementId" |
| | | :type="elementType" |
| | | :business-object="elementBusinessObject" |
| | | /> |
| | | </el-collapse-item> |
| | | </el-collapse> |
| | | </div> |
| | | </template> |
| | |
| | | import ElementProperties from './properties/ElementProperties.vue' |
| | | // import ElementForm from './form/ElementForm.vue' |
| | | import UserTaskListeners from './listeners/UserTaskListeners.vue' |
| | | import { getTaskCollapseItemName, isTaskCollapseItemShow } from './task/data' |
| | | |
| | | defineOptions({ name: 'MyPropertiesPanel' }) |
| | | |
| | |
| | | const conditionFormVisible = ref(false) // 流转条件设置 |
| | | const formVisible = ref(false) // 表单配置 |
| | | const bpmnElement = ref() |
| | | const isReady = ref(false) |
| | | |
| | | provide('prefix', props.prefix) |
| | | provide('width', props.width) |
| | | const bpmnInstances = () => (window as any)?.bpmnInstances |
| | | |
| | | // 监听 props.bpmnModeler 然后 initModels |
| | | const unwatchBpmn = watch( |
| | | () => props.bpmnModeler, |
| | | () => { |
| | | // 避免加载时 流程图 并未加载完成 |
| | | if (!props.bpmnModeler) { |
| | | console.log('缺少props.bpmnModeler') |
| | | return |
| | | } |
| | | |
| | | console.log('props.bpmnModeler 有值了!!!') |
| | | const w = window as any |
| | | w.bpmnInstances = { |
| | | // 初始化 bpmnInstances |
| | | const initBpmnInstances = () => { |
| | | if (!props.bpmnModeler) return false |
| | | try { |
| | | const instances = { |
| | | modeler: props.bpmnModeler, |
| | | modeling: props.bpmnModeler.get('modeling'), |
| | | moddle: props.bpmnModeler.get('moddle'), |
| | |
| | | selection: props.bpmnModeler.get('selection') |
| | | } |
| | | |
| | | console.log(bpmnInstances(), 'window.bpmnInstances') |
| | | getActiveElement() |
| | | unwatchBpmn() |
| | | // 检查所有实例是否都存在 |
| | | const allInstancesExist = Object.values(instances).every(instance => instance) |
| | | if (allInstancesExist) { |
| | | const w = window as any |
| | | w.bpmnInstances = instances |
| | | return true |
| | | } |
| | | return false |
| | | } catch (error) { |
| | | console.error('初始化 bpmnInstances 失败:', error) |
| | | return false |
| | | } |
| | | } |
| | | |
| | | const bpmnInstances = () => (window as any)?.bpmnInstances |
| | | |
| | | // 监听 props.bpmnModeler 然后 initModels |
| | | const unwatchBpmn = watch( |
| | | () => props.bpmnModeler, |
| | | async () => { |
| | | // 避免加载时 流程图 并未加载完成 |
| | | if (!props.bpmnModeler) { |
| | | console.log('缺少props.bpmnModeler') |
| | | return |
| | | } |
| | | |
| | | try { |
| | | // 等待 modeler 初始化完成 |
| | | await nextTick() |
| | | if (initBpmnInstances()) { |
| | | isReady.value = true |
| | | await nextTick() |
| | | getActiveElement() |
| | | } else { |
| | | console.error('modeler 实例未完全初始化') |
| | | } |
| | | } catch (error) { |
| | | console.error('初始化失败:', error) |
| | | } |
| | | }, |
| | | { |
| | | immediate: true |
| | |
| | | ) |
| | | |
| | | const getActiveElement = () => { |
| | | if (!isReady.value || !props.bpmnModeler) return |
| | | |
| | | // 初始第一个选中元素 bpmn:Process |
| | | initFormOnChanged(null) |
| | | props.bpmnModeler.on('import.done', (e) => { |
| | |
| | | } |
| | | }) |
| | | } |
| | | |
| | | // 初始化数据 |
| | | const initFormOnChanged = (element) => { |
| | | if (!isReady.value || !bpmnInstances()) return |
| | | |
| | | let activatedElement = element |
| | | if (!activatedElement) { |
| | | activatedElement = |
| | |
| | | bpmnInstances().elementRegistry.find((el) => el.type === 'bpmn:Collaboration') |
| | | } |
| | | if (!activatedElement) return |
| | | console.log(` |
| | | ---------- |
| | | select element changed: |
| | | id: ${activatedElement.id} |
| | | type: ${activatedElement.businessObject.$type} |
| | | ---------- |
| | | `) |
| | | console.log('businessObject: ', activatedElement.businessObject) |
| | | bpmnInstances().bpmnElement = activatedElement |
| | | bpmnElement.value = activatedElement |
| | | elementId.value = activatedElement.id |
| | | elementType.value = activatedElement.type.split(':')[1] || '' |
| | | elementBusinessObject.value = JSON.parse(JSON.stringify(activatedElement.businessObject)) |
| | | conditionFormVisible.value = !!( |
| | | elementType.value === 'SequenceFlow' && |
| | | activatedElement.source && |
| | | activatedElement.source.type.indexOf('StartEvent') === -1 |
| | | ) |
| | | formVisible.value = elementType.value === 'UserTask' || elementType.value === 'StartEvent' |
| | | |
| | | try { |
| | | console.log(` |
| | | ---------- |
| | | select element changed: |
| | | id: ${activatedElement.id} |
| | | type: ${activatedElement.businessObject.$type} |
| | | ---------- |
| | | `) |
| | | console.log('businessObject: ', activatedElement.businessObject) |
| | | bpmnInstances().bpmnElement = activatedElement |
| | | bpmnElement.value = activatedElement |
| | | elementId.value = activatedElement.id |
| | | elementType.value = activatedElement.type.split(':')[1] || '' |
| | | elementBusinessObject.value = JSON.parse(JSON.stringify(activatedElement.businessObject)) |
| | | conditionFormVisible.value = !!( |
| | | elementType.value === 'SequenceFlow' && |
| | | activatedElement.source && |
| | | activatedElement.source.type.indexOf('StartEvent') === -1 |
| | | ) |
| | | formVisible.value = elementType.value === 'UserTask' || elementType.value === 'StartEvent' |
| | | } catch (error) { |
| | | console.error('初始化表单数据失败:', error) |
| | | } |
| | | } |
| | | |
| | | onBeforeUnmount(() => { |
| | | const w = window as any |
| | | w.bpmnInstances = null |
| | | console.log(props, 'props1') |
| | | console.log(props.bpmnModeler, 'props.bpmnModeler1') |
| | | isReady.value = false |
| | | }) |
| | | |
| | | watch( |
对比新文件 |
| | |
| | | <template> |
| | | <div class="panel-tab__content"> |
| | | <component :is="customConfigComponent" v-bind="$props" /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | import { CustomConfigMap } from './data' |
| | | |
| | | defineOptions({ name: 'ElementCustomConfig' }) |
| | | |
| | | const props = defineProps({ |
| | | id: String, |
| | | type: String, |
| | | businessObject: { |
| | | type: Object, |
| | | default: () => {} |
| | | } |
| | | }) |
| | | |
| | | const bpmnInstances = () => (window as any)?.bpmnInstances |
| | | const customConfigComponent = ref<any>(null) |
| | | |
| | | watch( |
| | | () => props.businessObject, |
| | | () => { |
| | | if (props.type && props.businessObject) { |
| | | let val = props.type |
| | | if (props.businessObject.eventDefinitions) { |
| | | val += props.businessObject.eventDefinitions[0]?.$type.split(':')[1] || '' |
| | | } |
| | | customConfigComponent.value = CustomConfigMap[val]?.componet |
| | | } |
| | | }, |
| | | { immediate: true } |
| | | ) |
| | | </script> |
| | | |
| | | <style lang="scss" scoped></style> |
对比新文件 |
| | |
| | | <template> |
| | | <div> |
| | | <el-divider content-position="left">审批人超时未处理时</el-divider> |
| | | <el-form-item label="启用开关" prop="timeoutHandlerEnable"> |
| | | <el-switch |
| | | v-model="timeoutHandlerEnable" |
| | | active-text="开启" |
| | | inactive-text="关闭" |
| | | @change="timeoutHandlerChange" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="执行动作" prop="timeoutHandlerType" v-if="timeoutHandlerEnable"> |
| | | <el-radio-group v-model="timeoutHandlerType.value" @change="onTimeoutHandlerTypeChanged"> |
| | | <el-radio-button |
| | | v-for="item in TIMEOUT_HANDLER_TYPES" |
| | | :key="item.value" |
| | | :value="item.value" |
| | | :label="item.label" |
| | | /> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="超时时间设置" v-if="timeoutHandlerEnable"> |
| | | <span class="mr-2">当超过</span> |
| | | <el-form-item prop="timeDuration"> |
| | | <el-input-number |
| | | class="mr-2" |
| | | :style="{ width: '100px' }" |
| | | v-model="timeDuration" |
| | | :min="1" |
| | | controls-position="right" |
| | | @change="() => updateTimeModdle()" |
| | | /> |
| | | </el-form-item> |
| | | <el-select |
| | | v-model="timeUnit" |
| | | class="mr-2" |
| | | :style="{ width: '100px' }" |
| | | @change="onTimeUnitChange" |
| | | > |
| | | <el-option |
| | | v-for="item in TIME_UNIT_TYPES" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | 未处理 |
| | | </el-form-item> |
| | | <el-form-item |
| | | label="最大提醒次数" |
| | | prop="maxRemindCount" |
| | | v-if="timeoutHandlerEnable && timeoutHandlerType.value === 1" |
| | | > |
| | | <el-input-number |
| | | v-model="maxRemindCount" |
| | | :min="1" |
| | | :max="10" |
| | | @change="() => updateTimeModdle()" |
| | | /> |
| | | </el-form-item> |
| | | </div> |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | import { |
| | | TimeUnitType, |
| | | TIME_UNIT_TYPES, |
| | | TIMEOUT_HANDLER_TYPES, |
| | | } from '@/components/SimpleProcessDesignerV2/src/consts' |
| | | import { convertTimeUnit } from '@/components/SimpleProcessDesignerV2/src/utils' |
| | | |
| | | defineOptions({ name: 'ElementCustomConfig4BoundaryEventTimer' }) |
| | | const props = defineProps({ |
| | | id: String, |
| | | type: String |
| | | }) |
| | | const prefix = inject('prefix') |
| | | |
| | | const bpmnElement = ref() |
| | | const bpmnInstances = () => (window as any)?.bpmnInstances |
| | | |
| | | const timeoutHandlerEnable = ref(false) |
| | | const boundaryEventType = ref() |
| | | const timeoutHandlerType = ref({ |
| | | value: undefined |
| | | }) |
| | | const timeModdle = ref() |
| | | const timeDuration = ref(6) |
| | | const timeUnit = ref(TimeUnitType.HOUR) |
| | | const maxRemindCount = ref(1) |
| | | |
| | | const elExtensionElements = ref() |
| | | const otherExtensions = ref() |
| | | const configExtensions = ref([]) |
| | | const eventDefinition = ref() |
| | | |
| | | const resetElement = () => { |
| | | bpmnElement.value = bpmnInstances().bpmnElement |
| | | eventDefinition.value = bpmnElement.value.businessObject.eventDefinitions[0] |
| | | |
| | | // 获取元素扩展属性 或者 创建扩展属性 |
| | | elExtensionElements.value = |
| | | bpmnElement.value.businessObject?.extensionElements ?? |
| | | bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] }) |
| | | |
| | | // 是否开启自定义用户任务超时处理 |
| | | boundaryEventType.value = elExtensionElements.value.values?.filter( |
| | | (ex) => ex.$type === `${prefix}:BoundaryEventType` |
| | | )?.[0] |
| | | if (boundaryEventType.value && boundaryEventType.value.value === 1) { |
| | | timeoutHandlerEnable.value = true |
| | | configExtensions.value.push(boundaryEventType.value) |
| | | } |
| | | |
| | | // 执行动作 |
| | | timeoutHandlerType.value = elExtensionElements.value.values?.filter( |
| | | (ex) => ex.$type === `${prefix}:TimeoutHandlerType` |
| | | )?.[0] |
| | | if (timeoutHandlerType.value) { |
| | | configExtensions.value.push(timeoutHandlerType.value) |
| | | if (eventDefinition.value.timeCycle) { |
| | | const timeStr = eventDefinition.value.timeCycle.body |
| | | const maxRemindCountStr = timeStr.split('/')[0] |
| | | const timeDurationStr = timeStr.split('/')[1] |
| | | console.log(maxRemindCountStr) |
| | | maxRemindCount.value = parseInt(maxRemindCountStr.slice(1)) |
| | | timeDuration.value = parseInt(timeDurationStr.slice(2, timeDurationStr.length - 1)) |
| | | timeUnit.value = convertTimeUnit(timeDurationStr.slice(timeDurationStr.length - 1)) |
| | | timeModdle.value = eventDefinition.value.timeCycle |
| | | } |
| | | if (eventDefinition.value.timeDuration) { |
| | | const timeDurationStr = eventDefinition.value.timeDuration.body |
| | | timeDuration.value = parseInt(timeDurationStr.slice(2, timeDurationStr.length - 1)) |
| | | timeUnit.value = convertTimeUnit(timeDurationStr.slice(timeDurationStr.length - 1)) |
| | | timeModdle.value = eventDefinition.value.timeDuration |
| | | } |
| | | } |
| | | |
| | | // 保留剩余扩展元素,便于后面更新该元素对应属性 |
| | | otherExtensions.value = |
| | | elExtensionElements.value.values?.filter( |
| | | (ex) => |
| | | ex.$type !== `${prefix}:BoundaryEventType` && ex.$type !== `${prefix}:TimeoutHandlerType` |
| | | ) ?? [] |
| | | } |
| | | |
| | | const timeoutHandlerChange = (val) => { |
| | | timeoutHandlerEnable.value = val |
| | | if (val) { |
| | | // 启用自定义用户任务超时处理 |
| | | // 边界事件类型 --- 超时 |
| | | boundaryEventType.value = bpmnInstances().moddle.create(`${prefix}:BoundaryEventType`, { |
| | | value: 1 |
| | | }) |
| | | configExtensions.value.push(boundaryEventType.value) |
| | | // 超时处理类型 |
| | | timeoutHandlerType.value = bpmnInstances().moddle.create(`${prefix}:TimeoutHandlerType`, { |
| | | value: 1 |
| | | }) |
| | | configExtensions.value.push(timeoutHandlerType.value) |
| | | // 超时时间表达式 |
| | | timeDuration.value = 6 |
| | | timeUnit.value = 2 |
| | | maxRemindCount.value = 1 |
| | | timeModdle.value = bpmnInstances().moddle.create(`bpmn:Expression`, { |
| | | body: 'PT6H' |
| | | }) |
| | | eventDefinition.value.timeDuration = timeModdle.value |
| | | } else { |
| | | // 关闭自定义用户任务超时处理 |
| | | configExtensions.value = [] |
| | | delete eventDefinition.value.timeDuration |
| | | delete eventDefinition.value.timeCycle |
| | | } |
| | | updateElementExtensions() |
| | | } |
| | | |
| | | const onTimeoutHandlerTypeChanged = () => { |
| | | maxRemindCount.value = 1 |
| | | updateElementExtensions() |
| | | updateTimeModdle() |
| | | } |
| | | |
| | | const onTimeUnitChange = () => { |
| | | // 分钟,默认是 60 分钟 |
| | | if (timeUnit.value === TimeUnitType.MINUTE) { |
| | | timeDuration.value = 60 |
| | | } |
| | | // 小时,默认是 6 个小时 |
| | | if (timeUnit.value === TimeUnitType.HOUR) { |
| | | timeDuration.value = 6 |
| | | } |
| | | // 天, 默认 1天 |
| | | if (timeUnit.value === TimeUnitType.DAY) { |
| | | timeDuration.value = 1 |
| | | } |
| | | updateTimeModdle() |
| | | } |
| | | |
| | | const updateTimeModdle = () => { |
| | | if (maxRemindCount.value > 1) { |
| | | timeModdle.value.body = 'R' + maxRemindCount.value + '/' + isoTimeDuration() |
| | | if (!eventDefinition.value.timeCycle) { |
| | | delete eventDefinition.value.timeDuration |
| | | eventDefinition.value.timeCycle = timeModdle.value |
| | | } |
| | | } else { |
| | | timeModdle.value.body = isoTimeDuration() |
| | | if (!eventDefinition.value.timeDuration) { |
| | | delete eventDefinition.value.timeCycle |
| | | eventDefinition.value.timeDuration = timeModdle.value |
| | | } |
| | | } |
| | | } |
| | | |
| | | const isoTimeDuration = () => { |
| | | let strTimeDuration = 'PT' |
| | | if (timeUnit.value === TimeUnitType.MINUTE) { |
| | | strTimeDuration += timeDuration.value + 'M' |
| | | } |
| | | if (timeUnit.value === TimeUnitType.HOUR) { |
| | | strTimeDuration += timeDuration.value + 'H' |
| | | } |
| | | if (timeUnit.value === TimeUnitType.DAY) { |
| | | strTimeDuration += timeDuration.value + 'D' |
| | | } |
| | | return strTimeDuration |
| | | } |
| | | |
| | | const updateElementExtensions = () => { |
| | | const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', { |
| | | values: [...otherExtensions.value, ...configExtensions.value] |
| | | }) |
| | | bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { |
| | | extensionElements: extensions |
| | | }) |
| | | } |
| | | |
| | | watch( |
| | | () => props.id, |
| | | (val) => { |
| | | val && |
| | | val.length && |
| | | nextTick(() => { |
| | | resetElement() |
| | | }) |
| | | }, |
| | | { immediate: true } |
| | | ) |
| | | </script> |
| | | |
| | | <style lang="scss" scoped></style> |
对比新文件 |
| | |
| | | <!-- UserTask 自定义配置: |
| | | 1. 审批人与提交人为同一人时 |
| | | 2. 审批人拒绝时 |
| | | 3. 审批人为空时 |
| | | 4. 操作按钮 |
| | | 5. 字段权限 |
| | | 6. 审批类型 |
| | | --> |
| | | <template> |
| | | <div> |
| | | <el-divider content-position="left">审批类型</el-divider> |
| | | <el-form-item prop="approveType"> |
| | | <el-radio-group v-model="approveType.value"> |
| | | <el-radio |
| | | v-for="(item, index) in APPROVE_TYPE" |
| | | :key="index" |
| | | :value="item.value" |
| | | :label="item.value" |
| | | > |
| | | {{ item.label }} |
| | | </el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | |
| | | <el-divider content-position="left">审批人拒绝时</el-divider> |
| | | <el-form-item prop="rejectHandlerType"> |
| | | <el-radio-group |
| | | v-model="rejectHandlerType" |
| | | :disabled="returnTaskList.length === 0" |
| | | @change="updateRejectHandlerType" |
| | | > |
| | | <div class="flex-col"> |
| | | <div v-for="(item, index) in REJECT_HANDLER_TYPES" :key="index"> |
| | | <el-radio :key="item.value" :value="item.value" :label="item.label" /> |
| | | </div> |
| | | </div> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="rejectHandlerType == RejectHandlerType.RETURN_USER_TASK" |
| | | label="驳回节点" |
| | | prop="returnNodeId" |
| | | > |
| | | <el-select v-model="returnNodeId" clearable style="width: 100%" @change="updateReturnNodeId"> |
| | | <el-option |
| | | v-for="item in returnTaskList" |
| | | :key="item.id" |
| | | :label="item.name" |
| | | :value="item.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-divider content-position="left">审批人为空时</el-divider> |
| | | <el-form-item prop="assignEmptyHandlerType"> |
| | | <el-radio-group v-model="assignEmptyHandlerType" @change="updateAssignEmptyHandlerType"> |
| | | <div class="flex-col"> |
| | | <div v-for="(item, index) in ASSIGN_EMPTY_HANDLER_TYPES" :key="index"> |
| | | <el-radio :key="item.value" :value="item.value" :label="item.label" /> |
| | | </div> |
| | | </div> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="assignEmptyHandlerType == AssignEmptyHandlerType.ASSIGN_USER" |
| | | label="指定用户" |
| | | prop="assignEmptyHandlerUserIds" |
| | | span="24" |
| | | > |
| | | <el-select |
| | | v-model="assignEmptyUserIds" |
| | | clearable |
| | | multiple |
| | | style="width: 100%" |
| | | @change="updateAssignEmptyUserIds" |
| | | > |
| | | <el-option |
| | | v-for="item in userOptions" |
| | | :key="item.id" |
| | | :label="item.nickname" |
| | | :value="item.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-divider content-position="left">审批人与提交人为同一人时</el-divider> |
| | | <el-radio-group v-model="assignStartUserHandlerType" @change="updateAssignStartUserHandlerType"> |
| | | <div class="flex-col"> |
| | | <div v-for="(item, index) in ASSIGN_START_USER_HANDLER_TYPES" :key="index"> |
| | | <el-radio :key="item.value" :value="item.value" :label="item.label" /> |
| | | </div> |
| | | </div> |
| | | </el-radio-group> |
| | | |
| | | <el-divider content-position="left">操作按钮</el-divider> |
| | | <div class="button-setting-pane"> |
| | | <div class="button-setting-title"> |
| | | <div class="button-title-label">操作按钮</div> |
| | | <div class="pl-4 button-title-label">显示名称</div> |
| | | <div class="button-title-label">启用</div> |
| | | </div> |
| | | <div class="button-setting-item" v-for="(item, index) in buttonsSettingEl" :key="index"> |
| | | <div class="button-setting-item-label"> {{ OPERATION_BUTTON_NAME.get(item.id) }} </div> |
| | | <div class="button-setting-item-label"> |
| | | <input |
| | | type="text" |
| | | class="editable-title-input" |
| | | @blur="btnDisplayNameBlurEvent(index)" |
| | | v-mountedFocus |
| | | v-model="item.displayName" |
| | | :placeholder="item.displayName" |
| | | v-if="btnDisplayNameEdit[index]" |
| | | /> |
| | | <el-button v-else text @click="changeBtnDisplayName(index)" |
| | | >{{ item.displayName }} <Icon icon="ep:edit" |
| | | /></el-button> |
| | | </div> |
| | | <div class="button-setting-item-label"> |
| | | <el-switch v-model="item.enable" /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <el-divider content-position="left">字段权限</el-divider> |
| | | <div class="field-setting-pane" v-if="formType === 10"> |
| | | <div class="field-permit-title"> |
| | | <div class="setting-title-label first-title"> 字段名称 </div> |
| | | <div class="other-titles"> |
| | | <span class="setting-title-label">只读</span> |
| | | <span class="setting-title-label">可编辑</span> |
| | | <span class="setting-title-label">隐藏</span> |
| | | </div> |
| | | </div> |
| | | <div class="field-setting-item" v-for="(item, index) in fieldsPermissionEl" :key="index"> |
| | | <div class="field-setting-item-label"> {{ item.title }} </div> |
| | | <el-radio-group class="field-setting-item-group" v-model="item.permission"> |
| | | <div class="item-radio-wrap"> |
| | | <el-radio |
| | | :value="FieldPermissionType.READ" |
| | | size="large" |
| | | :label="FieldPermissionType.READ" |
| | | ><span></span |
| | | ></el-radio> |
| | | </div> |
| | | <div class="item-radio-wrap"> |
| | | <el-radio |
| | | :value="FieldPermissionType.WRITE" |
| | | size="large" |
| | | :label="FieldPermissionType.WRITE" |
| | | ><span></span |
| | | ></el-radio> |
| | | </div> |
| | | <div class="item-radio-wrap"> |
| | | <el-radio |
| | | :value="FieldPermissionType.NONE" |
| | | size="large" |
| | | :label="FieldPermissionType.NONE" |
| | | ><span></span |
| | | ></el-radio> |
| | | </div> |
| | | </el-radio-group> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | import { |
| | | ASSIGN_START_USER_HANDLER_TYPES, |
| | | RejectHandlerType, |
| | | REJECT_HANDLER_TYPES, |
| | | ASSIGN_EMPTY_HANDLER_TYPES, |
| | | AssignEmptyHandlerType, |
| | | OPERATION_BUTTON_NAME, |
| | | DEFAULT_BUTTON_SETTING, |
| | | FieldPermissionType, |
| | | APPROVE_TYPE, |
| | | ApproveType, |
| | | ButtonSetting |
| | | } from '@/components/SimpleProcessDesignerV2/src/consts' |
| | | import * as UserApi from '@/api/system/user' |
| | | import { useFormFieldsPermission } from '@/components/SimpleProcessDesignerV2/src/node' |
| | | |
| | | defineOptions({ name: 'ElementCustomConfig4UserTask' }) |
| | | const props = defineProps({ |
| | | id: String, |
| | | type: String |
| | | }) |
| | | const prefix = inject('prefix') |
| | | |
| | | // 审批人与提交人为同一人时 |
| | | const assignStartUserHandlerTypeEl = ref() |
| | | const assignStartUserHandlerType = ref() |
| | | |
| | | // 审批人拒绝时 |
| | | const rejectHandlerTypeEl = ref() |
| | | const rejectHandlerType = ref() |
| | | const returnNodeIdEl = ref() |
| | | const returnNodeId = ref() |
| | | const returnTaskList = ref([]) |
| | | |
| | | // 审批人为空时 |
| | | const assignEmptyHandlerTypeEl = ref() |
| | | const assignEmptyHandlerType = ref() |
| | | const assignEmptyUserIdsEl = ref() |
| | | const assignEmptyUserIds = ref() |
| | | |
| | | // 操作按钮 |
| | | const buttonsSettingEl = ref() |
| | | const { btnDisplayNameEdit, changeBtnDisplayName, btnDisplayNameBlurEvent } = useButtonsSetting() |
| | | |
| | | // 字段权限 |
| | | const fieldsPermissionEl = ref([]) |
| | | const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission( |
| | | FieldPermissionType.READ |
| | | ) |
| | | |
| | | // 审批类型 |
| | | const approveType = ref({ value: ApproveType.USER }) |
| | | |
| | | const elExtensionElements = ref() |
| | | const otherExtensions = ref() |
| | | const bpmnElement = ref() |
| | | const bpmnInstances = () => (window as any)?.bpmnInstances |
| | | |
| | | const resetCustomConfigList = () => { |
| | | bpmnElement.value = bpmnInstances().bpmnElement |
| | | |
| | | // 获取可回退的列表 |
| | | returnTaskList.value = findAllPredecessorsExcludingStart( |
| | | bpmnElement.value.id, |
| | | bpmnInstances().modeler |
| | | ) |
| | | |
| | | // 获取元素扩展属性 或者 创建扩展属性 |
| | | elExtensionElements.value = |
| | | bpmnElement.value.businessObject?.extensionElements ?? |
| | | bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] }) |
| | | |
| | | // 审批类型 |
| | | approveType.value = |
| | | elExtensionElements.value.values?.filter((ex) => ex.$type === `${prefix}:ApproveType`)?.[0] || |
| | | bpmnInstances().moddle.create(`${prefix}:ApproveType`, { value: ApproveType.USER }) |
| | | |
| | | // 审批人与提交人为同一人时 |
| | | assignStartUserHandlerTypeEl.value = |
| | | elExtensionElements.value.values?.filter( |
| | | (ex) => ex.$type === `${prefix}:AssignStartUserHandlerType` |
| | | )?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignStartUserHandlerType`, { value: 1 }) |
| | | assignStartUserHandlerType.value = assignStartUserHandlerTypeEl.value.value |
| | | |
| | | // 审批人拒绝时 |
| | | rejectHandlerTypeEl.value = |
| | | elExtensionElements.value.values?.filter( |
| | | (ex) => ex.$type === `${prefix}:RejectHandlerType` |
| | | )?.[0] || bpmnInstances().moddle.create(`${prefix}:RejectHandlerType`, { value: 1 }) |
| | | rejectHandlerType.value = rejectHandlerTypeEl.value.value |
| | | returnNodeIdEl.value = |
| | | elExtensionElements.value.values?.filter( |
| | | (ex) => ex.$type === `${prefix}:RejectReturnTaskId` |
| | | )?.[0] || bpmnInstances().moddle.create(`${prefix}:RejectReturnTaskId`, { value: '' }) |
| | | returnNodeId.value = returnNodeIdEl.value.value |
| | | |
| | | // 审批人为空时 |
| | | assignEmptyHandlerTypeEl.value = |
| | | elExtensionElements.value.values?.filter( |
| | | (ex) => ex.$type === `${prefix}:AssignEmptyHandlerType` |
| | | )?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignEmptyHandlerType`, { value: 1 }) |
| | | assignEmptyHandlerType.value = assignEmptyHandlerTypeEl.value.value |
| | | assignEmptyUserIdsEl.value = |
| | | elExtensionElements.value.values?.filter( |
| | | (ex) => ex.$type === `${prefix}:AssignEmptyUserIds` |
| | | )?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignEmptyUserIds`, { value: '' }) |
| | | assignEmptyUserIds.value = assignEmptyUserIdsEl.value.value?.split(',').map((item) => { |
| | | // 如果数字超出了最大安全整数范围,则将其作为字符串处理 |
| | | let num = Number(item) |
| | | return num > Number.MAX_SAFE_INTEGER || num < -Number.MAX_SAFE_INTEGER ? item : num |
| | | }) |
| | | |
| | | // 操作按钮 |
| | | buttonsSettingEl.value = elExtensionElements.value.values?.filter( |
| | | (ex) => ex.$type === `${prefix}:ButtonsSetting` |
| | | ) |
| | | if (buttonsSettingEl.value.length === 0) { |
| | | DEFAULT_BUTTON_SETTING.forEach((item) => { |
| | | buttonsSettingEl.value.push( |
| | | bpmnInstances().moddle.create(`${prefix}:ButtonsSetting`, { |
| | | 'flowable:id': item.id, |
| | | 'flowable:displayName': item.displayName, |
| | | 'flowable:enable': item.enable |
| | | }) |
| | | ) |
| | | }) |
| | | } |
| | | |
| | | // 字段权限 |
| | | if (formType.value === 10) { |
| | | const fieldsPermissionList = elExtensionElements.value.values?.filter( |
| | | (ex) => ex.$type === `${prefix}:FieldsPermission` |
| | | ) |
| | | fieldsPermissionEl.value = [] |
| | | getNodeConfigFormFields() |
| | | // 由于默认添加了发起人元素,这里需要删掉 |
| | | fieldsPermissionConfig.value = fieldsPermissionConfig.value.slice(1) |
| | | fieldsPermissionConfig.value.forEach((element) => { |
| | | element.permission = |
| | | fieldsPermissionList?.find((obj) => obj.field === element.field)?.permission ?? '1' |
| | | fieldsPermissionEl.value.push( |
| | | bpmnInstances().moddle.create(`${prefix}:FieldsPermission`, element) |
| | | ) |
| | | }) |
| | | } |
| | | |
| | | // 保留剩余扩展元素,便于后面更新该元素对应属性 |
| | | otherExtensions.value = |
| | | elExtensionElements.value.values?.filter( |
| | | (ex) => |
| | | ex.$type !== `${prefix}:AssignStartUserHandlerType` && |
| | | ex.$type !== `${prefix}:RejectHandlerType` && |
| | | ex.$type !== `${prefix}:RejectReturnTaskId` && |
| | | ex.$type !== `${prefix}:AssignEmptyHandlerType` && |
| | | ex.$type !== `${prefix}:AssignEmptyUserIds` && |
| | | ex.$type !== `${prefix}:ButtonsSetting` && |
| | | ex.$type !== `${prefix}:FieldsPermission` && |
| | | ex.$type !== `${prefix}:ApproveType` |
| | | ) ?? [] |
| | | |
| | | // 更新元素扩展属性,避免后续报错 |
| | | updateElementExtensions() |
| | | } |
| | | |
| | | const updateAssignStartUserHandlerType = () => { |
| | | assignStartUserHandlerTypeEl.value.value = assignStartUserHandlerType.value |
| | | |
| | | updateElementExtensions() |
| | | } |
| | | |
| | | const updateRejectHandlerType = () => { |
| | | rejectHandlerTypeEl.value.value = rejectHandlerType.value |
| | | |
| | | returnNodeId.value = returnTaskList.value[0].id |
| | | returnNodeIdEl.value.value = returnNodeId.value |
| | | |
| | | updateElementExtensions() |
| | | } |
| | | |
| | | const updateReturnNodeId = () => { |
| | | returnNodeIdEl.value.value = returnNodeId.value |
| | | |
| | | updateElementExtensions() |
| | | } |
| | | |
| | | const updateAssignEmptyHandlerType = () => { |
| | | assignEmptyHandlerTypeEl.value.value = assignEmptyHandlerType.value |
| | | |
| | | updateElementExtensions() |
| | | } |
| | | |
| | | const updateAssignEmptyUserIds = () => { |
| | | assignEmptyUserIdsEl.value.value = assignEmptyUserIds.value.toString() |
| | | |
| | | updateElementExtensions() |
| | | } |
| | | |
| | | const updateElementExtensions = () => { |
| | | const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', { |
| | | values: [ |
| | | ...otherExtensions.value, |
| | | assignStartUserHandlerTypeEl.value, |
| | | rejectHandlerTypeEl.value, |
| | | returnNodeIdEl.value, |
| | | assignEmptyHandlerTypeEl.value, |
| | | assignEmptyUserIdsEl.value, |
| | | approveType.value, |
| | | ...buttonsSettingEl.value, |
| | | ...fieldsPermissionEl.value |
| | | ] |
| | | }) |
| | | bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { |
| | | extensionElements: extensions |
| | | }) |
| | | } |
| | | |
| | | watch( |
| | | () => props.id, |
| | | (val) => { |
| | | val && |
| | | val.length && |
| | | nextTick(() => { |
| | | resetCustomConfigList() |
| | | }) |
| | | }, |
| | | { immediate: true } |
| | | ) |
| | | |
| | | function findAllPredecessorsExcludingStart(elementId, modeler) { |
| | | const elementRegistry = modeler.get('elementRegistry') |
| | | const allConnections = elementRegistry.filter((element) => element.type === 'bpmn:SequenceFlow') |
| | | const predecessors = new Set() // 使用 Set 来避免重复节点 |
| | | const visited = new Set() // 用于记录已访问的节点 |
| | | |
| | | // 检查是否是开始事件节点 |
| | | function isStartEvent(element) { |
| | | return element.type === 'bpmn:StartEvent' |
| | | } |
| | | |
| | | function findPredecessorsRecursively(element) { |
| | | // 如果该节点已经访问过,直接返回,避免循环 |
| | | if (visited.has(element)) { |
| | | return |
| | | } |
| | | |
| | | // 标记当前节点为已访问 |
| | | visited.add(element) |
| | | |
| | | // 获取与当前节点相连的所有连接 |
| | | const incomingConnections = allConnections.filter((connection) => connection.target === element) |
| | | |
| | | incomingConnections.forEach((connection) => { |
| | | const source = connection.source // 获取前置节点 |
| | | |
| | | // 只添加不是开始事件的前置节点 |
| | | if (!isStartEvent(source)) { |
| | | predecessors.add(source.businessObject) |
| | | // 递归查找前置节点 |
| | | findPredecessorsRecursively(source) |
| | | } |
| | | }) |
| | | } |
| | | |
| | | const targetElement = elementRegistry.get(elementId) |
| | | if (targetElement) { |
| | | findPredecessorsRecursively(targetElement) |
| | | } |
| | | |
| | | return Array.from(predecessors) // 返回前置节点数组 |
| | | } |
| | | |
| | | function useButtonsSetting() { |
| | | const buttonsSetting = ref<ButtonSetting[]>() |
| | | // 操作按钮显示名称可编辑 |
| | | const btnDisplayNameEdit = ref<boolean[]>([]) |
| | | const changeBtnDisplayName = (index: number) => { |
| | | btnDisplayNameEdit.value[index] = true |
| | | } |
| | | const btnDisplayNameBlurEvent = (index: number) => { |
| | | btnDisplayNameEdit.value[index] = false |
| | | const buttonItem = buttonsSetting.value![index] |
| | | buttonItem.displayName = buttonItem.displayName || OPERATION_BUTTON_NAME.get(buttonItem.id)! |
| | | } |
| | | return { |
| | | buttonsSetting, |
| | | btnDisplayNameEdit, |
| | | changeBtnDisplayName, |
| | | btnDisplayNameBlurEvent |
| | | } |
| | | } |
| | | |
| | | const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表 |
| | | onMounted(async () => { |
| | | // 获得用户列表 |
| | | userOptions.value = await UserApi.getSimpleUserList() |
| | | }) |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .button-setting-pane { |
| | | display: flex; |
| | | flex-direction: column; |
| | | font-size: 14px; |
| | | margin-top: 8px; |
| | | |
| | | .button-setting-desc { |
| | | padding-right: 8px; |
| | | margin-bottom: 16px; |
| | | font-size: 16px; |
| | | font-weight: 700; |
| | | } |
| | | |
| | | .button-setting-title { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | height: 45px; |
| | | padding-left: 12px; |
| | | background-color: #f8fafc0a; |
| | | border: 1px solid #1f38581a; |
| | | |
| | | & > :first-child { |
| | | width: 100px !important; |
| | | text-align: left !important; |
| | | } |
| | | |
| | | & > :last-child { |
| | | text-align: center !important; |
| | | } |
| | | |
| | | .button-title-label { |
| | | width: 150px; |
| | | font-size: 13px; |
| | | font-weight: 700; |
| | | color: #000; |
| | | text-align: left; |
| | | } |
| | | } |
| | | |
| | | .button-setting-item { |
| | | align-items: center; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | height: 38px; |
| | | padding-left: 12px; |
| | | border: 1px solid #1f38581a; |
| | | border-top: 0; |
| | | |
| | | & > :first-child { |
| | | width: 100px !important; |
| | | } |
| | | |
| | | & > :last-child { |
| | | text-align: center !important; |
| | | } |
| | | |
| | | .button-setting-item-label { |
| | | width: 150px; |
| | | overflow: hidden; |
| | | text-align: left; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .editable-title-input { |
| | | height: 24px; |
| | | max-width: 130px; |
| | | margin-left: 4px; |
| | | line-height: 24px; |
| | | border: 1px solid #d9d9d9; |
| | | border-radius: 4px; |
| | | transition: all 0.3s; |
| | | |
| | | &:focus { |
| | | border-color: #40a9ff; |
| | | outline: 0; |
| | | box-shadow: 0 0 0 2px rgb(24 144 255 / 20%); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .field-setting-pane { |
| | | display: flex; |
| | | flex-direction: column; |
| | | font-size: 14px; |
| | | |
| | | .field-setting-desc { |
| | | padding-right: 8px; |
| | | margin-bottom: 16px; |
| | | font-size: 16px; |
| | | font-weight: 700; |
| | | } |
| | | |
| | | .field-permit-title { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | height: 45px; |
| | | padding-left: 12px; |
| | | line-height: 45px; |
| | | background-color: #f8fafc0a; |
| | | border: 1px solid #1f38581a; |
| | | |
| | | .first-title { |
| | | text-align: left !important; |
| | | } |
| | | |
| | | .other-titles { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | } |
| | | |
| | | .setting-title-label { |
| | | display: inline-block; |
| | | width: 100px; |
| | | padding: 5px 0; |
| | | font-size: 13px; |
| | | font-weight: 700; |
| | | color: #000; |
| | | text-align: center; |
| | | } |
| | | } |
| | | |
| | | .field-setting-item { |
| | | align-items: center; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | height: 38px; |
| | | padding-left: 12px; |
| | | border: 1px solid #1f38581a; |
| | | border-top: 0; |
| | | |
| | | .field-setting-item-label { |
| | | display: inline-block; |
| | | width: 100px; |
| | | min-height: 16px; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | cursor: text; |
| | | } |
| | | |
| | | .field-setting-item-group { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | |
| | | .item-radio-wrap { |
| | | display: inline-block; |
| | | width: 100px; |
| | | text-align: center; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | </style> |
对比新文件 |
| | |
| | | import UserTaskCustomConfig from './components/UserTaskCustomConfig.vue' |
| | | import BoundaryEventTimer from './components/BoundaryEventTimer.vue' |
| | | |
| | | export const CustomConfigMap = { |
| | | UserTask: { |
| | | name: '用户任务', |
| | | componet: UserTaskCustomConfig |
| | | }, |
| | | BoundaryEventTimerEventDefinition: { |
| | | name: '定时边界事件(非中断)', |
| | | componet: BoundaryEventTimer |
| | | } |
| | | } |
| | |
| | | const resetFormList = () => { |
| | | bpmnELement.value = bpmnInstances().bpmnElement |
| | | formKey.value = bpmnELement.value.businessObject.formKey |
| | | if (formKey.value?.length > 0) { |
| | | formKey.value = parseInt(formKey.value) |
| | | } |
| | | // if (formKey.value?.length > 0) { |
| | | // formKey.value = parseInt(formKey.value) |
| | | // } |
| | | // 获取元素扩展属性 或者 创建扩展属性 |
| | | elExtensionElements.value = |
| | | bpmnELement.value.businessObject.get('extensionElements') || |
| | |
| | | } |
| | | // 打开 监听器详情 侧边栏 |
| | | const openListenerForm = (listener, index?) => { |
| | | // debugger |
| | | console.log(listener) |
| | | if (listener) { |
| | | listenerForm.value = initListenerForm(listener) |
| | | editingListenerIndex.value = index |
| | |
| | | } |
| | | // 移除监听器 |
| | | const removeListener = (index) => { |
| | | // debugger |
| | | debugger |
| | | ElMessageBox.confirm('确认移除该监听器吗?', '提示', { |
| | | confirmButtonText: '确 认', |
| | | cancelButtonText: '取 消' |
| | |
| | | const bpmnInstances = () => (window as any)?.bpmnInstances |
| | | |
| | | const resetListenersList = () => { |
| | | console.log( |
| | | bpmnInstances().bpmnElement, |
| | | 'window.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElement' |
| | | ) |
| | | bpmnElement.value = bpmnInstances().bpmnElement |
| | | otherExtensionList.value = [] |
| | | bpmnElementListeners.value = |
| | | bpmnElement.value.businessObject?.extensionElements?.values.filter( |
| | | (ex) => ex.$type === `${prefix}:TaskListener` |
| | | ) ?? [] |
| | | console.log(bpmnElementListeners.value.map) |
| | | elementListenersList.value = bpmnElementListeners.value.map((listener) => |
| | | initListenerType(listener) |
| | | ) |
| | |
| | | // 初始化表单数据 |
| | | export function initListenerForm(listener) { |
| | | console.log(listener) |
| | | let self = { |
| | | ...listener |
| | | } |
| | |
| | | } |
| | | |
| | | export function initListenerType(listener) { |
| | | listener.id = listener.$attrs.id |
| | | let listenerType |
| | | if (listener.class) listenerType = 'classListener' |
| | | if (listener.expression) listenerType = 'expressionListener' |
| | |
| | | <template> |
| | | <div class="panel-tab__content"> |
| | | <el-form label-width="90px"> |
| | | <el-radio-group v-model="approveMethod" @change="onApproveMethodChange"> |
| | | <div class="flex-col"> |
| | | <div v-for="(item, index) in APPROVE_METHODS" :key="index"> |
| | | <el-radio :value="item.value" :label="item.value"> |
| | | {{ item.label }} |
| | | </el-radio> |
| | | <el-form-item prop="approveRatio"> |
| | | <el-input-number |
| | | v-model="approveRatio" |
| | | :min="10" |
| | | :max="100" |
| | | :step="10" |
| | | size="small" |
| | | v-if=" |
| | | item.value === ApproveMethodType.APPROVE_BY_RATIO && |
| | | approveMethod === ApproveMethodType.APPROVE_BY_RATIO |
| | | " |
| | | @change="onApproveRatioChange" |
| | | /> |
| | | </el-form-item> |
| | | </div> |
| | | </div> |
| | | </el-radio-group> |
| | | <!-- 与Simple设计器配置合并,保留以前的代码 --> |
| | | <el-form label-width="90px" style="display: none"> |
| | | <el-form-item label="快捷配置"> |
| | | <el-button size="small" @click="changeConfig('依次审批')">依次审批</el-button> |
| | | <el-button size="small" @click="changeConfig('会签')">会签</el-button> |
| | |
| | | <el-checkbox |
| | | v-model="loopInstanceForm.asyncBefore" |
| | | label="异步前" |
| | | value="异步前" |
| | | @change="updateLoopAsync('asyncBefore')" |
| | | /> |
| | | <el-checkbox |
| | | v-model="loopInstanceForm.asyncAfter" |
| | | label="异步后" |
| | | value="异步后" |
| | | @change="updateLoopAsync('asyncAfter')" |
| | | /> |
| | | <el-checkbox |
| | | v-model="loopInstanceForm.exclusive" |
| | | v-if="loopInstanceForm.asyncAfter || loopInstanceForm.asyncBefore" |
| | | label="排除" |
| | | value="排除" |
| | | @change="updateLoopAsync('exclusive')" |
| | | /> |
| | | </el-form-item> |
| | |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | import { ApproveMethodType, APPROVE_METHODS } from '@/components/SimpleProcessDesignerV2/src/consts' |
| | | |
| | | defineOptions({ name: 'ElementMultiInstance' }) |
| | | |
| | | const props = defineProps({ |
| | | businessObject: Object, |
| | | type: String |
| | | type: String, |
| | | id: String |
| | | }) |
| | | const prefix = inject('prefix') |
| | | const loopCharacteristics = ref('') |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * -----新版本多实例----- |
| | | */ |
| | | const approveMethod = ref() |
| | | const approveRatio = ref(100) |
| | | const otherExtensions = ref() |
| | | const getElementLoopNew = () => { |
| | | const extensionElements = |
| | | bpmnElement.value.businessObject?.extensionElements ?? |
| | | bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] }) |
| | | approveMethod.value = extensionElements.values.filter( |
| | | (ex) => ex.$type === `${prefix}:ApproveMethod` |
| | | )?.[0]?.value |
| | | |
| | | otherExtensions.value = |
| | | extensionElements.values.filter((ex) => ex.$type !== `${prefix}:ApproveMethod`) ?? [] |
| | | |
| | | if (!approveMethod.value) { |
| | | approveMethod.value = ApproveMethodType.SEQUENTIAL_APPROVE |
| | | updateLoopCharacteristics() |
| | | } |
| | | } |
| | | const onApproveMethodChange = () => { |
| | | approveRatio.value = 100 |
| | | updateLoopCharacteristics() |
| | | } |
| | | const onApproveRatioChange = () => { |
| | | updateLoopCharacteristics() |
| | | } |
| | | const updateLoopCharacteristics = () => { |
| | | // 根据ApproveMethod生成multiInstanceLoopCharacteristics节点 |
| | | if (approveMethod.value === ApproveMethodType.RANDOM_SELECT_ONE_APPROVE) { |
| | | bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { |
| | | loopCharacteristics: null |
| | | }) |
| | | } else { |
| | | if (approveMethod.value === ApproveMethodType.APPROVE_BY_RATIO) { |
| | | multiLoopInstance.value = bpmnInstances().moddle.create( |
| | | 'bpmn:MultiInstanceLoopCharacteristics', |
| | | { isSequential: false, collection: '${coll_userList}' } |
| | | ) |
| | | multiLoopInstance.value.completionCondition = bpmnInstances().moddle.create( |
| | | 'bpmn:FormalExpression', |
| | | { |
| | | body: '${ nrOfCompletedInstances/nrOfInstances >= ' + approveRatio.value / 100 + '}' |
| | | } |
| | | ) |
| | | } |
| | | if (approveMethod.value === ApproveMethodType.ANY_APPROVE) { |
| | | multiLoopInstance.value = bpmnInstances().moddle.create( |
| | | 'bpmn:MultiInstanceLoopCharacteristics', |
| | | { isSequential: false, collection: '${coll_userList}' } |
| | | ) |
| | | multiLoopInstance.value.completionCondition = bpmnInstances().moddle.create( |
| | | 'bpmn:FormalExpression', |
| | | { |
| | | body: '${ nrOfCompletedInstances > 0 }' |
| | | } |
| | | ) |
| | | } |
| | | if (approveMethod.value === ApproveMethodType.SEQUENTIAL_APPROVE) { |
| | | multiLoopInstance.value = bpmnInstances().moddle.create( |
| | | 'bpmn:MultiInstanceLoopCharacteristics', |
| | | { isSequential: true, collection: '${coll_userList}' } |
| | | ) |
| | | multiLoopInstance.value.loopCardinality = bpmnInstances().moddle.create( |
| | | 'bpmn:FormalExpression', |
| | | { |
| | | body: '1' |
| | | } |
| | | ) |
| | | multiLoopInstance.value.completionCondition = bpmnInstances().moddle.create( |
| | | 'bpmn:FormalExpression', |
| | | { |
| | | body: '${ nrOfCompletedInstances >= nrOfInstances }' |
| | | } |
| | | ) |
| | | } |
| | | bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { |
| | | loopCharacteristics: toRaw(multiLoopInstance.value) |
| | | }) |
| | | } |
| | | |
| | | // 添加ApproveMethod到ExtensionElements |
| | | const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', { |
| | | values: [ |
| | | ...otherExtensions.value, |
| | | bpmnInstances().moddle.create(`${prefix}:ApproveMethod`, { |
| | | value: approveMethod.value |
| | | }) |
| | | ] |
| | | }) |
| | | bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { |
| | | extensionElements: extensions |
| | | }) |
| | | } |
| | | |
| | | onBeforeUnmount(() => { |
| | | multiLoopInstance.value = null |
| | | bpmnElement.value = null |
| | | }) |
| | | |
| | | watch( |
| | | () => props.businessObject, |
| | | () => props.id, |
| | | (val) => { |
| | | bpmnElement.value = bpmnInstances().bpmnElement |
| | | getElementLoop(val) |
| | | if (val) { |
| | | nextTick(() => { |
| | | bpmnElement.value = bpmnInstances().bpmnElement |
| | | // getElementLoop(val) |
| | | getElementLoopNew() |
| | | }) |
| | | } |
| | | }, |
| | | { immediate: true } |
| | | ) |
| | |
| | | const bpmnInstances = () => (window as any)?.bpmnInstances |
| | | |
| | | const resetAttributesList = () => { |
| | | console.log(window, 'windowwindowwindowwindowwindowwindowwindow') |
| | | bpmnElement.value = bpmnInstances().bpmnElement |
| | | otherExtensionList.value = [] // 其他扩展配置 |
| | | bpmnElementProperties.value = |
| | | // bpmnElement.value.businessObject?.extensionElements?.filter((ex) => { |
| | | bpmnElement.value.businessObject?.extensionElements?.values.filter((ex) => { |
| | | bpmnElement.value.businessObject?.extensionElements?.values?.filter((ex) => { |
| | | if (ex.$type !== `${prefix}:Properties`) { |
| | | otherExtensionList.value.push(ex) |
| | | } |
| | | return ex.$type === `${prefix}:Properties` |
| | | }) ?? [] |
| | | }) ?? []; |
| | | |
| | | // 保存所有的 扩展属性字段 |
| | | bpmnElementPropertyList.value = bpmnElementProperties.value.reduce( |
| | |
| | | <el-checkbox |
| | | v-model="taskConfigForm.asyncBefore" |
| | | label="异步前" |
| | | value="异步前" |
| | | @change="changeTaskAsync" |
| | | /> |
| | | <el-checkbox v-model="taskConfigForm.asyncAfter" label="异步后" @change="changeTaskAsync" /> |
| | | <el-checkbox |
| | | v-model="taskConfigForm.asyncAfter" |
| | | label="异步后" |
| | | value="异步后" |
| | | @change="changeTaskAsync" |
| | | /> |
| | | <el-checkbox |
| | | v-model="taskConfigForm.exclusive" |
| | | v-if="taskConfigForm.asyncAfter || taskConfigForm.asyncBefore" |
| | | label="排除" |
| | | value="排除" |
| | | @change="changeTaskAsync" |
| | | /> |
| | | </el-form-item> |
| | |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | import UserTask from './task-components/UserTask.vue' |
| | | import ScriptTask from './task-components/ScriptTask.vue' |
| | | import ReceiveTask from './task-components/ReceiveTask.vue' |
| | | import { installedComponent } from './data' |
| | | |
| | | defineOptions({ name: 'ElementTaskConfig' }) |
| | | |
| | |
| | | exclusive: false |
| | | }) |
| | | const witchTaskComponent = ref() |
| | | const installedComponent = ref({ |
| | | // 手工任务与普通任务一致,不需要其他配置 |
| | | // 接收消息任务,需要在全局下插入新的消息实例,并在该节点下的 messageRef 属性绑定该实例 |
| | | // 发送任务、服务任务、业务规则任务共用一个相同配置 |
| | | UserTask: 'UserTask', // 用户任务配置 |
| | | ScriptTask: 'ScriptTask', // 脚本任务配置 |
| | | ReceiveTask: 'ReceiveTask' // 消息接收任务 |
| | | }) |
| | | |
| | | const bpmnElement = ref() |
| | | |
| | | const bpmnInstances = () => (window as any).bpmnInstances |
| | |
| | | watch( |
| | | () => props.type, |
| | | () => { |
| | | // witchTaskComponent.value = installedComponent.value[props.type] |
| | | if (props.type == installedComponent.value.UserTask) { |
| | | witchTaskComponent.value = UserTask |
| | | } |
| | | if (props.type == installedComponent.value.ScriptTask) { |
| | | witchTaskComponent.value = ScriptTask |
| | | } |
| | | if (props.type == installedComponent.value.ReceiveTask) { |
| | | witchTaskComponent.value = ReceiveTask |
| | | if (props.type) { |
| | | witchTaskComponent.value = installedComponent[props.type].component |
| | | } |
| | | }, |
| | | { immediate: true } |
对比新文件 |
| | |
| | | import UserTask from './task-components/UserTask.vue' |
| | | import ServiceTask from './task-components/ServiceTask.vue' |
| | | import ScriptTask from './task-components/ScriptTask.vue' |
| | | import ReceiveTask from './task-components/ReceiveTask.vue' |
| | | import CallActivity from './task-components/CallActivity.vue' |
| | | |
| | | export const installedComponent = { |
| | | UserTask: { |
| | | name: '用户任务', |
| | | component: UserTask |
| | | }, |
| | | ServiceTask: { |
| | | name: '服务任务', |
| | | component: ServiceTask |
| | | }, |
| | | ScriptTask: { |
| | | name: '脚本任务', |
| | | component: ScriptTask |
| | | }, |
| | | ReceiveTask: { |
| | | name: '接收任务', |
| | | component: ReceiveTask |
| | | }, |
| | | CallActivity: { |
| | | name: '调用活动', |
| | | component: CallActivity |
| | | } |
| | | } |
| | | |
| | | export const getTaskCollapseItemName = (elementType) => { |
| | | return installedComponent[elementType].name |
| | | } |
| | | |
| | | export const isTaskCollapseItemShow = (elementType) => { |
| | | return installedComponent[elementType] |
| | | } |
对比新文件 |
| | |
| | | <template> |
| | | <div> |
| | | <el-form label-width="100px"> |
| | | <el-form-item label="实例名称" prop="processInstanceName"> |
| | | <el-input |
| | | v-model="formData.processInstanceName" |
| | | clearable |
| | | placeholder="请输入实例名称" |
| | | @change="updateCallActivityAttr('processInstanceName')" |
| | | /> |
| | | </el-form-item> |
| | | |
| | | <!-- TODO 需要可选择已存在的流程 --> |
| | | <el-form-item label="被调用流程" prop="calledElement"> |
| | | <el-input |
| | | v-model="formData.calledElement" |
| | | clearable |
| | | placeholder="请输入被调用流程" |
| | | @change="updateCallActivityAttr('calledElement')" |
| | | /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="继承变量" prop="inheritVariables"> |
| | | <el-switch |
| | | v-model="formData.inheritVariables" |
| | | @change="updateCallActivityAttr('inheritVariables')" |
| | | /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="继承业务键" prop="inheritBusinessKey"> |
| | | <el-switch |
| | | v-model="formData.inheritBusinessKey" |
| | | @change="updateCallActivityAttr('inheritBusinessKey')" |
| | | /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item v-if="!formData.inheritBusinessKey" label="业务键表达式" prop="businessKey"> |
| | | <el-input |
| | | v-model="formData.businessKey" |
| | | clearable |
| | | placeholder="请输入业务键表达式" |
| | | @change="updateCallActivityAttr('businessKey')" |
| | | /> |
| | | </el-form-item> |
| | | |
| | | <el-divider /> |
| | | <div> |
| | | <div class="flex mb-10px"> |
| | | <el-text>输入参数</el-text> |
| | | <XButton |
| | | class="ml-auto" |
| | | type="primary" |
| | | preIcon="ep:plus" |
| | | title="添加参数" |
| | | size="small" |
| | | @click="openVariableForm('in', null, -1)" |
| | | /> |
| | | </div> |
| | | <el-table :data="inVariableList" max-height="240" fit border> |
| | | <el-table-column label="源" prop="source" min-width="100px" show-overflow-tooltip /> |
| | | <el-table-column label="目标" prop="target" min-width="100px" show-overflow-tooltip /> |
| | | <el-table-column label="操作" width="110px"> |
| | | <template #default="scope"> |
| | | <el-button link @click="openVariableForm('in', scope.row, scope.$index)" size="small"> |
| | | 编辑 |
| | | </el-button> |
| | | <el-divider direction="vertical" /> |
| | | <el-button |
| | | link |
| | | size="small" |
| | | style="color: #ff4d4f" |
| | | @click="removeVariable('in', scope.$index)" |
| | | > |
| | | 移除 |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | |
| | | <el-divider /> |
| | | <div> |
| | | <div class="flex mb-10px"> |
| | | <el-text>输出参数</el-text> |
| | | <XButton |
| | | class="ml-auto" |
| | | type="primary" |
| | | preIcon="ep:plus" |
| | | title="添加参数" |
| | | size="small" |
| | | @click="openVariableForm('out', null, -1)" |
| | | /> |
| | | </div> |
| | | <el-table :data="outVariableList" max-height="240" fit border> |
| | | <el-table-column label="源" prop="source" min-width="100px" show-overflow-tooltip /> |
| | | <el-table-column label="目标" prop="target" min-width="100px" show-overflow-tooltip /> |
| | | <el-table-column label="操作" width="110px"> |
| | | <template #default="scope"> |
| | | <el-button |
| | | link |
| | | @click="openVariableForm('out', scope.row, scope.$index)" |
| | | size="small" |
| | | > |
| | | 编辑 |
| | | </el-button> |
| | | <el-divider direction="vertical" /> |
| | | <el-button |
| | | link |
| | | size="small" |
| | | style="color: #ff4d4f" |
| | | @click="removeVariable('out', scope.$index)" |
| | | > |
| | | 移除 |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </el-form> |
| | | |
| | | <!-- 添加或修改参数 --> |
| | | <el-dialog |
| | | v-model="variableDialogVisible" |
| | | title="参数配置" |
| | | width="600px" |
| | | append-to-body |
| | | destroy-on-close |
| | | > |
| | | <el-form :model="varialbeFormData" label-width="80px" ref="varialbeFormRef"> |
| | | <el-form-item label="源:" prop="source"> |
| | | <el-input v-model="varialbeFormData.source" clearable /> |
| | | </el-form-item> |
| | | <el-form-item label="目标:" prop="target"> |
| | | <el-input v-model="varialbeFormData.target" clearable /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <el-button @click="variableDialogVisible = false">取 消</el-button> |
| | | <el-button type="primary" @click="saveVariable">确 定</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | defineOptions({ name: 'CallActivity' }) |
| | | const props = defineProps({ |
| | | id: String, |
| | | type: String |
| | | }) |
| | | const prefix = inject('prefix') |
| | | const message = useMessage() |
| | | |
| | | const formData = ref({ |
| | | processInstanceName: '', |
| | | calledElement: '', |
| | | inheritVariables: false, |
| | | businessKey: '', |
| | | inheritBusinessKey: false, |
| | | calledElementType: 'key' |
| | | }) |
| | | const inVariableList = ref() |
| | | const outVariableList = ref() |
| | | const variableType = ref() // 参数类型 |
| | | const editingVariableIndex = ref(-1) // 编辑参数下标 |
| | | const variableDialogVisible = ref(false) |
| | | const varialbeFormRef = ref() |
| | | const varialbeFormData = ref({ |
| | | source: '', |
| | | target: '' |
| | | }) |
| | | |
| | | const bpmnInstances = () => (window as any)?.bpmnInstances |
| | | const bpmnElement = ref() |
| | | const otherExtensionList = ref() |
| | | |
| | | const initCallActivity = () => { |
| | | bpmnElement.value = bpmnInstances().bpmnElement |
| | | console.log(bpmnElement.value.businessObject, 'callActivity') |
| | | |
| | | // 初始化所有配置项 |
| | | Object.keys(formData.value).forEach((key) => { |
| | | formData.value[key] = bpmnElement.value.businessObject[key] ?? formData.value[key] |
| | | }) |
| | | |
| | | otherExtensionList.value = [] // 其他扩展配置 |
| | | inVariableList.value = [] |
| | | outVariableList.value = [] |
| | | // 初始化输入参数 |
| | | bpmnElement.value.businessObject?.extensionElements?.values?.forEach((ex) => { |
| | | if (ex.$type === `${prefix}:In`) { |
| | | inVariableList.value.push(ex) |
| | | } else if (ex.$type === `${prefix}:Out`) { |
| | | outVariableList.value.push(ex) |
| | | } else { |
| | | otherExtensionList.value.push(ex) |
| | | } |
| | | }) |
| | | |
| | | // 默认添加 |
| | | // bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { |
| | | // calledElementType: 'key' |
| | | // }) |
| | | } |
| | | |
| | | const updateCallActivityAttr = (attr) => { |
| | | bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { |
| | | [attr]: formData.value[attr] |
| | | }) |
| | | } |
| | | |
| | | const openVariableForm = (type, data, index) => { |
| | | editingVariableIndex.value = index |
| | | variableType.value = type |
| | | varialbeFormData.value = index === -1 ? {} : { ...data } |
| | | variableDialogVisible.value = true |
| | | } |
| | | |
| | | const removeVariable = async (type, index) => { |
| | | try { |
| | | await message.delConfirm() |
| | | if (type === 'in') { |
| | | inVariableList.value.splice(index, 1) |
| | | } |
| | | if (type === 'out') { |
| | | outVariableList.value.splice(index, 1) |
| | | } |
| | | updateElementExtensions() |
| | | } catch {} |
| | | } |
| | | |
| | | const saveVariable = () => { |
| | | if (editingVariableIndex.value === -1) { |
| | | if (variableType.value === 'in') { |
| | | inVariableList.value.push( |
| | | bpmnInstances().moddle.create(`${prefix}:In`, { ...varialbeFormData.value }) |
| | | ) |
| | | } |
| | | if (variableType.value === 'out') { |
| | | outVariableList.value.push( |
| | | bpmnInstances().moddle.create(`${prefix}:Out`, { ...varialbeFormData.value }) |
| | | ) |
| | | } |
| | | updateElementExtensions() |
| | | } else { |
| | | if (variableType.value === 'in') { |
| | | inVariableList.value[editingVariableIndex.value].source = varialbeFormData.value.source |
| | | inVariableList.value[editingVariableIndex.value].target = varialbeFormData.value.target |
| | | } |
| | | if (variableType.value === 'out') { |
| | | outVariableList.value[editingVariableIndex.value].source = varialbeFormData.value.source |
| | | outVariableList.value[editingVariableIndex.value].target = varialbeFormData.value.target |
| | | } |
| | | } |
| | | variableDialogVisible.value = false |
| | | } |
| | | |
| | | const updateElementExtensions = () => { |
| | | const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', { |
| | | values: [...inVariableList.value, ...outVariableList.value, ...otherExtensionList.value] |
| | | }) |
| | | bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { |
| | | extensionElements: extensions |
| | | }) |
| | | } |
| | | |
| | | watch( |
| | | () => props.id, |
| | | (val) => { |
| | | val && |
| | | val.length && |
| | | nextTick(() => { |
| | | initCallActivity() |
| | | }) |
| | | }, |
| | | { immediate: true } |
| | | ) |
| | | </script> |
| | | |
| | | <style lang="scss" scoped></style> |
对比新文件 |
| | |
| | | <template> |
| | | <div> |
| | | <el-form-item label="执行类型" key="executeType"> |
| | | <el-select v-model="serviceTaskForm.executeType"> |
| | | <el-option label="Java类" value="class" /> |
| | | <el-option label="表达式" value="expression" /> |
| | | <el-option label="代理表达式" value="delegateExpression" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="serviceTaskForm.executeType === 'class'" |
| | | label="Java类" |
| | | prop="class" |
| | | key="execute-class" |
| | | > |
| | | <el-input v-model="serviceTaskForm.class" clearable @change="updateElementTask" /> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="serviceTaskForm.executeType === 'expression'" |
| | | label="表达式" |
| | | prop="expression" |
| | | key="execute-expression" |
| | | > |
| | | <el-input v-model="serviceTaskForm.expression" clearable @change="updateElementTask" /> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="serviceTaskForm.executeType === 'delegateExpression'" |
| | | label="代理表达式" |
| | | prop="delegateExpression" |
| | | key="execute-delegate" |
| | | > |
| | | <el-input v-model="serviceTaskForm.delegateExpression" clearable @change="updateElementTask" /> |
| | | </el-form-item> |
| | | </div> |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | defineOptions({ name: 'ServiceTask' }) |
| | | const props = defineProps({ |
| | | id: String, |
| | | type: String |
| | | }) |
| | | |
| | | const defaultTaskForm = ref({ |
| | | executeType: '', |
| | | class: '', |
| | | expression: '', |
| | | delegateExpression: '' |
| | | }) |
| | | |
| | | const serviceTaskForm = ref<any>({}) |
| | | const bpmnElement = ref() |
| | | |
| | | const bpmnInstances = () => (window as any)?.bpmnInstances |
| | | |
| | | const resetTaskForm = () => { |
| | | for (let key in defaultTaskForm.value) { |
| | | let value = bpmnElement.value?.businessObject[key] || defaultTaskForm.value[key] |
| | | serviceTaskForm.value[key] = value |
| | | if (value) { |
| | | serviceTaskForm.value.executeType = key |
| | | } |
| | | } |
| | | } |
| | | |
| | | const updateElementTask = () => { |
| | | let taskAttr = Object.create(null); |
| | | const type = serviceTaskForm.value.executeType; |
| | | for (let key in serviceTaskForm.value) { |
| | | if (key !== 'executeType' && key !== type) taskAttr[key] = null; |
| | | } |
| | | taskAttr[type] = serviceTaskForm.value[type] || ""; |
| | | bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), taskAttr) |
| | | } |
| | | |
| | | onBeforeUnmount(() => { |
| | | bpmnElement.value = null |
| | | }) |
| | | |
| | | watch( |
| | | () => props.id, |
| | | () => { |
| | | bpmnElement.value = bpmnInstances().bpmnElement |
| | | nextTick(() => { |
| | | resetTaskForm() |
| | | }) |
| | | }, |
| | | { immediate: true } |
| | | ) |
| | | |
| | | </script> |
| | |
| | | <template> |
| | | <el-form label-width="100px"> |
| | | <el-form label-width="120px"> |
| | | <el-form-item label="规则类型" prop="candidateStrategy"> |
| | | <el-select |
| | | v-model="userTaskForm.candidateStrategy" |
| | |
| | | @change="changeCandidateStrategy" |
| | | > |
| | | <el-option |
| | | v-for="dict in getIntDictOptions(DICT_TYPE.BPM_TASK_CANDIDATE_STRATEGY)" |
| | | :key="dict.value" |
| | | v-for="(dict, index) in CANDIDATE_STRATEGY" |
| | | :key="index" |
| | | :label="dict.label" |
| | | :value="dict.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="userTaskForm.candidateStrategy == 10" |
| | | v-if="userTaskForm.candidateStrategy == CandidateStrategy.ROLE" |
| | | label="指定角色" |
| | | prop="candidateParam" |
| | | > |
| | |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="userTaskForm.candidateStrategy == 20 || userTaskForm.candidateStrategy == 21" |
| | | v-if=" |
| | | userTaskForm.candidateStrategy == CandidateStrategy.DEPT_MEMBER || |
| | | userTaskForm.candidateStrategy == CandidateStrategy.DEPT_LEADER || |
| | | userTaskForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER |
| | | " |
| | | label="指定部门" |
| | | prop="candidateParam" |
| | | span="24" |
| | |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="userTaskForm.candidateStrategy == 22" |
| | | v-if="userTaskForm.candidateStrategy == CandidateStrategy.POST" |
| | | label="指定岗位" |
| | | prop="candidateParam" |
| | | span="24" |
| | |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="userTaskForm.candidateStrategy == 30" |
| | | v-if="userTaskForm.candidateStrategy == CandidateStrategy.USER" |
| | | label="指定用户" |
| | | prop="candidateParam" |
| | | span="24" |
| | |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="userTaskForm.candidateStrategy === 40" |
| | | v-if="userTaskForm.candidateStrategy === CandidateStrategy.USER_GROUP" |
| | | label="指定用户组" |
| | | prop="candidateParam" |
| | | > |
| | |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="userTaskForm.candidateStrategy === 60" |
| | | v-if="userTaskForm.candidateStrategy === CandidateStrategy.FORM_USER" |
| | | label="表单内用户字段" |
| | | prop="formUser" |
| | | > |
| | | <el-select |
| | | v-model="userTaskForm.candidateParam" |
| | | clearable |
| | | style="width: 100%" |
| | | @change="handleFormUserChange" |
| | | > |
| | | <el-option |
| | | v-for="(item, idx) in userFieldOnFormOptions" |
| | | :key="idx" |
| | | :label="item.title" |
| | | :value="item.field" |
| | | :disabled="!item.required" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="userTaskForm.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER" |
| | | label="表单内部门字段" |
| | | prop="formDept" |
| | | > |
| | | <el-select |
| | | v-model="userTaskForm.candidateParam" |
| | | clearable |
| | | style="width: 100%" |
| | | @change="updateElementTask" |
| | | > |
| | | <el-option |
| | | v-for="(item, idx) in deptFieldOnFormOptions" |
| | | :key="idx" |
| | | :label="item.title" |
| | | :value="item.field" |
| | | :disabled="!item.required" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if=" |
| | | userTaskForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER || |
| | | userTaskForm.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER || |
| | | userTaskForm.candidateStrategy == CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER || |
| | | userTaskForm.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER |
| | | " |
| | | :label="deptLevelLabel!" |
| | | prop="deptLevel" |
| | | span="24" |
| | | > |
| | | <el-select v-model="deptLevel" clearable @change="updateElementTask"> |
| | | <el-option |
| | | v-for="(item, index) in MULTI_LEVEL_DEPT" |
| | | :key="index" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="userTaskForm.candidateStrategy === CandidateStrategy.EXPRESSION" |
| | | label="流程表达式" |
| | | prop="candidateParam" |
| | | > |
| | |
| | | type="textarea" |
| | | v-model="userTaskForm.candidateParam[0]" |
| | | clearable |
| | | style="width: 72%" |
| | | style="width: 100%" |
| | | @change="updateElementTask" |
| | | /> |
| | | <el-button class="ml-5px" size="small" type="success" @click="openProcessExpressionDialog" |
| | | >选择表达式</el-button |
| | | > |
| | | <XButton |
| | | class="!w-1/1 mt-5px" |
| | | type="success" |
| | | preIcon="ep:select" |
| | | title="选择表达式" |
| | | size="small" |
| | | @click="openProcessExpressionDialog" |
| | | /> |
| | | <!-- 选择弹窗 --> |
| | | <ProcessExpressionDialog ref="processExpressionDialogRef" @select="selectProcessExpression" /> |
| | | </el-form-item> |
| | |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' |
| | | import { |
| | | CANDIDATE_STRATEGY, |
| | | CandidateStrategy, |
| | | FieldPermissionType, |
| | | MULTI_LEVEL_DEPT |
| | | } from '@/components/SimpleProcessDesignerV2/src/consts' |
| | | import { defaultProps, handleTree } from '@/utils/tree' |
| | | import * as RoleApi from '@/api/system/role' |
| | | import * as DeptApi from '@/api/system/dept' |
| | |
| | | import * as UserGroupApi from '@/api/bpm/userGroup' |
| | | import ProcessExpressionDialog from './ProcessExpressionDialog.vue' |
| | | import { ProcessExpressionVO } from '@/api/bpm/processExpression' |
| | | import { useFormFieldsPermission } from '@/components/SimpleProcessDesignerV2/src/node' |
| | | |
| | | defineOptions({ name: 'UserTask' }) |
| | | const props = defineProps({ |
| | | id: String, |
| | | type: String |
| | | }) |
| | | const prefix = inject('prefix') |
| | | const userTaskForm = ref({ |
| | | candidateStrategy: undefined, // 分配规则 |
| | | candidateParam: [] // 分配选项 |
| | |
| | | const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表 |
| | | const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表 |
| | | |
| | | const { formFieldOptions } = useFormFieldsPermission(FieldPermissionType.READ) |
| | | // 表单内用户字段选项, 必须是必填和用户选择器 |
| | | const userFieldOnFormOptions = computed(() => { |
| | | return formFieldOptions.filter((item) => item.type === 'UserSelect') |
| | | }) |
| | | // 表单内部门字段选项, 必须是必填和部门选择器 |
| | | const deptFieldOnFormOptions = computed(() => { |
| | | return formFieldOptions.filter((item) => item.type === 'DeptSelect') |
| | | }) |
| | | |
| | | const deptLevel = ref(1) |
| | | const deptLevelLabel = computed(() => { |
| | | let label = '部门负责人来源' |
| | | if (userTaskForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER) { |
| | | label = label + '(指定部门向上)' |
| | | } else if (userTaskForm.value.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER) { |
| | | label = label + '(表单内部门向上)' |
| | | } else { |
| | | label = label + '(发起人部门向上)' |
| | | } |
| | | return label |
| | | }) |
| | | |
| | | const otherExtensions = ref() |
| | | |
| | | const resetTaskForm = () => { |
| | | const businessObject = bpmnElement.value.businessObject |
| | | if (!businessObject) { |
| | | return |
| | | } |
| | | |
| | | const extensionElements = |
| | | businessObject?.extensionElements ?? |
| | | bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] }) |
| | | userTaskForm.value.candidateStrategy = extensionElements.values?.filter( |
| | | (ex) => ex.$type === `${prefix}:CandidateStrategy` |
| | | )?.[0]?.value |
| | | const candidateParamStr = extensionElements.values?.filter( |
| | | (ex) => ex.$type === `${prefix}:CandidateParam` |
| | | )?.[0]?.value |
| | | if (candidateParamStr && candidateParamStr.length > 0) { |
| | | if (userTaskForm.value.candidateStrategy === CandidateStrategy.EXPRESSION) { |
| | | // 特殊:流程表达式,只有一个 input 输入框 |
| | | userTaskForm.value.candidateParam = [candidateParamStr] |
| | | } else if (userTaskForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER) { |
| | | // 特殊:多级不部门负责人,需要通过'|'分割 |
| | | userTaskForm.value.candidateParam = candidateParamStr |
| | | .split('|')[0] |
| | | .split(',') |
| | | .map((item) => { |
| | | // 如果数字超出了最大安全整数范围,则将其作为字符串处理 |
| | | let num = Number(item) |
| | | return num > Number.MAX_SAFE_INTEGER || num < -Number.MAX_SAFE_INTEGER ? item : num |
| | | }) |
| | | deptLevel.value = +candidateParamStr.split('|')[1] |
| | | } else if ( |
| | | userTaskForm.value.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER || |
| | | userTaskForm.value.candidateStrategy == CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER |
| | | ) { |
| | | userTaskForm.value.candidateParam = +candidateParamStr |
| | | deptLevel.value = +candidateParamStr |
| | | } else if (userTaskForm.value.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER) { |
| | | userTaskForm.value.candidateParam = candidateParamStr.split('|')[0] |
| | | deptLevel.value = +candidateParamStr.split('|')[1] |
| | | } else { |
| | | userTaskForm.value.candidateParam = candidateParamStr.split(',').map((item) => { |
| | | // 如果数字超出了最大安全整数范围,则将其作为字符串处理 |
| | | let num = Number(item) |
| | | return num > Number.MAX_SAFE_INTEGER || num < -Number.MAX_SAFE_INTEGER ? item : num |
| | | }) |
| | | } |
| | | } else { |
| | | userTaskForm.value.candidateParam = [] |
| | | } |
| | | |
| | | otherExtensions.value = |
| | | extensionElements.values?.filter( |
| | | (ex) => ex.$type !== `${prefix}:CandidateStrategy` && ex.$type !== `${prefix}:CandidateParam` |
| | | ) ?? [] |
| | | |
| | | // 改用通过extensionElements来存储数据 |
| | | return |
| | | if (businessObject.candidateStrategy != undefined) { |
| | | userTaskForm.value.candidateStrategy = parseInt(businessObject.candidateStrategy) as any |
| | | } else { |
| | |
| | | } else { |
| | | userTaskForm.value.candidateParam = businessObject.candidateParam |
| | | .split(',') |
| | | .map((item) => +item) |
| | | .map((item) => item) |
| | | } |
| | | } else { |
| | | userTaskForm.value.candidateParam = [] |
| | |
| | | /** 更新 candidateStrategy 字段时,需要清空 candidateParam,并触发 bpmn 图更新 */ |
| | | const changeCandidateStrategy = () => { |
| | | userTaskForm.value.candidateParam = [] |
| | | deptLevel.value = 1 |
| | | if (userTaskForm.value.candidateStrategy === CandidateStrategy.FORM_USER) { |
| | | // 特殊处理表单内用户字段,当只有发起人选项时应选中发起人 |
| | | if (!userFieldOnFormOptions.value || userFieldOnFormOptions.value.length <= 1) { |
| | | userTaskForm.value.candidateStrategy = CandidateStrategy.START_USER |
| | | } |
| | | } |
| | | updateElementTask() |
| | | } |
| | | |
| | | /** 选中某个 options 时候,更新 bpmn 图 */ |
| | | const updateElementTask = () => { |
| | | let candidateParam = |
| | | userTaskForm.value.candidateParam instanceof Array |
| | | ? userTaskForm.value.candidateParam.join(',') |
| | | : userTaskForm.value.candidateParam |
| | | |
| | | // 特殊处理多级部门情况 |
| | | if ( |
| | | userTaskForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER || |
| | | userTaskForm.value.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER |
| | | ) { |
| | | candidateParam += '|' + deptLevel.value |
| | | } |
| | | // 特殊处理发起人部门负责人、发起人连续部门负责人 |
| | | if ( |
| | | userTaskForm.value.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER || |
| | | userTaskForm.value.candidateStrategy == CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER |
| | | ) { |
| | | candidateParam = deptLevel.value + '' |
| | | } |
| | | |
| | | const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', { |
| | | values: [ |
| | | ...otherExtensions.value, |
| | | bpmnInstances().moddle.create(`${prefix}:CandidateStrategy`, { |
| | | value: userTaskForm.value.candidateStrategy |
| | | }), |
| | | bpmnInstances().moddle.create(`${prefix}:CandidateParam`, { |
| | | value: candidateParam |
| | | }) |
| | | ] |
| | | }) |
| | | bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { |
| | | extensionElements: extensions |
| | | }) |
| | | |
| | | // 改用通过extensionElements来存储数据 |
| | | return |
| | | bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { |
| | | candidateStrategy: userTaskForm.value.candidateStrategy, |
| | | candidateParam: userTaskForm.value.candidateParam.join(',') |
| | |
| | | updateElementTask() |
| | | } |
| | | |
| | | const handleFormUserChange = (e) => { |
| | | if (e === 'PROCESS_START_USER_ID') { |
| | | userTaskForm.value.candidateParam = [] |
| | | userTaskForm.value.candidateStrategy = CandidateStrategy.START_USER |
| | | } |
| | | updateElementTask() |
| | | } |
| | | |
| | | watch( |
| | | () => props.id, |
| | | () => { |
| | |
| | | /* 改变 icon 字体路径变量,必需 */ |
| | | $--font-path: '~element-ui/lib/theme-chalk/fonts'; |
| | | |
| | | @import '~element-ui/packages/theme-chalk/src/index'; |
| | | @use '~element-ui/packages/theme-chalk/src/index'; |
| | | |
| | | .el-table td, |
| | | .el-table th { |
| | |
| | | @import './process-designer.scss'; |
| | | @import './process-panel.scss'; |
| | | @use './process-designer.scss'; |
| | | @use './process-panel.scss'; |
| | | |
| | | $success-color: #4eb819; |
| | | $primary-color: #409EFF; |
| | | $danger-color: #F56C6C; |
| | | $cancel-color: #909399; |
| | | |
| | | .process-viewer { |
| | | position: relative; |
| | | border: 1px solid #EFEFEF; |
| | | background: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImEiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdGggZD0iTTAgMTBoNDBNMTAgMHY0ME0wIDIwaDQwTTIwIDB2NDBNMCAzMGg0ME0zMCAwdjQwIiBmaWxsPSJub25lIiBzdHJva2U9IiNlMGUwZTAiIG9wYWNpdHk9Ii4yIi8+PHBhdGggZD0iTTQwIDBIMHY0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZTBlMGUwIi8+PC9wYXR0ZXJuPjwvZGVmcz48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2EpIi8+PC9zdmc+') repeat!important; |
| | | |
| | | .success-arrow { |
| | | fill: $success-color; |
| | | stroke: $success-color; |
| | | } |
| | | |
| | | .success-conditional { |
| | | fill: white; |
| | | stroke: $success-color; |
| | | } |
| | | |
| | | .success.djs-connection { |
| | | .djs-visual path { |
| | | stroke: $success-color!important; |
| | | //marker-end: url(#sequenceflow-end-white-success)!important; |
| | | } |
| | | } |
| | | |
| | | .success.djs-connection.condition-expression { |
| | | .djs-visual path { |
| | | //marker-start: url(#conditional-flow-marker-white-success)!important; |
| | | } |
| | | } |
| | | |
| | | .success.djs-shape { |
| | | .djs-visual rect { |
| | | stroke: $success-color!important; |
| | | fill: $success-color!important; |
| | | fill-opacity: 0.15!important; |
| | | } |
| | | |
| | | .djs-visual polygon { |
| | | stroke: $success-color!important; |
| | | } |
| | | |
| | | .djs-visual path:nth-child(2) { |
| | | stroke: $success-color!important; |
| | | fill: $success-color!important; |
| | | } |
| | | |
| | | .djs-visual circle { |
| | | stroke: $success-color!important; |
| | | fill: $success-color!important; |
| | | fill-opacity: 0.15!important; |
| | | } |
| | | } |
| | | |
| | | .primary.djs-shape { |
| | | .djs-visual rect { |
| | | stroke: $primary-color!important; |
| | | fill: $primary-color!important; |
| | | fill-opacity: 0.15!important; |
| | | } |
| | | |
| | | .djs-visual polygon { |
| | | stroke: $primary-color!important; |
| | | } |
| | | |
| | | .djs-visual circle { |
| | | stroke: $primary-color!important; |
| | | fill: $primary-color!important; |
| | | fill-opacity: 0.15!important; |
| | | } |
| | | } |
| | | |
| | | .danger.djs-shape { |
| | | .djs-visual rect { |
| | | stroke: $danger-color!important; |
| | | fill: $danger-color!important; |
| | | fill-opacity: 0.15!important; |
| | | } |
| | | |
| | | .djs-visual polygon { |
| | | stroke: $danger-color!important; |
| | | } |
| | | |
| | | .djs-visual circle { |
| | | stroke: $danger-color!important; |
| | | fill: $danger-color!important; |
| | | fill-opacity: 0.15!important; |
| | | } |
| | | } |
| | | |
| | | .cancel.djs-shape { |
| | | .djs-visual rect { |
| | | stroke: $cancel-color!important; |
| | | fill: $cancel-color!important; |
| | | fill-opacity: 0.15!important; |
| | | } |
| | | |
| | | .djs-visual polygon { |
| | | stroke: $cancel-color!important; |
| | | } |
| | | |
| | | .djs-visual circle { |
| | | stroke: $cancel-color!important; |
| | | fill: $cancel-color!important; |
| | | fill-opacity: 0.15!important; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .process-viewer .djs-tooltip-container, .process-viewer .djs-overlay-container, .process-viewer .djs-palette { |
| | | display: none; |
| | | } |
| | |
| | | @import 'bpmn-js-token-simulation/assets/css/bpmn-js-token-simulation.css'; |
| | | @import 'bpmn-js-token-simulation/assets/css/font-awesome.min.css'; |
| | | @import 'bpmn-js-token-simulation/assets/css/normalize.css'; |
| | | @use 'bpmn-js-token-simulation/assets/css/bpmn-js-token-simulation.css'; |
| | | |
| | | // 边框被 token-simulation 样式覆盖了 |
| | | .djs-palette { |
| | |
| | | height: 100%; |
| | | position: relative; |
| | | background: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImEiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdGggZD0iTTAgMTBoNDBNMTAgMHY0ME0wIDIwaDQwTTIwIDB2NDBNMCAzMGg0ME0zMCAwdjQwIiBmaWxsPSJub25lIiBzdHJva2U9IiNlMGUwZTAiIG9wYWNpdHk9Ii4yIi8+PHBhdGggZD0iTTQwIDBIMHY0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZTBlMGUwIi8+PC9wYXR0ZXJuPjwvZGVmcz48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2EpIi8+PC9zdmc+') |
| | | repeat !important; |
| | | repeat !important; |
| | | div.toggle-mode { |
| | | display: none; |
| | | } |
| | |
| | | box-sizing: border-box; |
| | | } |
| | | } |
| | | svg { |
| | | width: 100%; |
| | | height: 100%; |
| | | min-height: 100%; |
| | | overflow: hidden; |
| | | } |
| | | // svg { |
| | | // width: 100%; |
| | | // height: 100%; |
| | | // min-height: 100%; |
| | | // overflow: hidden; |
| | | // } |
| | | } |
| | | } |
| | | |
| | |
| | | const bpmnInstances = () => (window as any)?.bpmnInstances |
| | | // 创建监听器实例 |
| | | export function createListenerObject(options, isTask, prefix) { |
| | | // debugger |
| | | debugger |
| | | const listenerObj = Object.create(null) |
| | | listenerObj.event = options.event |
| | | isTask && (listenerObj.id = options.id) // 任务监听器特有的 id 字段 |
| | |
| | | import axios, { |
| | | AxiosError, |
| | | AxiosInstance, |
| | | AxiosRequestHeaders, |
| | | AxiosResponse, |
| | | InternalAxiosRequestConfig |
| | | } from 'axios' |
| | | import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios' |
| | | |
| | | import { ElMessage, ElMessageBox, ElNotification } from 'element-plus' |
| | | import qs from 'qs' |
| | |
| | | const service: AxiosInstance = axios.create({ |
| | | baseURL: base_url, // api 的 base_url |
| | | timeout: request_timeout, // 请求超时时间 |
| | | withCredentials: false // 禁用 Cookie 等信息 |
| | | withCredentials: false, // 禁用 Cookie 等信息 |
| | | // 自定义参数序列化函数 |
| | | paramsSerializer: (params) => { |
| | | return qs.stringify(params, { allowDots: true }) |
| | | } |
| | | }) |
| | | |
| | | // request拦截器 |
| | |
| | | // 是否需要设置 token |
| | | let isToken = (config!.headers || {}).isToken === false |
| | | whiteList.some((v) => { |
| | | if (config.url) { |
| | | config.url.indexOf(v) > -1 |
| | | if (config.url && config.url.indexOf(v) > -1) { |
| | | return (isToken = false) |
| | | } |
| | | }) |
| | | if (getAccessToken() && !isToken) { |
| | | ;(config as Recordable).headers.Authorization = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token |
| | | config.headers.Authorization = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token |
| | | } |
| | | // 设置租户 |
| | | if (tenantEnable && tenantEnable === 'true') { |
| | | const tenantId = getTenantId() |
| | | if (tenantId) (config as Recordable).headers['tenant-id'] = tenantId |
| | | if (tenantId) config.headers['tenant-id'] = tenantId |
| | | } |
| | | const params = config.params || {} |
| | | const data = config.data || false |
| | | if ( |
| | | config.method?.toUpperCase() === 'POST' && |
| | | (config.headers as AxiosRequestHeaders)['Content-Type'] === |
| | | 'application/x-www-form-urlencoded' |
| | | ) { |
| | | config.data = qs.stringify(data) |
| | | const method = config.method?.toUpperCase() |
| | | // 防止 GET 请求缓存 |
| | | if (method === 'GET') { |
| | | config.headers['Cache-Control'] = 'no-cache' |
| | | config.headers['Pragma'] = 'no-cache' |
| | | } |
| | | // get参数编码 |
| | | if (config.method?.toUpperCase() === 'GET' && params) { |
| | | config.params = {} |
| | | const paramsStr = qs.stringify(params, { allowDots: true }) |
| | | if (paramsStr) { |
| | | config.url = config.url + '?' + paramsStr |
| | | // 自定义参数序列化函数 |
| | | else if (method === 'POST') { |
| | | const contentType = config.headers['Content-Type'] || config.headers['content-type'] |
| | | if (contentType === 'application/x-www-form-urlencoded') { |
| | | if (config.data && typeof config.data !== 'string') { |
| | | config.data = qs.stringify(config.data) |
| | | } |
| | | } |
| | | } |
| | | return config |
| | |
| | | t('sys.api.errMsg901') + |
| | | '</div>' + |
| | | '<div> </div>' + |
| | | '<div>参考 https://xxxx/ 教程</div>' + |
| | | '<div>参考 https://doc.iailab.cn/ 教程</div>' + |
| | | '<div> </div>' + |
| | | '<div>5 分钟搭建本地环境</div>' |
| | | }) |
| | |
| | | const handleAuthorized = () => { |
| | | const { t } = useI18n() |
| | | if (!isRelogin.show) { |
| | | // 如果已经到重新登录页面则不进行弹窗提示 |
| | | if (window.location.href.includes('login?redirect=')) { |
| | | return |
| | | } |
| | | isRelogin.show = true |
| | | ElMessageBox.confirm(t('sys.api.timeoutMessage'), t('common.confirmTitle'), { |
| | | showCancelButton: false, |
| | | closeOnClickModal: false, |
| | | showClose: false, |
| | | closeOnPressEscape: false, |
| | | confirmButtonText: t('login.relogin'), |
| | | type: 'warning' |
| | | }).then(() => { |
| | |
| | | hasRole(app) |
| | | hasPermi(app) |
| | | } |
| | | |
| | | /** |
| | | * 导出指令:v-mountedFocus |
| | | */ |
| | | export const setupMountedFocus = (app: App<Element>) => { |
| | | app.directive('mountedFocus', { |
| | | mounted(el) { |
| | | el.focus() |
| | | } |
| | | }) |
| | | } |
| | |
| | | |
| | | export function hasPermi(app: App<Element>) { |
| | | app.directive('hasPermi', (el, binding) => { |
| | | const { wsCache } = useCache() |
| | | const { value } = binding |
| | | const all_permission = '*:*:*' |
| | | const permissions = wsCache.get(CACHE_KEY.USER).permissions |
| | | |
| | | if (value && value instanceof Array && value.length > 0) { |
| | | const permissionFlag = value |
| | | |
| | | const hasPermissions = permissions.some((permission: string) => { |
| | | return all_permission === permission || permissionFlag.includes(permission) |
| | | }) |
| | | const hasPermissions = hasPermission(value) |
| | | |
| | | if (!hasPermissions) { |
| | | el.parentNode && el.parentNode.removeChild(el) |
| | |
| | | } |
| | | }) |
| | | } |
| | | |
| | | export const hasPermission = (permission: string[]) => { |
| | | const { wsCache } = useCache() |
| | | const all_permission = '*:*:*' |
| | | const userInfo = wsCache.get(CACHE_KEY.USER) |
| | | const permissions = userInfo?.permissions || [] |
| | | |
| | | return permissions.some((p: string) => { |
| | | return all_permission === p || permission.includes(p) |
| | | }) |
| | | } |
| | |
| | | app.directive('hasRole', (el, binding) => { |
| | | const { wsCache } = useCache() |
| | | const { value } = binding |
| | | const super_admin = 'admin' |
| | | const roles = wsCache.get(CACHE_KEY.USER).roles |
| | | const super_admin = 'super_admin' |
| | | const userInfo = wsCache.get(CACHE_KEY.USER) |
| | | const roles = userInfo?.roles || [] |
| | | |
| | | if (value && value instanceof Array && value.length > 0) { |
| | | const roleFlag = value |
| | |
| | | wsCache.delete(CACHE_KEY.ROLE_ROUTERS) |
| | | // 注意,不要清理 LoginForm 登录表单 |
| | | } |
| | | |
| | | export const useSessionCache = (type: CacheType = 'sessionStorage') => { |
| | | const wsSessionCache: WebStorageCache = new WebStorageCache({ |
| | | storage: type |
| | | }) |
| | | |
| | | return { |
| | | wsSessionCache |
| | | } |
| | | } |
| | | |
| | | export const deleteUserSessionCache = () => { |
| | | const { wsSessionCache } = useSessionCache() |
| | | wsSessionCache.delete(CACHE_KEY.ROLE_ROUTERS) |
| | | // 注意,不要清理 用户和 LoginForm 登录表单 |
| | | } |
| | |
| | | cancelButtonText: t('common.cancel'), |
| | | type: 'warning' |
| | | }) |
| | | }, |
| | | // 启用窗体 |
| | | enableConfirm(ids, content?: string, tip?: string) { |
| | | return ElMessageBox.confirm( |
| | | content ? content : t('确定启用选中的'+ ids.length +'项数据?'), |
| | | tip ? tip : t('common.confirmTitle'), |
| | | { |
| | | confirmButtonText: t('common.ok'), |
| | | cancelButtonText: t('common.cancel'), |
| | | type: 'warning' |
| | | } |
| | | ) |
| | | }, |
| | | // 禁用窗体 |
| | | disableConfirm(ids, content?: string, tip?: string) { |
| | | return ElMessageBox.confirm( |
| | | content ? content : t('确定禁用选中的'+ ids.length +'项数据?'), |
| | | tip ? tip : t('common.confirmTitle'), |
| | | { |
| | | confirmButtonText: t('common.ok'), |
| | | cancelButtonText: t('common.cancel'), |
| | | type: 'warning' |
| | | } |
| | | ) |
| | | } |
| | | } |
| | | } |
| | |
| | | <template> |
| | | <section |
| | | :class="[ |
| | | 'p-[var(--app-content-padding)] w-[calc(100%-var(--app-content-padding)-var(--app-content-padding))] bg-[var(--app-content-bg-color)] dark:bg-[var(--el-bg-color)]', |
| | | 'p-[var(--app-content-padding)] w-full bg-[var(--app-content-bg-color)] dark:bg-[var(--el-bg-color)]', |
| | | { |
| | | '!min-h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height))]': |
| | | (fixedHeader && |
| | | (layout === 'classic' || layout === 'topLeft' || layout === 'top') && |
| | | footer) || |
| | | (!tagsView && layout === 'top' && footer), |
| | | '!min-h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height)-var(--tags-view-height))]': |
| | | tagsView && layout === 'top' && footer, |
| | | |
| | | '!min-h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--top-tool-height)-var(--app-footer-height))]': |
| | | !fixedHeader && layout === 'classic' && footer, |
| | | |
| | | '!min-h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height))]': |
| | | !fixedHeader && layout === 'topLeft' && footer, |
| | | |
| | | '!min-h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding))]': |
| | | fixedHeader && layout === 'cutMenu' && footer, |
| | | |
| | | '!min-h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding)-var(--tags-view-height))]': |
| | | !fixedHeader && layout === 'cutMenu' && footer |
| | | '!min-h-[calc(100vh-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))] pb-0': |
| | | footer |
| | | } |
| | | ]" |
| | | > |
| | |
| | | $prefix-cls: #{$elNamespace}-breadcrumb; |
| | | |
| | | .#{$prefix-cls} { |
| | | :deep(&__item) { |
| | | :deep(.#{$prefix-cls}__item) { |
| | | display: flex; |
| | | .#{$prefix-cls}__inner { |
| | | display: flex; |
| | |
| | | } |
| | | } |
| | | |
| | | :deep(&__item):not(:last-child) { |
| | | :deep(.#{$prefix-cls}__item):not(:last-child) { |
| | | .#{$prefix-cls}__inner { |
| | | color: var(--top-header-text-color); |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | :deep(&__item):last-child { |
| | | :deep(.#{$prefix-cls}__item):last-child { |
| | | .#{$prefix-cls}__inner { |
| | | display: flex; |
| | | align-items: center; |
| | |
| | | const appStore = useAppStore() |
| | | |
| | | const title = computed(() => appStore.getTitle) |
| | | |
| | | |
| | | // 添加当前年份计算属性 |
| | | const currentYear = computed(() => new Date().getFullYear()) |
| | | </script> |
| | | |
| | | <template> |
| | | <div |
| | | :class="prefixCls" |
| | | class="h-[var(--app-footer-height)] bg-[var(--app-content-bg-color)] text-center leading-[var(--app-footer-height)] text-[var(--el-text-color-placeholder)] dark:bg-[var(--el-bg-color)]" |
| | | class="h-[var(--app-footer-height)] bg-[var(--app-content-bg-color)] text-center leading-[var(--app-footer-height)] text-[var(--el-text-color-placeholder)] dark:bg-[var(--el-bg-color)] overflow-hidden" |
| | | > |
| | | <span class="text-14px">Copyright ©2024-{{ title }}</span> |
| | | <span class="text-14px">Copyright ©{{ currentYear }} {{ title }}</span> |
| | | </div> |
| | | </template> |
| | |
| | | <script lang="ts" setup> |
| | | import { computed, onMounted, ref, unref, watch } from 'vue' |
| | | import { useAppStore } from '@/store/modules/app' |
| | | import { useUserStoreWithOut } from '@/store/modules/user' |
| | | import { usePermissionStoreWithOut } from '@/store/modules/permission' |
| | | import { useDesign } from '@/hooks/web/useDesign' |
| | | import * as authUtil from "@/utils/auth"; |
| | | import {isRelogin} from "@/config/axios/service"; |
| | | import router from "@/router"; |
| | | import type {RouteRecordRaw} from "vue-router"; |
| | | import {CACHE_KEY, useCache, useSessionCache} from "@/hooks/web/useCache"; |
| | | import {getAccessToken} from "@/utils/auth"; |
| | | import {getInfo} from "@/api/login"; |
| | | const { wsCache } = useCache() |
| | | const { wsSessionCache } = useSessionCache() |
| | | |
| | | defineOptions({ name: 'Logo' }) |
| | | |
| | |
| | | } |
| | | } |
| | | ) |
| | | |
| | | /** 刷新所有菜单权限 */ |
| | | const gotoHome = async () => { |
| | | const permissionStore = usePermissionStoreWithOut() |
| | | isRelogin.show = true |
| | | let userInfo = await getInfo() |
| | | wsCache.set(CACHE_KEY.USER, userInfo) |
| | | wsSessionCache.set(CACHE_KEY.ROLE_ROUTERS, userInfo.menus) |
| | | isRelogin.show = false |
| | | // 后端过滤菜单 |
| | | await permissionStore.generateRoutes() |
| | | permissionStore.getAddRouters.forEach((route) => { |
| | | router.addRoute(route as unknown as RouteRecordRaw) // 动态添加可访问路由表 |
| | | }) |
| | | } |
| | | |
| | | </script> |
| | | |
| | | <template> |
| | |
| | | layout !== 'classic' ? `${prefixCls}__Top` : '', |
| | | 'flex !h-[var(--logo-height)] items-center cursor-pointer pl-8px relative decoration-none overflow-hidden' |
| | | ]" |
| | | @click="gotoHome" |
| | | :to="homePath" |
| | | > |
| | | <img |
| | |
| | | backgroundColor="var(--left-menu-bg-color)" |
| | | textColor="var(--left-menu-text-color)" |
| | | activeTextColor="var(--left-menu-text-active-color)" |
| | | popperClass={ |
| | | unref(menuMode) === 'vertical' |
| | | ? `${prefixCls}-popper--vertical` |
| | | : `${prefixCls}-popper--horizontal` |
| | | } |
| | | onSelect={menuSelect} |
| | | > |
| | | {{ |
| | |
| | | } |
| | | } |
| | | |
| | | // 垂直菜单 |
| | | &__vertical { |
| | | :deep(.#{$elNamespace}-menu--vertical) { |
| | | &:not(.#{$elNamespace}-menu--collapse) .#{$elNamespace}-sub-menu__title, |
| | | .#{$elNamespace}-menu-item { |
| | | padding-right: 0; |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 水平菜单 |
| | | &__horizontal { |
| | | height: calc(var(--top-tool-height)) !important; |
| | |
| | | <script lang="ts" setup> |
| | | import { formatDate } from '@/utils/formatTime' |
| | | import * as NotifyMessageApi from '@/api/system/notify/message' |
| | | import { useUserStoreWithOut } from '@/store/modules/user' |
| | | |
| | | defineOptions({ name: 'Message' }) |
| | | |
| | | const { push } = useRouter() |
| | | const userStore = useUserStoreWithOut() |
| | | const activeName = ref('notice') |
| | | const unreadCount = ref(0) // 未读消息数量 |
| | | const list = ref<any[]>([]) // 消息列表 |
| | |
| | | // 轮询刷新小红点 |
| | | setInterval( |
| | | () => { |
| | | getUnreadCount() |
| | | if (userStore.getIsSetUser) { |
| | | getUnreadCount() |
| | | } else { |
| | | unreadCount.value = 0 |
| | | } |
| | | }, |
| | | 1000 * 60 * 2 |
| | | ) |
| | |
| | | message: ${appStore.getMessage}, |
| | | // 标签页 |
| | | tagsView: ${appStore.getTagsView}, |
| | | // 标签页 |
| | | tagsViewImmerse: ${appStore.getTagsViewImmerse}, |
| | | // 标签页图标 |
| | | getTagsViewIcon: ${appStore.getTagsViewIcon}, |
| | | tagsViewIcon: ${appStore.getTagsViewIcon}, |
| | | // logo |
| | | logo: ${appStore.getLogo}, |
| | | // 菜单手风琴 |
| | |
| | | |
| | | .#{$prefix-cls} { |
| | | border-radius: 6px 0 0 6px; |
| | | z-index: 1200;/*修正没有z-index会被表格层覆盖,值不要超过4000*/ |
| | | } |
| | | </style> |
| | |
| | | id={`${variables.namespace}-menu`} |
| | | class={[ |
| | | prefixCls, |
| | | 'relative bg-[var(--left-menu-bg-color)] top-1px layout-border__right', |
| | | 'relative bg-[var(--left-menu-bg-color)] layout-border__right', |
| | | { |
| | | 'w-[var(--tab-menu-max-width)]': !unref(collapse), |
| | | 'w-[var(--tab-menu-min-width)]': unref(collapse) |
| | |
| | | ]} |
| | | onMouseleave={mouseleave} |
| | | > |
| | | <ElScrollbar class="!h-[calc(100%-var(--tab-menu-collapse-height)-1px)]"> |
| | | <ElScrollbar class="!h-[calc(100%-var(--tab-menu-collapse-height))]"> |
| | | <div> |
| | | {() => { |
| | | return unref(tabRouters).map((v) => { |
| | |
| | | { |
| | | '!left-[var(--tab-menu-min-width)]': unref(collapse), |
| | | '!left-[var(--tab-menu-max-width)]': !unref(collapse), |
| | | '!w-[calc(var(--left-menu-max-width)+1px)]': unref(showMenu) || unref(fixedMenu), |
| | | '!w-[var(--left-menu-max-width)]': unref(showMenu) || unref(fixedMenu), |
| | | '!w-0': !unref(showMenu) && !unref(fixedMenu) |
| | | } |
| | | ]} |
| | |
| | | <script lang="ts" setup> |
| | | import { onMounted, watch, computed, unref, ref, nextTick } from 'vue' |
| | | import { useRouter } from 'vue-router' |
| | | import { computed, nextTick, onMounted, ref, unref, watch } from 'vue' |
| | | import type { RouteLocationNormalizedLoaded, RouterLinkProps } from 'vue-router' |
| | | import { useRouter } from 'vue-router' |
| | | import { usePermissionStore } from '@/store/modules/permission' |
| | | import { useTagsViewStore } from '@/store/modules/tagsView' |
| | | import { useAppStore } from '@/store/modules/app' |
| | |
| | | const affixTagArr = ref<RouteLocationNormalizedLoaded[]>([]) |
| | | |
| | | const appStore = useAppStore() |
| | | |
| | | const tagsViewImmerse = computed(() => appStore.getTagsViewImmerse) |
| | | |
| | | const tagsViewIcon = computed(() => appStore.getTagsViewIcon) |
| | | |
| | |
| | | const moveToCurrentTag = async () => { |
| | | await nextTick() |
| | | for (const v of unref(visitedViews)) { |
| | | if (v.fullPath === unref(currentRoute).path) { |
| | | if (v.fullPath === unref(currentRoute).fullPath) { |
| | | moveToTarget(v) |
| | | if (v.fullPath !== unref(currentRoute).fullPath) { |
| | | tagsViewStore.updateVisitedView(unref(currentRoute)) |
| | | } |
| | | |
| | | break |
| | | } |
| | | } |
| | |
| | | |
| | | // 是否是当前tag |
| | | const isActive = (route: RouteLocationNormalizedLoaded): boolean => { |
| | | return route.path === unref(currentRoute).path |
| | | return route.fullPath === unref(currentRoute).fullPath |
| | | } |
| | | |
| | | // 所有右键菜单组件的元素 |
| | |
| | | class="relative w-full flex bg-[#fff] dark:bg-[var(--el-bg-color)]" |
| | | > |
| | | <span |
| | | :class="`${prefixCls}__tool ${prefixCls}__tool--first`" |
| | | :class="tagsViewImmerse ? '' : `${prefixCls}__tool ${prefixCls}__tool--first`" |
| | | class="h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center" |
| | | @click="move(-200)" |
| | | > |
| | | <Icon |
| | | icon="ep:d-arrow-left" |
| | | color="var(--el-text-color-placeholder)" |
| | | :hover-color="isDark ? '#fff' : 'var(--el-color-black)'" |
| | | color="var(--el-text-color-placeholder)" |
| | | icon="ep:d-arrow-left" |
| | | /> |
| | | </span> |
| | | <div class="flex-1 overflow-hidden"> |
| | | <ElScrollbar ref="scrollbarRef" class="h-full" @scroll="scroll"> |
| | | <div class="h-full flex"> |
| | | <div class="h-[var(--tags-view-height)] flex"> |
| | | <ContextMenu |
| | | v-for="item in visitedViews" |
| | | :key="item.fullPath" |
| | | :ref="itemRefs.set" |
| | | :class="[ |
| | | `${prefixCls}__item`, |
| | | tagsViewImmerse ? `${prefixCls}__item--immerse` : '', |
| | | tagsViewIcon ? `${prefixCls}__item--icon` : '', |
| | | tagsViewImmerse && tagsViewIcon ? `${prefixCls}__item--immerse--icon` : '', |
| | | item?.meta?.affix ? `${prefixCls}__item--affix` : '', |
| | | { |
| | | 'is-active': isActive(item) |
| | | } |
| | | ]" |
| | | :schema="[ |
| | | { |
| | | icon: 'ep:refresh', |
| | |
| | | } |
| | | } |
| | | ]" |
| | | v-for="item in visitedViews" |
| | | :key="item.fullPath" |
| | | :tag-item="item" |
| | | :class="[ |
| | | `${prefixCls}__item`, |
| | | item?.meta?.affix ? `${prefixCls}__item--affix` : '', |
| | | { |
| | | 'is-active': isActive(item) |
| | | } |
| | | ]" |
| | | @visible-change="visibleChange" |
| | | > |
| | | <div> |
| | | <router-link :ref="tagLinksRefs.set" :to="{ ...item }" custom v-slot="{ navigate }"> |
| | | <router-link :ref="tagLinksRefs.set" v-slot="{ navigate }" :to="{ ...item }" custom> |
| | | <div |
| | | :class="`h-full flex items-center justify-center whitespace-nowrap pl-15px ${prefixCls}__item--label`" |
| | | @click="navigate" |
| | | class="h-full flex items-center justify-center whitespace-nowrap pl-15px" |
| | | > |
| | | <Icon |
| | | v-if=" |
| | | item?.matched && |
| | | item?.matched[1] && |
| | | item?.matched[1]?.meta?.icon && |
| | | tagsViewIcon |
| | | tagsViewIcon && |
| | | (item?.meta?.icon || |
| | | (item?.matched && |
| | | item.matched[0] && |
| | | item.matched[item.matched.length - 1].meta?.icon)) |
| | | " |
| | | :icon="item?.matched[1]?.meta?.icon" |
| | | :icon="item?.meta?.icon || item.matched[item.matched.length - 1].meta.icon" |
| | | :size="12" |
| | | class="mr-5px" |
| | | /> |
| | | {{ t(item?.meta?.title as string) }} |
| | | {{ |
| | | t(item?.meta?.title as string) + |
| | | (item?.meta?.titleSuffix ? ` (${item?.meta?.titleSuffix})` : '') |
| | | }} |
| | | <Icon |
| | | :class="`${prefixCls}__item--close`" |
| | | :size="12" |
| | | color="#333" |
| | | icon="ep:close" |
| | | :size="12" |
| | | @click.prevent.stop="closeSelectedTag(item)" |
| | | /> |
| | | </div> |
| | |
| | | </ElScrollbar> |
| | | </div> |
| | | <span |
| | | :class="`${prefixCls}__tool`" |
| | | :class="tagsViewImmerse ? '' : `${prefixCls}__tool`" |
| | | class="h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center" |
| | | @click="move(200)" |
| | | > |
| | | <Icon |
| | | icon="ep:d-arrow-right" |
| | | color="var(--el-text-color-placeholder)" |
| | | :hover-color="isDark ? '#fff' : 'var(--el-color-black)'" |
| | | color="var(--el-text-color-placeholder)" |
| | | icon="ep:d-arrow-right" |
| | | /> |
| | | </span> |
| | | <span |
| | | :class="`${prefixCls}__tool`" |
| | | :class="tagsViewImmerse ? '' : `${prefixCls}__tool`" |
| | | class="h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center" |
| | | @click="refreshSelectedTag(selectedTag)" |
| | | > |
| | | <Icon |
| | | icon="ep:refresh-right" |
| | | color="var(--el-text-color-placeholder)" |
| | | :hover-color="isDark ? '#fff' : 'var(--el-color-black)'" |
| | | color="var(--el-text-color-placeholder)" |
| | | icon="ep:refresh-right" |
| | | /> |
| | | </span> |
| | | <ContextMenu |
| | | trigger="click" |
| | | :schema="[ |
| | | { |
| | | icon: 'ep:refresh', |
| | |
| | | } |
| | | } |
| | | ]" |
| | | trigger="click" |
| | | > |
| | | <span |
| | | :class="`${prefixCls}__tool`" |
| | | :class="tagsViewImmerse ? '' : `${prefixCls}__tool`" |
| | | class="block h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center" |
| | | > |
| | | <Icon |
| | | icon="ep:menu" |
| | | color="var(--el-text-color-placeholder)" |
| | | :hover-color="isDark ? '#fff' : 'var(--el-color-black)'" |
| | | color="var(--el-text-color-placeholder)" |
| | | icon="ep:menu" |
| | | /> |
| | | </span> |
| | | </ContextMenu> |
| | |
| | | |
| | | &::before { |
| | | position: absolute; |
| | | top: 1px; |
| | | top: 0; |
| | | left: 0; |
| | | width: 100%; |
| | | height: calc(100% - 1px); |
| | | height: 100%; |
| | | border-left: 1px solid var(--el-border-color); |
| | | content: ''; |
| | | } |
| | |
| | | &--first { |
| | | &::before { |
| | | position: absolute; |
| | | top: 1px; |
| | | top: 0; |
| | | left: 0; |
| | | width: 100%; |
| | | height: calc(100% - 1px); |
| | | height: 100%; |
| | | border-right: 1px solid var(--el-border-color); |
| | | border-left: none; |
| | | content: ''; |
| | |
| | | |
| | | &__item { |
| | | position: relative; |
| | | top: 2px; |
| | | top: 3px; |
| | | height: calc(100% - 6px); |
| | | padding-right: 25px; |
| | | padding-right: 15px; |
| | | margin-left: 4px; |
| | | font-size: 12px; |
| | | cursor: pointer; |
| | | border: 1px solid #d9d9d9; |
| | | border-radius: 2px; |
| | | box-sizing: border-box; |
| | | |
| | | &--close { |
| | | position: absolute; |
| | |
| | | display: none; |
| | | transform: translate(0, -50%); |
| | | } |
| | | |
| | | &:not(.#{$prefix-cls}__item--affix):hover { |
| | | .#{$prefix-cls}__item--close { |
| | | display: block; |
| | | } |
| | | } |
| | | } |
| | | |
| | | &__item--icon { |
| | | padding-right: 20px; |
| | | } |
| | | |
| | | &__item:not(.is-active) { |
| | |
| | | color: var(--el-color-white); |
| | | background-color: var(--el-color-primary); |
| | | border: 1px solid var(--el-color-primary); |
| | | |
| | | .#{$prefix-cls}__item--close { |
| | | :deep(span) { |
| | | color: var(--el-color-white) !important; |
| | | } |
| | | } |
| | | } |
| | | |
| | | &__item--immerse { |
| | | top: 2px; |
| | | height: calc(100% - 3px); |
| | | padding-right: 35px; |
| | | margin: 0 -10px; |
| | | border: none !important; |
| | | -webkit-mask-box-image: url("data:image/svg+xml,%3Csvg width='68' height='34' viewBox='0 0 68 34' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='m27,0c-7.99582,0 -11.95105,0.00205 -12,12l0,6c0,8.284 -0.48549,16.49691 -8.76949,16.49691l54.37857,-0.11145c-8.284,0 -8.60908,-8.10146 -8.60908,-16.38546l0,-6c0.11145,-12.08445 -4.38441,-12 -12,-12l-13,0z' fill='%23409eff'/%3E%3C/svg%3E") |
| | | 12 27 15; |
| | | |
| | | .#{$prefix-cls}__item--label { |
| | | padding-left: 35px; |
| | | } |
| | | |
| | | .#{$prefix-cls}__item--close { |
| | | right: 20px; |
| | | } |
| | | } |
| | | |
| | | &__item--immerse--icon { |
| | | padding-right: 35px; |
| | | } |
| | | |
| | | &__item--immerse:not(.is-active) { |
| | | &:hover { |
| | | color: var(--el-color-white); |
| | | background-color: var(--el-color-primary); |
| | | |
| | | .#{$prefix-cls}__item--close { |
| | | :deep(span) { |
| | | color: var(--el-color-white) !important; |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | color: var(--el-color-white); |
| | | background-color: var(--el-color-primary); |
| | | border: 1px solid var(--el-color-primary); |
| | | |
| | | .#{$prefix-cls}__item--close { |
| | | :deep(span) { |
| | | color: var(--el-color-white) !important; |
| | | } |
| | | } |
| | | } |
| | | |
| | | &__item--immerse:not(.is-active) { |
| | | &:hover { |
| | | color: var(--el-color-white); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | </style> |
| | |
| | | |
| | | const prefixCls = getPrefixCls('user-info') |
| | | |
| | | const avatar = computed(() => userStore.user.avatar ?? avatarImg) |
| | | const avatar = computed(() => userStore.user.avatar || avatarImg) |
| | | const userName = computed(() => userStore.user.nickname ?? 'Admin') |
| | | |
| | | // 锁定屏幕 |
| | |
| | | }) |
| | | await userStore.loginOut() |
| | | tagsViewStore.delAllViews() |
| | | replace('/login?redirect=/index') |
| | | await replace('/login?redirect=/index') |
| | | } catch {} |
| | | } |
| | | const toProfile = async () => { |
| | | push('/user/profile') |
| | | } |
| | | const toDocument = () => { |
| | | window.open('https://xxxx/') |
| | | window.open('https://doc.iailab.cn/') |
| | | } |
| | | </script> |
| | | |
| | |
| | | }) |
| | | |
| | | const userStore = useUserStore() |
| | | const avatar = computed(() => userStore.user.avatar ?? avatarImg) |
| | | const avatar = computed(() => userStore.user.avatar || avatarImg) |
| | | const userName = computed(() => userStore.user.nickname ?? 'Admin') |
| | | |
| | | const emit = defineEmits(['update:modelValue']) |
| | |
| | | const { getPrefixCls } = useDesign() |
| | | const prefixCls = getPrefixCls('lock-page') |
| | | |
| | | const avatar = computed(() => userStore.user.avatar ?? avatarImg) |
| | | const avatar = computed(() => userStore.user.avatar || avatarImg) |
| | | const userName = computed(() => userStore.user.nickname ?? 'Admin') |
| | | |
| | | const lockStore = useLockStore() |
| | |
| | | |
| | | <ToolHeader class="flex-1"></ToolHeader> |
| | | </div> |
| | | <div class="absolute left-0 top-[var(--logo-height)+1px] h-[calc(100%-1px-var(--logo-height))] w-full flex"> |
| | | <div class="absolute left-0 top-[var(--logo-height)] h-[calc(100%-var(--logo-height))] w-full flex"> |
| | | <Menu class="relative layout-border__right !h-full"></Menu> |
| | | <div |
| | | class={[ |
| | |
| | | 'layout-border__bottom absolute', |
| | | { |
| | | '!fixed top-0 left-0 z-10': fixedHeader.value, |
| | | 'w-[calc(100%-var(--left-menu-min-width))] !left-[var(--left-menu-min-width)] mt-[calc(var(--logo-height)+1px)]': |
| | | 'w-[calc(100%-var(--left-menu-min-width))] !left-[var(--left-menu-min-width)] mt-[var(--logo-height)]': |
| | | collapse.value && fixedHeader.value, |
| | | 'w-[calc(100%-var(--left-menu-max-width))] !left-[var(--left-menu-max-width)] mt-[calc(var(--logo-height)+1px)]': |
| | | 'w-[calc(100%-var(--left-menu-max-width))] !left-[var(--left-menu-max-width)] mt-[var(--logo-height)]': |
| | | !collapse.value && fixedHeader.value |
| | | } |
| | | ]} |
| | |
| | | <Menu class="h-[var(--top-tool-height)] flex-1 px-10px"></Menu> |
| | | <ToolHeader></ToolHeader> |
| | | </div> |
| | | <div |
| | | class={[ |
| | | `${prefixCls}-content`, |
| | | 'w-full', |
| | | { |
| | | 'h-[calc(100%-var(--app-footer-height))]': !fixedHeader.value, |
| | | 'h-[calc(100%-var(--tags-view-height)-var(--app-footer-height))]': fixedHeader.value |
| | | } |
| | | ]} |
| | | > |
| | | <div class={[`${prefixCls}-content`, 'w-full h-[calc(100%-var(--top-tool-height))]']}> |
| | | <ElScrollbar |
| | | v-loading={pageLoading.value} |
| | | class={[ |
| | | `${prefixCls}-content-scrollbar`, |
| | | { |
| | | 'mt-[var(--tags-view-height)] !pb-[calc(var(--tags-view-height)+var(--app-footer-height))]': |
| | | fixedHeader.value, |
| | | 'pb-[var(--app-footer-height)]': !fixedHeader.value |
| | | '!h-[calc(100%-var(--tags-view-height))] mt-[calc(var(--tags-view-height))]': |
| | | fixedHeader.value |
| | | } |
| | | ]} |
| | | > |
| | |
| | | class={[ |
| | | 'layout-border__bottom layout-border__top relative', |
| | | { |
| | | '!fixed w-full top-[calc(var(--top-tool-height)+1px)] left-0': fixedHeader.value |
| | | '!fixed w-full top-[var(--top-tool-height)] left-0': fixedHeader.value |
| | | } |
| | | ]} |
| | | style="transition: width var(--transition-time-02), left var(--transition-time-02);" |
| | |
| | | |
| | | <ToolHeader class="flex-1"></ToolHeader> |
| | | </div> |
| | | <div class="absolute left-0 top-[var(--logo-height)] h-[calc(100%-var(--logo-height))] w-[calc(100%-2px)] flex"> |
| | | <div class="absolute left-0 top-[var(--logo-height)] h-[calc(100%-var(--logo-height))] w-full flex"> |
| | | <TabMenu></TabMenu> |
| | | <div |
| | | class={[ |
| | |
| | | {tagsView.value ? ( |
| | | <TagsView |
| | | class={[ |
| | | 'relative layout-border__bottom layout-border__top', |
| | | 'relative layout-border__bottom', |
| | | { |
| | | '!fixed top-0 left-0 z-10': fixedHeader.value, |
| | | 'w-[calc(100%-var(--tab-menu-min-width))] !left-[var(--tab-menu-min-width)] mt-[var(--logo-height)]': |
| | | collapse.value && fixedHeader.value, |
| | | collapse.value && fixedHeader.value && !fixedMenu.value, |
| | | 'w-[calc(100%-var(--tab-menu-max-width))] !left-[var(--tab-menu-max-width)] mt-[var(--logo-height)]': |
| | | !collapse.value && fixedHeader.value, |
| | | '!fixed top-0 !left-[var(--tab-menu-min-width)+var(--left-menu-max-width)] z-10': |
| | | fixedHeader.value && fixedMenu.value, |
| | | 'w-[calc(100%-var(--tab-menu-min-width)-var(--left-menu-max-width))] !left-[var(--tab-menu-min-width)+var(--left-menu-max-width)] mt-[var(--logo-height)]': |
| | | !collapse.value && fixedHeader.value && !fixedMenu.value, |
| | | 'w-[calc(100%-var(--tab-menu-min-width)-var(--left-menu-max-width))] !left-[calc(var(--tab-menu-min-width)+var(--left-menu-max-width))] mt-[var(--logo-height)]': |
| | | collapse.value && fixedHeader.value && fixedMenu.value, |
| | | 'w-[calc(100%-var(--tab-menu-max-width)-var(--left-menu-max-width))] !left-[var(--tab-menu-max-width)+var(--left-menu-max-width)] mt-[var(--logo-height)]': |
| | | 'w-[calc(100%-var(--tab-menu-max-width)-var(--left-menu-max-width))] !left-[calc(var(--tab-menu-max-width)+var(--left-menu-max-width))] mt-[var(--logo-height)]': |
| | | !collapse.value && fixedHeader.value && fixedMenu.value |
| | | } |
| | | ]} |
| | |
| | | // 导入全局的svg图标 |
| | | import '@/plugins/svgIcon' |
| | | |
| | | import Iconify from '@iconify/iconify' |
| | | import epJson from '@iconify/json/json/ep.json' |
| | | import faJson from '@iconify/json/json/fa.json' |
| | | import faSolidJson from '@iconify/json/json/fa-solid.json' |
| | | |
| | | Iconify.addCollection(epJson) |
| | | Iconify.addCollection(faJson) |
| | | Iconify.addCollection(faSolidJson) |
| | | |
| | | export * from '@iconify/iconify' |
| | | |
| | | export default Iconify |
| | | |
| | | // 初始化多语言 |
| | | import { setupI18n } from '@/plugins/vueI18n' |
| | | |
| | |
| | | import router, { setupRouter } from '@/router' |
| | | |
| | | // 权限 |
| | | import { setupAuth } from '@/directives' |
| | | import { setupAuth, setupMountedFocus } from '@/directives' |
| | | |
| | | import { createApp } from 'vue' |
| | | |
| | |
| | | |
| | | setupAuth(app) |
| | | |
| | | setupMountedFocus(app) |
| | | |
| | | await router.isReady() |
| | | |
| | | app.use(VueDOMPurifyHTML) |
| | |
| | | import type { App } from 'vue' |
| | | // 👇使用 form-create 需额外全局引入 element plus 组件 |
| | | import { |
| | | // ElAutocomplete, |
| | | // ElButton, |
| | | // ElCascader, |
| | | // ElCheckbox, |
| | | // ElCheckboxButton, |
| | | // ElCheckboxGroup, |
| | | // ElCol, |
| | | // ElColorPicker, |
| | | // ElDatePicker, |
| | | // ElDialog, |
| | | // ElForm, |
| | | // ElInput, |
| | | // ElInputNumber, |
| | | // ElPopover, |
| | | // ElRadio, |
| | | // ElRadioButton, |
| | | // ElRadioGroup, |
| | | // ElRate, |
| | | // ElRow, |
| | | // ElSelect, |
| | | // ElSlider, |
| | | // ElSwitch, |
| | | // ElTimePicker, |
| | | // ElTooltip, |
| | | // ElTree, |
| | | // ElUpload, |
| | | // ElIcon, |
| | | // ElProgress, |
| | | // 以上会由 @form-create/element-ui/auto-import 自动引入 |
| | | ElAlert, |
| | | ElTransfer, |
| | | ElAside, |
| | | ElContainer, |
| | | ElDivider, |
| | |
| | | ElTableColumn, |
| | | ElTabPane, |
| | | ElTabs, |
| | | ElTransfer |
| | | ElDropdown, |
| | | ElDropdownMenu, |
| | | ElDropdownItem, |
| | | ElBadge, |
| | | ElTag, |
| | | ElText, |
| | | ElMenu, |
| | | ElMenuItem, |
| | | ElFooter, |
| | | ElMessage, |
| | | ElCollapse, |
| | | ElCollapseItem, |
| | | ElCard, |
| | | // ElFormItem, |
| | | // ElOption |
| | | } from 'element-plus' |
| | | import FcDesigner from '@form-create/designer' |
| | | import formCreate from '@form-create/element-ui' |
| | |
| | | }) |
| | | |
| | | const components = [ |
| | | ElAlert, |
| | | ElTransfer, |
| | | ElAside, |
| | | ElPopconfirm, |
| | | ElHeader, |
| | | ElMain, |
| | | ElContainer, |
| | | ElDivider, |
| | | ElTransfer, |
| | | ElAlert, |
| | | ElTabs, |
| | | ElHeader, |
| | | ElMain, |
| | | ElPopconfirm, |
| | | ElTable, |
| | | ElTableColumn, |
| | | ElTabPane, |
| | | ElTabs, |
| | | ElDropdown, |
| | | ElDropdownMenu, |
| | | ElDropdownItem, |
| | | ElBadge, |
| | | ElTag, |
| | | ElText, |
| | | ElMenu, |
| | | ElMenuItem, |
| | | ElFooter, |
| | | ElMessage, |
| | | // ElFormItem, |
| | | // ElOption, |
| | | UploadImg, |
| | | UploadImgs, |
| | | UploadFile, |
| | |
| | | UserSelect, |
| | | DeptSelect, |
| | | ApiSelect, |
| | | Editor |
| | | Editor, |
| | | ElCollapse, |
| | | ElCollapseItem, |
| | | ElCard, |
| | | ] |
| | | |
| | | // 参考 http://www.form-create.com/v3/element-ui/auto-import.html 文档 |
| | |
| | | |
| | | // 创建路由实例 |
| | | const router = createRouter({ |
| | | history: createWebHistory('/plat'), // createWebHashHistory URL带#,createWebHistory URL不带# |
| | | history: createWebHistory(import.meta.env.VITE_BASE_PATH), // createWebHashHistory URL带#,createWebHistory URL不带# |
| | | strict: true, |
| | | routes: remainingRouter as RouteRecordRaw[], |
| | | scrollBehavior: () => ({ left: 0, top: 0 }) |
| | |
| | | path: '/', |
| | | component: Layout, |
| | | name: 'Home', |
| | | redirect: '/index', |
| | | meta: { |
| | | hidden: true, |
| | | noTagsView: true |
| | |
| | | } |
| | | }, |
| | | { |
| | | path: 'manager/simple/model', |
| | | component: () => import('@/views/bpm/simple/SimpleModelDesign.vue'), |
| | | name: 'SimpleModelDesign', |
| | | meta: { |
| | | noCache: true, |
| | | hidden: true, |
| | | canTo: true, |
| | | title: '仿钉钉设计流程', |
| | | activeMenu: '/bpm/manager/model' |
| | | } |
| | | }, |
| | | { |
| | | path: 'manager/definition', |
| | | component: () => import('@/views/bpm/definition/index.vue'), |
| | | name: 'BpmProcessDefinition', |
| | |
| | | canTo: true, |
| | | title: '流程详情', |
| | | activeMenu: '/bpm/task/my' |
| | | } |
| | | }, |
| | | props: (route) => ({ |
| | | id: route.query.id, |
| | | taskId: route.query.taskId, |
| | | activityId: route.query.activityId |
| | | }) |
| | | }, |
| | | { |
| | | path: 'oa/leave/create', |
| | |
| | | title: '查看 OA 请假', |
| | | activeMenu: '/bpm/oa/leave' |
| | | } |
| | | }, |
| | | { |
| | | path: 'manager/model/create', |
| | | component: () => import('@/views/bpm/model/form/index.vue'), |
| | | name: 'BpmModelCreate', |
| | | meta: { |
| | | noCache: true, |
| | | hidden: true, |
| | | canTo: true, |
| | | title: '创建流程', |
| | | activeMenu: '/bpm/manager/model' |
| | | } |
| | | }, |
| | | { |
| | | path: 'manager/model/update/:id', |
| | | component: () => import('@/views/bpm/model/form/index.vue'), |
| | | name: 'BpmModelUpdate', |
| | | meta: { |
| | | noCache: true, |
| | | hidden: true, |
| | | canTo: true, |
| | | title: '修改流程', |
| | | activeMenu: '/bpm/manager/model' |
| | | } |
| | | } |
| | | ] |
| | | }, |
| | |
| | | import { defineStore } from 'pinia' |
| | | import { store } from '../index' |
| | | import { setCssVar, humpToUnderline } from '@/utils' |
| | | import { humpToUnderline, setCssVar } from '@/utils' |
| | | import { ElMessage } from 'element-plus' |
| | | import { CACHE_KEY, useCache } from '@/hooks/web/useCache' |
| | | import { ElementPlusSize } from '@/types/elementPlus' |
| | |
| | | locale: boolean |
| | | message: boolean |
| | | tagsView: boolean |
| | | tagsViewImmerse: boolean |
| | | tagsViewIcon: boolean |
| | | logo: boolean |
| | | fixedHeader: boolean |
| | |
| | | locale: true, // 多语言图标 |
| | | message: true, // 消息图标 |
| | | tagsView: true, // 标签页 |
| | | tagsViewImmerse: false, // 标签页沉浸 |
| | | tagsViewIcon: true, // 是否显示标签图标 |
| | | logo: true, // logo |
| | | fixedHeader: true, // 固定toolheader |
| | |
| | | }, |
| | | getTagsView(): boolean { |
| | | return this.tagsView |
| | | }, |
| | | getTagsViewImmerse(): boolean { |
| | | return this.tagsViewImmerse |
| | | }, |
| | | getTagsViewIcon(): boolean { |
| | | return this.tagsViewIcon |
| | |
| | | setTagsView(tagsView: boolean) { |
| | | this.tagsView = tagsView |
| | | }, |
| | | setTagsViewImmerse(tagsViewImmerse: boolean) { |
| | | this.tagsViewImmerse = tagsViewImmerse |
| | | }, |
| | | setTagsViewIcon(tagsViewIcon: boolean) { |
| | | this.tagsViewIcon = tagsViewIcon |
| | | }, |
对比新文件 |
| | |
| | | import { store } from '../../index' |
| | | import { defineStore } from 'pinia' |
| | | |
| | | export const useWorkFlowStore = defineStore('simpleWorkflow', { |
| | | state: () => ({ |
| | | tableId: '', |
| | | isTried: false, |
| | | promoterDrawer: false, |
| | | approverDrawer: false, |
| | | approverConfig1: {}, |
| | | copyerDrawer: false, |
| | | copyerConfig: {}, |
| | | conditionDrawer: false, |
| | | conditionsConfig1: { |
| | | conditionNodes: [] |
| | | }, |
| | | userTaskConfig: {} |
| | | }), |
| | | actions: { |
| | | setTableId(payload) { |
| | | this.tableId = payload |
| | | }, |
| | | setIsTried(payload) { |
| | | this.isTried = payload |
| | | }, |
| | | setPromoter(payload) { |
| | | this.promoterDrawer = payload |
| | | }, |
| | | setApproverDrawer(payload) { |
| | | this.approverDrawer = payload |
| | | }, |
| | | setApproverConfig(payload) { |
| | | this.approverConfig1 = payload |
| | | }, |
| | | setCopyerDrawer(payload) { |
| | | this.copyerDrawer = payload |
| | | }, |
| | | setCopyerConfig(payload) { |
| | | this.copyerConfig = payload |
| | | }, |
| | | setCondition(payload) { |
| | | this.conditionDrawer = payload |
| | | }, |
| | | setConditionsConfig(payload) { |
| | | this.conditionsConfig1 = payload |
| | | }, |
| | | setUserTaskConfig(payload) { |
| | | this.userTaskConfig = payload |
| | | } |
| | | } |
| | | }) |
| | | |
| | | export const useWorkFlowStoreWithOut = () => { |
| | | return useWorkFlowStore(store) |
| | | } |
| | |
| | | import { cloneDeep } from 'lodash-es' |
| | | import remainingRouter from '@/router/modules/remaining' |
| | | import { flatMultiLevelRoutes, generateRoute } from '@/utils/routerHelper' |
| | | import { CACHE_KEY, useCache } from '@/hooks/web/useCache' |
| | | import {CACHE_KEY, useSessionCache} from '@/hooks/web/useCache' |
| | | |
| | | const { wsCache } = useCache() |
| | | const { wsSessionCache } = useSessionCache() |
| | | |
| | | export interface PermissionState { |
| | | routers: AppRouteRecordRaw[] |
| | |
| | | return new Promise<void>(async (resolve) => { |
| | | // 获得菜单列表,它在登录的时候,setUserInfoAction 方法中已经进行获取 |
| | | let res: AppCustomRouteRecordRaw[] = [] |
| | | if (wsCache.get(CACHE_KEY.ROLE_ROUTERS)) { |
| | | res = wsCache.get(CACHE_KEY.ROLE_ROUTERS) as AppCustomRouteRecordRaw[] |
| | | const roleRouters = wsSessionCache.get(CACHE_KEY.ROLE_ROUTERS) |
| | | if (roleRouters) { |
| | | res = roleRouters as AppCustomRouteRecordRaw[] |
| | | } |
| | | const routerMap: AppRouteRecordRaw[] = generateRoute(res) |
| | | // 动态路由,404一定要放到最后面 |
| | | // preschooler:vue-router@4以后已支持静态404路由,此处可不再追加 |
| | | this.addRouters = routerMap.concat([ |
| | | { |
| | | path: '/:path(.*)*', |
| | | redirect: '/404', |
| | | component: () => import('@/views/Error/404.vue'), |
| | | name: '404Page', |
| | | meta: { |
| | | hidden: true, |
| | |
| | | }, |
| | | // 新增tag |
| | | addVisitedView(view: RouteLocationNormalizedLoaded) { |
| | | if (this.visitedViews.some((v) => v.path === view.path)) return |
| | | if (this.visitedViews.some((v) => v.fullPath === view.fullPath)) return |
| | | if (view.meta?.noTagsView) return |
| | | this.visitedViews.push( |
| | | Object.assign({}, view, { |
| | | title: view.meta?.title || 'no-name' |
| | | const visitedView = Object.assign({}, view, { title: view.meta?.title || 'no-name' }) |
| | | |
| | | if (visitedView.meta) { |
| | | const titleSuffixList: string[] = [] |
| | | this.visitedViews.forEach((v) => { |
| | | if (v.path === visitedView.path && v.meta?.title === visitedView.meta?.title) { |
| | | titleSuffixList.push(v.meta?.titleSuffix || '1') |
| | | } |
| | | }) |
| | | ) |
| | | if (titleSuffixList.length) { |
| | | let titleSuffix = 1 |
| | | while (titleSuffixList.includes(`${titleSuffix}`)) { |
| | | titleSuffix += 1 |
| | | } |
| | | visitedView.meta.titleSuffix = titleSuffix === 1 ? undefined : `${titleSuffix}` |
| | | } |
| | | } |
| | | |
| | | this.visitedViews.push(visitedView) |
| | | }, |
| | | // 新增缓存 |
| | | addCachedView() { |
| | |
| | | // 删除tag |
| | | delVisitedView(view: RouteLocationNormalizedLoaded) { |
| | | for (const [i, v] of this.visitedViews.entries()) { |
| | | if (v.path === view.path) { |
| | | if (v.fullPath === view.fullPath) { |
| | | this.visitedViews.splice(i, 1) |
| | | break |
| | | } |
| | |
| | | // 删除其他tag |
| | | delOthersVisitedViews(view: RouteLocationNormalizedLoaded) { |
| | | this.visitedViews = this.visitedViews.filter((v) => { |
| | | return v?.meta?.affix || v.path === view.path |
| | | return v?.meta?.affix || v.fullPath === view.fullPath |
| | | }) |
| | | }, |
| | | // 删除左侧 |
| | | delLeftViews(view: RouteLocationNormalizedLoaded) { |
| | | const index = findIndex<RouteLocationNormalizedLoaded>( |
| | | this.visitedViews, |
| | | (v) => v.path === view.path |
| | | (v) => v.fullPath === view.fullPath |
| | | ) |
| | | if (index > -1) { |
| | | this.visitedViews = this.visitedViews.filter((v, i) => { |
| | | return v?.meta?.affix || v.path === view.path || i > index |
| | | return v?.meta?.affix || v.fullPath === view.fullPath || i > index |
| | | }) |
| | | this.addCachedView() |
| | | } |
| | |
| | | delRightViews(view: RouteLocationNormalizedLoaded) { |
| | | const index = findIndex<RouteLocationNormalizedLoaded>( |
| | | this.visitedViews, |
| | | (v) => v.path === view.path |
| | | (v) => v.fullPath === view.fullPath |
| | | ) |
| | | if (index > -1) { |
| | | this.visitedViews = this.visitedViews.filter((v, i) => { |
| | | return v?.meta?.affix || v.path === view.path || i < index |
| | | return v?.meta?.affix || v.fullPath === view.fullPath || i < index |
| | | }) |
| | | this.addCachedView() |
| | | } |
| | | }, |
| | | updateVisitedView(view: RouteLocationNormalizedLoaded) { |
| | | for (let v of this.visitedViews) { |
| | | if (v.path === view.path) { |
| | | if (v.fullPath === view.fullPath) { |
| | | v = Object.assign(v, view) |
| | | break |
| | | } |
| | |
| | | import { store } from '@/store' |
| | | import { defineStore } from 'pinia' |
| | | import { getAccessToken, removeToken } from '@/utils/auth' |
| | | import { CACHE_KEY, useCache, deleteUserCache } from '@/hooks/web/useCache' |
| | | import { |
| | | CACHE_KEY, |
| | | useCache, |
| | | deleteUserCache, |
| | | useSessionCache, |
| | | deleteUserSessionCache |
| | | } from '@/hooks/web/useCache' |
| | | import { getInfo, loginOut } from '@/api/login' |
| | | |
| | | const { wsCache } = useCache() |
| | | const { wsSessionCache } = useSessionCache() |
| | | |
| | | interface UserVO { |
| | | id: number |
| | |
| | | this.user = userInfo.user |
| | | this.isSetUser = true |
| | | wsCache.set(CACHE_KEY.USER, userInfo) |
| | | wsCache.set(CACHE_KEY.ROLE_ROUTERS, userInfo.menus) |
| | | if(!wsSessionCache.get(CACHE_KEY.ROLE_ROUTERS)) { |
| | | wsSessionCache.set(CACHE_KEY.ROLE_ROUTERS, userInfo.menus) |
| | | } |
| | | }, |
| | | async setUserAvatarAction(avatar: string) { |
| | | const userInfo = wsCache.get(CACHE_KEY.USER) |
| | |
| | | await loginOut() |
| | | removeToken() |
| | | deleteUserCache() // 删除用户缓存 |
| | | deleteUserSessionCache() //删除路由缓存 |
| | | this.resetState() |
| | | }, |
| | | resetState() { |
| | |
| | | @import './variables.scss'; |
| | | @use './variables.scss' as *; |
| | | // 导出变量 |
| | | :export { |
| | | namespace: $namespace; |
| | |
| | | @import './var.css'; |
| | | @import './FormCreate/index.scss'; |
| | | @import 'element-plus/theme-chalk/dark/css-vars.css'; |
| | | @use './var.css'; |
| | | @use './FormCreate/index.scss'; |
| | | @use './theme.scss'; |
| | | @use 'element-plus/theme-chalk/dark/css-vars.css'; |
| | | |
| | | .reset-margin [class*='el-icon'] + span { |
| | | margin-left: 2px !important; |
| | |
| | | -webkit-font-smoothing: antialiased; |
| | | -moz-osx-font-smoothing: grayscale; |
| | | } |
| | | |
| | | *, |
| | | :after, |
| | | :before { |
| | | margin: 0; |
| | | padding: 0; |
| | | box-sizing: border-box; |
| | | } |
| | |
| | | TRUE: true, // 启用 |
| | | FALSE: false // 禁用 |
| | | } |
| | | |
| | | // ========== BPM 模块 ========== |
| | | |
| | | export const BpmModelType = { |
| | | BPMN: 10, // BPMN 设计器 |
| | | SIMPLE: 20 // 简易设计器 |
| | | } |
| | | |
| | | export const BpmModelFormType = { |
| | | NORMAL: 10, // 流程表单 |
| | | CUSTOM: 20 // 业务表单 |
| | | } |
| | | |
| | | export const BpmProcessInstanceStatus = { |
| | | NOT_START: -1, // 未开始 |
| | | RUNNING: 1, // 审批中 |
| | | APPROVE: 2, // 审批通过 |
| | | REJECT: 3, // 审批不通过 |
| | | CANCEL: 4 // 已取消 |
| | | } |
| | |
| | | INFRA_OPERATE_TYPE = 'infra_operate_type', |
| | | |
| | | // ========== BPM 模块 ========== |
| | | BPM_MODEL_TYPE = 'bpm_model_type', |
| | | BPM_MODEL_FORM_TYPE = 'bpm_model_form_type', |
| | | BPM_TASK_CANDIDATE_STRATEGY = 'bpm_task_candidate_strategy', |
| | | BPM_PROCESS_INSTANCE_STATUS = 'bpm_process_instance_status', |
| | |
| | | MODEL_METHOD_SETTING_VALUE_TYPE = 'model_method_setting_value_type', |
| | | PRED_GRANULARITY = 'pred_granularity', |
| | | ITEM_RUN_STATUS = 'item_run_status', |
| | | |
| | | RESULT_TYPE = 'result_type', |
| | | // ========== DATA - 数据平台模块 ========== |
| | | DATA_FIELD_TYPE = 'data_field_type', |
| | | TAG_DATA_TYPE = 'tag_data_type', |
| | |
| | | CAMERA_BRAND = 'camera_brand', |
| | | CAPTURE_TYPE = 'capture_type', |
| | | MODEL_RESULT_TYPE = 'model_result_type', |
| | | DATA_QUALITY = 'data_quality' |
| | | } |
| | |
| | | value?: object |
| | | ) => { |
| | | if (isRef(detailPreview)) { |
| | | // @ts-ignore |
| | | detailPreview = detailPreview.value |
| | | } |
| | | // @ts-ignore |
| | |
| | | const { wsCache } = useCache() |
| | | const permissionDatas = value |
| | | const all_permission = '*:*:*' |
| | | const permissions = wsCache.get(CACHE_KEY.USER).permissions |
| | | const userInfo = wsCache.get(CACHE_KEY.USER) |
| | | const permissions = userInfo?.permissions || [] |
| | | const hasPermission = permissions.some((permission) => { |
| | | return all_permission === permission || permissionDatas.includes(permission) |
| | | }) |
| | |
| | | const { wsCache } = useCache() |
| | | const permissionRoles = value |
| | | const super_admin = 'admin' |
| | | const roles = wsCache.get(CACHE_KEY.USER).roles |
| | | const userInfo = wsCache.get(CACHE_KEY.USER) |
| | | const roles = userInfo?.roles || [] |
| | | const hasRole = roles.some((role) => { |
| | | return super_admin === role || permissionRoles.includes(role) |
| | | }) |
| | |
| | | /* Layout */ |
| | | export const Layout = () => import('@/layout/Layout.vue') |
| | | |
| | | |
| | | export const getParentLayout = () => { |
| | | return () => |
| | | new Promise((resolve) => { |
| | |
| | | noCache: !route.keepAlive, |
| | | alwaysShow: |
| | | route.children && |
| | | route.children.length === 1 && |
| | | route.children.length > 0 && |
| | | (route.alwaysShow !== undefined ? route.alwaysShow : true) |
| | | } as any |
| | | // 特殊逻辑:如果后端配置的 MenuDO.component 包含 ?,则表示需要传递参数 |
| | |
| | | // 2. 生成 data(AppRouteRecordRaw) |
| | | // 路由地址转首字母大写驼峰,作为路由名称,适配keepAlive |
| | | let data: AppRouteRecordRaw = { |
| | | path: route.path.indexOf('?') > -1 ? route.path.split('?')[0] : route.path, |
| | | path: |
| | | route.path.indexOf('?') > -1 && !isUrl(route.path) ? route.path.split('?')[0] : route.path, // 注意,需要排除 http 这种 url,避免它带 ? 参数被截取掉 |
| | | name: |
| | | route.componentName && route.componentName.length > 0 |
| | | ? route.componentName |
| | |
| | | data.children = [childrenData] |
| | | } else { |
| | | // 目录 |
| | | if (route.children) { |
| | | if (route.children?.length) { |
| | | data.component = Layout |
| | | data.redirect = getRedirect(route.path, route.children) |
| | | // 外链 |
| | |
| | | let str = '' |
| | | |
| | | function performAThoroughValidation(arr) { |
| | | if (typeof arr === 'undefined' || !Array.isArray(arr) || arr.length === 0) { |
| | | return false |
| | | } |
| | | for (const item of arr) { |
| | | if (item.id === nodeId) { |
| | | str += ` / ${item.name}` |
| | |
| | | <template> |
| | | <div> |
| | | <h1>IAILAB 平台主页</h1> |
| | | <div id="title"> |
| | | <span>工业互联网平台</span> |
| | | </div> |
| | | <el-skeleton :loading="loading" animated> |
| | | <div id="app" v-for="(item, index) in appList" :key="`dynamics-${index}`"> |
| | | <div class="card" @click="gotoApp(item)"> |
| | | <img :src="item.icon" style="width: 100px; height: 100px"/> |
| | | <div id="app"> |
| | | <div class="card" v-for="(item, index) in appList" :key="`dynamics-${index}`"> |
| | | <div> |
| | | {{ item.appName }} |
| | | <img class="card-left" :src="item.icon"/> |
| | | <div class="card-right"> |
| | | <div class="app-title"> |
| | | {{ item.appName }} |
| | | </div> |
| | | <div class="goto-app" @click="gotoApp(item)"> |
| | | <div>进入应用</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | |
| | | |
| | | import * as AppApi from '@/api/system/app' |
| | | import {Apps} from "@/views/Home/types"; |
| | | import {CACHE_KEY, useCache} from "@/hooks/web/useCache"; |
| | | import {CACHE_KEY, useCache, useSessionCache} from "@/hooks/web/useCache"; |
| | | |
| | | |
| | | defineOptions({name: 'Home'}) |
| | | |
| | | const {wsCache} = useCache() |
| | | const {wsSessionCache} = useSessionCache() |
| | | |
| | | const loading = ref(true) |
| | | |
| | |
| | | const getAppMenuList = async (id, appCode) => { |
| | | const data = await AppApi.getAppMenuList(id) |
| | | let userInfo = wsCache.get(CACHE_KEY.USER) |
| | | userInfo.menus = data |
| | | // userInfo.menus = data |
| | | wsCache.set(CACHE_KEY.USER, userInfo) |
| | | wsCache.set(CACHE_KEY.ROLE_ROUTERS, data) |
| | | window.location.href = '/plat/index' |
| | | wsSessionCache.set(CACHE_KEY.ROLE_ROUTERS, data) |
| | | window.location.href = import.meta.env.VITE_BASE_PATH + 'index' |
| | | } |
| | | |
| | | const getAllApi = async () => { |
| | |
| | | const gotoApp = async (item) => { |
| | | let path = window.location.pathname |
| | | let appName = path.split("/")[0] |
| | | console.log(appName) |
| | | let id = item.id |
| | | let type = item.type |
| | | let appCode = item.appCode |
| | | if (type === 0) { |
| | | await getAppMenuList(id, appCode) |
| | | } else { |
| | | const data = await AppApi.getAppMenuList(id) |
| | | let userInfo = wsCache.get(CACHE_KEY.USER) |
| | | userInfo.menus = data |
| | | wsCache.set(CACHE_KEY.USER, userInfo) |
| | | wsCache.set(CACHE_KEY.ROLE_ROUTERS, data) |
| | | // const data = await AppApi.getAppMenuList(id) |
| | | // let userInfo = wsCache.get(CACHE_KEY.USER) |
| | | // userInfo.menus = data |
| | | // wsCache.set(CACHE_KEY.USER, userInfo) |
| | | // wsSessionCache.set(CACHE_KEY.ROLE_ROUTERS, data) |
| | | localStorage.setItem(appCode, id) |
| | | window.open(item.appDomain + '/index', '_blank') |
| | | // window.open('/plat/shasteel', '_blank') |
| | | // window.location.href = '/plat/shasteel' |
| | |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | #title { |
| | | width: 280px; |
| | | height: 51px; |
| | | margin: 30px auto; |
| | | font-family: Microsoft YaHei UI, Microsoft YaHei UI; |
| | | font-weight: bold; |
| | | font-size: 40px; |
| | | color: #282F3D; |
| | | } |
| | | |
| | | #app { |
| | | width: 300px; |
| | | height: 200px; |
| | | display: inline-block; |
| | | background: transparent; |
| | | margin: 0 96px; |
| | | width: 100%; |
| | | } |
| | | |
| | | .card { |
| | | border: thin dashed gainsboro; |
| | | width: 150px; |
| | | height: 120px; |
| | | padding: 30px; |
| | | text-align: center; |
| | | justify-content: center; |
| | | font-size: 15px; |
| | | font-weight: bolder; |
| | | color: blue; |
| | | background: aliceblue; |
| | | border-radius: 10px; |
| | | width: 354px; |
| | | height: 200px; |
| | | margin: 0 24px 24px 0; |
| | | background: linear-gradient(180deg, #E9F0FA 0%, #FFFFFF 100%); |
| | | border-radius: 12px 12px 12px 12px; |
| | | border: 2px solid; |
| | | border-image: linear-gradient(180deg, rgba(255, 255, 255, 1), rgba(255, 255, 255, 1)) 2 2; |
| | | display: inline-block; |
| | | } |
| | | |
| | | .card-left { |
| | | height: 100px; |
| | | width: 100px; |
| | | float: left; |
| | | margin: 50px 30px; |
| | | } |
| | | |
| | | .card-right { |
| | | float: right; |
| | | margin: 61px 10px; |
| | | } |
| | | |
| | | .app-title { |
| | | width: 162px; |
| | | font-family: Microsoft YaHei, Microsoft YaHei; |
| | | font-weight: bold; |
| | | font-size: 24px; |
| | | color: #282F3D; |
| | | } |
| | | |
| | | .goto-app { |
| | | width: 96px; |
| | | height: 35px; |
| | | margin-top: 5px; |
| | | background: #3A99FD; |
| | | border-radius: 80px 80px 80px 80px; |
| | | cursor: pointer; |
| | | } |
| | | |
| | | .goto-app > div { |
| | | padding: 6px; |
| | | margin-left: 5px; |
| | | font-family: Microsoft YaHei UI, Microsoft YaHei UI; |
| | | font-weight: 400; |
| | | font-size: 18px; |
| | | color: #FFFFFF; |
| | | } |
| | | </style> |
| | |
| | | <el-radio |
| | | v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" |
| | | :key="dict.value" |
| | | :label="dict.value" |
| | | :value="dict.value" |
| | | > |
| | | {{ dict.label }} |
| | | </el-radio> |
| | |
| | | <script setup lang="ts"> |
| | | import { getIntDictOptions, DICT_TYPE } from '@/utils/dict' |
| | | import { CategoryApi, CategoryVO } from '@/api/bpm/category' |
| | | import { CommonStatusEnum } from '@/utils/constants' |
| | | |
| | | /** BPM 流程分类 表单 */ |
| | | defineOptions({ name: 'CategoryForm' }) |
| | |
| | | id: undefined, |
| | | name: undefined, |
| | | code: undefined, |
| | | status: undefined, |
| | | status: CommonStatusEnum.ENABLE, |
| | | sort: undefined |
| | | }) |
| | | const formRules = reactive({ |
| | |
| | | id: undefined, |
| | | name: undefined, |
| | | code: undefined, |
| | | status: undefined, |
| | | status: CommonStatusEnum.ENABLE, |
| | | sort: undefined |
| | | } |
| | | formRef.value?.resetFields() |
| | |
| | | |
| | | <!-- 弹窗:流程模型图的预览 --> |
| | | <Dialog title="流程图" v-model="bpmnDetailVisible" width="800"> |
| | | <MyProcessViewer |
| | | key="designer" |
| | | v-model="bpmnXml" |
| | | :value="bpmnXml as any" |
| | | v-bind="bpmnControlForm" |
| | | :prefix="bpmnControlForm.prefix" |
| | | /> |
| | | <MyProcessViewer style="height: 700px" key="designer" :xml="bpmnXml" /> |
| | | </Dialog> |
| | | </template> |
| | | |
| | |
| | | rule: [], |
| | | option: {} |
| | | }) |
| | | const handleFormDetail = async (row) => { |
| | | const handleFormDetail = async (row: any) => { |
| | | if (row.formType == 10) { |
| | | // 设置表单 |
| | | setConfAndFields2(formDetailPreview, row.formConf, row.formFields) |
| | |
| | | |
| | | /** 流程图的详情按钮操作 */ |
| | | const bpmnDetailVisible = ref(false) |
| | | const bpmnXml = ref(null) |
| | | const bpmnControlForm = ref({ |
| | | prefix: 'flowable' |
| | | }) |
| | | const handleBpmnDetail = async (row) => { |
| | | bpmnXml.value = (await DefinitionApi.getProcessDefinition(row.id))?.bpmnXml |
| | | const bpmnXml = ref('') |
| | | const handleBpmnDetail = async (row: any) => { |
| | | // 设置可见 |
| | | bpmnXml.value = '' |
| | | bpmnDetailVisible.value = true |
| | | // 加载 BPMN XML |
| | | bpmnXml.value = (await DefinitionApi.getProcessDefinition(row.id))?.bpmnXml |
| | | } |
| | | |
| | | /** 初始化 **/ |
| | |
| | | <template> |
| | | <ContentWrap> |
| | | <ContentWrap :body-style="{ padding: '0px' }" class="!mb-0"> |
| | | <!-- 表单设计器 --> |
| | | <FcDesigner ref="designer" height="780px"> |
| | | <template #handle> |
| | | <el-button round size="small" type="primary" @click="handleSave"> |
| | | <Icon class="mr-5px" icon="ep:plus" /> |
| | | 保存 |
| | | </el-button> |
| | | </template> |
| | | </FcDesigner> |
| | | <div |
| | | class="h-[calc(100vh-var(--top-tool-height)-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-2px)]" |
| | | > |
| | | <fc-designer class="my-designer" ref="designer" :config="designerConfig"> |
| | | <template #handle> |
| | | <el-button size="small" type="success" plain @click="handleSave"> |
| | | <Icon class="mr-5px" icon="ep:plus" /> |
| | | 保存 |
| | | </el-button> |
| | | </template> |
| | | </fc-designer> |
| | | </div> |
| | | </ContentWrap> |
| | | |
| | | <!-- 表单保存的弹窗 --> |
| | |
| | | <el-radio |
| | | v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" |
| | | :key="dict.value" |
| | | :label="dict.value" |
| | | :value="dict.value" |
| | | > |
| | | {{ dict.label }} |
| | | </el-radio> |
| | |
| | | const { query } = useRoute() // 路由信息 |
| | | const { delView } = useTagsViewStore() // 视图操作 |
| | | |
| | | // 表单设计器配置 |
| | | const designerConfig = ref({ |
| | | switchType: [], // 是否可以切换组件类型,或者可以相互切换的字段 |
| | | autoActive: true, // 是否自动选中拖入的组件 |
| | | useTemplate: false, // 是否生成vue2语法的模板组件 |
| | | formOptions: { |
| | | form: { |
| | | labelWidth: '100px' // 设置默认的 label 宽度为 100px |
| | | } |
| | | }, // 定义表单配置默认值 |
| | | fieldReadonly: false, // 配置field是否可以编辑 |
| | | hiddenDragMenu: false, // 隐藏拖拽操作按钮 |
| | | hiddenDragBtn: false, // 隐藏拖拽按钮 |
| | | hiddenMenu: [], // 隐藏部分菜单 |
| | | hiddenItem: [], // 隐藏部分组件 |
| | | hiddenItemConfig: {}, // 隐藏组件的部分配置项 |
| | | disabledItemConfig: {}, // 禁用组件的部分配置项 |
| | | showSaveBtn: false, // 是否显示保存按钮 |
| | | showConfig: true, // 是否显示右侧的配置界面 |
| | | showBaseForm: true, // 是否显示组件的基础配置表单 |
| | | showControl: true, // 是否显示组件联动 |
| | | showPropsForm: true, // 是否显示组件的属性配置表单 |
| | | showEventForm: true, // 是否显示组件的事件配置表单 |
| | | showValidateForm: true, // 是否显示组件的验证配置表单 |
| | | showFormConfig: true, // 是否显示表单配置 |
| | | showInputData: true, // 是否显示录入按钮 |
| | | showDevice: true, // 是否显示多端适配选项 |
| | | appendConfigData: [] // 定义渲染规则所需的formData |
| | | }) |
| | | const designer = ref() // 表单设计器 |
| | | useFormCreateDesigner(designer) // 表单设计器增强 |
| | | const dialogVisible = ref(false) // 弹窗是否展示 |
| | |
| | | setConfAndFields(designer, data.conf, data.fields) |
| | | }) |
| | | </script> |
| | | |
| | | <style> |
| | | .my-designer { |
| | | ._fc-l, |
| | | ._fc-m, |
| | | ._fc-r { |
| | | border-top: none; |
| | | } |
| | | } |
| | | </style> |
| | |
| | | <template> |
| | | |
| | | <ContentWrap> |
| | | <!-- 搜索工作栏 --> |
| | | <el-form |
| | |
| | | const toRouter: { name: string; query?: { id: number } } = { |
| | | name: 'BpmFormEditor' |
| | | } |
| | | console.log(typeof id) |
| | | // 表单新建的时候id传的是event需要排除 |
| | | if (typeof id === 'number') { |
| | | if (typeof id === 'number' || typeof id === 'string') { |
| | | toRouter.query = { |
| | | id |
| | | } |
| | |
| | | <el-radio |
| | | v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" |
| | | :key="dict.value" |
| | | :label="dict.value" |
| | | :value="dict.value" |
| | | > |
| | | {{ dict.label }} |
| | | </el-radio> |
对比新文件 |
| | |
| | | <template> |
| | | <div class="flex items-center h-50px"> |
| | | <!-- 头部:分类名 --> |
| | | <div class="flex items-center"> |
| | | <el-tooltip content="拖动排序" v-if="isCategorySorting"> |
| | | <Icon |
| | | :size="22" |
| | | icon="ic:round-drag-indicator" |
| | | class="ml-10px category-drag-icon cursor-move text-#8a909c" |
| | | /> |
| | | </el-tooltip> |
| | | <h3 class="ml-20px mr-8px text-18px">{{ categoryInfo.name }}</h3> |
| | | <div class="color-gray-600 text-16px"> ({{ categoryInfo.modelList?.length || 0 }}) </div> |
| | | </div> |
| | | <!-- 头部:操作 --> |
| | | <div class="flex-1 flex" v-if="!isCategorySorting"> |
| | | <div |
| | | v-if="categoryInfo.modelList.length > 0" |
| | | class="ml-20px flex items-center" |
| | | :class="[ |
| | | 'transition-transform duration-300 cursor-pointer', |
| | | isExpand ? 'rotate-180' : 'rotate-0' |
| | | ]" |
| | | @click="isExpand = !isExpand" |
| | | > |
| | | <Icon icon="ep:arrow-down-bold" color="#999" /> |
| | | </div> |
| | | <div class="ml-auto flex items-center" :class="isModelSorting ? 'mr-15px' : 'mr-45px'"> |
| | | <template v-if="!isModelSorting"> |
| | | <el-button |
| | | v-if="categoryInfo.modelList.length > 0" |
| | | link |
| | | type="info" |
| | | class="mr-20px" |
| | | @click.stop="handleModelSort" |
| | | > |
| | | <Icon icon="fa:sort-amount-desc" class="mr-5px" /> |
| | | 排序 |
| | | </el-button> |
| | | <el-button v-else link type="info" class="mr-20px" @click.stop="openModelForm('create')"> |
| | | <Icon icon="fa:plus" class="mr-5px" /> |
| | | 新建 |
| | | </el-button> |
| | | <el-dropdown |
| | | @command="(command) => handleCategoryCommand(command, categoryInfo)" |
| | | placement="bottom" |
| | | > |
| | | <el-button link type="info"> |
| | | <Icon icon="ep:setting" class="mr-5px" /> |
| | | 分类 |
| | | </el-button> |
| | | <template #dropdown> |
| | | <el-dropdown-menu> |
| | | <el-dropdown-item command="handleRename"> 重命名 </el-dropdown-item> |
| | | <el-dropdown-item command="handleDeleteCategory"> 删除该类 </el-dropdown-item> |
| | | </el-dropdown-menu> |
| | | </template> |
| | | </el-dropdown> |
| | | </template> |
| | | <template v-else> |
| | | <el-button @click.stop="handleModelSortCancel"> 取 消 </el-button> |
| | | <el-button type="primary" @click.stop="handleModelSortSubmit"> 保存排序 </el-button> |
| | | </template> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 模型列表 --> |
| | | <el-collapse-transition> |
| | | <div v-show="isExpand"> |
| | | <el-table |
| | | :class="categoryInfo.name" |
| | | ref="tableRef" |
| | | :header-cell-style="{ backgroundColor: isDark ? '' : '#edeff0', paddingLeft: '10px' }" |
| | | :cell-style="{ paddingLeft: '10px' }" |
| | | :row-style="{ height: '68px' }" |
| | | :data="modelList" |
| | | row-key="id" |
| | | > |
| | | <el-table-column label="流程名" prop="name" min-width="150"> |
| | | <template #default="scope"> |
| | | <div class="flex items-center"> |
| | | <el-tooltip content="拖动排序" v-if="isModelSorting"> |
| | | <Icon |
| | | icon="ic:round-drag-indicator" |
| | | class="drag-icon cursor-move text-#8a909c mr-10px" |
| | | /> |
| | | </el-tooltip> |
| | | <el-image :src="scope.row.icon" class="h-38px w-38px mr-10px rounded" /> |
| | | {{ scope.row.name }} |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="可见范围" prop="startUserIds" min-width="150"> |
| | | <template #default="scope"> |
| | | <el-text v-if="!scope.row.startUsers || scope.row.startUsers.length === 0"> |
| | | 全部可见 |
| | | </el-text> |
| | | <el-text v-else-if="scope.row.startUsers.length == 1"> |
| | | {{ scope.row.startUsers[0].nickname }} |
| | | </el-text> |
| | | <el-text v-else> |
| | | <el-tooltip |
| | | class="box-item" |
| | | effect="dark" |
| | | placement="top" |
| | | :content="scope.row.startUsers.map((user: any) => user.nickname).join('、')" |
| | | > |
| | | {{ scope.row.startUsers[0].nickname }}等 {{ scope.row.startUsers.length }} 人可见 |
| | | </el-tooltip> |
| | | </el-text> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="表单信息" prop="formType" min-width="150"> |
| | | <template #default="scope"> |
| | | <el-button |
| | | v-if="scope.row.formType === BpmModelFormType.NORMAL" |
| | | type="primary" |
| | | link |
| | | @click="handleFormDetail(scope.row)" |
| | | > |
| | | <span>{{ scope.row.formName }}</span> |
| | | </el-button> |
| | | <el-button |
| | | v-else-if="scope.row.formType === BpmModelFormType.CUSTOM" |
| | | type="primary" |
| | | link |
| | | @click="handleFormDetail(scope.row)" |
| | | > |
| | | <span>{{ scope.row.formCustomCreatePath }}</span> |
| | | </el-button> |
| | | <label v-else>暂无表单</label> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="最后发布" prop="deploymentTime" min-width="250"> |
| | | <template #default="scope"> |
| | | <div class="flex items-center"> |
| | | <span v-if="scope.row.processDefinition" class="w-150px"> |
| | | {{ formatDate(scope.row.processDefinition.deploymentTime) }} |
| | | </span> |
| | | <el-tag v-if="scope.row.processDefinition"> |
| | | v{{ scope.row.processDefinition.version }} |
| | | </el-tag> |
| | | <el-tag v-else type="warning">未部署</el-tag> |
| | | <el-tag |
| | | v-if="scope.row.processDefinition?.suspensionState === 2" |
| | | type="warning" |
| | | class="ml-10px" |
| | | > |
| | | 已停用 |
| | | </el-tag> |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" width="200" fixed="right"> |
| | | <template #default="scope"> |
| | | <el-button |
| | | link |
| | | type="primary" |
| | | @click="openModelForm('update', scope.row.id)" |
| | | v-hasPermi="['bpm:model:update']" |
| | | :disabled="!isManagerUser(scope.row)" |
| | | > |
| | | 修改 |
| | | </el-button> |
| | | <el-button |
| | | link |
| | | class="!ml-5px" |
| | | type="primary" |
| | | @click="handleDeploy(scope.row)" |
| | | v-hasPermi="['bpm:model:deploy']" |
| | | :disabled="!isManagerUser(scope.row)" |
| | | > |
| | | 发布 |
| | | </el-button> |
| | | <el-dropdown |
| | | class="!align-middle ml-5px" |
| | | @command="(command) => handleModelCommand(command, scope.row)" |
| | | v-hasPermi="['bpm:process-definition:query', 'bpm:model:update', 'bpm:model:delete']" |
| | | > |
| | | <el-button type="primary" link>更多</el-button> |
| | | <template #dropdown> |
| | | <el-dropdown-menu> |
| | | <el-dropdown-item |
| | | command="handleDefinitionList" |
| | | v-if="checkPermi(['bpm:process-definition:query'])" |
| | | > |
| | | 历史 |
| | | </el-dropdown-item> |
| | | <el-dropdown-item |
| | | command="handleChangeState" |
| | | v-if="checkPermi(['bpm:model:update']) && scope.row.processDefinition" |
| | | :disabled="!isManagerUser(scope.row)" |
| | | > |
| | | {{ scope.row.processDefinition.suspensionState === 1 ? '停用' : '启用' }} |
| | | </el-dropdown-item> |
| | | <el-dropdown-item |
| | | type="danger" |
| | | command="handleDelete" |
| | | v-if="checkPermi(['bpm:model:delete'])" |
| | | :disabled="!isManagerUser(scope.row)" |
| | | > |
| | | 删除 |
| | | </el-dropdown-item> |
| | | </el-dropdown-menu> |
| | | </template> |
| | | </el-dropdown> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </el-collapse-transition> |
| | | |
| | | <!-- 弹窗:重命名分类 --> |
| | | <Dialog :fullscreen="false" class="rename-dialog" v-model="renameCategoryVisible" width="400"> |
| | | <template #title> |
| | | <div class="pl-10px font-bold text-18px"> 重命名分类 </div> |
| | | </template> |
| | | <div class="px-30px"> |
| | | <el-input v-model="renameCategoryForm.name" /> |
| | | </div> |
| | | <template #footer> |
| | | <div class="pr-25px pb-25px"> |
| | | <el-button @click="renameCategoryVisible = false">取 消</el-button> |
| | | <el-button type="primary" @click="handleRenameConfirm">确 定</el-button> |
| | | </div> |
| | | </template> |
| | | </Dialog> |
| | | |
| | | <!-- 表单弹窗:添加流程模型 --> |
| | | <ModelForm :categoryId="categoryInfo.code" ref="modelFormRef" @success="emit('success')" /> |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | import ModelForm from './ModelForm.vue' |
| | | import { CategoryApi, CategoryVO } from '@/api/bpm/category' |
| | | import Sortable from 'sortablejs' |
| | | import { propTypes } from '@/utils/propTypes' |
| | | import { formatDate } from '@/utils/formatTime' |
| | | import * as ModelApi from '@/api/bpm/model' |
| | | import * as FormApi from '@/api/bpm/form' |
| | | import { setConfAndFields2 } from '@/utils/formCreate' |
| | | import { BpmModelFormType } from '@/utils/constants' |
| | | import { checkPermi } from '@/utils/permission' |
| | | import { useUserStoreWithOut } from '@/store/modules/user' |
| | | import { useAppStore } from '@/store/modules/app' |
| | | import { cloneDeep } from 'lodash-es' |
| | | |
| | | defineOptions({ name: 'BpmModel' }) |
| | | |
| | | const props = defineProps({ |
| | | categoryInfo: propTypes.object.def([]), // 分类后的数据 |
| | | isCategorySorting: propTypes.bool.def(false) // 是否分类在排序 |
| | | }) |
| | | const emit = defineEmits(['success']) |
| | | const message = useMessage() // 消息弹窗 |
| | | const { t } = useI18n() // 国际化 |
| | | const { push } = useRouter() // 路由 |
| | | const userStore = useUserStoreWithOut() // 用户信息缓存 |
| | | const isDark = computed(() => useAppStore().getIsDark) // 是否黑暗模式 |
| | | |
| | | const isModelSorting = ref(false) // 是否正处于排序状态 |
| | | const originalData: any = ref([]) // 原始数据 |
| | | const modelList: any = ref([]) // 模型列表 |
| | | const isExpand = ref(false) // 是否处于展开状态 |
| | | |
| | | /** '更多'操作按钮 */ |
| | | const handleModelCommand = (command: string, row: any) => { |
| | | switch (command) { |
| | | case 'handleDefinitionList': |
| | | handleDefinitionList(row) |
| | | break |
| | | case 'handleDelete': |
| | | handleDelete(row) |
| | | break |
| | | case 'handleChangeState': |
| | | handleChangeState(row) |
| | | break |
| | | default: |
| | | break |
| | | } |
| | | } |
| | | |
| | | /** '分类'操作按钮 */ |
| | | const handleCategoryCommand = async (command: string, row: any) => { |
| | | switch (command) { |
| | | case 'handleRename': |
| | | renameCategoryForm.value = await CategoryApi.getCategory(row.id) |
| | | renameCategoryVisible.value = true |
| | | break |
| | | case 'handleDeleteCategory': |
| | | await handleDeleteCategory() |
| | | break |
| | | default: |
| | | break |
| | | } |
| | | } |
| | | |
| | | /** 删除按钮操作 */ |
| | | const handleDelete = async (row: any) => { |
| | | try { |
| | | // 删除的二次确认 |
| | | await message.delConfirm() |
| | | // 发起删除 |
| | | await ModelApi.deleteModel(row.id) |
| | | message.success(t('common.delSuccess')) |
| | | // 刷新列表 |
| | | emit('success') |
| | | } catch {} |
| | | } |
| | | |
| | | /** 更新状态操作 */ |
| | | const handleChangeState = async (row: any) => { |
| | | const state = row.processDefinition.suspensionState |
| | | const newState = state === 1 ? 2 : 1 |
| | | try { |
| | | // 修改状态的二次确认 |
| | | const id = row.id |
| | | debugger |
| | | const statusState = state === 1 ? '停用' : '启用' |
| | | const content = '是否确认' + statusState + '流程名字为"' + row.name + '"的数据项?' |
| | | await message.confirm(content) |
| | | // 发起修改状态 |
| | | await ModelApi.updateModelState(id, newState) |
| | | message.success(statusState + '成功') |
| | | // 刷新列表 |
| | | emit('success') |
| | | } catch {} |
| | | } |
| | | |
| | | /** 发布流程 */ |
| | | const handleDeploy = async (row: any) => { |
| | | try { |
| | | // 删除的二次确认 |
| | | await message.confirm('是否部署该流程!!') |
| | | // 发起部署 |
| | | await ModelApi.deployModel(row.id) |
| | | message.success(t('部署成功')) |
| | | // 刷新列表 |
| | | emit('success') |
| | | } catch {} |
| | | } |
| | | |
| | | /** 跳转到指定流程定义列表 */ |
| | | const handleDefinitionList = (row: any) => { |
| | | push({ |
| | | name: 'BpmProcessDefinition', |
| | | query: { |
| | | key: row.key |
| | | } |
| | | }) |
| | | } |
| | | |
| | | /** 流程表单的详情按钮操作 */ |
| | | const formDetailVisible = ref(false) |
| | | const formDetailPreview = ref({ |
| | | rule: [], |
| | | option: {} |
| | | }) |
| | | const handleFormDetail = async (row: any) => { |
| | | if (row.formType == 10) { |
| | | // 设置表单 |
| | | const data = await FormApi.getForm(row.formId) |
| | | setConfAndFields2(formDetailPreview, data.conf, data.fields) |
| | | // 弹窗打开 |
| | | formDetailVisible.value = true |
| | | } else { |
| | | await push({ |
| | | path: row.formCustomCreatePath |
| | | }) |
| | | } |
| | | } |
| | | |
| | | /** 判断是否可以操作 */ |
| | | const isManagerUser = (row: any) => { |
| | | const userId = userStore.getUser.id |
| | | return row.managerUserIds && row.managerUserIds.includes(userId) |
| | | } |
| | | |
| | | /** 处理模型的排序 **/ |
| | | const handleModelSort = () => { |
| | | // 保存初始数据 |
| | | originalData.value = cloneDeep(props.categoryInfo.modelList) |
| | | isModelSorting.value = true |
| | | initSort() |
| | | } |
| | | |
| | | /** 处理模型的排序提交 */ |
| | | const handleModelSortSubmit = async () => { |
| | | // 保存排序 |
| | | const ids = modelList.value.map((item: any) => item.id) |
| | | await ModelApi.updateModelSortBatch(ids) |
| | | // 刷新列表 |
| | | isModelSorting.value = false |
| | | message.success('排序模型成功') |
| | | emit('success') |
| | | } |
| | | |
| | | /** 处理模型的排序取消 */ |
| | | const handleModelSortCancel = () => { |
| | | // 恢复初始数据 |
| | | modelList.value = cloneDeep(originalData.value) |
| | | isModelSorting.value = false |
| | | } |
| | | |
| | | /** 创建拖拽实例 */ |
| | | const tableRef = ref() |
| | | const initSort = () => { |
| | | const table = document.querySelector(`.${props.categoryInfo.name} .el-table__body-wrapper tbody`) |
| | | Sortable.create(table, { |
| | | group: 'shared', |
| | | animation: 150, |
| | | draggable: '.el-table__row', |
| | | handle: '.drag-icon', |
| | | // 结束拖动事件 |
| | | onEnd: ({ newDraggableIndex, oldDraggableIndex }) => { |
| | | if (oldDraggableIndex !== newDraggableIndex) { |
| | | modelList.value.splice( |
| | | newDraggableIndex, |
| | | 0, |
| | | modelList.value.splice(oldDraggableIndex, 1)[0] |
| | | ) |
| | | } |
| | | } |
| | | }) |
| | | } |
| | | |
| | | /** 更新 modelList 模型列表 */ |
| | | const updateModeList = () => { |
| | | modelList.value = cloneDeep(props.categoryInfo.modelList) |
| | | if (props.categoryInfo.modelList.length > 0) { |
| | | isExpand.value = true |
| | | } |
| | | } |
| | | |
| | | /** 重命名弹窗确定 */ |
| | | const renameCategoryVisible = ref(false) |
| | | const renameCategoryForm = ref({ |
| | | name: '' |
| | | }) |
| | | const handleRenameConfirm = async () => { |
| | | if (renameCategoryForm.value?.name.length === 0) { |
| | | return message.warning('请输入名称') |
| | | } |
| | | // 发起修改 |
| | | await CategoryApi.updateCategory(renameCategoryForm.value as CategoryVO) |
| | | message.success('重命名成功') |
| | | // 刷新列表 |
| | | renameCategoryVisible.value = false |
| | | emit('success') |
| | | } |
| | | |
| | | /** 删除分类 */ |
| | | const handleDeleteCategory = async () => { |
| | | try { |
| | | if (props.categoryInfo.modelList.length > 0) { |
| | | return message.warning('该分类下仍有流程定义,不允许删除') |
| | | } |
| | | await message.confirm('确认删除分类吗?') |
| | | // 发起删除 |
| | | await CategoryApi.deleteCategory(props.categoryInfo.id) |
| | | message.success(t('common.delSuccess')) |
| | | // 刷新列表 |
| | | emit('success') |
| | | } catch {} |
| | | } |
| | | |
| | | /** 添加流程模型弹窗 */ |
| | | const modelFormRef = ref() |
| | | const openModelForm = (type: string, id?: number) => { |
| | | if (type === 'create') { |
| | | push({ name: 'BpmModelCreate' }) |
| | | } else { |
| | | push({ |
| | | name: 'BpmModelUpdate', |
| | | params: { id } |
| | | }) |
| | | } |
| | | } |
| | | |
| | | watch(() => props.categoryInfo.modelList, updateModeList, { immediate: true }) |
| | | watch( |
| | | () => props.isCategorySorting, |
| | | (val) => { |
| | | if (val) isExpand.value = false |
| | | }, |
| | | { immediate: true } |
| | | ) |
| | | </script> |
| | | |
| | | <style lang="scss"> |
| | | .rename-dialog.el-dialog { |
| | | padding: 0 !important; |
| | | |
| | | .el-dialog__header { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | .el-dialog__footer { |
| | | border-top: none !important; |
| | | } |
| | | } |
| | | </style> |
| | | <style lang="scss" scoped> |
| | | :deep() { |
| | | .el-table__cell { |
| | | overflow: hidden; |
| | | border-bottom: none !important; |
| | | } |
| | | } |
| | | </style> |
| | |
| | | label-width="110px" |
| | | > |
| | | <el-form-item label="流程标识" prop="key"> |
| | | <el-input |
| | | v-model="formData.key" |
| | | :disabled="!!formData.id" |
| | | placeholder="请输入流标标识" |
| | | style="width: 330px" |
| | | /> |
| | | <el-input v-model="formData.key" :disabled="!!formData.id" placeholder="请输入流标标识" /> |
| | | <el-tooltip |
| | | v-if="!formData.id" |
| | | class="item" |
| | |
| | | placeholder="请输入流程名称" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item v-if="formData.id" label="流程分类" prop="category"> |
| | | <el-form-item label="流程分类" prop="category"> |
| | | <el-select |
| | | v-model="formData.category" |
| | | clearable |
| | |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item v-if="formData.id" label="流程图标" prop="icon"> |
| | | <UploadImg v-model="formData.icon" :limit="1" height="128px" width="128px" /> |
| | | <el-form-item label="流程图标" prop="icon"> |
| | | <UploadImg v-model="formData.icon" :limit="1" height="64px" width="64px" /> |
| | | </el-form-item> |
| | | <el-form-item label="流程描述" prop="description"> |
| | | <el-input v-model="formData.description" clearable type="textarea" /> |
| | | </el-form-item> |
| | | <div v-if="formData.id"> |
| | | <el-form-item label="表单类型" prop="formType"> |
| | | <el-radio-group v-model="formData.formType"> |
| | | <el-radio |
| | | v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_FORM_TYPE)" |
| | | :key="dict.value" |
| | | :label="dict.value" |
| | | > |
| | | {{ dict.label }} |
| | | </el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item v-if="formData.formType === 10" label="流程表单" prop="formId"> |
| | | <el-select v-model="formData.formId" clearable style="width: 100%"> |
| | | <el-option |
| | | v-for="form in formList" |
| | | :key="form.id" |
| | | :label="form.name" |
| | | :value="form.id" |
| | | <el-form-item label="流程类型" prop="type"> |
| | | <el-radio-group v-model="formData.type"> |
| | | <el-radio |
| | | v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_TYPE)" |
| | | :key="dict.value" |
| | | :label="dict.value" |
| | | > |
| | | {{ dict.label }} |
| | | </el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="表单类型" prop="formType"> |
| | | <el-radio-group v-model="formData.formType"> |
| | | <el-radio |
| | | v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_FORM_TYPE)" |
| | | :key="dict.value" |
| | | :label="dict.value" |
| | | > |
| | | {{ dict.label }} |
| | | </el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item v-if="formData.formType === 10" label="流程表单" prop="formId"> |
| | | <el-select v-model="formData.formId" clearable style="width: 100%"> |
| | | <el-option v-for="form in formList" :key="form.id" :label="form.name" :value="form.id" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="formData.formType === 20" |
| | | label="表单提交路由" |
| | | prop="formCustomCreatePath" |
| | | > |
| | | <el-input |
| | | v-model="formData.formCustomCreatePath" |
| | | placeholder="请输入表单提交路由" |
| | | style="width: 330px" |
| | | /> |
| | | <el-tooltip |
| | | class="item" |
| | | content="自定义表单的提交路径,使用 Vue 的路由地址,例如说:bpm/oa/leave/create.vue" |
| | | effect="light" |
| | | placement="top" |
| | | > |
| | | <i class="el-icon-question" style="padding-left: 5px"></i> |
| | | </el-tooltip> |
| | | </el-form-item> |
| | | <el-form-item v-if="formData.formType === 20" label="表单查看地址" prop="formCustomViewPath"> |
| | | <el-input |
| | | v-model="formData.formCustomViewPath" |
| | | placeholder="请输入表单查看的组件地址" |
| | | style="width: 330px" |
| | | /> |
| | | <el-tooltip |
| | | class="item" |
| | | content="自定义表单的查看组件地址,使用 Vue 的组件地址,例如说:bpm/oa/leave/detail.vue" |
| | | effect="light" |
| | | placement="top" |
| | | > |
| | | <i class="el-icon-question" style="padding-left: 5px"></i> |
| | | </el-tooltip> |
| | | </el-form-item> |
| | | <el-form-item label="是否可见" prop="visible"> |
| | | <el-radio-group v-model="formData.visible"> |
| | | <el-radio |
| | | v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)" |
| | | :key="dict.value as string" |
| | | :label="dict.value" |
| | | > |
| | | {{ dict.label }} |
| | | </el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="谁可以发起" prop="startUserType"> |
| | | <el-select |
| | | v-model="formData.startUserType" |
| | | placeholder="请选择谁可以发起" |
| | | @change="handleStartUserTypeChange" |
| | | > |
| | | <el-option label="全员" :value="0" /> |
| | | <el-option label="指定人员" :value="1" /> |
| | | <el-option label="均不可提交" :value="2" /> |
| | | </el-select> |
| | | <div v-if="formData.startUserType === 1" class="mt-2 flex flex-wrap gap-2"> |
| | | <div |
| | | v-for="user in selectedStartUsers" |
| | | :key="user.id" |
| | | class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative" |
| | | > |
| | | <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" /> |
| | | <el-avatar class="!m-5px" :size="28" v-else> |
| | | {{ user.nickname.substring(0, 1) }} |
| | | </el-avatar> |
| | | {{ user.nickname }} |
| | | <Icon |
| | | icon="ep:close" |
| | | class="ml-2 cursor-pointer hover:text-red-500" |
| | | @click="handleRemoveStartUser(user)" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="formData.formType === 20" |
| | | label="表单提交路由" |
| | | prop="formCustomCreatePath" |
| | | </div> |
| | | <el-button type="primary" link @click="openStartUserSelect"> |
| | | <Icon icon="ep:plus" />选择人员 |
| | | </el-button> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="流程管理员" prop="managerUserType"> |
| | | <el-select |
| | | v-model="formData.managerUserType" |
| | | placeholder="请选择流程管理员" |
| | | @change="handleManagerUserTypeChange" |
| | | > |
| | | <el-input |
| | | v-model="formData.formCustomCreatePath" |
| | | placeholder="请输入表单提交路由" |
| | | style="width: 330px" |
| | | /> |
| | | <el-tooltip |
| | | class="item" |
| | | content="自定义表单的提交路径,使用 Vue 的路由地址,例如说:bpm/oa/leave/create" |
| | | effect="light" |
| | | placement="top" |
| | | <el-option label="全员" :value="0" /> |
| | | <el-option label="指定人员" :value="1" /> |
| | | <el-option label="均不可提交" :value="2" /> |
| | | </el-select> |
| | | <div v-if="formData.managerUserType === 1" class="mt-2 flex flex-wrap gap-2"> |
| | | <div |
| | | v-for="user in selectedManagerUsers" |
| | | :key="user.id" |
| | | class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative" |
| | | > |
| | | <i class="el-icon-question" style="padding-left: 5px"></i> |
| | | </el-tooltip> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="formData.formType === 20" |
| | | label="表单查看地址" |
| | | prop="formCustomViewPath" |
| | | > |
| | | <el-input |
| | | v-model="formData.formCustomViewPath" |
| | | placeholder="请输入表单查看的组件地址" |
| | | style="width: 330px" |
| | | /> |
| | | <el-tooltip |
| | | class="item" |
| | | content="自定义表单的查看组件地址,使用 Vue 的组件地址,例如说:bpm/oa/leave/detail" |
| | | effect="light" |
| | | placement="top" |
| | | > |
| | | <i class="el-icon-question" style="padding-left: 5px"></i> |
| | | </el-tooltip> |
| | | </el-form-item> |
| | | </div> |
| | | <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" /> |
| | | <el-avatar class="!m-5px" :size="28" v-else> |
| | | {{ user.nickname.substring(0, 1) }} |
| | | </el-avatar> |
| | | {{ user.nickname }} |
| | | <Icon |
| | | icon="ep:close" |
| | | class="ml-2 cursor-pointer hover:text-red-500" |
| | | @click="handleRemoveManagerUser(user)" |
| | | /> |
| | | </div> |
| | | <el-button type="primary" link @click="openManagerUserSelect"> |
| | | <Icon icon="ep:plus" />选择人员 |
| | | </el-button> |
| | | </div> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button> |
| | | <el-button @click="dialogVisible = false">取 消</el-button> |
| | | </template> |
| | | </Dialog> |
| | | <UserSelectForm ref="userSelectFormRef" @confirm="handleUserSelectConfirm" /> |
| | | </template> |
| | | <script lang="ts" setup> |
| | | import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' |
| | | import { propTypes } from '@/utils/propTypes' |
| | | import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '@/utils/dict' |
| | | import { ElMessageBox } from 'element-plus' |
| | | import * as ModelApi from '@/api/bpm/model' |
| | | import * as FormApi from '@/api/bpm/form' |
| | | import { CategoryApi } from '@/api/bpm/category' |
| | | import { CategoryApi, CategoryVO } from '@/api/bpm/category' |
| | | import { BpmModelFormType, BpmModelType } from '@/utils/constants' |
| | | import { UserVO } from '@/api/system/user' |
| | | import * as UserApi from '@/api/system/user' |
| | | import { useUserStoreWithOut } from '@/store/modules/user' |
| | | import { FormVO } from '@/api/bpm/form' |
| | | |
| | | defineOptions({ name: 'ModelForm' }) |
| | | |
| | | const { t } = useI18n() // 国际化 |
| | | const message = useMessage() // 消息弹窗 |
| | | |
| | | const userStore = useUserStoreWithOut() // 用户信息缓存 |
| | | const props = defineProps({ |
| | | categoryId: propTypes.number |
| | | }) |
| | | const dialogVisible = ref(false) // 弹窗的是否展示 |
| | | const dialogTitle = ref('') // 弹窗的标题 |
| | | const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 |
| | | const formType = ref('') // 表单的类型:create - 新增;update - 修改 |
| | | const formData = ref({ |
| | | formType: 10, |
| | | const formData: any = ref({ |
| | | id: undefined, |
| | | name: '', |
| | | key: '', |
| | | category: undefined, |
| | | icon: undefined, |
| | | description: '', |
| | | type: BpmModelType.BPMN, |
| | | formType: BpmModelFormType.NORMAL, |
| | | formId: '', |
| | | formCustomCreatePath: '', |
| | | formCustomViewPath: '' |
| | | formCustomViewPath: '', |
| | | visible: true, |
| | | startUserType: undefined, |
| | | managerUserType: undefined, |
| | | startUserIds: [], |
| | | managerUserIds: [] |
| | | }) |
| | | const formRules = reactive({ |
| | | name: [{ required: true, message: '参数名称不能为空', trigger: 'blur' }], |
| | | key: [{ required: true, message: '参数键名不能为空', trigger: 'blur' }], |
| | | category: [{ required: true, message: '参数分类不能为空', trigger: 'blur' }], |
| | | icon: [{ required: true, message: '参数图标不能为空', trigger: 'blur' }], |
| | | value: [{ required: true, message: '参数键值不能为空', trigger: 'blur' }], |
| | | visible: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }] |
| | | name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }], |
| | | key: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }], |
| | | category: [{ required: true, message: '流程分类不能为空', trigger: 'blur' }], |
| | | icon: [{ required: true, message: '流程图标不能为空', trigger: 'blur' }], |
| | | type: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }], |
| | | formType: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }], |
| | | formId: [{ required: true, message: '流程表单不能为空', trigger: 'blur' }], |
| | | formCustomCreatePath: [{ required: true, message: '表单提交路由不能为空', trigger: 'blur' }], |
| | | formCustomViewPath: [{ required: true, message: '表单查看地址不能为空', trigger: 'blur' }], |
| | | visible: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }], |
| | | managerUserIds: [{ required: true, message: '流程管理员不能为空', trigger: 'blur' }] |
| | | }) |
| | | const formRef = ref() // 表单 Ref |
| | | const formList = ref([]) // 流程表单的下拉框的数据 |
| | | const categoryList = ref([]) // 流程分类列表 |
| | | const formList = ref<FormVO[]>([]) // 流程表单的下拉框的数据 |
| | | const categoryList = ref<CategoryVO[]>([]) // 流程分类列表 |
| | | const userList = ref<UserVO[]>([]) // 用户列表 |
| | | const selectedStartUsers = ref<UserVO[]>([]) // 已选择的发起人列表 |
| | | const selectedManagerUsers = ref<UserVO[]>([]) // 已选择的管理员列表 |
| | | const userSelectFormRef = ref() // 用户选择弹窗 ref |
| | | const currentSelectType = ref<'start' | 'manager'>('start') // 当前选择的是发起人还是管理员 |
| | | |
| | | /** 打开弹窗 */ |
| | | const open = async (type: string, id?: number) => { |
| | | const open = async (type: string, id?: string) => { |
| | | dialogVisible.value = true |
| | | dialogTitle.value = t('action.' + type) |
| | | formType.value = type |
| | |
| | | } finally { |
| | | formLoading.value = false |
| | | } |
| | | // 加载数据时,根据已有的用户ID列表初始化已选用户 |
| | | if (formData.value.startUserIds?.length) { |
| | | formData.value.startUserType = 1 |
| | | selectedStartUsers.value = userList.value.filter((user) => |
| | | formData.value.startUserIds.includes(user.id) |
| | | ) |
| | | } |
| | | if (formData.value.managerUserIds?.length) { |
| | | formData.value.managerUserType = 1 |
| | | selectedManagerUsers.value = userList.value.filter((user) => |
| | | formData.value.managerUserIds.includes(user.id) |
| | | ) |
| | | } |
| | | } else { |
| | | formData.value.managerUserIds.push(userStore.getUser.id) |
| | | } |
| | | // 获得流程表单的下拉框的数据 |
| | | formList.value = await FormApi.getFormSimpleList() |
| | | // 查询流程分类列表 |
| | | categoryList.value = await CategoryApi.getCategorySimpleList() |
| | | // 查询用户列表 |
| | | userList.value = await UserApi.getSimpleUserList() |
| | | if (props.categoryId) { |
| | | formData.value.category = props.categoryId |
| | | } |
| | | } |
| | | defineExpose({ open }) // 提供 open 方法,用于打开弹窗 |
| | | |
| | |
| | | await ModelApi.createModel(data) |
| | | // 提示,引导用户做后续的操作 |
| | | await ElMessageBox.alert( |
| | | '<strong>新建模型成功!</strong>后续需要执行如下 3 个步骤:' + |
| | | '<div>1. 点击【修改流程】按钮,配置流程的分类、表单信息</div>' + |
| | | '<div>2. 点击【设计流程】按钮,绘制流程图</div>' + |
| | | '<div>3. 点击【发布流程】按钮,完成流程的最终发布</div>' + |
| | | '另外,每次流程修改后,都需要点击【发布流程】按钮,才能正式生效!!!', |
| | | '<strong>新建模型成功!</strong>后续需要执行如下 2 个步骤:' + |
| | | '<div>1. 点击【设计流程】按钮,绘制流程图</div>' + |
| | | '<div>2. 点击【发布流程】按钮,完成流程的最终发布</div>' + |
| | | '另外,每次流程修改后,都需要点击【发布流程】按钮,才能正式生效!!!', |
| | | '重要提示', |
| | | { |
| | | dangerouslyUseHTMLString: true, |
| | |
| | | /** 重置表单 */ |
| | | const resetForm = () => { |
| | | formData.value = { |
| | | formType: 10, |
| | | id: undefined, |
| | | name: '', |
| | | key: '', |
| | | category: undefined, |
| | | icon: '', |
| | | icon: undefined, |
| | | description: '', |
| | | type: BpmModelType.BPMN, |
| | | formType: BpmModelFormType.NORMAL, |
| | | formId: '', |
| | | formCustomCreatePath: '', |
| | | formCustomViewPath: '' |
| | | formCustomViewPath: '', |
| | | visible: true, |
| | | startUserType: undefined, |
| | | managerUserType: undefined, |
| | | startUserIds: [], |
| | | managerUserIds: [] |
| | | } |
| | | formRef.value?.resetFields() |
| | | selectedStartUsers.value = [] |
| | | selectedManagerUsers.value = [] |
| | | } |
| | | |
| | | /** 处理发起人类型变化 */ |
| | | const handleStartUserTypeChange = (value: number) => { |
| | | if (value !== 1) { |
| | | selectedStartUsers.value = [] |
| | | formData.value.startUserIds = [] |
| | | } |
| | | } |
| | | |
| | | /** 处理管理员类型变化 */ |
| | | const handleManagerUserTypeChange = (value: number) => { |
| | | if (value !== 1) { |
| | | selectedManagerUsers.value = [] |
| | | formData.value.managerUserIds = [] |
| | | } |
| | | } |
| | | |
| | | /** 打开发起人选择 */ |
| | | const openStartUserSelect = () => { |
| | | currentSelectType.value = 'start' |
| | | userSelectFormRef.value.open(0, selectedStartUsers.value) |
| | | } |
| | | |
| | | /** 打开管理员选择 */ |
| | | const openManagerUserSelect = () => { |
| | | currentSelectType.value = 'manager' |
| | | userSelectFormRef.value.open(0, selectedManagerUsers.value) |
| | | } |
| | | |
| | | /** 处理用户选择确认 */ |
| | | const handleUserSelectConfirm = (_, users: UserVO[]) => { |
| | | if (currentSelectType.value === 'start') { |
| | | selectedStartUsers.value = users |
| | | formData.value.startUserIds = users.map((u) => u.id) |
| | | } else { |
| | | selectedManagerUsers.value = users |
| | | formData.value.managerUserIds = users.map((u) => u.id) |
| | | } |
| | | } |
| | | |
| | | /** 移除发起人 */ |
| | | const handleRemoveStartUser = (user: UserVO) => { |
| | | selectedStartUsers.value = selectedStartUsers.value.filter((u) => u.id !== user.id) |
| | | formData.value.startUserIds = formData.value.startUserIds.filter((id: number) => id !== user.id) |
| | | } |
| | | |
| | | /** 移除管理员 */ |
| | | const handleRemoveManagerUser = (user: UserVO) => { |
| | | selectedManagerUsers.value = selectedManagerUsers.value.filter((u) => u.id !== user.id) |
| | | formData.value.managerUserIds = formData.value.managerUserIds.filter( |
| | | (id: number) => id !== user.id |
| | | ) |
| | | } |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .bg-gray-100 { |
| | | background-color: #f5f7fa; |
| | | transition: all 0.3s; |
| | | |
| | | &:hover { |
| | | background-color: #e6e8eb; |
| | | } |
| | | |
| | | .ep-close { |
| | | font-size: 14px; |
| | | color: #909399; |
| | | transition: color 0.3s; |
| | | |
| | | &:hover { |
| | | color: #f56c6c; |
| | | } |
| | | } |
| | | } |
| | | </style> |
| | |
| | | <!-- 流程设计器,负责绘制流程等 --> |
| | | <MyProcessDesigner |
| | | key="designer" |
| | | v-if="xmlString !== undefined" |
| | | v-model="xmlString" |
| | | :value="xmlString" |
| | | v-bind="controlForm" |
| | |
| | | ref="processDesigner" |
| | | @init-finished="initModeler" |
| | | :additionalModel="controlForm.additionalModel" |
| | | :model="model" |
| | | @save="save" |
| | | /> |
| | | <!-- 流程属性器,负责编辑每个流程节点的属性 --> |
| | | <MyProcessPenal |
| | | v-if="isModelerReady && modeler" |
| | | key="penal" |
| | | :bpmnModeler="modeler as any" |
| | | :bpmnModeler="modeler" |
| | | :prefix="controlForm.prefix" |
| | | class="process-panel" |
| | | :model="model" |
| | |
| | | |
| | | defineOptions({ name: 'BpmModelEditor' }) |
| | | |
| | | const router = useRouter() // 路由 |
| | | const { query } = useRoute() // 路由的查询 |
| | | const props = defineProps<{ |
| | | modelId?: string |
| | | modelKey?: string |
| | | modelName?: string |
| | | value?: string |
| | | }>() |
| | | |
| | | const emit = defineEmits(['success', 'init-finished']) |
| | | const message = useMessage() // 国际化 |
| | | |
| | | const xmlString = ref(undefined) // BPMN XML |
| | | const modeler = ref(null) // BPMN Modeler |
| | | // 表单信息 |
| | | const formFields = ref<string[]>([]) |
| | | const formType = ref(20) |
| | | provide('formFields', formFields) |
| | | provide('formType', formType) |
| | | |
| | | const xmlString = ref<string>('') // BPMN XML |
| | | const modeler = shallowRef() // BPMN Modeler |
| | | const processDesigner = ref() |
| | | const isModelerReady = ref(false) |
| | | const controlForm = ref({ |
| | | simulation: true, |
| | | labelEditing: false, |
| | |
| | | }) |
| | | const model = ref<ModelApi.ModelVO>() // 流程模型的信息 |
| | | |
| | | // 初始化 bpmnInstances |
| | | const initBpmnInstances = () => { |
| | | if (!modeler.value) return false |
| | | try { |
| | | const instances = { |
| | | modeler: modeler.value, |
| | | modeling: modeler.value.get('modeling'), |
| | | moddle: modeler.value.get('moddle'), |
| | | eventBus: modeler.value.get('eventBus'), |
| | | bpmnFactory: modeler.value.get('bpmnFactory'), |
| | | elementFactory: modeler.value.get('elementFactory'), |
| | | elementRegistry: modeler.value.get('elementRegistry'), |
| | | replace: modeler.value.get('replace'), |
| | | selection: modeler.value.get('selection') |
| | | } |
| | | |
| | | // 检查所有实例是否都存在 |
| | | return Object.values(instances).every((instance) => instance) |
| | | } catch (error) { |
| | | console.error('初始化 bpmnInstances 失败:', error) |
| | | return false |
| | | } |
| | | } |
| | | |
| | | /** 初始化 modeler */ |
| | | const initModeler = (item) => { |
| | | setTimeout(() => { |
| | | const initModeler = async (item) => { |
| | | try { |
| | | modeler.value = item |
| | | }, 10) |
| | | // 等待 modeler 初始化完成 |
| | | await nextTick() |
| | | |
| | | // 确保 modeler 的所有实例都已经准备好 |
| | | if (initBpmnInstances()) { |
| | | isModelerReady.value = true |
| | | emit('init-finished') |
| | | |
| | | // 初始化完成后,设置初始值 |
| | | if (props.modelId) { |
| | | // 编辑模式 |
| | | const data = await ModelApi.getModel(props.modelId) |
| | | model.value = { |
| | | ...data, |
| | | bpmnXml: undefined // 清空 bpmnXml 属性 |
| | | } |
| | | xmlString.value = data.bpmnXml || getDefaultBpmnXml(data.key, data.name) |
| | | } else if (props.modelKey && props.modelName) { |
| | | // 新建模式 |
| | | xmlString.value = props.value || getDefaultBpmnXml(props.modelKey, props.modelName) |
| | | model.value = { |
| | | key: props.modelKey, |
| | | name: props.modelName |
| | | } as ModelApi.ModelVO |
| | | } |
| | | |
| | | // 导入XML并刷新视图 |
| | | await nextTick() |
| | | try { |
| | | await modeler.value.importXML(xmlString.value) |
| | | if (processDesigner.value?.refresh) { |
| | | processDesigner.value.refresh() |
| | | } |
| | | } catch (error) { |
| | | console.error('导入XML失败:', error) |
| | | } |
| | | } else { |
| | | console.error('modeler 实例未完全初始化') |
| | | } |
| | | } catch (error) { |
| | | console.error('初始化 modeler 失败:', error) |
| | | } |
| | | } |
| | | |
| | | /** 获取默认的BPMN XML */ |
| | | const getDefaultBpmnXml = (key: string, name: string) => { |
| | | return `<?xml version="1.0" encoding="UTF-8"?> |
| | | <definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.activiti.org/processdef"> |
| | | <process id="${key}" name="${name}" isExecutable="true" /> |
| | | <bpmndi:BPMNDiagram id="BPMNDiagram"> |
| | | <bpmndi:BPMNPlane id="${key}_di" bpmnElement="${key}" /> |
| | | </bpmndi:BPMNDiagram> |
| | | </definitions>` |
| | | } |
| | | |
| | | /** 添加/修改模型 */ |
| | | const save = async (bpmnXml) => { |
| | | const data = { |
| | | ...model.value, |
| | | bpmnXml: bpmnXml // bpmnXml 只是初始化流程图,后续修改无法通过它获得 |
| | | } as unknown as ModelApi.ModelVO |
| | | // 提交 |
| | | if (data.id) { |
| | | await ModelApi.updateModel(data) |
| | | message.success('修改成功') |
| | | } else { |
| | | await ModelApi.createModel(data) |
| | | message.success('新增成功') |
| | | const save = async (bpmnXml: string) => { |
| | | try { |
| | | xmlString.value = bpmnXml |
| | | if (props.modelId) { |
| | | // 编辑模式 |
| | | const data = { |
| | | ...model.value, |
| | | bpmnXml: bpmnXml |
| | | } as unknown as ModelApi.ModelVO |
| | | await ModelApi.updateModelBpmn(data) |
| | | emit('success') |
| | | } else { |
| | | // 新建模式,直接返回XML |
| | | emit('success', bpmnXml) |
| | | } |
| | | } catch (error) { |
| | | console.error('保存失败:', error) |
| | | message.error('保存失败') |
| | | } |
| | | // 跳转回去 |
| | | close() |
| | | } |
| | | |
| | | /** 关闭按钮 */ |
| | | const close = () => { |
| | | router.push({ path: '/bpm/manager/model' }) |
| | | // 监听 key、name 和 value 的变化 |
| | | watch( |
| | | [() => props.modelKey, () => props.modelName, () => props.value], |
| | | async ([newKey, newName, newValue]) => { |
| | | if (!props.modelId && isModelerReady.value) { |
| | | let shouldRefresh = false |
| | | |
| | | if (newKey && newName) { |
| | | const newXml = newValue || getDefaultBpmnXml(newKey, newName) |
| | | if (newXml !== xmlString.value) { |
| | | xmlString.value = newXml |
| | | shouldRefresh = true |
| | | } |
| | | model.value = { |
| | | ...model.value, |
| | | key: newKey, |
| | | name: newName |
| | | } as ModelApi.ModelVO |
| | | } else if (newValue && newValue !== xmlString.value) { |
| | | xmlString.value = newValue |
| | | shouldRefresh = true |
| | | } |
| | | |
| | | if (shouldRefresh) { |
| | | // 确保更新后重新渲染 |
| | | await nextTick() |
| | | if (processDesigner.value?.refresh) { |
| | | try { |
| | | await modeler.value?.importXML(xmlString.value) |
| | | processDesigner.value.refresh() |
| | | } catch (error) { |
| | | console.error('导入XML失败:', error) |
| | | } |
| | | } |
| | | } |
| | | } |
| | | }, |
| | | { deep: true } |
| | | ) |
| | | |
| | | // 在组件卸载时清理 |
| | | onBeforeUnmount(() => { |
| | | isModelerReady.value = false |
| | | modeler.value = null |
| | | // 清理全局实例 |
| | | const w = window as any |
| | | if (w.bpmnInstances) { |
| | | w.bpmnInstances = null |
| | | } |
| | | }) |
| | | |
| | | /** 获取 XML 字符串 */ |
| | | const saveXML = async () => { |
| | | if (!modeler.value) { |
| | | return { xml: xmlString.value } |
| | | } |
| | | try { |
| | | const result = await modeler.value.saveXML({ format: true }) |
| | | xmlString.value = result.xml |
| | | return result |
| | | } catch (error) { |
| | | console.error('获取XML失败:', error) |
| | | return { xml: xmlString.value } |
| | | } |
| | | } |
| | | |
| | | /** 初始化 */ |
| | | onMounted(async () => { |
| | | const modelId = query.modelId as unknown as number |
| | | if (!modelId) { |
| | | message.error('缺少模型 modelId 编号') |
| | | return |
| | | /** 获取SVG字符串 */ |
| | | const saveSVG = async () => { |
| | | if (!modeler.value) { |
| | | return { svg: undefined } |
| | | } |
| | | // 查询模型 |
| | | const data = await ModelApi.getModel(modelId) |
| | | if (!data.bpmnXml) { |
| | | // 首次创建的 Model 模型,它是没有 bpmnXml,此时需要给它一个默认的 |
| | | data.bpmnXml = ` <?xml version="1.0" encoding="UTF-8"?> |
| | | <definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.activiti.org/processdef"> |
| | | <process id="${data.key}" name="${data.name}" isExecutable="true" /> |
| | | <bpmndi:BPMNDiagram id="BPMNDiagram"> |
| | | <bpmndi:BPMNPlane id="${data.key}_di" bpmnElement="${data.key}" /> |
| | | </bpmndi:BPMNDiagram> |
| | | </definitions>` |
| | | try { |
| | | return await modeler.value.saveSVG() |
| | | } catch (error) { |
| | | console.error('获取SVG失败:', error) |
| | | return { svg: undefined } |
| | | } |
| | | model.value = { |
| | | ...data, |
| | | bpmnXml: undefined // 清空 bpmnXml 属性 |
| | | } |
| | | |
| | | /** 刷新视图 */ |
| | | const refresh = async () => { |
| | | if (processDesigner.value?.refresh && modeler.value) { |
| | | try { |
| | | await modeler.value.importXML(xmlString.value) |
| | | processDesigner.value.refresh() |
| | | } catch (error) { |
| | | console.error('刷新视图失败:', error) |
| | | } |
| | | } |
| | | xmlString.value = data.bpmnXml |
| | | } |
| | | |
| | | // 暴露必要的属性和方法给父组件 |
| | | defineExpose({ |
| | | modeler, |
| | | isModelerReady, |
| | | saveXML, |
| | | saveSVG, |
| | | refresh |
| | | }) |
| | | </script> |
| | | <style lang="scss"> |
| | | .process-panel__container { |
| | | position: absolute; |
| | | top: 90px; |
| | | right: 60px; |
| | | top: 172px; |
| | | right: 70px; |
| | | } |
| | | </style> |
对比新文件 |
| | |
| | | <template> |
| | | <el-form ref="formRef" :model="modelData" :rules="rules" label-width="120px" class="mt-20px"> |
| | | <el-form-item label="流程标识" prop="key" class="mb-20px"> |
| | | <div class="flex items-center"> |
| | | <el-input |
| | | class="!w-440px" |
| | | v-model="modelData.key" |
| | | :disabled="!!modelData.id" |
| | | placeholder="请输入流标标识" |
| | | /> |
| | | <el-tooltip |
| | | class="item" |
| | | :content="modelData.id ? '流程标识不可修改!' : '新建后,流程标识不可修改!'" |
| | | effect="light" |
| | | placement="top" |
| | | > |
| | | <Icon icon="ep:question-filled" class="ml-5px" /> |
| | | </el-tooltip> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="流程名称" prop="name" class="mb-20px"> |
| | | <el-input |
| | | v-model="modelData.name" |
| | | :disabled="!!modelData.id" |
| | | clearable |
| | | placeholder="请输入流程名称" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="流程分类" prop="category" class="mb-20px"> |
| | | <el-select |
| | | class="!w-full" |
| | | v-model="modelData.category" |
| | | clearable |
| | | placeholder="请选择流程分类" |
| | | > |
| | | <el-option |
| | | v-for="category in categoryList" |
| | | :key="category.code" |
| | | :label="category.name" |
| | | :value="category.code" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="流程图标" prop="icon" class="mb-20px"> |
| | | <UploadImg v-model="modelData.icon" :limit="1" height="64px" width="64px" /> |
| | | </el-form-item> |
| | | <el-form-item label="流程描述" prop="description" class="mb-20px"> |
| | | <el-input v-model="modelData.description" clearable type="textarea" /> |
| | | </el-form-item> |
| | | <el-form-item label="流程类型" prop="type" class="mb-20px"> |
| | | <el-radio-group v-model="modelData.type"> |
| | | <el-radio |
| | | v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_TYPE)" |
| | | :key="dict.value" |
| | | :value="dict.value" |
| | | > |
| | | {{ dict.label }} |
| | | </el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="是否可见" prop="visible" class="mb-20px"> |
| | | <el-radio-group v-model="modelData.visible"> |
| | | <el-radio |
| | | v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)" |
| | | :key="dict.value" |
| | | :value="dict.value" |
| | | > |
| | | {{ dict.label }} |
| | | </el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="谁可以发起" prop="startUserType" class="mb-20px"> |
| | | <el-select |
| | | v-model="modelData.startUserType" |
| | | placeholder="请选择谁可以发起" |
| | | @change="handleStartUserTypeChange" |
| | | > |
| | | <el-option label="全员" :value="0" /> |
| | | <el-option label="指定人员" :value="1" /> |
| | | <el-option label="均不可提交" :value="2" /> |
| | | </el-select> |
| | | <div v-if="modelData.startUserType === 1" class="mt-2 flex flex-wrap gap-2"> |
| | | <div |
| | | v-for="user in selectedStartUsers" |
| | | :key="user.id" |
| | | class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative" |
| | | > |
| | | <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" /> |
| | | <el-avatar class="!m-5px" :size="28" v-else> |
| | | {{ user.nickname.substring(0, 1) }} |
| | | </el-avatar> |
| | | {{ user.nickname }} |
| | | <Icon |
| | | icon="ep:close" |
| | | class="ml-2 cursor-pointer hover:text-red-500" |
| | | @click="handleRemoveStartUser(user)" |
| | | /> |
| | | </div> |
| | | <el-button type="primary" link @click="openStartUserSelect"> |
| | | <Icon icon="ep:plus" />选择人员 |
| | | </el-button> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="流程管理员" prop="managerUserType" class="mb-20px"> |
| | | <el-select |
| | | v-model="modelData.managerUserType" |
| | | placeholder="请选择流程管理员" |
| | | @change="handleManagerUserTypeChange" |
| | | > |
| | | <el-option label="全员" :value="0" /> |
| | | <el-option label="指定人员" :value="1" /> |
| | | <el-option label="均不可提交" :value="2" /> |
| | | </el-select> |
| | | <div v-if="modelData.managerUserType === 1" class="mt-2 flex flex-wrap gap-2"> |
| | | <div |
| | | v-for="user in selectedManagerUsers" |
| | | :key="user.id" |
| | | class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative" |
| | | > |
| | | <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" /> |
| | | <el-avatar class="!m-5px" :size="28" v-else> |
| | | {{ user.nickname.substring(0, 1) }} |
| | | </el-avatar> |
| | | {{ user.nickname }} |
| | | <Icon |
| | | icon="ep:close" |
| | | class="ml-2 cursor-pointer hover:text-red-500" |
| | | @click="handleRemoveManagerUser(user)" |
| | | /> |
| | | </div> |
| | | <el-button type="primary" link @click="openManagerUserSelect"> |
| | | <Icon icon="ep:plus" />选择人员 |
| | | </el-button> |
| | | </div> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <!-- 用户选择弹窗 --> |
| | | <UserSelectForm ref="userSelectFormRef" @confirm="handleUserSelectConfirm" /> |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '@/utils/dict' |
| | | import { UserVO } from '@/api/system/user' |
| | | |
| | | const props = defineProps({ |
| | | modelValue: { |
| | | type: Object, |
| | | required: true |
| | | }, |
| | | categoryList: { |
| | | type: Array, |
| | | required: true |
| | | }, |
| | | userList: { |
| | | type: Array, |
| | | required: true |
| | | } |
| | | }) |
| | | |
| | | const emit = defineEmits(['update:modelValue']) |
| | | |
| | | const formRef = ref() |
| | | const selectedStartUsers = ref<UserVO[]>([]) |
| | | const selectedManagerUsers = ref<UserVO[]>([]) |
| | | const userSelectFormRef = ref() |
| | | const currentSelectType = ref<'start' | 'manager'>('start') |
| | | |
| | | const rules = { |
| | | name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }], |
| | | key: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }], |
| | | category: [{ required: true, message: '流程分类不能为空', trigger: 'blur' }], |
| | | icon: [{ required: true, message: '流程图标不能为空', trigger: 'blur' }], |
| | | type: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }], |
| | | visible: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }], |
| | | managerUserIds: [{ required: true, message: '流程管理员不能为空', trigger: 'blur' }] |
| | | } |
| | | |
| | | // 创建本地数据副本 |
| | | const modelData = computed({ |
| | | get: () => props.modelValue, |
| | | set: (val) => emit('update:modelValue', val) |
| | | }) |
| | | |
| | | // 初始化选中的用户 |
| | | watch( |
| | | () => props.modelValue, |
| | | (newVal) => { |
| | | if (newVal.startUserIds?.length) { |
| | | selectedStartUsers.value = props.userList.filter((user: UserVO) => |
| | | newVal.startUserIds.includes(user.id) |
| | | ) as UserVO[] |
| | | } |
| | | if (newVal.managerUserIds?.length) { |
| | | selectedManagerUsers.value = props.userList.filter((user: UserVO) => |
| | | newVal.managerUserIds.includes(user.id) |
| | | ) as UserVO[] |
| | | } |
| | | }, |
| | | { immediate: true } |
| | | ) |
| | | |
| | | /** 打开发起人选择 */ |
| | | const openStartUserSelect = () => { |
| | | currentSelectType.value = 'start' |
| | | userSelectFormRef.value.open(0, selectedStartUsers.value) |
| | | } |
| | | |
| | | /** 打开管理员选择 */ |
| | | const openManagerUserSelect = () => { |
| | | currentSelectType.value = 'manager' |
| | | userSelectFormRef.value.open(0, selectedManagerUsers.value) |
| | | } |
| | | |
| | | /** 处理用户选择确认 */ |
| | | const handleUserSelectConfirm = (_, users: UserVO[]) => { |
| | | if (currentSelectType.value === 'start') { |
| | | selectedStartUsers.value = users |
| | | emit('update:modelValue', { |
| | | ...modelData.value, |
| | | startUserIds: users.map((u) => u.id) |
| | | }) |
| | | } else { |
| | | selectedManagerUsers.value = users |
| | | emit('update:modelValue', { |
| | | ...modelData.value, |
| | | managerUserIds: users.map((u) => u.id) |
| | | }) |
| | | } |
| | | } |
| | | |
| | | /** 处理发起人类型变化 */ |
| | | const handleStartUserTypeChange = (value: number) => { |
| | | if (value !== 1) { |
| | | selectedStartUsers.value = [] |
| | | emit('update:modelValue', { |
| | | ...modelData.value, |
| | | startUserIds: [] |
| | | }) |
| | | } |
| | | } |
| | | |
| | | /** 处理管理员类型变化 */ |
| | | const handleManagerUserTypeChange = (value: number) => { |
| | | if (value !== 1) { |
| | | selectedManagerUsers.value = [] |
| | | emit('update:modelValue', { |
| | | ...modelData.value, |
| | | managerUserIds: [] |
| | | }) |
| | | } |
| | | } |
| | | |
| | | /** 移除发起人 */ |
| | | const handleRemoveStartUser = (user: UserVO) => { |
| | | selectedStartUsers.value = selectedStartUsers.value.filter((u) => u.id !== user.id) |
| | | emit('update:modelValue', { |
| | | ...modelData.value, |
| | | startUserIds: modelData.value.startUserIds.filter((id: number) => id !== user.id) |
| | | }) |
| | | } |
| | | |
| | | /** 移除管理员 */ |
| | | const handleRemoveManagerUser = (user: UserVO) => { |
| | | selectedManagerUsers.value = selectedManagerUsers.value.filter((u) => u.id !== user.id) |
| | | emit('update:modelValue', { |
| | | ...modelData.value, |
| | | managerUserIds: modelData.value.managerUserIds.filter((id: number) => id !== user.id) |
| | | }) |
| | | } |
| | | |
| | | /** 表单校验 */ |
| | | const validate = async () => { |
| | | await formRef.value?.validate() |
| | | } |
| | | |
| | | defineExpose({ |
| | | validate |
| | | }) |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .bg-gray-100 { |
| | | background-color: #f5f7fa; |
| | | transition: all 0.3s; |
| | | |
| | | &:hover { |
| | | background-color: #e6e8eb; |
| | | } |
| | | |
| | | .ep-close { |
| | | font-size: 14px; |
| | | color: #909399; |
| | | transition: color 0.3s; |
| | | |
| | | &:hover { |
| | | color: #f56c6c; |
| | | } |
| | | } |
| | | } |
| | | </style> |
对比新文件 |
| | |
| | | <template> |
| | | <el-form ref="formRef" :model="modelData" :rules="rules" label-width="120px" class="mt-20px"> |
| | | <el-form-item label="表单类型" prop="formType" class="mb-20px"> |
| | | <el-radio-group v-model="modelData.formType"> |
| | | <el-radio |
| | | v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_FORM_TYPE)" |
| | | :key="dict.value" |
| | | :value="dict.value" |
| | | > |
| | | {{ dict.label }} |
| | | </el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item v-if="modelData.formType === 10" label="流程表单" prop="formId"> |
| | | <el-select v-model="modelData.formId" clearable style="width: 100%"> |
| | | <el-option v-for="form in formList" :key="form.id" :label="form.name" :value="form.id" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item v-if="modelData.formType === 20" label="表单提交路由" prop="formCustomCreatePath"> |
| | | <el-input |
| | | v-model="modelData.formCustomCreatePath" |
| | | placeholder="请输入表单提交路由" |
| | | style="width: 330px" |
| | | /> |
| | | <el-tooltip |
| | | class="item" |
| | | content="自定义表单的提交路径,使用 Vue 的路由地址,例如说:bpm/oa/leave/create.vue" |
| | | effect="light" |
| | | placement="top" |
| | | > |
| | | <Icon icon="ep:question" class="ml-5px" /> |
| | | </el-tooltip> |
| | | </el-form-item> |
| | | <el-form-item v-if="modelData.formType === 20" label="表单查看地址" prop="formCustomViewPath"> |
| | | <el-input |
| | | v-model="modelData.formCustomViewPath" |
| | | placeholder="请输入表单查看的组件地址" |
| | | style="width: 330px" |
| | | /> |
| | | <el-tooltip |
| | | class="item" |
| | | content="自定义表单的查看组件地址,使用 Vue 的组件地址,例如说:bpm/oa/leave/detail.vue" |
| | | effect="light" |
| | | placement="top" |
| | | > |
| | | <Icon icon="ep:question" class="ml-5px" /> |
| | | </el-tooltip> |
| | | </el-form-item> |
| | | <!-- 表单预览 --> |
| | | <div |
| | | v-if="modelData.formType === 10 && modelData.formId && formPreview.rule.length > 0" |
| | | class="mt-20px" |
| | | > |
| | | <div class="flex items-center mb-15px"> |
| | | <div class="h-15px w-4px bg-[#1890ff] mr-10px"></div> |
| | | <span class="text-15px font-bold">表单预览</span> |
| | | </div> |
| | | <form-create |
| | | v-model="formPreview.formData" |
| | | :rule="formPreview.rule" |
| | | :option="formPreview.option" |
| | | /> |
| | | </div> |
| | | </el-form> |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' |
| | | import * as FormApi from '@/api/bpm/form' |
| | | import { setConfAndFields2 } from '@/utils/formCreate' |
| | | |
| | | const props = defineProps({ |
| | | modelValue: { |
| | | type: Object, |
| | | required: true |
| | | }, |
| | | formList: { |
| | | type: Array, |
| | | required: true |
| | | } |
| | | }) |
| | | |
| | | const emit = defineEmits(['update:modelValue']) |
| | | |
| | | const formRef = ref() |
| | | |
| | | // 创建本地数据副本 |
| | | const modelData = computed({ |
| | | get: () => props.modelValue, |
| | | set: (val) => emit('update:modelValue', val) |
| | | }) |
| | | |
| | | // 表单预览数据 |
| | | const formPreview = ref({ |
| | | formData: {}, |
| | | rule: [], |
| | | option: { |
| | | submitBtn: false, |
| | | resetBtn: false, |
| | | formData: {} |
| | | } |
| | | }) |
| | | |
| | | // 监听表单ID变化,加载表单数据 |
| | | watch( |
| | | () => modelData.value.formId, |
| | | async (newFormId) => { |
| | | if (newFormId && modelData.value.formType === 10) { |
| | | const data = await FormApi.getForm(newFormId) |
| | | setConfAndFields2(formPreview.value, data.conf, data.fields) |
| | | // 设置只读 |
| | | formPreview.value.rule.forEach((item: any) => { |
| | | item.props = { ...item.props, disabled: true } |
| | | }) |
| | | } else { |
| | | formPreview.value.rule = [] |
| | | } |
| | | }, |
| | | { immediate: true } |
| | | ) |
| | | |
| | | const rules = { |
| | | formType: [{ required: true, message: '表单类型不能为空', trigger: 'blur' }], |
| | | formId: [{ required: true, message: '流程表单不能为空', trigger: 'blur' }], |
| | | formCustomCreatePath: [{ required: true, message: '表单提交路由不能为空', trigger: 'blur' }], |
| | | formCustomViewPath: [{ required: true, message: '表单查看地址不能为空', trigger: 'blur' }] |
| | | } |
| | | |
| | | /** 表单校验 */ |
| | | const validate = async () => { |
| | | await formRef.value?.validate() |
| | | } |
| | | |
| | | defineExpose({ |
| | | validate |
| | | }) |
| | | </script> |
对比新文件 |
| | |
| | | <template> |
| | | <!-- BPMN设计器 --> |
| | | <template v-if="modelData.type === BpmModelType.BPMN"> |
| | | <BpmModelEditor |
| | | v-if="showDesigner" |
| | | :model-id="modelData.id" |
| | | :model-key="modelData.key" |
| | | :model-name="modelData.name" |
| | | :value="currentBpmnXml" |
| | | ref="bpmnEditorRef" |
| | | @success="handleDesignSuccess" |
| | | @init-finished="handleEditorInit" |
| | | /> |
| | | </template> |
| | | |
| | | <!-- Simple设计器 --> |
| | | <template v-else> |
| | | <SimpleModelDesign |
| | | v-if="showDesigner" |
| | | :model-id="modelData.id" |
| | | :model-key="modelData.key" |
| | | :model-name="modelData.name" |
| | | :start-user-ids="modelData.startUserIds" |
| | | :value="currentSimpleModel" |
| | | ref="simpleEditorRef" |
| | | @success="handleDesignSuccess" |
| | | @init-finished="handleEditorInit" |
| | | /> |
| | | </template> |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | import { BpmModelType } from '@/utils/constants' |
| | | import BpmModelEditor from '../editor/index.vue' |
| | | import SimpleModelDesign from '../../simple/SimpleModelDesign.vue' |
| | | |
| | | const props = defineProps({ |
| | | modelValue: { |
| | | type: Object, |
| | | required: true |
| | | } |
| | | }) |
| | | |
| | | const emit = defineEmits(['update:modelValue', 'success']) |
| | | |
| | | const bpmnEditorRef = ref() |
| | | const simpleEditorRef = ref() |
| | | const isEditorInitialized = ref(false) |
| | | |
| | | // 创建本地数据副本 |
| | | const modelData = computed({ |
| | | get: () => props.modelValue, |
| | | set: (val) => emit('update:modelValue', val) |
| | | }) |
| | | |
| | | // 保存当前的流程XML或数据 |
| | | const currentBpmnXml = ref('') |
| | | const currentSimpleModel = ref('') |
| | | |
| | | // 初始化或更新当前的XML数据 |
| | | const initOrUpdateXmlData = () => { |
| | | if (modelData.value) { |
| | | if (modelData.value.type === BpmModelType.BPMN) { |
| | | currentBpmnXml.value = modelData.value.bpmnXml || '' |
| | | } else { |
| | | currentSimpleModel.value = modelData.value.simpleModel || '' |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 监听modelValue的变化,更新数据 |
| | | watch( |
| | | () => props.modelValue, |
| | | (newVal) => { |
| | | if (newVal) { |
| | | if (newVal.type === BpmModelType.BPMN) { |
| | | if (newVal.bpmnXml && newVal.bpmnXml !== currentBpmnXml.value) { |
| | | currentBpmnXml.value = newVal.bpmnXml |
| | | // 如果编辑器已经初始化,刷新视图 |
| | | if (isEditorInitialized.value && bpmnEditorRef.value?.refresh) { |
| | | nextTick(() => { |
| | | bpmnEditorRef.value.refresh() |
| | | }) |
| | | } |
| | | } |
| | | } else { |
| | | if (newVal.simpleModel && newVal.simpleModel !== currentSimpleModel.value) { |
| | | currentSimpleModel.value = newVal.simpleModel |
| | | // 如果编辑器已经初始化,刷新视图 |
| | | if (isEditorInitialized.value && simpleEditorRef.value?.refresh) { |
| | | nextTick(() => { |
| | | simpleEditorRef.value.refresh() |
| | | }) |
| | | } |
| | | } |
| | | } |
| | | } |
| | | }, |
| | | { immediate: true, deep: true } |
| | | ) |
| | | |
| | | /** 编辑器初始化完成的回调 */ |
| | | const handleEditorInit = async () => { |
| | | isEditorInitialized.value = true |
| | | |
| | | // 等待下一个tick,确保编辑器已经准备好 |
| | | await nextTick() |
| | | |
| | | // 初始化完成后,设置初始值 |
| | | if (modelData.value.type === BpmModelType.BPMN) { |
| | | if (modelData.value.bpmnXml) { |
| | | currentBpmnXml.value = modelData.value.bpmnXml |
| | | if (bpmnEditorRef.value?.refresh) { |
| | | await nextTick() |
| | | bpmnEditorRef.value.refresh() |
| | | } |
| | | } |
| | | } else { |
| | | if (modelData.value.simpleModel) { |
| | | currentSimpleModel.value = modelData.value.simpleModel |
| | | if (simpleEditorRef.value?.refresh) { |
| | | await nextTick() |
| | | simpleEditorRef.value.refresh() |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** 获取当前流程数据 */ |
| | | const getProcessData = async () => { |
| | | try { |
| | | if (modelData.value.type === BpmModelType.BPMN) { |
| | | if (!bpmnEditorRef.value || !isEditorInitialized.value) { |
| | | return currentBpmnXml.value || undefined |
| | | } |
| | | const { xml } = await bpmnEditorRef.value.saveXML() |
| | | if (xml) { |
| | | currentBpmnXml.value = xml |
| | | return xml |
| | | } |
| | | } else { |
| | | if (!simpleEditorRef.value || !isEditorInitialized.value) { |
| | | return currentSimpleModel.value || undefined |
| | | } |
| | | const flowData = await simpleEditorRef.value.getCurrentFlowData() |
| | | if (flowData) { |
| | | currentSimpleModel.value = flowData |
| | | return flowData |
| | | } |
| | | } |
| | | return modelData.value.type === BpmModelType.BPMN |
| | | ? currentBpmnXml.value |
| | | : currentSimpleModel.value |
| | | } catch (error) { |
| | | console.error('获取流程数据失败:', error) |
| | | return modelData.value.type === BpmModelType.BPMN |
| | | ? currentBpmnXml.value |
| | | : currentSimpleModel.value |
| | | } |
| | | } |
| | | |
| | | /** 表单校验 */ |
| | | const validate = async () => { |
| | | try { |
| | | // 获取最新的流程数据 |
| | | const processData = await getProcessData() |
| | | if (!processData) { |
| | | throw new Error('请设计流程') |
| | | } |
| | | return true |
| | | } catch (error) { |
| | | throw error |
| | | } |
| | | } |
| | | |
| | | /** 处理设计器保存成功 */ |
| | | const handleDesignSuccess = async (data?: any) => { |
| | | if (data) { |
| | | if (modelData.value.type === BpmModelType.BPMN) { |
| | | currentBpmnXml.value = data |
| | | } else { |
| | | currentSimpleModel.value = data |
| | | } |
| | | |
| | | // 创建新的对象以触发响应式更新 |
| | | const newModelData = { |
| | | ...modelData.value, |
| | | bpmnXml: modelData.value.type === BpmModelType.BPMN ? data : null, |
| | | simpleModel: modelData.value.type === BpmModelType.BPMN ? null : data |
| | | } |
| | | |
| | | // 使用emit更新父组件的数据 |
| | | await nextTick() |
| | | emit('update:modelValue', newModelData) |
| | | emit('success', data) |
| | | } |
| | | } |
| | | |
| | | /** 是否显示设计器 */ |
| | | const showDesigner = computed(() => { |
| | | return Boolean(modelData.value?.key && modelData.value?.name) |
| | | }) |
| | | |
| | | // 组件创建时初始化数据 |
| | | onMounted(() => { |
| | | initOrUpdateXmlData() |
| | | }) |
| | | |
| | | // 组件卸载前保存数据 |
| | | onBeforeUnmount(async () => { |
| | | try { |
| | | // 获取并保存最新的流程数据 |
| | | const data = await getProcessData() |
| | | if (data) { |
| | | // 创建新的对象以触发响应式更新 |
| | | const newModelData = { |
| | | ...modelData.value, |
| | | bpmnXml: modelData.value.type === BpmModelType.BPMN ? data : null, |
| | | simpleModel: modelData.value.type === BpmModelType.BPMN ? null : data |
| | | } |
| | | |
| | | // 使用emit更新父组件的数据 |
| | | await nextTick() |
| | | emit('update:modelValue', newModelData) |
| | | } |
| | | } catch (error) { |
| | | console.error('保存数据失败:', error) |
| | | } |
| | | }) |
| | | |
| | | defineExpose({ |
| | | validate, |
| | | getProcessData |
| | | }) |
| | | </script> |
对比新文件 |
| | |
| | | <template> |
| | | <ContentWrap> |
| | | <div class="mx-auto"> |
| | | <!-- 头部导航栏 --> |
| | | <div |
| | | class="absolute top-0 left-0 right-0 h-50px bg-white border-bottom z-10 flex items-center px-20px" |
| | | > |
| | | <!-- 左侧标题 --> |
| | | <div class="w-200px flex items-center overflow-hidden"> |
| | | <Icon icon="ep:arrow-left" class="cursor-pointer flex-shrink-0" @click="handleBack" /> |
| | | <span class="ml-10px text-16px truncate" :title="formData.name || '创建流程'"> |
| | | {{ formData.name || '创建流程' }} |
| | | </span> |
| | | </div> |
| | | |
| | | <!-- 步骤条 --> |
| | | <div class="flex-1 flex items-center justify-center h-full"> |
| | | <div class="w-400px flex items-center justify-between h-full"> |
| | | <div |
| | | v-for="(step, index) in steps" |
| | | :key="index" |
| | | class="flex items-center cursor-pointer mx-15px relative h-full" |
| | | :class="[ |
| | | currentStep === index |
| | | ? 'text-[#3473ff] border-[#3473ff] border-b-2 border-b-solid' |
| | | : 'text-gray-500' |
| | | ]" |
| | | @click="handleStepClick(index)" |
| | | > |
| | | <div |
| | | class="w-28px h-28px rounded-full flex items-center justify-center mr-8px border-2 border-solid text-15px" |
| | | :class="[ |
| | | currentStep === index |
| | | ? 'bg-[#3473ff] text-white border-[#3473ff]' |
| | | : 'border-gray-300 bg-white text-gray-500' |
| | | ]" |
| | | > |
| | | {{ index + 1 }} |
| | | </div> |
| | | <span class="text-16px font-bold whitespace-nowrap">{{ step.title }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 右侧按钮 --> |
| | | <div class="w-200px flex items-center justify-end gap-2"> |
| | | <el-button v-if="route.params.id" type="success" @click="handleDeploy">发 布</el-button> |
| | | <el-button type="primary" @click="handleSave">保 存</el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 主体内容 --> |
| | | <div class="mt-50px"> |
| | | <!-- 第一步:基本信息 --> |
| | | <div v-if="currentStep === 0" class="mx-auto w-560px"> |
| | | <BasicInfo |
| | | v-model="formData" |
| | | :categoryList="categoryList" |
| | | :userList="userList" |
| | | ref="basicInfoRef" |
| | | /> |
| | | </div> |
| | | |
| | | <!-- 第二步:表单设计 --> |
| | | <div v-if="currentStep === 1" class="mx-auto w-560px"> |
| | | <FormDesign v-model="formData" :formList="formList" ref="formDesignRef" /> |
| | | </div> |
| | | |
| | | <!-- 第三步:流程设计 --> |
| | | <ProcessDesign |
| | | v-if="currentStep === 2" |
| | | v-model="formData" |
| | | ref="processDesignRef" |
| | | @success="handleDesignSuccess" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </ContentWrap> |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { useMessage } from '@/hooks/web/useMessage' |
| | | import * as ModelApi from '@/api/bpm/model' |
| | | import * as FormApi from '@/api/bpm/form' |
| | | import { CategoryApi } from '@/api/bpm/category' |
| | | import * as UserApi from '@/api/system/user' |
| | | import { useUserStoreWithOut } from '@/store/modules/user' |
| | | import { BpmModelFormType, BpmModelType } from '@/utils/constants' |
| | | import BasicInfo from './BasicInfo.vue' |
| | | import FormDesign from './FormDesign.vue' |
| | | import ProcessDesign from './ProcessDesign.vue' |
| | | import { useTagsViewStore } from '@/store/modules/tagsView' |
| | | |
| | | const router = useRouter() |
| | | const { delView } = useTagsViewStore() // 视图操作 |
| | | const route = useRoute() |
| | | const message = useMessage() |
| | | const userStore = useUserStoreWithOut() |
| | | |
| | | // 组件引用 |
| | | const basicInfoRef = ref() |
| | | const formDesignRef = ref() |
| | | const processDesignRef = ref() |
| | | |
| | | /** 步骤校验函数 */ |
| | | const validateBasic = async () => { |
| | | await basicInfoRef.value?.validate() |
| | | } |
| | | |
| | | /** 表单设计校验 */ |
| | | const validateForm = async () => { |
| | | await formDesignRef.value?.validate() |
| | | } |
| | | |
| | | /** 流程设计校验 */ |
| | | const validateProcess = async () => { |
| | | await processDesignRef.value?.validate() |
| | | } |
| | | |
| | | const currentStep = ref(0) // 步骤控制 |
| | | const steps = [ |
| | | { title: '基本信息', validator: validateBasic }, |
| | | { title: '表单设计', validator: validateForm }, |
| | | { title: '流程设计', validator: validateProcess } |
| | | ] |
| | | |
| | | // 表单数据 |
| | | const formData: any = ref({ |
| | | id: undefined, |
| | | name: '', |
| | | key: '', |
| | | category: undefined, |
| | | icon: undefined, |
| | | description: '', |
| | | type: BpmModelType.BPMN, |
| | | formType: BpmModelFormType.NORMAL, |
| | | formId: '', |
| | | formCustomCreatePath: '', |
| | | formCustomViewPath: '', |
| | | visible: true, |
| | | startUserType: undefined, |
| | | managerUserType: undefined, |
| | | startUserIds: [], |
| | | managerUserIds: [] |
| | | }) |
| | | |
| | | // 数据列表 |
| | | const formList = ref([]) |
| | | const categoryList = ref([]) |
| | | const userList = ref<UserApi.UserVO[]>([]) |
| | | |
| | | /** 初始化数据 */ |
| | | const initData = async () => { |
| | | const modelId = route.params.id as string |
| | | if (modelId) { |
| | | // 修改场景 |
| | | formData.value = await ModelApi.getModel(modelId) |
| | | } else { |
| | | // 新增场景 |
| | | formData.value.managerUserIds.push(userStore.getUser.id) |
| | | } |
| | | |
| | | // 获取表单列表 |
| | | formList.value = await FormApi.getFormSimpleList() |
| | | // 获取分类列表 |
| | | categoryList.value = await CategoryApi.getCategorySimpleList() |
| | | // 获取用户列表 |
| | | userList.value = await UserApi.getSimpleUserList() |
| | | } |
| | | |
| | | /** 校验所有步骤数据是否完整 */ |
| | | const validateAllSteps = async () => { |
| | | try { |
| | | // 基本信息校验 |
| | | await basicInfoRef.value?.validate() |
| | | if (!formData.value.key || !formData.value.name || !formData.value.category) { |
| | | currentStep.value = 0 |
| | | throw new Error('请完善基本信息') |
| | | } |
| | | |
| | | // 表单设计校验 |
| | | await formDesignRef.value?.validate() |
| | | if (formData.value.formType === 10 && !formData.value.formId) { |
| | | currentStep.value = 1 |
| | | throw new Error('请选择流程表单') |
| | | } |
| | | if ( |
| | | formData.value.formType === 20 && |
| | | (!formData.value.formCustomCreatePath || !formData.value.formCustomViewPath) |
| | | ) { |
| | | currentStep.value = 1 |
| | | throw new Error('请完善自定义表单信息') |
| | | } |
| | | |
| | | // 流程设计校验 |
| | | // 如果已经有流程数据,则不需要重新校验 |
| | | if (!formData.value.bpmnXml && !formData.value.simpleModel) { |
| | | // 如果当前不在第三步,需要先保存当前步骤数据 |
| | | if (currentStep.value !== 2) { |
| | | await steps[currentStep.value].validator() |
| | | // 切换到第三步 |
| | | currentStep.value = 2 |
| | | // 等待组件渲染完成 |
| | | await nextTick() |
| | | } |
| | | |
| | | // 校验流程设计 |
| | | await processDesignRef.value?.validate() |
| | | const processData = await processDesignRef.value?.getProcessData() |
| | | if (!processData) { |
| | | throw new Error('请设计流程') |
| | | } |
| | | |
| | | // 保存流程数据 |
| | | if (formData.value.type === BpmModelType.BPMN) { |
| | | formData.value.bpmnXml = processData |
| | | formData.value.simpleModel = null |
| | | } else { |
| | | formData.value.bpmnXml = null |
| | | formData.value.simpleModel = processData |
| | | } |
| | | } |
| | | |
| | | return true |
| | | } catch (error) { |
| | | throw error |
| | | } |
| | | } |
| | | |
| | | /** 保存操作 */ |
| | | const handleSave = async () => { |
| | | try { |
| | | // 保存前校验所有步骤的数据 |
| | | await validateAllSteps() |
| | | |
| | | // 更新表单数据 |
| | | const modelData = { |
| | | ...formData.value |
| | | } |
| | | |
| | | // 如果当前在第三步,获取最新的流程设计数据 |
| | | if (currentStep.value === 2) { |
| | | const processData = await processDesignRef.value?.getProcessData() |
| | | if (processData) { |
| | | if (formData.value.type === BpmModelType.BPMN) { |
| | | modelData.bpmnXml = processData |
| | | modelData.simpleModel = null |
| | | } else { |
| | | modelData.bpmnXml = null |
| | | modelData.simpleModel = processData |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (formData.value.id) { |
| | | // 修改场景 |
| | | await ModelApi.updateModel(modelData) |
| | | // 询问是否发布流程 |
| | | try { |
| | | await message.confirm('修改流程成功,是否发布流程?') |
| | | // 用户点击确认,执行发布 |
| | | await handleDeploy() |
| | | } catch { |
| | | // 用户点击取消,停留在当前页面 |
| | | } |
| | | } else { |
| | | // 新增场景 |
| | | formData.value.id = await ModelApi.createModel(modelData) |
| | | message.success('新增成功') |
| | | try { |
| | | await message.confirm('创建流程成功,是否继续编辑?') |
| | | // 用户点击继续编辑,跳转到编辑页面 |
| | | await nextTick() |
| | | // 先删除当前页签 |
| | | delView(unref(router.currentRoute)) |
| | | // 跳转到编辑页面 |
| | | await router.push({ |
| | | name: 'BpmModelUpdate', |
| | | params: { id: formData.value.id } |
| | | }) |
| | | } catch { |
| | | // 先删除当前页签 |
| | | delView(unref(router.currentRoute)) |
| | | // 用户点击返回列表 |
| | | await router.push({ name: 'BpmModel' }) |
| | | } |
| | | } |
| | | } catch (error: any) { |
| | | console.error('保存失败:', error) |
| | | message.warning(error.message || '请完善所有步骤的必填信息') |
| | | } |
| | | } |
| | | |
| | | /** 发布操作 */ |
| | | const handleDeploy = async () => { |
| | | try { |
| | | // 修改场景下直接发布,新增场景下需要先确认 |
| | | if (!formData.value.id) { |
| | | await message.confirm('是否确认发布该流程?') |
| | | } |
| | | |
| | | // 校验所有步骤 |
| | | await validateAllSteps() |
| | | |
| | | // 更新表单数据 |
| | | const modelData = { |
| | | ...formData.value |
| | | } |
| | | |
| | | // 如果当前在第三步,获取最新的流程设计数据 |
| | | if (currentStep.value === 2) { |
| | | const processData = await processDesignRef.value?.getProcessData() |
| | | if (processData) { |
| | | if (formData.value.type === BpmModelType.BPMN) { |
| | | modelData.bpmnXml = processData |
| | | modelData.simpleModel = null |
| | | } else { |
| | | modelData.bpmnXml = null |
| | | modelData.simpleModel = processData |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 先保存所有数据 |
| | | if (formData.value.id) { |
| | | await ModelApi.updateModel(modelData) |
| | | } else { |
| | | const result = await ModelApi.createModel(modelData) |
| | | formData.value.id = result.id |
| | | } |
| | | |
| | | // 发布 |
| | | await ModelApi.deployModel(formData.value.id) |
| | | message.success('发布成功') |
| | | // 返回列表页 |
| | | await router.push({ name: 'BpmModel' }) |
| | | } catch (error: any) { |
| | | console.error('发布失败:', error) |
| | | message.warning(error.message || '发布失败') |
| | | } |
| | | } |
| | | |
| | | /** 步骤切换处理 */ |
| | | const handleStepClick = async (index: number) => { |
| | | try { |
| | | // 如果是切换到第三步(流程设计),需要校验key和name |
| | | if (index === 2) { |
| | | if (!formData.value.key || !formData.value.name) { |
| | | message.warning('请先填写流程标识和流程名称') |
| | | return |
| | | } |
| | | } |
| | | |
| | | // 保存当前步骤的数据 |
| | | if (currentStep.value === 2) { |
| | | const processData = await processDesignRef.value?.getProcessData() |
| | | if (processData) { |
| | | if (formData.value.type === BpmModelType.BPMN) { |
| | | formData.value.bpmnXml = processData |
| | | formData.value.simpleModel = null |
| | | } else { |
| | | formData.value.bpmnXml = null |
| | | formData.value.simpleModel = processData |
| | | } |
| | | } |
| | | } else { |
| | | // 只有在向后切换时才进行校验 |
| | | if (index > currentStep.value) { |
| | | if (typeof steps[currentStep.value].validator === 'function') { |
| | | await steps[currentStep.value].validator() |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 切换步骤 |
| | | currentStep.value = index |
| | | |
| | | // 如果切换到流程设计步骤,等待组件渲染完成后刷新设计器 |
| | | if (index === 2) { |
| | | await nextTick() |
| | | // 等待更长时间确保组件完全初始化 |
| | | await new Promise(resolve => setTimeout(resolve, 200)) |
| | | if (processDesignRef.value?.refresh) { |
| | | await processDesignRef.value.refresh() |
| | | } |
| | | } |
| | | } catch (error) { |
| | | console.error('步骤切换失败:', error) |
| | | message.warning('请先完善当前步骤必填信息') |
| | | } |
| | | } |
| | | |
| | | /** 处理设计器保存成功 */ |
| | | const handleDesignSuccess = (bpmnXml?: string) => { |
| | | if (bpmnXml) { |
| | | formData.value.bpmnXml = bpmnXml |
| | | } |
| | | } |
| | | |
| | | /** 返回列表页 */ |
| | | const handleBack = () => { |
| | | // 先删除当前页签 |
| | | delView(unref(router.currentRoute)) |
| | | // 跳转到列表页 |
| | | router.push({ name: 'BpmModel' }) |
| | | } |
| | | |
| | | /** 初始化 */ |
| | | onMounted(async () => { |
| | | await initData() |
| | | }) |
| | | |
| | | // 添加组件卸载前的清理代码 |
| | | onBeforeUnmount(() => { |
| | | // 清理所有的引用 |
| | | basicInfoRef.value = null |
| | | formDesignRef.value = null |
| | | processDesignRef.value = null |
| | | }) |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .border-bottom { |
| | | border-bottom: 1px solid #dcdfe6; |
| | | } |
| | | |
| | | .text-primary { |
| | | color: #3473ff; |
| | | } |
| | | |
| | | .bg-primary { |
| | | background-color: #3473ff; |
| | | } |
| | | |
| | | .border-primary { |
| | | border-color: #3473ff; |
| | | } |
| | | </style> |
| | |
| | | <template> |
| | | <ContentWrap> |
| | | <!-- 搜索工作栏 --> |
| | | <el-form |
| | | class="-mb-15px" |
| | | :model="queryParams" |
| | | ref="queryFormRef" |
| | | :inline="true" |
| | | label-width="68px" |
| | | > |
| | | <el-form-item label="流程标识" prop="key"> |
| | | <el-input |
| | | v-model="queryParams.key" |
| | | placeholder="请输入流程标识" |
| | | clearable |
| | | @keyup.enter="handleQuery" |
| | | class="!w-240px" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="流程名称" prop="name"> |
| | | <el-input |
| | | v-model="queryParams.name" |
| | | placeholder="请输入流程名称" |
| | | clearable |
| | | @keyup.enter="handleQuery" |
| | | class="!w-240px" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="流程分类" prop="category"> |
| | | <el-select |
| | | v-model="queryParams.category" |
| | | placeholder="请选择流程分类" |
| | | clearable |
| | | class="!w-240px" |
| | | > |
| | | <el-option |
| | | v-for="category in categoryList" |
| | | :key="category.code" |
| | | :label="category.name" |
| | | :value="category.code" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> |
| | | <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> |
| | | <el-button |
| | | type="primary" |
| | | plain |
| | | @click="openForm('create')" |
| | | v-hasPermi="['bpm:model:create']" |
| | | > |
| | | <Icon icon="ep:plus" class="mr-5px" /> 新建流程 |
| | | </el-button> |
| | | <el-button type="success" plain @click="openImportForm" v-hasPermi="['bpm:model:import']"> |
| | | <Icon icon="ep:upload" class="mr-5px" /> 导入流程 |
| | | </el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | </ContentWrap> |
| | | <div class="flex justify-between pl-20px items-center"> |
| | | <h3 class="font-extrabold">流程模型</h3> |
| | | <!-- 搜索工作栏 --> |
| | | <el-form |
| | | v-if="!isCategorySorting" |
| | | class="-mb-15px flex mr-10px" |
| | | :model="queryParams" |
| | | ref="queryFormRef" |
| | | :inline="true" |
| | | label-width="68px" |
| | | @submit.prevent |
| | | > |
| | | <el-form-item prop="name" class="ml-auto"> |
| | | <el-input |
| | | v-model="queryParams.name" |
| | | placeholder="搜索流程" |
| | | clearable |
| | | @keyup.enter="handleQuery" |
| | | class="!w-240px" |
| | | > |
| | | <template #prefix> |
| | | <Icon icon="ep:search" class="mx-10px" /> |
| | | </template> |
| | | </el-input> |
| | | </el-form-item> |
| | | <!-- 右上角:新建模型、更多操作 --> |
| | | <el-form-item> |
| | | <el-button type="primary" @click="openForm('create')" v-hasPermi="['bpm:model:create']"> |
| | | <Icon icon="ep:plus" class="mr-5px" /> 新建模型 |
| | | </el-button> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-dropdown @command="(command) => handleCommand(command)" placement="bottom-end"> |
| | | <el-button class="w-30px" plain> |
| | | <Icon icon="ep:setting" /> |
| | | </el-button> |
| | | <template #dropdown> |
| | | <el-dropdown-menu> |
| | | <el-dropdown-item command="handleCategoryAdd"> |
| | | <Icon icon="ep:circle-plus" :size="13" class="mr-5px" /> |
| | | 新建分类 |
| | | </el-dropdown-item> |
| | | <el-dropdown-item command="handleCategorySort"> |
| | | <Icon icon="fa:sort-amount-desc" :size="13" class="mr-5px" /> |
| | | 分类排序 |
| | | </el-dropdown-item> |
| | | </el-dropdown-menu> |
| | | </template> |
| | | </el-dropdown> |
| | | </el-form-item> |
| | | </el-form> |
| | | <div class="mr-20px" v-else> |
| | | <el-button @click="handleCategorySortCancel"> 取 消 </el-button> |
| | | <el-button type="primary" @click="handleCategorySortSubmit"> 保存排序 </el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 列表 --> |
| | | <ContentWrap> |
| | | <el-table v-loading="loading" :data="list"> |
| | | <el-table-column label="流程标识" align="center" prop="key" width="200" /> |
| | | <el-table-column label="流程名称" align="center" prop="name" width="200"> |
| | | <template #default="scope"> |
| | | <el-button type="primary" link @click="handleBpmnDetail(scope.row)"> |
| | | <span>{{ scope.row.name }}</span> |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="流程图标" align="center" prop="icon" width="100"> |
| | | <template #default="scope"> |
| | | <el-image :src="scope.row.icon" class="w-32px h-32px" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="流程分类" align="center" prop="categoryName" width="100" /> |
| | | <el-table-column label="表单信息" align="center" prop="formType" width="200"> |
| | | <template #default="scope"> |
| | | <el-button |
| | | v-if="scope.row.formType === 10" |
| | | type="primary" |
| | | link |
| | | @click="handleFormDetail(scope.row)" |
| | | <el-divider /> |
| | | |
| | | <!-- 按照分类,展示其所属的模型列表 --> |
| | | <div class="px-15px"> |
| | | <draggable |
| | | :disabled="!isCategorySorting" |
| | | v-model="categoryGroup" |
| | | item-key="id" |
| | | :animation="400" |
| | | > |
| | | <template #item="{ element }"> |
| | | <ContentWrap |
| | | class="rounded-lg transition-all duration-300 ease-in-out hover:shadow-xl" |
| | | v-loading="loading" |
| | | :body-style="{ padding: 0 }" |
| | | :key="element.id" |
| | | > |
| | | <span>{{ scope.row.formName }}</span> |
| | | </el-button> |
| | | <el-button |
| | | v-else-if="scope.row.formType === 20" |
| | | type="primary" |
| | | link |
| | | @click="handleFormDetail(scope.row)" |
| | | > |
| | | <span>{{ scope.row.formCustomCreatePath }}</span> |
| | | </el-button> |
| | | <label v-else>暂无表单</label> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column |
| | | label="创建时间" |
| | | align="center" |
| | | prop="createTime" |
| | | width="180" |
| | | :formatter="dateFormatter" |
| | | /> |
| | | <el-table-column label="最新部署的流程定义" align="center"> |
| | | <el-table-column |
| | | label="流程版本" |
| | | align="center" |
| | | prop="processDefinition.version" |
| | | width="100" |
| | | > |
| | | <template #default="scope"> |
| | | <el-tag v-if="scope.row.processDefinition"> |
| | | v{{ scope.row.processDefinition.version }} |
| | | </el-tag> |
| | | <el-tag v-else type="warning">未部署</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column |
| | | label="激活状态" |
| | | align="center" |
| | | prop="processDefinition.version" |
| | | width="85" |
| | | > |
| | | <template #default="scope"> |
| | | <el-switch |
| | | v-if="scope.row.processDefinition" |
| | | v-model="scope.row.processDefinition.suspensionState" |
| | | :active-value="1" |
| | | :inactive-value="2" |
| | | @change="handleChangeState(scope.row)" |
| | | <CategoryDraggableModel |
| | | :isCategorySorting="isCategorySorting" |
| | | :categoryInfo="element" |
| | | @success="getList" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="部署时间" align="center" prop="deploymentTime" width="180"> |
| | | <template #default="scope"> |
| | | <span v-if="scope.row.processDefinition"> |
| | | {{ formatDate(scope.row.processDefinition.deploymentTime) }} |
| | | </span> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table-column> |
| | | <el-table-column label="操作" align="center" width="240" fixed="right"> |
| | | <template #default="scope"> |
| | | <el-button |
| | | link |
| | | type="primary" |
| | | @click="openForm('update', scope.row.id)" |
| | | v-hasPermi="['bpm:model:update']" |
| | | > |
| | | 修改流程 |
| | | </el-button> |
| | | <el-button |
| | | link |
| | | type="primary" |
| | | @click="handleDesign(scope.row)" |
| | | v-hasPermi="['bpm:model:update']" |
| | | > |
| | | 设计流程 |
| | | </el-button> |
| | | <el-button |
| | | link |
| | | type="primary" |
| | | @click="handleDeploy(scope.row)" |
| | | v-hasPermi="['bpm:model:deploy']" |
| | | > |
| | | 发布流程 |
| | | </el-button> |
| | | <el-button |
| | | link |
| | | type="primary" |
| | | v-hasPermi="['bpm:process-definition:query']" |
| | | @click="handleDefinitionList(scope.row)" |
| | | > |
| | | 流程定义 |
| | | </el-button> |
| | | <el-button |
| | | link |
| | | type="danger" |
| | | @click="handleDelete(scope.row.id)" |
| | | v-hasPermi="['bpm:model:delete']" |
| | | > |
| | | 删除 |
| | | </el-button> |
| | | </ContentWrap> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <!-- 分页 --> |
| | | <Pagination |
| | | :total="total" |
| | | v-model:page="queryParams.pageNo" |
| | | v-model:limit="queryParams.pageSize" |
| | | @pagination="getList" |
| | | /> |
| | | </draggable> |
| | | </div> |
| | | </ContentWrap> |
| | | |
| | | <!-- 表单弹窗:添加/修改流程 --> |
| | | <ModelForm ref="formRef" @success="getList" /> |
| | | |
| | | <!-- 表单弹窗:导入流程 --> |
| | | <ModelImportForm ref="importFormRef" @success="getList" /> |
| | | |
| | | <!-- 表单弹窗:添加分类 --> |
| | | <CategoryForm ref="categoryFormRef" @success="getList" /> |
| | | <!-- 弹窗:表单详情 --> |
| | | <Dialog title="表单详情" v-model="formDetailVisible" width="800"> |
| | | <form-create :rule="formDetailPreview.rule" :option="formDetailPreview.option" /> |
| | | </Dialog> |
| | | |
| | | <!-- 弹窗:流程模型图的预览 --> |
| | | <Dialog title="流程图" v-model="bpmnDetailVisible" width="800"> |
| | | <MyProcessViewer |
| | | key="designer" |
| | | v-model="bpmnXML" |
| | | :value="bpmnXML as any" |
| | | v-bind="bpmnControlForm" |
| | | :prefix="bpmnControlForm.prefix" |
| | | /> |
| | | </Dialog> |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | import { dateFormatter, formatDate } from '@/utils/formatTime' |
| | | import { MyProcessViewer } from '@/components/bpmnProcessDesigner/package' |
| | | import * as ModelApi from '@/api/bpm/model' |
| | | import * as FormApi from '@/api/bpm/form' |
| | | import ModelForm from './ModelForm.vue' |
| | | import ModelImportForm from '@/views/bpm/model/ModelImportForm.vue' |
| | | import { setConfAndFields2 } from '@/utils/formCreate' |
| | | import draggable from 'vuedraggable' |
| | | import { CategoryApi } from '@/api/bpm/category' |
| | | import * as ModelApi from '@/api/bpm/model' |
| | | import ModelForm from './ModelForm.vue' |
| | | import CategoryForm from '../category/CategoryForm.vue' |
| | | import { cloneDeep } from 'lodash-es' |
| | | import CategoryDraggableModel from './CategoryDraggableModel.vue' |
| | | |
| | | defineOptions({ name: 'BpmModel' }) |
| | | |
| | | const { push } = useRouter() |
| | | const message = useMessage() // 消息弹窗 |
| | | const { t } = useI18n() // 国际化 |
| | | const { push } = useRouter() // 路由 |
| | | |
| | | const loading = ref(true) // 列表的加载中 |
| | | const total = ref(0) // 列表的总页数 |
| | | const list = ref([]) // 列表的数据 |
| | | const isCategorySorting = ref(false) // 是否 category 正处于排序状态 |
| | | const queryParams = reactive({ |
| | | pageNo: 1, |
| | | pageSize: 10, |
| | | key: undefined, |
| | | name: undefined, |
| | | category: undefined |
| | | name: undefined |
| | | }) |
| | | const queryFormRef = ref() // 搜索的表单 |
| | | const categoryList = ref([]) // 流程分类列表 |
| | | |
| | | /** 查询列表 */ |
| | | const getList = async () => { |
| | | loading.value = true |
| | | try { |
| | | const data = await ModelApi.getModelPage(queryParams) |
| | | list.value = data.list |
| | | total.value = data.total |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | | } |
| | | const categoryGroup: any = ref([]) // 按照 category 分组的数据 |
| | | const originalData: any = ref([]) // 原始数据 |
| | | |
| | | /** 搜索按钮操作 */ |
| | | const handleQuery = () => { |
| | | queryParams.pageNo = 1 |
| | | getList() |
| | | } |
| | | |
| | | /** 重置按钮操作 */ |
| | | const resetQuery = () => { |
| | | queryFormRef.value.resetFields() |
| | | handleQuery() |
| | | } |
| | | |
| | | /** 添加/修改操作 */ |
| | | const formRef = ref() |
| | | const openForm = (type: string, id?: number) => { |
| | | formRef.value.open(type, id) |
| | | } |
| | | |
| | | /** 添加/修改操作 */ |
| | | const importFormRef = ref() |
| | | const openImportForm = () => { |
| | | importFormRef.value.open() |
| | | } |
| | | |
| | | /** 删除按钮操作 */ |
| | | const handleDelete = async (id: number) => { |
| | | try { |
| | | // 删除的二次确认 |
| | | await message.delConfirm() |
| | | // 发起删除 |
| | | await ModelApi.deleteModel(id) |
| | | message.success(t('common.delSuccess')) |
| | | // 刷新列表 |
| | | await getList() |
| | | } catch {} |
| | | } |
| | | |
| | | /** 更新状态操作 */ |
| | | const handleChangeState = async (row) => { |
| | | const state = row.processDefinition.suspensionState |
| | | try { |
| | | // 修改状态的二次确认 |
| | | const id = row.id |
| | | const statusState = state === 1 ? '激活' : '挂起' |
| | | const content = '是否确认' + statusState + '流程名字为"' + row.name + '"的数据项?' |
| | | await message.confirm(content) |
| | | // 发起修改状态 |
| | | await ModelApi.updateModelState(id, state) |
| | | // 刷新列表 |
| | | await getList() |
| | | } catch { |
| | | // 取消后,进行恢复按钮 |
| | | row.processDefinition.suspensionState = state === 1 ? 2 : 1 |
| | | if (type === 'create') { |
| | | push({ name: 'BpmModelCreate' }) |
| | | } else { |
| | | push({ |
| | | name: 'BpmModelUpdate', |
| | | params: { id } |
| | | }) |
| | | } |
| | | } |
| | | |
| | | /** 设计流程 */ |
| | | const handleDesign = (row) => { |
| | | push({ |
| | | name: 'BpmModelEditor', |
| | | query: { |
| | | modelId: row.id |
| | | } |
| | | }) |
| | | } |
| | | |
| | | /** 发布流程 */ |
| | | const handleDeploy = async (row) => { |
| | | try { |
| | | // 删除的二次确认 |
| | | await message.confirm('是否部署该流程!!') |
| | | // 发起部署 |
| | | await ModelApi.deployModel(row.id) |
| | | message.success(t('部署成功')) |
| | | // 刷新列表 |
| | | await getList() |
| | | } catch {} |
| | | } |
| | | |
| | | /** 跳转到指定流程定义列表 */ |
| | | const handleDefinitionList = (row) => { |
| | | push({ |
| | | name: 'BpmProcessDefinition', |
| | | query: { |
| | | key: row.key |
| | | } |
| | | }) |
| | | } |
| | | |
| | | /** 流程表单的详情按钮操作 */ |
| | |
| | | rule: [], |
| | | option: {} |
| | | }) |
| | | const handleFormDetail = async (row) => { |
| | | if (row.formType == 10) { |
| | | // 设置表单 |
| | | const data = await FormApi.getForm(row.formId) |
| | | setConfAndFields2(formDetailPreview, data.conf, data.fields) |
| | | // 弹窗打开 |
| | | formDetailVisible.value = true |
| | | } else { |
| | | await push({ |
| | | path: row.formCustomCreatePath |
| | | }) |
| | | |
| | | /** 右上角设置按钮 */ |
| | | const handleCommand = (command: string) => { |
| | | switch (command) { |
| | | case 'handleCategoryAdd': |
| | | handleCategoryAdd() |
| | | break |
| | | case 'handleCategorySort': |
| | | handleCategorySort() |
| | | break |
| | | default: |
| | | break |
| | | } |
| | | } |
| | | |
| | | /** 流程图的详情按钮操作 */ |
| | | const bpmnDetailVisible = ref(false) |
| | | const bpmnXML = ref(null) |
| | | const bpmnControlForm = ref({ |
| | | prefix: 'flowable' |
| | | }) |
| | | const handleBpmnDetail = async (row) => { |
| | | const data = await ModelApi.getModel(row.id) |
| | | bpmnXML.value = data.bpmnXml || '' |
| | | bpmnDetailVisible.value = true |
| | | /** 新建分类 */ |
| | | const categoryFormRef = ref() |
| | | const handleCategoryAdd = () => { |
| | | categoryFormRef.value.open('create') |
| | | } |
| | | |
| | | /** 分类排序的提交 */ |
| | | const handleCategorySort = () => { |
| | | // 保存初始数据 |
| | | originalData.value = cloneDeep(categoryGroup.value) |
| | | isCategorySorting.value = true |
| | | } |
| | | |
| | | /** 分类排序的取消 */ |
| | | const handleCategorySortCancel = () => { |
| | | // 恢复初始数据 |
| | | categoryGroup.value = cloneDeep(originalData.value) |
| | | isCategorySorting.value = false |
| | | } |
| | | |
| | | /** 分类排序的保存 */ |
| | | const handleCategorySortSubmit = async () => { |
| | | // 保存排序 |
| | | const ids = categoryGroup.value.map((item: any) => item.id) |
| | | await CategoryApi.updateCategorySortBatch(ids) |
| | | // 刷新列表 |
| | | isCategorySorting.value = false |
| | | message.success('排序分类成功') |
| | | await getList() |
| | | } |
| | | |
| | | /** 加载数据 */ |
| | | const getList = async () => { |
| | | loading.value = true |
| | | try { |
| | | // 查询模型 + 分裂的列表 |
| | | const modelList = await ModelApi.getModelList(queryParams.name) |
| | | const categoryList = await CategoryApi.getCategorySimpleList() |
| | | // 按照 category 聚合 |
| | | // 注意:必须一次性赋值给 categoryGroup,否则每次操作后,列表会重新渲染,滚动条的位置会偏离!!! |
| | | categoryGroup.value = categoryList.map((category: any) => ({ |
| | | ...category, |
| | | modelList: modelList.filter((model: any) => model.categoryName == category.name) |
| | | })) |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | /** 初始化 **/ |
| | | onMounted(async () => { |
| | | await getList() |
| | | // 查询流程分类列表 |
| | | categoryList.value = await CategoryApi.getCategorySimpleList() |
| | | onMounted(() => { |
| | | getList() |
| | | }) |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | :deep() { |
| | | .el-table--fit .el-table__inner-wrapper:before { |
| | | height: 0; |
| | | } |
| | | .el-card { |
| | | border-radius: 8px; |
| | | } |
| | | .el-form--inline .el-form-item { |
| | | margin-right: 10px; |
| | | } |
| | | .el-divider--horizontal { |
| | | margin-top: 6px; |
| | | } |
| | | } |
| | | </style> |
对比新文件 |
| | |
| | | <template> |
| | | <doc-alert title="流程设计器(BPMN)" url="https://doc.iocoder.cn/bpm/model-designer-dingding/" /> |
| | | <doc-alert |
| | | title="流程设计器(钉钉、飞书)" |
| | | url="https://doc.iocoder.cn/bpm/model-designer-bpmn/" |
| | | /> |
| | | <doc-alert title="选择审批人、发起人自选" url="https://doc.iocoder.cn/bpm/assignee/" /> |
| | | <doc-alert title="会签、或签、依次审批" url="https://doc.iocoder.cn/bpm/multi-instance/" /> |
| | | |
| | | <ContentWrap> |
| | | <!-- 搜索工作栏 --> |
| | | <el-form |
| | | class="-mb-15px" |
| | | :model="queryParams" |
| | | ref="queryFormRef" |
| | | :inline="true" |
| | | label-width="68px" |
| | | > |
| | | <el-form-item label="流程标识" prop="key"> |
| | | <el-input |
| | | v-model="queryParams.key" |
| | | placeholder="请输入流程标识" |
| | | clearable |
| | | @keyup.enter="handleQuery" |
| | | class="!w-240px" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="流程名称" prop="name"> |
| | | <el-input |
| | | v-model="queryParams.name" |
| | | placeholder="请输入流程名称" |
| | | clearable |
| | | @keyup.enter="handleQuery" |
| | | class="!w-240px" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="流程分类" prop="category"> |
| | | <el-select |
| | | v-model="queryParams.category" |
| | | placeholder="请选择流程分类" |
| | | clearable |
| | | class="!w-240px" |
| | | > |
| | | <el-option |
| | | v-for="category in categoryList" |
| | | :key="category.code" |
| | | :label="category.name" |
| | | :value="category.code" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> |
| | | <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> |
| | | <el-button |
| | | type="primary" |
| | | plain |
| | | @click="openForm('create')" |
| | | v-hasPermi="['bpm:model:create']" |
| | | > |
| | | <Icon icon="ep:plus" class="mr-5px" /> 新建 |
| | | </el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | </ContentWrap> |
| | | |
| | | <!-- 列表 --> |
| | | <ContentWrap> |
| | | <el-table v-loading="loading" :data="list"> |
| | | <el-table-column label="流程名称" align="center" prop="name" min-width="200" /> |
| | | <el-table-column label="流程图标" align="center" prop="icon" min-width="100"> |
| | | <template #default="scope"> |
| | | <el-image :src="scope.row.icon" class="h-32px w-32px" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="可见范围" align="center" prop="startUserIds" min-width="100"> |
| | | <template #default="scope"> |
| | | <el-text v-if="!scope.row.startUsers || scope.row.startUsers.length === 0"> |
| | | 全部可见 |
| | | </el-text> |
| | | <el-text v-else-if="scope.row.startUsers.length == 1"> |
| | | {{ scope.row.startUsers[0].nickname }} |
| | | </el-text> |
| | | <el-text v-else> |
| | | <el-tooltip |
| | | class="box-item" |
| | | effect="dark" |
| | | placement="top" |
| | | :content="scope.row.startUsers.map((user: any) => user.nickname).join('、')" |
| | | > |
| | | {{ scope.row.startUsers[0].nickname }}等 {{ scope.row.startUsers.length }} 人可见 |
| | | </el-tooltip> |
| | | </el-text> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="流程分类" align="center" prop="categoryName" min-width="100" /> |
| | | <el-table-column label="表单信息" align="center" prop="formType" min-width="200"> |
| | | <template #default="scope"> |
| | | <el-button |
| | | v-if="scope.row.formType === 10" |
| | | type="primary" |
| | | link |
| | | @click="handleFormDetail(scope.row)" |
| | | > |
| | | <span>{{ scope.row.formName }}</span> |
| | | </el-button> |
| | | <el-button |
| | | v-else-if="scope.row.formType === 20" |
| | | type="primary" |
| | | link |
| | | @click="handleFormDetail(scope.row)" |
| | | > |
| | | <span>{{ scope.row.formCustomCreatePath }}</span> |
| | | </el-button> |
| | | <label v-else>暂无表单</label> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="最后发布" align="center" prop="deploymentTime" min-width="250"> |
| | | <template #default="scope"> |
| | | <span v-if="scope.row.processDefinition"> |
| | | {{ formatDate(scope.row.processDefinition.deploymentTime) }} |
| | | </span> |
| | | <el-tag v-if="scope.row.processDefinition" class="ml-10px"> |
| | | v{{ scope.row.processDefinition.version }} |
| | | </el-tag> |
| | | <el-tag v-else type="warning">未部署</el-tag> |
| | | <el-tag |
| | | v-if="scope.row.processDefinition?.suspensionState === 2" |
| | | type="warning" |
| | | class="ml-10px" |
| | | > |
| | | 已停用 |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" align="center" width="200" fixed="right"> |
| | | <template #default="scope"> |
| | | <el-button |
| | | link |
| | | type="primary" |
| | | @click="openForm('update', scope.row.id)" |
| | | v-hasPermi="['bpm:model:update']" |
| | | :disabled="!isManagerUser(scope.row)" |
| | | > |
| | | 修改 |
| | | </el-button> |
| | | <el-button |
| | | link |
| | | class="!ml-5px" |
| | | type="primary" |
| | | @click="handleDesign(scope.row)" |
| | | v-hasPermi="['bpm:model:update']" |
| | | :disabled="!isManagerUser(scope.row)" |
| | | > |
| | | 设计 |
| | | </el-button> |
| | | <el-button |
| | | link |
| | | class="!ml-5px" |
| | | type="primary" |
| | | @click="handleDeploy(scope.row)" |
| | | v-hasPermi="['bpm:model:deploy']" |
| | | :disabled="!isManagerUser(scope.row)" |
| | | > |
| | | 发布 |
| | | </el-button> |
| | | <el-dropdown |
| | | class="!align-middle ml-5px" |
| | | @command="(command) => handleCommand(command, scope.row)" |
| | | v-hasPermi="['bpm:process-definition:query', 'bpm:model:update', 'bpm:model:delete']" |
| | | > |
| | | <el-button type="primary" link>更多</el-button> |
| | | <template #dropdown> |
| | | <el-dropdown-menu> |
| | | <el-dropdown-item |
| | | command="handleDefinitionList" |
| | | v-if="checkPermi(['bpm:process-definition:query'])" |
| | | > |
| | | 历史 |
| | | </el-dropdown-item> |
| | | <el-dropdown-item |
| | | command="handleChangeState" |
| | | v-if="checkPermi(['bpm:model:update']) && scope.row.processDefinition" |
| | | :disabled="!isManagerUser(scope.row)" |
| | | > |
| | | {{ scope.row.processDefinition.suspensionState === 1 ? '停用' : '启用' }} |
| | | </el-dropdown-item> |
| | | <el-dropdown-item |
| | | type="danger" |
| | | command="handleDelete" |
| | | v-if="checkPermi(['bpm:model:delete'])" |
| | | :disabled="!isManagerUser(scope.row)" |
| | | > |
| | | 删除 |
| | | </el-dropdown-item> |
| | | </el-dropdown-menu> |
| | | </template> |
| | | </el-dropdown> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <!-- 分页 --> |
| | | <Pagination |
| | | :total="total" |
| | | v-model:page="queryParams.pageNo" |
| | | v-model:limit="queryParams.pageSize" |
| | | @pagination="getList" |
| | | /> |
| | | </ContentWrap> |
| | | |
| | | <!-- 表单弹窗:添加/修改流程 --> |
| | | <ModelForm ref="formRef" @success="getList" /> |
| | | |
| | | <!-- 弹窗:表单详情 --> |
| | | <Dialog title="表单详情" v-model="formDetailVisible" width="800"> |
| | | <form-create :rule="formDetailPreview.rule" :option="formDetailPreview.option" /> |
| | | </Dialog> |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | import { formatDate } from '@/utils/formatTime' |
| | | import * as ModelApi from '@/api/bpm/model' |
| | | import * as FormApi from '@/api/bpm/form' |
| | | import ModelForm from './ModelForm.vue' |
| | | import { setConfAndFields2 } from '@/utils/formCreate' |
| | | import { CategoryApi } from '@/api/bpm/category' |
| | | import { BpmModelType } from '@/utils/constants' |
| | | import { checkPermi } from '@/utils/permission' |
| | | import { useUserStoreWithOut } from '@/store/modules/user' |
| | | |
| | | defineOptions({ name: 'BpmModel' }) |
| | | |
| | | const message = useMessage() // 消息弹窗 |
| | | const { t } = useI18n() // 国际化 |
| | | const { push } = useRouter() // 路由 |
| | | const userStore = useUserStoreWithOut() // 用户信息缓存 |
| | | |
| | | const loading = ref(true) // 列表的加载中 |
| | | const total = ref(0) // 列表的总页数 |
| | | const list = ref([]) // 列表的数据 |
| | | const queryParams = reactive({ |
| | | pageNo: 1, |
| | | pageSize: 10, |
| | | key: undefined, |
| | | name: undefined, |
| | | category: undefined |
| | | }) |
| | | const queryFormRef = ref() // 搜索的表单 |
| | | const categoryList = ref([]) // 流程分类列表 |
| | | |
| | | /** 查询列表 */ |
| | | const getList = async () => { |
| | | loading.value = true |
| | | try { |
| | | const data = await ModelApi.getModelList(queryParams) |
| | | list.value = data.list |
| | | total.value = data.total |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | /** 搜索按钮操作 */ |
| | | const handleQuery = () => { |
| | | queryParams.pageNo = 1 |
| | | getList() |
| | | } |
| | | |
| | | /** 重置按钮操作 */ |
| | | const resetQuery = () => { |
| | | queryFormRef.value.resetFields() |
| | | handleQuery() |
| | | } |
| | | |
| | | /** '更多'操作按钮 */ |
| | | const handleCommand = (command: string, row: any) => { |
| | | switch (command) { |
| | | case 'handleDefinitionList': |
| | | handleDefinitionList(row) |
| | | break |
| | | case 'handleDelete': |
| | | handleDelete(row) |
| | | break |
| | | case 'handleChangeState': |
| | | handleChangeState(row) |
| | | break |
| | | default: |
| | | break |
| | | } |
| | | } |
| | | |
| | | /** 添加/修改操作 */ |
| | | const formRef = ref() |
| | | const openForm = (type: string, id?: number) => { |
| | | formRef.value.open(type, id) |
| | | } |
| | | |
| | | /** 删除按钮操作 */ |
| | | const handleDelete = async (row: any) => { |
| | | try { |
| | | // 删除的二次确认 |
| | | await message.delConfirm() |
| | | // 发起删除 |
| | | await ModelApi.deleteModel(row.id) |
| | | message.success(t('common.delSuccess')) |
| | | // 刷新列表 |
| | | await getList() |
| | | } catch {} |
| | | } |
| | | |
| | | /** 更新状态操作 */ |
| | | const handleChangeState = async (row: any) => { |
| | | const state = row.processDefinition.suspensionState |
| | | const newState = state === 1 ? 2 : 1 |
| | | try { |
| | | // 修改状态的二次确认 |
| | | const id = row.id |
| | | debugger |
| | | const statusState = state === 1 ? '停用' : '启用' |
| | | const content = '是否确认' + statusState + '流程名字为"' + row.name + '"的数据项?' |
| | | await message.confirm(content) |
| | | // 发起修改状态 |
| | | await ModelApi.updateModelState(id, newState) |
| | | message.success(statusState + '成功') |
| | | // 刷新列表 |
| | | await getList() |
| | | } catch {} |
| | | } |
| | | |
| | | /** 设计流程 */ |
| | | const handleDesign = (row: any) => { |
| | | if (row.type == BpmModelType.BPMN) { |
| | | push({ |
| | | name: 'BpmModelEditor', |
| | | query: { |
| | | modelId: row.id |
| | | } |
| | | }) |
| | | } else { |
| | | push({ |
| | | name: 'SimpleModelDesign', |
| | | query: { |
| | | modelId: row.id |
| | | } |
| | | }) |
| | | } |
| | | } |
| | | |
| | | /** 发布流程 */ |
| | | const handleDeploy = async (row: any) => { |
| | | try { |
| | | // 删除的二次确认 |
| | | await message.confirm('是否部署该流程!!') |
| | | // 发起部署 |
| | | await ModelApi.deployModel(row.id) |
| | | message.success(t('部署成功')) |
| | | // 刷新列表 |
| | | await getList() |
| | | } catch {} |
| | | } |
| | | |
| | | /** 跳转到指定流程定义列表 */ |
| | | const handleDefinitionList = (row) => { |
| | | push({ |
| | | name: 'BpmProcessDefinition', |
| | | query: { |
| | | key: row.key |
| | | } |
| | | }) |
| | | } |
| | | |
| | | /** 流程表单的详情按钮操作 */ |
| | | const formDetailVisible = ref(false) |
| | | const formDetailPreview = ref({ |
| | | rule: [], |
| | | option: {} |
| | | }) |
| | | const handleFormDetail = async (row: any) => { |
| | | if (row.formType == 10) { |
| | | // 设置表单 |
| | | const data = await FormApi.getForm(row.formId) |
| | | setConfAndFields2(formDetailPreview, data.conf, data.fields) |
| | | // 弹窗打开 |
| | | formDetailVisible.value = true |
| | | } else { |
| | | await push({ |
| | | path: row.formCustomCreatePath |
| | | }) |
| | | } |
| | | } |
| | | |
| | | /** 判断是否可以操作 */ |
| | | const isManagerUser = (row: any) => { |
| | | const userId = userStore.getUser.id |
| | | return row.managerUserIds && row.managerUserIds.includes(userId) |
| | | } |
| | | |
| | | /** 初始化 **/ |
| | | onMounted(async () => { |
| | | await getList() |
| | | // 查询流程分类列表 |
| | | categoryList.value = await CategoryApi.getCategorySimpleList() |
| | | }) |
| | | </script> |
| | |
| | | <el-radio |
| | | v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" |
| | | :key="dict.value" |
| | | :label="dict.value" |
| | | :value="dict.value" |
| | | > |
| | | {{ dict.label }} |
| | | </el-radio> |
对比新文件 |
| | |
| | | <template> |
| | | <ContentWrap :bodyStyle="{ padding: '10px 20px 0' }"> |
| | | <div class="processInstance-wrap-main"> |
| | | <el-scrollbar> |
| | | <div class="text-#878c93 h-15px">流程:{{ selectProcessDefinition.name }}</div> |
| | | <el-divider class="!my-8px" /> |
| | | |
| | | <!-- 中间主要内容 tab 栏 --> |
| | | <el-tabs v-model="activeTab"> |
| | | <!-- 表单信息 --> |
| | | <el-tab-pane label="表单填写" name="form"> |
| | | <div class="form-scroll-area" v-loading="processInstanceStartLoading"> |
| | | <el-scrollbar> |
| | | <el-row> |
| | | <el-col :span="17"> |
| | | <form-create |
| | | :rule="detailForm.rule" |
| | | v-model:api="fApi" |
| | | v-model="detailForm.value" |
| | | :option="detailForm.option" |
| | | @submit="submitForm" |
| | | /> |
| | | </el-col> |
| | | |
| | | <el-col :span="6" :offset="1"> |
| | | <!-- 流程时间线 --> |
| | | <ProcessInstanceTimeline |
| | | ref="timelineRef" |
| | | :activity-nodes="activityNodes" |
| | | :show-status-icon="false" |
| | | @select-user-confirm="selectUserConfirm" |
| | | /> |
| | | </el-col> |
| | | </el-row> |
| | | </el-scrollbar> |
| | | </div> |
| | | </el-tab-pane> |
| | | <!-- 流程图 --> |
| | | <el-tab-pane label="流程图" name="diagram"> |
| | | <div class="form-scroll-area"> |
| | | <!-- BPMN 流程图预览 --> |
| | | <ProcessInstanceBpmnViewer |
| | | :bpmn-xml="bpmnXML" |
| | | v-if="BpmModelType.BPMN === selectProcessDefinition.modelType" |
| | | /> |
| | | |
| | | <!-- Simple 流程图预览 --> |
| | | <ProcessInstanceSimpleViewer |
| | | :simple-json="simpleJson" |
| | | v-if="BpmModelType.SIMPLE === selectProcessDefinition.modelType" |
| | | /> |
| | | </div> |
| | | </el-tab-pane> |
| | | </el-tabs> |
| | | |
| | | <!-- 底部操作栏 --> |
| | | <div class="b-t-solid border-t-1px border-[var(--el-border-color)]"> |
| | | <!-- 操作栏按钮 --> |
| | | <div |
| | | v-if="activeTab === 'form'" |
| | | class="h-50px bottom-10 text-14px flex items-center color-#32373c dark:color-#fff font-bold btn-container" |
| | | > |
| | | <el-button plain type="success" @click="submitForm"> |
| | | <Icon icon="ep:select" /> 发起 |
| | | </el-button> |
| | | <el-button plain type="danger" @click="handleCancel"> |
| | | <Icon icon="ep:close" /> 取消 |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | </el-scrollbar> |
| | | </div> |
| | | </ContentWrap> |
| | | </template> |
| | | <script lang="ts" setup> |
| | | import { decodeFields, setConfAndFields2 } from '@/utils/formCreate' |
| | | import { BpmModelType } from '@/utils/constants' |
| | | import { |
| | | CandidateStrategy, |
| | | NodeId, |
| | | FieldPermissionType |
| | | } from '@/components/SimpleProcessDesignerV2/src/consts' |
| | | import ProcessInstanceBpmnViewer from '../detail/ProcessInstanceBpmnViewer.vue' |
| | | import ProcessInstanceSimpleViewer from '../detail/ProcessInstanceSimpleViewer.vue' |
| | | import ProcessInstanceTimeline from '../detail/ProcessInstanceTimeline.vue' |
| | | import type { ApiAttrs } from '@form-create/element-ui/types/config' |
| | | import { useTagsViewStore } from '@/store/modules/tagsView' |
| | | import * as ProcessInstanceApi from '@/api/bpm/processInstance' |
| | | import * as DefinitionApi from '@/api/bpm/definition' |
| | | import { ApprovalNodeInfo } from '@/api/bpm/processInstance' |
| | | |
| | | defineOptions({ name: 'ProcessDefinitionDetail' }) |
| | | const props = defineProps<{ |
| | | selectProcessDefinition: any |
| | | }>() |
| | | const emit = defineEmits(['cancel']) |
| | | const processInstanceStartLoading = ref(false) // 流程实例发起中 |
| | | const { push, currentRoute } = useRouter() // 路由 |
| | | const message = useMessage() // 消息弹窗 |
| | | const { delView } = useTagsViewStore() // 视图操作 |
| | | |
| | | const detailForm: any = ref({ |
| | | rule: [], |
| | | option: {}, |
| | | value: {} |
| | | }) // 流程表单详情 |
| | | const fApi = ref<ApiAttrs>() |
| | | // 指定审批人 |
| | | const startUserSelectTasks: any = ref([]) // 发起人需要选择审批人或抄送人的任务列表 |
| | | const startUserSelectAssignees = ref({}) // 发起人选择审批人的数据 |
| | | const bpmnXML: any = ref(null) // BPMN 数据 |
| | | const simpleJson = ref<string | undefined>() // Simple 设计器数据 json 格式 |
| | | |
| | | const activeTab = ref('form') // 当前的 Tab |
| | | const activityNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([]) // 审批节点信息 |
| | | |
| | | /** 设置表单信息、获取流程图数据 **/ |
| | | const initProcessInfo = async (row: any, formVariables?: any) => { |
| | | // 重置指定审批人 |
| | | startUserSelectTasks.value = [] |
| | | startUserSelectAssignees.value = {} |
| | | |
| | | // 情况一:流程表单 |
| | | if (row.formType == 10) { |
| | | // 设置表单 |
| | | // 注意:需要从 formVariables 中,移除不在 row.formFields 的值。 |
| | | // 原因是:后端返回的 formVariables 里面,会有一些非表单的信息。例如说,某个流程节点的审批人。 |
| | | // 这样,就可能导致一个流程被审批不通过后,重新发起时,会直接后端报错!!! |
| | | const allowedFields = decodeFields(row.formFields).map((fieldObj: any) => fieldObj.field) |
| | | for (const key in formVariables) { |
| | | if (!allowedFields.includes(key)) { |
| | | delete formVariables[key] |
| | | } |
| | | } |
| | | setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables) |
| | | |
| | | await nextTick() |
| | | fApi.value?.btn.show(false) // 隐藏提交按钮 |
| | | |
| | | // 获取流程审批信息 |
| | | await getApprovalDetail(row) |
| | | |
| | | // 加载流程图 |
| | | const processDefinitionDetail = await DefinitionApi.getProcessDefinition(row.id) |
| | | if (processDefinitionDetail) { |
| | | bpmnXML.value = processDefinitionDetail.bpmnXml |
| | | simpleJson.value = processDefinitionDetail.simpleModel |
| | | } |
| | | // 情况二:业务表单 |
| | | } else if (row.formCustomCreatePath) { |
| | | await push({ |
| | | path: row.formCustomCreatePath |
| | | }) |
| | | // 这里暂时无需加载流程图,因为跳出到另外个 Tab; |
| | | } |
| | | } |
| | | |
| | | /** 获取审批详情 */ |
| | | const getApprovalDetail = async (row: any) => { |
| | | try { |
| | | // TODO 获取审批详情,设置 activityId 为发起人节点(为了获取字段权限。暂时只对 Simple 设计器有效) |
| | | const data = await ProcessInstanceApi.getApprovalDetail({ |
| | | processDefinitionId: row.id, |
| | | activityId: NodeId.START_USER_NODE_ID |
| | | }) |
| | | |
| | | if (!data) { |
| | | message.error('查询不到审批详情信息!') |
| | | return |
| | | } |
| | | |
| | | // 获取发起人自选的任务 |
| | | startUserSelectTasks.value = data.activityNodes?.filter( |
| | | (node: ApprovalNodeInfo) => CandidateStrategy.START_USER_SELECT === node.candidateStrategy |
| | | ) |
| | | if (startUserSelectTasks.value?.length > 0) { |
| | | for (const node of startUserSelectTasks.value) { |
| | | startUserSelectAssignees.value[node.id] = [] |
| | | } |
| | | } |
| | | |
| | | // 获取审批节点,显示 Timeline 的数据 |
| | | activityNodes.value = data.activityNodes |
| | | // 获取表单字段权限 |
| | | const formFieldsPermission = data.formFieldsPermission |
| | | // 设置表单字段权限 |
| | | if (formFieldsPermission) { |
| | | Object.keys(formFieldsPermission).forEach((item) => { |
| | | setFieldPermission(item, formFieldsPermission[item]) |
| | | }) |
| | | } |
| | | } finally { |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 设置表单权限 |
| | | */ |
| | | const setFieldPermission = (field: string, permission: string) => { |
| | | if (permission === FieldPermissionType.READ) { |
| | | //@ts-ignore |
| | | fApi.value?.disabled(true, field) |
| | | } |
| | | if (permission === FieldPermissionType.WRITE) { |
| | | //@ts-ignore |
| | | fApi.value?.disabled(false, field) |
| | | } |
| | | if (permission === FieldPermissionType.NONE) { |
| | | //@ts-ignore |
| | | fApi.value?.hidden(true, field) |
| | | } |
| | | } |
| | | |
| | | /** 提交按钮 */ |
| | | const submitForm = async () => { |
| | | if (!fApi.value || !props.selectProcessDefinition) { |
| | | return |
| | | } |
| | | // 流程表单校验 |
| | | await fApi.value.validate() |
| | | // 如果有指定审批人,需要校验 |
| | | if (startUserSelectTasks.value?.length > 0) { |
| | | for (const userTask of startUserSelectTasks.value) { |
| | | if ( |
| | | Array.isArray(startUserSelectAssignees.value[userTask.id]) && |
| | | startUserSelectAssignees.value[userTask.id].length === 0 |
| | | ) |
| | | return message.warning(`请选择${userTask.name}的候选人`) |
| | | } |
| | | } |
| | | |
| | | // 提交请求 |
| | | processInstanceStartLoading.value = true |
| | | try { |
| | | await ProcessInstanceApi.createProcessInstance({ |
| | | processDefinitionId: props.selectProcessDefinition.id, |
| | | variables: detailForm.value.value, |
| | | startUserSelectAssignees: startUserSelectAssignees.value |
| | | }) |
| | | // 提示 |
| | | message.success('发起流程成功') |
| | | // 跳转回去 |
| | | delView(unref(currentRoute)) |
| | | await push({ |
| | | name: 'BpmProcessInstanceMy' |
| | | }) |
| | | } finally { |
| | | processInstanceStartLoading.value = false |
| | | } |
| | | } |
| | | |
| | | /** 取消发起审批 */ |
| | | const handleCancel = () => { |
| | | emit('cancel') |
| | | } |
| | | |
| | | /** 选择发起人 */ |
| | | const selectUserConfirm = (id: string, userList: any[]) => { |
| | | startUserSelectAssignees.value[id] = userList?.map((item: any) => item.id) |
| | | } |
| | | |
| | | defineExpose({ initProcessInfo }) |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | $wrap-padding-height: 20px; |
| | | $wrap-margin-height: 15px; |
| | | $button-height: 51px; |
| | | $process-header-height: 105px; |
| | | |
| | | .processInstance-wrap-main { |
| | | height: calc( |
| | | 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px |
| | | ); |
| | | max-height: calc( |
| | | 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px |
| | | ); |
| | | overflow: auto; |
| | | |
| | | .form-scroll-area { |
| | | height: calc( |
| | | 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px - |
| | | $process-header-height - 40px |
| | | ); |
| | | max-height: calc( |
| | | 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px - |
| | | $process-header-height - 40px |
| | | ); |
| | | overflow: auto; |
| | | } |
| | | } |
| | | |
| | | .form-box { |
| | | :deep(.el-card) { |
| | | border: none; |
| | | } |
| | | } |
| | | </style> |
| | |
| | | <template> |
| | | |
| | | <!-- 第一步,通过流程定义的列表,选择对应的流程 --> |
| | | <ContentWrap v-if="!selectProcessDefinition" v-loading="loading"> |
| | | <el-tabs tab-position="left" v-model="categoryActive"> |
| | | <el-tab-pane |
| | | :label="category.name" |
| | | :name="category.code" |
| | | :key="category.code" |
| | | v-for="category in categoryList" |
| | | > |
| | | <el-row :gutter="20"> |
| | | <el-col |
| | | :lg="6" |
| | | :sm="12" |
| | | :xs="24" |
| | | v-for="definition in categoryProcessDefinitionList" |
| | | :key="definition.id" |
| | | > |
| | | <el-card |
| | | shadow="hover" |
| | | class="mb-20px cursor-pointer" |
| | | @click="handleSelect(definition)" |
| | | <template v-if="!selectProcessDefinition"> |
| | | <el-input |
| | | v-model="searchName" |
| | | class="!w-50% mb-15px" |
| | | placeholder="请输入流程名称" |
| | | clearable |
| | | @input="handleQuery" |
| | | @clear="handleQuery" |
| | | > |
| | | <template #prefix> |
| | | <Icon icon="ep:search" /> |
| | | </template> |
| | | </el-input> |
| | | <ContentWrap |
| | | :class="{ 'process-definition-container': filteredProcessDefinitionList?.length }" |
| | | class="position-relative pb-20px h-700px" |
| | | v-loading="loading" |
| | | > |
| | | <el-row v-if="filteredProcessDefinitionList?.length" :gutter="20" class="!flex-nowrap"> |
| | | <el-col :span="5"> |
| | | <div class="flex flex-col"> |
| | | <div |
| | | v-for="category in availableCategories" |
| | | :key="category.code" |
| | | class="flex items-center p-10px cursor-pointer text-14px rounded-md" |
| | | :class="categoryActive.code === category.code ? 'text-#3e7bff bg-#e8eeff' : ''" |
| | | @click="handleCategoryClick(category)" |
| | | > |
| | | <template #default> |
| | | <div class="flex"> |
| | | <el-image :src="definition.icon" class="w-32px h-32px" /> |
| | | <el-text class="!ml-10px" size="large">{{ definition.name }}</el-text> |
| | | </div> |
| | | </template> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | </el-tab-pane> |
| | | </el-tabs> |
| | | </ContentWrap> |
| | | {{ category.name }} |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="19"> |
| | | <el-scrollbar ref="scrollWrapper" height="700" @scroll="handleScroll"> |
| | | <div |
| | | class="mb-20px pl-10px" |
| | | v-for="(definitions, categoryCode) in processDefinitionGroup" |
| | | :key="categoryCode" |
| | | :ref="`category-${categoryCode}`" |
| | | > |
| | | <h3 class="text-18px font-bold mb-10px mt-5px"> |
| | | {{ getCategoryName(categoryCode as any) }} |
| | | </h3> |
| | | <div class="grid grid-cols-3 gap3"> |
| | | <el-tooltip |
| | | v-for="definition in definitions" |
| | | :key="definition.id" |
| | | :content="definition.description" |
| | | :disabled="!definition.description || definition.description.trim().length === 0" |
| | | placement="top" |
| | | > |
| | | <el-card |
| | | shadow="hover" |
| | | class="cursor-pointer definition-item-card" |
| | | @click="handleSelect(definition)" |
| | | > |
| | | <template #default> |
| | | <div class="flex"> |
| | | <el-image :src="definition.icon" class="w-32px h-32px" /> |
| | | <el-text class="!ml-10px" size="large">{{ definition.name }}</el-text> |
| | | </div> |
| | | </template> |
| | | </el-card> |
| | | </el-tooltip> |
| | | </div> |
| | | </div> |
| | | </el-scrollbar> |
| | | </el-col> |
| | | </el-row> |
| | | <el-empty class="!py-200px" :image-size="200" description="没有找到搜索结果" v-else /> |
| | | </ContentWrap> |
| | | </template> |
| | | |
| | | <!-- 第二步,填写表单,进行流程的提交 --> |
| | | <ContentWrap v-else> |
| | | <el-card class="box-card"> |
| | | <div class="clearfix"> |
| | | <span class="el-icon-document">申请信息【{{ selectProcessDefinition.name }}】</span> |
| | | <el-button style="float: right" type="primary" @click="selectProcessDefinition = undefined"> |
| | | <Icon icon="ep:delete" /> 选择其它流程 |
| | | </el-button> |
| | | </div> |
| | | <el-col :span="16" :offset="6" style="margin-top: 20px"> |
| | | <form-create |
| | | :rule="detailForm.rule" |
| | | v-model:api="fApi" |
| | | v-model="detailForm.value" |
| | | :option="detailForm.option" |
| | | @submit="submitForm" |
| | | > |
| | | <template #type-startUserSelect> |
| | | <el-col :span="24"> |
| | | <el-card class="mb-10px"> |
| | | <template #header>指定审批人</template> |
| | | <el-form |
| | | :model="startUserSelectAssignees" |
| | | :rules="startUserSelectAssigneesFormRules" |
| | | ref="startUserSelectAssigneesFormRef" |
| | | > |
| | | <el-form-item |
| | | v-for="userTask in startUserSelectTasks" |
| | | :key="userTask.id" |
| | | :label="`任务【${userTask.name}】`" |
| | | :prop="userTask.id" |
| | | > |
| | | <el-select |
| | | v-model="startUserSelectAssignees[userTask.id]" |
| | | multiple |
| | | placeholder="请选择审批人" |
| | | > |
| | | <el-option |
| | | v-for="user in userList" |
| | | :key="user.id" |
| | | :label="user.nickname" |
| | | :value="user.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-form> |
| | | </el-card> |
| | | </el-col> |
| | | </template> |
| | | </form-create> |
| | | </el-col> |
| | | </el-card> |
| | | <!-- 流程图预览 --> |
| | | <ProcessInstanceBpmnViewer :bpmn-xml="bpmnXML as any" /> |
| | | </ContentWrap> |
| | | <ProcessDefinitionDetail |
| | | v-else |
| | | ref="processDefinitionDetailRef" |
| | | :selectProcessDefinition="selectProcessDefinition" |
| | | @cancel="selectProcessDefinition = undefined" |
| | | /> |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | import * as DefinitionApi from '@/api/bpm/definition' |
| | | import * as ProcessInstanceApi from '@/api/bpm/processInstance' |
| | | import { setConfAndFields2 } from '@/utils/formCreate' |
| | | import type { ApiAttrs } from '@form-create/element-ui/types/config' |
| | | import ProcessInstanceBpmnViewer from '../detail/ProcessInstanceBpmnViewer.vue' |
| | | import { CategoryApi } from '@/api/bpm/category' |
| | | import { useTagsViewStore } from '@/store/modules/tagsView' |
| | | import * as UserApi from '@/api/system/user' |
| | | import { CategoryApi, CategoryVO } from '@/api/bpm/category' |
| | | import ProcessDefinitionDetail from './ProcessDefinitionDetail.vue' |
| | | import { groupBy } from 'lodash-es' |
| | | |
| | | defineOptions({ name: 'BpmProcessInstanceCreate' }) |
| | | |
| | | const { proxy } = getCurrentInstance() as any |
| | | const route = useRoute() // 路由 |
| | | const { push, currentRoute } = useRouter() // 路由 |
| | | const message = useMessage() // 消息 |
| | | const { delView } = useTagsViewStore() // 视图操作 |
| | | |
| | | const processInstanceId = route.query.processInstanceId |
| | | const searchName = ref('') // 当前搜索关键字 |
| | | const processInstanceId: any = route.query.processInstanceId // 流程实例编号。场景:重新发起时 |
| | | const loading = ref(true) // 加载中 |
| | | const categoryList = ref([]) // 分类的列表 |
| | | const categoryActive = ref('') // 选中的分类 |
| | | const categoryList: any = ref([]) // 分类的列表 |
| | | const categoryActive: any = ref({}) // 选中的分类 |
| | | const processDefinitionList = ref([]) // 流程定义的列表 |
| | | |
| | | /** 查询列表 */ |
| | | const getList = async () => { |
| | | loading.value = true |
| | | try { |
| | | // 流程分类 |
| | | categoryList.value = await CategoryApi.getCategorySimpleList() |
| | | if (categoryList.value.length > 0) { |
| | | categoryActive.value = categoryList.value[0].code |
| | | } |
| | | // 流程定义 |
| | | processDefinitionList.value = await DefinitionApi.getProcessDefinitionList({ |
| | | suspensionState: 1 |
| | | }) |
| | | // 所有流程分类数据 |
| | | await getCategoryList() |
| | | // 所有流程定义数据 |
| | | await getProcessDefinitionList() |
| | | |
| | | // 如果 processInstanceId 非空,说明是重新发起 |
| | | if (processInstanceId?.length > 0) { |
| | |
| | | return |
| | | } |
| | | const processDefinition = processDefinitionList.value.find( |
| | | (item) => item.key == processInstance.processDefinition?.key |
| | | (item: any) => item.key == processInstance.processDefinition?.key |
| | | ) |
| | | if (!processDefinition) { |
| | | message.error('重新发起流程失败,原因:流程定义不存在') |
| | |
| | | } |
| | | } |
| | | |
| | | /** 选中分类对应的流程定义列表 */ |
| | | const categoryProcessDefinitionList = computed(() => { |
| | | return processDefinitionList.value.filter((item) => item.category == categoryActive.value) |
| | | /** 获取所有流程分类数据 */ |
| | | const getCategoryList = async () => { |
| | | try { |
| | | // 流程分类 |
| | | categoryList.value = await CategoryApi.getCategorySimpleList() |
| | | } finally { |
| | | } |
| | | } |
| | | |
| | | /** 获取所有流程定义数据 */ |
| | | const getProcessDefinitionList = async () => { |
| | | try { |
| | | // 流程定义 |
| | | processDefinitionList.value = await DefinitionApi.getProcessDefinitionList({ |
| | | suspensionState: 1 |
| | | }) |
| | | // 初始化过滤列表为全部流程定义 |
| | | filteredProcessDefinitionList.value = processDefinitionList.value |
| | | |
| | | // 在获取完所有数据后,设置第一个有效分类为激活状态 |
| | | if (availableCategories.value.length > 0 && !categoryActive.value?.code) { |
| | | categoryActive.value = availableCategories.value[0] |
| | | } |
| | | } finally { |
| | | } |
| | | } |
| | | |
| | | /** 搜索流程 */ |
| | | const filteredProcessDefinitionList = ref([]) // 用于存储搜索过滤后的流程定义 |
| | | const handleQuery = () => { |
| | | if (searchName.value.trim()) { |
| | | // 如果有搜索关键字,进行过滤 |
| | | filteredProcessDefinitionList.value = processDefinitionList.value.filter( |
| | | (definition: any) => definition.name.toLowerCase().includes(searchName.value.toLowerCase()) // 假设搜索依据是流程定义的名称 |
| | | ) |
| | | } else { |
| | | // 如果没有搜索关键字,恢复所有数据 |
| | | filteredProcessDefinitionList.value = processDefinitionList.value |
| | | } |
| | | } |
| | | |
| | | /** 流程定义的分组 */ |
| | | const processDefinitionGroup: any = computed(() => { |
| | | if (!processDefinitionList.value?.length) { |
| | | return {} |
| | | } |
| | | |
| | | const grouped = groupBy(filteredProcessDefinitionList.value, 'category') |
| | | // 按照 categoryList 的顺序重新组织数据 |
| | | const orderedGroup = {} |
| | | categoryList.value.forEach((category: any) => { |
| | | if (grouped[category.code]) { |
| | | orderedGroup[category.code] = grouped[category.code] |
| | | } |
| | | }) |
| | | return orderedGroup |
| | | }) |
| | | |
| | | // ========== 表单相关 ========== |
| | | const fApi = ref<ApiAttrs>() |
| | | const detailForm = ref({ |
| | | rule: [], |
| | | option: {}, |
| | | value: {} |
| | | }) // 流程表单详情 |
| | | const selectProcessDefinition = ref() // 选择的流程定义 |
| | | /** 左侧分类切换 */ |
| | | const handleCategoryClick = (category: any) => { |
| | | categoryActive.value = category |
| | | const categoryRef = proxy.$refs[`category-${category.code}`] // 获取点击分类对应的 DOM 元素 |
| | | if (categoryRef?.length) { |
| | | const scrollWrapper = proxy.$refs.scrollWrapper // 获取右侧滚动容器 |
| | | const categoryOffsetTop = categoryRef[0].offsetTop |
| | | |
| | | // 指定审批人 |
| | | const bpmnXML = ref(null) // BPMN 数据 |
| | | const startUserSelectTasks = ref([]) // 发起人需要选择审批人的用户任务列表 |
| | | const startUserSelectAssignees = ref({}) // 发起人选择审批人的数据 |
| | | const startUserSelectAssigneesFormRef = ref() // 发起人选择审批人的表单 Ref |
| | | const startUserSelectAssigneesFormRules = ref({}) // 发起人选择审批人的表单 Rules |
| | | const userList = ref<any[]>([]) // 用户列表 |
| | | // 滚动到对应位置 |
| | | scrollWrapper.scrollTo({ top: categoryOffsetTop, behavior: 'smooth' }) |
| | | } |
| | | } |
| | | |
| | | /** 通过分类 code 获取对应的名称 */ |
| | | const getCategoryName = (categoryCode: string) => { |
| | | return categoryList.value?.find((ctg: any) => ctg.code === categoryCode)?.name |
| | | } |
| | | |
| | | // ========== 表单相关 ========== |
| | | const selectProcessDefinition = ref() // 选择的流程定义 |
| | | const processDefinitionDetailRef = ref() |
| | | |
| | | /** 处理选择流程的按钮操作 **/ |
| | | const handleSelect = async (row, formVariables) => { |
| | | const handleSelect = async (row, formVariables?) => { |
| | | // 设置选择的流程 |
| | | selectProcessDefinition.value = row |
| | | // 初始化流程定义详情 |
| | | await nextTick() |
| | | processDefinitionDetailRef.value?.initProcessInfo(row, formVariables) |
| | | } |
| | | |
| | | // 重置指定审批人 |
| | | startUserSelectTasks.value = [] |
| | | startUserSelectAssignees.value = {} |
| | | startUserSelectAssigneesFormRules.value = {} |
| | | /** 处理滚动事件,和左侧分类联动 */ |
| | | const handleScroll = (e: any) => { |
| | | // 直接使用事件对象获取滚动位置 |
| | | const scrollTop = e.scrollTop |
| | | |
| | | // 情况一:流程表单 |
| | | if (row.formType == 10) { |
| | | // 设置表单 |
| | | setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables) |
| | | // 加载流程图 |
| | | const processDefinitionDetail = await DefinitionApi.getProcessDefinition(row.id) |
| | | if (processDefinitionDetail) { |
| | | bpmnXML.value = processDefinitionDetail.bpmnXml |
| | | startUserSelectTasks.value = processDefinitionDetail.startUserSelectTasks |
| | | |
| | | // 设置指定审批人 |
| | | if (startUserSelectTasks.value?.length > 0) { |
| | | detailForm.value.rule.push({ |
| | | type: 'startUserSelect', |
| | | props: { |
| | | title: '指定审批人' |
| | | } |
| | | }) |
| | | // 设置校验规则 |
| | | for (const userTask of startUserSelectTasks.value) { |
| | | startUserSelectAssignees.value[userTask.id] = [] |
| | | startUserSelectAssigneesFormRules.value[userTask.id] = [ |
| | | { required: true, message: '请选择审批人', trigger: 'blur' } |
| | | ] |
| | | // 获取所有分类区域的位置信息 |
| | | const categoryPositions = categoryList.value |
| | | .map((category: CategoryVO) => { |
| | | const categoryRef = proxy.$refs[`category-${category.code}`] |
| | | if (categoryRef?.[0]) { |
| | | return { |
| | | code: category.code, |
| | | offsetTop: categoryRef[0].offsetTop, |
| | | height: categoryRef[0].offsetHeight |
| | | } |
| | | // 加载用户列表 |
| | | userList.value = await UserApi.getSimpleUserList() |
| | | } |
| | | return null |
| | | }) |
| | | .filter(Boolean) |
| | | |
| | | // 查找当前滚动位置对应的分类 |
| | | let currentCategory = categoryPositions[0] |
| | | for (const position of categoryPositions) { |
| | | // 为了更好的用户体验,可以添加一个缓冲区域(比如 50px) |
| | | if (scrollTop >= position.offsetTop - 50) { |
| | | currentCategory = position |
| | | } else { |
| | | break |
| | | } |
| | | // 情况二:业务表单 |
| | | } else if (row.formCustomCreatePath) { |
| | | await push({ |
| | | path: row.formCustomCreatePath |
| | | }) |
| | | // 这里暂时无需加载流程图,因为跳出到另外个 Tab; |
| | | } |
| | | |
| | | // 更新当前 active 的分类 |
| | | if (currentCategory && categoryActive.value.code !== currentCategory.code) { |
| | | categoryActive.value = categoryList.value.find( |
| | | (c: CategoryVO) => c.code === currentCategory.code |
| | | ) |
| | | } |
| | | } |
| | | |
| | | /** 提交按钮 */ |
| | | const submitForm = async (formData) => { |
| | | if (!fApi.value || !selectProcessDefinition.value) { |
| | | return |
| | | } |
| | | // 如果有指定审批人,需要校验 |
| | | if (startUserSelectTasks.value?.length > 0) { |
| | | await startUserSelectAssigneesFormRef.value.validate() |
| | | /** 过滤出有流程的分类列表。目的:只展示有流程的分类 */ |
| | | const availableCategories = computed(() => { |
| | | if (!categoryList.value?.length || !processDefinitionGroup.value) { |
| | | return [] |
| | | } |
| | | |
| | | // 提交请求 |
| | | fApi.value.btn.loading(true) |
| | | try { |
| | | await ProcessInstanceApi.createProcessInstance({ |
| | | processDefinitionId: selectProcessDefinition.value.id, |
| | | variables: formData, |
| | | startUserSelectAssignees: startUserSelectAssignees.value |
| | | }) |
| | | // 提示 |
| | | message.success('发起流程成功') |
| | | // 跳转回去 |
| | | delView(unref(currentRoute)) |
| | | await push({ |
| | | name: 'BpmProcessInstanceMy' |
| | | }) |
| | | } finally { |
| | | fApi.value.btn.loading(false) |
| | | } |
| | | } |
| | | // 获取所有有流程的分类代码 |
| | | const availableCategoryCodes = Object.keys(processDefinitionGroup.value) |
| | | |
| | | // 过滤出有流程的分类 |
| | | return categoryList.value.filter((category: CategoryVO) => |
| | | availableCategoryCodes.includes(category.code) |
| | | ) |
| | | }) |
| | | |
| | | /** 初始化 */ |
| | | onMounted(() => { |
| | | getList() |
| | | }) |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .process-definition-container::before { |
| | | content: ''; |
| | | border-left: 1px solid #e6e6e6; |
| | | position: absolute; |
| | | left: 20.8%; |
| | | height: 100%; |
| | | } |
| | | :deep() { |
| | | .definition-item-card { |
| | | .el-card__body { |
| | | padding: 14px; |
| | | } |
| | | } |
| | | } |
| | | </style> |
对比新文件 |
| | |
| | | <template> |
| | | |
| | | <!-- 第一步,通过流程定义的列表,选择对应的流程 --> |
| | | <ContentWrap v-if="!selectProcessDefinition" v-loading="loading"> |
| | | <el-tabs tab-position="left" v-model="categoryActive"> |
| | | <el-tab-pane |
| | | :label="category.name" |
| | | :name="category.code" |
| | | :key="category.code" |
| | | v-for="category in categoryList" |
| | | > |
| | | <el-row :gutter="20"> |
| | | <el-col |
| | | :lg="6" |
| | | :sm="12" |
| | | :xs="24" |
| | | v-for="definition in categoryProcessDefinitionList" |
| | | :key="definition.id" |
| | | > |
| | | <el-card |
| | | shadow="hover" |
| | | class="mb-20px cursor-pointer" |
| | | @click="handleSelect(definition)" |
| | | > |
| | | <template #default> |
| | | <div class="flex"> |
| | | <el-image :src="definition.icon" class="w-32px h-32px" /> |
| | | <el-text class="!ml-10px" size="large">{{ definition.name }}</el-text> |
| | | </div> |
| | | </template> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | </el-tab-pane> |
| | | </el-tabs> |
| | | </ContentWrap> |
| | | |
| | | <!-- 第二步,填写表单,进行流程的提交 --> |
| | | <ContentWrap v-else> |
| | | <el-card class="box-card"> |
| | | <div class="clearfix"> |
| | | <span class="el-icon-document">申请信息【{{ selectProcessDefinition.name }}】</span> |
| | | <el-button style="float: right" type="primary" @click="selectProcessDefinition = undefined"> |
| | | <Icon icon="ep:delete" /> 选择其它流程 |
| | | </el-button> |
| | | </div> |
| | | <el-col :span="16" :offset="6" style="margin-top: 20px"> |
| | | <form-create |
| | | :rule="detailForm.rule" |
| | | v-model:api="fApi" |
| | | v-model="detailForm.value" |
| | | :option="detailForm.option" |
| | | @submit="submitForm" |
| | | > |
| | | <template #type-startUserSelect> |
| | | <el-col :span="24"> |
| | | <el-card class="mb-10px"> |
| | | <template #header>指定审批人</template> |
| | | <el-form |
| | | :model="startUserSelectAssignees" |
| | | :rules="startUserSelectAssigneesFormRules" |
| | | ref="startUserSelectAssigneesFormRef" |
| | | > |
| | | <el-form-item |
| | | v-for="userTask in startUserSelectTasks" |
| | | :key="userTask.id" |
| | | :label="`任务【${userTask.name}】`" |
| | | :prop="userTask.id" |
| | | > |
| | | <el-select |
| | | v-model="startUserSelectAssignees[userTask.id]" |
| | | multiple |
| | | placeholder="请选择审批人" |
| | | > |
| | | <el-option |
| | | v-for="user in userList" |
| | | :key="user.id" |
| | | :label="user.nickname" |
| | | :value="user.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-form> |
| | | </el-card> |
| | | </el-col> |
| | | </template> |
| | | </form-create> |
| | | </el-col> |
| | | </el-card> |
| | | <!-- 流程图预览 --> |
| | | <ProcessInstanceBpmnViewer :bpmn-xml="bpmnXML as any" /> |
| | | </ContentWrap> |
| | | </template> |
| | | <script lang="ts" setup> |
| | | import * as DefinitionApi from '@/api/bpm/definition' |
| | | import * as ProcessInstanceApi from '@/api/bpm/processInstance' |
| | | import { decodeFields, setConfAndFields2 } from '@/utils/formCreate' |
| | | import type { ApiAttrs } from '@form-create/element-ui/types/config' |
| | | import ProcessInstanceBpmnViewer from '../detail/ProcessInstanceBpmnViewer.vue' |
| | | import { CategoryApi } from '@/api/bpm/category' |
| | | import { useTagsViewStore } from '@/store/modules/tagsView' |
| | | import * as UserApi from '@/api/system/user' |
| | | |
| | | defineOptions({ name: 'BpmProcessInstanceCreate' }) |
| | | |
| | | const route = useRoute() // 路由 |
| | | const { push, currentRoute } = useRouter() // 路由 |
| | | const message = useMessage() // 消息 |
| | | const { delView } = useTagsViewStore() // 视图操作 |
| | | |
| | | const processInstanceId = route.query.processInstanceId |
| | | const loading = ref(true) // 加载中 |
| | | const categoryList = ref([]) // 分类的列表 |
| | | const categoryActive = ref('') // 选中的分类 |
| | | const processDefinitionList = ref([]) // 流程定义的列表 |
| | | |
| | | /** 查询列表 */ |
| | | const getList = async () => { |
| | | loading.value = true |
| | | try { |
| | | // 流程分类 |
| | | categoryList.value = await CategoryApi.getCategorySimpleList() |
| | | if (categoryList.value.length > 0) { |
| | | categoryActive.value = categoryList.value[0].code |
| | | } |
| | | // 流程定义 |
| | | processDefinitionList.value = await DefinitionApi.getProcessDefinitionList({ |
| | | suspensionState: 1 |
| | | }) |
| | | |
| | | // 如果 processInstanceId 非空,说明是重新发起 |
| | | if (processInstanceId?.length > 0) { |
| | | const processInstance = await ProcessInstanceApi.getProcessInstance(processInstanceId) |
| | | if (!processInstance) { |
| | | message.error('重新发起流程失败,原因:流程实例不存在') |
| | | return |
| | | } |
| | | const processDefinition = processDefinitionList.value.find( |
| | | (item) => item.key == processInstance.processDefinition?.key |
| | | ) |
| | | if (!processDefinition) { |
| | | message.error('重新发起流程失败,原因:流程定义不存在') |
| | | return |
| | | } |
| | | await handleSelect(processDefinition, processInstance.formVariables) |
| | | } |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | /** 选中分类对应的流程定义列表 */ |
| | | const categoryProcessDefinitionList = computed(() => { |
| | | return processDefinitionList.value.filter((item) => item.category == categoryActive.value) |
| | | }) |
| | | |
| | | // ========== 表单相关 ========== |
| | | const fApi = ref<ApiAttrs>() |
| | | const detailForm = ref({ |
| | | rule: [], |
| | | option: {}, |
| | | value: {} |
| | | }) // 流程表单详情 |
| | | const selectProcessDefinition = ref() // 选择的流程定义 |
| | | |
| | | // 指定审批人 |
| | | const bpmnXML = ref(null) // BPMN 数据 |
| | | const startUserSelectTasks = ref([]) // 发起人需要选择审批人的用户任务列表 |
| | | const startUserSelectAssignees = ref({}) // 发起人选择审批人的数据 |
| | | const startUserSelectAssigneesFormRef = ref() // 发起人选择审批人的表单 Ref |
| | | const startUserSelectAssigneesFormRules = ref({}) // 发起人选择审批人的表单 Rules |
| | | const userList = ref<any[]>([]) // 用户列表 |
| | | |
| | | /** 处理选择流程的按钮操作 **/ |
| | | const handleSelect = async (row, formVariables) => { |
| | | // 设置选择的流程 |
| | | selectProcessDefinition.value = row |
| | | |
| | | // 重置指定审批人 |
| | | startUserSelectTasks.value = [] |
| | | startUserSelectAssignees.value = {} |
| | | startUserSelectAssigneesFormRules.value = {} |
| | | |
| | | // 情况一:流程表单 |
| | | if (row.formType == 10) { |
| | | // 设置表单 |
| | | // 注意:需要从 formVariables 中,移除不在 row.formFields 的值。 |
| | | // 原因是:后端返回的 formVariables 里面,会有一些非表单的信息。例如说,某个流程节点的审批人。 |
| | | // 这样,就可能导致一个流程被审批不通过后,重新发起时,会直接后端报错!!! |
| | | const allowedFields = decodeFields(row.formFields).map((fieldObj: any) => fieldObj.field) |
| | | for (const key in formVariables) { |
| | | if (!allowedFields.includes(key)) { |
| | | delete formVariables[key] |
| | | } |
| | | } |
| | | setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables) |
| | | |
| | | // 加载流程图 |
| | | const processDefinitionDetail = await DefinitionApi.getProcessDefinition(row.id) |
| | | if (processDefinitionDetail) { |
| | | bpmnXML.value = processDefinitionDetail.bpmnXml |
| | | startUserSelectTasks.value = processDefinitionDetail.startUserSelectTasks |
| | | |
| | | // 设置指定审批人 |
| | | if (startUserSelectTasks.value?.length > 0) { |
| | | detailForm.value.rule.push({ |
| | | type: 'startUserSelect', |
| | | props: { |
| | | title: '指定审批人' |
| | | } |
| | | }) |
| | | // 设置校验规则 |
| | | for (const userTask of startUserSelectTasks.value) { |
| | | startUserSelectAssignees.value[userTask.id] = [] |
| | | startUserSelectAssigneesFormRules.value[userTask.id] = [ |
| | | { required: true, message: '请选择审批人', trigger: 'blur' } |
| | | ] |
| | | } |
| | | // 加载用户列表 |
| | | userList.value = await UserApi.getSimpleUserList() |
| | | } |
| | | } |
| | | // 情况二:业务表单 |
| | | } else if (row.formCustomCreatePath) { |
| | | await push({ |
| | | path: row.formCustomCreatePath |
| | | }) |
| | | // 这里暂时无需加载流程图,因为跳出到另外个 Tab; |
| | | } |
| | | } |
| | | |
| | | /** 提交按钮 */ |
| | | const submitForm = async (formData) => { |
| | | if (!fApi.value || !selectProcessDefinition.value) { |
| | | return |
| | | } |
| | | // 如果有指定审批人,需要校验 |
| | | if (startUserSelectTasks.value?.length > 0) { |
| | | await startUserSelectAssigneesFormRef.value.validate() |
| | | } |
| | | |
| | | // 提交请求 |
| | | fApi.value.btn.loading(true) |
| | | try { |
| | | await ProcessInstanceApi.createProcessInstance({ |
| | | processDefinitionId: selectProcessDefinition.value.id, |
| | | variables: formData, |
| | | startUserSelectAssignees: startUserSelectAssignees.value |
| | | }) |
| | | // 提示 |
| | | message.success('发起流程成功') |
| | | // 跳转回去 |
| | | delView(unref(currentRoute)) |
| | | await push({ |
| | | name: 'BpmProcessInstanceMy' |
| | | }) |
| | | } finally { |
| | | fApi.value.btn.loading(false) |
| | | } |
| | | } |
| | | |
| | | /** 初始化 */ |
| | | onMounted(() => { |
| | | getList() |
| | | }) |
| | | </script> |
| | |
| | | <template> |
| | | <el-card v-loading="loading" class="box-card"> |
| | | <template #header> |
| | | <span class="el-icon-picture-outline">流程图</span> |
| | | </template> |
| | | <MyProcessViewer |
| | | key="designer" |
| | | :activityData="activityList" |
| | | :prefix="bpmnControlForm.prefix" |
| | | :processInstanceData="processInstance" |
| | | :taskData="tasks" |
| | | :value="bpmnXml" |
| | | v-bind="bpmnControlForm" |
| | | /> |
| | | <MyProcessViewer key="designer" :xml="view.bpmnXml" :view="view" class="process-viewer" /> |
| | | </el-card> |
| | | </template> |
| | | <script lang="ts" setup> |
| | | import { propTypes } from '@/utils/propTypes' |
| | | import { MyProcessViewer } from '@/components/bpmnProcessDesigner/package' |
| | | import * as ActivityApi from '@/api/bpm/activity' |
| | | |
| | | defineOptions({ name: 'BpmProcessInstanceBpmnViewer' }) |
| | | |
| | | const props = defineProps({ |
| | | loading: propTypes.bool, // 是否加载中 |
| | | id: propTypes.string, // 流程实例的编号 |
| | | processInstance: propTypes.any, // 流程实例的信息 |
| | | tasks: propTypes.array, // 流程任务的数组 |
| | | bpmnXml: propTypes.string // BPMN XML |
| | | loading: propTypes.bool.def(false), // 是否加载中 |
| | | bpmnXml: propTypes.string, // BPMN XML |
| | | modelView: propTypes.object |
| | | }) |
| | | |
| | | const bpmnControlForm = ref({ |
| | | prefix: 'flowable' |
| | | }) |
| | | const activityList = ref([]) // 任务列表 |
| | | const view = ref({ |
| | | bpmnXml: '' |
| | | }) // BPMN 流程图数据 |
| | | |
| | | |
| | | /** 只有 loading 完成时,才去加载流程列表 */ |
| | | watch( |
| | | () => props.loading, |
| | | async (value) => { |
| | | if (value && props.id) { |
| | | activityList.value = await ActivityApi.getActivityList({ |
| | | processInstanceId: props.id |
| | | }) |
| | | () => props.modelView, |
| | | async (newModelView) => { |
| | | // 加载最新 |
| | | if (newModelView) { |
| | | //@ts-ignore |
| | | view.value = newModelView |
| | | } |
| | | } |
| | | ) |
| | | |
| | | /** 监听 bpmnXml */ |
| | | watch( |
| | | () => props.bpmnXml, |
| | | (value) => { |
| | | view.value.bpmnXml = value |
| | | } |
| | | ) |
| | | </script> |
| | | <style> |
| | | <style lang="scss" scoped> |
| | | .box-card { |
| | | height: 100%; |
| | | width: 100%; |
| | | margin-bottom: 20px; |
| | | margin-bottom: 0; |
| | | |
| | | :deep(.el-card__body) { |
| | | height: 100%; |
| | | padding: 0; |
| | | } |
| | | |
| | | :deep(.process-viewer) { |
| | | height: 100% !important; |
| | | min-height: 100%; |
| | | width: 100%; |
| | | overflow: auto; |
| | | } |
| | | } |
| | | </style> |
对比新文件 |
| | |
| | | <template> |
| | | <div |
| | | class="h-50px bottom-10 text-14px flex items-center color-#32373c dark:color-#fff font-bold btn-container" |
| | | > |
| | | <!-- 【通过】按钮 --> |
| | | <el-popover |
| | | :visible="popOverVisible.approve" |
| | | placement="top-end" |
| | | :width="420" |
| | | trigger="click" |
| | | v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.APPROVE)" |
| | | > |
| | | <template #reference> |
| | | <el-button plain type="success" @click="openPopover('approve')"> |
| | | <Icon icon="ep:select" /> {{ getButtonDisplayName(OperationButtonType.APPROVE) }} |
| | | </el-button> |
| | | </template> |
| | | <!-- 审批表单 --> |
| | | <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading"> |
| | | <el-form |
| | | label-position="top" |
| | | class="mb-auto" |
| | | ref="approveFormRef" |
| | | :model="approveReasonForm" |
| | | :rules="approveReasonRule" |
| | | label-width="100px" |
| | | > |
| | | <el-card v-if="runningTask?.formId > 0" class="mb-15px !-mt-10px"> |
| | | <template #header> |
| | | <span class="el-icon-picture-outline"> 填写表单【{{ runningTask?.formName }}】 </span> |
| | | </template> |
| | | <form-create |
| | | v-model="approveForm.value" |
| | | v-model:api="approveFormFApi" |
| | | :option="approveForm.option" |
| | | :rule="approveForm.rule" |
| | | /> |
| | | </el-card> |
| | | <el-form-item label="审批意见" prop="reason"> |
| | | <el-input |
| | | v-model="approveReasonForm.reason" |
| | | placeholder="请输入审批意见" |
| | | type="textarea" |
| | | :rows="4" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button :disabled="formLoading" type="success" @click="handleAudit(true, approveFormRef)"> |
| | | {{ getButtonDisplayName(OperationButtonType.APPROVE) }} |
| | | </el-button> |
| | | <el-button @click="closePropover('approve', approveFormRef)"> 取消 </el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | </div> |
| | | </el-popover> |
| | | |
| | | <!-- 【拒绝】按钮 --> |
| | | <el-popover |
| | | :visible="popOverVisible.reject" |
| | | placement="top-end" |
| | | :width="420" |
| | | trigger="click" |
| | | v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.REJECT)" |
| | | > |
| | | <template #reference> |
| | | <el-button class="mr-20px" plain type="danger" @click="openPopover('reject')"> |
| | | <Icon icon="ep:close" /> {{ getButtonDisplayName(OperationButtonType.REJECT) }} |
| | | </el-button> |
| | | </template> |
| | | <!-- 审批表单 --> |
| | | <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading"> |
| | | <el-form |
| | | label-position="top" |
| | | class="mb-auto" |
| | | ref="rejectFormRef" |
| | | :model="rejectReasonForm" |
| | | :rules="rejectReasonRule" |
| | | label-width="100px" |
| | | > |
| | | <el-form-item label="审批意见" prop="reason"> |
| | | <el-input |
| | | v-model="rejectReasonForm.reason" |
| | | placeholder="请输入审批意见" |
| | | type="textarea" |
| | | :rows="4" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button :disabled="formLoading" type="danger" @click="handleAudit(false,rejectFormRef)"> |
| | | {{ getButtonDisplayName(OperationButtonType.REJECT) }} |
| | | </el-button> |
| | | <el-button @click="closePropover('reject', rejectFormRef)"> 取消 </el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | </div> |
| | | </el-popover> |
| | | |
| | | <!-- 【抄送】按钮 --> |
| | | <el-popover |
| | | :visible="popOverVisible.copy" |
| | | placement="top-start" |
| | | :width="420" |
| | | trigger="click" |
| | | v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.COPY)" |
| | | > |
| | | <template #reference> |
| | | <div @click="openPopover('copy')" class="hover-bg-gray-100 rounded-xl p-6px"> |
| | | <Icon :size="14" icon="svg-icon:send" /> |
| | | {{ getButtonDisplayName(OperationButtonType.COPY) }} |
| | | </div> |
| | | </template> |
| | | <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading"> |
| | | <el-form |
| | | label-position="top" |
| | | class="mb-auto" |
| | | ref="copyFormRef" |
| | | :model="copyForm" |
| | | :rules="copyFormRule" |
| | | label-width="100px" |
| | | > |
| | | <el-form-item label="抄送人" prop="copyUserIds"> |
| | | <el-select |
| | | v-model="copyForm.copyUserIds" |
| | | clearable |
| | | style="width: 100%" |
| | | multiple |
| | | placeholder="请选择抄送人" |
| | | > |
| | | <el-option |
| | | v-for="item in userOptions" |
| | | :key="item.id" |
| | | :label="item.nickname" |
| | | :value="item.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="抄送意见" prop="copyReason"> |
| | | <el-input |
| | | v-model="copyForm.copyReason" |
| | | clearable |
| | | placeholder="请输入抄送意见" |
| | | type="textarea" |
| | | :rows="3" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button :disabled="formLoading" type="primary" @click="handleCopy"> |
| | | {{ getButtonDisplayName(OperationButtonType.COPY) }} |
| | | </el-button> |
| | | <el-button @click="closePropover('copy', copyFormRef)"> 取消 </el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | </div> |
| | | </el-popover> |
| | | |
| | | <!-- 【转办】按钮 --> |
| | | <el-popover |
| | | :visible="popOverVisible.transfer" |
| | | placement="top-start" |
| | | :width="420" |
| | | trigger="click" |
| | | v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.TRANSFER)" |
| | | > |
| | | <template #reference> |
| | | <div @click="openPopover('transfer')" class="hover-bg-gray-100 rounded-xl p-6px"> |
| | | <Icon :size="14" icon="fa:share-square-o" /> |
| | | {{ getButtonDisplayName(OperationButtonType.TRANSFER) }} |
| | | </div> |
| | | </template> |
| | | <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading"> |
| | | <el-form |
| | | label-position="top" |
| | | class="mb-auto" |
| | | ref="transferFormRef" |
| | | :model="transferForm" |
| | | :rules="transferFormRule" |
| | | label-width="100px" |
| | | > |
| | | <el-form-item label="新审批人" prop="assigneeUserId"> |
| | | <el-select v-model="transferForm.assigneeUserId" clearable style="width: 100%"> |
| | | <el-option |
| | | v-for="item in userOptions" |
| | | :key="item.id" |
| | | :label="item.nickname" |
| | | :value="item.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="审批意见" prop="reason"> |
| | | <el-input |
| | | v-model="transferForm.reason" |
| | | clearable |
| | | placeholder="请输入审批意见" |
| | | type="textarea" |
| | | :rows="3" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button :disabled="formLoading" type="primary" @click="handleTransfer()"> |
| | | {{ getButtonDisplayName(OperationButtonType.TRANSFER) }} |
| | | </el-button> |
| | | <el-button @click="closePropover('transfer', transferFormRef)"> 取消 </el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | </div> |
| | | </el-popover> |
| | | |
| | | <!-- 【委派】按钮 --> |
| | | <el-popover |
| | | :visible="popOverVisible.delegate" |
| | | placement="top-start" |
| | | :width="420" |
| | | trigger="click" |
| | | v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.DELEGATE)" |
| | | > |
| | | <template #reference> |
| | | <div @click="openPopover('delegate')" class="hover-bg-gray-100 rounded-xl p-6px"> |
| | | <Icon :size="14" icon="ep:position" /> |
| | | {{ getButtonDisplayName(OperationButtonType.DELEGATE) }} |
| | | </div> |
| | | </template> |
| | | <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading"> |
| | | <el-form |
| | | label-position="top" |
| | | class="mb-auto" |
| | | ref="delegateFormRef" |
| | | :model="delegateForm" |
| | | :rules="delegateFormRule" |
| | | label-width="100px" |
| | | > |
| | | <el-form-item label="接收人" prop="delegateUserId"> |
| | | <el-select v-model="delegateForm.delegateUserId" clearable style="width: 100%"> |
| | | <el-option |
| | | v-for="item in userOptions" |
| | | :key="item.id" |
| | | :label="item.nickname" |
| | | :value="item.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="审批意见" prop="reason"> |
| | | <el-input |
| | | v-model="delegateForm.reason" |
| | | clearable |
| | | placeholder="请输入审批意见" |
| | | type="textarea" |
| | | :rows="3" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button :disabled="formLoading" type="primary" @click="handleDelegate()"> |
| | | {{ getButtonDisplayName(OperationButtonType.DELEGATE) }} |
| | | </el-button> |
| | | <el-button @click="closePropover('delegate', delegateFormRef)"> 取消 </el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | </div> |
| | | </el-popover> |
| | | |
| | | <!-- 【加签】按钮 当前任务审批人为A,向前加签选了一个C,则需要C先审批,然后再是A审批,向后加签B,A审批完,需要B再审批完,才算完成这个任务节点 --> |
| | | <el-popover |
| | | :visible="popOverVisible.addSign" |
| | | placement="top-start" |
| | | :width="420" |
| | | trigger="click" |
| | | v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.ADD_SIGN)" |
| | | > |
| | | <template #reference> |
| | | <div @click="openPopover('addSign')" class="hover-bg-gray-100 rounded-xl p-6px"> |
| | | <Icon :size="14" icon="ep:plus" /> |
| | | {{ getButtonDisplayName(OperationButtonType.ADD_SIGN) }} |
| | | </div> |
| | | </template> |
| | | <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading"> |
| | | <el-form |
| | | label-position="top" |
| | | class="mb-auto" |
| | | ref="addSignFormRef" |
| | | :model="addSignForm" |
| | | :rules="addSignFormRule" |
| | | label-width="100px" |
| | | > |
| | | <el-form-item label="加签处理人" prop="addSignUserIds"> |
| | | <el-select v-model="addSignForm.addSignUserIds" multiple clearable style="width: 100%"> |
| | | <el-option |
| | | v-for="item in userOptions" |
| | | :key="item.id" |
| | | :label="item.nickname" |
| | | :value="item.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="审批意见" prop="reason"> |
| | | <el-input |
| | | v-model="addSignForm.reason" |
| | | clearable |
| | | placeholder="请输入审批意见" |
| | | type="textarea" |
| | | :rows="3" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button :disabled="formLoading" type="primary" @click="handlerAddSign('before')"> |
| | | 向前{{ getButtonDisplayName(OperationButtonType.ADD_SIGN) }} |
| | | </el-button> |
| | | <el-button :disabled="formLoading" type="primary" @click="handlerAddSign('after')"> |
| | | 向后{{ getButtonDisplayName(OperationButtonType.ADD_SIGN) }} |
| | | </el-button> |
| | | <el-button @click="closePropover('addSign', addSignFormRef)"> 取消 </el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | </div> |
| | | </el-popover> |
| | | |
| | | <!-- 【减签】按钮 --> |
| | | <el-popover |
| | | :visible="popOverVisible.deleteSign" |
| | | placement="top-start" |
| | | :width="420" |
| | | trigger="click" |
| | | v-if="runningTask?.children.length > 0" |
| | | > |
| | | <template #reference> |
| | | <div @click="openPopover('deleteSign')" class="hover-bg-gray-100 rounded-xl p-6px"> |
| | | <Icon :size="14" icon="ep:semi-select" /> 减签 |
| | | </div> |
| | | </template> |
| | | <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading"> |
| | | <el-form |
| | | label-position="top" |
| | | class="mb-auto" |
| | | ref="deleteSignFormRef" |
| | | :model="deleteSignForm" |
| | | :rules="deleteSignFormRule" |
| | | label-width="100px" |
| | | > |
| | | <el-form-item label="减签人员" prop="deleteSignTaskId"> |
| | | <el-select v-model="deleteSignForm.deleteSignTaskId" clearable style="width: 100%"> |
| | | <el-option |
| | | v-for="item in runningTask.children" |
| | | :key="item.id" |
| | | :label="getDeleteSignUserLabel(item)" |
| | | :value="item.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="审批意见" prop="reason"> |
| | | <el-input |
| | | v-model="deleteSignForm.reason" |
| | | clearable |
| | | placeholder="请输入审批意见" |
| | | type="textarea" |
| | | :rows="3" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button :disabled="formLoading" type="primary" @click="handlerDeleteSign()"> |
| | | 减签 |
| | | </el-button> |
| | | <el-button @click="closePropover('deleteSign', deleteSignFormRef)"> 取消 </el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | </div> |
| | | </el-popover> |
| | | |
| | | <!-- 【退回】按钮 --> |
| | | <el-popover |
| | | :visible="popOverVisible.return" |
| | | placement="top-start" |
| | | :width="420" |
| | | trigger="click" |
| | | v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.RETURN)" |
| | | > |
| | | <template #reference> |
| | | <div @click="openPopover('return')" class="hover-bg-gray-100 rounded-xl p-6px"> |
| | | <Icon :size="14" icon="ep:back" /> |
| | | {{ getButtonDisplayName(OperationButtonType.RETURN) }} |
| | | </div> |
| | | </template> |
| | | <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading"> |
| | | <el-form |
| | | label-position="top" |
| | | class="mb-auto" |
| | | ref="returnFormRef" |
| | | :model="returnForm" |
| | | :rules="returnFormRule" |
| | | label-width="100px" |
| | | > |
| | | <el-form-item label="退回节点" prop="targetTaskDefinitionKey"> |
| | | <el-select v-model="returnForm.targetTaskDefinitionKey" clearable style="width: 100%"> |
| | | <el-option |
| | | v-for="item in returnList" |
| | | :key="item.taskDefinitionKey" |
| | | :label="item.name" |
| | | :value="item.taskDefinitionKey" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="退回理由" prop="returnReason"> |
| | | <el-input |
| | | v-model="returnForm.returnReason" |
| | | clearable |
| | | placeholder="请输入退回理由" |
| | | type="textarea" |
| | | :rows="3" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button :disabled="formLoading" type="primary" @click="handleReturn()"> |
| | | {{ getButtonDisplayName(OperationButtonType.RETURN) }} |
| | | </el-button> |
| | | <el-button @click="closePropover('return', returnFormRef)"> 取消 </el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | </div> |
| | | </el-popover> |
| | | |
| | | <!--【取消】按钮 这个对应发起人的取消, 只有发起人可以取消 --> |
| | | <el-popover |
| | | :visible="popOverVisible.cancel" |
| | | placement="top-start" |
| | | :width="420" |
| | | trigger="click" |
| | | v-if=" |
| | | userId === processInstance?.startUser?.id && !isEndProcessStatus(processInstance?.status) |
| | | " |
| | | > |
| | | <template #reference> |
| | | <div @click="openPopover('cancel')" class="hover-bg-gray-100 rounded-xl p-6px"> |
| | | <Icon :size="14" icon="fa:mail-reply" /> 取消 |
| | | </div> |
| | | </template> |
| | | <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading"> |
| | | <el-form |
| | | label-position="top" |
| | | class="mb-auto" |
| | | ref="cancelFormRef" |
| | | :model="cancelForm" |
| | | :rules="cancelFormRule" |
| | | label-width="100px" |
| | | > |
| | | <el-form-item label="取消理由" prop="cancelReason"> |
| | | <span class="text-#878c93 text-12px"> 取消后,该审批流程将自动结束</span> |
| | | <el-input |
| | | v-model="cancelForm.cancelReason" |
| | | clearable |
| | | placeholder="请输入取消理由" |
| | | type="textarea" |
| | | :rows="3" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button :disabled="formLoading" type="primary" @click="handleCancel()"> |
| | | 确认 |
| | | </el-button> |
| | | <el-button @click="closePropover('cancel', cancelFormRef)"> 取消 </el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | </div> |
| | | </el-popover> |
| | | <!-- 【再次提交】 按钮--> |
| | | <div |
| | | @click="handleReCreate()" |
| | | class="hover-bg-gray-100 rounded-xl p-6px" |
| | | v-if=" |
| | | userId === processInstance?.startUser?.id && |
| | | isEndProcessStatus(processInstance?.status) && |
| | | processDefinition?.formType === 10 |
| | | " |
| | | > |
| | | <Icon :size="14" icon="ep:refresh" /> 再次提交 |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <script lang="ts" setup> |
| | | import { useUserStoreWithOut } from '@/store/modules/user' |
| | | import { setConfAndFields2 } from '@/utils/formCreate' |
| | | import * as TaskApi from '@/api/bpm/task' |
| | | import * as ProcessInstanceApi from '@/api/bpm/processInstance' |
| | | import * as UserApi from '@/api/system/user' |
| | | import { |
| | | OperationButtonType, |
| | | OPERATION_BUTTON_NAME |
| | | } from '@/components/SimpleProcessDesignerV2/src/consts' |
| | | import { BpmProcessInstanceStatus, BpmModelFormType } from '@/utils/constants' |
| | | import type { FormInstance, FormRules } from 'element-plus' |
| | | defineOptions({ name: 'ProcessInstanceBtnContainer' }) |
| | | |
| | | const router = useRouter() // 路由 |
| | | const message = useMessage() // 消息弹窗 |
| | | |
| | | const userId = useUserStoreWithOut().getUser.id // 当前登录的编号 |
| | | const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 |
| | | |
| | | const props = defineProps< { |
| | | processInstance: any, // 流程实例信息 |
| | | processDefinition: any, // 流程定义信息 |
| | | userOptions: UserApi.UserVO[], |
| | | normalForm: any, // 流程表单 formCreate |
| | | normalFormApi: any, // 流程表单 formCreate Api |
| | | writableFields: string[] // 流程表单可以编辑的字段 |
| | | }>() |
| | | |
| | | const formLoading = ref(false) // 表单加载中 |
| | | const popOverVisible = ref({ |
| | | approve: false, |
| | | reject: false, |
| | | transfer: false, |
| | | delegate: false, |
| | | addSign: false, |
| | | return: false, |
| | | copy: false, |
| | | cancel: false, |
| | | deleteSign: false |
| | | }) // 气泡卡是否展示 |
| | | const returnList = ref([] as any) // 退回节点 |
| | | |
| | | // ========== 审批信息 ========== |
| | | const runningTask = ref<any>() // 运行中的任务 |
| | | const approveForm = ref<any>({}) // 审批通过时,额外的补充信息 |
| | | const approveFormFApi = ref<any>({}) // approveForms 的 fAPi |
| | | |
| | | // 审批通过意见表单 |
| | | const approveFormRef = ref<FormInstance>() |
| | | const approveReasonForm = reactive({ |
| | | reason: '' |
| | | }) |
| | | const approveReasonRule = reactive<FormRules<typeof approveReasonForm>>({ |
| | | reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }], |
| | | }) |
| | | // 拒绝表单 |
| | | const rejectFormRef = ref<FormInstance>() |
| | | const rejectReasonForm = reactive({ |
| | | reason: '' |
| | | }) |
| | | const rejectReasonRule = reactive<FormRules<typeof rejectReasonForm>>({ |
| | | reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }], |
| | | }) |
| | | |
| | | // 抄送表单 |
| | | const copyFormRef = ref<FormInstance>() |
| | | const copyForm = reactive({ |
| | | copyUserIds: [], |
| | | copyReason: '' |
| | | }) |
| | | const copyFormRule = reactive<FormRules<typeof copyForm>>({ |
| | | copyUserIds: [{ required: true, message: '抄送人不能为空', trigger: 'change' }] |
| | | }) |
| | | |
| | | // 转办表单 |
| | | const transferFormRef = ref<FormInstance>() |
| | | const transferForm = reactive({ |
| | | assigneeUserId: undefined, |
| | | reason: '' |
| | | }) |
| | | const transferFormRule = reactive<FormRules<typeof transferForm>>({ |
| | | assigneeUserId: [{ required: true, message: '新审批人不能为空', trigger: 'change' }], |
| | | reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }], |
| | | }) |
| | | |
| | | // 委派表单 |
| | | const delegateFormRef = ref<FormInstance>() |
| | | const delegateForm = reactive({ |
| | | delegateUserId: undefined, |
| | | reason: '' |
| | | }) |
| | | const delegateFormRule = reactive<FormRules<typeof delegateForm>>({ |
| | | delegateUserId: [{ required: true, message: '接收人不能为空', trigger: 'change' }], |
| | | reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }], |
| | | }) |
| | | |
| | | // 加签表单 |
| | | const addSignFormRef = ref<FormInstance>() |
| | | const addSignForm = reactive({ |
| | | addSignUserIds: undefined, |
| | | reason: '' |
| | | }) |
| | | const addSignFormRule = reactive<FormRules<typeof addSignForm>>({ |
| | | addSignUserIds: [{ required: true, message: '加签处理人不能为空', trigger: 'change' }], |
| | | reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }], |
| | | }) |
| | | |
| | | // 减签表单 |
| | | const deleteSignFormRef = ref<FormInstance>() |
| | | const deleteSignForm = reactive({ |
| | | deleteSignTaskId: undefined, |
| | | reason: '' |
| | | }) |
| | | const deleteSignFormRule = reactive<FormRules<typeof deleteSignForm>>({ |
| | | deleteSignTaskId: [{ required: true, message: '减签人员不能为空', trigger: 'change' }], |
| | | reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }], |
| | | }) |
| | | |
| | | // 退回表单 |
| | | const returnFormRef = ref<FormInstance>() |
| | | const returnForm = reactive({ |
| | | targetTaskDefinitionKey: undefined, |
| | | returnReason: '' |
| | | }) |
| | | const returnFormRule = reactive<FormRules<typeof returnForm>>({ |
| | | targetTaskDefinitionKey: [{ required: true, message: '退回节点不能为空', trigger: 'change' }], |
| | | returnReason: [{ required: true, message: '退回理由不能为空', trigger: 'blur' }] |
| | | }) |
| | | |
| | | // 取消表单 |
| | | const cancelFormRef = ref<FormInstance>() |
| | | const cancelForm = reactive({ |
| | | cancelReason: '' |
| | | }) |
| | | const cancelFormRule = reactive<FormRules<typeof cancelForm>>({ |
| | | cancelReason: [{ required: true, message: '取消理由不能为空', trigger: 'blur' }], |
| | | }) |
| | | |
| | | /** 监听 approveFormFApis,实现它对应的 form-create 初始化后,隐藏掉对应的表单提交按钮 */ |
| | | watch( |
| | | () => approveFormFApi.value, |
| | | (val) => { |
| | | val?.btn?.show(false) |
| | | val?.resetBtn?.show(false) |
| | | }, |
| | | { |
| | | deep: true |
| | | } |
| | | ) |
| | | |
| | | /** 弹出气泡卡 */ |
| | | const openPopover = async (type: string) => { |
| | | if (type === 'approve') { |
| | | // 校验流程表单 |
| | | const valid = await validateNormalForm(); |
| | | if (!valid) { |
| | | message.warning('表单校验不通过,请先完善表单!!') |
| | | return; |
| | | } |
| | | } |
| | | if (type === 'return') { |
| | | // 获取退回节点 |
| | | returnList.value = await TaskApi.getTaskListByReturn(runningTask.value.id) |
| | | if (returnList.value.length === 0) { |
| | | message.warning('当前没有可退回的节点') |
| | | return |
| | | } |
| | | } |
| | | Object.keys(popOverVisible.value).forEach((item) => { |
| | | popOverVisible.value[item] = item === type |
| | | }) |
| | | // await nextTick() |
| | | // formRef.value.resetFields() |
| | | } |
| | | |
| | | /** 关闭气泡卡 */ |
| | | const closePropover = (type: string, formRef: FormInstance | undefined) => { |
| | | if (formRef) { |
| | | formRef.resetFields() |
| | | } |
| | | popOverVisible.value[type] = false |
| | | } |
| | | |
| | | /** 处理审批通过和不通过的操作 */ |
| | | const handleAudit = async (pass: boolean, formRef: FormInstance | undefined) => { |
| | | formLoading.value = true |
| | | try { |
| | | // 校验表单 |
| | | if (!formRef) return |
| | | await formRef.validate() |
| | | if (pass) { |
| | | // 获取修改的流程变量, 暂时只支持流程表单 |
| | | const variables = getUpdatedProcessInstanceVaiables(); |
| | | // 审批通过数据 |
| | | const data = { |
| | | id: runningTask.value.id, |
| | | reason: approveReasonForm.reason, |
| | | variables // 审批通过, 把修改的字段值赋于流程实例变量 |
| | | } |
| | | // 多表单处理,并且有额外的 approveForm 表单,需要校验 + 拼接到 data 表单里提交 |
| | | // TODO 芋艿 任务有多表单这里要如何处理,会和可编辑的字段冲突 |
| | | const formCreateApi = approveFormFApi.value |
| | | if (Object.keys(formCreateApi)?.length > 0) { |
| | | await formCreateApi.validate() |
| | | // @ts-ignore |
| | | data.variables = approveForm.value.value |
| | | } |
| | | await TaskApi.approveTask(data) |
| | | popOverVisible.value.approve = false |
| | | message.success('审批通过成功') |
| | | } else { |
| | | // 审批不通过数据 |
| | | const data = { |
| | | id: runningTask.value.id, |
| | | reason: rejectReasonForm.reason, |
| | | } |
| | | await TaskApi.rejectTask(data) |
| | | popOverVisible.value.reject = false |
| | | message.success('审批不通过成功') |
| | | } |
| | | // 重置表单 |
| | | formRef.resetFields() |
| | | // 加载最新数据 |
| | | reload() |
| | | } finally { |
| | | formLoading.value = false |
| | | } |
| | | } |
| | | |
| | | /** 处理抄送 */ |
| | | const handleCopy = async () => { |
| | | formLoading.value = true |
| | | try { |
| | | // 1. 校验表单 |
| | | if (!copyFormRef.value) return |
| | | await copyFormRef.value.validate() |
| | | // 2. 提交抄送 |
| | | const data = { |
| | | id: runningTask.value.id, |
| | | reason: copyForm.copyReason, |
| | | copyUserIds:copyForm.copyUserIds |
| | | } |
| | | await TaskApi.copyTask(data) |
| | | copyFormRef.value.resetFields() |
| | | popOverVisible.value.copy = false |
| | | message.success('操作成功') |
| | | } finally { |
| | | formLoading.value = false |
| | | } |
| | | } |
| | | |
| | | /** 处理转交 */ |
| | | const handleTransfer = async () => { |
| | | formLoading.value = true |
| | | try { |
| | | // 1.1 校验表单 |
| | | if (!transferFormRef.value) return |
| | | await transferFormRef.value.validate() |
| | | // 1.2 提交转交 |
| | | const data = { |
| | | id: runningTask.value.id, |
| | | reason: transferForm.reason, |
| | | assigneeUserId: transferForm.assigneeUserId |
| | | } |
| | | await TaskApi.transferTask(data) |
| | | transferFormRef.value.resetFields() |
| | | popOverVisible.value.transfer = false |
| | | message.success('操作成功') |
| | | // 2. 加载最新数据 |
| | | reload() |
| | | } finally { |
| | | formLoading.value = false |
| | | } |
| | | } |
| | | |
| | | /** 处理委派 */ |
| | | const handleDelegate = async () => { |
| | | formLoading.value = true |
| | | try { |
| | | |
| | | // 1.1 校验表单 |
| | | if (!delegateFormRef.value) return |
| | | await delegateFormRef.value.validate() |
| | | // 1.2 处理委派 |
| | | const data = { |
| | | id: runningTask.value.id, |
| | | reason: delegateForm.reason, |
| | | delegateUserId: delegateForm.delegateUserId |
| | | } |
| | | |
| | | await TaskApi.delegateTask(data) |
| | | popOverVisible.value.delegate = false |
| | | delegateFormRef.value.resetFields() |
| | | message.success('操作成功') |
| | | // 2. 加载最新数据 |
| | | reload() |
| | | } finally { |
| | | formLoading.value = false |
| | | } |
| | | } |
| | | |
| | | /** 处理加签 */ |
| | | const handlerAddSign = async (type: string) => { |
| | | formLoading.value = true |
| | | try { |
| | | // 1.1 校验表单 |
| | | if (!addSignFormRef.value) return |
| | | await addSignFormRef.value.validate() |
| | | // 1.2 提交加签 |
| | | const data = { |
| | | id: runningTask.value.id, |
| | | type, |
| | | reason: addSignForm.reason, |
| | | userIds: addSignForm.addSignUserIds |
| | | } |
| | | await TaskApi.signCreateTask(data) |
| | | message.success('操作成功') |
| | | addSignFormRef.value.resetFields() |
| | | popOverVisible.value.addSign = false |
| | | // 2 加载最新数据 |
| | | reload() |
| | | } finally { |
| | | formLoading.value = false |
| | | } |
| | | } |
| | | |
| | | /** 处理退回 */ |
| | | const handleReturn = async () => { |
| | | formLoading.value = true |
| | | try { |
| | | // 1.1 校验表单 |
| | | if (!returnFormRef.value) return |
| | | await returnFormRef.value.validate() |
| | | // 1.2 提交退回 |
| | | const data = { |
| | | id: runningTask.value.id, |
| | | reason: returnForm.returnReason, |
| | | targetTaskDefinitionKey: returnForm.targetTaskDefinitionKey |
| | | } |
| | | |
| | | await TaskApi.returnTask(data) |
| | | popOverVisible.value.return = false |
| | | returnFormRef.value.resetFields() |
| | | message.success('操作成功') |
| | | // 2 重新加载数据 |
| | | reload() |
| | | } finally { |
| | | formLoading.value = false |
| | | } |
| | | } |
| | | |
| | | /** 处理取消 */ |
| | | const handleCancel = async () => { |
| | | formLoading.value = true |
| | | try { |
| | | // 1.1 校验表单 |
| | | if (!cancelFormRef.value) return |
| | | await cancelFormRef.value.validate() |
| | | // 1.2 提交取消 |
| | | await ProcessInstanceApi.cancelProcessInstanceByStartUser( |
| | | props.processInstance.id, |
| | | cancelForm.cancelReason |
| | | ) |
| | | popOverVisible.value.return = false |
| | | message.success('操作成功') |
| | | cancelFormRef.value.resetFields() |
| | | // 2 重新加载数据 |
| | | reload() |
| | | } finally { |
| | | formLoading.value = false |
| | | } |
| | | } |
| | | |
| | | /** 处理再次提交 */ |
| | | const handleReCreate = async () => { |
| | | // 跳转发起流程界面 |
| | | await router.push({ |
| | | name: 'BpmProcessInstanceCreate', |
| | | query: { processInstanceId: props.processInstance?.id } |
| | | }) |
| | | } |
| | | |
| | | /** 获取减签人员标签 */ |
| | | const getDeleteSignUserLabel = (task: any): string => { |
| | | const deptName = task?.assigneeUser?.deptName || task?.ownerUser?.deptName |
| | | const nickname = task?.assigneeUser?.nickname || task?.ownerUser?.nickname |
| | | return `${nickname} ( 所属部门:${deptName} )` |
| | | } |
| | | /** 处理减签 */ |
| | | const handlerDeleteSign = async () => { |
| | | formLoading.value = true |
| | | try { |
| | | // 1.1 校验表单 |
| | | if (!deleteSignFormRef.value) return |
| | | await deleteSignFormRef.value.validate() |
| | | // 1.2 提交减签 |
| | | const data = { |
| | | id: deleteSignForm.deleteSignTaskId, |
| | | reason: deleteSignForm.reason |
| | | } |
| | | await TaskApi.signDeleteTask(data) |
| | | message.success('减签成功') |
| | | deleteSignFormRef.value.resetFields() |
| | | popOverVisible.value.deleteSign = false |
| | | // 2 加载最新数据 |
| | | reload() |
| | | } finally { |
| | | formLoading.value = false |
| | | } |
| | | } |
| | | /** 重新加载数据 */ |
| | | const reload = () => { |
| | | emit('success') |
| | | } |
| | | |
| | | /** 任务是否为处理中状态 */ |
| | | const isHandleTaskStatus = () => { |
| | | let canHandle = false |
| | | if (TaskApi.TaskStatusEnum.RUNNING === runningTask.value?.status) { |
| | | canHandle = true |
| | | } |
| | | return canHandle |
| | | } |
| | | |
| | | /** 流程状态是否为结束状态 */ |
| | | const isEndProcessStatus = (status: number) => { |
| | | let isEndStatus = false |
| | | if ( |
| | | BpmProcessInstanceStatus.APPROVE === status || |
| | | BpmProcessInstanceStatus.REJECT === status || |
| | | BpmProcessInstanceStatus.CANCEL === status |
| | | ) { |
| | | isEndStatus = true |
| | | } |
| | | return isEndStatus |
| | | } |
| | | |
| | | /** 是否显示按钮 */ |
| | | const isShowButton = (btnType: OperationButtonType): boolean => { |
| | | let isShow = true |
| | | if (runningTask.value?.buttonsSetting && runningTask.value?.buttonsSetting[btnType]) { |
| | | isShow = runningTask.value.buttonsSetting[btnType].enable |
| | | } |
| | | return isShow |
| | | } |
| | | |
| | | /** 获取按钮的显示名称 */ |
| | | const getButtonDisplayName = (btnType: OperationButtonType) => { |
| | | let displayName = OPERATION_BUTTON_NAME.get(btnType) |
| | | if (runningTask.value?.buttonsSetting && runningTask.value?.buttonsSetting[btnType]) { |
| | | displayName = runningTask.value.buttonsSetting[btnType].displayName |
| | | } |
| | | return displayName |
| | | } |
| | | |
| | | const loadTodoTask = (task: any) => { |
| | | approveForm.value = {} |
| | | approveFormFApi.value = {} |
| | | runningTask.value = task |
| | | // 处理 approve 表单. |
| | | if (task && task.formId && task.formConf) { |
| | | const tempApproveForm = {} |
| | | setConfAndFields2(tempApproveForm, task.formConf, task.formFields, task.formVariables) |
| | | approveForm.value = tempApproveForm |
| | | } else { |
| | | approveForm.value = {} // 占位,避免为空 |
| | | } |
| | | } |
| | | |
| | | /** 校验流程表单 */ |
| | | const validateNormalForm = async () => { |
| | | if (props.processDefinition?.formType === BpmModelFormType.NORMAL) { |
| | | let valid = true |
| | | try { |
| | | await props.normalFormApi?.validate() |
| | | } catch { |
| | | valid = false; |
| | | } |
| | | return valid; |
| | | } else { |
| | | return true; |
| | | } |
| | | } |
| | | /** 从可以编辑的流程表单字段,获取需要修改的流程实例的变量 */ |
| | | const getUpdatedProcessInstanceVaiables = ()=> { |
| | | const variables = {} |
| | | props.writableFields.forEach( (field) => { |
| | | const fieldValue = props.normalFormApi.getValue(field) |
| | | variables[field] = fieldValue; |
| | | }) |
| | | return variables |
| | | } |
| | | |
| | | defineExpose({ loadTodoTask }) |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | :deep(.el-affix--fixed) { |
| | | background-color: var(--el-bg-color); |
| | | } |
| | | |
| | | .btn-container { |
| | | > div { |
| | | display: flex; |
| | | margin: 0 8px; |
| | | cursor: pointer; |
| | | align-items: center; |
| | | |
| | | &:hover { |
| | | color: #6db5ff; |
| | | } |
| | | } |
| | | } |
| | | </style> |
src/views/bpm/processInstance/detail/ProcessInstanceSimpleViewer.vue
src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue
src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue
src/views/bpm/processInstance/detail/dialog/TaskDelegateForm.vue (已删除)
src/views/bpm/processInstance/detail/dialog/TaskReturnForm.vue (已删除)
src/views/bpm/processInstance/detail/dialog/TaskSignCreateForm.vue (已删除)
src/views/bpm/processInstance/detail/dialog/TaskSignDeleteForm.vue (已删除)
src/views/bpm/processInstance/detail/dialog/TaskSignList.vue (已删除)
src/views/bpm/processInstance/detail/dialog/TaskTransferForm.vue (已删除)
src/views/bpm/processInstance/detail/index.vue
src/views/bpm/processInstance/index.vue
src/views/bpm/processInstance/manager/index.vue
src/views/bpm/processListener/ProcessListenerForm.vue
src/views/bpm/simple/SimpleModelDesign.vue
src/views/bpm/simpleWorkflow/index.vue
src/views/bpm/task/copy/index.vue
src/views/bpm/task/done/index.vue
src/views/bpm/task/manager/index.vue
src/views/bpm/task/todo/index.vue
src/views/data/channel/http/api/tag/index.vue
src/views/data/ind/category/CategoryForm.vue
src/views/data/ind/data/DataSetForm.vue
src/views/data/ind/item/AtomIndDefineForm.vue
src/views/data/ind/item/CalIndDefineForm.vue
src/views/data/ind/item/DerIndDefineForm.vue
src/views/data/plan/category/CategoryForm.vue
src/views/data/point/DaPointChart.vue
src/views/data/point/DaPointForm.vue
src/views/data/point/index.vue
src/views/infra/apiAccessLog/index.vue
src/views/infra/apiErrorLog/index.vue
src/views/infra/config/index.vue
src/views/infra/dataSourceConfig/index.vue
src/views/infra/file/index.vue
src/views/micro/index.vue
src/views/model/mpk/file/MpkForm.vue
src/views/model/mpk/file/MpkRun.vue
src/views/model/mpk/file/index.vue
src/views/model/mpk/icon/index.vue
src/views/model/mpk/menu/index.vue
src/views/model/mpk/pack/index.vue
src/views/model/mpk/project/ProjectForm.vue
src/views/model/mpk/project/ProjectPackage.vue
src/views/model/mpk/project/index.vue
src/views/model/pre/dm/index.vue
src/views/model/pre/item/MmPredictItemChart.vue
src/views/model/pre/item/MmPredictItemForm.vue
src/views/model/pre/item/index.vue
src/views/model/pre/type/index.vue
src/views/model/sche/model/ScheduleModelForm.vue
src/views/model/sche/model/index.vue
src/views/model/sche/scheme/ScheduleSchemeForm.vue
src/views/model/sche/scheme/index.vue
src/views/model/sche/scheme/record/index.vue
src/views/report/drag/index.vue
src/views/report/goview/index.vue
src/views/report/jmreport/index.vue
src/views/system/app/AppForm.vue
src/views/system/app/index.vue
src/views/system/appmenu/AppMenuForm.vue
src/views/system/appmenu/index.vue
src/views/system/area/index.vue
src/views/system/loginlog/index.vue
src/views/system/menu/MenuForm.vue
src/views/system/menu/index.vue
src/views/system/operatelog/index.vue
src/views/system/role/index.vue
src/views/system/tenant/index.vue
src/views/system/tenantPackage/TenantPackageForm.vue
src/views/system/tenantPackage/index.vue
stylelint.config.js
tsconfig.json
types/env.d.ts
types/router.d.ts
uno.config.ts
vite.config.ts
web-types.json |