HEL-162: 按方案A重做首页一体卡片主从区域
合并公司往来合计/明细为一张共享头卡片;左侧改列表行选中态,右侧加汇总条与克制空态;取消固定大高度,模块内滚动。 Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
co-authored by
Cursor
multica-agent
parent
b42b6481f5
commit
5b3ebad757
+144
-38
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user