diff --git a/web/admin.html b/web/admin.html index 47a4b03..854d467 100644 --- a/web/admin.html +++ b/web/admin.html @@ -5,7 +5,7 @@ 管理端 · 金牛集团 - + @@ -82,49 +82,74 @@ -
+
+
+ + 公司往来合计 · 往来明细 + 单位:万元 · 左侧选择公司,右侧查看其往来明细 + +
+ + +
+
-
- 公司往来合计单位:万元 - -
-
- - - - - - - - - - - - -
公司明细笔数期末净值状态
加载中…
+
+
加载中…
-
-
- 公司往来明细选择左侧公司查看会计式分级明细 +
+
+
+ 请选择左侧公司 + 按对方公司分组 · 二级默认收起 · 已确认 / 待确认分列 +
-
- - - - - - - - - - - - - - -
对方 / 摘要笔数期初本期借方本期贷方期末
请选择左侧公司
+ +
+
+ + + + + + + + + + + + + + +
对方 / 摘要笔数期初本期借方本期贷方期末
请选择左侧公司
+
+
@@ -917,6 +942,6 @@
- + diff --git a/web/app.js b/web/app.js index b981355..cefa640 100644 --- a/web/app.js +++ b/web/app.js @@ -1161,33 +1161,43 @@ function updateDashAuditCard(audit) { } function renderDashCompanyRows(companies, selectedId) { - const tbody = $("#dashCompanyRows"); - if (!tbody) return; + const list = $("#dashCompanyRows"); + if (!list) return; if (!companies.length) { - tbody.innerHTML = '暂无公司或尚无归集往来'; + list.innerHTML = '
暂无公司或尚无归集往来
'; return; } - tbody.replaceChildren(...companies.map((company) => { - const tr = document.createElement("tr"); - tr.className = "clickable"; - tr.dataset.companyId = company.id; + const monthLabel = state.dashPeriodMonth ? `${state.dashPeriodMonth}月` : ""; + list.replaceChildren(...companies.map((company) => { + const item = document.createElement("div"); + item.className = "dash-company-item"; + item.setAttribute("role", "option"); + item.dataset.companyId = company.id; + item.tabIndex = 0; if (String(company.id) === String(selectedId)) { - tr.classList.add("is-selected"); + item.classList.add("is-selected"); + item.setAttribute("aria-selected", "true"); + } else { + item.setAttribute("aria-selected", "false"); } - const nameTd = document.createElement("td"); - nameTd.className = "dash-company-name"; - nameTd.title = company.name; - nameTd.textContent = company.name; - const countTd = document.createElement("td"); - countTd.className = "num-col"; - countTd.textContent = String(company.detail_count ?? 0); - const netTd = document.createElement("td"); - netTd.className = `num-col ${netClass(company.net_wan)}`; - netTd.textContent = formatWan(company.net_wan); - const statusTd = document.createElement("td"); - statusTd.innerHTML = statusPill(company.period_status_label); - tr.append(nameTd, countTd, netTd, statusTd); - return tr; + const main = document.createElement("div"); + main.className = "dash-company-main"; + const name = document.createElement("span"); + name.className = "dash-company-name"; + name.title = company.name; + name.textContent = company.name; + const meta = document.createElement("span"); + meta.className = "dash-company-meta"; + const statusLabel = company.period_status_label || "—"; + meta.textContent = monthLabel + ? `${company.detail_count ?? 0}笔 · ${monthLabel} ${statusLabel}` + : `${company.detail_count ?? 0}笔 · ${statusLabel}`; + main.append(name, meta); + const net = document.createElement("span"); + net.className = `dash-company-net ${netClass(company.net_wan)}`; + net.textContent = formatWan(company.net_wan); + item.append(main, net); + return item; })); } @@ -1199,22 +1209,94 @@ function chevronSvg(expanded) { return ''; } -function renderDashDetail(payload, { expandFirst = false } = {}) { +function formatPlainWan(value) { + const num = Number(value); + if (!Number.isFinite(num)) return "0.00"; + return Math.abs(num).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); +} + +function updateDashDetailSummary(company, payload) { + const bar = $("#dashDetailSummary"); + if (!bar) return; + if (!company) { + bar.hidden = true; + return; + } + bar.hidden = false; + const count = payload?.groups + ? payload.groups.reduce((sum, g) => sum + Number(g.count || 0), 0) + : Number(company.detail_count || 0); + const debit = payload?.groups + ? payload.groups.reduce((sum, g) => sum + Number(g.debit_wan || 0), 0) + : Number(company.debit_wan || 0); + const credit = payload?.groups + ? payload.groups.reduce((sum, g) => sum + Number(g.credit_wan || 0), 0) + : Number(company.credit_wan || 0); + const ending = payload?.groups + ? payload.groups.reduce((sum, g) => sum + Number(g.ending_wan || 0), 0) + : Number(company.net_wan || 0); + const setText = (id, text, className = "") => { + const el = $(id); + if (!el) return; + el.textContent = text; + el.className = `dash-summary-value num ${className}`.trim(); + }; + setText("#dashSumCount", String(count)); + setText("#dashSumOpening", "—", "meta"); + setText("#dashSumDebit", formatPlainWan(debit)); + setText("#dashSumCredit", formatPlainWan(credit)); + setText("#dashSumEnding", formatWan(ending), netClass(ending)); +} + +function showDashDetailEmpty(company) { + const wrap = $("#dashDetailWrap"); + const empty = $("#dashDetailEmpty"); + const desc = $("#dashDetailEmptyDesc"); + if (wrap) wrap.hidden = true; + if (empty) empty.hidden = false; + if (desc) { + const cutoff = state.dashCutoff || "—"; + // 方案 A 空态:两行克制说明;期初口径仍为不可用(—),不伪造 0.00 + desc.textContent = `期初 — · 截至 ${cutoff} 无明细记录 · 导入流水并归集后在此展示`; + } + updateDashDetailSummary(null); +} + +function showDashDetailTable() { + const wrap = $("#dashDetailWrap"); + const empty = $("#dashDetailEmpty"); + if (wrap) wrap.hidden = false; + if (empty) empty.hidden = true; +} + +function renderDashDetail(payload, { expandFirst = false, company = null } = {}) { const tbody = $("#dashDetailRows"); const title = $("#dashDetailTitle"); if (!tbody) return; + const selected = company || (state.dashCompanies || []).find( + (c) => String(c.id) === String(payload?.company_id || state.dashSelectedId) + ) || null; + if (!payload) { + if (title) title.textContent = "请选择左侧公司"; + showDashDetailTable(); tbody.innerHTML = '请选择左侧公司'; + updateDashDetailSummary(null); return; } - if (title) { - title.innerHTML = `公司往来明细 · ${payload.company_name}按对方公司分组 · 二级默认收起 · 期初待 B-45`; - } + if (title) title.textContent = payload.company_name || selected?.name || "公司往来明细"; + const sub = $("#dashDetailSub"); + if (sub) sub.textContent = "按对方公司分组 · 二级默认收起 · 已确认 / 待确认分列"; + const groups = payload.groups || []; if (!groups.length) { - tbody.innerHTML = '该公司区间内暂无已归集往来'; + showDashDetailEmpty(selected || { detail_count: 0, debit_wan: 0, credit_wan: 0, net_wan: 0 }); + tbody.replaceChildren(); return; } + + showDashDetailTable(); + updateDashDetailSummary(selected, payload); const rows = []; groups.forEach((group, index) => { const expanded = expandFirst && index === 0; @@ -1268,7 +1350,12 @@ async function resolveDashStartDate() { async function loadDashCompanyDetail(companyId) { if (!companyId) return; state.dashSelectedId = companyId; + const company = (state.dashCompanies || []).find((c) => String(c.id) === String(companyId)) || null; renderDashCompanyRows(state.dashCompanies || [], companyId); + const title = $("#dashDetailTitle"); + if (title && company) title.textContent = company.name; + updateDashDetailSummary(company, null); + showDashDetailTable(); const tbody = $("#dashDetailRows"); if (tbody) tbody.innerHTML = '加载中…'; const from = await resolveDashStartDate(); @@ -1278,7 +1365,7 @@ async function loadDashCompanyDetail(companyId) { return; } const result = await response.json().catch(() => null); - renderDashDetail(result); + renderDashDetail(result, { company }); } async function loadAdminDashboard() { @@ -1286,22 +1373,30 @@ async function loadAdminDashboard() { const from = await resolveDashStartDate(); const response = await fetch(`/api/admin/dashboard?from=${encodeURIComponent(from)}`).catch(() => null); if (!response?.ok) { - $("#dashCompanyRows").innerHTML = '总览加载失败'; + $("#dashCompanyRows").innerHTML = '
总览加载失败
'; return; } const data = await response.json().catch(() => null); if (!data || data.status !== "ok") { - $("#dashCompanyRows").innerHTML = '总览加载失败'; + $("#dashCompanyRows").innerHTML = '
总览加载失败
'; return; } state.dashCompanies = data.companies || []; state.dashFrom = data.from_date; state.dashCutoff = data.cutoff; + state.dashPeriodMonth = data.period_month; updateDashAuditCard(data.audit); - const statusHead = $("#dashStatusHead"); - if (statusHead && data.period_month) statusHead.textContent = `${data.period_month}月状态`; const listSub = $("#dashListSub"); - if (listSub) listSub.textContent = `${data.from_date} 至 ${data.cutoff} · 单位:万元`; + if (listSub) { + listSub.textContent = `${data.from_date} 至 ${data.cutoff} · 单位:万元 · 左侧选择公司,右侧查看其往来明细`; + } + const meta = $("#dashMasterMeta"); + if (meta) { + const totals = data.totals || {}; + const companyCount = totals.company_count ?? state.dashCompanies.length; + const detailCount = totals.detail_count ?? 0; + meta.textContent = `${companyCount} 家公司 · 明细 ${detailCount} 笔`; + } const selected = state.dashSelectedId || state.dashCompanies[0]?.id; renderDashCompanyRows(state.dashCompanies, selected); if (selected) await loadDashCompanyDetail(selected); @@ -1309,10 +1404,21 @@ async function loadAdminDashboard() { function initDashboard() { if (!$("#companyMasterDetail")) return; + const selectCompany = (companyId) => { + if (!companyId) return; + loadDashCompanyDetail(companyId); + }; $("#dashCompanyRows")?.addEventListener("click", (event) => { - const tr = event.target.closest("tr[data-company-id]"); - if (!tr) return; - loadDashCompanyDetail(tr.dataset.companyId); + const item = event.target.closest(".dash-company-item[data-company-id]"); + if (!item) return; + selectCompany(item.dataset.companyId); + }); + $("#dashCompanyRows")?.addEventListener("keydown", (event) => { + if (event.key !== "Enter" && event.key !== " ") return; + const item = event.target.closest(".dash-company-item[data-company-id]"); + if (!item) return; + event.preventDefault(); + selectCompany(item.dataset.companyId); }); $("#dashDetailRows")?.addEventListener("click", (event) => { const tr = event.target.closest("tr[data-peer-group]"); @@ -1330,8 +1436,8 @@ function initDashboard() { }); $("#company-search")?.addEventListener("input", function () { const q = this.value.trim(); - $$("#dashCompanyRows tr[data-company-id]").forEach((tr) => { - tr.style.display = (!q || tr.textContent.includes(q)) ? "" : "none"; + $$("#dashCompanyRows .dash-company-item[data-company-id]").forEach((item) => { + item.style.display = (!q || item.textContent.includes(q)) ? "" : "none"; }); }); loadAdminDashboard(); diff --git a/web/design-system.css b/web/design-system.css index a10f46d..f219049 100644 --- a/web/design-system.css +++ b/web/design-system.css @@ -216,23 +216,176 @@ p { margin: 0; } /* 管理端首页主从:左栏约 480px(定稿 HEL-153),右侧明细更宽 */ .grid-480-1 { grid-template-columns: minmax(0, 480px) minmax(0, 1fr); align-items: stretch; } +/* 方案 A:一体卡片 · 分栏主从(HEL-162) */ +.dash-master { + margin-top: 14px; + padding: 0; + overflow: hidden; +} +.dash-master-head { + padding: 12px 16px 10px; + margin-bottom: 0; + gap: 10px; + border-bottom: 1px solid var(--border); + flex-wrap: wrap; +} +.dash-master-head .card-title .sub { + display: inline; + margin-left: 6px; +} +.dash-master-head-actions { + display: flex; + align-items: center; + gap: 10px; + flex: none; + margin-left: auto; +} +.dash-master-head-actions .input { + width: 132px; + min-height: 28px; + padding: 4px 8px; + flex: none; +} .dash-master-split { gap: 0; - height: 492px; + /* 少数据自适应;整卡(含共享头)目标约 340px;多数据模块内滚动 */ + max-height: 300px; } .dash-master-pane { display: flex; flex-direction: column; min-width: 0; min-height: 0; + overflow: hidden; } .dash-master-pane--list { border-right: 1px solid var(--border); } -.dash-master-head { - padding: 10px 14px 8px; - margin-bottom: 0; - gap: 8px; +.dash-company-list { + flex: 1; + min-height: 0; + overflow: auto; +} +.dash-company-loading, +.dash-company-empty { + padding: 16px; + font-size: 13px; +} +.dash-company-item { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 14px 8px 16px; + border-bottom: 1px solid var(--border); + cursor: pointer; + transition: background 0.12s ease; +} +.dash-company-item:last-child { + border-bottom: 0; +} +.dash-company-item:hover { + background: var(--fg-soft); +} +.dash-company-item.is-selected { + background: var(--accent-soft); + box-shadow: inset 3px 0 0 var(--accent); + padding-left: 13px; +} +.dash-company-item.is-selected:hover { + background: var(--accent-soft); +} +.dash-company-main { + min-width: 0; + flex: 1; +} +.dash-company-name { + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: 13.5px; + font-weight: 550; + color: var(--fg); + line-height: 1.35; +} +.dash-company-meta { + display: block; + margin-top: 2px; + font-size: 12px; + color: var(--muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.dash-company-net { + flex: none; + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + font-size: 13px; + font-weight: 600; + text-align: right; + min-width: 4.5em; +} +.dash-detail-toolbar { + padding: 10px 14px 0; + flex: none; +} +.dash-detail-heading { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} +.dash-detail-name { + font-size: 14px; + font-weight: 650; + color: var(--fg); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.dash-detail-summary { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 0; + margin: 10px 14px 0; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--bg); + flex: none; +} +.dash-summary-item { + padding: 8px 10px; + border-right: 1px solid var(--border); + min-width: 0; +} +.dash-summary-item:last-child { + border-right: 0; +} +.dash-summary-label { + display: block; + font-family: var(--font-mono); + font-size: 11px; + letter-spacing: 0.04em; + color: var(--muted); + margin-bottom: 2px; +} +.dash-summary-value { + display: block; + font-size: 13px; + font-weight: 600; + color: var(--fg); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.dash-detail-body { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + position: relative; + margin-top: 8px; } .dash-master-scroll { border: 0; @@ -241,6 +394,32 @@ p { margin: 0; } min-height: 0; overflow: auto; } +.dash-detail-empty { + flex: 1; + min-height: 120px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + padding: 20px 24px; + color: var(--muted); + font-size: 13px; +} +.dash-detail-empty .e-title { + font-weight: 600; + color: var(--fg); + margin-bottom: 6px; +} +.dash-detail-empty .e-desc { + max-width: 360px; + line-height: 1.55; +} +.dash-master-scroll[hidden], +.dash-detail-empty[hidden], +.dash-detail-summary[hidden] { + display: none !important; +} .ds-table.ds-table--compact { min-width: 0; } @@ -248,18 +427,16 @@ p { margin: 0; } .ds-table.ds-table--compact td { padding: 6px 12px; } +.ds-table.ds-table--compact th { + font-size: 11px; + white-space: nowrap; +} .ds-table tbody tr.is-selected { background: var(--accent-soft); } .ds-table tbody tr.is-selected:hover { background: var(--accent-soft); } -.dash-company-name { - white-space: nowrap; - max-width: 280px; - overflow: hidden; - text-overflow: ellipsis; -} .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; } @@ -271,15 +448,53 @@ p { margin: 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-480-1 { grid-template-columns: minmax(0, 1fr); } - .dash-master-split { height: auto; max-height: none; } - .dash-master-pane--list { border-right: 0; border-bottom: 1px solid var(--border); } - .dash-master-pane { max-height: 420px; } + .dash-master-split { + max-height: none; + } + .dash-master-pane--list { + border-right: 0; + border-bottom: 1px solid var(--border); + max-height: 280px; + } + .dash-master-pane--detail { + max-height: 420px; + } + .dash-detail-summary { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + .dash-summary-item:nth-child(3) { + border-right: 0; + } + .dash-summary-item:nth-child(n+4) { + border-top: 1px solid var(--border); + } } @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; } + .dash-master-head-actions { + width: 100%; + margin-left: 0; + justify-content: space-between; + } + .dash-master-head-actions .input { + flex: 1; + width: auto; + } + .dash-detail-summary { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .dash-summary-item:nth-child(2n) { + border-right: 0; + } + .dash-summary-item:nth-child(3) { + border-right: 1px solid var(--border); + } + .dash-summary-item:nth-child(n+3) { + border-top: 1px solid var(--border); + } } /* ─── 数据表 ────────────────────────────────────────────────────── */