Files
flow-center-front/src/components/FlowAssignSelector.vue
T

122 lines
5.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>