Files
flow-center-front/src/views/flow/admin/def-editor/components/AssigneePreviewModal.vue
T
chenxing bed57846f4
build / build (push) Successful in 1m31s
feat(flow): 落地 FL2.0 原型——部门选人抽屉 / 授权管理页 / 退回重提入口(基线 origin/main)
在 c93313c 之上增量落地原型与前端规范,保留既有流程图入口/失败提示/指派预览/线级停发:

- 审批/抄送抽屉按原型重构(422→1026 / 239→742 行):科室树+成员多选、SYSTEM/CUSTOM 分支、
  CUSTOM 分类导航自动定位、跨节点导航重置、保存后重开回显
- 新增授权管理页 /admin/usage-grants(UsageGrants.vue,417 行):校域清单、按流程线筛选、
  授予/撤销后回读,失败不宣称成功
- 路由与 API:新增 usage-grant(schoolList/summary/grant/revoke)、department(groups/options/resolve/members)、
  instance/resubmit;TaskHandle 增加「退回待修改」入口(仅 status=50 显示)
- 错误处理沿用 flowError 统一出口,不透出后端原文
- 组件/契约测试:flow-error、flow2-contract、department-roundtrip、task-result

不含构建产物(dist-verify)与依赖目录
2026-10-30 10:15:47 +08:00

94 lines
4.5 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">
/**
* 审批节点候选人预览弹窗(编辑器内入口,验收报告 P2-1)。
* 只读展示 resolveNodePreview 的结果:草稿解析 + 已发布版本对账 + 明确的不可预览原因。
* 关闭不做任何写回——它只接收抽屉里的本地表单快照。
*/
import { ref } from "vue";
import { Modal, Alert, Tag, Button } from "view-ui-plus";
import { resolveNodePreview, type PreviewApi, type PreviewDept, type PreviewNodeConfig } from "../assignPreview";
import { flowApi } from "@/api/module/flow";
const api: PreviewApi = {
deptMembers: (deptId) => flowApi.deptMembers(deptId).send() as Promise<any[]>,
deptDirectoryMembers: (type, departmentId, scope) =>
flowApi.deptDirectoryMembers(type, departmentId, scope as "select" | "history" | undefined).send() as Promise<any[]>,
lookupUser: (userId) => flowApi.mapLookupUser(userId).send() as Promise<any>,
applyPreview: (flowGuid) => flowApi.applyPreview(flowGuid).send() as Promise<any>,
};
const visible = ref(false);
const loading = ref(false);
const title = ref("预览候选人");
const result = ref<Awaited<ReturnType<typeof resolveNodePreview>> | null>(null);
async function show(cfg: PreviewNodeConfig, depts: PreviewDept[], flowGuid = ""): Promise<void> {
visible.value = true;
loading.value = true;
result.value = null;
title.value = `预览候选人 · ${cfg.name || "未命名节点"}`;
try {
result.value = await resolveNodePreview(cfg, depts, api, flowGuid);
} finally {
loading.value = false;
}
}
defineExpose({ show });
</script>
<template>
<Modal v-model="visible" :title="title" :width="620" :footer-hide="true">
<div v-if="loading" class="muted" style="padding: 24px 0; text-align: center">正在解析候选人…</div>
<template v-else-if="result">
<Alert v-if="result.blocked" type="warning" show-icon class="pv-block">{{ result.blocked }}</Alert>
<div class="pv-headline">
<b>{{ result.headline }}</b>
<span class="pv-source">数据源:{{ result.rows.length ? "FL 科室映射 / UserBase 既有接口" : "未取到候选" }}</span>
</div>
<div v-if="result.rows.length" class="pv-rows">
<div v-for="r in result.rows" :key="r.userId" class="pv-row">
<Tag :color="r.warn ? 'warning' : 'blue'">{{ r.userName }}</Tag>
<span class="pv-id">#{{ r.userId }}</span>
<span v-if="r.warn" class="pv-warn">{{ r.warn }}</span>
</div>
</div>
<div v-else class="muted">(本节点当前没有可展示的候选人)</div>
<div class="pv-pub" v-if="result.published">
<div class="pv-pub-title">已发布版本对账</div>
<div class="pv-pub-body">
<Tag :color="result.published.state === 'ok' ? 'success' : result.published.state === 'missing' ? 'default' : 'warning'">
{{ result.published.state === "ok" ? "可比对" : result.published.state === "missing" ? "草稿节点未发布" : "不可比对" }}
</Tag>
<span>{{ result.published.text }}</span>
</div>
<div v-if="result.published.state === 'ok' && result.published.rows.length" class="pv-rows">
<Tag v-for="p in result.published.rows" :key="p.userId" color="green">{{ p.userName }}</Tag>
</div>
</div>
<ul class="pv-notes">
<li v-for="(n, i) in result.notes" :key="i">{{ n }}</li>
</ul>
</template>
<div style="margin-top: 12px; text-align: right">
<Button @click="visible = false">关闭</Button>
</div>
</Modal>
</template>
<style scoped>
.pv-block { margin-bottom: 10px; }
.pv-headline { display: flex; align-items: baseline; gap: 10px; margin-bottom: 10px; }
.pv-source { color: #808695; font-size: 12px; }
.pv-rows { display: flex; flex-wrap: wrap; gap: 6px 0; margin-bottom: 12px; }
.pv-row { display: flex; align-items: center; gap: 6px; width: 100%; }
.pv-id { color: #c5c8ce; font-size: 12px; }
.pv-warn { color: #ff9900; font-size: 12px; }
.pv-pub { border-top: 1px dashed #e8eaec; padding-top: 10px; margin-bottom: 10px; }
.pv-pub-title { font-weight: 600; font-size: 13px; margin-bottom: 6px; }
.pv-pub-body { display: flex; align-items: flex-start; gap: 8px; font-size: 13px; }
.pv-notes { margin: 0; padding-left: 18px; color: #808695; font-size: 12px; line-height: 1.7; }
</style>