@@ -238,6 +238,10 @@ function renderFinance() {
setTimeout(() => renderFinanceOverview(), 10);
return `
`;
}
+ if (state.finFilter === 'quarterlyOverview') {
+ setTimeout(() => renderFinanceQuarterlyOverview(), 10);
+ return `
`;
+ }
if (state.finFilter === 'expense') {
setTimeout(() => renderExpense(), 10);
return `
`;
@@ -929,55 +933,43 @@ window.deleteFinanceItem = async () => {
}
}
-// 总览渲染
+// 月度总览渲染
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 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 fm = financeMonthly || [];
+ const mLbl = ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月'];
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 = '
| 指标 | 年度累计 | 上季度累计 | 上月累计 | 本季度累计 | 季环比 | 本月累计 | 月环比 |
|---|
';
+ const annualVals = [m.signed_annual||m.signed_amount||0, m.revenue_annual||0, m.gross_annual||0, m.cost_annual||0, m.profit_annual||0, m.payment_annual||0, m.paid_annual||0, m.cashflow_annual||0];
+ const fmFields = ['sign','revenue','gross','cost',null,'payment','paid',null];
+ var thead = '
| 指标 | 年度累计 | ';
+ for (var mi = 0; mi < 12; mi++) { thead += '' + mLbl[mi] + ' | '; }
+ 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]) + ' |
';
+ var rowHtml = '
| ' + LABELS[ri] + ' | ';
+ rowHtml += '' + moneyIntL(annualVals[ri]) + ' | ';
+ for (var mi = 0; mi < 12; mi++) {
+ var val = 0;
+ if (fm[mi]) {
+ if (ri === 4) { val = (fm[mi].gross || 0) - (fm[mi].cost || 0); }
+ else if (ri === 7) { val = (fm[mi].payment || 0) - (fm[mi].paid || 0); }
+ else { var fk = fmFields[ri]; if (fk) val = fm[mi][fk] || 0; }
+ }
+ var cls = 'text-slate-700';
+ if (ri === 4) cls = val >= 0 ? 'text-green-600' : 'text-red-600';
+ if (ri === 7) cls = val >= 0 ? 'text-green-600' : 'text-red-600';
+ rowHtml += '' + moneyIntL(val) + ' | ';
+ }
+ tbody += rowHtml + '
';
}
document.querySelector("#financeOverview").innerHTML = '
' +
- card('
业务(项目)财务概览
合同 → 确收 → 毛利 → 成本 → 项目利润 → 回款 → 已付 → 现金流
' + thead + '' + tbody + '
', 'p-4') +
+ card('
业务(项目)财务月度概览
合同 → 确收 → 毛利 → 成本 → 项目利润 → 回款 → 已付 → 现金流
' + thead + '' + tbody + '
', 'p-4') +
'
' +
card('
月度签约趋势
2026', 'p-4') +
card('
月度确收与毛利
2026', 'p-4') +
@@ -986,12 +978,66 @@ function renderFinanceOverview() {
'
';
if (window.lucide) window.lucide.createIcons();
- // 渲染图表(复用 home.js 的 moneyTick / chartOptions / monthLabels)
if (financeMonthly && window.Chart) {
renderOverviewCharts(financeMonthly);
}
}
+// 季度总览渲染
+function renderFinanceQuarterlyOverview() {
+ const { summary, financeMonthly } = state.data;
+ if (!summary) return;
+ const m = summary.metrics;
+ const moneyIntL = (v) => Math.round(Number(v || 0)).toLocaleString("zh-CN");
+ const card = (body, cls) => '
' + body + '
';
+ const fm = financeMonthly || [];
+ const LABELS = ['合同金额','确收金额','确收毛利','成本','项目利润','回款金额','已付','现金流'];
+ const qLbl = ['Q1','Q2','Q3','Q4'];
+ const qRanges = [[0,1,2],[3,4,5],[6,7,8],[9,10,11]];
+ const annualVals = [m.signed_annual||m.signed_amount||0, m.revenue_annual||0, m.gross_annual||0, m.cost_annual||0, m.profit_annual||0, m.payment_annual||0, m.paid_annual||0, m.cashflow_annual||0];
+ const fmFields = ['sign','revenue','gross','cost',null,'payment','paid',null];
+ var qData = [[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0]];
+ for (var qi = 0; qi < 4; qi++) {
+ for (var ri = 0; ri < 8; ri++) {
+ var sum = 0;
+ for (var mi = 0; mi < 3; mi++) {
+ var idx = qRanges[qi][mi];
+ if (fm[idx]) {
+ if (ri === 4) { sum += (fm[idx].gross||0) - (fm[idx].cost||0); }
+ else if (ri === 7) { sum += (fm[idx].payment||0) - (fm[idx].paid||0); }
+ else { var fk = fmFields[ri]; if (fk) sum += fm[idx][fk] || 0; }
+ }
+ }
+ qData[ri][qi] = sum;
+ }
+ }
+ var thead = '
| 指标 | 年度累计 | ';
+ thead += 'Q1 | ';
+ thead += 'Q2 | 环比 | ';
+ thead += 'Q3 | 环比 | ';
+ thead += 'Q4 | 环比 | ';
+ thead += '
|---|
';
+ var qoq = function(cur, prev) { if (!prev) return '
—'; var pct = Math.round((cur - prev) / prev * 100); var cls = pct >= 0 ? 'text-green-600' : 'text-red-600'; var sign = pct >= 0 ? '+' : ''; return '
' + sign + pct + '%'; };
+ var tbody = '';
+ for (var ri = 0; ri < 8; ri++) {
+ var rowHtml = '
| ' + LABELS[ri] + ' | ';
+ rowHtml += '' + moneyIntL(annualVals[ri]) + ' | ';
+ for (var qi = 0; qi < 4; qi++) {
+ var val = qData[ri][qi];
+ var cls = 'text-slate-700';
+ if (ri === 4) cls = val >= 0 ? 'text-green-600' : 'text-red-600';
+ if (ri === 7) cls = val >= 0 ? 'text-green-600' : 'text-red-600';
+ rowHtml += '' + moneyIntL(val) + ' | ';
+ if (qi > 0) { rowHtml += '' + qoq(val, qData[ri][qi - 1]) + ' | '; }
+ }
+ tbody += rowHtml + '
';
+ }
+ document.querySelector("#financeQuarterlyOverview").innerHTML = '
' +
+ card('
业务(项目)财务季度概览
合同 → 确收 → 毛利 → 成本 → 项目利润 → 回款 → 已付 → 现金流
' + thead + '' + tbody + '
', 'p-4') +
+ '
';
+ if (window.lucide) window.lucide.createIcons();
+}
+
function renderOverviewCharts(data) {
if (typeof monthLabels !== 'function') return;
const labels = monthLabels(data);
diff --git a/static/modules/plan.js b/static/modules/plan.js
index 02437c4..ec64d5c 100644
--- a/static/modules/plan.js
+++ b/static/modules/plan.js
@@ -111,7 +111,7 @@ function renderPlan() {
const finHeaderBase = `
|按季度:${qLabels.map((q,i) => ``).join('')}|按月度:${activeMonths.map(m => { const ms = `2026-${String(m).padStart(2,'0')}`; return ``; }).join('')}
`;
const finAddBtn = `
`;
- const planFilterTabs = `
`;
+ const planFilterTabs = `
`;
document.querySelector("#plan").innerHTML = `
${planFilterTabs}
@@ -119,7 +119,7 @@ function renderPlan() {
计划提醒:计划也是预算,只有真正能够列出计划的预算,才有靠谱的落地可能性
- ${state.planFilter !== 'expense' && state.planFilter !== 'overview' ? card(`
${finHeaderBase}
${finAddBtn}
`, "p-4") : ''}
+ ${state.planFilter !== 'expense' && state.planFilter !== 'overview' && state.planFilter !== 'quarterlyOverview' ? card(`
${finHeaderBase}
${finAddBtn}
`, "p-4") : ''}
@@ -240,6 +240,10 @@ function renderPlan() {
setTimeout(() => renderPlanOverview(), 10);
return `
`;
}
+ if (state.planFilter === 'quarterlyOverview') {
+ setTimeout(() => renderPlanQuarterlyOverview(), 10);
+ return `
`;
+ }
if (state.planFilter === 'expense') {
setTimeout(() => renderPlanExpense(), 10);
return `
`;
@@ -1028,6 +1032,69 @@ function renderPlanOverview() {
}
}
+
+function renderPlanQuarterlyOverview() {
+ const { summary, financeMonthly } = state.data;
+ if (!summary) return;
+ const m = summary.metrics;
+ const moneyIntL = (v) => Math.round(Number(v || 0)).toLocaleString("zh-CN");
+ const card = (body, cls) => '
' + body + '
';
+ const fm = financeMonthly || [];
+ const LABELS = ['合同金额','确收金额','确收毛利','成本','项目利润','回款金额','已付','现金流'];
+ const qLbl = ['Q1','Q2','Q3','Q4'];
+ const qRanges = [[0,1,2],[3,4,5],[6,7,8],[9,10,11]];
+ const annualVals = [m.signed_annual||m.signed_amount||0, m.revenue_annual||0, m.gross_annual||0, m.cost_annual||0, m.profit_annual||0, m.payment_annual||0, m.paid_annual||0, m.cashflow_annual||0];
+ const fmFields = ['sign','revenue','gross','cost',null,'payment','paid',null];
+
+ // Calculate quarterly sums
+ var qData = [[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0]];
+ for (var qi = 0; qi < 4; qi++) {
+ for (var ri = 0; ri < 8; ri++) {
+ var sum = 0;
+ for (var mi = 0; mi < 3; mi++) {
+ var idx = qRanges[qi][mi];
+ if (fm[idx]) {
+ if (ri === 4) { sum += (fm[idx].gross||0) - (fm[idx].cost||0); }
+ else if (ri === 7) { sum += (fm[idx].payment||0) - (fm[idx].paid||0); }
+ else { var fk = fmFields[ri]; if (fk) sum += fm[idx][fk] || 0; }
+ }
+ }
+ qData[ri][qi] = sum;
+ }
+ }
+
+ var thead = '
| 指标 | 年度累计 | ';
+ thead += 'Q1 | ';
+ thead += 'Q2 | 环比 | ';
+ thead += 'Q3 | 环比 | ';
+ thead += 'Q4 | 环比 | ';
+ thead += '
|---|
';
+
+ var qoq = function(cur, prev) { if (!prev) return '
—'; var pct = Math.round((cur - prev) / prev * 100); var cls = pct >= 0 ? 'text-green-600' : 'text-red-600'; var sign = pct >= 0 ? '+' : ''; return '
' + sign + pct + '%'; };
+
+ var tbody = '';
+ for (var ri = 0; ri < 8; ri++) {
+ var rowHtml = '
| ' + LABELS[ri] + ' | ';
+ rowHtml += '' + moneyIntL(annualVals[ri]) + ' | ';
+ for (var qi = 0; qi < 4; qi++) {
+ var val = qData[ri][qi];
+ var cls = 'text-slate-700';
+ if (ri === 4) cls = val >= 0 ? 'text-green-600' : 'text-red-600';
+ if (ri === 7) cls = val >= 0 ? 'text-green-600' : 'text-red-600';
+ rowHtml += '' + moneyIntL(val) + ' | ';
+ if (qi > 0) {
+ rowHtml += '' + qoq(val, qData[ri][qi - 1]) + ' | ';
+ }
+ }
+ tbody += rowHtml + '
';
+ }
+
+ document.querySelector("#planQuarterlyOverview").innerHTML = '
' +
+ card('
业务(项目)财务季度概览
合同 → 确收 → 毛利 → 成本 → 项目利润 → 回款 → 已付 → 现金流
' + thead + '' + tbody + '
', 'p-4') +
+ '
';
+ if (window.lucide) window.lucide.createIcons();
+}
+
function renderPlanCharts(data) {
if (typeof monthLabels !== 'function') return;
const labels = monthLabels(data);
diff --git a/templates/index.html b/templates/index.html
index f3cb77a..81029ec 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -80,7 +80,7 @@
-
OPC Manager v1.0.0.26
+
OPC Manager v1.0.0.27