feat: 汇总卡片精简+表格列调整+总览改名+颜色规则

- 计划+实际模块: 汇总卡片移除项目利润和执行率
- 计划+实际模块: 表格现金流移到最后一列
- 计划+实际模块: 月度总览改名为经营月报, 季度总览改名为季度月报
- 实际模块: 已付全部改名为支出
- 月度总览: 毛利统一绿色, 利润/现金流条件着色
- 版本号 v1.0.0.34
This commit is contained in:
mac
2026-07-08 18:55:27 +08:00
parent 1e82ca73d9
commit e1b549b3f9
3 changed files with 36 additions and 35 deletions

View File

@@ -111,7 +111,7 @@ function renderPlan() {
const finHeaderBase = `<div class="flex items-center gap-2"><button onclick="setPlanView('overview')" class="px-3 py-1 rounded text-sm font-medium ${state.planView==='overview'?'bg-blue-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-1 rounded text-sm font-medium ${state.planView==='quarterly'&&activeQ===i?'bg-blue-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 = `2026-${String(m).padStart(2,'0')}`; return `<button onclick="setPlanMonth('${ms}')" class="px-3 py-1 rounded text-sm font-medium ${state.planView==='monthly'&&state.planMonth===ms?'bg-blue-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 planFilterTabs = `<div class="finance-tabs" style="padding:0 0 0 0;border-bottom:1px solid #e2e8f0;margin-bottom:0;display:flex;gap:4px"><button onclick="switchPlanFilter('overview')" class="finance-tab${state.planFilter==='overview'?' active':''}" style="font-size:14px;font-weight:600;padding:8px 16px;margin-right:0">月度总览</button><button onclick="switchPlanFilter('quarterlyOverview')" class="finance-tab${state.planFilter==='quarterlyOverview'?' active':''}" style="font-size:14px;font-weight:600">季度总览</button><button onclick="switchPlanFilter('projects')" class="finance-tab${state.planFilter==='projects'?' active':''}" style="font-size:14px;font-weight:600">项目计划 (${pfs.length})</button><button onclick="switchPlanFilter('expense')" class="finance-tab${state.planFilter==='expense'?' active':''}" style="font-size:14px;font-weight:600">平台费用计划</button></div>`;
const planFilterTabs = `<div class="finance-tabs" style="padding:0 0 0 0;border-bottom:1px solid #e2e8f0;margin-bottom:0;display:flex;gap:4px"><button onclick="switchPlanFilter('overview')" class="finance-tab${state.planFilter==='overview'?' active':''}" style="font-size:14px;font-weight:600;padding:8px 16px;margin-right:0">经营月报</button><button onclick="switchPlanFilter('quarterlyOverview')" class="finance-tab${state.planFilter==='quarterlyOverview'?' active':''}" style="font-size:14px;font-weight:600">季度月报</button><button onclick="switchPlanFilter('projects')" class="finance-tab${state.planFilter==='projects'?' active':''}" style="font-size:14px;font-weight:600">项目计划 (${pfs.length})</button><button onclick="switchPlanFilter('expense')" class="finance-tab${state.planFilter==='expense'?' active':''}" style="font-size:14px;font-weight:600">平台费用计划</button></div>`;
document.querySelector("#plan").innerHTML = `<div class="grid gap-4">
${planFilterTabs}
@@ -254,11 +254,9 @@ function renderPlan() {
{ label: '已确收', hideUnsigned: true, value: ctx => moneyWan(ctx.sumRev), color: 'text-blue-700' },
{ label: '毛利', value: ctx => money(ctx.sumGross), color: 'text-green-700' },
{ label: '成本', value: ctx => moneyWan(ctx.sumCost), color: 'text-rose-700' },
{ label: '项目利润', value: ctx => { const v = ctx.sumGross; return { val: moneyWan(v), cls: v >= 0 ? 'text-green-600' : 'text-red-600' }; }, color: null },
{ label: '已回款', hideUnsigned: true, value: ctx => moneyWan(ctx.sumPay), color: 'text-amber-700' },
{ label: '已付', hideUnsigned: true, value: ctx => moneyWan(ctx.sumPaid), color: 'text-purple-700' },
{ label: '现金流', hideUnsigned: true, value: ctx => { const v = ctx.sumPay - ctx.sumPaid; return { val: money(v), cls: v >= 0 ? 'text-green-600' : 'text-red-600' }; }, color: null },
{ label: '执行率', hideUnsigned: true, value: ctx => ctx.sumRev && ctx.signTotal ? Math.round(ctx.sumRev / ctx.signTotal * 100) + '%' : '—', color: 'text-slate-500' },
{ label: '毛利率', hideUnsigned: true, value: ctx => ctx.sumRev && ctx.sumGross ? Math.round(ctx.sumGross / ctx.sumRev * 100) + '%' : '—', color: 'text-slate-500' },
{ label: '回款率', hideUnsigned: true, value: ctx => ctx.sumRev && ctx.sumPay ? Math.round(ctx.sumPay / ctx.sumRev * 100) + '%' : '—', color: 'text-slate-500' },
{ label: '付款率', hideUnsigned: true, value: ctx => ctx.sumCost && ctx.sumPaid ? Math.round(ctx.sumPaid / ctx.sumCost * 100) + '%' : '—', color: 'text-slate-500' },
@@ -298,11 +296,11 @@ function renderPlan() {
{ key: 'gross', label: '毛利' },
{ key: 'gross_rate', label: '毛利率' },
{ key: 'payment', label: '回款' },
{ key: 'cashflow', label: '现金流' },
{ key: 'pay_rate', label: '回款率' },
{ key: 'cost', label: '成本' },
{ key: 'paid', label: '付款' },
{ key: 'paid_rate', label: '付款率' },
{ key: 'cashflow', label: '现金流' },
];
const UNSIGNED_COLS = [
{ key: 'client_name', label: '客户' },
@@ -344,7 +342,7 @@ function renderPlan() {
const grossR = rev && gross ? Math.round(gross / rev * 100) + '%' : '<span class="text-slate-300">—</span>';
const cf = cashflow ? money(cashflow) : '<span class="text-slate-300">—</span>';
const cfCls = cashflow >= 0 ? 'text-green-600' : 'text-red-600';
return `<tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="planOpenPfEditModal(${pf.id})"><td class="p-2 text-center align-middle text-sm">${esc(pf.client_name || "")}</td><td class="p-2 text-sm font-medium text-center align-middle truncate" style="min-width:300px;max-width:300px;width:300px" title="${esc(pf.customer_name)}">${esc(pf.customer_name)}</td><td class="p-2 text-center align-middle text-sm" onclick="event.stopPropagation()">${planInlineSelect('weight', pf.id, 'weight', pf.weight || '20%', ['20%','40%','60%','80%','100%'])}</td><td class="p-2 text-center align-middle text-sm" onclick="event.stopPropagation()">${planInlineSelect('ptype', pf.id, 'project_type', pf.project_type || '待签约', ['待签约','已签约'])}</td><td class="p-2 text-center align-middle text-sm font-medium">${money(pf.sign_amount)}</td><td class="p-2 text-center text-blue-700 align-middle">${fmtNoUnit(rev)}</td><td class="p-2 text-center align-middle text-sm font-medium">${fmtNoUnit(gross)}</td><td class="p-2 text-center align-middle text-sm">${grossR}</td><td class="p-2 text-center text-amber-700 align-middle">${fmtNoUnit(payment)}</td><td class="p-2 text-center font-semibold align-middle ${cfCls}">${cf}</td><td class="p-2 text-center align-middle text-sm">${payR}</td><td class="p-2 text-center text-rose-700 align-middle">${fmtNoUnit(cost)}</td><td class="p-2 text-center text-purple-700 align-middle">${fmtNoUnit(paid)}</td><td class="p-2 text-center align-middle text-sm">${paidR}</td></tr>`;
return `<tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="planOpenPfEditModal(${pf.id})"><td class="p-2 text-center align-middle text-sm">${esc(pf.client_name || "")}</td><td class="p-2 text-sm font-medium text-center align-middle truncate" style="min-width:300px;max-width:300px;width:300px" title="${esc(pf.customer_name)}">${esc(pf.customer_name)}</td><td class="p-2 text-center align-middle text-sm" onclick="event.stopPropagation()">${planInlineSelect('weight', pf.id, 'weight', pf.weight || '20%', ['20%','40%','60%','80%','100%'])}</td><td class="p-2 text-center align-middle text-sm" onclick="event.stopPropagation()">${planInlineSelect('ptype', pf.id, 'project_type', pf.project_type || '待签约', ['待签约','已签约'])}</td><td class="p-2 text-center align-middle text-sm font-medium">${money(pf.sign_amount)}</td><td class="p-2 text-center text-blue-700 align-middle">${fmtNoUnit(rev)}</td><td class="p-2 text-center align-middle text-sm font-medium">${fmtNoUnit(gross)}</td><td class="p-2 text-center align-middle text-sm">${grossR}</td><td class="p-2 text-center text-amber-700 align-middle">${fmtNoUnit(payment)}</td><td class="p-2 text-center align-middle text-sm">${payR}</td><td class="p-2 text-center text-rose-700 align-middle">${fmtNoUnit(cost)}</td><td class="p-2 text-center text-purple-700 align-middle">${fmtNoUnit(paid)}</td><td class="p-2 text-center align-middle text-sm">${paidR}</td><td class="p-2 text-center font-semibold align-middle ${cfCls}">${cf}</td></tr>`;
};
// 待签约简版行
const tdRowUnsigned = (pf, cost, gross) => {
@@ -1010,7 +1008,7 @@ function renderPlanOverview() {
annualSums.payment += fm[i].payment; annualSums.paid += fm[i].paid;
}
const mLbl = ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月'];
const LABELS = ['合同金额','确收金额','确收毛利','成本','已付','回款','项目现金流'];
const LABELS = ['合同金额','确收金额','确收毛利','成本','支出','回款','项目现金流'];
// 每行的年度累计 + 12个月值
const annualVals = [
annualSums.sign, annualSums.revenue, annualSums.gross, annualSums.cost, annualSums.paid,
@@ -1032,6 +1030,7 @@ function renderPlanOverview() {
else { var fk = fmFields[ri]; if (fk) val = fm[mi][fk] || 0; }
}
var cls = 'text-slate-700';
if (ri === 2) cls = 'text-green-600';
if (ri === 6) cls = val >= 0 ? 'text-green-600' : 'text-red-600';
rowHtml += '<td class="py-2 text-right text-sm whitespace-nowrap ' + cls + '">' + moneyIntL(val) + '</td>';
}
@@ -1060,11 +1059,12 @@ function renderPlanOverview() {
for (var dri = 0; dri < 3; dri++) {
var dr = deptRows[dri];
var rowCls = dri === 2 ? 'font-semibold' : '';
var valCls = dri === 2 ? 'text-green-600' : 'text-slate-700';
var rowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-2 text-left ' + rowCls + ' text-slate-700 whitespace-nowrap">' + dr.label + '</td>';
rowHtml += '<td class="py-2 text-right ' + rowCls + ' ' + valCls + ' whitespace-nowrap">' + moneyIntL(dr.annual) + '</td>';
var annualCls = dri === 2 ? (dr.annual >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-700';
rowHtml += '<td class="py-2 text-right ' + rowCls + ' ' + annualCls + ' whitespace-nowrap">' + moneyIntL(dr.annual) + '</td>';
for (var dmi = 0; dmi < 12; dmi++) {
rowHtml += '<td class="py-2 text-right text-sm whitespace-nowrap ' + valCls + '">' + moneyIntL(dr.vals[dmi]) + '</td>';
var cellCls = dri === 2 ? (dr.vals[dmi] >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-700';
rowHtml += '<td class="py-2 text-right text-sm whitespace-nowrap ' + cellCls + '">' + moneyIntL(dr.vals[dmi]) + '</td>';
}
deptTbody += rowHtml + '</tr>';
}
@@ -1128,7 +1128,7 @@ function renderPlanQuarterlyOverview() {
const fm = computePlanMonthly(planFinances);
const moneyIntL = (v) => Math.round(Number(v || 0)).toLocaleString("zh-CN");
const card = (body, cls) => '<div class="card ' + (cls || 'p-4') + '">' + body + '</div>';
const LABELS = ['合同金额','确收金额','确收毛利','成本','已付'];
const LABELS = ['合同金额','确收金额','确收毛利','成本','支出'];
const qLbl = ['Q1','Q2','Q3','Q4'];
const qRanges = [[0,1,2],[3,4,5],[6,7,8],[9,10,11]];
var annualSums2 = { sign: 0, revenue: 0, gross: 0, cost: 0, payment: 0, paid: 0 };