Files
flow-center-front/src/views/flow/admin/def-editor/components/ApprovalDrawer.vue
T

264 lines
8.7 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">
// 审批节点配置抽屉(方案 §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>