Files
opc-manager/static/modules/home.js
mac ac312741df feat: 首页表格优化 — 列重排+项目费用回血+正负着色+命名统一
- 列顺序:年度累计→上季度累计→上月累计→本季度累计→季环比→本月累计→月环比
- 业务财务概览加回项目费用行(橙色,现金流之前)
- 本月新增→本月累计,季度累计→本季度累计
- 现金流/项目利润/部门净利:正值绿负值红
- 部门利润→部门毛利
2026-07-06 19:34:24 +08:00

234 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 rows7 = [
["年度累计", moneyInt(m.paid_annual || 0)],
["本季度累计", moneyInt(m.paid_q2 || 0)],
["本月累计", moneyInt(m.paid_month || 0)],
["上本季度累计", moneyInt(m.paid_prev_q || 0)],
["上月累计", moneyInt(m.paid_prev_month || 0)],
];
const rowsProjExpense = [
["年度累计", moneyInt(m.proj_expense_annual || 0)],
["本季度累计", moneyInt(m.proj_expense_q2 || 0)],
["本月累计", moneyInt(m.proj_expense_month || 0)],
["上本季度累计", moneyInt(m.proj_expense_prev_q || 0)],
["上月累计", moneyInt(m.proj_expense_prev_month || 0)],
];
const rows8 = [
["年度累计", moneyInt(m.cashflow_annual || 0)],
["本季度累计", moneyInt(m.cashflow_q2 || 0)],
["本月累计", moneyInt(m.cashflow_month || 0)],
["上本季度累计", moneyInt(m.cashflow_prev_q || 0)],
["上月累计", moneyInt(m.cashflow_prev_month || 0)],
];
const rows9 = [
["年度累计", moneyInt(m.profit_annual || 0)],
["本季度累计", moneyInt(m.profit_q2 || 0)],
["本月累计", moneyInt(m.profit_month || 0)],
["上本季度累计", moneyInt(m.profit_prev_q || 0)],
["上月累计", moneyInt(m.profit_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-purple-600','text-orange-600','text-cyan-600','text-indigo-600'];
const LABELS = ['合同金额','确收金额','确收毛利','回款金额','应付金额','已付','项目费用','现金流','项目利润'];
const Q_FIELDS = [m.signed_q2||0, m.revenue_q2, m.gross_q2, m.payment_q2||0, m.cost_q2||0, m.paid_q2||0, m.proj_expense_q2||0, m.cashflow_q2||0, m.profit_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.paid_prev_q||0, m.proj_expense_prev_q||0, m.cashflow_prev_q||0, m.profit_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.paid_month||0, m.proj_expense_month||0, m.cashflow_month||0, m.profit_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.paid_prev_month||0, m.proj_expense_prev_month||0, m.cashflow_prev_month||0, m.profit_prev_month||0];
const ROWS = [rows1, rows2, rows3, rows4, rows5, rows7, rowsProjExpense, 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 valCls = function(ri) { return (ri === 7 || ri === 8) ? '' : 'text-slate-800'; };
var tdVal = function(ri, col, val) {
if (ri === 7) return '<td class="py-2 text-right font-semibold ' + (CF_RAW[col] >= 0 ? 'text-green-600' : 'text-red-600') + '">' + val + '</td>';
if (ri === 8) return '<td class="py-2 text-right font-semibold ' + (PF_RAW[col] >= 0 ? 'text-green-600' : 'text-red-600') + '">' + val + '</td>';
return '<td class="py-2 text-right font-semibold text-slate-800">' + val + '</td>';
};
var thead = '<tr class="border-b border-slate-200"><th class="py-2 text-left text-slate-500">指标</th><th class="py-2 text-right font-semibold text-slate-700">年度累计</th><th class="py-2 text-right font-semibold text-slate-700">上本季度累计</th><th class="py-2 text-right font-semibold text-slate-700">上月累计</th><th class="py-2 text-right font-semibold text-slate-700">本季度累计</th><th class="py-2 text-right font-semibold text-slate-400">季环比</th><th class="py-2 text-right font-semibold text-slate-700">本月累计</th><th class="py-2 text-right font-semibold text-slate-400">月环比</th></tr>';
var tbody = '';
for (var ri = 0; ri < 9; ri++) {
var vals = ROWS[ri];
tbody += '<tr class="border-b border-slate-100 last:border-0"><td class="py-2 text-left font-semibold text-slate-700">' + LABELS[ri] + '</td>' +
tdVal(ri, 0, vals[0][1]) +
tdVal(ri, 3, vals[3][1]) +
tdVal(ri, 4, vals[4][1]) +
tdVal(ri, 1, vals[1][1]) +
'<td class="py-2 text-right">' + qoq(Q_FIELDS[ri], Q_PREV[ri]) + '</td>' +
tdVal(ri, 2, vals[2][1]) +
'<td class="py-2 text-right">' + qoq(M_FIELDS[ri], M_PREV[ri]) + '</td></tr>';
}
document.querySelector("#home").innerHTML = `
<div class="grid gap-5">
${state.tenant === "总工作台" ? "" : `<div class="grid grid-cols-4 gap-3">
${[
["项目管理", m.total_projects, "finance"],
["重点工作与台账", m.total_proposals, "projects"],
["业务方案", m.total_products, "proposals"],
["产品迭代", 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>`}
${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><tr class="border-b border-slate-200"><th class="py-2 text-left text-slate-500">指标</th><th class="py-2 text-right font-semibold text-slate-700">年度累计</th><th class="py-2 text-right font-semibold text-slate-700">上本季度累计</th><th class="py-2 text-right font-semibold text-slate-700">上月累计</th><th class="py-2 text-right font-semibold text-slate-700">本季度累计</th><th class="py-2 text-right font-semibold text-slate-400">季环比</th><th class="py-2 text-right font-semibold text-slate-700">本月累计</th><th class="py-2 text-right font-semibold text-slate-400">月环比</th></tr></thead><tbody>
${(() => {
var d = [
['部门毛利', [m.profit_annual||0, m.profit_q2||0, m.profit_month||0, m.profit_prev_q||0, m.profit_prev_month||0],
[m.profit_q2||0,m.profit_prev_q||0], [m.profit_month||0,m.profit_prev_month||0]],
['部门费用', [m.expense_annual||0, m.expense_q2||0, (m.expense_month||0), m.expense_prev_q||0, m.expense_prev_month||0],
[m.expense_q2||0,m.expense_prev_q||0], [(m.expense_month||0),m.expense_prev_month||0]],
['部门净利', [
(m.profit_annual||0)-(m.expense_annual||0), (m.profit_q2||0)-(m.expense_q2||0), (m.profit_month||0)-(m.expense_month||0),
(m.profit_prev_q||0)-(m.expense_prev_q||0), (m.profit_prev_month||0)-(m.expense_prev_month||0)
], [
(m.profit_q2||0)-(m.expense_q2||0), (m.profit_prev_q||0)-(m.expense_prev_q||0)
], [
(m.profit_month||0)-(m.expense_month||0), (m.profit_prev_month||0)-(m.expense_prev_month||0)
]]
];
var h = '';
var netCls = function(di, raw) { return di === 2 ? (raw >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-800'; };
for (var di = 0; di < 3; di++) {
var r = d[di];
h += '<tr class="border-b border-slate-100 last:border-0"><td class="py-2 text-left font-semibold text-slate-700">' + r[0] + '</td>' +
'<td class="py-2 text-right font-semibold ' + netCls(di, r[1][0]) + '">' + moneyInt(r[1][0]) + '</td>' +
'<td class="py-2 text-right font-semibold ' + netCls(di, r[1][3]) + '">' + moneyInt(r[1][3]) + '</td>' +
'<td class="py-2 text-right font-semibold ' + netCls(di, r[1][4]) + '">' + moneyInt(r[1][4]) + '</td>' +
'<td class="py-2 text-right font-semibold ' + netCls(di, r[1][1]) + '">' + moneyInt(r[1][1]) + '</td>' +
'<td class="py-2 text-right">' + qoq(r[2][0], r[2][1]) + '</td>' +
'<td class="py-2 text-right font-semibold ' + netCls(di, r[1][2]) + '">' + moneyInt(r[1][2]) + '</td>' +
'<td class="py-2 text-right">' + qoq(r[3][0], r[3][1]) + '</td></tr>';
}
return h;
})()}
</tbody></table></div>`, "p-4")}
${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-4 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")}
${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="chartCash"></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="chartProfit"></canvas></div>`, "p-4")}
</div>
${card(`<h2 class="text-lg font-bold">近期动态</h2><div class="mt-4 grid gap-2">${summary.recent.map((r) => `<div class="flex items-start justify-between rounded-md bg-slate-50 px-3 py-2 text-sm group"><span class="break-words">${r.content}</span><div class="flex items-center gap-2 flex-shrink-0 ml-2"><span class="text-xs text-slate-400">${r.followed_at}</span><button class="btn btn-ghost btn-sm text-red-400 opacity-0 group-hover:opacity-100 p-0 w-5 h-5" onclick="event.preventDefault();deleteActivity(${r.id})" title="删除动态"><i data-lucide="trash-2" style="width:14px;height:14px"></i></button></div></div>`).join("")}</div>`, "p-5")}
</div>
`;
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: "line",
data: { labels, datasets: [
{ label: "回款", data: data.map((x) => x.payment || 0), borderColor: "#d97706", backgroundColor: "rgba(217,119,6,0.06)", fill: true, tension: 0.3 },
{ label: "已付", data: data.map((x) => x.cost || 0), borderColor: "#7c3aed", backgroundColor: "rgba(124,58,237,0.06)", fill: true, tension: 0.3 },
]},
options: baseOpts,
});
}
// 图4月度项目利润
const c4 = document.querySelector("#chartProfit");
if (c4 && window.Chart) {
if (state.chart4) state.chart4.destroy();
state.chart4 = new Chart(c4, {
type: "line",
data: { labels, datasets: [
{ label: "利润", data: data.map((x) => (x.gross || 0)), borderColor: "#6366f1", backgroundColor: "rgba(99,102,241,0.06)", fill: true, tension: 0.3 },
]},
options: baseOpts,
});
}
}