@@ -1,6 +1,6 @@
{
"name": "yudao-ui-admin-vue3",
- "version": "2.2.0-snapshot",
+ "version": "2.3.0-snapshot",
"description": "基于vue3、vite4、element-plus、typesScript",
"author": "xingyu",
"private": false,
@@ -9,11 +9,11 @@
"dev": "vite --mode env.local",
"dev-server": "vite --mode dev",
"ts:check": "vue-tsc --noEmit",
- "build:local": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build",
- "build:dev": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode dev",
- "build:test": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode test",
- "build:stage": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode stage",
- "build:prod": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode prod",
+ "build:local": "node ./node_modules/vite/bin/vite.js build",
+ "build:dev": "node ./node_modules/vite/bin/vite.js build --mode dev",
+ "build:test": "node ./node_modules/vite/bin/vite.js build --mode test",
+ "build:stage": "node ./node_modules/vite/bin/vite.js build --mode stage",
+ "build:prod": "node ./node_modules/vite/bin/vite.js build --mode prod",
"serve:dev": "vite preview --mode dev",
"serve:prod": "vite preview --mode prod",
"preview": "pnpm build:local && vite preview",
@@ -26,8 +26,8 @@
},
"dependencies": {
"@element-plus/icons-vue": "^2.1.0",
- "@form-create/designer": "^3.1.3",
- "@form-create/element-ui": "^3.1.24",
+ "@form-create/designer": "^3.2.6",
+ "@form-create/element-ui": "^3.2.11",
"@iconify/iconify": "^3.1.1",
"@microsoft/fetch-event-source": "^2.0.1",
"@videojs-player/vue": "^1.0.0",
@@ -47,7 +47,7 @@
"driver.js": "^1.3.1",
"echarts": "^5.5.0",
"echarts-wordcloud": "^2.1.0",
- "element-plus": "2.8.0",
+ "element-plus": "2.8.4",
"fast-xml-parser": "^4.3.2",
"highlight.js": "^11.9.0",
"jsencrypt": "^3.3.2",
@@ -67,7 +67,7 @@
"steady-xml": "^0.1.0",
"url": "^0.11.3",
"video.js": "^7.21.5",
- "vue": "3.4.21",
+ "vue": "3.5.12",
"vue-dompurify-html": "^4.1.4",
"vue-i18n": "9.10.2",
"vue-router": "^4.3.0",
@@ -130,7 +130,7 @@
"vite-plugin-progress": "^0.0.7",
"vite-plugin-purge-icons": "^0.10.0",
"vite-plugin-svg-icons": "^2.0.1",
- "vite-plugin-top-level-await": "^1.3.1",
+ "vite-plugin-top-level-await": "^1.4.4",
"vue-eslint-parser": "^9.3.2",
"vue-tsc": "^1.8.27"
@@ -30,7 +30,7 @@ export const getModelPage = async (params) => {
return await request.get({ url: '/bpm/model/page', params })
}
-export const getModel = async (id: number) => {
+export const getModel = async (id: string) => {
return await request.get({ url: '/bpm/model/get?id=' + id })
@@ -38,6 +38,10 @@ export const updateModel = async (data: ModelVO) => {
return await request.put({ url: '/bpm/model/update', data: data })
+export const updateModelBpmn = async (data: ModelVO) => {
+ return await request.put({ url: '/bpm/model/update-bpmn', data: data })
+}
+
// 任务状态修改
export const updateModelState = async (id: number, state: number) => {
const data = {
import request from '@/config/axios'
import { ProcessDefinitionVO } from '@/api/bpm/model'
-
+import { NodeType } from '@/components/SimpleProcessDesignerV2/src/consts'
export type Task = {
id: string
name: string
@@ -22,6 +22,35 @@ export type ProcessInstanceVO = {
processDefinition?: ProcessDefinitionVO
+// 用户信息
+export type User = {
+ id: number,
+ nickname: string,
+ avatar: string
+// 审批任务信息
+export type ApprovalTaskInfo = {
+ ownerUser: User,
+ assigneeUser: User,
+ status: number,
+ reason: string
+// 审批节点信息
+export type ApprovalNodeInfo = {
+ id : number
+ name: string
+ nodeType: NodeType
+ status: number
+ startTime?: Date
+ endTime?: Date
+ candidateUserList?: User[]
+ tasks: ApprovalTaskInfo[]
export const getProcessInstanceMyPage = async (params: any) => {
return await request.get({ url: '/bpm/process-instance/my-page', params })
@@ -57,3 +86,14 @@ export const getProcessInstance = async (id: string) => {
export const getProcessInstanceCopyPage = async (params: any) => {
return await request.get({ url: '/bpm/process-instance/copy/page', params })
+// 获取审批详情
+export const getApprovalDetail = async (processInstanceId?:string, processDefinitionId?:string) => {
+ const param = processInstanceId ? '?processInstanceId='+ processInstanceId : '?processDefinitionId='+ processDefinitionId
+ return await request.get({ url: 'bpm/process-instance/get-approval-detail'+ param })
+// 获取表单字段权限
+export const getFormFieldsPermission = async (params: any) => {
+ return await request.get({ url: '/bpm/process-instance/get-form-fields-permission', params })
@@ -0,0 +1,15 @@
+import request from '@/config/axios'
+export const updateBpmSimpleModel = async (data) => {
+ return await request.post({
+ url: '/bpm/model/simple/update',
+ data: data
+ })
+export const getBpmSimpleModel = async (id) => {
+ return await request.get({
+ url: '/bpm/model/simple/get?id=' + id
@@ -1,5 +1,51 @@
+/**
+ * 任务状态枚举
+ */
+export enum TaskStatusEnum {
+ /**
+ * 未开始
+ NOT_START = -1,
+ * 待审批
+ WAIT = 0,
+ * 审批中
+ RUNNING = 1,
+ * 审批通过
+ APPROVE = 2,
+ * 审批不通过
+ REJECT = 3,
+ * 已取消
+ CANCEL = 4,
+ * 已退回
+ RETURN = 5,
+ * 委派中
+ DELEGATE = 6,
+ * 审批通过中
+ APPROVING = 7,
export type TaskVO = {
id: number
@@ -0,0 +1,74 @@
+// IoT 设备 VO
+export interface DeviceVO {
+ id: number // 设备 ID,主键,自增
+ deviceKey: string // 设备唯一标识符
+ deviceName: string // 设备名称
+ productId: number // 产品编号
+ productKey: string // 产品标识
+ deviceType: number // 设备类型
+ nickname: string // 设备备注名称
+ gatewayId: number // 网关设备 ID
+ status: number // 设备状态
+ statusLastUpdateTime: Date // 设备状态最后更新时间
+ lastOnlineTime: Date // 最后上线时间
+ lastOfflineTime: Date // 最后离线时间
+ activeTime: Date // 设备激活时间
+ createTime: Date // 创建时间
+ ip: string // 设备的 IP 地址
+ firmwareVersion: string // 设备的固件版本
+ deviceSecret: string // 设备密钥,用于设备认证,需安全存储
+ mqttClientId: string // MQTT 客户端 ID
+ mqttUsername: string // MQTT 用户名
+ mqttPassword: string // MQTT 密码
+ authType: string // 认证类型
+ latitude: number // 设备位置的纬度
+ longitude: number // 设备位置的经度
+ areaId: number // 地区编码
+ address: string // 设备详细地址
+ serialNumber: string // 设备序列号
+export interface DeviceUpdateStatusVO {
+// 设备 API
+export const DeviceApi = {
+ // 查询设备分页
+ getDevicePage: async (params: any) => {
+ return await request.get({ url: `/iot/device/page`, params })
+ },
+ // 查询设备详情
+ getDevice: async (id: number) => {
+ return await request.get({ url: `/iot/device/get?id=` + id })
+ // 新增设备
+ createDevice: async (data: DeviceVO) => {
+ return await request.post({ url: `/iot/device/create`, data })
+ // 修改设备
+ updateDevice: async (data: DeviceVO) => {
+ return await request.put({ url: `/iot/device/update`, data })
+ // 修改设备状态
+ updateDeviceStatus: async (data: DeviceUpdateStatusVO) => {
+ return await request.put({ url: `/iot/device/update-status`, data })
+ // 删除设备
+ deleteDevice: async (id: number) => {
+ return await request.delete({ url: `/iot/device/delete?id=` + id })
+ // 获取设备数量
+ getDeviceCount: async (productId: number) => {
+ return await request.get({ url: `/iot/device/count?productId=` + productId })
+ }
@@ -0,0 +1,62 @@
+// IoT 产品 VO
+export interface ProductVO {
+ id: number // 产品编号
+ name: string // 产品名称
+ protocolId: number // 协议编号
+ categoryId: number // 产品所属品类标识符
+ description: string // 产品描述
+ validateType: number // 数据校验级别
+ status: number // 产品状态
+ netType: number // 联网方式
+ protocolType: number // 接入网关协议
+ dataFormat: number // 数据格式
+ deviceCount: number // 设备数量
+// IoT 产品 API
+export const ProductApi = {
+ // 查询产品分页
+ getProductPage: async (params: any) => {
+ return await request.get({ url: `/iot/product/page`, params })
+ // 查询产品详情
+ getProduct: async (id: number) => {
+ return await request.get({ url: `/iot/product/get?id=` + id })
+ // 新增产品
+ createProduct: async (data: ProductVO) => {
+ return await request.post({ url: `/iot/product/create`, data })
+ // 修改产品
+ updateProduct: async (data: ProductVO) => {
+ return await request.put({ url: `/iot/product/update`, data })
+ // 删除产品
+ deleteProduct: async (id: number) => {
+ return await request.delete({ url: `/iot/product/delete?id=` + id })
+ // 导出产品 Excel
+ exportProduct: async (params) => {
+ return await request.download({ url: `/iot/product/export-excel`, params })
+ // 更新产品状态
+ updateProductStatus: async (id: number, status: number) => {
+ return await request.put({ url: `/iot/product/update-status?id=` + id + `&status=` + status })
+ // 查询产品(精简)列表
+ getSimpleProductList() {
+ return request.get({ url: '/iot/product/list-all-simple' })
@@ -0,0 +1,55 @@
+// IoT 产品物模型 VO
+export interface ThinkModelFunctionVO {
+ id: number // 物模型功能编号
+ identifier: string // 功能标识
+ name: string // 功能名称
+ description: string // 功能描述
+ type: number // 功能类型
+ property: string // 属性
+ event: string // 事件
+ service: string // 服务
+// IoT 产品物模型 API
+export const ThinkModelFunctionApi = {
+ // 查询产品物模型分页
+ getThinkModelFunctionPage: async (params: any) => {
+ return await request.get({ url: `/iot/think-model-function/page`, params })
+ // 获得产品物模型
+ getThinkModelFunctionListByProductId: async (params: any) => {
+ url: `/iot/think-model-function/list-by-product-id`,
+ params
+ // 查询产品物模型详情
+ getThinkModelFunction: async (id: number) => {
+ return await request.get({ url: `/iot/think-model-function/get?id=` + id })
+ // 新增产品物模型
+ createThinkModelFunction: async (data: ThinkModelFunctionVO) => {
+ return await request.post({ url: `/iot/think-model-function/create`, data })
+ // 修改产品物模型
+ updateThinkModelFunction: async (data: ThinkModelFunctionVO) => {
+ return await request.put({ url: `/iot/think-model-function/update`, data })
+ // 删除产品物模型
+ deleteThinkModelFunction: async (id: number) => {
+ return await request.delete({ url: `/iot/think-model-function/delete?id=` + id })
+ // 导出产品物模型 Excel
+ exportThinkModelFunction: async (params) => {
+ return await request.download({ url: `/iot/think-model-function/export-excel`, params })
@@ -11,7 +11,7 @@ const prefixCls = getPrefixCls('content-wrap')
defineProps({
title: propTypes.string.def(''),
message: propTypes.string.def(''),
- bodyStyle: propTypes.object.def({ padding: '20px' })
+ bodyStyle: propTypes.object.def({ padding: '10px' })
})
</script>
@@ -79,7 +79,7 @@
</template>
<script setup lang="ts">
-import { TabBarProperty, THEME_LIST } from './config'
+import { TabBarProperty, component, THEME_LIST } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
// 底部导航栏
defineOptions({ name: 'TabBarProperty' })
@@ -88,6 +88,9 @@ const props = defineProps<{ modelValue: 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)
@@ -20,6 +20,7 @@
<div v-else class="custom-hover" @click.stop="showTopSearch = !showTopSearch">
<Icon icon="ep:search" />
<el-select
+ @click.stop
filterable
:reserve-keyword="false"
remote
@@ -1,237 +0,0 @@
-/* stylelint-disable order/properties-order */
-<template>
- <div class="add-node-btn-box">
- <div class="add-node-btn">
- <el-popover placement="right-start" v-model="visible" width="auto">
- <div class="add-node-popover-body">
- <a class="add-node-popover-item approver" @click="addType(1)">
- <div class="item-wrapper">
- <span class="iconfont"></span>
- </div>
- <p>审批人</p>
- </a>
- <a class="add-node-popover-item notifier" @click="addType(2)">
- <span class="iconfont"></span>
- <p>抄送人</p>
- <a class="add-node-popover-item condition" @click="addType(4)">
- <span class="iconfont"></span>
- <p>条件分支</p>
- <template #reference>
- <button class="btn" type="button">
- <span class="iconfont"></span>
- </button>
- </template>
- </el-popover>
-</template>
-<script setup>
-import { ref } from 'vue'
-let props = defineProps({
- childNodeP: {
- type: Object,
- default: () => ({})
- }
-})
-let emits = defineEmits(['update:childNodeP'])
-let visible = ref(false)
-const addType = (type) => {
- visible.value = false
- if (type != 4) {
- var data
- if (type == 1) {
- data = {
- nodeName: '审核人',
- error: true,
- type: 1,
- settype: 1,
- selectMode: 0,
- selectRange: 0,
- directorLevel: 1,
- examineMode: 1,
- noHanderAction: 1,
- examineEndDirectorLevel: 0,
- childNode: props.childNodeP,
- nodeUserList: []
- } else if (type == 2) {
- nodeName: '抄送人',
- type: 2,
- ccSelfSelectFlag: 1,
- emits('update:childNodeP', data)
- } else {
- emits('update:childNodeP', {
- nodeName: '路由',
- type: 4,
- childNode: null,
- conditionNodes: [
- {
- nodeName: '条件1',
- type: 3,
- priorityLevel: 1,
- conditionList: [],
- nodeUserList: [],
- childNode: props.childNodeP
- },
- nodeName: '条件2',
- priorityLevel: 2,
- childNode: null
- ]
- })
-}
-</script>
-<style scoped lang="scss">
-.add-node-btn-box {
- width: 240px;
- display: inline-flex;
- -ms-flex-negative: 0;
- flex-shrink: 0;
- -webkit-box-flex: 1;
- -ms-flex-positive: 1;
- position: relative;
- &:before {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- z-index: -1;
- margin: auto;
- width: 2px;
- height: 100%;
- background-color: #cacaca;
- .add-node-btn {
- user-select: none;
- padding: 20px 0 32px;
- display: flex;
- -webkit-box-pack: center;
- justify-content: center;
- flex-grow: 1;
- .btn {
- outline: none;
- box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
- width: 30px;
- height: 30px;
- background: #3296fa;
- border-radius: 50%;
- border: none;
- line-height: 30px;
- -webkit-transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
- transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
- .iconfont {
- color: #fff;
- font-size: 16px;
- &:hover {
- transform: scale(1.3);
- box-shadow: 0 13px 27px 0 rgba(0, 0, 0, 0.1);
- &:active {
- transform: none;
- background: #1e83e9;
-.add-node-popover-body {
- .add-node-popover-item {
- margin-right: 10px;
- cursor: pointer;
- text-align: center;
- flex: 1;
- color: #191f25 !important;
- .item-wrapper {
- display: inline-block;
- width: 80px;
- height: 80px;
- margin-bottom: 5px;
- background: #fff;
- border: 1px solid #e2e2e2;
- font-size: 35px;
- line-height: 80px;
- &.approver {
- color: #ff943e;
- &.notifier {
- color: #3296fa;
- &.condition {
- color: #15bc83;
- box-shadow: 0 10px 20px 0 rgba(50, 150, 250, 0.4);
- box-shadow: none;
- background: #eaeaea;
- color: inherit;
-</style>
@@ -1,297 +0,0 @@
-<!-- eslint-disable vue/no-mutating-props -->
-<!--
- * @Date: 2022-09-21 14:41:53
- * @LastEditors: StavinLi 495727881@qq.com
- * @LastEditTime: 2023-05-24 15:20:24
- * @FilePath: /Workflow-Vue3/src/components/nodeWrap.vue
--->
- <div class="node-wrap" v-if="nodeConfig.type < 3">
- <div class="node-wrap-box" :class="(nodeConfig.type == 0 ? 'start-node ' : '') +(isTried && nodeConfig.error ? 'active error' : '')">
- <div class="title" :style="`background: rgb(${bgColors[nodeConfig.type]});`">
- <span v-if="nodeConfig.type == 0">{{ nodeConfig.nodeName }}</span>
- <template v-else>
- <span class="iconfont">{{nodeConfig.type == 1?'':''}}</span>
- <input
- v-if="isInput"
- type="text"
- class="ant-input editable-title-input"
- @blur="blurEvent()"
- @focus="$event.currentTarget.select()"
- v-focus
- v-model="nodeConfig.nodeName"
- :placeholder="defaultText"
- />
- <span v-else class="editable-title" @click="clickEvent()">{{ nodeConfig.nodeName }}</span>
- <i class="anticon anticon-close close" @click="delNode"></i>
- <div class="content" @click="setPerson">
- <div class="text">
- <span class="placeholder" v-if="!showText">请选择{{defaultText}}</span>
- {{showText}}
- <i class="anticon anticon-right arrow"></i>
- <div class="error_tip" v-if="isTried && nodeConfig.error">
- <i class="anticon anticon-exclamation-circle"></i>
- <addNode v-model:childNodeP="nodeConfig.childNode" />
- <div class="branch-wrap" v-if="nodeConfig.type == 4">
- <div class="branch-box-wrap">
- <div class="branch-box">
- <button class="add-branch" @click="addTerm">添加条件</button>
- <div class="col-box" v-for="(item, index) in nodeConfig.conditionNodes" :key="index">
- <div class="condition-node">
- <div class="condition-node-box">
- <div class="auto-judge" :class="isTried && item.error ? 'error active' : ''">
- <div class="sort-left" v-if="index != 0" @click="arrTransfer(index, -1)"><</div>
- <div class="title-wrapper">
- v-if="isInputList[index]"
- @blur="blurEvent(index)"
- v-model="item.nodeName"
- <span v-else class="editable-title" @click="clickEvent(index)">{{ item.nodeName }}</span>
- <span class="priority-title" @click="setPerson(item.priorityLevel)">优先级{{ item.priorityLevel }}</span>
- <i class="anticon anticon-close close" @click="delTerm(index)"></i>
- <div class="sort-right" v-if="index != nodeConfig.conditionNodes.length - 1" @click="arrTransfer(index)">></div>
- <div class="content" @click="setPerson(item.priorityLevel)">{{ conditionStr(nodeConfig, index) }}</div>
- <div class="error_tip" v-if="isTried && item.error">
- <addNode v-model:childNodeP="item.childNode" />
- <nodeWrap v-if="item.childNode" v-model:nodeConfig="item.childNode" />
- <template v-if="index == 0">
- <div class="top-left-cover-line"></div>
- <div class="bottom-left-cover-line"></div>
- <template v-if="index == nodeConfig.conditionNodes.length - 1">
- <div class="top-right-cover-line"></div>
- <div class="bottom-right-cover-line"></div>
- <nodeWrap v-if="nodeConfig.childNode" v-model:nodeConfig="nodeConfig.childNode" />
-import addNode from './addNode.vue'
-import { onMounted, ref, watch, getCurrentInstance, computed } from 'vue'
-import {
- arrToStr,
- conditionStr,
- setApproverStr,
- copyerStr,
- bgColors,
- placeholderList
-} from './util'
-import { useWorkFlowStoreWithOut } from '@/store/modules/simpleWorkflow'
-let _uid = getCurrentInstance().uid
- nodeConfig: {
- flowPermission: {
- // eslint-disable-next-line vue/require-valid-default-prop
- default: () => []
-let defaultText = computed(() => {
- return placeholderList[props.nodeConfig.type]
-let showText = computed(() => {
- if (props.nodeConfig.type == 0) return arrToStr(props.flowPermission) || '所有人'
- if (props.nodeConfig.type == 1) return setApproverStr(props.nodeConfig)
- return copyerStr(props.nodeConfig)
-let isInputList = ref([])
-let isInput = ref(false)
-const resetConditionNodesErr = () => {
- for (var i = 0; i < props.nodeConfig.conditionNodes.length; i++) {
- // eslint-disable-next-line vue/no-mutating-props
- props.nodeConfig.conditionNodes[i].error =
- conditionStr(props.nodeConfig, i) == '请设置条件' &&
- i != props.nodeConfig.conditionNodes.length - 1
-onMounted(() => {
- if (props.nodeConfig.type == 1) {
- props.nodeConfig.error = !setApproverStr(props.nodeConfig)
- } else if (props.nodeConfig.type == 2) {
- props.nodeConfig.error = !copyerStr(props.nodeConfig)
- } else if (props.nodeConfig.type == 4) {
- resetConditionNodesErr()
-let emits = defineEmits(['update:flowPermission', 'update:nodeConfig'])
-let store = useWorkFlowStoreWithOut()
-let {
- setPromoter,
- setApprover,
- setCopyer,
- setCondition,
- setFlowPermission,
- setApproverConfig,
- setCopyerConfig,
- setConditionsConfig
-} = store
-let isTried = computed(() => store.isTried)
-let flowPermission1 = computed(() => store.flowPermission1)
-let approverConfig1 = computed(() => store.approverConfig1)
-let copyerConfig1 = computed(() => store.copyerConfig1)
-let conditionsConfig1 = computed(() => store.conditionsConfig1)
-watch(flowPermission1, (flow) => {
- if (flow.flag && flow.id === _uid) {
- emits('update:flowPermission', flow.value)
-watch(approverConfig1, (approver) => {
- if (approver.flag && approver.id === _uid) {
- emits('update:nodeConfig', approver.value)
-watch(copyerConfig1, (copyer) => {
- if (copyer.flag && copyer.id === _uid) {
- emits('update:nodeConfig', copyer.value)
-watch(conditionsConfig1, (condition) => {
- if (condition.flag && condition.id === _uid) {
- emits('update:nodeConfig', condition.value)
-const clickEvent = (index) => {
- if (index || index === 0) {
- isInputList.value[index] = true
- isInput.value = true
-const blurEvent = (index) => {
- isInputList.value[index] = false
- props.nodeConfig.conditionNodes[index].nodeName =
- props.nodeConfig.conditionNodes[index].nodeName || '条件'
- isInput.value = false
- props.nodeConfig.nodeName = props.nodeConfig.nodeName || defaultText
-const delNode = () => {
- emits('update:nodeConfig', props.nodeConfig.childNode)
-const addTerm = () => {
- let len = props.nodeConfig.conditionNodes.length + 1
- props.nodeConfig.conditionNodes.push({
- nodeName: '条件' + len,
- priorityLevel: len,
- emits('update:nodeConfig', props.nodeConfig)
-const delTerm = (index) => {
- props.nodeConfig.conditionNodes.splice(index, 1)
- props.nodeConfig.conditionNodes.map((item, index) => {
- item.priorityLevel = index + 1
- item.nodeName = `条件${index + 1}`
- if (props.nodeConfig.conditionNodes.length == 1) {
- if (props.nodeConfig.childNode) {
- if (props.nodeConfig.conditionNodes[0].childNode) {
- reData(props.nodeConfig.conditionNodes[0].childNode, props.nodeConfig.childNode)
- props.nodeConfig.conditionNodes[0].childNode = props.nodeConfig.childNode
- emits('update:nodeConfig', props.nodeConfig.conditionNodes[0].childNode)
-const reData = (data, addData) => {
- if (!data.childNode) {
- data.childNode = addData
- reData(data.childNode, addData)
-const setPerson = (priorityLevel) => {
- var { type } = props.nodeConfig
- if (type == 0) {
- setPromoter(true)
- setFlowPermission({
- value: props.flowPermission,
- flag: false,
- id: _uid
- } else if (type == 1) {
- setApprover(true)
- setApproverConfig({
- value: {
- ...JSON.parse(JSON.stringify(props.nodeConfig)),
- ...{ settype: props.nodeConfig.settype ? props.nodeConfig.settype : 1 }
- setCopyer(true)
- setCopyerConfig({
- value: JSON.parse(JSON.stringify(props.nodeConfig)),
- setCondition(true)
- setConditionsConfig({
- priorityLevel,
-const arrTransfer = (index, type = 1) => {
- //向左-1,向右1
- props.nodeConfig.conditionNodes[index] = props.nodeConfig.conditionNodes.splice(
- index + type,
- 1,
- props.nodeConfig.conditionNodes[index]
- )[0]
@@ -1,165 +0,0 @@
-/**
- * todo
- */
-export const arrToStr = (arr?: [{ name: string }]) => {
- if (arr) {
- return arr
- .map((item) => {
- return item.name
- .toString()
-export const setApproverStr = (nodeConfig: any) => {
- if (nodeConfig.settype == 1) {
- if (nodeConfig.nodeUserList.length == 1) {
- return nodeConfig.nodeUserList[0].name
- } else if (nodeConfig.nodeUserList.length > 1) {
- if (nodeConfig.examineMode == 1) {
- return arrToStr(nodeConfig.nodeUserList)
- } else if (nodeConfig.examineMode == 2) {
- return nodeConfig.nodeUserList.length + '人会签'
- } else if (nodeConfig.settype == 2) {
- const level =
- nodeConfig.directorLevel == 1 ? '直接主管' : '第' + nodeConfig.directorLevel + '级主管'
- return level
- return level + '会签'
- } else if (nodeConfig.settype == 4) {
- if (nodeConfig.selectRange == 1) {
- return '发起人自选'
- if (nodeConfig.nodeUserList.length > 0) {
- if (nodeConfig.selectRange == 2) {
- return '发起人从' + nodeConfig.nodeUserList[0].name + '中自选'
- return ''
- } else if (nodeConfig.settype == 5) {
- return '发起人自己'
- } else if (nodeConfig.settype == 7) {
- return '从直接主管到通讯录中级别最高的第' + nodeConfig.examineEndDirectorLevel + '个层级主管'
-export const copyerStr = (nodeConfig: any) => {
- if (nodeConfig.nodeUserList.length != 0) {
- if (nodeConfig.ccSelfSelectFlag == 1) {
-export const conditionStr = (nodeConfig, index) => {
- const { conditionList, nodeUserList } = nodeConfig.conditionNodes[index]
- if (conditionList.length == 0) {
- return index == nodeConfig.conditionNodes.length - 1 &&
- nodeConfig.conditionNodes[0].conditionList.length != 0
- ? '其他条件进入此流程'
- : '请设置条件'
- let str = ''
- for (let i = 0; i < conditionList.length; i++) {
- const {
- columnId,
- columnType,
- showType,
- showName,
- optType,
- zdy1,
- opt1,
- zdy2,
- opt2,
- fixedDownBoxValue
- } = conditionList[i]
- if (columnId == 0) {
- if (nodeUserList.length != 0) {
- str += '发起人属于:'
- str +=
- nodeUserList
- .join('或') + ' 并且 '
- if (columnType == 'String' && showType == '3') {
- if (zdy1) {
- str += showName + '属于:' + dealStr(zdy1, JSON.parse(fixedDownBoxValue)) + ' 并且 '
- if (columnType == 'Double') {
- if (optType != 6 && zdy1) {
- const optTypeStr = ['', '<', '>', '≤', '=', '≥'][optType]
- str += `${showName} ${optTypeStr} ${zdy1} 并且 `
- } else if (optType == 6 && zdy1 && zdy2) {
- str += `${zdy1} ${opt1} ${showName} ${opt2} ${zdy2} 并且 `
- return str ? str.substring(0, str.length - 4) : '请设置条件'
-export const dealStr = (str: string, obj) => {
- const arr = []
- const list = str.split(',')
- for (const elem in obj) {
- list.map((item) => {
- if (item == elem) {
- arr.push(obj[elem].value)
- return arr.join('或')
-export const removeEle = (arr, elem, key = 'id') => {
- let includesIndex
- arr.map((item, index) => {
- if (item[key] == elem[key]) {
- includesIndex = index
- arr.splice(includesIndex, 1)
-export const bgColors = ['87, 106, 149', '255, 148, 62', '50, 150, 250']
-export const placeholderList = ['发起人', '审核人', '抄送人']
-export const setTypes = [
- { value: 1, label: '指定成员' },
- { value: 2, label: '主管' },
- { value: 4, label: '发起人自选' },
- { value: 5, label: '发起人自己' },
- { value: 7, label: '连续多级主管' }
-]
-export const selectModes = [
- { value: 1, label: '选一个人' },
- { value: 2, label: '选多个人' }
-export const selectRanges = [
- { value: 1, label: '全公司' },
- { value: 2, label: '指定成员' },
- { value: 3, label: '指定角色' }
-export const optTypes = [
- { value: '1', label: '小于' },
- { value: '2', label: '大于' },
- { value: '3', label: '小于等于' },
- { value: '4', label: '等于' },
- { value: '5', label: '大于等于' },
- { value: '6', label: '介于两个数之间' }
@@ -1,1292 +0,0 @@
-.clearfix {
- zoom: 1
-.clearfix:after,
-.clearfix:before {
- content: "";
- display: table
-.clearfix:after {
- clear: both
-@font-face {
- font-family: anticon;
- font-display: fallback;
- src: url("https://at.alicdn.com/t/font_148784_v4ggb6wrjmkotj4i.eot");
- src: url("https://at.alicdn.com/t/font_148784_v4ggb6wrjmkotj4i.woff") format("woff"), url("https://at.alicdn.com/t/font_148784_v4ggb6wrjmkotj4i.ttf") format("truetype"), url("https://at.alicdn.com/t/font_148784_v4ggb6wrjmkotj4i.svg#iconfont") format("svg")
-.anticon {
- font-style: normal;
- vertical-align: baseline;
- text-transform: none;
- line-height: 1;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale
-.anticon:before {
- display: block;
- font-family: anticon!important
-.anticon-close:before {
- content: "\E633"
-.anticon-right:before {
- content: "\E61F"
-.anticon-exclamation-circle{
- color: rgb(242, 86, 67)
-.anticon-exclamation-circle:before {
- content: "\E62C"
-.anticon-left:before {
- content: "\E620"
-.anticon-close-circle:before {
- content: "\E62E"
-.ant-btn {
- line-height: 1.5;
- font-weight: 400;
- touch-action: manipulation;
- background-image: none;
- border: 1px solid transparent;
- white-space: nowrap;
- padding: 0 15px;
- font-size: 14px;
- border-radius: 4px;
- height: 32px;
- transition: all .3s cubic-bezier(.645, .045, .355, 1);
- color: rgba(0, 0, 0, .65);
- background-color: #fff;
- border-color: #d9d9d9
-.ant-btn>.anticon {
- line-height: 1
-.ant-btn,
-.ant-btn:active,
-.ant-btn:focus {
- outline: 0
-.ant-btn>a:only-child {
- color: currentColor
-.ant-btn>a:only-child:after {
- background: transparent
-.ant-btn:focus,
-.ant-btn:hover {
- color: #40a9ff;
- border-color: #40a9ff
-.ant-btn:focus>a:only-child,
-.ant-btn:hover>a:only-child {
-.ant-btn:focus>a:only-child:after,
-.ant-btn:hover>a:only-child:after {
-.ant-btn.active,
-.ant-btn:active {
- color: #096dd9;
- border-color: #096dd9
-.ant-btn.active>a:only-child,
-.ant-btn:active>a:only-child {
-.ant-btn.active>a:only-child:after,
-.ant-btn:active>a:only-child:after {
- text-decoration: none
-.ant-btn>i,
-.ant-btn>span {
- pointer-events: none
-.ant-btn:before {
- top: -1px;
- left: -1px;
- bottom: -1px;
- right: -1px;
- opacity: .35;
- border-radius: inherit;
- z-index: 1;
- transition: opacity .2s;
- pointer-events: none;
- display: none
-.ant-btn .anticon {
- transition: margin-left .3s cubic-bezier(.645, .045, .355, 1)
-.ant-btn:active>span,
-.ant-btn:focus>span {
- position: relative
-.ant-btn>.anticon+span,
-.ant-btn>span+.anticon {
- margin-left: 8px
-.ant-input {
- font-family: Chinese Quote, -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif;
- font-variant: tabular-nums;
- box-sizing: border-box;
- margin: 0;
- padding: 0;
- list-style: none;
- padding: 4px 11px;
- width: 100%;
- border: 1px solid #d9d9d9;
- transition: all .3s
-.ant-input::-moz-placeholder {
- color: #bfbfbf;
- opacity: 1
-.ant-input:-ms-input-placeholder {
- color: #bfbfbf
-.ant-input::-webkit-input-placeholder {
-.ant-input:focus,
-.ant-input:hover {
- border-color: #40a9ff;
- border-right-width: 1px!important
-.ant-input:focus {
- outline: 0;
- box-shadow: 0 0 0 2px rgba(24, 144, 255, .2)
-textarea.ant-input {
- max-width: 100%;
- height: auto;
- vertical-align: bottom;
- transition: all .3s, height 0s;
- min-height: 32px
-a,
-abbr,
-acronym,
-address,
-applet,
-article,
-aside,
-audio,
-b,
-big,
-blockquote,
-body,
-canvas,
-caption,
-center,
-cite,
-code,
-dd,
-del,
-details,
-dfn,
-div,
-dl,
-dt,
-em,
-fieldset,
-figcaption,
-figure,
-footer,
-form,
-h1,
-h2,
-h3,
-h4,
-h5,
-h6,
-header,
-hgroup,
-html,
-i,
-iframe,
-img,
-ins,
-kbd,
-label,
-legend,
-li,
-mark,
-menu,
-nav,
-object,
-ol,
-p,
-pre,
-q,
-s,
-samp,
-section,
-small,
-span,
-strike,
-strong,
-sub,
-summary,
-sup,
-table,
-tbody,
-td,
-tfoot,
-th,
-thead,
-time,
-tr,
-tt,
-u,
-ul,
-var,
-video {
- border: 0;
- font-size: 100%;
- font: inherit;
- vertical-align: baseline
-*,
-:after,
-:before {
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box
-html {
- font-family: sans-serif;
- -ms-text-size-adjust: 100%;
- -webkit-text-size-adjust: 100%
- font-size: 14px
-body {
- font-family: Microsoft Yahei, Lucida Grande, Lucida Sans Unicode, Helvetica, Arial, Verdana, sans-serif;
- line-height: 1.6;
- position: static!important;
- -webkit-tap-highlight-color: rgba(0, 0, 0, 0)
-ul {
- list-style-type: none
-strong {
- font-weight: 700
-img {
- border: 0
-button,
-input,
-select,
-textarea {
- font-family: inherit;
- margin: 0
- overflow: auto;
- vertical-align: top;
- -webkit-appearance: none
-input {
- line-height: normal
-select {
- text-transform: none
-html input[type=button],
-input[type=reset],
-input[type=submit] {
- -webkit-appearance: button;
- cursor: pointer
-input[type=search] {
- -webkit-appearance: textfield;
- -moz-box-sizing: content-box;
- -webkit-box-sizing: content-box;
- box-sizing: content-box
-input[type=search]::-webkit-search-cancel-button,
-input[type=search]::-webkit-search-decoration {
-button::-moz-focus-inner,
-input::-moz-focus-inner {
- padding: 0
-table {
- border-spacing: 0;
- border-collapse: collapse
-th {
- vertical-align: top
- font-weight: 700;
- text-align: left
-thead th {
- white-space: nowrap
-a {
- text-decoration: none;
- color: #3296fa
-a:active,
-a:hover {
-small {
- font-size: 80%
- font-size: 12px!important;
- color: #191f25!important;
- background: #f6f6f6!important
-.wrap {
- display: -webkit-box;
- display: -ms-flexbox;
- -webkit-box-orient: vertical;
- -webkit-box-direction: normal;
- -ms-flex-direction: column;
- flex-direction: column;
- height: 100%
- font-family: IconFont;
- src: url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.eot");
- src: url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.eot?#iefix") format("embedded-opentype"), url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.woff") format("woff"), url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.ttf") format("truetype"), url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.svg#IconFont") format("svg")
-.iconfont {
- font-family: IconFont!important;
- -webkit-text-stroke-width: .2px;
-.fd-nav {
- position: fixed;
- z-index: 997;
- height: 60px;
- align-items: center
-.fd-nav>* {
- width: 100%
-.fd-nav .fd-nav-left {
-.fd-nav .fd-nav-center {
- flex: none;
- width: 600px;
- text-align: center
-.fd-nav .fd-nav-right {
- align-items: center;
- justify-content: flex-end;
- text-align: right
-.fd-nav .fd-nav-back {
- width: 60px;
- font-size: 22px;
- border-right: 1px solid #1583f2;
-.fd-nav .fd-nav-back:hover {
- background: #5af
-.fd-nav .fd-nav-back:active {
- background: #1583f2
-.fd-nav .fd-nav-back .anticon {
- line-height: 60px
-.fd-nav .fd-nav-title {
- width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- padding: 0 15px
-.fd-nav a {
- margin-left: 12px
-.fd-nav .button-publish {
- min-width: 80px;
- margin-left: 4px;
- margin-right: 15px;
- border-color: #fff
-.fd-nav .button-publish.ant-btn:focus,
-.fd-nav .button-publish.ant-btn:hover {
- border-color: #fff;
- box-shadow: 0 10px 20px 0 rgba(0, 0, 0, .3)
-.fd-nav .button-publish.ant-btn:active {
- background: #d6eaff;
- box-shadow: none
-.fd-nav .button-preview {
- margin-left: 16px;
- margin-right: 4px;
-.fd-nav .button-preview.ant-btn:focus,
-.fd-nav .button-preview.ant-btn:hover {
- background: #59acfc
-.fd-nav .button-preview.ant-btn:active {
- background: #2186ef
-.fd-nav-content {
- top: 60px;
- overflow-x: hidden;
- overflow-y: auto;
- padding-bottom: 30px
-.error-modal-desc {
- font-size: 13px;
- color: rgba(25, 31, 37, .56);
- line-height: 22px;
- margin-bottom: 14px
-.error-modal-list {
- height: 200px;
- margin-right: -25px;
- padding-right: 25px
-.error-modal-item {
- padding: 10px 20px;
- line-height: 21px;
- background: #f6f6f6;
- justify-content: space-between;
- margin-bottom: 8px;
- border-radius: 4px
-.error-modal-item-label {
- font-size: 15px;
- padding-right: 10px
-.error-modal-item-content {
- text-align: right;
- color: #191f25
-#body.blur {
- -webkit-filter: blur(3px);
- filter: blur(3px)
-.zoom {
- -webkit-box-align: center;
- -ms-flex-align: center;
- -webkit-box-pack: justify;
- -ms-flex-pack: justify;
- height: 40px;
- width: 125px;
- right: 40px;
- margin-top: 30px;
- z-index: 10
-.zoom .zoom-in,
-.zoom .zoom-out {
- color: #c1c1cd;
- background-size: 100%;
- background-repeat: no-repeat
- background-image: url(https://gw.alicdn.com/tfs/TB1s0qhBHGYBuNjy0FoXXciBFXa-90-90.png)
-.zoom .zoom-out.disabled {
- opacity: .5
-.zoom .zoom-in {
- background-image: url(https://gw.alicdn.com/tfs/TB1UIgJBTtYBeNjy1XdXXXXyVXa-90-90.png)
-.zoom .zoom-in.disabled {
-.auto-judge:hover .editable-title,
-.node-wrap-box:hover .editable-title {
- border-bottom: 1px dashed #fff
-.auto-judge:hover .editable-title.editing,
-.node-wrap-box:hover .editable-title.editing {
- border: 1px solid #d9d9d9
-.auto-judge:hover .editable-title {
- border-color: #15bc83
-.editable-title {
- line-height: 15px;
- border-bottom: 1px dashed transparent
-.editable-title:before {
- right: 40px
-.editable-title:hover {
-.editable-title-input {
- height: 18px;
- padding-left: 4px;
- text-indent: 0;
- font-size: 12px;
- line-height: 18px;
- z-index: 1
-.editable-title-input:hover {
-.node-wrap-box {
- display: -webkit-inline-box;
- display: -ms-inline-flexbox;
- width: 220px;
- min-height: 72px;
-.node-wrap-box:after {
- z-index: 2;
- transition: all .1s cubic-bezier(.645, .045, .355, 1);
- box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .1)
-.node-wrap-box.active:after,
-.node-wrap-box:active:after,
-.node-wrap-box:hover:after {
- border: 1px solid #3296fa;
- box-shadow: 0 0 6px 0 rgba(50, 150, 250, .3)
-.node-wrap-box.active .close,
-.node-wrap-box:active .close,
-.node-wrap-box:hover .close {
- display: block
-.node-wrap-box.error:after {
- border: 1px solid #f25643;
-.node-wrap-box .title {
- padding-left: 16px;
- padding-right: 30px;
- height: 24px;
- line-height: 24px;
- text-align: left;
- background: #576a95;
- border-radius: 4px 4px 0 0
-.node-wrap-box .title .iconfont {
- margin-right: 5px
-.node-wrap-box .placeholder {
-.node-wrap-box .close {
- display: none;
- right: 10px;
- top: 50%;
- transform: translateY(-50%);
- width: 20px;
- height: 20px;
- line-height: 20px
-.node-wrap-box .content {
- padding: 16px;
- padding-right: 30px
-.node-wrap-box .content .text {
- -webkit-line-clamp: 3;
- -webkit-box-orient: vertical
-.node-wrap-box .content .arrow {
- height: 14px;
- color: #979797
-.start-node.node-wrap-box .content .text {
-.node-wrap-box:before {
- top: -12px;
- left: 50%;
- -webkit-transform: translateX(-50%);
- transform: translateX(-50%);
- height: 4px;
- border-style: solid;
- border-width: 8px 6px 4px;
- border-color: #cacaca transparent transparent;
- background: #f5f5f7
-.node-wrap-box.start-node:before {
- content: none
-.top-left-cover-line {
- left: -1px
-.top-left-cover-line,
-.top-right-cover-line {
- height: 8px;
- width: 50%;
- background-color: #f5f5f7;
- top: -4px
- right: -1px
-.bottom-left-cover-line {
-.bottom-left-cover-line,
-.bottom-right-cover-line {
- bottom: -4px
-.dingflow-design {
- top: 0
-.dingflow-design .box-scale {
- transform: scale(1);
- padding: 54.5px 0;
- -webkit-box-align: start;
- -ms-flex-align: start;
- align-items: flex-start;
- -ms-flex-pack: center;
- -ms-flex-wrap: wrap;
- flex-wrap: wrap;
- min-width: -webkit-min-content;
- min-width: -moz-min-content;
- min-width: min-content;
- transform-origin: 50% 0px 0px;
-.dingflow-design .node-wrap {
- -webkit-box-pack: start;
- -ms-flex-pack: start;
- justify-content: flex-start;
- padding: 0 50px;
-.dingflow-design .branch-wrap,
-.dingflow-design .branch-box-wrap {
- min-height: 270px;
- flex-shrink: 0
-.dingflow-design .branch-box {
- overflow: visible;
- min-height: 180px;
- border-bottom: 2px solid #ccc;
- border-top: 2px solid #ccc;
- margin-top: 15px
-.dingflow-design .branch-box .col-box {
-.dingflow-design .branch-box .col-box:before {
- z-index: 0;
- background-color: #cacaca
-.dingflow-design .add-branch {
- padding: 0 10px;
- border-radius: 15px;
- box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .1);
- top: -16px;
- transform-origin: center center;
- -webkit-transition: all .3s cubic-bezier(.645, .045, .355, 1);
- transition: all .3s cubic-bezier(.645, .045, .355, 1)
-.dingflow-design .add-branch:hover {
- transform: translateX(-50%) scale(1.1);
- box-shadow: 0 8px 16px 0 rgba(0, 0, 0, .1)
-.dingflow-design .add-branch:active {
-.dingflow-design .col-box {
-.dingflow-design .condition-node {
- min-height: 220px
-.dingflow-design .condition-node,
-.dingflow-design .condition-node-box {
- -webkit-box-flex: 1
- padding-top: 30px;
- padding-right: 50px;
- padding-left: 50px;
-.dingflow-design .condition-node-box:before {
-.dingflow-design .auto-judge {
- padding: 14px 19px;
-.dingflow-design .auto-judge:after {
-.dingflow-design .auto-judge.active:after,
-.dingflow-design .auto-judge:active:after,
-.dingflow-design .auto-judge:hover:after {
-.dingflow-design .auto-judge.active .close,
-.dingflow-design .auto-judge:active .close,
-.dingflow-design .auto-judge:hover .close {
-.dingflow-design .auto-judge.error:after {
-.dingflow-design .auto-judge .title-wrapper {
- line-height: 16px
-.dingflow-design .auto-judge .title-wrapper .editable-title {
- max-width: 120px;
- text-overflow: ellipsis
-.dingflow-design .auto-judge .title-wrapper .priority-title {
- float: right;
- color: rgba(25, 31, 37, .56)
-.dingflow-design .auto-judge .placeholder {
-.dingflow-design .auto-judge .close {
- right: -10px;
- top: -10px;
- color: rgba(0, 0, 0, .25);
- line-height: 20px;
- z-index: 2
-.dingflow-design .auto-judge .content {
- color: #191f25;
- margin-top: 6px;
-.dingflow-design .auto-judge .sort-left,
-.dingflow-design .auto-judge .sort-right {
-.dingflow-design .auto-judge .sort-left {
- border-right: 1px solid #f6f6f6
- border-left: 1px solid #f6f6f6
-.dingflow-design .auto-judge:hover .sort-left,
-.dingflow-design .auto-judge:hover .sort-right {
-.dingflow-design .auto-judge .sort-left:hover,
-.dingflow-design .auto-judge .sort-right:hover {
- background: #efefef
-.dingflow-design .end-node {
- color: rgba(25, 31, 37, .4);
-.dingflow-design .end-node .end-node-circle {
- width: 10px;
- height: 10px;
- background: #dbdcdc
-.dingflow-design .end-node .end-node-text {
- margin-top: 5px;
-.approval-setting {
- border-radius: 2px;
- margin: 20px 0;
- background: #fff
@@ -0,0 +1,168 @@
+<template>
+ <div class="node-handler-wrapper">
+ <div class="node-handler" v-if="props.showAdd">
+ <el-popover
+ trigger="hover"
+ v-model:visible="popoverShow"
+ placement="right-start"
+ width="auto"
+ >
+ <div class="handler-item-wrapper">
+ <div class="handler-item" @click="addNode(NodeType.USER_TASK_NODE)">
+ <div class="approve handler-item-icon">
+ <span class="iconfont icon-approve icon-size"></span>
+ </div>
+ <div class="handler-item-text">审批人</div>
+ <div class="handler-item" @click="addNode(NodeType.COPY_TASK_NODE)">
+ <div class="handler-item-icon copy">
+ <span class="iconfont icon-size icon-copy"></span>
+ <div class="handler-item-text">抄送</div>
+ <div class="handler-item" @click="addNode(NodeType.CONDITION_BRANCH_NODE)">
+ <div class="handler-item-icon condition">
+ <span class="iconfont icon-size icon-exclusive"></span>
+ <div class="handler-item-text">条件分支</div>
+ <div class="handler-item" @click="addNode(NodeType.PARALLEL_BRANCH_NODE)">
+ <span class="iconfont icon-size icon-parallel"></span>
+ <div class="handler-item-text">并行分支</div>
+ <template #reference>
+ <div class="add-icon"><Icon icon="ep:plus" /></div>
+ </template>
+ </el-popover>
+</template>
+<script setup lang="ts">
+import {
+ ApproveMethodType,
+ AssignEmptyHandlerType,
+ AssignStartUserHandlerType,
+ NODE_DEFAULT_NAME,
+ NodeType,
+ RejectHandlerType,
+ SimpleFlowNode
+} from './consts'
+import { generateUUID } from '@/utils'
+defineOptions({
+ name: 'NodeHandler'
+})
+const popoverShow = ref(false)
+const props = defineProps({
+ childNode: {
+ type: Object as () => SimpleFlowNode,
+ default: null
+ showAdd: {
+ // 是否显示添加节点
+ type: Boolean,
+ default: true
+const emits = defineEmits(['update:childNode'])
+const addNode = (type: number) => {
+ popoverShow.value = false
+ if (type === NodeType.USER_TASK_NODE) {
+ const id = 'Activity_' + generateUUID()
+ const data: SimpleFlowNode = {
+ id: id,
+ name: NODE_DEFAULT_NAME.get(NodeType.USER_TASK_NODE) as string,
+ showText: '',
+ type: NodeType.USER_TASK_NODE,
+ approveMethod: ApproveMethodType.SEQUENTIAL_APPROVE,
+ // 超时处理
+ rejectHandler: {
+ type: RejectHandlerType.FINISH_PROCESS
+ timeoutHandler: {
+ enable: false
+ assignEmptyHandler: {
+ type: AssignEmptyHandlerType.APPROVE
+ assignStartUserHandlerType: AssignStartUserHandlerType.START_USER_AUDIT,
+ childNode: props.childNode
+ emits('update:childNode', data)
+ if (type === NodeType.COPY_TASK_NODE) {
+ id: 'Activity_' + generateUUID(),
+ name: NODE_DEFAULT_NAME.get(NodeType.COPY_TASK_NODE) as string,
+ type: NodeType.COPY_TASK_NODE,
+ if (type === NodeType.CONDITION_BRANCH_NODE) {
+ name: '条件分支',
+ type: NodeType.CONDITION_BRANCH_NODE,
+ id: 'GateWay_' + generateUUID(),
+ childNode: props.childNode,
+ conditionNodes: [
+ {
+ id: 'Flow_' + generateUUID(),
+ name: '条件1',
+ type: NodeType.CONDITION_NODE,
+ childNode: undefined,
+ conditionType: 1,
+ defaultFlow: false
+ name: '其它情况',
+ showText: '其它情况进入此流程',
+ conditionType: undefined,
+ defaultFlow: true
+ ]
+ if (type === NodeType.PARALLEL_BRANCH_NODE) {
+ name: '并行分支',
+ type: NodeType.PARALLEL_BRANCH_NODE,
+ name: '并行1',
+ showText: '无需配置条件同时执行',
+ childNode: undefined
+ name: '并行2',
+</script>
+<style lang="scss" scoped></style>
@@ -0,0 +1,107 @@
+ <!-- 发起人节点 -->
+ <StartUserNode
+ v-if="currentNode && currentNode.type === NodeType.START_USER_NODE"
+ :flow-node="currentNode"
+ />
+ <!-- 审批节点 -->
+ <UserTaskNode
+ v-if="currentNode && currentNode.type === NodeType.USER_TASK_NODE"
+ @update:flow-node="handleModelValueUpdate"
+ @find:parent-node="findFromParentNode"
+ <!-- 抄送节点 -->
+ <CopyTaskNode
+ v-if="currentNode && currentNode.type === NodeType.COPY_TASK_NODE"
+ <!-- 条件节点 -->
+ <ExclusiveNode
+ v-if="currentNode && currentNode.type === NodeType.CONDITION_BRANCH_NODE"
+ @update:model-value="handleModelValueUpdate"
+ <!-- 并行节点 -->
+ <ParallelNode
+ v-if="currentNode && currentNode.type === NodeType.PARALLEL_BRANCH_NODE"
+ <!-- 递归显示孩子节点 -->
+ <ProcessNodeTree
+ v-if="currentNode && currentNode.childNode"
+ v-model:flow-node="currentNode.childNode"
+ :parent-node="currentNode"
+ @find:recursive-find-parent-node="recursiveFindParentNode"
+ <!-- 结束节点 -->
+ <EndEventNode v-if="currentNode && currentNode.type === NodeType.END_EVENT_NODE" />
+import StartUserNode from './nodes/StartUserNode.vue'
+import EndEventNode from './nodes/EndEventNode.vue'
+import UserTaskNode from './nodes/UserTaskNode.vue'
+import CopyTaskNode from './nodes/CopyTaskNode.vue'
+import ExclusiveNode from './nodes/ExclusiveNode.vue'
+import ParallelNode from './nodes/ParallelNode.vue'
+import { SimpleFlowNode, NodeType } from './consts'
+import { useWatchNode } from './node'
+ name: 'ProcessNodeTree'
+ parentNode: {
+ default: () => null
+ flowNode: {
+const emits = defineEmits<{
+ 'update:flowNode': [node: SimpleFlowNode | undefined]
+ 'find:recursiveFindParentNode': [
+ nodeList: SimpleFlowNode[],
+ curentNode: SimpleFlowNode,
+ nodeType: number
+}>()
+const currentNode = useWatchNode(props)
+// 用于删除节点
+const handleModelValueUpdate = (updateValue) => {
+ emits('update:flowNode', updateValue)
+const findFromParentNode = (nodeList: SimpleFlowNode[], nodeType: number) => {
+ emits('find:recursiveFindParentNode', nodeList, props.parentNode, nodeType)
+// 递归从父节点中查询匹配的节点
+const recursiveFindParentNode = (
+ findNode: SimpleFlowNode,
+) => {
+ if (!findNode) {
+ return
+ if (findNode.type === NodeType.START_USER_NODE) {
+ nodeList.push(findNode)
+ if (findNode.type === nodeType) {
@@ -0,0 +1,212 @@
+ <div class="simple-flow-canvas" v-loading="loading">
+ <div class="simple-flow-container">
+ <div class="top-area-container">
+ <div class="top-actions">
+ <div class="canvas-control">
+ <span class="control-scale-group">
+ <span class="control-scale-button"> <Icon icon="ep:plus" @click="zoomOut()" /></span>
+ <span class="control-scale-label">{{ scaleValue }}%</span>
+ <span class="control-scale-button"><Icon icon="ep:minus" @click="zoomIn()" /></span>
+ </span>
+ <el-button type="primary" @click="saveSimpleFlowModel">保存</el-button>
+ <!-- <el-button type="primary">全局设置</el-button> -->
+ <div class="scale-container" :style="`transform: scale(${scaleValue / 100});`">
+ <ProcessNodeTree v-if="processNodeTree" v-model:flow-node="processNodeTree" />
+ <Dialog v-model="errorDialogVisible" title="保存失败" width="400" :fullscreen="false">
+ <div class="mb-2">以下节点内容不完善,请修改后保存</div>
+ <div
+ class="mb-3 b-rounded-1 bg-gray-100 p-2 line-height-normal"
+ v-for="(item, index) in errorNodes"
+ :key="index"
+ {{ item.name }} : {{ NODE_DEFAULT_TEXT.get(item.type) }}
+ <template #footer>
+ <el-button type="primary" @click="errorDialogVisible = false">知道了</el-button>
+ </Dialog>
+import ProcessNodeTree from './ProcessNodeTree.vue'
+import { updateBpmSimpleModel, getBpmSimpleModel } from '@/api/bpm/simple'
+import { SimpleFlowNode, NodeType, NodeId, NODE_DEFAULT_TEXT } from './consts'
+import { getModel } from '@/api/bpm/model'
+import { getForm, FormVO } from '@/api/bpm/form'
+import { handleTree } from '@/utils/tree'
+import * as RoleApi from '@/api/system/role'
+import * as DeptApi from '@/api/system/dept'
+import * as PostApi from '@/api/system/post'
+import * as UserApi from '@/api/system/user'
+import * as UserGroupApi from '@/api/bpm/userGroup'
+ name: 'SimpleProcessDesigner'
+const router = useRouter() // 路由
+ modelId: {
+ type: String,
+ required: true
+const loading = ref(true)
+const formFields = ref<string[]>([])
+const formType = ref(20)
+const roleOptions = ref<RoleApi.RoleVO[]>([]) // 角色列表
+const postOptions = ref<PostApi.PostVO[]>([]) // 岗位列表
+const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
+const deptOptions = ref<DeptApi.DeptVO[]>([]) // 部门列表
+const deptTreeOptions = ref()
+const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表
+provide('formFields', formFields)
+provide('formType', formType)
+provide('roleList', roleOptions)
+provide('postList', postOptions)
+provide('userList', userOptions)
+provide('deptList', deptOptions)
+provide('userGroupList', userGroupOptions)
+provide('deptTree', deptTreeOptions)
+const message = useMessage() // 国际化
+const processNodeTree = ref<SimpleFlowNode | undefined>()
+const errorDialogVisible = ref(false)
+let errorNodes: SimpleFlowNode[] = []
+const saveSimpleFlowModel = async () => {
+ if (!props.modelId) {
+ message.error('缺少模型 modelId 编号')
+ errorNodes = []
+ validateNode(processNodeTree.value, errorNodes)
+ if (errorNodes.length > 0) {
+ errorDialogVisible.value = true
+ const data = {
+ id: props.modelId,
+ simpleModel: processNodeTree.value
+ const result = await updateBpmSimpleModel(data)
+ if (result) {
+ message.success('修改成功')
+ close()
+ } else {
+ message.alert('修改失败')
+// 校验节点设置。 暂时以 showText 为空 未节点错误配置
+const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNode[]) => {
+ if (node) {
+ const { type, showText, conditionNodes } = node
+ if (type == NodeType.END_EVENT_NODE) {
+ if (type == NodeType.START_USER_NODE) {
+ validateNode(node.childNode, errorNodes)
+ if (!showText) {
+ errorNodes.push(node)
+ if (type === NodeType.CONDITION_NODE) {
+ if (type == NodeType.CONDITION_BRANCH_NODE) {
+ conditionNodes?.forEach((item) => {
+ validateNode(item, errorNodes)
+const close = () => {
+ router.push({ path: '/bpm/manager/model' })
+let scaleValue = ref(100)
+const MAX_SCALE_VALUE = 200
+const MIN_SCALE_VALUE = 50
+// 放大
+const zoomOut = () => {
+ if (scaleValue.value == MAX_SCALE_VALUE) {
+ scaleValue.value += 10
+// 缩小
+const zoomIn = () => {
+ if (scaleValue.value == MIN_SCALE_VALUE) {
+ scaleValue.value -= 10
+onMounted(async () => {
+ try {
+ loading.value = true
+ // 获取表单字段
+ const bpmnModel = await getModel(props.modelId)
+ if (bpmnModel) {
+ formType.value = bpmnModel.formType
+ if (formType.value === 10) {
+ const bpmnForm = (await getForm(bpmnModel.formId)) as unknown as FormVO
+ formFields.value = bpmnForm?.fields
+ // 获得角色列表
+ roleOptions.value = await RoleApi.getSimpleRoleList()
+ // 获得岗位列表
+ postOptions.value = await PostApi.getSimplePostList()
+ // 获得用户列表
+ userOptions.value = await UserApi.getSimpleUserList()
+ // 获得部门列表
+ deptOptions.value = await DeptApi.getSimpleDeptList()
+ deptTreeOptions.value = handleTree(deptOptions.value as DeptApi.DeptVO[], 'id')
+ // 获取用户组列表
+ userGroupOptions.value = await UserGroupApi.getUserGroupSimpleList()
+ // 获取 SIMPLE 设计器模型
+ const result = await getBpmSimpleModel(props.modelId)
+ processNodeTree.value = result
+ // 初始值
+ processNodeTree.value = {
+ name: '发起人',
+ type: NodeType.START_USER_NODE,
+ id: NodeId.START_USER_NODE_ID,
+ id: NodeId.END_EVENT_NODE_ID,
+ name: '结束',
+ type: NodeType.END_EVENT_NODE
+ } finally {
+ loading.value = false
@@ -0,0 +1,544 @@
+// @ts-ignore
+import { DictDataVO } from '@/api/system/dict/types'
+ * 节点类型
+export enum NodeType {
+ * 结束节点
+ END_EVENT_NODE = 1,
+ * 发起人节点
+ START_USER_NODE = 10,
+ * 审批人节点
+ USER_TASK_NODE = 11,
+ * 抄送人节点
+ COPY_TASK_NODE = 12,
+ * 条件节点
+ CONDITION_NODE = 50,
+ * 条件分支节点 (对应排他网关)
+ CONDITION_BRANCH_NODE = 51,
+ * 并行分支节点 (对应并行网关)
+ PARALLEL_BRANCH_NODE = 52,
+ * 包容分支节点 (对应包容网关)
+ INCLUSIVE_BRANCH_NODE = 53
+export enum NodeId {
+ * 发起人节点 Id
+ START_USER_NODE_ID = 'StartUserNode',
+ END_EVENT_NODE_ID = 'EndEvent'
+ * 节点结构定义
+export interface SimpleFlowNode {
+ id: string
+ type: NodeType
+ showText?: string
+ // 孩子节点
+ childNode?: SimpleFlowNode
+ // 条件节点
+ conditionNodes?: SimpleFlowNode[]
+ // 审批类型
+ approveType?: ApproveType
+ // 候选人策略
+ candidateStrategy?: number
+ // 候选人参数
+ candidateParam?: string
+ // 多人审批方式
+ approveMethod?: ApproveMethodType
+ //通过比例
+ approveRatio?: number
+ // 审批按钮设置
+ buttonsSetting?: any[]
+ // 表单权限
+ fieldsPermission?: Array<Record<string, string>>
+ // 审批任务超时处理
+ timeoutHandler?: TimeoutHandler
+ // 审批任务拒绝处理
+ rejectHandler?: RejectHandler
+ // 审批人为空的处理
+ assignEmptyHandler?: AssignEmptyHandler
+ // 审批节点的审批人与发起人相同时,对应的处理类型
+ assignStartUserHandlerType?: number
+ // 条件类型
+ conditionType?: ConditionType
+ // 条件表达式
+ conditionExpression?: string
+ // 条件组
+ conditionGroups?: ConditionGroup
+ // 是否默认的条件
+ defaultFlow?: boolean
+// 候选人策略枚举 ( 用于审批节点。抄送节点 )
+export enum CandidateStrategy {
+ * 指定角色
+ ROLE = 10,
+ * 部门成员
+ DEPT_MEMBER = 20,
+ * 部门的负责人
+ DEPT_LEADER = 21,
+ * 连续多级部门的负责人
+ MULTI_LEVEL_DEPT_LEADER = 23,
+ * 指定岗位
+ POST = 22,
+ * 指定用户
+ USER = 30,
+ * 发起人自选
+ START_USER_SELECT = 35,
+ * 发起人自己
+ START_USER = 36,
+ * 发起人部门负责人
+ START_USER_DEPT_LEADER = 37,
+ * 发起人连续多级部门的负责人
+ START_USER_MULTI_LEVEL_DEPT_LEADER = 38,
+ * 指定用户组
+ USER_GROUP = 40,
+ * 流程表达式
+ EXPRESSION = 60
+// 多人审批方式类型枚举 ( 用于审批节点 )
+export enum ApproveMethodType {
+ * 随机挑选一人审批
+ RANDOM_SELECT_ONE_APPROVE = 1,
+ * 多人会签(按通过比例)
+ APPROVE_BY_RATIO = 2,
+ * 多人或签(通过只需一人,拒绝只需一人)
+ ANY_APPROVE = 3,
+ * 多人依次审批
+ SEQUENTIAL_APPROVE = 4
+ * 审批拒绝结构定义
+export type RejectHandler = {
+ // 审批拒绝类型
+ type: RejectHandlerType
+ // 回退节点 Id
+ returnNodeId?: string
+ * 审批超时结构定义
+export type TimeoutHandler = {
+ // 是否开启超时处理
+ enable: boolean
+ // 超时执行的动作
+ type?: number
+ // 超时时间设置
+ timeDuration?: string
+ // 执行动作是自动提醒, 最大提醒次数
+ maxRemindCount?: number
+ * 审批人为空的结构定义
+export type AssignEmptyHandler = {
+ // 审批人为空的处理类型
+ type: AssignEmptyHandlerType
+ // 指定用户的编号数组
+ userIds?: number[]
+// 审批拒绝类型枚举
+export enum RejectHandlerType {
+ * 结束流程
+ FINISH_PROCESS = 1,
+ * 驳回到指定节点
+ RETURN_USER_TASK = 2
+// 用户任务超时处理类型枚举
+export enum TimeoutHandlerType {
+ * 自动提醒
+ REMINDER = 1,
+ * 自动同意
+ * 自动拒绝
+ REJECT = 3
+// 用户任务的审批人为空时,处理类型枚举
+export enum AssignEmptyHandlerType {
+ * 自动通过
+ APPROVE = 1,
+ REJECT = 2,
+ * 指定人员审批
+ ASSIGN_USER,
+ * 转交给流程管理员
+ ASSIGN_ADMIN = 4
+// 用户任务的审批人与发起人相同时,处理类型枚举
+export enum AssignStartUserHandlerType {
+ * 由发起人对自己审批
+ START_USER_AUDIT = 1,
+ * 自动跳过【参考飞书】:1)如果当前节点还有其他审批人,则交由其他审批人进行审批;2)如果当前节点没有其他审批人,则该节点自动通过
+ SKIP = 2,
+ * 转交给部门负责人审批
+ ASSIGN_DEPT_LEADER = 3
+// 用户任务的审批类型。 【参考飞书】
+export enum ApproveType {
+ * 人工审批
+ USER = 1,
+ AUTO_APPROVE = 2,
+ AUTO_REJECT = 3
+// 时间单位枚举
+export enum TimeUnitType {
+ * 分钟
+ MINUTE = 1,
+ * 小时
+ HOUR = 2,
+ * 天
+ DAY = 3
+// 条件配置类型 ( 用于条件节点配置 )
+export enum ConditionType {
+ * 条件表达式
+ EXPRESSION = 1,
+ * 条件规则
+ RULE = 2
+ * 表单权限的枚举
+export enum FieldPermissionType {
+ * 只读
+ READ = '1',
+ * 编辑
+ WRITE = '2',
+ * 隐藏
+ NONE = '3'
+ * 操作按钮权限结构定义
+export type ButtonSetting = {
+ id: OperationButtonType
+ displayName: string
+// 操作按钮类型枚举 (用于审批节点)
+export enum OperationButtonType {
+ * 通过
+ * 拒绝
+ * 转办
+ TRANSFER = 3,
+ * 委派
+ DELEGATE = 4,
+ * 加签
+ ADD_SIGN = 5,
+ * 回退
+ RETURN = 6
+ * 条件规则结构定义
+export type ConditionRule = {
+ type: number
+ opName: string
+ opCode: string
+ leftSide: string
+ rightSide: string
+ * 条件组结构定义
+export type ConditionGroup = {
+ // 条件组的逻辑关系是否为且
+ and: boolean
+ // 条件数组
+ conditions: Condition[]
+ * 条件结构定义
+export type Condition = {
+ // 条件规则的逻辑关系是否为且
+ rules: ConditionRule[]
+export const NODE_DEFAULT_TEXT = new Map<number, string>()
+NODE_DEFAULT_TEXT.set(NodeType.USER_TASK_NODE, '请配置审批人')
+NODE_DEFAULT_TEXT.set(NodeType.COPY_TASK_NODE, '请配置抄送人')
+NODE_DEFAULT_TEXT.set(NodeType.CONDITION_NODE, '请设置条件')
+NODE_DEFAULT_TEXT.set(NodeType.START_USER_NODE, '请设置发起人')
+export const NODE_DEFAULT_NAME = new Map<number, string>()
+NODE_DEFAULT_NAME.set(NodeType.USER_TASK_NODE, '审批人')
+NODE_DEFAULT_NAME.set(NodeType.COPY_TASK_NODE, '抄送人')
+NODE_DEFAULT_NAME.set(NodeType.CONDITION_NODE, '条件')
+NODE_DEFAULT_NAME.set(NodeType.START_USER_NODE, '发起人')
+// 候选人策略。暂时不从字典中取。 后续可能调整。控制显示顺序
+export const CANDIDATE_STRATEGY: DictDataVO[] = [
+ { label: '指定成员', value: CandidateStrategy.USER },
+ { label: '指定角色', value: CandidateStrategy.ROLE },
+ { label: '部门成员', value: CandidateStrategy.DEPT_MEMBER },
+ { label: '部门负责人', value: CandidateStrategy.DEPT_LEADER },
+ { label: '连续多级部门负责人', value: CandidateStrategy.MULTI_LEVEL_DEPT_LEADER },
+ { label: '发起人自选', value: CandidateStrategy.START_USER_SELECT },
+ { label: '发起人本人', value: CandidateStrategy.START_USER },
+ { label: '发起人部门负责人', value: CandidateStrategy.START_USER_DEPT_LEADER },
+ { label: '发起人连续部门负责人', value: CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER },
+ { label: '用户组', value: CandidateStrategy.USER_GROUP },
+ { label: '流程表达式', value: CandidateStrategy.EXPRESSION }
+]
+// 审批节点 的审批类型
+export const APPROVE_TYPE: DictDataVO[] = [
+ { label: '人工审批', value: ApproveType.USER },
+ { label: '自动通过', value: ApproveType.AUTO_APPROVE },
+ { label: '自动拒绝', value: ApproveType.AUTO_REJECT }
+export const APPROVE_METHODS: DictDataVO[] = [
+ { label: '按顺序依次审批', value: ApproveMethodType.SEQUENTIAL_APPROVE },
+ { label: '会签(可同时审批,至少 % 人必须审批通过)', value: ApproveMethodType.APPROVE_BY_RATIO },
+ { label: '或签(可同时审批,有一人通过即可)', value: ApproveMethodType.ANY_APPROVE },
+ { label: '随机挑选一人审批', value: ApproveMethodType.RANDOM_SELECT_ONE_APPROVE }
+export const CONDITION_CONFIG_TYPES: DictDataVO[] = [
+ { label: '条件表达式', value: ConditionType.EXPRESSION },
+ { label: '条件规则', value: ConditionType.RULE }
+// 时间单位类型
+export const TIME_UNIT_TYPES: DictDataVO[] = [
+ { label: '分钟', value: TimeUnitType.MINUTE },
+ { label: '小时', value: TimeUnitType.HOUR },
+ { label: '天', value: TimeUnitType.DAY }
+// 超时处理执行动作类型
+export const TIMEOUT_HANDLER_TYPES: DictDataVO[] = [
+ { label: '自动提醒', value: 1 },
+ { label: '自动同意', value: 2 },
+ { label: '自动拒绝', value: 3 }
+export const REJECT_HANDLER_TYPES: DictDataVO[] = [
+ { label: '终止流程', value: RejectHandlerType.FINISH_PROCESS },
+ { label: '驳回到指定节点', value: RejectHandlerType.RETURN_USER_TASK }
+ // { label: '结束任务', value: RejectHandlerType.FINISH_TASK }
+export const ASSIGN_EMPTY_HANDLER_TYPES: DictDataVO[] = [
+ { label: '自动通过', value: 1 },
+ { label: '自动拒绝', value: 2 },
+ { label: '指定成员审批', value: 3 },
+ { label: '转交给流程管理员', value: 4 }
+export const ASSIGN_START_USER_HANDLER_TYPES: DictDataVO[] = [
+ { label: '由发起人对自己审批', value: 1 },
+ { label: '自动跳过', value: 2 },
+ { label: '转交给部门负责人审批', value: 3 }
+// 比较运算符
+export const COMPARISON_OPERATORS: DictDataVO = [
+ value: '==',
+ label: '等于'
+ value: '!=',
+ label: '不等于'
+ value: '>',
+ label: '大于'
+ value: '>=',
+ label: '大于等于'
+ value: '<',
+ label: '小于'
+ value: '<=',
+ label: '小于等于'
+// 审批操作按钮名称
+export const OPERATION_BUTTON_NAME = new Map<number, string>()
+OPERATION_BUTTON_NAME.set(OperationButtonType.APPROVE, '通过')
+OPERATION_BUTTON_NAME.set(OperationButtonType.REJECT, '拒绝')
+OPERATION_BUTTON_NAME.set(OperationButtonType.TRANSFER, '转办')
+OPERATION_BUTTON_NAME.set(OperationButtonType.DELEGATE, '委派')
+OPERATION_BUTTON_NAME.set(OperationButtonType.ADD_SIGN, '加签')
+OPERATION_BUTTON_NAME.set(OperationButtonType.RETURN, '回退')
+// 默认的按钮权限设置
+export const DEFAULT_BUTTON_SETTING: ButtonSetting[] = [
+ { id: OperationButtonType.APPROVE, displayName: '通过', enable: true },
+ { id: OperationButtonType.REJECT, displayName: '拒绝', enable: true },
+ { id: OperationButtonType.TRANSFER, displayName: '转办', enable: false },
+ { id: OperationButtonType.DELEGATE, displayName: '委派', enable: false },
+ { id: OperationButtonType.ADD_SIGN, displayName: '加签', enable: false },
+ { id: OperationButtonType.RETURN, displayName: '回退', enable: false }
+// 发起人的按钮权限。暂时定死,不可以编辑
+export const START_USER_BUTTON_SETTING: ButtonSetting[] = [
+ { id: OperationButtonType.APPROVE, displayName: '提交', enable: true },
+ { id: OperationButtonType.REJECT, displayName: '拒绝', enable: false },
+export const MULTI_LEVEL_DEPT: DictDataVO = [
+ { label: '第 1 级部门', value: 1 },
+ { label: '第 2 级部门', value: 2 },
+ { label: '第 3 级部门', value: 3 },
+ { label: '第 4 级部门', value: 4 },
+ { label: '第 5 级部门', value: 5 },
+ { label: '第 6 级部门', value: 6 },
+ { label: '第 7 级部门', value: 7 },
+ { label: '第 8 级部门', value: 8 },
+ { label: '第 9 级部门', value: 9 },
+ { label: '第 10 级部门', value: 10 },
+ { label: '第 11 级部门', value: 11 },
+ { label: '第 12 级部门', value: 12 },
+ { label: '第 13 级部门', value: 13 },
+ { label: '第 14 级部门', value: 14 },
+ { label: '第 15 级部门', value: 15 }
@@ -0,0 +1,4 @@
+import SimpleProcessDesigner from './SimpleProcessDesigner.vue'
+import '../theme/simple-process-designer.scss'
+export { SimpleProcessDesigner }
@@ -0,0 +1,478 @@
+import { cloneDeep } from 'lodash-es'
+ SimpleFlowNode,
+ CandidateStrategy,
+ FieldPermissionType
+export function useWatchNode(props: { flowNode: SimpleFlowNode }): Ref<SimpleFlowNode> {
+ const node = ref<SimpleFlowNode>(props.flowNode)
+ watch(
+ () => props.flowNode,
+ (newValue) => {
+ node.value = newValue
+ )
+ return node
+ * @description 表单数据权限配置,用于发起人节点 、审批节点、抄送节点
+export function useFormFieldsPermission(defaultPermission: FieldPermissionType) {
+ // 字段权限配置. 需要有 field, title, permissioin 属性
+ const fieldsPermissionConfig = ref<Array<Record<string, string>>>([])
+ const formType = inject<Ref<number>>('formType') // 表单类型
+ const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段
+ const getNodeConfigFormFields = (nodeFormFields?: Array<Record<string, string>>) => {
+ nodeFormFields = toRaw(nodeFormFields)
+ fieldsPermissionConfig.value =
+ cloneDeep(nodeFormFields) || getDefaultFieldsPermission(unref(formFields))
+ // 默认的表单权限: 获取表单的所有字段,设置字段默认权限为只读
+ const getDefaultFieldsPermission = (formFields?: string[]) => {
+ const defaultFieldsPermission: Array<Record<string, string>> = []
+ if (formFields) {
+ formFields.forEach((fieldStr: string) => {
+ parseFieldsSetDefaultPermission(JSON.parse(fieldStr), defaultFieldsPermission)
+ return defaultFieldsPermission
+ // 解析字段。赋给默认权限
+ const parseFieldsSetDefaultPermission = (
+ rule: Record<string, any>,
+ fieldsPermission: Array<Record<string, string>>,
+ parentTitle: string = ''
+ ) => {
+ const { /**type,*/ field, title: tempTitle, children } = rule
+ if (field && tempTitle) {
+ let title = tempTitle
+ if (parentTitle) {
+ title = `${parentTitle}.${tempTitle}`
+ fieldsPermission.push({
+ field,
+ title,
+ permission: defaultPermission
+ // TODO 子表单 需要处理子表单字段
+ // if (type === 'group' && rule.props?.rule && Array.isArray(rule.props.rule)) {
+ // // 解析子表单的字段
+ // rule.props.rule.forEach((item) => {
+ // parseFieldsSetDefaultPermission(item, fieldsPermission, title)
+ // })
+ // }
+ if (children && Array.isArray(children)) {
+ children.forEach((rule) => {
+ parseFieldsSetDefaultPermission(rule, fieldsPermission)
+ return {
+ formType,
+ fieldsPermissionConfig,
+ getNodeConfigFormFields
+ * @description 获取表单的字段
+export function useFormFields() {
+ // 解析后的表单字段
+ const parseFormFields = () => {
+ const parsedFormFields: Array<Record<string, string>> = []
+ formFields.value.forEach((fieldStr: string) => {
+ parseField(JSON.parse(fieldStr), parsedFormFields)
+ return parsedFormFields
+ // 解析字段。
+ const parseField = (
+ parsedFormFields: Array<Record<string, string>>,
+ const { field, title: tempTitle, children, type } = rule
+ parsedFormFields.push({
+ type
+ parseField(rule, parsedFormFields)
+ return parseFormFields()
+export type UserTaskFormType = {
+ //candidateParamArray: any[]
+ candidateStrategy: CandidateStrategy
+ approveMethod: ApproveMethodType
+ roleIds?: number[] // 角色
+ deptIds?: number[] // 部门
+ deptLevel?: number // 部门层级
+ userIds?: number[] // 用户
+ userGroups?: number[] // 用户组
+ postIds?: number[] // 岗位
+ expression?: string // 流程表达式
+ rejectHandlerType?: RejectHandlerType
+ timeoutHandlerEnable?: boolean
+ timeoutHandlerType?: number
+ assignEmptyHandlerType?: AssignEmptyHandlerType
+ assignEmptyHandlerUserIds?: number[]
+ assignStartUserHandlerType?: AssignStartUserHandlerType
+ timeDuration?: number
+ buttonsSetting: any[]
+export type CopyTaskFormType = {
+ // candidateParamArray: any[]
+ * @description 节点表单数据。 用于审批节点、抄送节点
+export function useNodeForm(nodeType: NodeType) {
+ const roleOptions = inject<Ref<RoleApi.RoleVO[]>>('roleList') // 角色列表
+ const postOptions = inject<Ref<PostApi.PostVO[]>>('postList') // 岗位列表
+ const userOptions = inject<Ref<UserApi.UserVO[]>>('userList') // 用户列表
+ const deptOptions = inject<Ref<DeptApi.DeptVO[]>>('deptList') // 部门列表
+ const userGroupOptions = inject<Ref<UserGroupApi.UserGroupVO[]>>('userGroupList') // 用户组列表
+ const deptTreeOptions = inject('deptTree') // 部门树
+ const configForm = ref<UserTaskFormType | CopyTaskFormType>()
+ if (nodeType === NodeType.USER_TASK_NODE) {
+ configForm.value = {
+ candidateStrategy: CandidateStrategy.USER,
+ approveRatio: 100,
+ rejectHandlerType: RejectHandlerType.FINISH_PROCESS,
+ returnNodeId: '',
+ timeoutHandlerEnable: false,
+ timeoutHandlerType: 1,
+ timeDuration: 6, // 默认 6小时
+ maxRemindCount: 1, // 默认 提醒 1次
+ buttonsSetting: []
+ candidateStrategy: CandidateStrategy.USER
+ const getShowText = (): string => {
+ let showText = ''
+ // 指定成员
+ if (configForm.value?.candidateStrategy === CandidateStrategy.USER) {
+ if (configForm.value?.userIds!.length > 0) {
+ const candidateNames: string[] = []
+ userOptions?.value.forEach((item) => {
+ if (configForm.value?.userIds!.includes(item.id)) {
+ candidateNames.push(item.nickname)
+ showText = `指定成员:${candidateNames.join(',')}`
+ // 指定角色
+ if (configForm.value?.candidateStrategy === CandidateStrategy.ROLE) {
+ if (configForm.value.roleIds!.length > 0) {
+ roleOptions?.value.forEach((item) => {
+ if (configForm.value?.roleIds!.includes(item.id)) {
+ candidateNames.push(item.name)
+ showText = `指定角色:${candidateNames.join(',')}`
+ // 指定部门
+ if (
+ configForm.value?.candidateStrategy === CandidateStrategy.DEPT_MEMBER ||
+ configForm.value?.candidateStrategy === CandidateStrategy.DEPT_LEADER ||
+ configForm.value?.candidateStrategy === CandidateStrategy.MULTI_LEVEL_DEPT_LEADER
+ ) {
+ if (configForm.value?.deptIds!.length > 0) {
+ deptOptions?.value.forEach((item) => {
+ if (configForm.value?.deptIds!.includes(item.id!)) {
+ if (configForm.value.candidateStrategy === CandidateStrategy.DEPT_MEMBER) {
+ showText = `部门成员:${candidateNames.join(',')}`
+ } else if (configForm.value.candidateStrategy === CandidateStrategy.DEPT_LEADER) {
+ showText = `部门的负责人:${candidateNames.join(',')}`
+ showText = `多级部门的负责人:${candidateNames.join(',')}`
+ // 指定岗位
+ if (configForm.value?.candidateStrategy === CandidateStrategy.POST) {
+ if (configForm.value.postIds!.length > 0) {
+ postOptions?.value.forEach((item) => {
+ if (configForm.value?.postIds!.includes(item.id!)) {
+ showText = `指定岗位: ${candidateNames.join(',')}`
+ // 指定用户组
+ if (configForm.value?.candidateStrategy === CandidateStrategy.USER_GROUP) {
+ if (configForm.value?.userGroups!.length > 0) {
+ userGroupOptions?.value.forEach((item) => {
+ if (configForm.value?.userGroups!.includes(item.id)) {
+ showText = `指定用户组: ${candidateNames.join(',')}`
+ // 发起人自选
+ if (configForm.value?.candidateStrategy === CandidateStrategy.START_USER_SELECT) {
+ showText = `发起人自选`
+ // 发起人自己
+ if (configForm.value?.candidateStrategy === CandidateStrategy.START_USER) {
+ showText = `发起人自己`
+ // 发起人的部门负责人
+ if (configForm.value?.candidateStrategy === CandidateStrategy.START_USER_DEPT_LEADER) {
+ showText = `发起人的部门负责人`
+ configForm.value?.candidateStrategy === CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER
+ showText = `发起人连续部门负责人`
+ // 流程表达式
+ if (configForm.value?.candidateStrategy === CandidateStrategy.EXPRESSION) {
+ showText = `流程表达式:${configForm.value.expression}`
+ return showText
+ * 处理候选人参数的赋值
+ const handleCandidateParam = () => {
+ let candidateParam: undefined | string = undefined
+ if (!configForm.value) {
+ return candidateParam
+ switch (configForm.value.candidateStrategy) {
+ case CandidateStrategy.USER:
+ candidateParam = configForm.value.userIds!.join(',')
+ break
+ case CandidateStrategy.ROLE:
+ candidateParam = configForm.value.roleIds!.join(',')
+ case CandidateStrategy.POST:
+ candidateParam = configForm.value.postIds!.join(',')
+ case CandidateStrategy.USER_GROUP:
+ candidateParam = configForm.value.userGroups!.join(',')
+ case CandidateStrategy.EXPRESSION:
+ candidateParam = configForm.value.expression!
+ case CandidateStrategy.DEPT_MEMBER:
+ case CandidateStrategy.DEPT_LEADER:
+ candidateParam = configForm.value.deptIds!.join(',')
+ // 发起人部门负责人
+ case CandidateStrategy.START_USER_DEPT_LEADER:
+ case CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER:
+ candidateParam = configForm.value.deptLevel + ''
+ // 指定连续多级部门的负责人
+ case CandidateStrategy.MULTI_LEVEL_DEPT_LEADER: {
+ // 候选人参数格式: | 分隔 。左边为部门(多个部门用 , 分隔)。 右边为部门层级
+ const deptIds = configForm.value.deptIds!.join(',')
+ candidateParam = deptIds.concat('|' + configForm.value.deptLevel + '')
+ default:
+ * 解析候选人参数
+ const parseCandidateParam = (
+ candidateStrategy: CandidateStrategy,
+ candidateParam: string | undefined
+ if (!configForm.value || !candidateParam) {
+ switch (candidateStrategy) {
+ case CandidateStrategy.USER: {
+ configForm.value.userIds = candidateParam.split(',').map((item) => +item)
+ configForm.value.roleIds = candidateParam.split(',').map((item) => +item)
+ configForm.value.postIds = candidateParam.split(',').map((item) => +item)
+ configForm.value.userGroups = candidateParam.split(',').map((item) => +item)
+ configForm.value.expression = candidateParam
+ configForm.value.deptIds = candidateParam.split(',').map((item) => +item)
+ configForm.value.deptLevel = +candidateParam
+ const paramArray = candidateParam.split('|')
+ configForm.value.deptIds = paramArray[0].split(',').map((item) => +item)
+ configForm.value.deptLevel = +paramArray[1]
+ configForm,
+ roleOptions,
+ postOptions,
+ userOptions,
+ userGroupOptions,
+ deptTreeOptions,
+ handleCandidateParam,
+ parseCandidateParam,
+ getShowText
+ * @description 抽屉配置
+export function useDrawer() {
+ // 抽屉配置是否可见
+ const settingVisible = ref(false)
+ // 关闭配置抽屉
+ const closeDrawer = () => {
+ settingVisible.value = false
+ // 打开配置抽屉
+ const openDrawer = () => {
+ settingVisible.value = true
+ settingVisible,
+ closeDrawer,
+ openDrawer
+ * @description 节点名称配置
+export function useNodeName(nodeType: NodeType) {
+ // 节点名称
+ const nodeName = ref<string>()
+ // 节点名称输入框
+ const showInput = ref(false)
+ // 点击节点名称编辑图标
+ const clickIcon = () => {
+ showInput.value = true
+ // 节点名称输入框失去焦点
+ const blurEvent = () => {
+ showInput.value = false
+ nodeName.value = nodeName.value || (NODE_DEFAULT_NAME.get(nodeType) as string)
+ nodeName,
+ showInput,
+ clickIcon,
+ blurEvent
+export function useNodeName2(node: Ref<SimpleFlowNode>, nodeType: NodeType) {
+ // 显示节点名称输入框
+ node.value.name = node.value.name || (NODE_DEFAULT_NAME.get(nodeType) as string)
+ // 点击节点标题进行输入
+ const clickTitle = () => {
+ clickTitle,
@@ -0,0 +1,419 @@
+ <el-drawer
+ :append-to-body="true"
+ v-model="settingVisible"
+ :show-close="false"
+ :size="588"
+ :before-close="handleClose"
+ <template #header>
+ <div class="config-header">
+ <input
+ v-if="showInput"
+ type="text"
+ class="config-editable-input"
+ @blur="blurEvent()"
+ v-mountedFocus
+ v-model="currentNode.name"
+ :placeholder="currentNode.name"
+ <div v-else class="node-name"
+ >{{ currentNode.name }}
+ <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()"
+ /></div>
+ <div class="divide-line"></div>
+ <div>
+ <div class="mb-3 font-size-16px" v-if="currentNode.defaultFlow">其它条件不满足进入此分支(该分支不可编辑和删除)</div>
+ <div v-else>
+ <el-form
+ ref="formRef"
+ :model="currentNode"
+ :rules="formRules"
+ label-position="top"
+ <el-form-item label="配置方式" prop="conditionType">
+ <el-radio-group
+ v-model="currentNode.conditionType"
+ @change="changeConditionType"
+ <el-radio
+ v-for="(dict, index) in conditionConfigTypes"
+ :value="dict.value"
+ :label="dict.value"
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item
+ v-if="currentNode.conditionType === 1"
+ label="条件表达式"
+ prop="conditionExpression"
+ <el-input
+ type="textarea"
+ v-model="currentNode.conditionExpression"
+ clearable
+ style="width: 100%"
+ <el-form-item v-if="currentNode.conditionType === 2" label="条件规则">
+ <div class="condition-group-tool">
+ <div class="flex items-center">
+ <div class="mr-4">条件组关系</div>
+ <el-switch
+ v-model="conditionGroups.and"
+ inline-prompt
+ active-text="且"
+ inactive-text="或"
+ <el-space direction="vertical" :spacer="conditionGroups.and ? '且' : '或'">
+ <el-card
+ class="condition-group"
+ style="width: 530px"
+ v-for="(condition, cIdx) in conditionGroups.conditions"
+ :key="cIdx"
+ <div class="condition-group-delete" v-if="conditionGroups.conditions.length > 1">
+ <Icon
+ color="#0089ff"
+ icon="ep:circle-close-filled"
+ :size="18"
+ @click="deleteConditionGroup(cIdx)"
+ <div class="flex items-center justify-between">
+ <div>条件组</div>
+ <div class="flex">
+ <div class="mr-4">规则关系</div>
+ v-model="condition.and"
+ <div class="flex pt-2" v-for="(rule, rIdx) in condition.rules" :key="rIdx">
+ <div class="mr-2">
+ <el-select style="width: 160px" v-model="rule.leftSide">
+ <el-option
+ v-for="(item, index) in fieldsInfo"
+ :label="item.title"
+ :value="item.field"
+ </el-select>
+ <el-select v-model="rule.opCode" style="width: 100px">
+ v-for="item in COMPARISON_OPERATORS"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ <el-input v-model="rule.rightSide" style="width: 160px" />
+ <div class="mr-1 flex items-center" v-if="condition.rules.length > 1">
+ icon="ep:delete"
+ @click="deleteConditionRule(condition, rIdx)"
+ <Icon icon="ep:plus" :size="18" @click="addConditionRule(condition, rIdx)" />
+ </el-card>
+ </el-space>
+ <div title="添加条件组" class="mt-4 cursor-pointer">
+ <Icon color="#0089ff" icon="ep:plus" :size="24" @click="addConditionGroup" />
+ </el-form>
+ <el-divider />
+ <el-button type="primary" @click="saveConfig">确 定</el-button>
+ <el-button @click="closeDrawer">取 消</el-button>
+ </el-drawer>
+ CONDITION_CONFIG_TYPES,
+ ConditionType,
+ COMPARISON_OPERATORS,
+ ConditionGroup,
+ Condition,
+ ConditionRule
+} from '../consts'
+import { getDefaultConditionNodeName } from '../utils'
+import { useFormFields } from '../node'
+const message = useMessage() // 消息弹窗
+ name: 'ConditionNodeConfig'
+const formType = inject<Ref<number>>('formType') // 表单类型
+const conditionConfigTypes = computed(() => {
+ return CONDITION_CONFIG_TYPES.filter((item) => {
+ // 业务表单暂时去掉条件规则选项
+ if (formType?.value !== 10) {
+ return item.value === ConditionType.RULE
+ return true
+ conditionNode: {
+ nodeIndex: {
+ type: Number,
+const settingVisible = ref(false)
+const open = () => {
+ if (currentNode.value.conditionType === ConditionType.RULE) {
+ if (currentNode.value.conditionGroups) {
+ conditionGroups.value = currentNode.value.conditionGroups
+watch(
+ () => props.conditionNode,
+ currentNode.value = newValue
+)
+// 显示名称输入框
+const showInput = ref(false)
+const clickIcon = () => {
+// 输入框失去焦点
+const blurEvent = () => {
+ currentNode.value.name =
+ currentNode.value.name ||
+ getDefaultConditionNodeName(props.nodeIndex, currentNode.value?.defaultFlow)
+const currentNode = ref<SimpleFlowNode>(props.conditionNode)
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+// 关闭
+const closeDrawer = () => {
+const handleClose = async (done: (cancel?: boolean) => void) => {
+ const isSuccess = await saveConfig()
+ if (!isSuccess) {
+ done(true) // 传入 true 阻止关闭
+ done()
+// 表单校验规则
+const formRules = reactive({
+ conditionType: [{ required: true, message: '配置方式不能为空', trigger: 'blur' }],
+ conditionExpression: [{ required: true, message: '条件表达式不能为空', trigger: 'blur' }]
+const formRef = ref() // 表单 Ref
+// 保存配置
+const saveConfig = async () => {
+ if (!currentNode.value.defaultFlow) {
+ // 校验表单
+ if (!formRef) return false
+ const valid = await formRef.value.validate()
+ if (!valid) return false
+ const showText = getShowText()
+ return false
+ currentNode.value.showText = showText
+ if (currentNode.value.conditionType === ConditionType.EXPRESSION) {
+ currentNode.value.conditionGroups = undefined
+ currentNode.value.conditionExpression = undefined
+ currentNode.value.conditionGroups = conditionGroups.value
+const getShowText = (): string => {
+ if (currentNode.value.conditionExpression) {
+ showText = `表达式:${currentNode.value.conditionExpression}`
+ // 条件组是否为与关系
+ const groupAnd = conditionGroups.value.and
+ let warningMesg: undefined | string = undefined
+ const conditionGroup = conditionGroups.value.conditions.map((item) => {
+ return (
+ '(' +
+ item.rules
+ .map((rule) => {
+ if (rule.leftSide && rule.rightSide) {
+ getFieldTitle(rule.leftSide) + ' ' + getOpName(rule.opCode) + ' ' + rule.rightSide
+ // 有一条规则不完善。提示错误
+ warningMesg = '请完善条件规则'
+ return ''
+ .join(item.and ? ' 且 ' : ' 或 ') +
+ ' ) '
+ if (warningMesg) {
+ message.warning(warningMesg)
+ showText = ''
+ showText = conditionGroup.join(groupAnd ? ' 且 ' : ' 或 ')
+// 改变条件配置方式
+const changeConditionType = () => {}
+const conditionGroups = ref<ConditionGroup>({
+ and: true,
+ conditions: [
+ rules: [
+ type: 1,
+ opName: '等于',
+ opCode: '==',
+ leftSide: '',
+ rightSide: ''
+// 添加条件组
+const addConditionGroup = () => {
+ const condition = {
+ conditionGroups.value.conditions.push(condition)
+// 删除条件组
+const deleteConditionGroup = (idx: number) => {
+ conditionGroups.value.conditions.splice(idx, 1)
+// 添加条件规则
+const addConditionRule = (condition: Condition, idx: number) => {
+ const rule: ConditionRule = {
+ condition.rules.splice(idx + 1, 0, rule)
+const deleteConditionRule = (condition: Condition, idx: number) => {
+ condition.rules.splice(idx, 1)
+const fieldsInfo = useFormFields()
+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)
+ return opName?.label
+<style lang="scss" scoped>
+.condition-group-tool {
+ display: flex;
+ justify-content: space-between;
+ width: 500px;
+ margin-bottom: 20px;
+.condition-group {
+ position: relative;
+ &:hover {
+ border-color: #0089ff;
+ .condition-group-delete {
+ opacity: 1;
+ position: absolute;
+ top: 0;
+ left: 0;
+ cursor: pointer;
+ opacity: 0;
+::v-deep(.el-card__header) {
+ padding: 8px var(--el-card-padding);
+ border-bottom: 1px solid var(--el-card-border-color);
+ box-sizing: border-box;
+</style>
@@ -0,0 +1,307 @@
+ :size="550"
+ :before-close="saveConfig"
+ v-model="nodeName"
+ :placeholder="nodeName"
+ <div v-else class="node-name">
+ {{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
+ <el-tabs type="border-card" v-model="activeTabName">
+ <el-tab-pane label="抄送人" name="user">
+ <el-form ref="formRef" :model="configForm" label-position="top" :rules="formRules">
+ <el-form-item label="抄送人设置" prop="candidateStrategy">
+ v-model="configForm.candidateStrategy"
+ @change="changeCandidateStrategy"
+ v-for="(dict, index) in copyUserStrategies"
+ v-if="configForm.candidateStrategy == CandidateStrategy.ROLE"
+ label="指定角色"
+ prop="roleIds"
+ <el-select v-model="configForm.roleIds" clearable multiple style="width: 100%">
+ v-for="item in roleOptions"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ v-if="
+ configForm.candidateStrategy == CandidateStrategy.DEPT_MEMBER ||
+ configForm.candidateStrategy == CandidateStrategy.DEPT_LEADER
+ "
+ label="指定部门"
+ prop="deptIds"
+ span="24"
+ <el-tree-select
+ ref="treeRef"
+ v-model="configForm.deptIds"
+ :data="deptTreeOptions"
+ :props="defaultProps"
+ empty-text="加载中,请稍后"
+ multiple
+ node-key="id"
+ show-checkbox
+ v-if="configForm.candidateStrategy == CandidateStrategy.POST"
+ label="指定岗位"
+ prop="postIds"
+ <el-select v-model="configForm.postIds" clearable multiple style="width: 100%">
+ v-for="item in postOptions"
+ :value="item.id!"
+ v-if="configForm.candidateStrategy == CandidateStrategy.USER"
+ label="指定用户"
+ prop="userIds"
+ <el-select v-model="configForm.userIds" clearable multiple style="width: 100%">
+ v-for="item in userOptions"
+ :label="item.nickname"
+ v-if="configForm.candidateStrategy === CandidateStrategy.USER_GROUP"
+ label="指定用户组"
+ prop="userGroups"
+ <el-select v-model="configForm.userGroups" clearable multiple style="width: 100%">
+ v-for="item in userGroupOptions"
+ v-if="configForm.candidateStrategy === CandidateStrategy.EXPRESSION"
+ label="流程表达式"
+ prop="expression"
+ v-model="configForm.expression"
+ </el-tab-pane>
+ <el-tab-pane label="表单字段权限" name="fields" v-if="formType === 10">
+ <div class="field-setting-pane">
+ <div class="field-setting-desc">字段权限</div>
+ <div class="field-permit-title">
+ <div class="setting-title-label first-title"> 字段名称 </div>
+ <div class="other-titles">
+ <span class="setting-title-label">只读</span>
+ <span class="setting-title-label">可编辑</span>
+ <span class="setting-title-label">隐藏</span>
+ class="field-setting-item"
+ v-for="(item, index) in fieldsPermissionConfig"
+ <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">
+ :value="FieldPermissionType.READ"
+ size="large"
+ :label="FieldPermissionType.WRITE"
+ ><span></span
+ ></el-radio>
+ :value="FieldPermissionType.WRITE"
+ disabled
+ :value="FieldPermissionType.NONE"
+ :label="FieldPermissionType.NONE"
+ </el-tabs>
+ CANDIDATE_STRATEGY,
+ useWatchNode,
+ useDrawer,
+ useNodeName,
+ useFormFieldsPermission,
+ useNodeForm,
+ CopyTaskFormType
+} from '../node'
+import { defaultProps } from '@/utils/tree'
+ name: 'CopyTaskNodeConfig'
+// 抽屉配置
+const { settingVisible, closeDrawer, openDrawer } = useDrawer()
+// 当前节点
+// 节点名称
+const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.COPY_TASK_NODE)
+// 激活的 Tab 标签页
+const activeTabName = ref('user')
+// 表单字段权限配置
+const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission(
+ FieldPermissionType.READ
+// 抄送人表单配置
+ candidateStrategy: [{ required: true, message: '抄送人设置不能为空', trigger: 'change' }],
+ userIds: [{ required: true, message: '用户不能为空', trigger: 'change' }],
+ roleIds: [{ required: true, message: '角色不能为空', trigger: 'change' }],
+ deptIds: [{ required: true, message: '部门不能为空', trigger: 'change' }],
+ userGroups: [{ required: true, message: '用户组不能为空', trigger: 'change' }],
+ postIds: [{ required: true, message: '岗位不能为空', trigger: 'change' }],
+ expression: [{ required: true, message: '流程表达式不能为空', trigger: 'blur' }]
+const {
+ configForm: tempConfigForm,
+ getShowText,
+ parseCandidateParam
+} = useNodeForm(NodeType.COPY_TASK_NODE)
+const configForm = tempConfigForm as Ref<CopyTaskFormType>
+// 抄送人策略, 去掉发起人自选 和 发起人自己
+const copyUserStrategies = computed(() => {
+ return CANDIDATE_STRATEGY.filter(
+ (item) =>
+ item.value !== CandidateStrategy.START_USER_SELECT &&
+ item.value !== CandidateStrategy.START_USER
+// 改变抄送人设置策略
+const changeCandidateStrategy = () => {
+ configForm.value.userIds = []
+ configForm.value.deptIds = []
+ configForm.value.roleIds = []
+ configForm.value.postIds = []
+ configForm.value.userGroups = []
+ configForm.value.deptLevel = 1
+ activeTabName.value = 'user'
+ if (!showText) return false
+ currentNode.value.name = nodeName.value!
+ currentNode.value.candidateParam = handleCandidateParam()
+ currentNode.value.candidateStrategy = configForm.value.candidateStrategy
+ currentNode.value.fieldsPermission = fieldsPermissionConfig.value
+// 显示抄送节点配置, 由父组件传过来
+const showCopyTaskNodeConfig = (node: SimpleFlowNode) => {
+ nodeName.value = node.name
+ // 抄送人设置
+ configForm.value.candidateStrategy = node.candidateStrategy!
+ parseCandidateParam(node.candidateStrategy!, node?.candidateParam)
+ // 表单字段权限
+ getNodeConfigFormFields(node.fieldsPermission)
+defineExpose({ openDrawer, showCopyTaskNodeConfig }) // 暴露方法给父组件
@@ -0,0 +1,136 @@
+ <el-tab-pane label="权限" name="user">
+ <div> 待实现 </div>
+ :label="FieldPermissionType.READ"
+import { SimpleFlowNode, NodeType, FieldPermissionType, START_USER_BUTTON_SETTING } from '../consts'
+import { useWatchNode, useDrawer, useNodeName, useFormFieldsPermission } from '../node'
+ name: 'StartUserNodeConfig'
+ FieldPermissionType.WRITE
+ // TODO 暂时写死。后续可以显示谁有权限可以发起
+ currentNode.value.showText = '已设置'
+ // 设置表单权限
+ // 设置发起人的按钮权限
+ currentNode.value.buttonsSetting = START_USER_BUTTON_SETTING
+ console.log('currentNode.value.buttonsSetting==>', currentNode.value.buttonsSetting)
+// 显示发起人节点配置, 由父组件传过来
+const showStartUserNodeConfig = (node: SimpleFlowNode) => {
+defineExpose({ openDrawer, showStartUserNodeConfig }) // 暴露方法给父组件
@@ -0,0 +1,901 @@
+ class="justify-start"
+ <div class="flex flex-items-center mb-3">
+ <span class="font-size-16px mr-3">审批类型 :</span>
+ <el-radio-group v-model="approveType">
+ v-for="(item, index) in APPROVE_TYPE"
+ :label="item.value"
+ {{ item.label }}
+ <el-tabs type="border-card" v-model="activeTabName" v-if="approveType === ApproveType.USER">
+ <el-tab-pane label="审批人" name="user">
+ <el-form-item label="审批人设置" prop="candidateStrategy">
+ v-for="(dict, index) in CANDIDATE_STRATEGY"
+ configForm.candidateStrategy == CandidateStrategy.DEPT_LEADER ||
+ configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER
+ :check-strictly="true"
+ configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER ||
+ configForm.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER ||
+ configForm.candidateStrategy == CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER
+ :label="deptLevelLabel!"
+ prop="deptLevel"
+ <el-select v-model="configForm.deptLevel" clearable>
+ v-for="(item, index) in MULTI_LEVEL_DEPT"
+ <el-select
+ v-model="configForm.userIds"
+ @change="changedCandidateUsers"
+ <!-- TODO @jason:后续要支持选择已经存好的表达式 -->
+ <el-form-item label="多人审批方式" prop="approveMethod">
+ <el-radio-group v-model="configForm.approveMethod" @change="approveMethodChanged">
+ <div class="flex-col">
+ v-for="(item, index) in APPROVE_METHODS"
+ class="flex items-center"
+ :disabled="
+ item.value !== ApproveMethodType.RANDOM_SELECT_ONE_APPROVE &&
+ notAllowedMultiApprovers
+ <el-form-item prop="approveRatio">
+ <el-input-number
+ v-model="configForm.approveRatio"
+ :min="10"
+ :max="100"
+ :step="10"
+ size="small"
+ item.value === ApproveMethodType.APPROVE_BY_RATIO &&
+ configForm.approveMethod === ApproveMethodType.APPROVE_BY_RATIO
+ <el-divider content-position="left">审批人拒绝时</el-divider>
+ <el-form-item prop="rejectHandlerType">
+ <el-radio-group v-model="configForm.rejectHandlerType">
+ <div v-for="(item, index) in REJECT_HANDLER_TYPES" :key="index">
+ <el-radio :key="item.value" :value="item.value" :label="item.label" />
+ v-if="configForm.rejectHandlerType == RejectHandlerType.RETURN_USER_TASK"
+ label="驳回节点"
+ prop="returnNodeId"
+ <el-select v-model="configForm.returnNodeId" clearable style="width: 100%">
+ v-for="item in returnTaskList"
+ <el-divider content-position="left">审批人超时未处理时</el-divider>
+ <el-form-item label="启用开关" prop="timeoutHandlerEnable">
+ v-model="configForm.timeoutHandlerEnable"
+ active-text="开启"
+ inactive-text="关闭"
+ @change="timeoutHandlerChange"
+ label="执行动作"
+ prop="timeoutHandlerType"
+ v-if="configForm.timeoutHandlerEnable"
+ v-model="configForm.timeoutHandlerType"
+ @change="timeoutHandlerTypeChanged"
+ <el-radio-button
+ v-for="item in TIMEOUT_HANDLER_TYPES"
+ <el-form-item label="超时时间设置" v-if="configForm.timeoutHandlerEnable">
+ <span class="mr-2">当超过</span>
+ <el-form-item prop="timeDuration">
+ class="mr-2"
+ :style="{ width: '100px' }"
+ v-model="configForm.timeDuration"
+ :min="1"
+ controls-position="right"
+ v-model="timeUnit"
+ @change="timeUnitChange"
+ v-for="item in TIME_UNIT_TYPES"
+ 未处理
+ label="最大提醒次数"
+ prop="maxRemindCount"
+ v-if="configForm.timeoutHandlerEnable && configForm.timeoutHandlerType === 1"
+ <el-input-number v-model="configForm.maxRemindCount" :min="1" :max="10" />
+ <el-divider content-position="left">审批人为空时</el-divider>
+ <el-form-item prop="assignEmptyHandlerType">
+ <el-radio-group v-model="configForm.assignEmptyHandlerType">
+ <div v-for="(item, index) in ASSIGN_EMPTY_HANDLER_TYPES" :key="index">
+ v-if="configForm.assignEmptyHandlerType == AssignEmptyHandlerType.ASSIGN_USER"
+ prop="assignEmptyHandlerUserIds"
+ v-model="configForm.assignEmptyHandlerUserIds"
+ <el-divider content-position="left">审批人与提交人为同一人时</el-divider>
+ <el-form-item prop="assignStartUserHandlerType">
+ <el-radio-group v-model="configForm.assignStartUserHandlerType">
+ <div v-for="(item, index) in ASSIGN_START_USER_HANDLER_TYPES" :key="index">
+ <el-tab-pane label="操作按钮设置" name="buttons">
+ <div class="button-setting-pane">
+ <div class="button-setting-desc">操作按钮</div>
+ <div class="button-setting-title">
+ <div class="button-title-label">操作按钮</div>
+ <div class="pl-4 button-title-label">显示名称</div>
+ <div class="button-title-label">启用</div>
+ <div class="button-setting-item" v-for="(item, index) in buttonsSetting" :key="index">
+ <div class="button-setting-item-label"> {{ OPERATION_BUTTON_NAME.get(item.id) }} </div>
+ <div class="button-setting-item-label">
+ class="editable-title-input"
+ @blur="btnDisplayNameBlurEvent(index)"
+ 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>
+ <el-switch v-model="item.enable" />
+ APPROVE_TYPE,
+ ApproveType,
+ APPROVE_METHODS,
+ TimeUnitType,
+ TIMEOUT_HANDLER_TYPES,
+ TIME_UNIT_TYPES,
+ REJECT_HANDLER_TYPES,
+ DEFAULT_BUTTON_SETTING,
+ OPERATION_BUTTON_NAME,
+ ButtonSetting,
+ MULTI_LEVEL_DEPT,
+ ASSIGN_START_USER_HANDLER_TYPES,
+ TimeoutHandlerType,
+ ASSIGN_EMPTY_HANDLER_TYPES,
+ UserTaskFormType,
+ useDrawer
+import { convertTimeUnit, getApproveTypeText } from '../utils'
+ name: 'UserTaskNodeConfig'
+ 'find:returnTaskNodes': [nodeList: SimpleFlowNode[]]
+const deptLevelLabel = computed(() => {
+ let label = '部门负责人来源'
+ if (configForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER) {
+ label = label + '(指定部门向上)'
+ label = label + '(发起人部门向上)'
+ return label
+// 监控节点的变化
+// 节点名称配置
+const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.USER_TASK_NODE)
+// 表单字段权限设置
+// 操作按钮设置
+const { buttonsSetting, btnDisplayNameEdit, changeBtnDisplayName, btnDisplayNameBlurEvent } =
+ useButtonsSetting()
+const approveType = ref(ApproveType.USER)
+// 审批人表单设置
+ candidateStrategy: [{ required: true, message: '审批人设置不能为空', trigger: 'change' }],
+ expression: [{ required: true, message: '流程表达式不能为空', trigger: 'blur' }],
+ approveMethod: [{ required: true, message: '多人审批方式不能为空', trigger: 'change' }],
+ approveRatio: [{ required: true, message: '通过比例不能为空', trigger: 'blur' }],
+ returnNodeId: [{ required: true, message: '驳回节点不能为空', trigger: 'change' }],
+ timeoutHandlerEnable: [{ required: true }],
+ timeoutHandlerType: [{ required: true }],
+ timeDuration: [{ required: true, message: '超时时间不能为空', trigger: 'blur' }],
+ maxRemindCount: [{ required: true, message: '提醒次数不能为空', trigger: 'blur' }],
+ assignEmptyHandlerType: [{ required: true }],
+ assignEmptyHandlerUserIds: [{ required: true, message: '用户不能为空', trigger: 'change' }],
+ assignStartUserHandlerType: [{ required: true }]
+} = useNodeForm(NodeType.USER_TASK_NODE)
+const configForm = tempConfigForm as Ref<UserTaskFormType>
+// 不允许多人审批
+const notAllowedMultiApprovers = ref(false)
+// 改变审批人设置策略
+ configForm.value.approveMethod = ApproveMethodType.SEQUENTIAL_APPROVE
+ configForm.value.candidateStrategy === CandidateStrategy.START_USER ||
+ configForm.value.candidateStrategy === CandidateStrategy.USER
+ notAllowedMultiApprovers.value = true
+ notAllowedMultiApprovers.value = false
+// 改变审批候选人
+const changedCandidateUsers = () => {
+ configForm.value.userIds &&
+ configForm.value.userIds?.length <= 1 &&
+ configForm.value.approveMethod = ApproveMethodType.RANDOM_SELECT_ONE_APPROVE
+ configForm.value.rejectHandlerType = RejectHandlerType.FINISH_PROCESS
+// 审批方式改变
+const approveMethodChanged = () => {
+ if (configForm.value.approveMethod === ApproveMethodType.APPROVE_BY_RATIO) {
+ configForm.value.approveRatio = 100
+ formRef.value.clearValidate('approveRatio')
+// 审批拒绝 可回退的节点
+const returnTaskList = ref<SimpleFlowNode[]>([])
+// 审批人超时未处理设置
+ timeoutHandlerChange,
+ cTimeoutType,
+ timeoutHandlerTypeChanged,
+ timeUnit,
+ timeUnitChange,
+ isoTimeDuration,
+ cTimeoutMaxRemindCount
+} = useTimeoutHandler()
+ // 设置审批节点名称
+ // 设置审批类型
+ currentNode.value.approveType = approveType.value
+ // 如果不是人工审批。返回
+ if (approveType.value !== ApproveType.USER) {
+ currentNode.value.showText = getApproveTypeText(approveType.value)
+ // 处理 candidateParam 参数
+ // 设置审批方式
+ currentNode.value.approveMethod = configForm.value.approveMethod
+ currentNode.value.approveRatio = configForm.value.approveRatio
+ // 设置拒绝处理
+ currentNode.value.rejectHandler = {
+ type: configForm.value.rejectHandlerType!,
+ returnNodeId: configForm.value.returnNodeId
+ // 设置超时处理
+ currentNode.value.timeoutHandler = {
+ enable: configForm.value.timeoutHandlerEnable!,
+ type: cTimeoutType.value,
+ timeDuration: isoTimeDuration.value,
+ maxRemindCount: cTimeoutMaxRemindCount.value
+ // 设置审批人为空时
+ currentNode.value.assignEmptyHandler = {
+ type: configForm.value.assignEmptyHandlerType!,
+ userIds:
+ configForm.value.assignEmptyHandlerType === AssignEmptyHandlerType.ASSIGN_USER
+ ? configForm.value.assignEmptyHandlerUserIds
+ : undefined
+ // 设置审批人与发起人相同时
+ currentNode.value.assignStartUserHandlerType = configForm.value.assignStartUserHandlerType
+ // 设置按钮权限
+ currentNode.value.buttonsSetting = buttonsSetting.value
+// 显示审批节点配置, 由父组件传过来
+const showUserTaskNodeConfig = (node: SimpleFlowNode) => {
+ // 1 审批类型
+ approveType.value = node.approveType ? node.approveType : ApproveType.USER
+ // 如果审批类型不是人工审批返回
+ //2.1 审批人设置
+ // 解析候选人参数
+ if (configForm.value.userIds && configForm.value.userIds.length > 1) {
+ // 2.2 设置审批方式
+ configForm.value.approveMethod = node.approveMethod!
+ if (node.approveMethod == ApproveMethodType.APPROVE_BY_RATIO) {
+ configForm.value.approveRatio = node.approveRatio!
+ // 2.3 设置审批拒绝处理
+ configForm.value.rejectHandlerType = node.rejectHandler!.type
+ configForm.value.returnNodeId = node.rejectHandler?.returnNodeId
+ const matchNodeList = []
+ emits('find:returnTaskNodes', matchNodeList)
+ returnTaskList.value = matchNodeList
+ // 2.4 设置审批超时处理
+ configForm.value.timeoutHandlerEnable = node.timeoutHandler!.enable
+ if (node.timeoutHandler?.enable && node.timeoutHandler?.timeDuration) {
+ const strTimeDuration = node.timeoutHandler.timeDuration
+ let parseTime = strTimeDuration.slice(2, strTimeDuration.length - 1)
+ let parseTimeUnit = strTimeDuration.slice(strTimeDuration.length - 1)
+ configForm.value.timeDuration = parseInt(parseTime)
+ timeUnit.value = convertTimeUnit(parseTimeUnit)
+ configForm.value.timeoutHandlerType = node.timeoutHandler?.type
+ configForm.value.maxRemindCount = node.timeoutHandler?.maxRemindCount
+ // 2.5 设置审批人为空时
+ configForm.value.assignEmptyHandlerType = node.assignEmptyHandler?.type
+ configForm.value.assignEmptyHandlerUserIds = node.assignEmptyHandler?.userIds
+ // 2.6 设置用户任务的审批人与发起人相同时
+ configForm.value.assignStartUserHandlerType = node.assignStartUserHandlerType
+ // 3. 操作按钮设置
+ buttonsSetting.value = cloneDeep(node.buttonsSetting) || DEFAULT_BUTTON_SETTING
+ // 4. 表单字段权限配置
+defineExpose({ openDrawer, showUserTaskNodeConfig }) // 暴露方法给父组件
+ * @description 操作按钮设置
+function useButtonsSetting() {
+ const buttonsSetting = ref<ButtonSetting[]>()
+ // 操作按钮显示名称可编辑
+ const btnDisplayNameEdit = ref<boolean[]>([])
+ const changeBtnDisplayName = (index: number) => {
+ btnDisplayNameEdit.value[index] = true
+ const btnDisplayNameBlurEvent = (index: number) => {
+ btnDisplayNameEdit.value[index] = false
+ const buttonItem = buttonsSetting.value![index]
+ buttonItem.displayName = buttonItem.displayName || OPERATION_BUTTON_NAME.get(buttonItem.id)!
+ buttonsSetting,
+ btnDisplayNameEdit,
+ changeBtnDisplayName,
+ btnDisplayNameBlurEvent
+ * @description 审批人超时未处理配置
+function useTimeoutHandler() {
+ // 时间单位
+ const timeUnit = ref(TimeUnitType.HOUR)
+ // 超时开关改变
+ const timeoutHandlerChange = () => {
+ if (configForm.value.timeoutHandlerEnable) {
+ timeUnit.value = 2
+ configForm.value.timeDuration = 6
+ configForm.value.timeoutHandlerType = 1
+ configForm.value.maxRemindCount = 1
+ const cTimeoutType = computed(() => {
+ if (!configForm.value.timeoutHandlerEnable) {
+ return undefined
+ return configForm.value.timeoutHandlerType
+ // 超时处理动作改变
+ const timeoutHandlerTypeChanged = () => {
+ if (configForm.value.timeoutHandlerType === TimeoutHandlerType.REMINDER) {
+ configForm.value.maxRemindCount = 1 // 超时提醒次数,默认为1
+ // 时间单位改变
+ const timeUnitChange = () => {
+ // 分钟,默认是 60 分钟
+ if (timeUnit.value === TimeUnitType.MINUTE) {
+ configForm.value.timeDuration = 60
+ // 小时,默认是 6 个小时
+ if (timeUnit.value === TimeUnitType.HOUR) {
+ // 天, 默认 1天
+ if (timeUnit.value === TimeUnitType.DAY) {
+ configForm.value.timeDuration = 1
+ // 超时时间的 ISO 表示
+ const isoTimeDuration = computed(() => {
+ let strTimeDuration = 'PT'
+ strTimeDuration += configForm.value.timeDuration + 'M'
+ strTimeDuration += configForm.value.timeDuration + 'H'
+ strTimeDuration += configForm.value.timeDuration + 'D'
+ return strTimeDuration
+ // 超时最大提醒次数
+ const cTimeoutMaxRemindCount = computed(() => {
+ if (configForm.value.timeoutHandlerType !== TimeoutHandlerType.REMINDER) {
+ return configForm.value.maxRemindCount
+.button-setting-pane {
+ flex-direction: column;
+ font-size: 14px;
+ .button-setting-desc {
+ padding-right: 8px;
+ margin-bottom: 16px;
+ font-size: 16px;
+ font-weight: 700;
+ .button-setting-title {
+ 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;
+ color: #000;
+ text-align: left;
+ .button-setting-item {
+ height: 38px;
+ border-top: 0;
+ .button-setting-item-label {
+ overflow: hidden;
+ 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%);
@@ -0,0 +1,79 @@
+ <div class="node-wrapper">
+ <div class="node-container">
+ <div class="node-box" :class="{ 'node-config-error': !currentNode.showText }">
+ <div class="node-title-container">
+ <div class="node-title-icon copy-task"><span class="iconfont icon-copy"></span></div>
+ <div v-else class="node-title" @click="clickTitle">
+ {{ currentNode.name }}
+ <div class="node-content" @click="openNodeConfig">
+ <div class="node-text" :title="currentNode.showText" v-if="currentNode.showText">
+ {{ currentNode.showText }}
+ <div class="node-text" v-else>
+ {{ NODE_DEFAULT_TEXT.get(NodeType.COPY_TASK_NODE) }}
+ <Icon icon="ep:arrow-right-bold" />
+ <div class="node-toolbar">
+ <div class="toolbar-icon"
+ ><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode"
+ <!-- 传递子节点给添加节点组件。会在子节点前面添加节点 -->
+ <NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
+ <CopyTaskNodeConfig v-if="currentNode" ref="nodeSetting" :flow-node="currentNode" />
+import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
+import NodeHandler from '../NodeHandler.vue'
+import { useNodeName2, useWatchNode } from '../node'
+import CopyTaskNodeConfig from '../nodes-config/CopyTaskNodeConfig.vue'
+ name: 'CopyTaskNode'
+// 定义事件,更新父组件。
+// 节点名称编辑
+const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.COPY_TASK_NODE)
+const nodeSetting = ref()
+// 打开节点配置
+const openNodeConfig = () => {
+ nodeSetting.value.showCopyTaskNodeConfig(currentNode.value)
+ nodeSetting.value.openDrawer()
+// 删除节点。更新当前节点为孩子节点
+const deleteNode = () => {
+ emits('update:flowNode', currentNode.value.childNode)
@@ -0,0 +1,13 @@
+ <div class="end-node-wrapper">
+ <div class="end-node-box">
+ <span class="node-fixed-name" title="结束">结束</span>
+ name: 'EndEventNode'
@@ -0,0 +1,207 @@
+ <div class="branch-node-wrapper">
+ <div class="branch-node-container">
+ <div class="branch-node-add" @click="addCondition">添加条件</div>
+ class="branch-node-item"
+ v-for="(item, index) in currentNode.conditionNodes"
+ <template v-if="index == 0">
+ <div class="branch-line-first-top"> </div>
+ <div class="branch-line-first-bottom"></div>
+ <template v-if="index + 1 == currentNode.conditionNodes?.length">
+ <div class="branch-line-last-top"></div>
+ <div class="branch-line-last-bottom"></div>
+ <div class="node-box" :class="{ 'node-config-error': !item.showText }">
+ <div class="branch-node-title-container">
+ <div v-if="showInputs[index]">
+ class="input-max-width editable-title-input"
+ @blur="blurEvent(index)"
+ v-model="item.name"
+ <div v-else class="branch-title" @click="clickEvent(index)"> {{ item.name }} </div>
+ <div class="branch-priority"> 优先级{{ index + 1 }} </div>
+ <div class="branch-node-content" @click="conditionNodeConfig(item.id)">
+ <div class="branch-node-text" :title="item.showText" v-if="item.showText">
+ {{ item.showText }}
+ <div class="branch-node-text" v-else>
+ {{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }}
+ <div class="node-toolbar" v-if="index + 1 !== currentNode.conditionNodes?.length">
+ <div class="toolbar-icon">
+ @click="deleteCondition(index)"
+ class="branch-node-move move-node-left"
+ v-if="index != 0 && index + 1 !== currentNode.conditionNodes?.length"
+ @click="moveNode(index, -1)"
+ <Icon icon="ep:arrow-left" />
+ class="branch-node-move move-node-right"
+ v-if="currentNode.conditionNodes && index < currentNode.conditionNodes.length - 2"
+ @click="moveNode(index, 1)"
+ <Icon icon="ep:arrow-right" />
+ <NodeHandler v-model:child-node="item.childNode" />
+ <ConditionNodeConfig :node-index="index" :condition-node="item" :ref="item.id" />
+ <!-- 递归显示子节点 -->
+ v-if="item && item.childNode"
+ :parent-node="item"
+ v-model:flow-node="item.childNode"
+import ProcessNodeTree from '../ProcessNodeTree.vue'
+import ConditionNodeConfig from '../nodes-config/ConditionNodeConfig.vue'
+const { proxy } = getCurrentInstance() as any
+ name: 'ExclusiveNode'
+ // parentNode : {
+ // type: Object as () => SimpleFlowNode,
+ // required: true
+ // },
+// 定义事件,更新父组件
+ 'update:modelValue': [node: SimpleFlowNode | undefined]
+ 'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: number]
+const currentNode = ref<SimpleFlowNode>(props.flowNode)
+// const conditionNodes = computed(() => currentNode.value.conditionNodes);
+const showInputs = ref<boolean[]>([])
+// 失去焦点
+const blurEvent = (index: number) => {
+ showInputs.value[index] = false
+ const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode
+ conditionNode.name =
+ conditionNode.name || getDefaultConditionNodeName(index, conditionNode.defaultFlow)
+// 点击条件名称
+const clickEvent = (index: number) => {
+ showInputs.value[index] = true
+const conditionNodeConfig = (nodeId: string) => {
+ const conditionNode = proxy.$refs[nodeId][0]
+ conditionNode.open()
+// 新增条件
+const addCondition = () => {
+ const conditionNodes = currentNode.value.conditionNodes
+ if (conditionNodes) {
+ const len = conditionNodes.length
+ let lastIndex = len - 1
+ const conditionData: SimpleFlowNode = {
+ name: '条件' + len,
+ conditionNodes: [],
+ conditionNodes.splice(lastIndex, 0, conditionData)
+// 删除条件
+const deleteCondition = (index: number) => {
+ conditionNodes.splice(index, 1)
+ if (conditionNodes.length == 1) {
+ const childNode = currentNode.value.childNode
+ // 更新此节点为后续孩子节点
+ emits('update:modelValue', childNode)
+// 移动节点
+const moveNode = (index: number, to: number) => {
+ // -1 :向左 1: 向右
+ if (currentNode.value.conditionNodes) {
+ currentNode.value.conditionNodes[index] = currentNode.value.conditionNodes.splice(
+ index + to,
+ 1,
+ currentNode.value.conditionNodes[index]
+ )[0]
+ node: SimpleFlowNode,
+ if (!node || node.type === NodeType.START_EVENT_NODE) {
+ if (node.type === nodeType) {
+ nodeList.push(node)
+ // 条件节点 (NodeType.CONDITION_NODE) 比较特殊。需要调用其父节点条件分支节点(NodeType.EXCLUSIVE_NODE) 继续查找
+ emits('find:parentNode', nodeList, nodeType)
@@ -0,0 +1,181 @@
+ <div class="branch-node-add" @click="addCondition">添加分支</div>
+ <div class="branch-line-first-top"></div>
+ <div class="node-box">
+ <div class="branch-priority">无优先级</div>
+ <!-- <div
+ v-if="index != 0 && index + 1 !== currentNode.conditionNodes?.length" @click="moveNode(index, -1)">
+ </div> -->
+ @click="moveNode(index, 1)">
+ name: 'ParallelNode'
+ conditionNode.name = conditionNode.name || `并行${index + 1}`
+ name: '并行' + len,
+ conditionNodes: []
+ // 条件节点 (NodeType.CONDITION_NODE) 比较特殊。需要调用其父节点并行节点(NodeType.PARALLEL_NODE) 继续查找
@@ -0,0 +1,69 @@
+ <div class="node-title-icon start-user"
+ ><span class="iconfont icon-start-user"></span
+ ></div>
+ {{ NODE_DEFAULT_TEXT.get(NodeType.START_USER_NODE) }}
+ <StartUserNodeConfig v-if="currentNode" ref="nodeSetting" :flow-node="currentNode" />
+import { useWatchNode, useNodeName2 } from '../node'
+import { SimpleFlowNode, NODE_DEFAULT_TEXT, NodeType } from '../consts'
+import StartUserNodeConfig from '../nodes-config/StartUserNodeConfig.vue'
+ name: 'StartEventNode'
+// 监控节点变化
+const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.START_USER_NODE)
+ // 把当前节点传递给配置组件
+ nodeSetting.value.showStartUserNodeConfig(currentNode.value)
@@ -0,0 +1,88 @@
+ <div class="node-title-icon user-task"><span class="iconfont icon-approve"></span></div>
+ {{ NODE_DEFAULT_TEXT.get(NodeType.USER_TASK_NODE) }}
+ <UserTaskNodeConfig
+ v-if="currentNode"
+ ref="nodeSetting"
+ @find:return-task-nodes="findReturnTaskNodes"
+import UserTaskNodeConfig from '../nodes-config/UserTaskNodeConfig.vue'
+ name: 'UserTaskNode'
+ 'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: NodeType]
+ nodeSetting.value.showUserTaskNodeConfig(currentNode.value)
+// 查找可以驳回用户节点
+const findReturnTaskNodes = (
+ matchNodeList: SimpleFlowNode[] // 匹配的节点
+ // 从父节点查找
+ emits('find:parentNode', matchNodeList, NodeType.USER_TASK_NODE)
@@ -0,0 +1,33 @@
+import { TimeUnitType, ApproveType, APPROVE_TYPE } from './consts'
+// 获取条件节点默认的名称
+export const getDefaultConditionNodeName = (index: number, defaultFlow: boolean | undefined): string => {
+ if (defaultFlow) {
+ return '其它情况'
+ return '条件' + (index + 1)
+export const convertTimeUnit = (strTimeUnit: string) => {
+ if (strTimeUnit === 'M') {
+ return TimeUnitType.MINUTE
+ if (strTimeUnit === 'H') {
+ return TimeUnitType.HOUR
+ if (strTimeUnit === 'D') {
+ return TimeUnitType.DAY
+export const getApproveTypeText = (approveType: ApproveType): string => {
+ let approveTypeText = ''
+ APPROVE_TYPE.forEach((item) => {
+ if (item.value === approveType) {
+ approveTypeText = item.label
+ return approveTypeText
@@ -0,0 +1,714 @@
+.simple-flow-canvas {
+ inset: 0;
+ z-index: 1;
+ overflow: auto;
+ background-color: #fafafa;
+ user-select: none;
+ .simple-flow-container {
+ justify-content: center;
+ .top-area-container {
+ position: sticky;
+ width: 100%;
+ height: 42px;
+ // padding: 4px 0;
+ background-color: #fff;
+ justify-content: flex-end;
+ .top-actions {
+ margin: 4px;
+ margin-right: 8px;
+ .canvas-control {
+ .control-scale-group {
+ display: inline-flex;
+ .control-scale-button {
+ width: 28px;
+ height: 28px;
+ padding: 2px;
+ text-align: center;
+ .control-scale-label {
+ margin: 0 4px;
+ .scale-container {
+ margin-top: 16px;
+ transform-origin: 50% 0 0;
+ transform: scale(1);
+ transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+ // 节点容器 定义节点宽度
+ .node-container {
+ width: 200px;
+ // 节点
+ .node-box {
+ min-height: 70px;
+ padding: 5px 10px 8px;
+ border: 2px solid transparent;
+ // border-color: #0089ff;
+ border-radius: 8px;
+ box-shadow: 0 1px 4px 0 rgba(10, 30, 65, 0.16);
+ transition: all 0.1s cubic-bezier(0.645, 0.045, 0.355, 1);
+ .node-toolbar {
+ .branch-node-move {
+ // 普通节点标题
+ .node-title-container {
+ padding: 4px;
+ border-radius: 4px 4px 0 0;
+ .node-title-icon {
+ &.user-task {
+ color: #ff943e;
+ &.copy-task {
+ color: #3296fa;
+ &.start-user {
+ color: #676565;
+ .node-title {
+ font-weight: 600;
+ color: #1f1f1f;
+ line-height: 18px;
+ border-bottom: 1px dashed #f60;
+ // 条件节点标题
+ .branch-node-title-container {
+ padding: 4px 0;
+ .input-max-width {
+ max-width: 115px !important;
+ .branch-title {
+ color: #f60;
+ border-bottom: 1px dashed #000;
+ .branch-priority {
+ min-width: 50px;
+ .node-content {
+ min-height: 32px;
+ padding: 4px 8px;
+ margin-top: 4px;
+ line-height: 32px;
+ color: #111f2c;
+ background: rgba(0, 0, 0, 0.03);
+ .node-text {
+ display: -webkit-box;
+ word-break: break-all;
+ -webkit-line-clamp: 2; /* 这将限制文本显示为两行 */
+ -webkit-box-orient: vertical;
+ //条件节点内容
+ .branch-node-content {
+ .branch-node-text {
+ // 节点操作 :删除
+ top: -20px;
+ right: 0px;
+ .toolbar-icon {
+ vertical-align: middle;
+ // 条件节点左右移动
+ width: 10px;
+ display: none;
+ height: 100%;
+ .move-node-left {
+ left: -2px;
+ top: 0px;
+ background: rgba(126, 134, 142, 0.08);
+ border-top-left-radius: 8px;
+ border-bottom-left-radius: 8px;
+ .move-node-right {
+ right: -2px;
+ border-top-right-radius: 6px;
+ border-bottom-right-radius: 6px;
+ .node-config-error {
+ border-color: #ff5219 !important;
+ // 普通节点包装
+ .node-wrapper {
+ // 节点连线处理
+ .node-handler-wrapper {
+ height: 70px;
+ &::before {
+ right: 0;
+ // bottom: 5px;
+ bottom: 0px;
+ z-index: 0;
+ width: 2px;
+ // height: calc(100% - 5px);
+ margin: auto;
+ background-color: #dedede;
+ content: '';
+ .node-handler {
+ .add-icon {
+ top: -5px;
+ width: 25px;
+ height: 25px;
+ color: #fff;
+ background-color: #0089ff;
+ border-radius: 50%;
+ transform: scale(1.1);
+ .node-handler-arrow {
+ bottom: 0;
+ left: 50%;
+ transform: translateX(-50%);
+ // 条件节点包装
+ .branch-node-wrapper {
+ .branch-node-container {
+ width: 4px;
+ transform: translate(-50%);
+ .branch-node-add {
+ top: -18px;
+ height: 36px;
+ padding: 0 10px;
+ font-size: 12px;
+ line-height: 36px;
+ color: #222;
+ background: #fff;
+ border: 2px solid #dedede;
+ border-radius: 18px;
+ transform-origin: center center;
+ transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+ .branch-node-item {
+ min-width: 280px;
+ padding: 40px 40px 0;
+ background: transparent;
+ border-top: 2px solid #dedede;
+ border-bottom: 2px solid #dedede;
+ // 覆盖条件节点第一个节点左上角的线
+ .branch-line-first-top {
+ left: -1px;
+ width: 50%;
+ height: 7px;
+ // 覆盖条件节点第一个节点左下角的线
+ .branch-line-first-bottom {
+ bottom: -5px;
+ // 覆盖条件节点最后一个节点右上角的线
+ .branch-line-last-top {
+ right: -1px;
+ // 覆盖条件节点最后一个节点右下角的线
+ .branch-line-last-bottom {
+ .node-fixed-name {
+ display: inline-block;
+ width: auto;
+ padding: 0 4px;
+ // 开始节点包装
+ .start-node-wrapper {
+ .start-node-container {
+ .start-node-box {
+ width: 90px;
+ padding: 3px 4px;
+ color: #212121;
+ // background: #2c2c2c;
+ background: #fafafa;
+ border-radius: 30px;
+ box-shadow: 0 1px 5px 0 rgba(10, 30, 65, 0.08);
+ // 结束节点包装
+ .end-node-wrapper {
+ .end-node-box {
+ width: 80px;
+ // background: #6e6e6e;
+ // 可编辑的 title 输入框
+ height: 20px;
+ max-width: 145px;
+ line-height: 20px;
+ box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
+// 配置节点头部
+.config-header {
+ .node-name {
+ .divide-line {
+ height: 1px;
+ background: #eee;
+ .config-editable-input {
+ max-width: 510px;
+// 表单字段权限
+.field-setting-pane {
+ .field-setting-desc {
+ .field-permit-title {
+ line-height: 45px;
+ .first-title {
+ .other-titles {
+ .setting-title-label {
+ width: 110px;
+ padding: 5px 0;
+ .field-setting-item {
+ .field-setting-item-label {
+ min-height: 16px;
+ cursor: text;
+ .field-setting-item-group {
+ .item-radio-wrap {
+// 节点连线气泡卡片样式
+.handler-item-wrapper {
+ .handler-item {
+ .handler-item-icon {
+ height: 80px;
+ border: 1px solid #e2e2e2;
+ background: #e2e2e2;
+ box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
+ .icon-size {
+ font-size: 35px;
+ line-height: 80px;
+ .approve {
+ .copy {
+ .condition {
+ color: #15bc83;
+ .handler-item-text {
+// iconfont 样式
+@font-face {
+ font-family: 'iconfont'; /* Project id 4495938 */
+ src:
+ url('iconfont.woff2?t=1724339470412') format('woff2'),
+ url('iconfont.woff?t=1724339470412') format('woff'),
+ url('iconfont.ttf?t=1724339470412') format('truetype');
+.iconfont {
+ font-family: 'iconfont' !important;
+ font-style: normal;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+.icon-start-user:before {
+ content: '\e679';
+.icon-inclusive:before {
+ content: '\e602';
+.icon-copy:before {
+ content: '\e7eb';
+.icon-handle:before {
+ content: '\e61c';
+.icon-exclusive:before {
+ content: '\e717';
+.icon-approve:before {
+ content: '\e715';
+.icon-parallel:before {
+ content: '\e688';
@@ -11,3 +11,14 @@ export const setupAuth = (app: App<Element>) => {
hasRole(app)
hasPermi(app)
+ * 导出指令:v-mountedFocus
+export const setupMountedFocus = (app: App<Element>) => {
+ app.directive('mountedFocus', {
+ mounted(el) {
+ el.focus()
@@ -28,8 +28,8 @@ import '@/plugins/animate.css'
// 路由
import router, { setupRouter } from '@/router'
-// 权限
-import { setupAuth } from '@/directives'
+// 指令
+import { setupAuth, setupMountedFocus } from '@/directives'
import { createApp } from 'vue'
@@ -58,7 +58,9 @@ const setupAll = async () => {
setupRouter(app)
+ // directives 指令
setupAuth(app)
+ setupMountedFocus(app)
await router.isReady()
@@ -1,7 +1,37 @@
import type { App } from 'vue'
// 👇使用 form-create 需额外全局引入 element plus 组件
import {
+ // ElAutocomplete,
+ // ElButton,
+ // ElCascader,
+ // ElCheckbox,
+ // ElCheckboxButton,
+ // ElCheckboxGroup,
+ // ElCol,
+ // ElColorPicker,
+ // ElDatePicker,
+ // ElDialog,
+ // ElForm,
+ // ElInput,
+ // ElInputNumber,
+ // ElPopover,
+ // ElRadio,
+ // ElRadioButton,
+ // ElRadioGroup,
+ // ElRate,
+ // ElRow,
+ // ElSelect,
+ // ElSlider,
+ // ElSwitch,
+ // ElTimePicker,
+ // ElTooltip,
+ // ElTree,
+ // ElUpload,
+ // ElIcon,
+ // ElProgress,
+ // 以上会由 @form-create/element-ui/auto-import 自动引入
ElAlert,
+ ElTransfer,
ElAside,
ElContainer,
ElDivider,
@@ -12,7 +42,18 @@ import {
ElTableColumn,
ElTabPane,
ElTabs,
- ElTransfer
+ ElDropdown,
+ ElDropdownMenu,
+ ElDropdownItem,
+ ElBadge,
+ ElTag,
+ ElText,
+ ElMenu,
+ ElMenuItem,
+ ElFooter,
+ ElMessage
+ // ElFormItem,
+ // ElOption
} from 'element-plus'
import FcDesigner from '@form-create/designer'
import formCreate from '@form-create/element-ui'
@@ -41,18 +82,30 @@ const ApiSelect = useApiSelect({
const components = [
+ ElAlert,
- ElPopconfirm,
- ElHeader,
- ElMain,
- ElTransfer,
- ElAlert,
- ElTabs,
+ ElHeader,
+ ElMain,
+ ElPopconfirm,
ElTable,
+ ElTabs,
+ ElMessage,
+ // ElOption,
UploadImg,
UploadImgs,
UploadFile,
@@ -292,6 +292,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
path: 'process-instance/detail',
+ // component: () => import('@/views/bpm/processInstance/detail/index_new.vue'), // TODO 芋艿:新审批界面,已适配 simple 模式,未来会适配 bpmn 模式
component: () => import('@/views/bpm/processInstance/detail/index.vue'),
name: 'BpmProcessInstanceDetail',
meta: {
@@ -300,7 +301,12 @@ const remainingRouter: AppRouteRecordRaw[] = [
canTo: true,
title: '流程详情',
activeMenu: '/bpm/task/my'
+ props: (route) => ({
+ id: route.query.id,
+ taskId: route.query.taskId,
+ activityId: route.query.activityId
path: 'oa/leave/create',
@@ -603,6 +609,38 @@ const remainingRouter: AppRouteRecordRaw[] = [
hidden: true,
breadcrumb: false
+ path: '/iot',
+ component: Layout,
+ name: 'IOT',
+ meta: {
+ hidden: true
+ children: [
+ path: 'product/detail/:id',
+ name: 'IoTProductDetail',
+ title: '产品详情',
+ noCache: true,
+ hidden: true,
+ activeMenu: '/iot/product'
+ component: () => import('@/views/iot/product/detail/index.vue')
+ path: 'device/detail/:id',
+ name: 'IoTDeviceDetail',
+ title: '设备详情',
+ activeMenu: '/iot/device'
+ component: () => import('@/views/iot/device/detail/index.vue')
]
@@ -1,4 +1,4 @@
-import { store } from '../index'
+import { store } from '../../index'
import { defineStore } from 'pinia'
export const useWorkFlowStore = defineStore('simpleWorkflow', {
@@ -6,15 +6,15 @@ export const useWorkFlowStore = defineStore('simpleWorkflow', {
tableId: '',
isTried: false,
promoterDrawer: false,
- flowPermission1: {},
approverDrawer: false,
approverConfig1: {},
copyerDrawer: false,
- copyerConfig1: {},
+ copyerConfig: {},
conditionDrawer: false,
conditionsConfig1: {
conditionNodes: []
+ userTaskConfig: {}
}),
actions: {
setTableId(payload) {
@@ -26,26 +26,26 @@ export const useWorkFlowStore = defineStore('simpleWorkflow', {
setPromoter(payload) {
this.promoterDrawer = payload
- setFlowPermission(payload) {
- this.flowPermission1 = payload
- setApprover(payload) {
+ setApproverDrawer(payload) {
this.approverDrawer = payload
setApproverConfig(payload) {
this.approverConfig1 = payload
- setCopyer(payload) {
+ setCopyerDrawer(payload) {
this.copyerDrawer = payload
setCopyerConfig(payload) {
- this.copyerConfig1 = payload
+ this.copyerConfig = payload
setCondition(payload) {
this.conditionDrawer = payload
setConditionsConfig(payload) {
this.conditionsConfig1 = payload
+ setUserTaskConfig(payload) {
+ this.userTaskConfig = payload
@@ -437,3 +437,15 @@ export const ErpBizType = {
SALE_OUT: 21,
SALE_RETURN: 22
+// ========== BPM 模块 ==========
+export const BpmModelType = {
+ BPMN: 10, // BPMN 设计器
+ SIMPLE: 20 // 简易设计器
+export const BpmModelFormType = {
+ NORMAL: 10, // 流程表单
+ CUSTOM: 20 // 业务表单
@@ -143,6 +143,7 @@ export enum DICT_TYPE {
INFRA_OPERATE_TYPE = 'infra_operate_type',
// ========== BPM 模块 ==========
+ BPM_MODEL_TYPE = 'bpm_model_type',
BPM_MODEL_FORM_TYPE = 'bpm_model_form_type',
BPM_TASK_CANDIDATE_STRATEGY = 'bpm_task_candidate_strategy',
BPM_PROCESS_INSTANCE_STATUS = 'bpm_process_instance_status',
@@ -225,5 +226,18 @@ export enum DICT_TYPE {
AI_WRITE_LENGTH = 'ai_write_length', // AI 写作长度
AI_WRITE_FORMAT = 'ai_write_format', // AI 写作格式
AI_WRITE_TONE = 'ai_write_tone', // AI 写作语气
- AI_WRITE_LANGUAGE = 'ai_write_language' // AI 写作语言
+ AI_WRITE_LANGUAGE = 'ai_write_language', // AI 写作语言
+ // ========== IOT - 物联网模块 ==========
+ IOT_NET_TYPE = 'iot_net_type', // IOT 联网方式
+ IOT_VALIDATE_TYPE = 'iot_validate_type', // IOT 数据校验级别
+ IOT_PRODUCT_STATUS = 'iot_product_status', // IOT 产品状态
+ IOT_PRODUCT_DEVICE_TYPE = 'iot_product_device_type', // IOT 产品设备类型
+ IOT_DATA_FORMAT = 'iot_data_format', // IOT 数据格式
+ IOT_PROTOCOL_TYPE = 'iot_protocol_type', // IOT 接入网关协议
+ IOT_DEVICE_STATUS = 'iot_device_status', // IOT 设备状态
+ IOT_PRODUCT_FUNCTION_TYPE = 'iot_product_function_type', // IOT 产品功能类型
+ IOT_DATA_TYPE = 'iot_data_type', // IOT 数据类型
+ IOT_UNIT_TYPE = 'iot_unit_type', // IOT 单位类型
+ IOT_RW_TYPE = 'iot_rw_type' // IOT 读写类型
@@ -1,14 +1,18 @@
<template>
- <ContentWrap>
+ <ContentWrap :body-style="{ padding: '0px' }" class="!mb-0">
<!-- 表单设计器 -->
- <FcDesigner ref="designer" height="780px">
- <template #handle>
- <el-button round size="small" type="primary" @click="handleSave">
- <Icon class="mr-5px" icon="ep:plus" />
- 保存
- </el-button>
- </FcDesigner>
+ class="h-[calc(100vh-var(--top-tool-height)-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-2px)]"
+ <fc-designer class="my-designer" ref="designer" :config="designerConfig">
+ <template #handle>
+ <el-button size="small" type="success" plain @click="handleSave">
+ <Icon class="mr-5px" icon="ep:plus" />
+ 保存
+ </el-button>
+ </fc-designer>
</ContentWrap>
<!-- 表单保存的弹窗 -->
@@ -55,6 +59,31 @@ const { push, currentRoute } = useRouter() // 路由
const { query } = useRoute() // 路由信息
const { delView } = useTagsViewStore() // 视图操作
+// 表单设计器配置
+const designerConfig = ref({
+ switchType: [], // 是否可以切换组件类型,或者可以相互切换的字段
+ autoActive: true, // 是否自动选中拖入的组件
+ useTemplate: false, // 是否生成vue2语法的模板组件
+ formOptions: {}, // 定义表单配置默认值
+ fieldReadonly: false, // 配置field是否可以编辑
+ hiddenDragMenu: false, // 隐藏拖拽操作按钮
+ hiddenDragBtn: false, // 隐藏拖拽按钮
+ hiddenMenu: [], // 隐藏部分菜单
+ hiddenItem: [], // 隐藏部分组件
+ hiddenItemConfig: {}, // 隐藏组件的部分配置项
+ disabledItemConfig: {}, // 禁用组件的部分配置项
+ showSaveBtn: false, // 是否显示保存按钮
+ showConfig: true, // 是否显示右侧的配置界面
+ showBaseForm: true, // 是否显示组件的基础配置表单
+ showControl: true, // 是否显示组件联动
+ showPropsForm: true, // 是否显示组件的属性配置表单
+ showEventForm: true, // 是否显示组件的事件配置表单
+ showValidateForm: true, // 是否显示组件的验证配置表单
+ showFormConfig: true, // 是否显示表单配置
+ showInputData: true, // 是否显示录入按钮
+ showDevice: true, // 是否显示多端适配选项
+ appendConfigData: [] // 定义渲染规则所需的formData
const designer = ref() // 表单设计器
useFormCreateDesigner(designer) // 表单设计器增强
const dialogVisible = ref(false) // 弹窗是否展示
@@ -119,3 +148,13 @@ onMounted(async () => {
setConfAndFields(designer, data.conf, data.fields)
+<style>
+.my-designer {
+ ._fc-l,
+ ._fc-m,
+ ._fc-r {
+ border-top: none;
@@ -8,12 +8,7 @@
label-width="110px"
>
<el-form-item label="流程标识" prop="key">
- <el-input
- v-model="formData.key"
- :disabled="!!formData.id"
- placeholder="请输入流标标识"
- style="width: 330px"
+ <el-input v-model="formData.key" :disabled="!!formData.id" placeholder="请输入流标标识" />
<el-tooltip
v-if="!formData.id"
class="item"
@@ -35,7 +30,7 @@
placeholder="请输入流程名称"
/>
</el-form-item>
- <el-form-item v-if="formData.id" label="流程分类" prop="category">
+ <el-form-item label="流程分类" prop="category">
v-model="formData.category"
clearable
@@ -50,73 +45,108 @@
</el-select>
- <el-form-item v-if="formData.id" label="流程图标" prop="icon">
- <UploadImg v-model="formData.icon" :limit="1" height="128px" width="128px" />
+ <el-form-item label="流程图标" prop="icon">
+ <UploadImg v-model="formData.icon" :limit="1" height="64px" width="64px" />
<el-form-item label="流程描述" prop="description">
<el-input v-model="formData.description" clearable type="textarea" />
- <div v-if="formData.id">
- <el-form-item label="表单类型" prop="formType">
- <el-radio-group v-model="formData.formType">
- <el-radio
- v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_FORM_TYPE)"
- :key="dict.value"
- :value="dict.value"
- >
- {{ dict.label }}
- </el-radio>
- </el-radio-group>
- </el-form-item>
- <el-form-item v-if="formData.formType === 10" label="流程表单" prop="formId">
- <el-select v-model="formData.formId" clearable style="width: 100%">
- <el-option
- v-for="form in formList"
- :key="form.id"
- :label="form.name"
- :value="form.id"
- </el-select>
- <el-form-item
- v-if="formData.formType === 20"
- label="表单提交路由"
- prop="formCustomCreatePath"
+ <el-form-item label="流程类型" prop="type">
+ <el-radio-group v-model="formData.type">
+ v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_TYPE)"
+ :key="dict.value"
+ <el-form-item label="表单类型" prop="formType">
+ <el-radio-group v-model="formData.formType">
+ v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_FORM_TYPE)"
+ <el-form-item v-if="formData.formType === 10" label="流程表单" prop="formId">
+ <el-select v-model="formData.formId" clearable style="width: 100%">
+ <el-option v-for="form in formList" :key="form.id" :label="form.name" :value="form.id" />
+ v-if="formData.formType === 20"
+ label="表单提交路由"
+ prop="formCustomCreatePath"
+ v-model="formData.formCustomCreatePath"
+ placeholder="请输入表单提交路由"
+ style="width: 330px"
+ <el-tooltip
+ class="item"
+ content="自定义表单的提交路径,使用 Vue 的路由地址,例如说:bpm/oa/leave/create.vue"
+ effect="light"
+ placement="top"
- v-model="formData.formCustomCreatePath"
- placeholder="请输入表单提交路由"
- <el-tooltip
- class="item"
- content="自定义表单的提交路径,使用 Vue 的路由地址,例如说:bpm/oa/leave/create"
- effect="light"
- placement="top"
+ <i class="el-icon-question" style="padding-left: 5px"></i>
+ </el-tooltip>
+ <el-form-item v-if="formData.formType === 20" label="表单查看地址" prop="formCustomViewPath">
+ v-model="formData.formCustomViewPath"
+ placeholder="请输入表单查看的组件地址"
+ content="自定义表单的查看组件地址,使用 Vue 的组件地址,例如说:bpm/oa/leave/detail.vue"
+ <el-form-item label="是否可见" prop="visible">
+ <el-radio-group v-model="formData.visible">
+ v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+ :key="dict.value as string"
- <i class="el-icon-question" style="padding-left: 5px"></i>
- </el-tooltip>
- label="表单查看地址"
- prop="formCustomViewPath"
+ <el-form-item label="谁可以发起" prop="startUserIds">
+ v-model="formData.startUserIds"
+ placeholder="请选择可发起人,默认(不选择)则所有人都可以发起"
- v-model="formData.formCustomViewPath"
- placeholder="请输入表单查看的组件地址"
+ v-for="user in userList"
+ :key="user.id"
+ :label="user.nickname"
+ :value="user.id"
- content="自定义表单的查看组件地址,使用 Vue 的组件地址,例如说:bpm/oa/leave/detail"
+ <el-form-item label="流程管理员" prop="managerUserIds">
+ <el-select v-model="formData.managerUserIds" multiple placeholder="请选择流程管理员">
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
@@ -125,45 +155,62 @@
</Dialog>
<script lang="ts" setup>
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '@/utils/dict'
import { ElMessageBox } from 'element-plus'
import * as ModelApi from '@/api/bpm/model'
import * as FormApi from '@/api/bpm/form'
import { CategoryApi } from '@/api/bpm/category'
+import { BpmModelFormType, BpmModelType } from '@/utils/constants'
+import { UserVO } from '@/api/system/user'
+import { useUserStoreWithOut } from '@/store/modules/user'
defineOptions({ name: 'ModelForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
+const userStore = useUserStoreWithOut() // 用户信息缓存
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formType = ref('') // 表单的类型:create - 新增;update - 修改
const formData = ref({
- formType: 10,
+ id: undefined,
name: '',
+ key: '',
category: undefined,
icon: undefined,
description: '',
+ type: BpmModelType.BPMN,
+ formType: BpmModelFormType.NORMAL,
formId: '',
formCustomCreatePath: '',
- formCustomViewPath: ''
+ formCustomViewPath: '',
+ visible: true,
+ startUserIds: [],
+ managerUserIds: []
const formRules = reactive({
- name: [{ required: true, message: '参数名称不能为空', trigger: 'blur' }],
- key: [{ required: true, message: '参数键名不能为空', trigger: 'blur' }],
- category: [{ required: true, message: '参数分类不能为空', trigger: 'blur' }],
- icon: [{ required: true, message: '参数图标不能为空', trigger: 'blur' }],
- value: [{ required: true, message: '参数键值不能为空', trigger: 'blur' }],
- visible: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }]
+ name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }],
+ key: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }],
+ category: [{ required: true, message: '流程分类不能为空', trigger: 'blur' }],
+ icon: [{ required: true, message: '流程图标不能为空', trigger: 'blur' }],
+ type: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],
+ formType: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],
+ formId: [{ required: true, message: '流程表单不能为空', trigger: 'blur' }],
+ formCustomCreatePath: [{ required: true, message: '表单提交路由不能为空', trigger: 'blur' }],
+ formCustomViewPath: [{ required: true, message: '表单查看地址不能为空', trigger: 'blur' }],
+ visible: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],
+ managerUserIds: [{ required: true, message: '流程管理员不能为空', trigger: 'blur' }]
const formRef = ref() // 表单 Ref
const formList = ref([]) // 流程表单的下拉框的数据
const categoryList = ref([]) // 流程分类列表
+const userList = ref<UserVO[]>([]) // 用户列表
/** 打开弹窗 */
-const open = async (type: string, id?: number) => {
+const open = async (type: string, id?: string) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
@@ -176,11 +223,15 @@ const open = async (type: string, id?: number) => {
} finally {
formLoading.value = false
+ formData.value.managerUserIds.push(userStore.getUser.id)
// 获得流程表单的下拉框的数据
formList.value = await FormApi.getFormSimpleList()
// 查询流程分类列表
categoryList.value = await CategoryApi.getCategorySimpleList()
+ // 查询用户列表
+ userList.value = await UserApi.getSimpleUserList()
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
@@ -199,10 +250,9 @@ const submitForm = async () => {
await ModelApi.createModel(data)
// 提示,引导用户做后续的操作
await ElMessageBox.alert(
- '<strong>新建模型成功!</strong>后续需要执行如下 3 个步骤:' +
- '<div>1. 点击【修改流程】按钮,配置流程的分类、表单信息</div>' +
- '<div>2. 点击【设计流程】按钮,绘制流程图</div>' +
- '<div>3. 点击【发布流程】按钮,完成流程的最终发布</div>' +
+ '<strong>新建模型成功!</strong>后续需要执行如下 2 个步骤:' +
+ '<div>1. 点击【设计流程】按钮,绘制流程图</div>' +
+ '<div>2. 点击【发布流程】按钮,完成流程的最终发布</div>' +
'另外,每次流程修改后,都需要点击【发布流程】按钮,才能正式生效!!!',
'重要提示',
@@ -225,14 +275,20 @@ const submitForm = async () => {
/** 重置表单 */
const resetForm = () => {
formData.value = {
- icon: '',
+ icon: undefined,
formRef.value?.resetFields()
@@ -1,141 +0,0 @@
- <Dialog v-model="dialogVisible" title="导入流程" width="400">
- <div>
- <el-upload
- ref="uploadRef"
- v-model:file-list="fileList"
- :action="importUrl"
- :auto-upload="false"
- :data="formData"
- :disabled="formLoading"
- :headers="uploadHeaders"
- :limit="1"
- :on-error="submitFormError"
- :on-exceed="handleExceed"
- :on-success="submitFormSuccess"
- accept=".bpmn, .xml"
- drag
- name="bpmnFile"
- <Icon class="el-icon--upload" icon="ep:upload-filled" />
- <div class="el-upload__text"> 将文件拖到此处,或 <em>点击上传</em></div>
- <template #tip>
- <div class="el-upload__tip" style="color: red">
- 提示:仅允许导入“bpm”或“xml”格式文件!
- <el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px">
- <el-form-item label="流程标识" prop="key">
- style="width: 250px"
- <el-form-item label="流程名称" prop="name">
- <el-input v-model="formData.name" clearable placeholder="请输入流程名称" />
- <el-form-item label="流程描述" prop="description">
- <el-input v-model="formData.description" clearable type="textarea" />
- </el-form>
- </el-upload>
- <template #footer>
- <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
- <el-button @click="dialogVisible = false">取 消</el-button>
- </Dialog>
-<script lang="ts" setup>
-import { getAccessToken, getTenantId } from '@/utils/auth'
-defineOptions({ name: 'ModelImportForm' })
-const message = useMessage() // 消息弹窗
-const dialogVisible = ref(false) // 弹窗的是否展示
-const formLoading = ref(false) // 表单的加载中
-const formData = ref({
- key: '',
- name: '',
- description: ''
-const formRules = reactive({
- key: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }],
- name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }]
-const formRef = ref() // 表单 Ref
-const uploadRef = ref() // 上传 Ref
-const importUrl = import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/bpm/model/import'
-const uploadHeaders = ref() // 上传 Header 头
-const fileList = ref([]) // 文件列表
-/** 打开弹窗 */
-const open = async () => {
- dialogVisible.value = true
- resetForm()
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-/** 提交表单 */
-const submitForm = async () => {
- // 校验表单
- if (!formRef) return
- const valid = await formRef.value.validate()
- if (!valid) return
- if (fileList.value.length == 0) {
- message.error('请上传文件')
- return
- // 提交请求
- uploadHeaders.value = {
- Authorization: 'Bearer ' + getAccessToken(),
- 'tenant-id': getTenantId()
- formLoading.value = true
- uploadRef.value!.submit()
-/** 文件上传成功 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitFormSuccess = async (response: any) => {
- if (response.code !== 0) {
- message.error(response.msg)
- formLoading.value = false
- // 提示成功
- message.success('导入流程成功!请点击【设计流程】按钮,进行编辑保存后,才可以进行【发布流程】')
- dialogVisible.value = false
- // 发送操作成功的事件
- emit('success')
-/** 上传错误提示 */
-const submitFormError = (): void => {
- message.error('导入流程失败,请您重新上传!')
-/** 重置表单 */
-const resetForm = () => {
- // 重置上传状态和文件
- uploadRef.value?.clearFiles()
- // 重置表单
- formData.value = {
- formRef.value?.resetFields()
-/** 文件数超出提示 */
-const handleExceed = (): void => {
- message.error('最多只能上传一个文件!')
@@ -58,17 +58,17 @@ const initModeler = (item) => {
/** 添加/修改模型 */
-const save = async (bpmnXml) => {
+const save = async (bpmnXml: string) => {
...model.value,
bpmnXml: bpmnXml // bpmnXml 只是初始化流程图,后续修改无法通过它获得
} as unknown as ModelApi.ModelVO
// 提交
if (data.id) {
- await ModelApi.updateModel(data)
+ await ModelApi.updateModelBpmn(data)
message.success('修改成功')
} else {
- await ModelApi.createModel(data)
message.success('新增成功')
// 跳转回去
@@ -58,10 +58,7 @@
@click="openForm('create')"
v-hasPermi="['bpm:model:create']"
- <Icon icon="ep:plus" class="mr-5px" /> 新建流程
- <el-button type="success" plain @click="openImportForm" v-hasPermi="['bpm:model:import']">
- <Icon icon="ep:upload" class="mr-5px" /> 导入流程
+ <Icon icon="ep:plus" class="mr-5px" /> 新建
</el-button>
@@ -70,21 +67,34 @@
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list">
- <el-table-column label="流程标识" align="center" prop="key" width="200" />
- <el-table-column label="流程名称" align="center" prop="name" width="200">
+ <el-table-column label="流程名称" align="center" prop="name" min-width="200" />
+ <el-table-column label="流程图标" align="center" prop="icon" min-width="100">
<template #default="scope">
- <el-button type="primary" link @click="handleBpmnDetail(scope.row)">
- <span>{{ scope.row.name }}</span>
+ <el-image :src="scope.row.icon" class="h-32px w-32px" />
</el-table-column>
- <el-table-column label="流程图标" align="center" prop="icon" width="100">
+ <el-table-column label="可见范围" align="center" prop="startUserIds" min-width="100">
- <el-image :src="scope.row.icon" class="w-32px h-32px" />
+ <el-text v-if="!scope.row.startUsers || scope.row.startUsers.length === 0">
+ 全部可见
+ </el-text>
+ <el-text v-else-if="scope.row.startUsers.length == 1">
+ {{ scope.row.startUsers[0].nickname }}
+ <el-text v-else>
+ class="box-item"
+ effect="dark"
+ :content="scope.row.startUsers.map((user: any) => user.nickname).join('、')"
+ {{ scope.row.startUsers[0].nickname }}等 {{ scope.row.startUsers.length }} 人可见
- <el-table-column label="流程分类" align="center" prop="categoryName" width="100" />
- <el-table-column label="表单信息" align="center" prop="formType" width="200">
+ <el-table-column label="流程分类" align="center" prop="categoryName" min-width="100" />
+ <el-table-column label="表单信息" align="center" prop="formType" min-width="200">
<el-button
v-if="scope.row.formType === 10"
@@ -105,101 +115,87 @@
<label v-else>暂无表单</label>
- <el-table-column
- label="创建时间"
- align="center"
- prop="createTime"
- width="180"
- :formatter="dateFormatter"
- <el-table-column label="最新部署的流程定义" align="center">
- label="流程版本"
- prop="processDefinition.version"
- width="100"
- <template #default="scope">
- <el-tag v-if="scope.row.processDefinition">
- v{{ scope.row.processDefinition.version }}
- </el-tag>
- <el-tag v-else type="warning">未部署</el-tag>
- </el-table-column>
- label="激活状态"
- width="85"
- <el-switch
- v-if="scope.row.processDefinition"
- v-model="scope.row.processDefinition.suspensionState"
- :active-value="1"
- :inactive-value="2"
- @change="handleChangeState(scope.row)"
- <el-table-column label="部署时间" align="center" prop="deploymentTime" width="180">
- <span v-if="scope.row.processDefinition">
- {{ formatDate(scope.row.processDefinition.deploymentTime) }}
- </span>
+ <el-table-column label="最后发布" align="center" prop="deploymentTime" min-width="250">
+ <template #default="scope">
+ <span v-if="scope.row.processDefinition">
+ {{ formatDate(scope.row.processDefinition.deploymentTime) }}
+ <el-tag v-if="scope.row.processDefinition" class="ml-10px">
+ v{{ scope.row.processDefinition.version }}
+ </el-tag>
+ <el-tag v-else type="warning">未部署</el-tag>
+ <el-tag
+ v-if="scope.row.processDefinition?.suspensionState === 2"
+ type="warning"
+ class="ml-10px"
+ 已停用
- <el-table-column label="操作" align="center" min-width="240" fixed="right">
+ <el-table-column label="操作" align="center" width="200" fixed="right">
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['bpm:model:update']"
+ :disabled="!isManagerUser(scope.row)"
- 修改流程
+ 修改
+ class="!ml-5px"
@click="handleDesign(scope.row)"
- 设计流程
- <el-button
- link
- type="primary"
- @click="handleSimpleDesign(scope.row.id)"
- v-hasPermi="['bpm:model:update']"
- 仿钉钉设计流程
+ 设计
@click="handleDeploy(scope.row)"
v-hasPermi="['bpm:model:deploy']"
- 发布流程
- v-hasPermi="['bpm:process-definition:query']"
- @click="handleDefinitionList(scope.row)"
- 流程定义
+ 发布
- type="danger"
- @click="handleDelete(scope.row.id)"
- v-hasPermi="['bpm:model:delete']"
+ <el-dropdown
+ class="!align-middle ml-5px"
+ @command="(command) => handleCommand(command, scope.row)"
+ v-hasPermi="['bpm:process-definition:query', 'bpm:model:update', 'bpm:model:delete']"
- 删除
+ <el-button type="primary" link>更多</el-button>
+ <template #dropdown>
+ <el-dropdown-menu>
+ <el-dropdown-item
+ command="handleDefinitionList"
+ v-if="checkPermi(['bpm:process-definition:query'])"
+ 历史
+ </el-dropdown-item>
+ command="handleChangeState"
+ v-if="checkPermi(['bpm:model:update']) && scope.row.processDefinition"
+ {{ scope.row.processDefinition.suspensionState === 1 ? '停用' : '启用' }}
+ type="danger"
+ command="handleDelete"
+ v-if="checkPermi(['bpm:model:delete'])"
+ 删除
+ </el-dropdown-menu>
+ </el-dropdown>
</el-table>
@@ -215,41 +211,29 @@
<!-- 表单弹窗:添加/修改流程 -->
<ModelForm ref="formRef" @success="getList" />
- <!-- 表单弹窗:导入流程 -->
- <ModelImportForm ref="importFormRef" @success="getList" />
<!-- 弹窗:表单详情 -->
<Dialog title="表单详情" v-model="formDetailVisible" width="800">
<form-create :rule="formDetailPreview.rule" :option="formDetailPreview.option" />
- <!-- 弹窗:流程模型图的预览 -->
- <Dialog title="流程图" v-model="bpmnDetailVisible" width="800">
- <MyProcessViewer
- key="designer"
- v-model="bpmnXML"
- :value="bpmnXML as any"
- v-bind="bpmnControlForm"
- :prefix="bpmnControlForm.prefix"
-import { dateFormatter, formatDate } from '@/utils/formatTime'
-import { MyProcessViewer } from '@/components/bpmnProcessDesigner/package'
+import { formatDate } from '@/utils/formatTime'
import ModelForm from './ModelForm.vue'
-import ModelImportForm from '@/views/bpm/model/ModelImportForm.vue'
import { setConfAndFields2 } from '@/utils/formCreate'
+import { BpmModelType } from '@/utils/constants'
+import { checkPermi } from '@/utils/permission'
defineOptions({ name: 'BpmModel' })
const { push } = useRouter() // 路由
const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数
@@ -288,25 +272,36 @@ const resetQuery = () => {
handleQuery()
+/** '更多'操作按钮 */
+const handleCommand = (command: string, row: any) => {
+ switch (command) {
+ case 'handleDefinitionList':
+ handleDefinitionList(row)
+ case 'handleDelete':
+ handleDelete(row)
+ case 'handleChangeState':
+ handleChangeState(row)
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
-/** 添加/修改操作 */
-const importFormRef = ref()
-const openImportForm = () => {
- importFormRef.value.open()
/** 删除按钮操作 */
-const handleDelete = async (id: number) => {
+const handleDelete = async (row: any) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
- await ModelApi.deleteModel(id)
+ await ModelApi.deleteModel(row.id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
@@ -314,45 +309,45 @@ const handleDelete = async (id: number) => {
/** 更新状态操作 */
-const handleChangeState = async (row) => {
+const handleChangeState = async (row: any) => {
const state = row.processDefinition.suspensionState
+ const newState = state === 1 ? 2 : 1
// 修改状态的二次确认
const id = row.id
- const statusState = state === 1 ? '激活' : '挂起'
+ debugger
+ const statusState = state === 1 ? '停用' : '启用'
const content = '是否确认' + statusState + '流程名字为"' + row.name + '"的数据项?'
await message.confirm(content)
// 发起修改状态
- await ModelApi.updateModelState(id, state)
+ await ModelApi.updateModelState(id, newState)
+ message.success(statusState + '成功')
- } catch {
- // 取消后,进行恢复按钮
- row.processDefinition.suspensionState = state === 1 ? 2 : 1
+ } catch {}
/** 设计流程 */
-const handleDesign = (row) => {
- push({
- name: 'BpmModelEditor',
- query: {
- modelId: row.id
-const handleSimpleDesign = (row) => {
- name: 'SimpleWorkflowDesignEditor',
+const handleDesign = (row: any) => {
+ if (row.type == BpmModelType.BPMN) {
+ push({
+ name: 'BpmModelEditor',
+ query: {
+ modelId: row.id
+ name: 'SimpleWorkflowDesignEditor',
/** 发布流程 */
-const handleDeploy = async (row) => {
+const handleDeploy = async (row: any) => {
await message.confirm('是否部署该流程!!')
@@ -380,7 +375,7 @@ const formDetailPreview = ref({
rule: [],
option: {}
-const handleFormDetail = async (row) => {
+const handleFormDetail = async (row: any) => {
if (row.formType == 10) {
// 设置表单
const data = await FormApi.getForm(row.formId)
@@ -394,16 +389,10 @@ const handleFormDetail = async (row) => {
-/** 流程图的详情按钮操作 */
-const bpmnDetailVisible = ref(false)
-const bpmnXML = ref(null)
-const bpmnControlForm = ref({
- prefix: 'flowable'
-const handleBpmnDetail = async (row) => {
- const data = await ModelApi.getModel(row.id)
- bpmnXML.value = data.bpmnXml || ''
- bpmnDetailVisible.value = true
+/** 判断是否可以操作 */
+const isManagerUser = (row: any) => {
+ const userId = userStore.getUser.id
+ return row.managerUserIds && row.managerUserIds.includes(userId)
/** 初始化 **/
@@ -1,13 +1,22 @@
<div
- class="h-50px position-fixed bottom-10 text-14px flex items-center color-#32373c dark:color-#fff font-bold btn-container"
+ class="h-50px bottom-10 text-14px flex items-center color-#32373c dark:color-#fff font-bold btn-container"
+ v-if="runningTask.id"
- <el-popover :visible="passVisible" placement="top-end" :width="500" trigger="click">
+ <!-- 【通过】按钮 -->
+ :visible="passVisible"
+ placement="top-end"
+ :width="500"
+ trigger="click"
+ v-if="isShowButton(OperationButtonType.APPROVE)"
<template #reference>
<el-button plain type="success" @click="openPopover('1')">
- <Icon icon="ep:select" /> 通过
+ <Icon icon="ep:select" /> {{ getButtonDisplayName(OperationButtonType.APPROVE) }}
+ <!-- 审批表单 -->
<div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
<el-form
label-position="top"
@@ -50,19 +59,28 @@
<el-form-item>
<el-button :disabled="formLoading" type="success" @click="handleAudit(true)">
- 通过
+ {{ getButtonDisplayName(OperationButtonType.APPROVE) }}
<el-button @click="passVisible = false"> 取消 </el-button>
</div>
</el-popover>
- <el-popover :visible="rejectVisible" placement="top-end" :width="500" trigger="click">
+ <!-- 【拒绝】按钮 -->
+ :visible="rejectVisible"
+ v-if="isShowButton(OperationButtonType.REJECT)"
<el-button class="mr-20px" plain type="danger" @click="openPopover('2')">
- <Icon icon="ep:close" /> 拒绝
+ <Icon icon="ep:close" /> {{ getButtonDisplayName(OperationButtonType.REJECT) }}
@@ -105,21 +123,46 @@
<el-button :disabled="formLoading" type="danger" @click="handleAudit(false)">
- 拒绝
+ {{ getButtonDisplayName(OperationButtonType.REJECT) }}
<el-button @click="rejectVisible = false"> 取消 </el-button>
+ <!-- 【抄送】按钮 -->
<div @click="handleSend"> <Icon :size="14" icon="svg-icon:send" /> 抄送 </div>
- <div @click="openTaskUpdateAssigneeForm">
- <Icon :size="14" icon="fa:share-square-o" /> 转交
+ <!-- 【转交】按钮 -->
+ <div @click="openTaskUpdateAssigneeForm" v-if="isShowButton(OperationButtonType.TRANSFER)">
+ <Icon :size="14" icon="fa:share-square-o" />
+ {{ getButtonDisplayName(OperationButtonType.TRANSFER) }}
+ <!-- 【委托】按钮 -->
+ <div @click="handleDelegate" v-if="isShowButton(OperationButtonType.DELEGATE)">
+ <Icon :size="14" icon="ep:position" />
+ {{ getButtonDisplayName(OperationButtonType.DELEGATE) }}
+ <!-- 【加签】 -->
+ <div @click="handleSign" v-if="isShowButton(OperationButtonType.ADD_SIGN)">
+ <Icon :size="14" icon="ep:plus" />
+ {{ getButtonDisplayName(OperationButtonType.ADD_SIGN) }}
+ <!-- TODO @jason:减签 -->
+ <!-- 【退回】按钮 -->
+ <div @click="handleBack" v-if="isShowButton(OperationButtonType.RETURN)">
+ <Icon :size="14" icon="fa:mail-reply" />
+ {{ getButtonDisplayName(OperationButtonType.RETURN) }}
- <div @click="handleDelegate"> <Icon :size="14" icon="ep:position" /> 委派 </div>
- <div @click="handleSign"> <Icon :size="14" icon="ep:plus" /> 加签 </div>
- <div @click="handleBack"> <Icon :size="14" icon="fa:mail-reply" /> 退回 </div>
+ <!--TODO @jason:撤回 -->
+ <!--TODO @jason:再次发起 -->
<!-- 弹窗:转派审批人 -->
<TaskTransferForm ref="taskTransferFormRef" @success="getDetail" />
<!-- 弹窗:回退节点 -->
@@ -129,7 +172,6 @@
<!-- 弹窗:加签,当前任务审批人为A,向前加签选了一个C,则需要C先审批,然后再是A审批,向后加签B,A审批完,需要B再审批完,才算完成这个任务节点 -->
<TaskSignCreateForm ref="taskSignCreateFormRef" @success="getDetail" />
import { useUserStore } from '@/store/modules/user'
@@ -140,7 +182,10 @@ import TaskDelegateForm from './dialog/TaskDelegateForm.vue'
import TaskTransferForm from './dialog/TaskTransferForm.vue'
import TaskSignCreateForm from './dialog/TaskSignCreateForm.vue'
import { isEmpty } from '@/utils/is'
+ OperationButtonType,
+ OPERATION_BUTTON_NAME
+} from '@/components/SimpleProcessDesignerV2/src/consts'
defineOptions({ name: 'ProcessInstanceBtnConatiner' })
const userId = useUserStore().getUser.id // 当前登录的编号
@@ -175,15 +220,17 @@ watch(
deep: true
)
+// TODO @jaosn:具体的审批任务,要不改成后端返回。让前端弱化下
/**
* 设置 runningTasks 中的任务
*/
-const loadRunningTask = (tasks) => {
+const loadRunningTask = (tasks: any[]) => {
runningTask.value = {}
auditForm.value = {}
approveForm.value = {}
approveFormFApi.value = {}
- tasks.forEach((task) => {
+ tasks.forEach((task: any) => {
if (!isEmpty(task.children)) {
loadRunningTask(task.children)
@@ -214,7 +261,7 @@ const loadRunningTask = (tasks) => {
/** 处理审批通过和不通过的操作 */
-const handleAudit = async (pass) => {
+const handleAudit = async (pass: any) => {
formLoading.value = true
const auditFormRef = proxy.$refs['formRef']
@@ -254,6 +301,7 @@ const handleAudit = async (pass) => {
/* 抄送 TODO */
const handleSend = () => {}
+// TODO 代码优化:这里 flag 改成 approve: boolean 。因为 flag 目前就只有 1 和 2
const openPopover = (flag) => {
passVisible.value = false
rejectVisible.value = false
@@ -289,6 +337,24 @@ const getDetail = () => {
emit('success')
+/** 是否显示按钮 */
+const isShowButton = (btnType: OperationButtonType): boolean => {
+ let isShow = true
+ if (runningTask.value.buttonsSetting && runningTask.value.buttonsSetting[btnType]) {
+ isShow = runningTask.value.buttonsSetting[btnType].enable
+ return isShow
+/** 获取按钮的显示名称 */
+const getButtonDisplayName = (btnType: OperationButtonType) => {
+ let displayName = OPERATION_BUTTON_NAME.get(btnType)
+ displayName = runningTask.value.buttonsSetting[btnType].displayName
+ return displayName
defineExpose({ loadRunningTask })
@@ -299,10 +365,11 @@ defineExpose({ loadRunningTask })
.btn-container {
> div {
margin: 0 15px;
cursor: pointer;
align-items: center;
&:hover {
color: #6db5ff;
@@ -1,9 +1,111 @@
+<!-- 审批详情的右侧:审批流 -->
<el-timeline class="pt-20px">
- <el-timeline-item v-for="(activity, index) in mockData" :key="index" size="large">
+ <!-- 遍历每个审批节点 -->
+ <el-timeline-item
+ v-for="(activity, index) in approveNodes"
+ :icon="getApprovalNodeIcon(activity.status, activity.nodeType)"
+ :color="getApprovalNodeColor(activity.status)"
<div class="flex flex-col items-start">
<div class="font-bold"> {{ activity.name }}</div>
- <div class="color-#a1a6ae text-12px mb-10px"> {{ activity.assigneeUser.nickname }}</div>
+ <div class="flex items-center mt-1">
+ <!-- 情况一:遍历每个审批节点下的【进行中】task 任务 -->
+ <div v-for="(task, idx) in activity.tasks" :key="idx" class="flex items-center">
+ <div class="flex items-center flex-col pr-2">
+ <div class="position-relative" v-if="task.assigneeUser || task.ownerUser">
+ <!-- 信息:头像 -->
+ <el-avatar
+ :size="36"
+ v-if="task.assigneeUser && task.assigneeUser.avatar"
+ :src="task.assigneeUser.avatar"
+ <el-avatar v-else-if="task.assigneeUser && task.assigneeUser.nickname">
+ {{ task.assigneeUser.nickname.substring(0, 1) }}
+ </el-avatar>
+ v-else-if="task.ownerUser && task.ownerUser.avatar"
+ :src="task.ownerUser.avatar"
+ <el-avatar v-else-if="task.ownerUser && task.ownerUser.nickname">
+ {{ task.ownerUser.nickname.substring(0, 1) }}
+ <!-- 信息:任务 ICON -->
+ class="position-absolute top-26px left-26px bg-#fff rounded-full flex items-center p-2px"
+ :size="12"
+ :icon="statusIconMap2[task.status]?.icon"
+ :color="statusIconMap2[task.status]?.color"
+ <div class="flex flex-col mt-1">
+ <!-- 信息:昵称 -->
+ v-if="task.assigneeUser && task.assigneeUser.nickname"
+ class="text-10px text-align-center"
+ {{ task.assigneeUser.nickname }}
+ v-else-if="task.ownerUser && task.ownerUser.nickname"
+ {{ task.ownerUser.nickname }}
+ <!-- TODO @jason:审批意见,要展示哈。 -->
+ <!-- <div v-if="task.reason" :title="task.reason" class="text-13px text-truncate w-150px mt-1"> 审批意见: {{ task.reason }}</div> -->
+ <!-- 情况二:遍历每个审批节点下的【候选的】task 任务。例如说,1)依次审批,2)未来的审批任务等 -->
+ v-for="(user, idx1) in activity.candidateUserList"
+ :key="idx1"
+ <div class="position-relative">
+ <el-avatar :size="36" v-if="user.avatar" :src="user.avatar" />
+ <el-avatar v-else-if="user.nickname && user.nickname">
+ {{ user.nickname.substring(0, 1) }}
+ :icon="statusIconMap2['-1']?.icon"
+ :color="statusIconMap2['-1']?.color"
+ <div v-if="user.nickname" class="text-10px text-align-center">
+ {{ user.nickname }}
+ <!-- 信息:时间 -->
+ v-if="activity.status !== TaskStatusEnum.NOT_START"
+ class="text-#a5a5a5 text-13px mt-1"
+ {{ getApprovalNodeTime(activity) }}
+ <!-- <div class="color-#a1a6ae text-12px mb-10px"> {{ activity.assigneeUser.nickname }}</div>
<div v-if="activity.opinion" class="text-#a5a5a5 text-12px w-100%">
<div class="mb-5px">审批意见:</div>
@@ -14,148 +116,118 @@
<div v-if="activity.createTime" class="text-#a5a5a5 text-13px">
{{ formatDate(activity.createTime) }}
- <!-- 该节点用户的头像 -->
- <template #dot>
- <div class="w-35px h-35px position-relative">
- <img
- src="@/assets/imgs/avatar.jpg"
- class="rounded-full w-full h-full position-absolute bottom-6px right-12px"
- alt=""
- <div
- class="position-absolute top-16px left-8px bg-#fff rounded-full flex items-center content-center p-2px"
- <Icon
- :size="12"
- :icon="optIconMap[activity.status]?.icon"
- :color="optIconMap[activity.status]?.color"
</el-timeline-item>
</el-timeline>
import { formatDate } from '@/utils/formatTime'
-import { propTypes } from '@/utils/propTypes'
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+import { TaskStatusEnum } from '@/api/bpm/task'
+import { Check, Close, Loading, Clock, Minus, Delete } from '@element-plus/icons-vue'
defineOptions({ name: 'BpmProcessInstanceTimeline' })
-defineProps({
- tasks: propTypes.array // 流程任务的数组
+ // 流程实例编号
+ processInstanceId: {
+ required: false,
+ default: ''
+ // 流程定义编号
+ processDefinitionId: {
-const optIconMap = {
+// 审批节点
+const approveNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([])
+const statusIconMap2 = {
+ // 未开始
+ '-1': { color: '#e5e7ec', icon: 'ep-clock' },
+ // 待审批
+ '0': { color: '#e5e7ec', icon: 'ep:loading' },
// 审批中
- '1': {
- color: '#00b32a',
- icon: 'fa-solid:clock'
+ '1': { color: '#448ef7', icon: 'ep:loading' },
// 审批通过
- '2': { color: '#00b32a', icon: 'fa-solid:check-circle' },
+ '2': { color: '#00b32a', icon: 'ep:circle-check-filled' },
// 审批不通过
- '3': { color: '#f46b6c', icon: 'fa-solid:times-circle' }
+ '3': { color: '#f46b6c', icon: 'fa-solid:times-circle' },
+ // 取消
+ '4': { color: '#cccccc', icon: 'ep:delete-filled' },
+ // 回退
+ '5': { color: '#f46b6c', icon: 'ep:remove-filled' },
+ // 委派中
+ '6': { color: '#448ef7', icon: 'ep:loading' },
+ // 审批通过中
+ '7': { color: '#00b32a', icon: 'ep:circle-check-filled' }
-const mockData: any = [
- id: 'fe1190ee-68c3-11ef-9c7d-00a6181404fd',
- name: '发起人',
- createTime: 1725237646192,
- endTime: null,
- durationInMillis: null,
- status: 1,
- reason: null,
- ownerUser: null,
- assigneeUser: {
- id: 104,
- nickname: '测试号',
- deptId: 107,
- deptName: '运维部门'
- taskDefinitionKey: 'task-01',
- processInstanceId: 'fe0c60c6-68c3-11ef-9c7d-00a6181404fd',
- processInstance: {
- id: 'fe0c60c6-68c3-11ef-9c7d-00a6181404fd',
- name: 'oa_leave',
- createTime: null,
- processDefinitionId: 'oa_leave:1:6e5ac269-5f87-11ef-bdb6-00a6181404fd',
- startUser: null
- parentTaskId: null,
- children: null,
- formId: null,
- formName: null,
- formConf: null,
- formFields: null,
- formVariables: null
- name: '领导审批',
- status: 2,
- nickname: '领导',
- name: '财务总监审核',
- status: 3,
- nickname: '财务总监',
+const statusIconMap = {
+ // 审批未开始
+ '-1': { color: '#e5e7ec', icon: Clock },
+ '0': { color: '#e5e7ec', icon: Clock },
+ // 审批中
+ '1': { color: '#448ef7', icon: Loading },
+ // 审批通过
+ '2': { color: '#00b32a', icon: Check },
+ // 审批不通过
+ '3': { color: '#f46b6c', icon: Close },
+ // 已取消
+ '4': { color: '#cccccc', icon: Delete },
+ '5': { color: '#f46b6c', icon: Minus },
+ '6': { color: '#448ef7', icon: Loading },
+ '7': { color: '#00b32a', icon: Check }
+/** 获得审批详情 */
+const getApprovalDetail = async () => {
+ const data = await ProcessInstanceApi.getApprovalDetail(
+ props.processInstanceId,
+ props.processDefinitionId
+ approveNodes.value = data.approveNodes
+const getApprovalNodeIcon = (taskStatus: number, nodeType: NodeType) => {
+ if (taskStatus == TaskStatusEnum.NOT_START) {
+ return statusIconMap[taskStatus]?.icon
+ if (nodeType === NodeType.START_USER_NODE || nodeType === NodeType.USER_TASK_NODE) {
+const getApprovalNodeColor = (taskStatus: number) => {
+ return statusIconMap[taskStatus]?.color
+const getApprovalNodeTime = (node: ProcessInstanceApi.ApprovalNodeInfo) => {
+ if (node.endTime) {
+ return `结束时间:${formatDate(node.endTime)}`
+ if (node.startTime) {
+ return `创建时间:${formatDate(node.startTime)}`
+/** 重新刷新审批详情 */
+const refresh = () => {
+ getApprovalDetail()
+defineExpose({ refresh })
+ await getApprovalDetail()
@@ -56,29 +56,73 @@
<div style="margin-bottom: 20px; margin-left: 10%; font-size: 14px">
- <el-button type="success" @click="handleAudit(item, true)">
+ <!-- TODO @jason:建议搞个 if 来判断,替代现有的 !item.buttonsSetting || item.buttonsSetting[OpsButtonType.APPROVE]?.enable -->
+ <el-button
+ type="success"
+ v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.APPROVE]?.enable"
+ @click="handleAudit(item, true)"
<Icon icon="ep:select" />
+ <!-- TODO @jason:这个也是类似哈,搞个方法来生成名字 -->
+ {{
+ item.buttonsSetting?.[OperationButtonType.APPROVE]?.displayName ||
+ OPERATION_BUTTON_NAME.get(OperationButtonType.APPROVE)
+ }}
- <el-button type="danger" @click="handleAudit(item, false)">
+ v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.REJECT]?.enable"
+ @click="handleAudit(item, false)"
<Icon icon="ep:close" />
- 不通过
+ item.buttonsSetting?.[OperationButtonType.REJECT].displayName ||
+ OPERATION_BUTTON_NAME.get(OperationButtonType.REJECT)
- <el-button type="primary" @click="openTaskUpdateAssigneeForm(item.id)">
+ v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.TRANSFER]?.enable"
+ type="primary"
+ @click="openTaskUpdateAssigneeForm(item.id)"
<Icon icon="ep:edit" />
- 转办
+ item.buttonsSetting?.[OperationButtonType.TRANSFER]?.displayName ||
+ OPERATION_BUTTON_NAME.get(OperationButtonType.TRANSFER)
- <el-button type="primary" @click="handleDelegate(item)">
+ v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.DELEGATE]?.enable"
+ @click="handleDelegate(item)"
<Icon icon="ep:position" />
- 委派
+ item.buttonsSetting?.[OperationButtonType.DELEGATE]?.displayName ||
+ OPERATION_BUTTON_NAME.get(OperationButtonType.DELEGATE)
- <el-button type="primary" @click="handleSign(item)">
+ v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.ADD_SIGN]?.enable"
+ @click="handleSign(item)"
<Icon icon="ep:plus" />
- 加签
+ item.buttonsSetting?.[OperationButtonType.ADD_SIGN]?.displayName ||
+ OPERATION_BUTTON_NAME.get(OperationButtonType.ADD_SIGN)
- <el-button type="warning" @click="handleBack(item)">
+ v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.RETURN]?.enable"
+ @click="handleBack(item)"
<Icon icon="ep:back" />
- 回退
+ item.buttonsSetting?.[OperationButtonType.RETURN]?.displayName ||
+ OPERATION_BUTTON_NAME.get(OperationButtonType.RETURN)
</el-col>
@@ -147,6 +191,10 @@ import TaskSignCreateForm from './dialog/TaskSignCreateForm.vue'
import { registerComponent } from '@/utils/routerHelper'
import * as UserApi from '@/api/system/user'
defineOptions({ name: 'BpmProcessInstanceDetail' })
@@ -200,8 +248,14 @@ const handleAudit = async (task, pass) => {
// 1.2 校验表单
const elForm = unref(auditFormRef)
if (!elForm) return
- const valid = await elForm.validate()
+ let valid = await elForm.validate()
if (!valid) return
+ // 校验申请表单(可编辑字段)
+ // TODO @jason:之前这里是 if (!fApi.value) return;针对业务表单的情况下,会导致没办法审核,可能要看下。我这里改了点,看看是不是还有别的地方兼容性
+ if (fApi.value) {
+ valid = await fApi.value.validate()
+ if (!valid) return
// 2.1 提交审批
@@ -216,6 +270,11 @@ const handleAudit = async (task, pass) => {
await formCreateApi.validate()
data.variables = approveForms.value[index].value
+ // 获取表单可编辑字段的值
+ data.variables = getWritableValueOfForm(task.fieldsPermission)
await TaskApi.approveTask(data)
message.success('审批通过成功')
@@ -251,11 +310,11 @@ const handleSign = async (task: any) => {
/** 获得详情 */
-const getDetail = () => {
- // 1. 获得流程实例相关
+const getDetail = async () => {
+ // 1. 获得流程任务列表(审批记录)。 需要先获取任务,表单的权限设置需要根据任务来设置
+ await getTaskList()
+ // 2. 获得流程实例相关
getProcessInstance()
- // 2. 获得流程任务列表(审批记录)
- getTaskList()
/** 加载流程实例 */
@@ -273,16 +332,29 @@ const getProcessInstance = async () => {
// 设置表单信息
const processDefinition = data.processDefinition
if (processDefinition.formType === 10) {
- setConfAndFields2(
- detailForm,
- processDefinition.formConf,
- processDefinition.formFields,
- data.formVariables
- )
+ if (detailForm.value.rule.length > 0) {
+ detailForm.value.value = data.formVariables
+ setConfAndFields2(
+ detailForm,
+ processDefinition.formConf,
+ processDefinition.formFields,
+ data.formVariables
nextTick().then(() => {
fApi.value?.btn.show(false)
fApi.value?.resetBtn.show(false)
fApi.value?.disabled(true)
+ // 设置表单权限。后续需要改造成。只处理一个运行中的任务
+ if (runningTasks.value.length > 0) {
+ const task = runningTasks.value.at(0)
+ if (task.fieldsPermission) {
+ Object.keys(task.fieldsPermission).forEach((item) => {
+ setFieldPermission(item, task.fieldsPermission[item])
// 注意:data.processDefinition.formCustomViewPath 是组件的全路径,例如说:/crm/contract/detail/index.vue
@@ -353,6 +425,7 @@ const loadRunningTask = (tasks) => {
if (!task.assigneeUser || task.assigneeUser.id !== userId) {
return
// 2.3 添加到处理任务
runningTasks.value.push({ ...task })
auditForms.value.push({
@@ -371,6 +444,35 @@ const loadRunningTask = (tasks) => {
+ * 设置表单权限
+const setFieldPermission = (field: string, permission: string) => {
+ if (permission === '1') {
+ fApi.value?.disabled(true, field)
+ if (permission === '2') {
+ fApi.value?.disabled(false, field)
+ if (permission === '3') {
+ fApi.value?.hidden(true, field)
+ * 获取可以编辑字段的值
+const getWritableValueOfForm = (fieldsPermission: Object) => {
+ const fieldsValue = {}
+ if (fieldsPermission && fApi.value) {
+ Object.keys(fieldsPermission).forEach((item) => {
+ if (fieldsPermission[item] === '2') {
+ fieldsValue[item] = fApi.value.getValue(item)
+ return fieldsValue
/** 初始化 */
const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
onMounted(async () => {
@@ -1,88 +1,106 @@
<ContentWrap :bodyStyle="{ padding: '10px 20px' }" class="position-relative">
- class="position-absolute right-20px"
- width="150"
- :src="auditIcons[processInstance.status]"
- <div class="text-#878c93">编号:{{ id }}</div>
- <el-divider class="!my-8px" />
- <div class="flex items-center gap-5 mb-10px">
- <div class="text-26px font-bold mb-5px">{{ processInstance.name }}</div>
- <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="processInstance.status" />
+ <div class="processInstance-wrap-main">
+ <el-scrollbar>
+ <img
+ class="position-absolute right-20px"
+ width="150"
+ :src="auditIcons[processInstance.status]"
+ alt=""
+ <div class="text-#878c93 h-15px">编号:{{ id }}</div>
+ <el-divider class="!my-8px" />
+ <div class="flex items-center gap-5 mb-10px h-40px">
+ <div class="text-26px font-bold mb-5px">{{ processInstance.name }}</div>
+ <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="processInstance.status" />
- <div class="flex items-center gap-5 mb-10px text-13px">
- <div class="bg-gray-100 h-35px rounded-3xl flex items-center p-8px gap-2 dark:color-gray-600">
- <img class="rounded-full h-28px" src="@/assets/imgs/avatar.jpg" alt="" />
- {{ processInstance?.startUser?.nickname }}
- <div class="text-#878c93"> {{ formatDate(processInstance.startTime) }} 提交 </div>
+ <div class="flex items-center gap-5 mb-10px text-13px h-35px">
+ class="bg-gray-100 h-35px rounded-3xl flex items-center p-8px gap-2 dark:color-gray-600"
+ <img class="rounded-full h-28px" src="@/assets/imgs/avatar.jpg" alt="" />
+ {{ processInstance?.startUser?.nickname }}
+ <div class="text-#878c93"> {{ formatDate(processInstance.startTime) }} 提交 </div>
- <el-tabs>
- <!-- 表单信息 -->
- <el-tab-pane label="表单信息">
- <el-row :gutter="10">
- <el-col :span="18" class="!flex !flex-col formCol">
- <div v-loading="processInstanceLoading" class="form-box flex flex-col mb-30px flex-1">
- <!-- 情况一:流程表单 -->
- <el-col
- v-if="processInstance?.processDefinition?.formType === 10"
- :offset="6"
- :span="16"
- <form-create
- v-model="detailForm.value"
- v-model:api="fApi"
- :option="detailForm.option"
- :rule="detailForm.rule"
- </el-col>
- <!-- 情况二:业务表单 -->
- <div v-if="processInstance?.processDefinition?.formType === 20">
- <BusinessFormComponent :id="processInstance.businessKey" />
+ <el-tabs v-model="activeTab">
+ <!-- 表单信息 -->
+ <el-tab-pane label="审批详情" name="form">
+ <div class="form-scroll-area">
+ <el-row :gutter="10">
+ <el-col :span="18" class="!flex !flex-col formCol">
+ v-loading="processInstanceLoading"
+ class="form-box flex flex-col mb-30px flex-1"
+ <!-- 情况一:流程表单 -->
+ <el-col
+ v-if="processInstance?.processDefinition?.formType === 10"
+ :offset="6"
+ :span="16"
+ <form-create
+ v-model="detailForm.value"
+ v-model:api="fApi"
+ :option="detailForm.option"
+ :rule="detailForm.rule"
+ </el-col>
+ <!-- 情况二:业务表单 -->
+ <div v-if="processInstance?.processDefinition?.formType === 20">
+ <BusinessFormComponent :id="processInstance.businessKey" />
+ <el-col :span="6">
+ <!-- 审批记录时间线 -->
+ <ProcessInstanceTimeline ref="timelineRef" :process-instance-id="id" />
+ </el-row>
+ </el-scrollbar>
- <!-- 操作栏按钮 -->
- <ProcessInstanceOperationButton
- ref="operationButtonRef"
- :processInstance="processInstance"
- :userOptions="userOptions"
- @success="getDetail"
+ <!-- 流程图 -->
+ <el-tab-pane label="流程图" name="diagram">
+ <ProcessInstanceBpmnViewer
+ :id="`${id}`"
+ :bpmn-xml="bpmnXml"
+ :loading="processInstanceLoading"
+ :process-instance="processInstance"
+ :tasks="tasks"
- <el-col :span="6">
- <!-- 审批记录时间线 -->
- <ProcessInstanceTimeline :process-instance="processInstance" :tasks="tasks" />
- </el-row>
- </el-tab-pane>
- <!-- 流程图 -->
- <el-tab-pane label="流程图">
- <ProcessInstanceBpmnViewer
- :id="`${id}`"
- :bpmn-xml="bpmnXml"
- :loading="processInstanceLoading"
- :process-instance="processInstance"
- :tasks="tasks"
- <!-- 流转记录 -->
- <el-tab-pane label="流转记录">
- <ProcessInstanceTaskList
- :loading="tasksLoad"
- @refresh="getTaskList"
- <!-- 流转评论 -->
- <el-tab-pane label="流转评论"> 流转评论 </el-tab-pane>
- </el-tabs>
+ <!-- 流转记录 -->
+ <el-tab-pane label="流转记录" name="record">
+ <ProcessInstanceTaskList
+ :loading="tasksLoad"
+ @refresh="getTaskList"
+ <!-- 流转评论 TODO 待开发 -->
+ <el-tab-pane label="流转评论" name="comment"> 流转评论 </el-tab-pane>
+ class="b-t-solid border-t-1px border-[var(--el-border-color)]"
+ v-if="activeTab === 'form'"
+ <!-- 操作栏按钮 -->
+ <ProcessInstanceOperationButton
+ ref="operationButtonRef"
+ :processInstance="processInstance"
+ :userOptions="userOptions"
+ @success="refresh"
@@ -99,18 +117,22 @@ import ProcessInstanceOperationButton from './ProcessInstanceOperationButton.vue
import ProcessInstanceTimeline from './ProcessInstanceTimeline.vue'
+import { FieldPermissionType } from '@/components/SimpleProcessDesignerV2/src/consts'
import audit1 from '@/assets/svgs/bpm/audit1.svg'
import audit2 from '@/assets/svgs/bpm/audit2.svg'
import audit3 from '@/assets/svgs/bpm/audit3.svg'
-const { query } = useRoute() // 查询参数
+const props = defineProps<{
+ id: string // 流程实例的编号
+ taskId?: string // 任务编号
+ activityId?: string //流程活动编号,用于抄送查看
-const id = query.id as unknown as string // 流程实例的编号
const processInstanceLoading = ref(false) // 流程实例的加载中
const processInstance = ref<any>({}) // 流程实例
const operationButtonRef = ref()
+const timelineRef = ref()
const bpmnXml = ref('') // BPMN XML
const tasksLoad = ref(true) // 任务的加载中
const tasks = ref<any[]>([]) // 任务列表
@@ -141,7 +163,7 @@ const BusinessFormComponent = ref<any>(null) // 异步组件
const getProcessInstance = async () => {
processInstanceLoading.value = true
- const data = await ProcessInstanceApi.getProcessInstance(id)
+ const data = await ProcessInstanceApi.getProcessInstance(props.id)
if (!data) {
message.error('查询不到流程信息!')
@@ -151,6 +173,15 @@ const getProcessInstance = async () => {
+ // 获取表单字段权限
+ let fieldsPermission = undefined
+ if (props.taskId || props.activityId) {
+ fieldsPermission = await ProcessInstanceApi.getFormFieldsPermission({
+ processInstanceId: props.id,
+ taskId: props.taskId,
+ activityId: props.activityId
setConfAndFields2(
detailForm,
processDefinition.formConf,
@@ -161,6 +192,11 @@ const getProcessInstance = async () => {
+ if (fieldsPermission) {
+ setFieldPermission(item, fieldsPermission[item])
@@ -174,15 +210,30 @@ const getProcessInstance = async () => {
+ if (permission === FieldPermissionType.READ) {
+ if (permission === FieldPermissionType.WRITE) {
+ if (permission === FieldPermissionType.NONE) {
/** 加载任务列表 */
const getTaskList = async () => {
// 获得未取消的任务
tasksLoad.value = true
- const data = await TaskApi.getTaskListByProcessInstanceId(id)
+ const data = await TaskApi.getTaskListByProcessInstanceId(props.id)
tasks.value = []
// 1.1 移除已取消的审批
- data.forEach((task) => {
+ data.forEach((task: any) => {
if (task.status !== 4) {
tasks.value.push(task)
@@ -209,6 +260,19 @@ const getTaskList = async () => {
+ * 操作成功后刷新
+ // 重新获取详情
+ getDetail()
+ // 刷新审批详情 Timeline
+ timelineRef.value?.refresh()
+/** 当前的Tab */
+const activeTab = ref('form')
@@ -219,6 +283,33 @@ onMounted(async () => {
<style lang="scss" scoped>
+$wrap-padding-height: 30px;
+$wrap-margin-height: 15px;
+$button-height: 51px;
+$process-header-height: 194px;
+.processInstance-wrap-main {
+ height: calc(
+ 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 45px
+ );
+ max-height: calc(
+ .form-scroll-area {
+ 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 45px -
+ $process-header-height - 40px
.form-box {
:deep(.el-card) {
border: none;
@@ -19,10 +19,10 @@
class="!w-240px"
- <el-form-item label="所属流程" prop="processDefinitionId">
+ <el-form-item label="所属流程" prop="processDefinitionKey">
<el-input
- v-model="queryParams.processDefinitionId"
- placeholder="请输入流程定义的编号"
+ v-model="queryParams.processDefinitionKey"
+ placeholder="请输入流程定义的标识"
@keyup.enter="handleQuery"
@@ -183,7 +183,7 @@ const queryParams = reactive({
pageNo: 1,
pageSize: 10,
- processDefinitionId: undefined,
+ processDefinitionKey: undefined,
status: undefined,
createTime: []
@@ -79,6 +79,10 @@
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
@@ -1,28 +1,13 @@
- <section class="dingflow-design">
- <div class="box-scale">
- <nodeWrap v-model:nodeConfig="nodeConfig" />
- <div class="end-node">
- <div class="end-node-circle"></div>
- <div class="end-node-text">流程结束</div>
- </section>
+ <SimpleProcessDesigner :model-id="modelId" />
-import nodeWrap from '@/components/SimpleProcessDesigner/src/nodeWrap.vue'
-defineOptions({ name: 'SimpleWorkflowDesignEditor' })
-let nodeConfig = ref({
- nodeName: '发起人',
- type: 0,
- id: 'root',
- formPerms: {},
- childNode: {}
+import { SimpleProcessDesigner } from '@/components/SimpleProcessDesignerV2/src/'
+ name: 'SimpleWorkflowDesignEditor'
+const { query } = useRoute() // 路由的查询
+const modelId = query.modelId as string
-<style>
-@import url('@/components/SimpleProcessDesigner/theme/workflow.css');
@@ -111,11 +111,16 @@ const getList = async () => {
/** 处理审批按钮 */
const handleAudit = (row: any) => {
+ const query = {
+ id: row.processInstanceId,
+ activityId: undefined
+ if (row.activityId) {
+ query.activityId = row.activityId
push({
- id: row.processInstanceId
+ query: query
@@ -158,7 +158,8 @@ const handleAudit = (row: any) => {
query: {
- id: row.processInstance.id
+ id: row.processInstance.id,
+ taskId: row.id
@@ -140,7 +140,8 @@ const handleAudit = (row: any) => {
@@ -1,16 +1,17 @@
- <el-row>
- <el-col>
- <div class="float-right mb-2">
- <el-button size="small" type="primary" @click="showJson">生成 JSON</el-button>
- <el-button size="small" type="success" @click="showOption">生成 Options</el-button>
- <el-button size="small" type="danger" @click="showTemplate">生成组件</el-button>
- <FcDesigner ref="designer" height="780px" />
+ <el-button size="small" type="primary" plain @click="showJson">生成JSON</el-button>
+ <el-button size="small" type="success" plain @click="showOption">生成Options</el-button>
+ <el-button size="small" type="danger" plain @click="showTemplate">生成组件</el-button>
<!-- 弹窗:表单预览 -->
@@ -43,6 +44,31 @@ defineOptions({ name: 'InfraBuild' })
const message = useMessage() // 消息
@@ -140,3 +166,13 @@ onMounted(async () => {
hljs.registerLanguage('json', json)
@@ -71,7 +71,7 @@
import { useWebSocket } from '@vueuse/core'
-import { getAccessToken } from '@/utils/auth'
+import { getRefreshToken } from '@/utils/auth'
defineOptions({ name: 'InfraWebSocket' })
@@ -79,7 +79,9 @@ defineOptions({ name: 'InfraWebSocket' })
const server = ref(
- (import.meta.env.VITE_BASE_URL + '/infra/ws').replace('http', 'ws') + '?token=' + getAccessToken()
+ (import.meta.env.VITE_BASE_URL + '/infra/ws').replace('http', 'ws') +
+ '?token=' +
+ getRefreshToken() // 使用 getRefreshToken() 方法,而不使用 getAccessToken() 方法的原因:WebSocket 无法方便的刷新访问令牌
) // WebSocket 服务地址
const getIsOpen = computed(() => status.value === 'OPEN') // WebSocket 连接是否打开
const getTagColor = computed(() => (getIsOpen.value ? 'success' : 'red')) // WebSocket 连接的展示颜色
@@ -0,0 +1,156 @@
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ :model="formData"
+ label-width="100px"
+ v-loading="formLoading"
+ <el-form-item label="产品" prop="productId">
+ v-model="formData.productId"
+ placeholder="请选择产品"
+ :disabled="formType === 'update'"
+ v-for="product in products"
+ :key="product.id"
+ :label="product.name"
+ :value="product.id"
+ <el-form-item label="DeviceName" prop="deviceName">
+ v-model="formData.deviceName"
+ placeholder="请输入 DeviceName"
+ <el-form-item label="备注名称" prop="nickname">
+ <el-input v-model="formData.nickname" placeholder="请输入备注名称" />
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+ <el-button @click="dialogVisible = false">取 消</el-button>
+import { DeviceApi, DeviceVO } from '@/api/iot/device'
+import { ProductApi } from '@/api/iot/product'
+/** IoT 设备 表单 */
+defineOptions({ name: 'IoTDeviceForm' })
+const { t } = useI18n() // 国际化
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+ productId: undefined,
+ deviceName: undefined,
+ nickname: undefined
+ productId: [{ required: true, message: '产品不能为空', trigger: 'blur' }],
+ deviceName: [
+ pattern: /^[a-zA-Z0-9_.\-:@]{4,32}$/,
+ message:
+ '支持英文字母、数字、下划线(_)、中划线(-)、点号(.)、半角冒号(:)和特殊字符@,长度限制为 4~32 个字符',
+ trigger: 'blur'
+ ],
+ nickname: [
+ validator: (rule, value, callback) => {
+ if (value === undefined || value === null) {
+ callback()
+ const length = value.replace(/[\u4e00-\u9fa5\u3040-\u30ff]/g, 'aa').length
+ if (length < 4 || length > 64) {
+ callback(new Error('备注名称长度限制为 4~64 个字符,中文及日文算 2 个字符'))
+ } else if (!/^[\u4e00-\u9fa5\u3040-\u30ff_a-zA-Z0-9]+$/.test(value)) {
+ callback(new Error('备注名称只能包含中文、英文字母、日文、数字和下划线(_)'))
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 修改时,设置数据
+ if (id) {
+ formLoading.value = true
+ formData.value = await DeviceApi.getDevice(id)
+ formLoading.value = false
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+ await formRef.value.validate()
+ // 提交请求
+ const data = formData.value as unknown as DeviceVO
+ if (formType.value === 'create') {
+ await DeviceApi.createDevice(data)
+ message.success(t('common.createSuccess'))
+ await DeviceApi.updateDevice(data)
+ message.success(t('common.updateSuccess'))
+ dialogVisible.value = false
+ // 发送操作成功的事件
+ emit('success')
+/** 重置表单 */
+const resetForm = () => {
+ formData.value = {
+ formRef.value?.resetFields()
+/** 查询字典下拉列表 */
+const products = ref()
+const getProducts = async () => {
+ products.value = await ProductApi.getSimpleProductList()
+onMounted(() => {
+ getProducts()
@@ -0,0 +1,76 @@
+ <div class="flex items-start justify-between">
+ <el-col>
+ <el-row>
+ <span class="text-xl font-bold">{{ device.deviceName }}</span>
+ <!-- 右上:按钮 -->
+ @click="openForm('update', device.id)"
+ v-hasPermi="['iot:device:update']"
+ v-if="product.status === 0"
+ 编辑
+ <ContentWrap class="mt-10px">
+ <el-descriptions :column="5" direction="horizontal">
+ <el-descriptions-item label="产品">
+ <el-link @click="goToProductDetail(product.id)">{{ product.name }}</el-link>
+ </el-descriptions-item>
+ <el-descriptions-item label="ProductKey">
+ {{ product.productKey }}
+ <el-button @click="copyToClipboard(product.productKey)">复制</el-button>
+ </el-descriptions>
+ </ContentWrap>
+ <!-- 表单弹窗:添加/修改 -->
+ <DeviceForm ref="formRef" @success="emit('refresh')" />
+import { ref } from 'vue'
+import DeviceForm from '@/views/iot/device/DeviceForm.vue'
+import { ProductVO } from '@/api/iot/product'
+import { DeviceVO } from '@/api/iot/device'
+import { useRouter } from 'vue-router'
+const message = useMessage()
+const router = useRouter()
+// 操作修改
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+const { product, device } = defineProps<{ product: ProductVO; device: DeviceVO }>()
+const emit = defineEmits(['refresh'])
+ * 将文本复制到剪贴板
+ *
+ * @param text 需要复制的文本
+const copyToClipboard = (text: string) => {
+ // TODO @haohao:可以考虑用 await 异步转同步哈
+ navigator.clipboard.writeText(text).then(() => {
+ message.success('复制成功')
+ * 跳转到产品详情页面
+ * @param productId 产品 ID
+const goToProductDetail = (productId: number) => {
+ router.push({ name: 'IoTProductDetail', params: { id: productId } })
@@ -0,0 +1,123 @@
+ <ContentWrap>
+ <el-collapse v-model="activeNames">
+ <el-descriptions :column="3" title="设备信息">
+ <el-descriptions-item label="产品名称">{{ product.name }}</el-descriptions-item>
+ <el-descriptions-item label="设备类型">
+ <dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="product.deviceType" />
+ <el-descriptions-item label="DeviceName">
+ {{ device.deviceName }}
+ <el-button @click="copyToClipboard(device.deviceName)">复制</el-button>
+ <el-descriptions-item label="备注名称">{{ device.nickname }}</el-descriptions-item>
+ <el-descriptions-item label="创建时间">
+ {{ formatDate(device.createTime) }}
+ <el-descriptions-item label="激活时间">
+ {{ formatDate(device.activeTime) }}
+ <el-descriptions-item label="最后上线时间">
+ {{ formatDate(device.lastOnlineTime) }}
+ <el-descriptions-item label="当前状态">
+ <dict-tag :type="DICT_TYPE.IOT_DEVICE_STATUS" :value="device.status" />
+ <el-descriptions-item label="最后离线时间" :span="3">
+ {{ formatDate(device.lastOfflineTime) }}
+ <el-descriptions-item label="MQTT 连接参数">
+ <el-button type="primary" @click="openMqttParams">查看</el-button>
+ </el-collapse>
+ <!-- MQTT 连接参数弹框 -->
+ <Dialog
+ title="MQTT 连接参数"
+ v-model="mqttDialogVisible"
+ width="50%"
+ :before-close="handleCloseMqttDialog"
+ <el-form :model="mqttParams" label-width="120px">
+ <el-form-item label="clientId">
+ <el-input v-model="mqttParams.mqttClientId" readonly>
+ <template #append>
+ <el-button @click="copyToClipboard(mqttParams.mqttClientId)" type="primary">
+ <Icon icon="ph:copy" />
+ </el-input>
+ <el-form-item label="username">
+ <el-input v-model="mqttParams.mqttUsername" readonly>
+ <el-button @click="copyToClipboard(mqttParams.mqttUsername)" type="primary">
+ <el-form-item label="passwd">
+ <el-input v-model="mqttParams.mqttPassword" readonly type="password">
+ <el-button @click="copyToClipboard(mqttParams.mqttPassword)" type="primary">
+ <el-button @click="mqttDialogVisible = false">关闭</el-button>
+import { DICT_TYPE } from '@/utils/dict'
+const message = useMessage() // 消息提示
+const { product, device } = defineProps<{ product: ProductVO; device: DeviceVO }>() // 定义 Props
+const emit = defineEmits(['refresh']) // 定义 Emits
+const activeNames = ref(['basicInfo']) // 展示的折叠面板
+const mqttDialogVisible = ref(false) // 定义 MQTT 弹框的可见性
+const mqttParams = ref({
+ mqttClientId: '',
+ mqttUsername: '',
+ mqttPassword: ''
+}) // 定义 MQTT 参数对象
+/** 复制到剪贴板方法 */
+/** 打开 MQTT 参数弹框的方法 */
+const openMqttParams = () => {
+ mqttParams.value = {
+ mqttClientId: device.mqttClientId || 'N/A',
+ mqttUsername: device.mqttUsername || 'N/A',
+ mqttPassword: device.mqttPassword || 'N/A'
+ mqttDialogVisible.value = true
+/** 关闭 MQTT 弹框的方法 */
+const handleCloseMqttDialog = () => {
+ mqttDialogVisible.value = false
@@ -0,0 +1,66 @@
+ <DeviceDetailsHeader
+ :loading="loading"
+ :product="product"
+ :device="device"
+ @refresh="getDeviceData(id)"
+ <el-tabs>
+ <el-tab-pane label="设备信息">
+ <DeviceDetailsInfo :product="product" :device="device" />
+ <el-tab-pane label="Topic 列表" />
+ <el-tab-pane label="物模型数据" />
+ <el-tab-pane label="子设备管理" />
+<script lang="ts" setup>
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import { ProductApi, ProductVO } from '@/api/iot/product'
+import DeviceDetailsHeader from '@/views/iot/device/detail/DeviceDetailsHeader.vue'
+import DeviceDetailsInfo from '@/views/iot/device/detail/DeviceDetailsInfo.vue'
+defineOptions({ name: 'IoTDeviceDetail' })
+const route = useRoute()
+const id = Number(route.params.id) // 编号
+const loading = ref(true) // 加载中
+const product = ref<ProductVO>({} as ProductVO) // 产品详情
+const device = ref<DeviceVO>({} as DeviceVO) // 设备详情
+/** 获取设备详情 */
+const getDeviceData = async (id: number) => {
+ device.value = await DeviceApi.getDevice(id)
+ console.log(product.value)
+ await getProductData(device.value.productId)
+/** 获取产品详情 */
+const getProductData = async (id: number) => {
+ product.value = await ProductApi.getProduct(id)
+/** 获取物模型 */
+/** 初始化 */
+const { delView } = useTagsViewStore() // 视图操作
+const { currentRoute } = useRouter() // 路由
+ if (!id) {
+ message.warning('参数错误,产品不能为空!')
+ delView(unref(currentRoute))
+ await getDeviceData(id)
@@ -0,0 +1,267 @@
+ <!-- 搜索工作栏 -->
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ v-model="queryParams.productId"
+ class="!w-240px"
+ v-model="queryParams.deviceName"
+ @keyup.enter="handleQuery"
+ v-model="queryParams.nickname"
+ placeholder="请输入备注名称"
+ <el-form-item label="设备类型" prop="deviceType">
+ v-model="queryParams.deviceType"
+ placeholder="请选择设备类型"
+ v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE)"
+ :label="dict.label"
+ <el-form-item label="设备状态" prop="status">
+ v-model="queryParams.status"
+ placeholder="请选择设备状态"
+ v-for="dict in getIntDictOptions(DICT_TYPE.IOT_DEVICE_STATUS)"
+ <el-button @click="handleQuery">
+ <Icon icon="ep:search" class="mr-5px" />
+ 搜索
+ <el-button @click="resetQuery">
+ <Icon icon="ep:refresh" class="mr-5px" />
+ 重置
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['iot:device:create']"
+ <Icon icon="ep:plus" class="mr-5px" />
+ 新增
+ <!-- 列表 -->
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="DeviceName" align="center" prop="deviceName">
+ <el-link @click="openDetail(scope.row.id)">{{ scope.row.deviceName }}</el-link>
+ </el-table-column>
+ <el-table-column label="备注名称" align="center" prop="nickname" />
+ <el-table-column label="设备所属产品" align="center" prop="productId">
+ {{ productMap[scope.row.productId] }}
+ <el-table-column label="设备类型" align="center" prop="deviceType">
+ <dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="scope.row.deviceType" />
+ <el-table-column label="设备状态" align="center" prop="status">
+ <dict-tag :type="DICT_TYPE.IOT_DEVICE_STATUS" :value="scope.row.status" />
+ <el-table-column
+ label="最后上线时间"
+ align="center"
+ prop="lastOnlineTime"
+ :formatter="dateFormatter"
+ width="180px"
+ <el-table-column label="操作" align="center" min-width="120px">
+ link
+ @click="openDetail(scope.row.id)"
+ v-hasPermi="['iot:product:query']"
+ 查看
+ @click="openForm('update', scope.row.id)"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['iot:device:delete']"
+ </el-table>
+ <!-- 分页 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ <DeviceForm ref="formRef" @success="getList" />
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import DeviceForm from './DeviceForm.vue'
+/** IoT 设备 列表 */
+defineOptions({ name: 'IoTDevice' })
+const loading = ref(true) // 列表的加载中
+const list = ref<DeviceVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ deviceType: undefined,
+ nickname: undefined,
+ status: undefined
+const queryFormRef = ref() // 搜索的表单
+/** 产品标号和名称的映射 */
+const productMap = reactive({})
+/** 查询列表 */
+const getList = async () => {
+ const data = await DeviceApi.getDevicePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ // 获取产品ID列表
+ const productIds = [...new Set(data.list.map((device) => device.productId))]
+ // 获取产品名称
+ // TODO @haohao:最好后端拼接哈
+ const products = await Promise.all(productIds.map((id) => ProductApi.getProduct(id)))
+ products.forEach((product) => {
+ productMap[product.id] = product.name
+/** 搜索按钮操作 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+/** 重置按钮操作 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+/** 添加/修改操作 */
+/** 打开详情 */
+const { push } = useRouter()
+const openDetail = (id: number) => {
+ push({ name: 'IoTDeviceDetail', params: { id } })
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+ // 删除的二次确认
+ await message.delConfirm()
+ // 发起删除
+ await DeviceApi.deleteDevice(id)
+ message.success(t('common.delSuccess'))
+ // 刷新列表
+ await getList()
+/** 初始化 **/
@@ -0,0 +1,204 @@
+ <el-form-item label="产品名称" prop="name">
+ <el-input v-model="formData.name" placeholder="请输入产品名称" />
+ v-model="formData.deviceType"
+ v-if="formData.deviceType === 0 || formData.deviceType === 2"
+ label="联网方式"
+ prop="netType"
+ v-model="formData.netType"
+ placeholder="请选择联网方式"
+ v-for="dict in getIntDictOptions(DICT_TYPE.IOT_NET_TYPE)"
+ <el-form-item v-if="formData.deviceType === 1" label="接入网关协议" prop="protocolType">
+ v-model="formData.protocolType"
+ placeholder="请选择接入网关协议"
+ v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PROTOCOL_TYPE)"
+ <el-form-item label="数据格式" prop="dataFormat">
+ v-model="formData.dataFormat"
+ placeholder="请选择接数据格式"
+ v-for="dict in getIntDictOptions(DICT_TYPE.IOT_DATA_FORMAT)"
+ <el-form-item label="数据校验级别" prop="validateType">
+ <el-radio-group v-model="formData.validateType" :disabled="formType === 'update'">
+ v-for="dict in getIntDictOptions(DICT_TYPE.IOT_VALIDATE_TYPE)"
+ <el-form-item label="产品描述" prop="description">
+ <el-input type="textarea" v-model="formData.description" placeholder="请输入产品描述" />
+defineOptions({ name: 'IoTProductForm' })
+const { t } = useI18n()
+const dialogVisible = ref(false)
+const dialogTitle = ref('')
+const formLoading = ref(false)
+const formType = ref('')
+ name: undefined,
+ productKey: undefined,
+ protocolId: undefined,
+ categoryId: undefined,
+ description: undefined,
+ validateType: undefined,
+ status: undefined,
+ netType: undefined,
+ protocolType: undefined,
+ dataFormat: undefined
+ name: [{ required: true, message: '产品名称不能为空', trigger: 'blur' }],
+ deviceType: [{ required: true, message: '设备类型不能为空', trigger: 'change' }],
+ netType: [
+ // TODO @haohao:0、1、/2 最好前端也枚举下;另外,这里的 required 可以直接设置为 true。然后表单那些 v-if。只要不存在,它自动就不校验了哈
+ required: formData.deviceType === 0 || formData.deviceType === 2,
+ message: '联网方式不能为空',
+ trigger: 'change'
+ protocolType: [
+ { required: formData.deviceType === 1, message: '接入网关协议不能为空', trigger: 'change' }
+ dataFormat: [{ required: true, message: '数据格式不能为空', trigger: 'change' }],
+ validateType: [{ required: true, message: '数据校验级别不能为空', trigger: 'change' }]
+ formData.value = await ProductApi.getProduct(id)
+defineExpose({ open, close: () => (dialogVisible.value = false) })
+const emit = defineEmits(['success'])
+ const data = formData.value as unknown as ProductVO
+ await ProductApi.createProduct(data)
+ await ProductApi.updateProduct(data)
+ dialogVisible.value = false // 确保关闭弹框
@@ -0,0 +1,103 @@
+ <span class="text-xl font-bold">{{ product.name }}</span>
+ @click="openForm('update', product.id)"
+ v-hasPermi="['iot:product:update']"
+ @click="confirmPublish(product.id)"
+ @click="confirmUnpublish(product.id)"
+ v-if="product.status === 1"
+ 撤销发布
+ <el-descriptions-item label="设备数">
+ {{ product.deviceCount }}
+ <el-button @click="goToManagement(product.id)">前往管理</el-button>
+ <ProductForm ref="formRef" @success="emit('refresh')" />
+import ProductForm from '@/views/iot/product/ProductForm.vue'
+const { product } = defineProps<{ product: ProductVO }>() // 定义 Props
+/** 处理复制 */
+/** 路由跳转到设备管理 */
+const goToManagement = (productId: string) => {
+ push({ name: 'IoTDevice', query: { productId } })
+/** 操作修改 */
+const confirmPublish = async (id: number) => {
+ await ProductApi.updateProductStatus(id, 1)
+ message.success('发布成功')
+ formRef.value.close() // 关闭弹框
+ emit('refresh')
+ } catch (error) {
+ message.error('发布失败')
+const confirmUnpublish = async (id: number) => {
+ await ProductApi.updateProductStatus(id, 0)
+ message.success('撤销发布成功')
+ message.error('撤销发布失败')
@@ -0,0 +1,44 @@
+ <el-descriptions :column="3" title="产品信息">
+ {{ formatDate(product.createTime) }}
+ <el-descriptions-item label="数据格式">
+ <dict-tag :type="DICT_TYPE.IOT_DATA_FORMAT" :value="product.dataFormat" />
+ <el-descriptions-item label="数据校验级别">
+ <dict-tag :type="DICT_TYPE.IOT_VALIDATE_TYPE" :value="product.validateType" />
+ <el-descriptions-item label="产品状态">
+ <dict-tag :type="DICT_TYPE.IOT_PRODUCT_STATUS" :value="product.status" />
+ <el-descriptions-item
+ v-if="product.deviceType === 0 || product.deviceType === 2"
+ <dict-tag :type="DICT_TYPE.IOT_NET_TYPE" :value="product.netType" />
+ <el-descriptions-item label="接入网关协议" v-if="product.deviceType === 1">
+ <dict-tag :type="DICT_TYPE.IOT_PROTOCOL_TYPE" :value="product.protocolType" />
+ <el-descriptions-item label="产品描述">{{ product.description }}</el-descriptions-item>
+const { product } = defineProps<{ product: ProductVO }>()
+// 展示的折叠面板
+const activeNames = ref(['basicInfo'])
@@ -0,0 +1,243 @@
+ <el-tab-pane label="基础通信 Topic">
+ <Table
+ :columns="columns1"
+ :data="data1"
+ :span-method="createSpanMethod(data1)"
+ align="left"
+ headerAlign="left"
+ border="true"
+ <el-tab-pane label="物模型通信 Topic">
+ :columns="columns2"
+ :data="data2"
+ :span-method="createSpanMethod(data2)"
+const props = defineProps<{ product: ProductVO }>()
+// 定义列
+const columns1 = reactive([
+ { label: '功能', field: 'function', width: 150 },
+ { label: 'Topic 类', field: 'topicClass', width: 800 },
+ { label: '操作权限', field: 'operationPermission', width: 100 },
+ { label: '描述', field: 'description' }
+])
+const columns2 = reactive([
+// TODO @haohao:这个,有没可能写到一个枚举里,方便后续维护? /Users/yunai/Java/yudao-ui-admin-vue3/src/views/ai/utils/constants.ts
+const data1 = computed(() => {
+ if (!props.product || !props.product.productKey) return []
+ return [
+ function: 'OTA 升级',
+ topicClass: `/ota/device/inform/${props.product.productKey}/\${deviceName}`,
+ operationPermission: '发布',
+ description: '设备上报固件升级信息'
+ topicClass: `/ota/device/upgrade/${props.product.productKey}/\${deviceName}`,
+ operationPermission: '订阅',
+ description: '固件升级信息下行'
+ topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/ota/firmware/get`,
+ description: '设备上报固件升级进度'
+ description: '设备主动拉取固件升级信息'
+ function: '设备标签',
+ topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/deviceinfo/update`,
+ description: '设备上报标签数据'
+ topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/deviceinfo/update_reply`,
+ description: '云端响应标签上报'
+ topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/deviceinfo/delete`,
+ description: '设备删除标签信息'
+ topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/deviceinfo/delete_reply`,
+ description: '云端响应标签删除'
+ function: '时钟同步',
+ topicClass: `/ext/ntp/${props.product.productKey}/\${deviceName}/request`,
+ description: 'NTP 时钟同步请求'
+ topicClass: `/ext/ntp/${props.product.productKey}/\${deviceName}/response`,
+ description: 'NTP 时钟同步响应'
+ function: '设备影子',
+ topicClass: `/shadow/update/${props.product.productKey}/\${deviceName}`,
+ description: '设备影子发布'
+ topicClass: `/shadow/get/${props.product.productKey}/\${deviceName}`,
+ description: '设备接收影子变更'
+ function: '配置更新',
+ topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/config/push`,
+ description: '云端主动下推配置信息'
+ topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/config/get`,
+ description: '设备端查询配置信息'
+ topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/config/get_reply`,
+ description: '云端响应配置信息'
+ function: '广播',
+ topicClass: `/broadcast/${props.product.productKey}/\${identifier}`,
+ description: '广播 Topic,identifier 为用户自定义字符串'
+const data2 = computed(() => {
+ function: '属性上报',
+ topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/event/property/post`,
+ description: '设备属性上报'
+ topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/event/property/post_reply`,
+ description: '云端响应属性上报'
+ function: '属性设置',
+ topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/service/property/set`,
+ description: '设备属性设置'
+ function: '事件上报',
+ topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/event/\${tsl.event.identifier}/post`,
+ description: '设备事件上报'
+ topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/event/\${tsl.event.identifier}/post_reply`,
+ description: '云端响应事件上报'
+ function: '服务调用',
+ topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/service/\${tsl.service.identifier}`,
+ description: '设备服务调用'
+ topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/service/\${tsl.service.identifier}_reply`,
+ description: '设备端响应服务调用'
+// 通用的单元格合并方法生成器
+const createSpanMethod = (data: any[]) => {
+ // 预处理,计算每个功能的合并行数
+ const rowspanMap: Record<number, number> = {}
+ let currentFunction = ''
+ let startIndex = 0
+ let count = 0
+ data.forEach((item, index) => {
+ if (item.function !== currentFunction) {
+ if (count > 0) {
+ rowspanMap[startIndex] = count
+ currentFunction = item.function
+ startIndex = index
+ count = 1
+ count++
+ // 处理最后一组
+ // 返回 span 方法
+ return ({ row, column, rowIndex, columnIndex }: SpanMethodProps) => {
+ if (columnIndex === 0) {
+ // 仅对“功能”列进行合并
+ const rowspan = rowspanMap[rowIndex] || 0
+ if (rowspan > 0) {
+ rowspan,
+ colspan: 1
+ rowspan: 0,
+ colspan: 0
@@ -0,0 +1,154 @@
+ <el-form-item label="功能类型" prop="name">
+ v-model="queryParams.type"
+ placeholder="请选择功能类型"
+ v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PRODUCT_FUNCTION_TYPE)"
+ v-hasPermi="['iot:think-model-function:create']"
+ <Icon icon="ep:plus" class="mr-5px" /> 添加功能
+ <el-table-column label="功能类型" align="center" prop="type">
+ <dict-tag :type="DICT_TYPE.IOT_PRODUCT_FUNCTION_TYPE" :value="scope.row.type" />
+ <el-table-column label="功能名称" align="center" prop="name" />
+ <el-table-column label="标识符" align="center" prop="identifier" />
+ <el-table-column label="操作" align="center">
+ v-hasPermi="[`iot:think-model-function:update`]"
+ v-hasPermi="['iot:think-model-function:delete']"
+ <ThinkModelFunctionForm ref="formRef" :product="product" @success="getList" />
+import { ThinkModelFunctionApi, ThinkModelFunctionVO } from '@/api/iot/thinkmodelfunction'
+import ThinkModelFunctionForm from '@/views/iot/product/detail/ThinkModelFunctionForm.vue'
+const list = ref<ThinkModelFunctionVO[]>([]) // 列表的数据
+ type: undefined,
+ productId: -1
+ queryParams.productId = props.product.id
+ const data = await ThinkModelFunctionApi.getThinkModelFunctionPage(queryParams)
+ queryParams.type = undefined
+ await ThinkModelFunctionApi.deleteThinkModelFunction(id)
@@ -0,0 +1,229 @@
+ <el-form-item label="功能类型" prop="type">
+ <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-form-item label="功能名称" prop="name">
+ <el-input v-model="formData.name" placeholder="请输入功能名称" />
+ <el-form-item label="标识符" prop="identifier">
+ v-model="formData.identifier"
+ placeholder="请输入标识符"
+ <el-form-item label="数据类型" prop="type">
+ v-model="formData.property.dataType.type"
+ placeholder="请选择数据类型"
+ <el-option key="int" label="int32 (整数型)" value="int" />
+ <el-option key="float" label="float (单精度浮点型)" value="float" />
+ <el-option key="double" label="double (双精度浮点型)" value="double" />
+ <!-- <el-option key="text" label="text (文本型)" value="text" />-->
+ <!-- <el-option key="date" label="date (日期型)" value="date" />-->
+ <!-- <el-option key="bool" label="bool (布尔型)" value="bool" />-->
+ <!-- <el-option key="enum" label="enum (枚举型)" value="enum" />-->
+ <!-- <el-option key="struct" label="struct (结构体)" value="struct" />-->
+ <!-- <el-option key="array" label="array (数组)" value="array" />-->
+ <el-form-item label="取值范围" prop="max">
+ <el-input v-model="formData.property.dataType.specs.min" placeholder="请输入最小值" />
+ <span class="mx-2">~</span>
+ <el-input v-model="formData.property.dataType.specs.max" placeholder="请输入最大值" />
+ <el-form-item label="步长" prop="step">
+ <el-input v-model="formData.property.dataType.specs.step" placeholder="请输入步长" />
+ <el-form-item label="单位" prop="unit">
+ <el-input v-model="formData.property.dataType.specs.unit" placeholder="请输入单位" />
+ <el-form-item label="读写类型" prop="accessMode">
+ <el-radio-group v-model="formData.property.accessMode">
+ <el-radio label="rw">读写</el-radio>
+ <el-radio label="r">只读</el-radio>
+ <el-form-item label="属性描述" prop="property.description">
+ v-model="formData.property.description"
+ placeholder="请输入属性描述"
+defineOptions({ name: 'ThinkModelFunctionForm' })
+ identifier: undefined,
+ type: '1',
+ property: {
+ accessMode: 'rw',
+ required: true,
+ dataType: {
+ specs: {
+ min: undefined,
+ max: undefined,
+ step: undefined,
+ unit: undefined
+ description: undefined // 添加这一行
+ name: [
+ { required: true, message: '功能名称不能为空', trigger: 'blur' },
+ pattern: /^[\u4e00-\u9fa5a-zA-Z0-9][\u4e00-\u9fa5a-zA-Z0-9\-_/\.]{0,29}$/,
+ '支持中文、大小写字母、日文、数字、短划线、下划线、斜杠和小数点,必须以中文、英文或数字开头,不超过 30 个字符',
+ type: [{ required: true, message: '功能类型不能为空', trigger: 'blur' }],
+ identifier: [
+ { required: true, message: '标识符不能为空', trigger: 'blur' },
+ pattern: /^[a-zA-Z0-9_]{1,50}$/,
+ message: '支持大小写字母、数字和下划线,不超过 50 个字符',
+ const reservedKeywords = ['set', 'get', 'post', 'property', 'event', 'time', 'value']
+ if (reservedKeywords.includes(value)) {
+ callback(
+ new Error(
+ 'set, get, post, property, event, time, value 是系统保留字段,不能用于标识符定义'
+ type: [{ required: true, message: '数据类型不能为空', trigger: 'blur' }]
+ accessMode: [{ required: true, message: '读写类型不能为空', trigger: 'blur' }]
+ formData.value = await ThinkModelFunctionApi.getThinkModelFunction(id)
+ const data = formData.value as unknown as ThinkModelFunctionVO
+ data.productId = props.product.id
+ data.productKey = props.product.productKey
+ await ThinkModelFunctionApi.createThinkModelFunction(data)
+ await ThinkModelFunctionApi.updateThinkModelFunction(data)
+ type: '1', // todo @HAOHAO:看看枚举下
+ description: undefined // 确保重置 description 字段
@@ -0,0 +1,80 @@
+ <ProductDetailsHeader :loading="loading" :product="product" @refresh="() => getProductData(id)" />
+ <el-tab-pane label="产品信息" name="info">
+ <ProductDetailsInfo v-if="activeTab === 'info'" :product="product" />
+ <el-tab-pane label="Topic 类列表" name="topic">
+ <ProductTopic v-if="activeTab === 'topic'" :product="product" />
+ <el-tab-pane label="功能定义" name="function">
+ <ThinkModelFunction v-if="activeTab === 'function'" :product="product" />
+ <el-tab-pane label="消息解析" name="message" />
+ <el-tab-pane label="服务端订阅" name="subscription" />
+import { DeviceApi } from '@/api/iot/device'
+import ProductDetailsHeader from '@/views/iot/product/detail/ProductDetailsHeader.vue'
+import ProductDetailsInfo from '@/views/iot/product/detail/ProductDetailsInfo.vue'
+import ProductTopic from '@/views/iot/product/detail/ProductTopic.vue'
+import ThinkModelFunction from '@/views/iot/product/detail/ThinkModelFunction.vue'
+defineOptions({ name: 'IoTProductDetail' })
+const { currentRoute } = useRouter()
+const product = ref<ProductVO>({} as ProductVO) // 详情
+const activeTab = ref('info') // 默认激活的标签页
+/** 获取详情 */
+ console.log('Product data:', product.value)
+// 查询设备数量
+const getDeviceCount = async (productId: number) => {
+ const count = await DeviceApi.getDeviceCount(productId)
+ console.log('Device count response:', count)
+ return count
+ console.error('Error fetching device count:', error)
+ return 0
+ await getProductData(id)
+ // 查询设备数量
+ if (product.value.id) {
+ product.value.deviceCount = await getDeviceCount(product.value.id)
+ console.log('Device count:', product.value.deviceCount)
+ console.error('Product ID is undefined')
@@ -0,0 +1,191 @@
+ v-model="queryParams.name"
+ placeholder="请输入产品名称"
+ <el-form-item label="ProductKey" prop="productKey">
+ v-model="queryParams.productKey"
+ placeholder="请输入产品标识"
+ v-hasPermi="['iot:product:create']"
+ <Icon icon="ep:plus" class="mr-5px" /> 新增
+ <el-table-column label="产品名称" align="center" prop="name">
+ <el-link @click="openDetail(scope.row.id)">{{ scope.row.name }}</el-link>
+ <el-table-column label="ProductKey" align="center" prop="productKey" />
+ label="创建时间"
+ prop="createTime"
+ <el-table-column label="产品状态" align="center" prop="status">
+ <dict-tag :type="DICT_TYPE.IOT_PRODUCT_STATUS" :value="scope.row.status" />
+ v-hasPermi="['iot:product:delete']"
+ :disabled="scope.row.status === 1"
+ <ProductForm ref="formRef" @success="getList" />
+import ProductForm from './ProductForm.vue'
+/** iot 产品 列表 */
+defineOptions({ name: 'IoTProduct' })
+const list = ref<ProductVO[]>([]) // 列表的数据
+ createTime: [],
+ const data = await ProductApi.getProductPage(queryParams)
+ push({ name: 'IoTProductDetail', params: { id } })
+ await ProductApi.deleteProduct(id)
@@ -129,7 +129,7 @@ const emit = defineEmits<{
(e: 'change', v: CouponTemplateApi.CouponTemplateVO[]): void
}>()
-const dialogTitle = ref('选择优惠卷') // 弹窗的标题
+const dialogTitle = ref('选择优惠劵') // 弹窗的标题
@@ -115,7 +115,7 @@
<el-radio-group v-model="formData.takeType">
<el-radio :key="1" :value="1">直接领取</el-radio>
<el-radio :key="2" :value="2">指定发放</el-radio>
- <el-radio :key="2" :value="3">新人卷</el-radio>
+ <el-radio :key="2" :value="3">新人劵</el-radio>
</el-radio-group>
<el-form-item v-if="formData.takeType === 1" label="发放数量" prop="totalCount">
@@ -190,7 +190,7 @@ const submitForm = async () => {
const products = cloneDeep(spuAndSkuListRef.value.getSkuConfigs('productConfig'))
products.forEach((item: DiscountActivityApi.DiscountProductVO) => {
item.discountPercent = convertToInteger(item.discountPercent)
- item.discountPrice = convertToInteger(yuanToFen(item.discountPrice))
+ item.discountPrice = convertToInteger(item.discountPrice)
const data = cloneDeep(formRef.value.formModel) as DiscountActivityApi.DiscountActivityVO
data.products = products
@@ -70,17 +70,6 @@ const crudSchemas = reactive<CrudSchema[]>([
width: 120
- label: '优惠类型',
- field: 'discountType',
- dictType: DICT_TYPE.PROMOTION_DISCOUNT_TYPE,
- dictClass: 'number',
- isSearch: true,
- form: {
- component: 'Radio',
- value: 1
label: '活动商品',
field: 'spuId',
@@ -22,13 +22,15 @@
<div class="ml-10px w-100%">
<div class="flex justify-between items-center w-100%">
<span class="username">{{ item.userNickname }}</span>
- <span class="color-[var(--left-menu-text-color)]" style="font-size: 13px;">
+ <span class="color-[var(--left-menu-text-color)]" style="font-size: 13px">
{{ formatPast(item.lastMessageTime, 'YYYY-MM-DD') }}
</span>
<!-- 最后聊天内容 -->
- v-dompurify-html="getConversationDisplayText(item.lastMessageContentType, item.lastMessageContent)"
+ v-dompurify-html="
+ getConversationDisplayText(item.lastMessageContentType, item.lastMessageContent)
class="last-message flex items-center color-[var(--left-menu-text-color)]"
@@ -205,7 +207,7 @@ watch(showRightMenu, (val) => {
.active {
border-left: 5px #3271ff solid;
- background-color: var(--left-menu-bg-active-color);
+ background-color: var(--login-bg-color);
.pinned {
@@ -215,7 +217,7 @@ watch(showRightMenu, (val) => {
.right-menu-ul {
position: absolute;
background-color: var(--app-content-bg-color);
- padding: 10px;
+ padding: 5px;
margin: 0;
list-style-type: none; /* 移除默认的项目符号 */
border-radius: 12px;
@@ -1,8 +1,13 @@
- <div v-if="isObject(getMessageContent)" @click="openDetail(getMessageContent.id)" style="cursor: pointer;">
+ <div v-if="isObject(getMessageContent)">
<div :key="getMessageContent.id" class="order-list-card-box mt-14px">
<div class="order-card-header flex items-center justify-between p-x-5px">
- <div class="order-no">订单号:{{ getMessageContent.no }}</div>
+ <div class="order-no">
+ 订单号:
+ <span style="cursor: pointer" @click="openDetail(getMessageContent.id)">
+ {{ getMessageContent.no }}
<div :class="formatOrderColor(getMessageContent)" class="order-state font-16">
{{ formatOrderStatus(getMessageContent) }}
@@ -113,8 +118,15 @@ function formatOrderStatus(order: any) {
height: 28px;
.order-no {
- font-size: 10px;
font-weight: 500;
+ span {
+ text-decoration: underline;
+ color: var(--left-menu-bg-active-color);
@@ -25,7 +25,7 @@
import { KeFuConversationList, KeFuMessageList, MemberBrowsingHistory } from './components'
import { WebSocketMessageTypeConstants } from './components/tools/constants'
import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
defineOptions({ name: 'KeFu' })
@@ -34,7 +34,9 @@ const message = useMessage() // 消息弹窗
// ======================= WebSocket start =======================
/** 发起 WebSocket 连接 */
@@ -1,5 +1,5 @@
- <el-button class="ml-10px" type="text" @click="selectCoupon">添加优惠卷</el-button>
+ <el-button class="ml-10px" type="text" @click="selectCoupon">添加优惠劵</el-button>
v-for="(item, index) in list"
@@ -57,7 +57,7 @@ const emits = defineEmits<{
const rewardRule = useVModel(props, 'modelValue', emits) // 赠送规则
const list = ref<GiveCouponVO[]>([]) // 选择的优惠券列表
-/** 选择赠送的优惠卷类型拓展 */
+/** 选择赠送的优惠类型拓展 */
interface GiveCouponVO extends CouponTemplateApi.CouponTemplateVO {
giveCount?: number
@@ -140,7 +140,7 @@
'member:user:update',
'member:user:update-level',
'member:user:update-point',
- 'member:user:update-balance'
+ 'pay:wallet:update-balance'
]"
@command="(command) => handleCommand(command, scope.row)"
@@ -169,7 +169,7 @@
修改积分
</el-dropdown-item>
<el-dropdown-item
- v-if="checkPermi(['member:user:update-balance'])"
+ v-if="checkPermi(['pay:wallet:update-balance'])"
command="handleUpdateBlance"
修改余额
@@ -6,9 +6,10 @@
defineOptions({ name: 'JimuReport' })
-const src = ref(import.meta.env.VITE_BASE_URL + '/jmreport/list?token=' + getAccessToken())
+// 使用 getRefreshToken() 方法,而不使用 getAccessToken() 方法的原因:积木报表无法方便的刷新访问令牌
+const src = ref(import.meta.env.VITE_BASE_URL + '/jmreport/list?token=' + getRefreshToken())