From 95ad9c939a83c9908bb222300eea465d0a01110d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=85=BE=E8=AE=AFWorkBuddy?= Date: Thu, 20 Aug 2026 01:54:32 +0800 Subject: [PATCH] =?UTF-8?q?B-44:=20=E8=BF=94=E4=BF=AE=E2=80=94=E2=80=94375?= =?UTF-8?q?px=20=E8=B6=85=E5=A4=A7=E9=87=91=E9=A2=9D=E9=99=8D=E7=BA=A7?= =?UTF-8?q?=E4=B8=8E=E5=AE=A1=E6=A0=B8=E5=BC=B9=E7=AA=97=E5=BF=85=E5=B1=95?= =?UTF-8?q?=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- tests/b44_ui_check.js | 41 ++++++++++++++++++ tests/test_b44_frontend.py | 12 +++++- web/app.js | 87 +++++++++++++++++++++++++++++++------- web/styles.css | 32 +++++++++----- 4 files changed, 146 insertions(+), 26 deletions(-) diff --git a/tests/b44_ui_check.js b/tests/b44_ui_check.js index dd658a6..b801e9a 100644 --- a/tests/b44_ui_check.js +++ b/tests/b44_ui_check.js @@ -38,6 +38,47 @@ assert.strictEqual(ui.resultDirection(10).label, "应收"); assert.strictEqual(ui.resultDirection(-10).label, "应付"); assert.strictEqual(ui.resultDirection(0).label, "持平"); +assert.strictEqual(ui.isCompactAmount(1280), false); +assert.strictEqual(ui.isCompactAmount(999999999), false); +assert.strictEqual(ui.isCompactAmount(1000000000), true); +assert.strictEqual(ui.isCompactAmount("123456789012345"), true); +assert.ok(ui.amountWithCurrency("123456789012345", "CNY").includes("is-compact")); +assert.ok(!ui.amountWithCurrency(1280, "CNY").includes("is-compact")); + +assert.strictEqual(ui.cashDirectionLabel("outgoing"), "转出"); +assert.strictEqual(ui.cashDirectionLabel("incoming"), "转入"); +assert.strictEqual(ui.relatedFlowLabel(null), "无关联流水"); +assert.strictEqual(ui.relatedFlowLabel(""), "无关联流水"); +assert.strictEqual(ui.relatedFlowLabel("42"), "42"); + +const subjectFields = ui.auditEvidenceFields("subject-review", { + direction: "outgoing", + effectiveAt: "2026-07-18T09:00:00", + amount: "600", + currency: "CNY", + summary: "资金调拨", +}); +assert.deepStrictEqual(subjectFields.map((item) => item[0]), ["方向", "日期", "金额", "摘要"]); +assert.strictEqual(subjectFields[0][1], "转出"); + +const manualFields = ui.auditEvidenceFields("manual-review", { + counterpartyName: "乙公司", + direction: "incoming", + relatedSourceRowId: "", + submittedBy: "出纳甲", + attachmentName: "", + amount: "80", + currency: "CNY", + summary: "补记", +}); +assert.deepStrictEqual( + manualFields.map((item) => item[0]), + ["对方公司名", "方向", "关联流水号", "提交人", "附件", "摘要", "金额"], +); +assert.strictEqual(manualFields[0][1], "乙公司"); +assert.strictEqual(manualFields[1][1], "转入"); +assert.strictEqual(manualFields[2][1], "无关联流水"); + assert.ok(ui.accountCell({ visibility: "visible", label: "中信 5316" }).includes("中信 5316")); assert.ok(ui.accountCell({ visibility: "masked" }).includes("按对方授权不可见")); assert.ok(ui.accountCell({ visibility: "missing" }).includes("源行缺失")); diff --git a/tests/test_b44_frontend.py b/tests/test_b44_frontend.py index 516b1cb..0eb2b9e 100644 --- a/tests/test_b44_frontend.py +++ b/tests/test_b44_frontend.py @@ -39,7 +39,6 @@ class B44FrontendContractTests(unittest.TestCase): self.assertIn("

往来余额

", self.company) def test_css_directory_and_drawer_breakpoints(self) -> None: - self.assertIn("@media (max-width: 768px)", self.css) self.assertIn("@media (max-width: 375px)", self.css) self.assertIn("@media (max-width: 1179px)", self.css) self.assertIn("@media (max-width: 767px)", self.css) @@ -52,6 +51,11 @@ class B44FrontendContractTests(unittest.TestCase): self.assertIn(".drawer .event-table { min-width: 860px; }", self.css) self.assertIn(".company-row.is-balance-counterparty", self.css) self.assertIn(".ledger-hide-md", self.css) + self.assertIn(".company-name b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }", self.css) + self.assertIn(".amount-with-currency.is-compact { font-size: 11px; }", self.css) + self.assertIn("minmax(88px, 1fr) minmax(0, max-content) 24px", self.css) + self.assertIn("grid-row: 1 / span 2", self.css) + self.assertIn("flex-direction: column; align-items: flex-end; gap: 2px;", self.css) def test_js_selectors_and_event_table(self) -> None: self.assertIn('loadAdminBalances($("#balanceLedgers"))', self.js) @@ -68,6 +72,12 @@ class B44FrontendContractTests(unittest.TestCase): self.assertIn("function eventIsNegative(", self.js) self.assertIn("fmtAbsMoney", self.js) self.assertIn("is-balance-counterparty", self.js) + self.assertIn("function isCompactAmount(", self.js) + self.assertIn("function auditEvidenceFields(", self.js) + self.assertIn("无关联流水", self.js) + self.assertIn('row.dataset.direction = "outgoing"', self.js) + self.assertIn("row.dataset.counterpartyName", self.js) + self.assertIn("row.dataset.relatedSourceRowId", self.js) heads = re.search( r"function eventTableHead\(\) \{\s*return `([^`]+)`", self.js, diff --git a/web/app.js b/web/app.js index bf27241..c24fb9c 100644 --- a/web/app.js +++ b/web/app.js @@ -522,11 +522,57 @@ function eventIsNegative(event) { return event.posting_kind === "reversal" || Boolean(event.is_repayment); } +function isCompactAmount(value) { + const num = Number(value); + if (!Number.isFinite(num)) return false; + return String(Math.trunc(Math.abs(num))).length >= 10; +} + function amountWithCurrency(value, currency, { signed = false, negative = false } = {}) { const abs = fmtAbsMoney(value); const sign = (signed && (negative || Number(value) < 0)) ? "−" : ""; const code = currency ? `${esc(currency)}` : ""; - return `${code}${sign}${abs}`; + const compact = isCompactAmount(value) ? " is-compact" : ""; + return `${code}${sign}${abs}`; +} + +function cashDirectionLabel(direction) { + if (direction === "outgoing") return "转出"; + if (direction === "incoming") return "转入"; + return "—"; +} + +function relatedFlowLabel(relatedId) { + if (relatedId === null || relatedId === undefined || String(relatedId).trim() === "") { + return "无关联流水"; + } + return String(relatedId); +} + +function auditEvidenceFields(kind, data = {}) { + const amountText = data.amount + ? `${fmtAbsMoney(data.amount)}${data.currency ? ` ${data.currency}` : ""}`.trim() + : "—"; + if (kind === "subject-review") { + return [ + ["方向", cashDirectionLabel(data.direction || "outgoing")], + ["日期", data.effectiveAt ? fmtDate(data.effectiveAt) : "—"], + ["金额", amountText], + ["摘要", data.summary || "—"], + ]; + } + if (kind === "manual-review") { + return [ + ["对方公司名", data.counterpartyName || "—"], + ["方向", cashDirectionLabel(data.direction)], + ["关联流水号", relatedFlowLabel(data.relatedSourceRowId)], + ["提交人", data.submittedBy || "—"], + ["附件", data.attachmentName || "无"], + ["摘要", data.summary || "—"], + ["金额", amountText], + ]; + } + return []; } function accountCell(chip) { @@ -1114,6 +1160,7 @@ function appendSubjectReviewRow(item) { row.dataset.amount = String(item.amount || ""); row.dataset.currency = item.currency || ""; row.dataset.effectiveAt = item.effective_at || ""; + row.dataset.direction = "outgoing"; row.innerHTML = ` ${esc(item.payer_company_name)} ↔ ${esc(item.payee_company_name)}规则建议:${esc(suggestions)} @@ -1140,6 +1187,9 @@ function appendManualRecordReviewRow(record) { row.dataset.summary = record.summary || ""; row.dataset.amount = String(record.amount || ""); row.dataset.currency = record.currency || ""; + row.dataset.direction = record.direction || ""; + row.dataset.counterpartyName = record.counterparty_company_name || ""; + row.dataset.relatedSourceRowId = record.related_source_row_id == null ? "" : String(record.related_source_row_id); row.innerHTML = ` ${esc(record.company_name)} · MR-${record.id}${esc(record.direction === "outgoing" ? "付款" : "收款")} ${fmtMoney(record.amount)} ${esc(record.currency)} · ${esc(record.counterparty_company_name)} · ${esc(b44.subjectLabels[record.requested_subject] || "")} @@ -1151,19 +1201,27 @@ function appendManualRecordReviewRow(record) { tbody.append(row); } +function appendEvidenceGrid(parent, fields) { + const grid = document.createElement("div"); + grid.className = "evidence-grid"; + fields.forEach(([label, value]) => { + const wrap = document.createElement("div"); + const dt = document.createElement("dt"); + dt.textContent = label; + const dd = document.createElement("dd"); + dd.textContent = value; + wrap.append(dt, dd); + grid.append(wrap); + }); + parent.append(grid); +} + function fillAuditEvidence(evidence, row, cells) { const heading = document.createElement("strong"); heading.textContent = cells[1].querySelector("strong")?.textContent || ""; evidence.append(heading); if (row.dataset.recordKind === "subject-review") { - const meta = document.createElement("small"); - meta.textContent = `${fmtDate(row.dataset.effectiveAt)} · ${fmtAbsMoney(row.dataset.amount)} ${row.dataset.currency || ""}`.trim(); - evidence.append(meta); - if (row.dataset.summary) { - const summary = document.createElement("small"); - summary.textContent = `摘要:${row.dataset.summary}`; - evidence.append(summary); - } + appendEvidenceGrid(evidence, auditEvidenceFields("subject-review", row.dataset)); const suggestions = JSON.parse(row.dataset.suggestions || "[]"); const list = document.createElement("ul"); list.className = "candidate-list"; @@ -1182,12 +1240,7 @@ function fillAuditEvidence(evidence, row, cells) { return; } if (row.dataset.recordKind === "manual-review") { - const meta = document.createElement("small"); - meta.textContent = `提交人 ${row.dataset.submittedBy || "—"} · 附件 ${row.dataset.attachmentName || "无"}`; - evidence.append(meta); - const summary = document.createElement("small"); - summary.textContent = `摘要:${row.dataset.summary || "—"} · ${fmtAbsMoney(row.dataset.amount)} ${row.dataset.currency || ""}`.trim(); - evidence.append(summary); + appendEvidenceGrid(evidence, auditEvidenceFields("manual-review", row.dataset)); const reason = document.createElement("p"); reason.className = "reason-full"; reason.textContent = row.dataset.reasonText || cells[1].querySelector("small")?.textContent || ""; @@ -2654,7 +2707,11 @@ if (typeof module !== "undefined" && module.exports) { cycleTab, drawerEscAction, amountWithCurrency, + isCompactAmount, resultDirection, accountCell, + cashDirectionLabel, + relatedFlowLabel, + auditEvidenceFields, }; } diff --git a/web/styles.css b/web/styles.css index 30224c3..8655feb 100644 --- a/web/styles.css +++ b/web/styles.css @@ -220,7 +220,7 @@ main { width: min(1560px, 100%); margin: 0 auto; padding: 12px 32px 58px; } .company-row.is-warning .company-row-mark { background: var(--color-warning-wash); color: var(--color-warning); } .company-row.is-danger .company-row-mark { background: var(--color-danger-wash); color: var(--color-danger); } .company-row-body { display: flex; flex-direction: column; min-width: 0; } -.company-row-body strong { font-size: 14px; } +.company-row-body strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 14px; } .company-row-body small { color: var(--color-ink-muted); font-size: 11px; } .company-row-figure { display: flex; flex-direction: column; align-items: flex-end; text-align: right; min-width: 92px; } .company-row-figure strong { font: 600 14px var(--font-data); color: var(--color-ink); } @@ -261,8 +261,9 @@ main { width: min(1560px, 100%); margin: 0 auto; padding: 12px 32px 58px; } .company-ledger summary:hover { background: rgba(255, 255, 255, 0.035); } .company-ledger summary > svg { transition: transform var(--duration-standard) var(--ease-out); } .company-ledger[open] summary > svg { transform: rotate(180deg); } -.company-name { display: grid; grid-template-columns: 38px minmax(0, 1fr); align-items: center; column-gap: 10px; } +.company-name { display: grid; grid-template-columns: 38px minmax(0, 1fr); align-items: center; column-gap: 10px; min-width: 0; } .company-name i { width: 36px; height: 36px; grid-row: 1 / 3; display: grid; place-items: center; border-radius: 12px; background: var(--color-primary-wash); color: var(--color-primary); font-style: normal; font-weight: 800; } +.company-name b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; } .company-name small { color: var(--color-ink-muted); } .amount { font: 600 13px var(--font-data); } .amount.debit { color: var(--color-positive); } @@ -744,6 +745,7 @@ body::before { content: ""; position: fixed; inset: 0; z-index: 0; pointer-event .amount-neutral { color: var(--color-ink-muted); } .currency-code { font-size: 10px; color: var(--color-ink-muted); margin-right: 4px; } .amount-with-currency { white-space: nowrap; font-variant-numeric: tabular-nums; } +.amount-with-currency.is-compact { font-size: 11px; } .account-cell { white-space: nowrap; } .summary-cell { max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .candidate-list { margin: 8px 0 0; padding: 0; list-style: none; color: var(--color-ink-soft); font-size: 12px; } @@ -756,12 +758,13 @@ body::before { content: ""; position: fixed; inset: 0; z-index: 0; pointer-event /* Balance directory: 公司 | 借方 | 贷方 | 期末结果 | 未决 | 截止日 | ▸ */ .ledger-head.is-balances, .company-ledger.is-balances summary { - grid-template-columns: minmax(180px, 1.3fr) minmax(100px, 0.7fr) minmax(100px, 0.7fr) minmax(150px, 0.9fr) minmax(110px, 0.7fr) minmax(96px, 0.55fr) 24px; + grid-template-columns: minmax(180px, 1.3fr) minmax(0, 0.7fr) minmax(0, 0.7fr) minmax(0, 0.9fr) minmax(0, 0.7fr) minmax(96px, 0.55fr) 24px; } .company-ledger.is-balances summary { min-height: 72px; } +.company-ledger.is-balances summary .company-name { min-width: 96px; } .company-ledger.is-balances summary .ledger-cutoff { color: var(--color-ink-muted); font-size: 11px; white-space: nowrap; } .company-ledger.is-balances .ledger-result { display: flex; align-items: center; gap: 7px; min-width: 0; } -.company-ledger.is-balances .ledger-result b { font: 600 14px var(--font-data); font-variant-numeric: tabular-nums; white-space: nowrap; } +.company-ledger.is-balances .ledger-result b { font: 600 14px var(--font-data); font-variant-numeric: tabular-nums; white-space: nowrap; min-width: 0; } .company-ledger.is-balances .ledger-unresolved { font-size: 11px; white-space: nowrap; } .company-ledger.is-balances .ledger-unresolved.is-empty { color: var(--color-ink-muted); } .company-ledger.is-balances .ledger-unresolved.is-active { color: var(--color-warning); } @@ -770,25 +773,32 @@ body::before { content: ""; position: fixed; inset: 0; z-index: 0; pointer-event @media (max-width: 900px) { .ledger-head.is-balances { display: grid; } } -@media (max-width: 768px) { +@media (max-width: 1179px) { .ledger-head.is-balances, .company-ledger.is-balances summary { - grid-template-columns: minmax(140px, 1.2fr) minmax(140px, 0.9fr) minmax(110px, 0.7fr) minmax(96px, 0.55fr) 24px; + grid-template-columns: minmax(120px, 1.2fr) minmax(0, 0.9fr) minmax(0, 0.7fr) minmax(96px, 0.55fr) 24px; } .ledger-head.is-balances .ledger-hide-md, .company-ledger.is-balances summary .ledger-hide-md { display: none; } + .company-ledger.is-balances summary .amount-with-currency .currency-code { display: none; } } @media (max-width: 375px) { .ledger-head.is-balances { display: none; } .company-ledger.is-balances summary { - grid-template-columns: minmax(0, 1fr) minmax(0, auto) 24px; + grid-template-columns: minmax(88px, 1fr) minmax(0, max-content) 24px; grid-template-rows: auto auto; align-items: center; column-gap: 10px; row-gap: 4px; } - .company-ledger.is-balances summary .company-name { grid-column: 1; grid-row: 1; } - .company-ledger.is-balances summary .ledger-result { grid-column: 2; grid-row: 1; justify-self: end; } - .company-ledger.is-balances summary .ledger-unresolved { grid-column: 1; grid-row: 2; } + .company-ledger.is-balances summary .company-name { grid-column: 1; grid-row: 1; min-width: 88px; } + .company-ledger.is-balances summary .ledger-result { + grid-column: 2; grid-row: 1; justify-self: end; + flex-direction: column; align-items: flex-end; gap: 2px; + } + .company-ledger.is-balances summary .ledger-result .currency-code { display: none; } + .company-ledger.is-balances summary .ledger-unresolved { + grid-column: 1; grid-row: 2; min-width: 0; white-space: normal; + } .company-ledger.is-balances summary .ledger-cutoff { grid-column: 2; grid-row: 2; justify-self: end; } .company-ledger.is-balances summary > svg { grid-column: 3; grid-row: 1 / span 2; align-self: center; } .company-ledger.is-balances summary .ledger-hide-md { display: none; } @@ -807,6 +817,7 @@ body::before { content: ""; position: fixed; inset: 0; z-index: 0; pointer-event .pair-balance-line.is-six div:nth-child(5), .pair-balance-line.is-six div:nth-child(6) { border-top: 1px solid var(--color-line); } } .amount-with-currency { font-variant-numeric: tabular-nums; white-space: nowrap; } +.amount-with-currency.is-compact { font-size: 11px; } .amount-with-currency .currency-code { margin-right: 4px; color: var(--color-ink-muted); font-size: 10px; font-weight: 500; } /* Evidence drawer: <768 full, 768–1179 480px, >=1180 640px */ @@ -825,6 +836,7 @@ body::before { content: ""; position: fixed; inset: 0; z-index: 0; pointer-event .drawer .pair-balance-line { border: 1px solid var(--color-line); border-radius: var(--radius-md); overflow: hidden; margin-bottom: 16px; } .drawer .pair-balance-line div { min-height: 64px; min-width: 0; overflow: hidden; } .drawer .pair-balance-line strong { overflow-wrap: anywhere; } +.drawer .pair-balance-line .amount-with-currency.is-compact { white-space: normal; overflow-wrap: anywhere; } .drawer .pair-balance-line .pair-cutoff, .drawer .pair-balance-line strong.amount-neutral { white-space: nowrap; } .drawer .pair-balance-line.is-six {