264 lines
8.7 KiB
Vue
264 lines
8.7 KiB
Vue
<script setup lang="ts">
|
||
// 审批节点配置抽屉(方案 §3.4):打开时把节点值拷进本地表单,确定时写回——
|
||
// 分段控件切换只改本地状态,不会因重渲染丢失未保存输入(原型阶段踩过该坑)。
|
||
// 指定用户改为树形选人弹窗(参照 Notice 自定义组选人),id+name 同帧拿到,不再手输 ID。
|
||
import { computed, inject, reactive, ref, watch } from "vue";
|
||
import { Message } from "view-ui-plus";
|
||
import UserCheckModal from "@/components/UserCheckModal.vue";
|
||
import { flowApi } from "@/api/module/flow";
|
||
import { ApprovalNode, EDITOR_CTX } from "../model";
|
||
|
||
const props = defineProps<{
|
||
modelValue: boolean;
|
||
node: ApprovalNode | null;
|
||
}>();
|
||
|
||
const emit = defineEmits<{
|
||
"update:modelValue": [boolean];
|
||
/** 节点已删除(由抽屉内删除按钮触发),由外层关闭抽屉并重算校验 */
|
||
removed: [];
|
||
}>();
|
||
|
||
const ctx = inject(EDITOR_CTX)!;
|
||
|
||
const form = reactive({
|
||
name: "",
|
||
type: "DEPT_KEY" as "DEPT_KEY" | "USERS",
|
||
deptKey: "",
|
||
userIds: [] as number[],
|
||
countersign: "ALL" as "ALL" | "ANY",
|
||
deadlineHours: null as number | null,
|
||
});
|
||
|
||
const userCheckModal = ref<any>();
|
||
const userModalSources = [
|
||
{
|
||
name: "基础平台",
|
||
loader: () => flowApi.adminUserTree().send() as Promise<any[]>,
|
||
},
|
||
];
|
||
|
||
function syncFromNode(): void {
|
||
const n = props.node;
|
||
if (!n) return;
|
||
form.name = n.name;
|
||
form.type = n.assignee?.type === "USERS" ? "USERS" : "DEPT_KEY";
|
||
form.deptKey = n.assignee?.type === "DEPT_KEY" ? n.assignee.deptKey : "";
|
||
form.userIds = n.assignee?.type === "USERS" ? [...n.assignee.userIds] : [];
|
||
form.countersign = n.countersign;
|
||
form.deadlineHours = n.deadlineHours;
|
||
}
|
||
|
||
watch(
|
||
() => [props.modelValue, props.node?.uid],
|
||
() => {
|
||
if (props.modelValue) syncFromNode();
|
||
},
|
||
{ immediate: true }
|
||
);
|
||
|
||
/** 科室下拉项:mapList 聚合 + 当前值(可能已不在映射表中)兜底,避免选项丢失 */
|
||
const deptOptions = computed(() => {
|
||
const list = ctx.depts.map((d) => ({
|
||
value: d.deptKey,
|
||
label: `${d.deptName || d.deptKey}(${d.deptKey})· ${d.count} 名办理人`,
|
||
}));
|
||
if (form.deptKey && !list.some((o) => o.value === form.deptKey)) {
|
||
list.unshift({ value: form.deptKey, label: `${form.deptKey}(未配置映射)` });
|
||
}
|
||
return list;
|
||
});
|
||
|
||
/** 当前科室的映射状态提示(与 validate.ts 同口径:仅提示,不阻断存草稿) */
|
||
const deptHint = computed(() => {
|
||
if (form.type !== "DEPT_KEY" || !form.deptKey) return "";
|
||
const d = ctx.depts.find((x) => x.deptKey === form.deptKey);
|
||
if (!d) return `科室映射「${form.deptKey}」不存在,发布会被拦截`;
|
||
if (!d.count) return `科室「${d.deptName || d.deptKey}」无启用办理人,发布会被拦截`;
|
||
return "";
|
||
});
|
||
|
||
function userNameOf(id: number): string {
|
||
return ctx.userNames[id] || `#${id}`;
|
||
}
|
||
|
||
/** 选人弹窗提交:选中后的 id+name 合入本地 userIds 与姓名快照(不再逐个 byId 检索) */
|
||
function onUsersPicked(arr: any[]): void {
|
||
let added = 0;
|
||
for (const u of arr || []) {
|
||
if (u?.id == null) continue;
|
||
const id = Number(u.id);
|
||
if (!form.userIds.includes(id)) {
|
||
form.userIds.push(id);
|
||
added++;
|
||
}
|
||
if (u.name) ctx.userNames[id] = u.name;
|
||
}
|
||
Message.success(`已选择 ${added} 名用户`);
|
||
}
|
||
|
||
function removeUser(id: number): void {
|
||
const i = form.userIds.indexOf(id);
|
||
if (i >= 0) form.userIds.splice(i, 1);
|
||
}
|
||
|
||
function onOk(): void {
|
||
const n = props.node;
|
||
if (!n) {
|
||
emit("update:modelValue", false);
|
||
return;
|
||
}
|
||
if (!form.name.trim()) {
|
||
Message.warning("节点名称不能为空");
|
||
return;
|
||
}
|
||
n.name = form.name.trim();
|
||
n.assignee =
|
||
form.type === "USERS"
|
||
? { type: "USERS", userIds: [...form.userIds] }
|
||
: { type: "DEPT_KEY", deptKey: form.deptKey };
|
||
n.countersign = form.countersign;
|
||
n.deadlineHours = form.deadlineHours && form.deadlineHours > 0 ? form.deadlineHours : null;
|
||
emit("update:modelValue", false);
|
||
}
|
||
|
||
function onRemove(): void {
|
||
if (props.node) ctx.removeNode(props.node.uid);
|
||
emit("removed");
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<Drawer
|
||
:model-value="modelValue"
|
||
title="配置审批节点"
|
||
:width="430"
|
||
:mask-closable="false"
|
||
@update:model-value="emit('update:modelValue', $event)"
|
||
>
|
||
<Form :label-width="0" class="dw-form">
|
||
<div class="fld">
|
||
<label>节点名称</label>
|
||
<Input v-model="form.name" placeholder="如:科室审核" :maxlength="30" />
|
||
</div>
|
||
|
||
<div class="fld">
|
||
<label>审批人类型</label>
|
||
<RadioGroup v-model="form.type" type="button" button-style="solid">
|
||
<Radio label="DEPT_KEY">按科室映射</Radio>
|
||
<Radio label="USERS">指定用户</Radio>
|
||
</RadioGroup>
|
||
</div>
|
||
|
||
<div v-if="form.type === 'DEPT_KEY'" class="fld">
|
||
<label>科室映射</label>
|
||
<Select v-model="form.deptKey" filterable placeholder="请选择科室">
|
||
<Option v-for="o in deptOptions" :key="o.value" :value="o.value" :label="o.label" />
|
||
</Select>
|
||
<div v-if="deptHint" class="hint warn">⚠ {{ deptHint }}</div>
|
||
<div v-else class="hint">科室 → 办理人的映射在「办理人映射」页维护;发布时校验至少 1 名启用办理人</div>
|
||
</div>
|
||
|
||
<template v-else>
|
||
<div class="fld">
|
||
<label>指定用户</label>
|
||
<div class="user-add">
|
||
<Button type="primary" @click="userCheckModal?.show()">选择用户</Button>
|
||
</div>
|
||
<div class="user-tags">
|
||
<Tag
|
||
v-for="id in form.userIds"
|
||
:key="id"
|
||
closable
|
||
color="blue"
|
||
@on-close="removeUser(id)"
|
||
>
|
||
{{ userNameOf(id) }}
|
||
</Tag>
|
||
<span v-if="!form.userIds.length" class="hint">尚未添加用户</span>
|
||
</div>
|
||
<div class="hint">会签时每名用户各自产生一条待办任务</div>
|
||
</div>
|
||
</template>
|
||
|
||
<div class="fld">
|
||
<label>多人审批方式</label>
|
||
<RadioGroup v-model="form.countersign" type="button" button-style="solid">
|
||
<Radio label="ALL">会签(全部同意)</Radio>
|
||
<Radio label="ANY">或签(一人同意)</Radio>
|
||
</RadioGroup>
|
||
<div class="hint">
|
||
会签:每名办理人都收到任务,全部同意才通过;或签:任一人办理即通过,其余待办自动失效
|
||
</div>
|
||
</div>
|
||
|
||
<div class="fld">
|
||
<label>办理时限(小时)</label>
|
||
<InputNumber v-model="form.deadlineHours" :min="1" placeholder="可空" style="width: 100%" />
|
||
<div class="hint">超过时限后进入催办提醒(取到达时间 + N 小时)</div>
|
||
</div>
|
||
</Form>
|
||
|
||
<div class="dw-foot">
|
||
<Poptip confirm title="删除该审批节点?" ok-text="删除" cancel-text="取消" transfer @on-ok="onRemove">
|
||
<Button type="error" ghost>删除节点</Button>
|
||
</Poptip>
|
||
<span style="flex: 1" />
|
||
<Button @click="emit('update:modelValue', false)">取消</Button>
|
||
<Button type="primary" @click="onOk">确定</Button>
|
||
</div>
|
||
</Drawer>
|
||
|
||
<UserCheckModal
|
||
ref="userCheckModal"
|
||
:sources="userModalSources"
|
||
:userids="form.userIds"
|
||
@on-submit="onUsersPicked"
|
||
/>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.dw-form {
|
||
padding-bottom: 8px;
|
||
}
|
||
.fld {
|
||
margin-bottom: 18px;
|
||
}
|
||
.fld > label {
|
||
display: block;
|
||
margin-bottom: 6px;
|
||
font-weight: 600;
|
||
font-size: 13px;
|
||
}
|
||
.hint {
|
||
color: #808695;
|
||
font-size: 12px;
|
||
line-height: 1.6;
|
||
margin-top: 5px;
|
||
}
|
||
.hint.warn {
|
||
color: #ff9900;
|
||
}
|
||
.user-add {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
.user-tags {
|
||
margin-top: 8px;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 4px 0;
|
||
}
|
||
.dw-foot {
|
||
position: sticky;
|
||
bottom: -16px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 12px 0;
|
||
margin-top: 8px;
|
||
background: #fff;
|
||
border-top: 1px solid #f0f0f0;
|
||
}
|
||
</style>
|