潘志宝
4 天以前 ca22cdd5550cfa0defb0f430c538698182cdaec1
Merge branch 'master' of http://dlindusit.com:53929/r/iailab-plat-ui-vue3
已删除6个文件
已添加10个文件
已修改96个文件
5450 ■■■■■ 文件已修改
.env.dev 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.env.local 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.env.prod 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AppLinkInput/data.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ContentWrap/src/ContentWrap.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Crontab/src/Crontab.vue 66 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DictTag/src/DictTag.vue 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/ComponentContainer.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/ComponentContainerProperty.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/Carousel/config.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/Carousel/property.vue 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/CouponCard/property.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/Divider/property.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/FloatingActionButton/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/FloatingActionButton/property.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/MenuGrid/property.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/MenuSwiper/property.vue 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/NavigationBar/components/CellProperty.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/NavigationBar/property.vue 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/NoticeBar/config.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/Popover/property.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/ProductCard/index.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/ProductCard/property.vue 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/ProductList/index.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/ProductList/property.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/PromotionCombination/config.ts 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/PromotionCombination/index.vue 244 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/PromotionCombination/property.vue 86 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/PromotionPoint/config.ts 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/PromotionPoint/index.vue 202 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/PromotionPoint/property.vue 154 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/PromotionSeckill/config.ts 42 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/PromotionSeckill/index.vue 244 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/PromotionSeckill/property.vue 86 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/SearchBar/property.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/TabBar/config.ts 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/TabBar/property.vue 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/TitleBar/property.vue 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/UserCard/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Draggable/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/IFrame/src/IFrame.vue 33 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Icon/src/Icon.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Icon/src/IconSelect.vue 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/RouterSearch/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ShortcutDateRangePicker/index.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/node.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/nodes-config/ConditionNodeConfig.vue 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/UploadFile/src/UploadFile.vue 24 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/UploadFile/src/UploadImgs.vue 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/UploadFile/src/useUpload.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json 157 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/designer/plugins/palette/CustomPalette.js 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/designer/plugins/palette/paletteProvider.js 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/designer/plugins/translate/zh.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/custom-config/ElementCustomConfig.vue 284 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/custom-config/components/BoundaryEventTimer.vue 252 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/custom-config/components/UserTaskCustomConfig.vue 623 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/custom-config/data.ts 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/listeners/utilSelf.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue 139 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/properties/ElementProperties.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue 24 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/task/data.ts 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/task/task-components/CallActivity.vue 280 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/task/task-components/ServiceTask.vue 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue 235 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/directives/index.ts 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/directives/permission/hasPermi.ts 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/directives/permission/hasRole.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/web/useMessage.ts 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/UserInfo/src/UserInfo.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main.ts 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/app.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/permission.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/styles/global.module.scss 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/styles/index.scss 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/styles/var.css 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/formCreate.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/permission.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/tree.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Home/Index.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/form/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/model/CategoryDraggableModel.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/model/editor/index.vue 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/create/ProcessDefinitionDetail.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue 422 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/detail/dialog/TaskDelegateForm.vue 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/detail/dialog/TaskReturnForm.vue 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/detail/dialog/TaskSignCreateForm.vue 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/detail/dialog/TaskSignDeleteForm.vue 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/detail/dialog/TaskSignList.vue 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/detail/dialog/TaskTransferForm.vue 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/detail/index.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/index.vue 77 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/task/done/index.vue 114 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/task/todo/index.vue 95 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/infra/config/index.vue 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/infra/dataSourceConfig/index.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/app/AppForm.vue 34 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/app/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/role/index.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
stylelint.config.js 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tsconfig.json 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
types/router.d.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uno.config.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.ts 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web-types.json 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.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'
.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'
.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
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"
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'
      },
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>
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="范围">
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>
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;
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'">
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',
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
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>
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>
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
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">
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>
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>
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. 文字 -->
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'">
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: '',
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>
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' })
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'">
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' })
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>
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,
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>
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
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>
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>
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>
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,
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>
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
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>
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'
      }
    ]
  }
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)
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'">
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>
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
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>
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' })
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>
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
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"
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
}
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>
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')
})
// 表单内部门字段选项, 必须是必填和部门选择器
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>
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>
src/components/UploadFile/src/useUpload.ts
@@ -101,6 +101,4 @@
enum UPLOAD_TYPE {
  // 客户端直接上传(只支持S3服务)
  CLIENT = 'client',
  // 客户端发送到后端上传
  SERVER = 'server'
}
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": []
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',
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',
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': '创建数据存储',
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' })
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>
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>
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 }} &nbsp;<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>
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
  }
}
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
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)
  )
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'
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 }
)
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(
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 }
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]
}
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>
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>
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,
  () => {
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()
    }
  })
}
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
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
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'
        }
      )
    }
  }
}
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>
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)
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
    },
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: {
src/styles/global.module.scss
@@ -1,4 +1,4 @@
@import './variables.scss';
@use './variables.scss' as *;
// 导出变量
:export {
  namespace: $namespace;
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;
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;
}
src/utils/formCreate.ts
@@ -44,6 +44,7 @@
  value?: object
) => {
  if (isRef(detailPreview)) {
    // @ts-ignore
    detailPreview = detailPreview.value
  }
  // @ts-ignore
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)
    })
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}`
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
src/views/bpm/form/index.vue
@@ -1,5 +1,4 @@
<template>
  <ContentWrap>
    <!-- 搜索工作栏 -->
    <el-form
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>
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 属性
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
  }
}
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" />&nbsp;
          {{ 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">&nbsp; 取消后,该审批流程将自动结束</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>
src/views/bpm/processInstance/detail/dialog/TaskDelegateForm.vue
文件已删除
src/views/bpm/processInstance/detail/dialog/TaskReturnForm.vue
文件已删除
src/views/bpm/processInstance/detail/dialog/TaskSignCreateForm.vue
文件已删除
src/views/bpm/processInstance/detail/dialog/TaskSignDeleteForm.vue
文件已删除
src/views/bpm/processInstance/detail/dialog/TaskSignList.vue
文件已删除
src/views/bpm/processInstance/detail/dialog/TaskTransferForm.vue
文件已删除
src/views/bpm/processInstance/detail/index.vue
@@ -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) {
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?.()
}
/** 激活时 **/
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>
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>
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
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
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">
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" />
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"
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',
      {
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"]
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
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);
vite.config.ts
@@ -44,7 +44,8 @@
      preprocessorOptions: {
        scss: {
          additionalData: '@use "@/styles/variables.scss" as *;',
          javascriptEnabled: true
          javascriptEnabled: true,
          silenceDeprecations: ["legacy-js-api"],
        }
      }
    },
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"
        }
      ]
    }
  }
}