365 lines
10 KiB
Vue
365 lines
10 KiB
Vue
<!-- 流程配置(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 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[]>([]);
|
||
|
||
/* 深拷贝节点:编辑区不能直接改列表里的对象 */
|
||
const cloneNodes = (nodes?: ApprovalFlowNode[]) => {
|
||
return JSON.parse(JSON.stringify(nodes || [])) as ApprovalFlowNode[];
|
||
};
|
||
|
||
/* 加载编辑区:整链替换时递增 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>
|