// home.js — 首页渲染 + 财务趋势图 function renderHome() { const { summary, financeMonthly } = state.data; const m = summary.metrics; const moneyInt = (v) => `${Math.round(Number(v || 0)).toLocaleString("zh-CN")} 元`; const rows1 = [ ["年度累计", moneyInt(m.signed_annual || m.signed_amount)], ["季度累计", moneyInt(m.signed_q2 || 0)], ["本月新增", moneyInt(m.signed_month || 0)], ["上季度累计", moneyInt(m.signed_prev_q || 0)], ["上月累计", moneyInt(m.signed_prev_month || 0)], ]; const rows2 = [ ["年度累计", moneyInt(m.revenue_annual)], ["季度累计", moneyInt(m.revenue_q2)], ["本月新增", moneyInt(m.monthly_revenue)], ["上季度累计", moneyInt(m.revenue_prev_q || 0)], ["上月累计", moneyInt(m.revenue_prev_month || 0)], ]; const rows3 = [ ["年度累计", moneyInt(m.gross_annual)], ["季度累计", moneyInt(m.gross_q2)], ["本月新增", moneyInt(m.monthly_net_profit)], ["上季度累计", moneyInt(m.gross_prev_q || 0)], ["上月累计", moneyInt(m.gross_prev_month || 0)], ]; const rows4 = [ ["年度累计", moneyInt(m.payment_annual || 0)], ["季度累计", moneyInt(m.payment_q2 || 0)], ["本月新增", moneyInt(m.payment_month || 0)], ["上季度累计", moneyInt(m.payment_prev_q || 0)], ["上月累计", moneyInt(m.payment_prev_month || 0)], ]; const rows5 = [ ["年度累计", moneyInt(m.cost_annual || 0)], ["季度累计", moneyInt(m.cost_q2 || 0)], ["本月新增", moneyInt(m.cost_month || 0)], ["上季度累计", moneyInt(m.cost_prev_q || 0)], ["上月累计", moneyInt(m.cost_prev_month || 0)], ]; 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 === "总工作台" ? "" : `
${[ ["经营管理", m.total_projects, "finance"], ["重点工作与台账", m.total_proposals, "projects"], ["业务方案", m.total_products, "proposals"], ["产品迭代", m.upcoming_products, "products"], ].map(([label, value, tab]) => ``).join("")}
`} ${card(`

财务概览

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

月度签约趋势

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

月度确收与毛利

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

月度回款与费用

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

近期动态

${summary.recent.map((r) => `
${r.content}
${r.followed_at}
`).join("")}
`, "p-5")}
`; renderCharts(financeMonthly); } function chartOptions(yCallback) { return { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: "bottom", labels: { boxWidth: 12, font: { size: 11 } } } }, scales: { x: { ticks: { font: { size: 10 } }, grid: { display: false } }, y: { ticks: { font: { size: 11 }, callback: yCallback } }, }, }; } const moneyTick = (v) => v >= 10000 ? (v / 10000).toFixed(0) + "万" : v; const monthLabels = (data) => data.map((x) => parseInt(x.month.split("-")[1]) + "月"); function renderCharts(data) { const labels = monthLabels(data); const baseOpts = chartOptions(moneyTick); // 图1:月度签约 const c1 = document.querySelector("#chartSign"); if (c1 && window.Chart) { if (state.chart) state.chart.destroy(); state.chart = new Chart(c1, { type: "line", data: { labels, datasets: [ { label: "签约金额", data: data.map((x) => x.sign || 0), borderColor: "#6366f1", backgroundColor: "rgba(99,102,241,0.06)", fill: true, tension: 0.3 }, ]}, options: baseOpts, }); } // 图2:月度确收与毛利 const c2 = document.querySelector("#chartRev"); if (c2 && window.Chart) { if (state.chart2) state.chart2.destroy(); state.chart2 = new Chart(c2, { type: "line", data: { labels, datasets: [ { label: "确收", data: data.map((x) => x.revenue || 0), borderColor: "#2563eb", backgroundColor: "rgba(37,99,235,0.06)", fill: true, tension: 0.3 }, { label: "毛利", data: data.map((x) => x.gross || 0), borderColor: "#059669", backgroundColor: "rgba(5,150,105,0.06)", fill: true, tension: 0.3 }, ]}, options: baseOpts, }); } // 图3:月度回款与费用 const c3 = document.querySelector("#chartCash"); if (c3 && window.Chart) { if (state.chart3) state.chart3.destroy(); state.chart3 = new Chart(c3, { type: "bar", data: { labels, datasets: [ { label: "回款", data: data.map((x) => x.payment || 0), backgroundColor: "#d97706", borderRadius: 4 }, { label: "费用", data: data.map((x) => x.cost || 0), backgroundColor: "#ef4444", borderRadius: 4 }, ]}, options: baseOpts, }); } }