feat(p3.3): 应用内审批前端——flow-approval 组件族本地化 + /todo 我的审批页 + 详情审批卡片
build / build (push) Successful in 2m12s

- 从 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)
This commit is contained in:
chenxing
2026-10-19 23:59:48 +08:00
parent f5b39b96c6
commit 351a513547
12 changed files with 1567 additions and 1 deletions
@@ -0,0 +1,130 @@
<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>