HEL-162: 按方案A重做首页一体卡片主从区域

合并公司往来合计/明细为一张共享头卡片;左侧改列表行选中态,右侧加汇总条与克制空态;取消固定大高度,模块内滚动。

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
施工员
2026-08-26 15:40:07 +00:00
co-authored by Cursor multica-agent
parent b42b6481f5
commit 5b3ebad757
3 changed files with 438 additions and 92 deletions
+144 -38
View File
@@ -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 = '<tr><td colspan="4" class="muted">暂无公司或尚无归集往来</td></tr>';
list.innerHTML = '<div class="dash-company-empty muted">暂无公司或尚无归集往来</div>';
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 '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" style="width:12px;height:12px;display:inline-block;vertical-align:-1px;margin-right:6px;"><path d="m9 18 6-6-6-6"/></svg>';
}
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 = '<tr><td colspan="6" class="muted">请选择左侧公司</td></tr>';
updateDashDetailSummary(null);
return;
}
if (title) {
title.innerHTML = `公司往来明细 · ${payload.company_name}<span class="sub" id="dashDetailSub">按对方公司分组 · 二级默认收起 · 期初待 B-45</span>`;
}
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 = '<tr><td colspan="6" class="muted">该公司区间内暂无已归集往来</td></tr>';
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 = '<tr class="loading-row"><td colspan="6">加载中…</td></tr>';
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 = '<tr><td colspan="4" class="muted">总览加载失败</td></tr>';
$("#dashCompanyRows").innerHTML = '<div class="dash-company-empty muted">总览加载失败</div>';
return;
}
const data = await response.json().catch(() => null);
if (!data || data.status !== "ok") {
$("#dashCompanyRows").innerHTML = '<tr><td colspan="4" class="muted">总览加载失败</td></tr>';
$("#dashCompanyRows").innerHTML = '<div class="dash-company-empty muted">总览加载失败</div>';
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();