Compare commits
4
Commits
98ea069691
...
04fd54e945
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04fd54e945 | ||
|
|
c7e7a7aa85 | ||
|
|
33500e23d6 | ||
|
|
650665224c |
+15
-3
@@ -277,9 +277,13 @@ class FlowApi {
|
||||
routerStatus(id: number | string, enabled: boolean) {
|
||||
return post(`${ROOT}/admin/router/status/${id}?enabled=${enabled}`);
|
||||
}
|
||||
/** 重跑路由(判断失败挂起的恢复入口:重新调判断,成功即按出口续走) */
|
||||
instanceReroute(instanceId: number | string) {
|
||||
return post(`${ROOT}/admin/instance/${instanceId}/reroute`);
|
||||
/**
|
||||
* 重跑路由(判断失败挂起的恢复入口:重新调判断,成功即按出口续走)。
|
||||
* useLatestDef=true(F5-4 僵尸逃生门):按该流程当前已发布最高版本重跑并刷新办件定义快照。
|
||||
*/
|
||||
instanceReroute(instanceId: number | string, useLatestDef?: boolean) {
|
||||
const suffix = useLatestDef ? "?useLatestDef=true" : "";
|
||||
return post(`${ROOT}/admin/instance/${instanceId}/reroute${suffix}`);
|
||||
}
|
||||
/** 表达式调试求值(2026-10-10:与运行时同一求值核心;不落库不走流程) */
|
||||
routerEval(body: { expression: string; variablesJson?: string }) {
|
||||
@@ -290,6 +294,14 @@ class FlowApi {
|
||||
adminInstances(params: { status?: number; keyword?: string; page?: number; size?: number }) {
|
||||
return get(`${ROOT}/admin/instance/page`, params);
|
||||
}
|
||||
/**
|
||||
* 发起时指派预览(F6 共享选择器数据源;与 oapi /oapi/flow/apply/preview 同形:
|
||||
* {flowGuid, defVersion, nodes:[{nodeId,nodeName,countersign,selection,candidates}]})。
|
||||
* 纯预置:后端 /api 代理端点属 F6 正式接入批次,接线时同批点亮(FlowAssignSelector 当前为备用组件)。
|
||||
*/
|
||||
applyPreview(flowGuid: string) {
|
||||
return get(`${ROOT}/apply/preview`, { flowGuid });
|
||||
}
|
||||
}
|
||||
|
||||
export const flowApi = new FlowApi();
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* F6 发起时指派·共享选择器(纯备用组件,2026-10-18 P6.6;未接入任何页面)。
|
||||
*
|
||||
* 用法(未来接入发起页时):
|
||||
* <FlowAssignSelector ref="assignSelector" :flow-guid="boundFlowGuid" />
|
||||
* 提交前:const assigns = assignSelector.value?.collect(); // [{nodeId, userIds}],中心 ApplyBody.assigns 同构
|
||||
* 校验:const err = assignSelector.value?.validate(); // 非空字符串=未通过(必填/数量约束)
|
||||
* 重提/换线场景:flowGuid 变化自动重拉(watch),已选清空并提示重选。
|
||||
*
|
||||
* 数据源:api/module/flow.ts 的 applyPreview(后端 /api 代理端点属 F6 正式接入批次,
|
||||
* 当前组件与 API 方法均为预置,页面接线时后端同批补端点即可点亮)。
|
||||
* 渲染规则:只渲染开启指派的节点(preview.nodes 已过滤);countersign/selection.mode
|
||||
* 决定单选(SINGLE)/多选(MULTI);required 必填标 *;min/max 数量约束在 validate() 收口。
|
||||
*/
|
||||
import { ref, watch } from "vue";
|
||||
import { Message } from "view-ui-plus";
|
||||
import { flowApi } from "@/api/module/flow";
|
||||
|
||||
const props = defineProps<{ flowGuid: string }>();
|
||||
|
||||
interface PreviewUser { userId: number; userName: string }
|
||||
interface PreviewSelection { mode: string; min?: number; max?: number; required?: boolean }
|
||||
interface PreviewNode {
|
||||
nodeId: string;
|
||||
nodeName?: string;
|
||||
countersign?: string;
|
||||
selection?: PreviewSelection;
|
||||
candidates: PreviewUser[];
|
||||
}
|
||||
|
||||
const loading = ref(false);
|
||||
const nodes = ref<PreviewNode[]>([]);
|
||||
/** 已选:nodeId → userIds(单选也用数组,collect 统一输出) */
|
||||
const selected = ref<Record<string, number[]>>({});
|
||||
|
||||
async function reload() {
|
||||
if (!props.flowGuid) {
|
||||
nodes.value = [];
|
||||
selected.value = {};
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
const prev = selected.value;
|
||||
try {
|
||||
const res: any = await flowApi.applyPreview(props.flowGuid).send();
|
||||
nodes.value = res?.nodes || [];
|
||||
selected.value = {};
|
||||
if (Object.keys(prev).length > 0) {
|
||||
Message.info("流程定义已切换,指派选择已重置,请重新选择审批人");
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.flowGuid, reload, { immediate: true });
|
||||
|
||||
/** 输出标准 assigns:[{nodeId, userIds}](只含有选择的节点;无指派节点=空数组) */
|
||||
function collect(): { nodeId: string; userIds: number[] }[] {
|
||||
return nodes.value
|
||||
.filter((n) => (selected.value[n.nodeId] || []).length > 0)
|
||||
.map((n) => ({ nodeId: n.nodeId, userIds: [...selected.value[n.nodeId]] }));
|
||||
}
|
||||
|
||||
/** 校验:返回错误文案;空串=通过。必填与 min/max 数量约束在此收口 */
|
||||
function validate(): string {
|
||||
for (const n of nodes.value) {
|
||||
const picked = selected.value[n.nodeId] || [];
|
||||
const required = n.selection?.required;
|
||||
const min = n.selection?.min;
|
||||
const max = n.selection?.max;
|
||||
if (required && picked.length === 0) {
|
||||
return `「${n.nodeName || n.nodeId}」为必选节点,请选择审批人`;
|
||||
}
|
||||
if (min != null && picked.length > 0 && picked.length < min) {
|
||||
return `「${n.nodeName || n.nodeId}」至少选择 ${min} 人`;
|
||||
}
|
||||
if (max != null && picked.length > max) {
|
||||
return `「${n.nodeName || n.nodeId}」最多选择 ${max} 人`;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
defineExpose({ collect, validate, reload });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flow-assign-selector">
|
||||
<p v-if="loading" style="color: #808695">正在加载指派节点…</p>
|
||||
<p v-else-if="nodes.length === 0" style="color: #808695">
|
||||
该流程未开启发起时指派,无需选择审批人
|
||||
</p>
|
||||
<div v-for="node in nodes" :key="node.nodeId" class="assign-node">
|
||||
<label class="assign-label">
|
||||
{{ node.nodeName || node.nodeId }}
|
||||
<span v-if="node.selection?.required" style="color: #ed4014">*</span>
|
||||
<span style="color: #808695; font-weight: normal">
|
||||
({{ node.countersign === "ANY" ? "或签" : "会签" }}{{ node.selection?.mode === "SINGLE" ? "·单选" : "·多选" }})
|
||||
</span>
|
||||
</label>
|
||||
<Select v-if="node.selection?.mode === 'SINGLE'" :model-value="selected[node.nodeId]?.[0]"
|
||||
clearable placeholder="选择审批人" style="width: 260px"
|
||||
@on-change="(v: number) => (selected[node.nodeId] = v == null ? [] : [v])">
|
||||
<Option v-for="c in node.candidates" :key="c.userId" :value="c.userId" :label="c.userName" />
|
||||
</Select>
|
||||
<CheckboxGroup v-else :model-value="selected[node.nodeId]"
|
||||
@on-change="(v: number[]) => (selected[node.nodeId] = v || [])">
|
||||
<Checkbox v-for="c in node.candidates" :key="c.userId" :label="c.userId">
|
||||
{{ c.userName }}
|
||||
</Checkbox>
|
||||
</CheckboxGroup>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.assign-node { margin-bottom: 12px; }
|
||||
.assign-label { display: block; margin-bottom: 4px; font-weight: 600; }
|
||||
</style>
|
||||
@@ -410,8 +410,9 @@ const healthColumns = [
|
||||
align: "center",
|
||||
render: (h: any, p: any) =>
|
||||
h(Button, {
|
||||
props: { type: "text", size: "small" },
|
||||
on: { click: () => onViewLogs(p.row) },
|
||||
type: "text",
|
||||
size: "small",
|
||||
onClick: () => onViewLogs(p.row),
|
||||
}, () => "查推送日志"),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// 办件记录(管理端):全校办件查询;CONDITION 判断失败挂起的办件在此"重跑路由"恢复
|
||||
import { ref } from "vue";
|
||||
import { Message, Tag } from "view-ui-plus";
|
||||
import MyConfirm from "@/components/MyConfirm";
|
||||
import { OperatorEnum } from "@/types";
|
||||
import TablePageAlovaNew from "@/components/TablePageAlovaNew.vue";
|
||||
import InstanceDrawer from "@/views/flow/components/InstanceDrawer.vue";
|
||||
@@ -10,7 +9,8 @@ import { flowApi, INSTANCE_STATUS, INSTANCE_STATUS_COLOR } from "@/api/module/fl
|
||||
import { actionColumn } from "@/utils/tableActions";
|
||||
|
||||
const tableRef = ref<InstanceType<typeof TablePageAlovaNew> | null>(null);
|
||||
const dataParams = ref<{ status?: number; keyword?: string }>({ status: undefined, keyword: "" });
|
||||
// status 下拉含伪选项 'hung'(路由挂起):表内是 string,出站转 hung=true(不再传 status)
|
||||
const dataParams = ref<{ status?: number | string; keyword?: string }>({ status: undefined, keyword: "" });
|
||||
const drawerVisible = ref(false);
|
||||
const drawerInstanceId = ref<string | number>("");
|
||||
|
||||
@@ -18,8 +18,11 @@ const columns = [
|
||||
{ title: "序号", type: "index", width: 70, align: "center" },
|
||||
{ title: "办件标题", key: "bizTitle", minWidth: 220 },
|
||||
{ title: "业务", width: 170, render: (h: any, p: any) => h("span", `${p.row.bizType || "-"}/${p.row.bizId || "-"}`) },
|
||||
{ title: "状态", width: 100, align: "center", render: (h: any, p: any) =>
|
||||
h(Tag, { color: INSTANCE_STATUS_COLOR[p.row.status] || "gray" }, () => INSTANCE_STATUS[p.row.status] || p.row.status) },
|
||||
{ title: "状态", width: 120, align: "center", render: (h: any, p: any) => h("span", [
|
||||
h(Tag, { color: INSTANCE_STATUS_COLOR[p.row.status] || "gray" }, () => INSTANCE_STATUS[p.row.status] || p.row.status),
|
||||
// F11-4 路由挂起红标:接口回传 routeHung(最新 attempt 路由判断仍无出口)
|
||||
p.row.routeHung ? h(Tag, { color: "red" }, () => "路由挂起") : null,
|
||||
]) },
|
||||
{ title: "提交人", key: "submitterName", width: 100 },
|
||||
{ title: "提交时间", width: 160, render: (h: any, p: any) => h("span", (p.row.submitTime || "").replace("T", " ")) },
|
||||
actionColumn(
|
||||
@@ -37,24 +40,36 @@ function openDetail(instanceId: string | number) {
|
||||
drawerVisible.value = true;
|
||||
}
|
||||
|
||||
/** 重跑路由(2026-09-24 §4.4):重新调判断服务,成功即按出口续走;仍失败回业务错(办件继续挂起) */
|
||||
/** 重跑路由(2026-09-24 §4.4 + F5-4 逃生门):确认框带「按当前已发布定义重跑」复选——
|
||||
* 僵尸办件(挂起时的表达式已修好并发布为新版)勾选后按新版续走,否则按落库快照重跑 */
|
||||
const rerouteVisible = ref(false);
|
||||
const rerouteUseLatest = ref(false);
|
||||
const rerouteRow = ref<any>(null);
|
||||
|
||||
function onReroute(row: any) {
|
||||
MyConfirm({
|
||||
title: "重跑路由",
|
||||
msg: `确定对办件「${row.bizTitle || row.id}」重跑条件路由判断吗?`,
|
||||
info: "仅对挂在条件分支节点的挂起办件生效(未挂起的会提示无需重跑)",
|
||||
async onOk() {
|
||||
await flowApi.instanceReroute(row.id).send();
|
||||
Message.success("路由已重跑,流程按新判断结果继续");
|
||||
tableRef.value?.reloadData(OperatorEnum.edit);
|
||||
},
|
||||
});
|
||||
rerouteRow.value = row;
|
||||
rerouteUseLatest.value = false;
|
||||
rerouteVisible.value = true;
|
||||
}
|
||||
|
||||
async function confirmReroute() {
|
||||
const row = rerouteRow.value;
|
||||
if (!row) return;
|
||||
await flowApi.instanceReroute(row.id, rerouteUseLatest.value).send();
|
||||
rerouteVisible.value = false;
|
||||
Message.success("路由已重跑,流程按判断结果继续");
|
||||
tableRef.value?.reloadData(OperatorEnum.edit);
|
||||
}
|
||||
|
||||
// dataApi 返回 Method(alova usePagination 负责发送与解包),不能调 .send()
|
||||
const tableApi = (p: { pageIndex: number; pageSize: number }) => {
|
||||
const params: any = { page: p.pageIndex, size: p.pageSize };
|
||||
if (dataParams.value.status) params.status = dataParams.value.status;
|
||||
if (dataParams.value.status === "hung") {
|
||||
// F11-4 路由挂起伪选项:只传 hung,不带 status(后端 hung 过滤已内含审批中)
|
||||
params.hung = true;
|
||||
} else if (dataParams.value.status) {
|
||||
params.status = dataParams.value.status;
|
||||
}
|
||||
if (dataParams.value.keyword) params.keyword = dataParams.value.keyword.trim();
|
||||
return flowApi.adminInstances(params);
|
||||
};
|
||||
@@ -73,6 +88,7 @@ function onSearch() {
|
||||
<Option :value="20" label="已完成" />
|
||||
<Option :value="30" label="已拒绝" />
|
||||
<Option :value="40" label="已撤回" />
|
||||
<Option value="hung" label="路由挂起" />
|
||||
</Select>
|
||||
<Input class="fr" v-model="dataParams.keyword" placeholder="标题关键字" search suffix="ios-search"
|
||||
style="width: 200px" @on-search="onSearch" @on-clear="onSearch" />
|
||||
@@ -80,5 +96,10 @@ function onSearch() {
|
||||
<TablePageAlovaNew ref="tableRef" :dataApi="tableApi" :columns="columns" />
|
||||
</div>
|
||||
<InstanceDrawer v-model="drawerVisible" :instance-id="drawerInstanceId" />
|
||||
<Modal v-model="rerouteVisible" title="重跑路由" :mask-closable="false" @on-ok="confirmReroute">
|
||||
<p style="margin: 0 0 8px">确定对办件「{{ rerouteRow?.bizTitle || rerouteRow?.id }}」重跑条件路由判断吗?</p>
|
||||
<p style="margin: 0 0 12px; color: #808695">仅对挂在条件分支节点的挂起办件生效(未挂起的会提示无需重跑)</p>
|
||||
<Checkbox v-model="rerouteUseLatest">按当前已发布流程定义重跑(救僵尸办件)</Checkbox>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// 零新增表、零新增写接口。配置生成器对齐《子应用接入流程中心规范.md》§7.2/§7.5。
|
||||
// 方案文档:docs/流程中心接入向导与配置生成器-实施方案-20261015.md
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { Message, Tag } from "view-ui-plus";
|
||||
import { Message, Tag, Button } from "view-ui-plus";
|
||||
import { flowApi } from "@/api/module/flow";
|
||||
import Utils from "@/utils";
|
||||
import { extractExprVars, EXPR_BUILTIN_VARS } from "@/views/flow/admin/def-editor/model";
|
||||
@@ -589,7 +589,7 @@ async function goto(n: number) {
|
||||
{ title: '流程', key: 'name', minWidth: 180 },
|
||||
{ title: 'flowGuid', key: 'flowGuid', minWidth: 200 },
|
||||
{ title: '状态', width: 90, align: 'center', render: (h: any, p: any) => h(Tag, { color: p.row.status === 1 ? 'green' : p.row.status === 0 ? 'orange' : 'gray' }, () => DEF_STATUS[p.row.status] || p.row.status) },
|
||||
{ title: '操作', width: 90, align: 'center', render: (h: any, p: any) => h(Button, { props: { type: 'text', size: 'small' }, on: { click: () => inspectDef(p.row.id) } }, () => '查看条件') },
|
||||
{ title: '操作', width: 90, align: 'center', render: (h: any, p: any) => h(Button, { type: 'text', size: 'small', onClick: () => inspectDef(p.row.id) }, () => '查看条件') },
|
||||
]" :data="defs" :loading="defLoading" size="small" max-height="220"
|
||||
no-data-text="该应用无流程定义(先回第①步确认应用,或去流程定义页新建)" />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user