diff --git a/static/modules/finance.js b/static/modules/finance.js
index 9c0dc85..c2462de 100644
--- a/static/modules/finance.js
+++ b/static/modules/finance.js
@@ -116,11 +116,11 @@ function renderFinance() {
const finHeaderBase = `视图:${toolDateSelect}`;
const finAddBtn = ``;
- const finFilterTabs = `
`;
+ const finFilterTabs = ``;
document.querySelector("#finance").innerHTML = `
${finFilterTabs}
- ${state.finFilter !== 'expense' ? card(`
${finHeaderBase}
${finAddBtn}
`, "p-4") : ''}
+ ${state.finFilter !== 'expense' && state.finFilter !== 'overview' ? card(`
${finHeaderBase}
${finAddBtn}
`, "p-4") : ''}
@@ -237,6 +237,10 @@ function renderFinance() {
${(() => {
+ if (state.finFilter === 'overview') {
+ setTimeout(() => renderFinanceOverview(), 10);
+ return ``;
+ }
if (state.finFilter === 'expense') {
setTimeout(() => renderExpense(), 10);
return ``;
@@ -876,4 +880,95 @@ window.deleteFinanceItem = async () => {
} catch (error) {
toast("删除失败:" + error.message, "error");
}
+}
+
+// 总览渲染
+function renderFinanceOverview() {
+ const { summary, financeMonthly } = state.data;
+ if (!summary) return;
+ const m = summary.metrics;
+ const moneyIntL = (v) => `${Math.round(Number(v || 0)).toLocaleString("zh-CN")} 元`;
+ const moneyWan = (v) => {
+ const n = Number(v || 0);
+ return n >= 10000 ? (n / 10000).toFixed(0) + "万" : n.toLocaleString("zh-CN");
+ };
+ const card = (body, cls) => '' + body + '
';
+
+ const rows1 = [["年度累计", moneyIntL(m.signed_annual || m.signed_amount)], ["本季度累计", moneyIntL(m.signed_q2 || 0)], ["本月累计", moneyIntL(m.signed_month || 0)], ["上本季度累计", moneyIntL(m.signed_prev_q || 0)], ["上月累计", moneyIntL(m.signed_prev_month || 0)]];
+ const rows2 = [["年度累计", moneyIntL(m.revenue_annual || 0)], ["本季度累计", moneyIntL(m.revenue_q2 || 0)], ["本月累计", moneyIntL(m.monthly_revenue || 0)], ["上本季度累计", moneyIntL(m.revenue_prev_q || 0)], ["上月累计", moneyIntL(m.revenue_prev_month || 0)]];
+ const rows3 = [["年度累计", moneyIntL(m.gross_annual || 0)], ["本季度累计", moneyIntL(m.gross_q2 || 0)], ["本月累计", moneyIntL(m.monthly_net_profit || 0)], ["上本季度累计", moneyIntL(m.gross_prev_q || 0)], ["上月累计", moneyIntL(m.gross_prev_month || 0)]];
+ const rows4 = [["年度累计", moneyIntL(m.cost_annual || 0)], ["本季度累计", moneyIntL(m.cost_q2 || 0)], ["本月累计", moneyIntL(m.cost_month || 0)], ["上本季度累计", moneyIntL(m.cost_prev_q || 0)], ["上月累计", moneyIntL(m.cost_prev_month || 0)]];
+ const rows5 = [["年度累计", moneyIntL(m.profit_annual || 0)], ["本季度累计", moneyIntL(m.profit_q2 || 0)], ["本月累计", moneyIntL(m.profit_month || 0)], ["上本季度累计", moneyIntL(m.profit_prev_q || 0)], ["上月累计", moneyIntL(m.profit_prev_month || 0)]];
+ const rows7 = [["年度累计", moneyIntL(m.payment_annual || 0)], ["本季度累计", moneyIntL(m.payment_q2 || 0)], ["本月累计", moneyIntL(m.payment_month || 0)], ["上本季度累计", moneyIntL(m.payment_prev_q || 0)], ["上月累计", moneyIntL(m.payment_prev_month || 0)]];
+ const rows8 = [["年度累计", moneyIntL(m.paid_annual || 0)], ["本季度累计", moneyIntL(m.paid_q2 || 0)], ["本月累计", moneyIntL(m.paid_month || 0)], ["上本季度累计", moneyIntL(m.paid_prev_q || 0)], ["上月累计", moneyIntL(m.paid_prev_month || 0)]];
+ const rows9 = [["年度累计", moneyIntL(m.cashflow_annual || 0)], ["本季度累计", moneyIntL(m.cashflow_q2 || 0)], ["本月累计", moneyIntL(m.cashflow_month || 0)], ["上本季度累计", moneyIntL(m.cashflow_prev_q || 0)], ["上月累计", moneyIntL(m.cashflow_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 LABELS = ['合同金额','确收金额','确收毛利','成本','项目利润','回款金额','已付','现金流'];
+ const Q_FIELDS = [m.signed_q2||0, m.revenue_q2, m.gross_q2, m.cost_q2||0, m.profit_q2||0, m.payment_q2||0, m.paid_q2||0, m.cashflow_q2||0];
+ const Q_PREV = [m.signed_prev_q||0, m.revenue_prev_q||0, m.gross_prev_q||0, m.cost_prev_q||0, m.profit_prev_q||0, m.payment_prev_q||0, m.paid_prev_q||0, m.cashflow_prev_q||0];
+ const M_FIELDS = [m.signed_month||0, m.monthly_revenue, m.monthly_net_profit, m.cost_month||0, m.profit_month||0, m.payment_month||0, m.paid_month||0, m.cashflow_month||0];
+ const M_PREV = [m.signed_prev_month||0, m.revenue_prev_month||0, m.gross_prev_month||0, m.cost_prev_month||0, m.profit_prev_month||0, m.payment_prev_month||0, m.paid_prev_month||0, m.cashflow_prev_month||0];
+ const ROWS = [rows1, rows2, rows3, rows4, rows5, rows7, rows8, rows9];
+ const CF_RAW = [m.cashflow_annual||0, m.cashflow_q2||0, m.cashflow_month||0, m.cashflow_prev_q||0, m.cashflow_prev_month||0];
+ const PF_RAW = [m.profit_annual||0, m.profit_q2||0, m.profit_month||0, m.profit_prev_q||0, m.profit_prev_month||0];
+
+ var tdVal = function(ri, col, val) {
+ if (ri === 4) return '' + val + ' | ';
+ if (ri === 7) return '' + val + ' | ';
+ return '' + val + ' | ';
+ };
+ var thead = '| 指标 | 年度累计 | 上季度累计 | 上月累计 | 本季度累计 | 季环比 | 本月累计 | 月环比 |
';
+ var tbody = '';
+ for (var ri = 0; ri < 8; ri++) {
+ var vals = ROWS[ri];
+ tbody += '| ' + LABELS[ri] + ' | ' +
+ tdVal(ri, 0, vals[0][1]) + tdVal(ri, 3, vals[3][1]) + tdVal(ri, 4, vals[4][1]) + tdVal(ri, 1, vals[1][1]) +
+ '' + qoq(Q_FIELDS[ri], Q_PREV[ri]) + ' | ' +
+ tdVal(ri, 2, vals[2][1]) +
+ '' + qoq(M_FIELDS[ri], M_PREV[ri]) + ' |
';
+ }
+
+ document.querySelector("#financeOverview").innerHTML = '' +
+ card('
业务(项目)财务概览
合同 → 确收 → 毛利 → 成本 → 项目利润 → 回款 → 已付 → 现金流
' + thead + '' + tbody + '
', 'p-4') +
+ '
' +
+ card('
月度签约趋势
2026', 'p-4') +
+ card('
月度确收与毛利
2026', 'p-4') +
+ card('
月度回款与已付
2026', 'p-4') +
+ card('
月度项目利润
2026', 'p-4') +
+ '
';
+
+ if (window.lucide) window.lucide.createIcons();
+ // 渲染图表(复用 home.js 的 moneyTick / chartOptions / monthLabels)
+ if (financeMonthly && window.Chart) {
+ renderOverviewCharts(financeMonthly);
+ }
+}
+
+function renderOverviewCharts(data) {
+ if (typeof monthLabels !== 'function') return;
+ const labels = monthLabels(data);
+ const baseOpts = typeof chartOptions === 'function' ? chartOptions(moneyTick) : { responsive: true, maintainAspectRatio: false };
+ var iconf = { type: "line", options: baseOpts, data: { labels: labels } };
+
+ var c1 = document.querySelector("#finChartSign");
+ if (c1 && window.Chart) {
+ if (state.finChart1) state.finChart1.destroy();
+ state.finChart1 = new Chart(c1, Object.assign({}, iconf, { data: { labels: labels, datasets: [{ label: "签约金额", data: data.map(function(x) { return x.sign || 0; }), borderColor: "#6366f1", backgroundColor: "rgba(99,102,241,0.06)", fill: true, tension: 0.3 }] } }));
+ }
+ var c2 = document.querySelector("#finChartRev");
+ if (c2 && window.Chart) {
+ if (state.finChart2) state.finChart2.destroy();
+ state.finChart2 = new Chart(c2, Object.assign({}, iconf, { data: { labels: labels, datasets: [{ label: "确收", data: data.map(function(x) { return x.revenue || 0; }), borderColor: "#2563eb", backgroundColor: "rgba(37,99,235,0.06)", fill: true, tension: 0.3 }, { label: "毛利", data: data.map(function(x) { return x.gross || 0; }), borderColor: "#059669", backgroundColor: "rgba(5,150,105,0.06)", fill: true, tension: 0.3 }] } }));
+ }
+ var c3 = document.querySelector("#finChartCash");
+ if (c3 && window.Chart) {
+ if (state.finChart3) state.finChart3.destroy();
+ state.finChart3 = new Chart(c3, Object.assign({}, iconf, { data: { labels: labels, datasets: [{ label: "回款", data: data.map(function(x) { return x.payment || 0; }), borderColor: "#d97706", backgroundColor: "rgba(217,119,6,0.06)", fill: true, tension: 0.3 }, { label: "已付", data: data.map(function(x) { return x.cost || 0; }), borderColor: "#7c3aed", backgroundColor: "rgba(124,58,237,0.06)", fill: true, tension: 0.3 }] } }));
+ }
+ var c4 = document.querySelector("#finChartProfit");
+ if (c4 && window.Chart) {
+ if (state.finChart4) state.finChart4.destroy();
+ state.finChart4 = new Chart(c4, Object.assign({}, iconf, { data: { labels: labels, datasets: [{ label: "利润", data: data.map(function(x) { return x.gross || 0; }), borderColor: "#6366f1", backgroundColor: "rgba(99,102,241,0.06)", fill: true, tension: 0.3 }] } }));
+ }
};
diff --git a/templates/index.html b/templates/index.html
index d4450cf..0dd8180 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -76,7 +76,7 @@
-
OPC Manager v1.0.0.12
+
OPC Manager v1.0.0.13