feat: 首页财务概览重构 — 5卡片合并为表格 + 费用列 + 环比列
- 5张独立财务卡片合并为一张表格(行=时间维度,列=6项指标) - 新增费用列(数据源expense_records,单/多租户路径均支持) - 每列增加环比列(季度累计行=季度环比,本月新增行=月度环比) - 环比:正值绿+xx%,负值红-xx% - 后端新增expense_annual/q2/month/prev_q/prev_month指标
This commit is contained in:
@@ -247,6 +247,7 @@ def bootstrap():
|
||||
t_ops = attach_common(conn, "operations", rows(conn, "SELECT * FROM operation_projects WHERE tenant=? ORDER BY id ASC", [t]))
|
||||
t_sales = attach_common(conn, "sales", rows(conn, "SELECT * FROM sales_leads WHERE tenant=? ORDER BY id DESC", [t]))
|
||||
t_products = attach_common(conn, "products", rows(conn, "SELECT * FROM product_versions WHERE tenant=? ORDER BY id DESC", [t]))
|
||||
t_expense = rows(conn, "SELECT * FROM expense_records WHERE tenant=?", [t])
|
||||
t_proposals = attach_common(conn, "proposals", rows(conn, "SELECT * FROM business_proposals WHERE tenant=? ORDER BY id DESC", [t]))
|
||||
t_signed_pfs = [x for x in t_pfs if x["status"] == "已签约"]
|
||||
def t_parse_budget(pf):
|
||||
@@ -305,10 +306,26 @@ def bootstrap():
|
||||
"cost_prev_q": t_sum_budget("cost", _prev_q_range),
|
||||
"cost_prev_month": t_sum_budget("cost", [_prev_month]) if _prev_month else 0,
|
||||
})
|
||||
# 费用汇总(从 expense_records 表计算)
|
||||
def t_expense_sum(m_range):
|
||||
total = 0
|
||||
for r in t_expense:
|
||||
m = (r.get("expense_month") or "").strip()
|
||||
if not m or "-" not in m: continue
|
||||
try:
|
||||
if int(m.split("-")[1]) in m_range:
|
||||
total += float(r.get("amount") or 0)
|
||||
except: pass
|
||||
return total
|
||||
all_metrics[-1]["expense_annual"] = t_expense_sum(range(1, 13))
|
||||
all_metrics[-1]["expense_q2"] = t_expense_sum(_q_range)
|
||||
all_metrics[-1]["expense_month"] = t_expense_sum([_now_month])
|
||||
all_metrics[-1]["expense_prev_q"] = t_expense_sum(_prev_q_range)
|
||||
all_metrics[-1]["expense_prev_month"] = t_expense_sum([_prev_month]) if _prev_month else 0
|
||||
all_monthly.append(monthly_finance(conn, t))
|
||||
all_recent.extend(rows(conn, "SELECT * FROM follow_up_records WHERE tenant=? ORDER BY id DESC LIMIT 4", [t]))
|
||||
agg = {}
|
||||
for key in ["total_projects","total_proposals","total_products","upcoming_products","signed_amount","signed_annual","signed_q2","signed_month","signed_prev_q","signed_prev_month","revenue_annual","revenue_q2","monthly_revenue","revenue_prev_q","revenue_prev_month","gross_annual","gross_q2","monthly_net_profit","gross_prev_q","gross_prev_month","payment_annual","payment_q2","payment_month","payment_prev_q","payment_prev_month","cost_annual","cost_q2","cost_month","cost_prev_q","cost_prev_month"]:
|
||||
for key in ["total_projects","total_proposals","total_products","upcoming_products","signed_amount","signed_annual","signed_q2","signed_month","signed_prev_q","signed_prev_month","revenue_annual","revenue_q2","monthly_revenue","revenue_prev_q","revenue_prev_month","gross_annual","gross_q2","monthly_net_profit","gross_prev_q","gross_prev_month","payment_annual","payment_q2","payment_month","payment_prev_q","payment_prev_month","cost_annual","cost_q2","cost_month","cost_prev_q","cost_prev_month","expense_annual","expense_q2","expense_month","expense_prev_q","expense_prev_month"]:
|
||||
agg[key] = sum(m.get(key, 0) for m in all_metrics)
|
||||
merged_monthly = []
|
||||
for i in range(12):
|
||||
@@ -380,6 +397,21 @@ def bootstrap():
|
||||
cost_prev_q = sum_budget("cost", _prev_q_range)
|
||||
payment_prev_month = sum_budget("payment", [_prev_month]) if _prev_month else 0
|
||||
cost_prev_month = sum_budget("cost", [_prev_month]) if _prev_month else 0
|
||||
def expense_sum(month_range):
|
||||
total = 0
|
||||
for r in expense:
|
||||
m = (r.get("expense_month") or "").strip()
|
||||
if not m or "-" not in m: continue
|
||||
try:
|
||||
if int(m.split("-")[1]) in month_range:
|
||||
total += float(r.get("amount") or 0)
|
||||
except: pass
|
||||
return total
|
||||
expense_annual = expense_sum(range(1, 13))
|
||||
expense_q2 = expense_sum(_q_range)
|
||||
expense_month_val = expense_sum([_now_month])
|
||||
expense_prev_q = expense_sum(_prev_q_range)
|
||||
expense_prev_month = expense_sum([_prev_month]) if _prev_month else 0
|
||||
def pf_status_sum(status):
|
||||
return sum(x["sign_amount"] or 0 for x in pfs if x["status"] == status)
|
||||
signed_amount = pf_status_sum("已签约")
|
||||
@@ -431,6 +463,11 @@ def bootstrap():
|
||||
"cost_month": cost_month,
|
||||
"cost_prev_q": cost_prev_q,
|
||||
"cost_prev_month": cost_prev_month,
|
||||
"expense_annual": expense_annual,
|
||||
"expense_q2": expense_q2,
|
||||
"expense_month": expense_month_val,
|
||||
"expense_prev_q": expense_prev_q,
|
||||
"expense_prev_month": expense_prev_month,
|
||||
"signed_not_executed": signed_not_executed,
|
||||
},
|
||||
"recent": q("SELECT * FROM follow_up_records WHERE tenant=? ORDER BY id DESC LIMIT 8", tenant),
|
||||
|
||||
@@ -39,24 +39,48 @@ function renderHome() {
|
||||
["上季度累计", moneyInt(m.cost_prev_q || 0)],
|
||||
["上月累计", moneyInt(m.cost_prev_month || 0)],
|
||||
];
|
||||
let _cardId = 0;
|
||||
const tblCard = (title, rows) => {
|
||||
const id = 'finCard' + (++_cardId);
|
||||
const visible = rows.slice(0, 3);
|
||||
const extra = rows.slice(3);
|
||||
const rowHtml = (r) => r.map(([label, value]) => `<tr class="border-b border-slate-100 last:border-0"><td class="py-2 pr-4 text-slate-500">${label}</td><td class="py-2 text-right font-semibold text-slate-800">${value}</td></tr>`).join("");
|
||||
return card(`<h3 class="text-sm font-bold text-slate-700 mb-3">${title}</h3><table class="w-full text-sm"><tbody>${rowHtml(visible)}</tbody></table><table class="w-full text-sm hidden" id="${id}"><tbody>${rowHtml(extra)}</tbody></table><button class="w-full text-center py-1 text-xs text-slate-400 hover:text-slate-600" onclick="toggleFinCard('${id}')" id="${id}_btn"><i data-lucide="chevron-down" style="width:14px;height:14px;display:inline"></i></button>`, "p-4");
|
||||
};
|
||||
window.toggleFinCard = (id) => {
|
||||
const t = document.getElementById(id);
|
||||
if (!t) return;
|
||||
t.classList.toggle("hidden");
|
||||
const btn = document.getElementById(id + "_btn");
|
||||
btn.innerHTML = t.classList.contains("hidden")
|
||||
? '<i data-lucide="chevron-down" style="width:14px;height:14px;display:inline"></i>'
|
||||
: '<i data-lucide="chevron-up" style="width:14px;height:14px;display:inline"></i>';
|
||||
if (window.lucide) window.lucide.createIcons();
|
||||
};
|
||||
const rows6 = [
|
||||
["年度累计", moneyInt(m.expense_annual || 0)],
|
||||
["季度累计", moneyInt(m.expense_q2 || 0)],
|
||||
["本月新增", moneyInt(m.expense_month || 0)],
|
||||
["上季度累计", moneyInt(m.expense_prev_q || 0)],
|
||||
["上月累计", moneyInt(m.expense_prev_month || 0)],
|
||||
];
|
||||
const qoq = (cur, prev) => {
|
||||
if (!prev) return '<span class="text-slate-300">—</span>';
|
||||
const pct = Math.round((cur - prev) / prev * 100);
|
||||
const cls = pct >= 0 ? 'text-green-600' : 'text-red-600';
|
||||
const sign = pct >= 0 ? '+' : '';
|
||||
return '<span class="' + cls + '">' + sign + pct + '%</span>';
|
||||
};
|
||||
const COLORS = ['text-slate-700','text-blue-600','text-green-600','text-amber-600','text-rose-600','text-orange-600'];
|
||||
const LABELS = ['合同金额','确收金额','确收毛利','回款金额','应付金额','费用'];
|
||||
const ALL_ROWS = [rows1, rows2, rows3, rows4, rows5, rows6];
|
||||
// 季度环比:Q 值 vs prev_Q 值;月度环比:month 值 vs prev_month 值
|
||||
const Q_FIELDS = [m.signed_q2||0, m.revenue_q2, m.gross_q2, m.payment_q2||0, m.cost_q2||0, m.expense_q2||0];
|
||||
const Q_PREV = [m.signed_prev_q||0, m.revenue_prev_q||0, m.gross_prev_q||0, m.payment_prev_q||0, m.cost_prev_q||0, m.expense_prev_q||0];
|
||||
const M_FIELDS = [m.signed_month||0, m.monthly_revenue, m.monthly_net_profit, m.payment_month||0, m.cost_month||0, m.expense_month||0];
|
||||
const M_PREV = [m.signed_prev_month||0, m.revenue_prev_month||0, m.gross_prev_month||0, m.payment_prev_month||0, m.cost_prev_month||0, m.expense_prev_month||0];
|
||||
var thead = '<tr class="border-b border-slate-200"><th class="py-2 text-left text-slate-500"></th>';
|
||||
for (var ci = 0; ci < 6; ci++) {
|
||||
thead += '<th class="py-2 text-right font-semibold ' + COLORS[ci] + '">' + LABELS[ci] + '</th><th class="py-2 text-right font-semibold text-slate-400" style="font-size:11px">环比</th>';
|
||||
}
|
||||
thead += '</tr>';
|
||||
var tbody = '';
|
||||
for (var ri = 0; ri < 5; ri++) {
|
||||
var rowLabel = rows1[ri][0];
|
||||
var rowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-2 text-slate-500">' + rowLabel + '</td>';
|
||||
for (var ci = 0; ci < 6; ci++) {
|
||||
rowHtml += '<td class="py-2 text-right font-semibold text-slate-800">' + ALL_ROWS[ci][ri][1] + '</td>';
|
||||
var momHtml = '';
|
||||
if (ri === 1) momHtml = qoq(Q_FIELDS[ci], Q_PREV[ci]); // 季度累计行:季度环比
|
||||
else if (ri === 2) momHtml = qoq(M_FIELDS[ci], M_PREV[ci]); // 本月新增行:月度环比
|
||||
else momHtml = '<span class="text-slate-300">—</span>';
|
||||
rowHtml += '<td class="py-2 text-right" style="font-size:12px">' + momHtml + '</td>';
|
||||
}
|
||||
rowHtml += '</tr>';
|
||||
tbody += rowHtml;
|
||||
}
|
||||
document.querySelector("#home").innerHTML = `
|
||||
<div class="grid gap-5">
|
||||
${state.tenant === "总工作台" ? "" : `<div class="grid grid-cols-4 gap-3">
|
||||
@@ -67,7 +91,7 @@ window.toggleFinCard = (id) => {
|
||||
["产品迭代", m.upcoming_products, "products"],
|
||||
].map(([label, value, tab]) => `<button class="metric-card" onclick="switchTab('${tab}')"><span class="flex items-center gap-2 text-xs text-slate-500"><i data-lucide="gauge"></i>${label}</span><strong class="mt-2 block text-2xl">${value}</strong></button>`).join("")}
|
||||
</div>`}
|
||||
<div class="grid grid-cols-5 gap-5">${tblCard("合同金额", rows1)}${tblCard("确收金额", rows2)}${tblCard("确收毛利", rows3)}${tblCard("回款金额", rows4)}${tblCard("应付金额", rows5)}</div>
|
||||
${card(`<h3 class="text-sm font-bold text-slate-700 mb-3">财务概览</h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead>${thead}</thead><tbody>${tbody}</tbody></table></div>`, "p-4")}
|
||||
<div class="grid grid-cols-3 gap-5">
|
||||
${card(`<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度签约趋势</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="chartSign"></canvas></div>`, "p-4")}
|
||||
${card(`<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度确收与毛利</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="chartRev"></canvas></div>`, "p-4")}
|
||||
|
||||
Reference in New Issue
Block a user