Merge branch 'master' of http://dlindusit.com:53929/r/iailab-plat-ui-vue3
已删除6个文件
已添加10个文件
已修改96个文件
| | |
| | | # 开发环境:本地只启动前端项目,依赖开发环境(后端、APP) |
| | | NODE_ENV=production |
| | | NODE_ENV=development |
| | | |
| | | VITE_DEV=true |
| | | |
| | | # 请求路径 |
| | | VITE_BASE_URL='http://localhost:48080' |
| | | VITE_BASE_URL='http://localhost' |
| | | |
| | | # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务 |
| | | VITE_UPLOAD_TYPE=server |
| | | # 上传路径 |
| | | VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload' |
| | | |
| | | # 接口地址 |
| | | VITE_API_URL=/admin-api |
| | |
| | | VITE_SOURCEMAP=true |
| | | |
| | | # 打包路径 |
| | | VITE_BASE_PATH=/plat |
| | | VITE_BASE_PATH=/plat/ |
| | | |
| | | # 输出路径 |
| | | VITE_OUT_DIR=dist |
| | | |
| | | # 公共静态文件路径 |
| | | VITE_STATIC_DIR=/ |
| | | VITE_STATIC_DIR=/plat/ |
| | | |
| | | # 商城H5会员端域名iai |
| | | VITE_MALL_H5_DOMAIN='http://' |
| | |
| | | VITE_APP_CAPTCHA_ENABLE=false |
| | | |
| | | # MDK模型上传路径 |
| | | MDK_UPLOAD_URL='http://localhost:48080/admin-api/model//pre/item/upload-model' |
| | | MDK_UPLOAD_URL='http://localhost/admin-api/model/pre/item/upload-model' |
| | |
| | | |
| | | # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务 |
| | | VITE_UPLOAD_TYPE=server |
| | | # 上传路径 |
| | | VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload' |
| | | |
| | | # 接口地址 |
| | | VITE_API_URL=/admin-api |
| | |
| | | VITE_SOURCEMAP=false |
| | | |
| | | # 打包路径 |
| | | VITE_BASE_PATH=/plat |
| | | VITE_BASE_PATH=/plat/ |
| | | |
| | | # 公共静态文件路径 |
| | | VITE_STATIC_DIR=/ |
| | | VITE_STATIC_DIR=/plat/ |
| | | |
| | | # 商城H5会员端域名 |
| | | VITE_MALL_H5_DOMAIN='http://localhost:3000' |
| | |
| | | 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 |
| | |
| | | VITE_BASE_PATH=/plat |
| | | |
| | | # 数据采集服务所在服务器,映射截图图片用 |
| | | VITE_VIDEO_CAMERA_DOMAIN='10.88.4.131' |
| | | VITE_VIDEO_CAMERA_DOMAIN='10.50.37.62' |
| | | |
| | | # 输出路径 |
| | | VITE_OUT_DIR=dist |
| | |
| | | "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", |
| | |
| | | "vue-types": "^5.1.1", |
| | | "vuedraggable": "^4.1.0", |
| | | "web-storage-cache": "^1.1.1", |
| | | "wujie-vue3": "^1.0.22", |
| | | "xml-js": "^1.6.11" |
| | | "xml-js": "^1.6.11", |
| | | "wujie-vue3": "^1.0.22" |
| | | }, |
| | | "devDependencies": { |
| | | "@commitlint/cli": "^19.0.1", |
| | |
| | | "@vitejs/plugin-vue": "^5.0.4", |
| | | "@vitejs/plugin-vue-jsx": "^3.1.0", |
| | | "autoprefixer": "^10.4.17", |
| | | "bpmn-js": "8.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", |
| | |
| | | "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", |
| | |
| | | "url": "https://xxxx" |
| | | }, |
| | | "homepage": "https://xxxx", |
| | | "web-types": "./web-types.json", |
| | | "engines": { |
| | | "node": ">= 16.0.0", |
| | | "pnpm": ">=8.6.0" |
| | |
| | | // 链接列表 |
| | | links: AppLink[] |
| | | } |
| | | |
| | | // APP 链接 |
| | | export interface AppLink { |
| | | // 链接名称 |
| | |
| | | ACTIVITY_COMBINATION, |
| | | // 秒杀活动 |
| | | ACTIVITY_SECKILL, |
| | | // 积分商城活动 |
| | | ACTIVITY_POINT, |
| | | // 文章详情 |
| | | ARTICLE_DETAIL, |
| | | // 优惠券详情 |
| | |
| | | type: APP_LINK_TYPE_ENUM.ACTIVITY_SECKILL |
| | | }, |
| | | { |
| | | name: '积分商城活动', |
| | | path: '/pages/activity/point/list', |
| | | type: APP_LINK_TYPE_ENUM.ACTIVITY_POINT |
| | | }, |
| | | { |
| | | name: '签到中心', |
| | | path: '/pages/app/sign' |
| | | }, |
| | |
| | | defineProps({ |
| | | title: propTypes.string.def(''), |
| | | message: propTypes.string.def(''), |
| | | bodyStyle: propTypes.object.def({ padding: '20px' }) |
| | | bodyStyle: propTypes.object.def({ padding: '10px' }) |
| | | }) |
| | | </script> |
| | | |
| | |
| | | <el-form> |
| | | <el-form-item label="类型"> |
| | | <el-radio-group v-model="cronValue.second.type"> |
| | | <el-radio-button label="0">任意值</el-radio-button> |
| | | <el-radio-button label="1">范围</el-radio-button> |
| | | <el-radio-button label="2">间隔</el-radio-button> |
| | | <el-radio-button label="3">指定</el-radio-button> |
| | | <el-radio-button value="0">任意值</el-radio-button> |
| | | <el-radio-button value="1">范围</el-radio-button> |
| | | <el-radio-button value="2">间隔</el-radio-button> |
| | | <el-radio-button value="3">指定</el-radio-button> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item v-if="cronValue.second.type == '1'" label="范围"> |
| | |
| | | <el-form> |
| | | <el-form-item label="类型"> |
| | | <el-radio-group v-model="cronValue.minute.type"> |
| | | <el-radio-button label="0">任意值</el-radio-button> |
| | | <el-radio-button label="1">范围</el-radio-button> |
| | | <el-radio-button label="2">间隔</el-radio-button> |
| | | <el-radio-button label="3">指定</el-radio-button> |
| | | <el-radio-button value="0">任意值</el-radio-button> |
| | | <el-radio-button value="1">范围</el-radio-button> |
| | | <el-radio-button value="2">间隔</el-radio-button> |
| | | <el-radio-button value="3">指定</el-radio-button> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item v-if="cronValue.minute.type == '1'" label="范围"> |
| | |
| | | <el-form> |
| | | <el-form-item label="类型"> |
| | | <el-radio-group v-model="cronValue.hour.type"> |
| | | <el-radio-button label="0">任意值</el-radio-button> |
| | | <el-radio-button label="1">范围</el-radio-button> |
| | | <el-radio-button label="2">间隔</el-radio-button> |
| | | <el-radio-button label="3">指定</el-radio-button> |
| | | <el-radio-button value="0">任意值</el-radio-button> |
| | | <el-radio-button value="1">范围</el-radio-button> |
| | | <el-radio-button value="2">间隔</el-radio-button> |
| | | <el-radio-button value="3">指定</el-radio-button> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item v-if="cronValue.hour.type == '1'" label="范围"> |
| | |
| | | <el-form> |
| | | <el-form-item label="类型"> |
| | | <el-radio-group v-model="cronValue.day.type"> |
| | | <el-radio-button label="0">任意值</el-radio-button> |
| | | <el-radio-button label="1">范围</el-radio-button> |
| | | <el-radio-button label="2">间隔</el-radio-button> |
| | | <el-radio-button label="3">指定</el-radio-button> |
| | | <el-radio-button label="4">本月最后一天</el-radio-button> |
| | | <el-radio-button label="5">不指定</el-radio-button> |
| | | <el-radio-button value="0">任意值</el-radio-button> |
| | | <el-radio-button value="1">范围</el-radio-button> |
| | | <el-radio-button value="2">间隔</el-radio-button> |
| | | <el-radio-button value="3">指定</el-radio-button> |
| | | <el-radio-button value="4">本月最后一天</el-radio-button> |
| | | <el-radio-button value="5">不指定</el-radio-button> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item v-if="cronValue.day.type == '1'" label="范围"> |
| | |
| | | <el-form> |
| | | <el-form-item label="类型"> |
| | | <el-radio-group v-model="cronValue.month.type"> |
| | | <el-radio-button label="0">任意值</el-radio-button> |
| | | <el-radio-button label="1">范围</el-radio-button> |
| | | <el-radio-button label="2">间隔</el-radio-button> |
| | | <el-radio-button label="3">指定</el-radio-button> |
| | | <el-radio-button value="0">任意值</el-radio-button> |
| | | <el-radio-button value="1">范围</el-radio-button> |
| | | <el-radio-button value="2">间隔</el-radio-button> |
| | | <el-radio-button value="3">指定</el-radio-button> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item v-if="cronValue.month.type == '1'" label="范围"> |
| | |
| | | <el-form> |
| | | <el-form-item label="类型"> |
| | | <el-radio-group v-model="cronValue.week.type"> |
| | | <el-radio-button label="0">任意值</el-radio-button> |
| | | <el-radio-button label="1">范围</el-radio-button> |
| | | <el-radio-button label="2">间隔</el-radio-button> |
| | | <el-radio-button label="3">指定</el-radio-button> |
| | | <el-radio-button label="4">本月最后一周</el-radio-button> |
| | | <el-radio-button label="5">不指定</el-radio-button> |
| | | <el-radio-button value="0">任意值</el-radio-button> |
| | | <el-radio-button value="1">范围</el-radio-button> |
| | | <el-radio-button value="2">间隔</el-radio-button> |
| | | <el-radio-button value="3">指定</el-radio-button> |
| | | <el-radio-button value="4">本月最后一周</el-radio-button> |
| | | <el-radio-button value="5">不指定</el-radio-button> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item v-if="cronValue.week.type == '1'" label="范围"> |
| | |
| | | <el-form> |
| | | <el-form-item label="类型"> |
| | | <el-radio-group v-model="cronValue.year.type"> |
| | | <el-radio-button label="-1">忽略</el-radio-button> |
| | | <el-radio-button label="0">任意值</el-radio-button> |
| | | <el-radio-button label="1">范围</el-radio-button> |
| | | <el-radio-button label="2">间隔</el-radio-button> |
| | | <el-radio-button label="3">指定</el-radio-button> |
| | | <el-radio-button value="-1">忽略</el-radio-button> |
| | | <el-radio-button value="0">任意值</el-radio-button> |
| | | <el-radio-button value="1">范围</el-radio-button> |
| | | <el-radio-button value="2">间隔</el-radio-button> |
| | | <el-radio-button value="3">指定</el-radio-button> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item v-if="cronValue.year.type == '1'" label="范围"> |
| | |
| | | <script lang="tsx"> |
| | | import { defineComponent, PropType, ref } from 'vue' |
| | | import { computed, defineComponent, PropType } from 'vue' |
| | | import { isHexColor } from '@/utils/color' |
| | | import { ElTag } from 'element-plus' |
| | | import { DictDataType, getDictOptions } from '@/utils/dict' |
| | | import { isArray, isBoolean, isNumber, isString } from '@/utils/is' |
| | | |
| | | export default defineComponent({ |
| | | name: 'DictTag', |
| | |
| | | required: true |
| | | }, |
| | | value: { |
| | | type: [String, Number, Boolean] as PropType<string | number | boolean>, |
| | | type: [String, Number, Boolean, Array], |
| | | required: true |
| | | }, |
| | | // 字符串分隔符 只有当 props.value 传入值为字符串时有效 |
| | | separator: { |
| | | type: String as PropType<string>, |
| | | default: ',' |
| | | }, |
| | | // 每个 tag 之间的间隔,默认为 5px,参考的 el-row 的 gutter |
| | | gutter: { |
| | | type: String as PropType<string>, |
| | | default: '5px' |
| | | } |
| | | }, |
| | | setup(props) { |
| | | const dictData = ref<DictDataType>() |
| | | const getDictObj = (dictType: string, value: string) => { |
| | | const dictOptions = getDictOptions(dictType) |
| | | dictOptions.forEach((dict: DictDataType) => { |
| | | if (dict.value === value) { |
| | | if (dict.colorType + '' === 'default') { |
| | | dict.colorType = 'info' |
| | | } |
| | | dictData.value = dict |
| | | } |
| | | }) |
| | | } |
| | | const rederDictTag = () => { |
| | | const valueArr: any = computed(() => { |
| | | // 1. 是 Number 类型和 Boolean 类型的情况 |
| | | if (isNumber(props.value) || isBoolean(props.value)) { |
| | | return [String(props.value)] |
| | | } |
| | | // 2. 是字符串(进一步判断是否有包含分隔符号 -> props.sepSymbol ) |
| | | else if (isString(props.value)) { |
| | | return props.value.split(props.separator) |
| | | } |
| | | // 3. 数组 |
| | | else if (isArray(props.value)) { |
| | | return props.value.map(String) |
| | | } |
| | | return [] |
| | | }) |
| | | const renderDictTag = () => { |
| | | if (!props.type) { |
| | | return null |
| | | } |
| | | // 解决自定义字典标签值为零时标签不渲染的问题 |
| | | if (props.value === undefined || props.value === null) { |
| | | if (props.value === undefined || props.value === null || props.value === '') { |
| | | return null |
| | | } |
| | | getDictObj(props.type, props.value.toString()) |
| | | // 添加标签的文字颜色为白色,解决自定义背景颜色时标签文字看不清的问题 |
| | | const dictOptions = getDictOptions(props.type) |
| | | |
| | | return ( |
| | | <ElTag |
| | | style={dictData.value?.cssClass ? 'color: #fff' : ''} |
| | | type={dictData.value?.colorType} |
| | | color={ |
| | | dictData.value?.cssClass && isHexColor(dictData.value?.cssClass) |
| | | ? dictData.value?.cssClass |
| | | : '' |
| | | } |
| | | disableTransitions={true} |
| | | <div |
| | | class="dict-tag" |
| | | style={{ |
| | | display: 'inline-flex', |
| | | gap: props.gutter, |
| | | justifyContent: 'center', |
| | | alignItems: 'center' |
| | | }} |
| | | > |
| | | {dictData.value?.label} |
| | | </ElTag> |
| | | {dictOptions.map((dict: DictDataType) => { |
| | | if (valueArr.value.includes(dict.value)) { |
| | | if (dict.colorType + '' === 'primary' || dict.colorType + '' === 'default') { |
| | | dict.colorType = '' |
| | | } |
| | | return ( |
| | | // 添加标签的文字颜色为白色,解决自定义背景颜色时标签文字看不清的问题 |
| | | <ElTag |
| | | style={dict?.cssClass ? 'color: #fff' : ''} |
| | | type={dict?.colorType || null} |
| | | color={dict?.cssClass && isHexColor(dict?.cssClass) ? dict?.cssClass : ''} |
| | | disableTransitions={true} |
| | | > |
| | | {dict?.label} |
| | | </ElTag> |
| | | ) |
| | | } |
| | | })} |
| | | </div> |
| | | ) |
| | | } |
| | | return () => rederDictTag() |
| | | return () => renderDictTag() |
| | | } |
| | | }) |
| | | </script> |
| | |
| | | width: 80px; |
| | | height: 25px; |
| | | font-size: 12px; |
| | | color: #6a6a6a; |
| | | line-height: 25px; |
| | | text-align: center; |
| | | background: #fff; |
| | |
| | | <el-form :model="formData" label-width="80px"> |
| | | <el-form-item label="组件背景" prop="bgType"> |
| | | <el-radio-group v-model="formData.bgType"> |
| | | <el-radio label="color">纯色</el-radio> |
| | | <el-radio label="img">图片</el-radio> |
| | | <el-radio value="color">纯色</el-radio> |
| | | <el-radio value="img">图片</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="选择颜色" prop="bgColor" v-if="formData.bgType === 'color'"> |
| | |
| | | autoplay: false, |
| | | interval: 3, |
| | | items: [ |
| | | { type: 'img', imgUrl: 'https://xxxx/banner-01.jpg', videoUrl: '' }, |
| | | { type: 'img', imgUrl: 'https://xxxx/banner-02.jpg', videoUrl: '' } |
| | | { type: 'img', imgUrl: 'https://static.iocoder.cn/mall/banner-01.jpg', videoUrl: '' }, |
| | | { type: 'img', imgUrl: 'https://static.iocoder.cn/mall/banner-02.jpg', videoUrl: '' } |
| | | ] as CarouselItemProperty[], |
| | | style: { |
| | | bgType: 'color', |
| | |
| | | <el-form-item label="样式" prop="type"> |
| | | <el-radio-group v-model="formData.type"> |
| | | <el-tooltip class="item" content="默认" placement="bottom"> |
| | | <el-radio-button label="default"> |
| | | <el-radio-button value="default"> |
| | | <Icon icon="system-uicons:carousel" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <el-tooltip class="item" content="卡片" placement="bottom"> |
| | | <el-radio-button label="card"> |
| | | <el-radio-button value="card"> |
| | | <Icon icon="ic:round-view-carousel" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | |
| | | </el-form-item> |
| | | <el-form-item label="指示器" prop="indicator"> |
| | | <el-radio-group v-model="formData.indicator"> |
| | | <el-radio label="dot">小圆点</el-radio> |
| | | <el-radio label="number">数字</el-radio> |
| | | <el-radio value="dot">小圆点</el-radio> |
| | | <el-radio value="number">数字</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="是否轮播" prop="autoplay"> |
| | |
| | | <template #default="{ element }"> |
| | | <el-form-item label="类型" prop="type" class="m-b-8px!" label-width="40px"> |
| | | <el-radio-group v-model="element.type"> |
| | | <el-radio label="img">图片</el-radio> |
| | | <el-radio label="video">视频</el-radio> |
| | | <el-radio value="img">图片</el-radio> |
| | | <el-radio value="video">视频</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item |
| | |
| | | <el-form-item label="列数" prop="type"> |
| | | <el-radio-group v-model="formData.columns"> |
| | | <el-tooltip class="item" content="一列" placement="bottom"> |
| | | <el-radio-button :label="1"> |
| | | <el-radio-button :value="1"> |
| | | <Icon icon="fluent:text-column-one-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <el-tooltip class="item" content="二列" placement="bottom"> |
| | | <el-radio-button :label="2"> |
| | | <el-radio-button :value="2"> |
| | | <Icon icon="fluent:text-column-two-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <el-tooltip class="item" content="三列" placement="bottom"> |
| | | <el-radio-button :label="3"> |
| | | <el-radio-button :value="3"> |
| | | <Icon icon="fluent:text-column-three-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | |
| | | :key="index" |
| | | :content="item.text" |
| | | > |
| | | <el-radio-button :label="item.type"> |
| | | <el-radio-button :value="item.type"> |
| | | <Icon :icon="item.icon" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | |
| | | <el-form-item label="左右边距" prop="paddingType"> |
| | | <el-radio-group v-model="formData!.paddingType"> |
| | | <el-tooltip content="无边距" placement="top"> |
| | | <el-radio-button label="none"> |
| | | <el-radio-button value="none"> |
| | | <Icon icon="tabler:box-padding" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <el-tooltip content="左右留边" placement="top"> |
| | | <el-radio-button label="horizontal"> |
| | | <el-radio-button value="horizontal"> |
| | | <Icon icon="vaadin:padding" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | |
| | | defineProps<{ property: FloatingActionButtonProperty }>() |
| | | |
| | | // 是否展开 |
| | | const expanded = ref(true) |
| | | const expanded = ref(false) |
| | | // 处理展开/折叠 |
| | | const handleToggleFab = () => { |
| | | expanded.value = !expanded.value |
| | |
| | | <el-card header="按钮配置" class="property-group" shadow="never"> |
| | | <el-form-item label="展开方向" prop="direction"> |
| | | <el-radio-group v-model="formData.direction"> |
| | | <el-radio label="vertical">垂直</el-radio> |
| | | <el-radio label="horizontal">水平</el-radio> |
| | | <el-radio value="vertical">垂直</el-radio> |
| | | <el-radio value="horizontal">水平</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="显示文字" prop="showText"> |
| | |
| | | <el-form label-width="80px" :model="formData" class="m-t-8px"> |
| | | <el-form-item label="每行数量" prop="column"> |
| | | <el-radio-group v-model="formData.column"> |
| | | <el-radio :label="3">3个</el-radio> |
| | | <el-radio :label="4">4个</el-radio> |
| | | <el-radio :value="3">3个</el-radio> |
| | | <el-radio :value="4">4个</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | |
| | |
| | | <el-form label-width="80px" :model="formData" class="m-t-8px"> |
| | | <el-form-item label="布局" prop="layout"> |
| | | <el-radio-group v-model="formData.layout"> |
| | | <el-radio label="iconText">图标+文字</el-radio> |
| | | <el-radio label="icon">仅图标</el-radio> |
| | | <el-radio value="iconText">图标+文字</el-radio> |
| | | <el-radio value="icon">仅图标</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="行数" prop="row"> |
| | | <el-radio-group v-model="formData.row"> |
| | | <el-radio :label="1">1行</el-radio> |
| | | <el-radio :label="2">2行</el-radio> |
| | | <el-radio :value="1">1行</el-radio> |
| | | <el-radio :value="2">2行</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="列数" prop="column"> |
| | | <el-radio-group v-model="formData.column"> |
| | | <el-radio :label="3">3列</el-radio> |
| | | <el-radio :label="4">4列</el-radio> |
| | | <el-radio :label="5">5列</el-radio> |
| | | <el-radio :value="3">3列</el-radio> |
| | | <el-radio :value="4">4列</el-radio> |
| | | <el-radio :value="5">5列</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | |
| | |
| | | <template v-if="selectedHotAreaIndex === cellIndex"> |
| | | <el-form-item label="类型" :prop="`cell[${cellIndex}].type`"> |
| | | <el-radio-group v-model="cell.type"> |
| | | <el-radio label="text">文字</el-radio> |
| | | <el-radio label="image">图片</el-radio> |
| | | <el-radio label="search">搜索框</el-radio> |
| | | <el-radio value="text">文字</el-radio> |
| | | <el-radio value="image">图片</el-radio> |
| | | <el-radio value="search">搜索框</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <!-- 1. 文字 --> |
| | |
| | | <el-form label-width="80px" :model="formData" :rules="rules"> |
| | | <el-form-item label="样式" prop="styleType"> |
| | | <el-radio-group v-model="formData!.styleType"> |
| | | <el-radio label="normal">标准</el-radio> |
| | | <el-radio value="normal">标准</el-radio> |
| | | <el-tooltip |
| | | content="沉侵式头部仅支持微信小程序、APP,建议页面第一个组件为图片展示类组件" |
| | | placement="top" |
| | | > |
| | | <el-radio label="inner">沉浸式</el-radio> |
| | | <el-radio value="inner">沉浸式</el-radio> |
| | | </el-tooltip> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="常驻显示" prop="alwaysShow" v-if="formData.styleType === 'inner'"> |
| | | <el-radio-group v-model="formData!.alwaysShow"> |
| | | <el-radio :label="false">关闭</el-radio> |
| | | <el-radio :value="false">关闭</el-radio> |
| | | <el-tooltip content="常驻显示关闭后,头部小组件将在页面滑动时淡入" placement="top"> |
| | | <el-radio :label="true">开启</el-radio> |
| | | <el-radio :value="true">开启</el-radio> |
| | | </el-tooltip> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="背景类型" prop="bgType"> |
| | | <el-radio-group v-model="formData.bgType"> |
| | | <el-radio label="color">纯色</el-radio> |
| | | <el-radio label="img">图片</el-radio> |
| | | <el-radio value="color">纯色</el-radio> |
| | | <el-radio value="img">图片</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="背景颜色" prop="bgColor" v-if="formData.bgType === 'color'"> |
| | |
| | | name: '公告栏', |
| | | icon: 'ep:bell', |
| | | property: { |
| | | iconUrl: 'http://xxxx/static/images/xinjian.png', |
| | | iconUrl: 'http://mall.yudao.iocoder.cn/static/images/xinjian.png', |
| | | contents: [ |
| | | { |
| | | text: '', |
| | |
| | | <el-form-item label="显示次数" :prop="`list[${index}].showType`"> |
| | | <el-radio-group v-model="element.showType"> |
| | | <el-tooltip content="只显示一次,下次打开时不显示" placement="bottom"> |
| | | <el-radio label="once">一次</el-radio> |
| | | <el-radio value="once">一次</el-radio> |
| | | </el-tooltip> |
| | | <el-tooltip content="每次打开时都会显示" placement="bottom"> |
| | | <el-radio label="always">不限</el-radio> |
| | | <el-radio value="always">不限</el-radio> |
| | | </el-tooltip> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | |
| | | class="text-16px" |
| | | :style="{ color: property.fields.price.color }" |
| | | > |
| | | ¥{{ spu.price }} |
| | | ¥{{ fenToYuan(spu.price as any) }} |
| | | </span> |
| | | <!-- 市场价 --> |
| | | <span |
| | | v-if="property.fields.marketPrice.show && spu.marketPrice" |
| | | class="ml-4px text-10px line-through" |
| | | :style="{ color: property.fields.marketPrice.color }" |
| | | >¥{{ spu.marketPrice }}</span |
| | | > |
| | | >¥{{ fenToYuan(spu.marketPrice) }} |
| | | </span> |
| | | </div> |
| | | <div class="text-12px"> |
| | | <!-- 销量 --> |
| | |
| | | <script setup lang="ts"> |
| | | import { ProductCardProperty } from './config' |
| | | import * as ProductSpuApi from '@/api/mall/product/spu' |
| | | import { fenToYuan } from '../../../../../utils' |
| | | |
| | | /** 商品卡片 */ |
| | | defineOptions({ name: 'ProductCard' }) |
| | |
| | | <el-form-item label="布局" prop="type"> |
| | | <el-radio-group v-model="formData.layoutType"> |
| | | <el-tooltip class="item" content="单列大图" placement="bottom"> |
| | | <el-radio-button label="oneColBigImg"> |
| | | <el-radio-button value="oneColBigImg"> |
| | | <Icon icon="fluent:text-column-one-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <el-tooltip class="item" content="单列小图" placement="bottom"> |
| | | <el-radio-button label="oneColSmallImg"> |
| | | <el-radio-button value="oneColSmallImg"> |
| | | <Icon icon="fluent:text-column-two-left-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <el-tooltip class="item" content="双列" placement="bottom"> |
| | | <el-radio-button label="twoCol"> |
| | | <el-radio-button value="twoCol"> |
| | | <Icon icon="fluent:text-column-two-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | |
| | | <el-card header="按钮" class="property-group" shadow="never"> |
| | | <el-form-item label="按钮类型" prop="btnBuy.type"> |
| | | <el-radio-group v-model="formData.btnBuy.type"> |
| | | <el-radio-button label="text">文字</el-radio-button> |
| | | <el-radio-button label="img">图片</el-radio-button> |
| | | <el-radio-button value="text">文字</el-radio-button> |
| | | <el-radio-button value="img">图片</el-radio-button> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <template v-if="formData.btnBuy.type === 'text'"> |
| | |
| | | class="text-12px" |
| | | :style="{ color: property.fields.price.color }" |
| | | > |
| | | ¥{{ spu.price }} |
| | | ¥{{ fenToYuan(spu.price) }} |
| | | </span> |
| | | </div> |
| | | </div> |
| | |
| | | <script setup lang="ts"> |
| | | import { ProductListProperty } from './config' |
| | | import * as ProductSpuApi from '@/api/mall/product/spu' |
| | | import { fenToYuan } from '@/utils' |
| | | |
| | | /** 商品栏 */ |
| | | defineOptions({ name: 'ProductList' }) |
| | |
| | | <el-form-item label="布局" prop="type"> |
| | | <el-radio-group v-model="formData.layoutType"> |
| | | <el-tooltip class="item" content="双列" placement="bottom"> |
| | | <el-radio-button label="twoCol"> |
| | | <el-radio-button value="twoCol"> |
| | | <Icon icon="fluent:text-column-two-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <el-tooltip class="item" content="三列" placement="bottom"> |
| | | <el-radio-button label="threeCol"> |
| | | <el-radio-button value="threeCol"> |
| | | <Icon icon="fluent:text-column-three-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <el-tooltip class="item" content="水平滑动" placement="bottom"> |
| | | <el-radio-button label="horizSwiper"> |
| | | <el-radio-button value="horizSwiper"> |
| | | <Icon icon="system-uicons:carousel" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | |
| | | /** 拼团属性 */ |
| | | export interface PromotionCombinationProperty { |
| | | // 布局类型:单列 | 三列 |
| | | layoutType: 'oneCol' | 'threeCol' |
| | | layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol' |
| | | // 商品字段 |
| | | fields: { |
| | | // 商品名称 |
| | | name: PromotionCombinationFieldProperty |
| | | // 商品简介 |
| | | introduction: PromotionCombinationFieldProperty |
| | | // 商品价格 |
| | | price: PromotionCombinationFieldProperty |
| | | // 市场价 |
| | | marketPrice: PromotionCombinationFieldProperty |
| | | // 商品销量 |
| | | salesCount: PromotionCombinationFieldProperty |
| | | // 商品库存 |
| | | stock: PromotionCombinationFieldProperty |
| | | } |
| | | // 角标 |
| | | badge: { |
| | | // 是否显示 |
| | | show: boolean |
| | | // 角标图片 |
| | | imgUrl: string |
| | | } |
| | | // 按钮 |
| | | btnBuy: { |
| | | // 类型:文字 | 图片 |
| | | type: 'text' | 'img' |
| | | // 文字 |
| | | text: string |
| | | // 文字按钮:背景渐变起始颜色 |
| | | bgBeginColor: string |
| | | // 文字按钮:背景渐变结束颜色 |
| | | bgEndColor: string |
| | | // 图片按钮:图片地址 |
| | | imgUrl: string |
| | | } |
| | | // 上圆角 |
| | |
| | | // 间距 |
| | | space: number |
| | | // 拼团活动编号 |
| | | activityId: number |
| | | activityIds: number[] |
| | | // 组件样式 |
| | | style: ComponentStyle |
| | | } |
| | |
| | | name: '拼团', |
| | | icon: 'mdi:account-group', |
| | | property: { |
| | | layoutType: 'oneCol', |
| | | layoutType: 'oneColBigImg', |
| | | fields: { |
| | | name: { show: true, color: '#000' }, |
| | | price: { show: true, color: '#ff3000' } |
| | | introduction: { show: true, color: '#999' }, |
| | | price: { show: true, color: '#ff3000' }, |
| | | marketPrice: { show: true, color: '#c4c4c4' }, |
| | | salesCount: { show: true, color: '#c4c4c4' }, |
| | | stock: { show: false, color: '#c4c4c4' } |
| | | }, |
| | | badge: { show: false, imgUrl: '' }, |
| | | btnBuy: { |
| | | type: 'text', |
| | | text: '去拼团', |
| | | bgBeginColor: '#FF6000', |
| | | bgEndColor: '#FE832A', |
| | | imgUrl: '' |
| | | }, |
| | | borderRadiusTop: 8, |
| | | borderRadiusBottom: 8, |
| | | space: 8, |
| | |
| | | <template> |
| | | <el-scrollbar class="z-1 min-h-30px" wrap-class="w-full" ref="containerRef"> |
| | | <!-- 商品网格 --> |
| | | <div :class="`box-content min-h-30px w-full flex flex-row flex-wrap`" ref="containerRef"> |
| | | <div |
| | | class="grid overflow-x-auto" |
| | | class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white" |
| | | :style="{ |
| | | gridGap: `${property.space}px`, |
| | | gridTemplateColumns, |
| | | width: scrollbarWidth |
| | | ...calculateSpace(index), |
| | | ...calculateWidth(), |
| | | borderTopLeftRadius: `${property.borderRadiusTop}px`, |
| | | borderTopRightRadius: `${property.borderRadiusTop}px`, |
| | | borderBottomLeftRadius: `${property.borderRadiusBottom}px`, |
| | | borderBottomRightRadius: `${property.borderRadiusBottom}px` |
| | | }" |
| | | v-for="(spu, index) in spuList" |
| | | :key="index" |
| | | > |
| | | <!-- 商品 --> |
| | | <!-- 角标 --> |
| | | <div v-if="property.badge.show" class="absolute left-0 top-0 z-1 items-center justify-center"> |
| | | <el-image fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" /> |
| | | </div> |
| | | <!-- 商品封面图 --> |
| | | <div |
| | | class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white" |
| | | :style="{ |
| | | borderTopLeftRadius: `${property.borderRadiusTop}px`, |
| | | borderTopRightRadius: `${property.borderRadiusTop}px`, |
| | | borderBottomLeftRadius: `${property.borderRadiusBottom}px`, |
| | | borderBottomRightRadius: `${property.borderRadiusBottom}px` |
| | | }" |
| | | v-for="(spu, index) in spuList" |
| | | :key="index" |
| | | :class="[ |
| | | 'h-140px', |
| | | { |
| | | 'w-full': property.layoutType !== 'oneColSmallImg', |
| | | 'w-140px': property.layoutType === 'oneColSmallImg' |
| | | } |
| | | ]" |
| | | > |
| | | <!-- 角标 --> |
| | | <el-image fit="cover" class="h-full w-full" :src="spu.picUrl" /> |
| | | </div> |
| | | <div |
| | | :class="[ |
| | | ' flex flex-col gap-8px p-8px box-border', |
| | | { |
| | | 'w-full': property.layoutType !== 'oneColSmallImg', |
| | | 'w-[calc(100%-140px-16px)]': property.layoutType === 'oneColSmallImg' |
| | | } |
| | | ]" |
| | | > |
| | | <!-- 商品名称 --> |
| | | <div |
| | | v-if="property.badge.show" |
| | | class="absolute left-0 top-0 z-1 items-center justify-center" |
| | | > |
| | | <el-image fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" /> |
| | | </div> |
| | | <!-- 商品封面图 --> |
| | | <el-image fit="cover" :src="spu.picUrl" :style="{ width: imageSize, height: imageSize }" /> |
| | | <div |
| | | v-if="property.fields.name.show" |
| | | :class="[ |
| | | 'flex flex-col gap-8px p-8px box-border', |
| | | 'text-14px ', |
| | | { |
| | | 'w-[calc(100%-64px)]': columns === 2, |
| | | 'w-full': columns === 3 |
| | | truncate: property.layoutType !== 'oneColSmallImg', |
| | | 'overflow-ellipsis line-clamp-2': property.layoutType === 'oneColSmallImg' |
| | | } |
| | | ]" |
| | | :style="{ color: property.fields.name.color }" |
| | | > |
| | | <!-- 商品名称 --> |
| | | <div |
| | | v-if="property.fields.name.show" |
| | | class="truncate text-12px" |
| | | :style="{ color: property.fields.name.color }" |
| | | {{ spu.name }} |
| | | </div> |
| | | <!-- 商品简介 --> |
| | | <div |
| | | v-if="property.fields.introduction.show" |
| | | class="truncate text-12px" |
| | | :style="{ color: property.fields.introduction.color }" |
| | | > |
| | | {{ spu.introduction }} |
| | | </div> |
| | | <div> |
| | | <!-- 价格 --> |
| | | <span |
| | | v-if="property.fields.price.show" |
| | | class="text-16px" |
| | | :style="{ color: property.fields.price.color }" |
| | | > |
| | | {{ spu.name }} |
| | | </div> |
| | | <div> |
| | | <!-- 商品价格 --> |
| | | <span |
| | | v-if="property.fields.price.show" |
| | | class="text-12px" |
| | | :style="{ color: property.fields.price.color }" |
| | | > |
| | | ¥{{ spu.price }} |
| | | </span> |
| | | </div> |
| | | ¥{{ fenToYuan(spu.price || Infinity) }} |
| | | </span> |
| | | <!-- 市场价 --> |
| | | <span |
| | | v-if="property.fields.marketPrice.show && spu.marketPrice" |
| | | class="ml-4px text-10px line-through" |
| | | :style="{ color: property.fields.marketPrice.color }" |
| | | >¥{{ fenToYuan(spu.marketPrice) }}</span |
| | | > |
| | | </div> |
| | | <div class="text-12px"> |
| | | <!-- 销量 --> |
| | | <span |
| | | v-if="property.fields.salesCount.show" |
| | | :style="{ color: property.fields.salesCount.color }" |
| | | > |
| | | 已售{{ (spu.salesCount || 0) + (spu.virtualSalesCount || 0) }}件 |
| | | </span> |
| | | <!-- 库存 --> |
| | | <span v-if="property.fields.stock.show" :style="{ color: property.fields.stock.color }"> |
| | | 库存{{ spu.stock || 0 }} |
| | | </span> |
| | | </div> |
| | | </div> |
| | | <!-- 购买按钮 --> |
| | | <div class="absolute bottom-8px right-8px"> |
| | | <!-- 文字按钮 --> |
| | | <span |
| | | v-if="property.btnBuy.type === 'text'" |
| | | class="rounded-full p-x-12px p-y-4px text-12px text-white" |
| | | :style="{ |
| | | background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}` |
| | | }" |
| | | > |
| | | {{ property.btnBuy.text }} |
| | | </span> |
| | | <!-- 图片按钮 --> |
| | | <el-image |
| | | v-else |
| | | class="h-28px w-28px rounded-full" |
| | | fit="cover" |
| | | :src="property.btnBuy.imgUrl" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </el-scrollbar> |
| | | </div> |
| | | </template> |
| | | <script setup lang="ts"> |
| | | import { PromotionCombinationProperty } from './config' |
| | | import * as ProductSpuApi from '@/api/mall/product/spu' |
| | | import * as CombinationActivityApi from '@/api/mall/promotion/combination/combinationActivity' |
| | | import { fenToYuan } from '@/utils' |
| | | |
| | | /** 拼团 */ |
| | | /** 拼团卡片 */ |
| | | defineOptions({ name: 'PromotionCombination' }) |
| | | // 定义属性 |
| | | const props = defineProps<{ property: PromotionCombinationProperty }>() |
| | | // 商品列表 |
| | | const spuList = ref<ProductSpuApi.Spu[]>([]) |
| | | const spuIdList = ref<number[]>([]) |
| | | const combinationActivityList = ref<CombinationActivityApi.CombinationActivityVO[]>([]) |
| | | |
| | | watch( |
| | | () => props.property.activityId, |
| | | () => props.property.activityIds, |
| | | async () => { |
| | | if (!props.property.activityId) return |
| | | const activity = await CombinationActivityApi.getCombinationActivity(props.property.activityId) |
| | | if (!activity?.spuId) return |
| | | spuList.value = [await ProductSpuApi.getSpu(activity.spuId)] |
| | | try { |
| | | // 新添加的拼团组件,是没有活动ID的 |
| | | const activityIds = props.property.activityIds |
| | | // 检查活动ID的有效性 |
| | | if (Array.isArray(activityIds) && activityIds.length > 0) { |
| | | // 获取拼团活动详情列表 |
| | | combinationActivityList.value = |
| | | await CombinationActivityApi.getCombinationActivityListByIds(activityIds) |
| | | |
| | | // 获取拼团活动的 SPU 详情列表 |
| | | spuList.value = [] |
| | | spuIdList.value = combinationActivityList.value |
| | | .map((activity) => activity.spuId) |
| | | .filter((spuId): spuId is number => typeof spuId === 'number') |
| | | if (spuIdList.value.length > 0) { |
| | | spuList.value = await ProductSpuApi.getSpuDetailList(spuIdList.value) |
| | | } |
| | | |
| | | // 更新 SPU 的最低价格 |
| | | combinationActivityList.value.forEach((activity) => { |
| | | // 匹配spuId |
| | | const spu = spuList.value.find((spu) => spu.id === activity.spuId) |
| | | if (spu) { |
| | | // 赋值活动价格,哪个最便宜就赋值哪个 |
| | | spu.price = Math.min(activity.combinationPrice || Infinity, spu.price || Infinity) |
| | | } |
| | | }) |
| | | } |
| | | } catch (error) { |
| | | console.error('获取拼团活动细节或 SPU 细节时出错:', error) |
| | | } |
| | | }, |
| | | { |
| | | immediate: true, |
| | | deep: true |
| | | } |
| | | ) |
| | | // 手机宽度 |
| | | const phoneWidth = ref(375) |
| | | |
| | | /** |
| | | * 计算商品的间距 |
| | | * @param index 商品索引 |
| | | */ |
| | | const calculateSpace = (index: number) => { |
| | | // 商品的列数 |
| | | const columns = props.property.layoutType === 'twoCol' ? 2 : 1 |
| | | // 第一列没有左边距 |
| | | const marginLeft = index % columns === 0 ? '0' : props.property.space + 'px' |
| | | // 第一行没有上边距 |
| | | const marginTop = index < columns ? '0' : props.property.space + 'px' |
| | | |
| | | return { marginLeft, marginTop } |
| | | } |
| | | |
| | | // 容器 |
| | | const containerRef = ref() |
| | | // 商品的列数 |
| | | const columns = ref(2) |
| | | // 滚动条宽度 |
| | | const scrollbarWidth = ref('100%') |
| | | // 商品图大小 |
| | | const imageSize = ref('0') |
| | | // 商品网络列数 |
| | | const gridTemplateColumns = ref('') |
| | | // 计算布局参数 |
| | | watch( |
| | | () => [props.property, phoneWidth, spuList.value.length], |
| | | () => { |
| | | // 计算列数 |
| | | columns.value = props.property.layoutType === 'oneCol' ? 1 : 3 |
| | | // 每列的宽度为:(总宽度 - 间距 * (列数 - 1))/ 列数 |
| | | const productWidth = |
| | | (phoneWidth.value - props.property.space * (columns.value - 1)) / columns.value |
| | | // 商品图布局:2列时,左右布局 3列时,上下布局 |
| | | imageSize.value = columns.value === 2 ? '64px' : `${productWidth}px` |
| | | // 指定列数 |
| | | gridTemplateColumns.value = `repeat(${columns.value}, auto)` |
| | | // 不滚动 |
| | | scrollbarWidth.value = '100%' |
| | | }, |
| | | { immediate: true, deep: true } |
| | | ) |
| | | onMounted(() => { |
| | | // 提取手机宽度 |
| | | phoneWidth.value = containerRef.value?.wrapRef?.offsetWidth || 375 |
| | | }) |
| | | // 计算商品的宽度 |
| | | const calculateWidth = () => { |
| | | let width = '100%' |
| | | // 双列时每列的宽度为:(总宽度 - 间距)/ 2 |
| | | if (props.property.layoutType === 'twoCol') { |
| | | width = `${(containerRef.value.offsetWidth - props.property.space) / 2}px` |
| | | } |
| | | return { width } |
| | | } |
| | | </script> |
| | | |
| | | <style scoped lang="scss"></style> |
| | |
| | | <ComponentContainerProperty v-model="formData.style"> |
| | | <el-form label-width="80px" :model="formData"> |
| | | <el-card header="拼团活动" class="property-group" shadow="never"> |
| | | <el-form-item label="拼团活动" prop="activityId"> |
| | | <el-select v-model="formData.activityId"> |
| | | <el-option |
| | | v-for="activity in activityList" |
| | | :key="activity.id" |
| | | :label="activity.name" |
| | | :value="activity.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <CombinationShowcase v-model="formData.activityIds" /> |
| | | </el-card> |
| | | <el-card header="商品样式" class="property-group" shadow="never"> |
| | | <el-form-item label="布局" prop="type"> |
| | | <el-radio-group v-model="formData.layoutType"> |
| | | <el-tooltip class="item" content="单列" placement="bottom"> |
| | | <el-radio-button label="oneCol"> |
| | | <el-tooltip class="item" content="单列大图" placement="bottom"> |
| | | <el-radio-button value="oneColBigImg"> |
| | | <Icon icon="fluent:text-column-one-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <el-tooltip class="item" content="三列" placement="bottom"> |
| | | <el-radio-button label="threeCol"> |
| | | <Icon icon="fluent:text-column-three-24-filled" /> |
| | | <el-tooltip class="item" content="单列小图" placement="bottom"> |
| | | <el-radio-button value="oneColSmallImg"> |
| | | <Icon icon="fluent:text-column-two-left-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <el-tooltip class="item" content="双列" placement="bottom"> |
| | | <el-radio-button value="twoCol"> |
| | | <Icon icon="fluent:text-column-two-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <!--<el-tooltip class="item" content="三列" placement="bottom"> |
| | | <el-radio-button value="threeCol"> |
| | | <Icon icon="fluent:text-column-three-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip>--> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="商品名称" prop="fields.name.show"> |
| | |
| | | <el-checkbox v-model="formData.fields.name.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="商品简介" prop="fields.introduction.show"> |
| | | <div class="flex gap-8px"> |
| | | <ColorInput v-model="formData.fields.introduction.color" /> |
| | | <el-checkbox v-model="formData.fields.introduction.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="商品价格" prop="fields.price.show"> |
| | | <div class="flex gap-8px"> |
| | | <ColorInput v-model="formData.fields.price.color" /> |
| | | <el-checkbox v-model="formData.fields.price.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="市场价" prop="fields.marketPrice.show"> |
| | | <div class="flex gap-8px"> |
| | | <ColorInput v-model="formData.fields.marketPrice.color" /> |
| | | <el-checkbox v-model="formData.fields.marketPrice.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="商品销量" prop="fields.salesCount.show"> |
| | | <div class="flex gap-8px"> |
| | | <ColorInput v-model="formData.fields.salesCount.color" /> |
| | | <el-checkbox v-model="formData.fields.salesCount.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="商品库存" prop="fields.stock.show"> |
| | | <div class="flex gap-8px"> |
| | | <ColorInput v-model="formData.fields.stock.color" /> |
| | | <el-checkbox v-model="formData.fields.stock.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | </el-card> |
| | |
| | | </el-form-item> |
| | | <el-form-item label="角标" prop="badge.imgUrl" v-if="formData.badge.show"> |
| | | <UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px"> |
| | | <template #tip> 建议尺寸:36 * 22 </template> |
| | | <template #tip> 建议尺寸:36 * 22</template> |
| | | </UploadImg> |
| | | </el-form-item> |
| | | </el-card> |
| | | <el-card header="按钮" class="property-group" shadow="never"> |
| | | <el-form-item label="按钮类型" prop="btnBuy.type"> |
| | | <el-radio-group v-model="formData.btnBuy.type"> |
| | | <el-radio-button value="text">文字</el-radio-button> |
| | | <el-radio-button value="img">图片</el-radio-button> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <template v-if="formData.btnBuy.type === 'text'"> |
| | | <el-form-item label="按钮文字" prop="btnBuy.text"> |
| | | <el-input v-model="formData.btnBuy.text" /> |
| | | </el-form-item> |
| | | <el-form-item label="左侧背景" prop="btnBuy.bgBeginColor"> |
| | | <ColorInput v-model="formData.btnBuy.bgBeginColor" /> |
| | | </el-form-item> |
| | | <el-form-item label="右侧背景" prop="btnBuy.bgEndColor"> |
| | | <ColorInput v-model="formData.btnBuy.bgEndColor" /> |
| | | </el-form-item> |
| | | </template> |
| | | <template v-else> |
| | | <el-form-item label="图片" prop="btnBuy.imgUrl"> |
| | | <UploadImg v-model="formData.btnBuy.imgUrl" height="56px" width="56px"> |
| | | <template #tip> 建议尺寸:56 * 56</template> |
| | | </UploadImg> |
| | | </el-form-item> |
| | | </template> |
| | | </el-card> |
| | | <el-card header="商品样式" class="property-group" shadow="never"> |
| | | <el-form-item label="上圆角" prop="borderRadiusTop"> |
| | |
| | | import { usePropertyForm } from '@/components/DiyEditor/util' |
| | | import * as CombinationActivityApi from '@/api/mall/promotion/combination/combinationActivity' |
| | | import { CommonStatusEnum } from '@/utils/constants' |
| | | import CombinationShowcase from '@/views/mall/promotion/combination/components/CombinationShowcase.vue' |
| | | |
| | | // 拼团属性面板 |
| | | defineOptions({ name: 'PromotionCombinationProperty' }) |
| | |
| | | const emit = defineEmits(['update:modelValue']) |
| | | const { formData } = usePropertyForm(props.modelValue, emit) |
| | | // 活动列表 |
| | | const activityList = ref<CombinationActivityApi.CombinationActivityVO>([]) |
| | | const activityList = ref<CombinationActivityApi.CombinationActivityVO[]>([]) |
| | | onMounted(async () => { |
| | | const { list } = await CombinationActivityApi.getCombinationActivityPage({ |
| | | status: CommonStatusEnum.ENABLE |
对比新文件 |
| | |
| | | import {ComponentStyle, DiyComponent} from '@/components/DiyEditor/util' |
| | | |
| | | /** 积分商城属性 */ |
| | | export interface PromotionPointProperty { |
| | | // 布局类型:单列 | 三列 |
| | | layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol' |
| | | // 商品字段 |
| | | fields: { |
| | | // 商品名称 |
| | | name: PromotionPointFieldProperty |
| | | // 商品简介 |
| | | introduction: PromotionPointFieldProperty |
| | | // 商品价格 |
| | | price: PromotionPointFieldProperty |
| | | // 市场价 |
| | | marketPrice: PromotionPointFieldProperty |
| | | // 商品销量 |
| | | salesCount: PromotionPointFieldProperty |
| | | // 商品库存 |
| | | stock: PromotionPointFieldProperty |
| | | } |
| | | // 角标 |
| | | badge: { |
| | | // 是否显示 |
| | | show: boolean |
| | | // 角标图片 |
| | | imgUrl: string |
| | | } |
| | | // 按钮 |
| | | btnBuy: { |
| | | // 类型:文字 | 图片 |
| | | type: 'text' | 'img' |
| | | // 文字 |
| | | text: string |
| | | // 文字按钮:背景渐变起始颜色 |
| | | bgBeginColor: string |
| | | // 文字按钮:背景渐变结束颜色 |
| | | bgEndColor: string |
| | | // 图片按钮:图片地址 |
| | | imgUrl: string |
| | | } |
| | | // 上圆角 |
| | | borderRadiusTop: number |
| | | // 下圆角 |
| | | borderRadiusBottom: number |
| | | // 间距 |
| | | space: number |
| | | // 秒杀活动编号 |
| | | activityIds: number[] |
| | | // 组件样式 |
| | | style: ComponentStyle |
| | | } |
| | | |
| | | // 商品字段 |
| | | export interface PromotionPointFieldProperty { |
| | | // 是否显示 |
| | | show: boolean |
| | | // 颜色 |
| | | color: string |
| | | } |
| | | |
| | | // 定义组件 |
| | | export const component = { |
| | | id: 'PromotionPoint', |
| | | name: '积分商城', |
| | | icon: 'ep:present', |
| | | property: { |
| | | layoutType: 'oneColBigImg', |
| | | fields: { |
| | | name: { show: true, color: '#000' }, |
| | | introduction: { show: true, color: '#999' }, |
| | | price: { show: true, color: '#ff3000' }, |
| | | marketPrice: { show: true, color: '#c4c4c4' }, |
| | | salesCount: { show: true, color: '#c4c4c4' }, |
| | | stock: { show: false, color: '#c4c4c4' } |
| | | }, |
| | | badge: { show: false, imgUrl: '' }, |
| | | btnBuy: { |
| | | type: 'text', |
| | | text: '立即兑换', |
| | | bgBeginColor: '#FF6000', |
| | | bgEndColor: '#FE832A', |
| | | imgUrl: '' |
| | | }, |
| | | borderRadiusTop: 8, |
| | | borderRadiusBottom: 8, |
| | | space: 8, |
| | | style: { |
| | | bgType: 'color', |
| | | bgColor: '', |
| | | marginLeft: 8, |
| | | marginRight: 8, |
| | | marginBottom: 8 |
| | | } as ComponentStyle |
| | | } |
| | | } as DiyComponent<PromotionPointProperty> |
对比新文件 |
| | |
| | | <template> |
| | | <div ref="containerRef" :class="`box-content min-h-30px w-full flex flex-row flex-wrap`"> |
| | | <div |
| | | v-for="(spu, index) in spuList" |
| | | :key="index" |
| | | :style="{ |
| | | ...calculateSpace(index), |
| | | ...calculateWidth(), |
| | | borderTopLeftRadius: `${property.borderRadiusTop}px`, |
| | | borderTopRightRadius: `${property.borderRadiusTop}px`, |
| | | borderBottomLeftRadius: `${property.borderRadiusBottom}px`, |
| | | borderBottomRightRadius: `${property.borderRadiusBottom}px` |
| | | }" |
| | | class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white" |
| | | > |
| | | <!-- 角标 --> |
| | | <div v-if="property.badge.show" class="absolute left-0 top-0 z-1 items-center justify-center"> |
| | | <el-image :src="property.badge.imgUrl" class="h-26px w-38px" fit="cover" /> |
| | | </div> |
| | | <!-- 商品封面图 --> |
| | | <div |
| | | :class="[ |
| | | 'h-140px', |
| | | { |
| | | 'w-full': property.layoutType !== 'oneColSmallImg', |
| | | 'w-140px': property.layoutType === 'oneColSmallImg' |
| | | } |
| | | ]" |
| | | > |
| | | <el-image :src="spu.picUrl" class="h-full w-full" fit="cover" /> |
| | | </div> |
| | | <div |
| | | :class="[ |
| | | ' flex flex-col gap-8px p-8px box-border', |
| | | { |
| | | 'w-full': property.layoutType !== 'oneColSmallImg', |
| | | 'w-[calc(100%-140px-16px)]': property.layoutType === 'oneColSmallImg' |
| | | } |
| | | ]" |
| | | > |
| | | <!-- 商品名称 --> |
| | | <div |
| | | v-if="property.fields.name.show" |
| | | :class="[ |
| | | 'text-14px ', |
| | | { |
| | | truncate: property.layoutType !== 'oneColSmallImg', |
| | | 'overflow-ellipsis line-clamp-2': property.layoutType === 'oneColSmallImg' |
| | | } |
| | | ]" |
| | | :style="{ color: property.fields.name.color }" |
| | | > |
| | | {{ spu.name }} |
| | | </div> |
| | | <!-- 商品简介 --> |
| | | <div |
| | | v-if="property.fields.introduction.show" |
| | | :style="{ color: property.fields.introduction.color }" |
| | | class="truncate text-12px" |
| | | > |
| | | {{ spu.introduction }} |
| | | </div> |
| | | <div> |
| | | <!-- 积分 --> |
| | | <span |
| | | v-if="property.fields.price.show" |
| | | :style="{ color: property.fields.price.color }" |
| | | class="text-16px" |
| | | > |
| | | {{ spu.point }}积分 |
| | | {{ !spu.pointPrice || spu.pointPrice === 0 ? '' : `+${fenToYuan(spu.pointPrice)}元` }} |
| | | </span> |
| | | <!-- 市场价 --> |
| | | <span |
| | | v-if="property.fields.marketPrice.show && spu.marketPrice" |
| | | :style="{ color: property.fields.marketPrice.color }" |
| | | class="ml-4px text-10px line-through" |
| | | > |
| | | ¥{{ fenToYuan(spu.marketPrice) }} |
| | | </span> |
| | | </div> |
| | | <div class="text-12px"> |
| | | <!-- 销量 --> |
| | | <span |
| | | v-if="property.fields.salesCount.show" |
| | | :style="{ color: property.fields.salesCount.color }" |
| | | > |
| | | 已兑{{ (spu.pointTotalStock || 0) - (spu.pointStock || 0) }}件 |
| | | </span> |
| | | <!-- 库存 --> |
| | | <span v-if="property.fields.stock.show" :style="{ color: property.fields.stock.color }"> |
| | | 库存{{ spu.pointTotalStock || 0 }} |
| | | </span> |
| | | </div> |
| | | </div> |
| | | <!-- 购买按钮 --> |
| | | <div class="absolute bottom-8px right-8px"> |
| | | <!-- 文字按钮 --> |
| | | <span |
| | | v-if="property.btnBuy.type === 'text'" |
| | | :style="{ |
| | | background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}` |
| | | }" |
| | | class="rounded-full p-x-12px p-y-4px text-12px text-white" |
| | | > |
| | | {{ property.btnBuy.text }} |
| | | </span> |
| | | <!-- 图片按钮 --> |
| | | <el-image |
| | | v-else |
| | | :src="property.btnBuy.imgUrl" |
| | | class="h-28px w-28px rounded-full" |
| | | fit="cover" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <script lang="ts" setup> |
| | | import { PromotionPointProperty } from './config' |
| | | import * as ProductSpuApi from '@/api/mall/product/spu' |
| | | import { PointActivityApi, PointActivityVO, SpuExtension0 } from '@/api/mall/promotion/point' |
| | | import { fenToYuan } from '@/utils' |
| | | |
| | | /** 积分商城卡片 */ |
| | | defineOptions({ name: 'PromotionPoint' }) |
| | | // 定义属性 |
| | | const props = defineProps<{ property: PromotionPointProperty }>() |
| | | // 商品列表 |
| | | const spuList = ref<SpuExtension0[]>([]) |
| | | const spuIdList = ref<number[]>([]) |
| | | const pointActivityList = ref<PointActivityVO[]>([]) |
| | | |
| | | watch( |
| | | () => props.property.activityIds, |
| | | async () => { |
| | | try { |
| | | // 新添加的积分商城组件,是没有活动ID的 |
| | | const activityIds = props.property.activityIds |
| | | // 检查活动ID的有效性 |
| | | if (Array.isArray(activityIds) && activityIds.length > 0) { |
| | | // 获取积分商城活动详情列表 |
| | | pointActivityList.value = await PointActivityApi.getPointActivityListByIds(activityIds) |
| | | |
| | | // 获取积分商城活动的 SPU 详情列表 |
| | | spuList.value = [] |
| | | spuIdList.value = pointActivityList.value.map((activity) => activity.spuId) |
| | | if (spuIdList.value.length > 0) { |
| | | spuList.value = await ProductSpuApi.getSpuDetailList(spuIdList.value) |
| | | } |
| | | |
| | | // 更新 SPU 的最低兑换积分和所需兑换金额 |
| | | pointActivityList.value.forEach((activity) => { |
| | | // 匹配spuId |
| | | const spu = spuList.value.find((spu) => spu.id === activity.spuId) |
| | | if (spu) { |
| | | spu.pointStock = activity.stock |
| | | spu.pointTotalStock = activity.totalStock |
| | | spu.point = activity.point |
| | | spu.pointPrice = activity.price |
| | | } |
| | | }) |
| | | } |
| | | } catch (error) { |
| | | console.error('获取积分商城活动细节或 SPU 细节时出错:', error) |
| | | } |
| | | }, |
| | | { |
| | | immediate: true, |
| | | deep: true |
| | | } |
| | | ) |
| | | |
| | | /** |
| | | * 计算商品的间距 |
| | | * @param index 商品索引 |
| | | */ |
| | | const calculateSpace = (index: number) => { |
| | | // 商品的列数 |
| | | const columns = props.property.layoutType === 'twoCol' ? 2 : 1 |
| | | // 第一列没有左边距 |
| | | const marginLeft = index % columns === 0 ? '0' : props.property.space + 'px' |
| | | // 第一行没有上边距 |
| | | const marginTop = index < columns ? '0' : props.property.space + 'px' |
| | | |
| | | return { marginLeft, marginTop } |
| | | } |
| | | |
| | | // 容器 |
| | | const containerRef = ref() |
| | | // 计算商品的宽度 |
| | | const calculateWidth = () => { |
| | | let width = '100%' |
| | | // 双列时每列的宽度为:(总宽度 - 间距)/ 2 |
| | | if (props.property.layoutType === 'twoCol') { |
| | | width = `${(containerRef.value.offsetWidth - props.property.space) / 2}px` |
| | | } |
| | | return { width } |
| | | } |
| | | </script> |
| | | |
| | | <style lang="scss" scoped></style> |
对比新文件 |
| | |
| | | <template> |
| | | <ComponentContainerProperty v-model="formData.style"> |
| | | <el-form :model="formData" label-width="80px"> |
| | | <el-card class="property-group" header="积分商城活动" shadow="never"> |
| | | <PointShowcase v-model="formData.activityIds" /> |
| | | </el-card> |
| | | <el-card class="property-group" header="商品样式" shadow="never"> |
| | | <el-form-item label="布局" prop="type"> |
| | | <el-radio-group v-model="formData.layoutType"> |
| | | <el-tooltip class="item" content="单列大图" placement="bottom"> |
| | | <el-radio-button value="oneColBigImg"> |
| | | <Icon icon="fluent:text-column-one-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <el-tooltip class="item" content="单列小图" placement="bottom"> |
| | | <el-radio-button value="oneColSmallImg"> |
| | | <Icon icon="fluent:text-column-two-left-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <el-tooltip class="item" content="双列" placement="bottom"> |
| | | <el-radio-button value="twoCol"> |
| | | <Icon icon="fluent:text-column-two-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <!--<el-tooltip class="item" content="三列" placement="bottom"> |
| | | <el-radio-button value="threeCol"> |
| | | <Icon icon="fluent:text-column-three-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip>--> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="商品名称" prop="fields.name.show"> |
| | | <div class="flex gap-8px"> |
| | | <ColorInput v-model="formData.fields.name.color" /> |
| | | <el-checkbox v-model="formData.fields.name.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="商品简介" prop="fields.introduction.show"> |
| | | <div class="flex gap-8px"> |
| | | <ColorInput v-model="formData.fields.introduction.color" /> |
| | | <el-checkbox v-model="formData.fields.introduction.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="商品价格" prop="fields.price.show"> |
| | | <div class="flex gap-8px"> |
| | | <ColorInput v-model="formData.fields.price.color" /> |
| | | <el-checkbox v-model="formData.fields.price.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="市场价" prop="fields.marketPrice.show"> |
| | | <div class="flex gap-8px"> |
| | | <ColorInput v-model="formData.fields.marketPrice.color" /> |
| | | <el-checkbox v-model="formData.fields.marketPrice.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="商品销量" prop="fields.salesCount.show"> |
| | | <div class="flex gap-8px"> |
| | | <ColorInput v-model="formData.fields.salesCount.color" /> |
| | | <el-checkbox v-model="formData.fields.salesCount.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="商品库存" prop="fields.stock.show"> |
| | | <div class="flex gap-8px"> |
| | | <ColorInput v-model="formData.fields.stock.color" /> |
| | | <el-checkbox v-model="formData.fields.stock.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | </el-card> |
| | | <el-card class="property-group" header="角标" shadow="never"> |
| | | <el-form-item label="角标" prop="badge.show"> |
| | | <el-switch v-model="formData.badge.show" /> |
| | | </el-form-item> |
| | | <el-form-item v-if="formData.badge.show" label="角标" prop="badge.imgUrl"> |
| | | <UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px"> |
| | | <template #tip> 建议尺寸:36 * 22</template> |
| | | </UploadImg> |
| | | </el-form-item> |
| | | </el-card> |
| | | <el-card class="property-group" header="按钮" shadow="never"> |
| | | <el-form-item label="按钮类型" prop="btnBuy.type"> |
| | | <el-radio-group v-model="formData.btnBuy.type"> |
| | | <el-radio-button value="text">文字</el-radio-button> |
| | | <el-radio-button value="img">图片</el-radio-button> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <template v-if="formData.btnBuy.type === 'text'"> |
| | | <el-form-item label="按钮文字" prop="btnBuy.text"> |
| | | <el-input v-model="formData.btnBuy.text" /> |
| | | </el-form-item> |
| | | <el-form-item label="左侧背景" prop="btnBuy.bgBeginColor"> |
| | | <ColorInput v-model="formData.btnBuy.bgBeginColor" /> |
| | | </el-form-item> |
| | | <el-form-item label="右侧背景" prop="btnBuy.bgEndColor"> |
| | | <ColorInput v-model="formData.btnBuy.bgEndColor" /> |
| | | </el-form-item> |
| | | </template> |
| | | <template v-else> |
| | | <el-form-item label="图片" prop="btnBuy.imgUrl"> |
| | | <UploadImg v-model="formData.btnBuy.imgUrl" height="56px" width="56px"> |
| | | <template #tip> 建议尺寸:56 * 56</template> |
| | | </UploadImg> |
| | | </el-form-item> |
| | | </template> |
| | | </el-card> |
| | | <el-card class="property-group" header="商品样式" shadow="never"> |
| | | <el-form-item label="上圆角" prop="borderRadiusTop"> |
| | | <el-slider |
| | | v-model="formData.borderRadiusTop" |
| | | :max="100" |
| | | :min="0" |
| | | :show-input-controls="false" |
| | | input-size="small" |
| | | show-input |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="下圆角" prop="borderRadiusBottom"> |
| | | <el-slider |
| | | v-model="formData.borderRadiusBottom" |
| | | :max="100" |
| | | :min="0" |
| | | :show-input-controls="false" |
| | | input-size="small" |
| | | show-input |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="间隔" prop="space"> |
| | | <el-slider |
| | | v-model="formData.space" |
| | | :max="100" |
| | | :min="0" |
| | | :show-input-controls="false" |
| | | input-size="small" |
| | | show-input |
| | | /> |
| | | </el-form-item> |
| | | </el-card> |
| | | </el-form> |
| | | </ComponentContainerProperty> |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | import { PromotionPointProperty } from './config' |
| | | import { usePropertyForm } from '@/components/DiyEditor/util' |
| | | import PointShowcase from '@/views/mall/promotion/point/components/PointShowcase.vue' |
| | | |
| | | // 秒杀属性面板 |
| | | defineOptions({ name: 'PromotionPointProperty' }) |
| | | |
| | | const props = defineProps<{ modelValue: PromotionPointProperty }>() |
| | | const emit = defineEmits(['update:modelValue']) |
| | | const { formData } = usePropertyForm(props.modelValue, emit) |
| | | </script> |
| | | |
| | | <style lang="scss" scoped></style> |
| | |
| | | /** 秒杀属性 */ |
| | | export interface PromotionSeckillProperty { |
| | | // 布局类型:单列 | 三列 |
| | | layoutType: 'oneCol' | 'threeCol' |
| | | layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol' |
| | | // 商品字段 |
| | | fields: { |
| | | // 商品名称 |
| | | name: PromotionSeckillFieldProperty |
| | | // 商品简介 |
| | | introduction: PromotionSeckillFieldProperty |
| | | // 商品价格 |
| | | price: PromotionSeckillFieldProperty |
| | | // 市场价 |
| | | marketPrice: PromotionSeckillFieldProperty |
| | | // 商品销量 |
| | | salesCount: PromotionSeckillFieldProperty |
| | | // 商品库存 |
| | | stock: PromotionSeckillFieldProperty |
| | | } |
| | | // 角标 |
| | | badge: { |
| | | // 是否显示 |
| | | show: boolean |
| | | // 角标图片 |
| | | imgUrl: string |
| | | } |
| | | // 按钮 |
| | | btnBuy: { |
| | | // 类型:文字 | 图片 |
| | | type: 'text' | 'img' |
| | | // 文字 |
| | | text: string |
| | | // 文字按钮:背景渐变起始颜色 |
| | | bgBeginColor: string |
| | | // 文字按钮:背景渐变结束颜色 |
| | | bgEndColor: string |
| | | // 图片按钮:图片地址 |
| | | imgUrl: string |
| | | } |
| | | // 上圆角 |
| | |
| | | // 间距 |
| | | space: number |
| | | // 秒杀活动编号 |
| | | activityId: number |
| | | activityIds: number[] |
| | | // 组件样式 |
| | | style: ComponentStyle |
| | | } |
| | | |
| | | // 商品字段 |
| | | export interface PromotionSeckillFieldProperty { |
| | | // 是否显示 |
| | |
| | | name: '秒杀', |
| | | icon: 'mdi:calendar-time', |
| | | property: { |
| | | activityId: undefined, |
| | | layoutType: 'oneCol', |
| | | layoutType: 'oneColBigImg', |
| | | fields: { |
| | | name: { show: true, color: '#000' }, |
| | | price: { show: true, color: '#ff3000' } |
| | | introduction: { show: true, color: '#999' }, |
| | | price: { show: true, color: '#ff3000' }, |
| | | marketPrice: { show: true, color: '#c4c4c4' }, |
| | | salesCount: { show: true, color: '#c4c4c4' }, |
| | | stock: { show: false, color: '#c4c4c4' } |
| | | }, |
| | | badge: { show: false, imgUrl: '' }, |
| | | btnBuy: { |
| | | type: 'text', |
| | | text: '立即秒杀', |
| | | bgBeginColor: '#FF6000', |
| | | bgEndColor: '#FE832A', |
| | | imgUrl: '' |
| | | }, |
| | | borderRadiusTop: 8, |
| | | borderRadiusBottom: 8, |
| | | space: 8, |
| | |
| | | <template> |
| | | <el-scrollbar class="z-1 min-h-30px" wrap-class="w-full" ref="containerRef"> |
| | | <!-- 商品网格 --> |
| | | <div :class="`box-content min-h-30px w-full flex flex-row flex-wrap`" ref="containerRef"> |
| | | <div |
| | | class="grid overflow-x-auto" |
| | | class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white" |
| | | :style="{ |
| | | gridGap: `${property.space}px`, |
| | | gridTemplateColumns, |
| | | width: scrollbarWidth |
| | | ...calculateSpace(index), |
| | | ...calculateWidth(), |
| | | borderTopLeftRadius: `${property.borderRadiusTop}px`, |
| | | borderTopRightRadius: `${property.borderRadiusTop}px`, |
| | | borderBottomLeftRadius: `${property.borderRadiusBottom}px`, |
| | | borderBottomRightRadius: `${property.borderRadiusBottom}px` |
| | | }" |
| | | v-for="(spu, index) in spuList" |
| | | :key="index" |
| | | > |
| | | <!-- 商品 --> |
| | | <!-- 角标 --> |
| | | <div v-if="property.badge.show" class="absolute left-0 top-0 z-1 items-center justify-center"> |
| | | <el-image fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" /> |
| | | </div> |
| | | <!-- 商品封面图 --> |
| | | <div |
| | | class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white" |
| | | :style="{ |
| | | borderTopLeftRadius: `${property.borderRadiusTop}px`, |
| | | borderTopRightRadius: `${property.borderRadiusTop}px`, |
| | | borderBottomLeftRadius: `${property.borderRadiusBottom}px`, |
| | | borderBottomRightRadius: `${property.borderRadiusBottom}px` |
| | | }" |
| | | v-for="(spu, index) in spuList" |
| | | :key="index" |
| | | :class="[ |
| | | 'h-140px', |
| | | { |
| | | 'w-full': property.layoutType !== 'oneColSmallImg', |
| | | 'w-140px': property.layoutType === 'oneColSmallImg' |
| | | } |
| | | ]" |
| | | > |
| | | <!-- 角标 --> |
| | | <el-image fit="cover" class="h-full w-full" :src="spu.picUrl" /> |
| | | </div> |
| | | <div |
| | | :class="[ |
| | | ' flex flex-col gap-8px p-8px box-border', |
| | | { |
| | | 'w-full': property.layoutType !== 'oneColSmallImg', |
| | | 'w-[calc(100%-140px-16px)]': property.layoutType === 'oneColSmallImg' |
| | | } |
| | | ]" |
| | | > |
| | | <!-- 商品名称 --> |
| | | <div |
| | | v-if="property.badge.show" |
| | | class="absolute left-0 top-0 z-1 items-center justify-center" |
| | | > |
| | | <el-image fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" /> |
| | | </div> |
| | | <!-- 商品封面图 --> |
| | | <el-image fit="cover" :src="spu.picUrl" :style="{ width: imageSize, height: imageSize }" /> |
| | | <div |
| | | v-if="property.fields.name.show" |
| | | :class="[ |
| | | 'flex flex-col gap-8px p-8px box-border', |
| | | 'text-14px ', |
| | | { |
| | | 'w-[calc(100%-64px)]': columns === 2, |
| | | 'w-full': columns === 3 |
| | | truncate: property.layoutType !== 'oneColSmallImg', |
| | | 'overflow-ellipsis line-clamp-2': property.layoutType === 'oneColSmallImg' |
| | | } |
| | | ]" |
| | | :style="{ color: property.fields.name.color }" |
| | | > |
| | | <!-- 商品名称 --> |
| | | <div |
| | | v-if="property.fields.name.show" |
| | | class="truncate text-12px" |
| | | :style="{ color: property.fields.name.color }" |
| | | {{ spu.name }} |
| | | </div> |
| | | <!-- 商品简介 --> |
| | | <div |
| | | v-if="property.fields.introduction.show" |
| | | class="truncate text-12px" |
| | | :style="{ color: property.fields.introduction.color }" |
| | | > |
| | | {{ spu.introduction }} |
| | | </div> |
| | | <div> |
| | | <!-- 价格 --> |
| | | <span |
| | | v-if="property.fields.price.show" |
| | | class="text-16px" |
| | | :style="{ color: property.fields.price.color }" |
| | | > |
| | | {{ spu.name }} |
| | | </div> |
| | | <div> |
| | | <!-- 商品价格 --> |
| | | <span |
| | | v-if="property.fields.price.show" |
| | | class="text-12px" |
| | | :style="{ color: property.fields.price.color }" |
| | | > |
| | | ¥{{ spu.price }} |
| | | </span> |
| | | </div> |
| | | ¥{{ fenToYuan(spu.price || Infinity) }} |
| | | </span> |
| | | <!-- 市场价 --> |
| | | <span |
| | | v-if="property.fields.marketPrice.show && spu.marketPrice" |
| | | class="ml-4px text-10px line-through" |
| | | :style="{ color: property.fields.marketPrice.color }" |
| | | >¥{{ fenToYuan(spu.marketPrice) }}</span |
| | | > |
| | | </div> |
| | | <div class="text-12px"> |
| | | <!-- 销量 --> |
| | | <span |
| | | v-if="property.fields.salesCount.show" |
| | | :style="{ color: property.fields.salesCount.color }" |
| | | > |
| | | 已售{{ (spu.salesCount || 0) + (spu.virtualSalesCount || 0) }}件 |
| | | </span> |
| | | <!-- 库存 --> |
| | | <span v-if="property.fields.stock.show" :style="{ color: property.fields.stock.color }"> |
| | | 库存{{ spu.stock || 0 }} |
| | | </span> |
| | | </div> |
| | | </div> |
| | | <!-- 购买按钮 --> |
| | | <div class="absolute bottom-8px right-8px"> |
| | | <!-- 文字按钮 --> |
| | | <span |
| | | v-if="property.btnBuy.type === 'text'" |
| | | class="rounded-full p-x-12px p-y-4px text-12px text-white" |
| | | :style="{ |
| | | background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}` |
| | | }" |
| | | > |
| | | {{ property.btnBuy.text }} |
| | | </span> |
| | | <!-- 图片按钮 --> |
| | | <el-image |
| | | v-else |
| | | class="h-28px w-28px rounded-full" |
| | | fit="cover" |
| | | :src="property.btnBuy.imgUrl" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </el-scrollbar> |
| | | </div> |
| | | </template> |
| | | <script setup lang="ts"> |
| | | import { PromotionSeckillProperty } from './config' |
| | | import * as ProductSpuApi from '@/api/mall/product/spu' |
| | | import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity' |
| | | import { fenToYuan } from '@/utils' |
| | | |
| | | /** 秒杀 */ |
| | | /** 秒杀卡片 */ |
| | | defineOptions({ name: 'PromotionSeckill' }) |
| | | // 定义属性 |
| | | const props = defineProps<{ property: PromotionSeckillProperty }>() |
| | | // 商品列表 |
| | | const spuList = ref<ProductSpuApi.Spu[]>([]) |
| | | const spuIdList = ref<number[]>([]) |
| | | const seckillActivityList = ref<SeckillActivityApi.SeckillActivityVO[]>([]) |
| | | |
| | | watch( |
| | | () => props.property.activityId, |
| | | () => props.property.activityIds, |
| | | async () => { |
| | | if (!props.property.activityId) return |
| | | const activity = await SeckillActivityApi.getSeckillActivity(props.property.activityId) |
| | | if (!activity?.spuId) return |
| | | spuList.value = [await ProductSpuApi.getSpu(activity.spuId)] |
| | | try { |
| | | // 新添加的秒杀组件,是没有活动ID的 |
| | | const activityIds = props.property.activityIds |
| | | // 检查活动ID的有效性 |
| | | if (Array.isArray(activityIds) && activityIds.length > 0) { |
| | | // 获取秒杀活动详情列表 |
| | | seckillActivityList.value = |
| | | await SeckillActivityApi.getSeckillActivityListByIds(activityIds) |
| | | |
| | | // 获取秒杀活动的 SPU 详情列表 |
| | | spuList.value = [] |
| | | spuIdList.value = seckillActivityList.value |
| | | .map((activity) => activity.spuId) |
| | | .filter((spuId): spuId is number => typeof spuId === 'number') |
| | | if (spuIdList.value.length > 0) { |
| | | spuList.value = await ProductSpuApi.getSpuDetailList(spuIdList.value) |
| | | } |
| | | |
| | | // 更新 SPU 的最低价格 |
| | | seckillActivityList.value.forEach((activity) => { |
| | | // 匹配spuId |
| | | const spu = spuList.value.find((spu) => spu.id === activity.spuId) |
| | | if (spu) { |
| | | // 赋值活动价格,哪个最便宜就赋值哪个 |
| | | spu.price = Math.min(activity.seckillPrice || Infinity, spu.price || Infinity) |
| | | } |
| | | }) |
| | | } |
| | | } catch (error) { |
| | | console.error('获取秒杀活动细节或 SPU 细节时出错:', error) |
| | | } |
| | | }, |
| | | { |
| | | immediate: true, |
| | | deep: true |
| | | } |
| | | ) |
| | | // 手机宽度 |
| | | const phoneWidth = ref(375) |
| | | |
| | | /** |
| | | * 计算商品的间距 |
| | | * @param index 商品索引 |
| | | */ |
| | | const calculateSpace = (index: number) => { |
| | | // 商品的列数 |
| | | const columns = props.property.layoutType === 'twoCol' ? 2 : 1 |
| | | // 第一列没有左边距 |
| | | const marginLeft = index % columns === 0 ? '0' : props.property.space + 'px' |
| | | // 第一行没有上边距 |
| | | const marginTop = index < columns ? '0' : props.property.space + 'px' |
| | | |
| | | return { marginLeft, marginTop } |
| | | } |
| | | |
| | | // 容器 |
| | | const containerRef = ref() |
| | | // 商品的列数 |
| | | const columns = ref(2) |
| | | // 滚动条宽度 |
| | | const scrollbarWidth = ref('100%') |
| | | // 商品图大小 |
| | | const imageSize = ref('0') |
| | | // 商品网络列数 |
| | | const gridTemplateColumns = ref('') |
| | | // 计算布局参数 |
| | | watch( |
| | | () => [props.property, phoneWidth, spuList.value.length], |
| | | () => { |
| | | // 计算列数 |
| | | columns.value = props.property.layoutType === 'oneCol' ? 1 : 3 |
| | | // 每列的宽度为:(总宽度 - 间距 * (列数 - 1))/ 列数 |
| | | const productWidth = |
| | | (phoneWidth.value - props.property.space * (columns.value - 1)) / columns.value |
| | | // 商品图布局:2列时,左右布局 3列时,上下布局 |
| | | imageSize.value = columns.value === 2 ? '64px' : `${productWidth}px` |
| | | // 指定列数 |
| | | gridTemplateColumns.value = `repeat(${columns.value}, auto)` |
| | | // 不滚动 |
| | | scrollbarWidth.value = '100%' |
| | | }, |
| | | { immediate: true, deep: true } |
| | | ) |
| | | onMounted(() => { |
| | | // 提取手机宽度 |
| | | phoneWidth.value = containerRef.value?.wrapRef?.offsetWidth || 375 |
| | | }) |
| | | // 计算商品的宽度 |
| | | const calculateWidth = () => { |
| | | let width = '100%' |
| | | // 双列时每列的宽度为:(总宽度 - 间距)/ 2 |
| | | if (props.property.layoutType === 'twoCol') { |
| | | width = `${(containerRef.value.offsetWidth - props.property.space) / 2}px` |
| | | } |
| | | return { width } |
| | | } |
| | | </script> |
| | | |
| | | <style scoped lang="scss"></style> |
| | |
| | | <ComponentContainerProperty v-model="formData.style"> |
| | | <el-form label-width="80px" :model="formData"> |
| | | <el-card header="秒杀活动" class="property-group" shadow="never"> |
| | | <el-form-item label="秒杀活动" prop="activityId"> |
| | | <el-select v-model="formData.activityId"> |
| | | <el-option |
| | | v-for="activity in activityList" |
| | | :key="activity.id" |
| | | :label="activity.name" |
| | | :value="activity.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <SeckillShowcase v-model="formData.activityIds" /> |
| | | </el-card> |
| | | <el-card header="商品样式" class="property-group" shadow="never"> |
| | | <el-form-item label="布局" prop="type"> |
| | | <el-radio-group v-model="formData.layoutType"> |
| | | <el-tooltip class="item" content="单列" placement="bottom"> |
| | | <el-radio-button label="oneCol"> |
| | | <el-tooltip class="item" content="单列大图" placement="bottom"> |
| | | <el-radio-button value="oneColBigImg"> |
| | | <Icon icon="fluent:text-column-one-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <el-tooltip class="item" content="三列" placement="bottom"> |
| | | <el-radio-button label="threeCol"> |
| | | <Icon icon="fluent:text-column-three-24-filled" /> |
| | | <el-tooltip class="item" content="单列小图" placement="bottom"> |
| | | <el-radio-button value="oneColSmallImg"> |
| | | <Icon icon="fluent:text-column-two-left-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <el-tooltip class="item" content="双列" placement="bottom"> |
| | | <el-radio-button value="twoCol"> |
| | | <Icon icon="fluent:text-column-two-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <!--<el-tooltip class="item" content="三列" placement="bottom"> |
| | | <el-radio-button value="threeCol"> |
| | | <Icon icon="fluent:text-column-three-24-filled" /> |
| | | </el-radio-button> |
| | | </el-tooltip>--> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="商品名称" prop="fields.name.show"> |
| | |
| | | <el-checkbox v-model="formData.fields.name.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="商品简介" prop="fields.introduction.show"> |
| | | <div class="flex gap-8px"> |
| | | <ColorInput v-model="formData.fields.introduction.color" /> |
| | | <el-checkbox v-model="formData.fields.introduction.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="商品价格" prop="fields.price.show"> |
| | | <div class="flex gap-8px"> |
| | | <ColorInput v-model="formData.fields.price.color" /> |
| | | <el-checkbox v-model="formData.fields.price.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="市场价" prop="fields.marketPrice.show"> |
| | | <div class="flex gap-8px"> |
| | | <ColorInput v-model="formData.fields.marketPrice.color" /> |
| | | <el-checkbox v-model="formData.fields.marketPrice.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="商品销量" prop="fields.salesCount.show"> |
| | | <div class="flex gap-8px"> |
| | | <ColorInput v-model="formData.fields.salesCount.color" /> |
| | | <el-checkbox v-model="formData.fields.salesCount.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="商品库存" prop="fields.stock.show"> |
| | | <div class="flex gap-8px"> |
| | | <ColorInput v-model="formData.fields.stock.color" /> |
| | | <el-checkbox v-model="formData.fields.stock.show" /> |
| | | </div> |
| | | </el-form-item> |
| | | </el-card> |
| | |
| | | </el-form-item> |
| | | <el-form-item label="角标" prop="badge.imgUrl" v-if="formData.badge.show"> |
| | | <UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px"> |
| | | <template #tip> 建议尺寸:36 * 22 </template> |
| | | <template #tip> 建议尺寸:36 * 22</template> |
| | | </UploadImg> |
| | | </el-form-item> |
| | | </el-card> |
| | | <el-card header="按钮" class="property-group" shadow="never"> |
| | | <el-form-item label="按钮类型" prop="btnBuy.type"> |
| | | <el-radio-group v-model="formData.btnBuy.type"> |
| | | <el-radio-button value="text">文字</el-radio-button> |
| | | <el-radio-button value="img">图片</el-radio-button> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <template v-if="formData.btnBuy.type === 'text'"> |
| | | <el-form-item label="按钮文字" prop="btnBuy.text"> |
| | | <el-input v-model="formData.btnBuy.text" /> |
| | | </el-form-item> |
| | | <el-form-item label="左侧背景" prop="btnBuy.bgBeginColor"> |
| | | <ColorInput v-model="formData.btnBuy.bgBeginColor" /> |
| | | </el-form-item> |
| | | <el-form-item label="右侧背景" prop="btnBuy.bgEndColor"> |
| | | <ColorInput v-model="formData.btnBuy.bgEndColor" /> |
| | | </el-form-item> |
| | | </template> |
| | | <template v-else> |
| | | <el-form-item label="图片" prop="btnBuy.imgUrl"> |
| | | <UploadImg v-model="formData.btnBuy.imgUrl" height="56px" width="56px"> |
| | | <template #tip> 建议尺寸:56 * 56</template> |
| | | </UploadImg> |
| | | </el-form-item> |
| | | </template> |
| | | </el-card> |
| | | <el-card header="商品样式" class="property-group" shadow="never"> |
| | | <el-form-item label="上圆角" prop="borderRadiusTop"> |
| | |
| | | import { usePropertyForm } from '@/components/DiyEditor/util' |
| | | import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity' |
| | | import { CommonStatusEnum } from '@/utils/constants' |
| | | import SeckillShowcase from '@/views/mall/promotion/seckill/components/SeckillShowcase.vue' |
| | | |
| | | // 秒杀属性面板 |
| | | defineOptions({ name: 'PromotionSeckillProperty' }) |
| | |
| | | const emit = defineEmits(['update:modelValue']) |
| | | const { formData } = usePropertyForm(props.modelValue, emit) |
| | | // 活动列表 |
| | | const activityList = ref<SeckillActivityApi.SeckillActivityVO>([]) |
| | | const activityList = ref<SeckillActivityApi.SeckillActivityVO[]>([]) |
| | | onMounted(async () => { |
| | | const { list } = await SeckillActivityApi.getSeckillActivityPage({ |
| | | status: CommonStatusEnum.ENABLE |
| | |
| | | <el-form-item label="框体样式"> |
| | | <el-radio-group v-model="formData!.borderRadius"> |
| | | <el-tooltip content="方形" placement="top"> |
| | | <el-radio-button :label="0"> |
| | | <el-radio-button :value="0"> |
| | | <Icon icon="tabler:input-search" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <el-tooltip content="圆形" placement="top"> |
| | | <el-radio-button :label="10"> |
| | | <el-radio-button :value="10"> |
| | | <Icon icon="iconoir:input-search" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | |
| | | <el-form-item label="文本位置" prop="placeholderPosition"> |
| | | <el-radio-group v-model="formData!.placeholderPosition"> |
| | | <el-tooltip content="居左" placement="top"> |
| | | <el-radio-button label="left"> |
| | | <el-radio-button value="left"> |
| | | <Icon icon="ant-design:align-left-outlined" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <el-tooltip content="居中" placement="top"> |
| | | <el-radio-button label="center"> |
| | | <el-radio-button value="center"> |
| | | <Icon icon="ant-design:align-center-outlined" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | |
| | | { |
| | | text: '首页', |
| | | url: '/pages/index/index', |
| | | iconUrl: 'http://xxxx/static/images/1-001.png', |
| | | activeIconUrl: 'http://xxxx/static/images/1-002.png' |
| | | iconUrl: 'http://mall.yudao.iocoder.cn/static/images/1-001.png', |
| | | activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/1-002.png' |
| | | }, |
| | | { |
| | | text: '分类', |
| | | url: '/pages/index/category?id=3', |
| | | iconUrl: 'http://xxxx/static/images/2-001.png', |
| | | activeIconUrl: 'http://xxxx/static/images/2-002.png' |
| | | iconUrl: 'http://mall.yudao.iocoder.cn/static/images/2-001.png', |
| | | activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/2-002.png' |
| | | }, |
| | | { |
| | | text: '购物车', |
| | | url: '/pages/index/cart', |
| | | iconUrl: 'http://xxxx/static/images/3-001.png', |
| | | activeIconUrl: 'http://xxxx/static/images/3-002.png' |
| | | iconUrl: 'http://mall.yudao.iocoder.cn/static/images/3-001.png', |
| | | activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/3-002.png' |
| | | }, |
| | | { |
| | | text: '我的', |
| | | url: '/pages/index/user', |
| | | iconUrl: 'http://xxxx/static/images/4-001.png', |
| | | activeIconUrl: 'http://xxxx/static/images/4-002.png' |
| | | iconUrl: 'http://mall.yudao.iocoder.cn/static/images/4-001.png', |
| | | activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/4-002.png' |
| | | } |
| | | ] |
| | | } |
| | |
| | | </el-form-item> |
| | | <el-form-item label="导航背景"> |
| | | <el-radio-group v-model="formData!.style.bgType"> |
| | | <el-radio-button label="color">纯色</el-radio-button> |
| | | <el-radio-button label="img">图片</el-radio-button> |
| | | <el-radio-button value="color">纯色</el-radio-button> |
| | | <el-radio-button value="img">图片</el-radio-button> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="选择颜色" v-if="formData!.style.bgType === 'color'"> |
| | |
| | | </template> |
| | | |
| | | <script setup lang="ts"> |
| | | import { TabBarProperty, THEME_LIST } from './config' |
| | | import { TabBarProperty, component, THEME_LIST } from './config' |
| | | import { usePropertyForm } from '@/components/DiyEditor/util' |
| | | // 底部导航栏 |
| | | defineOptions({ name: 'TabBarProperty' }) |
| | |
| | | const emit = defineEmits(['update:modelValue']) |
| | | const { formData } = usePropertyForm(props.modelValue, emit) |
| | | |
| | | // 将数据库的值更新到右侧属性栏 |
| | | component.property.items = formData.value.items |
| | | |
| | | // 要的主题 |
| | | const handleThemeChange = () => { |
| | | const theme = THEME_LIST.find((theme) => theme.id === formData.value.theme) |
| | |
| | | <el-form-item label="标题位置" prop="textAlign"> |
| | | <el-radio-group v-model="formData!.textAlign"> |
| | | <el-tooltip content="居左" placement="top"> |
| | | <el-radio-button label="left"> |
| | | <el-radio-button value="left"> |
| | | <Icon icon="ant-design:align-left-outlined" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | | <el-tooltip content="居中" placement="top"> |
| | | <el-radio-button label="center"> |
| | | <el-radio-button value="center"> |
| | | <Icon icon="ant-design:align-center-outlined" /> |
| | | </el-radio-button> |
| | | </el-tooltip> |
| | |
| | | <template v-if="formData.more.show"> |
| | | <el-form-item label="样式" prop="more.type"> |
| | | <el-radio-group v-model="formData.more.type"> |
| | | <el-radio label="text">文字</el-radio> |
| | | <el-radio label="icon">图标</el-radio> |
| | | <el-radio label="all">文字+图标</el-radio> |
| | | <el-radio value="text">文字</el-radio> |
| | | <el-radio value="icon">图标</el-radio> |
| | | <el-radio value="all">文字+图标</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="更多文字" prop="more.text" v-show="formData.more.type !== 'icon'"> |
| | |
| | | <el-avatar :size="60"> |
| | | <Icon icon="ep:avatar" :size="60" /> |
| | | </el-avatar> |
| | | <span class="text-18px font-bold">工业互联网平台</span> |
| | | <span class="text-18px font-bold">芋道源码</span> |
| | | </div> |
| | | <Icon icon="tdesign:qrcode" :size="20" /> |
| | | </div> |
| | |
| | | class="mb-4px flex flex-col gap-4px border border-gray-2 border-rounded rounded border-solid p-8px" |
| | | > |
| | | <!-- 操作按钮区 --> |
| | | <div class="m--8px m-b-4px flex flex-row items-center justify-between bg-gray-1 p-8px"> |
| | | <div class="m--8px m-b-4px flex flex-row items-center justify-between p-8px" style="background-color: var(--app-content-bg-color);"> |
| | | <el-tooltip content="拖动排序"> |
| | | <Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" /> |
| | | <Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" style="color: #8a909c;" /> |
| | | </el-tooltip> |
| | | <el-tooltip content="删除"> |
| | | <Icon |
| | |
| | | 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> |
| | |
| | | <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' }) |
| | |
| | | modelValue: { |
| | | require: false, |
| | | type: String |
| | | }, |
| | | clearable: { |
| | | require: false, |
| | | type: Boolean |
| | | } |
| | | }) |
| | | const emit = defineEmits<{ (e: 'update:modelValue', v: string) }>() |
| | |
| | | currentPage.value = page |
| | | } |
| | | |
| | | function clearIcon() { |
| | | icon.value = '' |
| | | emit('update:modelValue', '') |
| | | visible.value = false |
| | | } |
| | | |
| | | watch( |
| | | () => { |
| | | return props.modelValue |
| | |
| | | |
| | | <template> |
| | | <div class="selector"> |
| | | <ElInput v-model="inputValue" @click="visible = !visible"> |
| | | <ElInput v-model="inputValue" @click="visible = !visible" :clearable="props.clearable" @clear="clearIcon"> |
| | | <template #append> |
| | | <ElPopover |
| | | :popper-options="{ |
| | | placement: 'auto' |
| | | }" |
| | | :visible="visible" |
| | | :width="350" |
| | | :width="355" |
| | | popper-class="pure-popper" |
| | | trigger="click" |
| | | > |
| | |
| | | > |
| | | <ElDivider border-style="dashed" class="tab-divider" /> |
| | | <ElScrollbar height="220px"> |
| | | <ul class="ml-2 flex flex-wrap px-2"> |
| | | <ul class="ml-2 flex flex-wrap"> |
| | | <li |
| | | v-for="(item, key) in pageList" |
| | | :key="key" |
| | |
| | | background |
| | | class="h-10 flex items-center justify-center" |
| | | layout="prev, pager, next" |
| | | small |
| | | size="small" |
| | | @current-change="onCurrentChange" |
| | | /> |
| | | </ElPopover> |
| | |
| | | <div v-else class="custom-hover" @click.stop="showTopSearch = !showTopSearch"> |
| | | <Icon icon="ep:search" /> |
| | | <el-select |
| | | @click.stop |
| | | filterable |
| | | :reserve-keyword="false" |
| | | remote |
| | |
| | | <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" |
| | |
| | | AssignStartUserHandlerType, |
| | | AssignEmptyHandlerType, |
| | | FieldPermissionType, |
| | | ProcessVariableEnum |
| | | } from './consts' |
| | | import { parseFormFields } from '@/components/FormCreate/src/utils/index' |
| | | export function useWatchNode(props: { flowNode: SimpleFlowNode }): Ref<SimpleFlowNode> { |
| | |
| | | parseFormFields(JSON.parse(fieldStr), result) |
| | | }) |
| | | } |
| | | // 固定添加发起人 ID 字段 |
| | | result.unshift({ |
| | | field: ProcessVariableEnum.START_USER_ID, |
| | | title: '发起人', |
| | | type: 'UserSelect', |
| | | required: true |
| | | }) |
| | | return result |
| | | } |
| | | |
| | |
| | | </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" |
| | |
| | | <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> |
| | |
| | | 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' |
| | |
| | | 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 |
| | | } |
| | |
| | | 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> |
| | |
| | | TimeoutHandlerType, |
| | | ASSIGN_EMPTY_HANDLER_TYPES, |
| | | AssignEmptyHandlerType, |
| | | FieldPermissionType |
| | | FieldPermissionType, |
| | | ProcessVariableEnum |
| | | } from '../consts' |
| | | |
| | | import { |
| | |
| | | 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') |
| | | }) |
| | | // 表单内部门字段选项, 必须是必填和部门选择器 |
| | |
| | | <template> |
| | | <div class="upload-file"> |
| | | <div v-if="!disabled" class="upload-file"> |
| | | <el-upload |
| | | ref="uploadRef" |
| | | v-model:file-list="fileList" |
| | |
| | | class="upload-file-uploader" |
| | | name="file" |
| | | > |
| | | <el-button v-if="!disabled" type="primary"> |
| | | <el-button type="primary"> |
| | | <Icon icon="ep:upload-filled" /> |
| | | 选取文件 |
| | | </el-button> |
| | | <template v-if="isShowTip && !disabled" #tip> |
| | | <template v-if="isShowTip" #tip> |
| | | <div style="font-size: 8px"> |
| | | 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> |
| | | </div> |
| | |
| | | 格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b> 的文件 |
| | | </div> |
| | | </template> |
| | | <!-- TODO @puhui999:1)表单展示的时候,位置会偏掉,已发微信;2)disable 的时候,应该把【删除】按钮也隐藏掉? --> |
| | | <template #file="row"> |
| | | <div class="flex items-center"> |
| | | <span>{{ row.file.name }}</span> |
| | |
| | | </div> |
| | | </template> |
| | | </el-upload> |
| | | </div> |
| | | |
| | | <!-- 上传操作禁用时 --> |
| | | <div v-if="disabled" class="upload-file"> |
| | | <div v-for="(file, index) in fileList" :key="index" class="flex items-center file-list-item"> |
| | | <span>{{ file.name }}</span> |
| | | <div class="ml-10px"> |
| | | <el-link :href="file.url" :underline="false" download target="_blank" type="primary"> |
| | | 下载 |
| | | </el-link> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <script lang="ts" setup> |
| | |
| | | :deep(.ele-upload-list__item-content-action .el-link) { |
| | | margin-right: 10px; |
| | | } |
| | | |
| | | .file-list-item { |
| | | border: 1px dashed var(--el-border-color-darker); |
| | | border-radius: 8px; |
| | | } |
| | | </style> |
| | |
| | | <template #file="{ file }"> |
| | | <img :src="file.url" class="upload-image" /> |
| | | <div class="upload-handle" @click.stop> |
| | | <div class="handle-icon" @click="handlePictureCardPreview(file)"> |
| | | <div class="handle-icon" @click="imagePreview(file.url!)"> |
| | | <Icon icon="ep:zoom-in" /> |
| | | <span>查看</span> |
| | | </div> |
| | |
| | | <div class="el-upload__tip"> |
| | | <slot name="tip"></slot> |
| | | </div> |
| | | <el-image-viewer |
| | | v-if="imgViewVisible" |
| | | :url-list="[viewImageUrl]" |
| | | @close="imgViewVisible = false" |
| | | /> |
| | | </div> |
| | | </template> |
| | | <script lang="ts" setup> |
| | | import type { UploadFile, UploadProps, UploadUserFile } from 'element-plus' |
| | | import { ElNotification } from 'element-plus' |
| | | import { createImageViewer } from '@/components/ImageViewer' |
| | | |
| | | import { propTypes } from '@/utils/propTypes' |
| | | import { useUpload } from '@/components/UploadFile/src/useUpload' |
| | |
| | | defineOptions({ name: 'UploadImgs' }) |
| | | |
| | | const message = useMessage() // 消息弹窗 |
| | | // 查看图片 |
| | | const imagePreview = (imgUrl: string) => { |
| | | createImageViewer({ |
| | | zIndex: 9999999, |
| | | urlList: [imgUrl] |
| | | }) |
| | | } |
| | | |
| | | type FileTypes = |
| | | | 'image/apng' |
| | |
| | | message: `当前最多只能上传 ${props.limit} 张图片,请移除后上传!`, |
| | | type: 'warning' |
| | | }) |
| | | } |
| | | |
| | | // 图片预览 |
| | | const viewImageUrl = ref('') |
| | | const imgViewVisible = ref(false) |
| | | const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => { |
| | | viewImageUrl.value = uploadFile.url! |
| | | imgViewVisible.value = true |
| | | } |
| | | </script> |
| | | |
| | |
| | | enum UPLOAD_TYPE { |
| | | // 客户端直接上传(只支持S3服务) |
| | | CLIENT = 'client', |
| | | // 客户端发送到后端上传 |
| | | SERVER = 'server' |
| | | } |
| | |
| | | "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" |
| | | } |
| | | ] |
| | | }, |
| | |
| | | "isBody": true |
| | | } |
| | | ] |
| | | }, |
| | | { |
| | | "name": "ButtonsSetting", |
| | | "superClass": ["Element"], |
| | | "meta": { |
| | | "allowedIn": ["bpmn:UserTask"] |
| | | }, |
| | | "properties": [ |
| | | { |
| | | "name": "flowable:id", |
| | | "type": "Integer", |
| | | "isAttr": true |
| | | }, |
| | | { |
| | | "name": "flowable:enable", |
| | | "type": "Boolean", |
| | | "isAttr": true |
| | | }, |
| | | { |
| | | "name": "flowable:displayName", |
| | | "type": "String", |
| | | "isAttr": true |
| | | } |
| | | ] |
| | | }, |
| | | { |
| | | "name": "FieldsPermission", |
| | | "superClass": ["Element"], |
| | | "meta": { |
| | | "allowedIn": ["bpmn:UserTask"] |
| | | }, |
| | | "properties": [ |
| | | { |
| | | "name": "flowable:field", |
| | | "type": "String", |
| | | "isAttr": true |
| | | }, |
| | | { |
| | | "name": "flowable:title", |
| | | "type": "String", |
| | | "isAttr": true |
| | | }, |
| | | { |
| | | "name": "flowable:permission", |
| | | "type": "String", |
| | | "isAttr": true |
| | | } |
| | | ] |
| | | }, |
| | | { |
| | | "name": "BoundaryEventType", |
| | | "superClass": ["Element"], |
| | | "meta": { |
| | | "allowedIn": ["bpmn:BoundaryEvent"] |
| | | }, |
| | | "properties": [ |
| | | { |
| | | "name": "value", |
| | | "type": "Integer", |
| | | "isBody": true |
| | | } |
| | | ] |
| | | }, |
| | | { |
| | | "name": "TimeoutHandlerType", |
| | | "superClass": ["Element"], |
| | | "meta": { |
| | | "allowedIn": ["bpmn:BoundaryEvent"] |
| | | }, |
| | | "properties": [ |
| | | { |
| | | "name": "value", |
| | | "type": "Integer", |
| | | "isBody": true |
| | | } |
| | | ] |
| | | }, |
| | | { |
| | | "name": "ApproveType", |
| | | "superClass": ["Element"], |
| | | "meta": { |
| | | "allowedIn": ["bpmn:UserTask"] |
| | | }, |
| | | "properties": [ |
| | | { |
| | | "name": "value", |
| | | "type": "Integer", |
| | | "isBody": true |
| | | } |
| | | ] |
| | | }, |
| | | { |
| | | "name": "ApproveMethod", |
| | | "superClass": ["Element"], |
| | | "meta": { |
| | | "allowedIn": ["bpmn:UserTask"] |
| | | }, |
| | | "properties": [ |
| | | { |
| | | "name": "value", |
| | | "type": "Integer", |
| | | "isBody": true |
| | | } |
| | | ] |
| | | }, |
| | | { |
| | | "name": "CandidateStrategy", |
| | | "superClass": ["Element"], |
| | | "meta": { |
| | | "allowedIn": ["bpmn:UserTask"] |
| | | }, |
| | | "properties": [ |
| | | { |
| | | "name": "value", |
| | | "type": "Integer", |
| | | "isBody": true |
| | | } |
| | | ] |
| | | }, |
| | | { |
| | | "name": "CandidateParam", |
| | | "superClass": ["Element"], |
| | | "meta": { |
| | | "allowedIn": ["bpmn:UserTask"] |
| | | }, |
| | | "properties": [ |
| | | { |
| | | "name": "value", |
| | | "type": "String", |
| | | "isBody": true |
| | | } |
| | | ] |
| | | } |
| | | ], |
| | | "emumerations": [] |
| | |
| | | 'bpmn-icon-user-task', |
| | | translate('Create User Task') |
| | | ), |
| | | 'create.call-activity': createAction( |
| | | 'bpmn:CallActivity', |
| | | 'activity', |
| | | 'bpmn-icon-call-activity', |
| | | translate('Create Call Activity') |
| | | ), |
| | | 'create.service-task': createAction( |
| | | 'bpmn:ServiceTask', |
| | | 'activity', |
| | | 'bpmn-icon-service', |
| | | translate('Create Service Task') |
| | | ), |
| | | 'create.data-object': createAction( |
| | | 'bpmn:DataObjectReference', |
| | | 'data-object', |
| | |
| | | 'bpmn-icon-user-task', |
| | | translate('Create User Task') |
| | | ), |
| | | 'create.service-task': createAction( |
| | | 'bpmn:ServiceTask', |
| | | 'activity', |
| | | 'bpmn-icon-service', |
| | | translate('Create Service Task') |
| | | ), |
| | | 'create.data-object': createAction( |
| | | 'bpmn:DataObjectReference', |
| | | 'data-object', |
| | |
| | | 'Create EndEvent': '创建结束事件', |
| | | 'Create Task': '创建任务', |
| | | 'Create User Task': '创建用户任务', |
| | | 'Create Call Activity': '创建调用活动', |
| | | 'Create Service Task': '创建服务任务', |
| | | 'Create Gateway': '创建网关', |
| | | 'Create DataObjectReference': '创建数据对象', |
| | | 'Create DataStoreReference': '创建数据存储', |
| | |
| | | <template> |
| | | <div class="process-panel__container" :style="{ width: `${width}px`,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" --> |
| | |
| | | <template #title><Icon icon="ep:list" />表单</template> |
| | | <element-form :id="elementId" :type="elementType" /> |
| | | </el-collapse-item> |
| | | <el-collapse-item name="task" v-if="elementType.indexOf('Task') !== -1" key="task"> |
| | | <template #title><Icon icon="ep:checked" />任务(审批人)</template> |
| | | <el-collapse-item name="task" v-if="isTaskCollapseItemShow(elementType)" key="task"> |
| | | <template #title |
| | | ><Icon icon="ep:checked" />{{ getTaskCollapseItemName(elementType) }}</template |
| | | > |
| | | <element-task :id="elementId" :type="elementType" /> |
| | | </el-collapse-item> |
| | | <el-collapse-item |
| | |
| | | v-if="elementType.indexOf('Task') !== -1" |
| | | key="multiInstance" |
| | | > |
| | | <template #title><Icon icon="ep:help-filled" />多实例(会签配置)</template> |
| | | <element-multi-instance :business-object="elementBusinessObject" :type="elementType" /> |
| | | <template #title><Icon icon="ep:help-filled" />多人审批方式</template> |
| | | <element-multi-instance |
| | | :id="elementId" |
| | | :business-object="elementBusinessObject" |
| | | :type="elementType" |
| | | /> |
| | | </el-collapse-item> |
| | | <el-collapse-item name="listeners" key="listeners"> |
| | | <template #title><Icon icon="ep:bell-filled" />执行监听器</template> |
| | |
| | | <template #title><Icon icon="ep:promotion" />其他</template> |
| | | <element-other-config :id="elementId" /> |
| | | </el-collapse-item> |
| | | <el-collapse-item name="customConfig" 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> |
| | |
| | | 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' }) |
| | | |
| | |
| | | <!-- 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> |
对比新文件 |
| | |
| | | <template> |
| | | <div> |
| | | <el-divider content-position="left">审批人超时未处理时</el-divider> |
| | | <el-form-item label="启用开关" prop="timeoutHandlerEnable"> |
| | | <el-switch |
| | | v-model="timeoutHandlerEnable" |
| | | active-text="开启" |
| | | inactive-text="关闭" |
| | | @change="timeoutHandlerChange" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="执行动作" prop="timeoutHandlerType" v-if="timeoutHandlerEnable"> |
| | | <el-radio-group v-model="timeoutHandlerType.value" @change="onTimeoutHandlerTypeChanged"> |
| | | <el-radio-button |
| | | v-for="item in TIMEOUT_HANDLER_TYPES" |
| | | :key="item.value" |
| | | :value="item.value" |
| | | :label="item.label" |
| | | /> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="超时时间设置" v-if="timeoutHandlerEnable"> |
| | | <span class="mr-2">当超过</span> |
| | | <el-form-item prop="timeDuration"> |
| | | <el-input-number |
| | | class="mr-2" |
| | | :style="{ width: '100px' }" |
| | | v-model="timeDuration" |
| | | :min="1" |
| | | controls-position="right" |
| | | @change="() => updateTimeModdle()" |
| | | /> |
| | | </el-form-item> |
| | | <el-select |
| | | v-model="timeUnit" |
| | | class="mr-2" |
| | | :style="{ width: '100px' }" |
| | | @change="onTimeUnitChange" |
| | | > |
| | | <el-option |
| | | v-for="item in TIME_UNIT_TYPES" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | 未处理 |
| | | </el-form-item> |
| | | <el-form-item |
| | | label="最大提醒次数" |
| | | prop="maxRemindCount" |
| | | v-if="timeoutHandlerEnable && timeoutHandlerType.value === 1" |
| | | > |
| | | <el-input-number |
| | | v-model="maxRemindCount" |
| | | :min="1" |
| | | :max="10" |
| | | @change="() => updateTimeModdle()" |
| | | /> |
| | | </el-form-item> |
| | | </div> |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | import { |
| | | TimeUnitType, |
| | | TIME_UNIT_TYPES, |
| | | TIMEOUT_HANDLER_TYPES, |
| | | } from '@/components/SimpleProcessDesignerV2/src/consts' |
| | | import { convertTimeUnit } from '@/components/SimpleProcessDesignerV2/src/utils' |
| | | |
| | | defineOptions({ name: 'ElementCustomConfig4BoundaryEventTimer' }) |
| | | const props = defineProps({ |
| | | id: String, |
| | | type: String |
| | | }) |
| | | const prefix = inject('prefix') |
| | | |
| | | const bpmnElement = ref() |
| | | const bpmnInstances = () => (window as any)?.bpmnInstances |
| | | |
| | | const timeoutHandlerEnable = ref(false) |
| | | const boundaryEventType = ref() |
| | | const timeoutHandlerType = ref({ |
| | | value: undefined |
| | | }) |
| | | const timeModdle = ref() |
| | | const timeDuration = ref(6) |
| | | const timeUnit = ref(TimeUnitType.HOUR) |
| | | const maxRemindCount = ref(1) |
| | | |
| | | const elExtensionElements = ref() |
| | | const otherExtensions = ref() |
| | | const configExtensions = ref([]) |
| | | const eventDefinition = ref() |
| | | |
| | | const resetElement = () => { |
| | | bpmnElement.value = bpmnInstances().bpmnElement |
| | | eventDefinition.value = bpmnElement.value.businessObject.eventDefinitions[0] |
| | | |
| | | // 获取元素扩展属性 或者 创建扩展属性 |
| | | elExtensionElements.value = |
| | | bpmnElement.value.businessObject?.extensionElements ?? |
| | | bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] }) |
| | | |
| | | // 是否开启自定义用户任务超时处理 |
| | | boundaryEventType.value = elExtensionElements.value.values?.filter( |
| | | (ex) => ex.$type === `${prefix}:BoundaryEventType` |
| | | )?.[0] |
| | | if (boundaryEventType.value && boundaryEventType.value.value === 1) { |
| | | timeoutHandlerEnable.value = true |
| | | configExtensions.value.push(boundaryEventType.value) |
| | | } |
| | | |
| | | // 执行动作 |
| | | timeoutHandlerType.value = elExtensionElements.value.values?.filter( |
| | | (ex) => ex.$type === `${prefix}:TimeoutHandlerType` |
| | | )?.[0] |
| | | if (timeoutHandlerType.value) { |
| | | configExtensions.value.push(timeoutHandlerType.value) |
| | | if (eventDefinition.value.timeCycle) { |
| | | const timeStr = eventDefinition.value.timeCycle.body |
| | | const maxRemindCountStr = timeStr.split('/')[0] |
| | | const timeDurationStr = timeStr.split('/')[1] |
| | | console.log(maxRemindCountStr) |
| | | maxRemindCount.value = parseInt(maxRemindCountStr.slice(1)) |
| | | timeDuration.value = parseInt(timeDurationStr.slice(2, timeDurationStr.length - 1)) |
| | | timeUnit.value = convertTimeUnit(timeDurationStr.slice(timeDurationStr.length - 1)) |
| | | timeModdle.value = eventDefinition.value.timeCycle |
| | | } |
| | | if (eventDefinition.value.timeDuration) { |
| | | const timeDurationStr = eventDefinition.value.timeDuration.body |
| | | timeDuration.value = parseInt(timeDurationStr.slice(2, timeDurationStr.length - 1)) |
| | | timeUnit.value = convertTimeUnit(timeDurationStr.slice(timeDurationStr.length - 1)) |
| | | timeModdle.value = eventDefinition.value.timeDuration |
| | | } |
| | | } |
| | | |
| | | // 保留剩余扩展元素,便于后面更新该元素对应属性 |
| | | otherExtensions.value = |
| | | elExtensionElements.value.values?.filter( |
| | | (ex) => |
| | | ex.$type !== `${prefix}:BoundaryEventType` && ex.$type !== `${prefix}:TimeoutHandlerType` |
| | | ) ?? [] |
| | | } |
| | | |
| | | const timeoutHandlerChange = (val) => { |
| | | timeoutHandlerEnable.value = val |
| | | if (val) { |
| | | // 启用自定义用户任务超时处理 |
| | | // 边界事件类型 --- 超时 |
| | | boundaryEventType.value = bpmnInstances().moddle.create(`${prefix}:BoundaryEventType`, { |
| | | value: 1 |
| | | }) |
| | | configExtensions.value.push(boundaryEventType.value) |
| | | // 超时处理类型 |
| | | timeoutHandlerType.value = bpmnInstances().moddle.create(`${prefix}:TimeoutHandlerType`, { |
| | | value: 1 |
| | | }) |
| | | configExtensions.value.push(timeoutHandlerType.value) |
| | | // 超时时间表达式 |
| | | timeDuration.value = 6 |
| | | timeUnit.value = 2 |
| | | maxRemindCount.value = 1 |
| | | timeModdle.value = bpmnInstances().moddle.create(`bpmn:Expression`, { |
| | | body: 'PT6H' |
| | | }) |
| | | eventDefinition.value.timeDuration = timeModdle.value |
| | | } else { |
| | | // 关闭自定义用户任务超时处理 |
| | | configExtensions.value = [] |
| | | delete eventDefinition.value.timeDuration |
| | | delete eventDefinition.value.timeCycle |
| | | } |
| | | updateElementExtensions() |
| | | } |
| | | |
| | | const onTimeoutHandlerTypeChanged = () => { |
| | | maxRemindCount.value = 1 |
| | | updateElementExtensions() |
| | | updateTimeModdle() |
| | | } |
| | | |
| | | const onTimeUnitChange = () => { |
| | | // 分钟,默认是 60 分钟 |
| | | if (timeUnit.value === TimeUnitType.MINUTE) { |
| | | timeDuration.value = 60 |
| | | } |
| | | // 小时,默认是 6 个小时 |
| | | if (timeUnit.value === TimeUnitType.HOUR) { |
| | | timeDuration.value = 6 |
| | | } |
| | | // 天, 默认 1天 |
| | | if (timeUnit.value === TimeUnitType.DAY) { |
| | | timeDuration.value = 1 |
| | | } |
| | | updateTimeModdle() |
| | | } |
| | | |
| | | const updateTimeModdle = () => { |
| | | if (maxRemindCount.value > 1) { |
| | | timeModdle.value.body = 'R' + maxRemindCount.value + '/' + isoTimeDuration() |
| | | if (!eventDefinition.value.timeCycle) { |
| | | delete eventDefinition.value.timeDuration |
| | | eventDefinition.value.timeCycle = timeModdle.value |
| | | } |
| | | } else { |
| | | timeModdle.value.body = isoTimeDuration() |
| | | if (!eventDefinition.value.timeDuration) { |
| | | delete eventDefinition.value.timeCycle |
| | | eventDefinition.value.timeDuration = timeModdle.value |
| | | } |
| | | } |
| | | } |
| | | |
| | | const isoTimeDuration = () => { |
| | | let strTimeDuration = 'PT' |
| | | if (timeUnit.value === TimeUnitType.MINUTE) { |
| | | strTimeDuration += timeDuration.value + 'M' |
| | | } |
| | | if (timeUnit.value === TimeUnitType.HOUR) { |
| | | strTimeDuration += timeDuration.value + 'H' |
| | | } |
| | | if (timeUnit.value === TimeUnitType.DAY) { |
| | | strTimeDuration += timeDuration.value + 'D' |
| | | } |
| | | return strTimeDuration |
| | | } |
| | | |
| | | const updateElementExtensions = () => { |
| | | const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', { |
| | | values: [...otherExtensions.value, ...configExtensions.value] |
| | | }) |
| | | bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { |
| | | extensionElements: extensions |
| | | }) |
| | | } |
| | | |
| | | watch( |
| | | () => props.id, |
| | | (val) => { |
| | | val && |
| | | val.length && |
| | | nextTick(() => { |
| | | resetElement() |
| | | }) |
| | | }, |
| | | { immediate: true } |
| | | ) |
| | | </script> |
| | | |
| | | <style lang="scss" scoped></style> |
对比新文件 |
| | |
| | | <!-- UserTask 自定义配置: |
| | | 1. 审批人与提交人为同一人时 |
| | | 2. 审批人拒绝时 |
| | | 3. 审批人为空时 |
| | | 4. 操作按钮 |
| | | 5. 字段权限 |
| | | 6. 审批类型 |
| | | --> |
| | | <template> |
| | | <div> |
| | | <el-divider content-position="left">审批类型</el-divider> |
| | | <el-form-item prop="approveType"> |
| | | <el-radio-group v-model="approveType.value"> |
| | | <el-radio |
| | | v-for="(item, index) in APPROVE_TYPE" |
| | | :key="index" |
| | | :value="item.value" |
| | | :label="item.value" |
| | | > |
| | | {{ item.label }} |
| | | </el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | |
| | | <el-divider content-position="left">审批人拒绝时</el-divider> |
| | | <el-form-item prop="rejectHandlerType"> |
| | | <el-radio-group |
| | | v-model="rejectHandlerType" |
| | | :disabled="returnTaskList.length === 0" |
| | | @change="updateRejectHandlerType" |
| | | > |
| | | <div class="flex-col"> |
| | | <div v-for="(item, index) in REJECT_HANDLER_TYPES" :key="index"> |
| | | <el-radio :key="item.value" :value="item.value" :label="item.label" /> |
| | | </div> |
| | | </div> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="rejectHandlerType == RejectHandlerType.RETURN_USER_TASK" |
| | | label="驳回节点" |
| | | prop="returnNodeId" |
| | | > |
| | | <el-select v-model="returnNodeId" clearable style="width: 100%" @change="updateReturnNodeId"> |
| | | <el-option |
| | | v-for="item in returnTaskList" |
| | | :key="item.id" |
| | | :label="item.name" |
| | | :value="item.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-divider content-position="left">审批人为空时</el-divider> |
| | | <el-form-item prop="assignEmptyHandlerType"> |
| | | <el-radio-group v-model="assignEmptyHandlerType" @change="updateAssignEmptyHandlerType"> |
| | | <div class="flex-col"> |
| | | <div v-for="(item, index) in ASSIGN_EMPTY_HANDLER_TYPES" :key="index"> |
| | | <el-radio :key="item.value" :value="item.value" :label="item.label" /> |
| | | </div> |
| | | </div> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="assignEmptyHandlerType == AssignEmptyHandlerType.ASSIGN_USER" |
| | | label="指定用户" |
| | | prop="assignEmptyHandlerUserIds" |
| | | span="24" |
| | | > |
| | | <el-select |
| | | v-model="assignEmptyUserIds" |
| | | clearable |
| | | multiple |
| | | style="width: 100%" |
| | | @change="updateAssignEmptyUserIds" |
| | | > |
| | | <el-option |
| | | v-for="item in userOptions" |
| | | :key="item.id" |
| | | :label="item.nickname" |
| | | :value="item.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-divider content-position="left">审批人与提交人为同一人时</el-divider> |
| | | <el-radio-group v-model="assignStartUserHandlerType" @change="updateAssignStartUserHandlerType"> |
| | | <div class="flex-col"> |
| | | <div v-for="(item, index) in ASSIGN_START_USER_HANDLER_TYPES" :key="index"> |
| | | <el-radio :key="item.value" :value="item.value" :label="item.label" /> |
| | | </div> |
| | | </div> |
| | | </el-radio-group> |
| | | |
| | | <el-divider content-position="left">操作按钮</el-divider> |
| | | <div class="button-setting-pane"> |
| | | <div class="button-setting-title"> |
| | | <div class="button-title-label">操作按钮</div> |
| | | <div class="pl-4 button-title-label">显示名称</div> |
| | | <div class="button-title-label">启用</div> |
| | | </div> |
| | | <div class="button-setting-item" v-for="(item, index) in buttonsSettingEl" :key="index"> |
| | | <div class="button-setting-item-label"> {{ OPERATION_BUTTON_NAME.get(item.id) }} </div> |
| | | <div class="button-setting-item-label"> |
| | | <input |
| | | type="text" |
| | | class="editable-title-input" |
| | | @blur="btnDisplayNameBlurEvent(index)" |
| | | v-mountedFocus |
| | | v-model="item.displayName" |
| | | :placeholder="item.displayName" |
| | | v-if="btnDisplayNameEdit[index]" |
| | | /> |
| | | <el-button v-else text @click="changeBtnDisplayName(index)" |
| | | >{{ item.displayName }} <Icon icon="ep:edit" |
| | | /></el-button> |
| | | </div> |
| | | <div class="button-setting-item-label"> |
| | | <el-switch v-model="item.enable" /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <el-divider content-position="left">字段权限</el-divider> |
| | | <div class="field-setting-pane" v-if="formType === 10"> |
| | | <div class="field-permit-title"> |
| | | <div class="setting-title-label first-title"> 字段名称 </div> |
| | | <div class="other-titles"> |
| | | <span class="setting-title-label">只读</span> |
| | | <span class="setting-title-label">可编辑</span> |
| | | <span class="setting-title-label">隐藏</span> |
| | | </div> |
| | | </div> |
| | | <div class="field-setting-item" v-for="(item, index) in fieldsPermissionEl" :key="index"> |
| | | <div class="field-setting-item-label"> {{ item.title }} </div> |
| | | <el-radio-group class="field-setting-item-group" v-model="item.permission"> |
| | | <div class="item-radio-wrap"> |
| | | <el-radio |
| | | :value="FieldPermissionType.READ" |
| | | size="large" |
| | | :label="FieldPermissionType.READ" |
| | | ><span></span |
| | | ></el-radio> |
| | | </div> |
| | | <div class="item-radio-wrap"> |
| | | <el-radio |
| | | :value="FieldPermissionType.WRITE" |
| | | size="large" |
| | | :label="FieldPermissionType.WRITE" |
| | | ><span></span |
| | | ></el-radio> |
| | | </div> |
| | | <div class="item-radio-wrap"> |
| | | <el-radio |
| | | :value="FieldPermissionType.NONE" |
| | | size="large" |
| | | :label="FieldPermissionType.NONE" |
| | | ><span></span |
| | | ></el-radio> |
| | | </div> |
| | | </el-radio-group> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | import { |
| | | ASSIGN_START_USER_HANDLER_TYPES, |
| | | RejectHandlerType, |
| | | REJECT_HANDLER_TYPES, |
| | | ASSIGN_EMPTY_HANDLER_TYPES, |
| | | AssignEmptyHandlerType, |
| | | OPERATION_BUTTON_NAME, |
| | | DEFAULT_BUTTON_SETTING, |
| | | FieldPermissionType, |
| | | APPROVE_TYPE, |
| | | ApproveType, |
| | | ButtonSetting |
| | | } from '@/components/SimpleProcessDesignerV2/src/consts' |
| | | import * as UserApi from '@/api/system/user' |
| | | import { useFormFieldsPermission } from '@/components/SimpleProcessDesignerV2/src/node' |
| | | |
| | | defineOptions({ name: 'ElementCustomConfig4UserTask' }) |
| | | const props = defineProps({ |
| | | id: String, |
| | | type: String |
| | | }) |
| | | const prefix = inject('prefix') |
| | | |
| | | // 审批人与提交人为同一人时 |
| | | const assignStartUserHandlerTypeEl = ref() |
| | | const assignStartUserHandlerType = ref() |
| | | |
| | | // 审批人拒绝时 |
| | | const rejectHandlerTypeEl = ref() |
| | | const rejectHandlerType = ref() |
| | | const returnNodeIdEl = ref() |
| | | const returnNodeId = ref() |
| | | const returnTaskList = ref([]) |
| | | |
| | | // 审批人为空时 |
| | | const assignEmptyHandlerTypeEl = ref() |
| | | const assignEmptyHandlerType = ref() |
| | | const assignEmptyUserIdsEl = ref() |
| | | const assignEmptyUserIds = ref() |
| | | |
| | | // 操作按钮 |
| | | const buttonsSettingEl = ref() |
| | | const { btnDisplayNameEdit, changeBtnDisplayName, btnDisplayNameBlurEvent } = useButtonsSetting() |
| | | |
| | | // 字段权限 |
| | | const fieldsPermissionEl = ref([]) |
| | | const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission( |
| | | FieldPermissionType.READ |
| | | ) |
| | | |
| | | // 审批类型 |
| | | const approveType = ref({ value: ApproveType.USER }) |
| | | |
| | | const elExtensionElements = ref() |
| | | const otherExtensions = ref() |
| | | const bpmnElement = ref() |
| | | const bpmnInstances = () => (window as any)?.bpmnInstances |
| | | |
| | | const resetCustomConfigList = () => { |
| | | bpmnElement.value = bpmnInstances().bpmnElement |
| | | |
| | | // 获取可回退的列表 |
| | | returnTaskList.value = findAllPredecessorsExcludingStart( |
| | | bpmnElement.value.id, |
| | | bpmnInstances().modeler |
| | | ) |
| | | |
| | | // 获取元素扩展属性 或者 创建扩展属性 |
| | | elExtensionElements.value = |
| | | bpmnElement.value.businessObject?.extensionElements ?? |
| | | bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] }) |
| | | |
| | | // 审批类型 |
| | | approveType.value = |
| | | elExtensionElements.value.values?.filter((ex) => ex.$type === `${prefix}:ApproveType`)?.[0] || |
| | | bpmnInstances().moddle.create(`${prefix}:ApproveType`, { value: ApproveType.USER }) |
| | | |
| | | // 审批人与提交人为同一人时 |
| | | assignStartUserHandlerTypeEl.value = |
| | | elExtensionElements.value.values?.filter( |
| | | (ex) => ex.$type === `${prefix}:AssignStartUserHandlerType` |
| | | )?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignStartUserHandlerType`, { value: 1 }) |
| | | assignStartUserHandlerType.value = assignStartUserHandlerTypeEl.value.value |
| | | |
| | | // 审批人拒绝时 |
| | | rejectHandlerTypeEl.value = |
| | | elExtensionElements.value.values?.filter( |
| | | (ex) => ex.$type === `${prefix}:RejectHandlerType` |
| | | )?.[0] || bpmnInstances().moddle.create(`${prefix}:RejectHandlerType`, { value: 1 }) |
| | | rejectHandlerType.value = rejectHandlerTypeEl.value.value |
| | | returnNodeIdEl.value = |
| | | elExtensionElements.value.values?.filter( |
| | | (ex) => ex.$type === `${prefix}:RejectReturnTaskId` |
| | | )?.[0] || bpmnInstances().moddle.create(`${prefix}:RejectReturnTaskId`, { value: '' }) |
| | | returnNodeId.value = returnNodeIdEl.value.value |
| | | |
| | | // 审批人为空时 |
| | | assignEmptyHandlerTypeEl.value = |
| | | elExtensionElements.value.values?.filter( |
| | | (ex) => ex.$type === `${prefix}:AssignEmptyHandlerType` |
| | | )?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignEmptyHandlerType`, { value: 1 }) |
| | | assignEmptyHandlerType.value = assignEmptyHandlerTypeEl.value.value |
| | | assignEmptyUserIdsEl.value = |
| | | elExtensionElements.value.values?.filter( |
| | | (ex) => ex.$type === `${prefix}:AssignEmptyUserIds` |
| | | )?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignEmptyUserIds`, { value: '' }) |
| | | assignEmptyUserIds.value = assignEmptyUserIdsEl.value.value?.split(',').map((item) => { |
| | | // 如果数字超出了最大安全整数范围,则将其作为字符串处理 |
| | | let num = Number(item) |
| | | return num > Number.MAX_SAFE_INTEGER || num < -Number.MAX_SAFE_INTEGER ? item : num |
| | | }) |
| | | |
| | | // 操作按钮 |
| | | buttonsSettingEl.value = elExtensionElements.value.values?.filter( |
| | | (ex) => ex.$type === `${prefix}:ButtonsSetting` |
| | | ) |
| | | if (buttonsSettingEl.value.length === 0) { |
| | | DEFAULT_BUTTON_SETTING.forEach((item) => { |
| | | buttonsSettingEl.value.push( |
| | | bpmnInstances().moddle.create(`${prefix}:ButtonsSetting`, { |
| | | 'flowable:id': item.id, |
| | | 'flowable:displayName': item.displayName, |
| | | 'flowable:enable': item.enable |
| | | }) |
| | | ) |
| | | }) |
| | | } |
| | | |
| | | // 字段权限 |
| | | if (formType.value === 10) { |
| | | const fieldsPermissionList = elExtensionElements.value.values?.filter( |
| | | (ex) => ex.$type === `${prefix}:FieldsPermission` |
| | | ) |
| | | fieldsPermissionEl.value = [] |
| | | getNodeConfigFormFields() |
| | | // 由于默认添加了发起人元素,这里需要删掉 |
| | | fieldsPermissionConfig.value = fieldsPermissionConfig.value.slice(1) |
| | | fieldsPermissionConfig.value.forEach((element) => { |
| | | element.permission = |
| | | fieldsPermissionList?.find((obj) => obj.field === element.field)?.permission ?? '1' |
| | | fieldsPermissionEl.value.push( |
| | | bpmnInstances().moddle.create(`${prefix}:FieldsPermission`, element) |
| | | ) |
| | | }) |
| | | } |
| | | |
| | | // 保留剩余扩展元素,便于后面更新该元素对应属性 |
| | | otherExtensions.value = |
| | | elExtensionElements.value.values?.filter( |
| | | (ex) => |
| | | ex.$type !== `${prefix}:AssignStartUserHandlerType` && |
| | | ex.$type !== `${prefix}:RejectHandlerType` && |
| | | ex.$type !== `${prefix}:RejectReturnTaskId` && |
| | | ex.$type !== `${prefix}:AssignEmptyHandlerType` && |
| | | ex.$type !== `${prefix}:AssignEmptyUserIds` && |
| | | ex.$type !== `${prefix}:ButtonsSetting` && |
| | | ex.$type !== `${prefix}:FieldsPermission` && |
| | | ex.$type !== `${prefix}:ApproveType` |
| | | ) ?? [] |
| | | |
| | | // 更新元素扩展属性,避免后续报错 |
| | | updateElementExtensions() |
| | | } |
| | | |
| | | const updateAssignStartUserHandlerType = () => { |
| | | assignStartUserHandlerTypeEl.value.value = assignStartUserHandlerType.value |
| | | |
| | | updateElementExtensions() |
| | | } |
| | | |
| | | const updateRejectHandlerType = () => { |
| | | rejectHandlerTypeEl.value.value = rejectHandlerType.value |
| | | |
| | | returnNodeId.value = returnTaskList.value[0].id |
| | | returnNodeIdEl.value.value = returnNodeId.value |
| | | |
| | | updateElementExtensions() |
| | | } |
| | | |
| | | const updateReturnNodeId = () => { |
| | | returnNodeIdEl.value.value = returnNodeId.value |
| | | |
| | | updateElementExtensions() |
| | | } |
| | | |
| | | const updateAssignEmptyHandlerType = () => { |
| | | assignEmptyHandlerTypeEl.value.value = assignEmptyHandlerType.value |
| | | |
| | | updateElementExtensions() |
| | | } |
| | | |
| | | const updateAssignEmptyUserIds = () => { |
| | | assignEmptyUserIdsEl.value.value = assignEmptyUserIds.value.toString() |
| | | |
| | | updateElementExtensions() |
| | | } |
| | | |
| | | const updateElementExtensions = () => { |
| | | const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', { |
| | | values: [ |
| | | ...otherExtensions.value, |
| | | assignStartUserHandlerTypeEl.value, |
| | | rejectHandlerTypeEl.value, |
| | | returnNodeIdEl.value, |
| | | assignEmptyHandlerTypeEl.value, |
| | | assignEmptyUserIdsEl.value, |
| | | approveType.value, |
| | | ...buttonsSettingEl.value, |
| | | ...fieldsPermissionEl.value |
| | | ] |
| | | }) |
| | | bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { |
| | | extensionElements: extensions |
| | | }) |
| | | } |
| | | |
| | | watch( |
| | | () => props.id, |
| | | (val) => { |
| | | val && |
| | | val.length && |
| | | nextTick(() => { |
| | | resetCustomConfigList() |
| | | }) |
| | | }, |
| | | { immediate: true } |
| | | ) |
| | | |
| | | function findAllPredecessorsExcludingStart(elementId, modeler) { |
| | | const elementRegistry = modeler.get('elementRegistry') |
| | | const allConnections = elementRegistry.filter((element) => element.type === 'bpmn:SequenceFlow') |
| | | const predecessors = new Set() // 使用 Set 来避免重复节点 |
| | | const visited = new Set() // 用于记录已访问的节点 |
| | | |
| | | // 检查是否是开始事件节点 |
| | | function isStartEvent(element) { |
| | | return element.type === 'bpmn:StartEvent' |
| | | } |
| | | |
| | | function findPredecessorsRecursively(element) { |
| | | // 如果该节点已经访问过,直接返回,避免循环 |
| | | if (visited.has(element)) { |
| | | return |
| | | } |
| | | |
| | | // 标记当前节点为已访问 |
| | | visited.add(element) |
| | | |
| | | // 获取与当前节点相连的所有连接 |
| | | const incomingConnections = allConnections.filter((connection) => connection.target === element) |
| | | |
| | | incomingConnections.forEach((connection) => { |
| | | const source = connection.source // 获取前置节点 |
| | | |
| | | // 只添加不是开始事件的前置节点 |
| | | if (!isStartEvent(source)) { |
| | | predecessors.add(source.businessObject) |
| | | // 递归查找前置节点 |
| | | findPredecessorsRecursively(source) |
| | | } |
| | | }) |
| | | } |
| | | |
| | | const targetElement = elementRegistry.get(elementId) |
| | | if (targetElement) { |
| | | findPredecessorsRecursively(targetElement) |
| | | } |
| | | |
| | | return Array.from(predecessors) // 返回前置节点数组 |
| | | } |
| | | |
| | | function useButtonsSetting() { |
| | | const buttonsSetting = ref<ButtonSetting[]>() |
| | | // 操作按钮显示名称可编辑 |
| | | const btnDisplayNameEdit = ref<boolean[]>([]) |
| | | const changeBtnDisplayName = (index: number) => { |
| | | btnDisplayNameEdit.value[index] = true |
| | | } |
| | | const btnDisplayNameBlurEvent = (index: number) => { |
| | | btnDisplayNameEdit.value[index] = false |
| | | const buttonItem = buttonsSetting.value![index] |
| | | buttonItem.displayName = buttonItem.displayName || OPERATION_BUTTON_NAME.get(buttonItem.id)! |
| | | } |
| | | return { |
| | | buttonsSetting, |
| | | btnDisplayNameEdit, |
| | | changeBtnDisplayName, |
| | | btnDisplayNameBlurEvent |
| | | } |
| | | } |
| | | |
| | | const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表 |
| | | onMounted(async () => { |
| | | // 获得用户列表 |
| | | userOptions.value = await UserApi.getSimpleUserList() |
| | | }) |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .button-setting-pane { |
| | | display: flex; |
| | | flex-direction: column; |
| | | font-size: 14px; |
| | | margin-top: 8px; |
| | | |
| | | .button-setting-desc { |
| | | padding-right: 8px; |
| | | margin-bottom: 16px; |
| | | font-size: 16px; |
| | | font-weight: 700; |
| | | } |
| | | |
| | | .button-setting-title { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | height: 45px; |
| | | padding-left: 12px; |
| | | background-color: #f8fafc0a; |
| | | border: 1px solid #1f38581a; |
| | | |
| | | & > :first-child { |
| | | width: 100px !important; |
| | | text-align: left !important; |
| | | } |
| | | |
| | | & > :last-child { |
| | | text-align: center !important; |
| | | } |
| | | |
| | | .button-title-label { |
| | | width: 150px; |
| | | font-size: 13px; |
| | | font-weight: 700; |
| | | color: #000; |
| | | text-align: left; |
| | | } |
| | | } |
| | | |
| | | .button-setting-item { |
| | | align-items: center; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | height: 38px; |
| | | padding-left: 12px; |
| | | border: 1px solid #1f38581a; |
| | | border-top: 0; |
| | | |
| | | & > :first-child { |
| | | width: 100px !important; |
| | | } |
| | | |
| | | & > :last-child { |
| | | text-align: center !important; |
| | | } |
| | | |
| | | .button-setting-item-label { |
| | | width: 150px; |
| | | overflow: hidden; |
| | | text-align: left; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .editable-title-input { |
| | | height: 24px; |
| | | max-width: 130px; |
| | | margin-left: 4px; |
| | | line-height: 24px; |
| | | border: 1px solid #d9d9d9; |
| | | border-radius: 4px; |
| | | transition: all 0.3s; |
| | | |
| | | &:focus { |
| | | border-color: #40a9ff; |
| | | outline: 0; |
| | | box-shadow: 0 0 0 2px rgb(24 144 255 / 20%); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .field-setting-pane { |
| | | display: flex; |
| | | flex-direction: column; |
| | | font-size: 14px; |
| | | |
| | | .field-setting-desc { |
| | | padding-right: 8px; |
| | | margin-bottom: 16px; |
| | | font-size: 16px; |
| | | font-weight: 700; |
| | | } |
| | | |
| | | .field-permit-title { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | height: 45px; |
| | | padding-left: 12px; |
| | | line-height: 45px; |
| | | background-color: #f8fafc0a; |
| | | border: 1px solid #1f38581a; |
| | | |
| | | .first-title { |
| | | text-align: left !important; |
| | | } |
| | | |
| | | .other-titles { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | } |
| | | |
| | | .setting-title-label { |
| | | display: inline-block; |
| | | width: 100px; |
| | | padding: 5px 0; |
| | | font-size: 13px; |
| | | font-weight: 700; |
| | | color: #000; |
| | | text-align: center; |
| | | } |
| | | } |
| | | |
| | | .field-setting-item { |
| | | align-items: center; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | height: 38px; |
| | | padding-left: 12px; |
| | | border: 1px solid #1f38581a; |
| | | border-top: 0; |
| | | |
| | | .field-setting-item-label { |
| | | display: inline-block; |
| | | width: 100px; |
| | | min-height: 16px; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | cursor: text; |
| | | } |
| | | |
| | | .field-setting-item-group { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | |
| | | .item-radio-wrap { |
| | | display: inline-block; |
| | | width: 100px; |
| | | text-align: center; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | </style> |
对比新文件 |
| | |
| | | import UserTaskCustomConfig from './components/UserTaskCustomConfig.vue' |
| | | import BoundaryEventTimer from './components/BoundaryEventTimer.vue' |
| | | |
| | | export const CustomConfigMap = { |
| | | UserTask: { |
| | | name: '用户任务', |
| | | componet: UserTaskCustomConfig |
| | | }, |
| | | BoundaryEventTimerEventDefinition: { |
| | | name: '定时边界事件(非中断)', |
| | | componet: BoundaryEventTimer |
| | | } |
| | | } |
| | |
| | | } |
| | | // 打开 监听器详情 侧边栏 |
| | | const openListenerForm = (listener, index?) => { |
| | | // debugger |
| | | console.log(listener) |
| | | if (listener) { |
| | | listenerForm.value = initListenerForm(listener) |
| | | editingListenerIndex.value = index |
| | |
| | | const bpmnInstances = () => (window as any)?.bpmnInstances |
| | | |
| | | const resetListenersList = () => { |
| | | console.log( |
| | | bpmnInstances().bpmnElement, |
| | | 'window.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElement' |
| | | ) |
| | | bpmnElement.value = bpmnInstances().bpmnElement |
| | | otherExtensionList.value = [] |
| | | bpmnElementListeners.value = |
| | | bpmnElement.value.businessObject?.extensionElements?.values.filter( |
| | | (ex) => ex.$type === `${prefix}:TaskListener` |
| | | ) ?? [] |
| | | console.log(bpmnElementListeners.value.map) |
| | | elementListenersList.value = bpmnElementListeners.value.map((listener) => |
| | | initListenerType(listener) |
| | | ) |
| | |
| | | // 初始化表单数据 |
| | | export function initListenerForm(listener) { |
| | | console.log(listener) |
| | | let self = { |
| | | ...listener |
| | | } |
| | |
| | | } |
| | | |
| | | export function initListenerType(listener) { |
| | | listener.id = listener.$attrs.id |
| | | let listenerType |
| | | if (listener.class) listenerType = 'classListener' |
| | | if (listener.expression) listenerType = 'expressionListener' |
| | |
| | | <template> |
| | | <div class="panel-tab__content"> |
| | | <el-form label-width="90px"> |
| | | <el-radio-group v-model="approveMethod" @change="onApproveMethodChange"> |
| | | <div class="flex-col"> |
| | | <div v-for="(item, index) in APPROVE_METHODS" :key="index"> |
| | | <el-radio :value="item.value" :label="item.value"> |
| | | {{ item.label }} |
| | | </el-radio> |
| | | <el-form-item prop="approveRatio"> |
| | | <el-input-number |
| | | v-model="approveRatio" |
| | | :min="10" |
| | | :max="100" |
| | | :step="10" |
| | | size="small" |
| | | v-if=" |
| | | item.value === ApproveMethodType.APPROVE_BY_RATIO && |
| | | approveMethod === ApproveMethodType.APPROVE_BY_RATIO |
| | | " |
| | | @change="onApproveRatioChange" |
| | | /> |
| | | </el-form-item> |
| | | </div> |
| | | </div> |
| | | </el-radio-group> |
| | | <!-- 与Simple设计器配置合并,保留以前的代码 --> |
| | | <el-form label-width="90px" style="display: none"> |
| | | <el-form-item label="快捷配置"> |
| | | <el-button size="small" @click="changeConfig('依次审批')">依次审批</el-button> |
| | | <el-button size="small" @click="changeConfig('会签')">会签</el-button> |
| | |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | import { ApproveMethodType, APPROVE_METHODS } from '@/components/SimpleProcessDesignerV2/src/consts' |
| | | |
| | | defineOptions({ name: 'ElementMultiInstance' }) |
| | | |
| | | const props = defineProps({ |
| | | businessObject: Object, |
| | | type: String |
| | | type: String, |
| | | id: String |
| | | }) |
| | | const prefix = inject('prefix') |
| | | const loopCharacteristics = ref('') |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * -----新版本多实例----- |
| | | */ |
| | | const approveMethod = ref() |
| | | const approveRatio = ref(100) |
| | | const otherExtensions = ref() |
| | | const getElementLoopNew = () => { |
| | | const extensionElements = |
| | | bpmnElement.value.businessObject?.extensionElements ?? |
| | | bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] }) |
| | | approveMethod.value = extensionElements.values.filter( |
| | | (ex) => ex.$type === `${prefix}:ApproveMethod` |
| | | )?.[0]?.value |
| | | |
| | | otherExtensions.value = |
| | | extensionElements.values.filter((ex) => ex.$type !== `${prefix}:ApproveMethod`) ?? [] |
| | | |
| | | if (!approveMethod.value) { |
| | | approveMethod.value = ApproveMethodType.SEQUENTIAL_APPROVE |
| | | updateLoopCharacteristics() |
| | | } |
| | | } |
| | | const onApproveMethodChange = () => { |
| | | approveRatio.value = 100 |
| | | updateLoopCharacteristics() |
| | | } |
| | | const onApproveRatioChange = () => { |
| | | updateLoopCharacteristics() |
| | | } |
| | | const updateLoopCharacteristics = () => { |
| | | // 根据ApproveMethod生成multiInstanceLoopCharacteristics节点 |
| | | if (approveMethod.value === ApproveMethodType.RANDOM_SELECT_ONE_APPROVE) { |
| | | bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { |
| | | loopCharacteristics: null |
| | | }) |
| | | } else { |
| | | if (approveMethod.value === ApproveMethodType.APPROVE_BY_RATIO) { |
| | | multiLoopInstance.value = bpmnInstances().moddle.create( |
| | | 'bpmn:MultiInstanceLoopCharacteristics', |
| | | { isSequential: false, collection: '${coll_userList}' } |
| | | ) |
| | | multiLoopInstance.value.completionCondition = bpmnInstances().moddle.create( |
| | | 'bpmn:FormalExpression', |
| | | { |
| | | body: '${ nrOfCompletedInstances/nrOfInstances >= ' + approveRatio.value / 100 + '}' |
| | | } |
| | | ) |
| | | } |
| | | if (approveMethod.value === ApproveMethodType.ANY_APPROVE) { |
| | | multiLoopInstance.value = bpmnInstances().moddle.create( |
| | | 'bpmn:MultiInstanceLoopCharacteristics', |
| | | { isSequential: false, collection: '${coll_userList}' } |
| | | ) |
| | | multiLoopInstance.value.completionCondition = bpmnInstances().moddle.create( |
| | | 'bpmn:FormalExpression', |
| | | { |
| | | body: '${ nrOfCompletedInstances > 0 }' |
| | | } |
| | | ) |
| | | } |
| | | if (approveMethod.value === ApproveMethodType.SEQUENTIAL_APPROVE) { |
| | | multiLoopInstance.value = bpmnInstances().moddle.create( |
| | | 'bpmn:MultiInstanceLoopCharacteristics', |
| | | { isSequential: true, collection: '${coll_userList}' } |
| | | ) |
| | | multiLoopInstance.value.loopCardinality = bpmnInstances().moddle.create( |
| | | 'bpmn:FormalExpression', |
| | | { |
| | | body: '1' |
| | | } |
| | | ) |
| | | multiLoopInstance.value.completionCondition = bpmnInstances().moddle.create( |
| | | 'bpmn:FormalExpression', |
| | | { |
| | | body: '${ nrOfCompletedInstances >= nrOfInstances }' |
| | | } |
| | | ) |
| | | } |
| | | bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { |
| | | loopCharacteristics: toRaw(multiLoopInstance.value) |
| | | }) |
| | | } |
| | | |
| | | // 添加ApproveMethod到ExtensionElements |
| | | const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', { |
| | | values: [ |
| | | ...otherExtensions.value, |
| | | bpmnInstances().moddle.create(`${prefix}:ApproveMethod`, { |
| | | value: approveMethod.value |
| | | }) |
| | | ] |
| | | }) |
| | | bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { |
| | | extensionElements: extensions |
| | | }) |
| | | } |
| | | |
| | | onBeforeUnmount(() => { |
| | | multiLoopInstance.value = null |
| | | bpmnElement.value = null |
| | | }) |
| | | |
| | | watch( |
| | | () => props.businessObject, |
| | | () => props.id, |
| | | (val) => { |
| | | bpmnElement.value = bpmnInstances().bpmnElement |
| | | getElementLoop(val) |
| | | if (val) { |
| | | nextTick(() => { |
| | | bpmnElement.value = bpmnInstances().bpmnElement |
| | | // getElementLoop(val) |
| | | getElementLoopNew() |
| | | }) |
| | | } |
| | | }, |
| | | { immediate: true } |
| | | ) |
| | |
| | | const bpmnInstances = () => (window as any)?.bpmnInstances |
| | | |
| | | const resetAttributesList = () => { |
| | | console.log(window, 'windowwindowwindowwindowwindowwindowwindow') |
| | | bpmnElement.value = bpmnInstances().bpmnElement |
| | | otherExtensionList.value = [] // 其他扩展配置 |
| | | bpmnElementProperties.value = |
| | |
| | | otherExtensionList.value.push(ex) |
| | | } |
| | | return ex.$type === `${prefix}:Properties` |
| | | }) ?? [] |
| | | }) ?? []; |
| | | |
| | | // 保存所有的 扩展属性字段 |
| | | bpmnElementPropertyList.value = bpmnElementProperties.value.reduce( |
| | |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | import UserTask from './task-components/UserTask.vue' |
| | | import ScriptTask from './task-components/ScriptTask.vue' |
| | | import ReceiveTask from './task-components/ReceiveTask.vue' |
| | | import { installedComponent } from './data' |
| | | |
| | | defineOptions({ name: 'ElementTaskConfig' }) |
| | | |
| | |
| | | exclusive: false |
| | | }) |
| | | const witchTaskComponent = ref() |
| | | const installedComponent = ref({ |
| | | // 手工任务与普通任务一致,不需要其他配置 |
| | | // 接收消息任务,需要在全局下插入新的消息实例,并在该节点下的 messageRef 属性绑定该实例 |
| | | // 发送任务、服务任务、业务规则任务共用一个相同配置 |
| | | UserTask: 'UserTask', // 用户任务配置 |
| | | ScriptTask: 'ScriptTask', // 脚本任务配置 |
| | | ReceiveTask: 'ReceiveTask' // 消息接收任务 |
| | | }) |
| | | |
| | | const bpmnElement = ref() |
| | | |
| | | const bpmnInstances = () => (window as any).bpmnInstances |
| | |
| | | watch( |
| | | () => props.type, |
| | | () => { |
| | | // witchTaskComponent.value = installedComponent.value[props.type] |
| | | if (props.type == installedComponent.value.UserTask) { |
| | | witchTaskComponent.value = UserTask |
| | | } |
| | | if (props.type == installedComponent.value.ScriptTask) { |
| | | witchTaskComponent.value = ScriptTask |
| | | } |
| | | if (props.type == installedComponent.value.ReceiveTask) { |
| | | witchTaskComponent.value = ReceiveTask |
| | | if (props.type) { |
| | | witchTaskComponent.value = installedComponent[props.type].component |
| | | } |
| | | }, |
| | | { immediate: true } |
对比新文件 |
| | |
| | | import UserTask from './task-components/UserTask.vue' |
| | | import ServiceTask from './task-components/ServiceTask.vue' |
| | | import ScriptTask from './task-components/ScriptTask.vue' |
| | | import ReceiveTask from './task-components/ReceiveTask.vue' |
| | | import CallActivity from './task-components/CallActivity.vue' |
| | | |
| | | export const installedComponent = { |
| | | UserTask: { |
| | | name: '用户任务', |
| | | component: UserTask |
| | | }, |
| | | ServiceTask: { |
| | | name: '服务任务', |
| | | component: ServiceTask |
| | | }, |
| | | ScriptTask: { |
| | | name: '脚本任务', |
| | | component: ScriptTask |
| | | }, |
| | | ReceiveTask: { |
| | | name: '接收任务', |
| | | component: ReceiveTask |
| | | }, |
| | | CallActivity: { |
| | | name: '调用活动', |
| | | component: CallActivity |
| | | } |
| | | } |
| | | |
| | | export const getTaskCollapseItemName = (elementType) => { |
| | | return installedComponent[elementType].name |
| | | } |
| | | |
| | | export const isTaskCollapseItemShow = (elementType) => { |
| | | return installedComponent[elementType] |
| | | } |
对比新文件 |
| | |
| | | <template> |
| | | <div> |
| | | <el-form label-width="100px"> |
| | | <el-form-item label="实例名称" prop="processInstanceName"> |
| | | <el-input |
| | | v-model="formData.processInstanceName" |
| | | clearable |
| | | placeholder="请输入实例名称" |
| | | @change="updateCallActivityAttr('processInstanceName')" |
| | | /> |
| | | </el-form-item> |
| | | |
| | | <!-- TODO 需要可选择已存在的流程 --> |
| | | <el-form-item label="被调用流程" prop="calledElement"> |
| | | <el-input |
| | | v-model="formData.calledElement" |
| | | clearable |
| | | placeholder="请输入被调用流程" |
| | | @change="updateCallActivityAttr('calledElement')" |
| | | /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="继承变量" prop="inheritVariables"> |
| | | <el-switch |
| | | v-model="formData.inheritVariables" |
| | | @change="updateCallActivityAttr('inheritVariables')" |
| | | /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="继承业务键" prop="inheritBusinessKey"> |
| | | <el-switch |
| | | v-model="formData.inheritBusinessKey" |
| | | @change="updateCallActivityAttr('inheritBusinessKey')" |
| | | /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item v-if="!formData.inheritBusinessKey" label="业务键表达式" prop="businessKey"> |
| | | <el-input |
| | | v-model="formData.businessKey" |
| | | clearable |
| | | placeholder="请输入业务键表达式" |
| | | @change="updateCallActivityAttr('businessKey')" |
| | | /> |
| | | </el-form-item> |
| | | |
| | | <el-divider /> |
| | | <div> |
| | | <div class="flex mb-10px"> |
| | | <el-text>输入参数</el-text> |
| | | <XButton |
| | | class="ml-auto" |
| | | type="primary" |
| | | preIcon="ep:plus" |
| | | title="添加参数" |
| | | size="small" |
| | | @click="openVariableForm('in', null, -1)" |
| | | /> |
| | | </div> |
| | | <el-table :data="inVariableList" max-height="240" fit border> |
| | | <el-table-column label="源" prop="source" min-width="100px" show-overflow-tooltip /> |
| | | <el-table-column label="目标" prop="target" min-width="100px" show-overflow-tooltip /> |
| | | <el-table-column label="操作" width="110px"> |
| | | <template #default="scope"> |
| | | <el-button link @click="openVariableForm('in', scope.row, scope.$index)" size="small"> |
| | | 编辑 |
| | | </el-button> |
| | | <el-divider direction="vertical" /> |
| | | <el-button |
| | | link |
| | | size="small" |
| | | style="color: #ff4d4f" |
| | | @click="removeVariable('in', scope.$index)" |
| | | > |
| | | 移除 |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | |
| | | <el-divider /> |
| | | <div> |
| | | <div class="flex mb-10px"> |
| | | <el-text>输出参数</el-text> |
| | | <XButton |
| | | class="ml-auto" |
| | | type="primary" |
| | | preIcon="ep:plus" |
| | | title="添加参数" |
| | | size="small" |
| | | @click="openVariableForm('out', null, -1)" |
| | | /> |
| | | </div> |
| | | <el-table :data="outVariableList" max-height="240" fit border> |
| | | <el-table-column label="源" prop="source" min-width="100px" show-overflow-tooltip /> |
| | | <el-table-column label="目标" prop="target" min-width="100px" show-overflow-tooltip /> |
| | | <el-table-column label="操作" width="110px"> |
| | | <template #default="scope"> |
| | | <el-button |
| | | link |
| | | @click="openVariableForm('out', scope.row, scope.$index)" |
| | | size="small" |
| | | > |
| | | 编辑 |
| | | </el-button> |
| | | <el-divider direction="vertical" /> |
| | | <el-button |
| | | link |
| | | size="small" |
| | | style="color: #ff4d4f" |
| | | @click="removeVariable('out', scope.$index)" |
| | | > |
| | | 移除 |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </el-form> |
| | | |
| | | <!-- 添加或修改参数 --> |
| | | <el-dialog |
| | | v-model="variableDialogVisible" |
| | | title="参数配置" |
| | | width="600px" |
| | | append-to-body |
| | | destroy-on-close |
| | | > |
| | | <el-form :model="varialbeFormData" label-width="80px" ref="varialbeFormRef"> |
| | | <el-form-item label="源:" prop="source"> |
| | | <el-input v-model="varialbeFormData.source" clearable /> |
| | | </el-form-item> |
| | | <el-form-item label="目标:" prop="target"> |
| | | <el-input v-model="varialbeFormData.target" clearable /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <el-button @click="variableDialogVisible = false">取 消</el-button> |
| | | <el-button type="primary" @click="saveVariable">确 定</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | defineOptions({ name: 'CallActivity' }) |
| | | const props = defineProps({ |
| | | id: String, |
| | | type: String |
| | | }) |
| | | const prefix = inject('prefix') |
| | | const message = useMessage() |
| | | |
| | | const formData = ref({ |
| | | processInstanceName: '', |
| | | calledElement: '', |
| | | inheritVariables: false, |
| | | businessKey: '', |
| | | inheritBusinessKey: false, |
| | | calledElementType: 'key' |
| | | }) |
| | | const inVariableList = ref() |
| | | const outVariableList = ref() |
| | | const variableType = ref() // 参数类型 |
| | | const editingVariableIndex = ref(-1) // 编辑参数下标 |
| | | const variableDialogVisible = ref(false) |
| | | const varialbeFormRef = ref() |
| | | const varialbeFormData = ref({ |
| | | source: '', |
| | | target: '' |
| | | }) |
| | | |
| | | const bpmnInstances = () => (window as any)?.bpmnInstances |
| | | const bpmnElement = ref() |
| | | const otherExtensionList = ref() |
| | | |
| | | const initCallActivity = () => { |
| | | bpmnElement.value = bpmnInstances().bpmnElement |
| | | console.log(bpmnElement.value.businessObject, 'callActivity') |
| | | |
| | | // 初始化所有配置项 |
| | | Object.keys(formData.value).forEach((key) => { |
| | | formData.value[key] = bpmnElement.value.businessObject[key] ?? formData.value[key] |
| | | }) |
| | | |
| | | otherExtensionList.value = [] // 其他扩展配置 |
| | | inVariableList.value = [] |
| | | outVariableList.value = [] |
| | | // 初始化输入参数 |
| | | bpmnElement.value.businessObject?.extensionElements?.values?.forEach((ex) => { |
| | | if (ex.$type === `${prefix}:In`) { |
| | | inVariableList.value.push(ex) |
| | | } else if (ex.$type === `${prefix}:Out`) { |
| | | outVariableList.value.push(ex) |
| | | } else { |
| | | otherExtensionList.value.push(ex) |
| | | } |
| | | }) |
| | | |
| | | // 默认添加 |
| | | // bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { |
| | | // calledElementType: 'key' |
| | | // }) |
| | | } |
| | | |
| | | const updateCallActivityAttr = (attr) => { |
| | | bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { |
| | | [attr]: formData.value[attr] |
| | | }) |
| | | } |
| | | |
| | | const openVariableForm = (type, data, index) => { |
| | | editingVariableIndex.value = index |
| | | variableType.value = type |
| | | varialbeFormData.value = index === -1 ? {} : { ...data } |
| | | variableDialogVisible.value = true |
| | | } |
| | | |
| | | const removeVariable = async (type, index) => { |
| | | try { |
| | | await message.delConfirm() |
| | | if (type === 'in') { |
| | | inVariableList.value.splice(index, 1) |
| | | } |
| | | if (type === 'out') { |
| | | outVariableList.value.splice(index, 1) |
| | | } |
| | | updateElementExtensions() |
| | | } catch {} |
| | | } |
| | | |
| | | const saveVariable = () => { |
| | | if (editingVariableIndex.value === -1) { |
| | | if (variableType.value === 'in') { |
| | | inVariableList.value.push( |
| | | bpmnInstances().moddle.create(`${prefix}:In`, { ...varialbeFormData.value }) |
| | | ) |
| | | } |
| | | if (variableType.value === 'out') { |
| | | outVariableList.value.push( |
| | | bpmnInstances().moddle.create(`${prefix}:Out`, { ...varialbeFormData.value }) |
| | | ) |
| | | } |
| | | updateElementExtensions() |
| | | } else { |
| | | if (variableType.value === 'in') { |
| | | inVariableList.value[editingVariableIndex.value].source = varialbeFormData.value.source |
| | | inVariableList.value[editingVariableIndex.value].target = varialbeFormData.value.target |
| | | } |
| | | if (variableType.value === 'out') { |
| | | outVariableList.value[editingVariableIndex.value].source = varialbeFormData.value.source |
| | | outVariableList.value[editingVariableIndex.value].target = varialbeFormData.value.target |
| | | } |
| | | } |
| | | variableDialogVisible.value = false |
| | | } |
| | | |
| | | const updateElementExtensions = () => { |
| | | const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', { |
| | | values: [...inVariableList.value, ...outVariableList.value, ...otherExtensionList.value] |
| | | }) |
| | | bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { |
| | | extensionElements: extensions |
| | | }) |
| | | } |
| | | |
| | | watch( |
| | | () => props.id, |
| | | (val) => { |
| | | val && |
| | | val.length && |
| | | nextTick(() => { |
| | | initCallActivity() |
| | | }) |
| | | }, |
| | | { immediate: true } |
| | | ) |
| | | </script> |
| | | |
| | | <style lang="scss" scoped></style> |
对比新文件 |
| | |
| | | <template> |
| | | <div> |
| | | <el-form-item label="执行类型" key="executeType"> |
| | | <el-select v-model="serviceTaskForm.executeType"> |
| | | <el-option label="Java类" value="class" /> |
| | | <el-option label="表达式" value="expression" /> |
| | | <el-option label="代理表达式" value="delegateExpression" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="serviceTaskForm.executeType === 'class'" |
| | | label="Java类" |
| | | prop="class" |
| | | key="execute-class" |
| | | > |
| | | <el-input v-model="serviceTaskForm.class" clearable @change="updateElementTask" /> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="serviceTaskForm.executeType === 'expression'" |
| | | label="表达式" |
| | | prop="expression" |
| | | key="execute-expression" |
| | | > |
| | | <el-input v-model="serviceTaskForm.expression" clearable @change="updateElementTask" /> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="serviceTaskForm.executeType === 'delegateExpression'" |
| | | label="代理表达式" |
| | | prop="delegateExpression" |
| | | key="execute-delegate" |
| | | > |
| | | <el-input v-model="serviceTaskForm.delegateExpression" clearable @change="updateElementTask" /> |
| | | </el-form-item> |
| | | </div> |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | defineOptions({ name: 'ServiceTask' }) |
| | | const props = defineProps({ |
| | | id: String, |
| | | type: String |
| | | }) |
| | | |
| | | const defaultTaskForm = ref({ |
| | | executeType: '', |
| | | class: '', |
| | | expression: '', |
| | | delegateExpression: '' |
| | | }) |
| | | |
| | | const serviceTaskForm = ref<any>({}) |
| | | const bpmnElement = ref() |
| | | |
| | | const bpmnInstances = () => (window as any)?.bpmnInstances |
| | | |
| | | const resetTaskForm = () => { |
| | | for (let key in defaultTaskForm.value) { |
| | | let value = bpmnElement.value?.businessObject[key] || defaultTaskForm.value[key] |
| | | serviceTaskForm.value[key] = value |
| | | if (value) { |
| | | serviceTaskForm.value.executeType = key |
| | | } |
| | | } |
| | | } |
| | | |
| | | const updateElementTask = () => { |
| | | let taskAttr = Object.create(null); |
| | | const type = serviceTaskForm.value.executeType; |
| | | for (let key in serviceTaskForm.value) { |
| | | if (key !== 'executeType' && key !== type) taskAttr[key] = null; |
| | | } |
| | | taskAttr[type] = serviceTaskForm.value[type] || ""; |
| | | bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), taskAttr) |
| | | } |
| | | |
| | | onBeforeUnmount(() => { |
| | | bpmnElement.value = null |
| | | }) |
| | | |
| | | watch( |
| | | () => props.id, |
| | | () => { |
| | | bpmnElement.value = bpmnInstances().bpmnElement |
| | | nextTick(() => { |
| | | resetTaskForm() |
| | | }) |
| | | }, |
| | | { immediate: true } |
| | | ) |
| | | |
| | | </script> |
| | |
| | | <template> |
| | | <el-form label-width="100px"> |
| | | <el-form label-width="120px"> |
| | | <el-form-item label="规则类型" prop="candidateStrategy"> |
| | | <el-select |
| | | v-model="userTaskForm.candidateStrategy" |
| | |
| | | @change="changeCandidateStrategy" |
| | | > |
| | | <el-option |
| | | v-for="dict in getIntDictOptions(DICT_TYPE.BPM_TASK_CANDIDATE_STRATEGY)" |
| | | :key="dict.value" |
| | | v-for="(dict, index) in CANDIDATE_STRATEGY" |
| | | :key="index" |
| | | :label="dict.label" |
| | | :value="dict.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="userTaskForm.candidateStrategy == 10" |
| | | v-if="userTaskForm.candidateStrategy == CandidateStrategy.ROLE" |
| | | label="指定角色" |
| | | prop="candidateParam" |
| | | > |
| | |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="userTaskForm.candidateStrategy == 20 || userTaskForm.candidateStrategy == 21" |
| | | v-if=" |
| | | userTaskForm.candidateStrategy == CandidateStrategy.DEPT_MEMBER || |
| | | userTaskForm.candidateStrategy == CandidateStrategy.DEPT_LEADER || |
| | | userTaskForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER |
| | | " |
| | | label="指定部门" |
| | | prop="candidateParam" |
| | | span="24" |
| | |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="userTaskForm.candidateStrategy == 22" |
| | | v-if="userTaskForm.candidateStrategy == CandidateStrategy.POST" |
| | | label="指定岗位" |
| | | prop="candidateParam" |
| | | span="24" |
| | |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="userTaskForm.candidateStrategy == 30" |
| | | v-if="userTaskForm.candidateStrategy == CandidateStrategy.USER" |
| | | label="指定用户" |
| | | prop="candidateParam" |
| | | span="24" |
| | |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="userTaskForm.candidateStrategy === 40" |
| | | v-if="userTaskForm.candidateStrategy === CandidateStrategy.USER_GROUP" |
| | | label="指定用户组" |
| | | prop="candidateParam" |
| | | > |
| | |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="userTaskForm.candidateStrategy === 60" |
| | | v-if="userTaskForm.candidateStrategy === CandidateStrategy.FORM_USER" |
| | | label="表单内用户字段" |
| | | prop="formUser" |
| | | > |
| | | <el-select |
| | | v-model="userTaskForm.candidateParam" |
| | | clearable |
| | | style="width: 100%" |
| | | @change="handleFormUserChange" |
| | | > |
| | | <el-option |
| | | v-for="(item, idx) in userFieldOnFormOptions" |
| | | :key="idx" |
| | | :label="item.title" |
| | | :value="item.field" |
| | | :disabled="!item.required" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="userTaskForm.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER" |
| | | label="表单内部门字段" |
| | | prop="formDept" |
| | | > |
| | | <el-select |
| | | v-model="userTaskForm.candidateParam" |
| | | clearable |
| | | style="width: 100%" |
| | | @change="updateElementTask" |
| | | > |
| | | <el-option |
| | | v-for="(item, idx) in deptFieldOnFormOptions" |
| | | :key="idx" |
| | | :label="item.title" |
| | | :value="item.field" |
| | | :disabled="!item.required" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if=" |
| | | userTaskForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER || |
| | | userTaskForm.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER || |
| | | userTaskForm.candidateStrategy == CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER || |
| | | userTaskForm.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER |
| | | " |
| | | :label="deptLevelLabel!" |
| | | prop="deptLevel" |
| | | span="24" |
| | | > |
| | | <el-select v-model="deptLevel" clearable @change="updateElementTask"> |
| | | <el-option |
| | | v-for="(item, index) in MULTI_LEVEL_DEPT" |
| | | :key="index" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item |
| | | v-if="userTaskForm.candidateStrategy === CandidateStrategy.EXPRESSION" |
| | | label="流程表达式" |
| | | prop="candidateParam" |
| | | > |
| | |
| | | type="textarea" |
| | | v-model="userTaskForm.candidateParam[0]" |
| | | clearable |
| | | style="width: 72%" |
| | | style="width: 100%" |
| | | @change="updateElementTask" |
| | | /> |
| | | <el-button class="ml-5px" size="small" type="success" @click="openProcessExpressionDialog" |
| | | >选择表达式</el-button |
| | | > |
| | | <XButton |
| | | class="!w-1/1 mt-5px" |
| | | type="success" |
| | | preIcon="ep:select" |
| | | title="选择表达式" |
| | | size="small" |
| | | @click="openProcessExpressionDialog" |
| | | /> |
| | | <!-- 选择弹窗 --> |
| | | <ProcessExpressionDialog ref="processExpressionDialogRef" @select="selectProcessExpression" /> |
| | | </el-form-item> |
| | |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' |
| | | import { |
| | | CANDIDATE_STRATEGY, |
| | | CandidateStrategy, |
| | | FieldPermissionType, |
| | | MULTI_LEVEL_DEPT |
| | | } from '@/components/SimpleProcessDesignerV2/src/consts' |
| | | import { defaultProps, handleTree } from '@/utils/tree' |
| | | import * as RoleApi from '@/api/system/role' |
| | | import * as DeptApi from '@/api/system/dept' |
| | |
| | | import * as UserGroupApi from '@/api/bpm/userGroup' |
| | | import ProcessExpressionDialog from './ProcessExpressionDialog.vue' |
| | | import { ProcessExpressionVO } from '@/api/bpm/processExpression' |
| | | import { useFormFieldsPermission } from '@/components/SimpleProcessDesignerV2/src/node' |
| | | |
| | | defineOptions({ name: 'UserTask' }) |
| | | const props = defineProps({ |
| | | id: String, |
| | | type: String |
| | | }) |
| | | const prefix = inject('prefix') |
| | | const userTaskForm = ref({ |
| | | candidateStrategy: undefined, // 分配规则 |
| | | candidateParam: [] // 分配选项 |
| | |
| | | const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表 |
| | | const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表 |
| | | |
| | | const { formFieldOptions } = useFormFieldsPermission(FieldPermissionType.READ) |
| | | // 表单内用户字段选项, 必须是必填和用户选择器 |
| | | const userFieldOnFormOptions = computed(() => { |
| | | return formFieldOptions.filter((item) => item.type === 'UserSelect') |
| | | }) |
| | | // 表单内部门字段选项, 必须是必填和部门选择器 |
| | | const deptFieldOnFormOptions = computed(() => { |
| | | return formFieldOptions.filter((item) => item.type === 'DeptSelect') |
| | | }) |
| | | |
| | | const deptLevel = ref(1) |
| | | const deptLevelLabel = computed(() => { |
| | | let label = '部门负责人来源' |
| | | if (userTaskForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER) { |
| | | label = label + '(指定部门向上)' |
| | | } else if (userTaskForm.value.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER) { |
| | | label = label + '(表单内部门向上)' |
| | | } else { |
| | | label = label + '(发起人部门向上)' |
| | | } |
| | | return label |
| | | }) |
| | | |
| | | const otherExtensions = ref() |
| | | |
| | | const resetTaskForm = () => { |
| | | const businessObject = bpmnElement.value.businessObject |
| | | if (!businessObject) { |
| | | return |
| | | } |
| | | |
| | | const extensionElements = |
| | | businessObject?.extensionElements ?? |
| | | bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] }) |
| | | userTaskForm.value.candidateStrategy = extensionElements.values?.filter( |
| | | (ex) => ex.$type === `${prefix}:CandidateStrategy` |
| | | )?.[0]?.value |
| | | const candidateParamStr = extensionElements.values?.filter( |
| | | (ex) => ex.$type === `${prefix}:CandidateParam` |
| | | )?.[0]?.value |
| | | if (candidateParamStr && candidateParamStr.length > 0) { |
| | | if (userTaskForm.value.candidateStrategy === CandidateStrategy.EXPRESSION) { |
| | | // 特殊:流程表达式,只有一个 input 输入框 |
| | | userTaskForm.value.candidateParam = [candidateParamStr] |
| | | } else if (userTaskForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER) { |
| | | // 特殊:多级不部门负责人,需要通过'|'分割 |
| | | userTaskForm.value.candidateParam = candidateParamStr |
| | | .split('|')[0] |
| | | .split(',') |
| | | .map((item) => { |
| | | // 如果数字超出了最大安全整数范围,则将其作为字符串处理 |
| | | let num = Number(item) |
| | | return num > Number.MAX_SAFE_INTEGER || num < -Number.MAX_SAFE_INTEGER ? item : num |
| | | }) |
| | | deptLevel.value = +candidateParamStr.split('|')[1] |
| | | } else if ( |
| | | userTaskForm.value.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER || |
| | | userTaskForm.value.candidateStrategy == CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER |
| | | ) { |
| | | userTaskForm.value.candidateParam = +candidateParamStr |
| | | deptLevel.value = +candidateParamStr |
| | | } else if (userTaskForm.value.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER) { |
| | | userTaskForm.value.candidateParam = candidateParamStr.split('|')[0] |
| | | deptLevel.value = +candidateParamStr.split('|')[1] |
| | | } else { |
| | | userTaskForm.value.candidateParam = candidateParamStr.split(',').map((item) => { |
| | | // 如果数字超出了最大安全整数范围,则将其作为字符串处理 |
| | | let num = Number(item) |
| | | return num > Number.MAX_SAFE_INTEGER || num < -Number.MAX_SAFE_INTEGER ? item : num |
| | | }) |
| | | } |
| | | } else { |
| | | userTaskForm.value.candidateParam = [] |
| | | } |
| | | |
| | | otherExtensions.value = |
| | | extensionElements.values?.filter( |
| | | (ex) => ex.$type !== `${prefix}:CandidateStrategy` && ex.$type !== `${prefix}:CandidateParam` |
| | | ) ?? [] |
| | | |
| | | // 改用通过extensionElements来存储数据 |
| | | return |
| | | if (businessObject.candidateStrategy != undefined) { |
| | | userTaskForm.value.candidateStrategy = parseInt(businessObject.candidateStrategy) as any |
| | | } else { |
| | |
| | | } else { |
| | | userTaskForm.value.candidateParam = businessObject.candidateParam |
| | | .split(',') |
| | | .map((item) => +item) |
| | | .map((item) => item) |
| | | } |
| | | } else { |
| | | userTaskForm.value.candidateParam = [] |
| | |
| | | /** 更新 candidateStrategy 字段时,需要清空 candidateParam,并触发 bpmn 图更新 */ |
| | | const changeCandidateStrategy = () => { |
| | | userTaskForm.value.candidateParam = [] |
| | | deptLevel.value = 1 |
| | | if (userTaskForm.value.candidateStrategy === CandidateStrategy.FORM_USER) { |
| | | // 特殊处理表单内用户字段,当只有发起人选项时应选中发起人 |
| | | if (!userFieldOnFormOptions.value || userFieldOnFormOptions.value.length <= 1) { |
| | | userTaskForm.value.candidateStrategy = CandidateStrategy.START_USER |
| | | } |
| | | } |
| | | updateElementTask() |
| | | } |
| | | |
| | | /** 选中某个 options 时候,更新 bpmn 图 */ |
| | | const updateElementTask = () => { |
| | | let candidateParam = |
| | | userTaskForm.value.candidateParam instanceof Array |
| | | ? userTaskForm.value.candidateParam.join(',') |
| | | : userTaskForm.value.candidateParam |
| | | |
| | | // 特殊处理多级部门情况 |
| | | if ( |
| | | userTaskForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER || |
| | | userTaskForm.value.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER |
| | | ) { |
| | | candidateParam += '|' + deptLevel.value |
| | | } |
| | | // 特殊处理发起人部门负责人、发起人连续部门负责人 |
| | | if ( |
| | | userTaskForm.value.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER || |
| | | userTaskForm.value.candidateStrategy == CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER |
| | | ) { |
| | | candidateParam = deptLevel.value + '' |
| | | } |
| | | |
| | | const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', { |
| | | values: [ |
| | | ...otherExtensions.value, |
| | | bpmnInstances().moddle.create(`${prefix}:CandidateStrategy`, { |
| | | value: userTaskForm.value.candidateStrategy |
| | | }), |
| | | bpmnInstances().moddle.create(`${prefix}:CandidateParam`, { |
| | | value: candidateParam |
| | | }) |
| | | ] |
| | | }) |
| | | bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { |
| | | extensionElements: extensions |
| | | }) |
| | | |
| | | // 改用通过extensionElements来存储数据 |
| | | return |
| | | bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { |
| | | candidateStrategy: userTaskForm.value.candidateStrategy, |
| | | candidateParam: userTaskForm.value.candidateParam.join(',') |
| | |
| | | updateElementTask() |
| | | } |
| | | |
| | | const handleFormUserChange = (e) => { |
| | | if (e === 'PROCESS_START_USER_ID') { |
| | | userTaskForm.value.candidateParam = [] |
| | | userTaskForm.value.candidateStrategy = CandidateStrategy.START_USER |
| | | } |
| | | updateElementTask() |
| | | } |
| | | |
| | | watch( |
| | | () => props.id, |
| | | () => { |
| | |
| | | hasRole(app) |
| | | hasPermi(app) |
| | | } |
| | | |
| | | /** |
| | | * 导出指令:v-mountedFocus |
| | | */ |
| | | export const setupMountedFocus = (app: App<Element>) => { |
| | | app.directive('mountedFocus', { |
| | | mounted(el) { |
| | | el.focus() |
| | | } |
| | | }) |
| | | } |
| | |
| | | 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 |
| | |
| | | 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 |
| | |
| | | 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' |
| | | } |
| | | ) |
| | | } |
| | | } |
| | | } |
| | |
| | | }) |
| | | 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> |
| | | |
| | |
| | | import router, { setupRouter } from '@/router' |
| | | |
| | | // 权限 |
| | | import { setupAuth } from '@/directives' |
| | | import { setupAuth, setupMountedFocus } from '@/directives' |
| | | |
| | | import { createApp } from 'vue' |
| | | |
| | |
| | | |
| | | setupAuth(app) |
| | | |
| | | setupMountedFocus(app) |
| | | |
| | | await router.isReady() |
| | | |
| | | app.use(VueDOMPurifyHTML) |
| | |
| | | import { defineStore } from 'pinia' |
| | | import { store } from '../index' |
| | | import { setCssVar, humpToUnderline } from '@/utils' |
| | | import { humpToUnderline, setCssVar } from '@/utils' |
| | | import { ElMessage } from 'element-plus' |
| | | import { CACHE_KEY, useCache } from '@/hooks/web/useCache' |
| | | import { ElementPlusSize } from '@/types/elementPlus' |
| | |
| | | locale: boolean |
| | | message: boolean |
| | | tagsView: boolean |
| | | tagsViewImmerse: boolean |
| | | tagsViewIcon: boolean |
| | | logo: boolean |
| | | fixedHeader: boolean |
| | |
| | | locale: true, // 多语言图标 |
| | | message: true, // 消息图标 |
| | | tagsView: true, // 标签页 |
| | | tagsViewImmerse: false, // 标签页沉浸 |
| | | tagsViewIcon: true, // 是否显示标签图标 |
| | | logo: true, // logo |
| | | fixedHeader: true, // 固定toolheader |
| | |
| | | }, |
| | | getTagsView(): boolean { |
| | | return this.tagsView |
| | | }, |
| | | getTagsViewImmerse(): boolean { |
| | | return this.tagsViewImmerse |
| | | }, |
| | | getTagsViewIcon(): boolean { |
| | | return this.tagsViewIcon |
| | |
| | | setTagsView(tagsView: boolean) { |
| | | this.tagsView = tagsView |
| | | }, |
| | | setTagsViewImmerse(tagsViewImmerse: boolean) { |
| | | this.tagsViewImmerse = tagsViewImmerse |
| | | }, |
| | | setTagsViewIcon(tagsViewIcon: boolean) { |
| | | this.tagsViewIcon = tagsViewIcon |
| | | }, |
| | |
| | | this.addRouters = routerMap.concat([ |
| | | { |
| | | path: '/:path(.*)*', |
| | | // redirect: '/404', |
| | | component: () => import('@/views/Error/404.vue'), |
| | | name: '404Page', |
| | | meta: { |
| | |
| | | @import './variables.scss'; |
| | | @use './variables.scss' as *; |
| | | // 导出变量 |
| | | :export { |
| | | namespace: $namespace; |
| | |
| | | @import './var.css'; |
| | | @import './FormCreate/index.scss'; |
| | | @import 'element-plus/theme-chalk/dark/css-vars.css'; |
| | | @use './var.css'; |
| | | @use './FormCreate/index.scss'; |
| | | @use './theme.scss'; |
| | | @use 'element-plus/theme-chalk/dark/css-vars.css'; |
| | | |
| | | .reset-margin [class*='el-icon'] + span { |
| | | margin-left: 2px !important; |
| | |
| | | -webkit-font-smoothing: antialiased; |
| | | -moz-osx-font-smoothing: grayscale; |
| | | } |
| | | |
| | | *, |
| | | :after, |
| | | :before { |
| | | margin: 0; |
| | | padding: 0; |
| | | box-sizing: border-box; |
| | | } |
| | |
| | | value?: object |
| | | ) => { |
| | | if (isRef(detailPreview)) { |
| | | // @ts-ignore |
| | | detailPreview = detailPreview.value |
| | | } |
| | | // @ts-ignore |
| | |
| | | const { wsCache } = useCache() |
| | | const permissionDatas = value |
| | | const all_permission = '*:*:*' |
| | | const permissions = wsCache.get(CACHE_KEY.USER).permissions |
| | | const userInfo = wsCache.get(CACHE_KEY.USER) |
| | | const permissions = userInfo?.permissions || [] |
| | | const hasPermission = permissions.some((permission) => { |
| | | return all_permission === permission || permissionDatas.includes(permission) |
| | | }) |
| | |
| | | const { wsCache } = useCache() |
| | | const permissionRoles = value |
| | | const super_admin = 'admin' |
| | | const roles = wsCache.get(CACHE_KEY.USER).roles |
| | | const userInfo = wsCache.get(CACHE_KEY.USER) |
| | | const roles = userInfo?.roles || [] |
| | | const hasRole = roles.some((role) => { |
| | | return super_admin === role || permissionRoles.includes(role) |
| | | }) |
| | |
| | | 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}` |
| | |
| | | // 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 () => { |
| | |
| | | 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 |
| | |
| | | <template> |
| | | |
| | | <ContentWrap> |
| | | <!-- 搜索工作栏 --> |
| | | <el-form |
| | |
| | | </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> |
| | |
| | | // 自定义左侧菜单(修改 默认任务 为 用户任务) |
| | | 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 |
| | |
| | | </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 属性 |
| | |
| | | <!-- 中间主要内容 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"> |
| | |
| | | selectProcessDefinition: any |
| | | }>() |
| | | const emit = defineEmits(['cancel']) |
| | | |
| | | const processInstanceStartLoading = ref(false) // 流程实例发起中 |
| | | const { push, currentRoute } = useRouter() // 路由 |
| | | const message = useMessage() // 消息弹窗 |
| | | const { delView } = useTagsViewStore() // 视图操作 |
| | |
| | | if (!fApi.value || !props.selectProcessDefinition) { |
| | | return |
| | | } |
| | | // 流程表单校验 |
| | | await fApi.value.validate() |
| | | // 如果有指定审批人,需要校验 |
| | | if (startUserSelectTasks.value?.length > 0) { |
| | | for (const userTask of startUserSelectTasks.value) { |
| | |
| | | } |
| | | |
| | | // 提交请求 |
| | | fApi.value.btn.loading(true) |
| | | processInstanceStartLoading.value = true |
| | | try { |
| | | await ProcessInstanceApi.createProcessInstance({ |
| | | processDefinitionId: props.selectProcessDefinition.id, |
| | |
| | | name: 'BpmProcessInstanceMy' |
| | | }) |
| | | } finally { |
| | | fApi.value.btn.loading(false) |
| | | processInstanceStartLoading.value = false |
| | | } |
| | | } |
| | | |
| | |
| | | <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"> |
| | |
| | | </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> |
| | |
| | | <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> |
| | |
| | | <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 |
| | |
| | | </el-form-item> |
| | | <el-form-item label="抄送意见" prop="copyReason"> |
| | | <el-input |
| | | v-model="genericForm.copyReason" |
| | | v-model="copyForm.copyReason" |
| | | clearable |
| | | placeholder="请输入抄送意见" |
| | | type="textarea" |
| | |
| | | <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" |
| | |
| | | <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" |
| | |
| | | </el-form-item> |
| | | <el-form-item label="审批意见" prop="reason"> |
| | | <el-input |
| | | v-model="genericForm.reason" |
| | | v-model="transferForm.reason" |
| | | clearable |
| | | placeholder="请输入审批意见" |
| | | type="textarea" |
| | |
| | | <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> |
| | |
| | | <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" |
| | |
| | | </el-form-item> |
| | | <el-form-item label="审批意见" prop="reason"> |
| | | <el-input |
| | | v-model="genericForm.reason" |
| | | v-model="delegateForm.reason" |
| | | clearable |
| | | placeholder="请输入审批意见" |
| | | type="textarea" |
| | |
| | | <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> |
| | |
| | | <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" |
| | |
| | | </el-form-item> |
| | | <el-form-item label="审批意见" prop="reason"> |
| | | <el-input |
| | | v-model="genericForm.reason" |
| | | v-model="addSignForm.reason" |
| | | clearable |
| | | placeholder="请输入审批意见" |
| | | type="textarea" |
| | |
| | | <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> |
| | |
| | | <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" |
| | |
| | | </el-form-item> |
| | | <el-form-item label="审批意见" prop="reason"> |
| | | <el-input |
| | | v-model="genericForm.reason" |
| | | v-model="deleteSignForm.reason" |
| | | clearable |
| | | placeholder="请输入审批意见" |
| | | type="textarea" |
| | |
| | | <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> |
| | |
| | | v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.RETURN)" |
| | | > |
| | | <template #reference> |
| | | <div @click="openReturnPopover" class="hover-bg-gray-100 rounded-xl p-6px"> |
| | | <div @click="openPopover('return')" class="hover-bg-gray-100 rounded-xl p-6px"> |
| | | <Icon :size="14" icon="ep:back" /> |
| | | {{ getButtonDisplayName(OperationButtonType.RETURN) }} |
| | | </div> |
| | |
| | | <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" |
| | |
| | | </el-form-item> |
| | | <el-form-item label="退回理由" prop="returnReason"> |
| | | <el-input |
| | | v-model="genericForm.returnReason" |
| | | v-model="returnForm.returnReason" |
| | | clearable |
| | | placeholder="请输入退回理由" |
| | | type="textarea" |
| | |
| | | <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> |
| | |
| | | <el-form |
| | | label-position="top" |
| | | class="mb-auto" |
| | | ref="formRef" |
| | | :model="genericForm" |
| | | :rules="genericRule" |
| | | ref="cancelFormRef" |
| | | :model="cancelForm" |
| | | :rules="cancelFormRule" |
| | | label-width="100px" |
| | | > |
| | | <el-form-item label="取消理由" prop="cancelReason"> |
| | | <span class="text-#878c93 text-12px"> 取消后,该审批流程将自动结束</span> |
| | | <el-input |
| | | v-model="genericForm.cancelReason" |
| | | v-model="cancelForm.cancelReason" |
| | | clearable |
| | | placeholder="请输入取消理由" |
| | | type="textarea" |
| | |
| | | </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> |
| | |
| | | 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({ |
| | |
| | | |
| | | // ========== 审批信息 ========== |
| | | 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( |
| | |
| | | } |
| | | ) |
| | | |
| | | /** 弹出退回气泡卡 */ |
| | | 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() |
| | |
| | | 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 |
| | |
| | | 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 { |
| | |
| | | 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. 加载最新数据 |
| | |
| | | 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() |
| | |
| | | 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() |
| | |
| | | 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() |
| | |
| | | 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 { |
| | |
| | | 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() |
| | |
| | | } |
| | | |
| | | const loadTodoTask = (task: any) => { |
| | | genericForm.value = {} |
| | | approveForm.value = {} |
| | | approveFormFApi.value = {} |
| | | runningTask.value = task |
| | |
| | | } |
| | | } |
| | | |
| | | /** 校验流程表单 */ |
| | | 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> |
| | | |
| | |
| | | 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" |
| | |
| | | /> |
| | | </el-col> |
| | | <!-- 情况二:业务表单 --> |
| | | <div v-if="processDefinition?.formType === 20"> |
| | | <div v-if="processDefinition?.formType === BpmModelFormType.CUSTOM"> |
| | | <BusinessFormComponent :id="processInstance.businessKey" /> |
| | | </div> |
| | | </div> |
| | |
| | | :process-instance="processInstance" |
| | | :process-definition="processDefinition" |
| | | :userOptions="userOptions" |
| | | :normal-form="detailForm" |
| | | :normal-form-api="fApi" |
| | | :writable-fields="writableFields" |
| | | @success="refresh" |
| | | /> |
| | | </div> |
| | |
| | | <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' |
| | |
| | | value: {} |
| | | }) // 流程实例的表单详情 |
| | | |
| | | const writableFields: Array<string> = [] // 表单可以编辑的字段 |
| | | |
| | | /** 获得详情 */ |
| | | const getDetail = () => { |
| | | getApprovalDetail() |
| | |
| | | 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 { |
| | |
| | | if (permission === FieldPermissionType.WRITE) { |
| | | //@ts-ignore |
| | | fApi.value?.disabled(false, field) |
| | | // 加入可以编辑的字段 |
| | | writableFields.push(field) |
| | | } |
| | | if (permission === FieldPermissionType.NONE) { |
| | | //@ts-ignore |
| | |
| | | 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 |
| | |
| | | $process-header-height - 40px |
| | | ); |
| | | overflow: auto; |
| | | display: flex; |
| | | flex-direction: column; |
| | | |
| | | :deep(.box-card) { |
| | |
| | | <template> |
| | | |
| | | <ContentWrap> |
| | | <!-- 搜索工作栏 --> |
| | | <el-form |
| | |
| | | </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" |
| | |
| | | </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" |
| | |
| | | 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" |
| | |
| | | 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> |
| | |
| | | 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" /> |
| | |
| | | </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' |
| | |
| | | } |
| | | } |
| | | |
| | | const showPopover = ref(false) |
| | | |
| | | /** 搜索按钮操作 */ |
| | | const handleQuery = () => { |
| | | queryParams.pageNo = 1 |
| | |
| | | } |
| | | |
| | | /** 查看详情 */ |
| | | const handleDetail = (row) => { |
| | | const handleDetail = (row: ProcessInstanceVO) => { |
| | | router.push({ |
| | | name: 'BpmProcessInstanceDetail', |
| | | query: { |
| | |
| | | } |
| | | |
| | | /** 取消按钮操作 */ |
| | | const handleCancel = async (row) => { |
| | | const handleCancel = async (row: ProcessInstanceVO) => { |
| | | // 二次确认 |
| | | const { value } = await ElMessageBox.prompt('请输入取消原因', '取消流程', { |
| | | confirmButtonText: t('common.ok'), |
| | |
| | | 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?.() |
| | | } |
| | | |
| | | /** 激活时 **/ |
| | |
| | | <template> |
| | | |
| | | <ContentWrap> |
| | | <!-- 搜索工作栏 --> |
| | | <el-form |
| | |
| | | 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" |
| | |
| | | @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> |
| | | |
| | |
| | | </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' }) |
| | | |
| | |
| | | pageNo: 1, |
| | | pageSize: 10, |
| | | name: '', |
| | | category: undefined, |
| | | status: undefined, |
| | | createTime: [] |
| | | }) |
| | | const queryFormRef = ref() // 搜索的表单 |
| | | const categoryList = ref<CategoryVO[]>([]) // 流程分类列表 |
| | | const showPopover = ref(false) |
| | | |
| | | /** 查询任务列表 */ |
| | | const getList = async () => { |
| | |
| | | } |
| | | |
| | | /** 初始化 **/ |
| | | onMounted(() => { |
| | | getList() |
| | | onMounted(async () => { |
| | | await getList() |
| | | categoryList.value = await CategoryApi.getCategorySimpleList() |
| | | }) |
| | | </script> |
| | |
| | | <template> |
| | | |
| | | <ContentWrap> |
| | | <!-- 搜索工作栏 --> |
| | | <el-form |
| | |
| | | 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" |
| | |
| | | @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> |
| | | |
| | |
| | | <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' }) |
| | | |
| | |
| | | pageNo: 1, |
| | | pageSize: 10, |
| | | name: '', |
| | | category: undefined, |
| | | createTime: [] |
| | | }) |
| | | const queryFormRef = ref() // 搜索的表单 |
| | | const categoryList = ref<CategoryVO[]>([]) // 流程分类列表 |
| | | |
| | | /** 查询任务列表 */ |
| | | const getList = async () => { |
| | |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | const showPopover = ref(false) |
| | | |
| | | /** 搜索按钮操作 */ |
| | | const handleQuery = () => { |
| | |
| | | } |
| | | |
| | | /** 初始化 **/ |
| | | onMounted(() => { |
| | | getList() |
| | | onMounted(async () => { |
| | | await getList() |
| | | categoryList.value = await CategoryApi.getCategorySimpleList() |
| | | }) |
| | | </script> |
| | |
| | | <!-- 列表 --> |
| | | <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> |
| | |
| | | width="180" |
| | | :formatter="dateFormatter" |
| | | /> |
| | | <el-table-column label="操作" align="center"> |
| | | <el-table-column label="操作" align="center" width="120"> |
| | | <template #default="scope"> |
| | | <el-button |
| | | link |
| | |
| | | <!-- 列表 --> |
| | | <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" |
| | |
| | | width="180" |
| | | :formatter="dateFormatter" |
| | | /> |
| | | <el-table-column label="操作" align="center"> |
| | | <el-table-column label="操作" align="center" width="120"> |
| | | <template #default="scope"> |
| | | <el-button |
| | | link |
| | |
| | | <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"> |
| | |
| | | <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" /> |
| | |
| | | 菜单权限 |
| | | </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" |
| | |
| | | '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', |
| | | { |
| | |
| | | "@/*": ["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"] |
| | |
| | | |
| | | title: 'title' 设置该路由在侧边栏和面包屑中展示的名字 |
| | | |
| | | titleSuffix: '2' 当 path 和 title 重复时的后缀或备注 |
| | | |
| | | icon: 'svg-name' 设置该路由的图标 |
| | | |
| | | noCache: true 如果设置为true,则不会被 <keep-alive> 缓存(默认 false) |
| | |
| | | hidden?: boolean |
| | | alwaysShow?: boolean |
| | | title?: string |
| | | titleSuffix?: string |
| | | icon?: string |
| | | noCache?: boolean |
| | | breadcrumb?: boolean |
| | |
| | | ${selector} { |
| | | display: flex; |
| | | height: 100%; |
| | | padding: 1px 10px 0; |
| | | padding: 0 10px; |
| | | cursor: pointer; |
| | | align-items: center; |
| | | transition: background var(--transition-time-02); |
| | |
| | | preprocessorOptions: { |
| | | scss: { |
| | | additionalData: '@use "@/styles/variables.scss" as *;', |
| | | javascriptEnabled: true |
| | | javascriptEnabled: true, |
| | | silenceDeprecations: ["legacy-js-api"], |
| | | } |
| | | } |
| | | }, |
对比新文件 |
| | |
| | | { |
| | | "$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" |
| | | } |
| | | ] |
| | | } |
| | | } |
| | | } |