build / build (push) Successful in 1m31s
在 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)与依赖目录
94 lines
4.5 KiB
Vue
94 lines
4.5 KiB
Vue
<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>
|