Files
opc-manager/static/modules/home.js
mac 8a78c756b7 refactor: 项目模块重构 — 统一渲染 + 配置驱动 + 交互优化
核心重构:
- 提取 renderView() / tdRow() 统一函数,消除三视图 16000+ 字符重复代码
- METRIC_CARDS 配置驱动,调整卡片顺序/单位只需改配置
- 筛选区统一为下拉框(视图 + 状态 + 日期),删除筛选:前缀
- 日历选择器移至顶部工具栏,卡片内不再重复

功能优化:
- 项目状态简化为已签约/待签约
- 新增签约项目总数卡片(月度/季度按签约月份过滤)
- 签约金额/已确收/已回款/应付/已付改为万元单位
- 项目名称固定300px + 省略号 + title悬停
- body min-width 1400px + overflow-x:auto
- 首页费用金额→应付金额

代码清理:
- 删除 monthOpts/quarterOpts/qLabels 等死代码
- 删除 purchase.js(采购→费用模块已另表迁移)
- 财务→项目(侧边栏),采购→费用(全域重命名)
2026-07-06 15:39:39 +08:00

142 lines
7.6 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)],
];
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]) => `<tr class="border-b border-slate-100 last:border-0"><td class="py-2 pr-4 text-slate-500">${label}</td><td class="py-2 text-right font-semibold text-slate-800">${value}</td></tr>`).join("");
return card(`<h3 class="text-sm font-bold text-slate-700 mb-3">${title}</h3><table class="w-full text-sm"><tbody>${rowHtml(visible)}</tbody></table><table class="w-full text-sm hidden" id="${id}"><tbody>${rowHtml(extra)}</tbody></table><button class="w-full text-center py-1 text-xs text-slate-400 hover:text-slate-600" onclick="toggleFinCard('${id}')" id="${id}_btn"><i data-lucide="chevron-down" style="width:14px;height:14px;display:inline"></i></button>`, "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")
? '<i data-lucide="chevron-down" style="width:14px;height:14px;display:inline"></i>'
: '<i data-lucide="chevron-up" style="width:14px;height:14px;display:inline"></i>';
if (window.lucide) window.lucide.createIcons();
};
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>`}
<div class="grid grid-cols-5 gap-5">${tblCard("合同金额", rows1)}${tblCard("确收金额", rows2)}${tblCard("确收毛利", rows3)}${tblCard("回款金额", rows4)}${tblCard("应付金额", rows5)}</div>
<div class="grid grid-cols-3 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")}
</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: "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,
});
}
}