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
+138
View File
@@ -0,0 +1,138 @@
<script setup lang="ts">
/**
* 我的审批(/todo,P3.3):待我审批(中心 todo)+ 我提交的(本地办件)两页签。
* 完整办理=去壳任务页 iframe(FlowTaskEmbed);审批记录=token 只读抽屉(FlowRecordDrawer)。
*
* 宿主职责(P1-09):
* - open-task 换址请求带代际号——关闭/换件时废弃在途响应并清 taskUrl/选中 ID;
* - open-biz 只用与当前办件上下文一致的 bizId 构造 /apply-detail?id=,不照抄任意 URL。
*/
import { onBeforeUnmount, ref } from "vue";
import { useRouter } from "vue-router";
import { Message } from "view-ui-plus";
import FlowApprovalList from "@/components/flow-approval/FlowApprovalList.vue";
import FlowRecordDrawer from "@/components/flow-approval/FlowRecordDrawer.vue";
import FlowTaskEmbed from "@/components/flow-approval/FlowTaskEmbed.vue";
import svcFlowApprovalApi from "@/api/module/approvalAdapter";
const router = useRouter();
const listRef = ref<InstanceType<typeof FlowApprovalList> | null>(null);
// ===== 审批记录抽屉 =====
const recordVisible = ref(false);
const recordBizId = ref("");
function openRecord(bizId: string) {
recordBizId.value = bizId;
recordVisible.value = true;
}
// ===== 完整办理(去壳任务页) =====
const taskVisible = ref(false);
const taskUrl = ref("");
const taskTaskId = ref<number | string>("");
const taskBizId = ref<string | number>("");
/** 换址请求代际号:关闭/换件时 +1,晚到响应按代际丢弃(P1-09) */
let taskReqEpoch = 0;
async function openTask(payload: { bizId: string; taskId: number }) {
const epoch = ++taskReqEpoch;
taskBizId.value = payload.bizId;
taskTaskId.value = payload.taskId;
taskUrl.value = "";
taskVisible.value = true;
try {
const res: any = await svcFlowApprovalApi.taskPageUrl(payload.bizId, payload.taskId);
if (epoch !== taskReqEpoch) return; // 已关闭/已换件:废弃
const url = res?.pageUrl || "";
if (!url) {
Message.error("任务页地址获取失败");
taskVisible.value = false;
return;
}
taskUrl.value = url;
} catch (e: any) {
if (epoch !== taskReqEpoch) return;
Message.error(e?.msg || e?.message || "任务页地址获取失败");
taskVisible.value = false;
}
}
function onTaskClose() {
// 关闭即清上下文 + 废弃在途换址请求
taskReqEpoch++;
taskUrl.value = "";
taskTaskId.value = "";
taskBizId.value = "";
}
function onHandled() {
// 办理完成:刷新列表(本地 flow_status 由回调推进,前端只刷数据面)
listRef.value?.load();
}
/**
* open-biz:只用校验过的本办件 ID 构造 /apply-detail?id=(P1-09)。
* 消息 bizId 必须与打开任务页时锁定的办件上下文一致;缺失时用锁定上下文;绝不透传 bizUrl 路由。
*/
function onOpenBiz(payload: { bizId?: string | null; bizUrl?: string }) {
const msgBizId = payload.bizId != null ? String(payload.bizId) : "";
const ctxBizId = String(taskBizId.value || "");
if (!msgBizId && !ctxBizId) return;
if (msgBizId && ctxBizId && msgBizId !== ctxBizId) return; // 换件期间旧消息,作废
const applyId = Number(msgBizId || ctxBizId);
if (!Number.isInteger(applyId) || applyId <= 0) return;
taskVisible.value = false;
router.push(`/apply-detail?id=${applyId}`);
}
/** "我提交的"行查看详情(只用本行 id 构造路由) */
function openDetail(applyId: number) {
router.push(`/apply-detail?id=${applyId}`);
}
onBeforeUnmount(() => {
taskReqEpoch++;
});
</script>
<template>
<div class="page todo-page">
<div class="page-head">
<h2 class="page-title">我的审批</h2>
<span class="page-sub">「待我审批」为流程中心待办;「我提交的」为本人在大厅提交的办件</span>
</div>
<FlowApprovalList ref="listRef" :api="svcFlowApprovalApi" :tabs="['todo', 'my']"
@open-record="openRecord" @open-task="openTask" @handled="onHandled"
@open-detail="openDetail" />
<FlowRecordDrawer v-model="recordVisible" :api="svcFlowApprovalApi" :biz-id="recordBizId" />
<FlowTaskEmbed v-model="taskVisible" :page-url="taskUrl" :task-id="taskTaskId" :biz-id="taskBizId"
@handled="onHandled" @open-biz="onOpenBiz" @close="onTaskClose" />
</div>
</template>
<style scoped>
.todo-page {
max-width: 1080px;
margin: 0 auto;
width: 100%;
box-sizing: border-box;
}
.page-head {
display: flex;
align-items: baseline;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 12px;
}
.page-title {
font-size: 18px;
font-weight: 600;
margin: 0;
}
.page-sub {
font-size: 13px;
color: var(--sh-text-weak, #6b7280);
}
</style>