From 5816e8aa716303c474a1128bbedec27bee9bf461 Mon Sep 17 00:00:00 2001 From: leefer Date: Tue, 25 Aug 2026 18:35:41 +0800 Subject: [PATCH] =?UTF-8?q?baseline:=20=E6=B5=8B=E8=AF=95=E7=8E=AF?= =?UTF-8?q?=E5=A2=83=E7=8E=B0=E8=A1=8C=E6=A0=91=EF=BC=88b44=20=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=20+=20main=20web=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: multica-agent --- web/admin.html | 1211 ++++++++++++++--- web/app.js | 2890 ++++++++++++++++++---------------------- web/company.html | 1088 ++++++++++++--- web/design-system.css | 693 ++++++++++ web/index.html | 75 +- web/login-admin.html | 77 ++ web/login-company.html | 77 ++ web/login.js | 96 ++ web/styles.css | 936 ------------- 9 files changed, 4169 insertions(+), 2974 deletions(-) create mode 100644 web/design-system.css create mode 100644 web/login-admin.html create mode 100644 web/login-company.html create mode 100644 web/login.js delete mode 100644 web/styles.css diff --git a/web/admin.html b/web/admin.html index 3c84c21..1aa72b4 100644 --- a/web/admin.html +++ b/web/admin.html @@ -5,243 +5,1064 @@ 总账管理端 · 金牛集团 - + - -
+
-
-
+
+
+ 总账管理端 / 管理总览 +
+ 账期 2026-07 + 统计截止 2026-08-20 + +
+
+ +
-

管理总览

集团账务口径与高优先级事项

-
- -
- - - -
+
+
+

管理总览

+

2026-01-01 至 2026-08-20,集团 6 家成员公司相互往来累计发生 8.11 亿元、392 笔明细。页面上任意数字与公司均可逐级穿透,直至银行原始流水。

+
+
+
-
- -
-
-
待审核事项7
- -

2 项高风险 · 影响当前结账

-
-
-
流水异常账户3
- -

1 个已逾期 · 最长缺口 21 天

-
-
-
未完成提交公司2/ 6
- -

4 家已完成 · 截止 08.05

-
-
-
当前结账状态待处理
- -

2026 年 7 月 · 3 项条件未通过

-
-
- -
-
全局起算日2026.01.01
-
当前统计截止2026.07.31
-
每月结账日次月 5 日
-
2 家公司存在起算缺口最早缺口始于 2026.01.01
-
- -
-
-

优先处理

影响当前账期可靠性的事项

-
- - - -
-
- -
-
-

公司借贷目录

统计区间 2026.01.01—2026.07.31 · 单位:万元

-
公司借方合计贷方合计期末方向待审核
-
-
- AA公司4 个银行账户2,416.001,136.00应收1,280.001 笔 -
-

借方明细

2,416.00
-

贷方明细

1,136.00
-
-
-
BB公司3 个银行账户3,020.001,740.00应付1,280.003 笔

借方明细

3,020.00

贷方明细

1,740.00
-
CC公司2 个银行账户1,894.001,052.00应收842.000 笔

借方明细

1,894.00

贷方明细

1,052.00
+
+
+
往来借方总额 · 年初至今
+
42,040.30万元
+
6 家公司合计 · 明细 392 笔
-
+
+
往来贷方总额 · 年初至今
+
39,040.30万元
+
6 家公司合计 · 与借方同源互证
+
+
+
借贷轧差 · 未匹配
+
3,000.00万元
+
由单边流水与待确认往来形成,可在往来查询核对
+
+
+
7 月账期确认进度
+
50%
+
+
3 家已完成 · 2 家在途 · 1 家未提交
+
+
-
-

账期时间轴

起算日 2026.01.01 · 每月 5 日结上月账

1 处断档 · 7 月结账日 08.05
-
+ +
+
+
+ 待办队列12 项待处理 · 下方为影响结账的前 4 项,从第 1 项开始 + +
+
+ +
+
金牛置业 · 中行尾号 8821 账户 7 月流水断档 11 天
+
07-06 至 07-16 无流水,影响与金牛煤业 320 万元往来的归集
+
+ +
+
+ +
+
金牛农业 7 月全部账户未提交流水
+
已发 2 次系统提醒,最近一次 08-18,仍未响应
+
+ +
+
+ +
+
金牛新能源 ↔ 金牛贸易 3 笔单边流水待对方证据
+
合计 486 万元,需新能源侧选择对方银行流水佐证
+
+ +
+
+ +
+
5 笔手工记录待确认往来科目
+
应收 / 其他应收待判定,涉及煤业、物流、贸易
+
+ +
+
+ 其余 8 项为低风险与已挂起事项 + +
+
+ +
+
+ 账期与结账每月 5 日结账 · 全局起算日 2026-01-01 +
+
+
当前账期
2026-07 · 进行中
+
结账日
2026-08-29(顺延)· 距今 9 天
+
上一账期
2026-06 · 已于 07-03 结账
+
+ +
+
+
-

往来查询

公司间往来余额目录、公司对明细与逐层追溯

- 期初不可用 · 仅显示期间净变动 -
-
-
- - - - -
-
-
-

公司余额目录

每行余额都附带截止日、期初状态、本期借贷、结果与未决金额

-
公司借方合计贷方合计期末结果未决截止日
-
-
+
+
+

往来查询

+

按公司组合查询起算日 2026-01-01 起至统计截止日的逐笔往来余额,支持穿透至银行原始流水链路。

+
+
+ +
+
+ + +
+ +
+ + + +
+
+ + +
+ +
+ + + +
+
+ · 往来余额 + 统计口径 2026.01.01—2026.07.31 +
+ +
+
+
期初余额(2026-01-01)
+
0.00
+
起算日前结转
+
+
+
本期借方发生
+
0.00
+
应收增加 + 应付偿付
+
+
+
本期贷方发生
+
0.00
+
收回款项 + 应付计提
+
+
+
期末结果
+
+
含 1 笔待审核
+
+
+ +
+
+ + + + + +
+
+ + + + + + + + + + + + + + + +
交易日期方向科目本方账户对方账户摘要匹配金额(万元)操作
+
+
+ 点击「穿透」查看银行原始流水链路 +
+
+
-

审核中心

统一处理核算异常、账户登记与公司手工记录

-
-
风险公司 / 事项类型涉及期间影响状态操作
A公司 · 工商银行 9481上期截止 06.30,下期始于 07.22流水断档07.01—07.217 月结账已逾期
B公司 ↔ D公司金额与日期存在 2 个候选单边匹配07.18312.00 万元待审核
E公司 ↔ F公司规则无法区分应收与其他应收科目待确认07.0988.00 万元待确认
F公司 · 农业银行 3650公司已提交无业务说明起算校准01.01—01.16期初口径待复核
A公司 · MR-DEMO-001付款 80,000.00 元 · B公司 · 其他应收手工记录2026.07.29待确认往来 8.00 万元待复核
+
+
+

审核中心

+

集中处理流水断档、单边匹配、科目确认等待审事项。审核结论将同步至结账检查与各公司往来台账。

+
+ +
+ +
+ +
+
当前 0 项待审核
+
全部待审事项须在顺延后的结账日前处理完毕,逾期将阻断 7 月结账。
+
+
+ +
+
+ + + + + + + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
风险等级公司 / 事项类型期间影响状态操作
金牛置业 · 中行尾号 882107-06 至 07-16 无流水流水断档2026-0711 天无流水,与金牛煤业 320 万煤炭采购款往来无法归集,阻断 7 月结账待审核
金牛新能源 ↔ 金牛贸易3 笔单边合计 486 万单边匹配2026-07单边支付无对方对应流水,往来差异挂账,待对方选择流水佐证待审核
金牛煤业 · 手工记录场地押金 42 万科目确认2026-07「应收 / 其他应收」科目待判定,影响科目汇总口径待审核
金牛物流 · 工行基本户期初与上年结转差 28.6 万起算区间校准2026-01 起算期初余额与上年结转不衔接,全年累计数失真,需校准起算区间待审核
金牛贸易 · 手工补录现金缴存 64 万公司手工记录2026-07无银行回单附件,需人工核实后方可入账待审核
+
+ 共 0 项 · 待审核 0 项 + 审核人:系统管理员 · 操作实时写入审核日志 +
+
-

流水管理

查询全部公司的银行原始流水与归集状态

-
-
当前结果 4导出保留银行、账户、流水号、批次与源行定位
日期公司 / 银行账户方向对方户名摘要银行流水号归集状态金额
2026.07.18A公司中信银行 · 5316转出B公司往来款CIT260718018双边匹配10,000,000.00
2026.07.18B公司工商银行 · 9481转入A公司往来款ICB260718109双边匹配10,000,000.00
2026.07.12A公司建设银行 · 0845转出A公司同名账户调拨CCB260712031同公司调拨2,000,000.00
2026.07.09C公司建设银行 · 6120转出E公司资金调拨CCB260709088单边待核880,000.00
+
+
+

流水管理

+

六家成员公司上报的银行原始流水总表,含归集状态。点击任意一行可穿透查看原始流水要素与归集链路。

+
+
+ +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ +
+ + + + + + + + + + + + + + + +
日期公司银行账户方向对方摘要银行流水号归集状态金额
+
+ 共 0 笔 + 数据范围:2026-01-01 起算 · 每月 5 日结账(7 月顺延至 08-29) +
+
-

公司与账号

维护公司主档、银行账户和一对一公司账号

-
公司全称统一社会信用代码银行账户公司账号出纳人员状态操作
+
+
+

公司与账号

+

集团成员公司的档案、银行账户与登录账号管理。公司新登记的银行账户须经审核中心确认后方可参与流水归集。

+
+
+ +
+
+ + + +
+
+ 成员公司数据截至 2026-08-20 +
+
+ + + + + + + + + + + + +
公司名称银行账户数登录账号出纳启用状态操作
+
+
+ 共 0 家公司 + 登录账号初始密码由集团总账统一发放 +
+
-

结账与期初

统一管理计算窗口、月结规则和公司期初余额

-
-

系统计算口径

修改后影响全部公司

-

公司期初余额

按公司对录入,修改保留审计记录

本方对方科目方向金额(万元)生效日状态
A公司B公司应收借方0.002026.01.01已确认
C公司E公司其他应收借方120.002026.01.01待复核
-

月度结账

2026 年 7 月 · 当前未达到结账条件

已阻断
公司流水提交5 / 6 家已完成
账户连续性2 个账户存在断档
审核事项7 项尚未处理
期初余额已锁定 2026.01.01
最近结账:2026 年 6 月 · 系统管理员 · 2026.07.05 18:10
-
+
+
+

结账与期初

+

维护全局结账参数与各公司期初往来余额,并在月度检查全部通过后执行结账。

+
+
+ +
+
+ 账期时间轴全局起算日 2026-01-01 起,每月 5 日结账 +
+
+
2026-01
已结账
+
2026-02
已结账
+
2026-03
已结账
+
2026-04
已结账
+
2026-05
已结账
+
2026-06
已结账
+
2026-07
进行中
+
2026-08
归集中
+
+
+ +
+
+
+
+ 结账设置对全部成员公司生效 +
+
+
+ + + 期初余额以此日前一日的期末数为准 +
+
+ + + 每月几号执行上月结账 · 2026-07 因故顺延至 08-29 +
+
+ +
+ + +
+ 提醒对象为当月尚未提交流水的公司联系人 +
+
+ + +
+
+
+ +
+
+ 期初余额管理2026-01-01 起算的公司间往来期初数 + +
+
+ + + + + +
本方公司对方公司科目方向金额(万元)生效日状态
+
+
+
+ +
+
+ 月度结账2026 年 7 月 · 当前未达到结账条件 + 已阻断 +
+
+
+ 公司流水提交 +
流水提交
5 / 6 家已完成
+
待处理
+
+
+ 账户连续性 +
账户连续
2 个账户存在断档
+
阻断
+
+
+ 审核事项 +
审核完成
0 项尚未处理
+
阻断
+
+
+ 期初余额 +
期初锁定
已锁定 2026.01.01
+
已通过
+
+
+ + + + +
+ 最近结账:2026 年 6 月 · 系统管理员 · 2026.07.05 18:10 + + +
+
+
-

提醒管理

查看自动提醒,并向公司出纳发送站内提醒

-
-

发送手动提醒

当前仅支持系统内通知

-

提醒记录

自动与手动提醒统一留痕

A公司 7 月流水缺失自动提醒 · 截止 08.05 · 已逾期未处理
B公司单边流水待核验手动提醒 · 管理员 · 今天 09:30处理中
C公司 7 月流水已确认自动提醒 · 昨天 16:42已完成
+
+
+

提醒管理

+

向成员公司发送处理提醒,并跟踪系统提醒与人工提醒的触达与处理状态。

+
+
+ +
+
+
+ 发送处理提醒将以人工提醒形式送达所选公司出纳与财务负责人 +
+
+
+ +
+ +
+
+ + +
+
+ + + +
+
+ + +
+ + +
+
+ +
+
+ 提醒历史含系统自动触发与人工发送的全部提醒记录 +
+
+ + + +
+
+ + + + + + + + + + + + + +
公司类型内容摘要发送时间截止日期处理状态操作
+
+
+ 共 0 条提醒记录 + 未读 0 · 处理中 0 · 已完成 0 +
+
- -
-

新增公司

公司主档与公司账号可一次创建

-

创建后生成随机初始密码,仅显示一次,首次登录必须修改。

-
-
-
- -
-

新增期初余额

按本方视角记录公司间期初方向

-

提交后进入复核,不能直接修改已确认期初。

-
-
-
- -
-

审核事项

原始证据与处理决定将一并留痕

-
-
-
-
- -
-

执行 2026 年 7 月结账

结账后本期结果将锁定,补录流水需走重开流程

-
结账检查已通过公司提交、账户连续性、审核事项和期初余额均符合当前口径。
-
-
-
- - + +
+
+ + +
+
+ + +
+ +
+
+ + +
+
+ + +
+
+

创建后生成随机初始密码,仅显示一次,首次登录必须修改。

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
- + + diff --git a/web/app.js b/web/app.js index c24fb9c..eca6e74 100644 --- a/web/app.js +++ b/web/app.js @@ -1,18 +1,10 @@ -const $ = (selector, scope) => { - const root = scope ?? (typeof document !== "undefined" ? document : null); - return root ? root.querySelector(selector) : null; -}; -const $$ = (selector, scope) => { - const root = scope ?? (typeof document !== "undefined" ? document : null); - return root ? [...root.querySelectorAll(selector)] : []; -}; +const $ = (selector, scope = document) => scope.querySelector(selector); +const $$ = (selector, scope = document) => [...scope.querySelectorAll(selector)]; -const portal = (typeof document !== "undefined" && document.body) - ? (document.body.dataset.portal || "entry") - : "entry"; +const portal = document.body.dataset.portal || "entry"; const viewNames = portal === "admin" ? { dashboard: "管理总览", pair: "往来查询", audit: "审核中心", flows: "流水管理", companies: "公司与账号", settings: "结账与期初", reminders: "提醒管理" } - : { workspace: "工作台", upload: "流水导入", manual: "手工记录", flows: "流水管理", balances: "往来余额", reconcile: "往来确认", accounts: "银行账户", notifications: "通知" }; + : { workspace: "工作台", upload: "流水导入", manual: "手工记录", flows: "流水管理", reconcile: "往来确认", accounts: "银行账户", notifications: "通知" }; const storageKeys = { manual: "ledger-demo-manual-records", @@ -31,9 +23,7 @@ const state = { parseResult: null, }; -const motionQuery = (typeof window !== "undefined" && window.matchMedia) - ? window.matchMedia("(prefers-reduced-motion: reduce)") - : { matches: true, addEventListener() {}, removeEventListener() {} }; +const motionQuery = window.matchMedia("(prefers-reduced-motion: reduce)"); function animateView(view, { initial = false } = {}) { if (!view || motionQuery.matches || typeof view.animate !== "function") return; @@ -46,35 +36,21 @@ function animateView(view, { initial = false } = {}) { return; } const selectors = [ - ".page-heading > *", - ".metric-card", - ".period-ribbon", - ".company-alert", - ".admin-dashboard-grid > *", - ".company-dashboard-grid > *", - ".company-ledger-panel", - ".query-band", - ".filter-bar", - ".filter-grid", - ".pair-report", - ".panel", - ".work-progress", - ".reconcile-summary", - ".account-directory > article", + ".page-head", + ".stat-card", + ".card", + ".notice", + ".list-row", + ".filters", ]; - const elements = [...new Set(selectors.flatMap((selector) => [...view.querySelectorAll(selector)]))] - .filter((element) => !element.closest(".panel") || element.matches(".panel")); + const elements = [...new Set(selectors.flatMap((selector) => [...view.querySelectorAll(selector)]))]; elements.forEach((element, index) => { element.getAnimations().forEach((animation) => animation.cancel()); - // metric-card 的 3D 倾斜与悬停倾斜由 CSS 控制,入场动画只做淡入, - // 否则 fill:both 的 translateY(0) 会覆盖 CSS 的 transform,导致倾斜失效。 - const keyframes = element.classList.contains("metric-card") - ? [{ opacity: 0 }, { opacity: 1 }] - : [ - { opacity: 0, transform: `translateY(${initial ? 16 : 10}px)` }, - { opacity: 1, transform: "translateY(0)" }, - ]; + const keyframes = [ + { opacity: 0, transform: `translateY(${initial ? 16 : 10}px)` }, + { opacity: 1, transform: "translateY(0)" }, + ]; element.animate(keyframes, { duration: 440, delay: Math.min(index * 38, 260), @@ -88,22 +64,12 @@ function initMotion() { if (motionQuery.matches) return; animateView($(".app-view.is-active"), { initial: true }); - $$(".nav-item", $("#sidebar") || document).forEach((item, index) => { + $$(".side-nav a", $("#sidebar") || document).forEach((item, index) => { item.animate( [{ opacity: 0, transform: "translateX(-8px)" }, { opacity: 1, transform: "translateX(0)" }], { duration: 360, delay: 90 + index * 28, easing: "cubic-bezier(.22,1,.36,1)", fill: "both" }, ); }); - - $$(".company-ledger").forEach((ledger) => { - ledger.addEventListener("toggle", () => { - if (!ledger.open) return; - $(".ledger-breakdown", ledger)?.animate( - [{ opacity: 0, transform: "translateY(-8px)" }, { opacity: 1, transform: "translateY(0)" }], - { duration: 260, easing: "cubic-bezier(.22,1,.36,1)" }, - ); - }); - }); } function readStoredRecords(key) { @@ -120,7 +86,7 @@ function writeStoredRecords(key, records) { localStorage.setItem(key, JSON.stringify(records)); return true; } catch { - showToast("本机演示数据保存失败", "请检查浏览器是否允许本地存储"); + showToast("本机演示数据保存失败", "请检查浏览器是否允许本地存储", "danger"); return false; } } @@ -136,6 +102,10 @@ function accountStatusLabel(status) { return accountStatusLabels[status] || "待复核"; } +function pillClass(statusClass) { + return { success: "pill-success", danger: "pill-danger", warning: "pill-warn", neutral: "pill-muted", info: "pill-info" }[statusClass] || "pill-muted"; +} + function accountTail(masked) { return String(masked || "").replace(/^\*+/, ""); } @@ -144,21 +114,43 @@ function formatCurrency(value) { return Number(value).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } -function showToast(title, detail = "") { +function showTableLoading(tbody, cols = 6) { + if (!tbody) return; + tbody.innerHTML = `加载中…`; +} + +function showTableError(tbody, cols = 6) { + if (!tbody) return; + tbody.innerHTML = `加载失败,请稍后重试`; +} + +function showToast(title, detail = "", kind = "info") { const region = $("#toastRegion"); if (!region) return; const toast = document.createElement("div"); - toast.className = "toast"; - const heading = document.createElement("strong"); + toast.className = `toast ${["success", "warn", "danger", "info"].includes(kind) ? kind : "info"}`; + toast.setAttribute("role", "status"); + const dot = document.createElement("span"); + dot.className = "t-dot"; + const body = document.createElement("div"); + body.className = "t-body"; + const heading = document.createElement("div"); + heading.className = "t-title"; heading.textContent = title; - toast.append(heading); + body.append(heading); if (detail) { - const description = document.createElement("small"); + const description = document.createElement("div"); + description.className = "t-detail"; description.textContent = detail; - toast.append(description); + body.append(description); } + toast.append(dot, body); region.append(toast); - window.setTimeout(() => toast.remove(), 3400); + window.setTimeout(() => { + toast.style.opacity = "0"; + toast.style.transition = "opacity 0.2s ease"; + window.setTimeout(() => toast.remove(), 200); + }, 3400); } function closeNavigation({ restoreFocus = false } = {}) { @@ -178,9 +170,9 @@ function showView(view) { const navigationWasOpen = $("#sidebar")?.classList.contains("is-open"); state.currentView = view; $$(".app-view").forEach((page) => page.classList.toggle("is-active", page.dataset.page === view)); - $$(".nav-item[data-view]").forEach((item) => { + $$(".side-nav a[data-view]").forEach((item) => { const active = item.dataset.view === view; - item.classList.toggle("is-active", active); + item.classList.toggle("active", active); if (active) item.setAttribute("aria-current", "page"); else item.removeAttribute("aria-current"); }); @@ -192,28 +184,27 @@ function showView(view) { window.scrollTo({ top: 0, behavior: motionQuery.matches ? "auto" : "smooth" }); } +// 原型占位,非本司真待办/真断档数据:detailContent 仅用于演示总览/工作台事项 +// 点开抽屉的静态文案,真实待办与断档来自审核中心 / 往来确认等接口数据。 const detailContent = { - "gap-a": { tag: ["danger", "高风险"], title: "A公司 · 工行账户断档", desc: "工商银行 9481 缺少 07.01—07.21 流水,已影响 7 月结账。", fields: [["公司", "A公司"], ["账户", "工商银行 · 9481"], ["缺口期间", "2026.07.01—07.21 · 21 天"], ["影响", "7 月结账 · 账户覆盖 · 双边匹配"], ["当前状态", "已逾期 2 天"]], tip: "建议先向 A公司出纳发送补传提醒,补齐后在审核中心复核覆盖区间。", action: ["去审核中心处理", "audit"] }, - "match-bd": { tag: ["warning", "中风险"], title: "B公司 ↔ D公司 · 单边待匹配", desc: "D公司侧流水已到,B公司侧尚未确认,合计 312.00 万元。", fields: [["本方", "B公司"], ["对方", "D公司"], ["笔数 / 金额", "6 笔 · 312.00 万元"], ["候选情况", "金额与日期存在 2 个候选"], ["当前状态", "今日新增"]], tip: "建议按账号优先核对候选流水,金额与日期相同者先确认。", action: ["去审核中心匹配", "audit"] }, - "calib-f": { tag: ["warning", "中风险"], title: "F公司 · 起算区间待校准", desc: "01.01—01.16 无银行流水覆盖,公司已提交无业务说明。", fields: [["公司", "F公司"], ["账户", "农业银行 · 3650"], ["无覆盖期间", "2026.01.01—01.16"], ["现有依据", "公司已提交无业务说明"], ["当前状态", "待公司确认"]], tip: "无业务说明属于审计证据,复核通过后该区间标记为已校准,不生成银行流水。", action: ["去审核中心复核", "audit"] }, - "task-upload": { tag: ["danger", "最紧急"], title: "补传工商银行流水", desc: "账户尾号 9481 缺少 07.01—07.21 流水,已逾期 2 天。", fields: [["账户", "工商银行 · 9481"], ["缺少期间", "2026.07.01—07.21 · 21 天"], ["影响", "7 月账户覆盖与双边匹配"], ["截止", "08.05 集团结账日前"]], tip: "从工商银行网银导出 7 月流水后直接上传,系统会自动识别表头并重新计算匹配。", action: ["去上传流水", "upload"] }, - "task-match": { tag: ["warning", "待确认"], title: "确认 1 笔单边流水", desc: "07.18 转出 280.00 万元,系统找到 2 个对方候选。", fields: [["对方", "B公司"], ["日期 / 金额", "07.18 · 280.00 万元"], ["候选", "工商银行 9481(推荐)· 建设银行 2046"], ["核对点", "摘要与账号是否一致"]], tip: "系统推荐账号一致的候选,请核对回单后再确认。", action: ["去往来确认", "reconcile"] }, - "task-subject": { tag: ["warning", "待确认"], title: "确认往来科目", desc: "06.27 转出 600.00 万元,规则无法区分应收与其他应收。", fields: [["对方", "C公司"], ["日期 / 金额", "06.27 · 600.00 万元"], ["待确认", "应收 或 其他应收"], ["摘要", "资金调拨"]], tip: "科目只按确定性规则建议,拿不准时选“其他应收”并在说明里注明依据。", action: ["去确认科目", "reconcile"] }, - "task-notice": { tag: ["neutral", "提醒"], title: "阅读总账提醒", desc: "管理员要求 08.08 前完成 7 月银行流水确认。", fields: [["来自", "系统管理员 · 今天 09:30"], ["处理期限", "2026.08.08"], ["关联事项", "断档补传 · 2 项待确认往来"]], tip: "完成补传和两项确认后,再提交公司确认即可。", action: ["查看通知", "notifications"] }, - "acct-citic": { tag: ["success", "连续"], title: "中信银行 · 5316", desc: "基本户 · 起算日以来流水全部连续。", fields: [["账户类型", "基本户"], ["启用日期", "2026.01.01"], ["流水覆盖", "01.01—07.31 · 7 个月连续"], ["最近导入", "今天 09:42"]], tip: "该账户无需处理,8 月流水到期后正常上传即可。", action: ["查看银行账户", "accounts"] }, - "acct-abc": { tag: ["success", "连续"], title: "农业银行 · 3650", desc: "一般户 · 起算日以来流水全部连续。", fields: [["账户类型", "一般户"], ["启用日期", "2026.01.01"], ["流水覆盖", "01.01—07.31 · 7 个月连续"], ["最近导入", "08.01 08:01"]], tip: "该账户无需处理,8 月流水到期后正常上传即可。", action: ["查看银行账户", "accounts"] }, - "acct-icbc": { tag: ["danger", "断档"], title: "工商银行 · 9481", desc: "一般户 · 缺少 07.01—07.21 流水,已逾期 2 天。", fields: [["账户类型", "一般户"], ["缺口期间", "2026.07.01—07.21 · 21 天"], ["影响", "7 月结账与双边匹配"], ["最近导入", "07.31 16:18(6 月批次)"]], tip: "这是最紧急的一项:补齐后系统会自动重算覆盖与匹配。", action: ["去补传流水", "upload"] }, - "acct-ccb": { tag: ["success", "连续"], title: "建设银行 · 0845", desc: "一般户 · 起算日以来流水全部连续。", fields: [["账户类型", "一般户"], ["启用日期", "2026.01.01"], ["流水覆盖", "01.01—07.31 · 7 个月连续"], ["最近导入", "08.01 10:03"]], tip: "该账户无需处理,8 月流水到期后正常上传即可。", action: ["查看银行账户", "accounts"] }, + "admin-gap": { tag: ["danger", "高风险"], title: "金牛置业 · 中行账户断档", desc: "中国银行尾号 8821 缺少 07-06 至 07-16 流水,已影响 7 月结账。", fields: [["公司", "金牛置业"], ["账户", "中国银行 · 尾号 8821"], ["缺口期间", "2026-07-06 — 07-16 · 11 天"], ["影响", "7 月结账 · 与金牛煤业 320 万往来无法归集"], ["当前状态", "阻断结账"]], tip: "先向金牛置业出纳发送补传提醒,补齐流水后到审核中心复核覆盖区间。", action: ["去审核中心处理", "audit"] }, + "admin-unsubmitted": { tag: ["danger", "高风险"], title: "金牛农业 7 月未提交流水", desc: "全部账户 7 月流水均未提交,已发 2 次系统提醒。", fields: [["公司", "金牛农业"], ["账户", "全部账户"], ["已提醒", "2 次 · 最近一次 08-18"], ["当前状态", "未响应"]], tip: "可再次发送提醒,或在提醒管理里查看已发出的提醒记录。", action: ["去提醒管理", "reminders"] }, + "admin-unilateral": { tag: ["warning", "中风险"], title: "金牛新能源 ↔ 金牛贸易 单边流水", desc: "3 笔单边流水合计 486 万元,待对方选择银行流水佐证。", fields: [["本方", "金牛新能源"], ["对方", "金牛贸易"], ["笔数 / 金额", "3 笔 · 486 万元"], ["当前状态", "待对方证据"]], tip: "提醒新能源侧在往来确认中选择对方银行流水佐证后提交。", action: ["去审核中心匹配", "audit"] }, + "admin-subject": { tag: ["warning", "中风险"], title: "手工记录待确认往来科目", desc: "5 笔手工记录应收 / 其他应收待判定,涉及煤业、物流、贸易。", fields: [["待确认", "应收 / 其他应收"], ["涉及公司", "煤业、物流、贸易"], ["笔数", "5 笔"]], tip: "科目只按确定性规则建议,拿不准时选「其他应收」并注明依据。", action: ["去审核中心复核", "audit"] }, + "task-match": { tag: ["danger", "阻断"], title: "确认 3 笔单边流水", desc: "选择对方银行流水作为证据后提交确认。", fields: [["笔数", "3 笔"], ["处理方式", "选择对方银行流水佐证"], ["关联", "7 月结账阻断项"]], tip: "系统推荐账号一致的候选,请核对回单后再确认。", action: ["去往来确认", "reconcile"] }, + "task-subject": { tag: ["danger", "阻断"], title: "确认 2 笔其他应收科目", desc: "核对后改判或维持原科目。", fields: [["笔数", "2 笔"], ["处理方式", "改判或维持原科目"], ["关联", "7 月结账阻断项"]], tip: "科目只按确定性规则建议,拿不准时选「其他应收」并注明依据。", action: ["去手工记录", "manual"] }, + "task-upload": { tag: ["danger", "阻断"], title: "补传交行 7710 账户流水", desc: "账户已登记,待总行审核通过后即可导入。", fields: [["账户", "交通银行 · 尾号 7710"], ["状态", "待审核"], ["处理", "审核通过后上传 7 月流水"]], tip: "账户审核通过后,从交行网银导出 7 月流水直接上传。", action: ["去查看账户", "accounts"] }, + "task-reconcile": { tag: ["muted", "一般"], title: "核对与金牛置业 320 万往来", desc: "置业 8821 账户 7 月流水断档,需人工核对。", fields: [["对方", "金牛置业"], ["金额", "320 万元"], ["原因", "置业 8821 账户流水断档"]], tip: "先核对双方流水日期与金额是否一致,再决定是否提交确认。", action: ["去流水管理", "flows"] }, }; function initDetailDrawer() { const triggers = $$("[data-detail]"); if (!triggers.length) return; const drawer = document.createElement("aside"); - drawer.className = "detail-drawer"; + drawer.className = "drawer"; drawer.id = "detailDrawer"; drawer.setAttribute("aria-label", "事项详情"); - drawer.innerHTML = `

`; + drawer.innerHTML = `

`; document.body.append(drawer); let lastTrigger = null; @@ -227,17 +218,15 @@ function initDetailDrawer() { if (!item) return; lastTrigger = trigger; const tag = $("#detailTag", drawer); - tag.className = `status ${item.tag[0]}`; + tag.className = `pill ${pillClass(item.tag[0])}`; tag.textContent = item.tag[1]; $("#detailTitle", drawer).textContent = item.title; $("#detailDesc", drawer).textContent = item.desc; const fields = $("#detailFields", drawer); - fields.replaceChildren(...item.fields.map(([label, value]) => { - const row = document.createElement("div"); + fields.replaceChildren(...item.fields.flatMap(([label, value]) => { const dt = document.createElement("dt"); dt.textContent = label; const dd = document.createElement("dd"); dd.textContent = value; - row.append(dt, dd); - return row; + return [dt, dd]; })); const tip = $("#detailTip", drawer); tip.replaceChildren(); @@ -283,10 +272,16 @@ function initEntry() { const form = $("#loginForm"); if (!form) return; if (!motionQuery.matches) { - [$(".entry-brand"), $(".entry-statement"), ...$$(".entry-facts > div"), form].filter(Boolean).forEach((element, index) => { + [ + [$(".entry-form-wrap"), 0], + [$(".entry-statement h1"), 60], + [$(".entry-statement p"), 120], + [$(".entry-figure"), 120], + ].forEach(([element, delay]) => { + if (!element) return; element.animate( - [{ opacity: 0, transform: "translateY(16px)" }, { opacity: 1, transform: "translateY(0)" }], - { duration: 520, delay: index * 65, easing: "cubic-bezier(.22,1,.36,1)", fill: "both" }, + [{ opacity: 0, transform: "translateY(12px)" }, { opacity: 1, transform: "translateY(0)" }], + { duration: 300, delay, easing: "cubic-bezier(.16,1,.3,1)", fill: "both" }, ); }); } @@ -297,6 +292,22 @@ function initEntry() { const errorBox = $("#loginError"); const changeSection = $("#changePassword"); let pendingRole = null; + const submitButton = $('button[type="submit"]', form); + + function setLoading(loading) { + submitButton.disabled = loading; + submitButton.classList.toggle("is-loading", loading); + $$("input, button", form).forEach((el) => { + if (el !== submitButton) el.disabled = loading; + }); + if (loading) { + action.textContent = "登录中…"; + } else if (pendingRole) { + action.textContent = "设置新密码并进入"; + } else { + updateRole(); + } + } function showError(message) { errorBox.textContent = message; @@ -318,6 +329,7 @@ function initEntry() { form.addEventListener("submit", async (event) => { event.preventDefault(); errorBox.hidden = true; + setLoading(true); const role = pendingRole || $('input[name="role"]:checked', form).value; if (pendingRole) { @@ -325,6 +337,7 @@ function initEntry() { const confirmPassword = $('input[name="confirm_password"]', form).value; if (newPassword !== confirmPassword) { showError("两次输入的新密码不一致。"); + setLoading(false); return; } const changeResponse = await fetch("/api/password/change", { @@ -335,6 +348,7 @@ function initEntry() { const changeResult = await changeResponse?.json().catch(() => ({})); if (!changeResponse || !changeResponse.ok) { showError(changeResult?.message || "修改密码失败,请稍后重试。"); + setLoading(false); return; } window.location.href = role === "admin" ? "admin.html" : "company.html"; @@ -349,12 +363,13 @@ function initEntry() { const result = await response?.json().catch(() => ({})); if (!response || !response.ok) { showError(result?.message || "登录服务暂时不可用,请稍后重试。"); + setLoading(false); return; } if (result.must_change_password) { pendingRole = role; changeSection.hidden = false; - action.textContent = "设置新密码并进入"; + setLoading(false); $('input[name="new_password"]', form).focus(); return; } @@ -384,21 +399,26 @@ async function initAuthGuard() { } function applyCompanyIdentity(me) { - // The company portal always shows the session-bound company, never a - // hard-coded one. - if (portal !== "company" || !me?.company_name) return; - const context = $(".company-context"); - if (context) { - const mark = $("span", context); - if (mark) mark.textContent = me.company_name.slice(0, 1); - const name = $("strong", context); - if (name) name.textContent = me.company_name; + if (!me) return; + const row = $(".side-foot .user-row"); + if (row) { + const name = me.username || (portal === "company" ? (me.company_name || "公司用户") : "系统管理员"); + const avatar = $(".avatar", row); + if (avatar) avatar.textContent = name.slice(0, 1); + const nameEl = $(".user-name", row); + if (nameEl) nameEl.textContent = name; + const metaEl = $(".user-meta", row); + if (metaEl) metaEl.textContent = portal === "company" ? (me.company_name || "公司业务端") : "集团总账会计"; + } + // The company portal always shows the session-bound company in page copy. + if (portal === "company" && me.company_name) { + $$(".company-identity").forEach((el) => { el.textContent = me.company_name; }); } } function initShell() { - $$(".page-heading").forEach((heading) => { - if (heading.querySelector(".menu-button")) return; + $$(".topbar").forEach((bar) => { + if (bar.querySelector(".menu-button")) return; const button = document.createElement("button"); button.type = "button"; button.className = "icon-button menu-button"; @@ -407,7 +427,7 @@ function initShell() { button.setAttribute("aria-controls", "sidebar"); button.title = "打开导航"; button.innerHTML = ''; - heading.prepend(button); + bar.prepend(button); }); $$(".menu-button").forEach((menuButton) => { @@ -418,16 +438,16 @@ function initShell() { }); }); - $$(".nav-item").forEach((item) => { + $$(".side-nav a").forEach((item) => { const label = $("span", item)?.textContent.trim(); if (label) { item.setAttribute("aria-label", label); item.title = label; } }); - $$("[data-view]").forEach((button) => button.addEventListener("click", () => showView(button.dataset.view))); - $$("[data-view-link]").forEach((button) => button.addEventListener("click", () => showView(button.dataset.viewLink))); - $$('a.nav-item[href="index.html"]').forEach((link) => link.addEventListener("click", async (event) => { + $$("[data-view]").forEach((button) => button.addEventListener("click", (event) => { event.preventDefault(); showView(button.dataset.view); })); + $$("[data-view-link]").forEach((button) => button.addEventListener("click", (event) => { event.preventDefault(); showView(button.dataset.viewLink); })); + $$(".logout").forEach((control) => control.addEventListener("click", async (event) => { event.preventDefault(); try { await fetch("/api/logout", { method: "POST" }); @@ -459,15 +479,7 @@ function initShell() { const toastButton = event.target.closest("[data-toast]"); if (toastButton) showToast(toastButton.dataset.toast); }); - $(".nav-item[data-view].is-active")?.setAttribute("aria-current", "page"); - - $("#globalSearch")?.addEventListener("input", (event) => { - const view = $(`.app-view[data-page="${state.currentView}"]`); - const query = event.target.value.trim().toLowerCase(); - $$(".data-table tbody tr, .company-ledger, .notification-list article, .account-directory article", view).forEach((item) => { - item.hidden = query ? !item.textContent.toLowerCase().includes(query) : false; - }); - }); + $(".side-nav a[data-view].active")?.setAttribute("aria-current", "page"); document.addEventListener("click", (event) => { if ($("#sidebar")?.classList.contains("is-open") && !event.target.closest("#sidebar") && !event.target.closest(".menu-button")) closeNavigation({ restoreFocus: true }); @@ -480,1127 +492,227 @@ function initShell() { initMotion(); } -// --------------------------------------------------------------------------- -// B-44 intercompany balances: directory, pair drill-down and evidence drawer -// --------------------------------------------------------------------------- - -const b44 = { - subjectLabels: { receivable: "应收", payable: "应付", other_receivable: "其他应收", other_payable: "其他应付" }, - subjectCodes: { 应收: "receivable", 应付: "payable", 其他应收: "other_receivable", 其他应付: "other_payable" }, - reasonLabels: { subject_review: "待确认科目", unmatched_single: "单边未决", manual_pending: "手工记录待审" }, - from: "2026-01-01", - cutoff: "2026-07-31", - currency: "", - companyNames: new Map(), -}; - -function esc(value) { - return String(value ?? "").replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[char])); -} - -function fmtMoney(value) { - const num = Number(value); - return Number.isFinite(num) ? num.toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : "—"; -} - -function fmtAbsMoney(value) { - const num = Number(value); - return Number.isFinite(num) ? Math.abs(num).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : "—"; -} - -function homeCrumb() { - return portal === "admin" ? "往来查询" : "往来余额"; -} - -function resultDirection(signedOrDirection) { - if (signedOrDirection === "receivable" || Number(signedOrDirection) > 0) return { className: "success", label: "应收" }; - if (signedOrDirection === "payable" || Number(signedOrDirection) < 0) return { className: "danger", label: "应付" }; - return { className: "neutral", label: "持平" }; -} - -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)}` : ""; - 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) { - if (!chip || chip.visibility === "missing") return '源行缺失'; - if (chip.visibility === "masked") return `${esc(chip.label || "按对方授权不可见")}`; - return ``; -} - -function pickAccount(event, companyId, side) { - const isPayer = Number(event.payer_company_id) === Number(companyId); - if (side === "own") return isPayer ? event.payer_account : event.payee_account; - return isPayer ? event.payee_account : event.payer_account; -} - -function eventSummaryText(event) { - const text = String(event.summary || "").trim(); - if (text) return text; - const reason = String(event.reason || "").trim(); - return reason ? reason.split(/\r?\n/)[0] : "—"; -} - -function sixLineHtml({ currency, debit, credit, signed, resultLabel, unresolved, cutoff, conservationNote }) { - const direction = resultDirection(signed); - const unresolvedBlock = unresolved && "html" in (unresolved || {}) ? unresolved : unresolvedText(unresolved); - return ` -
-
期初余额期初不可用B-45 前无可靠期初
-
本期借方${amountWithCurrency(debit, currency)}
-
本期贷方${amountWithCurrency(credit, currency)}
-
期末结果${direction.label}${amountWithCurrency(signed, currency)}${esc(resultLabel || "期间净变动")}
-
未决金额${amountWithCurrency(unresolvedBlock.gross ?? unresolved?.gross_amount ?? 0, currency)}${unresolvedBlock.count ?? unresolved?.count ?? 0} 笔
-
截止日${fmtDate(cutoff)}${esc(conservationNote || "按币种独立展示")}
-
`; -} - -function fmtDate(iso) { - const text = String(iso || ""); - return text.slice(0, 10).replace(/-/g, "."); -} - -function b44Status(item) { - if (item.opening?.status !== "unavailable") { - return { label: "期末余额", className: "success" }; - } - return { label: "期间净变动", className: "neutral" }; -} - -function subjectOf(event, companyId) { - if (event.perspective_company_id === companyId) return event.subject_code; - return { receivable: "payable", payable: "receivable", other_receivable: "other_payable", other_payable: "other_receivable" }[event.subject_code] || null; -} - -function directionChip(direction) { - const label = direction === "outgoing" ? "转出" : "转入"; - const arrow = direction === "outgoing" ? "→" : "←"; - return `${arrow} ${label}`; -} - -function eventStateChip(state) { - if (state === "confirmed") return '已确认'; - if (state === "pending_subject") return '待确认科目'; - return '未决'; -} - -function postingLabel(event) { - if (event.posting_kind === "reversal") return "冲销"; - if (event.posting_kind === "adjustment") return "调整"; - return "正常"; -} - -function unresolvedText(unresolved) { - const gross = Number(unresolved?.gross_amount || 0); - const count = unresolved?.count || 0; - if (!count) return { html: '未决 0.00', active: false, gross: 0, count: 0 }; - const reasons = Object.entries(unresolved.by_reason || {}) - .map(([key, value]) => `${b44.reasonLabels[key] || key} ${fmtAbsMoney(value.gross_amount)} · ${value.count} 笔`) - .join(";"); +function pairData(from, to, endDate) { + const companies = ["A公司", "B公司", "C公司", "D公司", "E公司", "F公司"]; + const fromIndex = companies.indexOf(from) + 1; + const toIndex = companies.indexOf(to) + 1; + const low = Math.min(fromIndex, toIndex); + const high = Math.max(fromIndex, toIndex); + const seed = low * 13 + high * 7; + const canonical = { + receivable: 280 + seed * 18, + otherReceivable: 120 + seed * 9, + payable: 160 + ((seed * 11) % 720), + otherPayable: 80 + ((seed * 5) % 360), + }; + const reversed = fromIndex > toIndex; + const receivable = reversed ? canonical.payable : canonical.receivable; + const otherReceivable = reversed ? canonical.otherPayable : canonical.otherReceivable; + const payable = reversed ? canonical.receivable : canonical.payable; + const otherPayable = reversed ? canonical.otherReceivable : canonical.otherPayable; + const debit = receivable + otherReceivable; + const credit = payable + otherPayable; + const opening = ((fromIndex + toIndex) % 3) * 60 * (reversed ? -1 : 1); + const final = opening + debit - credit; + const end = new Date(`${endDate}T12:00:00`); + const dateBefore = (days) => { + const date = new Date(end); + date.setDate(date.getDate() - days); + return date.toISOString().slice(0, 10); + }; return { - html: `未决 ${fmtAbsMoney(gross)} · ${count} 笔${esc(reasons)}`, - active: true, - gross, - count, + opening, debit, credit, final, + totals: { 应收: receivable, 其他应收: otherReceivable, 应付: payable, 其他应付: otherPayable }, + rows: [ + [dateBefore(2), "转出", "应收", "中信 · 5316", `${to} · 9481`, "往来款", "双边匹配", receivable], + [dateBefore(9), "转出", "其他应收", "建行 · 0845", `${to} · 2046`, "资金调拨", "双边匹配", otherReceivable], + [dateBefore(17), "转入", "应付", "中信 · 5316", `${to} · 9481`, "归还往来款", "双边匹配", payable], + [dateBefore(25), "转入", "其他应付", "农行 · 3650", `${to} · 6120`, "临时往来", "单边待核", otherPayable], + ], }; } -const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'; +const pairSubjectOrder = ["应收", "其他应收", "应付", "其他应付"]; -function drawerFocusables(root) { - return [...root.querySelectorAll(FOCUSABLE)].filter((node) => !node.hasAttribute("disabled") && node.getAttribute("aria-hidden") !== "true"); -} - -function cycleTab(index, length, shift) { - if (!length) return 0; - if (shift) return (index - 1 + length) % length; - return (index + 1) % length; -} - -function drawerEscAction(depth) { - return depth > 1 ? "back" : "close"; -} - -function drawerState() { - const drawer = $("#evidenceDrawer"); - const scrim = $("#drawerScrim"); - let opener = null; - const stack = []; - - function paint() { - if (!drawer || !stack.length) return; - const layer = stack[stack.length - 1]; - $("#drawerTitle", drawer).textContent = layer.title; - const breadcrumb = $("#drawerBreadcrumb", drawer); - breadcrumb.replaceChildren(); - layer.crumbs.forEach((crumb, index) => { - if (index) breadcrumb.append(document.createTextNode("/")); - if (crumb.action) { - const button = document.createElement("button"); - button.type = "button"; - button.textContent = crumb.label; - button.addEventListener("click", crumb.action); - breadcrumb.append(button); - } else { - const span = document.createElement("span"); - span.textContent = crumb.label; - breadcrumb.append(span); - } - }); - const body = $("#drawerBody", drawer); - body.innerHTML = layer.html; - if (typeof layer.onMount === "function") layer.onMount(body); - requestAnimationFrame(() => { - const nodes = drawerFocusables(drawer); - (nodes[0] || body).focus({ preventScroll: true }); - if (layer.restoreSelector) { - const target = body.querySelector(layer.restoreSelector); - if (target) target.focus({ preventScroll: true }); - } - }); - } - - function open(triggerElement) { - if (!drawer) return; - if (!stack.length) opener = triggerElement || opener; - drawer.classList.add("is-open"); - drawer.setAttribute("aria-hidden", "false"); - scrim?.classList.add("is-open"); - } - - function close({ restoreFocus = true } = {}) { - if (!drawer) return; - drawer.classList.remove("is-open"); - drawer.setAttribute("aria-hidden", "true"); - scrim?.classList.remove("is-open"); - stack.length = 0; - if (restoreFocus && opener && document.contains(opener)) opener.focus(); - opener = null; - } - - function back() { - if (stack.length <= 1) { - close(); - return; - } - const leaving = stack.pop(); - paint(); - requestAnimationFrame(() => { - const body = $("#drawerBody", drawer); - const selector = leaving.triggerSelector; - const target = selector ? body?.querySelector(selector) : null; - if (target) target.focus({ preventScroll: true }); - }); - } - - function renderLayer(title, crumbs, html, onMount, options = {}) { - if (!drawer) return; - if (options.replaceTop && stack.length) stack.pop(); - stack.push({ - title, - crumbs, - html, - onMount, - triggerSelector: options.triggerSelector || null, - restoreSelector: options.restoreSelector || null, - }); - paint(); - } - - $$("[data-close-drawer]").forEach((button) => button.addEventListener("click", () => close())); - drawer?.addEventListener("keydown", (event) => { - if (event.key === "Escape") { - event.stopPropagation(); - event.preventDefault(); - if (drawerEscAction(stack.length) === "back") back(); - else close(); - return; - } - if (event.key !== "Tab" || !drawer.classList.contains("is-open")) return; - const nodes = drawerFocusables(drawer); - if (!nodes.length) return; - const current = nodes.indexOf(document.activeElement); - const next = cycleTab(current < 0 ? 0 : current, nodes.length, event.shiftKey); - event.preventDefault(); - nodes[next].focus(); +function setPair(from, to, endDate = "2026-07-31") { + $$('[data-pair-from]').forEach((item) => { item.textContent = from; }); + $$('[data-pair-to]').forEach((item) => { item.textContent = to; }); + $$("[data-pair-form]").forEach((form) => { + const fromSelect = $('[name="from"]', form); + const toSelect = $('[name="to"]', form); + if (fromSelect && [...fromSelect.options].some((option) => option.value === from)) fromSelect.value = from; + if (toSelect && [...toSelect.options].some((option) => option.value === to)) toSelect.value = to; + const endInput = $('[name="end"]', form); + if (endInput) endInput.value = endDate; }); - return { open, close, back, renderLayer, stack }; + if (!$("#pairReport")) return; + const data = pairData(from, to, endDate); + const format = (value) => value.toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + $("#pairPeriod").textContent = `统计口径 2026.01.01—${endDate}`; + $("#pairOpening").innerHTML = `${format(data.opening)}万元`; + $("#pairDebit").innerHTML = `${format(data.debit)}万元`; + $("#pairCredit").innerHTML = `${format(data.credit)}万元`; + $("#pairFinal").innerHTML = `${data.final >= 0 ? "应收" : "应付"} ${format(Math.abs(data.final))}万元`; + $("#pairReviewStatus").textContent = "含 1 笔待审核"; + $('[data-subject-total="all"]').textContent = `${data.rows.length} 笔`; + Object.entries(data.totals).forEach(([subject, value]) => { $(`[data-subject-total="${subject}"]`)?.replaceChildren(document.createTextNode(format(value))); }); + state.pairContext = { from, to, endDate }; + state.pairRows = data.rows.map(([date, direction, subject, ownAccount, counterparty, summary, match, amount]) => ({ date, direction, subject, ownAccount, counterparty, summary, match, amount })); + renderPairRows(); } -const drawer = drawerState(); - -function regionSkeleton(rows = 3) { - return Array.from({ length: rows }, () => '
').join(""); -} - -function regionError(container, message, retry) { - if (!container) return; - container.innerHTML = `
余额计算暂时不可用

${esc(message)}

`; - container.querySelector("[data-retry]")?.addEventListener("click", retry); -} - -function regionEmpty(container, title, hint, action) { - if (!container) return; - container.innerHTML = `
${esc(title)}

${esc(hint)}

${action || ""}
`; -} - -async function apiJson(url, options) { - const response = await fetch(url, options).catch(() => null); - if (response?.status === 401) { - window.location.href = "index.html"; - return null; - } - const body = await response?.json().catch(() => null); - if (!response || !response.ok) { - throw new Error(body?.message || "请求失败"); - } - return body; -} - -function balanceQuery() { - return `from=${encodeURIComponent(b44.from)}&cutoff=${encodeURIComponent(b44.cutoff)}${b44.currency ? `¤cy=${encodeURIComponent(b44.currency)}` : ""}`; -} - -// --- Admin: company balance directory -------------------------------------- - -async function loadAdminBalances(container) { - if (!container) return; - container.setAttribute("aria-busy", "true"); - container.innerHTML = regionSkeleton(3); - try { - const data = await apiJson(`/api/admin/intercompany/balances?${balanceQuery()}`); - container.setAttribute("aria-busy", "false"); - if (!data.items.length) { - regionEmpty(container, "该区间无往来事件", "请调整统计区间后重试", ''); - container.querySelector("[data-adjust-window]")?.addEventListener("click", () => document.querySelector("[data-balance-form] [name='from']")?.focus()); - return; - } - renderBalanceDirectory(container, data.items); - } catch (error) { - container.setAttribute("aria-busy", "false"); - regionError(container, error.message, () => loadAdminBalances(container)); - } -} - -function renderBalanceDirectory(container, items) { - container.replaceChildren(...items.map((item) => { - const details = document.createElement("details"); - details.className = "company-ledger is-balances"; - details.dataset.companyId = item.company_id; - const direction = resultDirection(item.result.direction); - const unresolved = unresolvedText(item.unresolved); - const name = item.company_name || "未命名公司"; - const summary = document.createElement("summary"); - summary.innerHTML = ` - ${esc(name.slice(0, 1))}${esc(name)}${esc(item.currency)} - ${amountWithCurrency(item.period.debit, item.currency)} - ${amountWithCurrency(item.period.credit, item.currency)} - ${direction.label}${amountWithCurrency(item.result.signed_amount, item.currency)} - ${unresolved.html} - 截止 ${fmtDate(item.window.cutoff)} - `; - details.append(summary); - const breakdown = document.createElement("div"); - breakdown.className = "ledger-breakdown"; - breakdown.setAttribute("aria-live", "polite"); - breakdown.innerHTML = '
'; - details.append(breakdown); - details.addEventListener("toggle", () => { - if (!details.open || details.dataset.loaded) return; - details.dataset.loaded = "1"; - loadCompanyBreakdown(breakdown, item, () => details.open = false); - }); - return details; - })); -} - -async function loadCompanyBreakdown(container, item, close) { - try { - const data = await apiJson(`/api/admin/intercompany/events?company_id=${item.company_id}&${balanceQuery()}`); - const debit = []; - const credit = []; - const byPair = new Map(); - for (const event of data.items) { - if (event.state !== "confirmed") continue; - if (event.currency && item.currency && event.currency !== item.currency) continue; - const isPayer = event.payer_company_id === item.company_id; - const counterpartyId = isPayer ? event.payee_company_id : event.payer_company_id; - const counterpartyName = isPayer ? event.payee_company_name : event.payer_company_name; - const subject = subjectOf(event, item.company_id); - const key = `${counterpartyId}|${subject}|${event.currency}`; - const bucket = byPair.get(key) || { counterpartyId, counterpartyName, subject, currency: event.currency, count: 0, amount: 0, isPayer }; - bucket.count += 1; - bucket.amount += Number(event.amount); - byPair.set(key, bucket); - (isPayer ? debit : credit).push(bucket); - } - if (!byPair.size) { - regionEmpty(container, "暂无已确认明细", "该公司的确认往来将在科目确认后出现"); - return; - } - const section = (title, total, rows, currency) => ` -

${title}

${amountWithCurrency(total, currency)}
- ${rows.map((row) => ` - `).join("")} -
`; - const debitRows = [...new Map(debit.map((row) => [`${row.counterpartyId}|${row.subject}`, row])).values()]; - const creditRows = [...new Map(credit.map((row) => [`${row.counterpartyId}|${row.subject}`, row])).values()]; - container.innerHTML = section("借方明细", debitRows.reduce((sum, row) => sum + row.amount, 0), debitRows, item.currency) - + section("贷方明细", creditRows.reduce((sum, row) => sum + row.amount, 0), creditRows, item.currency); - container.querySelectorAll("[data-open-pair]").forEach((button) => { - button.addEventListener("click", () => { - const counterpartyId = Number(button.dataset.openPair); - drawer.open(button); - renderAdminPair(item.company_id, counterpartyId, item.company_name); - }); - }); - } catch (error) { - regionError(container, error.message, close); - } -} - -function eventTableHead() { - return `交易日期方向科目本方账户对方账户摘要匹配状态金额`; -} - -function eventRowHtml(event, companyId) { - const subject = subjectOf(event, companyId) || event.own_subject; - const direction = event.direction || (event.payer_company_id === companyId ? "outgoing" : "incoming"); - const posting = event.posting_kind !== "normal" ? `${postingLabel(event)}` : ""; - const otherName = event.counterparty_company_name - || (event.payer_company_id === companyId ? event.payee_company_name : event.payer_company_name); - const summary = eventSummaryText(event); - return ` - - ${fmtDate(event.effective_at)}${posting} - ${directionChip(direction)} - ${event.state === "confirmed" ? esc(b44.subjectLabels[subject] || event.own_subject_label || "") : '待确认科目'} - ${accountCell(pickAccount(event, companyId, "own"))} - ${accountCell(pickAccount(event, companyId, "counterparty"))} - ${esc(summary)} - ${eventStateChip(event.state)} - ${amountWithCurrency(event.amount, event.currency, { signed: true, negative: eventIsNegative(event) })} +function renderPairRows() { + const tbody = $("#pairTransactions"); + if (!tbody) return; + const format = (value) => Number(value).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + tbody.innerHTML = state.pairRows.map((row, index) => { + const matched = row.match === "双边匹配"; + return ` + ${row.date} + ${row.direction} + ${row.subject} + ${row.ownAccount} + ${row.counterparty} + ${row.summary} + ${row.match} + ${format(row.amount)} + `; + }).join(""); } -function bindEventRows(root, tableId, companyId) { - $$(`#${tableId} tbody tr.event-row`, root).forEach((row) => { - const open = () => { - renderEventEvidence(Number(row.dataset.eventId), companyId, { - triggerSelector: `[data-event-id="${row.dataset.eventId}"]`, - }); - }; - row.addEventListener("click", open); - row.addEventListener("keydown", (event) => { - if (event.key === "Enter" || event.key === " ") { - event.preventDefault(); - open(); - } +function openTrace(index) { + const modal = $("#traceModal"); + if (!modal) return; + const row = state.pairRows?.[index]; + if (!row) return; + const ctx = state.pairContext || { from: "—", to: "—" }; + $("#traceSub").textContent = `${ctx.from} ↔ ${ctx.to} · ${row.subject} · ${row.date} · ${row.direction} ${formatCurrency(row.amount)} 万元`; + $("#kvOwnTx").textContent = "—(演示数据,未关联银行流水)"; + $("#kvOwnAcct").textContent = row.ownAccount; + $("#kvPeerCo").textContent = ctx.to; + $("#kvPeerAcct").textContent = row.counterparty; + $("#kvTime").textContent = row.date; + $("#kvAmt").textContent = `${formatCurrency(row.amount)} 万元`; + $("#kvBatch").textContent = "—"; + const evidence = $("#kvEvidence"); + if (row.match === "双边匹配") { evidence.textContent = "—"; evidence.style.color = ""; } + else { evidence.textContent = "待对方提供"; evidence.style.color = "var(--warn)"; } + modal.classList.add("open"); +} + +function initPairQueries() { + $$("[data-pair-form]").forEach((form) => { + $("[data-swap]", form)?.addEventListener("click", () => { + const from = $('[name="from"]', form); + const to = $('[name="to"]', form); + const previous = from.value; + from.value = to.value; + to.value = previous; }); - }); -} - -// --- Admin: pair detail + events ------------------------------------------- - -async function renderAdminPair(companyId, counterpartyId, companyName, extraCrumbs = []) { - const otherName = b44.companyNames.get(counterpartyId) || "对方公司"; - drawer.open(); - drawer.renderLayer( - `${companyName || "公司"} ↔ ${otherName}`, - [{ label: homeCrumb(), action: () => drawer.close() }, { label: `${companyName || companyId} ↔ ${otherName}` }], - regionSkeleton(4), - ); - try { - const data = await apiJson(`/api/admin/intercompany/pairs/${companyId}/${counterpartyId}?${balanceQuery()}`); - renderAdminPairBody(data, companyId, counterpartyId, extraCrumbs); - } catch (error) { - $("#drawerBody").innerHTML = `
余额计算暂时不可用

${esc(error.message)}

`; - $("#drawerBody [data-retry]")?.addEventListener("click", () => renderAdminPair(companyId, counterpartyId, companyName, extraCrumbs)); - } -} - -function renderAdminPairBody(data, companyId, counterpartyId) { - const a = data.companies.a; - const b = data.companies.b; - const lines = (data.items || []).map((item) => sixLineHtml({ - currency: item.currency, - debit: item.a.period.debit, - credit: item.a.period.credit, - signed: item.a.result.signed_amount, - resultLabel: item.a.result.label, - unresolved: item.unresolved, - cutoff: data.window.cutoff, - conservationNote: "双方守恒已校验", - })).join(""); - const first = data.items[0]; - const subjectButtons = Object.values(first?.subjects || {}).map((subject) => ` - `).join(""); - const eventCount = (data.items || []).reduce((sum, item) => sum + (item.trace?.event_count || 0), 0); - const body = ` - ${lines} -
- - ${subjectButtons} -
-
- ${eventTableHead()} - -
`; - drawer.renderLayer( - `${a.name} ↔ ${b.name}`, - [ - { label: homeCrumb(), action: () => drawer.close() }, - { label: `${a.name} ↔ ${b.name}` }, - ], - body, - (root) => { - const urls = (data.items || []).map((item) => item.trace.events_url); - loadPairEvents(root, companyId, counterpartyId, urls); - $$("[data-subject]", root).forEach((button) => button.addEventListener("click", () => { - $$("[data-subject]", root).forEach((item) => { - const active = item === button; - item.classList.toggle("is-active", active); - item.setAttribute("aria-pressed", String(active)); - }); - const subject = button.dataset.subject; - $$("#pairEventsTable tbody tr[data-subject-row]").forEach((row) => { - row.hidden = subject !== "all" && row.dataset.subjectRow !== subject; - }); - })); - }, - { replaceTop: true }, - ); -} - -async function loadPairEvents(root, companyId, counterpartyId, urls) { - const tbody = $("#pairEventsTable tbody", root); - if (!tbody) return; - tbody.setAttribute("aria-busy", "true"); - const pageSize = 50; - try { - const pages = await Promise.all((Array.isArray(urls) ? urls : [urls]).map((url) => apiJson(`${url}&limit=${pageSize}`))); - const items = pages.flatMap((page) => page.items || []); - tbody.setAttribute("aria-busy", "false"); - if (!items.length) { - tbody.innerHTML = '
该区间无往来事件

当前公司对在统计区间内没有逐笔事件

'; - return; - } - tbody.innerHTML = items.map((event) => eventRowHtml(event, companyId)).join(""); - bindEventRows(root, "pairEventsTable", companyId); - if (pages.some((page) => page.has_more)) { - const footer = document.createElement("tr"); - footer.innerHTML = '
超过 50 笔,请在服务端按截止日分段查看
'; - tbody.append(footer); - } - } catch (error) { - tbody.setAttribute("aria-busy", "false"); - tbody.innerHTML = `
逐笔事件加载失败

${esc(error.message)}

`; - } -} - -// --- Evidence drawer --------------------------------------------------------- - -async function renderEventEvidence(eventId, companyId, options = {}) { - const base = portal === "company" ? "/api/company/intercompany" : "/api/admin/intercompany"; - drawer.renderLayer("事件证据", [ - { label: homeCrumb(), action: () => drawer.close() }, - { label: "返回上一层", action: () => drawer.back() }, - ], regionSkeleton(5), null, { triggerSelector: options.triggerSelector }); - try { - const [detail, evidence] = await Promise.all([ - apiJson(`${base}/events/${eventId}`), - apiJson(`${base}/events/${eventId}/evidence`), - ]); - const event = detail.event; - const direction = companyId ? (event.payer_company_id === companyId ? "outgoing" : "incoming") : null; - const subjectLabel = companyId && event.subject_code ? (b44.subjectLabels[subjectOf(event, companyId)] || "") : (b44.subjectLabels[event.subject_code] || ""); - const blocks = evidence.blocks.map(renderEvidenceBlock).join('
双边匹配 · 已去重为同一事件
'); - drawer.renderLayer( - `${fmtDate(event.effective_at)} 事件`, - [ - { label: homeCrumb(), action: () => drawer.close() }, - { label: `${event.payer_company_name} ↔ ${event.payee_company_name}`, action: () => drawer.back() }, - { label: fmtDate(event.effective_at) }, - ], - ` -
-
方向${direction ? directionChip(direction) : ""}${esc(subjectLabel)}
-
金额${amountWithCurrency(event.amount, event.currency, { signed: true, negative: eventIsNegative(event) })}
-
匹配状态${eventStateChip(detail.state)}
-
-
${blocks}
`, - null, - { replaceTop: true, triggerSelector: options.triggerSelector }, - ); - } catch (error) { - $("#drawerBody").innerHTML = `
证据加载失败

${esc(error.message)}

`; - $("#drawerBody [data-retry]")?.addEventListener("click", () => renderEventEvidence(eventId, companyId, options)); - } -} - -function renderEvidenceBlock(block) { - if (block.visibility === "missing") { - return `
对方源行缺失 · 当前为单边记录该侧没有可展示的银行或手工证据
`; - } - const sideLabel = block.side === "own" ? "本方" : "对方"; - const kindLabel = block.source_kind === "manual" ? "手工记录" : "银行源行"; - const title = `${sideLabel} · ${kindLabel}${block.visibility === "masked" ? "(按对方授权不可见)" : ""}`; - if (block.visibility === "masked") { - const fields = block.fields || {}; - const rows = [ - fields.company_name ? ["对方公司", fields.company_name] : null, - fields.own_account_masked ? ["对方账号", fields.own_account_masked] : null, - fields.state ? ["记录状态", fields.state] : null, - ].filter(Boolean).map(([label, value]) => `
${esc(label)}
${esc(value)}
`).join(""); - return `
${esc(title)}
${rows || "
可见范围
仅对方公司名称与脱敏账号
"}
`; - } - const fields = block.fields || {}; - const pairs = []; - if (block.source_kind === "bank") { - pairs.push( - ["银行文件", fields.original_filename], ["工作表", fields.sheet_name], - ["源行号", fields.source_row], ["交易时间", fields.transaction_at], - ["收入", fields.income], ["支出", fields.expense], - ["本方账号", fields.own_account], ["本方户名", fields.own_name], - ["对方账号", fields.counterparty_account_masked || fields.counterparty_account], ["对方户名", fields.counterparty_name], - ["摘要", fields.summary], ["用途", fields.purpose], - ["流水号", fields.reference], ["币种", fields.currency], - ["观察角色", fields.role], - ); - } else { - pairs.push( - ["记录编号", fields.manual_record_id], ["提交公司", fields.company_name], - ["对方公司", fields.counterparty_company_name], ["业务日期", fields.occurred_at], - ["方向", fields.direction === "outgoing" ? "转出" : "转入"], ["金额", `${fields.amount} ${fields.currency}`], - ["资金来源", fields.funding_source], ["建议科目", b44.subjectLabels[fields.requested_subject] || fields.requested_subject], - ["摘要", fields.summary], ["补充说明", fields.reason], - ["状态", fields.state], - ); - } - const grid = pairs.filter(([, value]) => value !== null && value !== undefined && value !== "") - .map(([label, value]) => `
${esc(label)}
${esc(value)}
`).join(""); - return `
${esc(title)}
${grid}
`; -} - -// --- Admin: subject-review and manual-record audit rows ---------------------- - -function appendSubjectReviewRow(item) { - const tbody = $("#auditRows"); - if (!tbody) return; - const row = document.createElement("tr"); - row.dataset.auditType = "科目"; - row.dataset.recordKind = "subject-review"; - row.dataset.ledgerEventId = item.ledger_event_id; - row.dataset.perspectiveId = item.payer_company_id; - row.dataset.expectedRevision = item.revision_id; - row.dataset.company = item.payer_company_name; - const suggestions = (item.suggestions || []).map((s) => `${s.suggested_company_name} · ${s.suggested_subject_label}`).join(";") || "无规则建议(需人工判定)"; - row.dataset.suggestions = JSON.stringify(item.suggestions || []); - row.dataset.summary = item.summary || ""; - 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)} - 科目待确认 - ${fmtDate(item.effective_at)} - ${fmtMoney(item.amount)} ${esc(item.currency)} - 待确认科目 - `; - tbody.append(row); -} - -function appendManualRecordReviewRow(record) { - const tbody = $("#auditRows"); - if (!tbody) return; - const row = document.createElement("tr"); - row.dataset.auditType = "手工"; - row.dataset.recordKind = "manual-review"; - row.dataset.recordId = record.id; - row.dataset.expectedDecisionId = record.decision_id; - row.dataset.company = record.company_name; - row.dataset.submittedBy = record.submitted_by_username || ""; - row.dataset.attachmentName = record.attachment_name || ""; - row.dataset.reasonText = record.reason || ""; - 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] || "")} - 手工记录 - ${fmtDate(record.occurred_at)} - ${esc(record.summary || record.reason || "待补充说明")} - 待复核 - `; - 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") { - appendEvidenceGrid(evidence, auditEvidenceFields("subject-review", row.dataset)); - const suggestions = JSON.parse(row.dataset.suggestions || "[]"); - const list = document.createElement("ul"); - list.className = "candidate-list"; - if (!suggestions.length) { - const item = document.createElement("li"); - item.textContent = "无规则建议(需退回或转异常,不能猜测科目)"; - list.append(item); - } else { - suggestions.forEach((suggestion) => { - const item = document.createElement("li"); - item.textContent = `规则建议(只读):${suggestion.suggested_company_name || ""} · ${suggestion.suggested_subject_label || suggestion.suggested_subject_code || ""}`; - list.append(item); - }); - } - evidence.append(list); - return; - } - if (row.dataset.recordKind === "manual-review") { - 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 || ""; - evidence.append(reason); - return; - } - const detail = document.createElement("small"); - detail.textContent = cells[1].querySelector("small")?.textContent || ""; - const source = document.createElement("small"); - source.textContent = `证据:${row.dataset.evidence || "银行原始行、导入批次、账户覆盖区间与公司说明"}`; - evidence.append(detail, source); -} - -async function loadAdminAuditQueue() { - try { - const [subjects, records] = await Promise.all([ - apiJson(`/api/admin/subject-reviews?${balanceQuery()}&limit=100`), - apiJson("/api/admin/manual-records?limit=100"), - ]); - $$("#auditRows [data-record-kind='subject-review'], #auditRows [data-record-kind='manual-review']").forEach((row) => row.remove()); - (subjects.items || []).forEach(appendSubjectReviewRow); - (records.records || []).filter((record) => record.state === "pending").forEach(appendManualRecordReviewRow); - updateAuditCounts(); - } catch { - /* the demo audit table stays visible if the queue cannot load */ - } -} - -// --- Company portal: balances ------------------------------------------------ - -async function loadCompanyBalances() { - const summaryLine = $("#companyBalanceGroups") || $("#companyBalanceLine"); - const counterpartyList = $("#companyCounterparties"); - const alertBox = $("#companyUnresolvedAlert"); - if (!summaryLine && !counterpartyList) return; - if (summaryLine) summaryLine.innerHTML = regionSkeleton(1); - if (counterpartyList) counterpartyList.innerHTML = regionSkeleton(2); - try { - const data = await apiJson(`/api/company/intercompany/balances?${balanceQuery()}`); - $("#companyBalancePeriod").textContent = `统计口径 ${fmtDate(data.window.from)}—${fmtDate(data.window.cutoff)} · 期初不可用,仅显示期间净变动`; - if (summaryLine) { - const items = data.items || []; - if (!items.length) { - summaryLine.innerHTML = sixLineHtml({ - currency: "", debit: 0, credit: 0, signed: 0, resultLabel: "期间净变动", - unresolved: { gross_amount: "0", count: 0 }, cutoff: data.window.cutoff, - }); - } else { - summaryLine.innerHTML = items.map((item) => sixLineHtml({ - currency: item.currency, - debit: item.period.debit, - credit: item.period.credit, - signed: item.result.signed_amount, - resultLabel: item.result.label, - unresolved: item.unresolved, - cutoff: item.window?.cutoff || data.window.cutoff, - })).join(""); - } - } - const unresolvedCount = (data.items || []).reduce((sum, item) => sum + (item.unresolved?.count || 0), 0); - if (alertBox) { - if (unresolvedCount) { - alertBox.hidden = false; - const parts = (data.items || []) - .filter((item) => item.unresolved?.count) - .map((item) => `${item.currency} ${fmtAbsMoney(item.unresolved.gross_amount)}`); - $("#companyUnresolvedText").textContent = `有 ${unresolvedCount} 笔往来未确认,合计 ${parts.join("、")},不影响已确认余额`; - } else { - alertBox.hidden = true; - } - } - renderCompanyCounterparties(counterpartyList, data.counterparties || [], data.window.cutoff); - } catch (error) { - if (summaryLine) regionError(summaryLine, error.message, loadCompanyBalances); - if (counterpartyList) counterpartyList.innerHTML = ""; - } -} - -function renderCompanyCounterparties(container, counterparties, cutoff) { - if (!container) return; - if (!counterparties.length) { - regionEmpty(container, "暂无对方公司往来", "确认往来事件后,对方公司明细将在此展示"); - return; - } - container.replaceChildren(...counterparties.map((row) => { - const article = document.createElement("article"); - article.className = "company-row is-balance-counterparty"; - article.tabIndex = 0; - article.dataset.counterpartyId = row.counterparty_company_id; - article.setAttribute("role", "button"); - article.setAttribute("aria-label", `查看与 ${row.counterparty_company_name} 的往来明细`); - const direction = resultDirection(row.result.direction); - const unresolved = unresolvedText(row.unresolved); - article.innerHTML = ` - ${esc(row.counterparty_company_name.slice(0, 1))} -
${esc(row.counterparty_company_name)}${esc(row.currency)} · ${unresolved.active ? `未决 ${fmtAbsMoney(row.unresolved.gross_amount)} · ${row.unresolved.count} 笔` : "未决 0.00"}
-
${direction.label} ${amountWithCurrency(row.result.signed_amount, row.currency)}截止 ${fmtDate(cutoff)}
`; - const open = () => { - drawer.open(article); - renderCompanyPair(row.counterparty_company_id, row.counterparty_company_name); - }; - article.addEventListener("click", open); - article.addEventListener("keydown", (event) => { - if (event.key === "Enter" || event.key === " ") { - event.preventDefault(); - open(); - } - }); - return article; - })); -} - -async function renderCompanyPair(counterpartyId, counterpartyName) { - drawer.open(); - drawer.renderLayer(`${counterpartyName} · 往来明细`, [{ label: homeCrumb(), action: () => drawer.close() }], regionSkeleton(4)); - try { - const data = await apiJson(`/api/company/intercompany/pairs/${counterpartyId}?${balanceQuery()}`); - const own = data.companies.a; - const lines = (data.items || []).map((item) => sixLineHtml({ - currency: item.currency, - debit: item.a.period.debit, - credit: item.a.period.credit, - signed: item.a.result.signed_amount, - resultLabel: item.a.result.label, - unresolved: item.unresolved, - cutoff: data.window.cutoff, - conservationNote: "双方守恒已校验", - })).join(""); - const body = ` - ${lines} -
- ${eventTableHead()} - -
`; - drawer.renderLayer( - `${own.name} ↔ ${counterpartyName}`, - [{ label: homeCrumb(), action: () => drawer.close() }, { label: `${own.name} ↔ ${counterpartyName}` }], - body, - (root) => loadCompanyEvents(root, counterpartyId), - { replaceTop: true }, - ); - } catch (error) { - $("#drawerBody").innerHTML = `
余额计算暂时不可用

${esc(error.message)}

`; - $("#drawerBody [data-retry]")?.addEventListener("click", () => renderCompanyPair(counterpartyId, counterpartyName)); - } -} - -async function loadCompanyEvents(root, counterpartyId) { - const tbody = $("#companyEventsTable tbody", root); - if (!tbody) return; - try { - const data = await apiJson(`/api/company/intercompany/events?${balanceQuery()}&limit=100`); - const ownCompanyId = state.me?.company_id; - const filtered = (data.items || []).filter((event) => event.counterparty_company_id === counterpartyId); - if (!filtered.length) { - tbody.innerHTML = '
该区间无往来事件

当前统计区间内没有与对方公司的逐笔事件

'; - return; - } - tbody.innerHTML = filtered.map((event) => eventRowHtml(event, ownCompanyId)).join(""); - bindEventRows(root, "companyEventsTable", ownCompanyId); - } catch (error) { - tbody.innerHTML = `
逐笔事件加载失败

${esc(error.message)}

`; - } -} - -// --- B-44 wiring ------------------------------------------------------------- - -function initBalanceQueries() { - $$("[data-balance-form]").forEach((form) => { form.addEventListener("submit", (event) => { event.preventDefault(); const from = $('[name="from"]', form).value; - const cutoff = $('[name="cutoff"]', form).value; - const currency = $('[name="currency"]', form).value || ""; - if (from > cutoff) { - showToast("日期范围无效", "统计起始不能晚于统计截止"); + const to = $('[name="to"]', form).value; + const endDate = $('[name="end"]', form)?.value || "2026-07-31"; + const error = $("#pairError"); + if (from === to) { + if (error) { error.style.display = ""; } + showToast("请选择两个不同的公司", "同公司账户调拨不进入公司间往来查询", "warn"); return; } - b44.from = from; - b44.cutoff = cutoff; - b44.currency = currency; - loadAdminBalances($("#balanceLedgers")); - showToast("查询结果已更新", `${from} 至 ${cutoff} · 截止日口径`); + if (error) error.style.display = "none"; + const notice = $("#pairNotice"); + if (notice) notice.style.display = "none"; + setPair(from, to, endDate); + showView("pair"); }); }); - // Dashboard quick-pair form: resolve company names to ids and open the pair. - $$("[data-pair-form]").forEach((form) => form.addEventListener("submit", (event) => { - event.preventDefault(); - const from = $('[name="from"]', form).value; - const to = $('[name="to"]', form).value; - if (from === to) { - showToast("请选择两个不同的公司", "同公司账户调拨不进入公司间往来查询"); - return; - } - const fromId = [...b44.companyNames.entries()].find(([, name]) => name === from)?.[0]; - const toId = [...b44.companyNames.entries()].find(([, name]) => name === to)?.[0]; - if (!fromId || !toId) { - showToast("无法解析公司主档", "请先在「公司与账号」中建立公司"); - return; - } - renderAdminPair(fromId, toId, from); - })); - - // Dashboard quick-pair buttons and static pair links resolve to the drawer. $$("[data-pair-link]").forEach((button) => button.addEventListener("click", () => { - const [fromName, toName] = String(button.dataset.pairLink || "").split("|"); - const fromId = [...b44.companyNames.entries()].find(([, name]) => name === fromName)?.[0]; - const toId = [...b44.companyNames.entries()].find(([, name]) => name === toName)?.[0]; - if (!fromId || !toId) { - showToast("无法解析演示公司", "正式数据请先在「公司与账号」中建立公司主档"); - return; - } - renderAdminPair(fromId, toId, fromName); + const [from, to] = button.dataset.pairLink.split("|"); + setPair(from, to); + showView("pair"); })); -} -async function loadCompanyOptions() { - try { - const data = await apiJson("/api/admin/companies"); - (data.companies || []).forEach((company) => b44.companyNames.set(company.id, company.name)); - } catch { - /* best-effort name resolution for dashboard pair links */ - } -} - -async function loadCompanyDirectoryOptions() { - try { - const data = await apiJson("/api/company/companies"); - (data.companies || []).forEach((company) => b44.companyNames.set(company.id, company.name)); - const select = $("#manualEntryForm [name='counterparty']"); - if (select) { - const kept = [...select.options].filter((option) => option.dataset.keep); - select.replaceChildren(...kept); - (data.companies || []).forEach((company) => select.add(new Option(company.name, company.id))); - } - } catch { - /* the manual form keeps the free-text counterparty as fallback */ - } -} - -async function submitCompanyManualRecord(form) { - const data = new FormData(form); - const sourceOption = $('[name="sourceAccount"] option:checked', form); - const counterpartyOption = $('[name="counterparty"] option:checked', form); - const payload = { - counterparty_company_id: Number(counterpartyOption?.value || 0) || null, - occurred_at: `${String(data.get("transactionDate"))}T12:00:00`, - direction: String(data.get("direction")) === "付款" ? "outgoing" : "incoming", - amount: String(data.get("amount")), - currency: "CNY", - funding_source: sourceOption?.dataset.accountId ? "approved_bank_account" : "other", - bank_account_id: sourceOption?.dataset.accountId ? Number(sourceOption.dataset.accountId) : null, - requested_subject: b44.subjectCodes[String(data.get("subject"))] || "other_receivable", - summary: String(data.get("summary") || "").trim(), - reason: String(data.get("remark") || "").trim(), - request_key: `mr-${Date.now().toString(36)}`, - }; - const evidenceFile = data.get("evidence"); - if (evidenceFile instanceof File && evidenceFile.name) { - payload.evidence = { attachment_name: evidenceFile.name }; - } - if (!payload.counterparty_company_id) { - showToast("请选择对方公司", "手工记录对方必须是集团内部公司"); - return; - } - if (!payload.amount || Number(payload.amount) <= 0) { - showToast("金额无效", "金额必须大于零"); - return; - } - try { - const result = await apiJson("/api/company/manual-records", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), + $$("[data-subject-filter]").forEach((button) => button.addEventListener("click", () => { + const subject = button.dataset.subjectFilter; + $$("[data-subject-filter]").forEach((item) => { + const active = item === button; + item.classList.toggle("active", active); + item.setAttribute("aria-pressed", String(active)); }); - form.reset(); - showToast("手工记录已提交", "总账复核前不会纳入公司间往来计算"); - await loadCompanyManualRecords(); - return result; - } catch (error) { - showToast("手工记录提交失败", error.message); - } + $$("#pairTransactions tr").forEach((row) => { row.hidden = subject !== "all" && row.dataset.subject !== subject; }); + })); + + $("#pairTransactions")?.addEventListener("click", (event) => { + const button = event.target.closest("[data-trace]"); + if (button) openTrace(Number(button.dataset.trace)); + }); + $("#traceClose")?.addEventListener("click", () => $("#traceModal")?.classList.remove("open")); + $("#traceOk")?.addEventListener("click", () => $("#traceModal")?.classList.remove("open")); + $("#traceModal")?.addEventListener("click", (event) => { if (event.target === event.currentTarget) event.currentTarget.classList.remove("open"); }); } -async function loadCompanyManualRecords() { - const tbody = $("#manualRecordRows"); - if (!tbody) return; - try { - const data = await apiJson("/api/company/manual-records?limit=100"); - $$('[data-stored-record]', tbody).forEach((row) => row.remove()); - (data.records || []).forEach((record) => appendCompanyManualRow(record)); - const pending = (data.records || []).filter((record) => record.state === "pending").length; - const status = $("#manualPendingStatus"); - if (status) status.textContent = `${pending} 笔待总账复核`; - } catch { - /* keep the demo rows when the API is unavailable */ - } -} - -function appendCompanyManualRow(record) { - const tbody = $("#manualRecordRows"); - if (!tbody) return; - const row = document.createElement("tr"); - row.dataset.storedRecord = record.id; - const identity = document.createElement("td"); - const id = document.createElement("strong"); id.textContent = `MR-${record.id}`; - const date = document.createElement("small"); date.textContent = fmtDate(record.occurred_at); - identity.append(id, date); - const direction = document.createElement("td"); - const directionName = document.createElement("strong"); directionName.textContent = record.direction === "outgoing" ? "付款" : "收款"; - const subject = document.createElement("small"); subject.textContent = b44.subjectLabels[record.requested_subject] || record.requested_subject; - direction.append(directionName, subject); - const source = document.createElement("td"); source.textContent = { approved_bank_account: "已批准账户", personal_transit: "个人过账", other: "其他来源" }[record.funding_source] || record.funding_source; - const counterparty = document.createElement("td"); - const counterpartyName = document.createElement("strong"); counterpartyName.textContent = record.counterparty_company_name; - counterparty.append(counterpartyName); - const summary = document.createElement("td"); summary.textContent = record.summary || record.reason || "—"; - const amount = document.createElement("td"); amount.className = "number"; amount.textContent = fmtMoney(record.amount); - const statusCell = document.createElement("td"); - const stateMap = { pending: ["warning", "待总账复核"], approved: ["success", "已确认"], returned: ["danger", "已退回"], exception: ["neutral", "异常待处理"], reversed: ["neutral", "已冲销"] }; - const [className, label] = stateMap[record.state] || ["neutral", record.state]; - const badge = document.createElement("span"); badge.className = `status ${className}`; badge.textContent = label; - statusCell.append(badge); - row.append(identity, direction, source, counterparty, summary, amount, statusCell); - tbody.append(row); +function manualStatusMeta(status) { + if (status === "已确认") return { cls: "pill-success", label: "已通过" }; + if (status === "已驳回") return { cls: "pill-danger", label: "已驳回" }; + return { cls: "pill-info", label: "待审核" }; } function renderCompanyManualRecords() { const tbody = $("#manualRecordRows"); if (!tbody) return; - $$('[data-stored-record]', tbody).forEach((row) => row.remove()); + tbody.replaceChildren(); const records = readStoredRecords(storageKeys.manual).filter((record) => record.company === "A公司"); [...records].reverse().forEach((record) => { const row = document.createElement("tr"); row.dataset.storedRecord = record.id; - const identity = document.createElement("td"); - const id = document.createElement("strong"); id.textContent = record.id; - const date = document.createElement("small"); date.textContent = record.transactionDate; - identity.append(id, date); - const direction = document.createElement("td"); - const directionName = document.createElement("strong"); directionName.textContent = record.direction; - const subject = document.createElement("small"); subject.textContent = record.subject; - direction.append(directionName, subject); - const source = document.createElement("td"); source.textContent = record.sourceAccount; + + const date = document.createElement("td"); date.className = "num"; date.textContent = record.transactionDate || "—"; + + const direction = document.createElement("td"); direction.textContent = record.direction || "—"; + const counterparty = document.createElement("td"); - const counterpartyName = document.createElement("strong"); counterpartyName.textContent = record.counterparty; - const counterpartyType = document.createElement("small"); counterpartyType.textContent = record.counterpartyType; + const counterpartyName = document.createElement("span"); counterpartyName.className = "cell-main"; counterpartyName.textContent = record.counterparty; + const counterpartyType = document.createElement("span"); counterpartyType.className = "cell-sub"; counterpartyType.textContent = record.counterpartyType || ""; counterparty.append(counterpartyName, counterpartyType); - const summary = document.createElement("td"); summary.textContent = record.summary; - const amount = document.createElement("td"); amount.className = "number"; amount.textContent = formatCurrency(record.amount); + + const subject = document.createElement("td"); subject.innerHTML = `${record.subject || "—"}`; + + const isIn = record.direction === "收款"; + const amount = document.createElement("td"); + amount.className = `num-col ${isIn ? "amt-in" : "amt-out"}`; + amount.textContent = `${isIn ? "+" : "-"}¥ ${formatCurrency(record.amount)}`; + + const summary = document.createElement("td"); summary.className = "wrap"; summary.textContent = record.summary || "—"; + + const statusMeta = manualStatusMeta(record.status); const statusCell = document.createElement("td"); - const status = recordStatus(record.status); - const badge = document.createElement("span"); badge.className = `status ${status.className}`; badge.textContent = status.label; - statusCell.append(badge); - row.append(identity, direction, source, counterparty, summary, amount, statusCell); + statusCell.innerHTML = `${statusMeta.label}`; + + const action = document.createElement("td"); + if (record.status === "待总账复核") { + action.innerHTML = ''; + } else { + action.innerHTML = ''; + } + + row.append(date, direction, counterparty, subject, amount, summary, statusCell, action); tbody.append(row); }); - const pending = records.filter((record) => record.status === "待总账复核").length + 1; - if ($("#manualPendingStatus")) $("#manualPendingStatus").textContent = `${pending} 笔待总账复核`; + updateManualCounts(); +} + +function updateManualCounts() { + const records = readStoredRecords(storageKeys.manual).filter((record) => record.company === "A公司"); + const pending = records.filter((record) => record.status === "待总账复核").length; + const pendingEl = $("#manualPendingStatus"); + if (pendingEl) pendingEl.textContent = pending; + const foot = $("#manualFoot"); + if (foot) foot.textContent = `共 ${records.length} 条 · 待复核 ${pending} 条`; + const empty = $("#manualEmpty"); + if (empty) empty.hidden = records.length > 0; } function fillAccountSelects(accounts) { @@ -1618,58 +730,73 @@ function fillAccountSelects(accounts) { }); } +function companyAccountMeta(status) { + if (status === "active") return { status: { cls: "pill-success", label: "启用" }, audit: { cls: "pill-success", label: "已审核" } }; + if (status === "returned") return { status: { cls: "pill-danger", label: "已退回" }, audit: { cls: "pill-danger", label: "已退回" } }; + if (status === "disabled") return { status: { cls: "pill-muted", label: "停用" }, audit: { cls: "pill-success", label: "已审核" } }; + return { status: { cls: "pill-info", label: "待启用" }, audit: { cls: "pill-info", label: "待审核" } }; +} + function renderCompanyAccounts(accounts) { - const directory = $("#accountDirectory"); - if (directory) { - directory.replaceChildren(...accounts.map((account) => { - const article = document.createElement("article"); - const header = document.createElement("header"); - const mark = document.createElement("span"); mark.className = "bank-mark"; mark.textContent = account.bank_name.slice(0, 1); - const identity = document.createElement("div"); - const name = document.createElement("strong"); name.textContent = account.bank_name; - const meta = document.createElement("small"); meta.textContent = `${account.account_type} · 尾号 ${accountTail(account.account_number_masked)}`; - identity.append(name, meta); - const status = recordStatus(accountStatusLabel(account.status)); - const badge = document.createElement("em"); badge.className = `status ${status.className}`; badge.textContent = status.label; - header.append(mark, identity, badge); - const details = document.createElement("dl"); - const rows = [ - ["申请启用", account.effective_from || "待审核确定"], - ["流水覆盖", account.usable ? "尚未上传" : "不参与计算"], - ["提交时间", String(account.created_at || "").slice(0, 10) || "—"], - ]; - if (account.status === "returned" && account.review_reason) rows.push(["退回原因", account.review_reason]); - if (account.status === "disabled" && account.effective_to) rows.push(["停用日期", account.effective_to]); - rows.forEach(([term, value]) => { - const wrapper = document.createElement("div"); - const dt = document.createElement("dt"); dt.textContent = term; - const dd = document.createElement("dd"); dd.textContent = value; - wrapper.append(dt, dd); details.append(wrapper); - }); - article.append(header, details); - return article; + state.accounts = accounts; + const tbody = $("#account-tbody"); + if (tbody) { + tbody.replaceChildren(...accounts.map((account) => { + const row = document.createElement("tr"); + row.dataset.accountId = account.id; + + const identity = document.createElement("td"); + const name = document.createElement("span"); name.className = "cell-main"; name.textContent = account.bank_name; + const tail = document.createElement("span"); tail.className = "cell-sub"; tail.textContent = `尾号 ${accountTail(account.account_number_masked)}`; + identity.append(name, tail); + + const type = document.createElement("td"); type.innerHTML = `${account.account_type || "—"}`; + + const meta = companyAccountMeta(account.status); + const statusCell = document.createElement("td"); statusCell.innerHTML = `${meta.status.label}`; + + const coverage = document.createElement("td"); + coverage.className = "num muted"; + coverage.textContent = account.usable ? "尚未上传" : "—"; + + const auditCell = document.createElement("td"); auditCell.innerHTML = `${meta.audit.label}`; + + const action = document.createElement("td"); action.innerHTML = ''; + + row.append(identity, type, statusCell, coverage, auditCell, action); + return row; })); } + const count = $("#account-count"); + if (count) count.textContent = accounts.length; + const foot = $("#account-foot"); + if (foot) foot.textContent = `共 ${accounts.length} 个账户`; + const empty = $("#account-empty"); + if (empty) empty.hidden = accounts.length > 0; fillAccountSelects(accounts); } async function loadCompanyAccounts() { + const tbody = $("#account-tbody"); + showTableLoading(tbody, 6); const response = await fetch("/api/company/accounts").catch(() => null); if (response?.status === 401 || response?.status === 403) { window.location.href = "index.html"; return; } - if (!response?.ok) return; + if (!response?.ok) { + showTableError(tbody, 6); + return; + } const result = await response.json().catch(() => null); if (result?.accounts) renderCompanyAccounts(result.accounts); + else showTableError(tbody, 6); } function appendAdminReviewRow(record, kind) { const tbody = $("#auditRows"); if (!tbody) return; const isAccount = kind === "account"; - // Account rows come from the server (full number visible only in this - // authorized admin view); manual records are still browser-local demo data. const statusLabel = isAccount ? accountStatusLabel(record.status) : record.status; const row = document.createElement("tr"); row.dataset.storedReview = record.id; @@ -1682,48 +809,52 @@ function appendAdminReviewRow(record, kind) { ? "公司提交资料、开户行、账号、账户类型与启用日期" : "公司手工记录、关联银行流水号、证明附件与提交说明"; if (statusLabel !== "待复核" && statusLabel !== "待总账复核") row.dataset.resolved = "true"; + if (isAccount) row.dataset.accountStatus = record.status; const riskCell = document.createElement("td"); - const risk = document.createElement("span"); risk.className = "task-level warning"; risk.textContent = "中"; riskCell.append(risk); + riskCell.innerHTML = ''; + const identityCell = document.createElement("td"); - const identity = document.createElement("strong"); - const detail = document.createElement("small"); - const typeCell = document.createElement("td"); - const periodCell = document.createElement("td"); - const impactCell = document.createElement("td"); + const identity = document.createElement("span"); identity.className = "cell-main"; + const detail = document.createElement("span"); detail.className = "cell-sub"; if (isAccount) { identity.textContent = `${record.company_name} · ${record.bank_name} ${String(record.account_number).slice(-4)}`; detail.textContent = `${record.account_type} · 完整账号 ${record.account_number}`; - typeCell.textContent = "账户登记"; - periodCell.textContent = record.effective_from || "待审核确定"; - impactCell.textContent = "账户识别与流水上传"; } else { identity.textContent = `${record.company} · ${record.id}`; detail.textContent = `${record.direction} ${formatCurrency(record.amount)} 元 · ${record.counterparty} · ${record.subject}`; - typeCell.textContent = "手工记录"; - periodCell.textContent = record.transactionDate; - impactCell.textContent = `待确认往来 ${(Number(record.amount) / 10000).toFixed(2)} 万元`; } identityCell.append(identity, detail); + + const typeCell = document.createElement("td"); + typeCell.textContent = isAccount ? "账户登记" : "手工记录"; + + const periodCell = document.createElement("td"); + periodCell.textContent = isAccount ? (record.effective_from || "待审核确定") : record.transactionDate; + + const impactCell = document.createElement("td"); + impactCell.className = "wrap"; + impactCell.textContent = isAccount ? "账户识别与流水上传" : `待确认往来 ${(Number(record.amount) / 10000).toFixed(2)} 万元`; + const statusCell = document.createElement("td"); const status = recordStatus(statusLabel); - const badge = document.createElement("span"); badge.className = `status ${status.className}`; badge.textContent = status.label; statusCell.append(badge); + statusCell.innerHTML = `${status.label}`; + const actionCell = document.createElement("td"); - const action = document.createElement("button"); action.className = "text-button"; action.dataset.auditAction = ""; - if (isAccount) row.dataset.accountStatus = record.status; - action.textContent = row.dataset.resolved - ? (isAccount && record.status === "active" ? "管理" : "查看记录") - : "复核"; if (row.dataset.resolved) { - const decisionLabels = { active: "复核通过并启用账户", returned: "退回公司修改", disabled: "停用并驳回" }; - const decisionLabel = isAccount ? decisionLabels[record.status] || statusLabel : record.decision || record.status; - const reasonText = (isAccount ? record.review_reason : record.reviewReason) || "已留痕"; - action.dataset.record = `${decisionLabel} · ${reasonText}`; - action.dataset.decision = decisionLabel; - action.dataset.reason = reasonText; - action.dataset.processedAt = (isAccount ? record.reviewed_at : record.reviewedAt) || "时间未记录"; + const isActiveAccount = isAccount && record.status === "active"; + if (isActiveAccount) { + actionCell.innerHTML = ''; + } else { + const resolvedLabel = isAccount + ? ({ returned: "已退回", disabled: "已停用" })[record.status] || "已通过" + : (statusLabel === "已确认" ? "已通过" : "已驳回"); + actionCell.innerHTML = `${resolvedLabel} · 系统管理员`; + } + } else { + actionCell.innerHTML = '
'; } - actionCell.append(action); + row.append(riskCell, identityCell, typeCell, periodCell, impactCell, statusCell, actionCell); tbody.append(row); } @@ -1733,10 +864,14 @@ async function renderAdminAccountReviews() { if (!tbody) return; $$('[data-stored-review][data-record-kind="account"]', tbody).forEach((row) => row.remove()); const response = await fetch("/api/admin/accounts").catch(() => null); - if (!response?.ok) return; + if (!response?.ok) { + showToast("审核数据加载失败", "请稍后重试", "danger"); + return; + } const result = await response.json().catch(() => null); (result?.accounts || []).forEach((account) => appendAdminReviewRow(account, "account")); updateAuditCounts(); + updatePendingAccountNotice(); } function renderStoredAdminReviews() { @@ -1761,10 +896,18 @@ function updateAuditCounts() { $$('[data-audit-filter]').forEach((button) => { const type = button.dataset.auditFilter; const count = unresolved.filter((row) => type === "all" || row.dataset.auditType === type).length; - button.textContent = `${button.dataset.label} ${count}`; + const badge = $(".tab-count", button); + if (badge) badge.textContent = count; }); - const badge = $('.nav-item[data-view="audit"] b'); - if (badge) badge.textContent = unresolved.length; + const badge = $('.side-nav a[data-view="audit"] .nav-badge'); + if (badge) { + badge.textContent = unresolved.length; + badge.style.display = unresolved.length ? "" : "none"; + } + const pending = $("#pending-count"); + if (pending) pending.textContent = unresolved.length; + const foot = $("#auditFoot"); + if (foot) foot.textContent = `共 ${rows.length} 项 · 待审核 ${unresolved.length} 项`; } function companyStatusBadge(status) { @@ -1776,24 +919,42 @@ function companyStatusBadge(status) { function renderAdminCompanyTable(companies) { const tbody = $("#companyTable tbody"); if (!tbody) return; + state.companies = companies; tbody.replaceChildren(...companies.map((company) => { const row = document.createElement("tr"); + row.dataset.companyId = company.id; const nameCell = document.createElement("td"); - const name = document.createElement("strong"); name.textContent = company.name; - const code = document.createElement("small"); code.textContent = `COMP-${String(company.id).padStart(3, "0")}`; + const name = document.createElement("span"); name.className = "cell-main"; name.textContent = company.name; + const code = document.createElement("span"); code.className = "cell-sub"; code.textContent = company.credit_code || "统一社会信用代码待补充"; nameCell.append(name, code); - const credit = document.createElement("td"); credit.textContent = company.credit_code || "待补充"; - const accounts = document.createElement("td"); accounts.textContent = `${company.account_count ?? 0} 个`; - const usernames = document.createElement("td"); usernames.textContent = company.usernames || "未创建"; + const accounts = document.createElement("td"); accounts.className = "num-col"; accounts.textContent = `${company.account_count ?? 0}`; + const usernames = document.createElement("td"); usernames.className = "num"; usernames.textContent = company.usernames || "未创建"; const cashier = document.createElement("td"); cashier.textContent = company.cashier_name || "未指定"; const statusCell = document.createElement("td"); const badge = companyStatusBadge(company.status); - statusCell.innerHTML = `${badge.label}`; + statusCell.innerHTML = `${badge.label}`; const actionCell = document.createElement("td"); - actionCell.innerHTML = ``; - row.append(nameCell, credit, accounts, usernames, cashier, statusCell, actionCell); + actionCell.innerHTML = ``; + row.append(nameCell, accounts, usernames, cashier, statusCell, actionCell); return row; })); + const foot = $("#companyFoot"); + if (foot) foot.textContent = `共 ${companies.length} 家公司`; +} + +function updatePendingAccountNotice() { + const rows = $$('#auditRows tr[data-record-kind="account"]'); + const pending = rows.filter((r) => r.dataset.resolved !== "true"); + const notice = $("#notice-pending-account"); + if (!notice) return; + if (pending.length) { + notice.style.display = ""; + $("#pending-account-count").textContent = pending.length; + const names = [...new Set(pending.map((r) => r.dataset.company))].join("、"); + $("#pending-account-body").textContent = (names || "成员公司") + " 提交了新银行账户登记,等待审核通过后纳入账期流水归集范围。"; + } else { + notice.style.display = "none"; + } } function setSelectOptions(select, names, { keepFirst = false } = {}) { @@ -1814,34 +975,44 @@ function fillCompanySelects(names) { }); setSelectOptions($("#auditCompany"), names, { keepFirst: true }); setSelectOptions($("#flowCompany"), names, { keepFirst: true }); - setSelectOptions($('#reminderForm [name="company"]'), names, { keepFirst: true }); + const reminderList = $("#reminderCompanyList"); + if (reminderList) { + reminderList.replaceChildren(...names.map((name) => { + const label = document.createElement("label"); + label.className = "row"; + label.style.cssText = "gap:6px;font-size:13px;"; + const input = document.createElement("input"); + input.type = "checkbox"; + input.name = "company"; + input.value = name; + input.style.width = "auto"; + label.append(input, document.createTextNode(name)); + return label; + })); + } setSelectOptions($('#openingDialog [name="from"]'), names); setSelectOptions($('#openingDialog [name="to"]'), names); } async function loadAdminCompanies() { + const tbody = $("#companyTable tbody"); + showTableLoading(tbody, 6); const response = await fetch("/api/admin/companies").catch(() => null); - if (!response?.ok) return; + if (!response?.ok) { + showTableError(tbody, 6); + return; + } const result = await response.json().catch(() => null); const companies = result?.companies || []; renderAdminCompanyTable(companies); fillCompanySelects(companies.map((company) => company.name)); + if (companies.length >= 2) setPair(companies[0].name, companies[1].name); } function initAdmin() { renderStoredAdminReviews(); updateAuditCounts(); loadAdminCompanies(); - initBalanceQueries(); - loadCompanyOptions().then(() => { - loadAdminBalances($("#balanceLedgers")); - }); - loadAdminAuditQueue(); - const companySearch = $('[data-filter-target="companyLedgers"]'); - companySearch?.addEventListener("input", () => { - const query = companySearch.value.trim().toLowerCase(); - $$("#companyLedgers .company-ledger").forEach((item) => { item.hidden = query ? !item.textContent.toLowerCase().includes(query) : false; }); - }); let activeAuditType = "all"; function filterAuditRows() { @@ -1856,163 +1027,23 @@ function initAdmin() { activeAuditType = button.dataset.auditFilter; $$("[data-audit-filter]").forEach((item) => { const active = item === button; - item.classList.toggle("is-active", active); + item.classList.toggle("active", active); item.setAttribute("aria-pressed", String(active)); }); filterAuditRows(); })); $("#auditCompany")?.addEventListener("change", filterAuditRows); - const TRIAGE_DECISIONS = ["确认并纳入计算", "退回公司补充材料", "转为异常"]; - const auditDialog = $("#auditDialog"); - const syncExceptionNote = () => { - const note = $("#auditExceptionNote"); - const decision = $('#auditForm [name="decision"]'); - if (note) note.hidden = !String(decision?.value || "").includes("转为异常"); - }; - $$('[data-close-audit]').forEach((button) => button.addEventListener("click", () => auditDialog.close())); - $("#auditForm [name='decision']")?.addEventListener("change", syncExceptionNote); - // Delegated: account review rows arrive asynchronously from the API. - $("#auditRows")?.addEventListener("click", (event) => { - const button = event.target.closest("[data-audit-action]"); - if (!button) return; - const row = button.closest("tr"); - state.auditRow = row; - const cells = $$('td', row); - const form = $("#auditForm"); - const decision = $('[name="decision"]', form); - const reason = $('[name="reason"]', form); - const submit = $('button[type="submit"]', form); - form.reset(); - const isActiveAccount = row.dataset.recordKind === "account" && row.dataset.accountStatus === "active"; - const decisions = row.dataset.recordKind === "account" - ? (isActiveAccount ? ["停用并驳回"] : ["复核通过并启用账户", "退回公司修改", "停用并驳回"]) - : (row.dataset.recordKind === "subject-review" || row.dataset.recordKind === "manual-review") - ? TRIAGE_DECISIONS - : TRIAGE_DECISIONS; - decision.replaceChildren(new Option("请选择", ""), ...decisions.map((item) => new Option(item, item))); - decision.disabled = false; - reason.disabled = false; - submit.hidden = false; - $("#auditDialogTitle").textContent = `${cells[2].innerText.trim()} · ${cells[1].querySelector("strong").textContent}`; - $("#auditDialogMeta").textContent = `${cells[3].innerText.trim()} · 影响 ${cells[4].innerText.trim()}`; - const evidence = $("#auditEvidence"); - evidence.replaceChildren(); - fillAuditEvidence(evidence, row, cells); - syncExceptionNote(); - if (button.dataset.record && !isActiveAccount) { - decision.value = button.dataset.decision; - reason.value = button.dataset.reason; - decision.disabled = true; - reason.disabled = true; - submit.hidden = true; - $("#auditDialogMeta").textContent = `已处理 · ${button.dataset.processedAt} · 系统管理员`; - const record = document.createElement("small"); - record.textContent = `处理记录:${button.dataset.record}`; - evidence.append(record); - syncExceptionNote(); - } - auditDialog.showModal(); - }); - $("#auditForm")?.addEventListener("submit", async (event) => { - event.preventDefault(); - const data = new FormData(event.currentTarget); - const row = state.auditRow; - const decision = String(data.get("decision")); - const reason = String(data.get("reason")); + function openModal(id) { $("#" + id)?.classList.add("open"); } + function closeModal(id) { $("#" + id)?.classList.remove("open"); } + document.querySelectorAll("[data-close]").forEach((el) => el.addEventListener("click", () => closeModal(el.dataset.close))); + document.querySelectorAll(".modal-backdrop").forEach((bd) => bd.addEventListener("click", (e) => { if (e.target === bd) bd.classList.remove("open"); })); + document.addEventListener("keydown", (e) => { if (e.key === "Escape") document.querySelectorAll(".modal-backdrop.open").forEach((m) => m.classList.remove("open")); }); - // B-44 subject review: confirm uses the rule suggestion; return/exception park the event. - if (row.dataset.recordKind === "subject-review") { - const actionMap = { - "确认并纳入计算": "confirm", - "退回公司补充材料": "return", - "转为异常": "exception", - }; - const action = actionMap[decision]; - if (!action) { - showToast("请选择处理决定", "科目审核仅支持确认、退回或转异常"); - return; - } - const suggestions = JSON.parse(row.dataset.suggestions || "[]"); - const suggested = suggestions[0]; - if (action === "confirm" && !suggested?.suggested_subject_code) { - showToast("没有可入账的规则建议", "请退回公司补充材料或转为异常,不要猜测科目"); - return; - } - const body = { - perspective_company_id: Number(suggested?.suggested_perspective_company_id || row.dataset.perspectiveId), - reason, - expected_revision: Number(row.dataset.expectedRevision), - request_key: `subj-${row.dataset.ledgerEventId}-${Date.now().toString(36)}`, - }; - if (action === "confirm") { - body.subject_code = suggested.suggested_subject_code; - } else { - body.action = action; - } - const response = await apiJson(`/api/admin/intercompany/events/${row.dataset.ledgerEventId}/subject-decisions`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), - }).catch(() => null); - if (!response) { - showToast("科目处理失败", "请刷新后重试"); - return; - } - row.remove(); - updateAuditCounts(); - auditDialog.close(); - event.currentTarget.reset(); - syncExceptionNote(); - showToast( - action === "confirm" ? "科目已确认并纳入计算" : action === "return" ? "已退回公司补充材料" : "已转为异常", - action === "confirm" ? `确认科目:${b44.subjectLabels[suggested.suggested_subject_code]} · 已写入修订链` : "当前记录暂不纳入余额计算", - ); - await loadAdminAuditQueue(); - return; - } - - // B-44 manual record review: approve / return / exception. - if (row.dataset.recordKind === "manual-review") { - const actionMap = { - "确认并纳入计算": "approve_new", - "退回公司补充材料": "return", - "转为异常": "exception", - "转为异常待后续处理": "exception", - }; - const action = actionMap[decision]; - if (!action) { - showToast("请选择处理决定", "人工记录仅支持确认、退回或转异常"); - return; - } - const response = await apiJson(`/api/admin/manual-records/${row.dataset.recordId}/decisions`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - action, - reason, - expected_decision_id: Number(row.dataset.expectedDecisionId), - request_key: `mr-dec-${row.dataset.recordId}-${Date.now().toString(36)}`, - }), - }).catch(() => null); - if (!response) { - showToast("审核结果提交失败", "请刷新后重试"); - return; - } - const status = $("[data-audit-action]", row); - if (status) status.textContent = "查看记录"; - row.dataset.resolved = "true"; - const statusCell = row.children[5]; - statusCell.innerHTML = `${action === "approve_new" ? "已确认" : action === "return" ? "已退回" : "异常待处理"}`; - updateAuditCounts(); - auditDialog.close(); - event.currentTarget.reset(); - showToast("审核结果已记录", action === "approve_new" ? "已批准的手工记录将幂等纳入往来计算" : "当前记录不参与往来计算"); - await loadAdminAuditQueue(); - return; - } - - const approved = decision.includes("通过") || decision.includes("确认并纳入"); + async function submitAuditResult(row) { + const decision = state.auditDecision; + const reason = state.auditReason || ""; + const approved = decision.includes("通过") || decision.includes("确认并纳入") || decision.includes("启用"); const returned = decision.includes("退回"); let storedStatus; let reviewedAccount = null; @@ -2030,7 +1061,7 @@ function initAdmin() { } const result = await response?.json().catch(() => ({})); if (!response || !response.ok) { - showToast("审核结果提交失败", result?.message || "请稍后重试"); + showToast("审核结果提交失败", result?.message || "请稍后重试", "danger"); return; } reviewedAccount = result.account; @@ -2043,27 +1074,66 @@ function initAdmin() { } const status = recordStatus(storedStatus); const statusCell = row.children[5]; - statusCell.innerHTML = `${status.label}`; + statusCell.innerHTML = `${status.label}`; row.dataset.resolved = "true"; - const action = $("[data-audit-action]", row); - if (reviewedAccount) { - row.dataset.accountStatus = reviewedAccount.status; - action.textContent = reviewedAccount.status === "active" ? "管理" : "查看记录"; + if (reviewedAccount) row.dataset.accountStatus = reviewedAccount.status; + const actionCell = row.children[6]; + if (reviewedAccount && reviewedAccount.status === "active") { + actionCell.innerHTML = ''; } else { - action.textContent = "查看记录"; + actionCell.innerHTML = `${approved ? "已通过" : "已驳回"} · 系统管理员`; } - action.dataset.record = `${decision} · ${data.get("reason")}`; - action.dataset.decision = decision; - action.dataset.reason = data.get("reason"); - action.dataset.processedAt = new Date().toLocaleString("zh-CN", { hour12: false }); updateAuditCounts(); - auditDialog.close(); - event.currentTarget.reset(); - showToast("审核结果已记录", approved ? "已通过的数据取得相应使用或核算资格" : "当前记录不参与自动核算"); + showToast("审核结果已记录", approved ? "已通过的数据取得相应使用或核算资格" : "当前记录不参与自动核算", "success"); + } + + $("#auditRows")?.addEventListener("click", (event) => { + const button = event.target.closest("[data-audit-action]"); + if (!button) return; + const row = button.closest("tr"); + state.auditRow = row; + const cells = $$("td", row); + const company = cells[1]?.querySelector(".cell-main")?.textContent.trim() || ""; + const period = cells[3]?.textContent.trim() || ""; + const type = cells[2]?.textContent.trim() || ""; + const basis = row.dataset.evidence || "银行原始行、导入批次、账户覆盖区间与公司说明"; + const action = button.dataset.auditAction; + if (action === "approve") { + $("#approve-company").textContent = company; + $("#approve-period").textContent = period; + $("#approve-type").textContent = type; + $("#approve-basis").textContent = basis; + openModal("modal-approve"); + } else if (action === "reject") { + $("#reject-reason").value = ""; + $("#reject-hint").style.display = "none"; + openModal("modal-reject"); + } else if (action === "disable") { + state.auditDecision = "停用并驳回"; + state.auditReason = "停用并驳回该账户"; + submitAuditResult(row); + } }); - const dialog = $("#companyDialog"); - $("#openCompanyDialog")?.addEventListener("click", () => dialog.showModal()); + $("#approve-confirm")?.addEventListener("click", () => { + if (!state.auditRow) return; + closeModal("modal-approve"); + state.auditDecision = state.auditRow.dataset.recordKind === "account" ? "复核通过并启用账户" : "确认并纳入计算"; + state.auditReason = state.auditDecision; + submitAuditResult(state.auditRow); + }); + + $("#reject-confirm")?.addEventListener("click", () => { + if (!state.auditRow) return; + const reason = $("#reject-reason").value.trim(); + if (reason.length < 5) { $("#reject-hint").style.display = ""; return; } + closeModal("modal-reject"); + state.auditDecision = state.auditRow.dataset.recordKind === "account" ? "退回公司修改" : "退回公司补充材料"; + state.auditReason = reason; + submitAuditResult(state.auditRow); + }); + + $("#openCompanyDialog")?.addEventListener("click", () => openModal("companyDialog")); $("#companyForm")?.addEventListener("submit", async (event) => { event.preventDefault(); const data = new FormData(event.currentTarget); @@ -2089,98 +1159,359 @@ function initAdmin() { } const result = await response?.json().catch(() => ({})); if (!response || !response.ok) { - showToast("公司创建失败", result?.message || "请稍后重试"); + showToast("公司创建失败", result?.message || "请稍后重试", "danger"); return; } const accountCreated = Boolean(result.username); - dialog.close(); + closeModal("companyDialog"); event.currentTarget.reset(); await loadAdminCompanies(); showToast( accountCreated ? "公司与账号已创建" : "公司已创建", accountCreated ? `账号 ${result.username} 的初始密码已生成(仅此一次显示):${result.initial_password},首次登录必须修改` : "可稍后在账号管理中创建公司账号", + "success", ); }); + function openCompanyDetail(companyId) { + const company = (state.companies || []).find((c) => String(c.id) === String(companyId)); + if (!company) return; + $("#d-title").textContent = company.name; + $("#d-sub").textContent = `统一社会信用代码 ${company.credit_code || "待补充"}`; + $("#d-kv").innerHTML = + '
公司名称
' + company.name + '
' + + '
统一社会信用代码
' + (company.credit_code || "待补充") + '
' + + '
出纳
' + (company.cashier_name || "未指定") + '
' + + '
银行账户
' + (company.account_count ?? 0) + ' 个
' + + '
状态
' + companyStatusBadge(company.status).label + '
'; + $("#d-login-account").textContent = company.usernames || "未创建公司账号"; + $("#d-login-meta").textContent = company.usernames ? "公司端登录账号" : "该公司尚未创建登录账号"; + const resetBtn = $("#btn-reset-pwd"); + resetBtn.disabled = !company.usernames; + resetBtn.textContent = "重置密码"; + resetBtn.dataset.companyId = company.id; + openModal("modal-detail"); + } + + $("#companyTable tbody")?.addEventListener("click", (event) => { + const btn = event.target.closest("[data-company-view]"); + if (btn) openCompanyDetail(btn.dataset.companyView); + }); + + $("#btn-reset-pwd")?.addEventListener("click", async () => { + const companyId = $("#btn-reset-pwd").dataset.companyId; + const usersResponse = await fetch("/api/admin/users").catch(() => null); + const usersResult = await usersResponse?.json().catch(() => ({})); + const user = (usersResult?.users || []).find((u) => String(u.company_id) === String(companyId) && u.role === "company"); + if (!user) { showToast("重置失败", "该公司尚无公司登录账号", "danger"); return; } + const response = await fetch(`/api/admin/users/${user.id}/reset-password`, { method: "POST" }).catch(() => null); + if (response?.status === 401) { window.location.href = "index.html"; return; } + const result = await response?.json().catch(() => ({})); + if (!response || !response.ok) { showToast("重置失败", result?.message || "请稍后重试", "danger"); return; } + $("#btn-reset-pwd").disabled = true; + $("#btn-reset-pwd").textContent = "已重置"; + $("#d-login-meta").textContent = `临时密码已生成(仅此一次):${result.initial_password},首次登录必须修改`; + showToast("密码已重置", "临时密码仅本次显示,首次登录必须修改", "success"); + }); + + const remind = $("#cs-remind"); + const track = $("#cs-switch-track"); + const thumb = $("#cs-switch-thumb"); + const daysSel = $("#cs-remind-days"); + function renderSwitch() { + if (!remind) return; + track.style.background = remind.checked ? "var(--accent)" : "var(--fg-soft)"; + thumb.style.left = remind.checked ? "18px" : "2px"; + if (daysSel) daysSel.disabled = !remind.checked; + } + remind?.addEventListener("change", renderSwitch); + renderSwitch(); + $("#systemSettings")?.addEventListener("submit", (event) => { event.preventDefault(); - showToast("系统计算口径已保存", "正式系统将记录修改前后值与操作人"); + const day = parseInt($("#cs-day")?.value, 10); + const tip = $("#cs-save-tip"); + if (day && (day < 1 || day > 28)) { + if (tip) { tip.style.display = ""; tip.style.color = "var(--danger)"; tip.textContent = "结账日须为 1-28 之间的整数"; } + return; + } + if (tip) { tip.style.display = ""; tip.style.color = "var(--success)"; tip.textContent = "已保存 · 立即生效"; } + showToast("系统计算口径已保存", "正式系统将记录修改前后值与操作人", "success"); }); + $("#runClosingCheck")?.addEventListener("click", () => { - const unresolved = $$(".audit-table tbody tr").filter((row) => row.dataset.resolved !== "true" && !$(".status.success", row)).length; + const unresolved = $$(".audit-table tbody tr").filter((row) => row.dataset.resolved !== "true").length; + const blockNotice = $("#block-notice"); if (unresolved) { - showToast("结账检查未通过", `仍有 ${unresolved} 项审核事项,已打开审核中心`); + if (blockNotice) blockNotice.style.display = ""; + $("#closingDescription").textContent = `2026 年 7 月 · 仍有 ${unresolved} 项审核事项未处理`; + $("#closingStatus").className = "pill pill-danger"; + $("#closingStatus").textContent = "已阻断"; + $("#executeClosing").disabled = true; + showToast("结账检查未通过", `仍有 ${unresolved} 项审核事项,已打开审核中心`, "warn"); showView("audit"); } else { - $$("#closingPanel .is-blocked").forEach((item) => { - item.classList.remove("is-blocked"); - $("use", item).setAttribute("href", "icons.svg#circle-check"); - $("small", item).textContent = "检查已通过"; + if (blockNotice) blockNotice.style.display = "none"; + $$("#closingPanel [data-closing-state]").forEach((item) => { + item.className = "pill pill-success"; + item.textContent = "已通过"; }); $("#closingDescription").textContent = "2026 年 7 月 · 全部前置检查已通过"; - $("#closingStatus").className = "status success"; + $("#closingStatus").className = "pill pill-success"; $("#closingStatus").textContent = "可结账"; $("#executeClosing").disabled = false; $("#executeClosing").removeAttribute("title"); - showToast("结账检查通过", "执行结账按钮已解锁"); + showToast("结账检查通过", "执行结账按钮已解锁", "success"); } }); - const closingDialog = $("#closingDialog"); - $("#executeClosing")?.addEventListener("click", () => closingDialog.showModal()); - $$('[data-close-closing]').forEach((button) => button.addEventListener("click", () => closingDialog.close())); + + $("#executeClosing")?.addEventListener("click", () => openModal("closingDialog")); + $$("[data-close-closing]").forEach((button) => button.addEventListener("click", () => closeModal("closingDialog"))); $("#closingForm")?.addEventListener("submit", (event) => { event.preventDefault(); - closingDialog.close(); + closeModal("closingDialog"); $("#closingDescription").textContent = "2026 年 7 月 · 已完成集团结账"; - $("#closingStatus").className = "status success"; + $("#closingStatus").className = "pill pill-success"; $("#closingStatus").textContent = "已结账"; $("#closingHistory").textContent = `${new Date().toLocaleString("zh-CN", { hour12: false })} · 系统管理员执行 2026 年 7 月结账 · 已写入审计记录`; $("#runClosingCheck").disabled = true; $("#executeClosing").disabled = true; $("#executeClosing").textContent = "7 月已结账"; - showToast("2026 年 7 月已完成结账", "本期结果已锁定,后续补录将进入重开流程"); + const closedNotice = $("#closed-notice"); + if (closedNotice) closedNotice.style.display = ""; + const tlCurrent = $("#tl-current"); + tlCurrent?.classList.remove("current"); + tlCurrent?.classList.add("closed"); + $("#tl-current-state").textContent = "已结账"; + showToast("2026 年 7 月已完成结账", "本期结果已锁定,后续补录将进入重开流程", "success"); }); - const openingDialog = $("#openingDialog"); - $("#openOpeningDialog")?.addEventListener("click", () => openingDialog.showModal()); - $$('[data-close-opening]').forEach((button) => button.addEventListener("click", () => openingDialog.close())); + + $("#openOpeningDialog")?.addEventListener("click", () => openModal("openingDialog")); + $$("[data-close-opening]").forEach((button) => button.addEventListener("click", () => closeModal("openingDialog"))); $("#openingForm")?.addEventListener("submit", (event) => { event.preventDefault(); const data = new FormData(event.currentTarget); if (data.get("from") === data.get("to")) { - showToast("本方与对方不能相同", "同公司账户余额不属于公司间期初"); + showToast("本方与对方不能相同", "同公司账户余额不属于公司间期初", "warn"); return; } const row = document.createElement("tr"); - [data.get("from"), data.get("to"), data.get("subject"), data.get("direction")].forEach((value) => { - const cell = document.createElement("td"); cell.textContent = value; row.append(cell); - }); - const amount = document.createElement("td"); amount.className = "number"; amount.textContent = Number(data.get("amount")).toLocaleString("zh-CN", {minimumFractionDigits:2}); row.append(amount); - const date = document.createElement("td"); date.textContent = data.get("effectiveDate"); row.append(date); - const status = document.createElement("td"); status.innerHTML = '待复核'; row.append(status); + const self = document.createElement("td"); self.className = "cell-main"; self.textContent = data.get("from"); row.append(self); + const peer = document.createElement("td"); peer.textContent = data.get("to"); row.append(peer); + const subject = document.createElement("td"); subject.textContent = data.get("subject"); row.append(subject); + const direction = document.createElement("td"); direction.textContent = data.get("direction"); row.append(direction); + const amount = document.createElement("td"); amount.className = "num-col"; amount.textContent = Number(data.get("amount")).toLocaleString("zh-CN", { minimumFractionDigits: 2 }); row.append(amount); + const date = document.createElement("td"); date.className = "num"; date.textContent = data.get("effectiveDate"); row.append(date); + const status = document.createElement("td"); status.innerHTML = '待复核'; row.append(status); $("#openingRows").append(row); - openingDialog.close(); + closeModal("openingDialog"); event.currentTarget.reset(); - showToast("期初余额已提交复核", "正式系统将保留录入依据与操作人"); + showToast("期初余额已提交复核", "正式系统将保留录入依据与操作人", "success"); }); + $("#reminderForm")?.addEventListener("submit", (event) => { event.preventDefault(); - const data = new FormData(event.currentTarget); - const article = document.createElement("article"); - article.innerHTML = ''; - const content = document.createElement("span"); - const heading = document.createElement("strong"); - heading.textContent = `${data.get("company")} · ${data.get("message")}`; - const meta = document.createElement("small"); - meta.textContent = `手动提醒 · 截止 ${data.get("dueDate")} · 刚刚`; - content.append(heading, meta); - const status = document.createElement("em"); - status.className = "status warning"; - status.textContent = "未读"; - article.append(content, status); - $("#adminReminderList").prepend(article); - event.currentTarget.reset(); - showToast("提醒已发送", "对方将在公司业务端收到站内通知"); + const form = event.currentTarget; + const checked = Array.prototype.slice.call(form.querySelectorAll('input[name="company"]:checked')); + const type = $("#reminder-type").value; + const content = $("#reminder-content").value.trim(); + const deadline = $("#reminder-deadline").value; + $("#company-error").style.display = checked.length ? "none" : ""; + $("#content-error").style.display = content ? "none" : ""; + if (!checked.length || !content) return; + const companies = checked.map((c) => c.value).join("、"); + const summary = content.length > 46 ? content.slice(0, 46) + "…" : content; + const row = document.createElement("tr"); + row.dataset.source = "manual"; + row.innerHTML = + '' + companies + '' + + '' + type + '' + + '' + summary.replace(/' + + '刚刚人工 · 系统管理员' + + '' + (deadline || "—") + '' + + '未读' + + '
'; + $("#reminder-tbody").insertBefore(row, $("#reminder-tbody").firstChild); + refreshReminderCounts(); + form.querySelectorAll('input[name="company"]').forEach((c) => { c.checked = false; }); + $("#reminder-content").value = "请于截止日期前完成 2026 年 7 月银行流水上传与待确认事项处理。"; + $("#reminder-deadline").value = "2026-08-29"; + const sendHint = $("#send-hint"); + sendHint.style.display = ""; + sendHint.textContent = "已发送给 " + companies + ",共 " + checked.length + " 家公司。"; + clearTimeout(sendHint._t); + sendHint._t = setTimeout(() => { sendHint.style.display = "none"; }, 4000); }); + + // 提醒历史 tabs / 计数 + function refreshReminderCounts() { + const rows = $$("#reminder-tbody tr"); + let all = rows.length, sys = 0, man = 0; + rows.forEach((r) => { if (r.dataset.source === "system") sys++; else man++; }); + $("#count-all").textContent = all; + $("#count-system").textContent = sys; + $("#count-manual").textContent = man; + $("#table-foot-count").textContent = "共 " + all + " 条提醒记录"; + } + $("#reminder-tabs")?.addEventListener("click", (event) => { + const btn = event.target.closest("button[data-filter]"); + if (!btn) return; + $("#reminder-tabs").querySelectorAll("button").forEach((b) => b.classList.remove("active")); + btn.classList.add("active"); + const filter = btn.dataset.filter; + $$("#reminder-tbody tr").forEach((r) => { r.style.display = (filter === "all" || r.dataset.source === filter) ? "" : "none"; }); + }); + + $("#reminder-tbody")?.addEventListener("click", (event) => { + const remindBtn = event.target.closest(".act-remind"); + if (remindBtn) { + const tr = remindBtn.closest("tr"); + const timeCell = tr.children[3]; + const sub = timeCell.querySelector(".cell-sub"); + timeCell.firstChild.textContent = "刚刚"; + if (sub) sub.textContent = sub.textContent.replace(/\s*·?\s*第 \d+ 次/, "") + " · 再提醒"; + else timeCell.insertAdjacentHTML("beforeend", '再提醒'); + remindBtn.textContent = "已再提醒"; + remindBtn.disabled = true; + return; + } + const historyBtn = event.target.closest(".act-history"); + if (historyBtn) openReminderHistory(historyBtn.dataset.company); + }); + + const reminderHISTORIES = { + "金牛农业": [ + { time: "2026-08-18 09:02", src: "系统 · 第 2 次", type: "流水未提交", status: "danger", statusText: "未读", body: "贵公司 7 月全部银行账户流水尚未提交,距 7 月结账顺延截止日仅剩 11 天,请尽快上传。" }, + { time: "2026-08-11 09:00", src: "系统 · 第 1 次", type: "流水未提交", status: "danger", statusText: "未读", body: "2026 年 7 月银行流水上传提醒:请于截止日前完成全部账户流水提交。" }, + ], + "金牛新能源": [ + { time: "2026-08-15 09:00", src: "系统", type: "单边待确认", status: "warn", statusText: "处理中", body: "与金牛贸易 3 笔单边流水合计 ¥4,860,000.00 待确认,请选择对方银行流水佐证。" }, + ], + "金牛置业": [ + { time: "2026-08-14 16:40", src: "人工 · 张维", type: "流水未提交", status: "warn", statusText: "处理中", body: "中行尾号 8821 账户 7 月 6—16 日流水断档,请补传该期间银行回单或对账单。" }, + ], + "金牛贸易": [ + { time: "2026-08-15 09:00", src: "系统", type: "单边待确认", status: "success", statusText: "已完成", body: "与金牛新能源往来 3 笔单边流水待确认,请核对 7 月销售回款记录。" }, + ], + "金牛煤业": [ + { time: "2026-08-12 09:00", src: "系统", type: "科目待确认", status: "warn", statusText: "处理中", body: "2 笔手工记录往来科目待确认(应收 / 其他应收),涉及煤炭运费结算。" }, + ], + "金牛物流": [ + { time: "2026-08-10 11:25", src: "人工 · 张维", type: "科目待确认", status: "success", statusText: "已完成", body: "1 笔运费结算手工记录科目待确认(其他应付待判定),请于结账前完成。" }, + ], + }; + + function openReminderHistory(company) { + $("#history-title").textContent = company + " · 提醒历史"; + $("#history-sub").textContent = "2026 年 7 月账期以来的全部触达记录,按时间倒序。"; + const list = $("#history-list"); + const items = reminderHISTORIES[company] || []; + if (!items.length) { + list.innerHTML = '
暂无历史记录
该公司在 7 月账期内尚未收到过提醒。
'; + } else { + list.innerHTML = items.map((it) => { + return '
' + + '' + it.type + '' + + '
' + it.body + '
' + it.time + ' · ' + it.src + '
' + + '
' + it.statusText + '
'; + }).join(""); + } + openModal("history-modal"); + } + $("#history-close")?.addEventListener("click", () => closeModal("history-modal")); + $("#history-ok")?.addEventListener("click", () => closeModal("history-modal")); + + refreshReminderCounts(); +} + +const FLOW_DEMO = [ + { date: "2026-07-01", company: "金牛煤业", bank: "工行", account: "3305", acctLabel: "工行 · 尾号 3305", dir: "收", dirPill: "pill-success", peer: "平顶山市恒源电力燃料有限公司", summary: "煤炭销售款(6 月结算)", serial: "ICBC202607010031825", status: "未归集", statusPill: "pill-muted", amount: "1,860,000.00", amtClass: "amt-in", detail: { bank: "工商银行", account: "河南金牛煤业有限公司 · 基本户 1702 0231 0900 8133 05", time: "2026-07-01 09:42:17", peer: "平顶山市恒源电力燃料有限公司", peerAcct: "工行平顶山分行 1702 0218 0902 6641 20", amount: "¥ 1,860,000.00(收)", status: "未归集", pair: "—(外部单位交易)", subject: "—", batch: "—", note: "对方为集团外客户,不进入内部往来归集,仅作银行流水留档。" } }, + { date: "2026-07-03", company: "金牛新能源", bank: "工行", account: "6649", acctLabel: "工行 · 尾号 6649", dir: "付", dirPill: "pill-danger", peer: "河南金牛贸易有限公司", summary: "光伏支架材料款", serial: "ICBC202607030094417", status: "单边", statusPill: "pill-danger", amount: "1,620,000.00", amtClass: "amt-out", detail: { bank: "工商银行", account: "河南金牛新能源有限公司 · 一般户 1704 0512 0900 2266 49", time: "2026-07-03 14:08:52", peer: "河南金牛贸易有限公司", peerAcct: "农行郑州金水支行 1606 3301 0400 0220 8", amount: "¥ 1,620,000.00(付)", status: "单边", pair: "金牛新能源 ↔ 金牛贸易", subject: "其他应付(新能源侧)", batch: "UNI-2026-07-002", note: "贸易侧 7 月上报流水中未找到对应收款,已挂起为单边流水,待贸易侧补充银行凭证佐证。" } }, + { date: "2026-07-03", company: "金牛置业", bank: "中行", account: "8821", acctLabel: "中行 · 尾号 8821", dir: "收", dirPill: "pill-success", peer: "郑州市商品房预售资金监管专户", summary: "商品房预售款(A 区 12 号楼)", serial: "BOC202607030552108", status: "未归集", statusPill: "pill-muted", amount: "4,150,000.00", amtClass: "amt-in", detail: { bank: "中国银行", account: "河南金牛置业有限公司 · 一般户 2546 0387 0200 8821", time: "2026-07-03 10:26:31", peer: "郑州市商品房预售资金监管专户", peerAcct: "中行郑州郑东新区支行 2546 1180 0200 3477", amount: "¥ 4,150,000.00(收)", status: "未归集", pair: "—(外部单位交易)", subject: "—", batch: "—", note: "预售监管资金划入,属对外经营收款,不参与集团内部往来归集。" } }, + { date: "2026-07-05", company: "金牛煤业", bank: "工行", account: "3305", acctLabel: "工行 · 尾号 3305", dir: "付", dirPill: "pill-danger", peer: "河南金牛物流有限公司", summary: "矿区运输费(6 月)", serial: "ICBC202607050127663", status: "已归集", statusPill: "pill-success", amount: "1,240,000.00", amtClass: "amt-out", detail: { bank: "工商银行", account: "河南金牛煤业有限公司 · 基本户 1702 0231 0900 8133 05", time: "2026-07-05 11:15:09", peer: "河南金牛物流有限公司", peerAcct: "建行郑州经开区支行 4105 0167 8080 5562", amount: "¥ 1,240,000.00(付)", status: "已归集", pair: "金牛煤业 ↔ 金牛物流", subject: "应付(煤业侧)", batch: "JC-2026-07-014", note: "与物流侧建行尾号 5562 账户 07-05 收款流水双向匹配,金额一致。" } }, + { date: "2026-07-05", company: "金牛物流", bank: "建行", account: "5562", acctLabel: "建行 · 尾号 5562", dir: "收", dirPill: "pill-success", peer: "河南金牛煤业有限公司", summary: "矿区运输费(6 月)", serial: "CCB202607050312940", status: "已归集", statusPill: "pill-success", amount: "1,240,000.00", amtClass: "amt-in", detail: { bank: "建设银行", account: "河南金牛物流有限公司 · 基本户 4105 0167 8080 5562", time: "2026-07-05 11:15:36", peer: "河南金牛煤业有限公司", peerAcct: "工行平顶山分行 1702 0231 0900 8133 05", amount: "¥ 1,240,000.00(收)", status: "已归集", pair: "金牛物流 ↔ 金牛煤业", subject: "应收(物流侧)", batch: "JC-2026-07-014", note: "与煤业侧工行尾号 3305 账户 07-05 付款流水双向匹配,金额一致。" } }, + { date: "2026-07-08", company: "金牛煤业", bank: "工行", account: "3305", acctLabel: "工行 · 尾号 3305", dir: "收", dirPill: "pill-success", peer: "河南金牛置业有限公司", summary: "煤炭采购款(7 月)", serial: "ICBC202607080208554", status: "待确认", statusPill: "pill-warn", amount: "3,200,000.00", amtClass: "amt-in", detail: { bank: "工商银行", account: "河南金牛煤业有限公司 · 基本户 1702 0231 0900 8133 05", time: "2026-07-08 15:47:22", peer: "河南金牛置业有限公司", peerAcct: "中行郑州郑东新区支行 2546 0387 0200 8821", amount: "¥ 3,200,000.00(收)", status: "待确认", pair: "金牛煤业 ↔ 金牛置业", subject: "应收(煤业侧)", batch: "—(待归集)", note: "置业中行尾号 8821 账户 07-06 至 07-16 流水断档,对方付款凭证缺失,暂无法完成双边匹配,已列入审核中心高风险事项。" } }, + { date: "2026-07-09", company: "金牛贸易", bank: "农行", account: "2208", acctLabel: "农行 · 尾号 2208", dir: "收", dirPill: "pill-success", peer: "洛阳建工集团有限公司", summary: "钢材销售款(6 月发货)", serial: "ABC202607090773261", status: "未归集", statusPill: "pill-muted", amount: "2,480,000.00", amtClass: "amt-in", detail: { bank: "农业银行", account: "河南金牛贸易有限公司 · 基本户 1606 3301 0400 0220 8", time: "2026-07-09 09:58:44", peer: "洛阳建工集团有限公司", peerAcct: "中行洛阳分行 2546 2201 0500 7915", amount: "¥ 2,480,000.00(收)", status: "未归集", pair: "—(外部单位交易)", subject: "—", batch: "—", note: "对外钢材销售回款,不参与集团内部往来归集。" } }, + { date: "2026-07-11", company: "金牛新能源", bank: "工行", account: "6649", acctLabel: "工行 · 尾号 6649", dir: "付", dirPill: "pill-danger", peer: "河南金牛贸易有限公司", summary: "电缆及配电柜采购款", serial: "ICBC202607110158902", status: "单边", statusPill: "pill-danger", amount: "1,950,000.00", amtClass: "amt-out", detail: { bank: "工商银行", account: "河南金牛新能源有限公司 · 一般户 1704 0512 0900 2266 49", time: "2026-07-11 16:32:08", peer: "河南金牛贸易有限公司", peerAcct: "农行郑州金水支行 1606 3301 0400 0220 8", amount: "¥ 1,950,000.00(付)", status: "单边", pair: "金牛新能源 ↔ 金牛贸易", subject: "其他应付(新能源侧)", batch: "UNI-2026-07-005", note: "贸易侧无对应收款记录,单边挂起。新能源↔贸易本月累计 3 笔单边流水,合计 486 万元。" } }, + { date: "2026-07-14", company: "金牛煤业", bank: "中行", account: "9916", acctLabel: "中行 · 尾号 9916", dir: "付", dirPill: "pill-danger", peer: "平顶山天安煤业设备租赁有限公司", summary: "综采设备租赁费(7 月)", serial: "BOC202607140416337", status: "未归集", statusPill: "pill-muted", amount: "920,000.00", amtClass: "amt-out", detail: { bank: "中国银行", account: "河南金牛煤业有限公司 · 一般户 2546 0387 1100 9916", time: "2026-07-14 10:11:57", peer: "平顶山天安煤业设备租赁有限公司", peerAcct: "建行平顶山分行 4105 0229 8080 1347", amount: "¥ 920,000.00(付)", status: "未归集", pair: "—(外部单位交易)", subject: "—", batch: "—", note: "综采设备月度租赁支出,对方为集团外供应商,不参与内部归集。" } }, + { date: "2026-07-18", company: "金牛煤业", bank: "交行", account: "7710", acctLabel: "交行 · 尾号 7710", dir: "付", dirPill: "pill-danger", peer: "平顶山市安泰矿山设备有限公司", summary: "提升机大修款", serial: "BOCOM202607180062194", status: "待确认", statusPill: "pill-warn", amount: "685,400.00", amtClass: "amt-out", detail: { bank: "交通银行", account: "河南金牛煤业有限公司 · 一般户 4110 6120 0181 0077 10(账户待审核)", time: "2026-07-18 13:29:40", peer: "平顶山市安泰矿山设备有限公司", peerAcct: "工行平顶山分行 1702 0218 0902 9075 63", amount: "¥ 685,400.00(付)", status: "待确认", pair: "—(外部单位交易)", subject: "—", batch: "—", note: "付款账户(交行尾号 7710)为新开户,尚在账户审核流程中,流水暂挂待确认,审核通过后自动归档为外部交易。" } }, + { date: "2026-07-21", company: "金牛置业", bank: "中行", account: "8821", acctLabel: "中行 · 尾号 8821", dir: "收", dirPill: "pill-success", peer: "郑州市商品房预售资金监管专户", summary: "商品房预售款(A 区 15 号楼)", serial: "BOC202607210588420", status: "未归集", statusPill: "pill-muted", amount: "3,780,000.00", amtClass: "amt-in", detail: { bank: "中国银行", account: "河南金牛置业有限公司 · 一般户 2546 0387 0200 8821", time: "2026-07-21 09:35:12", peer: "郑州市商品房预售资金监管专户", peerAcct: "中行郑州郑东新区支行 2546 1180 0200 3477", amount: "¥ 3,780,000.00(收)", status: "未归集", pair: "—(外部单位交易)", subject: "—", batch: "—", note: "预售监管资金划入。该账户 07-06 至 07-16 存在流水断档,本笔为断档后首笔入账。" } }, + { date: "2026-07-22", company: "金牛物流", bank: "建行", account: "5562", acctLabel: "建行 · 尾号 5562", dir: "收", dirPill: "pill-success", peer: "河南金牛贸易有限公司", summary: "钢材干线运输费(6-7 月)", serial: "CCB202607220347815", status: "已归集", statusPill: "pill-success", amount: "462,800.00", amtClass: "amt-in", detail: { bank: "建设银行", account: "河南金牛物流有限公司 · 基本户 4105 0167 8080 5562", time: "2026-07-22 14:52:26", peer: "河南金牛贸易有限公司", peerAcct: "农行郑州金水支行 1606 3301 0400 0220 8", amount: "¥ 462,800.00(收)", status: "已归集", pair: "金牛物流 ↔ 金牛贸易", subject: "应收(物流侧)", batch: "JC-2026-07-021", note: "与贸易侧农行尾号 2208 账户 07-22 付款流水双向匹配,金额一致,已计入 7 月往来批次。" } }, + { date: "2026-07-24", company: "金牛新能源", bank: "工行", account: "6649", acctLabel: "工行 · 尾号 6649", dir: "付", dirPill: "pill-danger", peer: "河南金牛贸易有限公司", summary: "组件辅材结算款", serial: "ICBC202607240221476", status: "单边", statusPill: "pill-danger", amount: "1,290,000.00", amtClass: "amt-out", detail: { bank: "工商银行", account: "河南金牛新能源有限公司 · 一般户 1704 0512 0900 2266 49", time: "2026-07-24 11:06:33", peer: "河南金牛贸易有限公司", peerAcct: "农行郑州金水支行 1606 3301 0400 0220 8", amount: "¥ 1,290,000.00(付)", status: "单边", pair: "金牛新能源 ↔ 金牛贸易", subject: "其他应付(新能源侧)", batch: "UNI-2026-07-009", note: "贸易侧无对应收款记录,单边挂起。新能源↔贸易本月累计 3 笔单边流水,合计 486 万元。" } }, + { date: "2026-07-25", company: "金牛贸易", bank: "农行", account: "2208", acctLabel: "农行 · 尾号 2208", dir: "付", dirPill: "pill-danger", peer: "安阳钢铁集团有限责任公司", summary: "螺纹钢采购款(7 月)", serial: "ABC202607250819673", status: "未归集", statusPill: "pill-muted", amount: "5,620,000.00", amtClass: "amt-out", detail: { bank: "农业银行", account: "河南金牛贸易有限公司 · 基本户 1606 3301 0400 0220 8", time: "2026-07-25 10:19:05", peer: "安阳钢铁集团有限责任公司", peerAcct: "工行安阳分行 1706 0211 0900 4428 17", amount: "¥ 5,620,000.00(付)", status: "未归集", pair: "—(外部单位交易)", subject: "—", batch: "—", note: "对外螺纹钢采购付款,不参与集团内部往来归集。" } }, +]; + +// 原型占位,非本司真流水:COMPANY_FLOWS 仅用于演示公司端流水列表界面, +// 真实流水来自 /api/parse 导入与 /api/batches 批次,切勿把它当成真数据源。 +const COMPANY_FLOWS = [ + { date: "2026-07-02", company: "金牛煤业", bank: "工商银行", account: "3305", acctLabel: "工行 · 尾号 3305", dir: "收", dirPill: "pill-success", peer: "河南金牛置业有限公司", summary: "煤炭采购款(2026 年 6 月供煤合同结算)", serial: "ICBC2026070200185347", status: "已归集", statusPill: "pill-success", amount: "3,200,000.00", amtClass: "amt-in", detail: { bank: "工商银行", account: "河南金牛煤业有限公司 · 基本户 1702 0231 0900 8133 05", time: "2026-07-02 15:47:22", peer: "河南金牛置业有限公司", peerAcct: "中行郑州郑东新区支行 2546 0387 0200 8821", amount: "¥ 3,200,000.00(收)", status: "已归集", pair: "金牛煤业 ↔ 金牛置业", subject: "应收(煤业侧)", batch: "JH-202607-014", note: "与置业侧中行尾号 8821 账户付款流水已双向匹配。" } }, + { date: "2026-07-03", company: "金牛煤业", bank: "工商银行", account: "3305", acctLabel: "工行 · 尾号 3305", dir: "付", dirPill: "pill-danger", peer: "山西晋城王坡煤矿有限责任公司", summary: "原料煤采购预付款", serial: "ICBC2026070300221091", status: "未归集 · 外部", statusPill: "pill-muted", amount: "860,000.00", amtClass: "amt-out", detail: { bank: "工商银行", account: "河南金牛煤业有限公司 · 基本户 1702 0231 0900 8133 05", time: "2026-07-03 10:26:31", peer: "山西晋城王坡煤矿有限责任公司", peerAcct: "工行晋城分行 1702 0218 0902 6641 20", amount: "¥ 860,000.00(付)", status: "未归集 · 外部", pair: "—(外部单位交易)", subject: "—", batch: "—", note: "对方为集团外供应商,不参与内部往来归集,仅作银行流水留档。" } }, + { date: "2026-07-06", company: "金牛煤业", bank: "中国银行", account: "9916", acctLabel: "中行 · 尾号 9916", dir: "收", dirPill: "pill-success", peer: "河南神火运销有限公司", summary: "动力煤销售货款(7 月第一批)", serial: "BOC2026070600772018", status: "未归集 · 外部", statusPill: "pill-muted", amount: "1,246,800.00", amtClass: "amt-in", detail: { bank: "中国银行", account: "河南金牛煤业有限公司 · 一般户 2546 0387 1100 9916", time: "2026-07-06 09:58:44", peer: "河南神火运销有限公司", peerAcct: "工行永城分行 1702 0218 0902 9075 63", amount: "¥ 1,246,800.00(收)", status: "未归集 · 外部", pair: "—(外部单位交易)", subject: "—", batch: "—", note: "对外动力煤销售回款,不参与集团内部往来归集。" } }, + { date: "2026-07-08", company: "金牛煤业", bank: "工商银行", account: "3305", acctLabel: "工行 · 尾号 3305", dir: "付", dirPill: "pill-danger", peer: "河南金牛物流有限公司", summary: "6 月煤炭公路运输费结算", serial: "ICBC2026070800311276", status: "已归集", statusPill: "pill-success", amount: "486,500.00", amtClass: "amt-out", detail: { bank: "工商银行", account: "河南金牛煤业有限公司 · 基本户 1702 0231 0900 8133 05", time: "2026-07-08 11:15:09", peer: "河南金牛物流有限公司", peerAcct: "建行郑州经开区支行 4105 0167 8080 5562", amount: "¥ 486,500.00(付)", status: "已归集", pair: "金牛煤业 ↔ 金牛物流", subject: "应付(煤业侧)", batch: "JH-202607-014", note: "与物流侧建行收款流水已双向匹配,运单 42 张随附。" } }, + { date: "2026-07-10", company: "金牛煤业", bank: "中国银行", account: "9916", acctLabel: "中行 · 尾号 9916", dir: "付", dirPill: "pill-danger", peer: "河南金牛贸易有限公司", summary: "选煤设备配件代购款", serial: "BOC2026071000819455", status: "待确认", statusPill: "pill-warn", amount: "214,700.00", amtClass: "amt-out", detail: { bank: "中国银行", account: "河南金牛煤业有限公司 · 一般户 2546 0387 1100 9916", time: "2026-07-10 14:08:52", peer: "河南金牛贸易有限公司", peerAcct: "农行郑州金水支行 1606 3301 0400 0220 8", amount: "¥ 214,700.00(付)", status: "待确认", pair: "金牛煤业 ↔ 金牛贸易", subject: "其他应付(待复核)", batch: "JH-202607-021", note: "贸易侧已确认收款,科目待双方复核(应付 / 其他应付)。" } }, + { date: "2026-07-13", company: "金牛煤业", bank: "工商银行", account: "3305", acctLabel: "工行 · 尾号 3305", dir: "付", dirPill: "pill-danger", peer: "国网河南省电力公司新密市供电公司", summary: "7 月工业电费", serial: "ICBC2026071300458820", status: "未归集 · 外部", statusPill: "pill-muted", amount: "1,528,300.00", amtClass: "amt-out", detail: { bank: "工商银行", account: "河南金牛煤业有限公司 · 基本户 1702 0231 0900 8133 05", time: "2026-07-13 10:19:05", peer: "国网河南省电力公司新密市供电公司", peerAcct: "工行新密支行 1706 0211 0900 4428 17", amount: "¥ 1,528,300.00(付)", status: "未归集 · 外部", pair: "—(外部单位交易)", subject: "—", batch: "—", note: "对外电费支出,不参与集团内部往来归集。" } }, + { date: "2026-07-15", company: "金牛煤业", bank: "中国银行", account: "9916", acctLabel: "中行 · 尾号 9916", dir: "收", dirPill: "pill-success", peer: "河南金牛新能源有限公司", summary: "场区租赁费返还(二季度)", serial: "BOC2026071500923314", status: "单边", statusPill: "pill-danger", amount: "95,000.00", amtClass: "amt-in", detail: { bank: "中国银行", account: "河南金牛煤业有限公司 · 一般户 2546 0387 1100 9916", time: "2026-07-15 11:06:33", peer: "河南金牛新能源有限公司", peerAcct: "工行郑州分行 1704 0512 0900 2266 49", amount: "¥ 95,000.00(收)", status: "单边", pair: "金牛煤业 ↔ 金牛新能源", subject: "其他应收(煤业侧)", batch: "—(待归集)", note: "新能源侧尚未提报对应付款流水,形成单边。" } }, + { date: "2026-07-18", company: "金牛煤业", bank: "工商银行", account: "3305", acctLabel: "工行 · 尾号 3305", dir: "付", dirPill: "pill-danger", peer: "国家税务总局新密市税务局", summary: "增值税及附加税费(6 月属期)", serial: "ICBC2026071800506639", status: "未归集 · 外部", statusPill: "pill-muted", amount: "2,073,450.00", amtClass: "amt-out", detail: { bank: "工商银行", account: "河南金牛煤业有限公司 · 基本户 1702 0231 0900 8133 05", time: "2026-07-18 13:29:40", peer: "国家税务总局新密市税务局", peerAcct: "国库专户", amount: "¥ 2,073,450.00(付)", status: "未归集 · 外部", pair: "—(外部单位交易)", subject: "—", batch: "—", note: "税费缴库,不参与集团内部往来归集。" } }, + { date: "2026-07-21", company: "金牛煤业", bank: "中国银行", account: "9916", acctLabel: "中行 · 尾号 9916", dir: "收", dirPill: "pill-success", peer: "永城煤电控股集团有限公司", summary: "块煤销售款(年度长协第 7 批)", serial: "BOC2026072101054472", status: "未归集 · 外部", statusPill: "pill-muted", amount: "3,864,000.00", amtClass: "amt-in", detail: { bank: "中国银行", account: "河南金牛煤业有限公司 · 一般户 2546 0387 1100 9916", time: "2026-07-21 09:35:12", peer: "永城煤电控股集团有限公司", peerAcct: "工行永城分行 1702 0218 0902 3477 12", amount: "¥ 3,864,000.00(收)", status: "未归集 · 外部", pair: "—(外部单位交易)", subject: "—", batch: "—", note: "对外块煤销售回款,不参与集团内部往来归集。" } }, + { date: "2026-07-24", company: "金牛煤业", bank: "交通银行", account: "7710", acctLabel: "交行 · 尾号 7710", dir: "收", dirPill: "pill-success", peer: "河南金牛置业有限公司", summary: "临时往来款归还", serial: "BCM2026072400088116", status: "待确认", statusPill: "pill-warn", amount: "1,500,000.00", amtClass: "amt-in", detail: { bank: "交通银行", account: "河南金牛煤业有限公司 · 一般户 4110 6120 0181 0077 10(账户待审核)", time: "2026-07-24 14:52:26", peer: "河南金牛置业有限公司", peerAcct: "中行郑州郑东新区支行 2546 0387 0200 8821", amount: "¥ 1,500,000.00(收)", status: "待确认", pair: "金牛煤业 ↔ 金牛置业", subject: "其他应收(煤业侧)", batch: "JH-202607-021", note: "交行 7710 账户尚处待审核,归集结果以账户审核通过后为准。" } }, + { date: "2026-07-27", company: "金牛煤业", bank: "工商银行", account: "3305", acctLabel: "工行 · 尾号 3305", dir: "付", dirPill: "pill-danger", peer: "煤业职工工资代发(2026 年 7 月)", summary: "7 月职工工资及奖金代发,共 612 人", serial: "ICBC2026072700582241", status: "未归集 · 外部", statusPill: "pill-muted", amount: "2,416,780.00", amtClass: "amt-out", detail: { bank: "工商银行", account: "河南金牛煤业有限公司 · 基本户 1702 0231 0900 8133 05", time: "2026-07-27 10:11:57", peer: "煤业职工工资代发(2026 年 7 月)", peerAcct: "代发专户", amount: "¥ 2,416,780.00(付)", status: "未归集 · 外部", pair: "—(外部单位交易)", subject: "—", batch: "—", note: "工资代发,不参与集团内部往来归集。" } }, + { date: "2026-07-30", company: "金牛煤业", bank: "中国银行", account: "9916", acctLabel: "中行 · 尾号 9916", dir: "付", dirPill: "pill-danger", peer: "河南金牛农业科技发展有限公司", summary: "临时周转借款(约定 8 月归还)", serial: "BOC2026073001187903", status: "单边", statusPill: "pill-danger", amount: "800,000.00", amtClass: "amt-out", detail: { bank: "中国银行", account: "河南金牛煤业有限公司 · 一般户 2546 0387 1100 9916", time: "2026-07-30 16:32:08", peer: "河南金牛农业科技发展有限公司", peerAcct: "农行郑州金水支行 1606 3301 0400 0220 8", amount: "¥ 800,000.00(付)", status: "单边", pair: "金牛煤业 ↔ 金牛农业", subject: "其他应收(煤业侧)", batch: "—(待归集)", note: "农业 7 月未提交流水,暂无对方侧证据。" } }, +]; + +function currentFlowData() { + return portal === "admin" ? FLOW_DEMO : COMPANY_FLOWS; +} + +function renderFlowRows() { + const tbody = $("#flowTable tbody"); + if (!tbody) return; + const data = currentFlowData(); + tbody.innerHTML = data.map((row, index) => { + const companyCell = portal === "admin" ? `${row.company}` : ""; + return ` + ${row.date} + ${companyCell} + ${row.acctLabel} + ${row.dir} + ${row.peer} + ${row.summary} + ${row.serial} + ${row.status} + ¥ ${row.amount} + `; + }).join(""); + const count = $("#flowCount"); + if (count) count.textContent = `共 ${data.length} 笔`; + const empty = $("#flowEmpty"); + if (empty) empty.hidden = data.length > 0; + const sum = $("#flowSum"); + if (sum) { + let inflow = 0, outflow = 0; + data.forEach((row) => { if (row.dir === "收") inflow += Number(row.amount.replace(/,/g, "")); else outflow += Number(row.amount.replace(/,/g, "")); }); + sum.innerHTML = data.length ? `收 +¥ ${formatCurrency(inflow)} · 付 -¥ ${formatCurrency(outflow)}` : ""; + } +} + +function openFlowDetail(index) { + const modal = $("#tx-modal"); + if (!modal) return; + const row = currentFlowData()[index]; + if (!row) return; + $("#tx-modal-sub").textContent = `${row.company} · ${row.acctLabel} · ${row.date}`; + const d = row.detail; + $("#d-serial").textContent = row.serial; + $("#d-bank").textContent = d.bank; + $("#d-account").textContent = d.account; + $("#d-time").textContent = d.time; + $("#d-peer").textContent = d.peer; + $("#d-peer-acct").textContent = d.peerAcct; + $("#d-amount").textContent = d.amount; + $("#d-status").textContent = d.status; + $("#d-pair").textContent = d.pair; + $("#d-subject").textContent = d.subject; + $("#d-batch").textContent = d.batch; + $("#d-note").textContent = d.note; + modal.classList.add("open"); } function visibleRows(table) { @@ -2197,44 +1528,78 @@ function filterFlows() { const endDate = $("#flowEnd")?.value || "9999-12-31"; const keyword = $("#flowKeyword")?.value.trim().toLowerCase() || ""; if (startDate > endDate) { - showToast("日期范围无效", "开始日期不能晚于结束日期"); + showToast("日期范围无效", "开始日期不能晚于结束日期", "warn"); return; } let count = 0; $$("tbody tr", table).forEach((row) => { - const rowDate = $("td", row).textContent.trim().replaceAll(".", "-"); + const rowDate = row.dataset.date || $("td", row).textContent.trim().replaceAll(".", "-"); const matchesCompany = company === "全部公司" || row.dataset.company === company; const matchesBank = bank === "全部银行" || row.dataset.bank === bank; - const matchesAccount = account === "全部账户" || row.textContent.includes(account); + const matchesAccount = account === "全部账户" || row.dataset.account === account || (row.dataset.account === undefined && row.textContent.includes(account)); const matchesDate = rowDate >= startDate && rowDate <= endDate; const matchesKeyword = !keyword || row.textContent.toLowerCase().includes(keyword); row.hidden = !(matchesCompany && matchesBank && matchesAccount && matchesDate && matchesKeyword); if (!row.hidden) count += 1; }); - $("#flowCount").textContent = count; + $("#flowCount").textContent = count === 0 ? "共 0 条 · 无匹配记录" : `共 ${count} 笔 · 本页 1-${count}`; + const empty = $("#flowEmpty"); + if (empty) empty.hidden = count > 0; + const sum = $("#flowSum"); + if (sum) { + let inflow = 0, outflow = 0; + $$("tbody tr", table).forEach((row) => { + if (row.hidden) return; + const amount = Number(($("td:last-child", row)?.textContent || "").replace(/[^\d.]/g, "")); + if (row.dataset.dir === "收") inflow += amount; else outflow += amount; + }); + sum.innerHTML = count ? `收 +¥ ${formatCurrency(inflow)} · 付 -¥ ${formatCurrency(outflow)}` : ""; + } showToast("查询完成", `当前显示 ${count} 笔流水`); } function exportFlows() { const table = $("#flowTable"); const rows = visibleRows(table); - const headers = ["交易日期", "公司", "银行及账号", "收付方向", "对方户名及账号", "摘要", "银行流水号", "归集状态", "金额", "导入批次", "源行定位"]; + const headers = ["交易日期", "公司", "银行账户", "方向", "对方户名", "摘要", "银行流水号", "归集状态", "金额", "导入批次", "源行定位"]; const records = rows.map((row, index) => { const cells = $$('td', row).map((cell) => cell.innerText.replace(/\n/g, " ").trim()); - return [cells[0], portal === "company" ? "A公司" : cells[1].split(" ")[0], portal === "company" ? cells[1] : cells[1], ...cells.slice(2), `IMP-DEMO-${String(index + 1).padStart(3, "0")}`, `Sheet1!R${index + 8}`]; + const imp = `IMP-DEMO-${String(index + 1).padStart(3, "0")}`; + const loc = `Sheet1!R${index + 8}`; + const companyName = state.me?.company_name || "本公司"; + if (portal === "admin") return [...cells.slice(0, 9), imp, loc]; + return [cells[0], companyName, cells[1], ...cells.slice(2), imp, loc]; }); const csv = [headers, ...records].map((record) => record.map((value) => `"${String(value ?? "").replace(/"/g, '""')}"`).join(",")).join("\r\n"); const link = document.createElement("a"); link.href = URL.createObjectURL(new Blob(["\ufeff", csv], { type: "text/csv;charset=utf-8" })); - link.download = `${portal === "admin" ? "集团" : "A公司"}银行流水_202607.csv`; + link.download = `${portal === "admin" ? "集团" : (state.me?.company_name || "本公司")}银行流水_202607.csv`; link.click(); URL.revokeObjectURL(link.href); - showToast("导出已生成", `共 ${rows.length} 笔,已保留银行标识与源行定位`); + showToast("导出已生成", `共 ${rows.length} 笔,已保留银行标识与源行定位`, "success"); } function initFlowTools() { $("#applyFlowFilters")?.addEventListener("click", filterFlows); $("#exportFlows")?.addEventListener("click", exportFlows); + renderFlowRows(); + $("#resetFlowFilters")?.addEventListener("click", () => { + const company = $("#flowCompany"); + if (company) company.value = "全部公司"; + $("#flowBank").value = "全部银行"; + $("#flowAccount").value = "全部账户"; + $("#flowStart").value = "2026-07-01"; + $("#flowEnd").value = portal === "admin" ? "2026-07-31" : "2026-08-20"; + $("#flowKeyword").value = ""; + filterFlows(); + }); + $("#flowTable tbody")?.addEventListener("click", (event) => { + const row = event.target.closest("tr[data-flow-idx]"); + if (row) openFlowDetail(Number(row.dataset.flowIdx)); + }); + $("#tx-modal-close")?.addEventListener("click", () => $("#tx-modal")?.classList.remove("open")); + $("#tx-modal-ok")?.addEventListener("click", () => $("#tx-modal")?.classList.remove("open")); + $("#tx-modal")?.addEventListener("click", (event) => { if (event.target === event.currentTarget) event.currentTarget.classList.remove("open"); }); } function resetUpload() { @@ -2262,7 +1627,7 @@ function acceptFile(file) { if (!file) return; const extension = file.name.split(".").pop().toLowerCase(); if (!["xls", "xlsx"].includes(extension)) { - showToast("文件格式不支持", "请选择银行导出的 .xls 或 .xlsx 文件"); + showToast("文件格式不支持", "请选择银行导出的 .xls 或 .xlsx 文件", "danger"); return; } state.selectedFile = file; @@ -2306,8 +1671,10 @@ function renderParseResult(result, parsed) { const sheets = Array.isArray(result.sheets) ? result.sheets : []; const duplicated = result.status === "duplicate"; const opaque = duplicated && !sheets.length; - panel.classList.toggle("is-exception", !parsed); - $("use", panel).setAttribute("href", parsed ? "icons.svg#circle-check" : "icons.svg#circle-alert"); + panel.classList.toggle("info", parsed); + panel.classList.toggle("warn", !parsed); + const parseIcon = $("use", panel); + if (parseIcon) parseIcon.setAttribute("href", parsed ? "icons.svg#circle-check" : "icons.svg#circle-alert"); $("#parseTitle").textContent = duplicated ? "文件已导入过" : parsed ? "文件解析完成" : "未识别到银行模板"; const pendingCount = sheets.filter((s) => s.outcome === "parsed" && s.review_status === "pending").length; const exceptionCount = sheets.filter((s) => s.outcome === "exception").length; @@ -2326,6 +1693,7 @@ function renderParseResult(result, parsed) { const button = $("#parseButton"); button.dataset.stage = "confirm"; + button.disabled = false; if (!parsed) { button.querySelector("span").textContent = "关闭"; } else if (opaque) { @@ -2357,20 +1725,17 @@ function renderSheetList(sheets, batchId) { } function buildSheetItem(sheet, batchId) { - const item = document.createElement("article"); - item.className = "sheet-item"; - if (sheet.review_status === "pending") item.classList.add("is-pending"); + const item = document.createElement("div"); + item.className = "list-row"; const meta = sheetStatusMeta(sheet); - const head = document.createElement("div"); - head.className = "sheet-item-head"; - const name = document.createElement("strong"); - name.textContent = sheet.sheet_name; - const badge = document.createElement("em"); - badge.className = `status ${meta.className}`; - badge.textContent = meta.label; - head.append(name, badge); - const details = document.createElement("p"); - details.className = "sheet-item-meta"; + const main = document.createElement("div"); + main.className = "lr-main"; + const title = document.createElement("div"); + title.className = "lr-title"; + title.textContent = sheet.sheet_name; + main.append(title); + const details = document.createElement("div"); + details.className = "lr-sub"; if (sheet.outcome === "parsed" && sheet.bank) { const period = sheet.period_start ? ` · ${sheet.period_start}—${sheet.period_end}` : ""; details.textContent = `${sheet.bank} · ${sheet.transactions} 条明细${period}`; @@ -2382,28 +1747,34 @@ function buildSheetItem(sheet, batchId) { if (sheet.review_reason) { details.textContent += ` · 原因:${sheet.review_reason}`; } - const body = document.createElement("div"); - body.append(head, details); + main.append(details); + + const badge = document.createElement("span"); + badge.className = `pill ${pillClass(meta.className)}`; + badge.textContent = meta.label; + + item.append(main, badge); - const actions = document.createElement("div"); - actions.className = "sheet-item-actions"; if (sheet.review_status === "pending") { + const actions = document.createElement("div"); + actions.className = "lr-side"; + actions.style.cssText = "display:flex;gap:6px;flex:none;"; if (sheet.outcome === "parsed") { const confirmButton = document.createElement("button"); confirmButton.type = "button"; - confirmButton.className = "text-button"; + confirmButton.className = "btn btn-sm btn-primary"; confirmButton.textContent = "确认"; confirmButton.addEventListener("click", () => sheetReviewAction(batchId, sheet.sheet_name, "confirm")); actions.append(confirmButton); } const ignoreButton = document.createElement("button"); ignoreButton.type = "button"; - ignoreButton.className = "text-button"; + ignoreButton.className = "btn btn-sm"; ignoreButton.textContent = "忽略"; ignoreButton.addEventListener("click", () => sheetReviewAction(batchId, sheet.sheet_name, "ignore")); actions.append(ignoreButton); + item.append(actions); } - item.append(body, actions); return item; } @@ -2412,7 +1783,7 @@ async function sheetReviewAction(batchId, sheetName, decision) { if (decision === "ignore") { const reason = (window.prompt("请填写忽略原因(必填):", "") || "").trim(); if (!reason) { - showToast("忽略未提交", "必须填写忽略原因"); + showToast("忽略未提交", "必须填写忽略原因", "warn"); return; } payload.reason = reason; @@ -2428,10 +1799,10 @@ async function sheetReviewAction(batchId, sheetName, decision) { } const result = await response?.json().catch(() => ({})); if (!response || !response.ok) { - showToast("操作失败", result?.message || "请稍后重试"); + showToast("操作失败", result?.message || "请稍后重试", "danger"); return; } - showToast(decision === "confirm" ? "工作表已确认" : "工作表已忽略", `${sheetName}`); + showToast(decision === "confirm" ? "工作表已确认" : "工作表已忽略", `${sheetName}`, "success"); await refreshAfterSheetAction(result, batchId); } @@ -2457,7 +1828,7 @@ async function confirmImport() { .filter((s) => s.outcome === "parsed" && s.review_status === "pending") .map((s) => s.sheet_name); if (!pending.length) { - $("#uploadDialog").close(); + resetUpload(); await loadImportBatches(); showView("upload"); return; @@ -2478,43 +1849,43 @@ async function confirmImport() { if (!response || !response.ok) { button.disabled = false; button.querySelector("span").textContent = "确认失败,点击重试"; - showToast("确认失败", outcome?.message || "请稍后重试"); + showToast("确认失败", outcome?.message || "请稍后重试", "danger"); return; } button.disabled = false; if (Array.isArray(outcome.sheets)) renderSheetList(outcome.sheets, batchId); await loadImportBatches(); - $("#uploadDialog").close(); + resetUpload(); showView("upload"); - showToast("流水已确认", `${outcome.updated.length} 个工作表已确认;未确认的工作表不参与计算`); + showToast("流水已确认", `${outcome.updated.length} 个工作表已确认;未确认的工作表不参与计算`, "success"); } function submitImportException() { - $("#uploadDialog").close(); + resetUpload(); showView("upload"); - showToast("解析异常未入账", `${state.selectedFile?.name || "该文件"} 不会进入匹配与计算,请核对模板后重新导出`); + showToast("解析异常未入账", `${state.selectedFile?.name || "该文件"} 不会进入匹配与计算,请核对模板后重新导出`, "warn"); } function renderBatchRow(batch) { const row = document.createElement("tr"); + row.dataset.batchId = batch.id; const idCell = document.createElement("td"); - const id = document.createElement("strong"); - id.textContent = `IMP-${String(batch.id).padStart(6, "0")}`; - const file = document.createElement("small"); - file.textContent = batch.original_filename || ""; + const id = document.createElement("span"); id.className = "cell-main num"; id.textContent = `IMP-${String(batch.id).padStart(6, "0")}`; + const file = document.createElement("span"); file.className = "cell-sub"; file.textContent = batch.original_filename || ""; idCell.append(id, file); const bank = document.createElement("td"); bank.textContent = batch.bank_name || "—"; const period = document.createElement("td"); + period.className = "num"; period.textContent = batch.period_start && batch.period_end - ? `${batch.period_start}—${batch.period_end}` + ? `${batch.period_start} ~ ${batch.period_end}` : "—"; const count = document.createElement("td"); - count.className = "number"; - count.textContent = `${batch.confirmed_transactions ?? 0} 笔`; + count.className = "num-col"; + count.textContent = `${batch.confirmed_transactions ?? 0}`; const coverage = document.createElement("td"); const coverageStatus = batch.status === "exception" @@ -2524,44 +1895,295 @@ function renderBatchRow(batch) { : batch.confirmed_sheets > 0 ? { className: "success", label: "已确认" } : { className: "neutral", label: "待处理" }; - coverage.innerHTML = `${coverageStatus.label}`; + coverage.innerHTML = `${coverageStatus.label}`; const parseState = document.createElement("td"); const statusParts = []; if (batch.exception_sheets > 0) statusParts.push(`${batch.exception_sheets} 个异常`); if (batch.ignored_sheets > 0) statusParts.push(`${batch.ignored_sheets} 个忽略`); - parseState.textContent = statusParts.length ? statusParts.join("、") : "解析成功"; + if (statusParts.length) { + parseState.innerHTML = `${statusParts.join("、")}`; + } else { + parseState.innerHTML = '解析成功'; + } const time = document.createElement("td"); + time.className = "meta"; time.textContent = String(batch.created_at || "").slice(0, 16).replace("T", " "); - row.append(idCell, bank, period, count, coverage, parseState, time); + const action = document.createElement("td"); + action.innerHTML = ''; + + row.append(idCell, bank, period, count, coverage, parseState, time, action); return row; } async function loadImportBatches() { const tbody = $("#importRows"); if (!tbody) return; + showTableLoading(tbody, 8); const response = await fetch("/api/batches").catch(() => null); if (response?.status === 401 || response?.status === 403) { window.location.href = "index.html"; return; } + if (!response?.ok) { + showTableError(tbody, 8); + return; + } const result = await response?.json().catch(() => ({})); const batches = Array.isArray(result?.batches) ? result.batches : []; - if (batches.length) tbody.replaceChildren(...batches.map(renderBatchRow)); + state.batches = batches; + tbody.replaceChildren(...batches.map(renderBatchRow)); + const foot = $("#importFoot"); + if (foot) foot.textContent = batches.length ? `共 ${batches.length} 个批次` : "暂无批次"; +} + +function refreshWorkspacePending() { + const card = $("#workspaceTodos"); + if (!card) return; + const count = $$(".list-row", card).length; + const status = $("#workspacePendingStatus"); + if (status) status.textContent = `${count} 项待处理`; +} + +function initReconcile() { + const matchCards = $$("[data-match-card]"); + const subjectRows = $$("[data-subject-row]"); + if (!matchCards.length && !subjectRows.length) return; + const noticeTitle = $("#notice-title"); + const noticeBody = $("#notice-body"); + const notice = $("#blocking-notice"); + const matchedSummary = $("#matched-summary"); + const matchedList = $("#matched-list"); + const matchStack = $("#match-stack"); + let pendingMatch = matchCards.length; + let pendingSubject = subjectRows.length; + + function refreshCounts() { + const countMatch = $("#count-match"); + const countSubject = $("#count-subject"); + if (countMatch) countMatch.textContent = pendingMatch; + if (countSubject) countSubject.textContent = pendingSubject; + const total = pendingMatch + pendingSubject; + const badge = $('.side-nav a[data-view="reconcile"] .nav-badge'); + if (badge) { + badge.textContent = total; + badge.style.display = total ? "" : "none"; + } + if (noticeTitle) { + noticeTitle.textContent = total ? `${total} 项待确认,是 7 月结账的阻断项` : "全部确认完成"; + } + if (noticeBody) { + noticeBody.textContent = total + ? `含单边流水匹配 ${pendingMatch} 项、科目确认 ${pendingSubject} 项。请于 2026-08-29(7 月顺延结账日)前处理完毕,否则集团无法对贵公司执行 7 月结账。` + : "本公司 2026-07 账期已具备结账条件,集团将于 08-29 统一执行结账。"; + } + if (notice) { + notice.classList.toggle("warn", total > 0); + notice.classList.toggle("success", total === 0); + } + if (pendingMatch === 0) $('[data-task-type="match"]', $("#workspaceTodos"))?.remove(); + const matchRow = $('[data-task-type="match"]', $("#workspaceTodos")); + if (matchRow) { + const title = $(".lr-title", matchRow); + if (title) title.textContent = `处理 ${pendingMatch} 笔单边流水确认`; + } + const subjectRow = $('[data-task-type="subject"]', $("#workspaceTodos")); + if (subjectRow) { + const title = $(".lr-title", subjectRow); + if (title) title.textContent = `确认 ${pendingSubject} 笔其他应收科目`; + } + const flowState = $("#workspaceConfirmState"); + if (flowState) flowState.textContent = total ? `待处理 ${total} 笔` : "已完成"; + const flowMeta = $("#workspaceConfirmMeta"); + if (flowMeta) flowMeta.textContent = total ? `单边流水 ${pendingMatch} 笔 · 科目确认 ${pendingSubject} 笔` : "已全部确认,等待集团结账"; + refreshWorkspacePending(); + } + + matchCards.forEach((card) => { + const confirmButton = $("[data-match-confirm]", card); + const radios = $$('input[type="radio"]', card); + radios.forEach((radio) => radio.addEventListener("change", () => { if (confirmButton) confirmButton.disabled = false; })); + confirmButton?.addEventListener("click", () => { + const summaryText = confirmButton.dataset.matchSummary || "已确认匹配"; + const row = document.createElement("div"); + row.className = "list-row"; + const main = document.createElement("div"); + main.className = "lr-main"; + const title = document.createElement("div"); + title.className = "lr-title"; + title.textContent = summaryText; + main.append(title); + const pill = document.createElement("span"); + pill.className = "pill pill-success lr-side"; + pill.textContent = "已匹配"; + row.append(main, pill); + matchedList.append(row); + matchedSummary.style.display = ""; + card.remove(); + pendingMatch -= 1; + if (pendingMatch === 0) { + const empty = document.createElement("div"); + empty.className = "empty"; + empty.innerHTML = '
单边流水已全部匹配
确认结果将同步至集团审核中心复核
'; + matchStack.append(empty); + } + refreshCounts(); + showToast("匹配已确认", "待办状态、操作人、时间和依据已同步更新", "success"); + }); + }); + + $$(".subject-confirm-btn").forEach((button) => { + button.addEventListener("click", () => { + const row = button.closest("[data-subject-row]"); + const select = $("select", row); + const subject = select.value; + select.disabled = true; + button.disabled = true; + button.textContent = "已确认"; + const statusCell = $(".subject-status", row); + statusCell.innerHTML = `已确认 · ${subject}`; + pendingSubject -= 1; + refreshCounts(); + showToast("科目已确认", "待办状态、操作人、时间和依据已同步更新", "success"); + }); + }); + + const tabMatch = $("#tab-match"); + const tabSubject = $("#tab-subject"); + const panelMatch = $("#panel-match"); + const panelSubject = $("#panel-subject"); + function switchTab(which) { + tabMatch?.classList.toggle("active", which === "match"); + tabSubject?.classList.toggle("active", which === "subject"); + tabMatch?.setAttribute("aria-pressed", String(which === "match")); + tabSubject?.setAttribute("aria-pressed", String(which === "subject")); + if (panelMatch) panelMatch.style.display = which === "match" ? "" : "none"; + if (panelSubject) panelSubject.style.display = which === "subject" ? "" : "none"; + } + tabMatch?.addEventListener("click", () => switchTab("match")); + tabSubject?.addEventListener("click", () => switchTab("subject")); + + refreshCounts(); +} + +function initNotifications() { + const tabs = $("#notice-tabs"); + const list = $("#notice-list"); + if (!tabs || !list) return; + const rows = $$(".list-row", list); + const emptyBox = $("#notice-empty"); + let currentFilter = "all"; + + const STATUS_PILL = { unread: "pill-danger", doing: "pill-warn", done: "pill-success" }; + const STATUS_LABEL = { unread: "未读", doing: "处理中", done: "已完成" }; + + function counts() { + const c = { all: rows.length, unread: 0, doing: 0, done: 0 }; + rows.forEach((row) => { c[row.getAttribute("data-status")] += 1; }); + return c; + } + + function refreshCounts() { + const c = counts(); + $("#count-all").textContent = c.all; + $("#count-unread").textContent = c.unread; + $("#count-doing").textContent = c.doing; + $("#count-done").textContent = c.done; + const navBadge = $('.side-nav a[data-view="notifications"] .nav-badge'); + if (navBadge) { + navBadge.textContent = c.unread; + navBadge.style.display = c.unread ? "" : "none"; + } + } + + function applyFilter() { + let visible = 0; + rows.forEach((row) => { + const show = currentFilter === "all" || row.getAttribute("data-status") === currentFilter; + row.style.display = show ? "" : "none"; + if (show) visible += 1; + }); + emptyBox.style.display = visible === 0 ? "" : "none"; + } + + function setStatus(row, status) { + row.setAttribute("data-status", status); + const pill = $(".pill", row); + if (pill) { + pill.className = `pill ${STATUS_PILL[status]}`; + pill.textContent = STATUS_LABEL[status]; + } + } + + tabs.addEventListener("click", (event) => { + const btn = event.target.closest("button[data-filter]"); + if (!btn) return; + $$("button", tabs).forEach((b) => { b.classList.remove("active"); b.setAttribute("aria-pressed", "false"); }); + btn.classList.add("active"); + btn.setAttribute("aria-pressed", "true"); + currentFilter = btn.dataset.filter; + applyFilter(); + }); + + list.addEventListener("click", (event) => { + const btn = event.target.closest(".btn-mark-read"); + if (!btn) return; + const row = btn.closest(".list-row"); + setStatus(row, "done"); + btn.remove(); + refreshCounts(); + applyFilter(); + }); + + $("#mark-all-read")?.addEventListener("click", () => { + rows.forEach((row) => { + if (row.getAttribute("data-status") !== "unread") return; + setStatus(row, "done"); + $(".btn-mark-read", row)?.remove(); + }); + refreshCounts(); + applyFilter(); + showToast("通知已全部标为已读", "", "success"); + }); + + refreshCounts(); + applyFilter(); +} + +function openAccountDetail(account) { + const meta = companyAccountMeta(account.status); + $("#ad-title").textContent = `${account.bank_name || ""} · 尾号 ${accountTail(account.account_number_masked)}`; + $("#ad-sub").textContent = "本公司 · 登记账户明细"; + $("#ad-bank").textContent = account.bank_name || "—"; + $("#ad-tail").textContent = `尾号 ${accountTail(account.account_number_masked)}`; + $("#ad-type").textContent = account.account_type || "—"; + $("#ad-branch").textContent = account.account_name || "—"; + $("#ad-reg").textContent = String(account.created_at || "").slice(0, 10) || "—"; + $("#ad-status").textContent = meta.status.label; + $("#ad-audit").textContent = meta.audit.label; + $("#ad-purpose").textContent = "—"; + $("#ad-effective").textContent = account.effective_from || "待审核确定"; + $("#ad-range").textContent = account.usable ? "尚未上传" : "—"; + $("#ad-reason").textContent = account.status === "returned" && account.review_reason ? account.review_reason : "—"; + $("#modal-account-detail")?.classList.add("open"); } function initCompany() { - loadCompanyManualRecords(); + renderCompanyManualRecords(); loadCompanyAccounts(); - loadCompanyBalances(); - loadCompanyDirectoryOptions(); loadImportBatches(); - const uploadDialog = $("#uploadDialog"); - $$('[data-open-upload]').forEach((button) => button.addEventListener("click", () => uploadDialog.showModal())); - $$('[data-close-upload]').forEach((button) => button.addEventListener("click", () => uploadDialog.close())); - uploadDialog?.addEventListener("close", resetUpload); + + function openModal(id) { $("#" + id)?.classList.add("open"); } + function closeModal(id) { $("#" + id)?.classList.remove("open"); } + $$("[data-close]").forEach((el) => el.addEventListener("click", () => closeModal(el.dataset.close))); + $$(".modal-backdrop").forEach((bd) => bd.addEventListener("click", (e) => { if (e.target === bd) bd.classList.remove("open"); })); + document.addEventListener("keydown", (e) => { if (e.key === "Escape") $$(".modal-backdrop.open").forEach((m) => m.classList.remove("open")); }); + + $$('[data-open-upload]').forEach((button) => button.addEventListener("click", () => showView("upload"))); + + // ── 流水导入:多步流程(选文件 → 解析 → 分 sheet 审核 → 确认) ── $("#accountSelect")?.addEventListener("change", updateParseButton); $("#fileInput")?.addEventListener("change", (event) => acceptFile(event.target.files[0])); $("#removeFile")?.addEventListener("click", () => { @@ -2574,8 +2196,10 @@ function initCompany() { }); const dropzone = $("#dropzone"); if (dropzone) { - ["dragenter", "dragover"].forEach((type) => dropzone.addEventListener(type, (event) => { event.preventDefault(); dropzone.classList.add("is-dragging"); })); - ["dragleave", "drop"].forEach((type) => dropzone.addEventListener(type, (event) => { event.preventDefault(); dropzone.classList.remove("is-dragging"); })); + ["dragenter", "dragover"].forEach((type) => dropzone.addEventListener(type, (event) => { event.preventDefault(); dropzone.classList.add("dragover"); })); + ["dragleave", "drop"].forEach((type) => dropzone.addEventListener(type, (event) => { event.preventDefault(); dropzone.classList.remove("dragover"); })); + dropzone.addEventListener("click", () => $("#fileInput").click()); + dropzone.addEventListener("keydown", (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); $("#fileInput").click(); } }); dropzone.addEventListener("drop", (event) => acceptFile(event.dataTransfer.files[0])); } $("#uploadForm")?.addEventListener("submit", async (event) => { @@ -2587,7 +2211,7 @@ function initCompany() { if (parsed && stage === "confirm") { await confirmImport(); } else if (parsed) { - $("#uploadDialog").close(); + resetUpload(); showView("upload"); await loadImportBatches(); } else { @@ -2600,62 +2224,108 @@ function initCompany() { await parseFile(); }); - function finishReview(button, disposition) { - const item = button.closest("article"); - const type = item.dataset.reviewType; - const selectedMatch = $('input[name="matchCandidate"]:checked', item)?.value; - const selectedSubject = $("select", item)?.value; - const detail = type === "match" ? (selectedMatch || "转为匹配异常") : selectedSubject; - const history = $("#reviewHistory"); - const record = document.createElement("p"); - record.textContent = `${new Date().toLocaleString("zh-CN", { hour12: false })} · 牛女士 · ${disposition} · ${detail}`; - history.append(record); - history.hidden = false; - item.remove(); - if (type === "match") $("#matchPendingCount").textContent = "0 笔"; - else $("#subjectPendingCount").textContent = "0 笔"; - const remaining = $$("#reviewList > article").length; - const badge = $('.nav-item[data-view="reconcile"] b'); - if (badge) badge.textContent = remaining; - $(`.cashier-tasks [data-task-type="${type}"]`)?.remove(); - const workspaceRemaining = $$(".cashier-tasks > article").length; - $("#workspacePendingStatus").textContent = `${workspaceRemaining} 项待处理`; - const workspaceBadge = $('.nav-item[data-view="workspace"] b'); - if (workspaceBadge) workspaceBadge.textContent = workspaceRemaining; - showToast("处理结果已记录", "待办状态、操作人、时间和依据已同步更新"); - } - $$('[data-resolve]').forEach((button) => button.addEventListener("click", () => finishReview(button, "确认"))); - $$('[data-reject]').forEach((button) => button.addEventListener("click", () => finishReview(button, "转异常"))); - $("#markAllRead")?.addEventListener("click", () => { - $$("#companyNotifications .is-unread").forEach((item) => { - item.classList.remove("is-unread"); - const status = $(".status", item); - status.className = "status neutral"; - status.textContent = "已读"; - }); - showToast("通知已全部标为已读"); + // ── 导入批次详情弹窗 ── + $("#importRows")?.addEventListener("click", (event) => { + const btn = event.target.closest("[data-batch-view]"); + if (!btn) return; + const tr = btn.closest("tr"); + const batch = (state.batches || []).find((b) => String(b.id) === String(tr?.dataset.batchId)); + if (!batch) return; + $("#mb-sub").textContent = `IMP-${String(batch.id).padStart(6, "0")} · ${batch.original_filename || ""}`; + $("#mb-id").textContent = `IMP-${String(batch.id).padStart(6, "0")}`; + $("#mb-bank").textContent = batch.bank_name || "—"; + $("#mb-period").textContent = batch.period_start && batch.period_end ? `${batch.period_start} ~ ${batch.period_end}` : "—"; + $("#mb-count").textContent = `${batch.confirmed_transactions ?? 0} 条`; + $("#mb-cover").textContent = batch.status === "exception" ? "未导入" : batch.pending_sheets > 0 ? `${batch.pending_sheets} 个待确认` : batch.confirmed_sheets > 0 ? "已确认" : "待处理"; + $("#mb-parse").textContent = batch.exception_sheets > 0 ? `${batch.exception_sheets} 个异常` : batch.ignored_sheets > 0 ? `${batch.ignored_sheets} 个忽略` : "解析成功"; + $("#mb-time").textContent = String(batch.created_at || "").slice(0, 16).replace("T", " "); + openModal("modal-batch"); }); + // ── 手工记录:提交 + 撤回 ── $("#manualEntryForm")?.addEventListener("submit", (event) => { event.preventDefault(); const form = event.currentTarget; const data = new FormData(form); const evidence = data.get("evidence"); if (evidence instanceof File && evidence.size > 20 * 1024 * 1024) { - showToast("证明附件超过限制", "请选择不超过 20 MB 的文件"); + showToast("证明附件超过限制", "请选择不超过 20 MB 的文件", "warn"); return; } - submitCompanyManualRecord(form); + const records = readStoredRecords(storageKeys.manual); + const record = { + id: `MR-${Date.now().toString().slice(-10)}`, + company: "A公司", + transactionDate: String(data.get("transactionDate")), + direction: String(data.get("direction")), + amount: Number(data.get("amount")), + sourceAccount: String(data.get("sourceAccount")), + counterpartyType: String(data.get("counterpartyType")), + counterparty: String(data.get("counterparty")).trim(), + counterpartyAccount: String(data.get("counterpartyAccount") || "").trim(), + subject: String(data.get("subject")), + summary: String(data.get("summary")).trim(), + remark: String(data.get("remark")).trim(), + bankReference: String(data.get("bankReference") || "").trim(), + evidenceName: evidence instanceof File ? evidence.name : "", + status: "待总账复核", + createdAt: new Date().toLocaleString("zh-CN", { hour12: false }), + }; + records.push(record); + if (!writeStoredRecords(storageKeys.manual, records)) return; + renderCompanyManualRecords(); + form.reset(); + showToast("手工记录已提交", "总账复核前不会纳入公司间往来计算", "success"); }); - const accountDialog = $("#accountDialog"); - $("#openAccountDialog")?.addEventListener("click", () => accountDialog.showModal()); - $$('[data-close-account]').forEach((button) => button.addEventListener("click", () => accountDialog.close())); + let manualWithdrawRow = null; + $("#manualRecordRows")?.addEventListener("click", (event) => { + const btn = event.target.closest("button[data-action='withdraw']"); + if (!btn) return; + const tr = btn.closest("tr"); + manualWithdrawRow = tr; + const cells = tr.cells; + $("#wd-date").textContent = cells[0]?.textContent.trim() || "—"; + $("#wd-peer").textContent = cells[2]?.textContent.trim() || "—"; + $("#wd-amount").textContent = cells[4]?.textContent.trim() || "—"; + $("#wd-summary").textContent = cells[5]?.textContent.trim() || "—"; + openModal("withdraw-modal"); + }); + $("#wd-confirm")?.addEventListener("click", () => { + if (manualWithdrawRow) { + const id = manualWithdrawRow.dataset.storedRecord; + if (id) { + const records = readStoredRecords(storageKeys.manual).filter((r) => r.id !== id); + writeStoredRecords(storageKeys.manual, records); + } else { + manualWithdrawRow.remove(); + } + manualWithdrawRow = null; + } + closeModal("withdraw-modal"); + renderCompanyManualRecords(); + showToast("手工记录已撤回", "该记录已从审核队列中移除,需重新登记提交", "success"); + }); + + // ── 往来确认 ── + initReconcile(); + + // ── 通知 ── + initNotifications(); + + // ── 银行账户 ── + $("#openAccountDialog")?.addEventListener("click", () => openModal("accountDialog")); + $("#account-tbody")?.addEventListener("click", (event) => { + const btn = event.target.closest("[data-account-view]"); + if (!btn) return; + const tr = btn.closest("tr"); + const account = (state.accounts || []).find((a) => String(a.id) === String(tr?.dataset.accountId)); + if (!account) return; + openAccountDetail(account); + }); $("#accountForm")?.addEventListener("submit", async (event) => { event.preventDefault(); const data = new FormData(event.currentTarget); - // The server binds the account to the session company and normalizes the - // number; duplicates come back as 409. const response = await fetch("/api/company/accounts", { method: "POST", headers: { "Content-Type": "application/json" }, @@ -2672,46 +2342,28 @@ function initCompany() { } const result = await response?.json().catch(() => ({})); if (!response || !response.ok) { - showToast("账户登记失败", result?.message || "请稍后重试"); + showToast("账户登记失败", result?.message || "请稍后重试", "danger"); return; } - accountDialog.close(); + closeModal("accountDialog"); event.currentTarget.reset(); await loadCompanyAccounts(); - showToast("银行账户已提交登记", "复核通过前不能上传流水,也不参与账户识别和覆盖计算"); + showToast("银行账户已提交登记", "复核通过前不能上传流水,也不参与账户识别和覆盖计算", "success"); }); } -if (typeof document !== "undefined" && document.body) { - if (portal === "entry") { - initEntry(); - } else { - initAuthGuard().then((allowed) => { - if (!allowed) return; - initShell(); - initFlowTools(); - if (portal === "admin") { - initAdmin(); - } else { - initCompany(); - } - }); - } -} - -if (typeof module !== "undefined" && module.exports) { - module.exports = { - fmtAbsMoney, - fmtMoney, - eventIsNegative, - cycleTab, - drawerEscAction, - amountWithCurrency, - isCompactAmount, - resultDirection, - accountCell, - cashDirectionLabel, - relatedFlowLabel, - auditEvidenceFields, - }; +if (portal === "entry") { + initEntry(); +} else { + initAuthGuard().then((allowed) => { + if (!allowed) return; + initShell(); + initFlowTools(); + if (portal === "admin") { + initPairQueries(); + initAdmin(); + } else { + initCompany(); + } + }); } diff --git a/web/company.html b/web/company.html index 573490e..7b4a6b0 100644 --- a/web/company.html +++ b/web/company.html @@ -5,228 +5,958 @@ 公司业务端 · 金牛集团 - + - -
+
-
-
+
+
+ 公司业务端 / 工作台 +
+ 账期 2026-07 + 统计截止 2026-08-20 +
+
+ +
-

本月待办

A公司 · 距离集团结账日还有 3 天

4 项待处理
-
- -
- - - -
+
+
+

工作台

+

河南金牛煤业有限公司 · 2026 年 7 月账期。本月 18 项账期任务已完成 13 项,剩余 4 项待办,其中 3 项阻断结账。

-
-
-
-
待上传账户1/ 4
- -

工商银行 9481 · 缺少 21 天

-
-
-
待确认流水1
- -

280.00 万元 · 有 2 个候选

-
-
-
待复核记录1
- -

已提交总账 · 等待管理员处理

-
-
-
本月完成进度75%
- -

3 / 4 个账户 · 距结账还有 3 天

-
-
-
7 月数据尚不能结账

工商银行账户缺少 07.01—07.21 流水,补齐后系统将重新计算匹配结果。

-
-

账户覆盖

A公司 · 4 个银行账户

3 / 4 连续
-
-
中信银行 · 5316基本户 · 最近导入 09:42
01.01—07.317 个月连续
连续
-
农业银行 · 3650一般户 · 最近导入 08:01
01.01—07.317 个月连续
连续
-
工商银行 · 9481一般户 · 已逾期 2 天
缺口 21 天07.01—07.21
断档
-
建设银行 · 0845一般户 · 最近导入 08:01
01.01—07.317 个月连续
连续
+
+ +
-
-
-
-

需要处理

按结账影响和期限排序

-
-
1

补传工商银行流水

账户尾号 9481 · 缺少 07.01—07.21

影响:7 月账户覆盖与双边匹配
-
2

确认 1 笔单边流水

对方:B公司 · 07.18 · 280.00 万元

系统找到 2 个候选,请核对摘要与账号
-
3

确认往来科目

对方:C公司 · 06.27 · 600.00 万元

待确认:应收或其他应收
-
4

阅读总账提醒

请于 08.08 前完成 7 月银行流水确认

管理员 · 今天 09:30
-
-
-
-
1

流水上传3 / 4 个账户完成

2

异常处理2 项待确认

3

公司确认尚未提交

4

集团结账次月 5 日

-
-

账期时间轴

起算日 2026.01.01 · 每月 5 日结上月账

距 7 月结账日 08.05 还有 3 天
-
+
+ +
+
+
+
+ 本月待办本月任务 13/18 · 按处理顺序排列,前 3 项阻断结账 + 4 项待处理 +
+
+ 阻断 +
+
处理 3 笔单边流水确认
+
选择对方银行流水作为证据后提交确认
+
+ +
+
+ 阻断 +
+
确认 2 笔其他应收科目
+
核对后改判或维持原科目
+
+ +
+
+ 阻断 +
+
补传交行尾号 7710 账户 7 月流水
+
账户已登记,待总行审核通过后即可导入
+
+ +
+
+ 一般 +
+
核对与金牛置业 320 万往来
+
置业 8821 账户 7 月流水断档,需人工核对
+
+ +
+
+ 完成以上 4 项后,7 月账期即可提交集团结账 +
+
+ +
+
+ 账户覆盖情况本公司已登记银行账户与流水上传覆盖区间 + +
+
+ + + + + + + + + + + + + + + + + + + + + +
银行账户流水覆盖区间状态
工商银行尾号 3305 · 基本户2026-01-01 ~ 2026-08-20已覆盖
中国银行尾号 9916 · 一般户2026-01-01 ~ 2026-08-12部分覆盖
交通银行尾号 7710 · 一般户未上传待审核
+
+
+
+ +
+
+
+ 本公司往来合计2026-01-01 至 2026-08-20 · 与集团总账口径一致 · 单位:万元 +
+
+
+
借方合计
+
18,420.50万元
+
+
+
贷方合计
+
12,386.00万元
+
+
+
明细笔数
+
128
+
+
+
期末净往来 · 应收方向
+
+6,034.50万元
+
+
+ +
+ +
+
+ 最新通知来自集团总账 + +
+
+ 提醒 +
+
7 月账期结账日顺延至 08-29
+
请在此之前完成往来确认事项
+
+ 08-18 +
+
+ 提醒 +
+
3 笔单边流水待确认
+
涉及金牛置业、金牛贸易往来
+
+ 08-16 +
+
+ 通过 +
+
中行尾号 9916 流水导入成功
+
流水覆盖至 08-12
+
+ 08-12 +
+
+
+
-

流水导入

上传 A公司银行账户流水,系统按表头识别银行模板

-
最近导入批次仅已确认工作表参与匹配与计算
批次银行账户流水期间明细数覆盖状态解析状态上传时间
+
+
+

流水导入

+

上传银行导出的 Excel 流水文件,系统按表头识别银行模板、解析期间与明细并做覆盖校验。当前账期 2026-07,仅已审核通过的账户可导入。

+
+
+ +
+
+
+ 上传流水先选账户,再选择或拖入流水文件 +
+
+
+ + + 仅已审核通过的账户可上传;选择账户与文件后方可解析 +
+
+ + +
+ +
拖放银行流水文件到此处
+
点击选择文件,支持银行导出的 Excel
+
.XLS / .XLSX · MAX 20MB
+
+ +
+ + + + + + + 请先选择流水文件,选择后此按钮方可点击 + +
+
导入规则
+
+
· 同一账户同一月份重复导入时,新批次覆盖旧批次
+
· 导入后系统自动与对方公司流水轧算往来余额
+
· 无法解析的行转入审核中心,不影响其余明细入库
+
+
+
+
+ +
+
+ 历史导入批次2026 年 7 月账期 · 本公司全部账户 +
+
+ + + + + + + + + +
批次银行账户流水期间明细数覆盖状态解析状态上传时间操作
+
+
+ 暂无批次 + 同一账户同一月份重复导入时,新批次覆盖旧批次 +
+
+
-

手工记录

登记个人过账或银行流水之外的往来事实

1 笔待总账复核
-
-
-

新增记录

A公司 · 提交人:牛女士

-
-
-
-
-
- - -
-

手工记录与银行原始流水分开留痕;总账复核通过后才纳入往来计算。

+
+
+

手工记录

+

登记未通过银行流水体现的往来业务,如个人过账、现金收付与未入账回单。提交后由集团总账会计审核,通过后计入本公司往来台账。

+
+
+ +
+
+
+ 登记手工往来带 * 为必填项,提交后进入待审核状态
-
- -
-

本公司记录

按提交时间倒序

-
编号 / 日期方向 / 科目资金来源对方摘要金额(元)状态
MR-DEMO-0012026.07.29付款其他应收个人过账B公司集团内部公司个人代付后转回80,000.00待总账复核
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
手工记录与银行原始流水分开留痕
总账复核通过后才纳入往来计算,撤回后需重新登记提交。
+
+ +
+
+ +
+
+ 已提交记录账期 2026-07 内本公司提交的手工往来,0 笔待总账复核 +
+
+ + + + + + + + +
日期方向对方科目金额(元)摘要审核状态操作
+
+ +
+ 共 0 条 + 审核由集团总账会计在 1 个工作日内完成 +
+
-

流水管理

仅查询和导出 A公司的银行流水

-
-
当前结果 3金额单位:元
日期银行账户方向对方户名 / 账号摘要银行流水号归集状态金额
2026.07.18中信银行 · 5316转出B公司尾号 9481往来款CIT260718018双边匹配10,000,000.00
2026.07.12建设银行 · 0845转出A公司尾号 5316同名账户调拨CCB260712031同公司调拨2,000,000.00
2026.07.06中信银行 · 5316转入B公司尾号 9481归还往来款CIT260706041双边匹配3,200,000.00
-
+
+
+

流水管理

+

本公司名下银行账户的原始流水及归集状态。集团内往来流水将进入往来确认与归集链路,外部流水不参与归集。

+
+
+ +
+
-
-

往来余额

本公司往来余额、对方公司明细与允许查看的源证据

- 期初不可用 · 仅显示期间净变动 -
-
-

往来确认

系统计算为主,只处理无法确定的匹配与科目

-
自动确认18 笔无需人工处理
单边待匹配1 笔需选择对方证据
科目待确认1 笔需选择会计科目
-

待确认事项

每次确认都会记录操作人、时间和依据

匹配

2026.07.18 · 转出 280.00 万元

中信银行 5316 → B公司 · 摘要“往来款”

选择对方银行证据
科目

2026.06.27 · 转出 600.00 万元

建设银行 0845 → C公司 · 摘要“资金调拨”

+
+
+

往来确认

+

处理本公司流水与对方公司对账出现的单边记录,并确认往来科目归属。7 月结账日因故顺延至 08-29,逾期未确认将阻断本公司结账。

+
+
+ +
+ +
+
5 项待确认,是 7 月结账的阻断项
+
含单边流水匹配 3 项、科目确认 2 项。请于 2026-08-29(7 月顺延结账日)前处理完毕,否则集团无法对贵公司执行 7 月结账。
+
+
+ +
+ + +
+ +
+
+
+
+ 2026-07-14 付 ¥3,200,000.00 给金牛置业摘要:煤炭采购款 + 单边 +
+
+
+
本方流水号
MY-ICBC-3305-20260714-018
+
本方账户
工商银行基本户 · 尾号 3305
+
对方户名
河南金牛置业有限公司
+
+
+
+ + + + 提示:置业中行尾号 8821 账户 07-06 至 07-16 流水断档,其 07-15 记录为对方后补手工记录,匹配度较低,请优先核对建行 6642。 +
+
+ + 查看本方流水 +
+
+ +
+
+ 2026-07-22 付 ¥950,000.00 给金牛物流摘要:7 月矿区运输费 + 单边 +
+
+
+
本方流水号
MY-ICBC-3305-20260722-031
+
本方账户
工商银行基本户 · 尾号 3305
+
对方户名
河南金牛物流有限公司
+
+
+
+ + + +
+
+ + 查看本方流水 +
+
+ +
+
+ 2026-07-09 收 ¥1,860,000.00 自金牛贸易摘要:精煤销售款 + 单边 +
+
+
+
本方流水号
MY-BOC-9916-20260709-007
+
本方账户
中国银行一般户 · 尾号 9916
+
对方户名
河南金牛贸易有限公司
+
+
+
+ + + +
+
+ + 查看本方流水 +
+
+
+ + +
+ +
-

银行账户

A公司登记账户与流水覆盖区间

- +
+
+

银行账户

+

本公司名下已登记银行账户。仅审核通过的账户可上传流水,账号对外仅显示尾号。

+
+
+ +
+
+ +
+ +
+
新账户需总账管理员审核通过后方可上传流水
+
提交登记后由集团总账管理员核对开户信息,审核周期一般 1 个工作日;审核结果将通过站内通知告知。
+
+
+ +
+
+ 本公司账户0 个 · 统计截止 2026-08-20 +
+
+ + + + + + + + + + + + +
银行 / 账号账户类型启用状态流水覆盖区间审核状态操作
+
+ +
+ 共 0 个账户 + 仅审核通过的账户可上传流水并参与归集 +
+
-

通知

系统提醒与总账管理员通知

-
7 月工商银行流水尚未上传系统自动提醒 · 今天 08:00 · 截止 08.05

账户尾号 9481 缺少 07.01—07.21 流水。

未读
请完成 7 月银行流水确认系统管理员 · 今天 09:30 · 截止 08.08

请先处理断档和两项待确认往来,再提交公司确认。

未读
6 月账期已完成集团结账系统通知 · 07.05 18:10已读
+
+
+

通知

+

系统与总账管理员发送给本公司的全部消息。7 月账期结账因故顺延至 2026-08-29,请优先处理未读与处理中事项。

+
+
+ +
+
+ +
+
+ + + + +
+ +
+
+ 未读 + 系统 +
+
7 月流水存在断档风险:交行 尾号 7710 未上传
+
2026-08-20 14:32 · 交行 7710 账户(待审核)7 月流水尚未上传,距 7 月结账日(顺延至 08-29)仅剩 9 天,请尽快补传。
+
+
+
+
+ 未读 + 系统 +
+
您有 2 笔单边流水待选择对方证据
+
2026-08-19 09:15 · 与金牛置业的煤炭采购款 ¥3,200,000.00 等 2 笔流水仅有本方记录,需选择对方银行流水佐证。
+
+
+
+
+ 未读 + 总账管理员 +
+
总账管理员提醒:请于 08-29 前完成待确认事项
+
2026-08-18 16:40 · 7 月账期结账顺延至 08-29,贵公司当前仍有 5 项往来确认未完成,请合理安排时间。
+
+
+
+
+ 处理中 + 系统 +
+
手工记录科目待确认:其他应收 ¥86,500.00
+
2026-08-15 10:02 · 07-28 录入的矿区备用金垫付记录,往来科目「其他应收」待总账管理员复核。
+
+
+
+
+ 处理中 + 总账管理员 +
+
往来确认回复:置业已确认 320 万煤炭采购款
+
2026-08-12 11:26 · 金牛置业已确认 07-14 煤炭采购款 ¥3,200,000.00,请补充本方工行 3305 账户流水证据完成闭环。
+
+
+
+
+ 已完成 + 系统 +
+
流水导入成功:中行 尾号 9916 · 42 笔
+
2026-08-06 15:52 · 7 月中行一般户流水已导入,42 笔全部校验通过,无重复记录。
+
+
+
+ 已完成 + 系统 +
+
流水导入成功:工行 尾号 3305 · 86 笔
+
2026-08-06 15:48 · 7 月工行基本户流水已导入,86 笔全部校验通过,无重复记录。
+
+
+
+ 已完成 + 总账管理员 +
+
交行 尾号 7710 账户开户资料已受理
+
2026-07-22 13:10 · 新账户开户资料已提交总账管理员,审核通过后方可启用并上传流水。
+
+
+
+ 已完成 + 系统 +
+
2026 年 6 月账期已结账
+
2026-07-05 09:00 · 6 月账期已按每月 5 日结账规则完成结账,期末数据已锁定,不可再修改。
+
+
+
+ + +
- -
-

上传银行流水

A公司 · 系统将识别表头与银行模板

-
-
-
-
- -
-

登记银行账户

账户提交后由总账管理员复核

-
-
-
-
- - + +
+
+
批次号
+
账户
+
流水期间
+
明细数量
+
覆盖状态
+
解析状态
+
导入人
本公司出纳
+
导入时间
+
+
+ +
+ + + + + + + + + + + + +
- + diff --git a/web/design-system.css b/web/design-system.css new file mode 100644 index 0000000..a849891 --- /dev/null +++ b/web/design-system.css @@ -0,0 +1,693 @@ +/* ─── 金牛实业资金往来管理系统 · 共享样式 ───────────────────────────── + 方向:tech-utility(数据密集型工具)。六枚基础 token 绑定设计方向, + 状态色仅在此 :root 块内以 oklch 派生,组件一律引用变量。 */ + +:root { + --bg: oklch(98% 0.005 250); + --surface: oklch(100% 0 0); + --fg: oklch(22% 0.02 240); + --muted: oklch(50% 0.018 240); + --border: oklch(90% 0.008 240); + --accent: oklch(58% 0.16 145); + + /* 状态色 — 仅此处定义 */ + --success: oklch(55% 0.14 150); + --warn: oklch(62% 0.14 70); + --danger: oklch(55% 0.18 25); + --info: oklch(52% 0.12 240); + + --accent-soft: color-mix(in oklch, var(--accent) 12%, transparent); + --success-soft: color-mix(in oklch, var(--success) 12%, transparent); + --warn-soft: color-mix(in oklch, var(--warn) 14%, transparent); + --danger-soft: color-mix(in oklch, var(--danger) 11%, transparent); + --info-soft: color-mix(in oklch, var(--info) 10%, transparent); + --fg-soft: color-mix(in oklch, var(--fg) 5%, transparent); + + --font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", system-ui, sans-serif; + --font-mono: "JetBrains Mono", "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, monospace; + + --radius: 8px; + --radius-lg: 12px; + --side-w: 232px; +} + +/* ─── reset ─────────────────────────────────────────────────────── */ +*, *::before, *::after { box-sizing: border-box; } +body { + margin: 0; + background: var(--bg); + color: var(--fg); + font-family: var(--font-body); + font-size: 14px; + line-height: 1.55; + -webkit-font-smoothing: antialiased; +} +img, svg { display: block; } +a { color: inherit; text-decoration: none; } +button { font: inherit; cursor: pointer; } +h1, h2, h3, h4 { margin: 0; line-height: 1.3; } +p { margin: 0; } + +:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + border-radius: 4px; +} + +/* ─── 应用外壳 ──────────────────────────────────────────────────── */ +.shell { display: grid; grid-template-columns: var(--side-w) 1fr; min-height: 100vh; } + +.sidebar { + background: var(--surface); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + position: sticky; + top: 0; + height: 100vh; +} +.side-brand { + padding: 18px 20px 16px; + border-bottom: 1px solid var(--border); +} +.side-brand .brand-name { font-size: 15px; font-weight: 650; letter-spacing: -0.01em; } +.side-brand .brand-sub { font-family: var(--font-mono); font-size: 11px; color: var(--muted); margin-top: 3px; letter-spacing: 0.02em; } +.side-role { + display: inline-flex; align-items: center; gap: 6px; + margin-top: 10px; + padding: 3px 9px; + border-radius: 999px; + background: var(--accent-soft); + color: var(--accent); + font-size: 11px; font-weight: 600; +} +.side-role.company { background: var(--info-soft); color: var(--info); } + +.side-nav { flex: 1; overflow-y: auto; padding: 12px 10px; } +.side-nav .nav-group { font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.08em; color: var(--muted); padding: 14px 10px 6px; } +.side-nav a { + display: flex; align-items: center; gap: 10px; + padding: 8px 10px; + border-radius: var(--radius); + color: var(--muted); + font-size: 13.5px; + margin-bottom: 1px; +} +.side-nav a svg { width: 16px; height: 16px; flex: none; } +.side-nav a:hover { color: var(--fg); background: var(--fg-soft); } +.side-nav a.active { + color: var(--fg); + background: var(--accent-soft); + font-weight: 600; +} +.side-nav a.active svg { color: var(--accent); } +.side-nav a .nav-badge { + margin-left: auto; + font-family: var(--font-mono); + font-size: 10.5px; + padding: 1px 7px; + border-radius: 999px; + background: var(--danger-soft); + color: var(--danger); + font-weight: 600; +} +.side-foot { border-top: 1px solid var(--border); padding: 12px 16px; } +.side-foot .user-row { display: flex; align-items: center; gap: 10px; min-width: 0; } +.side-foot .user-row > div { min-width: 0; } +.side-foot .avatar { + width: 30px; height: 30px; border-radius: 50%; + background: var(--fg); color: var(--surface); + display: grid; place-items: center; + font-size: 12px; font-weight: 600; + flex: none; +} +.side-foot .user-name { font-size: 13px; font-weight: 600; white-space: nowrap; } +.side-foot .user-meta { font-family: var(--font-mono); font-size: 11px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.side-foot .logout { margin-left: auto; color: var(--muted); font-size: 12px; padding: 4px 6px; border-radius: 6px; background: none; border: 0; flex: none; white-space: nowrap; } +.side-foot .logout:hover { color: var(--danger); background: var(--danger-soft); } + +.main { min-width: 0; display: flex; flex-direction: column; } + +.topbar { + position: sticky; top: 0; z-index: 20; + background: color-mix(in oklch, var(--bg) 88%, transparent); + backdrop-filter: blur(10px); + border-bottom: 1px solid var(--border); + padding: 12px 28px; + display: flex; align-items: center; gap: 16px; + flex-wrap: wrap; +} +.topbar .crumb { font-family: var(--font-mono); font-size: 12px; color: var(--muted); } +.topbar .crumb b { color: var(--fg); font-weight: 600; } +.topbar .topbar-right { margin-left: auto; display: flex; align-items: center; gap: 10px; } + +.content { padding: 24px 28px 64px; max-width: 1440px; width: 100%; margin-inline: auto; } + +/* ─── 页头 ──────────────────────────────────────────────────────── */ +.page-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; margin-bottom: 20px; flex-wrap: wrap; } +.page-head h1 { font-size: 22px; font-weight: 700; letter-spacing: -0.015em; } +.page-head .page-sub { color: var(--muted); font-size: 13px; margin-top: 5px; max-width: 72ch; } +.page-head .page-actions { display: flex; gap: 8px; flex-wrap: wrap; } + +/* ─── 按钮 ──────────────────────────────────────────────────────── */ +.btn { + display: inline-flex; align-items: center; justify-content: center; gap: 7px; + padding: 8px 14px; + min-height: 34px; + border-radius: var(--radius); + border: 1px solid var(--border); + background: var(--surface); + color: var(--fg); + font-size: 13px; font-weight: 550; + transition: background 0.12s ease, border-color 0.12s ease; +} +.btn:hover { border-color: color-mix(in oklch, var(--fg) 40%, var(--border)); background: var(--fg-soft); } +.btn:active { transform: translateY(1px); } +.btn-primary { background: var(--accent); border-color: var(--accent); color: var(--surface); } +.btn-primary:hover { background: color-mix(in oklch, var(--accent) 88%, black); border-color: color-mix(in oklch, var(--accent) 88%, black); } +.btn-ghost { background: transparent; border-color: transparent; color: var(--muted); } +.btn-ghost:hover { color: var(--fg); background: var(--fg-soft); } +.btn-danger { color: var(--danger); border-color: color-mix(in oklch, var(--danger) 35%, var(--border)); } +.btn-danger:hover { background: var(--danger-soft); border-color: var(--danger); } +.btn-sm { padding: 4px 10px; min-height: 26px; font-size: 12px; border-radius: 6px; } +.btn[disabled] { opacity: 0.5; cursor: not-allowed; } +.btn-primary[disabled] { opacity: 1; background: var(--fg-soft); border-color: var(--border); color: var(--muted); } + +/* ─── 卡片 ──────────────────────────────────────────────────────── */ +.card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 18px 20px; +} +.card-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; } +.card-title { font-size: 14px; font-weight: 650; letter-spacing: -0.005em; } +.card-title .sub { display: block; font-size: 12px; font-weight: 400; color: var(--muted); margin-top: 2px; } + +/* ─── 指标卡 ────────────────────────────────────────────────────── */ +.stat-card { padding: 16px 18px; } +.stat-card .stat-label { font-size: 12.5px; color: var(--muted); display: flex; align-items: center; gap: 6px; } +.stat-card .stat-value { + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + font-size: 26px; font-weight: 650; + letter-spacing: -0.02em; + margin-top: 6px; +} +.stat-card .stat-value .unit { font-size: 13px; color: var(--muted); font-weight: 400; margin-left: 2px; } +.stat-card .stat-foot { font-size: 12px; color: var(--muted); margin-top: 6px; } +.stat-card.alert .stat-value { color: var(--danger); } +.stat-card.warn .stat-value { color: var(--warn); } +.stat-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; } +.stat-dot.danger { background: var(--danger); } +.stat-dot.warn { background: var(--warn); } +.stat-dot.success { background: var(--success); } +.stat-dot.info { background: var(--info); } + +/* ─── 栅格与布局工具 ────────────────────────────────────────────── */ +.grid { display: grid; gap: 14px; } +.grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } +.grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } +.grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } +.grid-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); } +.grid-2-1 { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); align-items: start; } +.grid-1-2 { grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); align-items: start; } +.grid-3-2 { grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); align-items: start; } +.row { display: flex; align-items: center; gap: 10px; } +.row-between { display: flex; align-items: center; justify-content: space-between; gap: 12px; } +.stack { display: flex; flex-direction: column; gap: 14px; } +.muted { color: var(--muted); } +.num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; } +.meta { font-family: var(--font-mono); font-size: 12px; color: var(--muted); } +.mt-0 { margin-top: 0; } + +@media (max-width: 1100px) { + .grid-4, .grid-5 { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .grid-3, .grid-2-1, .grid-1-2, .grid-3-2 { grid-template-columns: minmax(0, 1fr); } +} +@media (max-width: 860px) { + .shell { grid-template-columns: 1fr; } + .sidebar { position: static; height: auto; } + .grid-2 { grid-template-columns: 1fr; } + .content { padding: 16px 16px 48px; } +} + +/* ─── 数据表 ────────────────────────────────────────────────────── */ +.table-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--surface); } +.ds-table { width: 100%; border-collapse: collapse; font-size: 13px; min-width: 640px; } +.ds-table th, .ds-table td { padding: 9px 14px; text-align: left; border-bottom: 1px solid var(--border); white-space: nowrap; } +.ds-table th { + color: var(--muted); font-weight: 550; + font-family: var(--font-mono); font-size: 11px; + letter-spacing: 0.05em; + background: var(--bg); + position: sticky; top: 0; +} +.ds-table tbody tr:hover { background: var(--fg-soft); } +.ds-table tbody tr:last-child td { border-bottom: 0; } +.ds-table .num-col { font-family: var(--font-mono); font-variant-numeric: tabular-nums; text-align: right; } +.ds-table td.wrap, .ds-table th.wrap { white-space: normal; min-width: 180px; } +.ds-table .cell-main { font-weight: 550; } +.ds-table .cell-sub { display: block; font-family: var(--font-mono); font-size: 11px; color: var(--muted); margin-top: 1px; } +.ds-table tr.clickable { cursor: pointer; } +.ds-table .amt-in { color: var(--success); } +.ds-table .amt-out { color: var(--danger); } +.table-foot { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; border-top: 1px solid var(--border); font-size: 12px; color: var(--muted); } + +/* ─── 徽章 / 状态 ───────────────────────────────────────────────── */ +.pill { + display: inline-flex; align-items: center; gap: 5px; + padding: 2px 9px; + border-radius: 999px; + font-size: 11.5px; font-weight: 600; + white-space: nowrap; +} +.pill::before { content: ""; width: 5px; height: 5px; border-radius: 50%; background: currentColor; } +.pill-success { background: var(--success-soft); color: var(--success); } +.pill-warn { background: var(--warn-soft); color: color-mix(in oklch, var(--warn) 80%, black); } +.pill-danger { background: var(--danger-soft); color: var(--danger); } +.pill-info { background: var(--info-soft); color: var(--info); } +.pill-muted { background: var(--fg-soft); color: var(--muted); } +.pill-accent { background: var(--accent-soft); color: var(--accent); } + +.tag { + display: inline-flex; align-items: center; + padding: 2px 8px; + border: 1px solid var(--border); + border-radius: 6px; + font-family: var(--font-mono); + font-size: 11px; + color: var(--muted); + white-space: nowrap; +} + +/* ─── 筛选栏与表单 ──────────────────────────────────────────────── */ +.filters { + display: flex; align-items: flex-end; gap: 12px; flex-wrap: wrap; + padding: 14px 16px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + margin-bottom: 14px; +} +.field { display: flex; flex-direction: column; gap: 5px; min-width: 0; } +.field > label { font-size: 12px; color: var(--muted); font-weight: 550; } +.input, .select, .textarea, .field .input, .field .select, .field .textarea { + padding: 7px 11px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface); + color: var(--fg); + font: inherit; + font-size: 13px; + min-height: 34px; +} +.input:focus, .select:focus, .textarea:focus, .field .input:focus, .field .select:focus, .field .textarea:focus { + outline: 2px solid var(--accent-soft); + border-color: var(--accent); +} +.input.num-input { font-family: var(--font-mono); font-variant-numeric: tabular-nums; } +.textarea { min-height: 84px; resize: vertical; line-height: 1.55; } +.field .hint { font-size: 11.5px; color: var(--muted); } +.field .hint.error { color: var(--danger); } + +/* ─── Tabs ──────────────────────────────────────────────────────── */ +.tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--border); margin-bottom: 16px; overflow-x: auto; } +.tabs button { + background: none; border: 0; + padding: 9px 14px; + font-size: 13px; font-weight: 550; + color: var(--muted); + border-bottom: 2px solid transparent; + margin-bottom: -1px; + flex: none; + white-space: nowrap; +} +.tabs button:hover { color: var(--fg); } +.tabs button.active { color: var(--fg); border-bottom-color: var(--accent); } +.tabs button .tab-count { + font-family: var(--font-mono); font-size: 10.5px; + padding: 1px 6px; border-radius: 999px; + background: var(--fg-soft); color: var(--muted); + margin-left: 5px; +} +.tabs button.active .tab-count { background: var(--accent-soft); color: var(--accent); } + +/* ─── 时间轴(账期) ─────────────────────────────────────────────── */ +.timeline { display: flex; gap: 0; border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--surface); padding: 0; } +.tl-cell { flex: 1; padding: 12px 10px; border-right: 1px solid var(--border); text-align: center; min-width: 0; } +.tl-cell:last-child { border-right: 0; } +.tl-cell .tl-month { font-family: var(--font-mono); font-size: 11px; color: var(--muted); letter-spacing: 0.04em; } +.tl-cell .tl-state { font-size: 12px; font-weight: 600; margin-top: 4px; display: flex; align-items: center; justify-content: center; gap: 5px; } +.tl-cell.closed { background: var(--fg-soft); } +.tl-cell.closed .tl-state { color: var(--muted); } +.tl-cell.current { background: var(--accent-soft); } +.tl-cell.current .tl-state { color: var(--accent); } +.tl-cell.open .tl-state { color: var(--fg); } + +/* ─── 进度条 ────────────────────────────────────────────────────── */ +.progress { height: 6px; border-radius: 999px; background: var(--fg-soft); overflow: hidden; } +.progress > span { display: block; height: 100%; border-radius: 999px; background: var(--accent); } +.progress.warn > span { background: var(--warn); } +.progress.danger > span { background: var(--danger); } + +/* ─── 列表行(快查 / 待办 / 通知) ────────────────────────────────── */ +.list-row { + display: flex; align-items: center; gap: 12px; + padding: 10px 4px; + border-bottom: 1px solid var(--border); + font-size: 13px; +} +.list-row:last-child { border-bottom: 0; } +.list-row .lr-main { min-width: 0; flex: 1; } +.list-row .lr-title { font-weight: 550; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.list-row .lr-sub { font-size: 12px; color: var(--muted); margin-top: 1px; } +.list-row .lr-side { text-align: right; flex: none; } +[data-detail] { cursor: pointer; } +.list-row[data-detail]:hover { background: var(--fg-soft); } +.list-row[data-detail] { margin-inline: -4px; padding-inline: 4px; border-radius: var(--radius); } + +/* ─── 弹窗 ──────────────────────────────────────────────────────── */ +.modal-backdrop { + position: fixed; inset: 0; z-index: 50; + background: color-mix(in oklch, var(--fg) 45%, transparent); + display: none; + align-items: center; justify-content: center; + padding: 24px; +} +.modal-backdrop.open { display: flex; } +.modal { + background: var(--surface); + border-radius: var(--radius-lg); + border: 1px solid var(--border); + width: 100%; max-width: 560px; + max-height: 86vh; overflow-y: auto; + padding: 22px 24px; + box-shadow: 0 18px 50px color-mix(in oklch, var(--fg) 25%, transparent); +} +.modal.wide { max-width: 760px; } +.modal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; } +.modal-title { font-size: 16px; font-weight: 700; } +.modal-close { background: none; border: 0; color: var(--muted); font-size: 18px; padding: 4px 8px; border-radius: 6px; } +.modal-close:hover { background: var(--fg-soft); color: var(--fg); } +.modal-sub { font-size: 12.5px; color: var(--muted); margin-bottom: 16px; } +.modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 20px; } + +/* ─── 明细展开行 ────────────────────────────────────────────────── */ +.detail-box { + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 12px 14px; + font-size: 12.5px; +} +.kv { display: grid; grid-template-columns: auto 1fr; gap: 4px 18px; font-size: 12.5px; } +.kv dt { color: var(--muted); } +.kv dd { margin: 0; font-family: var(--font-mono); font-variant-numeric: tabular-nums; } + +/* ─── 提示条 ────────────────────────────────────────────────────── */ +.notice { + display: flex; gap: 10px; align-items: flex-start; + padding: 12px 14px; + border-radius: var(--radius-lg); + border: 1px solid var(--border); + font-size: 13px; + background: var(--surface); +} +.notice.warn { background: var(--warn-soft); border-color: color-mix(in oklch, var(--warn) 30%, transparent); } +.notice.danger { background: var(--danger-soft); border-color: color-mix(in oklch, var(--danger) 30%, transparent); } +.notice.info { background: var(--info-soft); border-color: color-mix(in oklch, var(--info) 25%, transparent); } +.notice .n-title { font-weight: 650; } +.notice .n-body { color: color-mix(in oklch, var(--fg) 80%, var(--muted)); margin-top: 2px; font-size: 12.5px; } + +/* ─── 登录页 ────────────────────────────────────────────────────── */ +.login-wrap { + min-height: 100vh; + display: grid; + grid-template-columns: 1fr 1fr; +} +.login-aside { + background: var(--fg); + color: var(--surface); + padding: 48px 56px; + display: flex; flex-direction: column; +} +.login-aside .brand-mark { font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.1em; opacity: 0.65; } +.login-aside h1 { font-size: 30px; font-weight: 700; letter-spacing: -0.02em; margin-top: 18px; line-height: 1.25; } +.login-aside .aside-sub { opacity: 0.72; font-size: 14px; margin-top: 14px; max-width: 40ch; } +.login-aside .aside-list { margin-top: auto; display: flex; flex-direction: column; gap: 12px; } +.login-aside .aside-item { display: flex; gap: 10px; font-size: 13px; opacity: 0.85; align-items: baseline; } +.login-aside .aside-item .tick { font-family: var(--font-mono); color: var(--accent); } +.login-panel { display: grid; place-items: center; padding: 48px 32px; } +.login-card { width: 100%; max-width: 400px; } +.login-card h2 { font-size: 20px; font-weight: 700; letter-spacing: -0.01em; } +.login-card .login-sub { color: var(--muted); font-size: 13px; margin: 6px 0 24px; } +.role-switch { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 18px; } +.role-switch button { + border: 1px solid var(--border); + background: var(--surface); + border-radius: var(--radius); + padding: 12px 10px; + text-align: left; + transition: border-color 0.12s ease, background 0.12s ease; +} +.role-switch button .r-name { font-size: 13.5px; font-weight: 650; } +.role-switch button .r-desc { font-size: 11.5px; color: var(--muted); margin-top: 2px; } +.role-switch button.active { border-color: var(--accent); background: var(--accent-soft); } +.role-switch button.active .r-name { color: var(--accent); } +.login-card .field { margin-bottom: 14px; } +.login-foot { margin-top: 16px; font-size: 12px; color: var(--muted); text-align: center; } +.login-foot a { color: var(--accent); font-weight: 550; } +.login-foot a:hover { text-decoration: underline; } +@media (max-width: 900px) { + .login-wrap { grid-template-columns: 1fr; } + .login-aside { display: none; } +} + +/* ─── 空状态 ────────────────────────────────────────────────────── */ +.empty { + padding: 40px 20px; + text-align: center; + color: var(--muted); + font-size: 13px; +} +.empty .e-title { font-weight: 600; color: var(--fg); margin-bottom: 4px; } + +/* ─── 账期流程条(公司端工作台) ─────────────────────────────────── */ +.flow { display: grid; grid-template-columns: repeat(5, 1fr); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--surface); } +.flow-step { padding: 14px 16px 13px; border-right: 1px solid var(--border); min-width: 0; } +.flow-step:last-child { border-right: 0; } +a.flow-step { cursor: pointer; transition: background 0.12s ease; } +a.flow-step:hover { background: var(--fg-soft); } +.flow-step .fs-top { display: flex; align-items: center; gap: 8px; } +.flow-step .fs-idx { font-family: var(--font-mono); font-size: 11px; color: var(--muted); } +.flow-step .fs-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--border); flex: none; } +.flow-step .fs-name { font-size: 13.5px; font-weight: 650; } +.flow-step .fs-state { font-size: 12px; font-weight: 600; margin-top: 7px; color: var(--muted); } +.flow-step .fs-meta { font-size: 12px; color: var(--muted); margin-top: 2px; } +.flow-step.done .fs-dot { background: var(--success); } +.flow-step.done .fs-state { color: var(--success); } +.flow-step.part .fs-dot { background: var(--warn); } +.flow-step.part .fs-state { color: color-mix(in oklch, var(--warn) 78%, black); } +.flow-step.doing { background: var(--warn-soft); } +.flow-step.doing .fs-dot { background: var(--warn); box-shadow: 0 0 0 3px color-mix(in oklch, var(--warn) 22%, transparent); } +.flow-step.doing .fs-state { color: color-mix(in oklch, var(--warn) 78%, black); } +a.flow-step.doing:hover { background: color-mix(in oklch, var(--warn) 16%, transparent); } +@media (max-width: 860px) { + .flow { grid-template-columns: 1fr; } + .flow-step { border-right: 0; border-bottom: 1px solid var(--border); } + .flow-step:last-child { border-bottom: 0; } +} + +/* ─── 迷你指标(2×2 数字块) ────────────────────────────────────── */ +.mini-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } +.mini-stat { background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); padding: 11px 13px; min-width: 0; } +.mini-stat .ms-label { font-size: 11.5px; color: var(--muted); } +.mini-stat .ms-value { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 17px; font-weight: 650; letter-spacing: -0.01em; margin-top: 3px; } +.mini-stat .ms-value .unit { font-size: 11px; font-weight: 400; color: var(--muted); margin-left: 2px; } +.mini-stat .ms-value.pos { color: var(--success); } +.mini-stat .ms-value.neg { color: var(--danger); } + +/* ─── 上传组件(拖放区 / 文件条 / 附件选择) ────────────────────── */ +.dropzone { + border: 1.5px dashed color-mix(in oklch, var(--fg) 28%, var(--border)); + border-radius: var(--radius-lg); + background: var(--bg); + padding: 30px 20px; + text-align: center; + cursor: pointer; + transition: border-color 0.12s ease, background 0.12s ease; +} +.dropzone:hover, .dropzone:focus-visible { border-color: var(--accent); background: var(--accent-soft); } +.dropzone.dragover { border-color: var(--accent); border-style: solid; background: var(--accent-soft); } +.dropzone svg { width: 28px; height: 28px; margin: 0 auto 10px; color: var(--muted); } +.dropzone .dz-title { font-size: 14px; font-weight: 650; } +.dropzone .dz-sub { font-size: 12.5px; color: var(--muted); margin-top: 5px; } +.dropzone .dz-sub .dz-browse { color: var(--accent); font-weight: 600; text-decoration: underline; text-underline-offset: 3px; } +.dropzone .dz-formats { font-family: var(--font-mono); font-size: 11px; color: var(--muted); margin-top: 12px; letter-spacing: 0.02em; } + +.file-chip { + display: flex; align-items: center; gap: 10px; + margin-top: 12px; + padding: 10px 12px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface); +} +.file-chip svg { width: 18px; height: 18px; color: var(--muted); flex: none; } +.file-chip .fc-info { min-width: 0; flex: 1; } +.file-chip .fc-name { font-size: 13px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.file-chip .fc-meta { font-family: var(--font-mono); font-size: 11px; color: var(--muted); margin-top: 1px; } +.file-chip .fc-remove { background: none; border: 0; color: var(--muted); font-size: 16px; line-height: 1; padding: 4px 8px; border-radius: 6px; flex: none; } +.file-chip .fc-remove:hover { color: var(--danger); background: var(--danger-soft); } + +.attach-pick { + display: inline-flex; align-items: center; gap: 7px; + padding: 7px 12px; + min-height: 34px; + border: 1.5px dashed color-mix(in oklch, var(--fg) 28%, var(--border)); + border-radius: var(--radius); + color: var(--muted); + font-size: 13px; + cursor: pointer; + transition: border-color 0.12s ease, color 0.12s ease, background 0.12s ease; +} +.attach-pick svg { width: 15px; height: 15px; } +.attach-pick:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); } + +/* ─── 数据表合计行 ─────────────────────────────────────────────── */ +.ds-table tfoot td { background: var(--bg); font-weight: 650; border-top: 1.5px solid color-mix(in oklch, var(--fg) 30%, var(--border)); border-bottom: 0; } + +/* ─── 入口页(index) ────────────────────────────────────────────── */ +.portal-wrap { min-height: 100vh; display: grid; place-items: center; padding: 40px 24px; } +.portal-inner { width: 100%; max-width: 880px; } +.portal-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 28px; } +.portal-card { + display: block; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 24px; + transition: border-color 0.12s ease, transform 0.12s ease; +} +.portal-card:hover { border-color: var(--accent); transform: translateY(-2px); } +.portal-card .pc-role { font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.08em; color: var(--accent); } +.portal-card h3 { font-size: 17px; font-weight: 700; margin-top: 8px; } +.portal-card p { color: var(--muted); font-size: 13px; margin-top: 6px; } +.portal-card .pc-go { font-size: 12.5px; font-weight: 600; color: var(--accent); margin-top: 14px; } +@media (max-width: 720px) { .portal-grid { grid-template-columns: 1fr; } } + +/* ─── SPA 视图切换 / 无障碍结构类 ───────────────────────────────── */ +.app-view { display: none; } +.app-view.is-active { display: block; } +.sr-only { position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } +.skip-link { + position: fixed; top: 8px; left: 50%; z-index: 500; + padding: 8px 14px; + border-radius: var(--radius); + background: var(--accent); color: var(--surface); + font-size: 13px; font-weight: 600; + transform: translate(-50%, -150%); + transition: transform 0.12s ease; +} +.skip-link:focus { transform: translate(-50%, 0); } + +/* ─── 图标按钮 / 窄屏侧栏折叠 ──────────────────────────────────── */ +.icon-button { + display: inline-grid; place-items: center; + width: 34px; height: 34px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface); color: var(--muted); + cursor: pointer; + transition: border-color 0.12s ease, background 0.12s ease, color 0.12s ease; +} +.icon-button:hover { color: var(--fg); border-color: color-mix(in oklch, var(--fg) 40%, var(--border)); background: var(--fg-soft); } +.icon-button svg { width: 18px; height: 18px; } +.menu-button { display: none; } +@media (max-width: 860px) { + .menu-button { display: inline-grid; } + .shell { grid-template-columns: 1fr; } + .sidebar { + position: fixed; top: 0; left: 0; bottom: 0; z-index: 40; + width: var(--side-w); height: 100dvh; + transform: translateX(-100%); + transition: transform 0.2s ease; + } + .sidebar.is-open { transform: none; box-shadow: 0 0 48px color-mix(in oklch, var(--fg) 22%, transparent); } +} + +/* ─── Toast 轻量提示 ───────────────────────────────────────────── */ +.toast-region { position: fixed; right: 16px; bottom: 16px; z-index: 300; display: grid; gap: 8px; } +.toast { + display: flex; align-items: flex-start; gap: 9px; + min-width: 260px; max-width: 380px; + padding: 11px 14px; + border: 1px solid var(--border); + border-radius: var(--radius-lg); + background: var(--surface); + box-shadow: 0 12px 34px color-mix(in oklch, var(--fg) 18%, transparent); + font-size: 13px; +} +.toast .t-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; margin-top: 5px; background: var(--muted); } +.toast .t-body { min-width: 0; } +.toast .t-title { font-weight: 650; } +.toast .t-detail { color: var(--muted); font-size: 12px; margin-top: 1px; } +.toast.success { border-color: color-mix(in oklch, var(--success) 35%, var(--border)); } +.toast.success .t-dot { background: var(--success); } +.toast.warn { border-color: color-mix(in oklch, var(--warn) 40%, var(--border)); } +.toast.warn .t-dot { background: var(--warn); } +.toast.danger { border-color: color-mix(in oklch, var(--danger) 35%, var(--border)); } +.toast.danger .t-dot { background: var(--danger); } +.toast.info { border-color: color-mix(in oklch, var(--info) 35%, var(--border)); } +.toast.info .t-dot { background: var(--info); } +@media (max-width: 720px) { + .toast-region { right: 12px; bottom: 12px; left: 12px; } + .toast { min-width: 0; max-width: none; } +} + +/* ─── 侧滑详情抽屉(用 modal/card token 重画) ─────────────────── */ +.drawer { + position: fixed; top: 0; right: 0; bottom: 0; z-index: 60; + width: min(400px, calc(100vw - 24px)); + display: flex; flex-direction: column; + background: var(--surface); + border-left: 1px solid var(--border); + box-shadow: -18px 0 50px color-mix(in oklch, var(--fg) 22%, transparent); + transform: translateX(100%); + visibility: hidden; + transition: transform 0.22s ease, visibility 0.22s; +} +.drawer.is-open { transform: none; visibility: visible; } +.drawer-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 18px 20px 12px; border-bottom: 1px solid var(--border); } +.drawer-head .d-title { font-size: 16px; font-weight: 700; margin-top: 6px; } +.drawer-head .d-desc { color: var(--muted); font-size: 12.5px; margin-top: 4px; } +.drawer-body { flex: 1; overflow-y: auto; padding: 16px 20px; } +.drawer-body .kv { grid-template-columns: auto 1fr; gap: 8px 18px; } +.drawer-body .kv dt { color: var(--muted); white-space: nowrap; } +.drawer-body .kv dd { text-align: right; word-break: break-all; } +.drawer-tip { margin: 0 20px 12px; padding: 12px 14px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg); color: var(--muted); font-size: 12.5px; } +.drawer-tip strong { display: block; color: var(--fg); margin-bottom: 4px; } +.drawer-foot { display: flex; justify-content: flex-end; gap: 8px; padding: 14px 20px; border-top: 1px solid var(--border); } + +/* ─── 加载中骨架 / 表内加载行 ──────────────────────────────────── */ +.skeleton { display: block; border-radius: var(--radius); background: var(--fg-soft); position: relative; overflow: hidden; } +.skeleton::after { + content: ""; position: absolute; inset: 0; + background: linear-gradient(90deg, transparent, color-mix(in oklch, var(--surface) 60%, transparent), transparent); + animation: skeleton-shimmer 1.3s ease-in-out infinite; +} +@keyframes skeleton-shimmer { from { transform: translateX(-100%); } to { transform: translateX(100%); } } +.skeleton-line { height: 12px; margin-bottom: 10px; } +.loading-row { padding: 40px 20px; text-align: center; color: var(--muted); font-family: var(--font-mono); font-size: 12.5px; letter-spacing: 0.04em; } +.loading-inline { display: inline-flex; align-items: center; gap: 8px; color: var(--muted); font-family: var(--font-mono); font-size: 12.5px; } +.loading-inline::before { + content: ""; width: 12px; height: 12px; border-radius: 50%; + border: 2px solid var(--border); border-top-color: var(--accent); + animation: loading-spin 0.7s linear infinite; +} +@keyframes loading-spin { to { transform: rotate(360deg); } } + +/* ─── 提示条补充:成功态 ───────────────────────────────────────── */ +.notice.success { background: var(--success-soft); border-color: color-mix(in oklch, var(--success) 30%, transparent); } +.notice.success .n-title { color: color-mix(in oklch, var(--success) 82%, black); } diff --git a/web/index.html b/web/index.html index 26c68b7..6e276ca 100644 --- a/web/index.html +++ b/web/index.html @@ -1,48 +1,33 @@ - - - - - 登录 · 河南金牛实业集团 - - - - -
-
-
河南金牛实业集团集团资金往来管理系统
-
-

登录

请选择与账号一致的工作端口

-
- - -
- - - - - - -
-
-
全局起算日
2026.01.01
-
当前账期
2026 年 7 月
-
覆盖银行
已对接 6 家
-
-
-
- - + + + + 金牛实业资金往来管理系统 · 入口 + + + + + diff --git a/web/login-admin.html b/web/login-admin.html new file mode 100644 index 0000000..537ff1a --- /dev/null +++ b/web/login-admin.html @@ -0,0 +1,77 @@ + + + + + + 登录 · 金牛实业资金往来管理系统 + + + + + + + + + + diff --git a/web/login-company.html b/web/login-company.html new file mode 100644 index 0000000..90c48b4 --- /dev/null +++ b/web/login-company.html @@ -0,0 +1,77 @@ + + + + + + 登录 · 金牛实业资金往来管理系统 + + + + + + + + + + diff --git a/web/login.js b/web/login.js new file mode 100644 index 0000000..94fc2ea --- /dev/null +++ b/web/login.js @@ -0,0 +1,96 @@ +const ROLE = document.body.dataset.role || "admin"; +const REDIRECT = ROLE === "admin" ? "admin.html" : "company.html"; + +const form = document.getElementById("login-form"); +const accountInput = document.getElementById("account"); +const passwordInput = document.getElementById("password"); +const accountErr = document.getElementById("account-err"); +const passwordErr = document.getElementById("password-err"); +const loginErr = document.getElementById("login-err"); +const submitButton = form.querySelector('button[type="submit"]'); + +const modal = document.getElementById("pwd-modal"); +const newPwd = document.getElementById("new-pwd"); +const newPwd2 = document.getElementById("new-pwd2"); +const newPwdErr = document.getElementById("new-pwd-err"); +const newPwd2Err = document.getElementById("new-pwd2-err"); +const pwdErr = document.getElementById("pwd-err"); +const pwdConfirm = document.getElementById("pwd-confirm"); + +const submitLabel = "登 录"; + +function setLoading(loading) { + submitButton.disabled = loading; + submitButton.textContent = loading ? "登录中…" : submitLabel; + accountInput.disabled = loading; + passwordInput.disabled = loading; +} + +function showLoginError(message) { + loginErr.textContent = message || ""; + loginErr.hidden = !message; +} + +function openPasswordModal() { + newPwd.value = ""; + newPwd2.value = ""; + newPwdErr.hidden = true; + newPwd2Err.hidden = true; + pwdErr.hidden = true; + modal.classList.add("open"); + newPwd.focus(); +} + +form.addEventListener("submit", async (event) => { + event.preventDefault(); + showLoginError(""); + const account = accountInput.value.trim(); + const password = passwordInput.value; + accountErr.hidden = Boolean(account); + passwordErr.hidden = Boolean(password); + if (!account || !password) return; + + setLoading(true); + const response = await fetch("/api/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username: account, password, portal: ROLE }), + }).catch(() => null); + const result = await response?.json().catch(() => ({})); + if (!response || !response.ok) { + showLoginError(result?.message || "登录服务暂时不可用,请稍后重试。"); + setLoading(false); + return; + } + if (result.must_change_password) { + setLoading(false); + openPasswordModal(); + return; + } + window.location.href = REDIRECT; +}); + +pwdConfirm.addEventListener("click", async () => { + const p1 = newPwd.value; + const p2 = newPwd2.value; + const valid = p1.length >= 8 && /[a-zA-Z]/.test(p1) && /\d/.test(p1); + newPwdErr.hidden = valid; + newPwd2Err.hidden = p1 === p2; + pwdErr.hidden = true; + if (!valid || p1 !== p2) return; + + pwdConfirm.disabled = true; + const response = await fetch("/api/password/change", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ old_password: passwordInput.value, new_password: p1 }), + }).catch(() => null); + const result = await response?.json().catch(() => ({})); + if (!response || !response.ok) { + pwdErr.textContent = result?.message || "修改密码失败,请稍后重试。"; + pwdErr.hidden = false; + pwdConfirm.disabled = false; + return; + } + window.location.href = REDIRECT; +}); diff --git a/web/styles.css b/web/styles.css deleted file mode 100644 index 4e9408a..0000000 --- a/web/styles.css +++ /dev/null @@ -1,936 +0,0 @@ -:root { - color-scheme: dark; - --font-ui: "Microsoft YaHei UI", "PingFang SC", "Noto Sans CJK SC", system-ui, sans-serif; - --font-data: "Segoe UI", "Microsoft YaHei UI", system-ui, sans-serif; - --color-bg: #050505; - --color-bg-soft: #0a0a0a; - --color-nav: #0a0a0a; - --color-surface: rgba(22, 22, 22, 0.82); - --color-surface-solid: #161616; - --color-surface-raised: #1c1c1c; - --color-surface-muted: rgba(255, 255, 255, 0.035); - --color-line: rgba(255, 255, 255, 0.09); - --color-line-strong: rgba(255, 255, 255, 0.16); - --color-ink: #f2f7f4; - --color-ink-soft: #a6b0aa; - --color-ink-muted: #89938d; - --color-primary: #37eb89; - --color-primary-strong: #70ffae; - --color-primary-dark: #08160e; - --color-primary-wash: rgba(55, 235, 137, 0.11); - --color-positive: #37eb89; - --color-positive-wash: rgba(55, 235, 137, 0.1); - --color-warning: #ffbc52; - --color-warning-wash: rgba(255, 188, 82, 0.11); - --color-danger: #ff626d; - --color-danger-wash: rgba(255, 98, 109, 0.11); - --color-info: #66a8ff; - --color-info-wash: rgba(102, 168, 255, 0.11); - --row-h-evidence: 55px; - --z-drawer: 150; - --radius-sm: 10px; - --radius-md: 16px; - --radius-lg: 22px; - --radius-xl: 28px; - --shadow-panel: 0 24px 70px rgba(0, 0, 0, 0.34), inset 0 1px rgba(255, 255, 255, 0.035); - --shadow-low: 0 12px 32px rgba(0, 0, 0, 0.22), inset 0 1px rgba(255, 255, 255, 0.06), inset 0 -1px 0 rgba(0, 0, 0, 0.22); - --shadow-glow: 0 0 28px rgba(55, 235, 137, 0.12); - --duration-fast: 150ms; - --duration-standard: 260ms; - --ease-out: cubic-bezier(0.22, 1, 0.36, 1); -} - -* { box-sizing: border-box; } -html { min-width: 320px; background: var(--color-bg); scroll-behavior: smooth; } -body { margin: 0; min-width: 320px; min-height: 100vh; background: var(--color-bg); color: var(--color-ink); font: 14px/1.6 var(--font-ui); -webkit-font-smoothing: antialiased; } -button, input, select, textarea { font: inherit; } -button, a, summary { -webkit-tap-highlight-color: transparent; } -button { color: inherit; } -a { color: inherit; text-decoration: none; } -h1, h2, h3, p, dl, dd { margin: 0; } -svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; } -strong, .amount, .number, dd { font-variant-numeric: tabular-nums; } -::selection { background: rgba(55, 235, 137, 0.26); color: #fff; } -::-webkit-scrollbar { width: 9px; height: 9px; } -::-webkit-scrollbar-track { background: transparent; } -::-webkit-scrollbar-thumb { border: 2px solid transparent; border-radius: 10px; background: rgba(255, 255, 255, 0.15); background-clip: padding-box; } - -.sr-only { position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } -.skip-link { position: fixed; top: 8px; left: 50%; z-index: 500; padding: 8px 12px; border-radius: var(--radius-sm); background: var(--color-primary); color: var(--color-primary-dark); transform: translate(-50%, -150%); } -.skip-link:focus { transform: translate(-50%, 0); } -:focus-visible { outline: 2px solid var(--color-primary-strong); outline-offset: 3px; } -[hidden] { display: none !important; } - -/* Application frame */ -.app-shell { min-height: 100vh; display: grid; grid-template-columns: 266px minmax(0, 1fr); } -.sidebar { position: fixed; inset: 18px auto 18px 18px; z-index: 120; width: 248px; display: flex; flex-direction: column; padding: 16px 12px; border: 1px solid var(--color-line); border-radius: var(--radius-xl); background: rgba(12, 12, 12, 0.9); box-shadow: var(--shadow-panel); backdrop-filter: blur(26px) saturate(125%); } -.brand { min-height: 60px; display: flex; align-items: center; gap: 11px; padding: 7px 10px 20px; border-bottom: 1px solid var(--color-line); } -.brand-mark { width: 36px; height: 36px; display: grid; place-items: center; flex: 0 0 auto; border: 1px solid rgba(112, 255, 174, 0.32); border-radius: 12px; background: var(--color-primary-wash); color: var(--color-primary-strong); box-shadow: var(--shadow-glow); font-size: 18px; font-weight: 800; } -.brand-copy { display: flex; flex-direction: column; min-width: 0; } -.brand-copy strong { font-size: 15px; } -.brand-copy small, .user-block small, .company-context small { color: var(--color-ink-muted); font-size: 10px; } -.company-context { min-height: 64px; display: flex; align-items: center; gap: 10px; margin: 12px 4px 2px; padding: 10px; border: 1px solid var(--color-line); border-radius: var(--radius-md); background: var(--color-surface-muted); } -.company-context > span { width: 34px; height: 34px; display: grid; place-items: center; border-radius: 11px; background: var(--color-primary); color: var(--color-primary-dark); font-weight: 800; } -.company-context div { display: flex; flex-direction: column; } -.nav-list { display: grid; gap: 8px; padding: 14px 2px; overflow-y: auto; } -.nav-item { position: relative; width: 100%; min-height: 46px; display: flex; align-items: center; gap: 11px; padding: 0 12px; border: 1px solid transparent; border-radius: 14px; background: transparent; color: var(--color-ink-soft); text-align: left; cursor: pointer; transition: color var(--duration-fast), background var(--duration-fast), border-color var(--duration-fast), transform var(--duration-fast); } -.nav-item:hover { color: var(--color-ink); background: rgba(255, 255, 255, 0.045); transform: translateX(2px); } -.nav-item.is-active { border-color: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.075); color: var(--color-ink); box-shadow: inset 0 1px rgba(255, 255, 255, 0.045), 0 8px 22px rgba(0, 0, 0, 0.2); } -.nav-item.is-active svg { color: var(--color-primary); filter: drop-shadow(0 0 8px rgba(55, 235, 137, 0.42)); } -.nav-item svg { width: 17px; flex: 0 0 auto; } -.nav-item span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.nav-item b { min-width: 19px; height: 19px; display: grid; place-items: center; margin-left: auto; border-radius: 7px; background: var(--color-primary-wash); color: var(--color-primary); font-size: 10px; } -.sidebar-footer { display: grid; gap: 8px; margin-top: auto; padding-top: 12px; border-top: 1px solid var(--color-line); } -.user-block { min-height: 51px; display: flex; align-items: center; gap: 10px; padding: 7px 9px; } -.user-block > span:last-child { display: flex; flex-direction: column; } -.avatar { width: 34px; height: 34px; display: grid; place-items: center; flex: 0 0 auto; border-radius: 50%; background: #242c28; color: var(--color-primary-strong); font-weight: 700; } -.workspace { min-width: 0; grid-column: 2; } -.topbar { position: sticky; top: 0; z-index: 90; height: 56px; display: flex; align-items: center; gap: 13px; padding: 0 30px; border-bottom: 1px solid rgba(255, 255, 255, 0.055); background: rgba(5, 5, 5, 0.82); backdrop-filter: blur(22px); } -.topbar-spacer { flex: 1; } -.workspace-name { display: flex; flex-direction: column; min-width: 150px; } -.workspace-name span { color: var(--color-ink-muted); font-size: 10px; } -.workspace-name strong { font-size: 14px; } -.demo-badge { padding: 3px 8px; border: 1px solid rgba(255, 188, 82, 0.22); border-radius: 8px; background: var(--color-warning-wash); color: var(--color-warning); font-size: 10px; } -.topbar-actions { display: flex; align-items: center; gap: 9px; margin-left: auto; } -.search-box { width: clamp(200px, 22vw, 340px); height: 40px; display: flex; align-items: center; gap: 9px; padding: 0 13px; border: 1px solid var(--color-line); border-radius: 14px; background: rgba(255, 255, 255, 0.035); color: var(--color-ink-muted); transition: border-color var(--duration-fast), background var(--duration-fast), box-shadow var(--duration-fast); } -.search-box:focus-within { border-color: rgba(55, 235, 137, 0.4); background: rgba(255, 255, 255, 0.055); box-shadow: var(--shadow-glow); } -.search-box input { width: 100%; border: 0; outline: 0; background: transparent; color: var(--color-ink); } -.search-box input::placeholder, input::placeholder, textarea::placeholder { color: #626b66; } -.period-button, .icon-button, .inside-icon, .swap-button { display: inline-grid; place-items: center; border: 1px solid var(--color-line); background: var(--color-surface-muted); color: var(--color-ink-soft); cursor: pointer; transition: border-color var(--duration-fast), color var(--duration-fast), background var(--duration-fast), transform var(--duration-fast); } -.period-button { min-height: 40px; grid-auto-flow: column; gap: 8px; padding: 0 13px; border-radius: 14px; } -.icon-button { position: relative; width: 40px; height: 40px; border-radius: 13px; } -.inside-icon { width: 36px; height: 36px; border: 0; background: transparent; } -.period-button:hover, .icon-button:hover, .inside-icon:hover, .swap-button:hover { border-color: var(--color-line-strong); background: rgba(255, 255, 255, 0.07); color: var(--color-ink); transform: translateY(-1px); } -.notification-dot { position: absolute; top: -5px; right: -5px; min-width: 17px; height: 17px; display: grid; place-items: center; border: 2px solid var(--color-bg); border-radius: 9px; background: var(--color-danger); color: #fff; font: 9px var(--font-data); } -.menu-button { display: none; } -main { width: min(1560px, 100%); margin: 0 auto; padding: 12px 32px 58px; } -.app-view { display: none; } -.app-view.is-active { display: block; } -.page-heading { min-height: 56px; display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; margin-bottom: 18px; } -.page-heading h1 { font-size: clamp(22px, 2.4vw, 30px); line-height: 1.2; letter-spacing: 0; } -.page-heading p { margin-top: 5px; color: var(--color-ink-muted); } -.content-toolbar { display: contents; } -.content-toolbar > .search-box { margin-inline: auto; width: clamp(260px, 42vw, 520px); } -.content-toolbar .toolbar-group { display: flex; align-items: center; gap: 10px; flex-shrink: 0; } -.dashboard-head { align-items: center; gap: 20px; margin-bottom: 20px; } - -/* Commands and fields */ -.button { min-height: 40px; display: inline-flex; align-items: center; justify-content: center; gap: 8px; padding: 0 15px; border: 1px solid transparent; border-radius: 13px; font-weight: 700; cursor: pointer; transition: transform var(--duration-fast), box-shadow var(--duration-fast), background var(--duration-fast), border-color var(--duration-fast); } -.button:hover { transform: translateY(-1px); } -.button:active { transform: translateY(0) scale(0.985); } -.button.primary { background: var(--color-primary); color: var(--color-primary-dark); box-shadow: 0 8px 24px rgba(55, 235, 137, 0.18); } -.button.primary:hover { background: var(--color-primary-strong); box-shadow: 0 10px 30px rgba(55, 235, 137, 0.25); } -.button.secondary { border-color: var(--color-line); background: rgba(255, 255, 255, 0.045); color: var(--color-ink); } -.button.secondary:hover { border-color: var(--color-line-strong); background: rgba(255, 255, 255, 0.08); } -.button.small { min-height: 34px; padding-inline: 11px; border-radius: 11px; font-size: 11px; } -.button.wide { width: 100%; } -.button:disabled { opacity: 0.42; cursor: not-allowed; transform: none; } -.text-button { padding: 3px 0; border: 0; background: none; color: var(--color-primary); cursor: pointer; } -.text-button:hover { color: var(--color-primary-strong); } -.full-text-button { width: 100%; padding: 13px; border-top: 1px solid var(--color-line); } -.field { min-width: 0; display: flex; flex-direction: column; gap: 6px; color: var(--color-ink-soft); } -.field > span { color: var(--color-ink-muted); font-size: 11px; } -.field input, .field select, .field textarea { width: 100%; min-height: 40px; padding: 9px 11px; border: 1px solid var(--color-line); border-radius: 11px; outline: 0; background: rgba(2, 5, 3, 0.48); color: var(--color-ink); transition: border-color var(--duration-fast), box-shadow var(--duration-fast), background var(--duration-fast); } -.field textarea { resize: vertical; } -.field input:focus, .field select:focus, .field textarea:focus { border-color: rgba(55, 235, 137, 0.5); background: rgba(2, 5, 3, 0.7); box-shadow: 0 0 0 3px rgba(55, 235, 137, 0.08); } -.field select { color-scheme: dark; } -.field.compact { min-width: 150px; } -.password-field { display: grid; grid-template-columns: 1fr 38px; border: 1px solid var(--color-line); border-radius: 12px; background: rgba(2, 5, 3, 0.48); } -.password-field input { border: 0; background: transparent; } -.check-field { display: flex; align-items: center; gap: 8px; color: var(--color-ink-soft); } -.check-field input { accent-color: var(--color-primary); } -.swap-button { width: 38px; height: 38px; border-radius: 12px; align-self: end; } - -/* Glass surfaces */ -.panel, .period-ribbon, .query-band, .filter-bar, .filter-grid, .pair-report, .work-progress, .reconcile-summary { position: relative; border: 1px solid var(--color-line); border-radius: var(--radius-lg); background: linear-gradient(160deg, rgba(28, 28, 28, 0.82) 0%, rgba(16, 16, 16, 0.86) 50%, rgba(10, 10, 10, 0.88) 100%); box-shadow: var(--shadow-low); backdrop-filter: blur(22px) saturate(120%); transition: transform var(--duration-standard) var(--ease-out), box-shadow var(--duration-standard), border-color var(--duration-standard); } -.panel::before, .period-ribbon::before, .reconcile-summary::before, .work-progress::before { content: ""; position: absolute; inset: 0; border-radius: inherit; background: linear-gradient(165deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.03) 40%, rgba(255, 255, 255, 0) 70%); pointer-events: none; } -.panel:hover { box-shadow: 0 20px 48px rgba(0, 0, 0, 0.32), inset 0 1px rgba(255, 255, 255, 0.07); border-color: rgba(255, 255, 255, 0.14); } -.panel { min-width: 0; overflow: hidden; } -.panel > * { position: relative; } -.panel-heading { min-height: 67px; display: flex; align-items: center; justify-content: space-between; gap: 15px; padding: 14px 17px; border-bottom: 1px solid var(--color-line); } -.panel-heading h2 { font-size: 15px; } -.panel-heading p { margin-top: 3px; color: var(--color-ink-muted); font-size: 11px; } -.status { display: inline-flex; align-items: center; justify-content: center; gap: 5px; width: max-content; min-height: 23px; padding: 2px 8px; border: 1px solid transparent; border-radius: 8px; font-size: 10px; font-style: normal; white-space: nowrap; } -.status.success { border-color: rgba(55, 235, 137, 0.18); background: var(--color-positive-wash); color: var(--color-positive); } -.status.warning { border-color: rgba(255, 188, 82, 0.18); background: var(--color-warning-wash); color: var(--color-warning); } -.status.danger { border-color: rgba(255, 98, 109, 0.18); background: var(--color-danger-wash); color: var(--color-danger); } -.status.neutral { border-color: var(--color-line); background: rgba(255, 255, 255, 0.045); color: var(--color-ink-soft); } -.task-level { width: 30px; height: 30px; display: grid; place-items: center; border-radius: 10px; font-size: 10px; font-style: normal; font-weight: 800; } -.task-level.danger { background: var(--color-danger-wash); color: var(--color-danger); } -.task-level.warning { background: var(--color-warning-wash); color: var(--color-warning); } -.task-level.neutral { background: rgba(255, 255, 255, 0.055); color: var(--color-ink-soft); } - -/* Dashboard */ -.metric-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px; margin: 0 0 18px; } -.metric-card { position: relative; min-height: 164px; display: grid; grid-template-columns: minmax(0, 1fr) 48px; align-content: space-between; gap: 14px; padding: 24px 22px; overflow: hidden; border: 1px solid var(--color-line); border-radius: var(--radius-lg); background: linear-gradient(160deg, rgba(28, 28, 28, 0.86) 0%, rgba(14, 14, 14, 0.88) 50%, rgba(8, 8, 8, 0.9) 100%); box-shadow: var(--shadow-low); backdrop-filter: blur(24px) saturate(125%); transform: perspective(900px) rotateX(0.6deg) rotateY(-0.8deg); transform-origin: center bottom; transition: transform var(--duration-standard) var(--ease-out), border-color var(--duration-standard), box-shadow var(--duration-standard); } -.metric-card::before { content: ""; position: absolute; inset: 0; border-radius: inherit; background: linear-gradient(165deg, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0.04) 42%, rgba(255, 255, 255, 0) 72%); pointer-events: none; } -.metric-card::after { content: ""; position: absolute; inset: auto 15% -1px 15%; height: 1px; background: currentColor; opacity: 0.35; box-shadow: 0 -8px 24px currentColor; transition: opacity var(--duration-standard), box-shadow var(--duration-standard); } -.metric-card:hover::after { opacity: 0.7; box-shadow: 0 -10px 34px currentColor; } -.metric-card:hover { transform: perspective(900px) rotateX(3.5deg) rotateY(-4.5deg) translateY(-10px); border-color: rgba(255, 255, 255, 0.32); box-shadow: 0 36px 80px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.06), inset 0 1px rgba(255, 255, 255, 0.14); } -.metric-copy { align-self: start; } -.metric-label { display: block; color: var(--color-ink-soft); font-size: 13px; } -.metric-value { display: block; margin-top: 7px; font: 700 34px/1.05 var(--font-data); letter-spacing: 0; } -.metric-value small { margin-left: 5px; color: var(--color-ink-muted); font: 11px var(--font-ui); } -.metric-icon { position: relative; width: 48px; height: 48px; display: grid; place-items: center; border: 1px solid var(--color-line); border-radius: 50%; background: radial-gradient(circle at 50% 35%, rgba(255, 255, 255, 0.09), rgba(255, 255, 255, 0.02) 70%); color: var(--color-ink-soft); box-shadow: inset 0 1px rgba(255, 255, 255, 0.05); } -.metric-icon::after { content: ""; position: absolute; inset: -3px; border-radius: 50%; border: 1px solid currentColor; opacity: 0.12; pointer-events: none; } -.metric-foot { grid-column: 1 / -1; color: var(--color-ink-muted); font-size: 11px; } -.metric-foot strong { color: var(--color-positive); } -.metric-card.warning { color: var(--color-warning); } -.metric-card.danger { color: var(--color-danger); } -.metric-card.success { color: var(--color-positive); } -.metric-card .metric-copy { color: var(--color-ink); } -.metric-card .metric-icon { color: var(--color-ink-soft); } -.metric-card .metric-foot { color: var(--color-ink-muted); } -.metric-card[data-metric-link], .metric-card[data-metric-action] { cursor: pointer; } -.metric-card.warning .metric-icon, .metric-card.warning .metric-foot strong { color: var(--color-warning); } -.metric-card.warning .metric-icon::after { opacity: 0.28; } -.metric-card.danger .metric-icon, .metric-card.danger .metric-foot strong { color: var(--color-danger); } -.metric-card.danger .metric-icon::after { opacity: 0.32; } -.metric-card.success .metric-icon { color: var(--color-positive); } -.metric-card.success .metric-icon::after { opacity: 0.22; border-color: var(--color-positive); } -.period-ribbon { display: grid; grid-template-columns: 1fr 1fr 1fr minmax(310px, 1.4fr); margin-bottom: 18px; overflow: hidden; } -.period-ribbon > div { min-height: 72px; display: flex; flex-direction: column; justify-content: center; padding: 13px 17px; border-left: 1px solid var(--color-line); } -.period-ribbon > div:first-child { border-left: 0; } -.period-ribbon span, .period-ribbon small { color: var(--color-ink-muted); } -.period-ribbon strong { margin-top: 3px; } -.period-ribbon > .period-warning { display: grid; grid-template-columns: 24px minmax(0, 1fr) auto; align-items: center; gap: 10px; } -.period-warning > svg { color: var(--color-warning); } -.period-warning > span { display: flex; flex-direction: column; } -.period-warning > span strong { color: var(--color-ink); } -.admin-dashboard-grid { display: grid; grid-template-columns: minmax(0, 1.42fr) minmax(310px, 0.58fr); gap: 18px; margin-bottom: 18px; } -.task-list button { width: 100%; min-height: 73px; display: grid; grid-template-columns: 32px minmax(0, 1fr) auto 18px; align-items: center; gap: 11px; padding: 12px 16px; border: 0; border-top: 1px solid var(--color-line); background: transparent; text-align: left; cursor: pointer; transition: background var(--duration-fast); } -.task-list button:first-child { border-top: 0; } -.task-list button:hover { background: rgba(255, 255, 255, 0.035); } -.task-list button > span:nth-child(2) { display: flex; flex-direction: column; } -.task-list small, .task-list button > b { color: var(--color-ink-muted); font-size: 11px; } -.task-list button > svg { color: var(--color-ink-muted); } -.quick-pair-form { display: grid; grid-template-columns: 1fr 38px 1fr; gap: 10px; padding: 16px; } -.quick-pair-form .button { grid-column: 1 / -1; } -.quick-result { margin: 0 16px 16px; padding: 14px; border: 1px solid rgba(55, 235, 137, 0.16); border-radius: var(--radius-md); background: var(--color-primary-wash); } -.quick-result span, .quick-result p { color: var(--color-ink-muted); } -.quick-result strong { display: block; margin: 4px 0; color: var(--color-primary-strong); font: 700 22px var(--font-data); } -.quick-result small { margin-left: 4px; font: 10px var(--font-ui); } - -/* Key-company / account-coverage flat list (replaces the floating 3D card stack) */ -.company-list { display: grid; gap: 8px; padding: 10px 12px 14px; } -.company-row { position: relative; display: grid; grid-template-columns: 34px minmax(0, 1fr) auto auto; align-items: center; gap: 12px; min-height: 62px; padding: 11px 13px; border: 1px solid var(--color-line); border-radius: var(--radius-md); background: linear-gradient(160deg, rgba(22, 22, 22, 0.7) 0%, rgba(12, 12, 12, 0.75) 100%); cursor: pointer; transition: background var(--duration-fast), border-color var(--duration-fast), transform var(--duration-standard) var(--ease-out), box-shadow var(--duration-standard); } -.company-row:hover { background: linear-gradient(160deg, rgba(32, 32, 32, 0.8) 0%, rgba(18, 18, 18, 0.85) 100%); border-color: var(--color-line-strong); box-shadow: 0 12px 28px rgba(0, 0, 0, 0.25); } -.company-row:focus-visible { outline: 2px solid var(--color-primary-strong); outline-offset: 2px; } -.company-row-mark { width: 34px; height: 34px; display: grid; place-items: center; flex: 0 0 auto; border-radius: 10px; background: var(--color-primary-wash); color: var(--color-primary); font-weight: 800; font-size: 13px; } -.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 { 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); } -.company-row-figure small { color: var(--color-ink-muted); font-size: 11px; } -.company-row .status { flex: 0 0 auto; } - -/* Timeline-strip: horizontal calculation window ribbon */ -.timeline-strip { display: grid; grid-template-columns: 96px minmax(0, 1fr); gap: 14px; align-items: stretch; padding: 16px 18px; border: 1px solid var(--color-line); border-radius: var(--radius-lg); background: linear-gradient(180deg, rgba(22, 22, 22, 0.86), rgba(10, 10, 10, 0.86)); box-shadow: var(--shadow-low); backdrop-filter: blur(22px) saturate(125%); } -.timeline-strip::before { content: ""; position: absolute; } -.timeline-legend { display: flex; flex-direction: column; justify-content: center; gap: 6px; } -.timeline-legend strong { font-size: 13px; } -.timeline-legend span { color: var(--color-ink-muted); font-size: 11px; } -.timeline-body { position: relative; min-height: 88px; padding-top: 6px; } -.timeline-axis { position: relative; height: 14px; display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); border-bottom: 1px solid var(--color-line); } -.timeline-axis span { font: 600 10px var(--font-data); color: var(--color-ink-muted); align-self: end; padding-bottom: 4px; text-align: left; } -.timeline-axis span + span { border-left: 1px solid var(--color-line); padding-left: 4px; } -.timeline-rows { display: grid; gap: 8px; margin-top: 12px; } -.timeline-row { position: relative; display: grid; grid-template-columns: 96px minmax(0, 1fr); align-items: center; gap: 10px; font-size: 11px; } -.timeline-row > span { color: var(--color-ink-soft); } -.timeline-track { position: relative; height: 14px; border-radius: 7px; background: rgba(255, 255, 255, 0.04); overflow: hidden; } -.timeline-track i { position: absolute; top: 0; bottom: 0; border-radius: inherit; } -.timeline-track i.coverage { background: linear-gradient(90deg, rgba(55, 235, 137, 0.35), rgba(55, 235, 137, 0.55)); box-shadow: inset 0 0 0 1px rgba(112, 255, 174, 0.32); } -.timeline-track i.warning { background: linear-gradient(90deg, rgba(255, 188, 82, 0.32), rgba(255, 188, 82, 0.52)); box-shadow: inset 0 0 0 1px rgba(255, 188, 82, 0.32); } -.timeline-track i.danger { background: linear-gradient(90deg, rgba(255, 98, 109, 0.32), rgba(255, 98, 109, 0.52)); box-shadow: inset 0 0 0 1px rgba(255, 98, 109, 0.32); } -.timeline-today { position: absolute; top: -2px; bottom: -2px; width: 2px; background: var(--color-primary-strong); box-shadow: 0 0 12px rgba(112, 255, 174, 0.6); border-radius: 1px; } -.timeline-today::after { content: ""; position: absolute; top: -6px; left: 50%; transform: translateX(-50%); width: 10px; height: 10px; border-radius: 50%; background: var(--color-primary-strong); box-shadow: 0 0 0 4px rgba(112, 255, 174, 0.18); } -.timeline-strip-panel { position: relative; } - -/* Ledger and queries */ -.inline-search { width: min(240px, 38vw); height: 36px; display: flex; align-items: center; gap: 8px; padding: 0 11px; border: 1px solid var(--color-line); border-radius: 11px; background: rgba(0, 0, 0, 0.18); color: var(--color-ink-muted); } -.inline-search input { width: 100%; border: 0; outline: 0; background: transparent; color: var(--color-ink); } -.ledger-head, .company-ledger summary { display: grid; grid-template-columns: minmax(210px, 1.4fr) repeat(2, minmax(100px, 0.75fr)) minmax(150px, 0.9fr) 86px 24px; align-items: center; gap: 10px; } -.ledger-head { min-height: 42px; padding: 0 16px; border-bottom: 1px solid var(--color-line); color: var(--color-ink-muted); font-size: 10px; } -.company-ledger { border-top: 1px solid var(--color-line); } -.company-ledger:first-child { border-top: 0; } -.company-ledger summary { min-height: 72px; padding: 10px 16px; list-style: none; cursor: pointer; transition: background var(--duration-fast); } -.company-ledger summary::-webkit-details-marker { display: none; } -.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; 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); } -.amount.credit { color: var(--color-warning); } -.ledger-breakdown { display: grid; grid-template-columns: 1fr 1fr; border-top: 1px solid var(--color-line); background: rgba(0, 0, 0, 0.13); } -.ledger-breakdown section { padding: 14px 16px; } -.ledger-breakdown section + section { border-left: 1px solid var(--color-line); } -.ledger-breakdown header { display: flex; justify-content: space-between; margin-bottom: 8px; color: var(--color-ink-soft); } -.subject-row { width: 100%; min-height: 48px; display: grid; grid-template-columns: minmax(0, 1fr) auto 18px; align-items: center; gap: 10px; padding: 8px 10px; border: 0; border-top: 1px solid var(--color-line); background: transparent; text-align: left; cursor: pointer; } -.subject-row:hover { background: rgba(255, 255, 255, 0.035); } -.subject-row span { display: flex; flex-direction: column; } -.subject-row small { color: var(--color-ink-muted); } -.query-band, .filter-grid { padding: 17px; margin-bottom: 18px; } -.pair-query-form { display: grid; grid-template-columns: minmax(150px, 1fr) 38px minmax(150px, 1fr) minmax(150px, 0.8fr) auto; align-items: end; gap: 11px; } -.pair-report { overflow: hidden; } -.pair-report-heading { min-height: 84px; display: flex; align-items: center; justify-content: space-between; gap: 18px; padding: 16px 18px; border-bottom: 1px solid var(--color-line); } -.pair-report-heading p { color: var(--color-ink-muted); } -.pair-result { text-align: right; } -.pair-result strong { display: block; color: var(--color-primary); font: 700 22px var(--font-data); } -.pair-balance-line { display: grid; grid-template-columns: repeat(5, 1fr); border-bottom: 1px solid var(--color-line); } -.pair-balance-line div { min-height: 76px; display: flex; flex-direction: column; justify-content: center; padding: 12px 16px; border-left: 1px solid var(--color-line); } -.pair-balance-line div:first-child { border-left: 0; } -.pair-balance-line span, .pair-balance-line small { color: var(--color-ink-muted); } -.pair-balance-line strong { margin-top: 3px; font: 600 15px var(--font-data); } -.pair-final strong { color: var(--color-primary); font-size: 19px; } -.subject-strip { display: grid; grid-template-columns: repeat(5, 1fr); border-bottom: 1px solid var(--color-line); } -.subject-strip button { min-height: 62px; display: flex; flex-direction: column; justify-content: center; padding: 9px 14px; border: 0; border-left: 1px solid var(--color-line); background: rgba(255, 255, 255, 0.018); text-align: left; cursor: pointer; } -.subject-strip button:first-child { border-left: 0; } -.subject-strip button.is-active { background: var(--color-primary-wash); color: var(--color-primary); box-shadow: inset 0 -2px var(--color-primary); } -.subject-strip span { color: var(--color-ink-muted); font-size: 10px; } - -/* Filters and tables */ -.filter-bar { display: flex; align-items: center; justify-content: space-between; gap: 14px; padding: 12px; margin-bottom: 18px; } -.segmented { display: flex; gap: 4px; padding: 4px; overflow-x: auto; border: 1px solid var(--color-line); border-radius: 13px; background: rgba(0, 0, 0, 0.18); } -.segmented button { min-height: 32px; padding: 0 11px; border: 0; border-radius: 9px; background: transparent; color: var(--color-ink-muted); white-space: nowrap; cursor: pointer; } -.segmented button.is-active { background: rgba(255, 255, 255, 0.09); color: var(--color-ink); } -.flow-filters { display: grid; grid-template-columns: repeat(5, minmax(130px, 1fr)) minmax(200px, 1.4fr) auto; align-items: end; gap: 11px; } -.flow-filters .grow { min-width: 0; } -.table-summary { min-height: 51px; display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 0 16px; border-bottom: 1px solid var(--color-line); color: var(--color-ink-muted); font-size: 11px; } -.table-scroll { max-width: 100%; overflow: auto; } -.data-table { width: 100%; min-width: 860px; border-collapse: collapse; } -.data-table th { height: 42px; padding: 8px 13px; border-bottom: 1px solid var(--color-line); background: rgba(255, 255, 255, 0.025); color: var(--color-ink-muted); font-size: 10px; font-weight: 500; text-align: left; white-space: nowrap; } -.data-table td { height: 55px; padding: 9px 13px; border-bottom: 1px solid rgba(255, 255, 255, 0.055); color: var(--color-ink-soft); vertical-align: middle; } -.data-table tbody tr { transition: background var(--duration-fast); } -.data-table tbody tr:hover { background: rgba(55, 235, 137, 0.035); } -.data-table tbody tr:last-child td { border-bottom: 0; } -.data-table td strong, .data-table td small { display: block; } -.data-table td strong { color: var(--color-ink); } -.data-table td small { color: var(--color-ink-muted); } -.data-table .number { color: var(--color-ink); text-align: right; font-family: var(--font-data); font-variant-numeric: tabular-nums; } - -/* Settings, reminders and company work */ -.settings-layout { display: grid; grid-template-columns: 340px minmax(0, 1fr); gap: 18px; } -.form-body { display: grid; gap: 15px; padding: 17px; } -.panel form footer, form.panel footer { display: flex; justify-content: flex-end; gap: 9px; padding: 13px 17px; border-top: 1px solid var(--color-line); } -.closing-panel { grid-column: 1 / -1; } -.closing-checks { display: grid; grid-template-columns: repeat(4, 1fr); } -.closing-checks article { min-height: 75px; display: flex; align-items: center; gap: 10px; padding: 13px 16px; border-left: 1px solid var(--color-line); } -.closing-checks article:first-child { border-left: 0; } -.closing-checks svg { color: var(--color-positive); } -.closing-checks .is-blocked svg { color: var(--color-danger); } -.closing-checks span { display: flex; flex-direction: column; } -.closing-checks small { color: var(--color-ink-muted); } -.closing-footer { min-height: 62px; display: flex; align-items: center; justify-content: flex-end; gap: 10px; padding: 10px 16px; border-top: 1px solid var(--color-line); } -.closing-footer > span { margin-right: auto; color: var(--color-ink-muted); } -.reminder-layout { display: grid; grid-template-columns: 360px minmax(0, 1fr); gap: 18px; } -.notification-list article { min-height: 76px; display: grid; grid-template-columns: 38px minmax(0, 1fr) auto; align-items: center; gap: 12px; padding: 12px 16px; border-top: 1px solid var(--color-line); } -.notification-list article:first-child { border-top: 0; } -.notification-list article.is-unread { background: rgba(102, 168, 255, 0.045); } -.notification-list article > span:nth-child(2) { display: flex; flex-direction: column; } -.notification-list small, .notification-list p { color: var(--color-ink-muted); } -.notification-list p { margin-top: 4px; } -.notification-icon { width: 35px; height: 35px; display: grid; place-items: center; border-radius: 12px; background: rgba(255, 255, 255, 0.055); color: var(--color-ink-soft); } -.notification-icon.danger { background: var(--color-danger-wash); color: var(--color-danger); } -.notification-icon.warning { background: var(--color-warning-wash); color: var(--color-warning); } -.notification-icon.success { background: var(--color-positive-wash); color: var(--color-positive); } -.company-alert { min-height: 78px; display: grid; grid-template-columns: 32px minmax(0, 1fr) auto; align-items: center; gap: 14px; margin-bottom: 18px; padding: 14px 16px; border: 1px solid rgba(255, 98, 109, 0.22); border-radius: var(--radius-lg); background: var(--color-danger-wash); box-shadow: var(--shadow-low); } -.company-alert > svg { color: var(--color-danger); } -.company-alert p { color: #c18e92; } -.company-dashboard-grid { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(300px, 0.65fr); gap: 18px; } -.cashier-tasks article { min-height: 92px; display: grid; grid-template-columns: 32px minmax(0, 1fr) auto; align-items: center; gap: 12px; padding: 13px 16px; border-top: 1px solid var(--color-line); } -.cashier-tasks article:first-child { border-top: 0; } -.cashier-tasks p, .cashier-tasks small { color: var(--color-ink-muted); } -.coverage-list article { min-height: 62px; display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 16px; border-top: 1px solid var(--color-line); } -.coverage-list article:first-child { border-top: 0; } -.coverage-list article > span { display: flex; flex-direction: column; } -.coverage-list small { color: var(--color-ink-muted); } -.work-progress { display: grid; grid-template-columns: minmax(130px, 1fr) 40px minmax(130px, 1fr) 40px minmax(130px, 1fr) 40px minmax(130px, 1fr); align-items: center; margin-top: 18px; padding: 18px; } -.work-progress div { display: flex; align-items: center; gap: 10px; } -.work-progress div > span { width: 31px; height: 31px; display: grid; place-items: center; border-radius: 11px; background: var(--color-positive); color: var(--color-primary-dark); font-weight: 800; } -.work-progress p { display: flex; flex-direction: column; } -.work-progress small { color: var(--color-ink-muted); } -.work-progress i { height: 1px; background: var(--color-line-strong); } -.work-progress .pending > span { background: rgba(255, 255, 255, 0.065); color: var(--color-ink-muted); } -.reconcile-summary { display: grid; grid-template-columns: repeat(3, 1fr); margin-bottom: 18px; overflow: hidden; } -.reconcile-summary div { min-height: 86px; display: flex; flex-direction: column; justify-content: center; padding: 14px 18px; border-left: 1px solid var(--color-line); } -.reconcile-summary div:first-child { border-left: 0; } -.reconcile-summary span, .reconcile-summary small { color: var(--color-ink-muted); } -.reconcile-summary strong { font: 600 19px var(--font-data); } -.review-list > article { padding: 16px; border-top: 1px solid var(--color-line); } -.review-list > article:first-child { border-top: 0; } -.review-main { display: grid; grid-template-columns: 38px 1fr; gap: 10px; } -.review-main p { color: var(--color-ink-muted); } -.candidate { display: flex; align-items: end; justify-content: space-between; gap: 16px; margin: 13px 0 0 48px; padding: 12px; border: 1px solid var(--color-line); border-radius: var(--radius-md); background: rgba(0, 0, 0, 0.15); } -.candidate-options { display: grid; gap: 8px; margin: 13px 0 0 48px; padding: 12px; border: 1px solid var(--color-line); border-radius: var(--radius-md); } -.candidate-options legend { padding: 0 5px; color: var(--color-ink-muted); } -.candidate-options label { display: flex; align-items: center; gap: 9px; padding: 9px; border-radius: 11px; background: rgba(255, 255, 255, 0.035); } -.candidate-options input { accent-color: var(--color-primary); } -.candidate-options label span { display: flex; flex-direction: column; } -.candidate-options small { color: var(--color-ink-muted); } -.review-actions { display: flex; justify-content: flex-end; gap: 9px; margin-top: 10px; } -.review-history { padding: 14px 16px; border-top: 1px solid var(--color-line); background: var(--color-positive-wash); } -.evidence-block { padding: 12px; border: 1px solid var(--color-line); border-radius: var(--radius-md); background: rgba(0, 0, 0, 0.16); } -.evidence-block strong, .evidence-block small { display: block; } -.evidence-block small { color: var(--color-ink-muted); } -.account-directory { display: grid; grid-template-columns: repeat(3, minmax(240px, 1fr)); gap: 16px; } -.account-directory article { overflow: hidden; border: 1px solid var(--color-line); border-radius: var(--radius-lg); background: var(--color-surface); box-shadow: var(--shadow-low); } -.account-directory header { display: grid; grid-template-columns: 38px minmax(0, 1fr) auto; align-items: center; gap: 10px; padding: 15px; border-bottom: 1px solid var(--color-line); } -.account-directory header div { display: flex; flex-direction: column; } -.account-directory header small { color: var(--color-ink-muted); } -.bank-mark { width: 36px; height: 36px; display: grid; place-items: center; border-radius: 12px; background: var(--color-primary-wash); color: var(--color-primary); font-weight: 800; } -.account-directory dl { display: grid; grid-template-columns: repeat(3, 1fr); } -.account-directory dl div { padding: 12px; border-left: 1px solid var(--color-line); } -.account-directory dl div:first-child { border-left: 0; } -.account-directory dt { color: var(--color-ink-muted); font-size: 10px; } -.account-directory dd { margin-top: 3px; font-size: 11px; } -.manual-layout { display: grid; grid-template-columns: minmax(340px, 390px) minmax(0, 1fr); align-items: start; gap: 18px; } -.manual-records-table { min-width: 900px; } - -/* Dialogs and feedback */ -.dialog { width: min(580px, calc(100% - 28px)); padding: 0; overflow: hidden; border: 1px solid var(--color-line-strong); border-radius: var(--radius-lg); background: rgba(17, 17, 17, 0.96); color: var(--color-ink); box-shadow: var(--shadow-panel); backdrop-filter: blur(28px); } -.dialog::backdrop { background: rgba(0, 0, 0, 0.74); backdrop-filter: blur(6px); } -.dialog > form > header { min-height: 72px; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 14px 18px; border-bottom: 1px solid var(--color-line); } -.dialog header p { color: var(--color-ink-muted); } -.dialog-body { display: grid; gap: 15px; padding: 18px; } -.dialog > form > footer { display: flex; justify-content: flex-end; gap: 9px; padding: 13px 18px; border-top: 1px solid var(--color-line); } -.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } -.form-callout { display: flex; align-items: center; gap: 9px; padding: 10px; border: 1px solid rgba(102, 168, 255, 0.22); border-radius: 12px; background: var(--color-info-wash); color: #9ac6ff; } -.dropzone { min-height: 168px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px; border: 1px dashed rgba(255, 255, 255, 0.24); border-radius: var(--radius-md); background: rgba(0, 0, 0, 0.15); color: var(--color-ink-soft); text-align: center; transition: border-color var(--duration-fast), background var(--duration-fast); } -.dropzone:hover, .dropzone.is-dragging { border-color: var(--color-primary); background: var(--color-primary-wash); } -.dropzone input { position: absolute; width: 1px; height: 1px; opacity: 0; } -.dropzone > svg { width: 30px; height: 30px; color: var(--color-primary); } -.file-preview { display: grid; grid-template-columns: 40px minmax(0, 1fr) 40px; align-items: center; gap: 10px; padding: 10px; border: 1px solid var(--color-line); border-radius: var(--radius-md); } -.file-preview > span:nth-child(2) { display: flex; flex-direction: column; min-width: 0; } -.file-preview strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.file-preview small { color: var(--color-ink-muted); } -.file-type { width: 38px; height: 38px; display: grid; place-items: center; border-radius: 12px; background: var(--color-primary-wash); color: var(--color-primary); } -.parse-result { display: grid; grid-template-columns: 38px 1fr; align-items: center; gap: 10px; padding: 11px; border: 1px solid rgba(55, 235, 137, 0.22); border-radius: var(--radius-md); background: var(--color-positive-wash); } -.parse-result p { color: #9bd7b6; } -.parse-result.is-exception { border-color: rgba(255, 188, 82, 0.25); background: var(--color-warning-wash); } -.parse-result.is-exception p { color: #e7bd78; } -.sheet-review { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--color-line); } -.sheet-review h3 { font-size: 14px; } -.sheet-review-hint { margin-top: 4px; color: var(--color-ink-muted); font-size: 12px; line-height: 1.5; } -.sheet-list { display: grid; gap: 8px; margin-top: 10px; max-height: 220px; overflow-y: auto; } -.sheet-item { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; padding: 10px; border: 1px solid var(--color-line); border-radius: var(--radius-md); background: var(--color-surface-muted); } -.sheet-item.is-pending { border-color: rgba(255, 188, 82, 0.45); } -.sheet-item-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } -.sheet-item-head strong { font-size: 13px; } -.sheet-item-head small { color: var(--color-ink-muted); } -.sheet-item-meta { margin-top: 5px; color: var(--color-ink-muted); font-size: 12px; line-height: 1.5; } -.sheet-item-actions { display: flex; align-items: center; gap: 6px; } -.sheet-item-actions .text-button { font-size: 12px; padding: 4px 8px; } -.sheet-item-reason { margin-top: 6px; padding: 6px 8px; border: 1px solid var(--color-line); border-radius: var(--radius-sm); color: var(--color-ink-muted); font-size: 12px; } -.toast-region { position: fixed; right: 20px; bottom: 20px; z-index: 200; display: grid; gap: 8px; } -.toast { min-width: 280px; max-width: 390px; padding: 13px 15px; border: 1px solid var(--color-line-strong); border-radius: var(--radius-md); background: rgba(20, 20, 20, 0.96); color: var(--color-ink); box-shadow: var(--shadow-panel); backdrop-filter: blur(20px); } -.toast strong, .toast small { display: block; } -.toast small { color: var(--color-ink-muted); } - -/* Login */ -.entry-page { overflow-x: hidden; } -.entry-shell { min-height: 100vh; display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(430px, 0.85fr); } -.entry-context { position: relative; min-height: 100vh; display: flex; flex-direction: column; justify-content: space-between; padding: clamp(32px, 5vw, 76px); border-right: 1px solid var(--color-line); background: #080808; } -.entry-context::after { content: ""; position: absolute; inset: 12% 8% 12% auto; width: 1px; background: rgba(55, 235, 137, 0.32); box-shadow: 0 0 38px rgba(55, 235, 137, 0.45); } -.entry-brand { display: flex; align-items: center; gap: 12px; } -.entry-brand > span:last-child { display: flex; flex-direction: column; } -.entry-brand small { color: var(--color-ink-muted); } -.entry-statement { max-width: 680px; margin: 80px 0; } -.entry-statement h1 { max-width: 640px; font-size: clamp(38px, 5vw, 70px); line-height: 1.12; letter-spacing: 0; } -.entry-statement p { margin-top: 20px; color: var(--color-ink-muted); } -.entry-facts { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; max-width: 670px; } -.entry-facts div { padding: 15px; border: 1px solid var(--color-line); border-radius: var(--radius-md); background: var(--color-surface-muted); } -.entry-facts dt { color: var(--color-ink-muted); } -.entry-facts dd { margin-top: 4px; color: var(--color-primary); } -.entry-form-wrap { min-height: 100vh; display: grid; place-items: center; padding: 40px; background: var(--color-bg); } -.entry-form { width: min(420px, 100%); display: grid; gap: 18px; padding: 28px; border: 1px solid var(--color-line); border-radius: var(--radius-xl); background: var(--color-surface); box-shadow: var(--shadow-panel); backdrop-filter: blur(24px); } -.entry-form header h2 { font-size: 25px; } -.entry-form header p, .entry-note { color: var(--color-ink-muted); } -.role-switch { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; } -.role-switch label { cursor: pointer; } -.role-switch input { position: absolute; opacity: 0; } -.role-switch span { min-height: 83px; display: grid; grid-template-columns: 30px 1fr; align-content: center; gap: 1px 9px; padding: 12px; border: 1px solid var(--color-line); border-radius: var(--radius-md); background: rgba(0, 0, 0, 0.14); transition: border-color var(--duration-fast), background var(--duration-fast), transform var(--duration-fast); } -.role-switch svg { grid-row: 1 / 3; align-self: center; color: var(--color-ink-muted); } -.role-switch small { color: var(--color-ink-muted); } -.role-switch input:checked + span { border-color: rgba(55, 235, 137, 0.4); background: var(--color-primary-wash); transform: translateY(-1px); } -.role-switch input:checked + span svg { color: var(--color-primary); } -.entry-note { font-size: 10px; } - -@media (max-width: 1180px) { - .app-shell { grid-template-columns: 88px minmax(0, 1fr); } - .sidebar { width: 70px; padding-inline: 9px; } - .workspace { grid-column: 2; } - .brand { justify-content: center; padding-inline: 0; } - .brand-copy, .nav-item span, .nav-item b, .user-block > span:last-child, .company-context div { display: none; } - .nav-item { justify-content: center; padding: 0; } - .user-block, .company-context { justify-content: center; padding-inline: 0; } - .metric-grid { grid-template-columns: repeat(2, 1fr); } - .period-ribbon { grid-template-columns: repeat(3, 1fr); } - .period-ribbon > .period-warning { grid-column: 1 / -1; border-top: 1px solid var(--color-line); border-left: 0; } - .admin-dashboard-grid, .company-dashboard-grid { grid-template-columns: 1fr; } - .flow-filters { grid-template-columns: repeat(3, 1fr); } - .flow-filters .button { min-height: 40px; } - .manual-layout { grid-template-columns: 1fr; } - .account-directory { grid-template-columns: repeat(2, 1fr); } -} - -@media (max-width: 900px) { - main { padding-inline: 20px; } - .search-box { display: none; } - .settings-layout, .reminder-layout { grid-template-columns: 1fr; } - .pair-query-form { grid-template-columns: 1fr 40px 1fr; } - .pair-query-form .field:nth-of-type(3) { grid-column: 1 / 3; } - .pair-query-form .button { grid-column: 3; } - .ledger-head { display: none; } - .company-ledger:not(.is-balances) summary { grid-template-columns: minmax(180px, 1.4fr) repeat(2, minmax(100px, 0.7fr)) 28px; } - .company-ledger:not(.is-balances) summary > span:nth-child(4), .company-ledger:not(.is-balances) summary > span:nth-child(5) { display: none; } - .ledger-breakdown { grid-template-columns: 1fr; } - .ledger-breakdown section + section { border-top: 1px solid var(--color-line); border-left: 0; } - .pair-balance-line { grid-template-columns: repeat(3, 1fr); } - .pair-balance-line .pair-final { grid-column: 1 / -1; border-top: 1px solid var(--color-line); border-left: 0; } - .subject-strip { grid-template-columns: repeat(3, 1fr); } - .work-progress { grid-template-columns: 1fr 24px 1fr; row-gap: 16px; } - .work-progress i:nth-of-type(2) { display: none; } - .entry-shell { grid-template-columns: 1fr; } - .entry-context { min-height: 58vh; border-right: 0; border-bottom: 1px solid var(--color-line); } - .entry-form-wrap { min-height: auto; } -} - -@media (max-width: 720px) { - .app-shell { display: block; } - .sidebar { inset: 10px auto 10px 10px; width: 242px; transform: translateX(calc(-100% - 20px)); transition: transform var(--duration-standard) var(--ease-out); } - .sidebar.is-open { transform: none; } - .sidebar.is-open .brand-copy, .sidebar.is-open .nav-item span, .sidebar.is-open .nav-item b, .sidebar.is-open .user-block > span:last-child, .sidebar.is-open .company-context div { display: flex; } - .sidebar.is-open .nav-item { justify-content: flex-start; padding: 0 12px; } - .sidebar.is-open .company-context { justify-content: flex-start; padding-inline: 10px; } - .workspace { display: block; } - .topbar { height: 64px; padding: 0 13px; gap: 9px; } - .menu-button { display: inline-grid; } - .workspace-name { min-width: 0; } - .workspace-name span { display: none; } - .demo-badge { padding: 2px 6px; } - .period-button { display: none; } - .topbar-actions .button { width: 40px; padding: 0; font-size: 0; } - main { padding: 12px 13px 42px; } - .page-heading { min-height: auto; flex-direction: column; margin-bottom: 18px; } - .page-heading h1 { font-size: 27px; } - .page-heading > .button { align-self: stretch; } - .metric-grid { grid-template-columns: 1fr 1fr; gap: 10px; } - .metric-card { min-height: 128px; grid-template-columns: minmax(0, 1fr) 38px; padding: 15px; } - .metric-icon { width: 38px; height: 38px; border-radius: 12px; } - .metric-value { font-size: 25px; } - .period-ribbon { grid-template-columns: 1fr 1fr; } - .period-ribbon > div { min-height: 64px; } - .period-ribbon > div:nth-child(3) { border-top: 1px solid var(--color-line); border-left: 0; } - .period-warning { grid-column: 1 / -1; } - .panel-heading { min-height: 60px; padding: 11px 13px; } - .inline-search { display: none; } - .task-list button { grid-template-columns: 30px minmax(0, 1fr) 18px; padding-inline: 12px; } - .task-list button > b { display: none; } - .quick-pair-form { grid-template-columns: 1fr 38px 1fr; padding-inline: 12px; } - .quick-result { margin-inline: 12px; } - .company-ledger:not(.is-balances) summary { grid-template-columns: minmax(145px, 1fr) 100px 24px; padding-inline: 12px; } - .company-ledger:not(.is-balances) summary > strong:nth-of-type(2), .company-ledger:not(.is-balances) summary > span:nth-child(4), .company-ledger:not(.is-balances) summary > span:nth-child(5) { display: none; } - .company-name { grid-template-columns: 30px minmax(0, 1fr); } - .company-name i { width: 28px; height: 28px; } - .filter-bar { align-items: stretch; flex-direction: column; } - .segmented { width: 100%; } - .flow-filters { grid-template-columns: 1fr 1fr; } - .flow-filters .grow, .flow-filters .button { grid-column: 1 / -1; } - .pair-query-form { grid-template-columns: 1fr 38px 1fr; } - .pair-query-form .field:nth-of-type(3), .pair-query-form .button { grid-column: 1 / -1; } - .pair-report-heading { align-items: flex-start; flex-direction: column; } - .pair-balance-line { grid-template-columns: 1fr 1fr; } - .pair-balance-line .pair-final { grid-column: auto; border-left: 1px solid var(--color-line); } - .subject-strip { grid-template-columns: 1fr 1fr; } - .company-alert { grid-template-columns: 28px 1fr; } - .company-alert .button { grid-column: 1 / -1; } - .cashier-tasks article { grid-template-columns: 30px minmax(0, 1fr); } - .cashier-tasks .button { grid-column: 2; justify-self: start; } - .work-progress { grid-template-columns: 1fr; } - .work-progress i { display: none; } - .reconcile-summary { grid-template-columns: 1fr; } - .reconcile-summary div { min-height: 70px; border-top: 1px solid var(--color-line); border-left: 0; } - .reconcile-summary div:first-child { border-top: 0; } - .candidate, .candidate-options { align-items: stretch; flex-direction: column; margin-left: 0; } - .account-directory { grid-template-columns: 1fr; } - .account-directory dl { grid-template-columns: 1fr 1fr; } - .closing-checks { grid-template-columns: 1fr; } - .closing-checks article { border-top: 1px solid var(--color-line); border-left: 0; } - .closing-footer { align-items: stretch; flex-direction: column; } - .notification-list article { grid-template-columns: 34px minmax(0, 1fr); } - .notification-list article > em { grid-column: 2; justify-self: start; } - .table-summary { align-items: flex-start; flex-direction: column; justify-content: center; padding-block: 9px; } - .dialog { width: 100%; max-width: none; height: 100%; max-height: none; border-radius: 0; } - .dialog > form { min-height: 100%; display: flex; flex-direction: column; } - .dialog-body { flex: 1; } - .form-grid { grid-template-columns: 1fr; } - .toast-region { right: 12px; bottom: 12px; left: 12px; } - .toast { min-width: 0; max-width: none; } - .entry-context { min-height: auto; padding: 30px 22px; } - .entry-statement { margin: 54px 0 42px; } - .entry-statement h1 { font-size: 39px; } - .entry-facts { grid-template-columns: 1fr; } - .entry-form-wrap { padding: 32px 16px 48px; } - .entry-form { padding: 22px; } -} - -@media (max-width: 460px) { - .metric-card { min-height: 150px; padding: 13px; } - .metric-copy { min-height: 52px; } - .metric-label { font-size: 11px; } - .metric-value { font-size: 23px; } - .role-switch { grid-template-columns: 1fr; } -} - -@media (max-width: 1180px) { - .timeline-strip { grid-template-columns: 1fr; } - .timeline-legend { padding-bottom: 4px; } -} - -@media (max-width: 900px) { - .company-row:not(.is-balance-counterparty) { grid-template-columns: 34px minmax(0, 1fr) auto; } - .company-row:not(.is-balance-counterparty) .company-row-figure { display: none; } -} - -@media (max-width: 720px) { - .timeline-strip { padding: 14px; } - .timeline-rows { gap: 6px; } - .timeline-row { grid-template-columns: 80px minmax(0, 1fr); } - .timeline-axis span { font-size: 9px; } -} - -/* ===== 参考图对齐:胶囊搜索条 / 账期时间轴 / 右侧详情面板 ===== */ -.content-toolbar > .search-box { border-radius: 999px; background: rgba(255, 255, 255, 0.045); backdrop-filter: blur(18px) saturate(120%); } - -.timeline-panel { margin-top: 18px; padding: 18px; } -.timeline { padding: 10px 4px 0; } -.timeline-bar { position: relative; height: 14px; border: 1px solid var(--color-line); border-radius: 999px; background: rgba(255, 255, 255, 0.05); } -.timeline-seg, .timeline-gap { position: absolute; top: 2px; bottom: 2px; border-radius: 999px; } -.timeline-seg.is-closed { background: linear-gradient(90deg, rgba(55, 235, 137, 0.5), rgba(55, 235, 137, 0.28)); } -.timeline-seg.is-current { background: linear-gradient(90deg, rgba(255, 188, 82, 0.4), rgba(255, 188, 82, 0.18)); } -.timeline-gap { background: repeating-linear-gradient(45deg, rgba(255, 98, 109, 0.75) 0 6px, rgba(255, 98, 109, 0.35) 6px 12px); box-shadow: 0 0 12px rgba(255, 98, 109, 0.25); } -.timeline-today { position: absolute; top: -4px; bottom: -4px; width: 2px; background: var(--color-primary-strong); box-shadow: 0 0 10px rgba(55, 235, 137, 0.6); } -.timeline-today::after { content: "今天"; position: absolute; top: -19px; right: 5px; font-size: 10px; font-style: normal; color: var(--color-primary-strong); white-space: nowrap; } -.timeline-ticks { position: relative; height: 34px; margin-top: 8px; } -.timeline-ticks span { position: absolute; display: flex; flex-direction: column; transform: translateX(-50%); font-size: 10px; color: var(--color-ink-muted); text-align: center; } -.timeline-ticks span:first-child { transform: none; text-align: left; } -.timeline-ticks span:last-child { transform: translateX(-100%); text-align: right; } -.timeline-ticks small { color: var(--color-ink-soft); } -.timeline-panel .timeline-legend { display: flex; flex-direction: row; flex-wrap: wrap; gap: 6px 18px; margin: 10px 0 0; padding: 0; list-style: none; font-size: 11px; color: var(--color-ink-soft); } -.timeline-panel .timeline-legend li { display: flex; align-items: center; gap: 6px; } -.timeline-panel .dot { width: 8px; height: 8px; border-radius: 50%; } -.dot.success { background: var(--color-positive); } -.dot.warning { background: var(--color-warning); } -.dot.danger { background: var(--color-danger); } -.dot.today { background: var(--color-primary-strong); box-shadow: 0 0 6px rgba(55, 235, 137, 0.7); } - -.detail-drawer { position: fixed; top: 16px; right: 16px; bottom: 16px; z-index: 80; width: min(380px, calc(100vw - 32px)); display: flex; flex-direction: column; border: 1px solid var(--color-line-strong); border-radius: var(--radius-lg); background: linear-gradient(160deg, rgba(28, 28, 28, 0.92), rgba(12, 12, 12, 0.94)); box-shadow: var(--shadow-panel); backdrop-filter: blur(26px) saturate(125%); transform: translateX(calc(100% + 32px)); visibility: hidden; transition: transform var(--duration-standard) var(--ease-out), visibility var(--duration-standard); } -.detail-drawer.is-open { transform: none; visibility: visible; } -.detail-drawer > header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 18px 18px 12px; border-bottom: 1px solid var(--color-line); } -.detail-drawer h2 { margin-top: 8px; font-size: 17px; } -.detail-drawer .detail-desc { margin-top: 4px; color: var(--color-ink-muted); font-size: 12px; } -.detail-fields { display: grid; gap: 10px; margin: 0; padding: 14px 18px; overflow-y: auto; } -.detail-fields > div { display: flex; justify-content: space-between; gap: 14px; padding-bottom: 10px; border-bottom: 1px dashed var(--color-line); } -.detail-fields dt { flex-shrink: 0; color: var(--color-ink-muted); font-size: 12px; } -.detail-fields dd { margin: 0; text-align: right; font-size: 12px; } -.detail-tip { margin: 0 18px 12px; padding: 12px; border: 1px solid var(--color-line); border-radius: var(--radius-md); background: var(--color-surface-muted); color: var(--color-ink-soft); font-size: 12px; } -.detail-tip strong { display: block; margin-bottom: 4px; color: var(--color-ink); } -.detail-drawer > footer { display: flex; gap: 10px; margin-top: auto; padding: 14px 18px; border-top: 1px solid var(--color-line); } -.detail-drawer > footer .button { flex: 1; justify-content: center; } -.detail-drawer .status { align-self: flex-start; } -[data-detail] { cursor: pointer; } - -@media (max-width: 720px) { - .detail-drawer { top: 8px; right: 8px; bottom: 8px; width: calc(100vw - 16px); } - .timeline-panel { padding: 14px; } -} - -/* ===== 质感打磨:玻璃光泽 / 氛围光 / 对比与呼吸感 ===== */ -:root { - --radius-lg: 24px; - --radius-xl: 30px; -} - -body::before { content: ""; position: fixed; inset: 0; z-index: 0; pointer-events: none; background: radial-gradient(52vw 42vh at 62% 32%, rgba(55, 235, 137, 0.055), transparent 62%), radial-gradient(40vw 34vh at 90% 4%, rgba(102, 168, 255, 0.032), transparent 60%), radial-gradient(46vw 40vh at 28% 96%, rgba(55, 235, 137, 0.028), transparent 60%); } -.app-shell { position: relative; z-index: 0; } - -.metric-card { border-color: rgba(255, 255, 255, 0.12); background: radial-gradient(120% 90% at 50% 108%, rgba(255, 255, 255, 0.09), rgba(255, 255, 255, 0.02) 42%, transparent 60%), linear-gradient(160deg, rgba(41, 41, 41, 0.86) 0%, rgba(23, 23, 23, 0.88) 50%, rgba(12, 12, 12, 0.9) 100%); } -.metric-card::before { background: radial-gradient(130% 100% at 50% 112%, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.03) 45%, transparent 62%), linear-gradient(165deg, rgba(255, 255, 255, 0.14) 0%, rgba(255, 255, 255, 0.05) 42%, transparent 72%); } -.panel::before, .period-ribbon::before, .reconcile-summary::before, .work-progress::before { background: radial-gradient(150% 110% at 50% 118%, rgba(255, 255, 255, 0.05), transparent 55%), linear-gradient(165deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.03) 40%, transparent 70%); } - -.nav-item.is-active { border-color: rgba(255, 255, 255, 0.16); background: linear-gradient(160deg, rgba(255, 255, 255, 0.14), rgba(255, 255, 255, 0.06)); box-shadow: inset 0 1px rgba(255, 255, 255, 0.14), 0 10px 26px rgba(0, 0, 0, 0.32), 0 0 18px rgba(55, 235, 137, 0.08); } -.metric-value { font-size: clamp(28px, 2.6vw, 38px); font-weight: 800; color: #ffffff; white-space: nowrap; } - -.metric-grid { gap: 20px; margin-bottom: 20px; } -.admin-dashboard-grid { gap: 20px; margin-bottom: 20px; } -.company-dashboard-grid { gap: 20px; } -.page-heading { margin-bottom: 22px; } - -@media (max-width: 720px) { - .metric-grid { gap: 10px; margin-bottom: 14px; } - .page-heading { margin-bottom: 18px; } -} - -/* ===== 反馈修正:侧栏质感 + 模块间距统一 ===== */ -.sidebar { border-color: rgba(255, 255, 255, 0.13); background: linear-gradient(170deg, rgba(26, 26, 26, 0.92), rgba(10, 10, 10, 0.94)); box-shadow: var(--shadow-panel), 0 0 44px rgba(55, 235, 137, 0.05); } -.sidebar::before { content: ""; position: absolute; inset: 0; border-radius: inherit; background: radial-gradient(150% 110% at 50% 118%, rgba(255, 255, 255, 0.06), transparent 55%), linear-gradient(170deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.02) 45%, transparent 70%); pointer-events: none; } -.nav-list { gap: 10px; } -.nav-item { min-height: 48px; border-radius: 16px; } -.user-block { border: 1px solid var(--color-line); border-radius: 16px; background: var(--color-surface-muted); } - -.company-alert { margin-bottom: 20px; } -.period-ribbon { margin-bottom: 20px; } -.company-stack-panel { margin-bottom: 20px; } -.work-progress { margin-top: 20px; } -.timeline-panel { margin-top: 20px; } - -/* ===== 反馈修正:整体提亮 + 列表改为玻璃行(去掉线条隔断) ===== */ -:root { - --color-ink-soft: #b6c0ba; - --color-ink-muted: #99a39d; -} - -.panel, .period-ribbon, .query-band, .filter-bar, .filter-grid, .pair-report, .work-progress, .reconcile-summary { border-color: rgba(255, 255, 255, 0.12); background: linear-gradient(160deg, rgba(37, 37, 37, 0.84) 0%, rgba(21, 21, 21, 0.87) 50%, rgba(12, 12, 12, 0.89) 100%); } -.panel::before, .period-ribbon::before, .reconcile-summary::before, .work-progress::before { background: radial-gradient(150% 110% at 50% 118%, rgba(255, 255, 255, 0.06), transparent 55%), linear-gradient(165deg, rgba(255, 255, 255, 0.13) 0%, rgba(255, 255, 255, 0.04) 40%, transparent 70%); } - -.task-list, .cashier-tasks, .coverage-list, .notification-list, .review-list { display: grid; gap: 8px; padding: 12px 14px 14px; } -.task-list button, .cashier-tasks article, .coverage-list article, .notification-list article, .review-list > article, .subject-row { border: 1px solid var(--color-line); border-radius: var(--radius-md); background: linear-gradient(160deg, rgba(32, 32, 32, 0.72) 0%, rgba(15, 15, 15, 0.78) 100%); transition: background var(--duration-fast), border-color var(--duration-fast), box-shadow var(--duration-fast), transform var(--duration-fast); } -.task-list button:hover, .cashier-tasks article:hover, .coverage-list article:hover, .notification-list article:hover, .review-list > article:hover, .subject-row:hover { border-color: rgba(255, 255, 255, 0.2); background: linear-gradient(160deg, rgba(44, 44, 44, 0.82) 0%, rgba(22, 22, 22, 0.86) 100%); box-shadow: 0 10px 26px rgba(0, 0, 0, 0.28); } -.notification-list article.is-unread { border-color: rgba(102, 168, 255, 0.3); background: linear-gradient(160deg, rgba(102, 168, 255, 0.1), rgba(102, 168, 255, 0.04)); } -.company-ledger { margin: 8px 14px; border: 1px solid var(--color-line); border-radius: var(--radius-md); background: linear-gradient(160deg, rgba(32, 32, 32, 0.72) 0%, rgba(15, 15, 15, 0.78) 100%); overflow: hidden; } -.company-ledger[open] { border-color: rgba(255, 255, 255, 0.18); } -.company-ledger:last-child { margin-bottom: 14px; } - -.data-table { border-collapse: separate; border-spacing: 0 8px; } -.data-table th { height: 28px; padding: 0 13px; border-bottom: 0; background: transparent; } -.data-table td { border-bottom: 0; border-top: 1px solid var(--color-line); border-bottom: 1px solid var(--color-line); background: linear-gradient(160deg, rgba(32, 32, 32, 0.72) 0%, rgba(15, 15, 15, 0.78) 100%); } -.data-table td:first-child { border-left: 1px solid var(--color-line); border-radius: 12px 0 0 12px; } -.data-table td:last-child { border-right: 1px solid var(--color-line); border-radius: 0 12px 12px 0; } -.data-table tbody tr:hover td { background: linear-gradient(160deg, rgba(44, 44, 44, 0.85) 0%, rgba(22, 22, 22, 0.88) 100%); } -.data-table tbody tr:last-child td { border-bottom: 1px solid var(--color-line); } -.table-scroll { padding: 2px 14px 8px; } -.table-summary { margin: 0 14px; } - -/* ===== 登录页重做:居中集团登录卡 ===== */ -.entry-shell { display: grid; grid-template-columns: 1fr; place-items: center; min-height: 100vh; padding: 48px 20px; } -.entry-card { position: relative; width: min(470px, 100%); overflow: hidden; border: 1px solid rgba(255, 255, 255, 0.13); border-radius: var(--radius-xl); background: linear-gradient(165deg, rgba(36, 36, 36, 0.9) 0%, rgba(16, 16, 16, 0.92) 60%, rgba(10, 10, 10, 0.94) 100%); box-shadow: var(--shadow-panel), 0 0 60px rgba(55, 235, 137, 0.06); backdrop-filter: blur(28px) saturate(125%); } -.entry-card::before { content: ""; position: absolute; inset: 0; background: radial-gradient(150% 110% at 50% 118%, rgba(255, 255, 255, 0.07), transparent 55%), linear-gradient(165deg, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0.03) 42%, transparent 70%); pointer-events: none; } -.entry-card::after { content: ""; position: absolute; top: 0; left: 18%; right: 18%; height: 2px; background: linear-gradient(90deg, transparent, rgba(55, 235, 137, 0.65), transparent); } -.entry-brand { flex-direction: column; justify-content: center; gap: 15px; min-height: 0; padding: 36px 28px 24px; border-bottom: 1px solid var(--color-line); text-align: center; } -.entry-brand .brand-mark { width: 54px; height: 54px; border-radius: 17px; font-size: 26px; } -.entry-brand > span:last-child { align-items: center; } -.entry-brand strong { font-size: 21px; letter-spacing: 0.03em; } -.entry-brand small { margin-top: 4px; font-size: 12px; letter-spacing: 0.08em; } -.entry-form { width: 100%; gap: 16px; padding: 24px 28px 10px; border: 0; background: transparent; box-shadow: none; backdrop-filter: none; } -.entry-form header { text-align: center; } -.entry-form header h2 { font-size: 22px; } -.entry-form header p { margin-top: 4px; color: var(--color-ink-muted); font-size: 12px; } -.entry-facts { max-width: none; gap: 10px; padding: 16px 28px 28px; } -.entry-facts div { padding: 12px; text-align: center; } -.entry-facts dd { margin-top: 2px; font-size: 12px; } - -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } -} - -/* ---- B-44 intercompany balances ---- */ -.status.info { border-color: rgba(102, 168, 255, 0.18); background: var(--color-info-wash); color: var(--color-info); } -.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; } -.candidate-list li { padding: 4px 0; border-bottom: 1px dashed var(--color-line); } -.candidate-list li:last-child { border-bottom: 0; } -.reason-full { max-height: calc(1.6em * 6); overflow: auto; white-space: pre-wrap; } -.decision-danger { margin-top: 8px; color: var(--color-danger); font-size: 12px; } -.balance-currency-groups { display: grid; gap: 12px; } -.balance-currency-groups .pair-balance-line { border: 1px solid var(--color-line); border-radius: var(--radius-md); overflow: hidden; } - -/* Balance directory: 公司 | 借方 | 贷方 | 期末结果 | 未决 | 截止日 | ▸ */ -.ledger-head.is-balances, .company-ledger.is-balances summary { - 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; 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); } -.company-ledger.is-balances .currency-tag { color: var(--color-ink-muted); font-size: 10px; } -.company-ledger.is-balances .subject-row small { white-space: nowrap; } -@media (max-width: 1179px) { - .ledger-head.is-balances, .company-ledger.is-balances summary { - grid-template-columns: minmax(120px, 1.2fr) minmax(136px, 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 (min-width: 376px) and (max-width: 900px) { - .ledger-head.is-balances { display: grid; } - .ledger-head.is-balances, .company-ledger.is-balances summary { - grid-template-columns: minmax(80px, 0.85fr) minmax(152px, 1.2fr) minmax(96px, 0.9fr) minmax(84px, 0.5fr) 24px; - } - .company-ledger.is-balances summary .ledger-result { - flex-direction: column; - align-items: flex-start; - justify-content: center; - gap: 2px; - } - .company-ledger.is-balances .ledger-result b { font-size: 11px; } - .company-ledger.is-balances .ledger-unresolved { - min-width: 0; - white-space: normal; - } - .company-ledger.is-balances .ledger-unresolved .status { - width: auto; - max-width: 100%; - height: auto; - white-space: normal; - justify-content: flex-start; - text-align: left; - } -} -@media (max-width: 375px) { - .ledger-head.is-balances { display: none; } - .company-ledger.is-balances summary { - 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; 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; } -} - -/* Pair report six-cell: page may be six-across; drawer is always two rows of three */ -.pair-balance-line.is-six { grid-template-columns: repeat(6, 1fr); } -.pair-balance-line.is-six .pair-final { grid-column: auto; border-left: 1px solid var(--color-line); } -.pair-balance-line .pair-open-note { color: var(--color-ink-muted); font-size: 10px; margin-top: 3px; } -.pair-balance-line .pair-unresolved b { color: var(--color-warning); } -.pair-balance-line .pair-unresolved.is-empty b { color: var(--color-ink); } -.pair-balance-line .pair-cutoff { color: var(--color-ink-muted); font-size: 10px; margin-top: 3px; } -@media (max-width: 1180px) { - .pair-balance-line.is-six { grid-template-columns: repeat(3, 1fr); } - .pair-balance-line.is-six div:nth-child(4) { border-top: 1px solid var(--color-line); border-left: 0; } - .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 */ -.drawer-scrim { position: fixed; inset: 0; z-index: calc(var(--z-drawer) - 1); background: rgba(5, 5, 5, 0.6); opacity: 0; pointer-events: none; transition: opacity var(--duration-standard) var(--ease-out); } -.drawer-scrim.is-open { opacity: 1; pointer-events: auto; } -.drawer { position: fixed; top: 0; right: 0; bottom: 0; z-index: var(--z-drawer); width: 640px; max-width: 100%; display: flex; flex-direction: column; background: rgba(17, 17, 17, 0.96); border-left: 1px solid var(--color-line-strong); box-shadow: var(--shadow-panel); transform: translateX(24px); opacity: 0; visibility: hidden; transition: transform var(--duration-standard) var(--ease-out), opacity var(--duration-standard) var(--ease-out), visibility 0s linear var(--duration-standard); } -.drawer.is-open { transform: translateX(0); opacity: 1; visibility: visible; transition-delay: 0s; } -@media (max-width: 1179px) { .drawer { width: 480px; } } -@media (max-width: 767px) { .drawer { width: 100%; } } -.drawer-header { min-height: 74px; display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--color-line); } -.drawer-header .drawer-breadcrumb { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; color: var(--color-ink-muted); font-size: 11px; } -.drawer-header .drawer-breadcrumb button { border: 0; padding: 0; background: none; color: var(--color-info); cursor: pointer; font-size: 11px; } -.drawer-header .drawer-breadcrumb button:hover { text-decoration: underline; } -.drawer-header h2 { margin-top: 6px; font-size: 17px; } -.drawer-body { flex: 1; overflow: auto; padding: 16px 18px 24px; contain: layout paint; } -.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 { - grid-template-columns: repeat(3, 1fr); -} -.drawer .pair-balance-line.is-six .pair-final { grid-column: auto; } -.drawer .pair-balance-line.is-six div:nth-child(4) { border-top: 1px solid var(--color-line); border-left: 0; } -.drawer .pair-balance-line.is-six div:nth-child(5), -.drawer .pair-balance-line.is-six div:nth-child(6) { border-top: 1px solid var(--color-line); } -.drawer .event-table { min-width: 860px; } -.drawer .event-table td { height: var(--row-h-evidence); } -.drawer .event-row { cursor: pointer; } -.drawer .event-row:focus-visible { outline: 2px solid var(--color-primary-strong); outline-offset: -2px; } -.drawer .evidence-stack { display: flex; flex-direction: column; gap: 10px; } -.drawer .evidence-midline { display: flex; align-items: center; gap: 10px; color: var(--color-ink-muted); font-size: 11px; } -.drawer .evidence-midline::before, .drawer .evidence-midline::after { content: ""; flex: 1; height: 1px; background: var(--color-line); } -.evidence-block.is-missing { border-color: rgba(102, 168, 255, 0.28); background: var(--color-info-wash); } -.evidence-block .evidence-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px 14px; margin-top: 10px; } -.evidence-block .evidence-grid div { display: flex; flex-direction: column; min-width: 0; } -.evidence-block .evidence-grid dt { color: var(--color-ink-muted); font-size: 10px; } -.evidence-block .evidence-grid dd { color: var(--color-ink-soft); font-size: 12px; word-break: break-all; } -.evidence-block .evidence-masked { color: var(--color-ink-muted); font-style: normal; } -@media (max-width: 720px) { .evidence-block .evidence-grid { grid-template-columns: 1fr; } } - -.company-row.is-balance-counterparty { grid-template-columns: 34px minmax(0, 1fr) minmax(0, auto); } -.company-row.is-balance-counterparty .company-row-figure { - display: flex; flex-direction: column; align-items: flex-end; gap: 4px; min-width: 0; -} -.company-row.is-balance-counterparty .company-row-figure strong { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 6px; } -@media (max-width: 900px) { - .company-row.is-balance-counterparty { - grid-template-columns: 34px minmax(0, 1fr); - row-gap: 8px; - } - .company-row.is-balance-counterparty .company-row-figure { - grid-column: 2; - align-items: flex-start; - text-align: left; - flex-wrap: wrap; - flex-direction: row; - column-gap: 10px; - row-gap: 4px; - } -} - -/* Loading skeletons */ -.skeleton-row { height: var(--row-h-evidence); display: flex; align-items: center; gap: 12px; padding: 0 14px; border-bottom: 1px solid var(--color-line); } -.skeleton-row span { height: 12px; border-radius: 6px; background: rgba(255, 255, 255, 0.06); } -.skeleton-row span:nth-child(1) { width: 22%; } .skeleton-row span:nth-child(2) { width: 14%; } -.skeleton-row span:nth-child(3) { width: 14%; } .skeleton-row span:nth-child(4) { width: 18%; } -.skeleton-row span:nth-child(5) { width: 12%; } -.skeleton-block { animation: skeleton-breathe 1200ms ease-in-out infinite; } - -/* Empty / error / no-permission states */ -.state-panel { display: flex; flex-direction: column; align-items: center; gap: 10px; justify-content: center; min-height: 240px; padding: 28px 18px; text-align: center; color: var(--color-ink-muted); } -.state-panel .state-icon { width: 40px; height: 40px; color: var(--color-ink-muted); opacity: 0.7; } -.state-panel strong { color: var(--color-ink-soft); font-size: 14px; } -.state-panel p { max-width: 420px; font-size: 12px; } -.state-panel .button { margin-top: 6px; } -.state-panel.is-error { border: 1px solid rgba(255, 98, 109, 0.3); border-radius: var(--radius-lg); background: var(--color-danger-wash); } -.state-panel.is-error strong { color: var(--color-danger); } -.state-panel.is-denied { border: 1px solid var(--color-line); border-radius: var(--radius-lg); background: var(--color-surface); } - -/* Company portal alert variant + direction chips */ -.balance-alert { border-color: rgba(255, 188, 82, 0.24); background: var(--color-warning-wash); } -.balance-alert > svg { color: var(--color-warning); } -.balance-alert .button { color: var(--color-warning); } -.company-row .result-direction { font-weight: 600; font-variant-numeric: tabular-nums; } -.subject-strip button { white-space: nowrap; } - -/* Direction chips: text-first, arrows as affordance, never color-only */ -.direction-chip { display: inline-flex; align-items: center; gap: 4px; min-height: 23px; padding: 2px 8px; border-radius: 8px; border: 1px solid var(--color-line); background: rgba(255, 255, 255, 0.045); color: var(--color-ink-soft); font-size: 10px; white-space: nowrap; } - -@keyframes skeleton-breathe { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } } -@media (prefers-reduced-motion: reduce) { .skeleton-block { animation: none; } }