| | |
| | | "animate.css": "^4.1.1", |
| | | "axios": "^1.6.8", |
| | | "benz-amr-recorder": "^1.1.5", |
| | | "bpmn-js-token-simulation": "^0.10.0", |
| | | "bpmn-js-token-simulation": "^0.36.0", |
| | | "camunda-bpmn-moddle": "^7.0.1", |
| | | "cropperjs": "^1.6.1", |
| | | "crypto-js": "^4.2.0", |
| | |
| | | "driver.js": "^1.3.1", |
| | | "echarts": "^5.5.0", |
| | | "echarts-wordcloud": "^2.1.0", |
| | | "element-plus": "2.8.4", |
| | | "element-plus": "2.9.1", |
| | | "fast-xml-parser": "^4.3.2", |
| | | "highlight.js": "^11.9.0", |
| | | "jsencrypt": "^3.3.2", |
| | |
| | | export const deleteScheduleScheme = (id: number) => { |
| | | return request.delete({ url: '/model/sche/scheme/delete?id=' + id }) |
| | | } |
| | | |
| | | // 启用 |
| | | export const enable = (ids) => { |
| | | const data = ids |
| | | return request.put({ url: '/model/sche/scheme/enable', data }) |
| | | } |
| | | |
| | | // 禁用 |
| | | export const disable = (ids) => { |
| | | const data = ids |
| | | return request.put({ url: '/model/sche/scheme/disable', data }) |
| | | } |
| | | |
对比新文件 |
| | |
| | | <?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1735905505218" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4277" width="200" height="200" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M561.778 454.929h198.117c0.549 0 0.994 0.444 0.994 1.001v97.553a0.998 0.998 0 0 1-0.994 1.001H463.224a1.005 1.005 0 0 1-1.002-1V207.04c0-0.552 0.444-1 1.002-1h97.552c0.553 0 1.002 0.455 1.002 1v247.89zM512 952.706c-247.424 0-448-200.576-448-448 0-247.423 200.576-448 448-448s448 200.577 448 448c0 247.424-200.576 448-448 448z m0-99.555c192.44 0 348.444-156.004 348.444-348.445 0-192.44-156.003-348.444-348.444-348.444-192.44 0-348.444 156.004-348.444 348.444 0 192.441 156.003 348.445 348.444 348.445z" fill="#3296FA" p-id="4278"></path></svg> |
| | |
| | | import { isString } from '@/utils/is' |
| | | import { useDesign } from '@/hooks/web/useDesign' |
| | | |
| | | import 'echarts/lib/component/markPoint' |
| | | import 'echarts/lib/component/markLine' |
| | | import 'echarts/lib/component/markArea' |
| | | |
| | | defineOptions({ name: 'EChart' }) |
| | | |
| | | const { getPrefixCls, variables } = useDesign() |
| | |
| | | |
| | | contentEl.value = document.getElementsByClassName(`${variables.namespace}-layout-content`)[0] |
| | | unref(contentEl) && |
| | | (unref(contentEl) as Element).addEventListener('transitionend', contentResizeHandler) |
| | | (unref(contentEl) as Element).addEventListener('transitionend', contentResizeHandler) |
| | | }) |
| | | |
| | | onBeforeUnmount(() => { |
| | | window.removeEventListener('resize', resizeHandler) |
| | | unref(contentEl) && |
| | | (unref(contentEl) as Element).removeEventListener('transitionend', contentResizeHandler) |
| | | (unref(contentEl) as Element).removeEventListener('transitionend', contentResizeHandler) |
| | | }) |
| | | |
| | | onActivated(() => { |
| | |
| | | |
| | | function handleChange(path) { |
| | | router.push({ path }) |
| | | hiddenSearch() |
| | | hiddenTopSearch() |
| | | } |
| | | |
| | | function hiddenSearch() { |
| | | showSearch.value = false |
| | | } |
| | | |
| | | function hiddenTopSearch() { |
| | |
| | | // 监听 ctrl + k |
| | | function listenKey(event) { |
| | | if ((event.ctrlKey || event.metaKey) && event.key === 'k') { |
| | | // 阻止触发浏览器默认事件 |
| | | event.preventDefault() |
| | | showSearch.value = !showSearch.value |
| | | // 这里可以执行相应的操作(例如打开搜索框等) |
| | | } |
| | |
| | | </div> |
| | | <div class="handler-item-text">包容分支</div> |
| | | </div> |
| | | <div class="handler-item" @click="addNode(NodeType.DELAY_TIMER_NODE)"> |
| | | <!-- TODO @芋艿 需要更换一下iconfont的图标 --> |
| | | <div class="handler-item-icon copy"> |
| | | <span class="iconfont icon-size icon-copy"></span> |
| | | </div> |
| | | <div class="handler-item-text">延迟器</div> |
| | | </div> |
| | | </div> |
| | | <template #reference> |
| | | <div class="add-icon"><Icon icon="ep:plus" /></div> |
| | |
| | | } |
| | | emits('update:childNode', data) |
| | | } |
| | | if (type === NodeType.DELAY_TIMER_NODE) { |
| | | const data: SimpleFlowNode = { |
| | | id: 'Activity_' + generateUUID(), |
| | | name: NODE_DEFAULT_NAME.get(NodeType.DELAY_TIMER_NODE) as string, |
| | | showText: '', |
| | | type: NodeType.DELAY_TIMER_NODE, |
| | | childNode: props.childNode |
| | | } |
| | | emits('update:childNode', data) |
| | | } |
| | | } |
| | | </script> |
| | | |
| | |
| | | @update:model-value="handleModelValueUpdate" |
| | | @find:parent-node="findFromParentNode" |
| | | /> |
| | | <!-- 延迟器节点 --> |
| | | <DelayTimerNode |
| | | v-if="currentNode && currentNode.type === NodeType.DELAY_TIMER_NODE" |
| | | :flow-node="currentNode" |
| | | @update:flow-node="handleModelValueUpdate" |
| | | /> |
| | | <!-- 递归显示孩子节点 --> |
| | | <ProcessNodeTree |
| | | v-if="currentNode && currentNode.childNode" |
| | |
| | | import ExclusiveNode from './nodes/ExclusiveNode.vue' |
| | | import ParallelNode from './nodes/ParallelNode.vue' |
| | | import InclusiveNode from './nodes/InclusiveNode.vue' |
| | | import DelayTimerNode from './nodes/DelayTimerNode.vue' |
| | | import { SimpleFlowNode, NodeType } from './consts' |
| | | import { useWatchNode } from './node' |
| | | defineOptions({ |
| | |
| | | <template> |
| | | <div v-loading="loading" class="overflow-auto"> |
| | | <SimpleProcessModel |
| | | ref="simpleProcessModelRef" |
| | | v-if="processNodeTree" |
| | | :flow-node="processNodeTree" |
| | | :readonly="false" |
| | |
| | | defineOptions({ |
| | | name: 'SimpleProcessDesigner' |
| | | }) |
| | | const emits = defineEmits(['success']) // 保存成功事件 |
| | | |
| | | const emits = defineEmits(['success', 'init-finished']) // 保存成功事件 |
| | | |
| | | const props = defineProps({ |
| | | modelId: { |
| | | type: String, |
| | | required: true |
| | | required: false |
| | | }, |
| | | modelKey: { |
| | | type: String, |
| | | required: false |
| | | }, |
| | | modelName: { |
| | | type: String, |
| | | required: false |
| | | }, |
| | | // 可发起流程的人员编号 |
| | | startUserIds : { |
| | | type: Array, |
| | | required: false |
| | | }, |
| | | value: { |
| | | type: [String, Object], |
| | | required: false |
| | | } |
| | | }) |
| | | |
| | |
| | | const deptOptions = ref<DeptApi.DeptVO[]>([]) // 部门列表 |
| | | const deptTreeOptions = ref() |
| | | const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表 |
| | | |
| | | // 添加当前值的引用 |
| | | const currentValue = ref<SimpleFlowNode | undefined>() |
| | | |
| | | provide('formFields', formFields) |
| | | provide('formType', formType) |
| | | provide('roleList', roleOptions) |
| | |
| | | provide('deptList', deptOptions) |
| | | provide('userGroupList', userGroupOptions) |
| | | provide('deptTree', deptTreeOptions) |
| | | provide('startUserIds', props.startUserIds) |
| | | |
| | | const message = useMessage() // 国际化 |
| | | const processNodeTree = ref<SimpleFlowNode | undefined>() |
| | | const errorDialogVisible = ref(false) |
| | | let errorNodes: SimpleFlowNode[] = [] |
| | | const saveSimpleFlowModel = async (simpleModelNode: SimpleFlowNode) => { |
| | | if (!simpleModelNode) { |
| | | message.error('模型数据为空') |
| | | return |
| | | } |
| | | try { |
| | | loading.value = true |
| | | const data = { |
| | | id: props.modelId, |
| | | simpleModel: simpleModelNode |
| | | |
| | | // 添加更新模型的方法 |
| | | const updateModel = () => { |
| | | if (!processNodeTree.value) { |
| | | processNodeTree.value = { |
| | | name: '发起人', |
| | | type: NodeType.START_USER_NODE, |
| | | id: NodeId.START_USER_NODE_ID, |
| | | childNode: { |
| | | id: NodeId.END_EVENT_NODE_ID, |
| | | name: '结束', |
| | | type: NodeType.END_EVENT_NODE |
| | | } |
| | | } |
| | | const result = await updateBpmSimpleModel(data) |
| | | if (result) { |
| | | message.success('修改成功') |
| | | emits('success') |
| | | } else { |
| | | message.alert('修改失败') |
| | | } |
| | | } finally { |
| | | loading.value = false |
| | | // 初始化时也触发一次保存 |
| | | saveSimpleFlowModel(processNodeTree.value) |
| | | } |
| | | } |
| | | |
| | | // 加载流程数据 |
| | | const loadProcessData = async (data: any) => { |
| | | try { |
| | | if (data) { |
| | | const parsedData = typeof data === 'string' ? JSON.parse(data) : data |
| | | processNodeTree.value = parsedData |
| | | currentValue.value = parsedData |
| | | // 确保数据加载后刷新视图 |
| | | await nextTick() |
| | | if (simpleProcessModelRef.value?.refresh) { |
| | | await simpleProcessModelRef.value.refresh() |
| | | } |
| | | } |
| | | } catch (error) { |
| | | console.error('加载流程数据失败:', error) |
| | | } |
| | | } |
| | | |
| | | // 监听属性变化 |
| | | watch( |
| | | () => props.value, |
| | | async (newValue, oldValue) => { |
| | | if (newValue && newValue !== oldValue) { |
| | | await loadProcessData(newValue) |
| | | } |
| | | }, |
| | | { immediate: true, deep: true } |
| | | ) |
| | | |
| | | // 监听流程节点树变化,自动保存 |
| | | watch( |
| | | () => processNodeTree.value, |
| | | async (newValue, oldValue) => { |
| | | if (newValue && oldValue && JSON.stringify(newValue) !== JSON.stringify(oldValue)) { |
| | | await saveSimpleFlowModel(newValue) |
| | | } |
| | | }, |
| | | { deep: true } |
| | | ) |
| | | |
| | | const saveSimpleFlowModel = async (simpleModelNode: SimpleFlowNode) => { |
| | | if (!simpleModelNode) { |
| | | return |
| | | } |
| | | |
| | | // 校验节点 |
| | | errorNodes = [] |
| | | validateNode(simpleModelNode, errorNodes) |
| | | if (errorNodes.length > 0) { |
| | | errorDialogVisible.value = true |
| | | return |
| | | } |
| | | |
| | | try { |
| | | if (props.modelId) { |
| | | // 编辑模式 |
| | | const data = { |
| | | id: props.modelId, |
| | | simpleModel: simpleModelNode |
| | | } |
| | | await updateBpmSimpleModel(data) |
| | | } |
| | | // 无论是编辑还是新建模式,都更新当前值并触发事件 |
| | | currentValue.value = simpleModelNode |
| | | emits('success', simpleModelNode) |
| | | } catch (error) { |
| | | console.error('保存失败:', error) |
| | | } |
| | | } |
| | | |
| | | // 校验节点设置。 暂时以 showText 为空 未节点错误配置 |
| | | const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNode[]) => { |
| | | if (node) { |
| | |
| | | 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 |
| | | if (props.modelId) { |
| | | const bpmnModel = await getModel(props.modelId) |
| | | if (bpmnModel) { |
| | | formType.value = bpmnModel.formType |
| | | if (formType.value === 10) { |
| | | const bpmnForm = (await getForm(bpmnModel.formId)) as unknown as FormVO |
| | | formFields.value = bpmnForm?.fields |
| | | } |
| | | } |
| | | } |
| | | // 获得角色列表 |
| | |
| | | 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) |
| | | if (result) { |
| | | processNodeTree.value = result |
| | | } else { |
| | | // 初始值 |
| | | processNodeTree.value = { |
| | | name: '发起人', |
| | | type: NodeType.START_USER_NODE, |
| | | id: NodeId.START_USER_NODE_ID, |
| | | childNode: { |
| | | id: NodeId.END_EVENT_NODE_ID, |
| | | name: '结束', |
| | | type: NodeType.END_EVENT_NODE |
| | | } |
| | | // 加载流程数据 |
| | | if (props.modelId) { |
| | | // 获取 SIMPLE 设计器模型 |
| | | const result = await getBpmSimpleModel(props.modelId) |
| | | if (result) { |
| | | await loadProcessData(result) |
| | | } else { |
| | | updateModel() |
| | | } |
| | | } else if (props.value) { |
| | | await loadProcessData(props.value) |
| | | } else { |
| | | updateModel() |
| | | } |
| | | } finally { |
| | | loading.value = false |
| | | emits('init-finished') |
| | | } |
| | | }) |
| | | |
| | | const simpleProcessModelRef = ref() |
| | | |
| | | /** 获取当前流程数据 */ |
| | | const getCurrentFlowData = async () => { |
| | | try { |
| | | if (simpleProcessModelRef.value) { |
| | | const data = await simpleProcessModelRef.value.getCurrentFlowData() |
| | | if (data) { |
| | | currentValue.value = data |
| | | return data |
| | | } |
| | | } |
| | | return currentValue.value |
| | | } catch (error) { |
| | | console.error('获取流程数据失败:', error) |
| | | return currentValue.value |
| | | } |
| | | } |
| | | |
| | | // 刷新方法 |
| | | const refresh = async () => { |
| | | try { |
| | | if (currentValue.value) { |
| | | await loadProcessData(currentValue.value) |
| | | } |
| | | } catch (error) { |
| | | console.error('刷新失败:', error) |
| | | } |
| | | } |
| | | |
| | | defineExpose({ |
| | | getCurrentFlowData, |
| | | updateModel, |
| | | loadProcessData, |
| | | refresh |
| | | }) |
| | | </script> |
| | |
| | | <el-button size="default" class="w-80px"> {{ scaleValue }}% </el-button> |
| | | <el-button size="default" :plain="true" :icon="ZoomIn" @click="zoomIn()" /> |
| | | </el-button-group> |
| | | <el-button |
| | | v-if="!readonly" |
| | | size="default" |
| | | class="ml-4px" |
| | | type="primary" |
| | | :icon="Select" |
| | | @click="saveSimpleFlowModel" |
| | | >保存模型</el-button |
| | | > |
| | | </el-row> |
| | | </div> |
| | | <div class="simple-process-model" :style="`transform: scale(${scaleValue / 100});`"> |
| | |
| | | import ProcessNodeTree from './ProcessNodeTree.vue' |
| | | import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from './consts' |
| | | import { useWatchNode } from './node' |
| | | import { Select, ZoomOut, ZoomIn, ScaleToOriginal } from '@element-plus/icons-vue' |
| | | import { ZoomOut, ZoomIn, ScaleToOriginal } from '@element-plus/icons-vue' |
| | | |
| | | defineOptions({ |
| | | name: 'SimpleProcessModel' |
| | | }) |
| | |
| | | default: true |
| | | } |
| | | }) |
| | | |
| | | const emits = defineEmits<{ |
| | | 'save': [node: SimpleFlowNode | undefined] |
| | | }>() |
| | |
| | | let scaleValue = ref(100) |
| | | const MAX_SCALE_VALUE = 200 |
| | | const MIN_SCALE_VALUE = 50 |
| | | |
| | | // 放大 |
| | | const zoomIn = () => { |
| | | if (scaleValue.value == MAX_SCALE_VALUE) { |
| | |
| | | } |
| | | scaleValue.value += 10 |
| | | } |
| | | |
| | | // 缩小 |
| | | const zoomOut = () => { |
| | | if (scaleValue.value == MIN_SCALE_VALUE) { |
| | |
| | | } |
| | | scaleValue.value -= 10 |
| | | } |
| | | |
| | | const processReZoom = () => { |
| | | scaleValue.value = 100 |
| | | } |
| | | |
| | | const errorDialogVisible = ref(false) |
| | | let errorNodes: SimpleFlowNode[] = [] |
| | | const saveSimpleFlowModel = async () => { |
| | | errorNodes = [] |
| | | validateNode(processNodeTree.value, errorNodes) |
| | | if (errorNodes.length > 0) { |
| | | errorDialogVisible.value = true |
| | | return |
| | | } |
| | | emits('save', processNodeTree.value) |
| | | } |
| | | |
| | | // 校验节点设置。 暂时以 showText 为空 未节点错误配置 |
| | | const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNode[]) => { |
| | | if (node) { |
| | |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** 获取当前流程数据 */ |
| | | const getCurrentFlowData = async () => { |
| | | try { |
| | | errorNodes = [] |
| | | validateNode(processNodeTree.value, errorNodes) |
| | | if (errorNodes.length > 0) { |
| | | errorDialogVisible.value = true |
| | | return undefined |
| | | } |
| | | return processNodeTree.value |
| | | } catch (error) { |
| | | console.error('获取流程数据失败:', error) |
| | | return undefined |
| | | } |
| | | } |
| | | |
| | | defineExpose({ |
| | | getCurrentFlowData |
| | | }) |
| | | </script> |
| | | |
| | | <style lang="scss" scoped></style> |
| | |
| | | COPY_TASK_NODE = 12, |
| | | |
| | | /** |
| | | * 延迟器节点 |
| | | */ |
| | | DELAY_TIMER_NODE = 14, |
| | | |
| | | /** |
| | | * 条件节点 |
| | | */ |
| | | CONDITION_NODE = 50, |
| | |
| | | defaultFlow?: boolean |
| | | // 活动的状态,用于前端节点状态展示 |
| | | activityStatus?: TaskStatusEnum |
| | | // 延迟设置 |
| | | delaySetting?: DelaySetting |
| | | } |
| | | // 候选人策略枚举 ( 用于审批节点。抄送节点 ) |
| | | export enum CandidateStrategy { |
| | |
| | | NODE_DEFAULT_TEXT.set(NodeType.COPY_TASK_NODE, '请配置抄送人') |
| | | NODE_DEFAULT_TEXT.set(NodeType.CONDITION_NODE, '请设置条件') |
| | | NODE_DEFAULT_TEXT.set(NodeType.START_USER_NODE, '请设置发起人') |
| | | NODE_DEFAULT_TEXT.set(NodeType.DELAY_TIMER_NODE, '请设置延迟器') |
| | | |
| | | export const NODE_DEFAULT_NAME = new Map<number, string>() |
| | | NODE_DEFAULT_NAME.set(NodeType.USER_TASK_NODE, '审批人') |
| | | NODE_DEFAULT_NAME.set(NodeType.COPY_TASK_NODE, '抄送人') |
| | | NODE_DEFAULT_NAME.set(NodeType.CONDITION_NODE, '条件') |
| | | NODE_DEFAULT_NAME.set(NodeType.START_USER_NODE, '发起人') |
| | | NODE_DEFAULT_NAME.set(NodeType.DELAY_TIMER_NODE, '延迟器') |
| | | |
| | | // 候选人策略。暂时不从字典中取。 后续可能调整。控制显示顺序 |
| | | export const CANDIDATE_STRATEGY: DictDataVO[] = [ |
| | |
| | | */ |
| | | START_USER_ID = 'PROCESS_START_USER_ID' |
| | | } |
| | | |
| | | /** |
| | | * 延迟设置 |
| | | */ |
| | | export type DelaySetting = { |
| | | // 延迟类型 |
| | | delayType: number |
| | | // 延迟时间表达式 |
| | | delayTime: string |
| | | } |
| | | /** |
| | | * 延迟类型 |
| | | */ |
| | | export enum DelayTypeEnum { |
| | | /** |
| | | * 固定时长 |
| | | */ |
| | | FIXED_TIME_DURATION = 1, |
| | | /** |
| | | * 固定日期时间 |
| | | */ |
| | | FIXED_DATE_TIME = 2 |
| | | } |
| | | export const DELAY_TYPE = [ |
| | | { label: '固定时长', value: DelayTypeEnum.FIXED_TIME_DURATION }, |
| | | { label: '固定日期', value: DelayTypeEnum.FIXED_DATE_TIME } |
| | | ] |
| | |
| | | import { cloneDeep } from 'lodash-es' |
| | | import { TaskStatusEnum } from '@/api/bpm/task' |
| | | import * as RoleApi from '@/api/system/role' |
| | | import * as DeptApi from '@/api/system/dept' |
| | |
| | | NODE_DEFAULT_NAME, |
| | | AssignStartUserHandlerType, |
| | | AssignEmptyHandlerType, |
| | | FieldPermissionType, |
| | | FieldPermissionType |
| | | } from './consts' |
| | | import { parseFormFields } from '@/components/FormCreate/src/utils/index' |
| | | export function useWatchNode(props: { flowNode: SimpleFlowNode }): Ref<SimpleFlowNode> { |
| | |
| | | |
| | | const getNodeConfigFormFields = (nodeFormFields?: Array<Record<string, string>>) => { |
| | | nodeFormFields = toRaw(nodeFormFields) |
| | | fieldsPermissionConfig.value = |
| | | cloneDeep(nodeFormFields) || getDefaultFieldsPermission(unref(formFields)) |
| | | if (!nodeFormFields || nodeFormFields.length === 0) { |
| | | fieldsPermissionConfig.value = getDefaultFieldsPermission(unref(formFields)) |
| | | } else { |
| | | fieldsPermissionConfig.value = mergeFieldsPermission(nodeFormFields, unref(formFields)) |
| | | } |
| | | } |
| | | // 合并已经设置的表单字段权限,当前流程表单字段 (可能新增,或删除了字段) |
| | | const mergeFieldsPermission = ( |
| | | formFieldsPermisson: Array<Record<string, string>>, |
| | | formFields?: string[] |
| | | ) => { |
| | | let mergedFieldsPermission: Array<Record<string, any>> = [] |
| | | if (formFields) { |
| | | mergedFieldsPermission = parseFormCreateFields(formFields).map((item) => { |
| | | const found = formFieldsPermisson.find( |
| | | (fieldPermission) => fieldPermission.field == item.field |
| | | ) |
| | | return { |
| | | field: item.field, |
| | | title: item.title, |
| | | permission: found ? found.permission : defaultPermission |
| | | } |
| | | }) |
| | | } |
| | | return mergedFieldsPermission |
| | | } |
| | | |
| | | // 默认的表单权限: 获取表单的所有字段,设置字段默认权限为只读 |
| | | const getDefaultFieldsPermission = (formFields?: string[]) => { |
| | | let defaultFieldsPermission: Array<Record<string, any>> = [] |
| | |
| | | |
| | | /** 获取字段名称 */ |
| | | const getFieldTitle = (field: string) => { |
| | | const item = fieldsInfo.find((item) => item.field === field) |
| | | const item = fieldOptions.value.find((item) => item.field === field) |
| | | return item?.title |
| | | } |
| | | |
对比新文件 |
| | |
| | | <template> |
| | | <el-drawer |
| | | :append-to-body="true" |
| | | v-model="settingVisible" |
| | | :show-close="false" |
| | | :size="550" |
| | | :before-close="saveConfig" |
| | | > |
| | | <template #header> |
| | | <div class="config-header"> |
| | | <input |
| | | v-if="showInput" |
| | | type="text" |
| | | class="config-editable-input" |
| | | @blur="blurEvent()" |
| | | v-mountedFocus |
| | | v-model="nodeName" |
| | | :placeholder="nodeName" |
| | | /> |
| | | <div v-else class="node-name"> |
| | | {{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" /> |
| | | </div> |
| | | <div class="divide-line"></div> |
| | | </div> |
| | | </template> |
| | | <div> |
| | | <el-form ref="formRef" :model="configForm" label-position="top" :rules="formRules"> |
| | | <el-form-item label="延迟时间" prop="delayType"> |
| | | <el-radio-group v-model="configForm.delayType"> |
| | | <el-radio-button |
| | | v-for="item in DELAY_TYPE" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item v-if="configForm.delayType === DelayTypeEnum.FIXED_TIME_DURATION"> |
| | | <el-form-item prop="timeDuration"> |
| | | <el-input-number |
| | | class="mr-2" |
| | | :style="{ width: '100px' }" |
| | | v-model="configForm.timeDuration" |
| | | :min="1" |
| | | controls-position="right" |
| | | /> |
| | | </el-form-item> |
| | | <el-select v-model="configForm.timeUnit" class="mr-2" :style="{ width: '100px' }"> |
| | | <el-option |
| | | v-for="item in TIME_UNIT_TYPES" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | <el-text>后进入下一节点</el-text> |
| | | </el-form-item> |
| | | <el-form-item v-if="configForm.delayType === DelayTypeEnum.FIXED_DATE_TIME" prop="dateTime"> |
| | | <el-date-picker |
| | | class="mr-2" |
| | | v-model="configForm.dateTime" |
| | | type="datetime" |
| | | placeholder="请选择日期和时间" |
| | | value-format="YYYY-MM-DDTHH:mm:ss" |
| | | /> |
| | | <el-text>后进入下一节点</el-text> |
| | | </el-form-item> |
| | | </el-form> |
| | | </div> |
| | | <template #footer> |
| | | <el-divider /> |
| | | <div> |
| | | <el-button type="primary" @click="saveConfig">确 定</el-button> |
| | | <el-button @click="closeDrawer">取 消</el-button> |
| | | </div> |
| | | </template> |
| | | </el-drawer> |
| | | </template> |
| | | <script setup lang="ts"> |
| | | import { |
| | | SimpleFlowNode, |
| | | NodeType, |
| | | TIME_UNIT_TYPES, |
| | | TimeUnitType, |
| | | DelayTypeEnum, |
| | | DELAY_TYPE |
| | | } from '../consts' |
| | | import { useWatchNode, useDrawer, useNodeName } from '../node' |
| | | import { convertTimeUnit } from '../utils' |
| | | defineOptions({ |
| | | name: 'DelayTimerNodeConfig' |
| | | }) |
| | | const props = defineProps({ |
| | | flowNode: { |
| | | type: Object as () => SimpleFlowNode, |
| | | required: true |
| | | } |
| | | }) |
| | | // 抽屉配置 |
| | | const { settingVisible, closeDrawer, openDrawer } = useDrawer() |
| | | // 当前节点 |
| | | const currentNode = useWatchNode(props) |
| | | // 节点名称 |
| | | const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.DELAY_TIMER_NODE) |
| | | // 抄送人表单配置 |
| | | const formRef = ref() // 表单 Ref |
| | | // 表单校验规则 |
| | | const formRules = reactive({ |
| | | delayType: [{ required: true, message: '延迟时间不能为空', trigger: 'change' }], |
| | | timeDuration: [{ required: true, message: '延迟时间不能为空', trigger: 'change' }], |
| | | dateTime: [{ required: true, message: '延迟时间不能为空', trigger: 'change' }] |
| | | }) |
| | | // 配置表单数据 |
| | | const configForm = ref({ |
| | | delayType: DelayTypeEnum.FIXED_TIME_DURATION, |
| | | timeDuration: 1, |
| | | timeUnit: TimeUnitType.HOUR, |
| | | dateTime: '' |
| | | }) |
| | | // 保存配置 |
| | | const saveConfig = async () => { |
| | | if (!formRef) return false |
| | | const valid = await formRef.value.validate() |
| | | if (!valid) return false |
| | | const showText = getShowText() |
| | | if (!showText) return false |
| | | currentNode.value.showText = showText |
| | | if (configForm.value.delayType === DelayTypeEnum.FIXED_TIME_DURATION) { |
| | | currentNode.value.delaySetting = { |
| | | delayType: configForm.value.delayType, |
| | | delayTime: getIsoTimeDuration() |
| | | } |
| | | } |
| | | if (configForm.value.delayType === DelayTypeEnum.FIXED_DATE_TIME) { |
| | | currentNode.value.delaySetting = { |
| | | delayType: configForm.value.delayType, |
| | | delayTime: configForm.value.dateTime |
| | | } |
| | | } |
| | | settingVisible.value = false |
| | | return true |
| | | } |
| | | const getShowText = (): string => { |
| | | let showText = '' |
| | | if (configForm.value.delayType === DelayTypeEnum.FIXED_TIME_DURATION) { |
| | | showText = `延迟${configForm.value.timeDuration}${TIME_UNIT_TYPES.find((item) => item.value === configForm.value.timeUnit).label}` |
| | | } |
| | | if (configForm.value.delayType === DelayTypeEnum.FIXED_DATE_TIME) { |
| | | showText = `延迟至${configForm.value.dateTime.replace('T', ' ')}` |
| | | } |
| | | return showText |
| | | } |
| | | const getIsoTimeDuration = () => { |
| | | let strTimeDuration = 'PT' |
| | | if (configForm.value.timeUnit === TimeUnitType.MINUTE) { |
| | | strTimeDuration += configForm.value.timeDuration + 'M' |
| | | } |
| | | if (configForm.value.timeUnit === TimeUnitType.HOUR) { |
| | | strTimeDuration += configForm.value.timeDuration + 'H' |
| | | } |
| | | if (configForm.value.timeUnit === TimeUnitType.DAY) { |
| | | strTimeDuration += configForm.value.timeDuration + 'D' |
| | | } |
| | | return strTimeDuration |
| | | } |
| | | // 显示延迟器节点配置, 由父组件传过来 |
| | | const showDelayTimerNodeConfig = (node: SimpleFlowNode) => { |
| | | nodeName.value = node.name |
| | | if (node.delaySetting) { |
| | | configForm.value.delayType = node.delaySetting.delayType |
| | | // 固定时长 |
| | | if (configForm.value.delayType === DelayTypeEnum.FIXED_TIME_DURATION) { |
| | | const strTimeDuration = node.delaySetting.delayTime |
| | | let parseTime = strTimeDuration.slice(2, strTimeDuration.length - 1) |
| | | let parseTimeUnit = strTimeDuration.slice(strTimeDuration.length - 1) |
| | | configForm.value.timeDuration = parseInt(parseTime) |
| | | configForm.value.timeUnit = convertTimeUnit(parseTimeUnit) |
| | | } |
| | | // 固定日期时间 |
| | | if (configForm.value.delayType === DelayTypeEnum.FIXED_DATE_TIME) { |
| | | configForm.value.dateTime = node.delaySetting.delayTime |
| | | } |
| | | } |
| | | } |
| | | |
| | | defineExpose({ openDrawer, showDelayTimerNodeConfig }) // 暴露方法给父组件 |
| | | </script> |
| | | |
| | | <style lang="scss" scoped></style> |
| | |
| | | </template> |
| | | <el-tabs type="border-card" v-model="activeTabName"> |
| | | <el-tab-pane label="权限" name="user"> |
| | | <div> 待实现 </div> |
| | | <el-text v-if="!startUserIds || startUserIds.length === 0"> 全部成员可以发起流程 </el-text> |
| | | <el-text v-else-if="startUserIds.length == 1"> |
| | | {{ getUserNicknames(startUserIds) }} 可发起流程 |
| | | </el-text> |
| | | <el-text v-else> |
| | | <el-tooltip |
| | | class="box-item" |
| | | effect="dark" |
| | | placement="top" |
| | | :content="getUserNicknames(startUserIds)" |
| | | > |
| | | {{ getUserNicknames(startUserIds.slice(0,2)) }} 等 {{ startUserIds.length }} 人可发起流程 |
| | | </el-tooltip> |
| | | </el-text> |
| | | </el-tab-pane> |
| | | <el-tab-pane label="表单字段权限" name="fields" v-if="formType === 10"> |
| | | <div class="field-setting-pane"> |
| | |
| | | <script setup lang="ts"> |
| | | import { SimpleFlowNode, NodeType, FieldPermissionType, START_USER_BUTTON_SETTING } from '../consts' |
| | | import { useWatchNode, useDrawer, useNodeName, useFormFieldsPermission } from '../node' |
| | | |
| | | import * as UserApi from '@/api/system/user' |
| | | defineOptions({ |
| | | name: 'StartUserNodeConfig' |
| | | }) |
| | |
| | | required: true |
| | | } |
| | | }) |
| | | // 可发起流程的用户编号 |
| | | const startUserIds = inject<Ref<any[]>>('startUserIds') |
| | | // 用户列表 |
| | | const userOptions = inject<Ref<UserApi.UserVO[]>>('userList') |
| | | // 抽屉配置 |
| | | const { settingVisible, closeDrawer, openDrawer } = useDrawer() |
| | | // 当前节点 |
| | |
| | | const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission( |
| | | FieldPermissionType.WRITE |
| | | ) |
| | | |
| | | const getUserNicknames = (userIds: number[]): string => { |
| | | if (!userIds || userIds.length === 0) { |
| | | return '' |
| | | } |
| | | const nicknames: string[] = [] |
| | | userIds.forEach((userId) => { |
| | | const found = userOptions?.value.find((item) => item.id === userId) |
| | | if (found && found.nickname) { |
| | | nicknames.push(found.nickname) |
| | | } |
| | | }) |
| | | return nicknames.join(',') |
| | | } |
| | | // 保存配置 |
| | | const saveConfig = async () => { |
| | | activeTabName.value = 'user' |
| | | currentNode.value.name = nodeName.value! |
| | | // TODO 暂时写死。后续可以显示谁有权限可以发起 |
| | | currentNode.value.showText = '已设置' |
| | | // 设置表单权限 |
| | | currentNode.value.fieldsPermission = fieldsPermissionConfig.value |
对比新文件 |
| | |
| | | <template> |
| | | <div class="node-wrapper"> |
| | | <div class="node-container"> |
| | | <div |
| | | class="node-box" |
| | | :class="[ |
| | | { 'node-config-error': !currentNode.showText }, |
| | | `${useTaskStatusClass(currentNode?.activityStatus)}` |
| | | ]" |
| | | > |
| | | <div class="node-title-container"> |
| | | <!-- TODO @芋艿 需要更换图标 --> |
| | | <div class="node-title-icon copy-task"><span class="iconfont icon-copy"></span></div> |
| | | <input |
| | | v-if="!readonly && showInput" |
| | | type="text" |
| | | class="editable-title-input" |
| | | @blur="blurEvent()" |
| | | v-mountedFocus |
| | | v-model="currentNode.name" |
| | | :placeholder="currentNode.name" |
| | | /> |
| | | <div v-else class="node-title" @click="clickTitle"> |
| | | {{ currentNode.name }} |
| | | </div> |
| | | </div> |
| | | <div class="node-content" @click="openNodeConfig"> |
| | | <div class="node-text" :title="currentNode.showText" v-if="currentNode.showText"> |
| | | {{ currentNode.showText }} |
| | | </div> |
| | | <div class="node-text" v-else> |
| | | {{ NODE_DEFAULT_TEXT.get(NodeType.DELAY_TIMER_NODE) }} |
| | | </div> |
| | | <Icon v-if="!readonly" icon="ep:arrow-right-bold" /> |
| | | </div> |
| | | <div v-if="!readonly" class="node-toolbar"> |
| | | <div class="toolbar-icon" |
| | | ><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode" |
| | | /></div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 传递子节点给添加节点组件。会在子节点前面添加节点 --> |
| | | <NodeHandler |
| | | v-if="currentNode" |
| | | v-model:child-node="currentNode.childNode" |
| | | :current-node="currentNode" |
| | | /> |
| | | </div> |
| | | <DelayTimerNodeConfig |
| | | v-if="!readonly && currentNode" |
| | | ref="nodeSetting" |
| | | :flow-node="currentNode" |
| | | /> |
| | | </div> |
| | | </template> |
| | | <script setup lang="ts"> |
| | | import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts' |
| | | import NodeHandler from '../NodeHandler.vue' |
| | | import { useNodeName2, useWatchNode, useTaskStatusClass } from '../node' |
| | | import DelayTimerNodeConfig from '../nodes-config/DelayTimerNodeConfig.vue' |
| | | defineOptions({ |
| | | name: 'DelayTimerNode' |
| | | }) |
| | | const props = defineProps({ |
| | | flowNode: { |
| | | type: Object as () => SimpleFlowNode, |
| | | required: true |
| | | } |
| | | }) |
| | | // 定义事件,更新父组件。 |
| | | const emits = defineEmits<{ |
| | | 'update:flowNode': [node: SimpleFlowNode | undefined] |
| | | }>() |
| | | // 是否只读 |
| | | const readonly = inject<Boolean>('readonly') |
| | | // 监控节点的变化 |
| | | const currentNode = useWatchNode(props) |
| | | // 节点名称编辑 |
| | | const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.DELAY_TIMER_NODE) |
| | | |
| | | const nodeSetting = ref() |
| | | // 打开节点配置 |
| | | const openNodeConfig = () => { |
| | | if (readonly) { |
| | | return |
| | | } |
| | | nodeSetting.value.showDelayTimerNodeConfig(currentNode.value) |
| | | nodeSetting.value.openDrawer() |
| | | } |
| | | |
| | | // 删除节点。更新当前节点为孩子节点 |
| | | const deleteNode = () => { |
| | | emits('update:flowNode', currentNode.value.childNode) |
| | | } |
| | | </script> |
| | | |
| | | <style lang="scss" scoped></style> |
| | |
| | | height: 100%; |
| | | padding-top: 32px; |
| | | background-color: #fafafa; |
| | | overflow-x: auto; |
| | | width: 100%; |
| | | |
| | | .simple-process-model { |
| | | display: flex; |
| | | flex-direction: column; |
| | | justify-content: center; |
| | | align-items: center; |
| | | transform-origin: 50% 0 0; |
| | | overflow: auto; |
| | | min-width: fit-content; |
| | | transform: scale(1); |
| | | transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); |
| | | background: url(@/assets/svgs/bpm/simple-process-bg.svg) 0 0 repeat; |
| | |
| | | .branch-node-container { |
| | | position: relative; |
| | | display: flex; |
| | | min-width: fit-content; |
| | | |
| | | &::before { |
| | | position: absolute; |
| | |
| | | background: transparent; |
| | | border-top: 2px solid #dedede; |
| | | border-bottom: 2px solid #dedede; |
| | | flex-shrink: 0; |
| | | |
| | | &::before { |
| | | position: absolute; |
| | |
| | | </Dialog> |
| | | </template> |
| | | <script lang="ts" setup> |
| | | import { defaultProps, findTreeNode, handleTree } from '@/utils/tree' |
| | | import { defaultProps, handleTree } from '@/utils/tree' |
| | | import * as DeptApi from '@/api/system/dept' |
| | | import * as UserApi from '@/api/system/user' |
| | | |
| | |
| | | const { t } = useI18n() // 国际 |
| | | const message = useMessage() // 消息弹窗 |
| | | const deptTree = ref<Tree[]>([]) // 部门树形结构化 |
| | | const deptList = ref<any[]>([]) // 保存扁平化的部门列表数据 |
| | | const userList = ref<UserApi.UserVO[]>([]) // 所有用户列表 |
| | | const filteredUserList = ref<UserApi.UserVO[]>([]) // 当前部门过滤后的用户列表 |
| | | const selectedUserIdList: any = ref([]) // 选中的用户列表 |
| | |
| | | resetForm() |
| | | |
| | | // 加载部门、用户列表 |
| | | deptTree.value = handleTree(await DeptApi.getSimpleDeptList()) |
| | | const deptData = await DeptApi.getSimpleDeptList() |
| | | deptList.value = deptData // 保存扁平结构的部门数据 |
| | | deptTree.value = handleTree(deptData) // 转换成树形结构 |
| | | userList.value = await UserApi.getSimpleUserList() |
| | | |
| | | // 初始状态下,过滤列表等于所有用户列表 |
| | |
| | | dialogVisible.value = true |
| | | } |
| | | |
| | | /** 获取指定部门及其所有子部门的ID列表 */ |
| | | const getChildDeptIds = (deptId: number, deptList: any[]): number[] => { |
| | | const ids = [deptId] |
| | | const children = deptList.filter((dept) => dept.parentId === deptId) |
| | | children.forEach((child) => { |
| | | ids.push(...getChildDeptIds(child.id, deptList)) |
| | | }) |
| | | return ids |
| | | } |
| | | |
| | | /** 获取部门过滤后的用户列表 */ |
| | | const getUserList = async (deptId?: number) => { |
| | | const filterUserList = async (deptId?: number) => { |
| | | formLoading.value = true |
| | | try { |
| | | // @ts-ignore |
| | | // TODO @芋艿:替换到 simple List 暂不支持 deptId 过滤 |
| | | // TODO @Zqqq:这个,可以使用前端过滤么?通过 deptList 获取到 deptId 子节点,然后去 userList |
| | | const data = await UserApi.getUserPage({ pageSize: 100, pageNo: 1, deptId }) |
| | | // 更新过滤后的用户列表 |
| | | filteredUserList.value = data.list |
| | | if (!deptId) { |
| | | // 如果没有选择部门,显示所有用户 |
| | | filteredUserList.value = [...userList.value] |
| | | return |
| | | } |
| | | |
| | | // 直接使用已保存的部门列表数据进行过滤 |
| | | const deptIds = getChildDeptIds(deptId, deptList.value) |
| | | |
| | | // 过滤出这些部门下的用户 |
| | | filteredUserList.value = userList.value.filter((user) => deptIds.includes(user.deptId)) |
| | | } finally { |
| | | formLoading.value = false |
| | | } |
| | |
| | | /** 重置表单 */ |
| | | const resetForm = () => { |
| | | deptTree.value = [] |
| | | deptList.value = [] |
| | | userList.value = [] |
| | | filteredUserList.value = [] |
| | | selectedUserIdList.value = [] |
| | |
| | | |
| | | /** 处理部门被点击 */ |
| | | const handleNodeClick = (row: { [key: string]: any }) => { |
| | | getUserList(row.id) |
| | | filterUserList(row.id) |
| | | } |
| | | |
| | | defineExpose({ open }) // 提供 open 方法,用于打开弹窗 |
| | |
| | | <XButton preIcon="ep:refresh" @click="processRestart()" /> |
| | | </el-tooltip> |
| | | </ElButtonGroup> |
| | | <XButton |
| | | preIcon="ep:plus" |
| | | title="保存模型" |
| | | @click="processSave" |
| | | :type="props.headerButtonType" |
| | | :disabled="simulationStatus" |
| | | /> |
| | | </template> |
| | | <!-- 用于打开本地文件--> |
| | | <input |
| | |
| | | ['default', 'primary', 'success', 'warning', 'danger', 'info'].indexOf(value) !== -1 |
| | | } |
| | | }) |
| | | |
| | | // 监听value变化,重新加载流程图 |
| | | watch( |
| | | () => props.value, |
| | | (newValue) => { |
| | | if (newValue && bpmnModeler) { |
| | | createNewDiagram(newValue) |
| | | } |
| | | }, |
| | | { immediate: true } |
| | | ) |
| | | |
| | | // 监听processId和processName变化 |
| | | watch( |
| | | [() => props.processId, () => props.processName], |
| | | ([newId, newName]) => { |
| | | if (newId && newName && !props.value) { |
| | | createNewDiagram(null) |
| | | } |
| | | }, |
| | | { immediate: true } |
| | | ) |
| | | |
| | | provide('configGlobal', props) |
| | | let bpmnModeler: any = null |
| | |
| | | defaultZoom.value = newZoom |
| | | bpmnModeler.get('canvas').zoom(defaultZoom.value) |
| | | } |
| | | // const processZoomTo = (newZoom = 1) => { |
| | | // if (newZoom < 0.2) { |
| | | // throw new Error('[Process Designer Warn ]: The zoom ratio cannot be less than 0.2') |
| | | // } |
| | | // if (newZoom > 4) { |
| | | // throw new Error('[Process Designer Warn ]: The zoom ratio cannot be greater than 4') |
| | | // } |
| | | // defaultZoom = newZoom |
| | | // bpmnModeler.get('canvas').zoom(newZoom) |
| | | // } |
| | | const processReZoom = () => { |
| | | defaultZoom.value = 1 |
| | | bpmnModeler.get('canvas').zoom('fit-viewport', 'auto') |
| | |
| | | } |
| | | const previewProcessJson = () => { |
| | | bpmnModeler.saveXML({ format: true }).then(({ xml }) => { |
| | | // console.log(xml, 'xml') |
| | | |
| | | // const rootNode = parseXmlString(xml) |
| | | // console.log(rootNode, 'rootNoderootNode') |
| | | const rootNodes = new XmlNode(XmlNodeType.Root, parseXmlString(xml)) |
| | | // console.log(rootNodes, 'rootNodesrootNodesrootNodes') |
| | | // console.log(rootNodes.parent.toJsObject(), 'rootNodes.toJSON()') |
| | | // console.log(JSON.stringify(rootNodes.parent.toJsObject()), 'rootNodes.toJSON()') |
| | | // console.log(JSON.stringify(rootNodes.parent.toJSON()), 'rootNodes.toJSON()') |
| | | |
| | | // const parser = new xml2js.XMLParser() |
| | | // let jObj = parser.parse(xml) |
| | | // console.log(jObj, 'jObjjObjjObjjObjjObj') |
| | | // const builder = new xml2js.XMLBuilder(xml) |
| | | // const xmlContent = builder |
| | | // console.log(xmlContent, 'xmlContent') |
| | | // console.log(xml2js, 'convertconvertconvert') |
| | | previewResult.value = rootNodes.parent?.toJSON() as unknown as string |
| | | // previewResult.value = jObj |
| | | // previewResult.value = convert.xml2json(xml, {explicitArray : false},{ spaces: 2 }) |
| | | previewType.value = 'json' |
| | | previewModelVisible.value = true |
| | | }) |
| | | } |
| | | |
| | | /* ------------------------------------------------ 芋道源码 methods ------------------------------------------------------ */ |
| | | const processSave = async () => { |
| | | // console.log(bpmnModeler, 'bpmnModelerbpmnModelerbpmnModelerbpmnModeler') |
| | | const { err, xml } = await bpmnModeler.saveXML() |
| | | // console.log(err, 'errerrerrerrerr') |
| | | // console.log(xml, 'xmlxmlxmlxmlxml') |
| | | // 读取异常时抛出异常 |
| | | if (err) { |
| | | // this.$modal.msgError('保存模型失败,请重试!') |
| | | alert('保存模型失败,请重试!') |
| | | return |
| | | } |
| | | // 触发 save 事件 |
| | | emit('save', xml) |
| | | } |
| | | /** 高亮显示 */ |
| | | // const highlightedCode = (previewType, previewResult) => { |
| | | // console.log(previewType, 'previewType, previewResult') |
| | | // console.log(previewResult, 'previewType, previewResult') |
| | | // console.log(hljs.highlight, 'hljs.highlight') |
| | | // const result = hljs.highlight(previewType, previewResult.value || '', true) |
| | | // return result.value || ' ' |
| | | // } |
| | | onBeforeMount(() => { |
| | | console.log(props, 'propspropspropsprops') |
| | | }) |
| | | onMounted(() => { |
| | | initBpmnModeler() |
| | | createNewDiagram(props.value) |
| | | }) |
| | | onBeforeUnmount(() => { |
| | | // this.$once('hook:beforeDestroy', () => { |
| | | // }) |
| | | if (bpmnModeler) bpmnModeler.destroy() |
| | | emit('destroy', bpmnModeler) |
| | | bpmnModeler = null |
| | |
| | | <template> |
| | | <div class="process-panel__container" :style="{ width: `${width}px`, maxHeight: '700px' }"> |
| | | <el-collapse v-model="activeTab"> |
| | | <div class="process-panel__container" :style="{ width: `${width}px` }"> |
| | | <el-collapse v-model="activeTab" v-if="isReady"> |
| | | <el-collapse-item name="base"> |
| | | <!-- class="panel-tab__title" --> |
| | | <template #title> |
| | |
| | | </el-collapse-item> |
| | | <el-collapse-item name="task" v-if="isTaskCollapseItemShow(elementType)" key="task"> |
| | | <template #title |
| | | ><Icon icon="ep:checked" />{{ getTaskCollapseItemName(elementType) }}</template |
| | | ><Icon icon="ep:checked" />{{ getTaskCollapseItemName(elementType) }}</template |
| | | > |
| | | <element-task :id="elementId" :type="elementType" /> |
| | | </el-collapse-item> |
| | |
| | | const conditionFormVisible = ref(false) // 流转条件设置 |
| | | const formVisible = ref(false) // 表单配置 |
| | | const bpmnElement = ref() |
| | | const isReady = ref(false) |
| | | |
| | | provide('prefix', props.prefix) |
| | | provide('width', props.width) |
| | | const bpmnInstances = () => (window as any)?.bpmnInstances |
| | | |
| | | // 监听 props.bpmnModeler 然后 initModels |
| | | const unwatchBpmn = watch( |
| | | () => props.bpmnModeler, |
| | | () => { |
| | | // 避免加载时 流程图 并未加载完成 |
| | | if (!props.bpmnModeler) { |
| | | console.log('缺少props.bpmnModeler') |
| | | return |
| | | } |
| | | |
| | | console.log('props.bpmnModeler 有值了!!!') |
| | | const w = window as any |
| | | w.bpmnInstances = { |
| | | // 初始化 bpmnInstances |
| | | const initBpmnInstances = () => { |
| | | if (!props.bpmnModeler) return false |
| | | try { |
| | | const instances = { |
| | | modeler: props.bpmnModeler, |
| | | modeling: props.bpmnModeler.get('modeling'), |
| | | moddle: props.bpmnModeler.get('moddle'), |
| | |
| | | selection: props.bpmnModeler.get('selection') |
| | | } |
| | | |
| | | console.log(bpmnInstances(), 'window.bpmnInstances') |
| | | getActiveElement() |
| | | unwatchBpmn() |
| | | // 检查所有实例是否都存在 |
| | | const allInstancesExist = Object.values(instances).every(instance => instance) |
| | | if (allInstancesExist) { |
| | | const w = window as any |
| | | w.bpmnInstances = instances |
| | | return true |
| | | } |
| | | return false |
| | | } catch (error) { |
| | | console.error('初始化 bpmnInstances 失败:', error) |
| | | return false |
| | | } |
| | | } |
| | | |
| | | const bpmnInstances = () => (window as any)?.bpmnInstances |
| | | |
| | | // 监听 props.bpmnModeler 然后 initModels |
| | | const unwatchBpmn = watch( |
| | | () => props.bpmnModeler, |
| | | async () => { |
| | | // 避免加载时 流程图 并未加载完成 |
| | | if (!props.bpmnModeler) { |
| | | console.log('缺少props.bpmnModeler') |
| | | return |
| | | } |
| | | |
| | | try { |
| | | // 等待 modeler 初始化完成 |
| | | await nextTick() |
| | | if (initBpmnInstances()) { |
| | | isReady.value = true |
| | | await nextTick() |
| | | getActiveElement() |
| | | } else { |
| | | console.error('modeler 实例未完全初始化') |
| | | } |
| | | } catch (error) { |
| | | console.error('初始化失败:', error) |
| | | } |
| | | }, |
| | | { |
| | | immediate: true |
| | |
| | | ) |
| | | |
| | | const getActiveElement = () => { |
| | | if (!isReady.value || !props.bpmnModeler) return |
| | | |
| | | // 初始第一个选中元素 bpmn:Process |
| | | initFormOnChanged(null) |
| | | props.bpmnModeler.on('import.done', (e) => { |
| | |
| | | } |
| | | }) |
| | | } |
| | | |
| | | // 初始化数据 |
| | | const initFormOnChanged = (element) => { |
| | | if (!isReady.value || !bpmnInstances()) return |
| | | |
| | | let activatedElement = element |
| | | if (!activatedElement) { |
| | | activatedElement = |
| | |
| | | bpmnInstances().elementRegistry.find((el) => el.type === 'bpmn:Collaboration') |
| | | } |
| | | if (!activatedElement) return |
| | | console.log(` |
| | | ---------- |
| | | select element changed: |
| | | id: ${activatedElement.id} |
| | | type: ${activatedElement.businessObject.$type} |
| | | ---------- |
| | | `) |
| | | console.log('businessObject: ', activatedElement.businessObject) |
| | | bpmnInstances().bpmnElement = activatedElement |
| | | bpmnElement.value = activatedElement |
| | | elementId.value = activatedElement.id |
| | | elementType.value = activatedElement.type.split(':')[1] || '' |
| | | elementBusinessObject.value = JSON.parse(JSON.stringify(activatedElement.businessObject)) |
| | | conditionFormVisible.value = !!( |
| | | elementType.value === 'SequenceFlow' && |
| | | activatedElement.source && |
| | | activatedElement.source.type.indexOf('StartEvent') === -1 |
| | | ) |
| | | formVisible.value = elementType.value === 'UserTask' || elementType.value === 'StartEvent' |
| | | |
| | | try { |
| | | console.log(` |
| | | ---------- |
| | | select element changed: |
| | | id: ${activatedElement.id} |
| | | type: ${activatedElement.businessObject.$type} |
| | | ---------- |
| | | `) |
| | | console.log('businessObject: ', activatedElement.businessObject) |
| | | bpmnInstances().bpmnElement = activatedElement |
| | | bpmnElement.value = activatedElement |
| | | elementId.value = activatedElement.id |
| | | elementType.value = activatedElement.type.split(':')[1] || '' |
| | | elementBusinessObject.value = JSON.parse(JSON.stringify(activatedElement.businessObject)) |
| | | conditionFormVisible.value = !!( |
| | | elementType.value === 'SequenceFlow' && |
| | | activatedElement.source && |
| | | activatedElement.source.type.indexOf('StartEvent') === -1 |
| | | ) |
| | | formVisible.value = elementType.value === 'UserTask' || elementType.value === 'StartEvent' |
| | | } catch (error) { |
| | | console.error('初始化表单数据失败:', error) |
| | | } |
| | | } |
| | | |
| | | onBeforeUnmount(() => { |
| | | const w = window as any |
| | | w.bpmnInstances = null |
| | | console.log(props, 'props1') |
| | | console.log(props.bpmnModeler, 'props.bpmnModeler1') |
| | | isReady.value = false |
| | | }) |
| | | |
| | | watch( |
| | |
| | | @import 'bpmn-js-token-simulation/assets/css/bpmn-js-token-simulation.css'; |
| | | @import 'bpmn-js-token-simulation/assets/css/font-awesome.min.css'; |
| | | @import 'bpmn-js-token-simulation/assets/css/normalize.css'; |
| | | @use 'bpmn-js-token-simulation/assets/css/bpmn-js-token-simulation.css'; |
| | | |
| | | // 边框被 token-simulation 样式覆盖了 |
| | | .djs-palette { |
| | |
| | | height: 100%; |
| | | position: relative; |
| | | background: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImEiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdGggZD0iTTAgMTBoNDBNMTAgMHY0ME0wIDIwaDQwTTIwIDB2NDBNMCAzMGg0ME0zMCAwdjQwIiBmaWxsPSJub25lIiBzdHJva2U9IiNlMGUwZTAiIG9wYWNpdHk9Ii4yIi8+PHBhdGggZD0iTTQwIDBIMHY0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZTBlMGUwIi8+PC9wYXR0ZXJuPjwvZGVmcz48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2EpIi8+PC9zdmc+') |
| | | repeat !important; |
| | | repeat !important; |
| | | div.toggle-mode { |
| | | display: none; |
| | | } |
| | |
| | | box-sizing: border-box; |
| | | } |
| | | } |
| | | svg { |
| | | width: 100%; |
| | | height: 100%; |
| | | min-height: 100%; |
| | | overflow: hidden; |
| | | } |
| | | // svg { |
| | | // width: 100%; |
| | | // height: 100%; |
| | | // min-height: 100%; |
| | | // overflow: hidden; |
| | | // } |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | export function hasPermi(app: App<Element>) { |
| | | app.directive('hasPermi', (el, binding) => { |
| | | const { wsCache } = useCache() |
| | | const { value } = binding |
| | | const all_permission = '*:*:*' |
| | | const userInfo = wsCache.get(CACHE_KEY.USER) |
| | | const permissions = userInfo?.permissions || [] |
| | | |
| | | if (value && value instanceof Array && value.length > 0) { |
| | | const permissionFlag = value |
| | | |
| | | const hasPermissions = permissions.some((permission: string) => { |
| | | return all_permission === permission || permissionFlag.includes(permission) |
| | | }) |
| | | const hasPermissions = hasPermission(value) |
| | | |
| | | if (!hasPermissions) { |
| | | el.parentNode && el.parentNode.removeChild(el) |
| | |
| | | } |
| | | }) |
| | | } |
| | | |
| | | export const hasPermission = (permission: string[]) => { |
| | | const { wsCache } = useCache() |
| | | const all_permission = '*:*:*' |
| | | const userInfo = wsCache.get(CACHE_KEY.USER) |
| | | const permissions = userInfo?.permissions || [] |
| | | |
| | | return permissions.some((p: string) => { |
| | | return all_permission === p || permission.includes(p) |
| | | }) |
| | | } |
| | |
| | | const appStore = useAppStore() |
| | | |
| | | const title = computed(() => appStore.getTitle) |
| | | |
| | | |
| | | // 添加当前年份计算属性 |
| | | const currentYear = computed(() => new Date().getFullYear()) |
| | | </script> |
| | | |
| | | <template> |
| | |
| | | :class="prefixCls" |
| | | class="h-[var(--app-footer-height)] bg-[var(--app-content-bg-color)] text-center leading-[var(--app-footer-height)] text-[var(--el-text-color-placeholder)] dark:bg-[var(--el-bg-color)] overflow-hidden" |
| | | > |
| | | <span class="text-14px">Copyright ©2022-{{ title }}</span> |
| | | <span class="text-14px">Copyright ©{{ currentYear }} {{ title }}</span> |
| | | </div> |
| | | </template> |
| | |
| | | title: '查看 OA 请假', |
| | | activeMenu: '/bpm/oa/leave' |
| | | } |
| | | }, |
| | | { |
| | | path: 'manager/model/create', |
| | | component: () => import('@/views/bpm/model/form/index.vue'), |
| | | name: 'BpmModelCreate', |
| | | meta: { |
| | | noCache: true, |
| | | hidden: true, |
| | | canTo: true, |
| | | title: '创建流程', |
| | | activeMenu: '/bpm/manager/model' |
| | | } |
| | | }, |
| | | { |
| | | path: 'manager/model/update/:id', |
| | | component: () => import('@/views/bpm/model/form/index.vue'), |
| | | name: 'BpmModelUpdate', |
| | | meta: { |
| | | noCache: true, |
| | | hidden: true, |
| | | canTo: true, |
| | | title: '修改流程', |
| | | activeMenu: '/bpm/manager/model' |
| | | } |
| | | } |
| | | ] |
| | | }, |
| | |
| | | noCache: !route.keepAlive, |
| | | alwaysShow: |
| | | route.children && |
| | | route.children.length === 1 && |
| | | route.children.length > 0 && |
| | | (route.alwaysShow !== undefined ? route.alwaysShow : true) |
| | | } as any |
| | | // 特殊逻辑:如果后端配置的 MenuDO.component 包含 ?,则表示需要传递参数 |
| | |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 模型列表 --> |
| | | <el-collapse-transition> |
| | | <div v-show="isExpand"> |
| | |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="可见范围" prop="startUserIds" min-width="100"> |
| | | <el-table-column label="可见范围" prop="startUserIds" min-width="150"> |
| | | <template #default="scope"> |
| | | <el-text v-if="!scope.row.startUsers || scope.row.startUsers.length === 0"> |
| | | 全部可见 |
| | |
| | | </el-text> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="表单信息" prop="formType" min-width="200"> |
| | | <el-table-column label="表单信息" prop="formType" min-width="150"> |
| | | <template #default="scope"> |
| | | <el-button |
| | | v-if="scope.row.formType === BpmModelFormType.NORMAL" |
| | |
| | | :disabled="!isManagerUser(scope.row)" |
| | | > |
| | | 修改 |
| | | </el-button> |
| | | <el-button |
| | | link |
| | | class="!ml-5px" |
| | | type="primary" |
| | | @click="handleDesign(scope.row)" |
| | | v-hasPermi="['bpm:model:update']" |
| | | :disabled="!isManagerUser(scope.row)" |
| | | > |
| | | 设计 |
| | | </el-button> |
| | | <el-button |
| | | link |
| | |
| | | </template> |
| | | </Dialog> |
| | | |
| | | <!-- 弹窗:表单详情 --> |
| | | <Dialog title="表单详情" v-model="formDetailVisible" width="800"> |
| | | <form-create :rule="formDetailPreview.rule" :option="formDetailPreview.option" /> |
| | | </Dialog> |
| | | |
| | | <!-- 表单弹窗:添加流程模型 --> |
| | | <ModelForm :categoryId="categoryInfo.code" ref="modelFormRef" @success="emit('success')" /> |
| | | </template> |
| | |
| | | import * as ModelApi from '@/api/bpm/model' |
| | | import * as FormApi from '@/api/bpm/form' |
| | | import { setConfAndFields2 } from '@/utils/formCreate' |
| | | import { BpmModelFormType, BpmModelType } from '@/utils/constants' |
| | | import { BpmModelFormType } from '@/utils/constants' |
| | | import { checkPermi } from '@/utils/permission' |
| | | import { useUserStoreWithOut } from '@/store/modules/user' |
| | | import { useAppStore } from '@/store/modules/app' |
| | |
| | | // 刷新列表 |
| | | emit('success') |
| | | } catch {} |
| | | } |
| | | |
| | | /** 设计流程 */ |
| | | const handleDesign = (row: any) => { |
| | | if (row.type == BpmModelType.BPMN) { |
| | | push({ |
| | | name: 'BpmModelEditor', |
| | | query: { |
| | | modelId: row.id |
| | | } |
| | | }) |
| | | } else { |
| | | push({ |
| | | name: 'SimpleModelDesign', |
| | | query: { |
| | | modelId: row.id |
| | | } |
| | | }) |
| | | } |
| | | } |
| | | |
| | | /** 发布流程 */ |
| | |
| | | /** 添加流程模型弹窗 */ |
| | | const modelFormRef = ref() |
| | | const openModelForm = (type: string, id?: number) => { |
| | | modelFormRef.value.open(type, id) |
| | | if (type === 'create') { |
| | | push({ name: 'BpmModelCreate' }) |
| | | } else { |
| | | push({ |
| | | name: 'BpmModelUpdate', |
| | | params: { id } |
| | | }) |
| | | } |
| | | } |
| | | |
| | | watch(() => props.categoryInfo.modelList, updateModeList, { immediate: true }) |
| | |
| | | </el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="谁可以发起" prop="startUserIds"> |
| | | <el-form-item label="谁可以发起" prop="startUserType"> |
| | | <el-select |
| | | v-model="formData.startUserIds" |
| | | multiple |
| | | placeholder="请选择可发起人,默认(不选择)则所有人都可以发起" |
| | | v-model="formData.startUserType" |
| | | placeholder="请选择谁可以发起" |
| | | @change="handleStartUserTypeChange" |
| | | > |
| | | <el-option |
| | | v-for="user in userList" |
| | | :key="user.id" |
| | | :label="user.nickname" |
| | | :value="user.id" |
| | | /> |
| | | <el-option label="全员" :value="0" /> |
| | | <el-option label="指定人员" :value="1" /> |
| | | <el-option label="均不可提交" :value="2" /> |
| | | </el-select> |
| | | <div v-if="formData.startUserType === 1" class="mt-2 flex flex-wrap gap-2"> |
| | | <div |
| | | v-for="user in selectedStartUsers" |
| | | :key="user.id" |
| | | class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative" |
| | | > |
| | | <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" /> |
| | | <el-avatar class="!m-5px" :size="28" v-else> |
| | | {{ user.nickname.substring(0, 1) }} |
| | | </el-avatar> |
| | | {{ user.nickname }} |
| | | <Icon |
| | | icon="ep:close" |
| | | class="ml-2 cursor-pointer hover:text-red-500" |
| | | @click="handleRemoveStartUser(user)" |
| | | /> |
| | | </div> |
| | | <el-button type="primary" link @click="openStartUserSelect"> |
| | | <Icon icon="ep:plus" />选择人员 |
| | | </el-button> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="流程管理员" prop="managerUserIds"> |
| | | <el-select v-model="formData.managerUserIds" multiple placeholder="请选择流程管理员"> |
| | | <el-option |
| | | v-for="user in userList" |
| | | :key="user.id" |
| | | :label="user.nickname" |
| | | :value="user.id" |
| | | /> |
| | | <el-form-item label="流程管理员" prop="managerUserType"> |
| | | <el-select |
| | | v-model="formData.managerUserType" |
| | | placeholder="请选择流程管理员" |
| | | @change="handleManagerUserTypeChange" |
| | | > |
| | | <el-option label="全员" :value="0" /> |
| | | <el-option label="指定人员" :value="1" /> |
| | | <el-option label="均不可提交" :value="2" /> |
| | | </el-select> |
| | | <div v-if="formData.managerUserType === 1" class="mt-2 flex flex-wrap gap-2"> |
| | | <div |
| | | v-for="user in selectedManagerUsers" |
| | | :key="user.id" |
| | | class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative" |
| | | > |
| | | <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" /> |
| | | <el-avatar class="!m-5px" :size="28" v-else> |
| | | {{ user.nickname.substring(0, 1) }} |
| | | </el-avatar> |
| | | {{ user.nickname }} |
| | | <Icon |
| | | icon="ep:close" |
| | | class="ml-2 cursor-pointer hover:text-red-500" |
| | | @click="handleRemoveManagerUser(user)" |
| | | /> |
| | | </div> |
| | | <el-button type="primary" link @click="openManagerUserSelect"> |
| | | <Icon icon="ep:plus" />选择人员 |
| | | </el-button> |
| | | </div> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | |
| | | <el-button @click="dialogVisible = false">取 消</el-button> |
| | | </template> |
| | | </Dialog> |
| | | <UserSelectForm ref="userSelectFormRef" @confirm="handleUserSelectConfirm" /> |
| | | </template> |
| | | <script lang="ts" setup> |
| | | import { propTypes } from '@/utils/propTypes' |
| | |
| | | import { ElMessageBox } from 'element-plus' |
| | | import * as ModelApi from '@/api/bpm/model' |
| | | import * as FormApi from '@/api/bpm/form' |
| | | import { CategoryApi } from '@/api/bpm/category' |
| | | import { CategoryApi, CategoryVO } from '@/api/bpm/category' |
| | | import { BpmModelFormType, BpmModelType } from '@/utils/constants' |
| | | import { UserVO } from '@/api/system/user' |
| | | import * as UserApi from '@/api/system/user' |
| | | import { useUserStoreWithOut } from '@/store/modules/user' |
| | | import { FormVO } from '@/api/bpm/form' |
| | | |
| | | defineOptions({ name: 'ModelForm' }) |
| | | |
| | |
| | | const dialogTitle = ref('') // 弹窗的标题 |
| | | const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 |
| | | const formType = ref('') // 表单的类型:create - 新增;update - 修改 |
| | | const formData = ref({ |
| | | const formData: any = ref({ |
| | | id: undefined, |
| | | name: '', |
| | | key: '', |
| | |
| | | formCustomCreatePath: '', |
| | | formCustomViewPath: '', |
| | | visible: true, |
| | | startUserType: undefined, |
| | | managerUserType: undefined, |
| | | startUserIds: [], |
| | | managerUserIds: [] |
| | | }) |
| | |
| | | managerUserIds: [{ required: true, message: '流程管理员不能为空', trigger: 'blur' }] |
| | | }) |
| | | const formRef = ref() // 表单 Ref |
| | | const formList = ref([]) // 流程表单的下拉框的数据 |
| | | const categoryList = ref([]) // 流程分类列表 |
| | | const formList = ref<FormVO[]>([]) // 流程表单的下拉框的数据 |
| | | const categoryList = ref<CategoryVO[]>([]) // 流程分类列表 |
| | | const userList = ref<UserVO[]>([]) // 用户列表 |
| | | const selectedStartUsers = ref<UserVO[]>([]) // 已选择的发起人列表 |
| | | const selectedManagerUsers = ref<UserVO[]>([]) // 已选择的管理员列表 |
| | | const userSelectFormRef = ref() // 用户选择弹窗 ref |
| | | const currentSelectType = ref<'start' | 'manager'>('start') // 当前选择的是发起人还是管理员 |
| | | |
| | | /** 打开弹窗 */ |
| | | const open = async (type: string, id?: string) => { |
| | |
| | | formData.value = await ModelApi.getModel(id) |
| | | } finally { |
| | | formLoading.value = false |
| | | } |
| | | // 加载数据时,根据已有的用户ID列表初始化已选用户 |
| | | if (formData.value.startUserIds?.length) { |
| | | formData.value.startUserType = 1 |
| | | selectedStartUsers.value = userList.value.filter((user) => |
| | | formData.value.startUserIds.includes(user.id) |
| | | ) |
| | | } |
| | | if (formData.value.managerUserIds?.length) { |
| | | formData.value.managerUserType = 1 |
| | | selectedManagerUsers.value = userList.value.filter((user) => |
| | | formData.value.managerUserIds.includes(user.id) |
| | | ) |
| | | } |
| | | } else { |
| | | formData.value.managerUserIds.push(userStore.getUser.id) |
| | |
| | | // 提示,引导用户做后续的操作 |
| | | await ElMessageBox.alert( |
| | | '<strong>新建模型成功!</strong>后续需要执行如下 2 个步骤:' + |
| | | '<div>1. 点击【设计流程】按钮,绘制流程图</div>' + |
| | | '<div>2. 点击【发布流程】按钮,完成流程的最终发布</div>' + |
| | | '另外,每次流程修改后,都需要点击【发布流程】按钮,才能正式生效!!!', |
| | | '<div>1. 点击【设计流程】按钮,绘制流程图</div>' + |
| | | '<div>2. 点击【发布流程】按钮,完成流程的最终发布</div>' + |
| | | '另外,每次流程修改后,都需要点击【发布流程】按钮,才能正式生效!!!', |
| | | '重要提示', |
| | | { |
| | | dangerouslyUseHTMLString: true, |
| | |
| | | formCustomCreatePath: '', |
| | | formCustomViewPath: '', |
| | | visible: true, |
| | | startUserType: undefined, |
| | | managerUserType: undefined, |
| | | startUserIds: [], |
| | | managerUserIds: [] |
| | | } |
| | | formRef.value?.resetFields() |
| | | selectedStartUsers.value = [] |
| | | selectedManagerUsers.value = [] |
| | | } |
| | | |
| | | /** 处理发起人类型变化 */ |
| | | const handleStartUserTypeChange = (value: number) => { |
| | | if (value !== 1) { |
| | | selectedStartUsers.value = [] |
| | | formData.value.startUserIds = [] |
| | | } |
| | | } |
| | | |
| | | /** 处理管理员类型变化 */ |
| | | const handleManagerUserTypeChange = (value: number) => { |
| | | if (value !== 1) { |
| | | selectedManagerUsers.value = [] |
| | | formData.value.managerUserIds = [] |
| | | } |
| | | } |
| | | |
| | | /** 打开发起人选择 */ |
| | | const openStartUserSelect = () => { |
| | | currentSelectType.value = 'start' |
| | | userSelectFormRef.value.open(0, selectedStartUsers.value) |
| | | } |
| | | |
| | | /** 打开管理员选择 */ |
| | | const openManagerUserSelect = () => { |
| | | currentSelectType.value = 'manager' |
| | | userSelectFormRef.value.open(0, selectedManagerUsers.value) |
| | | } |
| | | |
| | | /** 处理用户选择确认 */ |
| | | const handleUserSelectConfirm = (_, users: UserVO[]) => { |
| | | if (currentSelectType.value === 'start') { |
| | | selectedStartUsers.value = users |
| | | formData.value.startUserIds = users.map((u) => u.id) |
| | | } else { |
| | | selectedManagerUsers.value = users |
| | | formData.value.managerUserIds = users.map((u) => u.id) |
| | | } |
| | | } |
| | | |
| | | /** 移除发起人 */ |
| | | const handleRemoveStartUser = (user: UserVO) => { |
| | | selectedStartUsers.value = selectedStartUsers.value.filter((u) => u.id !== user.id) |
| | | formData.value.startUserIds = formData.value.startUserIds.filter((id: number) => id !== user.id) |
| | | } |
| | | |
| | | /** 移除管理员 */ |
| | | const handleRemoveManagerUser = (user: UserVO) => { |
| | | selectedManagerUsers.value = selectedManagerUsers.value.filter((u) => u.id !== user.id) |
| | | formData.value.managerUserIds = formData.value.managerUserIds.filter( |
| | | (id: number) => id !== user.id |
| | | ) |
| | | } |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .bg-gray-100 { |
| | | background-color: #f5f7fa; |
| | | transition: all 0.3s; |
| | | |
| | | &:hover { |
| | | background-color: #e6e8eb; |
| | | } |
| | | |
| | | .ep-close { |
| | | font-size: 14px; |
| | | color: #909399; |
| | | transition: color 0.3s; |
| | | |
| | | &:hover { |
| | | color: #f56c6c; |
| | | } |
| | | } |
| | | } |
| | | </style> |
| | |
| | | <!-- 流程设计器,负责绘制流程等 --> |
| | | <MyProcessDesigner |
| | | key="designer" |
| | | v-if="xmlString !== undefined" |
| | | v-model="xmlString" |
| | | :value="xmlString" |
| | | v-bind="controlForm" |
| | |
| | | ref="processDesigner" |
| | | @init-finished="initModeler" |
| | | :additionalModel="controlForm.additionalModel" |
| | | :model="model" |
| | | @save="save" |
| | | /> |
| | | <!-- 流程属性器,负责编辑每个流程节点的属性 --> |
| | | <MyProcessPenal |
| | | v-if="isModelerReady && modeler" |
| | | key="penal" |
| | | :bpmnModeler="modeler as any" |
| | | :bpmnModeler="modeler" |
| | | :prefix="controlForm.prefix" |
| | | class="process-panel" |
| | | :model="model" |
| | |
| | | // 自定义左侧菜单(修改 默认任务 为 用户任务) |
| | | import CustomPaletteProvider from '@/components/bpmnProcessDesigner/package/designer/plugins/palette' |
| | | import * as ModelApi from '@/api/bpm/model' |
| | | import { getForm, FormVO } from '@/api/bpm/form' |
| | | |
| | | defineOptions({ name: 'BpmModelEditor' }) |
| | | |
| | | const router = useRouter() // 路由 |
| | | const { query } = useRoute() // 路由的查询 |
| | | const props = defineProps<{ |
| | | modelId?: string |
| | | modelKey?: string |
| | | modelName?: string |
| | | value?: string |
| | | }>() |
| | | |
| | | const emit = defineEmits(['success', 'init-finished']) |
| | | const message = useMessage() // 国际化 |
| | | |
| | | // 表单信息 |
| | |
| | | provide('formFields', formFields) |
| | | provide('formType', formType) |
| | | |
| | | const xmlString = ref(undefined) // BPMN XML |
| | | const modeler = ref(null) // BPMN Modeler |
| | | const xmlString = ref<string>('') // BPMN XML |
| | | const modeler = shallowRef() // BPMN Modeler |
| | | const processDesigner = ref() |
| | | const isModelerReady = ref(false) |
| | | const controlForm = ref({ |
| | | simulation: true, |
| | | labelEditing: false, |
| | |
| | | }) |
| | | const model = ref<ModelApi.ModelVO>() // 流程模型的信息 |
| | | |
| | | // 初始化 bpmnInstances |
| | | const initBpmnInstances = () => { |
| | | if (!modeler.value) return false |
| | | try { |
| | | const instances = { |
| | | modeler: modeler.value, |
| | | modeling: modeler.value.get('modeling'), |
| | | moddle: modeler.value.get('moddle'), |
| | | eventBus: modeler.value.get('eventBus'), |
| | | bpmnFactory: modeler.value.get('bpmnFactory'), |
| | | elementFactory: modeler.value.get('elementFactory'), |
| | | elementRegistry: modeler.value.get('elementRegistry'), |
| | | replace: modeler.value.get('replace'), |
| | | selection: modeler.value.get('selection') |
| | | } |
| | | |
| | | // 检查所有实例是否都存在 |
| | | return Object.values(instances).every((instance) => instance) |
| | | } catch (error) { |
| | | console.error('初始化 bpmnInstances 失败:', error) |
| | | return false |
| | | } |
| | | } |
| | | |
| | | /** 初始化 modeler */ |
| | | const initModeler = (item) => { |
| | | setTimeout(() => { |
| | | const initModeler = async (item) => { |
| | | try { |
| | | modeler.value = item |
| | | }, 10) |
| | | // 等待 modeler 初始化完成 |
| | | await nextTick() |
| | | |
| | | // 确保 modeler 的所有实例都已经准备好 |
| | | if (initBpmnInstances()) { |
| | | isModelerReady.value = true |
| | | emit('init-finished') |
| | | |
| | | // 初始化完成后,设置初始值 |
| | | if (props.modelId) { |
| | | // 编辑模式 |
| | | const data = await ModelApi.getModel(props.modelId) |
| | | model.value = { |
| | | ...data, |
| | | bpmnXml: undefined // 清空 bpmnXml 属性 |
| | | } |
| | | xmlString.value = data.bpmnXml || getDefaultBpmnXml(data.key, data.name) |
| | | } else if (props.modelKey && props.modelName) { |
| | | // 新建模式 |
| | | xmlString.value = props.value || getDefaultBpmnXml(props.modelKey, props.modelName) |
| | | model.value = { |
| | | key: props.modelKey, |
| | | name: props.modelName |
| | | } as ModelApi.ModelVO |
| | | } |
| | | |
| | | // 导入XML并刷新视图 |
| | | await nextTick() |
| | | try { |
| | | await modeler.value.importXML(xmlString.value) |
| | | if (processDesigner.value?.refresh) { |
| | | processDesigner.value.refresh() |
| | | } |
| | | } catch (error) { |
| | | console.error('导入XML失败:', error) |
| | | } |
| | | } else { |
| | | console.error('modeler 实例未完全初始化') |
| | | } |
| | | } catch (error) { |
| | | console.error('初始化 modeler 失败:', error) |
| | | } |
| | | } |
| | | |
| | | /** 获取默认的BPMN XML */ |
| | | const getDefaultBpmnXml = (key: string, name: string) => { |
| | | return `<?xml version="1.0" encoding="UTF-8"?> |
| | | <definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.activiti.org/processdef"> |
| | | <process id="${key}" name="${name}" isExecutable="true" /> |
| | | <bpmndi:BPMNDiagram id="BPMNDiagram"> |
| | | <bpmndi:BPMNPlane id="${key}_di" bpmnElement="${key}" /> |
| | | </bpmndi:BPMNDiagram> |
| | | </definitions>` |
| | | } |
| | | |
| | | /** 添加/修改模型 */ |
| | | const save = async (bpmnXml: string) => { |
| | | const data = { |
| | | ...model.value, |
| | | bpmnXml: bpmnXml // bpmnXml 只是初始化流程图,后续修改无法通过它获得 |
| | | } as unknown as ModelApi.ModelVO |
| | | // 提交 |
| | | if (data.id) { |
| | | await ModelApi.updateModelBpmn(data) |
| | | message.success('修改成功') |
| | | } else { |
| | | await ModelApi.updateModelBpmn(data) |
| | | message.success('新增成功') |
| | | try { |
| | | xmlString.value = bpmnXml |
| | | if (props.modelId) { |
| | | // 编辑模式 |
| | | const data = { |
| | | ...model.value, |
| | | bpmnXml: bpmnXml |
| | | } as unknown as ModelApi.ModelVO |
| | | await ModelApi.updateModelBpmn(data) |
| | | emit('success') |
| | | } else { |
| | | // 新建模式,直接返回XML |
| | | emit('success', bpmnXml) |
| | | } |
| | | } catch (error) { |
| | | console.error('保存失败:', error) |
| | | message.error('保存失败') |
| | | } |
| | | // 跳转回去 |
| | | close() |
| | | } |
| | | |
| | | /** 关闭按钮 */ |
| | | const close = () => { |
| | | router.push({ path: '/bpm/manager/model' }) |
| | | // 监听 key、name 和 value 的变化 |
| | | watch( |
| | | [() => props.modelKey, () => props.modelName, () => props.value], |
| | | async ([newKey, newName, newValue]) => { |
| | | if (!props.modelId && isModelerReady.value) { |
| | | let shouldRefresh = false |
| | | |
| | | if (newKey && newName) { |
| | | const newXml = newValue || getDefaultBpmnXml(newKey, newName) |
| | | if (newXml !== xmlString.value) { |
| | | xmlString.value = newXml |
| | | shouldRefresh = true |
| | | } |
| | | model.value = { |
| | | ...model.value, |
| | | key: newKey, |
| | | name: newName |
| | | } as ModelApi.ModelVO |
| | | } else if (newValue && newValue !== xmlString.value) { |
| | | xmlString.value = newValue |
| | | shouldRefresh = true |
| | | } |
| | | |
| | | if (shouldRefresh) { |
| | | // 确保更新后重新渲染 |
| | | await nextTick() |
| | | if (processDesigner.value?.refresh) { |
| | | try { |
| | | await modeler.value?.importXML(xmlString.value) |
| | | processDesigner.value.refresh() |
| | | } catch (error) { |
| | | console.error('导入XML失败:', error) |
| | | } |
| | | } |
| | | } |
| | | } |
| | | }, |
| | | { deep: true } |
| | | ) |
| | | |
| | | // 在组件卸载时清理 |
| | | onBeforeUnmount(() => { |
| | | isModelerReady.value = false |
| | | modeler.value = null |
| | | // 清理全局实例 |
| | | const w = window as any |
| | | if (w.bpmnInstances) { |
| | | w.bpmnInstances = null |
| | | } |
| | | }) |
| | | |
| | | /** 获取 XML 字符串 */ |
| | | const saveXML = async () => { |
| | | if (!modeler.value) { |
| | | return { xml: xmlString.value } |
| | | } |
| | | try { |
| | | const result = await modeler.value.saveXML({ format: true }) |
| | | xmlString.value = result.xml |
| | | return result |
| | | } catch (error) { |
| | | console.error('获取XML失败:', error) |
| | | return { xml: xmlString.value } |
| | | } |
| | | } |
| | | |
| | | /** 初始化 */ |
| | | onMounted(async () => { |
| | | const modelId = query.modelId as unknown as number |
| | | if (!modelId) { |
| | | message.error('缺少模型 modelId 编号') |
| | | return |
| | | /** 获取SVG字符串 */ |
| | | const saveSVG = async () => { |
| | | if (!modeler.value) { |
| | | return { svg: undefined } |
| | | } |
| | | // 查询模型 |
| | | const data = await ModelApi.getModel(modelId) |
| | | if (!data.bpmnXml) { |
| | | // 首次创建的 Model 模型,它是没有 bpmnXml,此时需要给它一个默认的 |
| | | data.bpmnXml = ` <?xml version="1.0" encoding="UTF-8"?> |
| | | <definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.activiti.org/processdef"> |
| | | <process id="${data.key}" name="${data.name}" isExecutable="true" /> |
| | | <bpmndi:BPMNDiagram id="BPMNDiagram"> |
| | | <bpmndi:BPMNPlane id="${data.key}_di" bpmnElement="${data.key}" /> |
| | | </bpmndi:BPMNDiagram> |
| | | </definitions>` |
| | | try { |
| | | return await modeler.value.saveSVG() |
| | | } catch (error) { |
| | | console.error('获取SVG失败:', error) |
| | | return { svg: undefined } |
| | | } |
| | | } |
| | | |
| | | formType.value = data.formType |
| | | if (data.formType === 10) { |
| | | const bpmnForm = (await getForm(data.formId)) as unknown as FormVO |
| | | formFields.value = bpmnForm?.fields |
| | | /** 刷新视图 */ |
| | | const refresh = async () => { |
| | | if (processDesigner.value?.refresh && modeler.value) { |
| | | try { |
| | | await modeler.value.importXML(xmlString.value) |
| | | processDesigner.value.refresh() |
| | | } catch (error) { |
| | | console.error('刷新视图失败:', error) |
| | | } |
| | | } |
| | | } |
| | | |
| | | model.value = { |
| | | ...data, |
| | | bpmnXml: undefined // 清空 bpmnXml 属性 |
| | | } |
| | | xmlString.value = data.bpmnXml |
| | | // 暴露必要的属性和方法给父组件 |
| | | defineExpose({ |
| | | modeler, |
| | | isModelerReady, |
| | | saveXML, |
| | | saveSVG, |
| | | refresh |
| | | }) |
| | | </script> |
| | | <style lang="scss"> |
| | | .process-panel__container { |
| | | position: absolute; |
| | | top: 90px; |
| | | right: 60px; |
| | | top: 172px; |
| | | right: 70px; |
| | | } |
| | | </style> |
对比新文件 |
| | |
| | | <template> |
| | | <el-form ref="formRef" :model="modelData" :rules="rules" label-width="120px" class="mt-20px"> |
| | | <el-form-item label="流程标识" prop="key" class="mb-20px"> |
| | | <div class="flex items-center"> |
| | | <el-input |
| | | class="!w-440px" |
| | | v-model="modelData.key" |
| | | :disabled="!!modelData.id" |
| | | placeholder="请输入流标标识" |
| | | /> |
| | | <el-tooltip |
| | | class="item" |
| | | :content="modelData.id ? '流程标识不可修改!' : '新建后,流程标识不可修改!'" |
| | | effect="light" |
| | | placement="top" |
| | | > |
| | | <Icon icon="ep:question-filled" class="ml-5px" /> |
| | | </el-tooltip> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="流程名称" prop="name" class="mb-20px"> |
| | | <el-input |
| | | v-model="modelData.name" |
| | | :disabled="!!modelData.id" |
| | | clearable |
| | | placeholder="请输入流程名称" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="流程分类" prop="category" class="mb-20px"> |
| | | <el-select |
| | | class="!w-full" |
| | | v-model="modelData.category" |
| | | clearable |
| | | placeholder="请选择流程分类" |
| | | > |
| | | <el-option |
| | | v-for="category in categoryList" |
| | | :key="category.code" |
| | | :label="category.name" |
| | | :value="category.code" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="流程图标" prop="icon" class="mb-20px"> |
| | | <UploadImg v-model="modelData.icon" :limit="1" height="64px" width="64px" /> |
| | | </el-form-item> |
| | | <el-form-item label="流程描述" prop="description" class="mb-20px"> |
| | | <el-input v-model="modelData.description" clearable type="textarea" /> |
| | | </el-form-item> |
| | | <el-form-item label="流程类型" prop="type" class="mb-20px"> |
| | | <el-radio-group v-model="modelData.type"> |
| | | <el-radio |
| | | v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_TYPE)" |
| | | :key="dict.value" |
| | | :value="dict.value" |
| | | > |
| | | {{ dict.label }} |
| | | </el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="是否可见" prop="visible" class="mb-20px"> |
| | | <el-radio-group v-model="modelData.visible"> |
| | | <el-radio |
| | | v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)" |
| | | :key="dict.value" |
| | | :value="dict.value" |
| | | > |
| | | {{ dict.label }} |
| | | </el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="谁可以发起" prop="startUserType" class="mb-20px"> |
| | | <el-select |
| | | v-model="modelData.startUserType" |
| | | placeholder="请选择谁可以发起" |
| | | @change="handleStartUserTypeChange" |
| | | > |
| | | <el-option label="全员" :value="0" /> |
| | | <el-option label="指定人员" :value="1" /> |
| | | <el-option label="均不可提交" :value="2" /> |
| | | </el-select> |
| | | <div v-if="modelData.startUserType === 1" class="mt-2 flex flex-wrap gap-2"> |
| | | <div |
| | | v-for="user in selectedStartUsers" |
| | | :key="user.id" |
| | | class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative" |
| | | > |
| | | <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" /> |
| | | <el-avatar class="!m-5px" :size="28" v-else> |
| | | {{ user.nickname.substring(0, 1) }} |
| | | </el-avatar> |
| | | {{ user.nickname }} |
| | | <Icon |
| | | icon="ep:close" |
| | | class="ml-2 cursor-pointer hover:text-red-500" |
| | | @click="handleRemoveStartUser(user)" |
| | | /> |
| | | </div> |
| | | <el-button type="primary" link @click="openStartUserSelect"> |
| | | <Icon icon="ep:plus" />选择人员 |
| | | </el-button> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="流程管理员" prop="managerUserType" class="mb-20px"> |
| | | <el-select |
| | | v-model="modelData.managerUserType" |
| | | placeholder="请选择流程管理员" |
| | | @change="handleManagerUserTypeChange" |
| | | > |
| | | <el-option label="全员" :value="0" /> |
| | | <el-option label="指定人员" :value="1" /> |
| | | <el-option label="均不可提交" :value="2" /> |
| | | </el-select> |
| | | <div v-if="modelData.managerUserType === 1" class="mt-2 flex flex-wrap gap-2"> |
| | | <div |
| | | v-for="user in selectedManagerUsers" |
| | | :key="user.id" |
| | | class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative" |
| | | > |
| | | <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" /> |
| | | <el-avatar class="!m-5px" :size="28" v-else> |
| | | {{ user.nickname.substring(0, 1) }} |
| | | </el-avatar> |
| | | {{ user.nickname }} |
| | | <Icon |
| | | icon="ep:close" |
| | | class="ml-2 cursor-pointer hover:text-red-500" |
| | | @click="handleRemoveManagerUser(user)" |
| | | /> |
| | | </div> |
| | | <el-button type="primary" link @click="openManagerUserSelect"> |
| | | <Icon icon="ep:plus" />选择人员 |
| | | </el-button> |
| | | </div> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <!-- 用户选择弹窗 --> |
| | | <UserSelectForm ref="userSelectFormRef" @confirm="handleUserSelectConfirm" /> |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '@/utils/dict' |
| | | import { UserVO } from '@/api/system/user' |
| | | |
| | | const props = defineProps({ |
| | | modelValue: { |
| | | type: Object, |
| | | required: true |
| | | }, |
| | | categoryList: { |
| | | type: Array, |
| | | required: true |
| | | }, |
| | | userList: { |
| | | type: Array, |
| | | required: true |
| | | } |
| | | }) |
| | | |
| | | const emit = defineEmits(['update:modelValue']) |
| | | |
| | | const formRef = ref() |
| | | const selectedStartUsers = ref<UserVO[]>([]) |
| | | const selectedManagerUsers = ref<UserVO[]>([]) |
| | | const userSelectFormRef = ref() |
| | | const currentSelectType = ref<'start' | 'manager'>('start') |
| | | |
| | | const rules = { |
| | | name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }], |
| | | key: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }], |
| | | category: [{ required: true, message: '流程分类不能为空', trigger: 'blur' }], |
| | | icon: [{ required: true, message: '流程图标不能为空', trigger: 'blur' }], |
| | | type: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }], |
| | | visible: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }], |
| | | managerUserIds: [{ required: true, message: '流程管理员不能为空', trigger: 'blur' }] |
| | | } |
| | | |
| | | // 创建本地数据副本 |
| | | const modelData = computed({ |
| | | get: () => props.modelValue, |
| | | set: (val) => emit('update:modelValue', val) |
| | | }) |
| | | |
| | | // 初始化选中的用户 |
| | | watch( |
| | | () => props.modelValue, |
| | | (newVal) => { |
| | | if (newVal.startUserIds?.length) { |
| | | selectedStartUsers.value = props.userList.filter((user: UserVO) => |
| | | newVal.startUserIds.includes(user.id) |
| | | ) as UserVO[] |
| | | } |
| | | if (newVal.managerUserIds?.length) { |
| | | selectedManagerUsers.value = props.userList.filter((user: UserVO) => |
| | | newVal.managerUserIds.includes(user.id) |
| | | ) as UserVO[] |
| | | } |
| | | }, |
| | | { immediate: true } |
| | | ) |
| | | |
| | | /** 打开发起人选择 */ |
| | | const openStartUserSelect = () => { |
| | | currentSelectType.value = 'start' |
| | | userSelectFormRef.value.open(0, selectedStartUsers.value) |
| | | } |
| | | |
| | | /** 打开管理员选择 */ |
| | | const openManagerUserSelect = () => { |
| | | currentSelectType.value = 'manager' |
| | | userSelectFormRef.value.open(0, selectedManagerUsers.value) |
| | | } |
| | | |
| | | /** 处理用户选择确认 */ |
| | | const handleUserSelectConfirm = (_, users: UserVO[]) => { |
| | | if (currentSelectType.value === 'start') { |
| | | selectedStartUsers.value = users |
| | | emit('update:modelValue', { |
| | | ...modelData.value, |
| | | startUserIds: users.map((u) => u.id) |
| | | }) |
| | | } else { |
| | | selectedManagerUsers.value = users |
| | | emit('update:modelValue', { |
| | | ...modelData.value, |
| | | managerUserIds: users.map((u) => u.id) |
| | | }) |
| | | } |
| | | } |
| | | |
| | | /** 处理发起人类型变化 */ |
| | | const handleStartUserTypeChange = (value: number) => { |
| | | if (value !== 1) { |
| | | selectedStartUsers.value = [] |
| | | emit('update:modelValue', { |
| | | ...modelData.value, |
| | | startUserIds: [] |
| | | }) |
| | | } |
| | | } |
| | | |
| | | /** 处理管理员类型变化 */ |
| | | const handleManagerUserTypeChange = (value: number) => { |
| | | if (value !== 1) { |
| | | selectedManagerUsers.value = [] |
| | | emit('update:modelValue', { |
| | | ...modelData.value, |
| | | managerUserIds: [] |
| | | }) |
| | | } |
| | | } |
| | | |
| | | /** 移除发起人 */ |
| | | const handleRemoveStartUser = (user: UserVO) => { |
| | | selectedStartUsers.value = selectedStartUsers.value.filter((u) => u.id !== user.id) |
| | | emit('update:modelValue', { |
| | | ...modelData.value, |
| | | startUserIds: modelData.value.startUserIds.filter((id: number) => id !== user.id) |
| | | }) |
| | | } |
| | | |
| | | /** 移除管理员 */ |
| | | const handleRemoveManagerUser = (user: UserVO) => { |
| | | selectedManagerUsers.value = selectedManagerUsers.value.filter((u) => u.id !== user.id) |
| | | emit('update:modelValue', { |
| | | ...modelData.value, |
| | | managerUserIds: modelData.value.managerUserIds.filter((id: number) => id !== user.id) |
| | | }) |
| | | } |
| | | |
| | | /** 表单校验 */ |
| | | const validate = async () => { |
| | | await formRef.value?.validate() |
| | | } |
| | | |
| | | defineExpose({ |
| | | validate |
| | | }) |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .bg-gray-100 { |
| | | background-color: #f5f7fa; |
| | | transition: all 0.3s; |
| | | |
| | | &:hover { |
| | | background-color: #e6e8eb; |
| | | } |
| | | |
| | | .ep-close { |
| | | font-size: 14px; |
| | | color: #909399; |
| | | transition: color 0.3s; |
| | | |
| | | &:hover { |
| | | color: #f56c6c; |
| | | } |
| | | } |
| | | } |
| | | </style> |
对比新文件 |
| | |
| | | <template> |
| | | <el-form ref="formRef" :model="modelData" :rules="rules" label-width="120px" class="mt-20px"> |
| | | <el-form-item label="表单类型" prop="formType" class="mb-20px"> |
| | | <el-radio-group v-model="modelData.formType"> |
| | | <el-radio |
| | | v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_FORM_TYPE)" |
| | | :key="dict.value" |
| | | :value="dict.value" |
| | | > |
| | | {{ dict.label }} |
| | | </el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item v-if="modelData.formType === 10" label="流程表单" prop="formId"> |
| | | <el-select v-model="modelData.formId" clearable style="width: 100%"> |
| | | <el-option v-for="form in formList" :key="form.id" :label="form.name" :value="form.id" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item v-if="modelData.formType === 20" label="表单提交路由" prop="formCustomCreatePath"> |
| | | <el-input |
| | | v-model="modelData.formCustomCreatePath" |
| | | placeholder="请输入表单提交路由" |
| | | style="width: 330px" |
| | | /> |
| | | <el-tooltip |
| | | class="item" |
| | | content="自定义表单的提交路径,使用 Vue 的路由地址,例如说:bpm/oa/leave/create.vue" |
| | | effect="light" |
| | | placement="top" |
| | | > |
| | | <Icon icon="ep:question" class="ml-5px" /> |
| | | </el-tooltip> |
| | | </el-form-item> |
| | | <el-form-item v-if="modelData.formType === 20" label="表单查看地址" prop="formCustomViewPath"> |
| | | <el-input |
| | | v-model="modelData.formCustomViewPath" |
| | | placeholder="请输入表单查看的组件地址" |
| | | style="width: 330px" |
| | | /> |
| | | <el-tooltip |
| | | class="item" |
| | | content="自定义表单的查看组件地址,使用 Vue 的组件地址,例如说:bpm/oa/leave/detail.vue" |
| | | effect="light" |
| | | placement="top" |
| | | > |
| | | <Icon icon="ep:question" class="ml-5px" /> |
| | | </el-tooltip> |
| | | </el-form-item> |
| | | <!-- 表单预览 --> |
| | | <div |
| | | v-if="modelData.formType === 10 && modelData.formId && formPreview.rule.length > 0" |
| | | class="mt-20px" |
| | | > |
| | | <div class="flex items-center mb-15px"> |
| | | <div class="h-15px w-4px bg-[#1890ff] mr-10px"></div> |
| | | <span class="text-15px font-bold">表单预览</span> |
| | | </div> |
| | | <form-create |
| | | v-model="formPreview.formData" |
| | | :rule="formPreview.rule" |
| | | :option="formPreview.option" |
| | | /> |
| | | </div> |
| | | </el-form> |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' |
| | | import * as FormApi from '@/api/bpm/form' |
| | | import { setConfAndFields2 } from '@/utils/formCreate' |
| | | |
| | | const props = defineProps({ |
| | | modelValue: { |
| | | type: Object, |
| | | required: true |
| | | }, |
| | | formList: { |
| | | type: Array, |
| | | required: true |
| | | } |
| | | }) |
| | | |
| | | const emit = defineEmits(['update:modelValue']) |
| | | |
| | | const formRef = ref() |
| | | |
| | | // 创建本地数据副本 |
| | | const modelData = computed({ |
| | | get: () => props.modelValue, |
| | | set: (val) => emit('update:modelValue', val) |
| | | }) |
| | | |
| | | // 表单预览数据 |
| | | const formPreview = ref({ |
| | | formData: {}, |
| | | rule: [], |
| | | option: { |
| | | submitBtn: false, |
| | | resetBtn: false, |
| | | formData: {} |
| | | } |
| | | }) |
| | | |
| | | // 监听表单ID变化,加载表单数据 |
| | | watch( |
| | | () => modelData.value.formId, |
| | | async (newFormId) => { |
| | | if (newFormId && modelData.value.formType === 10) { |
| | | const data = await FormApi.getForm(newFormId) |
| | | setConfAndFields2(formPreview.value, data.conf, data.fields) |
| | | // 设置只读 |
| | | formPreview.value.rule.forEach((item: any) => { |
| | | item.props = { ...item.props, disabled: true } |
| | | }) |
| | | } else { |
| | | formPreview.value.rule = [] |
| | | } |
| | | }, |
| | | { immediate: true } |
| | | ) |
| | | |
| | | const rules = { |
| | | formType: [{ required: true, message: '表单类型不能为空', trigger: 'blur' }], |
| | | formId: [{ required: true, message: '流程表单不能为空', trigger: 'blur' }], |
| | | formCustomCreatePath: [{ required: true, message: '表单提交路由不能为空', trigger: 'blur' }], |
| | | formCustomViewPath: [{ required: true, message: '表单查看地址不能为空', trigger: 'blur' }] |
| | | } |
| | | |
| | | /** 表单校验 */ |
| | | const validate = async () => { |
| | | await formRef.value?.validate() |
| | | } |
| | | |
| | | defineExpose({ |
| | | validate |
| | | }) |
| | | </script> |
对比新文件 |
| | |
| | | <template> |
| | | <!-- BPMN设计器 --> |
| | | <template v-if="modelData.type === BpmModelType.BPMN"> |
| | | <BpmModelEditor |
| | | v-if="showDesigner" |
| | | :model-id="modelData.id" |
| | | :model-key="modelData.key" |
| | | :model-name="modelData.name" |
| | | :value="currentBpmnXml" |
| | | ref="bpmnEditorRef" |
| | | @success="handleDesignSuccess" |
| | | @init-finished="handleEditorInit" |
| | | /> |
| | | </template> |
| | | |
| | | <!-- Simple设计器 --> |
| | | <template v-else> |
| | | <SimpleModelDesign |
| | | v-if="showDesigner" |
| | | :model-id="modelData.id" |
| | | :model-key="modelData.key" |
| | | :model-name="modelData.name" |
| | | :start-user-ids="modelData.startUserIds" |
| | | :value="currentSimpleModel" |
| | | ref="simpleEditorRef" |
| | | @success="handleDesignSuccess" |
| | | @init-finished="handleEditorInit" |
| | | /> |
| | | </template> |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | import { BpmModelType } from '@/utils/constants' |
| | | import BpmModelEditor from '../editor/index.vue' |
| | | import SimpleModelDesign from '../../simple/SimpleModelDesign.vue' |
| | | |
| | | const props = defineProps({ |
| | | modelValue: { |
| | | type: Object, |
| | | required: true |
| | | } |
| | | }) |
| | | |
| | | const emit = defineEmits(['update:modelValue', 'success']) |
| | | |
| | | const bpmnEditorRef = ref() |
| | | const simpleEditorRef = ref() |
| | | const isEditorInitialized = ref(false) |
| | | |
| | | // 创建本地数据副本 |
| | | const modelData = computed({ |
| | | get: () => props.modelValue, |
| | | set: (val) => emit('update:modelValue', val) |
| | | }) |
| | | |
| | | // 保存当前的流程XML或数据 |
| | | const currentBpmnXml = ref('') |
| | | const currentSimpleModel = ref('') |
| | | |
| | | // 初始化或更新当前的XML数据 |
| | | const initOrUpdateXmlData = () => { |
| | | if (modelData.value) { |
| | | if (modelData.value.type === BpmModelType.BPMN) { |
| | | currentBpmnXml.value = modelData.value.bpmnXml || '' |
| | | } else { |
| | | currentSimpleModel.value = modelData.value.simpleModel || '' |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 监听modelValue的变化,更新数据 |
| | | watch( |
| | | () => props.modelValue, |
| | | (newVal) => { |
| | | if (newVal) { |
| | | if (newVal.type === BpmModelType.BPMN) { |
| | | if (newVal.bpmnXml && newVal.bpmnXml !== currentBpmnXml.value) { |
| | | currentBpmnXml.value = newVal.bpmnXml |
| | | // 如果编辑器已经初始化,刷新视图 |
| | | if (isEditorInitialized.value && bpmnEditorRef.value?.refresh) { |
| | | nextTick(() => { |
| | | bpmnEditorRef.value.refresh() |
| | | }) |
| | | } |
| | | } |
| | | } else { |
| | | if (newVal.simpleModel && newVal.simpleModel !== currentSimpleModel.value) { |
| | | currentSimpleModel.value = newVal.simpleModel |
| | | // 如果编辑器已经初始化,刷新视图 |
| | | if (isEditorInitialized.value && simpleEditorRef.value?.refresh) { |
| | | nextTick(() => { |
| | | simpleEditorRef.value.refresh() |
| | | }) |
| | | } |
| | | } |
| | | } |
| | | } |
| | | }, |
| | | { immediate: true, deep: true } |
| | | ) |
| | | |
| | | /** 编辑器初始化完成的回调 */ |
| | | const handleEditorInit = async () => { |
| | | isEditorInitialized.value = true |
| | | |
| | | // 等待下一个tick,确保编辑器已经准备好 |
| | | await nextTick() |
| | | |
| | | // 初始化完成后,设置初始值 |
| | | if (modelData.value.type === BpmModelType.BPMN) { |
| | | if (modelData.value.bpmnXml) { |
| | | currentBpmnXml.value = modelData.value.bpmnXml |
| | | if (bpmnEditorRef.value?.refresh) { |
| | | await nextTick() |
| | | bpmnEditorRef.value.refresh() |
| | | } |
| | | } |
| | | } else { |
| | | if (modelData.value.simpleModel) { |
| | | currentSimpleModel.value = modelData.value.simpleModel |
| | | if (simpleEditorRef.value?.refresh) { |
| | | await nextTick() |
| | | simpleEditorRef.value.refresh() |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** 获取当前流程数据 */ |
| | | const getProcessData = async () => { |
| | | try { |
| | | if (modelData.value.type === BpmModelType.BPMN) { |
| | | if (!bpmnEditorRef.value || !isEditorInitialized.value) { |
| | | return currentBpmnXml.value || undefined |
| | | } |
| | | const { xml } = await bpmnEditorRef.value.saveXML() |
| | | if (xml) { |
| | | currentBpmnXml.value = xml |
| | | return xml |
| | | } |
| | | } else { |
| | | if (!simpleEditorRef.value || !isEditorInitialized.value) { |
| | | return currentSimpleModel.value || undefined |
| | | } |
| | | const flowData = await simpleEditorRef.value.getCurrentFlowData() |
| | | if (flowData) { |
| | | currentSimpleModel.value = flowData |
| | | return flowData |
| | | } |
| | | } |
| | | return modelData.value.type === BpmModelType.BPMN |
| | | ? currentBpmnXml.value |
| | | : currentSimpleModel.value |
| | | } catch (error) { |
| | | console.error('获取流程数据失败:', error) |
| | | return modelData.value.type === BpmModelType.BPMN |
| | | ? currentBpmnXml.value |
| | | : currentSimpleModel.value |
| | | } |
| | | } |
| | | |
| | | /** 表单校验 */ |
| | | const validate = async () => { |
| | | try { |
| | | // 获取最新的流程数据 |
| | | const processData = await getProcessData() |
| | | if (!processData) { |
| | | throw new Error('请设计流程') |
| | | } |
| | | return true |
| | | } catch (error) { |
| | | throw error |
| | | } |
| | | } |
| | | |
| | | /** 处理设计器保存成功 */ |
| | | const handleDesignSuccess = async (data?: any) => { |
| | | if (data) { |
| | | if (modelData.value.type === BpmModelType.BPMN) { |
| | | currentBpmnXml.value = data |
| | | } else { |
| | | currentSimpleModel.value = data |
| | | } |
| | | |
| | | // 创建新的对象以触发响应式更新 |
| | | const newModelData = { |
| | | ...modelData.value, |
| | | bpmnXml: modelData.value.type === BpmModelType.BPMN ? data : null, |
| | | simpleModel: modelData.value.type === BpmModelType.BPMN ? null : data |
| | | } |
| | | |
| | | // 使用emit更新父组件的数据 |
| | | await nextTick() |
| | | emit('update:modelValue', newModelData) |
| | | emit('success', data) |
| | | } |
| | | } |
| | | |
| | | /** 是否显示设计器 */ |
| | | const showDesigner = computed(() => { |
| | | return Boolean(modelData.value?.key && modelData.value?.name) |
| | | }) |
| | | |
| | | // 组件创建时初始化数据 |
| | | onMounted(() => { |
| | | initOrUpdateXmlData() |
| | | }) |
| | | |
| | | // 组件卸载前保存数据 |
| | | onBeforeUnmount(async () => { |
| | | try { |
| | | // 获取并保存最新的流程数据 |
| | | const data = await getProcessData() |
| | | if (data) { |
| | | // 创建新的对象以触发响应式更新 |
| | | const newModelData = { |
| | | ...modelData.value, |
| | | bpmnXml: modelData.value.type === BpmModelType.BPMN ? data : null, |
| | | simpleModel: modelData.value.type === BpmModelType.BPMN ? null : data |
| | | } |
| | | |
| | | // 使用emit更新父组件的数据 |
| | | await nextTick() |
| | | emit('update:modelValue', newModelData) |
| | | } |
| | | } catch (error) { |
| | | console.error('保存数据失败:', error) |
| | | } |
| | | }) |
| | | |
| | | defineExpose({ |
| | | validate, |
| | | getProcessData |
| | | }) |
| | | </script> |
对比新文件 |
| | |
| | | <template> |
| | | <ContentWrap> |
| | | <div class="mx-auto"> |
| | | <!-- 头部导航栏 --> |
| | | <div |
| | | class="absolute top-0 left-0 right-0 h-50px bg-white border-bottom z-10 flex items-center px-20px" |
| | | > |
| | | <!-- 左侧标题 --> |
| | | <div class="w-200px flex items-center overflow-hidden"> |
| | | <Icon icon="ep:arrow-left" class="cursor-pointer flex-shrink-0" @click="handleBack" /> |
| | | <span class="ml-10px text-16px truncate" :title="formData.name || '创建流程'"> |
| | | {{ formData.name || '创建流程' }} |
| | | </span> |
| | | </div> |
| | | |
| | | <!-- 步骤条 --> |
| | | <div class="flex-1 flex items-center justify-center h-full"> |
| | | <div class="w-400px flex items-center justify-between h-full"> |
| | | <div |
| | | v-for="(step, index) in steps" |
| | | :key="index" |
| | | class="flex items-center cursor-pointer mx-15px relative h-full" |
| | | :class="[ |
| | | currentStep === index |
| | | ? 'text-[#3473ff] border-[#3473ff] border-b-2 border-b-solid' |
| | | : 'text-gray-500' |
| | | ]" |
| | | @click="handleStepClick(index)" |
| | | > |
| | | <div |
| | | class="w-28px h-28px rounded-full flex items-center justify-center mr-8px border-2 border-solid text-15px" |
| | | :class="[ |
| | | currentStep === index |
| | | ? 'bg-[#3473ff] text-white border-[#3473ff]' |
| | | : 'border-gray-300 bg-white text-gray-500' |
| | | ]" |
| | | > |
| | | {{ index + 1 }} |
| | | </div> |
| | | <span class="text-16px font-bold whitespace-nowrap">{{ step.title }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 右侧按钮 --> |
| | | <div class="w-200px flex items-center justify-end gap-2"> |
| | | <el-button v-if="route.params.id" type="success" @click="handleDeploy">发 布</el-button> |
| | | <el-button type="primary" @click="handleSave">保 存</el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 主体内容 --> |
| | | <div class="mt-50px"> |
| | | <!-- 第一步:基本信息 --> |
| | | <div v-if="currentStep === 0" class="mx-auto w-560px"> |
| | | <BasicInfo |
| | | v-model="formData" |
| | | :categoryList="categoryList" |
| | | :userList="userList" |
| | | ref="basicInfoRef" |
| | | /> |
| | | </div> |
| | | |
| | | <!-- 第二步:表单设计 --> |
| | | <div v-if="currentStep === 1" class="mx-auto w-560px"> |
| | | <FormDesign v-model="formData" :formList="formList" ref="formDesignRef" /> |
| | | </div> |
| | | |
| | | <!-- 第三步:流程设计 --> |
| | | <ProcessDesign |
| | | v-if="currentStep === 2" |
| | | v-model="formData" |
| | | ref="processDesignRef" |
| | | @success="handleDesignSuccess" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </ContentWrap> |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { useMessage } from '@/hooks/web/useMessage' |
| | | import * as ModelApi from '@/api/bpm/model' |
| | | import * as FormApi from '@/api/bpm/form' |
| | | import { CategoryApi } from '@/api/bpm/category' |
| | | import * as UserApi from '@/api/system/user' |
| | | import { useUserStoreWithOut } from '@/store/modules/user' |
| | | import { BpmModelFormType, BpmModelType } from '@/utils/constants' |
| | | import BasicInfo from './BasicInfo.vue' |
| | | import FormDesign from './FormDesign.vue' |
| | | import ProcessDesign from './ProcessDesign.vue' |
| | | import { useTagsViewStore } from '@/store/modules/tagsView' |
| | | |
| | | const router = useRouter() |
| | | const { delView } = useTagsViewStore() // 视图操作 |
| | | const route = useRoute() |
| | | const message = useMessage() |
| | | const userStore = useUserStoreWithOut() |
| | | |
| | | // 组件引用 |
| | | const basicInfoRef = ref() |
| | | const formDesignRef = ref() |
| | | const processDesignRef = ref() |
| | | |
| | | /** 步骤校验函数 */ |
| | | const validateBasic = async () => { |
| | | await basicInfoRef.value?.validate() |
| | | } |
| | | |
| | | /** 表单设计校验 */ |
| | | const validateForm = async () => { |
| | | await formDesignRef.value?.validate() |
| | | } |
| | | |
| | | /** 流程设计校验 */ |
| | | const validateProcess = async () => { |
| | | await processDesignRef.value?.validate() |
| | | } |
| | | |
| | | const currentStep = ref(0) // 步骤控制 |
| | | const steps = [ |
| | | { title: '基本信息', validator: validateBasic }, |
| | | { title: '表单设计', validator: validateForm }, |
| | | { title: '流程设计', validator: validateProcess } |
| | | ] |
| | | |
| | | // 表单数据 |
| | | const formData: any = ref({ |
| | | id: undefined, |
| | | name: '', |
| | | key: '', |
| | | category: undefined, |
| | | icon: undefined, |
| | | description: '', |
| | | type: BpmModelType.BPMN, |
| | | formType: BpmModelFormType.NORMAL, |
| | | formId: '', |
| | | formCustomCreatePath: '', |
| | | formCustomViewPath: '', |
| | | visible: true, |
| | | startUserType: undefined, |
| | | managerUserType: undefined, |
| | | startUserIds: [], |
| | | managerUserIds: [] |
| | | }) |
| | | |
| | | // 数据列表 |
| | | const formList = ref([]) |
| | | const categoryList = ref([]) |
| | | const userList = ref<UserApi.UserVO[]>([]) |
| | | |
| | | /** 初始化数据 */ |
| | | const initData = async () => { |
| | | const modelId = route.params.id as string |
| | | if (modelId) { |
| | | // 修改场景 |
| | | formData.value = await ModelApi.getModel(modelId) |
| | | } else { |
| | | // 新增场景 |
| | | formData.value.managerUserIds.push(userStore.getUser.id) |
| | | } |
| | | |
| | | // 获取表单列表 |
| | | formList.value = await FormApi.getFormSimpleList() |
| | | // 获取分类列表 |
| | | categoryList.value = await CategoryApi.getCategorySimpleList() |
| | | // 获取用户列表 |
| | | userList.value = await UserApi.getSimpleUserList() |
| | | } |
| | | |
| | | /** 校验所有步骤数据是否完整 */ |
| | | const validateAllSteps = async () => { |
| | | try { |
| | | // 基本信息校验 |
| | | await basicInfoRef.value?.validate() |
| | | if (!formData.value.key || !formData.value.name || !formData.value.category) { |
| | | currentStep.value = 0 |
| | | throw new Error('请完善基本信息') |
| | | } |
| | | |
| | | // 表单设计校验 |
| | | await formDesignRef.value?.validate() |
| | | if (formData.value.formType === 10 && !formData.value.formId) { |
| | | currentStep.value = 1 |
| | | throw new Error('请选择流程表单') |
| | | } |
| | | if ( |
| | | formData.value.formType === 20 && |
| | | (!formData.value.formCustomCreatePath || !formData.value.formCustomViewPath) |
| | | ) { |
| | | currentStep.value = 1 |
| | | throw new Error('请完善自定义表单信息') |
| | | } |
| | | |
| | | // 流程设计校验 |
| | | // 如果已经有流程数据,则不需要重新校验 |
| | | if (!formData.value.bpmnXml && !formData.value.simpleModel) { |
| | | // 如果当前不在第三步,需要先保存当前步骤数据 |
| | | if (currentStep.value !== 2) { |
| | | await steps[currentStep.value].validator() |
| | | // 切换到第三步 |
| | | currentStep.value = 2 |
| | | // 等待组件渲染完成 |
| | | await nextTick() |
| | | } |
| | | |
| | | // 校验流程设计 |
| | | await processDesignRef.value?.validate() |
| | | const processData = await processDesignRef.value?.getProcessData() |
| | | if (!processData) { |
| | | throw new Error('请设计流程') |
| | | } |
| | | |
| | | // 保存流程数据 |
| | | if (formData.value.type === BpmModelType.BPMN) { |
| | | formData.value.bpmnXml = processData |
| | | formData.value.simpleModel = null |
| | | } else { |
| | | formData.value.bpmnXml = null |
| | | formData.value.simpleModel = processData |
| | | } |
| | | } |
| | | |
| | | return true |
| | | } catch (error) { |
| | | throw error |
| | | } |
| | | } |
| | | |
| | | /** 保存操作 */ |
| | | const handleSave = async () => { |
| | | try { |
| | | // 保存前校验所有步骤的数据 |
| | | await validateAllSteps() |
| | | |
| | | // 更新表单数据 |
| | | const modelData = { |
| | | ...formData.value |
| | | } |
| | | |
| | | // 如果当前在第三步,获取最新的流程设计数据 |
| | | if (currentStep.value === 2) { |
| | | const processData = await processDesignRef.value?.getProcessData() |
| | | if (processData) { |
| | | if (formData.value.type === BpmModelType.BPMN) { |
| | | modelData.bpmnXml = processData |
| | | modelData.simpleModel = null |
| | | } else { |
| | | modelData.bpmnXml = null |
| | | modelData.simpleModel = processData |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (formData.value.id) { |
| | | // 修改场景 |
| | | await ModelApi.updateModel(modelData) |
| | | // 询问是否发布流程 |
| | | try { |
| | | await message.confirm('修改流程成功,是否发布流程?') |
| | | // 用户点击确认,执行发布 |
| | | await handleDeploy() |
| | | } catch { |
| | | // 用户点击取消,停留在当前页面 |
| | | } |
| | | } else { |
| | | // 新增场景 |
| | | formData.value.id = await ModelApi.createModel(modelData) |
| | | message.success('新增成功') |
| | | try { |
| | | await message.confirm('创建流程成功,是否继续编辑?') |
| | | // 用户点击继续编辑,跳转到编辑页面 |
| | | await nextTick() |
| | | // 先删除当前页签 |
| | | delView(unref(router.currentRoute)) |
| | | // 跳转到编辑页面 |
| | | await router.push({ |
| | | name: 'BpmModelUpdate', |
| | | params: { id: formData.value.id } |
| | | }) |
| | | } catch { |
| | | // 先删除当前页签 |
| | | delView(unref(router.currentRoute)) |
| | | // 用户点击返回列表 |
| | | await router.push({ name: 'BpmModel' }) |
| | | } |
| | | } |
| | | } catch (error: any) { |
| | | console.error('保存失败:', error) |
| | | message.warning(error.message || '请完善所有步骤的必填信息') |
| | | } |
| | | } |
| | | |
| | | /** 发布操作 */ |
| | | const handleDeploy = async () => { |
| | | try { |
| | | // 修改场景下直接发布,新增场景下需要先确认 |
| | | if (!formData.value.id) { |
| | | await message.confirm('是否确认发布该流程?') |
| | | } |
| | | |
| | | // 校验所有步骤 |
| | | await validateAllSteps() |
| | | |
| | | // 更新表单数据 |
| | | const modelData = { |
| | | ...formData.value |
| | | } |
| | | |
| | | // 如果当前在第三步,获取最新的流程设计数据 |
| | | if (currentStep.value === 2) { |
| | | const processData = await processDesignRef.value?.getProcessData() |
| | | if (processData) { |
| | | if (formData.value.type === BpmModelType.BPMN) { |
| | | modelData.bpmnXml = processData |
| | | modelData.simpleModel = null |
| | | } else { |
| | | modelData.bpmnXml = null |
| | | modelData.simpleModel = processData |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 先保存所有数据 |
| | | if (formData.value.id) { |
| | | await ModelApi.updateModel(modelData) |
| | | } else { |
| | | const result = await ModelApi.createModel(modelData) |
| | | formData.value.id = result.id |
| | | } |
| | | |
| | | // 发布 |
| | | await ModelApi.deployModel(formData.value.id) |
| | | message.success('发布成功') |
| | | // 返回列表页 |
| | | await router.push({ name: 'BpmModel' }) |
| | | } catch (error: any) { |
| | | console.error('发布失败:', error) |
| | | message.warning(error.message || '发布失败') |
| | | } |
| | | } |
| | | |
| | | /** 步骤切换处理 */ |
| | | const handleStepClick = async (index: number) => { |
| | | try { |
| | | // 如果是切换到第三步(流程设计),需要校验key和name |
| | | if (index === 2) { |
| | | if (!formData.value.key || !formData.value.name) { |
| | | message.warning('请先填写流程标识和流程名称') |
| | | return |
| | | } |
| | | } |
| | | |
| | | // 保存当前步骤的数据 |
| | | if (currentStep.value === 2) { |
| | | const processData = await processDesignRef.value?.getProcessData() |
| | | if (processData) { |
| | | if (formData.value.type === BpmModelType.BPMN) { |
| | | formData.value.bpmnXml = processData |
| | | formData.value.simpleModel = null |
| | | } else { |
| | | formData.value.bpmnXml = null |
| | | formData.value.simpleModel = processData |
| | | } |
| | | } |
| | | } else { |
| | | // 只有在向后切换时才进行校验 |
| | | if (index > currentStep.value) { |
| | | if (typeof steps[currentStep.value].validator === 'function') { |
| | | await steps[currentStep.value].validator() |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 切换步骤 |
| | | currentStep.value = index |
| | | |
| | | // 如果切换到流程设计步骤,等待组件渲染完成后刷新设计器 |
| | | if (index === 2) { |
| | | await nextTick() |
| | | // 等待更长时间确保组件完全初始化 |
| | | await new Promise(resolve => setTimeout(resolve, 200)) |
| | | if (processDesignRef.value?.refresh) { |
| | | await processDesignRef.value.refresh() |
| | | } |
| | | } |
| | | } catch (error) { |
| | | console.error('步骤切换失败:', error) |
| | | message.warning('请先完善当前步骤必填信息') |
| | | } |
| | | } |
| | | |
| | | /** 处理设计器保存成功 */ |
| | | const handleDesignSuccess = (bpmnXml?: string) => { |
| | | if (bpmnXml) { |
| | | formData.value.bpmnXml = bpmnXml |
| | | } |
| | | } |
| | | |
| | | /** 返回列表页 */ |
| | | const handleBack = () => { |
| | | // 先删除当前页签 |
| | | delView(unref(router.currentRoute)) |
| | | // 跳转到列表页 |
| | | router.push({ name: 'BpmModel' }) |
| | | } |
| | | |
| | | /** 初始化 */ |
| | | onMounted(async () => { |
| | | await initData() |
| | | }) |
| | | |
| | | // 添加组件卸载前的清理代码 |
| | | onBeforeUnmount(() => { |
| | | // 清理所有的引用 |
| | | basicInfoRef.value = null |
| | | formDesignRef.value = null |
| | | processDesignRef.value = null |
| | | }) |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .border-bottom { |
| | | border-bottom: 1px solid #dcdfe6; |
| | | } |
| | | |
| | | .text-primary { |
| | | color: #3473ff; |
| | | } |
| | | |
| | | .bg-primary { |
| | | background-color: #3473ff; |
| | | } |
| | | |
| | | .border-primary { |
| | | border-color: #3473ff; |
| | | } |
| | | </style> |
| | |
| | | |
| | | defineOptions({ name: 'BpmModel' }) |
| | | |
| | | const { push } = useRouter() |
| | | const message = useMessage() // 消息弹窗 |
| | | const loading = ref(true) // 列表的加载中 |
| | | const isCategorySorting = ref(false) // 是否 category 正处于排序状态 |
| | |
| | | /** 添加/修改操作 */ |
| | | const formRef = ref() |
| | | const openForm = (type: string, id?: number) => { |
| | | formRef.value.open(type, id) |
| | | if (type === 'create') { |
| | | push({ name: 'BpmModelCreate' }) |
| | | } else { |
| | | push({ |
| | | name: 'BpmModelUpdate', |
| | | params: { id } |
| | | }) |
| | | } |
| | | } |
| | | |
| | | /** 流程表单的详情按钮操作 */ |
| | |
| | | <!-- 中间主要内容 tab 栏 --> |
| | | <el-tabs v-model="activeTab"> |
| | | <!-- 表单信息 --> |
| | | <el-tab-pane label="表单填写" name="form" > |
| | | <el-tab-pane label="表单填写" name="form"> |
| | | <div class="form-scroll-area" v-loading="processInstanceStartLoading"> |
| | | <el-scrollbar> |
| | | <el-row> |
| | |
| | | <script lang="ts" setup> |
| | | import { decodeFields, setConfAndFields2 } from '@/utils/formCreate' |
| | | import { BpmModelType } from '@/utils/constants' |
| | | import { CandidateStrategy } from '@/components/SimpleProcessDesignerV2/src/consts' |
| | | import { |
| | | CandidateStrategy, |
| | | NodeId, |
| | | FieldPermissionType |
| | | } from '@/components/SimpleProcessDesignerV2/src/consts' |
| | | import ProcessInstanceBpmnViewer from '../detail/ProcessInstanceBpmnViewer.vue' |
| | | import ProcessInstanceSimpleViewer from '../detail/ProcessInstanceSimpleViewer.vue' |
| | | import ProcessInstanceTimeline from '../detail/ProcessInstanceTimeline.vue' |
| | |
| | | } |
| | | } |
| | | setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables) |
| | | |
| | | await nextTick() |
| | | fApi.value?.btn.show(false) // 隐藏提交按钮 |
| | | |
| | | // 获取流程审批信息 |
| | | await getApprovalDetail(row) |
| | | |
| | |
| | | /** 获取审批详情 */ |
| | | const getApprovalDetail = async (row: any) => { |
| | | try { |
| | | const data = await ProcessInstanceApi.getApprovalDetail({ processDefinitionId: row.id }) |
| | | // TODO 获取审批详情,设置 activityId 为发起人节点(为了获取字段权限。暂时只对 Simple 设计器有效) |
| | | const data = await ProcessInstanceApi.getApprovalDetail({ |
| | | processDefinitionId: row.id, |
| | | activityId: NodeId.START_USER_NODE_ID |
| | | }) |
| | | |
| | | if (!data) { |
| | | message.error('查询不到审批详情信息!') |
| | | return |
| | |
| | | |
| | | // 获取审批节点,显示 Timeline 的数据 |
| | | activityNodes.value = data.activityNodes |
| | | // 获取表单字段权限 |
| | | const formFieldsPermission = data.formFieldsPermission |
| | | // 设置表单字段权限 |
| | | if (formFieldsPermission) { |
| | | Object.keys(formFieldsPermission).forEach((item) => { |
| | | setFieldPermission(item, formFieldsPermission[item]) |
| | | }) |
| | | } |
| | | } finally { |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 设置表单权限 |
| | | */ |
| | | const setFieldPermission = (field: string, permission: string) => { |
| | | if (permission === FieldPermissionType.READ) { |
| | | //@ts-ignore |
| | | fApi.value?.disabled(true, field) |
| | | } |
| | | if (permission === FieldPermissionType.WRITE) { |
| | | //@ts-ignore |
| | | fApi.value?.disabled(false, field) |
| | | } |
| | | if (permission === FieldPermissionType.NONE) { |
| | | //@ts-ignore |
| | | fApi.value?.hidden(true, field) |
| | | } |
| | | } |
| | | |
| | |
| | | .form-scroll-area { |
| | | height: calc( |
| | | 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px - |
| | | $process-header-height - 40px |
| | | $process-header-height - 40px |
| | | ); |
| | | max-height: calc( |
| | | 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px - |
| | | $process-header-height - 40px |
| | | $process-header-height - 40px |
| | | ); |
| | | overflow: auto; |
| | | } |
| | |
| | | }) |
| | | const deleteSignFormRule = reactive<FormRules<typeof deleteSignForm>>({ |
| | | deleteSignTaskId: [{ required: true, message: '减签人员不能为空', trigger: 'change' }], |
| | | reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }], |
| | | reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }], |
| | | }) |
| | | |
| | | // 退回表单 |
| | |
| | | const openPopover = async (type: string) => { |
| | | if (type === 'approve') { |
| | | // 校验流程表单 |
| | | const valid = await validateNormalForm(); |
| | | if (!valid) { |
| | | const valid = await validateNormalForm(); |
| | | if (!valid) { |
| | | message.warning('表单校验不通过,请先完善表单!!') |
| | | return; |
| | | } |
| | | } |
| | | } |
| | | if (type === 'return') { |
| | | // 获取退回节点 |
| | |
| | | const closePropover = (type: string, formRef: FormInstance | undefined) => { |
| | | if (formRef) { |
| | | formRef.resetFields() |
| | | } |
| | | } |
| | | popOverVisible.value[type] = false |
| | | } |
| | | |
| | |
| | | if (!formRef) return |
| | | await formRef.validate() |
| | | if (pass) { |
| | | // 获取修改的流程变量, 暂时只支持流程表单 |
| | | const variables = getUpdatedProcessInstanceVaiables(); |
| | | // 获取修改的流程变量, 暂时只支持流程表单 |
| | | const variables = getUpdatedProcessInstanceVaiables(); |
| | | // 审批通过数据 |
| | | const data = { |
| | | id: runningTask.value.id, |
| | |
| | | popOverVisible.value.approve = false |
| | | message.success('审批通过成功') |
| | | } else { |
| | | // 审批不通过数据 |
| | | const data = { |
| | | // 审批不通过数据 |
| | | const data = { |
| | | id: runningTask.value.id, |
| | | reason: rejectReasonForm.reason, |
| | | } |
| | |
| | | const handleDelegate = async () => { |
| | | formLoading.value = true |
| | | try { |
| | | |
| | | |
| | | // 1.1 校验表单 |
| | | if (!delegateFormRef.value) return |
| | | await delegateFormRef.value.validate() |
| | |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <div class="flex flex-col items-start gap2" :id="`activity-task-${activity.id}`"> |
| | | <div class="flex flex-col items-start gap2" :id="`activity-task-${activity.id}-${index}`"> |
| | | <!-- 第一行:节点名称、时间 --> |
| | | <div class="flex w-full"> |
| | | <div class="font-bold"> {{ activity.name }}</div> |
| | |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <teleport defer :to="`#activity-task-${activity.id}`"> |
| | | <teleport defer :to="`#activity-task-${activity.id}-${index}`"> |
| | | <div |
| | | v-if=" |
| | | task.reason && |
| | |
| | | display: flex; |
| | | height: calc( |
| | | 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px - |
| | | $process-header-height - 40px |
| | | $process-header-height - 40px |
| | | ); |
| | | max-height: calc( |
| | | 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px - |
| | | $process-header-height - 40px |
| | | $process-header-height - 40px |
| | | ); |
| | | overflow: auto; |
| | | flex-direction: column; |
| | |
| | | <template> |
| | | <ContentWrap :bodyStyle="{ padding: '20px 16px' }"> |
| | | <SimpleProcessDesigner :model-id="modelId" @success="close" /> |
| | | <SimpleProcessDesigner |
| | | :model-id="modelId" |
| | | :model-key="modelKey" |
| | | :model-name="modelName" |
| | | :value="currentValue" |
| | | @success="handleSuccess" |
| | | @init-finished="handleInit" |
| | | :start-user-ids="startUserIds" |
| | | ref="designerRef" |
| | | /> |
| | | </ContentWrap> |
| | | </template> |
| | | <script setup lang="ts"> |
| | |
| | | defineOptions({ |
| | | name: 'SimpleModelDesign' |
| | | }) |
| | | const router = useRouter() // 路由 |
| | | const { query } = useRoute() // 路由的查询 |
| | | const modelId = query.modelId as string |
| | | const close = () => { |
| | | router.push({ path: '/bpm/manager/model' }) |
| | | |
| | | const props = defineProps<{ |
| | | modelId?: string |
| | | modelKey?: string |
| | | modelName?: string |
| | | value?: string |
| | | startUserIds?: number[] |
| | | }>() |
| | | |
| | | const emit = defineEmits(['success', 'init-finished']) |
| | | const designerRef = ref() |
| | | const isInitialized = ref(false) |
| | | const currentValue = ref('') |
| | | |
| | | // 初始化或更新当前值 |
| | | const initOrUpdateValue = async () => { |
| | | console.log('initOrUpdateValue', props.value) |
| | | if (props.value) { |
| | | currentValue.value = props.value |
| | | // 如果设计器已经初始化,立即加载数据 |
| | | if (isInitialized.value && designerRef.value) { |
| | | try { |
| | | await designerRef.value.loadProcessData(props.value) |
| | | await nextTick() |
| | | if (designerRef.value.refresh) { |
| | | await designerRef.value.refresh() |
| | | } |
| | | } catch (error) { |
| | | console.error('加载流程数据失败:', error) |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 监听属性变化 |
| | | watch( |
| | | [() => props.modelKey, () => props.modelName, () => props.value], |
| | | async ([newKey, newName, newValue], [oldKey, oldName, oldValue]) => { |
| | | if (designerRef.value && isInitialized.value) { |
| | | try { |
| | | if (newKey && newName && (newKey !== oldKey || newName !== oldName)) { |
| | | await designerRef.value.updateModel(newKey, newName) |
| | | } |
| | | if (newValue && newValue !== oldValue) { |
| | | currentValue.value = newValue |
| | | await designerRef.value.loadProcessData(newValue) |
| | | await nextTick() |
| | | if (designerRef.value.refresh) { |
| | | await designerRef.value.refresh() |
| | | } |
| | | } |
| | | } catch (error) { |
| | | console.error('更新流程数据失败:', error) |
| | | } |
| | | } |
| | | }, |
| | | { deep: true, immediate: true } |
| | | ) |
| | | |
| | | // 初始化完成回调 |
| | | const handleInit = async () => { |
| | | try { |
| | | isInitialized.value = true |
| | | emit('init-finished') |
| | | |
| | | // 等待下一个tick,确保设计器已经准备好 |
| | | await nextTick() |
| | | |
| | | // 初始化完成后,设置初始值 |
| | | if (props.modelKey && props.modelName) { |
| | | await designerRef.value.updateModel(props.modelKey, props.modelName) |
| | | } |
| | | if (props.value) { |
| | | currentValue.value = props.value |
| | | await designerRef.value.loadProcessData(props.value) |
| | | // 再次刷新确保数据正确加载 |
| | | await nextTick() |
| | | if (designerRef.value.refresh) { |
| | | await designerRef.value.refresh() |
| | | } |
| | | } |
| | | } catch (error) { |
| | | console.error('初始化流程数据失败:', error) |
| | | } |
| | | } |
| | | |
| | | // 修改成功回调 |
| | | const handleSuccess = (data?: any) => { |
| | | console.warn('handleSuccess', data) |
| | | if (data && data !== currentValue.value) { |
| | | currentValue.value = data |
| | | emit('success', data) |
| | | } |
| | | } |
| | | |
| | | /** 获取当前流程数据 */ |
| | | const getCurrentFlowData = async () => { |
| | | try { |
| | | if (designerRef.value) { |
| | | const data = await designerRef.value.getCurrentFlowData() |
| | | if (data) { |
| | | currentValue.value = data |
| | | } |
| | | return data |
| | | } |
| | | return currentValue.value || undefined |
| | | } catch (error) { |
| | | console.error('获取流程数据失败:', error) |
| | | return currentValue.value || undefined |
| | | } |
| | | } |
| | | |
| | | // 组件创建时初始化数据 |
| | | onMounted(() => { |
| | | initOrUpdateValue() |
| | | }) |
| | | |
| | | // 组件卸载前保存数据 |
| | | onBeforeUnmount(async () => { |
| | | try { |
| | | const data = await getCurrentFlowData() |
| | | if (data) { |
| | | emit('success', data) |
| | | } |
| | | } catch (error) { |
| | | console.error('保存数据失败:', error) |
| | | } |
| | | }) |
| | | |
| | | defineExpose({ |
| | | getCurrentFlowData, |
| | | refresh: () => designerRef.value?.refresh?.() |
| | | }) |
| | | </script> |
| | | <style lang="scss" scoped></style> |
| | |
| | | <el-button @click="handleQuery"> 确认</el-button> |
| | | <el-button @click="showPopover = false"> 取消</el-button> |
| | | <el-button @click="resetQuery"> 清空</el-button> |
| | | </el-form-item> |
| | | </el-form-item> |
| | | </el-popover> |
| | | </el-form-item> |
| | | |
| | |
| | | let ids = dataListSelections.map(item => { |
| | | return item.id |
| | | }) |
| | | // 启用的二次确认 |
| | | await message.enableConfirm(ids) |
| | | |
| | | // 二次确认 |
| | | await message.confirm('确认要开启所选测点?') |
| | | await DaPoint.enable(ids) |
| | | message.success(t('common.enableSuccess')) |
| | | await getList() |
| | |
| | | let ids = dataListSelections.map(item => { |
| | | return item.id |
| | | }) |
| | | // 启用的二次确认 |
| | | await message.disableConfirm(ids,) |
| | | |
| | | // 二次确认 |
| | | await message.confirm('确认要禁用所选测点?') |
| | | await DaPoint.disable(ids) |
| | | message.success(t('common.disableSuccess')) |
| | | await getList() |
| | |
| | | /> |
| | | <el-table-column label="操作" align="center"> |
| | | <template #default="scope"> |
| | | <el-button link type="primary" @click="copyToClipboard(scope.row.url)"> |
| | | 复制链接 |
| | | </el-button> |
| | | <el-button |
| | | link |
| | | type="danger" |
| | |
| | | formRef.value.open() |
| | | } |
| | | |
| | | /** 复制到剪贴板方法 */ |
| | | const copyToClipboard = (text: string) => { |
| | | navigator.clipboard.writeText(text).then(() => { |
| | | message.success('复制成功') |
| | | }) |
| | | } |
| | | |
| | | /** 删除按钮操作 */ |
| | | const handleDelete = async (id: number) => { |
| | | try { |
| | |
| | | <el-table-column |
| | | prop="" |
| | | label="键" |
| | | min-width="150" |
| | | align="center"> |
| | | <template #default="scope"> |
| | | <el-input v-model="scope.row.key" maxlength="20" clearable :disabled="true" |
| | |
| | | <el-table-column |
| | | prop="" |
| | | label="名称" |
| | | min-width="150" |
| | | align="center"> |
| | | <template #default="scope"> |
| | | <el-input v-model="scope.row.name" maxlength="20" clearable :disabled="true" |
| | |
| | | <el-table-column |
| | | prop="" |
| | | label="类型" |
| | | min-width="100" |
| | | align="center"> |
| | | <template #default="scope"> |
| | | <el-select v-model="scope.row.valuetype" placeholder="请选择" :disabled="true"> |
| | |
| | | <el-table-column |
| | | prop="" |
| | | label="值" |
| | | min-width="300" |
| | | align="center"> |
| | | <template #default="scope"> |
| | | <el-input v-model="scope.row.value" maxlength="256" clearable :disabled="scope.row.key === 'pyFile'" |
| | | <el-input v-model="scope.row.value" maxlength="256" clearable |
| | | :disabled="scope.row.key === 'pyFile_BAK'" |
| | | style="width:100%;hight:100%"/> |
| | | </template> |
| | | </el-table-column> |
| | | <!-- <el-table-column--> |
| | | <!-- prop=""--> |
| | | <!-- label="操作"--> |
| | | <!-- width="100"--> |
| | | <!-- align="center">--> |
| | | <!-- <template #default="scope">--> |
| | | <!-- <el-button--> |
| | | <!-- @click.prevent="addRow(scope.$index, formData.settingList)"--> |
| | | <!-- link--> |
| | | <!-- type="primary"--> |
| | | <!-- size="small">--> |
| | | <!-- 添加--> |
| | | <!-- </el-button>--> |
| | | <!-- <el-button--> |
| | | <!-- @click.prevent="deleteRow(scope.$index, formData.settingList)"--> |
| | | <!-- link--> |
| | | <!-- type="primary"--> |
| | | <!-- size="small">--> |
| | | <!-- 删除--> |
| | | <!-- </el-button>--> |
| | | <!-- </template>--> |
| | | <!-- </el-table-column>--> |
| | | </el-table> |
| | | <el-divider content-position="left">模型下发配置</el-divider> |
| | | <el-row :gutter="20"> |
| | |
| | | <el-table-column prop="disturbancePointNo’" label="无扰切换点位" align="center" min-width="200"> |
| | | <template #default="scope"> |
| | | <el-select v-model="scope.row.disturbancePointNo" |
| | | clearable |
| | | filterable |
| | | placeholder="请选择"> |
| | | <el-option |
| | |
| | | 重置 |
| | | </el-button> |
| | | <el-button |
| | | type="success" |
| | | plain |
| | | @click="enable" |
| | | v-hasPermi="['sche:scheme:update']" |
| | | >启用 |
| | | </el-button> |
| | | <el-button |
| | | type="danger" |
| | | plain |
| | | @click="disable" |
| | | v-hasPermi="['sche:scheme:update']" |
| | | >禁用 |
| | | </el-button> |
| | | <el-button |
| | | type="primary" |
| | | plain |
| | | @click="openForm('create')" |
| | |
| | | |
| | | <!-- 列表 --> |
| | | <ContentWrap> |
| | | <el-table v-loading="loading" :data="list"> |
| | | <el-table v-loading="loading" :data="list" @selection-change="selectionChangeHandle"> |
| | | <el-table-column type="selection" header-align="center" align="center" fixed="left" width="50"/> |
| | | <el-table-column label="方案编号" align="center" prop="code" min-width="100"/> |
| | | <el-table-column label="方案名称" header-align="center" align="left" prop="name" min-width="100"/> |
| | | <el-table-column label="触发方式" align="center" prop="triggerMethod" min-width="100"> |
| | |
| | | import * as ScheduleSchemeApi from '@/api/model/sche/scheme' |
| | | import ScheduleSchemeForm from './ScheduleSchemeForm.vue' |
| | | import RecordList from './record/index.vue' |
| | | import * as DaPoint from "@/api/data/da/point"; |
| | | import {reactive} from "vue"; |
| | | import {InfraJobStatusEnum} from "@/utils/constants"; |
| | | |
| | | defineOptions({name: 'ScheduleScheme'}) |
| | | |
| | |
| | | recordRef.value.open(id) |
| | | } |
| | | |
| | | let dataListSelections = reactive([]) |
| | | // 多选 |
| | | function selectionChangeHandle (val) { |
| | | dataListSelections = val |
| | | } |
| | | // 启用 |
| | | async function enable() { |
| | | let ids = dataListSelections.map(item => { |
| | | return item.id |
| | | }) |
| | | // 二次确认 |
| | | await message.confirm('是否确认要启用所选调度方案?') |
| | | await ScheduleSchemeApi.enable(ids) |
| | | message.success(t('common.enableSuccess')) |
| | | await getList() |
| | | } |
| | | // 禁用 |
| | | async function disable(){ |
| | | let ids = dataListSelections.map(item => { |
| | | return item.id |
| | | }) |
| | | // 二次确认 |
| | | await message.confirm('确认要禁用所选调度方案?') |
| | | await ScheduleSchemeApi.disable(ids) |
| | | message.success(t('common.disableSuccess')) |
| | | await getList() |
| | | } |
| | | |
| | | /** 初始化 **/ |
| | | onMounted(async () => { |
| | | await getList() |
| | |
| | | <template> |
| | | <doc-alert title="地区 & IP" url="https://doc.iocoder.cn/area-and-ip/" /> |
| | | |
| | | <!-- 操作栏 --> |
| | | <ContentWrap> |
| | | <el-button type="primary" plain @click="openForm()"> |
| | |
| | | <template #default="{ height, width }"> |
| | | <!-- Virtualized Table 虚拟化表格:高性能,解决表格在大数据量下的卡顿问题 --> |
| | | <el-table-v2 |
| | | v-loading="loading" |
| | | :columns="columns" |
| | | :data="list" |
| | | :width="width" |
| | |
| | | <AreaForm ref="formRef" /> |
| | | </template> |
| | | <script setup lang="tsx"> |
| | | import type { Column } from 'element-plus' |
| | | import { Column } from 'element-plus' |
| | | import AreaForm from './AreaForm.vue' |
| | | import * as AreaApi from '@/api/system/area' |
| | | |
| | |
| | | // 表格的 column 字段 |
| | | const columns: Column[] = [ |
| | | { |
| | | dataKey: 'id', // 需要渲染当前列的数据字段。例如说:{id:9527, name:'Mike'},则填 id |
| | | dataKey: 'id', // 需要渲染当前列的数据字段 |
| | | title: '编号', // 显示在单元格表头的文本 |
| | | width: 400, // 当前列的宽度,必须设置 |
| | | fixed: true, // 是否固定列 |
| | |
| | | width: 200 |
| | | } |
| | | ] |
| | | // 表格的数据 |
| | | const list = ref([]) |
| | | const loading = ref(true) // 列表的加载中 |
| | | const list = ref([]) // 表格的数据 |
| | | |
| | | /** |
| | | * 获得数据列表 |
| | | */ |
| | | /** 获得数据列表 */ |
| | | const getList = async () => { |
| | | list.value = await AreaApi.getAreaTree() |
| | | loading.value = true |
| | | try { |
| | | list.value = await AreaApi.getAreaTree() |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | /** 添加/修改操作 */ |
| | |
| | | <Icon class="mr-5px" icon="ep:plus" /> |
| | | 新增 |
| | | </el-button> |
| | | <el-button plain type="danger" @click="toggleExpandAll"> |
| | | <Icon class="mr-5px" icon="ep:sort" /> |
| | | 展开/折叠 |
| | | </el-button> |
| | | <el-button plain @click="refreshMenu"> |
| | | <Icon class="mr-5px" icon="ep:refresh" /> |
| | | 刷新菜单缓存 |
| | |
| | | |
| | | <!-- 列表 --> |
| | | <ContentWrap> |
| | | <el-table |
| | | v-if="refreshTable" |
| | | v-loading="loading" |
| | | :data="list" |
| | | :default-expand-all="isExpandAll" |
| | | row-key="id" |
| | | > |
| | | <el-table-column :show-overflow-tooltip="true" label="菜单名称" prop="name" width="250" /> |
| | | <el-table-column align="center" label="图标" prop="icon" width="100"> |
| | | <template #default="scope"> |
| | | <Icon :icon="scope.row.icon" /> |
| | | <div style="height: 700px"> |
| | | <!-- AutoResizer 自动调节大小 --> |
| | | <el-auto-resizer> |
| | | <template #default="{ height, width }"> |
| | | <!-- Virtualized Table 虚拟化表格:高性能,解决表格在大数据量下的卡顿问题 --> |
| | | <el-table-v2 |
| | | v-loading="loading" |
| | | :columns="columns" |
| | | :data="list" |
| | | :width="width" |
| | | :height="height" |
| | | expand-column-key="name" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="排序" prop="sort" width="60" /> |
| | | <el-table-column :show-overflow-tooltip="true" label="权限标识" prop="permission" /> |
| | | <el-table-column :show-overflow-tooltip="true" label="组件路径" prop="component" /> |
| | | <el-table-column :show-overflow-tooltip="true" label="组件名称" prop="componentName" /> |
| | | <el-table-column label="状态" prop="status" width="80"> |
| | | <template #default="scope"> |
| | | <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column align="center" label="操作"> |
| | | <template #default="scope"> |
| | | <el-button |
| | | v-hasPermi="['system:menu:update']" |
| | | link |
| | | type="primary" |
| | | @click="openForm('update', scope.row.id)" |
| | | > |
| | | 修改 |
| | | </el-button> |
| | | <el-button |
| | | v-hasPermi="['system:menu:create']" |
| | | link |
| | | type="primary" |
| | | @click="openForm('create', undefined, scope.row.id)" |
| | | > |
| | | 新增 |
| | | </el-button> |
| | | <el-button |
| | | v-hasPermi="['system:menu:delete']" |
| | | link |
| | | type="danger" |
| | | @click="handleDelete(scope.row.id)" |
| | | > |
| | | 删除 |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-auto-resizer> |
| | | </div> |
| | | </ContentWrap> |
| | | |
| | | <!-- 表单弹窗:添加/修改 --> |
| | |
| | | import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' |
| | | import { handleTree } from '@/utils/tree' |
| | | import * as MenuApi from '@/api/system/menu' |
| | | import { MenuVO } from '@/api/system/menu' |
| | | import MenuForm from './MenuForm.vue' |
| | | import {CACHE_KEY, useCache, useSessionCache} from '@/hooks/web/useCache' |
| | | import { CACHE_KEY, useCache, useSessionCache } from '@/hooks/web/useCache' |
| | | import { h } from 'vue' |
| | | import { Column, ElButton } from 'element-plus' |
| | | import { Icon } from '@/components/Icon' |
| | | import { hasPermission } from '@/directives/permission/hasPermi' |
| | | import { CommonStatusEnum } from '@/utils/constants' |
| | | |
| | | defineOptions({ name: 'SystemMenu' }) |
| | | |
| | |
| | | const { t } = useI18n() // 国际化 |
| | | const message = useMessage() // 消息弹窗 |
| | | |
| | | // 表格的 column 字段 |
| | | const columns: Column[] = [ |
| | | { |
| | | dataKey: 'name', |
| | | title: '菜单名称', |
| | | width: 250 |
| | | }, |
| | | { |
| | | dataKey: 'icon', |
| | | title: '图标', |
| | | width: 150, |
| | | cellRenderer: ({ rowData }) => { |
| | | return h(Icon, { |
| | | icon: rowData.icon |
| | | }) |
| | | } |
| | | }, |
| | | { |
| | | dataKey: 'sort', |
| | | title: '排序', |
| | | width: 100 |
| | | }, |
| | | { |
| | | dataKey: 'permission', |
| | | title: '权限标识', |
| | | width: 240 |
| | | }, |
| | | { |
| | | dataKey: 'component', |
| | | title: '组件路径', |
| | | width: 240 |
| | | }, |
| | | { |
| | | dataKey: 'componentName', |
| | | title: '组件名称', |
| | | width: 240 |
| | | }, |
| | | { |
| | | dataKey: 'status', |
| | | title: '状态', |
| | | width: 160, |
| | | cellRenderer: ({ rowData }) => { |
| | | return h(ElSwitch, { |
| | | modelValue: rowData.status, |
| | | activeValue: CommonStatusEnum.ENABLE, |
| | | inactiveValue: CommonStatusEnum.DISABLE, |
| | | loading: menuStatusUpdating.value[rowData.id], |
| | | disabled: !hasPermission(['system:menu:update']), |
| | | onChange: (val) => handleStatusChanged(rowData, val as number) |
| | | }) |
| | | } |
| | | }, |
| | | { |
| | | dataKey: 'operation', |
| | | title: '操作', |
| | | width: 200, |
| | | cellRenderer: ({ rowData }) => { |
| | | return h( |
| | | 'div', |
| | | [ |
| | | hasPermission(['system:menu:update']) && |
| | | h( |
| | | ElButton, |
| | | { |
| | | link: true, |
| | | type: 'primary', |
| | | onClick: () => openForm('update', rowData.id) |
| | | }, |
| | | '修改' |
| | | ), |
| | | hasPermission(['system:menu:create']) && |
| | | h( |
| | | ElButton, |
| | | { |
| | | link: true, |
| | | type: 'primary', |
| | | onClick: () => openForm('create', undefined, rowData.id) |
| | | }, |
| | | '新增' |
| | | ), |
| | | hasPermission(['system:menu:delete']) && |
| | | h( |
| | | ElButton, |
| | | { |
| | | link: true, |
| | | type: 'danger', |
| | | onClick: () => handleDelete(rowData.id) |
| | | }, |
| | | '删除' |
| | | ) |
| | | ].filter(Boolean) |
| | | ) |
| | | } |
| | | } |
| | | ] |
| | | const loading = ref(true) // 列表的加载中 |
| | | const list = ref<any>([]) // 列表的数据 |
| | | const queryParams = reactive({ |
| | |
| | | status: undefined |
| | | }) |
| | | const queryFormRef = ref() // 搜索的表单 |
| | | const isExpandAll = ref(false) // 是否展开,默认全部折叠 |
| | | const refreshTable = ref(true) // 重新渲染表格状态 |
| | | |
| | | /** 查询列表 */ |
| | | const getList = async () => { |
| | |
| | | formRef.value.open(type, id, parentId) |
| | | } |
| | | |
| | | /** 展开/折叠操作 */ |
| | | const toggleExpandAll = () => { |
| | | refreshTable.value = false |
| | | isExpandAll.value = !isExpandAll.value |
| | | nextTick(() => { |
| | | refreshTable.value = true |
| | | }) |
| | | } |
| | | |
| | | /** 刷新菜单缓存按钮操作 */ |
| | | const refreshMenu = async () => { |
| | | try { |
| | | await message.confirm('即将更新缓存刷新浏览器!', '刷新菜单缓存') |
| | | // 清空,从而触发刷新 |
| | | wsCache.delete(CACHE_KEY.USER) |
| | | wsCache.delete(CACHE_KEY.ROLE_ROUTERS) |
| | | wsSessionCache.delete(CACHE_KEY.USER) |
| | | wsSessionCache.delete(CACHE_KEY.ROLE_ROUTERS) |
| | | // 刷新浏览器 |
| | | location.reload() |
| | |
| | | } catch {} |
| | | } |
| | | |
| | | /** 开启/关闭菜单的状态 */ |
| | | const menuStatusUpdating = ref({}) // 菜单状态更新中的 menu 映射。key:菜单编号,value:是否更新中 |
| | | const handleStatusChanged = async (menu: MenuVO, val: number) => { |
| | | // 1. 标记 menu.id 更新中 |
| | | menuStatusUpdating.value[menu.id] = true |
| | | try { |
| | | // 2. 发起更新状态 |
| | | menu.status = val |
| | | await MenuApi.updateMenu(menu) |
| | | } finally { |
| | | // 3. 标记 menu.id 更新完成 |
| | | menuStatusUpdating.value[menu.id] = false |
| | | } |
| | | } |
| | | |
| | | /** 初始化 **/ |
| | | onMounted(() => { |
| | | getList() |