From 9259c2235e31708f954a3578bde3c6a7ab9753e8 Mon Sep 17 00:00:00 2001 From: houzhongjian <houzhongyi@126.com> Date: 星期一, 30 十二月 2024 15:51:40 +0800 Subject: [PATCH] 1、工作流相关组件更新 2、偶尔出现退出登录时路由报错的bug导致无法回到登录页面 3、全局配置文件修改,移除VITE_UPLOAD_URL配置等 --- src/directives/permission/hasPermi.ts | 3 src/components/DiyEditor/components/mobile/Carousel/property.vue | 12 src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue | 5 .env.local | 6 src/components/SimpleProcessDesignerV2/src/nodes-config/ConditionNodeConfig.vue | 44 src/components/bpmnProcessDesigner/package/designer/plugins/translate/zh.js | 2 src/components/DiyEditor/components/mobile/MenuSwiper/property.vue | 14 src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue | 235 ++ src/store/modules/permission.ts | 1 src/components/DiyEditor/components/mobile/ProductList/property.vue | 6 src/components/DiyEditor/components/mobile/FloatingActionButton/index.vue | 2 src/components/DiyEditor/components/mobile/NavigationBar/components/CellProperty.vue | 6 src/components/DiyEditor/components/mobile/Carousel/config.ts | 4 src/components/bpmnProcessDesigner/package/penal/task/data.ts | 36 src/components/DiyEditor/components/mobile/SearchBar/property.vue | 8 src/views/bpm/model/CategoryDraggableModel.vue | 5 src/views/bpm/processInstance/index.vue | 77 src/components/DiyEditor/components/mobile/PromotionPoint/config.ts | 96 + src/components/DiyEditor/components/mobile/PromotionPoint/index.vue | 202 ++ src/utils/tree.ts | 3 uno.config.ts | 2 src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json | 157 + src/views/system/app/AppForm.vue | 34 src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue | 10 .env.dev | 12 src/components/DiyEditor/components/mobile/FloatingActionButton/property.vue | 4 src/components/DiyEditor/components/mobile/PromotionSeckill/index.vue | 244 +- src/components/SimpleProcessDesignerV2/src/node.ts | 8 src/components/DiyEditor/components/ComponentContainer.vue | 1 src/components/DiyEditor/components/mobile/PromotionSeckill/config.ts | 42 src/components/AppLinkInput/data.ts | 8 src/views/system/role/index.vue | 10 src/components/bpmnProcessDesigner/package/penal/custom-config/ElementCustomConfig.vue | 284 --- src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue | 27 src/views/bpm/form/index.vue | 1 src/components/DiyEditor/components/mobile/PromotionPoint/property.vue | 154 + src/components/DiyEditor/components/mobile/Popover/property.vue | 4 src/components/DiyEditor/components/mobile/PromotionCombination/config.ts | 40 vite.config.ts | 3 src/utils/formCreate.ts | 1 src/components/DictTag/src/DictTag.vue | 90 src/components/bpmnProcessDesigner/package/penal/custom-config/components/UserTaskCustomConfig.vue | 623 +++++++ src/components/DiyEditor/components/mobile/NavigationBar/property.vue | 12 src/components/bpmnProcessDesigner/package/penal/properties/ElementProperties.vue | 3 src/styles/index.scss | 7 src/components/Crontab/src/Crontab.vue | 66 src/components/DiyEditor/components/mobile/ProductCard/property.vue | 10 src/views/Home/Index.vue | 3 src/main.ts | 4 src/components/DiyEditor/components/mobile/PromotionCombination/property.vue | 86 src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue | 24 src/components/DiyEditor/components/mobile/MenuGrid/property.vue | 4 src/views/bpm/model/editor/index.vue | 14 src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue | 422 +++- src/views/bpm/processInstance/create/ProcessDefinitionDetail.vue | 12 src/components/bpmnProcessDesigner/package/penal/custom-config/components/BoundaryEventTimer.vue | 252 +++ src/styles/var.css | 8 src/components/DiyEditor/components/mobile/CouponCard/property.vue | 6 src/components/DiyEditor/components/mobile/PromotionSeckill/property.vue | 86 src/views/bpm/task/done/index.vue | 114 + src/components/ShortcutDateRangePicker/index.vue | 6 src/components/UploadFile/src/useUpload.ts | 2 src/store/modules/app.ts | 10 src/components/Icon/src/IconSelect.vue | 18 src/components/Icon/src/Icon.vue | 3 src/components/DiyEditor/components/mobile/ProductList/index.vue | 3 src/components/DiyEditor/components/mobile/NoticeBar/config.ts | 2 src/components/bpmnProcessDesigner/package/penal/task/task-components/CallActivity.vue | 280 +++ src/components/RouterSearch/index.vue | 1 src/components/DiyEditor/components/mobile/TabBar/property.vue | 9 src/components/Draggable/index.vue | 4 src/utils/permission.ts | 6 src/components/DiyEditor/components/mobile/ProductCard/index.vue | 7 src/directives/index.ts | 11 src/hooks/web/useMessage.ts | 24 src/components/UploadFile/src/UploadImgs.vue | 23 src/components/IFrame/src/IFrame.vue | 33 src/components/DiyEditor/components/mobile/TitleBar/property.vue | 10 src/layout/components/UserInfo/src/UserInfo.vue | 4 src/components/bpmnProcessDesigner/package/penal/listeners/utilSelf.ts | 2 src/styles/global.module.scss | 2 src/views/infra/config/index.vue | 14 src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue | 2 tsconfig.json | 10 stylelint.config.js | 10 web-types.json | 19 src/components/UploadFile/src/UploadFile.vue | 24 src/components/bpmnProcessDesigner/package/designer/plugins/palette/CustomPalette.js | 12 src/components/DiyEditor/components/mobile/UserCard/index.vue | 2 src/components/DiyEditor/components/ComponentContainerProperty.vue | 4 src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue | 139 + src/views/bpm/processInstance/detail/index.vue | 22 src/components/bpmnProcessDesigner/package/penal/task/task-components/ServiceTask.vue | 91 + types/router.d.ts | 3 .env.prod | 6 src/components/ContentWrap/src/ContentWrap.vue | 2 src/components/DiyEditor/components/mobile/PromotionCombination/index.vue | 244 +- src/views/bpm/task/todo/index.vue | 95 src/components/DiyEditor/components/mobile/Divider/property.vue | 6 src/views/infra/dataSourceConfig/index.vue | 8 src/views/system/app/index.vue | 4 /dev/null | 89 - src/components/DiyEditor/components/mobile/TabBar/config.ts | 16 src/directives/permission/hasRole.ts | 5 package.json | 12 src/components/bpmnProcessDesigner/package/designer/plugins/palette/paletteProvider.js | 6 src/components/bpmnProcessDesigner/package/penal/custom-config/data.ts | 13 107 files changed, 3,800 insertions(+), 1,177 deletions(-) diff --git a/.env.dev b/.env.dev index 82ec13b..89a15bd 100644 --- a/.env.dev +++ b/.env.dev @@ -1,15 +1,13 @@ # 开发环境:本地只启动前端项目,依赖开发环境(后端、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 @@ -24,13 +22,13 @@ 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://' @@ -39,4 +37,4 @@ 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' diff --git a/.env.local b/.env.local index b28e8e2..9a61d8d 100644 --- a/.env.local +++ b/.env.local @@ -8,8 +8,6 @@ # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务 VITE_UPLOAD_TYPE=server -# 上传路径 -VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload' # 接口地址 VITE_API_URL=/admin-api @@ -24,10 +22,10 @@ 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' diff --git a/.env.prod b/.env.prod index 567c520..a602754 100644 --- a/.env.prod +++ b/.env.prod @@ -4,12 +4,10 @@ VITE_DEV=false # 请求路径 -VITE_BASE_URL='http://10.88.4.131' +VITE_BASE_URL='http://10.50.37.62' # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务 VITE_UPLOAD_TYPE=server -# 上传路径 -VITE_UPLOAD_URL='http://10.88.4.131/admin-api/infra/file/upload' # 接口地址 VITE_API_URL=/admin-api @@ -27,7 +25,7 @@ VITE_BASE_PATH=/plat # 数据采集服务所在服务器,映射截图图片用 -VITE_VIDEO_CAMERA_DOMAIN='10.88.4.131' +VITE_VIDEO_CAMERA_DOMAIN='10.50.37.62' # 输出路径 VITE_OUT_DIR=dist diff --git a/package.json b/package.json index f118cca..a7c562a 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,6 @@ "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", @@ -76,8 +75,8 @@ "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", @@ -98,8 +97,8 @@ "@vitejs/plugin-vue": "^5.0.4", "@vitejs/plugin-vue-jsx": "^3.1.0", "autoprefixer": "^10.4.17", - "bpmn-js": "8.10.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", @@ -122,7 +121,7 @@ "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", @@ -146,6 +145,7 @@ "url": "https://xxxx" }, "homepage": "https://xxxx", + "web-types": "./web-types.json", "engines": { "node": ">= 16.0.0", "pnpm": ">=8.6.0" diff --git a/src/components/AppLinkInput/data.ts b/src/components/AppLinkInput/data.ts index 1916e08..c9e3678 100644 --- a/src/components/AppLinkInput/data.ts +++ b/src/components/AppLinkInput/data.ts @@ -5,6 +5,7 @@ // 链接列表 links: AppLink[] } + // APP 链接 export interface AppLink { // 链接名称 @@ -21,6 +22,8 @@ ACTIVITY_COMBINATION, // 秒杀活动 ACTIVITY_SECKILL, + // 积分商城活动 + ACTIVITY_POINT, // 文章详情 ARTICLE_DETAIL, // 优惠券详情 @@ -131,6 +134,11 @@ 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' }, diff --git a/src/components/ContentWrap/src/ContentWrap.vue b/src/components/ContentWrap/src/ContentWrap.vue index c75e4b7..e603596 100644 --- a/src/components/ContentWrap/src/ContentWrap.vue +++ b/src/components/ContentWrap/src/ContentWrap.vue @@ -11,7 +11,7 @@ defineProps({ title: propTypes.string.def(''), message: propTypes.string.def(''), - bodyStyle: propTypes.object.def({ padding: '20px' }) + bodyStyle: propTypes.object.def({ padding: '10px' }) }) </script> diff --git a/src/components/Crontab/src/Crontab.vue b/src/components/Crontab/src/Crontab.vue index e61fef8..0914bb7 100644 --- a/src/components/Crontab/src/Crontab.vue +++ b/src/components/Crontab/src/Crontab.vue @@ -548,10 +548,10 @@ <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="范围"> @@ -607,10 +607,10 @@ <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="范围"> @@ -666,10 +666,10 @@ <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="范围"> @@ -725,12 +725,12 @@ <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="范围"> @@ -786,10 +786,10 @@ <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="范围"> @@ -846,12 +846,12 @@ <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="范围"> @@ -925,11 +925,11 @@ <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="范围"> diff --git a/src/components/DictTag/src/DictTag.vue b/src/components/DictTag/src/DictTag.vue index 1d075a1..6414eaa 100644 --- a/src/components/DictTag/src/DictTag.vue +++ b/src/components/DictTag/src/DictTag.vue @@ -1,8 +1,9 @@ <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', @@ -12,49 +13,78 @@ 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> diff --git a/src/components/DiyEditor/components/ComponentContainer.vue b/src/components/DiyEditor/components/ComponentContainer.vue index 0137278..4856722 100644 --- a/src/components/DiyEditor/components/ComponentContainer.vue +++ b/src/components/DiyEditor/components/ComponentContainer.vue @@ -165,6 +165,7 @@ width: 80px; height: 25px; font-size: 12px; + color: #6a6a6a; line-height: 25px; text-align: center; background: #fff; diff --git a/src/components/DiyEditor/components/ComponentContainerProperty.vue b/src/components/DiyEditor/components/ComponentContainerProperty.vue index 9d0750d..25119a5 100644 --- a/src/components/DiyEditor/components/ComponentContainerProperty.vue +++ b/src/components/DiyEditor/components/ComponentContainerProperty.vue @@ -11,8 +11,8 @@ <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'"> diff --git a/src/components/DiyEditor/components/mobile/Carousel/config.ts b/src/components/DiyEditor/components/mobile/Carousel/config.ts index 9cee5c2..3e74a51 100644 --- a/src/components/DiyEditor/components/mobile/Carousel/config.ts +++ b/src/components/DiyEditor/components/mobile/Carousel/config.ts @@ -38,8 +38,8 @@ 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', diff --git a/src/components/DiyEditor/components/mobile/Carousel/property.vue b/src/components/DiyEditor/components/mobile/Carousel/property.vue index c3a5154..e11b032 100644 --- a/src/components/DiyEditor/components/mobile/Carousel/property.vue +++ b/src/components/DiyEditor/components/mobile/Carousel/property.vue @@ -5,12 +5,12 @@ <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> @@ -18,8 +18,8 @@ </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"> @@ -43,8 +43,8 @@ <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 diff --git a/src/components/DiyEditor/components/mobile/CouponCard/property.vue b/src/components/DiyEditor/components/mobile/CouponCard/property.vue index 4f32c21..4f69000 100644 --- a/src/components/DiyEditor/components/mobile/CouponCard/property.vue +++ b/src/components/DiyEditor/components/mobile/CouponCard/property.vue @@ -26,17 +26,17 @@ <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> diff --git a/src/components/DiyEditor/components/mobile/Divider/property.vue b/src/components/DiyEditor/components/mobile/Divider/property.vue index 3d7be26..0c3cb0e 100644 --- a/src/components/DiyEditor/components/mobile/Divider/property.vue +++ b/src/components/DiyEditor/components/mobile/Divider/property.vue @@ -11,7 +11,7 @@ :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> @@ -24,12 +24,12 @@ <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> diff --git a/src/components/DiyEditor/components/mobile/FloatingActionButton/index.vue b/src/components/DiyEditor/components/mobile/FloatingActionButton/index.vue index 19e42cb..c2b9926 100644 --- a/src/components/DiyEditor/components/mobile/FloatingActionButton/index.vue +++ b/src/components/DiyEditor/components/mobile/FloatingActionButton/index.vue @@ -44,7 +44,7 @@ defineProps<{ property: FloatingActionButtonProperty }>() // 是否展开 -const expanded = ref(true) +const expanded = ref(false) // 处理展开/折叠 const handleToggleFab = () => { expanded.value = !expanded.value diff --git a/src/components/DiyEditor/components/mobile/FloatingActionButton/property.vue b/src/components/DiyEditor/components/mobile/FloatingActionButton/property.vue index 5db08d0..df459ff 100644 --- a/src/components/DiyEditor/components/mobile/FloatingActionButton/property.vue +++ b/src/components/DiyEditor/components/mobile/FloatingActionButton/property.vue @@ -3,8 +3,8 @@ <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"> diff --git a/src/components/DiyEditor/components/mobile/MenuGrid/property.vue b/src/components/DiyEditor/components/mobile/MenuGrid/property.vue index 7940fd0..bb944c9 100644 --- a/src/components/DiyEditor/components/mobile/MenuGrid/property.vue +++ b/src/components/DiyEditor/components/mobile/MenuGrid/property.vue @@ -4,8 +4,8 @@ <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> diff --git a/src/components/DiyEditor/components/mobile/MenuSwiper/property.vue b/src/components/DiyEditor/components/mobile/MenuSwiper/property.vue index 81266bc..fbae83c 100644 --- a/src/components/DiyEditor/components/mobile/MenuSwiper/property.vue +++ b/src/components/DiyEditor/components/mobile/MenuSwiper/property.vue @@ -4,21 +4,21 @@ <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> diff --git a/src/components/DiyEditor/components/mobile/NavigationBar/components/CellProperty.vue b/src/components/DiyEditor/components/mobile/NavigationBar/components/CellProperty.vue index edc85f1..2c3bd54 100644 --- a/src/components/DiyEditor/components/mobile/NavigationBar/components/CellProperty.vue +++ b/src/components/DiyEditor/components/mobile/NavigationBar/components/CellProperty.vue @@ -14,9 +14,9 @@ <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. 文字 --> diff --git a/src/components/DiyEditor/components/mobile/NavigationBar/property.vue b/src/components/DiyEditor/components/mobile/NavigationBar/property.vue index b2bc8c1..5b06772 100644 --- a/src/components/DiyEditor/components/mobile/NavigationBar/property.vue +++ b/src/components/DiyEditor/components/mobile/NavigationBar/property.vue @@ -2,27 +2,27 @@ <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'"> diff --git a/src/components/DiyEditor/components/mobile/NoticeBar/config.ts b/src/components/DiyEditor/components/mobile/NoticeBar/config.ts index 3a2ef53..b6b0860 100644 --- a/src/components/DiyEditor/components/mobile/NoticeBar/config.ts +++ b/src/components/DiyEditor/components/mobile/NoticeBar/config.ts @@ -28,7 +28,7 @@ name: '公告栏', icon: 'ep:bell', property: { - iconUrl: 'http://xxxx/static/images/xinjian.png', + iconUrl: 'http://mall.yudao.iocoder.cn/static/images/xinjian.png', contents: [ { text: '', diff --git a/src/components/DiyEditor/components/mobile/Popover/property.vue b/src/components/DiyEditor/components/mobile/Popover/property.vue index 6535e3b..2dd4351 100644 --- a/src/components/DiyEditor/components/mobile/Popover/property.vue +++ b/src/components/DiyEditor/components/mobile/Popover/property.vue @@ -11,10 +11,10 @@ <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> diff --git a/src/components/DiyEditor/components/mobile/ProductCard/index.vue b/src/components/DiyEditor/components/mobile/ProductCard/index.vue index f05d4fa..25f8cb8 100644 --- a/src/components/DiyEditor/components/mobile/ProductCard/index.vue +++ b/src/components/DiyEditor/components/mobile/ProductCard/index.vue @@ -67,15 +67,15 @@ 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"> <!-- 销量 --> @@ -117,6 +117,7 @@ <script setup lang="ts"> import { ProductCardProperty } from './config' import * as ProductSpuApi from '@/api/mall/product/spu' +import { fenToYuan } from '../../../../../utils' /** 商品卡片 */ defineOptions({ name: 'ProductCard' }) diff --git a/src/components/DiyEditor/components/mobile/ProductCard/property.vue b/src/components/DiyEditor/components/mobile/ProductCard/property.vue index cfa5008..110c8be 100644 --- a/src/components/DiyEditor/components/mobile/ProductCard/property.vue +++ b/src/components/DiyEditor/components/mobile/ProductCard/property.vue @@ -8,17 +8,17 @@ <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> @@ -74,8 +74,8 @@ <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'"> diff --git a/src/components/DiyEditor/components/mobile/ProductList/index.vue b/src/components/DiyEditor/components/mobile/ProductList/index.vue index 3ba6367..a51fc07 100644 --- a/src/components/DiyEditor/components/mobile/ProductList/index.vue +++ b/src/components/DiyEditor/components/mobile/ProductList/index.vue @@ -54,7 +54,7 @@ class="text-12px" :style="{ color: property.fields.price.color }" > - ¥{{ spu.price }} + ¥{{ fenToYuan(spu.price) }} </span> </div> </div> @@ -65,6 +65,7 @@ <script setup lang="ts"> import { ProductListProperty } from './config' import * as ProductSpuApi from '@/api/mall/product/spu' +import { fenToYuan } from '@/utils' /** 商品栏 */ defineOptions({ name: 'ProductList' }) diff --git a/src/components/DiyEditor/components/mobile/ProductList/property.vue b/src/components/DiyEditor/components/mobile/ProductList/property.vue index e9cf7c0..894687c 100644 --- a/src/components/DiyEditor/components/mobile/ProductList/property.vue +++ b/src/components/DiyEditor/components/mobile/ProductList/property.vue @@ -8,17 +8,17 @@ <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> diff --git a/src/components/DiyEditor/components/mobile/PromotionCombination/config.ts b/src/components/DiyEditor/components/mobile/PromotionCombination/config.ts index 0c7e9ff..f4fdf6e 100644 --- a/src/components/DiyEditor/components/mobile/PromotionCombination/config.ts +++ b/src/components/DiyEditor/components/mobile/PromotionCombination/config.ts @@ -3,19 +3,40 @@ /** 拼团属性 */ 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 } // 上圆角 @@ -25,7 +46,7 @@ // 间距 space: number // 拼团活动编号 - activityId: number + activityIds: number[] // 组件样式 style: ComponentStyle } @@ -44,12 +65,23 @@ 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, diff --git a/src/components/DiyEditor/components/mobile/PromotionCombination/index.vue b/src/components/DiyEditor/components/mobile/PromotionCombination/index.vue index fe6f3a8..d41bf1c 100644 --- a/src/components/DiyEditor/components/mobile/PromotionCombination/index.vue +++ b/src/components/DiyEditor/components/mobile/PromotionCombination/index.vue @@ -1,125 +1,201 @@ <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> diff --git a/src/components/DiyEditor/components/mobile/PromotionCombination/property.vue b/src/components/DiyEditor/components/mobile/PromotionCombination/property.vue index ec09dc4..ea901a0 100644 --- a/src/components/DiyEditor/components/mobile/PromotionCombination/property.vue +++ b/src/components/DiyEditor/components/mobile/PromotionCombination/property.vue @@ -2,30 +2,31 @@ <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"> @@ -34,10 +35,34 @@ <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> @@ -47,9 +72,35 @@ </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"> @@ -92,6 +143,7 @@ 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' }) @@ -100,7 +152,7 @@ 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 diff --git a/src/components/DiyEditor/components/mobile/PromotionPoint/config.ts b/src/components/DiyEditor/components/mobile/PromotionPoint/config.ts new file mode 100644 index 0000000..75aa0ff --- /dev/null +++ b/src/components/DiyEditor/components/mobile/PromotionPoint/config.ts @@ -0,0 +1,96 @@ +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> diff --git a/src/components/DiyEditor/components/mobile/PromotionPoint/index.vue b/src/components/DiyEditor/components/mobile/PromotionPoint/index.vue new file mode 100644 index 0000000..4acd93f --- /dev/null +++ b/src/components/DiyEditor/components/mobile/PromotionPoint/index.vue @@ -0,0 +1,202 @@ +<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> diff --git a/src/components/DiyEditor/components/mobile/PromotionPoint/property.vue b/src/components/DiyEditor/components/mobile/PromotionPoint/property.vue new file mode 100644 index 0000000..84a429b --- /dev/null +++ b/src/components/DiyEditor/components/mobile/PromotionPoint/property.vue @@ -0,0 +1,154 @@ +<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> diff --git a/src/components/DiyEditor/components/mobile/PromotionSeckill/config.ts b/src/components/DiyEditor/components/mobile/PromotionSeckill/config.ts index 800398b..022be92 100644 --- a/src/components/DiyEditor/components/mobile/PromotionSeckill/config.ts +++ b/src/components/DiyEditor/components/mobile/PromotionSeckill/config.ts @@ -3,19 +3,40 @@ /** 秒杀属性 */ 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 } // 上圆角 @@ -25,10 +46,11 @@ // 间距 space: number // 秒杀活动编号 - activityId: number + activityIds: number[] // 组件样式 style: ComponentStyle } + // 商品字段 export interface PromotionSeckillFieldProperty { // 是否显示 @@ -43,13 +65,23 @@ 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, diff --git a/src/components/DiyEditor/components/mobile/PromotionSeckill/index.vue b/src/components/DiyEditor/components/mobile/PromotionSeckill/index.vue index 1b4113b..3d34a3d 100644 --- a/src/components/DiyEditor/components/mobile/PromotionSeckill/index.vue +++ b/src/components/DiyEditor/components/mobile/PromotionSeckill/index.vue @@ -1,125 +1,201 @@ <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> diff --git a/src/components/DiyEditor/components/mobile/PromotionSeckill/property.vue b/src/components/DiyEditor/components/mobile/PromotionSeckill/property.vue index 8753782..6128759 100644 --- a/src/components/DiyEditor/components/mobile/PromotionSeckill/property.vue +++ b/src/components/DiyEditor/components/mobile/PromotionSeckill/property.vue @@ -2,30 +2,31 @@ <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"> @@ -34,10 +35,34 @@ <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> @@ -47,9 +72,35 @@ </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"> @@ -92,6 +143,7 @@ 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' }) @@ -100,7 +152,7 @@ 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 diff --git a/src/components/DiyEditor/components/mobile/SearchBar/property.vue b/src/components/DiyEditor/components/mobile/SearchBar/property.vue index 9002702..71f9493 100644 --- a/src/components/DiyEditor/components/mobile/SearchBar/property.vue +++ b/src/components/DiyEditor/components/mobile/SearchBar/property.vue @@ -13,12 +13,12 @@ <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> @@ -30,12 +30,12 @@ <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> diff --git a/src/components/DiyEditor/components/mobile/TabBar/config.ts b/src/components/DiyEditor/components/mobile/TabBar/config.ts index 7e52666..88d706f 100644 --- a/src/components/DiyEditor/components/mobile/TabBar/config.ts +++ b/src/components/DiyEditor/components/mobile/TabBar/config.ts @@ -53,26 +53,26 @@ { 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' } ] } diff --git a/src/components/DiyEditor/components/mobile/TabBar/property.vue b/src/components/DiyEditor/components/mobile/TabBar/property.vue index 6ace5af..d1da142 100644 --- a/src/components/DiyEditor/components/mobile/TabBar/property.vue +++ b/src/components/DiyEditor/components/mobile/TabBar/property.vue @@ -27,8 +27,8 @@ </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'"> @@ -79,7 +79,7 @@ </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' }) @@ -88,6 +88,9 @@ 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) diff --git a/src/components/DiyEditor/components/mobile/TitleBar/property.vue b/src/components/DiyEditor/components/mobile/TitleBar/property.vue index 4eb3259..44d6bb6 100644 --- a/src/components/DiyEditor/components/mobile/TitleBar/property.vue +++ b/src/components/DiyEditor/components/mobile/TitleBar/property.vue @@ -10,12 +10,12 @@ <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> @@ -88,9 +88,9 @@ <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'"> diff --git a/src/components/DiyEditor/components/mobile/UserCard/index.vue b/src/components/DiyEditor/components/mobile/UserCard/index.vue index 7005b97..14b447c 100644 --- a/src/components/DiyEditor/components/mobile/UserCard/index.vue +++ b/src/components/DiyEditor/components/mobile/UserCard/index.vue @@ -5,7 +5,7 @@ <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> diff --git a/src/components/Draggable/index.vue b/src/components/Draggable/index.vue index 2175946..3d7906b 100644 --- a/src/components/Draggable/index.vue +++ b/src/components/Draggable/index.vue @@ -13,9 +13,9 @@ 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 diff --git a/src/components/IFrame/src/IFrame.vue b/src/components/IFrame/src/IFrame.vue index 19de51a..64ffc0e 100644 --- a/src/components/IFrame/src/IFrame.vue +++ b/src/components/IFrame/src/IFrame.vue @@ -7,26 +7,41 @@ 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> diff --git a/src/components/Icon/src/Icon.vue b/src/components/Icon/src/Icon.vue index 4a59f68..a90bb37 100644 --- a/src/components/Icon/src/Icon.vue +++ b/src/components/Icon/src/Icon.vue @@ -1,7 +1,6 @@ <script lang="ts" setup> import { propTypes } from '@/utils/propTypes' -// import Iconify from '@purge-icons/generated' - +import Iconify from '@purge-icons/generated' import { useDesign } from '@/hooks/web/useDesign' defineOptions({ name: 'Icon' }) diff --git a/src/components/Icon/src/IconSelect.vue b/src/components/Icon/src/IconSelect.vue index d4a5b07..76cc6d5 100644 --- a/src/components/Icon/src/IconSelect.vue +++ b/src/components/Icon/src/IconSelect.vue @@ -11,6 +11,10 @@ modelValue: { require: false, type: String + }, + clearable: { + require: false, + type: Boolean } }) const emit = defineEmits<{ (e: 'update:modelValue', v: string) }>() @@ -92,6 +96,12 @@ currentPage.value = page } +function clearIcon() { + icon.value = '' + emit('update:modelValue', '') + visible.value = false +} + watch( () => { return props.modelValue @@ -115,14 +125,14 @@ <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" > @@ -147,7 +157,7 @@ > <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" @@ -171,7 +181,7 @@ background class="h-10 flex items-center justify-center" layout="prev, pager, next" - small + size="small" @current-change="onCurrentChange" /> </ElPopover> diff --git a/src/components/RouterSearch/index.vue b/src/components/RouterSearch/index.vue index c035242..3fa35f6 100644 --- a/src/components/RouterSearch/index.vue +++ b/src/components/RouterSearch/index.vue @@ -20,6 +20,7 @@ <div v-else class="custom-hover" @click.stop="showTopSearch = !showTopSearch"> <Icon icon="ep:search" /> <el-select + @click.stop filterable :reserve-keyword="false" remote diff --git a/src/components/ShortcutDateRangePicker/index.vue b/src/components/ShortcutDateRangePicker/index.vue index 117c079..78c5130 100644 --- a/src/components/ShortcutDateRangePicker/index.vue +++ b/src/components/ShortcutDateRangePicker/index.vue @@ -1,9 +1,9 @@ <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" diff --git a/src/components/SimpleProcessDesignerV2/src/node.ts b/src/components/SimpleProcessDesignerV2/src/node.ts index 0810c1f..4cbac6e 100644 --- a/src/components/SimpleProcessDesignerV2/src/node.ts +++ b/src/components/SimpleProcessDesignerV2/src/node.ts @@ -15,7 +15,6 @@ AssignStartUserHandlerType, AssignEmptyHandlerType, FieldPermissionType, - ProcessVariableEnum } from './consts' import { parseFormFields } from '@/components/FormCreate/src/utils/index' export function useWatchNode(props: { flowNode: SimpleFlowNode }): Ref<SimpleFlowNode> { @@ -37,13 +36,6 @@ parseFormFields(JSON.parse(fieldStr), result) }) } - // 固定添加发起人 ID 字段 - result.unshift({ - field: ProcessVariableEnum.START_USER_ID, - title: '发起人', - type: 'UserSelect', - required: true - }) return result } diff --git a/src/components/SimpleProcessDesignerV2/src/nodes-config/ConditionNodeConfig.vue b/src/components/SimpleProcessDesignerV2/src/nodes-config/ConditionNodeConfig.vue index ffbf187..49e5d9f 100644 --- a/src/components/SimpleProcessDesignerV2/src/nodes-config/ConditionNodeConfig.vue +++ b/src/components/SimpleProcessDesignerV2/src/nodes-config/ConditionNodeConfig.vue @@ -26,19 +26,13 @@ </div> </template> <div> - <div class="mb-3 font-size-16px" v-if="currentNode.defaultFlow">未满足其它条件时,将进入此分支(该分支不可编辑和删除)</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 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-group v-model="currentNode.conditionType" @change="changeConditionType"> <el-radio v-for="(dict, index) in conditionConfigTypes" :key="index" @@ -108,10 +102,11 @@ <div class="mr-2"> <el-select style="width: 160px" v-model="rule.leftSide"> <el-option - v-for="(item, index) in fieldsInfo" + v-for="(item, index) in fieldOptions" :key="index" :label="item.title" :value="item.field" + :disabled="!item.required" /> </el-select> </div> @@ -165,10 +160,12 @@ COMPARISON_OPERATORS, ConditionGroup, Condition, - ConditionRule + ConditionRule, + ProcessVariableEnum } from '../consts' import { getDefaultConditionNodeName } from '../utils' import { useFormFields } from '../node' +import { BpmModelFormType } from '@/utils/constants' const message = useMessage() // 消息弹窗 defineOptions({ name: 'ConditionNodeConfig' @@ -177,8 +174,8 @@ const conditionConfigTypes = computed(() => { return CONDITION_CONFIG_TYPES.filter((item) => { // 业务表单暂时去掉条件规则选项 - if (formType?.value !== 10) { - return item.value === ConditionType.RULE + if (formType?.value === BpmModelFormType.CUSTOM && item.value === ConditionType.RULE) { + return false } else { return true } @@ -368,16 +365,29 @@ 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 = fieldsInfo.find((item) => item.field === field) return item?.title } +/** 获取操作符名称 */ const getOpName = (opCode: string): string => { - const opName = COMPARISON_OPERATORS.find((item) => item.value === opCode) + const opName = COMPARISON_OPERATORS.find((item: any) => item.value === opCode) return opName?.label } </script> diff --git a/src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue b/src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue index a088b6d..fb5e780 100644 --- a/src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue +++ b/src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue @@ -469,7 +469,8 @@ TimeoutHandlerType, ASSIGN_EMPTY_HANDLER_TYPES, AssignEmptyHandlerType, - FieldPermissionType + FieldPermissionType, + ProcessVariableEnum } from '../consts' import { @@ -519,6 +520,13 @@ 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') }) // 表单内部门字段选项, 必须是必填和部门选择器 diff --git a/src/components/UploadFile/src/UploadFile.vue b/src/components/UploadFile/src/UploadFile.vue index 3beb377..9d0a904 100644 --- a/src/components/UploadFile/src/UploadFile.vue +++ b/src/components/UploadFile/src/UploadFile.vue @@ -1,5 +1,5 @@ <template> - <div class="upload-file"> + <div v-if="!disabled" class="upload-file"> <el-upload ref="uploadRef" v-model:file-list="fileList" @@ -20,11 +20,11 @@ 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> @@ -32,7 +32,6 @@ 格式为 <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> @@ -53,6 +52,18 @@ </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> @@ -211,4 +222,9 @@ :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> diff --git a/src/components/UploadFile/src/UploadImgs.vue b/src/components/UploadFile/src/UploadImgs.vue index 85da64c..59857a9 100644 --- a/src/components/UploadFile/src/UploadImgs.vue +++ b/src/components/UploadFile/src/UploadImgs.vue @@ -25,7 +25,7 @@ <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> @@ -39,16 +39,12 @@ <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' @@ -56,6 +52,13 @@ defineOptions({ name: 'UploadImgs' }) const message = useMessage() // 消息弹窗 +// 查看图片 +const imagePreview = (imgUrl: string) => { + createImageViewer({ + zIndex: 9999999, + urlList: [imgUrl] + }) +} type FileTypes = | 'image/apng' @@ -177,14 +180,6 @@ 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> diff --git a/src/components/UploadFile/src/useUpload.ts b/src/components/UploadFile/src/useUpload.ts index 2981e12..c846acb 100644 --- a/src/components/UploadFile/src/useUpload.ts +++ b/src/components/UploadFile/src/useUpload.ts @@ -101,6 +101,4 @@ enum UPLOAD_TYPE { // 客户端直接上传(只支持S3服务) CLIENT = 'client', - // 客户端发送到后端上传 - SERVER = 'server' } diff --git a/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json b/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json index d1ca4a4..7fe1fa7 100644 --- a/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json +++ b/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json @@ -406,6 +406,31 @@ "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" } ] }, @@ -1281,6 +1306,138 @@ "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": [] diff --git a/src/components/bpmnProcessDesigner/package/designer/plugins/palette/CustomPalette.js b/src/components/bpmnProcessDesigner/package/designer/plugins/palette/CustomPalette.js index 5e2803b..788e4d1 100644 --- a/src/components/bpmnProcessDesigner/package/designer/plugins/palette/CustomPalette.js +++ b/src/components/bpmnProcessDesigner/package/designer/plugins/palette/CustomPalette.js @@ -165,6 +165,18 @@ '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', diff --git a/src/components/bpmnProcessDesigner/package/designer/plugins/palette/paletteProvider.js b/src/components/bpmnProcessDesigner/package/designer/plugins/palette/paletteProvider.js index 7098981..304875c 100644 --- a/src/components/bpmnProcessDesigner/package/designer/plugins/palette/paletteProvider.js +++ b/src/components/bpmnProcessDesigner/package/designer/plugins/palette/paletteProvider.js @@ -171,6 +171,12 @@ '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', diff --git a/src/components/bpmnProcessDesigner/package/designer/plugins/translate/zh.js b/src/components/bpmnProcessDesigner/package/designer/plugins/translate/zh.js index 777db3e..cb92041 100644 --- a/src/components/bpmnProcessDesigner/package/designer/plugins/translate/zh.js +++ b/src/components/bpmnProcessDesigner/package/designer/plugins/translate/zh.js @@ -56,6 +56,8 @@ 'Create EndEvent': '创建结束事件', 'Create Task': '创建任务', 'Create User Task': '创建用户任务', + 'Create Call Activity': '创建调用活动', + 'Create Service Task': '创建服务任务', 'Create Gateway': '创建网关', 'Create DataObjectReference': '创建数据对象', 'Create DataStoreReference': '创建数据存储', diff --git a/src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue b/src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue index 96e8b08..d2409ee 100644 --- a/src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue +++ b/src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue @@ -1,5 +1,5 @@ <template> - <div class="process-panel__container" :style="{ width: `${width}px`,maxHeight: '700px' }"> + <div class="process-panel__container" :style="{ width: `${width}px`, maxHeight: '700px' }"> <el-collapse v-model="activeTab"> <el-collapse-item name="base"> <!-- class="panel-tab__title" --> @@ -26,8 +26,10 @@ <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 @@ -35,8 +37,12 @@ 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> @@ -54,9 +60,13 @@ <template #title><Icon icon="ep:promotion" />其他</template> <element-other-config :id="elementId" /> </el-collapse-item> - <el-collapse-item name="customConfig" v-if="elementType.indexOf('Task') !== -1" key="customConfig"> - <template #title><Icon icon="ep:circle-plus-filled" />自定义配置</template> - <element-custom-config :id="elementId" :type="elementType" /> + <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> @@ -72,6 +82,7 @@ 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' }) diff --git a/src/components/bpmnProcessDesigner/package/penal/custom-config/ElementCustomConfig.vue b/src/components/bpmnProcessDesigner/package/penal/custom-config/ElementCustomConfig.vue index e5497b0..f9cb9ac 100644 --- a/src/components/bpmnProcessDesigner/package/penal/custom-config/ElementCustomConfig.vue +++ b/src/components/bpmnProcessDesigner/package/penal/custom-config/ElementCustomConfig.vue @@ -1,283 +1,39 @@ -<!-- UserTask 自定义配置: - 1. 审批人与提交人为同一人时 - 2. 审批人拒绝时 - 3. 审批人为空时 ---> <template> <div class="panel-tab__content"> - <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> + <component :is="customConfigComponent" v-bind="$props" /> </div> </template> <script lang="ts" setup> -import { - ASSIGN_START_USER_HANDLER_TYPES, - RejectHandlerType, - REJECT_HANDLER_TYPES, - ASSIGN_EMPTY_HANDLER_TYPES, - AssignEmptyHandlerType -} from '@/components/SimpleProcessDesignerV2/src/consts' -import * as UserApi from '@/api/system/user' +import { CustomConfigMap } from './data' defineOptions({ name: 'ElementCustomConfig' }) + const props = defineProps({ id: String, - type: String + type: String, + businessObject: { + type: Object, + default: () => {} + } }) -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 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: [] }) - - // 审批人与提交人为同一人时 - 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 - }) - - // 保留剩余扩展元素,便于后面更新该元素对应属性 - 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` - ) ?? [] - - // 更新元素扩展属性,避免后续报错 - 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 - ] - }) - bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { - extensionElements: extensions - }) -} +const customConfigComponent = ref<any>(null) watch( - () => props.id, - (val) => { - val && - val.length && - nextTick(() => { - resetCustomConfigList() - }) + () => 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 } ) - -function findAllPredecessorsExcludingStart(elementId, modeler) { - const elementRegistry = modeler.get('elementRegistry') - const allConnections = elementRegistry.filter((element) => element.type === 'bpmn:SequenceFlow') - const predecessors = new Set() // 使用 Set 来避免重复节点 - - // 检查是否是开始事件节点 - function isStartEvent(element) { - return element.type === 'bpmn:StartEvent' - } - - function findPredecessorsRecursively(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) // 返回前置节点数组 -} - -const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表 -onMounted(async () => { - // 获得用户列表 - userOptions.value = await UserApi.getSimpleUserList() -}) </script> + +<style lang="scss" scoped></style> diff --git a/src/components/bpmnProcessDesigner/package/penal/custom-config/components/BoundaryEventTimer.vue b/src/components/bpmnProcessDesigner/package/penal/custom-config/components/BoundaryEventTimer.vue new file mode 100644 index 0000000..ca46b27 --- /dev/null +++ b/src/components/bpmnProcessDesigner/package/penal/custom-config/components/BoundaryEventTimer.vue @@ -0,0 +1,252 @@ +<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> diff --git a/src/components/bpmnProcessDesigner/package/penal/custom-config/components/UserTaskCustomConfig.vue b/src/components/bpmnProcessDesigner/package/penal/custom-config/components/UserTaskCustomConfig.vue new file mode 100644 index 0000000..ba38514 --- /dev/null +++ b/src/components/bpmnProcessDesigner/package/penal/custom-config/components/UserTaskCustomConfig.vue @@ -0,0 +1,623 @@ +<!-- 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> diff --git a/src/components/bpmnProcessDesigner/package/penal/custom-config/data.ts b/src/components/bpmnProcessDesigner/package/penal/custom-config/data.ts new file mode 100644 index 0000000..a45355e --- /dev/null +++ b/src/components/bpmnProcessDesigner/package/penal/custom-config/data.ts @@ -0,0 +1,13 @@ +import UserTaskCustomConfig from './components/UserTaskCustomConfig.vue' +import BoundaryEventTimer from './components/BoundaryEventTimer.vue' + +export const CustomConfigMap = { + UserTask: { + name: '用户任务', + componet: UserTaskCustomConfig + }, + BoundaryEventTimerEventDefinition: { + name: '定时边界事件(非中断)', + componet: BoundaryEventTimer + } +} diff --git a/src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue b/src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue index de5445c..7c5cd1f 100644 --- a/src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue +++ b/src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue @@ -302,7 +302,7 @@ } // 打开 监听器详情 侧边栏 const openListenerForm = (listener, index?) => { - // debugger + console.log(listener) if (listener) { listenerForm.value = initListenerForm(listener) editingListenerIndex.value = index diff --git a/src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue b/src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue index 76e0c80..4486698 100644 --- a/src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue +++ b/src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue @@ -337,16 +337,13 @@ 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) ) diff --git a/src/components/bpmnProcessDesigner/package/penal/listeners/utilSelf.ts b/src/components/bpmnProcessDesigner/package/penal/listeners/utilSelf.ts index b4eb1d2..6ca7eb6 100644 --- a/src/components/bpmnProcessDesigner/package/penal/listeners/utilSelf.ts +++ b/src/components/bpmnProcessDesigner/package/penal/listeners/utilSelf.ts @@ -1,5 +1,6 @@ // 初始化表单数据 export function initListenerForm(listener) { + console.log(listener) let self = { ...listener } @@ -28,6 +29,7 @@ } export function initListenerType(listener) { + listener.id = listener.$attrs.id let listenerType if (listener.class) listenerType = 'classListener' if (listener.expression) listenerType = 'expressionListener' diff --git a/src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue b/src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue index 7cd16f7..de2fb0d 100644 --- a/src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue +++ b/src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue @@ -1,6 +1,30 @@ <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> @@ -76,11 +100,14 @@ </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('') @@ -267,16 +294,118 @@ } } +/** + * -----新版本多实例----- + */ +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 } ) diff --git a/src/components/bpmnProcessDesigner/package/penal/properties/ElementProperties.vue b/src/components/bpmnProcessDesigner/package/penal/properties/ElementProperties.vue index 016cdf6..7bf4f0e 100644 --- a/src/components/bpmnProcessDesigner/package/penal/properties/ElementProperties.vue +++ b/src/components/bpmnProcessDesigner/package/penal/properties/ElementProperties.vue @@ -75,7 +75,6 @@ const bpmnInstances = () => (window as any)?.bpmnInstances const resetAttributesList = () => { - console.log(window, 'windowwindowwindowwindowwindowwindowwindow') bpmnElement.value = bpmnInstances().bpmnElement otherExtensionList.value = [] // 其他扩展配置 bpmnElementProperties.value = @@ -85,7 +84,7 @@ otherExtensionList.value.push(ex) } return ex.$type === `${prefix}:Properties` - }) ?? [] + }) ?? []; // 保存所有的 扩展属性字段 bpmnElementPropertyList.value = bpmnElementProperties.value.reduce( diff --git a/src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue b/src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue index bbeeb4c..3a71b4c 100644 --- a/src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue +++ b/src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue @@ -29,9 +29,7 @@ </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' }) @@ -45,14 +43,7 @@ exclusive: false }) const witchTaskComponent = ref() -const installedComponent = ref({ - // 手工任务与普通任务一致,不需要其他配置 - // 接收消息任务,需要在全局下插入新的消息实例,并在该节点下的 messageRef 属性绑定该实例 - // 发送任务、服务任务、业务规则任务共用一个相同配置 - UserTask: 'UserTask', // 用户任务配置 - ScriptTask: 'ScriptTask', // 脚本任务配置 - ReceiveTask: 'ReceiveTask' // 消息接收任务 -}) + const bpmnElement = ref() const bpmnInstances = () => (window as any).bpmnInstances @@ -78,15 +69,8 @@ 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 } diff --git a/src/components/bpmnProcessDesigner/package/penal/task/data.ts b/src/components/bpmnProcessDesigner/package/penal/task/data.ts new file mode 100644 index 0000000..805c9ac --- /dev/null +++ b/src/components/bpmnProcessDesigner/package/penal/task/data.ts @@ -0,0 +1,36 @@ +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] +} diff --git a/src/components/bpmnProcessDesigner/package/penal/task/task-components/CallActivity.vue b/src/components/bpmnProcessDesigner/package/penal/task/task-components/CallActivity.vue new file mode 100644 index 0000000..6d8268b --- /dev/null +++ b/src/components/bpmnProcessDesigner/package/penal/task/task-components/CallActivity.vue @@ -0,0 +1,280 @@ +<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> diff --git a/src/components/bpmnProcessDesigner/package/penal/task/task-components/ServiceTask.vue b/src/components/bpmnProcessDesigner/package/penal/task/task-components/ServiceTask.vue new file mode 100644 index 0000000..2f9c535 --- /dev/null +++ b/src/components/bpmnProcessDesigner/package/penal/task/task-components/ServiceTask.vue @@ -0,0 +1,91 @@ +<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> diff --git a/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue b/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue index f404ef7..e563bfd 100644 --- a/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue +++ b/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue @@ -1,5 +1,5 @@ <template> - <el-form label-width="100px"> + <el-form label-width="120px"> <el-form-item label="规则类型" prop="candidateStrategy"> <el-select v-model="userTaskForm.candidateStrategy" @@ -8,15 +8,15 @@ @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" > @@ -31,7 +31,11 @@ </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" @@ -49,7 +53,7 @@ /> </el-form-item> <el-form-item - v-if="userTaskForm.candidateStrategy == 22" + v-if="userTaskForm.candidateStrategy == CandidateStrategy.POST" label="指定岗位" prop="candidateParam" span="24" @@ -65,7 +69,7 @@ </el-select> </el-form-item> <el-form-item - v-if="userTaskForm.candidateStrategy == 30" + v-if="userTaskForm.candidateStrategy == CandidateStrategy.USER" label="指定用户" prop="candidateParam" span="24" @@ -86,7 +90,7 @@ </el-select> </el-form-item> <el-form-item - v-if="userTaskForm.candidateStrategy === 40" + v-if="userTaskForm.candidateStrategy === CandidateStrategy.USER_GROUP" label="指定用户组" prop="candidateParam" > @@ -106,7 +110,67 @@ </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" > @@ -114,12 +178,17 @@ 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> @@ -127,7 +196,12 @@ </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' @@ -136,12 +210,14 @@ 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: [] // 分配选项 @@ -155,11 +231,88 @@ 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 { @@ -172,7 +325,7 @@ } else { userTaskForm.value.candidateParam = businessObject.candidateParam .split(',') - .map((item) => +item) + .map((item) => item) } } else { userTaskForm.value.candidateParam = [] @@ -182,11 +335,55 @@ /** 更新 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(',') @@ -203,6 +400,14 @@ updateElementTask() } +const handleFormUserChange = (e) => { + if (e === 'PROCESS_START_USER_ID') { + userTaskForm.value.candidateParam = [] + userTaskForm.value.candidateStrategy = CandidateStrategy.START_USER + } + updateElementTask() +} + watch( () => props.id, () => { diff --git a/src/directives/index.ts b/src/directives/index.ts index 89cc8ba..1b99988 100644 --- a/src/directives/index.ts +++ b/src/directives/index.ts @@ -11,3 +11,14 @@ hasRole(app) hasPermi(app) } + +/** + * 导出指令:v-mountedFocus + */ +export const setupMountedFocus = (app: App<Element>) => { + app.directive('mountedFocus', { + mounted(el) { + el.focus() + } + }) +} diff --git a/src/directives/permission/hasPermi.ts b/src/directives/permission/hasPermi.ts index d86d2f5..931f44b 100644 --- a/src/directives/permission/hasPermi.ts +++ b/src/directives/permission/hasPermi.ts @@ -8,7 +8,8 @@ const { wsCache } = useCache() const { value } = binding const all_permission = '*:*:*' - const permissions = wsCache.get(CACHE_KEY.USER).permissions + const userInfo = wsCache.get(CACHE_KEY.USER) + const permissions = userInfo?.permissions || [] if (value && value instanceof Array && value.length > 0) { const permissionFlag = value diff --git a/src/directives/permission/hasRole.ts b/src/directives/permission/hasRole.ts index 31a352a..a512811 100644 --- a/src/directives/permission/hasRole.ts +++ b/src/directives/permission/hasRole.ts @@ -7,8 +7,9 @@ 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 diff --git a/src/hooks/web/useMessage.ts b/src/hooks/web/useMessage.ts index 2bbf5cb..ac2b552 100644 --- a/src/hooks/web/useMessage.ts +++ b/src/hooks/web/useMessage.ts @@ -90,30 +90,6 @@ 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' - } - ) } } } diff --git a/src/layout/components/UserInfo/src/UserInfo.vue b/src/layout/components/UserInfo/src/UserInfo.vue index 355aabc..714a088 100644 --- a/src/layout/components/UserInfo/src/UserInfo.vue +++ b/src/layout/components/UserInfo/src/UserInfo.vue @@ -43,14 +43,14 @@ }) 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://doc.iocoder.cn/') + window.open('https://doc.iailab.cn/') } </script> diff --git a/src/main.ts b/src/main.ts index 4694019..ed098e4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -42,7 +42,7 @@ import router, { setupRouter } from '@/router' // 权限 -import { setupAuth } from '@/directives' +import { setupAuth, setupMountedFocus } from '@/directives' import { createApp } from 'vue' @@ -87,6 +87,8 @@ setupAuth(app) + setupMountedFocus(app) + await router.isReady() app.use(VueDOMPurifyHTML) diff --git a/src/store/modules/app.ts b/src/store/modules/app.ts index 8733618..e3d6a56 100644 --- a/src/store/modules/app.ts +++ b/src/store/modules/app.ts @@ -1,6 +1,6 @@ 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' @@ -21,6 +21,7 @@ locale: boolean message: boolean tagsView: boolean + tagsViewImmerse: boolean tagsViewIcon: boolean logo: boolean fixedHeader: boolean @@ -58,6 +59,7 @@ locale: true, // 多语言图标 message: true, // 消息图标 tagsView: true, // 标签页 + tagsViewImmerse: false, // 标签页沉浸 tagsViewIcon: true, // 是否显示标签图标 logo: true, // logo fixedHeader: true, // 固定toolheader @@ -130,6 +132,9 @@ }, getTagsView(): boolean { return this.tagsView + }, + getTagsViewImmerse(): boolean { + return this.tagsViewImmerse }, getTagsViewIcon(): boolean { return this.tagsViewIcon @@ -208,6 +213,9 @@ setTagsView(tagsView: boolean) { this.tagsView = tagsView }, + setTagsViewImmerse(tagsViewImmerse: boolean) { + this.tagsViewImmerse = tagsViewImmerse + }, setTagsViewIcon(tagsViewIcon: boolean) { this.tagsViewIcon = tagsViewIcon }, diff --git a/src/store/modules/permission.ts b/src/store/modules/permission.ts index 2000b33..f32facc 100644 --- a/src/store/modules/permission.ts +++ b/src/store/modules/permission.ts @@ -45,7 +45,6 @@ this.addRouters = routerMap.concat([ { path: '/:path(.*)*', - // redirect: '/404', component: () => import('@/views/Error/404.vue'), name: '404Page', meta: { diff --git a/src/styles/global.module.scss b/src/styles/global.module.scss index 8448a92..af793f0 100644 --- a/src/styles/global.module.scss +++ b/src/styles/global.module.scss @@ -1,4 +1,4 @@ -@import './variables.scss'; +@use './variables.scss' as *; // 导出变量 :export { namespace: $namespace; diff --git a/src/styles/index.scss b/src/styles/index.scss index fbe76f2..7607941 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -1,6 +1,7 @@ -@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; diff --git a/src/styles/var.css b/src/styles/var.css index 63459ba..44f9405 100644 --- a/src/styles/var.css +++ b/src/styles/var.css @@ -64,3 +64,11 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } + +*, +:after, +:before { + margin: 0; + padding: 0; + box-sizing: border-box; +} diff --git a/src/utils/formCreate.ts b/src/utils/formCreate.ts index 850df8c..a93d9cd 100644 --- a/src/utils/formCreate.ts +++ b/src/utils/formCreate.ts @@ -44,6 +44,7 @@ value?: object ) => { if (isRef(detailPreview)) { + // @ts-ignore detailPreview = detailPreview.value } // @ts-ignore diff --git a/src/utils/permission.ts b/src/utils/permission.ts index a63ee62..43d7f95 100644 --- a/src/utils/permission.ts +++ b/src/utils/permission.ts @@ -12,7 +12,8 @@ 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) }) @@ -33,7 +34,8 @@ 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) }) diff --git a/src/utils/tree.ts b/src/utils/tree.ts index 91059ef..e5db503 100644 --- a/src/utils/tree.ts +++ b/src/utils/tree.ts @@ -376,6 +376,9 @@ 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}` diff --git a/src/views/Home/Index.vue b/src/views/Home/Index.vue index e931277..1558788 100644 --- a/src/views/Home/Index.vue +++ b/src/views/Home/Index.vue @@ -48,7 +48,7 @@ // userInfo.menus = data wsCache.set(CACHE_KEY.USER, userInfo) wsSessionCache.set(CACHE_KEY.ROLE_ROUTERS, data) - window.location.href = '/plat/index' + window.location.href = import.meta.env.VITE_BASE_PATH + 'index' } const getAllApi = async () => { @@ -64,7 +64,6 @@ 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 diff --git a/src/views/bpm/form/index.vue b/src/views/bpm/form/index.vue index fd55242..65699c4 100644 --- a/src/views/bpm/form/index.vue +++ b/src/views/bpm/form/index.vue @@ -1,5 +1,4 @@ <template> - <ContentWrap> <!-- 搜索工作栏 --> <el-form diff --git a/src/views/bpm/model/CategoryDraggableModel.vue b/src/views/bpm/model/CategoryDraggableModel.vue index 3546593..7bd58d7 100644 --- a/src/views/bpm/model/CategoryDraggableModel.vue +++ b/src/views/bpm/model/CategoryDraggableModel.vue @@ -236,6 +236,11 @@ </template> </Dialog> + <!-- 弹窗:表单详情 --> + <Dialog title="表单详情" v-model="formDetailVisible" width="800"> + <form-create :rule="formDetailPreview.rule" :option="formDetailPreview.option" /> + </Dialog> + <!-- 表单弹窗:添加流程模型 --> <ModelForm :categoryId="categoryInfo.code" ref="modelFormRef" @success="emit('success')" /> </template> diff --git a/src/views/bpm/model/editor/index.vue b/src/views/bpm/model/editor/index.vue index 3e77369..1a41a50 100644 --- a/src/views/bpm/model/editor/index.vue +++ b/src/views/bpm/model/editor/index.vue @@ -31,12 +31,19 @@ // 自定义左侧菜单(修改 默认任务 为 用户任务) import CustomPaletteProvider from '@/components/bpmnProcessDesigner/package/designer/plugins/palette' import * as ModelApi from '@/api/bpm/model' +import { getForm, FormVO } from '@/api/bpm/form' defineOptions({ name: 'BpmModelEditor' }) const router = useRouter() // 路由 const { query } = useRoute() // 路由的查询 const message = useMessage() // 国际化 + +// 表单信息 +const formFields = ref<string[]>([]) +const formType = ref(20) +provide('formFields', formFields) +provide('formType', formType) const xmlString = ref(undefined) // BPMN XML const modeler = ref(null) // BPMN Modeler @@ -99,6 +106,13 @@ </bpmndi:BPMNDiagram> </definitions>` } + + formType.value = data.formType + if (data.formType === 10) { + const bpmnForm = (await getForm(data.formId)) as unknown as FormVO + formFields.value = bpmnForm?.fields + } + model.value = { ...data, bpmnXml: undefined // 清空 bpmnXml 属性 diff --git a/src/views/bpm/processInstance/create/ProcessDefinitionDetail.vue b/src/views/bpm/processInstance/create/ProcessDefinitionDetail.vue index 610963e..3800f19 100644 --- a/src/views/bpm/processInstance/create/ProcessDefinitionDetail.vue +++ b/src/views/bpm/processInstance/create/ProcessDefinitionDetail.vue @@ -8,8 +8,8 @@ <!-- 中间主要内容 tab 栏 --> <el-tabs v-model="activeTab"> <!-- 表单信息 --> - <el-tab-pane label="表单填写" name="form"> - <div class="form-scroll-area"> + <el-tab-pane label="表单填写" name="form" > + <div class="form-scroll-area" v-loading="processInstanceStartLoading"> <el-scrollbar> <el-row> <el-col :span="17"> @@ -90,7 +90,7 @@ selectProcessDefinition: any }>() const emit = defineEmits(['cancel']) - +const processInstanceStartLoading = ref(false) // 流程实例发起中 const { push, currentRoute } = useRouter() // 路由 const message = useMessage() // 消息弹窗 const { delView } = useTagsViewStore() // 视图操作 @@ -179,6 +179,8 @@ if (!fApi.value || !props.selectProcessDefinition) { return } + // 流程表单校验 + await fApi.value.validate() // 如果有指定审批人,需要校验 if (startUserSelectTasks.value?.length > 0) { for (const userTask of startUserSelectTasks.value) { @@ -191,7 +193,7 @@ } // 提交请求 - fApi.value.btn.loading(true) + processInstanceStartLoading.value = true try { await ProcessInstanceApi.createProcessInstance({ processDefinitionId: props.selectProcessDefinition.id, @@ -206,7 +208,7 @@ name: 'BpmProcessInstanceMy' }) } finally { - fApi.value.btn.loading(false) + processInstanceStartLoading.value = false } } diff --git a/src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue b/src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue index b92be7e..894a5d4 100644 --- a/src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue +++ b/src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue @@ -20,9 +20,9 @@ <el-form label-position="top" class="mb-auto" - ref="formRef" - :model="genericForm" - :rules="genericRule" + ref="approveFormRef" + :model="approveReasonForm" + :rules="approveReasonRule" label-width="100px" > <el-card v-if="runningTask?.formId > 0" class="mb-15px !-mt-10px"> @@ -38,17 +38,17 @@ </el-card> <el-form-item label="审批意见" prop="reason"> <el-input - v-model="genericForm.reason" + v-model="approveReasonForm.reason" placeholder="请输入审批意见" type="textarea" :rows="4" /> </el-form-item> <el-form-item> - <el-button :disabled="formLoading" type="success" @click="handleAudit(true)"> + <el-button :disabled="formLoading" type="success" @click="handleAudit(true, approveFormRef)"> {{ getButtonDisplayName(OperationButtonType.APPROVE) }} </el-button> - <el-button @click="popOverVisible.approve = false"> 取消 </el-button> + <el-button @click="closePropover('approve', approveFormRef)"> 取消 </el-button> </el-form-item> </el-form> </div> @@ -72,35 +72,24 @@ <el-form label-position="top" class="mb-auto" - ref="formRef" - :model="genericForm" - :rules="genericRule" + ref="rejectFormRef" + :model="rejectReasonForm" + :rules="rejectReasonRule" 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="genericForm.reason" + v-model="rejectReasonForm.reason" placeholder="请输入审批意见" type="textarea" :rows="4" /> </el-form-item> <el-form-item> - <el-button :disabled="formLoading" type="danger" @click="handleAudit(false)"> + <el-button :disabled="formLoading" type="danger" @click="handleAudit(false,rejectFormRef)"> {{ getButtonDisplayName(OperationButtonType.REJECT) }} </el-button> - <el-button @click="popOverVisible.reject = false"> 取消 </el-button> + <el-button @click="closePropover('reject', rejectFormRef)"> 取消 </el-button> </el-form-item> </el-form> </div> @@ -124,14 +113,14 @@ <el-form label-position="top" class="mb-auto" - ref="formRef" - :model="genericForm" - :rules="genericRule" + ref="copyFormRef" + :model="copyForm" + :rules="copyFormRule" label-width="100px" > <el-form-item label="抄送人" prop="copyUserIds"> <el-select - v-model="genericForm.copyUserIds" + v-model="copyForm.copyUserIds" clearable style="width: 100%" multiple @@ -147,7 +136,7 @@ </el-form-item> <el-form-item label="抄送意见" prop="copyReason"> <el-input - v-model="genericForm.copyReason" + v-model="copyForm.copyReason" clearable placeholder="请输入抄送意见" type="textarea" @@ -158,13 +147,13 @@ <el-button :disabled="formLoading" type="primary" @click="handleCopy"> {{ getButtonDisplayName(OperationButtonType.COPY) }} </el-button> - <el-button @click="popOverVisible.copy = false"> 取消 </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" @@ -182,13 +171,13 @@ <el-form label-position="top" class="mb-auto" - ref="formRef" - :model="genericForm" - :rules="genericRule" + ref="transferFormRef" + :model="transferForm" + :rules="transferFormRule" label-width="100px" > <el-form-item label="新审批人" prop="assigneeUserId"> - <el-select v-model="genericForm.assigneeUserId" clearable style="width: 100%"> + <el-select v-model="transferForm.assigneeUserId" clearable style="width: 100%"> <el-option v-for="item in userOptions" :key="item.id" @@ -199,7 +188,7 @@ </el-form-item> <el-form-item label="审批意见" prop="reason"> <el-input - v-model="genericForm.reason" + v-model="transferForm.reason" clearable placeholder="请输入审批意见" type="textarea" @@ -210,7 +199,7 @@ <el-button :disabled="formLoading" type="primary" @click="handleTransfer()"> {{ getButtonDisplayName(OperationButtonType.TRANSFER) }} </el-button> - <el-button @click="popOverVisible.transfer = false"> 取消 </el-button> + <el-button @click="closePropover('transfer', transferFormRef)"> 取消 </el-button> </el-form-item> </el-form> </div> @@ -234,13 +223,13 @@ <el-form label-position="top" class="mb-auto" - ref="formRef" - :model="genericForm" - :rules="genericRule" + ref="delegateFormRef" + :model="delegateForm" + :rules="delegateFormRule" label-width="100px" > <el-form-item label="接收人" prop="delegateUserId"> - <el-select v-model="genericForm.delegateUserId" clearable style="width: 100%"> + <el-select v-model="delegateForm.delegateUserId" clearable style="width: 100%"> <el-option v-for="item in userOptions" :key="item.id" @@ -251,7 +240,7 @@ </el-form-item> <el-form-item label="审批意见" prop="reason"> <el-input - v-model="genericForm.reason" + v-model="delegateForm.reason" clearable placeholder="请输入审批意见" type="textarea" @@ -262,7 +251,7 @@ <el-button :disabled="formLoading" type="primary" @click="handleDelegate()"> {{ getButtonDisplayName(OperationButtonType.DELEGATE) }} </el-button> - <el-button @click="popOverVisible.delegate = false"> 取消 </el-button> + <el-button @click="closePropover('delegate', delegateFormRef)"> 取消 </el-button> </el-form-item> </el-form> </div> @@ -286,13 +275,13 @@ <el-form label-position="top" class="mb-auto" - ref="formRef" - :model="genericForm" - :rules="genericRule" + ref="addSignFormRef" + :model="addSignForm" + :rules="addSignFormRule" label-width="100px" > <el-form-item label="加签处理人" prop="addSignUserIds"> - <el-select v-model="genericForm.addSignUserIds" multiple clearable style="width: 100%"> + <el-select v-model="addSignForm.addSignUserIds" multiple clearable style="width: 100%"> <el-option v-for="item in userOptions" :key="item.id" @@ -303,7 +292,7 @@ </el-form-item> <el-form-item label="审批意见" prop="reason"> <el-input - v-model="genericForm.reason" + v-model="addSignForm.reason" clearable placeholder="请输入审批意见" type="textarea" @@ -317,7 +306,7 @@ <el-button :disabled="formLoading" type="primary" @click="handlerAddSign('after')"> 向后{{ getButtonDisplayName(OperationButtonType.ADD_SIGN) }} </el-button> - <el-button @click="popOverVisible.addSign = false"> 取消 </el-button> + <el-button @click="closePropover('addSign', addSignFormRef)"> 取消 </el-button> </el-form-item> </el-form> </div> @@ -340,13 +329,13 @@ <el-form label-position="top" class="mb-auto" - ref="formRef" - :model="genericForm" - :rules="genericRule" + ref="deleteSignFormRef" + :model="deleteSignForm" + :rules="deleteSignFormRule" label-width="100px" > <el-form-item label="减签人员" prop="deleteSignTaskId"> - <el-select v-model="genericForm.deleteSignTaskId" clearable style="width: 100%"> + <el-select v-model="deleteSignForm.deleteSignTaskId" clearable style="width: 100%"> <el-option v-for="item in runningTask.children" :key="item.id" @@ -357,7 +346,7 @@ </el-form-item> <el-form-item label="审批意见" prop="reason"> <el-input - v-model="genericForm.reason" + v-model="deleteSignForm.reason" clearable placeholder="请输入审批意见" type="textarea" @@ -368,7 +357,7 @@ <el-button :disabled="formLoading" type="primary" @click="handlerDeleteSign()"> 减签 </el-button> - <el-button @click="popOverVisible.deleteSign = false"> 取消 </el-button> + <el-button @click="closePropover('deleteSign', deleteSignFormRef)"> 取消 </el-button> </el-form-item> </el-form> </div> @@ -383,7 +372,7 @@ v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.RETURN)" > <template #reference> - <div @click="openReturnPopover" class="hover-bg-gray-100 rounded-xl p-6px"> + <div @click="openPopover('return')" class="hover-bg-gray-100 rounded-xl p-6px"> <Icon :size="14" icon="ep:back" /> {{ getButtonDisplayName(OperationButtonType.RETURN) }} </div> @@ -392,13 +381,13 @@ <el-form label-position="top" class="mb-auto" - ref="formRef" - :model="genericForm" - :rules="genericRule" + ref="returnFormRef" + :model="returnForm" + :rules="returnFormRule" label-width="100px" > <el-form-item label="退回节点" prop="targetTaskDefinitionKey"> - <el-select v-model="genericForm.targetTaskDefinitionKey" clearable style="width: 100%"> + <el-select v-model="returnForm.targetTaskDefinitionKey" clearable style="width: 100%"> <el-option v-for="item in returnList" :key="item.taskDefinitionKey" @@ -409,7 +398,7 @@ </el-form-item> <el-form-item label="退回理由" prop="returnReason"> <el-input - v-model="genericForm.returnReason" + v-model="returnForm.returnReason" clearable placeholder="请输入退回理由" type="textarea" @@ -420,7 +409,7 @@ <el-button :disabled="formLoading" type="primary" @click="handleReturn()"> {{ getButtonDisplayName(OperationButtonType.RETURN) }} </el-button> - <el-button @click="popOverVisible.return = false"> 取消 </el-button> + <el-button @click="closePropover('return', returnFormRef)"> 取消 </el-button> </el-form-item> </el-form> </div> @@ -445,15 +434,15 @@ <el-form label-position="top" class="mb-auto" - ref="formRef" - :model="genericForm" - :rules="genericRule" + 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="genericForm.cancelReason" + v-model="cancelForm.cancelReason" clearable placeholder="请输入取消理由" type="textarea" @@ -462,9 +451,9 @@ </el-form-item> <el-form-item> <el-button :disabled="formLoading" type="primary" @click="handleCancel()"> - 取消 + 确认 </el-button> - <el-button @click="popOverVisible.cancel = false"> 取消 </el-button> + <el-button @click="closePropover('cancel', cancelFormRef)"> 取消 </el-button> </el-form-item> </el-form> </div> @@ -488,26 +477,29 @@ import { setConfAndFields2 } from '@/utils/formCreate' import * as TaskApi from '@/api/bpm/task' import * as ProcessInstanceApi from '@/api/bpm/processInstance' -import { propTypes } from '@/utils/propTypes' +import * as UserApi from '@/api/system/user' import { OperationButtonType, OPERATION_BUTTON_NAME } from '@/components/SimpleProcessDesignerV2/src/consts' -import { BpmProcessInstanceStatus } from '@/utils/constants' - +import { BpmProcessInstanceStatus, BpmModelFormType } from '@/utils/constants' +import type { FormInstance, FormRules } from 'element-plus' defineOptions({ name: 'ProcessInstanceBtnContainer' }) const router = useRouter() // 路由 const message = useMessage() // 消息弹窗 -const { proxy } = getCurrentInstance() as any const userId = useUserStoreWithOut().getUser.id // 当前登录的编号 const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 -const props = defineProps({ - processInstance: propTypes.object, // 流程实例信息 - processDefinition: propTypes.object, // 流程定义信息 - userOptions: propTypes.any -}) + +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({ @@ -525,21 +517,99 @@ // ========== 审批信息 ========== const runningTask = ref<any>() // 运行中的任务 -const genericForm = ref<any>({}) // 通用表单 const approveForm = ref<any>({}) // 审批通过时,额外的补充信息 const approveFormFApi = ref<any>({}) // approveForms 的 fAPi -const formRef = ref() -const genericRule = reactive({ + +// 审批通过意见表单 +const approveFormRef = ref<FormInstance>() +const approveReasonForm = reactive({ + reason: '' +}) +const approveReasonRule = reactive<FormRules<typeof approveReasonForm>>({ reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }], - returnReason: [{ required: true, message: '退回理由不能为空', trigger: 'blur' }], - cancelReason: [{ required: true, message: '取消理由不能为空', trigger: 'blur' }], - copyUserIds: [{ required: true, message: '抄送人不能为空', trigger: 'change' }], +}) +// 拒绝表单 +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' }], - targetTaskDefinitionKey: [{ 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( @@ -553,43 +623,57 @@ } ) -/** 弹出退回气泡卡 */ -const openReturnPopover = async () => { - returnList.value = await TaskApi.getTaskListByReturn(runningTask.value.id) - if (returnList.value.length === 0) { - message.warning('当前没有可退回的节点') - return - } - await openPopover('return') -} - /** 弹出气泡卡 */ 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() + // 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) => { +const handleAudit = async (pass: boolean, formRef: FormInstance | undefined) => { formLoading.value = true try { - const genericFormRef = proxy.$refs['formRef'] - // 1.2 校验表单 - const elForm = unref(genericFormRef) - if (!elForm) return - const valid = await elForm.validate() - if (!valid) return - - // 2.1 提交审批 - const data = { - id: runningTask.value.id, - reason: genericForm.value.reason - } + // 校验表单 + if (!formRef) return + await formRef.validate() if (pass) { - // 审批通过,并且有额外的 approveForm 表单,需要校验 + 拼接到 data 表单里提交 + // 获取修改的流程变量, 暂时只支持流程表单 + 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() @@ -600,11 +684,18 @@ 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('审批不通过成功') } - // 2.2 加载最新数据 + // 重置表单 + formRef.resetFields() + // 加载最新数据 reload() } finally { formLoading.value = false @@ -615,19 +706,17 @@ const handleCopy = async () => { formLoading.value = true try { - const copyFormRef = proxy.$refs['formRef'] // 1. 校验表单 - const elForm = unref(copyFormRef) - if (!elForm) return - const valid = await elForm.validate() - if (!valid) return + if (!copyFormRef.value) return + await copyFormRef.value.validate() // 2. 提交抄送 const data = { id: runningTask.value.id, - reason: genericForm.value.copyReason, - copyUserIds: genericForm.value.copyUserIds + reason: copyForm.copyReason, + copyUserIds:copyForm.copyUserIds } await TaskApi.copyTask(data) + copyFormRef.value.resetFields() popOverVisible.value.copy = false message.success('操作成功') } finally { @@ -639,20 +728,17 @@ const handleTransfer = async () => { formLoading.value = true try { - const transferFormRef = proxy.$refs['formRef'] // 1.1 校验表单 - const elForm = unref(transferFormRef) - if (!elForm) return - const valid = await elForm.validate() - if (!valid) return + if (!transferFormRef.value) return + await transferFormRef.value.validate() // 1.2 提交转交 const data = { id: runningTask.value.id, - reason: genericForm.value.reason, - assigneeUserId: genericForm.value.assigneeUserId + reason: transferForm.reason, + assigneeUserId: transferForm.assigneeUserId } - await TaskApi.transferTask(data) + transferFormRef.value.resetFields() popOverVisible.value.transfer = false message.success('操作成功') // 2. 加载最新数据 @@ -666,21 +752,20 @@ const handleDelegate = async () => { formLoading.value = true try { - const deletegateFormRef = proxy.$refs['formRef'] + // 1.1 校验表单 - const elForm = unref(deletegateFormRef) - if (!elForm) return - const valid = await elForm.validate() - if (!valid) return + if (!delegateFormRef.value) return + await delegateFormRef.value.validate() // 1.2 处理委派 const data = { id: runningTask.value.id, - reason: genericForm.value.reason, - delegateUserId: genericForm.value.delegateUserId + reason: delegateForm.reason, + delegateUserId: delegateForm.delegateUserId } await TaskApi.delegateTask(data) popOverVisible.value.delegate = false + delegateFormRef.value.resetFields() message.success('操作成功') // 2. 加载最新数据 reload() @@ -693,21 +778,19 @@ const handlerAddSign = async (type: string) => { formLoading.value = true try { - const transferFormRef = proxy.$refs['formRef'] // 1.1 校验表单 - const elForm = unref(transferFormRef) - if (!elForm) return - const valid = await elForm.validate() - if (!valid) return + if (!addSignFormRef.value) return + await addSignFormRef.value.validate() // 1.2 提交加签 const data = { id: runningTask.value.id, type, - reason: genericForm.value.reason, - userIds: genericForm.value.addSignUserIds + reason: addSignForm.reason, + userIds: addSignForm.addSignUserIds } await TaskApi.signCreateTask(data) message.success('操作成功') + addSignFormRef.value.resetFields() popOverVisible.value.addSign = false // 2 加载最新数据 reload() @@ -720,21 +803,19 @@ const handleReturn = async () => { formLoading.value = true try { - const returnFormRef = proxy.$refs['formRef'] // 1.1 校验表单 - const elForm = unref(returnFormRef) - if (!elForm) return - const valid = await elForm.validate() - if (!valid) return + if (!returnFormRef.value) return + await returnFormRef.value.validate() // 1.2 提交退回 const data = { id: runningTask.value.id, - reason: genericForm.value.returnReason, - targetTaskDefinitionKey: genericForm.value.targetTaskDefinitionKey + reason: returnForm.returnReason, + targetTaskDefinitionKey: returnForm.targetTaskDefinitionKey } await TaskApi.returnTask(data) popOverVisible.value.return = false + returnFormRef.value.resetFields() message.success('操作成功') // 2 重新加载数据 reload() @@ -747,19 +828,17 @@ const handleCancel = async () => { formLoading.value = true try { - const cancelFormRef = proxy.$refs['formRef'] // 1.1 校验表单 - const elForm = unref(cancelFormRef) - if (!elForm) return - const valid = await elForm.validate() - if (!valid) return + if (!cancelFormRef.value) return + await cancelFormRef.value.validate() // 1.2 提交取消 await ProcessInstanceApi.cancelProcessInstanceByStartUser( props.processInstance.id, - genericForm.value.cancelReason + cancelForm.cancelReason ) popOverVisible.value.return = false message.success('操作成功') + cancelFormRef.value.resetFields() // 2 重新加载数据 reload() } finally { @@ -786,19 +865,17 @@ const handlerDeleteSign = async () => { formLoading.value = true try { - const deleteFormRef = proxy.$refs['formRef'] // 1.1 校验表单 - const elForm = unref(deleteFormRef) - if (!elForm) return - const valid = await elForm.validate() - if (!valid) return + if (!deleteSignFormRef.value) return + await deleteSignFormRef.value.validate() // 1.2 提交减签 const data = { - id: genericForm.value.deleteSignTaskId, - reason: genericForm.value.reason + id: deleteSignForm.deleteSignTaskId, + reason: deleteSignForm.reason } await TaskApi.signDeleteTask(data) message.success('减签成功') + deleteSignFormRef.value.resetFields() popOverVisible.value.deleteSign = false // 2 加载最新数据 reload() @@ -852,7 +929,6 @@ } const loadTodoTask = (task: any) => { - genericForm.value = {} approveForm.value = {} approveFormFApi.value = {} runningTask.value = task @@ -866,6 +942,30 @@ } } +/** 校验流程表单 */ +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> diff --git a/src/views/bpm/processInstance/detail/dialog/TaskDelegateForm.vue b/src/views/bpm/processInstance/detail/dialog/TaskDelegateForm.vue deleted file mode 100644 index 178b1b9..0000000 --- a/src/views/bpm/processInstance/detail/dialog/TaskDelegateForm.vue +++ /dev/null @@ -1,89 +0,0 @@ -<template> - <Dialog v-model="dialogVisible" title="委派任务" width="500"> - <el-form - ref="formRef" - v-loading="formLoading" - :model="formData" - :rules="formRules" - label-width="110px" - > - <el-form-item label="接收人" prop="delegateUserId"> - <el-select v-model="formData.delegateUserId" clearable style="width: 100%"> - <el-option - v-for="item in userList" - :key="item.id" - :label="item.nickname" - :value="item.id" - /> - </el-select> - </el-form-item> - <el-form-item label="委派理由" prop="reason"> - <el-input v-model="formData.reason" clearable placeholder="请输入委派理由" /> - </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> -</template> -<script lang="ts" setup> -import * as TaskApi from '@/api/bpm/task' -import * as UserApi from '@/api/system/user' - -defineOptions({ name: 'BpmTaskDelegateForm' }) - -const dialogVisible = ref(false) // 弹窗的是否展示 -const formLoading = ref(false) // 表单的加载中 -const formData = ref({ - id: '', - delegateUserId: undefined, - reason: '' -}) -const formRules = ref({ - delegateUserId: [{ required: true, message: '接收人不能为空', trigger: 'change' }], - reason: [{ required: true, message: '委派理由不能为空', trigger: 'blur' }] -}) - -const formRef = ref() // 表单 Ref -const userList = ref<any[]>([]) // 用户列表 - -/** 打开弹窗 */ -const open = async (id: string) => { - dialogVisible.value = true - resetForm() - formData.value.id = id - // 获得用户列表 - userList.value = await UserApi.getSimpleUserList() -} -defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗 - -/** 提交表单 */ -const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 -const submitForm = async () => { - // 校验表单 - if (!formRef) return - const valid = await formRef.value.validate() - if (!valid) return - // 提交请求 - formLoading.value = true - try { - await TaskApi.delegateTask(formData.value) - dialogVisible.value = false - // 发送操作成功的事件 - emit('success') - } finally { - formLoading.value = false - } -} - -/** 重置表单 */ -const resetForm = () => { - formData.value = { - id: '', - delegateUserId: undefined, - reason: '' - } - formRef.value?.resetFields() -} -</script> diff --git a/src/views/bpm/processInstance/detail/dialog/TaskReturnForm.vue b/src/views/bpm/processInstance/detail/dialog/TaskReturnForm.vue deleted file mode 100644 index a139169..0000000 --- a/src/views/bpm/processInstance/detail/dialog/TaskReturnForm.vue +++ /dev/null @@ -1,90 +0,0 @@ -<template> - <Dialog v-model="dialogVisible" title="回退任务" width="500"> - <el-form - ref="formRef" - v-loading="formLoading" - :model="formData" - :rules="formRules" - label-width="110px" - > - <el-form-item label="退回节点" prop="targetTaskDefinitionKey"> - <el-select v-model="formData.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="reason"> - <el-input v-model="formData.reason" clearable placeholder="请输入回退理由" /> - </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> -</template> -<script lang="ts" name="TaskRollbackDialogForm" setup> -import * as TaskApi from '@/api/bpm/task' - -const message = useMessage() // 消息弹窗 -const dialogVisible = ref(false) // 弹窗的是否展示 -const formLoading = ref(false) // 表单的加载中 -const formData = ref({ - id: '', - targetTaskDefinitionKey: undefined, - reason: '' -}) -const formRules = ref({ - targetTaskDefinitionKey: [{ required: true, message: '必须选择回退节点', trigger: 'change' }], - reason: [{ required: true, message: '回退理由不能为空', trigger: 'blur' }] -}) - -const formRef = ref() // 表单 Ref -const returnList = ref([] as any) -/** 打开弹窗 */ -const open = async (id: string) => { - returnList.value = await TaskApi.getTaskListByReturn(id) - if (returnList.value.length === 0) { - message.warning('当前没有可回退的节点') - return false - } - dialogVisible.value = true - resetForm() - formData.value.id = id -} -defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗 - -/** 提交表单 */ -const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 -const submitForm = async () => { - // 校验表单 - if (!formRef) return - const valid = await formRef.value.validate() - if (!valid) return - // 提交请求 - formLoading.value = true - try { - await TaskApi.returnTask(formData.value) - message.success('回退成功') - dialogVisible.value = false - // 发送操作成功的事件 - emit('success') - } finally { - formLoading.value = false - } -} - -/** 重置表单 */ -const resetForm = () => { - formData.value = { - id: '', - targetTaskDefinitionKey: undefined, - reason: '' - } - formRef.value?.resetFields() -} -</script> diff --git a/src/views/bpm/processInstance/detail/dialog/TaskSignCreateForm.vue b/src/views/bpm/processInstance/detail/dialog/TaskSignCreateForm.vue deleted file mode 100644 index 9e4998c..0000000 --- a/src/views/bpm/processInstance/detail/dialog/TaskSignCreateForm.vue +++ /dev/null @@ -1,99 +0,0 @@ -<template> - <Dialog v-model="dialogVisible" title="加签" width="500"> - <el-form - ref="formRef" - v-loading="formLoading" - :model="formData" - :rules="formRules" - label-width="110px" - > - <el-form-item label="加签处理人" prop="userIds"> - <el-select v-model="formData.userIds" multiple clearable style="width: 100%"> - <el-option - v-for="item in userList" - :key="item.id" - :label="item.nickname" - :value="item.id" - /> - </el-select> - </el-form-item> - <el-form-item label="加签理由" prop="reason"> - <el-input v-model="formData.reason" clearable placeholder="请输入加签理由" /> - </el-form-item> - </el-form> - <template #footer> - <el-button :disabled="formLoading" type="primary" @click="submitForm('before')"> - 向前加签 - </el-button> - <el-button :disabled="formLoading" type="primary" @click="submitForm('after')"> - 向后加签 - </el-button> - <el-button @click="dialogVisible = false">取 消</el-button> - </template> - </Dialog> -</template> -<script lang="ts" setup> -import * as TaskApi from '@/api/bpm/task' -import * as UserApi from '@/api/system/user' - -defineOptions({ name: 'TaskSignCreateForm' }) - -const message = useMessage() // 消息弹窗 -const dialogVisible = ref(false) // 弹窗的是否展示 -const formLoading = ref(false) // 表单的加载中 -const formData = ref({ - id: '', - userIds: [], - type: '', - reason: '' -}) -const formRules = ref({ - userIds: [{ required: true, message: '加签处理人不能为空', trigger: 'change' }], - reason: [{ required: true, message: '加签理由不能为空', trigger: 'change' }] -}) - -const formRef = ref() // 表单 Ref -const userList = ref<any[]>([]) // 用户列表 - -/** 打开弹窗 */ -const open = async (id: string) => { - dialogVisible.value = true - resetForm() - formData.value.id = id - // 获得用户列表 - userList.value = await UserApi.getSimpleUserList() -} -defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗 - -/** 提交表单 */ -const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 -const submitForm = async (type: string) => { - // 校验表单 - if (!formRef) return - const valid = await formRef.value.validate() - if (!valid) return - // 提交请求 - formLoading.value = true - formData.value.type = type - try { - await TaskApi.signCreateTask(formData.value) - message.success('加签成功') - dialogVisible.value = false - // 发送操作成功的事件 - emit('success') - } finally { - formLoading.value = false - } -} - -/** 重置表单 */ -const resetForm = () => { - formData.value = { - id: '', - userIds: [], - type: '', - reason: '' - } - formRef.value?.resetFields() -} -</script> diff --git a/src/views/bpm/processInstance/detail/dialog/TaskSignDeleteForm.vue b/src/views/bpm/processInstance/detail/dialog/TaskSignDeleteForm.vue deleted file mode 100644 index fc6d6b2..0000000 --- a/src/views/bpm/processInstance/detail/dialog/TaskSignDeleteForm.vue +++ /dev/null @@ -1,89 +0,0 @@ -<template> - <Dialog v-model="dialogVisible" title="减签" width="500"> - <el-form - ref="formRef" - v-loading="formLoading" - :model="formData" - :rules="formRules" - label-width="110px" - > - <el-form-item label="减签任务" prop="id"> - <el-radio-group v-model="formData.id"> - <el-radio-button v-for="item in childrenTaskList" :key="item.id" :value="item.id"> - {{ item.name }} - ({{ item.assigneeUser?.deptName || item.ownerUser?.deptName }} - - {{ item.assigneeUser?.nickname || item.ownerUser?.nickname }}) - </el-radio-button> - </el-radio-group> - </el-form-item> - <el-form-item label="减签理由" prop="reason"> - <el-input v-model="formData.reason" clearable placeholder="请输入减签理由" /> - </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> -</template> -<script lang="ts" setup> -import * as TaskApi from '@/api/bpm/task' -import { isEmpty } from '@/utils/is' - -defineOptions({ name: 'TaskSignDeleteForm' }) - -const message = useMessage() // 消息弹窗 -const dialogVisible = ref(false) // 弹窗的是否展示 -const formLoading = ref(false) // 表单的加载中 -const formData = ref({ - id: '', - reason: '' -}) -const formRules = ref({ - id: [{ required: true, message: '必须选择减签任务', trigger: 'change' }], - reason: [{ required: true, message: '减签理由不能为空', trigger: 'blur' }] -}) - -const formRef = ref() // 表单 Ref -const childrenTaskList = ref([]) -/** 打开弹窗 */ -const open = async (id: string) => { - childrenTaskList.value = await TaskApi.getChildrenTaskList(id) - if (isEmpty(childrenTaskList.value)) { - message.warning('当前没有可减签的任务') - return false - } - dialogVisible.value = true - resetForm() -} -defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗 - -/** 提交表单 */ -const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 -const submitForm = async () => { - // 校验表单 - if (!formRef) return - const valid = await formRef.value.validate() - if (!valid) return - // 提交请求 - formLoading.value = true - try { - await TaskApi.signDeleteTask(formData.value) - message.success('减签成功') - dialogVisible.value = false - // 发送操作成功的事件 - emit('success') - } finally { - formLoading.value = false - } -} - -/** 重置表单 */ -const resetForm = () => { - formData.value = { - id: '', - reason: '' - } - formRef.value?.resetFields() -} -</script> diff --git a/src/views/bpm/processInstance/detail/dialog/TaskSignList.vue b/src/views/bpm/processInstance/detail/dialog/TaskSignList.vue deleted file mode 100644 index 648e86b..0000000 --- a/src/views/bpm/processInstance/detail/dialog/TaskSignList.vue +++ /dev/null @@ -1,106 +0,0 @@ -<template> - <el-drawer v-model="drawerVisible" title="子任务" size="880px"> - <!-- 当前任务 --> - <template #header> - <h4>【{{ parentTask.name }} 】审批人:{{ parentTask?.assigneeUser?.nickname }}</h4> - <el-button - style="margin-left: 5px" - v-if="isSignDeleteButtonVisible(parentTask)" - type="danger" - plain - @click="handleSignDelete(parentTask)" - > - <Icon icon="ep:remove" /> 减签 - </el-button> - </template> - <!-- 子任务列表 --> - <el-table :data="parentTask.children" style="width: 100%" row-key="id" border> - <el-table-column prop="assigneeUser.nickname" label="审批人" min-width="100"> - <template #default="scope"> - {{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }} - </template> - </el-table-column> - <el-table-column prop="assigneeUser.deptName" label="所在部门" min-width="100"> - <template #default="scope"> - {{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }} - </template> - </el-table-column> - <el-table-column label="审批状态" prop="status" width="120"> - <template #default="scope"> - <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" /> - </template> - </el-table-column> - <el-table-column - label="提交时间" - align="center" - prop="createTime" - width="180" - :formatter="dateFormatter" - /> - <el-table-column - label="结束时间" - align="center" - prop="endTime" - width="180" - :formatter="dateFormatter" - /> - <el-table-column label="操作" prop="operation" width="90"> - <template #default="scope"> - <el-button - v-if="isSignDeleteButtonVisible(scope.row)" - type="danger" - plain - size="small" - @click="handleSignDelete(scope.row)" - > - <Icon icon="ep:remove" /> 减签 - </el-button> - </template> - </el-table-column> - </el-table> - - <!-- 减签 --> - <TaskSignDeleteForm ref="taskSignDeleteFormRef" @success="handleSignDeleteSuccess" /> - </el-drawer> -</template> -<script lang="ts" setup> -import { isEmpty } from '@/utils/is' -import { DICT_TYPE } from '@/utils/dict' -import { dateFormatter } from '@/utils/formatTime' -import TaskSignDeleteForm from './TaskSignDeleteForm.vue' - -defineOptions({ name: 'TaskSignList' }) - -const message = useMessage() // 消息弹窗 -const drawerVisible = ref(false) // 抽屉的是否展示 -const parentTask = ref({} as any) - -/** 打开弹窗 */ -const open = async (task: any) => { - if (isEmpty(task.children)) { - message.warning('该任务没有子任务') - return - } - parentTask.value = task - // 展开抽屉 - drawerVisible.value = true -} -defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗 - -/** 发起减签 */ -const taskSignDeleteFormRef = ref() -const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 -const handleSignDelete = (item: any) => { - taskSignDeleteFormRef.value.open(item.id) -} -const handleSignDeleteSuccess = () => { - emit('success') - // 关闭抽屉 - drawerVisible.value = false -} - -/** 是否显示减签按钮 */ -const isSignDeleteButtonVisible = (task: any) => { - return task && task.children && !isEmpty(task.children) -} -</script> diff --git a/src/views/bpm/processInstance/detail/dialog/TaskTransferForm.vue b/src/views/bpm/processInstance/detail/dialog/TaskTransferForm.vue deleted file mode 100644 index c1012ac..0000000 --- a/src/views/bpm/processInstance/detail/dialog/TaskTransferForm.vue +++ /dev/null @@ -1,89 +0,0 @@ -<template> - <Dialog v-model="dialogVisible" title="转派任务" width="500"> - <el-form - ref="formRef" - v-loading="formLoading" - :model="formData" - :rules="formRules" - label-width="110px" - > - <el-form-item label="新审批人" prop="assigneeUserId"> - <el-select v-model="formData.assigneeUserId" clearable style="width: 100%"> - <el-option - v-for="item in userList" - :key="item.id" - :label="item.nickname" - :value="item.id" - /> - </el-select> - </el-form-item> - <el-form-item label="转派理由" prop="reason"> - <el-input v-model="formData.reason" clearable placeholder="请输入转派理由" /> - </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> -</template> -<script lang="ts" setup> -import * as TaskApi from '@/api/bpm/task' -import * as UserApi from '@/api/system/user' - -defineOptions({ name: 'TaskTransferForm' }) - -const dialogVisible = ref(false) // 弹窗的是否展示 -const formLoading = ref(false) // 表单的加载中 -const formData = ref({ - id: '', - assigneeUserId: undefined, - reason: '' -}) -const formRules = ref({ - assigneeUserId: [{ required: true, message: '新审批人不能为空', trigger: 'change' }], - reason: [{ required: true, message: '转派理由不能为空', trigger: 'blur' }] -}) - -const formRef = ref() // 表单 Ref -const userList = ref<any[]>([]) // 用户列表 - -/** 打开弹窗 */ -const open = async (id: string) => { - dialogVisible.value = true - resetForm() - formData.value.id = id - // 获得用户列表 - userList.value = await UserApi.getSimpleUserList() -} -defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗 - -/** 提交表单 */ -const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 -const submitForm = async () => { - // 校验表单 - if (!formRef) return - const valid = await formRef.value.validate() - if (!valid) return - // 提交请求 - formLoading.value = true - try { - await TaskApi.transferTask(formData.value) - dialogVisible.value = false - // 发送操作成功的事件 - emit('success') - } finally { - formLoading.value = false - } -} - -/** 重置表单 */ -const resetForm = () => { - formData.value = { - id: '', - assigneeUserId: undefined, - reason: '' - } - formRef.value?.resetFields() -} -</script> diff --git a/src/views/bpm/processInstance/detail/index.vue b/src/views/bpm/processInstance/detail/index.vue index 0461a20..9809f7a 100644 --- a/src/views/bpm/processInstance/detail/index.vue +++ b/src/views/bpm/processInstance/detail/index.vue @@ -49,7 +49,7 @@ class="form-box flex flex-col mb-30px flex-1" > <!-- 情况一:流程表单 --> - <el-col v-if="processDefinition?.formType === 10"> + <el-col v-if="processDefinition?.formType === BpmModelFormType.NORMAL"> <form-create v-model="detailForm.value" v-model:api="fApi" @@ -58,7 +58,7 @@ /> </el-col> <!-- 情况二:业务表单 --> - <div v-if="processDefinition?.formType === 20"> + <div v-if="processDefinition?.formType === BpmModelFormType.CUSTOM"> <BusinessFormComponent :id="processInstance.businessKey" /> </div> </div> @@ -116,6 +116,9 @@ :process-instance="processInstance" :process-definition="processDefinition" :userOptions="userOptions" + :normal-form="detailForm" + :normal-form-api="fApi" + :writable-fields="writableFields" @success="refresh" /> </div> @@ -126,7 +129,7 @@ <script lang="ts" setup> import { formatDate } from '@/utils/formatTime' import { DICT_TYPE } from '@/utils/dict' -import { BpmModelType } from '@/utils/constants' +import { BpmModelType, BpmModelFormType } from '@/utils/constants' import { setConfAndFields2 } from '@/utils/formCreate' import { registerComponent } from '@/utils/routerHelper' import type { ApiAttrs } from '@form-create/element-ui/types/config' @@ -171,6 +174,8 @@ value: {} }) // 流程实例的表单详情 +const writableFields: Array<string> = [] // 表单可以编辑的字段 + /** 获得详情 */ const getDetail = () => { getApprovalDetail() @@ -202,11 +207,12 @@ processDefinition.value = data.processDefinition // 设置表单信息 - if (processDefinition.value.formType === 10) { + if (processDefinition.value.formType === BpmModelFormType.NORMAL) { // 获取表单字段权限 const formFieldsPermission = data.formFieldsPermission - - if (detailForm.value.rule.length > 0) { + // 清空可编辑字段为空 + writableFields.splice(0) + if (detailForm.value.rule?.length > 0) { // 避免刷新 form-create 显示不了 detailForm.value.value = processInstance.value.formVariables } else { @@ -271,6 +277,8 @@ if (permission === FieldPermissionType.WRITE) { //@ts-ignore fApi.value?.disabled(false, field) + // 加入可以编辑的字段 + writableFields.push(field) } if (permission === FieldPermissionType.NONE) { //@ts-ignore @@ -314,6 +322,7 @@ overflow: auto; .form-scroll-area { + display: flex; height: calc( 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px - $process-header-height - 40px @@ -323,7 +332,6 @@ $process-header-height - 40px ); overflow: auto; - display: flex; flex-direction: column; :deep(.box-card) { diff --git a/src/views/bpm/processInstance/index.vue b/src/views/bpm/processInstance/index.vue index f2bb29d..2ffb162 100644 --- a/src/views/bpm/processInstance/index.vue +++ b/src/views/bpm/processInstance/index.vue @@ -1,5 +1,4 @@ <template> - <ContentWrap> <!-- 搜索工作栏 --> <el-form @@ -24,13 +23,14 @@ </el-form-item> <!-- TODO @ tuituji:style 可以使用 unocss --> - <el-form-item label="" prop="category" :style="{ position: 'absolute', right: '130px' }"> - <!-- TODO @tuituji:应该选择好分类,就触发搜索啦。 --> + <el-form-item label="" prop="category" :style="{ position: 'absolute', right: '300px' }"> + <!-- TODO @tuituji:应该选择好分类,就触发搜索啦。 RE:done & to check--> <el-select v-model="queryParams.category" placeholder="请选择流程分类" clearable class="!w-155px" + @change="handleQuery" > <el-option v-for="category in categoryList" @@ -41,21 +41,38 @@ </el-select> </el-form-item> + <el-form-item label="" prop="status" :style="{ position: 'absolute', right: '130px' }"> + <el-select + v-model="queryParams.status" + placeholder="请选择流程状态" + clearable + class="!w-155px" + @change="handleQuery" + > + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <!-- 高级筛选 --> <!-- TODO @ tuituji:style 可以使用 unocss --> <el-form-item :style="{ position: 'absolute', right: '0px' }"> - <el-button v-popover="popoverRef" v-click-outside="onClickOutside" :icon="List"> - 高级筛选 - </el-button> <el-popover - ref="popoverRef" - trigger="click" - virtual-triggering + :visible="showPopover" persistent :width="400" :show-arrow="false" placement="bottom-end" > + <template #reference> + <el-button @click="showPopover = !showPopover"> + <Icon icon="ep:plus" class="mr-5px" />高级筛选 + </el-button> + </template> <el-form-item label="流程发起人" class="bold-label" label-position="top" prop="category"> <el-select v-model="queryParams.category" @@ -85,21 +102,6 @@ class="!w-390px" /> </el-form-item> - <el-form-item label="流程状态" class="bold-label" label-position="top" prop="status"> - <el-select - v-model="queryParams.status" - placeholder="请选择流程状态" - clearable - class="!w-390px" - > - <el-option - v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)" - :key="dict.value" - :label="dict.label" - :value="dict.value" - /> - </el-select> - </el-form-item> <el-form-item label="发起时间" class="bold-label" label-position="top" prop="createTime"> <el-date-picker v-model="queryParams.createTime" @@ -111,8 +113,13 @@ class="!w-240px" /> </el-form-item> + <!-- TODO tuituiji:参考钉钉,1)按照清空、取消、确认排序。2)右对齐。3)确认增加 primary --> + <el-form-item class="bold-label" label-position="top"> + <el-button @click="handleQuery"> 确认</el-button> + <el-button @click="showPopover = false"> 取消</el-button> + <el-button @click="resetQuery"> 清空</el-button> + </el-form-item> </el-popover> - <!-- TODO @tuituji:这里应该有确认,和取消、清空搜索条件,三个按钮。 --> </el-form-item> </el-form> </ContentWrap> @@ -129,7 +136,7 @@ fixed="left" /> <!-- TODO @芋艿:摘要 --> - <!-- TODO @tuituji:流程状态。可见需求文档里 --> + <!-- TODO tuituiji:参考钉钉;1)审批中时,展示审批任务;2)非审批中,展示状态 --> <el-table-column label="流程状态" prop="status" width="120"> <template #default="scope"> <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.status" /> @@ -197,8 +204,7 @@ </ContentWrap> </template> <script lang="ts" setup> -// TODO @tuituji:List 改成 <Icon icon="ep:plus" class="mr-5px" /> 类似这种组件哈。 -import { List } from '@element-plus/icons-vue' +// TODO @tuituji:List 改成 <Icon icon="ep:plus" class="mr-5px" /> 类似这种组件哈。 RE:done & to check import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import { dateFormatter } from '@/utils/formatTime' import { ElMessageBox } from 'element-plus' @@ -240,6 +246,8 @@ } } +const showPopover = ref(false) + /** 搜索按钮操作 */ const handleQuery = () => { queryParams.pageNo = 1 @@ -272,7 +280,7 @@ } /** 查看详情 */ -const handleDetail = (row) => { +const handleDetail = (row: ProcessInstanceVO) => { router.push({ name: 'BpmProcessInstanceDetail', query: { @@ -282,7 +290,7 @@ } /** 取消按钮操作 */ -const handleCancel = async (row) => { +const handleCancel = async (row: ProcessInstanceVO) => { // 二次确认 const { value } = await ElMessageBox.prompt('请输入取消原因', '取消流程', { confirmButtonText: t('common.ok'), @@ -295,15 +303,6 @@ message.success('取消成功') // 刷新列表 await getList() -} - -// TODO @tuituji:这个 import 是不是没用哈? -import { ClickOutside as vClickOutside } from 'element-plus' - -// TODO @tuituji:onClickAdvancedSearch。方法名叫这个,会更好一些哇?打开高级搜索。 -const popoverRef = ref() -const onClickOutside = () => { - unref(popoverRef).popperRef?.delayHide?.() } /** 激活时 **/ diff --git a/src/views/bpm/task/done/index.vue b/src/views/bpm/task/done/index.vue index fdcef66..1365104 100644 --- a/src/views/bpm/task/done/index.vue +++ b/src/views/bpm/task/done/index.vue @@ -1,5 +1,4 @@ <template> - <ContentWrap> <!-- 搜索工作栏 --> <el-form @@ -9,7 +8,7 @@ class="-mb-15px" label-width="68px" > - <el-form-item label="任务名称" prop="name"> + <el-form-item label="" prop="name"> <el-input v-model="queryParams.name" class="!w-240px" @@ -18,27 +17,96 @@ @keyup.enter="handleQuery" /> </el-form-item> - <el-form-item label="创建时间" prop="createTime"> - <el-date-picker - v-model="queryParams.createTime" - :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" - class="!w-240px" - end-placeholder="结束日期" - start-placeholder="开始日期" - type="daterange" - value-format="YYYY-MM-DD HH:mm:ss" - /> - </el-form-item> <el-form-item> <el-button @click="handleQuery"> <Icon class="mr-5px" icon="ep:search" /> 搜索 </el-button> - <el-button @click="resetQuery"> - <Icon class="mr-5px" icon="ep:refresh" /> - 重置 - </el-button> </el-form-item> + + <el-form-item label="" prop="category" :style="{ position: 'absolute', right: '300px' }"> + <el-select + v-model="queryParams.category" + placeholder="请选择流程分类" + clearable + class="!w-155px" + @change="handleQuery" + > + <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="status" :style="{ position: 'absolute', right: '130px' }"> + <el-select + v-model="queryParams.status" + placeholder="请选择流程状态" + clearable + class="!w-155px" + @change="handleQuery" + > + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + + <!-- 高级筛选 --> + <el-form-item :style="{ position: 'absolute', right: '0px' }"> + <el-popover + :visible="showPopover" + persistent + :width="400" + :show-arrow="false" + placement="bottom-end" + > + <template #reference> + <el-button @click="showPopover = !showPopover" > + <Icon icon="ep:plus" class="mr-5px" />高级筛选 + </el-button> + + </template> + <el-form-item label="流程发起人" class="bold-label" label-position="top" prop="category"> + <el-select + v-model="queryParams.category" + placeholder="请选择流程发起人" + clearable + class="!w-390px" + > + <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="发起时间" class="bold-label" label-position="top" prop="createTime"> + <el-date-picker + v-model="queryParams.createTime" + value-format="YYYY-MM-DD HH:mm:ss" + type="daterange" + start-placeholder="开始日期" + end-placeholder="结束日期" + :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" + class="!w-240px" + /> + </el-form-item> + <el-form-item class="bold-label" label-position="top"> + <el-button @click="handleQuery"> 确认</el-button> + <el-button @click="showPopover = false"> 取消</el-button> + <el-button @click="resetQuery"> 清空</el-button> + </el-form-item> + </el-popover> + </el-form-item> + </el-form> </ContentWrap> @@ -103,9 +171,10 @@ </ContentWrap> </template> <script lang="ts" setup> -import { DICT_TYPE } from '@/utils/dict' +import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import { dateFormatter, formatPast2 } from '@/utils/formatTime' import * as TaskApi from '@/api/bpm/task' +import { CategoryApi, CategoryVO } from '@/api/bpm/category' defineOptions({ name: 'BpmTodoTask' }) @@ -118,9 +187,13 @@ pageNo: 1, pageSize: 10, name: '', + category: undefined, + status: undefined, createTime: [] }) const queryFormRef = ref() // 搜索的表单 +const categoryList = ref<CategoryVO[]>([]) // 流程分类列表 +const showPopover = ref(false) /** 查询任务列表 */ const getList = async () => { @@ -158,7 +231,8 @@ } /** 初始化 **/ -onMounted(() => { - getList() +onMounted(async () => { + await getList() + categoryList.value = await CategoryApi.getCategorySimpleList() }) </script> diff --git a/src/views/bpm/task/todo/index.vue b/src/views/bpm/task/todo/index.vue index 60681fc..e1449b1 100644 --- a/src/views/bpm/task/todo/index.vue +++ b/src/views/bpm/task/todo/index.vue @@ -1,5 +1,4 @@ <template> - <ContentWrap> <!-- 搜索工作栏 --> <el-form @@ -9,7 +8,7 @@ class="-mb-15px" label-width="68px" > - <el-form-item label="任务名称" prop="name"> + <el-form-item label="" prop="name"> <el-input v-model="queryParams.name" class="!w-240px" @@ -18,27 +17,79 @@ @keyup.enter="handleQuery" /> </el-form-item> - <el-form-item label="创建时间" prop="createTime"> - <el-date-picker - v-model="queryParams.createTime" - :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" - class="!w-240px" - end-placeholder="结束日期" - start-placeholder="开始日期" - type="daterange" - value-format="YYYY-MM-DD HH:mm:ss" - /> - </el-form-item> <el-form-item> <el-button @click="handleQuery"> <Icon class="mr-5px" icon="ep:search" /> 搜索 </el-button> - <el-button @click="resetQuery"> - <Icon class="mr-5px" icon="ep:refresh" /> - 重置 - </el-button> </el-form-item> + + <el-form-item label="" prop="category" :style="{ position: 'absolute', right: '130px' }"> + <el-select + v-model="queryParams.category" + placeholder="请选择流程分类" + clearable + class="!w-155px" + @change="handleQuery" + > + <el-option + v-for="category in categoryList" + :key="category.code" + :label="category.name" + :value="category.code" + /> + </el-select> + </el-form-item> + + <!-- 高级筛选 --> + <el-form-item :style="{ position: 'absolute', right: '0px' }"> + <el-popover + :visible="showPopover" + persistent + :width="400" + :show-arrow="false" + placement="bottom-end" + > + <template #reference> + <el-button @click="showPopover = !showPopover" > + <Icon icon="ep:plus" class="mr-5px" />高级筛选 + </el-button> + + </template> + <el-form-item label="流程发起人" class="bold-label" label-position="top" prop="category"> + <el-select + v-model="queryParams.category" + placeholder="请选择流程发起人" + clearable + class="!w-390px" + > + <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="发起时间" class="bold-label" label-position="top" prop="createTime"> + <el-date-picker + v-model="queryParams.createTime" + value-format="YYYY-MM-DD HH:mm:ss" + type="daterange" + start-placeholder="开始日期" + end-placeholder="结束日期" + :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" + class="!w-240px" + /> + </el-form-item> + <el-form-item class="bold-label" label-position="top"> + <el-button @click="handleQuery"> 确认</el-button> + <el-button @click="showPopover = false"> 取消</el-button> + <el-button @click="resetQuery"> 清空</el-button> + </el-form-item> + </el-popover> + </el-form-item> + </el-form> </ContentWrap> @@ -88,6 +139,7 @@ <script lang="ts" setup> import { dateFormatter } from '@/utils/formatTime' import * as TaskApi from '@/api/bpm/task' +import { CategoryApi, CategoryVO } from '@/api/bpm/category' defineOptions({ name: 'BpmTodoTask' }) @@ -100,9 +152,11 @@ pageNo: 1, pageSize: 10, name: '', + category: undefined, createTime: [] }) const queryFormRef = ref() // 搜索的表单 +const categoryList = ref<CategoryVO[]>([]) // 流程分类列表 /** 查询任务列表 */ const getList = async () => { @@ -115,6 +169,8 @@ loading.value = false } } + +const showPopover = ref(false) /** 搜索按钮操作 */ const handleQuery = () => { @@ -140,7 +196,8 @@ } /** 初始化 **/ -onMounted(() => { - getList() +onMounted(async () => { + await getList() + categoryList.value = await CategoryApi.getCategorySimpleList() }) </script> diff --git a/src/views/infra/config/index.vue b/src/views/infra/config/index.vue index e12e86d..0f48ede 100644 --- a/src/views/infra/config/index.vue +++ b/src/views/infra/config/index.vue @@ -79,17 +79,17 @@ <!-- 列表 --> <ContentWrap> <el-table v-loading="loading" :data="list"> - <el-table-column label="参数主键" align="center" prop="id" /> - <el-table-column label="参数分类" align="center" prop="category" /> + <el-table-column label="参数主键" align="center" prop="id" width="80"/> + <el-table-column label="参数分类" align="center" prop="category" width="80"/> <el-table-column label="参数名称" align="center" prop="name" :show-overflow-tooltip="true" /> - <el-table-column label="参数键名" align="center" prop="key" :show-overflow-tooltip="true" /> - <el-table-column label="参数键值" align="center" prop="value" /> - <el-table-column label="是否可见" align="center" prop="visible"> + <el-table-column label="参数键名" align="center" prop="key" :show-overflow-tooltip="true"/> + <el-table-column label="参数键值" align="center" prop="value" width="350"/> + <el-table-column label="是否可见" align="center" prop="visible" width="80"> <template #default="scope"> <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.visible" /> </template> </el-table-column> - <el-table-column label="系统内置" align="center" prop="type"> + <el-table-column label="系统内置" align="center" prop="type" width="80"> <template #default="scope"> <dict-tag :type="DICT_TYPE.INFRA_CONFIG_TYPE" :value="scope.row.type" /> </template> @@ -102,7 +102,7 @@ width="180" :formatter="dateFormatter" /> - <el-table-column label="操作" align="center"> + <el-table-column label="操作" align="center" width="120"> <template #default="scope"> <el-button link diff --git a/src/views/infra/dataSourceConfig/index.vue b/src/views/infra/dataSourceConfig/index.vue index 92bd301..3c1cdf6 100644 --- a/src/views/infra/dataSourceConfig/index.vue +++ b/src/views/infra/dataSourceConfig/index.vue @@ -18,10 +18,10 @@ <!-- 列表 --> <ContentWrap> <el-table v-loading="loading" :data="list"> - <el-table-column label="主键编号" align="center" prop="id" /> - <el-table-column label="数据源名称" align="center" prop="name" /> + <el-table-column label="主键编号" align="center" prop="id" width="100"/> + <el-table-column label="数据源名称" align="center" prop="name" width="100"/> <el-table-column label="数据源连接" align="center" prop="url" :show-overflow-tooltip="true" /> - <el-table-column label="用户名" align="center" prop="username" /> + <el-table-column label="用户名" align="center" prop="username" width="100"/> <el-table-column label="创建时间" align="center" @@ -29,7 +29,7 @@ width="180" :formatter="dateFormatter" /> - <el-table-column label="操作" align="center"> + <el-table-column label="操作" align="center" width="120"> <template #default="scope"> <el-button link diff --git a/src/views/system/app/AppForm.vue b/src/views/system/app/AppForm.vue index 4dd599f..151620e 100644 --- a/src/views/system/app/AppForm.vue +++ b/src/views/system/app/AppForm.vue @@ -51,24 +51,24 @@ <el-input v-model="formData.appDomain" placeholder="请输入应用域名" /> </el-form-item> </el-col> - <el-col :span="12"> - <el-form-item label="接口域名" prop="apiDomain"> - <el-input v-model="formData.apiDomain" placeholder="请输入接口域名" /> - </el-form-item> - </el-col> +<!-- <el-col :span="12">--> +<!-- <el-form-item label="接口域名" prop="apiDomain">--> +<!-- <el-input v-model="formData.apiDomain" placeholder="请输入接口域名" />--> +<!-- </el-form-item>--> +<!-- </el-col>--> </el-row> - <el-row> - <el-col :span="12"> - <el-form-item label="应用账号" prop="appKey"> - <el-input v-model="formData.appKey" placeholder="请输入应用账号" /> - </el-form-item> - </el-col> - <el-col :span="12"> - <el-form-item label="应用密码" prop="appSecret"> - <el-input v-model="formData.appSecret" placeholder="请输入应用密码" /> - </el-form-item> - </el-col> - </el-row> +<!-- <el-row>--> +<!-- <el-col :span="12">--> +<!-- <el-form-item label="应用账号" prop="appKey">--> +<!-- <el-input v-model="formData.appKey" placeholder="请输入应用账号" />--> +<!-- </el-form-item>--> +<!-- </el-col>--> +<!-- <el-col :span="12">--> +<!-- <el-form-item label="应用密码" prop="appSecret">--> +<!-- <el-input v-model="formData.appSecret" placeholder="请输入应用密码" />--> +<!-- </el-form-item>--> +<!-- </el-col>--> +<!-- </el-row>--> <el-row> <el-col :span="24"> <el-form-item label="应用图标" prop="icon"> diff --git a/src/views/system/app/index.vue b/src/views/system/app/index.vue index 8e67647..9b29a84 100644 --- a/src/views/system/app/index.vue +++ b/src/views/system/app/index.vue @@ -93,8 +93,8 @@ <el-table-column label="应用编号" align="center" prop="appCode" /> <el-table-column label="应用名称" align="center" prop="appName" /> <el-table-column label="应用域名" align="center" prop="appDomain" /> - <el-table-column label="接口域名" align="center" prop="apiDomain" /> - <el-table-column label="应用账号" align="center" prop="appKey" /> +<!-- <el-table-column label="接口域名" align="center" prop="apiDomain" />--> +<!-- <el-table-column label="应用账号" align="center" prop="appKey" />--> <el-table-column label="应用图标" align="center" prop="logo"> <template #default="scope"> <img width="40px" height="40px" :src="scope.row.icon" /> diff --git a/src/views/system/role/index.vue b/src/views/system/role/index.vue index 5a8cdb3..f945726 100644 --- a/src/views/system/role/index.vue +++ b/src/views/system/role/index.vue @@ -125,6 +125,16 @@ 菜单权限 </el-button> <el-button + v-hasPermi="['system:permission:assign-role-data-scope']" + link + preIcon="ep:coin" + title="数据权限" + type="primary" + @click="openDataPermissionForm(scope.row)" + > + 数据权限 + </el-button> + <el-button v-hasPermi="['system:role:delete']" link type="danger" diff --git a/stylelint.config.js b/stylelint.config.js index 890b45b..b336785 100644 --- a/stylelint.config.js +++ b/stylelint.config.js @@ -13,19 +13,19 @@ 'at-rule-no-unknown': [ true, { - ignoreAtRules: ['function', 'if', 'each', 'include', 'mixin'] + ignoreAtRules: ['function', 'if', 'each', 'include', 'mixin', 'extend'] } ], 'media-query-no-invalid': null, 'function-no-unknown': null, 'no-empty-source': null, 'named-grid-areas-no-invalid': null, - 'unicode-bom': 'never', + // 'unicode-bom': 'never', 'no-descending-specificity': null, 'font-family-no-missing-generic-family-keyword': null, - 'declaration-colon-space-after': 'always-single-line', - 'declaration-colon-space-before': 'never', - 'declaration-block-trailing-semicolon': null, + // 'declaration-colon-space-after': 'always-single-line', + // 'declaration-colon-space-before': 'never', + // 'declaration-block-trailing-semicolon': null, 'rule-empty-line-before': [ 'always', { diff --git a/tsconfig.json b/tsconfig.json index 182852a..38376ef 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,11 +24,11 @@ "@/*": ["src/*"] }, "types": [ - "@intlify/unplugin-vue-i18n/types", - "vite/client", - "element-plus/global", - "@types/qrcode", - "vite-plugin-svg-icons/client" + // "@intlify/unplugin-vue-i18n/types", + "vite/client" + // "element-plus/global", + // "@types/qrcode", + // "vite-plugin-svg-icons/client" ], "outDir": "target", // 请保留这个属性,防止tsconfig.json文件报错 "typeRoots": ["./node_modules/@types/", "./types"] diff --git a/types/router.d.ts b/types/router.d.ts index 9b08b80..03e91f1 100644 --- a/types/router.d.ts +++ b/types/router.d.ts @@ -15,6 +15,8 @@ title: 'title' 设置该路由在侧边栏和面包屑中展示的名字 + titleSuffix: '2' 当 path 和 title 重复时的后缀或备注 + icon: 'svg-name' 设置该路由的图标 noCache: true 如果设置为true,则不会被 <keep-alive> 缓存(默认 false) @@ -37,6 +39,7 @@ hidden?: boolean alwaysShow?: boolean title?: string + titleSuffix?: string icon?: string noCache?: boolean breadcrumb?: boolean diff --git a/uno.config.ts b/uno.config.ts index d146731..e52457a 100644 --- a/uno.config.ts +++ b/uno.config.ts @@ -12,7 +12,7 @@ ${selector} { display: flex; height: 100%; - padding: 1px 10px 0; + padding: 0 10px; cursor: pointer; align-items: center; transition: background var(--transition-time-02); diff --git a/vite.config.ts b/vite.config.ts index 640fe50..891e5f9 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -44,7 +44,8 @@ preprocessorOptions: { scss: { additionalData: '@use "@/styles/variables.scss" as *;', - javascriptEnabled: true + javascriptEnabled: true, + silenceDeprecations: ["legacy-js-api"], } } }, diff --git a/web-types.json b/web-types.json new file mode 100644 index 0000000..602f212 --- /dev/null +++ b/web-types.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json.schemastore.org/web-types", + "framework": "vue", + "name": "name written in package.json", + "version": "version written in package.json", + "contributions": { + "html": { + "types-syntax": "typescript", + "attributes": [ + { + "name": "v-hasPermi" + }, + { + "name": "v-hasRole" + } + ] + } + } +} -- Gitblit v1.9.3