收敛审批动作面板并清理层叠遗留 [approval-action-panel-kiss-layering]

这个提交包含在:
2026-09-03 20:49:15 +08:00
父节点 d63ccdf77c
当前提交 c4b4427f9e
共修改 15 个文件,包含 418 行新增和 106 行删除
@@ -1247,20 +1247,18 @@ test("PC 壳层与详情断点保持结构一致", async () => {
expect(topbar).toContain('className="xl:hidden"');
expect(topbar).not.toContain('className="lg:hidden" size="icon" variant="secondary" aria-label="打开菜单"');
expect(detailTemplate).toContain("detail-action-panel lg:sticky lg:top-24");
expect(detailTemplate).toMatch(/if \(!hasStatus && !hasActionContent && !hasSummary && !hasChildren\) \{\s*return null;/u);
expect(detailTemplate).toMatch(/if \(!hasStatus && !hasBeforeActions && !hasActionContent && !hasSummary && !hasChildren\) \{\s*return null;/u);
expect(styles).toMatch(/\.detail-workspace-grid\s*\{[\s\S]*?lg:grid-cols-/u);
expect(styles).toMatch(/\.workflow-action-panel\s*\{[\s\S]*?relative z-30/u);
});
test("已完成审批只保留中文只读摘要", async ({ page }) => {
test("已完成审批不保留空动作面板", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto("/workflow/tasks/task-wave3-completed");
await expect(page.getByTestId("workflow-task-action-panel")).toBeVisible();
await expect(page.getByTestId("workflow-task-action-panel")).toHaveCount(0);
await expect(page.getByTestId("reimbursement-expense-items-table")).toBeVisible();
await expect(page.getByTestId("approval-item-evidence").getByRole("button")).toHaveCount(0);
await expect(page.getByTestId("reimbursement-desktop-workspace")).toBeVisible();
await expect(page.getByTestId("workflow-task-action-panel")).toContainText("审批角色:财务");
await expect(page.getByTestId("reimbursement-desktop-workspace")).toContainText("2026年5月27日 18:10");
await expect(page.getByTestId("workflow-task-approve")).toHaveCount(0);
await expect(page.getByTestId("workflow-task-return-toggle")).toHaveCount(0);
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
import path from "node:path";
import { expect, test } from "@playwright/test";
import { buildReimbursementApprovalEvidence } from "../../src/lib/reimbursementApprovalProjection";
import { resolveReimbursementApprovalBlockReason } from "../../src/lib/reimbursementApprovalReadiness";
import {
buildReimbursementAmountSummary,
parseMoneyToMinorUnits,
@@ -42,6 +43,53 @@ test("报销金额使用整数分判断完整性且不产生展示用第二总
});
});
test("报销审批五类阻断原因由同一投影给出且正常态保持安静", () => {
const base = {
amountIntegrity: "ready" as const,
detailState: "ready" as const,
online: true,
snapshotParseStatus: "ready",
};
expect(resolveReimbursementApprovalBlockReason(base)).toBeNull();
expect(resolveReimbursementApprovalBlockReason({ ...base, online: false })).toBe("报销审批需联网核对完整报销内容;当前不能通过、退回、驳回或转审。");
expect(resolveReimbursementApprovalBlockReason({ ...base, detailState: "loading" })).toBe("正在加载完整报销内容和票据;加载完成前不能作出审批决定。");
expect(resolveReimbursementApprovalBlockReason({ ...base, detailState: "error" })).toBe("报销内容未加载完整,暂不能审批,请刷新后重试");
expect(resolveReimbursementApprovalBlockReason({ ...base, snapshotParseStatus: "invalid" })).toBe("提交时检查记录无法解析,暂时不能审批;请联系系统管理员核对。");
expect(resolveReimbursementApprovalBlockReason({ ...base, snapshotParseStatus: undefined })).toBe("提交时检查记录无法解析,暂时不能审批;请联系系统管理员核对。");
expect(resolveReimbursementApprovalBlockReason({ ...base, amountIntegrity: "mismatch" })).toBe("报销总额与明细汇总不一致,请联系管理员");
expect(resolveReimbursementApprovalBlockReason({ ...base, amountIntegrity: "unavailable" })).toBe("明细金额无法完整核对,请联系管理员");
});
test("动作面板复用唯一阻断投影且预算三态判断保持原合同", () => {
const adapter = source("src/pages/reimbursement-workspace/ReimbursementApprovalAdapter.tsx");
expect(adapter.match(/resolveReimbursementApprovalBlockReason\(/g)).toHaveLength(1);
expect(adapter).toContain("throw new Error(decisionBlockReason");
expect(adapter).toContain("setActionError(decisionBlockReason");
expect(adapter).toContain("{decisionBlockReason}");
expect(adapter.match(/snapshot\?\.budgetCheckStatus === "missing"/g)).toHaveLength(3);
expect(adapter).not.toContain("完整内容已就绪");
expect(adapter).not.toContain("内容未就绪,审批动作已停用");
expect(adapter).not.toContain("statusSummary");
expect(adapter).toContain("actionError || activeSecondaryAction ?");
});
test("旧审批层叠 class 与更多菜单样式无定义或消费者", () => {
const styles = source("src/index.css");
const productSources = [
source("src/components/patterns/BusinessDetailTemplate.tsx"),
source("src/pages/WorkflowTaskDetailPage.tsx"),
source("src/pages/reimbursement-workspace/ReimbursementApprovalAdapter.tsx"),
].join("\n");
for (const legacyClass of ["workflow-action-panel", "workflow-action-more"]) {
expect(styles).not.toContain(legacyClass);
expect(productSources).not.toContain(legacyClass);
}
expect(productSources).toContain("detail-action-panel lg:sticky lg:top-24");
});
test("PC 与移动明细只展示共享模型中的服务端权威合计", () => {
const evidenceList = source("src/components/reimbursements/ReimbursementItemEvidenceList.tsx");
const desktopDetail = source("src/pages/ReimbursementDetailPage.tsx");
@@ -358,6 +358,77 @@ test.describe("R7E 审批动作二次确认", () => {
await expect.poll(() => actionCalls.return).toBe(1);
});
for (const scenario of [
{
entityType: "leave",
label: "请假",
detail: { leaveRequest: pendingTaskDetail.leaveRequest },
expectedText: "R7E 审批动作确认",
},
{
entityType: "attendance_correction",
label: "补卡",
detail: {
attendanceCorrection: {
id: "attendance-confirm-1", status: "reviewing", kind: "missed_punch", workDate: "2026-09-02",
missedPunchAt: "09:05", startAt: null, endAt: null, locationSummary: "公司", compensationPolicy: "按实际打卡修正",
reason: "忘记打卡", owner: { id: "employee-confirm-applicant", employeeNo: "E-confirm", name: "陈工程" },
},
},
expectedText: "忘记打卡",
},
{
entityType: "purchase_request",
label: "采购",
detail: {
purchaseRequest: {
id: "purchase-confirm-1", requestNo: "CG-CONFIRM-001", title: "办公用品采购", status: "reviewing",
totalAmount: "800.00", currencyCode: "CNY", expectedDeliveryDate: "2026-09-10", reason: "补充日常库存",
applicant: { id: "employee-confirm-applicant", employeeNo: "E-confirm", name: "陈工程" },
supplier: { id: "supplier-confirm", name: "合成供应商" }, legalEntity: { id: "entity-confirm", name: "示例公司" },
costCenter: { id: "cost-center-confirm", name: "研发中心" },
},
},
expectedText: "办公用品采购",
},
{
entityType: "travel_request",
label: "差旅",
detail: {
travelRequest: {
id: "travel-confirm-1", requestNo: "CL-CONFIRM-001", status: "reviewing", tripType: "domestic",
startDate: "2026-09-10", endDate: "2026-09-12", estimatedAmount: "1800.00", purpose: "客户现场交流",
applicant: { id: "employee-confirm-applicant", employeeNo: "E-confirm", name: "陈工程" },
legalEntity: { id: "entity-confirm", name: "示例公司" }, costCenter: { id: "cost-center-confirm", name: "研发中心" },
},
},
expectedText: "客户现场交流",
},
]) {
test(`1280px ${scenario.label}审批保持通用详情与动作可达`, async ({ page }) => {
const taskDetail = {
...pendingTaskDetail,
workflowInstance: { ...pendingTaskDetail.workflowInstance, entityType: scenario.entityType },
reimbursement: null,
leaveRequest: null,
attendanceCorrection: null,
purchaseRequest: null,
travelRequest: null,
...scenario.detail,
};
await mockWorkflowConfirmApis(page, { taskDetail });
await page.setViewportSize({ width: 1280, height: 800 });
await login(page, MANAGER);
await page.goto(`/workflow/tasks/${pendingTask.id}`);
await expect(page.getByTestId("workflow-task-summary")).toContainText(scenario.expectedText);
await expect(page.getByRole("heading", { name: "处理历史" })).toBeVisible();
await expect(page.getByTestId("workflow-task-action-panel")).toBeVisible();
await expect(page.getByTestId("workflow-task-approve")).toBeEnabled();
expect(await page.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth + 2)).toBe(true);
});
}
test("PC 报销审批同屏展示证据、引用退回意见且不暴露维护动作", async ({ page }) => {
const reimbursementId = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb";
const taskDetail = {
@@ -437,13 +508,18 @@ test.describe("R7E 审批动作二次确认", () => {
await expect(page.getByTestId("reimbursement-amount-summary")).toContainText("¥180.00");
await expect(page.getByText("报销总额与明细汇总不一致,请联系管理员")).toHaveCount(0);
await expect(page.getByText("明细金额无法完整核对,请联系管理员")).toHaveCount(0);
const actionPanel = page.getByTestId("workflow-task-action-panel");
await expect(actionPanel.getByText("完整内容已就绪", { exact: true })).toHaveCount(0);
await expect(actionPanel.getByText(/^节点:/)).toHaveCount(0);
await expect(actionPanel.getByText(/^状态:/)).toHaveCount(0);
await expect(actionPanel.getByText(/^审批角色:/)).toHaveCount(0);
await expect(page.getByTestId("workflow-task-approve")).toBeEnabled();
await expect(page.getByTestId("workflow-task-return-toggle")).toBeEnabled();
await expect(page.getByTestId("workflow-task-reject-toggle")).toBeEnabled();
await expect(page.getByTestId("workflow-task-transfer-menu-item")).toBeEnabled();
await expect(page.getByTestId("reimbursement-workflow-history").locator(".detail-timeline-item")).toHaveCount(6);
await expect(page.getByRole("heading", { name: "处理历史" })).toHaveCount(1);
const actionBox = await page.getByTestId("workflow-task-action-panel").boundingBox();
const actionBox = await actionPanel.boundingBox();
const historyBox = await page.getByTestId("reimbursement-workflow-history").boundingBox();
expect(actionBox).not.toBeNull();
expect(historyBox).not.toBeNull();
@@ -474,7 +550,7 @@ test.describe("R7E 审批动作二次确认", () => {
workflowInstance: {
...pendingTaskDetail.workflowInstance,
entityType: "reimbursement",
contextData: { standardViolations: [], budgetCheck: null, budgetCheckStatus: "not_applicable", parseStatus: "ready" },
contextData: { standardViolations: [], budgetCheck: null, budgetCheckStatus: "available", parseStatus: "ready" },
},
reimbursement: {
id: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", formNumber: "BX-FAIL-001", status: "reviewing",
@@ -487,7 +563,7 @@ test.describe("R7E 审批动作二次确认", () => {
await login(page, MANAGER);
await page.goto(`/workflow/tasks/${pendingTask.id}`);
await expect(page.getByRole("alert")).toContainText("服务暂不可用");
await expect(page.getByRole("alert")).toContainText("报销内容未加载完整,暂不能审批,请刷新后重试");
await expect(page.getByTestId("workflow-task-approve")).toBeDisabled();
await expect(page.getByTestId("workflow-task-return-toggle")).toBeDisabled();
await expect(page.getByTestId("workflow-task-reject-toggle")).toBeDisabled();
@@ -501,7 +577,7 @@ test.describe("R7E 审批动作二次确认", () => {
workflowInstance: {
...pendingTaskDetail.workflowInstance,
entityType: "reimbursement",
contextData: { standardViolations: [], budgetCheck: null, budgetCheckStatus: "not_applicable", parseStatus: "ready" },
contextData: { standardViolations: [], budgetCheck: null, budgetCheckStatus: "available", parseStatus: "ready" },
},
reimbursement: {
id: reimbursementId,
@@ -549,6 +625,7 @@ test.describe("R7E 审批动作二次确认", () => {
await page.goto(`/mobile/approvals/${pendingTask.id}`);
await expect(page.getByTestId("reimbursement-amount-summary")).toContainText("¥100.00");
await expect(page.locator("#reimbursement-workspace-snapshot")).toHaveCount(0);
await expect(page.getByTestId("reimbursement-workflow-history").locator(".detail-timeline-item")).toHaveCount(6);
const assertMobileGeometry = async () => {
expect(await page.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth + 2)).toBe(true);
@@ -627,7 +704,15 @@ test.describe("R7E 审批动作二次确认", () => {
await page.goto(`/workflow/tasks/${pendingTask.id}`);
await expect(page.getByTestId("reimbursement-amount-summary")).toContainText("¥100.00");
await expect(page.getByRole("alert").filter({ hasText: scenario.expectedMessage })).toBeVisible();
await expect(page.locator("#reimbursement-workspace-snapshot")).toHaveCount(0);
const blockReason = page.getByTestId("reimbursement-approval-block-reason");
await expect(blockReason).toHaveText(scenario.expectedMessage);
const primaryAction = page.getByTestId("workflow-task-approve");
const blockReasonBox = await blockReason.boundingBox();
const primaryActionBox = await primaryAction.boundingBox();
expect(blockReasonBox).not.toBeNull();
expect(primaryActionBox).not.toBeNull();
expect(blockReasonBox!.y + blockReasonBox!.height).toBeLessThanOrEqual(primaryActionBox!.y);
await expect(page.getByText("差额", { exact: true })).toHaveCount(0);
await expect(page.getByTestId("workflow-task-approve")).toBeDisabled();
await expect(page.getByTestId("workflow-task-return-toggle")).toBeDisabled();