From 340f8bd88c6cef7976d315989d496d98b1e5ed6d Mon Sep 17 00:00:00 2001 From: mac Date: Mon, 6 Jul 2026 18:20:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=A6=96=E9=A1=B5=E8=B4=A2=E5=8A=A1?= =?UTF-8?q?=E6=A6=82=E8=A7=88=E9=87=8D=E6=9E=84=20=E2=80=94=205=E5=8D=A1?= =?UTF-8?q?=E7=89=87=E5=90=88=E5=B9=B6=E4=B8=BA=E8=A1=A8=E6=A0=BC=20+=20?= =?UTF-8?q?=E8=B4=B9=E7=94=A8=E5=88=97=20+=20=E7=8E=AF=E6=AF=94=E5=88=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 5张独立财务卡片合并为一张表格(行=时间维度,列=6项指标) - 新增费用列(数据源expense_records,单/多租户路径均支持) - 每列增加环比列(季度累计行=季度环比,本月新增行=月度环比) - 环比:正值绿+xx%,负值红-xx% - 后端新增expense_annual/q2/month/prev_q/prev_month指标 --- backend/routes.py | 39 +++++++++++++++++++++++++- static/modules/home.js | 62 +++++++++++++++++++++++++++++------------- 2 files changed, 81 insertions(+), 20 deletions(-) diff --git a/backend/routes.py b/backend/routes.py index 2d06019..20e21e4 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -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), diff --git a/static/modules/home.js b/static/modules/home.js index c36426b..3b7c039 100644 --- a/static/modules/home.js +++ b/static/modules/home.js @@ -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]) => `${label}${value}`).join(""); - return card(`

${title}

${rowHtml(visible)}
${rowHtml(extra)}`, "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") - ? '' - : ''; - 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 ''; + const pct = Math.round((cur - prev) / prev * 100); + const cls = pct >= 0 ? 'text-green-600' : 'text-red-600'; + const sign = pct >= 0 ? '+' : ''; + return '' + sign + pct + '%'; + }; + 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 = ''; + for (var ci = 0; ci < 6; ci++) { + thead += '' + LABELS[ci] + '环比'; + } + thead += ''; + var tbody = ''; + for (var ri = 0; ri < 5; ri++) { + var rowLabel = rows1[ri][0]; + var rowHtml = '' + rowLabel + ''; + for (var ci = 0; ci < 6; ci++) { + rowHtml += '' + ALL_ROWS[ci][ri][1] + ''; + 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 = ''; + rowHtml += '' + momHtml + ''; + } + rowHtml += ''; + tbody += rowHtml; + } document.querySelector("#home").innerHTML = `
${state.tenant === "总工作台" ? "" : `
@@ -67,7 +91,7 @@ window.toggleFinCard = (id) => { ["产品迭代", m.upcoming_products, "products"], ].map(([label, value, tab]) => ``).join("")}
`} -
${tblCard("合同金额", rows1)}${tblCard("确收金额", rows2)}${tblCard("确收毛利", rows3)}${tblCard("回款金额", rows4)}${tblCard("应付金额", rows5)}
+ ${card(`

财务概览

${thead}${tbody}
`, "p-4")}
${card(`

月度签约趋势

2026
`, "p-4")} ${card(`

月度确收与毛利

2026
`, "p-4")}