feat: 版本号自动同步 + 五色标记 + 财务指标颜色优化

- 左上角版本号从 git tag 自动读取(flask_app.py → Jinja2 注入)
- 项目管理右键菜单新增标黄/蓝/绿/紫四种颜色标记 + 取消标记
- 应收/应付列正数绿负数红(finance+plan 双模块)
- 经营月报利润行负数标红
- 预算模块移除状态筛选标签,添加年份选择器
- 年份下拉框 pr-6 修复箭头重叠
This commit is contained in:
mac
2026-07-15 10:03:12 +08:00
parent d2d1b0f99e
commit 39513c0a7b
4 changed files with 77 additions and 37 deletions

View File

@@ -103,15 +103,15 @@ function renderPlan() {
const defaultMonth2 = now2.getFullYear() + "-" + String(now2.getMonth()+1).padStart(2,"0");
if (!state.planMonth) state.planMonth = defaultMonth2;
if (state.planQuarter === undefined) state.planQuarter = Math.floor(now2.getMonth() / 3);
if (!state.planYear) state.planYear = now2.getFullYear();
if (!state.planView) state.planView = 'annual';
if (state.planYear == null) state.planYear = now2.getFullYear();
if (!['annual','quarterly','monthly'].includes(state.planView)) state.planView = 'annual';
const qLabels = ['Q1','Q2','Q3','Q4'];
const qMonths = [[1,2,3],[4,5,6],[7,8,9],[10,11,12]];
const nowD = new Date();
const activeQ = state.planQuarter !== undefined ? state.planQuarter : Math.floor(nowD.getMonth() / 3);
const activeMonths = qMonths[activeQ];
const yearOpts = [2024,2025,2026,2027];
const finHeaderBase = `<div class="flex items-center gap-2"><button onclick="setPlanStatusFilter('all')" class="px-3 py-0.5 rounded text-sm font-medium ${state.planStatusFilter==='all'?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">全部</button><button onclick="setPlanStatusFilter('unsigned')" class="px-3 py-0.5 rounded text-sm font-medium ${state.planStatusFilter==='unsigned'?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">待签约</button><button onclick="setPlanStatusFilter('signed')" class="px-3 py-0.5 rounded text-sm font-medium ${state.planStatusFilter==='signed'?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">已签约</button><button onclick="setPlanStatusFilter('paymentonly')" class="px-3 py-0.5 rounded text-sm font-medium ${state.planStatusFilter==='paymentonly'?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">只付款</button><span class="text-slate-300 mx-2">|</span><span class="text-sm text-slate-500">年份</span><select onchange="setPlanYear(this.value)" class="px-2 py-0.5 rounded text-sm font-medium border border-slate-200 bg-white">${yearOpts.map(y => `<option value="${y}" ${state.planYear===y?'selected':''}>${y}</option>`).join('')}</select><span class="text-slate-300 mx-2">|</span><button onclick="setPlanView('annual')" class="px-3 py-0.5 rounded text-sm font-medium ${state.planView==='annual'?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">全年</button><span class="text-slate-300 mx-2">|</span><span class="text-sm text-slate-500">季度:</span>${qLabels.map((q,i) => `<button onclick="setPlanQuarter(${i})" class="px-3 py-0.5 rounded text-sm font-medium ${state.planView==='quarterly'&&activeQ===i?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">${q}</button>`).join('')}<span class="text-slate-300 mx-2">|</span><span class="text-sm text-slate-500">月度:</span>${activeMonths.map(m => { const ms = `${state.planYear}-${String(m).padStart(2,'0')}`; return `<button onclick="setPlanMonth('${ms}')" class="px-3 py-0.5 rounded text-sm font-medium ${state.planView==='monthly'&&state.planMonth===ms?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">${m}月</button>`; }).join('')}</div>`;
const yearOpts = [2020,2021,2022,2023,2024,2025,2026,2027];
const finHeaderBase = `<div class="flex items-center gap-2"><span class="text-sm text-slate-500">年份:</span><select onchange="setPlanYear(this.value)" class="px-2 py-0.5 pr-6 rounded text-sm font-medium border border-slate-200 bg-white"><option value="0"${state.planYear===0?' selected':''}>所有年份</option>${yearOpts.map(y => `<option value="${y}" ${state.planYear===y?'selected':''}>${y}</option>`).join('')}</select><span class="text-slate-300 mx-2">|</span><button onclick="setPlanView('annual')" class="px-3 py-0.5 rounded text-sm font-medium ${state.planView==='annual'?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">全年</button><span class="text-slate-300 mx-2">|</span><span class="text-sm text-slate-500">季度:</span>${qLabels.map((q,i) => `<button onclick="setPlanQuarter(${i})" class="px-3 py-0.5 rounded text-sm font-medium ${state.planView==='quarterly'&&activeQ===i?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">${q}</button>`).join('')}<span class="text-slate-300 mx-2">|</span><span class="text-sm text-slate-500">月度:</span>${activeMonths.map(m => { const ms = `${state.planYear}-${String(m).padStart(2,'0')}`; return `<button onclick="setPlanMonth('${ms}')" class="px-3 py-0.5 rounded text-sm font-medium ${state.planView==='monthly'&&state.planMonth===ms?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">${m}月</button>`; }).join('')}</div>`;
const finAddBtn = `<button class="btn btn-primary btn-sm" onclick="openPlanModal()">新增财务项目</button>`;
const isOverviewMode = false;
@@ -369,10 +369,10 @@ function renderPlan() {
const cf = cashflow ? money(cashflow) : '<span class="text-slate-300">—</span>';
const cfCls = cashflow >= 0 ? 'text-green-600' : 'text-red-600';
const payable = cost - paid;
const payableCls = payable > 0 ? 'text-rose-600' : payable < 0 ? 'text-green-600' : 'text-slate-400';
const payableCls = payable > 0 ? 'text-green-600' : payable < 0 ? 'text-red-600' : 'text-slate-400';
const payableVal = payable ? (payable > 0 ? '+' : '') + money(payable) : '<span class="text-slate-300">—</span>';
const pending = rev - payment;
const pendingCls = pending > 0 ? 'text-red-600' : pending < 0 ? 'text-green-600' : 'text-slate-700';
const pendingCls = pending > 0 ? 'text-green-600' : pending < 0 ? 'text-red-600' : 'text-slate-700';
const pendingVal = pending ? (pending > 0 ? '+' : '') + money(pending) : '<span class="text-slate-300">—</span>';
var budgetArr = [];
var expenseArr = [];
@@ -463,12 +463,12 @@ function renderPlan() {
const qRange = qRanges[selQ];
const sumBudget = (pf, field) => {
let total = 0;
try { JSON.parse(pf.budget_data || "[]").forEach(b => { const m = parseInt((b.month || "").substring(5)) || 0; const y = parseInt((b.month || "").substring(0, 4)) || 0; if (qRange.includes(m) && y === state.planYear) total += parseFloat(b[field] || 0); }); } catch (e) {}
try { JSON.parse(pf.budget_data || "[]").forEach(b => { const m = parseInt((b.month || "").substring(5)) || 0; const y = parseInt((b.month || "").substring(0, 4)) || 0; if (qRange.includes(m) && (y === state.planYear || state.planYear === 0)) total += parseFloat(b[field] || 0); }); } catch (e) {}
return total;
};
const sumExpense = (pf, field) => {
let total = 0;
try { JSON.parse(pf.expense_data || "[]").forEach(e => { const m = parseInt((e.month || "").substring(5)) || 0; const y = parseInt((e.month || "").substring(0, 4)) || 0; if (qRange.includes(m) && y === state.planYear) total += parseFloat(e[field] || 0); }); } catch (e) {}
try { JSON.parse(pf.expense_data || "[]").forEach(e => { const m = parseInt((e.month || "").substring(5)) || 0; const y = parseInt((e.month || "").substring(0, 4)) || 0; if (qRange.includes(m) && (y === state.planYear || state.planYear === 0)) total += parseFloat(e[field] || 0); }); } catch (e) {}
return total;
};
let dataItems = [];
@@ -1113,6 +1113,8 @@ function renderPlanOverview() {
for (var ei = 0; ei < planExp.length; ei++) {
var em = (planExp[ei].expense_month || '').substring(0, 7);
var mi = parseInt(em.substring(5)) - 1;
var emYear = parseInt(em.substring(0, 4)) || 0;
if (state.planYear !== 0 && emYear && emYear !== state.planYear) continue;
if (mi >= 0 && mi < 12) expMonthly[mi] += parseFloat(planExp[ei].amount || 0);
}
var expAnnual = 0; for (var ei = 0; ei < 12; ei++) expAnnual += expMonthly[ei];
@@ -1149,6 +1151,8 @@ function renderPlanOverview() {
for (var epi = 0; epi < planExp.length; epi++) {
var epm = (planExp[epi].expense_month || '').substring(0, 7);
var epmi = parseInt(epm.substring(5)) - 1;
var epmYear = parseInt(epm.substring(0, 4)) || 0;
if (state.planYear !== 0 && epmYear && epmYear !== state.planYear) continue;
if (epmi >= 0 && epmi < 12) expPaidMonthly[epmi] += parseFloat(planExp[epi].incurred_amount || 0);
}
var expPaidAnnual = 0; for (var epi = 0; epi < 12; epi++) expPaidAnnual += expPaidMonthly[epi];
@@ -1175,14 +1179,15 @@ function renderPlanOverview() {
}
var cfCard = card('<h3 class="text-base font-bold text-slate-700 mb-3">部门现金流 <span class="text-sm text-slate-500 font-normal ml-3">部门现金流=所有项目产生的现金流 - 部门一级因费用产生的现金流出或流入</span></h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + cfTbody + '</tbody></table></div>', 'p-4');
document.querySelector("#planOverview").innerHTML = '<div class="grid gap-2.5">' +
const yearSelectOv = `<div class="card px-4 py-2 mb-4"><div class="flex items-center gap-2"><span class="text-sm font-medium text-slate-500">📅 选择年份:</span><select onchange="setPlanYear(this.value)" class="px-2 py-0.5 pr-6 rounded text-sm font-medium border border-slate-200 bg-white"><option value="0"${state.planYear===0?' selected':''}>所有年份</option>${[2020,2021,2022,2023,2024,2025,2026,2027].map(y => '<option value="'+y+'"'+(state.planYear===y?' selected':'')+'>'+y+'年</option>').join('')}</select></div></div>`;
document.querySelector("#planOverview").innerHTML = yearSelectOv + '<div class="grid gap-2.5">' +
deptCard +
cfCard +
card('<h3 class="text-base font-bold text-slate-700 mb-3">项目经营详情 <span class="text-sm text-slate-500 font-normal ml-3">所有项目产生的签单,确收,毛利,回款,成本,支出,现金流</span></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-3 gap-2.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-600">' + state.planYear + '</span></div><div style="position:relative;height:200px"><canvas id="planChartSign"></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-600">' + state.planYear + '</span></div><div style="position:relative;height:200px"><canvas id="planChartRev"></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-600">' + state.planYear + '</span></div><div style="position:relative;height:200px"><canvas id="planChartCash"></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-600">' + (state.planYear===0 ? '所有年份' : state.planYear) + '</span></div><div style="position:relative;height:200px"><canvas id="planChartSign"></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-600">' + (state.planYear===0 ? '所有年份' : state.planYear) + '</span></div><div style="position:relative;height:200px"><canvas id="planChartRev"></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-600">' + (state.planYear===0 ? '所有年份' : state.planYear) + '</span></div><div style="position:relative;height:200px"><canvas id="planChartCash"></canvas></div>', 'p-4') +
'</div></div>';
if (window.lucide) window.lucide.createIcons();
@@ -1312,7 +1317,8 @@ function renderPlanQuarterlyOverview() {
cfTbody += rowHtml + '</tr>';
}
document.querySelector("#planQuarterlyOverview").innerHTML = '<div class="grid gap-2.5">' +
const yearSelectQ = `<div class="card px-4 py-2 mb-4"><div class="flex items-center gap-2"><span class="text-sm font-medium text-slate-500">📅 选择年份:</span><select onchange="setPlanYear(this.value)" class="px-2 py-0.5 pr-6 rounded text-sm font-medium border border-slate-200 bg-white"><option value="0"${state.planYear===0?' selected':''}>所有年份</option>${[2020,2021,2022,2023,2024,2025,2026,2027].map(y => '<option value="'+y+'"'+(state.planYear===y?' selected':'')+'>'+y+'年</option>').join('')}</select></div></div>`;
document.querySelector("#planQuarterlyOverview").innerHTML = yearSelectQ + '<div class="grid gap-2.5">' +
card('<h3 class="text-base font-bold text-slate-700 mb-3">部门损益情况 <span class="text-sm text-slate-500 font-normal ml-3">部门利润 = 所有项目产生的毛利 - 部门 1 级产生的人力成本,研发成本,差旅和其他支出</span></h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + deptTbody + '</tbody></table></div>', 'p-4') +
card('<h3 class="text-base font-bold text-slate-700 mb-3">部门现金流 <span class="text-sm text-slate-500 font-normal ml-3">部门现金流=所有项目产生的现金流 - 部门一级因费用产生的现金流出或流入</span></h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + cfTbody + '</tbody></table></div>', 'p-4') +
card('<h3 class="text-base font-bold text-slate-700 mb-3">项目经营详情 <span class="text-sm text-slate-500 font-normal ml-3">所有项目产生的签单,确收,毛利,回款,成本,支出,现金流</span></h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + thead + '</thead><tbody>' + tbody + '</tbody></table></div>', 'p-4') +