## 安全与性能 - .env 环境变量、debug=False、except 改 mysql.connector.Error+logging - attach_common 批量 IN 查询消除 N+1 - 批量 esc() XSS 转义 ## 架构 - app.js 拆分为 7 模块 + admin.js - .form-ctrl 统一表单控件 ## 经营管理 - 字段改名:客户名称→项目名称、销售人员→商务负责人 - 必填:项目名称/商务负责人/经营负责人/签约月份/签约金额>0 - 视图切换:确收/毛利 ↔ 回款/费用 ## 重点工作与台账 - 统计卡片样式与经营管理统一 - 任务状态简化 3 态 - 优先级点击切换、右键菜单(重命名/副本) - 修复新建任务绑定错误项目 bug ## 用户体系 - 新增工作台:MCN·无界、无界·无界 - 新增账号:mcn/wuji - 账号管理后台(admin 限定) - sidebar 顶部头像+显示名,点击弹菜单 - sidebar sticky 定位 ## 其他 - 登录页样式优化(参考 UOC 平台) - 首页财务趋势拆 3 图 - 业务方案标准资料库双 Tab
104 lines
5.6 KiB
JavaScript
104 lines
5.6 KiB
JavaScript
// home.js — 首页渲染 + 财务趋势图
|
||
|
||
function renderHome() {
|
||
const { summary, financeMonthly } = state.data;
|
||
const m = summary.metrics;
|
||
const rows1 = [
|
||
["年度累计签约", money(m.signed_annual || m.signed_amount)],
|
||
["Q2 累计签约", money(m.signed_q2 || 0)],
|
||
["本月新增签约", money(m.signed_month || 0)],
|
||
];
|
||
const rows2 = [
|
||
["年度累计确收", money(m.revenue_annual)],
|
||
["Q2 累计确收", money(m.revenue_q2)],
|
||
["本月新增确收", money(m.monthly_revenue)],
|
||
];
|
||
const rows3 = [
|
||
["年度累计毛利", money(m.gross_annual)],
|
||
["Q2 累计毛利", money(m.gross_q2)],
|
||
["本月新增毛利", money(m.monthly_net_profit)],
|
||
];
|
||
const tblCard = (title, rows) => card(`<h3 class="text-sm font-bold text-slate-700 mb-3">${title}</h3><table class="w-full text-sm"><tbody>${rows.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("")}</tbody></table>`, "p-4");
|
||
document.querySelector("#home").innerHTML = `
|
||
<div class="grid gap-5">
|
||
<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-3 gap-5">${tblCard("合同金额", rows1)}${tblCard("确收金额", rows2)}${tblCard("确收毛利", rows3)}</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="x" 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.net_profit || 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,
|
||
});
|
||
}
|
||
}
|