feat: 流程配置页(节点链编辑、出厂模板、版本历史,ADR 0015)
This commit is contained in:
@@ -0,0 +1,52 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import type { ApiResult } from '@/api';
|
||||||
|
import { THINK_API_URL } from '@/config/setting';
|
||||||
|
import type {
|
||||||
|
ApprovalFlow,
|
||||||
|
ApprovalFlowNode,
|
||||||
|
ApprovalFlowVersion
|
||||||
|
} from './model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流程定义列表(当前版本与出厂模板)
|
||||||
|
*/
|
||||||
|
export async function listFlowConfigs() {
|
||||||
|
const res = await request.get<ApiResult<ApprovalFlow[]>>(
|
||||||
|
THINK_API_URL + '/approval/flow'
|
||||||
|
);
|
||||||
|
if (res.data.code === 0 && res.data.data) {
|
||||||
|
return res.data.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流程定义历史版本
|
||||||
|
*/
|
||||||
|
export async function listFlowHistory(flowCode: string) {
|
||||||
|
const res = await request.get<ApiResult<ApprovalFlowVersion[]>>(
|
||||||
|
THINK_API_URL + '/approval/flow/history',
|
||||||
|
{ params: { flowCode } }
|
||||||
|
);
|
||||||
|
if (res.data.code === 0 && res.data.data) {
|
||||||
|
return res.data.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布流程定义新版本(保存即发布)
|
||||||
|
*/
|
||||||
|
export async function publishFlowConfig(data: {
|
||||||
|
flowCode: string;
|
||||||
|
nodes: ApprovalFlowNode[];
|
||||||
|
}) {
|
||||||
|
const res = await request.post<ApiResult<ApprovalFlow>>(
|
||||||
|
THINK_API_URL + '/approval/flow/publish',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.data.code === 0 && res.data.data) {
|
||||||
|
return res.data.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* 流程节点会签组(与后端 ApprovalNode.ApprovalGroup 对应)
|
||||||
|
*/
|
||||||
|
export interface ApprovalFlowNodeGroup {
|
||||||
|
key: string;
|
||||||
|
name: string;
|
||||||
|
resolver?: string;
|
||||||
|
orgCode?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流程节点回填字段(与后端 ApprovalNode.WritableField 对应)
|
||||||
|
*/
|
||||||
|
export interface ApprovalFlowWritableField {
|
||||||
|
field: string;
|
||||||
|
required: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流程节点(与后端 ApprovalNode 对应)
|
||||||
|
*/
|
||||||
|
export interface ApprovalFlowNode {
|
||||||
|
key: string;
|
||||||
|
type: string;
|
||||||
|
resolver?: string;
|
||||||
|
orgCode?: string;
|
||||||
|
name: string;
|
||||||
|
groups?: ApprovalFlowNodeGroup[];
|
||||||
|
writableFields?: ApprovalFlowWritableField[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 可写字段元数据(与后端 ApprovalFieldSpec 对应)
|
||||||
|
*/
|
||||||
|
export interface ApprovalFieldSpec {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
maxLength?: number;
|
||||||
|
unique?: boolean;
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流程定义当前版本
|
||||||
|
*/
|
||||||
|
export interface ApprovalFlow {
|
||||||
|
flowCode: string;
|
||||||
|
flowName: string;
|
||||||
|
version?: number;
|
||||||
|
definitionId?: number;
|
||||||
|
publishTime?: string;
|
||||||
|
nodes: ApprovalFlowNode[];
|
||||||
|
factoryNodes: ApprovalFlowNode[];
|
||||||
|
factoryVersion?: number;
|
||||||
|
fieldSpecs: ApprovalFieldSpec[];
|
||||||
|
resolvers: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流程定义历史版本
|
||||||
|
*/
|
||||||
|
export interface ApprovalFlowVersion {
|
||||||
|
definitionId: number;
|
||||||
|
version: number;
|
||||||
|
name: string;
|
||||||
|
publishTime?: string;
|
||||||
|
nodes: ApprovalFlowNode[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,337 @@
|
|||||||
|
<!-- 流程节点编辑卡片(ADR 0015):节点字段 + 会签组 + 回填字段 -->
|
||||||
|
<template>
|
||||||
|
<a-card size="small" class="node-card">
|
||||||
|
<template #title>
|
||||||
|
<span>{{ index + 1 }}. {{ form.name || '未命名节点' }}</span>
|
||||||
|
<a-tag color="blue" style="margin-left: 8px">
|
||||||
|
{{ nodeTypeLabel(form.type) }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
<template #extra>
|
||||||
|
<a-space :size="4">
|
||||||
|
<a-button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
:disabled="index === 0"
|
||||||
|
@click="emit('move', -1)"
|
||||||
|
>
|
||||||
|
上移
|
||||||
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
:disabled="index === total - 1"
|
||||||
|
@click="emit('move', 1)"
|
||||||
|
>
|
||||||
|
下移
|
||||||
|
</a-button>
|
||||||
|
<a-button type="link" danger size="small" @click="emit('remove')">
|
||||||
|
删除
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
<a-form layout="vertical">
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :md="8" :sm="24">
|
||||||
|
<a-form-item label="节点名称">
|
||||||
|
<a-input
|
||||||
|
v-model:value="form.name"
|
||||||
|
allow-clear
|
||||||
|
:maxlength="50"
|
||||||
|
placeholder="如:报送部门负责人"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :md="8" :sm="24">
|
||||||
|
<a-form-item label="节点标识">
|
||||||
|
<a-input
|
||||||
|
v-model:value="form.key"
|
||||||
|
allow-clear
|
||||||
|
:maxlength="40"
|
||||||
|
placeholder="同一流程内唯一,如:deptLeader"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :md="8" :sm="24">
|
||||||
|
<a-form-item label="节点类型">
|
||||||
|
<a-select v-model:value="form.type" :options="NODE_TYPE_OPTIONS" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col v-if="form.type !== 'COUNTERSIGN'" :md="8" :sm="24">
|
||||||
|
<a-form-item label="审批人解析器">
|
||||||
|
<a-select
|
||||||
|
v-model:value="form.resolver"
|
||||||
|
:options="resolverOptions"
|
||||||
|
allow-clear
|
||||||
|
show-search
|
||||||
|
option-filter-prop="label"
|
||||||
|
placeholder="选择解析器"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col v-if="needOrgCode" :md="8" :sm="24">
|
||||||
|
<a-form-item label="组织编码">
|
||||||
|
<a-select
|
||||||
|
v-model:value="form.orgCode"
|
||||||
|
:options="orgOptions"
|
||||||
|
allow-clear
|
||||||
|
show-search
|
||||||
|
option-filter-prop="label"
|
||||||
|
placeholder="选择组织"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<div v-if="form.type === 'COUNTERSIGN'" class="node-section">
|
||||||
|
<div class="node-section-title">
|
||||||
|
<span>会签组(组内或签,组间会签)</span>
|
||||||
|
<a-button type="link" size="small" @click="addGroup">
|
||||||
|
添加会签组
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<a-row
|
||||||
|
v-for="(group, groupIndex) in form.groups"
|
||||||
|
:key="groupIndex"
|
||||||
|
:gutter="12"
|
||||||
|
>
|
||||||
|
<a-col :md="5" :sm="24">
|
||||||
|
<a-form-item label="组标识">
|
||||||
|
<a-input v-model:value="group.key" allow-clear />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :md="5" :sm="24">
|
||||||
|
<a-form-item label="组名称">
|
||||||
|
<a-input v-model:value="group.name" allow-clear />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :md="6" :sm="24">
|
||||||
|
<a-form-item label="解析器">
|
||||||
|
<a-select
|
||||||
|
v-model:value="group.resolver"
|
||||||
|
:options="resolverOptions"
|
||||||
|
option-filter-prop="label"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col v-if="isOrgResolver(group.resolver)" :md="6" :sm="24">
|
||||||
|
<a-form-item label="组织编码">
|
||||||
|
<a-select
|
||||||
|
v-model:value="group.orgCode"
|
||||||
|
:options="orgOptions"
|
||||||
|
allow-clear
|
||||||
|
show-search
|
||||||
|
option-filter-prop="label"
|
||||||
|
placeholder="选择组织"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :md="2" :sm="24">
|
||||||
|
<a-form-item label=" ">
|
||||||
|
<a-button type="link" danger @click="removeGroup(groupIndex)">
|
||||||
|
删除
|
||||||
|
</a-button>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="writableVisible" class="node-section">
|
||||||
|
<div class="node-section-title">
|
||||||
|
<span>回填字段</span>
|
||||||
|
<a-button type="link" size="small" @click="addField">
|
||||||
|
添加回填字段
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<a-row
|
||||||
|
v-for="(field, fieldIndex) in form.writableFields"
|
||||||
|
:key="fieldIndex"
|
||||||
|
:gutter="12"
|
||||||
|
>
|
||||||
|
<a-col :md="8" :sm="24">
|
||||||
|
<a-form-item label="字段">
|
||||||
|
<a-select
|
||||||
|
v-model:value="field.field"
|
||||||
|
:options="fieldOptions"
|
||||||
|
show-search
|
||||||
|
option-filter-prop="label"
|
||||||
|
placeholder="选择可回填字段"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :md="4" :sm="24">
|
||||||
|
<a-form-item label="必填">
|
||||||
|
<a-switch v-model:checked="field.required" size="small" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :md="4" :sm="24">
|
||||||
|
<a-form-item label=" ">
|
||||||
|
<a-button type="link" danger @click="removeField(fieldIndex)">
|
||||||
|
删除
|
||||||
|
</a-button>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</div>
|
||||||
|
</a-form>
|
||||||
|
</a-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, reactive, watch } from 'vue';
|
||||||
|
import {
|
||||||
|
NODE_TYPE_OPTIONS,
|
||||||
|
ORG_RESOLVERS,
|
||||||
|
WRITABLE_TYPES,
|
||||||
|
nodeTypeLabel
|
||||||
|
} from '../constants';
|
||||||
|
import type {
|
||||||
|
ApprovalFieldSpec,
|
||||||
|
ApprovalFlowNode,
|
||||||
|
ApprovalFlowNodeGroup
|
||||||
|
} from '@/api/approval/flow/model';
|
||||||
|
import type { ApprovalOrganization } from '@/api/approval/instance/model';
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update', node: ApprovalFlowNode): void;
|
||||||
|
(e: 'remove'): void;
|
||||||
|
(e: 'move', step: number): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
node: ApprovalFlowNode;
|
||||||
|
index: number;
|
||||||
|
total: number;
|
||||||
|
resolvers: string[];
|
||||||
|
organizations: ApprovalOrganization[];
|
||||||
|
fieldSpecs: ApprovalFieldSpec[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const form = reactive<ApprovalFlowNode>({
|
||||||
|
key: props.node.key,
|
||||||
|
type: props.node.type,
|
||||||
|
resolver: props.node.resolver,
|
||||||
|
orgCode: props.node.orgCode,
|
||||||
|
name: props.node.name,
|
||||||
|
groups: (props.node.groups || []).map((group) => ({ ...group })),
|
||||||
|
writableFields: (props.node.writableFields || []).map((field) => ({
|
||||||
|
...field
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
|
const isOrgResolver = (resolver?: string) => {
|
||||||
|
return !!resolver && ORG_RESOLVERS.indexOf(resolver) !== -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const needOrgCode = computed(() => isOrgResolver(form.resolver));
|
||||||
|
|
||||||
|
const writableVisible = computed(() => {
|
||||||
|
return (
|
||||||
|
WRITABLE_TYPES.indexOf(form.type) !== -1 && props.fieldSpecs.length > 0
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const resolverOptions = computed(() => {
|
||||||
|
return props.resolvers.map((resolver) => ({
|
||||||
|
value: resolver,
|
||||||
|
label: resolver
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
const orgOptions = computed(() => {
|
||||||
|
return props.organizations.map((organization) => ({
|
||||||
|
value: organization.organizationCode,
|
||||||
|
label:
|
||||||
|
organization.organizationFullName || organization.organizationName || ''
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
const fieldOptions = computed(() => {
|
||||||
|
return props.fieldSpecs.map((spec) => ({
|
||||||
|
value: spec.key,
|
||||||
|
label: spec.label + '(' + spec.key + ')'
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
const addGroup = () => {
|
||||||
|
const group: ApprovalFlowNodeGroup = {
|
||||||
|
key: 'group' + ((form.groups ? form.groups.length : 0) + 1),
|
||||||
|
name: '会签组' + ((form.groups ? form.groups.length : 0) + 1),
|
||||||
|
resolver: 'orgLeaderByCode'
|
||||||
|
};
|
||||||
|
form.groups = [...(form.groups || []), group];
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeGroup = (index: number) => {
|
||||||
|
form.groups = (form.groups || []).filter((item, i) => i !== index);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addField = () => {
|
||||||
|
if (!props.fieldSpecs.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
form.writableFields = [
|
||||||
|
...(form.writableFields || []),
|
||||||
|
{ field: props.fieldSpecs[0].key, required: false }
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeField = (index: number) => {
|
||||||
|
form.writableFields = (form.writableFields || []).filter(
|
||||||
|
(item, i) => i !== index
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 节点类型切换时同步清理不适用的配置,避免提交被后端拦下 */
|
||||||
|
watch(
|
||||||
|
() => form.type,
|
||||||
|
(type) => {
|
||||||
|
if (type === 'COUNTERSIGN') {
|
||||||
|
form.resolver = undefined;
|
||||||
|
form.orgCode = undefined;
|
||||||
|
if (!form.groups || !form.groups.length) {
|
||||||
|
addGroup();
|
||||||
|
}
|
||||||
|
} else if (form.groups && form.groups.length) {
|
||||||
|
form.groups = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/* 换了非按组织取人的解析器,组织编码就不再适用 */
|
||||||
|
watch(
|
||||||
|
() => form.resolver,
|
||||||
|
(resolver) => {
|
||||||
|
if (!isOrgResolver(resolver)) {
|
||||||
|
form.orgCode = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
form,
|
||||||
|
() => {
|
||||||
|
emit('update', { ...form });
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.node-card {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-section {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: rgba(0, 0, 0, 0.65);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* 流程节点类型(与后端 ApprovalConstants 对应)
|
||||||
|
*/
|
||||||
|
export const NODE_TYPE_OPTIONS = [
|
||||||
|
{ value: 'APPROVE', label: '审批(或签)' },
|
||||||
|
{ value: 'COUNTERSIGN', label: '会签' },
|
||||||
|
{ value: 'DISPATCH', label: '派单' },
|
||||||
|
{ value: 'CONFIRM', label: '存档' },
|
||||||
|
{ value: 'ARCHIVE', label: '归档' }
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 需要固定组织编码的解析器 */
|
||||||
|
export const ORG_RESOLVERS = ['orgLeaderByCode', 'orgDeputyByCode'];
|
||||||
|
|
||||||
|
/** 可配置回填字段的节点类型 */
|
||||||
|
export const WRITABLE_TYPES = ['APPROVE', 'COUNTERSIGN', 'CONFIRM'];
|
||||||
|
|
||||||
|
/** 节点类型显示名 */
|
||||||
|
export function nodeTypeLabel(type?: string) {
|
||||||
|
const item = NODE_TYPE_OPTIONS.find((option) => option.value === type);
|
||||||
|
return item ? item.label : type || '';
|
||||||
|
}
|
||||||
@@ -0,0 +1,364 @@
|
|||||||
|
<!-- 流程配置(ADR 0015):编辑节点链并发布新版本 -->
|
||||||
|
<template>
|
||||||
|
<div class="ele-body">
|
||||||
|
<a-card :bordered="false" :body-style="{ padding: '16px' }">
|
||||||
|
<ele-split-layout
|
||||||
|
width="320px"
|
||||||
|
allow-collapse
|
||||||
|
:style="{ minHeight: 'calc(100vh - 152px)' }"
|
||||||
|
>
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<a-list :data-source="flows" size="small">
|
||||||
|
<template #renderItem="{ item }">
|
||||||
|
<a-list-item
|
||||||
|
class="flow-item"
|
||||||
|
:class="{ 'flow-item-active': item.flowCode === flowCode }"
|
||||||
|
@click="selectFlow(item)"
|
||||||
|
>
|
||||||
|
<div class="flow-item-body">
|
||||||
|
<div class="flow-item-title">{{ item.flowName }}</div>
|
||||||
|
<div class="flow-item-meta">
|
||||||
|
{{
|
||||||
|
item.version ? '当前版本 v' + item.version : '尚未发布'
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-list-item>
|
||||||
|
</template>
|
||||||
|
</a-list>
|
||||||
|
</a-spin>
|
||||||
|
<template #content>
|
||||||
|
<div v-if="current">
|
||||||
|
<a-alert
|
||||||
|
type="warning"
|
||||||
|
show-icon
|
||||||
|
message="保存即发布新版本,仅对之后提交的单据生效;已提交的单据继续走原版本。"
|
||||||
|
style="margin-bottom: 12px"
|
||||||
|
/>
|
||||||
|
<div class="flow-toolbar">
|
||||||
|
<div class="flow-toolbar-title">
|
||||||
|
{{ current.flowName }}
|
||||||
|
<span class="ele-text-secondary flow-toolbar-meta">
|
||||||
|
{{
|
||||||
|
current.version
|
||||||
|
? 'v' + current.version + ' · ' + current.publishTime
|
||||||
|
: '尚未发布(出厂模板 v' + current.factoryVersion + ')'
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<a-space :size="8">
|
||||||
|
<a-button @click="loadFactory">载入出厂模板</a-button>
|
||||||
|
<a-button :disabled="!current.version" @click="openHistory">
|
||||||
|
版本历史
|
||||||
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
v-permission="'approval:flow:publish'"
|
||||||
|
type="primary"
|
||||||
|
:loading="saving"
|
||||||
|
@click="publish"
|
||||||
|
>
|
||||||
|
保存并发布
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</div>
|
||||||
|
<a-empty
|
||||||
|
v-if="!nodes.length"
|
||||||
|
description="该流程尚未发布,点击「载入出厂模板」开始编辑"
|
||||||
|
/>
|
||||||
|
<node-card
|
||||||
|
v-for="(node, index) in nodes"
|
||||||
|
:key="editorRevision + '-' + index + '-' + node.key"
|
||||||
|
:node="node"
|
||||||
|
:index="index"
|
||||||
|
:total="nodes.length"
|
||||||
|
:resolvers="current.resolvers || []"
|
||||||
|
:organizations="organizations"
|
||||||
|
:field-specs="current.fieldSpecs || []"
|
||||||
|
@update="updateNode(index, $event)"
|
||||||
|
@remove="removeNode(index)"
|
||||||
|
@move="moveNode(index, $event)"
|
||||||
|
/>
|
||||||
|
<a-button
|
||||||
|
v-if="nodes.length"
|
||||||
|
type="dashed"
|
||||||
|
block
|
||||||
|
class="flow-add"
|
||||||
|
@click="addNode"
|
||||||
|
>
|
||||||
|
添加节点
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<a-empty v-else description="请选择左侧流程" />
|
||||||
|
</template>
|
||||||
|
</ele-split-layout>
|
||||||
|
</a-card>
|
||||||
|
<a-drawer v-model:open="historyVisible" title="版本历史" :width="560">
|
||||||
|
<a-spin :spinning="historyLoading">
|
||||||
|
<a-empty v-if="!history.length" description="暂无版本" />
|
||||||
|
<a-card
|
||||||
|
v-for="item in history"
|
||||||
|
:key="item.definitionId"
|
||||||
|
size="small"
|
||||||
|
class="history-card"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<span>v{{ item.version }}</span>
|
||||||
|
<span class="ele-text-secondary" style="margin-left: 8px">
|
||||||
|
{{ item.publishTime }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template #extra>
|
||||||
|
<a-button type="link" size="small" @click="loadVersion(item)">
|
||||||
|
以此版本为蓝本
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
v-for="(node, nodeIndex) in item.nodes"
|
||||||
|
:key="nodeIndex"
|
||||||
|
class="history-node"
|
||||||
|
>
|
||||||
|
{{ nodeIndex + 1 }}. {{ node.name }}({{
|
||||||
|
nodeTypeLabel(node.type)
|
||||||
|
}})
|
||||||
|
</div>
|
||||||
|
</a-card>
|
||||||
|
</a-spin>
|
||||||
|
</a-drawer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { Modal, message } from 'ant-design-vue';
|
||||||
|
import {
|
||||||
|
listFlowConfigs,
|
||||||
|
listFlowHistory,
|
||||||
|
publishFlowConfig
|
||||||
|
} from '@/api/approval/flow';
|
||||||
|
import { listApprovalOrganizations } from '@/api/approval/directory';
|
||||||
|
import NodeCard from './components/node-card.vue';
|
||||||
|
import { nodeTypeLabel } from './constants';
|
||||||
|
import type {
|
||||||
|
ApprovalFlow,
|
||||||
|
ApprovalFlowNode,
|
||||||
|
ApprovalFlowVersion
|
||||||
|
} from '@/api/approval/flow/model';
|
||||||
|
import type { ApprovalOrganization } from '@/api/approval/instance/model';
|
||||||
|
|
||||||
|
/* 深拷贝节点:编辑区不能直接改列表里的对象 */
|
||||||
|
const cloneNodes = (nodes?: ApprovalFlowNode[]) => {
|
||||||
|
return JSON.parse(JSON.stringify(nodes || [])) as ApprovalFlowNode[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const saving = ref(false);
|
||||||
|
const flows = ref<ApprovalFlow[]>([]);
|
||||||
|
const current = ref<ApprovalFlow | null>(null);
|
||||||
|
const flowCode = ref('');
|
||||||
|
const nodes = ref<ApprovalFlowNode[]>([]);
|
||||||
|
const editorRevision = ref(0);
|
||||||
|
const organizations = ref<ApprovalOrganization[]>([]);
|
||||||
|
const historyVisible = ref(false);
|
||||||
|
const historyLoading = ref(false);
|
||||||
|
const history = ref<ApprovalFlowVersion[]>([]);
|
||||||
|
|
||||||
|
/* 加载编辑区:整链替换时递增 revision,强制节点卡片重新初始化 */
|
||||||
|
const applyFlow = (flow: ApprovalFlow) => {
|
||||||
|
current.value = flow;
|
||||||
|
flowCode.value = flow.flowCode;
|
||||||
|
nodes.value = cloneNodes(flow.nodes);
|
||||||
|
editorRevision.value = editorRevision.value + 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshFlows = (keepCode?: string) => {
|
||||||
|
loading.value = true;
|
||||||
|
return listFlowConfigs()
|
||||||
|
.then((data) => {
|
||||||
|
loading.value = false;
|
||||||
|
flows.value = data;
|
||||||
|
const code = keepCode || flowCode.value;
|
||||||
|
const item = data.find((flow) => flow.flowCode === code);
|
||||||
|
if (item) {
|
||||||
|
applyFlow(item);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
loading.value = false;
|
||||||
|
message.error(e.message);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectFlow = (flow: ApprovalFlow) => {
|
||||||
|
applyFlow(flow);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateNode = (index: number, node: ApprovalFlowNode) => {
|
||||||
|
const list = [...nodes.value];
|
||||||
|
list[index] = node;
|
||||||
|
nodes.value = list;
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeNode = (index: number) => {
|
||||||
|
nodes.value = nodes.value.filter((item, i) => i !== index);
|
||||||
|
};
|
||||||
|
|
||||||
|
const moveNode = (index: number, step: number) => {
|
||||||
|
const target = index + step;
|
||||||
|
if (target < 0 || target >= nodes.value.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list = [...nodes.value];
|
||||||
|
const temp = list[index];
|
||||||
|
list[index] = list[target];
|
||||||
|
list[target] = temp;
|
||||||
|
nodes.value = list;
|
||||||
|
editorRevision.value = editorRevision.value + 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addNode = () => {
|
||||||
|
const next: ApprovalFlowNode = {
|
||||||
|
key: 'node' + (nodes.value.length + 1),
|
||||||
|
type: 'APPROVE',
|
||||||
|
name: '新节点',
|
||||||
|
resolver: 'reporterOrgLeader'
|
||||||
|
};
|
||||||
|
nodes.value = [...nodes.value, next];
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadFactory = () => {
|
||||||
|
if (!current.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Modal.confirm({
|
||||||
|
title: '载入出厂模板?',
|
||||||
|
content:
|
||||||
|
'会用代码里声明的节点覆盖当前编辑区,需要再点「保存并发布」才生效。',
|
||||||
|
okText: '载入',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk: () => {
|
||||||
|
nodes.value = cloneNodes(current.value?.factoryNodes);
|
||||||
|
editorRevision.value = editorRevision.value + 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const openHistory = () => {
|
||||||
|
if (!current.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
historyVisible.value = true;
|
||||||
|
historyLoading.value = true;
|
||||||
|
listFlowHistory(current.value.flowCode)
|
||||||
|
.then((data) => {
|
||||||
|
historyLoading.value = false;
|
||||||
|
history.value = data;
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
historyLoading.value = false;
|
||||||
|
message.error(e.message);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadVersion = (item: ApprovalFlowVersion) => {
|
||||||
|
nodes.value = cloneNodes(item.nodes);
|
||||||
|
editorRevision.value = editorRevision.value + 1;
|
||||||
|
historyVisible.value = false;
|
||||||
|
message.info('已载入 v' + item.version + ',确认后点「保存并发布」');
|
||||||
|
};
|
||||||
|
|
||||||
|
const publish = () => {
|
||||||
|
if (!current.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!nodes.value.length) {
|
||||||
|
message.warning('请先配置流程节点');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认发布新版本?',
|
||||||
|
content:
|
||||||
|
'保存即发布,立即对之后提交的单据生效;已提交的单据继续走原版本。',
|
||||||
|
okText: '发布',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk: () => {
|
||||||
|
saving.value = true;
|
||||||
|
return publishFlowConfig({
|
||||||
|
flowCode: current.value!.flowCode,
|
||||||
|
nodes: nodes.value
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
saving.value = false;
|
||||||
|
message.success('已发布 v' + data.version);
|
||||||
|
return refreshFlows(data.flowCode);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
saving.value = false;
|
||||||
|
message.error(e.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
refreshFlows();
|
||||||
|
listApprovalOrganizations()
|
||||||
|
.then((data) => {
|
||||||
|
organizations.value = data;
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.flow-item {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-item-active {
|
||||||
|
background: rgba(24, 144, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-item-title {
|
||||||
|
color: rgba(0, 0, 0, 0.85);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-item-meta {
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-toolbar-title {
|
||||||
|
color: rgba(0, 0, 0, 0.85);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-toolbar-meta {
|
||||||
|
margin-left: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-add {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-card {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-node {
|
||||||
|
color: rgba(0, 0, 0, 0.65);
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user