From 6a90fa9bd7ecad91f0570b22651cdf07775ca54a Mon Sep 17 00:00:00 2001 From: shunli Date: Wed, 6 May 2026 23:19:08 +0800 Subject: [PATCH] =?UTF-8?q?R103/A1-A6=20=E6=94=B6=E5=8F=A3=E8=B7=A8?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E8=AE=BE=E8=AE=A1=E4=B8=80=E8=87=B4=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/features/navigation/schema.ts | 14 +- client/src/pages/PurchaseRequestsPage.tsx | 52 ++- .../pages/invoice-pool/InvoiceBindCard.tsx | 3 +- .../r103-cross-page-ui-consistency.spec.ts | 381 ++++++++++++++++++ ...3-07-02-r103-cross-page-ui-consistency.csv | 7 + 5 files changed, 441 insertions(+), 16 deletions(-) create mode 100644 client/tests/e2e/r103-cross-page-ui-consistency.spec.ts create mode 100644 issues/2026-05-06_23-07-02-r103-cross-page-ui-consistency.csv diff --git a/client/src/features/navigation/schema.ts b/client/src/features/navigation/schema.ts index f7dc8527..2c81afb9 100644 --- a/client/src/features/navigation/schema.ts +++ b/client/src/features/navigation/schema.ts @@ -3,12 +3,19 @@ export const ADMIN_NAV_ORDER = ["/system/workspace", "/people", "/organization", export const BACKUP_NAV_ORDER = ["/attendance", "/workflow/tasks", "/contracts-registry", "/purchase-requests", "/travel-requests", "/budgets", "/payment-batches", "/ap-operations", "/invoice-pool", "/inventory-checks", "/system/cutover", "/system/integrations/erp", "/dashboard", "/settings/profile"]; const PAGE_TITLES: Array<{ path: string; title: string; group: string }> = [ - { path: "/portal", title: "Portal", group: "工作台" }, + { path: "/portal", title: "工作台", group: "工作台" }, { path: "/announcements", title: "公告", group: "主导航" }, { path: "/team", title: "团队", group: "主导航" }, { path: "/attendance", title: "考勤", group: "备用模块" }, { path: "/projects", title: "项目", group: "主导航" }, { path: "/contracts-registry", title: "合同台账", group: "备用模块" }, + { path: "/purchase-requests", title: "采购申请", group: "备用模块" }, + { path: "/travel-requests", title: "差旅申请", group: "备用模块" }, + { path: "/budgets", title: "预算管理", group: "备用模块" }, + { path: "/payment-batches", title: "付款批次", group: "备用模块" }, + { path: "/ap-operations", title: "应付作业", group: "备用模块" }, + { path: "/invoice-pool", title: "票据中心", group: "备用模块" }, + { path: "/inventory-checks", title: "盘点任务", group: "备用模块" }, { path: "/weekly-reports", title: "周报", group: "主导航" }, { path: "/todos", title: "待办", group: "主导航" }, { path: "/reimbursements/", title: "报销详情", group: "主导航" }, @@ -29,6 +36,9 @@ const PAGE_TITLES: Array<{ path: string; title: string; group: string }> = [ { path: "/system/workflow-definitions", title: "流程定义", group: "管理入口" }, { path: "/system/settings", title: "系统设置", group: "管理入口" }, { path: "/system/feature-flags", title: "功能开关", group: "管理入口" }, + { path: "/system/cutover", title: "切换控制台", group: "管理入口" }, + { path: "/system/integrations/erp", title: "ERP 集成", group: "管理入口" }, + { path: "/dashboard", title: "数据看板", group: "备用模块" }, { path: "/audit-logs", title: "审计日志", group: "管理入口" }, ]; @@ -41,7 +51,7 @@ export function resolvePageMeta(pathname: string) { } if (pathname === "/") { - return { title: "Portal", group: "工作台" }; + return { title: "工作台", group: "工作台" }; } return { title: pathname, group: "工作台" }; diff --git a/client/src/pages/PurchaseRequestsPage.tsx b/client/src/pages/PurchaseRequestsPage.tsx index 18f0095f..db554cd7 100644 --- a/client/src/pages/PurchaseRequestsPage.tsx +++ b/client/src/pages/PurchaseRequestsPage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useSearchParams } from "react-router-dom"; import { toast } from "sonner"; @@ -232,6 +232,8 @@ export function PurchaseRequestsPage() { const [items, setItems] = useState([]); const [archivePoNo, setArchivePoNo] = useState(""); const [archiveContractNo, setArchiveContractNo] = useState(""); + const [selectedAttachmentName, setSelectedAttachmentName] = useState(""); + const attachmentInputRef = useRef(null); const suppliersQuery = useQuery({ queryKey: ["purchase-requests", "suppliers"], @@ -701,7 +703,7 @@ export function PurchaseRequestsPage() {

附件

- { - const file = event.target.files?.[0]; - if (file) { - uploadMutation.mutate(file); - } - event.currentTarget.value = ""; - }} - /> +
+ { + const file = event.target.files?.[0]; + if (file) { + setSelectedAttachmentName(file.name); + uploadMutation.mutate(file); + } + event.currentTarget.value = ""; + }} + /> + + {uploadMutation.isPending + ? `正在上传:${uploadMutation.variables?.name ?? selectedAttachmentName}` + : selectedAttachmentName || "未选择文件"} + + +
{detail.attachments.length ? ( diff --git a/client/src/pages/invoice-pool/InvoiceBindCard.tsx b/client/src/pages/invoice-pool/InvoiceBindCard.tsx index 405440b4..8edcf4d5 100644 --- a/client/src/pages/invoice-pool/InvoiceBindCard.tsx +++ b/client/src/pages/invoice-pool/InvoiceBindCard.tsx @@ -3,6 +3,7 @@ import type { InvoicePoolListResponse } from "../../api/invoicePool"; import type { ReimbursementListResponse } from "../../api/reimbursements"; import type { TravelRequestDetail, TravelRequestListResponse } from "../../api/travelRequests"; import { Button } from "../../components/ui/button"; +import { resolveReimbursementDisplayCode } from "../../lib/reimbursementDisplay"; export type InvoiceBindMode = "reimbursement" | "travelOrder"; @@ -87,7 +88,7 @@ export function InvoiceBindCard({ {(reimbursementQuery.data?.items ?? []).map((item) => ( ))} diff --git a/client/tests/e2e/r103-cross-page-ui-consistency.spec.ts b/client/tests/e2e/r103-cross-page-ui-consistency.spec.ts new file mode 100644 index 00000000..bb52e70c --- /dev/null +++ b/client/tests/e2e/r103-cross-page-ui-consistency.spec.ts @@ -0,0 +1,381 @@ +import { mkdirSync } from "node:fs"; +import path from "node:path"; +import { expect, test, type Page, type Route } from "@playwright/test"; + +function envelope(data: unknown) { + return { requestId: "mock-r103-ui-consistency", data }; +} + +async function fulfillJson(route: Route, data: unknown, status = 200) { + await route.fulfill({ + status, + contentType: "application/json", + body: JSON.stringify(envelope(data)), + }); +} + +const screenshotDir = path.resolve(process.cwd(), "../output/playwright/r103"); + +function screenshotPath(fileName: string) { + mkdirSync(screenshotDir, { recursive: true }); + return path.join(screenshotDir, fileName); +} + +const me = { + id: "employee-r103", + employeeNo: "E103", + name: "Employee", + phone: "13800000000", + email: "employee@hyinda.com", + departmentId: "dept-r103", + directManagerId: null, + status: "active", + notificationPushEnabled: true, + roleCodes: ["employee"], + profileCompleteness: { + hasEmergencyContact: true, + hasBankAccount: true, + hasAvatar: false, + hasIdNumber: false, + score: 50, + }, + wecomBinding: null, +}; + +const purchaseId = "purchase-r103"; +const reimbursementId = "11111111-1111-4111-8111-111111111111"; +const invoiceId = "invoice-r103"; + +function navigationItem(key: string, href: string, label: string, icon: string) { + return { key, href, label, icon, placement: "main", permissionCodes: [], featureFlagKey: null }; +} + +function purchaseSummary() { + return { + id: purchaseId, + requestNo: "CG-20260506-0001", + applicantId: me.id, + submitterId: me.id, + departmentId: me.departmentId, + legalEntityId: "legal-r103", + costCenterId: "cost-r103", + supplierId: "supplier-r103", + documentCategoryId: "category-r103", + budgetPoolId: null, + title: "办公用品采购", + reason: "日常办公", + currencyCode: "CNY", + totalAmount: "299.00", + expectedDeliveryDate: "2026-05-10", + status: "draft", + createdAt: "2026-05-06T09:00:00.000Z", + updatedAt: "2026-05-06T09:10:00.000Z", + applicant: { id: me.id, employeeNo: me.employeeNo, name: me.name }, + submitter: { id: me.id, employeeNo: me.employeeNo, name: me.name }, + department: { id: me.departmentId, name: "研发部" }, + legalEntity: { id: "legal-r103", code: "HYD", name: "海音达" }, + costCenter: { id: "cost-r103", code: "RD", name: "研发部", legalEntityId: "legal-r103" }, + supplier: { id: "supplier-r103", code: "SUP", name: "测试供应商" }, + documentCategory: { id: "category-r103", code: "CG", name: "采购申请", entityType: "purchase_request" }, + budgetPool: null, + }; +} + +function purchaseDetail() { + return { + ...purchaseSummary(), + items: [ + { + id: "purchase-item-r103", + description: "键盘", + specification: "无线", + quantity: "1.00", + unitPrice: "299.00", + lineAmount: "299.00", + isAssetCandidate: false, + createdAt: "2026-05-06T09:00:00.000Z", + updatedAt: "2026-05-06T09:10:00.000Z", + }, + ], + attachments: [], + pendingWorkflow: null, + archiveOrder: null, + permissions: { + canEditDraft: true, + canSubmit: true, + canWithdraw: false, + canArchiveOrder: false, + }, + riskFlags: [], + }; +} + +function invoiceItem() { + return { + id: invoiceId, + status: "available", + ocrType: "mixed", + ocrProvider: "mock", + ocrAttempts: 1, + ocrLastError: null, + ocrCompletedAt: "2026-05-06T09:00:00.000Z", + invoiceCode: "044002600111", + invoiceNumber: "12345678", + invoiceDate: "2026-05-06", + amount: "100.00", + taxAmount: "6.00", + totalAmount: "106.00", + linkedReimbursementId: null, + linkedItemId: null, + linkedTravelOrderId: null, + originalName: "发票.pdf", + mimeType: "application/pdf", + fileSize: 1024, + verification: null, + owner: { id: me.id, employeeNo: me.employeeNo, name: me.name }, + uploadedBy: { id: me.id, employeeNo: me.employeeNo, name: me.name }, + createdAt: "2026-05-06T09:00:00.000Z", + updatedAt: "2026-05-06T09:10:00.000Z", + }; +} + +function reimbursementForBind() { + return { + id: reimbursementId, + formNumber: null, + status: "draft", + reimbursementDate: "2026-05-06", + totalAmount: "106.00", + description: "发票绑定测试", + payment: { + paymentVoucherNo: null, + paymentDate: null, + paymentChannel: null, + paidAt: null, + paidBy: null, + }, + owner: { id: me.id, employeeNo: me.employeeNo, name: me.name }, + submitter: { id: me.id, employeeNo: me.employeeNo, name: me.name }, + project: null, + pendingWorkflow: null, + createdAt: "2026-05-06T09:00:00.000Z", + updatedAt: "2026-05-06T09:10:00.000Z", + }; +} + +async function mockApis(page: Page) { + await page.route((url) => url.pathname.startsWith("/api/"), async (route) => { + const request = route.request(); + const url = new URL(request.url()); + const pathName = url.pathname; + + if (pathName === "/api/auth/sso/providers") { + await fulfillJson(route, []); + return; + } + + if (pathName === "/api/auth/wecom/config") { + await fulfillJson(route, { + enabled: false, + entryEnabled: false, + oauthEnabled: false, + corpId: "", + agentId: "", + loginUrl: null, + bindEnabled: false, + defaultReturnTo: "/portal", + status: "disabled", + statusReasons: [], + }); + return; + } + + if (pathName === "/api/auth/login" || pathName === "/api/auth/refresh") { + await route.fulfill({ + status: 200, + contentType: "application/json", + headers: { "set-cookie": "has_session=1; Path=/; SameSite=Lax" }, + body: JSON.stringify(envelope({ accessToken: "mock-r103-token", expiresIn: 900, mustChangePassword: false })), + }); + return; + } + + if (pathName === "/api/me") { + await fulfillJson(route, me); + return; + } + + if (pathName === "/api/system/navigation") { + await fulfillJson(route, { + workspace: { + id: "workspace-r103", + persisted: true, + profile: "small_team_internal", + orgMode: "single_company", + primaryEntryMode: "password", + notificationStrategy: "in_app", + devicePriority: "desktop", + moduleVisibility: {}, + createdAt: null, + updatedAt: null, + updatedBy: null, + }, + sections: { + main: [ + navigationItem("portal", "/portal", "工作台", "home_app_logo"), + navigationItem("purchase", "/purchase-requests", "采购申请", "shopping_cart"), + navigationItem("invoice", "/invoice-pool", "票据中心", "receipt_long"), + ], + admin: [], + backup: [ + navigationItem("payment", "/payment-batches", "付款批次", "payments"), + navigationItem("dashboard", "/dashboard", "数据看板", "monitoring"), + ], + }, + recommendedRolePresets: [], + }); + return; + } + + if (pathName === "/api/notifications") { + await fulfillJson(route, { page: 1, pageSize: 1, total: 0, unreadOnly: true, unreadCount: 0, items: [] }); + return; + } + + if (pathName === "/api/portal/home") { + await fulfillJson(route, { + workspace: { id: "workspace-r103", persisted: true, profile: "small_team_internal" }, + greeting: { name: me.name }, + cards: [], + pendingTasks: { count: 0, href: "/workflow/tasks" }, + myApplications: [], + riskSummary: null, + announcements: [], + quickLinks: [], + todayPresence: null, + teamPresence: null, + weeklyReport: null, + weeklySummary: null, + todoSummary: null, + projectSummary: null, + peopleLifecycle: null, + adoptionSummary: null, + recentActivities: [], + teamStatusBoard: [], + }); + return; + } + + if (pathName === "/api/master-data/legal-entities") { + await fulfillJson(route, { includeArchived: false, items: [{ id: "legal-r103", code: "HYD", name: "海音达", shortName: null, currencyCode: "CNY", taxNo: null, enabled: true, archivedAt: null, createdAt: null, updatedAt: null }] }); + return; + } + + if (pathName === "/api/master-data/cost-centers") { + await fulfillJson(route, { includeArchived: false, items: [{ id: "cost-r103", legalEntityId: "legal-r103", code: "RD", name: "研发部", parentId: null, ownerEmployeeId: null, enabled: true, archivedAt: null, createdAt: null, updatedAt: null }] }); + return; + } + + if (pathName === "/api/master-data/suppliers") { + await fulfillJson(route, { includeArchived: false, items: [{ id: "supplier-r103", code: "SUP", name: "测试供应商", taxNo: null, contactName: null, contactPhone: null, bankName: null, bankAccount: null, enabled: true, archivedAt: null, createdAt: null, updatedAt: null }] }); + return; + } + + if (pathName === "/api/master-data/document-categories") { + await fulfillJson(route, { includeArchived: false, items: [{ id: "category-r103", code: "CG", name: "采购申请", entityType: "purchase_request", sequencePrefix: "CG", numberLength: 4, resetPeriod: "daily", enabled: true, archivedAt: null, createdAt: null, updatedAt: null }] }); + return; + } + + if (pathName === "/api/purchase-requests" && request.method() === "GET") { + await fulfillJson(route, { page: 1, pageSize: 20, total: 1, mine: true, items: [purchaseSummary()] }); + return; + } + + if (pathName === `/api/purchase-requests/${purchaseId}`) { + await fulfillJson(route, purchaseDetail()); + return; + } + + if (pathName === `/api/purchase-requests/${purchaseId}/attachments` && request.method() === "POST") { + await fulfillJson(route, { + attachment: { + id: "attachment-r103", + originalName: "采购报价.pdf", + mimeType: "application/pdf", + fileSize: 4, + uploadedBy: me.id, + createdAt: "2026-05-06T09:20:00.000Z", + uploader: { id: me.id, employeeNo: me.employeeNo, name: me.name }, + }, + }); + return; + } + + if (pathName === "/api/invoice-pool") { + await fulfillJson(route, { page: 1, pageSize: 20, total: 1, ownerScope: "own", items: [invoiceItem()] }); + return; + } + + if (pathName === "/api/reimbursements") { + await fulfillJson(route, { page: 1, pageSize: 50, total: 1, scope: "own", items: [reimbursementForBind()] }); + return; + } + + await fulfillJson(route, {}); + }); +} + +async function login(page: Page) { + await page.goto("/login"); + await page.getByTestId("login-identifier").fill("employee@hyinda.com"); + await page.getByTestId("login-password").fill("EmployeePassword1234"); + await page.getByTestId("login-submit").click(); + await expect(page).toHaveURL(/\/portal/); +} + +test.describe("R103 cross page UI consistency", () => { + test.beforeEach(async ({ page }) => { + await mockApis(page); + await login(page); + }); + + test("采购页显示中文标题并使用中文附件上传控件", async ({ page }) => { + await page.setViewportSize({ width: 1440, height: 960 }); + await page.goto(`/purchase-requests?id=${purchaseId}`); + + await expect(page.locator("header").filter({ hasText: "备用模块" }).getByText("采购申请")).toBeVisible(); + await expect(page.getByRole("heading", { name: "采购申请" })).toBeVisible(); + await expect(page.getByText("/purchase-requests")).toHaveCount(0); + await expect(page.getByTestId("purchase-attachment-upload")).toHaveText("上传附件"); + await expect(page.getByText("Choose File")).toHaveCount(0); + + await page.getByTestId("purchase-attachment-file-input").setInputFiles({ + name: "采购报价.pdf", + mimeType: "application/pdf", + buffer: Buffer.from("r103"), + }); + await expect(page.getByTestId("purchase-attachment-selected-name")).toContainText("采购报价.pdf"); + await page.screenshot({ path: screenshotPath("purchase-desktop.png"), fullPage: true }); + }); + + test("票据中心绑定报销单不展示 UUID 兜底", async ({ page }) => { + await page.setViewportSize({ width: 1440, height: 960 }); + await page.goto("/invoice-pool"); + + await expect(page.locator("header").filter({ hasText: "备用模块" }).getByText("票据中心")).toBeVisible(); + await expect(page.getByTestId("invoice-bind-reimbursement")).toContainText("草稿单 · 2026-05-06"); + await expect(page.getByTestId("invoice-bind-reimbursement")).not.toContainText(reimbursementId); + await expect(page.getByText("/invoice-pool")).toHaveCount(0); + await page.screenshot({ path: screenshotPath("invoice-pool-desktop.png"), fullPage: true }); + }); + + test("移动宽度下关键页面仍保持浅色中文工作台风格", async ({ page }) => { + await page.setViewportSize({ width: 390, height: 844 }); + await page.goto("/invoice-pool"); + + await expect(page.getByRole("heading", { name: "票据中心" })).toBeVisible(); + await expect(page.getByTestId("invoice-pool-mobile-list")).toBeVisible(); + await expect(page.getByText("Choose File")).toHaveCount(0); + await page.screenshot({ path: screenshotPath("invoice-pool-mobile.png"), fullPage: true }); + }); +}); diff --git a/issues/2026-05-06_23-07-02-r103-cross-page-ui-consistency.csv b/issues/2026-05-06_23-07-02-r103-cross-page-ui-consistency.csv new file mode 100644 index 00000000..7e95bdee --- /dev/null +++ b/issues/2026-05-06_23-07-02-r103-cross-page-ui-consistency.csv @@ -0,0 +1,7 @@ +ID,Title,Description,Acceptance,Test_Method,Tools,Dev_Status,Review1_Status,Regression_Status,Files,Dependencies,Notes +A1,R103 跨页面设计一致性合同,"以 R100 设计规则和 R102 剩余诊断结果为输入,收口路由标题、采购上传控件、票据中心 UUID 展示和跨页面视觉验证。","本 CSV 校验通过;本批仅改前端展示和测试,不改后端 API、权限、状态机和数据结构。","python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-05-06_23-07-02-r103-cross-page-ui-consistency.csv;业务流程影响:固定跨页面 UI 一致性收尾范围。","python|manual",DONE,DONE,DONE,"issues/2026-05-06_23-07-02-r103-cross-page-ui-consistency.csv","[]","回滚路径:revert 本批提交即可;无 Prisma 迁移。" +A2,补齐导航中文标题,"为采购、差旅、预算、付款、应付、票据、盘点、系统切换、ERP 集成和 dashboard 路径补齐 PAGE_TITLES,避免顶栏显示路由路径。","node scripts/qa/check-ui-design-rules.mjs 不再报告 navigation.no_route_title_fallback;PC 顶栏显示中文业务标题。","node scripts/qa/check-ui-design-rules.mjs;cd client && npx playwright test tests/e2e/r103-cross-page-ui-consistency.spec.ts --reporter=list;业务流程:员工从导航进入采购/票据/付款等页面,顶部标题为中文。","node|playwright|typescript",DONE,DONE,DONE,"client/src/features/navigation/schema.ts","[""A1""]","只补展示映射,不重构导航 schema。" +A3,采购上传控件中文化,"采购申请页附件上传使用自定义中文按钮和文件名回显,不直接暴露浏览器原生文件输入。","静态诊断不再报告 PurchaseRequestsPage 原生上传控件;采购草稿页附件区域显示 上传附件 和已选文件名。","node scripts/qa/check-ui-design-rules.mjs;cd client && npm run typecheck;业务流程:采购申请草稿→选择附件→看到中文按钮和文件名回显。","node|typescript|playwright",DONE,DONE,DONE,"client/src/pages/PurchaseRequestsPage.tsx","[""A1""]","不改上传 API;原 input 保留为 visually hidden 触发器。" +A4,票据中心报销卡展示兜底,"票据中心绑定卡不再用报销 UUID 作为主展示;缺少业务编号时显示草稿单、报销单或日期摘要。","静态诊断不再报告 InvoiceBindCard 的 UUID 主展示;票据绑定列表主标题不展示 UUID。","node scripts/qa/check-ui-design-rules.mjs;cd client && npx playwright test tests/e2e/r103-cross-page-ui-consistency.spec.ts --reporter=list;业务流程:票据中心查看可绑定报销单,主标题为业务编号或摘要。","node|playwright|typescript",DONE,DONE,DONE,"client/src/pages/invoice-pool/InvoiceBindCard.tsx|client/src/lib/reimbursementDisplay.ts","[""A1""]","复用 R102 reimbursementDisplay,不新增重复兜底逻辑。" +A5,跨页面自动化视觉核验,"新增 R103 Playwright 用例,用 mock API 覆盖关键页面中文标题、票据绑定卡兜底和采购上传控件;保留桌面与移动视口截图用于回归定位。","R103 Playwright 通过;截图输出到 output/playwright/r103;页面不出现路由式标题、UUID 主展示或原生英文上传按钮。","cd client && npx playwright test tests/e2e/r103-cross-page-ui-consistency.spec.ts --reporter=list;业务流程:PC 访问 dashboard/采购/票据;移动视口访问采购或票据页面,关键文案和控件可见。","playwright",DONE,DONE,DONE,"client/tests/e2e/r103-cross-page-ui-consistency.spec.ts|output/playwright/r103","[""A2"",""A3"",""A4""]","测试使用 mock API,不依赖生产数据;截图目录不纳入 git。" +A6,验证回填与中文提交,"执行 CSV 校验、设计规则、前端 typecheck/build、R103 Playwright 和 git diff 检查,回填状态后中文提交。","A1-A5 状态 DONE;node scripts/qa/check-ui-design-rules.mjs 输出 0 findings;提交信息中文且引用 R103/A1-A6。","python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-05-06_23-07-02-r103-cross-page-ui-consistency.csv;node scripts/qa/check-ui-design-rules.mjs;cd client && npm run build;cd client && npx playwright test tests/e2e/r103-cross-page-ui-consistency.spec.ts --reporter=list;git diff --check;业务流程:跨页面 UI 一致性可构建可回归。","python|node|npm|playwright|git",DONE,DONE,DONE,"issues/2026-05-06_23-07-02-r103-cross-page-ui-consistency.csv|client","[""A1"",""A2"",""A3"",""A4"",""A5""]","若 build 仍有既有 Vite chunk warning,记录但不在本批处理。"