fix: 统一报销与付款详情整体宽度(09-04-reimbursement-state-gap-finance-view)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-09-12 08:51:31 +08:00
父节点 1eeda7cd92
当前提交 7bfc25461f
共修改 6 个文件,包含 36 行新增和 3 行删除
@@ -14,7 +14,7 @@ and verification contracts. Do not copy broad aesthetic guidance from
## Convention: Reimbursement Payment Handoff Consistency ## Convention: Reimbursement Payment Handoff Consistency
- Reimbursement desktop shells and payment detail opt into `detail-workspace-grid--payment`: a 320px aside at lg and above, single column below lg. A shared proportional grid does not guarantee equal asides when parent max-width differs. Verify actual widths on both pages at 1280/1440, not only matching class names; leave unrelated detail grids unchanged. - Reimbursement desktop shells and payment detail opt into `detail-workspace-grid--payment`: a 320px aside at lg and above, single column below lg. Both fill the same outer page width; do not add a reimbursement-only `max-w-7xl` or centered inner wrapper. Verify both workspace left/right boundaries, reimbursement hero alignment and 320px asides at 1280/1440/1920, plus narrow-screen overflow. Equal aside widths alone do not prove equal page widths; smaller viewports can hide a max-width defect. Leave unrelated detail grids unchanged.
- `BusinessDetailActionPanel({ inlineActions: true })` puts the primary action followed by the More trigger in a wrapping, content-width row. The default layout stays unchanged. Both detail pages use the existing DropdownMenu; menu items are portalled outside the card, so test via page-level menu roles. Keep disabled states, permission conditions, confirmations and mobile payment actions intact. Do not pass known-empty action components as children: a React element that renders null is still a nonempty slot and otherwise creates a phantom divider. - `BusinessDetailActionPanel({ inlineActions: true })` puts the primary action followed by the More trigger in a wrapping, content-width row. The default layout stays unchanged. Both detail pages use the existing DropdownMenu; menu items are portalled outside the card, so test via page-level menu roles. Keep disabled states, permission conditions, confirmations and mobile payment actions intact. Do not pass known-empty action components as children: a React element that renders null is still a nonempty slot and otherwise creates a phantom divider.
- Linked reimbursement invoices use a desktop table with number preview, seller, date and right-aligned amount; narrow rows stack without duplicating IDs or preview controls. Keep OCR warnings, unlinked correction cards, attachment delete confirmation and evidence anchors. Test five synthetic invoices, long names/numbers, empty attachments, preview calls, narrow layout and the existing approval/detail consumers. - Linked reimbursement invoices use a desktop table with number preview, seller, date and right-aligned amount; narrow rows stack without duplicating IDs or preview controls. Keep OCR warnings, unlinked correction cards, attachment delete confirmation and evidence anchors. Test five synthetic invoices, long names/numbers, empty attachments, preview calls, narrow layout and the existing approval/detail consumers.
- A compact payee-account reveal control stays beside the masked value as a 44px Eye button with title and aria-label `查看完整账号`. Preserve role/permission/status/account gates and the reason/audit dialog; icon clicks must not immediately request plaintext. Layout measurements wait for fonts and compare aligned centers; round subpixel box heights to hundredths before checking the 44px minimum (Chromium can return 43.99994 for a CSS 44px target). - A compact payee-account reveal control stays beside the masked value as a 44px Eye button with title and aria-label `查看完整账号`. Preserve role/permission/status/account gates and the reason/audit dialog; icon clicks must not immediately request plaintext. Layout measurements wait for fonts and compare aligned centers; round subpixel box heights to hundredths before checking the 44px minimum (Chromium can return 43.99994 for a CSS 44px target).
@@ -1,5 +1,9 @@
# 报销状态空隙与财务处理视角:设计 # 报销状态空隙与财务处理视角:设计
## 2026-09-12 修正整体宽度遗漏
此前仅统一侧栏,保留报销正文max-w-7xl而标题与付款正文未限宽;1920px时出现正文内缩。本次移除共享报销桌面壳的mx-auto/w-full/max-w-7xl,完全复用付款同款grid与外层PageScaffold。共享壳覆盖报销详情及审批工作台,不改全站容器或手机断点。测试除侧栏宽度外须比较两页grid左右边界及报销hero左右边界,不能只测1280/1440时尚未触发的最大宽度。
## 2026-09-11 等宽侧栏与票据表格 ## 2026-09-11 等宽侧栏与票据表格
动作卡通过可选inlineActions布局主动作和更多,默认合同不变。两页更多复用现有Radix DropdownMenu,Portal渲染后测试按page.getByRole("menuitem")定位,而不是动作卡内找按钮。业务回调和确认对话框不变;报销调用方仅在有撤回或动作错误时传ReimbursementActions,避免React组件返回null但外层误判为有内容而生成空分隔线。 动作卡通过可选inlineActions布局主动作和更多,默认合同不变。两页更多复用现有Radix DropdownMenu,Portal渲染后测试按page.getByRole("menuitem")定位,而不是动作卡内找按钮。业务回调和确认对话框不变;报销调用方仅在有撤回或动作错误时传ReimbursementActions,避免React组件返回null但外层误判为有内容而生成空分隔线。
@@ -1,5 +1,19 @@
# 报销状态空隙与财务处理视角:实施计划 # 报销状态空隙与财务处理视角:实施计划
## 2026-09-12 整体宽度修正
本地验证后用户明确同意复核并提交这六个文件、准备发布。本轮仅提交该增量,不推送、不部署;下文未提交状态为验证结束时的历史记录,实际提交标识以Git及本地发布准备回执为准。
- [x] 在旧构建复现1920px正文内缩并让整体边界断言失败。
- [x] 移除报销共享壳多余限宽;1280/1440/1920px两页整体对齐、320px侧栏、390px溢出与既有交互回归。
- [x] typecheck/build、定向浏览器与规约检查;仅本地实施,提交/推送/部署另批。
旧构建1920px测试退出1,报销grid与hero左边界差136px;本次不是只凭源码推断。运行时只移除共享壳的mx-auto/w-full/max-w-7xl,付款页、全局容器及业务逻辑未改。frontend-design不可用,按已批方案维护现有布局,无新视觉体系。
在本独立副本client执行npm run build(包含typecheck)退出0;本地preview配合合成mock API,执行 `npx playwright test tests/e2e/system-action-confirmation.spec.ts --grep '侧栏等宽|创建批次按钮短文案|票据紧凑展示|制单后返回报销|账号查看图标' --workers=1 --reporter=list`,12/12通过、退出0。覆盖1280/1440/1920两页grid左右边界、报销hero对齐、320px侧栏、主按钮/更多,以及390px票据/预览与手机交接。已视觉核看1920报销截图。构建、浏览器、UI strict、Trellis治理、process-contract日志位于artifacts/qa-layout/overall-width-*.log;各检查通过,git diff --check通过。
主会话复核通过,不冒充独立review。共享壳同时被审批适配器引用,其类型构建已验证;本轮未额外执行真实审批业务链或生产测试,截图仅合成数据。无后端/迁移/权限变化,未提交/推送/部署;下一步复核此六文件增量,获授权后提交发布。
## 2026-09-11 备注精简增量(本地验证完成,已获提交授权) ## 2026-09-11 备注精简增量(本地验证完成,已获提交授权)
用户随后明确同意复核、提交这六个文件并准备发布。本轮仅提交这一增量,不推送、不连接生产、不部署;实际提交标识以Git记录及本地发布准备回执为准。下文“未提交”描述的是本地验证结束时的历史阶段,不是提交完成后的状态。 用户随后明确同意复核、提交这六个文件并准备发布。本轮仅提交这一增量,不推送、不连接生产、不部署;实际提交标识以Git记录及本地发布准备回执为准。下文“未提交”描述的是本地验证结束时的历史阶段,不是提交完成后的状态。
@@ -2,6 +2,10 @@
## Goal ## Goal
### 2026-09-12 整体宽度统一(已批准本地实施)
报销详情标题与正文、付款详情使用同一外层宽度和边距,移除报销正文独有的1280px居中限制。正文自适应、桌面侧栏320px及手机单列不变。复用当前任务;验收1280/1440/1920px两页整体左右边界及报销标题/正文边界,390px无横向溢出。只维护现有布局,不引入新视觉方案;不改权限或业务动作,提交、部署另批。
### 2026-09-11 最新确认:制单备注只保留报销编号 ### 2026-09-11 最新确认:制单备注只保留报销编号
用户批准用途为“报销款”、备注仅完整报销编号;一张报销单对应一行银行转账,同一付款批次允许多张单,同人同账号不合并。本次复用当前任务承接,付款模板事实同步09-08任务,不新建task。只修改备注映射及回归,不改编号、权限、审批或生产数据;提交、推送和部署另批。 用户批准用途为“报销款”、备注仅完整报销编号;一张报销单对应一行银行转账,同一付款批次允许多张单,同人同账号不合并。本次复用当前任务承接,付款模板事实同步09-08任务,不新建task。只修改备注映射及回归,不改编号、权限、审批或生产数据;提交、推送和部署另批。
@@ -30,7 +30,7 @@ export function ReimbursementDesktopWorkspaceShell({ model, capabilities, slots
)} )}
/> />
{slots.transientFeedback} {slots.transientFeedback}
<BusinessDetailWorkspace className="detail-workspace-grid--payment mx-auto w-full max-w-7xl"> <BusinessDetailWorkspace className="detail-workspace-grid--payment">
<BusinessDetailMain> <BusinessDetailMain>
{slots.headerDetails} {slots.headerDetails}
{slots.beforeEvidence} {slots.beforeEvidence}
@@ -762,12 +762,13 @@ test.describe("报销付款交接一致性", () => {
}); });
} }
for (const width of [1280, 1440]) { for (const width of [1280, 1440, 1920]) {
test(`桌面${width}付款与报销侧栏等宽且更多紧随主按钮`, async ({ page }, testInfo) => { test(`桌面${width}付款与报销侧栏等宽且更多紧随主按钮`, async ({ page }, testInfo) => {
await page.setViewportSize({ width, height: 960 }); await page.setViewportSize({ width, height: 960 });
await mockSystemActionApis(page, { reimbursementStatus: "approved", reimbursementProgress: () => progress("reviewed") }); await mockSystemActionApis(page, { reimbursementStatus: "approved", reimbursementProgress: () => progress("reviewed") });
await login(page, user); await login(page, user);
const widths: number[] = []; const widths: number[] = [];
const boundaries: { left: number; right: number }[] = [];
for (const [url, panelId] of [ for (const [url, panelId] of [
[`/reimbursements/${reimbursementId}`, "reimbursement-action-panel"], [`/reimbursements/${reimbursementId}`, "reimbursement-action-panel"],
[`/payment-batches?id=${paymentBatchId}`, "payment-batch-action-panel"], [`/payment-batches?id=${paymentBatchId}`, "payment-batch-action-panel"],
@@ -776,6 +777,14 @@ test.describe("报销付款交接一致性", () => {
const panel = page.getByTestId(panelId); const panel = page.getByTestId(panelId);
await expect(panel).toBeVisible(); await expect(panel).toBeVisible();
await page.evaluate(() => document.fonts.ready); await page.evaluate(() => document.fonts.ready);
const grid = page.locator(".detail-workspace-grid--payment");
const gridBounds = (await grid.boundingBox())!;
boundaries.push({ left: gridBounds.x, right: gridBounds.x + gridBounds.width });
if (panelId === "reimbursement-action-panel") {
const heroBounds = (await page.getByTestId("reimbursement-desktop-workspace").locator(".workbench-hero").boundingBox())!;
expect(Math.abs(gridBounds.x - heroBounds.x)).toBeLessThan(2);
expect(Math.abs(gridBounds.x + gridBounds.width - heroBounds.x - heroBounds.width)).toBeLessThan(2);
}
widths.push((await panel.locator("../..").boundingBox())!.width); widths.push((await panel.locator("../..").boundingBox())!.width);
const row = panel.getByTestId("detail-inline-actions"); const row = panel.getByTestId("detail-inline-actions");
const primary = await row.locator("a, button").first().boundingBox(); const primary = await row.locator("a, button").first().boundingBox();
@@ -789,6 +798,8 @@ test.describe("报销付款交接一致性", () => {
await page.screenshot({ path: testInfo.outputPath(`${panelId}.png`), fullPage: true }); await page.screenshot({ path: testInfo.outputPath(`${panelId}.png`), fullPage: true });
} }
expect(widths).toEqual([320, 320]); expect(widths).toEqual([320, 320]);
expect(Math.abs(boundaries[0].left - boundaries[1].left)).toBeLessThan(2);
expect(Math.abs(boundaries[0].right - boundaries[1].right)).toBeLessThan(2);
}); });
test(`桌面${width}创建批次按钮短文案按内容宽度且只导航不组批`, async ({ page }, testInfo) => { test(`桌面${width}创建批次按钮短文案按内容宽度且只导航不组批`, async ({ page }, testInfo) => {