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") : ''}
${(() => { + 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

科普 OPC 工作台