Files
svc-hall-hall/src/components/flow-approval/FlowRecordDrawer.vue
T
chenxing 351a513547
build / build (push) Successful in 2m12s
feat(p3.3): 应用内审批前端——flow-approval 组件族本地化 + /todo 我的审批页 + 详情审批卡片
- 从 Notice/front 复制 flow-approval 六件并本地修复(漂移登记组件 README,Notice 不回改):
  · P0-04:四个写调用点(List 通过/驳回 + Card 通过/驳回)全部实际携带本行 bizId,
    选中 bizId 随弹窗生命周期锁定/清理
  · P0-05:删 useElementHeight/yh-* 平台布局依赖,本地 fa-* 样式(theme-base.css --sh-* 令牌)
  · P0-06:types.ts 本地契约——增 my(行=ApplyVo 子集/FlowPage 完整元数据/状态 0=待提交
    禁止当全部,全部用 -1 哨兵)、删 instances(不代理全校视角)、approve/reject 增 bizId 参
  · P1-05:卡片轨迹不自取(详情页把 /progress 转换 TraceVo 经 props 传入);节点摘要取
    active steps 节点名;MULTI_TASK 逐条列出本人待办各带动作,不默认 tasks[0];
    未进流程(flowInstanceId 空)详情页不渲染卡片(显式决策)
  · P1-09:FlowTaskEmbed 消息四条件(打开中+严格 origin+本 iframe+taskId/bizId 上下文一致),
    换址重置高度;宿主换址请求带代际号,关闭/换件废弃在途响应并清上下文;
    open-biz 只用与当前办件一致的 bizId 构造 /apply-detail?id=,不透传任意 URL
- ApprovalApi 类挂 svc.ts(todo/my/byBiz/approve/reject/recordUrl/taskPageUrl,写面
  taskId/applyId 查询参数+opinion/edgeName JSON body 两形态区分)+ approvalAdapter.ts 唯一映射
- /todo 路由 + 顶部导航第三链接「我的审批」+ MyApprovals.vue(双页签+记录抽屉+完整办理)
- ApplyDetail 挂审批卡片(审批操作区)+ TraceVo 转换器
- 小屏基础响应式(drawer/modal 宽度自适应、列表横向滚动;1200px 全量适配不在本批)
- 门禁:vue-tsc(ts4.9 外挂兼容组合)错误数 HEAD=工作区=18 零新增(仓内 vue-tsc1.8+ts5.9
  崩溃系历史遗留);vite build 绿(MyApprovals/approvalAdapter chunk 产出);
  preview 冒烟:/todo 渲染完整(导航/页签/表格空态/API 失败优雅降级)、详情页无崩溃(Stage S4/P3.3)
2026-10-19 23:59:48 +08:00

131 lines
4.0 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.
<template>
<Drawer :model-value="modelValue" :title="title" :width="drawerWidth" :transfer="false"
:styles="{ body: { padding: '0' } }" @update:model-value="close">
<div class="fa-embed-region">
<div v-if="error" class="fa-embed-error">
<Alert type="error" show-icon>{{ error }}</Alert>
<Button size="small" @click="load">重试</Button>
</div>
<!-- iframe 一旦给出就是只读嵌入页:本组件不放任何通过/驳回入口 -->
<iframe v-else-if="pageUrl" :src="pageUrl" :title="title" class="fa-embed-frame" />
<div v-else class="fa-embed-loading">{{ loading ? "加载中…" : "" }}</div>
</div>
</Drawer>
</template>
<script setup lang="ts">
/**
* 审批记录抽屉(hall 本地版,源自 Notice FlowRecordDrawer;漂移登记见本目录 README)。
* iframe 嵌 flow-center 的 **token 只读页**(instanceId 形态换址,后端代理)。
*
* 三条纪律写死在组件里,不给调用方犯错的机会:
* 1. pageUrl **每次打开现取**(token 10 分钟短时效),关闭/换件即清空——不缓存、不进 storage;
* 2. 只读:组件内不出现通过/驳回,也不接收这类回调;
* 3. 换址失败(未进入流程 / 无权限 / 过期)原文提示 + 重试,不白屏。
* P1-09 加严:换办件(bizId 变化)时立即废弃旧地址与在途请求结果——晚到响应只写当前 bizId。
*/
import { computed, ref, watch } from "vue";
import type { FlowApprovalApi } from "./types";
const props = withDefaults(
defineProps<{
modelValue: boolean;
api: FlowApprovalApi;
bizId: string;
title?: string;
width?: number;
}>(),
{ title: "审批记录", width: 740 }
);
const emit = defineEmits<{
(e: "update:modelValue", v: boolean): void;
}>();
/** 小屏自适应(基础响应式:1200px 全量适配不在本批) */
const drawerWidth = computed(() => (typeof window !== "undefined" && window.innerWidth < 768
? Math.min(props.width, window.innerWidth - 24)
: props.width));
const pageUrl = ref("");
const loading = ref(false);
const error = ref("");
async function load() {
const bizId = String(props.bizId);
loading.value = true;
error.value = "";
pageUrl.value = "";
try {
const res: any = await props.api.recordUrl(bizId);
// 晚到防护:请求期间已换件/已关闭,丢弃本次结果(P1-09)
if (bizId !== String(props.bizId) || !props.modelValue) return;
const url = res?.pageUrl || "";
if (!url) {
error.value = "审批记录地址获取失败,请稍后重试";
return;
}
pageUrl.value = url;
} catch (e: any) {
if (bizId !== String(props.bizId)) return;
// 后端信封 msg 原文透传("办件尚未进入流程,暂无审批记录"/"办件不存在"等)
error.value = e?.msg || e?.message || String(e) || "审批记录打开失败";
} finally {
loading.value = false;
}
}
watch(
() => props.modelValue,
(open) => {
if (open) load();
else {
// 关闭即丢地址:token 不进缓存,下次打开重新签发
pageUrl.value = "";
error.value = "";
}
}
);
// 换件即清(抽屉开着连续换办件的场景)
watch(
() => props.bizId,
() => {
pageUrl.value = "";
error.value = "";
if (props.modelValue) load();
}
);
function close(v: boolean) {
emit("update:modelValue", v);
}
</script>
<style scoped>
.fa-embed-region {
height: 100%;
display: flex;
flex-direction: column;
}
.fa-embed-frame {
display: block;
width: 100%;
height: 100%;
border: 0;
}
.fa-embed-loading {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.fa-embed-error {
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
align-items: flex-start;
}
</style>