fix(pc-detail-responsive-approval-polish): 协调详情断点并收敛审批展示
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-08-03 21:31:44 +08:00
父节点 a5b7e18507
当前提交 c018ea4eb8
共修改 14 个文件,包含 642 行新增和 71 行删除
@@ -1,4 +1,4 @@
import { mkdirSync } from "node:fs";
import { mkdirSync, readFileSync } from "node:fs";
import path from "node:path";
import type { Page, Route } from "@playwright/test";
@@ -7,10 +7,6 @@ import { expect, test } from "./helpers/fixtures";
test.use({ serviceWorkers: "block", timezoneId: "America/Los_Angeles" });
const outputDir = path.resolve(process.cwd(), "../output/playwright/pc-detail-mobile-visual-wave3");
const taskOutputDir = path.resolve(
process.cwd(),
"../output/playwright/07-18-small-team-internal-readiness",
);
const visualUser = {
identifier: "[email protected]",
@@ -24,7 +20,7 @@ const visualMe = {
name: visualUser.name,
email: visualUser.identifier,
phone: null,
roleCodes: ["employee", "manager", "finance"],
roleCodes: ["employee", "manager", "finance_mgr"],
permissions: [
"expense.create",
"expense.submit",
@@ -39,6 +35,10 @@ const visualMe = {
"invoice.manage",
"payment.batch.manage",
"risk.review.manage",
"asset.view_own",
"asset.view_all",
"asset.manage",
"asset.transfer",
"leave.view_own",
"attendance.view_own",
],
@@ -255,6 +255,77 @@ const workflowDetail = {
],
};
const completedWorkflowDetail = {
...workflowDetail,
task: {
...workflowDetail.task,
id: "task-wave3-completed",
assigneeRole: "finance",
status: "completed",
outcome: "approved",
completedAt: "2026-05-27T10:10:00.000Z",
},
workflowInstance: {
...workflowDetail.workflowInstance,
currentStatus: "approved",
completedAt: "2026-05-27T10:10:00.000Z",
},
allowedActions: [],
history: [
...workflowDetail.history,
{
id: "workflow-history-wave3-completed",
taskId: "task-wave3-completed",
actorId: visualMe.id,
actorName: visualMe.name,
actorEmployeeNo: visualMe.employeeNo,
action: "approve",
comment: "资料已核对,同意通过",
payload: null,
createdAt: "2026-05-27T10:10:00.000Z",
},
],
};
const assetDetail = {
id: "asset-wave3",
assetNo: "ZC-2026-研发中心-000018",
name: "研发中心高性能移动工作站与现场采集设备",
assetType: "fixed_asset",
sensitivityLevel: "internal",
categoryId: "asset-category-wave3",
legalEntityId: "legal-wave3",
costCenterId: "cost-wave3",
purchaseRequestId: null,
serialNo: "SN-WAVE3-20260527-000018",
location: "研发中心设备区 A-018",
status: "in_use",
assigneeEmployeeId: visualMe.id,
ownerEmployeeId: visualMe.id,
keeperEmployeeId: visualMe.id,
archiveLocation: null,
externalRef: null,
sourceHash: null,
verificationNo: "VER-2026-WAVE3-0018",
confidentialityNote: null,
metadata: null,
sensitiveFieldsRedacted: false,
lastVerifiedAt: "2026-05-27T09:20:00.000Z",
expiresAt: null,
activatedAt: "2026-05-20",
retiredAt: null,
createdAt: "2026-05-20T08:00:00.000Z",
updatedAt: "2026-05-27T09:20:00.000Z",
category: { id: "asset-category-wave3", code: "DEV", name: "研发设备", documentCategoryId: null, documentCategory: null },
legalEntity: { id: "legal-wave3", code: "HYB", name: "本公司" },
costCenter: { id: "cost-wave3", code: "RD", name: "研发中心", legalEntityId: "legal-wave3" },
purchaseRequest: null,
assignee: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name, department: { id: "dept-wave3", name: "研发部" } },
owner: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name, department: { id: "dept-wave3", name: "研发部" } },
keeper: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name, department: { id: "dept-wave3", name: "研发部" } },
permissions: { canEdit: true },
};
const notifications = [
{
id: "notification-wave3-1",
@@ -536,11 +607,6 @@ function screenshotPath(fileName: string) {
return path.join(outputDir, fileName);
}
function taskScreenshotPath(fileName: string) {
mkdirSync(taskOutputDir, { recursive: true });
return path.join(taskOutputDir, fileName);
}
function navigationItem(key: string, href: string, label: string, icon: string, placement = "main") {
return { key, href, label, icon, placement, permissionCodes: [], featureFlagKey: null };
}
@@ -960,6 +1026,11 @@ async function mockWave3Apis(page: Page) {
return;
}
if (pathName === "/api/workflow/tasks/task-wave3-completed") {
await fulfillJson(route, completedWorkflowDetail);
return;
}
if (pathName === "/api/workflow/tasks") {
await fulfillJson(route, {
page: 1,
@@ -988,6 +1059,44 @@ async function mockWave3Apis(page: Page) {
return;
}
if (pathName === "/api/asset-categories") {
await fulfillJson(route, {
includeArchived: false,
items: [assetDetail.category],
});
return;
}
if (pathName === "/api/assets/assignees") {
await fulfillJson(route, { items: [assetDetail.assignee] });
return;
}
if (pathName === "/api/assets/asset-wave3") {
await fulfillJson(route, { asset: assetDetail });
return;
}
if (pathName === "/api/assets") {
await fulfillJson(route, { mine: false, items: [assetDetail] });
return;
}
if (pathName === "/api/master-data/legal-entities") {
await fulfillJson(route, { includeArchived: false, items: [assetDetail.legalEntity] });
return;
}
if (pathName === "/api/master-data/cost-centers") {
await fulfillJson(route, { includeArchived: false, items: [assetDetail.costCenter] });
return;
}
if (pathName === "/api/master-data/document-categories") {
await fulfillJson(route, { includeArchived: false, items: [] });
return;
}
if (pathName === "/api/portal/home") {
await fulfillJson(route, {
workspace: { id: "workspace-wave3", persisted: true, profile: "small_team_internal" },
@@ -1085,6 +1194,71 @@ test.beforeEach(async ({ page }) => {
await login(page);
});
test("PC 壳层与详情断点保持结构一致", async () => {
const appShell = readFileSync(path.resolve(process.cwd(), "src/layouts/AppShellLayout.tsx"), "utf8");
const sidebar = readFileSync(path.resolve(process.cwd(), "src/components/navigation/Sidebar.tsx"), "utf8");
const topbar = readFileSync(path.resolve(process.cwd(), "src/components/navigation/Topbar.tsx"), "utf8");
const detailTemplate = readFileSync(path.resolve(process.cwd(), "src/components/patterns/BusinessDetailTemplate.tsx"), "utf8");
const styles = readFileSync(path.resolve(process.cwd(), "src/index.css"), "utf8");
expect(appShell).toContain('"xl:ml-72"');
expect(appShell).not.toContain('"lg:ml-72"');
expect(sidebar).toContain("xl:flex xl:flex-col");
expect(sidebar).not.toContain("lg:flex lg:flex-col");
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(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 }) => {
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")).toContainText("当前审批角色:财务");
await expect(page.getByTestId("workflow-task-action-panel")).toContainText("完成时间:2026年5月27日 18:10");
await expect(page.getByTestId("workflow-task-overview").getByText("可执行动作", { exact: true })).toHaveCount(0);
await expect(page.getByTestId("workflow-task-approve")).toHaveCount(0);
await expect(page.getByTestId("workflow-task-return-toggle")).toHaveCount(0);
await expect(page.getByTestId("workflow-task-reject-toggle")).toHaveCount(0);
await expect(page.getByText("更多", { exact: true })).toHaveCount(0);
await expect(page.getByText("通过 · 王顺理", { exact: true })).toBeVisible();
await expect(page.getByText("2026年5月27日 18:10", { exact: true })).toBeVisible();
await expect(page.getByTestId("workflow-task-back")).toContainText("返回审批列表");
});
test("1280px 资产详情保持可用双栏和可点击操作区", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto("/assets?id=asset-wave3");
await expect(page.getByTestId("asset-name")).toHaveValue(assetDetail.name);
await expect(page.getByTestId("asset-detail-action-panel")).toBeVisible();
await expect(page.locator(".detail-action-panel")).toHaveCSS("position", "sticky");
const assetLayout = await page.locator(".detail-workspace-grid").evaluate((workspace) => {
const [main, aside] = Array.from(workspace.children) as HTMLElement[];
return {
mainWidth: main.getBoundingClientRect().width,
asideWidth: aside.getBoundingClientRect().width,
columns: getComputedStyle(workspace).gridTemplateColumns.split(" ").filter(Boolean).length,
overflow: document.documentElement.scrollWidth - document.documentElement.clientWidth,
};
});
expect(assetLayout.columns).toBe(2);
expect(assetLayout.mainWidth).toBeGreaterThanOrEqual(560);
expect(assetLayout.asideWidth).toBeGreaterThanOrEqual(280);
expect(assetLayout.overflow).toBeLessThanOrEqual(2);
const saveButtonHitTest = await page.getByTestId("asset-save").evaluate((button) => {
const rect = button.getBoundingClientRect();
const hit = document.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2);
return hit === button || button.contains(hit);
});
expect(saveButtonHitTest).toBe(true);
});
test("报销详情固定使用上海时区并收敛只读信息", async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 960 });
await page.goto("/reimbursements/reim-wave3");
@@ -1151,8 +1325,16 @@ test("Wave3 PC 详情和通知中心视觉截图", async ({ page }) => {
await page.screenshot({ path: screenshotPath("01-pc-reimbursement-detail.png"), fullPage: true });
await page.goto("/workflow/tasks/task-wave3");
await page.setViewportSize({ width: 1024, height: 768 });
await page.setViewportSize({ width: 1100, height: 768 });
await expect(page.getByRole("heading", { name: "审批详情" })).toBeVisible();
await expect(page.locator(".app-shell-sidebar")).toBeHidden();
await expect(page.getByRole("button", { name: "打开菜单" })).toBeVisible();
await expect(page.locator(".detail-workspace-grid")).toHaveCSS("grid-template-columns", /.+ .+/);
await expectNoHorizontalOverflow(page);
await page.setViewportSize({ width: 1280, height: 800 });
await expect(page.locator(".app-shell-sidebar")).toBeVisible();
await expect(page.getByRole("button", { name: "打开菜单" })).toBeHidden();
await expect(page.getByTestId("workflow-task-next-action")).toHaveCount(0);
await expect(page.getByText("审批动作")).toHaveCount(0);
await expect(page.getByRole("heading", { name: "处理", exact: true })).toBeVisible();
@@ -1161,6 +1343,25 @@ test("Wave3 PC 详情和通知中心视觉截图", async ({ page }) => {
await expect(page.locator(".workflow-action-panel")).toHaveCSS("position", "sticky");
await expect(page.locator(".workflow-action-panel")).toHaveCSS("top", "96px");
await expect(page.locator(".workflow-action-panel")).toHaveCSS("z-index", "30");
const workflowLayout = await page.locator(".detail-workspace-grid").evaluate((workspace) => {
const [main, aside] = Array.from(workspace.children) as HTMLElement[];
return {
mainWidth: main.getBoundingClientRect().width,
asideWidth: aside.getBoundingClientRect().width,
columns: getComputedStyle(workspace).gridTemplateColumns.split(" ").filter(Boolean).length,
overflow: document.documentElement.scrollWidth - document.documentElement.clientWidth,
};
});
expect(workflowLayout.columns).toBe(2);
expect(workflowLayout.mainWidth).toBeGreaterThanOrEqual(560);
expect(workflowLayout.asideWidth).toBeGreaterThanOrEqual(280);
expect(workflowLayout.overflow).toBeLessThanOrEqual(2);
const formNumberLineCount = await page.getByText(reimbursementDetail.formNumber, { exact: true }).evaluate((element) => {
const range = document.createRange();
range.selectNodeContents(element);
return range.getClientRects().length;
});
expect(formNumberLineCount).toBe(1);
await page.setViewportSize({ width: 1440, height: 960 });
await page.getByTestId("workflow-task-reimbursement-detail-link").click();
await expect(page).toHaveURL(/\/reimbursements\/reim-wave3$/);
@@ -1217,7 +1418,7 @@ test("PC 报销详情按来源返回并保留列表筛选", async ({ page }) =>
await page.goto("/reimbursements?status=reviewing&page=1&scope=all");
await page.getByTestId("reimbursement-detail-link").first().click();
await expect(page).toHaveURL(/\/workflow\/tasks\/task-wave3$/);
await expect(page.getByTestId("workflow-task-back")).toContainText("返回报销列表");
await expect(page.getByTestId("workflow-task-back")).toContainText("返回审批列表");
await page.getByTestId("workflow-task-back").click();
await expect(page).toHaveURL(/\/reimbursements\?status=reviewing&page=1&scope=all$/);
@@ -1255,7 +1456,7 @@ test("/contracts-registry 在 768x1024 与 1024x768 下操作列可达", async (
await expect(operation).toBeVisible();
await operation.click({ trial: true });
await page.screenshot({
path: taskScreenshotPath(`contracts-registry-${viewport.width}x${viewport.height}.png`),
path: screenshotPath(`contracts-registry-${viewport.width}x${viewport.height}.png`),
fullPage: true,
});
}