feat(detail): 列表详情改为抽屉弹出+业务信息只认 summaryFields
build / build (push) Successful in 3m11s

- 新增 InstanceDrawer.vue(Drawer 740px + InstanceBody full 口径,非 iframe)
- Workbench/MyInstances/admin.Instances 详情按钮改为打开抽屉
- InstanceBody 移除 snapshotRows 回落,业务信息只显示子应用传递的
  summaryFields(中文 label),无值整块隐藏不再平铺英文 key
This commit is contained in:
chenxing
2026-10-11 12:01:43 +08:00
parent ecd4b9f4ea
commit 8506ad80d7
5 changed files with 137 additions and 31 deletions
+10 -3
View File
@@ -1,17 +1,18 @@
<script setup lang="ts">
// 我的办件(提交人视角):列表 + 撤回/催办/详情
import { ref } from "vue";
import { useRouter } from "vue-router";
import { Message, Tag } from "view-ui-plus";
import MyConfirm from "@/components/MyConfirm";
import TablePageAlovaNew from "@/components/TablePageAlovaNew.vue";
import InstanceDrawer from "@/views/flow/components/InstanceDrawer.vue";
import { OperatorEnum } from "@/types";
import { flowApi, INSTANCE_STATUS, INSTANCE_STATUS_COLOR } from "@/api/module/flow";
import { actionColumn } from "@/utils/tableActions";
const router = useRouter();
const tableRef = ref<InstanceType<typeof TablePageAlovaNew> | null>(null);
const dataParams = ref<{ status?: number }>({ status: undefined });
const drawerVisible = ref(false);
const drawerInstanceId = ref<string | number>("");
const columns = [
{ title: "序号", type: "index", width: 70, align: "center" },
@@ -24,7 +25,7 @@ const columns = [
{ title: "办结时间", width: 170, render: (h: any, p: any) => h("span", (p.row.finishTime || "").replace("T", " ") || "-") },
actionColumn(
[
{ key: "detail", label: "详情", icon: "icon-browse", on: (r: any) => router.push(`/instance/${r.instanceId}?pid=101`) },
{ key: "detail", label: "详情", icon: "icon-browse", on: (r: any) => openDetail(r.instanceId) },
{ key: "withdraw", label: "撤回", icon: "icon-yaohao", on: (r: any) => onWithdraw(r), disabledWhen: (r: any) => r.status !== 10, danger: true },
{ key: "urge", label: "催办", icon: "icon-fasong", on: (r: any) => onUrge(r), disabledWhen: (r: any) => r.status !== 10 },
],
@@ -40,6 +41,11 @@ function onSearch() {
tableRef.value?.refreshData();
}
function openDetail(instanceId: string | number) {
drawerInstanceId.value = instanceId;
drawerVisible.value = true;
}
async function onWithdraw(row: any) {
await MyConfirm({
title: "撤回",
@@ -80,5 +86,6 @@ async function onUrge(row: any) {
</div>
<TablePageAlovaNew ref="tableRef" :dataApi="tableApi" :columns="columns" />
</div>
<InstanceDrawer v-model="drawerVisible" :instance-id="drawerInstanceId" />
</div>
</template>
+11 -2
View File
@@ -5,12 +5,15 @@ import { ref } from "vue";
import { useRouter } from "vue-router";
import { Tag } from "view-ui-plus";
import TablePageAlovaNew from "@/components/TablePageAlovaNew.vue";
import InstanceDrawer from "@/views/flow/components/InstanceDrawer.vue";
import { OperatorEnum } from "@/types";
import { flowApi, TASK_STATUS, TASK_STATUS_COLOR, INSTANCE_STATUS } from "@/api/module/flow";
import { actionColumn } from "@/utils/tableActions";
const router = useRouter();
const tab = ref<"todo" | "done" | "cc">("todo");
const drawerVisible = ref(false);
const drawerInstanceId = ref<string | number>("");
const todoTableRef = ref<InstanceType<typeof TablePageAlovaNew> | null>(null);
const doneTableRef = ref<InstanceType<typeof TablePageAlovaNew> | null>(null);
const ccTableRef = ref<InstanceType<typeof TablePageAlovaNew> | null>(null);
@@ -43,7 +46,7 @@ const doneColumns = [
{ title: "意见", key: "opinion", minWidth: 120 },
{ title: "办理时间", width: 170, render: (h: any, p: any) => h("span", (p.row.handleTime || "").replace("T", " ")) },
{ title: "办件状态", width: 100, render: (h: any, p: any) => h("span", INSTANCE_STATUS[p.row.instanceStatus] || p.row.instanceStatus) },
actionColumn([{ key: "detail", label: "详情", icon: "icon-browse", on: (r: any) => router.push(`/instance/${r.instanceId}?pid=100`) }], { mode: "text", width: 100, fixed: "right" }),
actionColumn([{ key: "detail", label: "详情", icon: "icon-browse", on: (r: any) => openDetail(r.instanceId) }], { mode: "text", width: 100, fixed: "right" }),
];
// 抄送行是 NOTIFY 知会(非任务):无 taskId/status,直接跳办件详情
@@ -53,7 +56,7 @@ const ccColumns = [
{ title: "抄送节点", key: "nodeName", width: 160 },
{ title: "抄送时间", width: 170, render: (h: any, p: any) => h("span", (p.row.sentTime || "").replace("T", " ")) },
{ title: "办件状态", width: 100, render: (h: any, p: any) => h("span", INSTANCE_STATUS[p.row.instanceStatus] || p.row.instanceStatus || "-") },
actionColumn([{ key: "detail", label: "详情", icon: "icon-browse", on: (r: any) => router.push(`/instance/${r.instanceId}?pid=100`) }], { mode: "text", width: 100, fixed: "right" }),
actionColumn([{ key: "detail", label: "详情", icon: "icon-browse", on: (r: any) => openDetail(r.instanceId) }], { mode: "text", width: 100, fixed: "right" }),
];
// dataApi 返回 Method(alova usePagination 负责发送与解包),不能调 .send()
@@ -65,6 +68,11 @@ function goHandle(row: any) {
router.push(`/task/${row.taskId}?pid=100`);
}
function openDetail(instanceId: string | number) {
drawerInstanceId.value = instanceId;
drawerVisible.value = true;
}
// 两个内容区 v-show 共存,已办/抄送首次切换才拉取(保持原惰性加载行为)
const doneLoaded = ref(false);
const ccLoaded = ref(false);
@@ -100,5 +108,6 @@ function switchTab(name: string) {
<TablePageAlovaNew ref="ccTableRef" :dataApi="ccApi" :columns="ccColumns" :autoLoad="false" />
</div>
</div>
<InstanceDrawer v-model="drawerVisible" :instance-id="drawerInstanceId" />
</div>
</template>
+10 -3
View File
@@ -1,17 +1,18 @@
<script setup lang="ts">
// 办件记录(管理端):全校办件查询;CONDITION 判断失败挂起的办件在此"重跑路由"恢复
import { ref } from "vue";
import { useRouter } from "vue-router";
import { Message, Tag } from "view-ui-plus";
import MyConfirm from "@/components/MyConfirm";
import { OperatorEnum } from "@/types";
import TablePageAlovaNew from "@/components/TablePageAlovaNew.vue";
import InstanceDrawer from "@/views/flow/components/InstanceDrawer.vue";
import { flowApi, INSTANCE_STATUS, INSTANCE_STATUS_COLOR } from "@/api/module/flow";
import { actionColumn } from "@/utils/tableActions";
const router = useRouter();
const tableRef = ref<InstanceType<typeof TablePageAlovaNew> | null>(null);
const dataParams = ref<{ status?: number; keyword?: string }>({ status: undefined, keyword: "" });
const drawerVisible = ref(false);
const drawerInstanceId = ref<string | number>("");
const columns = [
{ title: "序号", type: "index", width: 70, align: "center" },
@@ -23,7 +24,7 @@ const columns = [
{ title: "提交时间", width: 160, render: (h: any, p: any) => h("span", (p.row.submitTime || "").replace("T", " ")) },
actionColumn(
[
{ key: "detail", label: "详情", icon: "icon-browse", on: (r: any) => router.push(`/instance/${r.id}?pid=102`) },
{ key: "detail", label: "详情", icon: "icon-browse", on: (r: any) => openDetail(r.id) },
// 仅审批中的办件可挂条件分支节点;其余行置灰保位(重跑入口对非挂起办件是业务错提示)
{ key: "reroute", label: "重跑路由", icon: "icon-play", on: (r: any) => onReroute(r), disabledWhen: (r: any) => r.status !== 10 },
],
@@ -31,6 +32,11 @@ const columns = [
),
];
function openDetail(instanceId: string | number) {
drawerInstanceId.value = instanceId;
drawerVisible.value = true;
}
/** 重跑路由(2026-09-24 §4.4):重新调判断服务,成功即按出口续走;仍失败回业务错(办件继续挂起) */
function onReroute(row: any) {
MyConfirm({
@@ -73,5 +79,6 @@ function onSearch() {
</div>
<TablePageAlovaNew ref="tableRef" :dataApi="tableApi" :columns="columns" />
</div>
<InstanceDrawer v-model="drawerVisible" :instance-id="drawerInstanceId" />
</div>
</template>
+7 -23
View File
@@ -3,7 +3,7 @@
// 数据源同出 InstanceDetail(OpenEmbedController 复用管理面出参),展示层共用本组件,样式单点维护。
// - embed(缺省,关键信息):基本信息 / 办理进度三视图 / 流程示意图折叠 / 业务信息(只认 summaryFields)
// - full(管理面,全量):另有 流程信息五项(流程编码/流程版本/业务类型/业务编号/实例号)、
// 业务表单数据折叠、业务详情链接;业务信息无 summaryFields 时回落 bizSnapshot 全键平铺(旧数据兜底)
// 业务表单数据折叠、业务详情链接;业务信息只认子应用传递的 summaryFields,无则隐藏
// 口径:人员/科室切换 emit('mode-change') 由管理面写 query 持久化(嵌入页 URL 归子应用管,忽略即可)。
// 宽度:版式按嵌入场景调优(740 抽屉内容约 708px),full 下根类 max-width 封顶防宽屏拉散(见 .is-full)。
import { ref, computed, onMounted, nextTick } from "vue";
@@ -14,7 +14,7 @@ import { fitDiagram } from "@/utils/diagramImg";
const props = defineProps<{
/** InstanceDetail 形状对象(两页同源) */
detail: any;
/** true=管理面全量(流程信息/业务表单数据/业务详情/摘要回落);缺省 embed 关键信息 */
/** true=管理面全量(流程信息/业务表单数据/业务详情);缺省 embed 关键信息 */
full?: boolean;
/** 流程示意图地址(两页端点不同:token 图 / instanceId 图,由页面生成传入) */
diagramSrc: string;
@@ -45,7 +45,8 @@ function onDiagramToggle(e: Event) {
if ((e.target as HTMLDetailsElement).open) fitDiagram(diagramEl.value);
}
/** 业务信息唯一内容 = 约定键 bizSnapshot.summaryFields;形状错 / 无值 ⇒ [](full 再走回落) */
/** 业务信息唯一内容 = 约定键 bizSnapshot.summaryFields;由发起流程的子应用决定展示字段与中文 label。
* 形状错 / 无值 ⇒ [],不再 fallback 平铺 bizSnapshot 原始英文 key(避免详情页出现 itemTitle / deptName 等)。 */
const summaryFields = computed(() => {
const raw = props.detail?.bizSnapshot;
if (!raw) return [];
@@ -68,25 +69,8 @@ const summaryFields = computed(() => {
}));
});
/** full 专用回落:无 summaryFields 时按 bizSnapshot 原始键平铺(旧数据/未适配子应用兜底) */
const snapshotRows = computed(() => {
if (!props.full || summaryFields.value.length) return [];
const raw = props.detail?.bizSnapshot;
if (!raw) return [];
let obj: any = null;
try {
obj = typeof raw === "string" ? JSON.parse(raw) : raw;
} catch {
return [{ label: "摘要", value: String(raw) }];
}
if (obj && typeof obj === "object" && !Array.isArray(obj)) {
return Object.entries(obj).map(([k, v]) => ({ label: k, value: v == null ? "-" : String(v) }));
}
return [];
});
/** 业务信息最终渲染行:summaryFields 优先,full 无值回落 snapshotRows */
const bizRows = computed(() => (summaryFields.value.length ? summaryFields.value : snapshotRows.value));
/** 业务信息最终渲染行:只认子应用传递的 summaryFields */
const bizRows = computed(() => summaryFields.value);
/** full 专用:流程信息五项(专业字段只进管理面,嵌入页不外露) */
const flowInfoRows = computed(() => {
@@ -429,7 +413,7 @@ onMounted(async () => {
</div>
</details>
<!-- 业务信息:summaryFields 优先;full 无值回落 bizSnapshot 平铺(嵌入页无值整块不显示) -->
<!-- 业务信息:只认子应用传递的 summaryFields,无值整块隐藏 -->
<details v-if="bizRows.length" class="embed-fold">
<summary>业务信息</summary>
<div class="embed-fold-b">
@@ -0,0 +1,99 @@
<script setup lang="ts">
// 办件详情抽屉(管理面列表页复用):非 iframe,内部直接复用 InstanceBody(full 口径)。
// 宽度与 Notice 审批记录抽屉对齐 740px;关闭后清空状态,下次打开重新加载。
import { ref, computed, watch } from "vue";
import { Drawer, Tag } from "view-ui-plus";
import { flowApi, INSTANCE_STATUS, INSTANCE_STATUS_COLOR } from "@/api/module/flow";
import InstanceBody from "@/views/flow/components/InstanceBody.vue";
import config from "@/assets/config/host.config";
const props = defineProps<{
modelValue: boolean;
instanceId?: string | number;
}>();
const emit = defineEmits<{ (e: "update:modelValue", val: boolean): void }>();
const visible = computed({
get: () => props.modelValue,
set: (val) => emit("update:modelValue", val),
});
const loading = ref(false);
const detail = ref<any>(null);
const mode = ref("user");
async function load() {
const id = props.instanceId;
if (!id || !visible.value) return;
loading.value = true;
try {
detail.value = await flowApi.detail(String(id)).send();
} finally {
loading.value = false;
}
}
watch(() => [props.instanceId, visible.value], load, { immediate: true });
watch(visible, (val) => {
if (!val) detail.value = null;
});
function switchMode(val: string) {
mode.value = val;
}
const diagramSrc = computed(() => {
if (!detail.value?.instanceId) return "";
return `${config.serviceHost}/api/flow/diagram.png?instanceId=${detail.value.instanceId}&mode=${mode.value}`;
});
function openDiagram() {
if (!detail.value?.instanceId) return;
const url = `${window.location.origin}${import.meta.env.BASE_URL}#/diagram?instanceId=${detail.value.instanceId}&mode=${mode.value}`;
window.open(url, "_blank");
}
function statusName(s: number) {
return INSTANCE_STATUS[s] || s;
}
function statusColor(s: number) {
return INSTANCE_STATUS_COLOR[s] || "gray";
}
</script>
<template>
<Drawer
v-model="visible"
:title="detail?.bizTitle || '办件详情'"
:width="740"
:transfer="false"
:styles="{ body: { padding: '0' } }"
>
<div v-if="loading" class="drawer-loading muted">加载中…</div>
<template v-else-if="detail">
<div class="drawer-head">
<Tag :color="statusColor(detail.status)">{{ statusName(detail.status) }}</Tag>
</div>
<div class="drawer-body">
<InstanceBody
full
:detail="detail"
:diagram-src="diagramSrc"
:initial-mode="mode"
@mode-change="switchMode"
>
<template #diagram-link>
<a class="fold-link" @click.stop.prevent="openDiagram">查看大图</a>
</template>
</InstanceBody>
</div>
</template>
</Drawer>
</template>
<style scoped>
.drawer-head { padding: 12px 16px 8px; }
.drawer-body { padding: 0 16px 16px; }
.drawer-loading { padding: 48px 16px; text-align: center; }
.fold-link { font-size: 12px; font-weight: normal; }
</style>