Compare commits
108 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2ed20caeb | ||
|
|
2340c69e26 | ||
|
|
a3f269046d | ||
|
|
983fd439d9 | ||
|
|
ec062fb2ec | ||
|
|
cae6bcf0e1 | ||
|
|
c4af452115 | ||
|
|
ce7152ab6a | ||
|
|
8c318fb3ad | ||
|
|
9c2beee51e | ||
|
|
6a582c65b8 | ||
|
|
3fff80425b | ||
|
|
02abe41793 | ||
|
|
f30d40fe70 | ||
|
|
eb300731a3 | ||
|
|
67bc00314c | ||
|
|
d34e52eff4 | ||
|
|
8e38f50c99 | ||
|
|
3a40e78c3e | ||
|
|
a1a4f41d79 | ||
|
|
d4889d56ec | ||
|
|
35706f123d | ||
|
|
e4d1cd59f3 | ||
|
|
1673603b02 | ||
|
|
db93eb8d31 | ||
|
|
94ad7aed55 | ||
|
|
043938670a | ||
|
|
9862d9fe56 | ||
|
|
150e627811 | ||
|
|
295bf5a81c | ||
|
|
e8a26059d5 | ||
|
|
543d620384 | ||
|
|
975034c589 | ||
|
|
71e90bd55f | ||
|
|
3042791a90 | ||
|
|
a8243acc5a | ||
|
|
cb0e3fdac2 | ||
|
|
f5c92d09af | ||
|
|
e18626105b | ||
|
|
b587530437 | ||
|
|
9a1260db68 | ||
|
|
7c33bc05f2 | ||
|
|
9de0a63be2 | ||
|
|
f3c94c57d6 | ||
|
|
9db627770b | ||
|
|
75b988c77a | ||
|
|
2b7f67436e | ||
|
|
ed5455f309 | ||
|
|
452d410843 | ||
|
|
c95373b85e | ||
|
|
6be10481d5 | ||
|
|
4ce0e66d90 | ||
|
|
0f9e1b6cf2 | ||
|
|
dc8d3d59d2 | ||
|
|
1e5c5b5706 | ||
|
|
b68144ebb0 | ||
|
|
3cae1d85cf | ||
|
|
ca0cd9eab1 | ||
|
|
2bdc0e0779 | ||
|
|
93dca56c3f | ||
|
|
8c4f50fc52 | ||
|
|
13864f1968 | ||
|
|
d11e8f9313 | ||
|
|
02a12a208c | ||
|
|
b977627895 | ||
|
|
edf8e6179c | ||
|
|
3148af05b3 | ||
|
|
4029933ada | ||
|
|
95d3f9809c | ||
|
|
8b040489e2 | ||
|
|
db9700c8ee | ||
|
|
11e30f33b9 | ||
|
|
95f52b61d0 | ||
|
|
a4012f2189 | ||
|
|
e1c5e65c1e | ||
|
|
a3d78b3ca0 | ||
|
|
9dc5631574 | ||
|
|
c5a59508d2 | ||
|
|
ab1d57492c | ||
|
|
9a81a30feb | ||
|
|
d6d21b03dd | ||
|
|
b9a05e8732 | ||
|
|
440707e19a | ||
|
|
e0dc1a56b2 | ||
|
|
f45afd954d | ||
|
|
664da22329 | ||
|
|
73a6ac126c | ||
|
|
878e26d4ca | ||
|
|
e12ed0936b | ||
|
|
8edb74f173 | ||
|
|
1a42c6e6f3 | ||
|
|
d21497bce3 | ||
|
|
574e3c5700 | ||
|
|
c70293b447 | ||
|
|
adeff08827 | ||
|
|
8bd40de41d | ||
|
|
ac6eacea82 | ||
|
|
83c2a5ca94 | ||
|
|
fbd2290d29 | ||
|
|
97fcf88c61 | ||
|
|
a01afef599 | ||
|
|
28fa244fe5 | ||
|
|
ad3885e0be | ||
|
|
0fb7ee2992 | ||
|
|
f6792cad39 | ||
|
|
bed6e9192a | ||
|
|
aaa213a765 | ||
|
|
207629a9bb |
@@ -53,3 +53,4 @@ curl http://127.0.0.1:5177/api/health
|
||||
- 产品:慰心斋产品路线图中的 5 个产品版本
|
||||
- 财务:首版财务样例和原财务 manager 合并方向
|
||||
|
||||
# test trigger
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// finance.js — 经营管理(财务)模块
|
||||
|
||||
const moneyInt = (v) => `${Math.round(Number(v || 0)).toLocaleString("zh-CN")}`;
|
||||
const moneyWan = (v) => `${(Number(v || 0) / 10000).toFixed(1)} 万`;
|
||||
const moneyWan = (v) => `${(Number(v || 0) / 10000).toFixed(0)} 万`;
|
||||
|
||||
function renderFinance() {
|
||||
const pfs = state.data.projectFinances || [];
|
||||
@@ -105,13 +105,15 @@ function renderFinance() {
|
||||
const defaultMonth2 = now2.getFullYear() + "-" + String(now2.getMonth()+1).padStart(2,"0");
|
||||
if (!state.finMonth) state.finMonth = defaultMonth2;
|
||||
if (state.finQuarter === undefined) state.finQuarter = Math.floor(now2.getMonth() / 3);
|
||||
if (!state.finYear) state.finYear = now2.getFullYear();
|
||||
if (!state.finView) state.finView = '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.finQuarter !== undefined ? state.finQuarter : 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="setFinView('overview')" class="px-3 py-0.5 rounded text-sm font-medium ${state.finView==='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><select onchange="setFinYear(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.finYear===y?'selected':''}>${y}</option>`).join('')}</select><span class="text-slate-300 mx-2">|</span><span class="text-sm text-slate-500">季度:</span>${qLabels.map((q,i) => `<button onclick="setFinQuarter(${i})" class="px-3 py-0.5 rounded text-sm font-medium ${state.finView==='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 = `${state.finYear}-${String(m).padStart(2,'0')}`; return `<button onclick="setFinMonth('${ms}')" class="px-3 py-0.5 rounded text-sm font-medium ${state.finView==='monthly'&&state.finMonth===ms?'bg-blue-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">${m}月</button>`; }).join('')}</div>`;
|
||||
const finHeaderBase = `<div class="flex items-center gap-2"><span class="text-sm text-slate-500">年份:</span><select onchange="setFinYear(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.finYear===y?'selected':''}>${y}</option>`).join('')}</select><span class="text-slate-300 mx-2">|</span><button onclick="setFinView('annual')" class="px-3 py-0.5 rounded text-sm font-medium ${state.finView==='annual'?'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="setFinQuarter(${i})" class="px-3 py-0.5 rounded text-sm font-medium ${state.finView==='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 = `${state.finYear}-${String(m).padStart(2,'0')}`; return `<button onclick="setFinMonth('${ms}')" class="px-3 py-0.5 rounded text-sm font-medium ${state.finView==='monthly'&&state.finMonth===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="openFinanceModal()">新增财务项目</button>`;
|
||||
|
||||
const isOverviewMode = false;
|
||||
@@ -124,7 +126,7 @@ function renderFinance() {
|
||||
</div>`}
|
||||
${finFilterTabs}
|
||||
${state.finFilter !== 'expense' && state.finFilter !== 'overview' && state.finFilter !== 'quarterlyOverview' ? '' : ''}
|
||||
<div id="financeModal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/40" onclick="closeFinanceModal()"><div class="bg-white rounded-2xl shadow-2xl w-full max-w-6xl mx-4 max-h-[92vh] overflow-y-auto" onclick="event.stopPropagation()"><div class="sticky top-0 z-10 bg-white/95 backdrop-blur border-b border-slate-100 px-8 py-5 flex items-center justify-between"><div><h3 class="text-xl font-bold text-slate-800" id="financeModalTitle">新增项目财务</h3><p class="text-xs text-slate-400 mt-0.5">填写项目财务信息与月度预算</p></div><div class="flex items-center gap-2"><button class="btn btn-ghost btn-sm text-red-600 hidden" id="financeDeleteBtn" onclick="deleteFinanceItem()"><i data-lucide="trash-2"></i>删除</button><button class="btn btn-ghost btn-sm rounded-full w-8 h-8 p-0" onclick="closeFinanceModal()"><i data-lucide="x"></i></button></div></div>
|
||||
<div id="financeModal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/40" onclick="closeFinanceModal()"><div class="bg-white rounded-2xl shadow-2xl w-full max-w-6xl mx-4 max-h-[92vh] overflow-y-auto" onclick="event.stopPropagation()"><div class="sticky top-0 z-10 bg-white/95 backdrop-blur border-b border-slate-100 px-8 py-5 flex items-center justify-between"><div><h3 class="text-xl font-bold text-slate-800" id="financeModalTitle">新增项目财务</h3><p class="text-xs text-slate-600 mt-0.5">填写项目财务信息与月度预算</p></div><div class="flex items-center gap-2"><button class="btn btn-ghost btn-sm text-red-600 hidden" id="financeDeleteBtn" onclick="deleteFinanceItem()"><i data-lucide="trash-2"></i>删除</button><button class="btn btn-ghost btn-sm rounded-full w-8 h-8 p-0" onclick="closeFinanceModal()"><i data-lucide="x"></i></button></div></div>
|
||||
<div class="finance-tabs">
|
||||
<button class="finance-tab active" data-tab="info" onclick="switchFinanceTab('info')"><i data-lucide="info" style="width:14px;height:14px;display:inline-block;vertical-align:-2px;margin-right:4px"></i>基本信息</button>
|
||||
<button class="finance-tab" data-tab="revpay" onclick="switchFinanceTab('revpay')"><i data-lucide="dollar-sign" style="width:14px;height:14px;display:inline-block;vertical-align:-2px;margin-right:4px"></i>确收与回款</button>
|
||||
@@ -285,8 +287,8 @@ function renderFinance() {
|
||||
{ key: '_expand', label: '' },
|
||||
{ key: 'client_name', label: '客户' }, { key: 'customer_name', label: '项目名称' }, { key: 'sign_amount', label: '签约金额' },
|
||||
{ key: 'rev', label: '已确收' }, { key: 'gross', label: '毛利' }, { key: 'gross_rate', label: '毛利率' },
|
||||
{ key: 'payment', label: '回款' }, { key: 'pending', label: '待回款' }, { key: 'pay_rate', label: '回款率' },
|
||||
{ key: 'cost', label: '成本' }, { key: 'paid', label: '支出' }, { key: 'paid_rate', label: '付款率' },
|
||||
{ key: 'payment', label: '回款' }, { key: 'pending', label: '应收' }, { key: 'pay_rate', label: '回款率' },
|
||||
{ key: 'cost', label: '成本' }, { key: 'paid', label: '支出' }, { key: 'payable', label: '应付' }, { key: 'paid_rate', label: '付款率' },
|
||||
{ key: 'cashflow', label: '现金流' },
|
||||
];
|
||||
const UNSIGNED_COLS = [
|
||||
@@ -302,25 +304,27 @@ function renderFinance() {
|
||||
const netAsset = receivable - payable;
|
||||
const m = moneyWan;
|
||||
const rows_data = [
|
||||
['签约金额', m(signTotal), '合同签约总额'],
|
||||
['确收金额', m(sumRev), '已确认收入'],
|
||||
['毛利', m(sumGross), '确收毛利'],
|
||||
['应收', m(receivable), '确收−回款'],
|
||||
['应付', m(payable), '成本−支出'],
|
||||
['现金流', m(cashflow), '回款−支出'],
|
||||
['项目净资产', m(netAsset), '应收−应付'],
|
||||
['签约金额', m(signTotal), '∑签约金额'],
|
||||
['确收金额', m(sumRev), '∑确收金额'],
|
||||
['毛利', m(sumGross), '∑毛利'],
|
||||
['回款', m(sumPay), '∑回款金额'],
|
||||
['支出', m(sumPaid), '∑支出金额'],
|
||||
['现金流', m(cashflow), '回款 − 支出'],
|
||||
['应收', m(receivable), '确收 − 回款'],
|
||||
['应付', m(payable), '成本 − 支出'],
|
||||
['往来余额', m(netAsset), '应收 − 应付'],
|
||||
];
|
||||
const fs = '<div class="grid grid-cols-4 gap-1.5 py-2">' + rows_data.map(r =>
|
||||
'<div class="card px-2 py-1.5 text-center"><div class="text-xs text-slate-400">' + r[0] + '</div><div class="text-sm font-semibold text-slate-800">' + r[1] + '</div></div>').join('') + '</div>';
|
||||
const cardHtml = (r, i) => '<div class="card px-2 py-1.5 text-center flex-1" style="background:' + (['#eff6ff','#eff6ff','#eff6ff','#fef3c7','#fef3c7','#fef3c7','#fef9c3','#fef9c3','#fef9c3'][i] || '#fff') + '"><div class="text-xs text-slate-600">' + r[0] + '</div><div class="text-sm font-semibold text-slate-800">' + r[1] + '</div></div>';
|
||||
const fs = '<div class="flex gap-1 items-stretch py-2">' + cardHtml(rows_data[0],0) + cardHtml(rows_data[1],1) + cardHtml(rows_data[2],2) + '<span class="w-px bg-slate-200 mx-1 self-stretch"></span>' + cardHtml(rows_data[3],3) + cardHtml(rows_data[4],4) + cardHtml(rows_data[5],5) + '<span class="w-px bg-slate-200 mx-1 self-stretch"></span>' + cardHtml(rows_data[6],6) + cardHtml(rows_data[7],7) + cardHtml(rows_data[8],8) + '</div>';
|
||||
const thead = isUnsigned ? buildThead(UNSIGNED_COLS, 'uns') : buildThead(SIGNED_COLS, 'sig');
|
||||
const theadCols = isUnsigned ? 6 : 14;
|
||||
const tbody = rows.length ? `<tbody style="line-height:1.37">${rows.join("")}</tbody>` : `<tr><td colspan="${theadCols}" class="p-6 text-center text-slate-400">${emptyText}</td></tr>`;
|
||||
const theadCols = isUnsigned ? 6 : 15;
|
||||
const tbody = rows.length ? `<tbody style="line-height:1.37">${rows.join("")}</tbody>` : `<tr><td colspan="${theadCols}" class="p-6 text-center text-slate-700">${emptyText}</td></tr>`;
|
||||
const subtitle = isUnsigned
|
||||
? '合同 → 毛利 → 成本 → 项目利润'
|
||||
: '合同 → 确收 → 毛利 → 成本 → 项目利润 → 回款 → 支出 → 现金流';
|
||||
return `${isUnsigned ? '' : `<div class="mb-1.5">${card(`<div class="flex justify-between items-center"><div class="flex items-center gap-2">${finHeaderBase}</div>${finAddBtn}</div>`, "py-2 px-4")}</div>`}` +
|
||||
card(fs +
|
||||
`<h3 class="text-sm font-bold text-slate-700 mt-2">项目财务明细</h3><p class="text-xs text-slate-400 mb-3">${subtitle}</p><div class="overflow-x-auto"><table class="w-full text-sm">${thead}${tbody}</table></div>`, "p-4");
|
||||
card(fs, "px-4 py-2") +
|
||||
card(`<h3 class="text-sm font-bold text-slate-700">项目财务明细</h3><p class="text-xs text-slate-600 mb-3">${subtitle}</p><div class="overflow-x-auto"><table class="w-full text-sm">${thead}${tbody}</table></div>`, "p-4");
|
||||
};
|
||||
|
||||
const fmtNoUnit = (v) => v ? `<span class="font-medium">${Number(v||0).toLocaleString('zh-CN')}</span>` : '<span class="text-slate-300">—</span>';
|
||||
@@ -331,11 +335,14 @@ function renderFinance() {
|
||||
const payRCls = payRVal === null ? '' : payRVal < 30 ? 'text-red-600' : payRVal < 80 ? 'text-amber-600' : 'text-green-600';
|
||||
const payR = payRVal !== null ? '<span class="' + payRCls + ' font-semibold">' + payRVal + '%</span>' : '<span class="text-slate-300">—</span>';
|
||||
const paidR = cost && paid ? Math.round(paid / cost * 100) + '%' : '<span class="text-slate-300">—</span>';
|
||||
const payable = cost - paid;
|
||||
const payableCls = payable > 0 ? 'text-rose-600' : payable < 0 ? 'text-green-600' : 'text-slate-700';
|
||||
const payableVal = payable ? (payable > 0 ? '+' : '') + money(payable) : '<span class="text-slate-300">—</span>';
|
||||
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';
|
||||
const pending = rev - payment;
|
||||
const pendingCls = pending > 0 ? 'text-red-600' : pending < 0 ? 'text-green-600' : 'text-slate-400';
|
||||
const pendingCls = pending > 0 ? 'text-red-600' : pending < 0 ? 'text-green-600' : 'text-slate-700';
|
||||
const pendingVal = pending ? (pending > 0 ? '+' : '') + money(pending) : '<span class="text-slate-300">—</span>';
|
||||
var budgetArr = [];
|
||||
var expenseArr = [];
|
||||
@@ -357,9 +364,9 @@ function renderFinance() {
|
||||
var bRev = parseFloat(b.rev || 0), bGross = parseFloat(b.gross || 0), bPay = parseFloat(b.payment || 0);
|
||||
var eCost = parseFloat(exp.cost || 0), ePaid = parseFloat(exp.paid || 0);
|
||||
var bCf = bPay - ePaid;
|
||||
return '<tr class="bg-slate-50"><td class="px-2 py-px"></td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-slate-400">' + m + '</td><td class="px-2 py-px text-center text-xs text-blue-600">' + (bRev ? money(bRev) : '—') + '</td><td class="px-2 py-px text-center text-xs text-green-600">' + (bGross ? money(bGross) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-amber-600">' + (bPay ? money(bPay) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-rose-600">' + (eCost ? money(eCost) : '—') + '</td><td class="px-2 py-px text-center text-xs text-purple-600">' + (ePaid ? money(ePaid) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs ' + (bCf >= 0 ? 'text-green-600' : 'text-red-600') + '">' + (bCf ? money(bCf) : '—') + '</td><td class="px-2 py-px text-center text-xs ' + (bRev - bPay > 0 ? 'text-red-600' : bRev - bPay < 0 ? 'text-green-600' : 'text-slate-300') + '">' + (bRev - bPay ? money(bRev - bPay) : '—') + '</td></tr>';
|
||||
return '<tr class="bg-slate-50"><td class="px-2 py-px"></td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-slate-600">' + m + '</td><td class="px-2 py-px text-center text-xs text-blue-600">' + (bRev ? money(bRev) : '—') + '</td><td class="px-2 py-px text-center text-xs text-green-600">' + (bGross ? money(bGross) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-amber-600">' + (bPay ? money(bPay) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-rose-600">' + (eCost ? money(eCost) : '—') + '</td><td class="px-2 py-px text-center text-xs text-purple-600">' + (ePaid ? money(ePaid) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs ' + (bCf >= 0 ? 'text-green-600' : 'text-red-600') + '">' + (bCf ? money(bCf) : '—') + '</td><td class="px-2 py-px text-center text-xs ' + (bRev - bPay > 0 ? 'text-red-600' : bRev - bPay < 0 ? 'text-green-600' : 'text-slate-300') + '">' + (bRev - bPay ? money(bRev - bPay) : '—') + '</td></tr>';
|
||||
}).join('');
|
||||
return `<tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="openPfEditModal(${pf.id})"><td class="px-2 py-px text-center align-middle text-sm" onclick="event.stopPropagation();finToggleExpand(${pf.id})"><i data-lucide="chevron-right" style="width:14px;height:14px;color:#94a3b8" id="fin_expand_icon_${pf.id}"></i></td><td class="px-2 py-px text-center align-middle text-sm truncate" style="max-width:100px" title="${esc(pf.client_name || '')}">${esc(pf.client_name || "")}</td><td class="px-2 py-px 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="px-2 py-px text-center align-middle text-sm font-medium">${money(pf.sign_amount)}</td><td class="px-2 py-px text-center text-blue-700 align-middle">${fmtNoUnit(rev)}</td><td class="px-2 py-px text-center align-middle text-sm font-medium">${fmtNoUnit(gross)}</td><td class="px-2 py-px text-center align-middle text-sm">${grossR}</td><td class="px-2 py-px text-center text-amber-700 align-middle">${fmtNoUnit(payment)}</td><td class="px-2 py-px text-center align-middle text-sm font-semibold ${pendingCls}">${pendingVal}</td><td class="px-2 py-px text-center align-middle text-sm">${payR}</td><td class="px-2 py-px text-center text-rose-700 align-middle">${fmtNoUnit(cost)}</td><td class="px-2 py-px text-center text-purple-700 align-middle">${fmtNoUnit(paid)}</td><td class="px-2 py-px text-center align-middle text-sm">${paidR}</td><td class="px-2 py-px text-center font-semibold align-middle ${cfCls}">${cf}</td></tr><tr class="hidden" id="fin_expand_${pf.id}"><td colspan="14"><table class="w-full"><thead><tr class="border-b border-slate-200"><th class="px-2 py-1"></th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">月份</th><th class="px-2 py-1 text-center text-xs text-slate-400">确收</th><th class="px-2 py-1 text-center text-xs text-slate-400">毛利</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">回款</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">成本</th><th class="px-2 py-1 text-center text-xs text-slate-400">支出</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">现金流</th><th class="px-2 py-1 text-center text-xs text-slate-400">待回款</th></tr></thead><tbody>${detailRows}</tbody></table></td></tr>`;
|
||||
return `<tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="openPfEditModal(${pf.id})"><td class="px-2 py-px text-center align-middle text-sm" onclick="event.stopPropagation();finToggleExpand(${pf.id})"><i data-lucide="chevron-right" style="width:14px;height:14px;color:#94a3b8" id="fin_expand_icon_${pf.id}"></i></td><td class="px-2 py-px text-center align-middle text-sm truncate" style="max-width:100px" title="${esc(pf.client_name || '')}">${esc(pf.client_name || "")}</td><td class="px-2 py-px 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="px-2 py-px text-center align-middle text-sm font-medium">${money(pf.sign_amount)}</td><td class="px-2 py-px text-center text-blue-700 align-middle">${fmtNoUnit(rev)}</td><td class="px-2 py-px text-center align-middle text-sm font-medium">${fmtNoUnit(gross)}</td><td class="px-2 py-px text-center align-middle text-sm">${grossR}</td><td class="px-2 py-px text-center text-amber-700 align-middle">${fmtNoUnit(payment)}</td><td class="px-2 py-px text-center align-middle text-sm font-semibold ${pendingCls}">${pendingVal}</td><td class="px-2 py-px text-center align-middle text-sm">${payR}</td><td class="px-2 py-px text-center text-rose-700 align-middle">${fmtNoUnit(cost)}</td><td class="px-2 py-px text-center text-purple-700 align-middle">${fmtNoUnit(paid)}</td><td class="px-2 py-px text-center align-middle text-sm font-semibold ${payableCls}">${payableVal}</td><td class="px-2 py-px text-center align-middle text-sm">${paidR}</td><td class="px-2 py-px text-center font-semibold align-middle ${cfCls}">${cf}</td></tr><tr class="hidden" id="fin_expand_${pf.id}"><td colspan="15"><table class="w-full"><thead><tr class="border-b border-slate-200"><th class="px-2 py-1"></th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-600">月份</th><th class="px-2 py-1 text-center text-xs text-slate-600">确收</th><th class="px-2 py-1 text-center text-xs text-slate-600">毛利</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-600">回款</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-600">成本</th><th class="px-2 py-1 text-center text-xs text-slate-600">支出</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-600">现金流</th><th class="px-2 py-1 text-center text-xs text-slate-600">应收</th></tr></thead><tbody>${detailRows}</tbody></table></td></tr>`;
|
||||
};
|
||||
// 待签约简版行
|
||||
const tdRowUnsigned = (pf, cost, gross) => {
|
||||
@@ -611,7 +618,7 @@ window.addTaskRow = (taskMonth = '', taskType = '', taskCount = '', executedCoun
|
||||
const isPreset = TASK_TYPES.includes(taskType);
|
||||
const typeCell = isPreset || !taskType
|
||||
? `<select name="task_type[]" class="form-ctrl form-ctrl-sm w-full" onchange="onTaskTypeChange(this)"><option value="">选择</option>${TASK_TYPES.map(t => `<option ${t === taskType ? 'selected' : ''}>${t}</option>`).join("")}<option value="__custom__" ${!isPreset && taskType ? 'selected' : ''}>自定义...</option></select>`
|
||||
: `<input name="task_type[]" class="form-ctrl form-ctrl-sm w-full" value="${esc(taskType)}" placeholder="输入自定义类型"><button type="button" class="btn btn-ghost btn-sm text-slate-400 p-0 w-5 h-5 ml-1" onclick="revertTaskType(this)" title="返回选择"><i data-lucide="rotate-ccw" style="width:12px;height:12px"></i></button>`;
|
||||
: `<input name="task_type[]" class="form-ctrl form-ctrl-sm w-full" value="${esc(taskType)}" placeholder="输入自定义类型"><button type="button" class="btn btn-ghost btn-sm text-slate-700 p-0 w-5 h-5 ml-1" onclick="revertTaskType(this)" title="返回选择"><i data-lucide="rotate-ccw" style="width:12px;height:12px"></i></button>`;
|
||||
row.innerHTML = `<td><select name="task_month[]" class="form-ctrl form-ctrl-sm w-full">${taskMonthOptions(taskMonth || defaultMonth)}</select></td>
|
||||
<td class="flex items-center">${typeCell}</td>
|
||||
<td><input name="task_count[]" type="number" step="1" min="0" class="form-ctrl form-ctrl-sm text-right" style="width:100px" placeholder="0" value="${taskCount}" oninput="updateTaskDiff(this)"></td>
|
||||
@@ -673,7 +680,7 @@ window.onTaskTypeChange = (sel) => {
|
||||
if (sel.value !== '__custom__') return;
|
||||
const td = sel.parentElement;
|
||||
const oldVal = sel.value;
|
||||
td.innerHTML = `<input name="task_type[]" class="form-ctrl form-ctrl-sm w-full" placeholder="输入自定义类型" autofocus><button type="button" class="btn btn-ghost btn-sm text-slate-400 p-0 w-5 h-5 ml-1" onclick="revertTaskType(this)" title="返回选择"><i data-lucide="rotate-ccw" style="width:12px;height:12px"></i></button>`;
|
||||
td.innerHTML = `<input name="task_type[]" class="form-ctrl form-ctrl-sm w-full" placeholder="输入自定义类型" autofocus><button type="button" class="btn btn-ghost btn-sm text-slate-700 p-0 w-5 h-5 ml-1" onclick="revertTaskType(this)" title="返回选择"><i data-lucide="rotate-ccw" style="width:12px;height:12px"></i></button>`;
|
||||
if (window.lucide) window.lucide.createIcons();
|
||||
td.querySelector('input').focus();
|
||||
};
|
||||
@@ -737,7 +744,7 @@ async function loadFinFollowups(pfId) {
|
||||
const fups = await api(`/api/followups/project_finance/${pfId}`);
|
||||
list.innerHTML = fups.length
|
||||
? fups.map(f => `<div class="activity-item"><div class="activity-icon"><i data-lucide="message-square"></i></div><div class="min-w-0 flex-1"><div class="flex justify-between gap-3 text-[12px] text-slate-500"><span>${esc(f.follower)} · ${esc(f.follow_up_method)}</span><span>${esc(f.followed_at)}</span></div><div class="mt-1 leading-5 text-slate-800 rich-content" data-html="${encodeURIComponent(f.content || '')}"></div>${f.next_action ? `<p class="mt-1 leading-5 text-blue-700">下一步:${text(f.next_action)}</p>` : ""}</div><button class="activity-delete" type="button" onclick="deleteFinFollowup(event, ${f.id})" title="删除评论"><i data-lucide="trash-2"></i></button></div>`).join("")
|
||||
: '<p class="text-sm text-slate-400 py-4 text-center">暂无跟进记录</p>';
|
||||
: '<p class="text-sm text-slate-700 py-4 text-center">暂无跟进记录</p>';
|
||||
if (window.lucide) window.lucide.createIcons();
|
||||
list.querySelectorAll(".rich-content").forEach(el => {
|
||||
const html = el.dataset.html;
|
||||
@@ -1081,9 +1088,9 @@ function renderFinanceOverview() {
|
||||
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-400">2026</span></div><div style="position:relative;height:200px"><canvas id="finChartSign"></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="finChartRev"></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="finChartCash"></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">2026</span></div><div style="position:relative;height:200px"><canvas id="finChartSign"></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">2026</span></div><div style="position:relative;height:200px"><canvas id="finChartRev"></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">2026</span></div><div style="position:relative;height:200px"><canvas id="finChartCash"></canvas></div>', 'p-4') +
|
||||
'</div></div>';
|
||||
|
||||
if (window.lucide) window.lucide.createIcons();
|
||||
@@ -1122,9 +1129,9 @@ function renderFinanceQuarterlyOverview() {
|
||||
}
|
||||
var thead = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
|
||||
thead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">Q1</th>';
|
||||
thead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">Q2</th><th class="py-1.5 text-right font-semibold text-slate-400 whitespace-nowrap">环比</th>';
|
||||
thead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">Q3</th><th class="py-1.5 text-right font-semibold text-slate-400 whitespace-nowrap">环比</th>';
|
||||
thead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">Q4</th><th class="py-1.5 text-right font-semibold text-slate-400 whitespace-nowrap">环比</th>';
|
||||
thead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">Q2</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">环比</th>';
|
||||
thead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">Q3</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">环比</th>';
|
||||
thead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">Q4</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">环比</th>';
|
||||
thead += '</tr>';
|
||||
var qoq = function(cur, prev) { if (!prev) return '<span class="text-slate-300">—</span>'; var pct = Math.round((cur - prev) / prev * 100); var cls = pct >= 0 ? 'text-green-600' : 'text-red-600'; var sign = pct >= 0 ? '+' : ''; return '<span class="' + cls + '">' + sign + pct + '%</span>'; };
|
||||
var tbody = '';
|
||||
@@ -1155,7 +1162,7 @@ function renderFinanceQuarterlyOverview() {
|
||||
var grossQ = [qData[2][0], qData[2][1], qData[2][2], qData[2][3]];
|
||||
var grossAnnual = m.gross_annual || 0;
|
||||
var deptThead = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
|
||||
for (var qi = 0; qi < 4; qi++) { deptThead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">Q' + (qi+1) + '</th>'; if (qi > 0) deptThead += '<th class="py-1.5 text-right font-semibold text-slate-400 whitespace-nowrap">环比</th>'; }
|
||||
for (var qi = 0; qi < 4; qi++) { deptThead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">Q' + (qi+1) + '</th>'; if (qi > 0) deptThead += '<th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">环比</th>'; }
|
||||
deptThead += '</tr>';
|
||||
var deptRows = [
|
||||
{ label: '项目毛利', vals: grossQ, annual: grossAnnual },
|
||||
|
||||
@@ -103,13 +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';
|
||||
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-blue-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-blue-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-blue-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-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><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><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-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 = `${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-blue-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">${m}月</button>`; }).join('')}</div>`;
|
||||
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-blue-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-blue-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-blue-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-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><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-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-0.5 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 = `${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-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 isOverviewMode = false;
|
||||
@@ -123,7 +125,7 @@ function renderPlan() {
|
||||
${planFilterTabs}
|
||||
${state.planFilter !== 'expense' && state.planFilter !== 'overview' && state.planFilter !== 'quarterlyOverview' ? '' : (state.planFilter === 'expense' ? '' : '')}
|
||||
${state.planFilter !== 'expense' && state.planFilter !== 'overview' && state.planFilter !== 'quarterlyOverview' ? '' : ''}
|
||||
<div id="planModal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/40" onclick="closePlanModal()"><div class="bg-white rounded-2xl shadow-2xl w-full max-w-6xl mx-4 max-h-[92vh] overflow-y-auto" onclick="event.stopPropagation()"><div class="sticky top-0 z-10 bg-white/95 backdrop-blur border-b border-slate-100 px-8 py-5 flex items-center justify-between"><div><h3 class="text-xl font-bold text-slate-800" id="planModalTitle">新增项目财务</h3><p class="text-xs text-slate-400 mt-0.5">填写项目财务信息与月度预算</p></div><div class="flex items-center gap-2"><button class="btn btn-ghost btn-sm text-red-600 hidden" id="planDeleteBtn" onclick="deletePlanItem()"><i data-lucide="trash-2"></i>删除</button><button class="btn btn-ghost btn-sm rounded-full w-8 h-8 p-0" onclick="closePlanModal()"><i data-lucide="x"></i></button></div></div>
|
||||
<div id="planModal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/40" onclick="closePlanModal()"><div class="bg-white rounded-2xl shadow-2xl w-full max-w-6xl mx-4 max-h-[92vh] overflow-y-auto" onclick="event.stopPropagation()"><div class="sticky top-0 z-10 bg-white/95 backdrop-blur border-b border-slate-100 px-8 py-5 flex items-center justify-between"><div><h3 class="text-xl font-bold text-slate-800" id="planModalTitle">新增项目财务</h3><p class="text-xs text-slate-600 mt-0.5">填写项目财务信息与月度预算</p></div><div class="flex items-center gap-2"><button class="btn btn-ghost btn-sm text-red-600 hidden" id="planDeleteBtn" onclick="deletePlanItem()"><i data-lucide="trash-2"></i>删除</button><button class="btn btn-ghost btn-sm rounded-full w-8 h-8 p-0" onclick="closePlanModal()"><i data-lucide="x"></i></button></div></div>
|
||||
<div class="finance-tabs">
|
||||
<button class="finance-tab active" data-tab="info" onclick="switchPlanTab('info')"><i data-lucide="info" style="width:14px;height:14px;display:inline-block;vertical-align:-2px;margin-right:4px"></i>基本信息</button>
|
||||
<button class="finance-tab" data-tab="revpay" onclick="switchPlanTab('revpay')"><i data-lucide="dollar-sign" style="width:14px;height:14px;display:inline-block;vertical-align:-2px;margin-right:4px"></i>确收与回款</button>
|
||||
@@ -295,8 +297,9 @@ function renderPlan() {
|
||||
{ key: 'gross', label: '毛利' },
|
||||
{ key: 'gross_rate', label: '毛利率' },
|
||||
{ key: 'payment', label: '回款' },
|
||||
{ key: 'pending', label: '待回款' },
|
||||
{ key: 'pending', label: '应收' },
|
||||
{ key: 'pay_rate', label: '回款率' },
|
||||
{ key: 'payable', label: '应付' },
|
||||
{ key: 'cost', label: '当期成本' },
|
||||
{ key: 'paid', label: '当期流出' },
|
||||
{ key: 'cashflow', label: '现金流' },
|
||||
@@ -320,25 +323,27 @@ function renderPlan() {
|
||||
const netAsset = receivable - payable;
|
||||
const m = moneyWan;
|
||||
const rows_data = [
|
||||
['签约金额', m(signTotal), '合同签约总额'],
|
||||
['确收金额', m(sumRev), '已确认收入'],
|
||||
['毛利', m(sumGross), '确收毛利'],
|
||||
['应收', m(receivable), '确收−回款'],
|
||||
['应付', m(payable), '成本−支出'],
|
||||
['现金流', m(cashflow), '回款−支出'],
|
||||
['项目净资产', m(netAsset), '应收−应付'],
|
||||
['签约金额', m(signTotal), '∑签约金额'],
|
||||
['确收金额', m(sumRev), '∑确收金额'],
|
||||
['毛利', m(sumGross), '∑毛利'],
|
||||
['回款', m(sumPay), '∑回款金额'],
|
||||
['支出', m(sumPaid), '∑支出金额'],
|
||||
['现金流', m(cashflow), '回款 − 支出'],
|
||||
['应收', m(receivable), '确收 − 回款'],
|
||||
['应付', m(payable), '成本 − 支出'],
|
||||
['往来余额', m(netAsset), '应收 − 应付'],
|
||||
];
|
||||
const fs = '<div class="grid grid-cols-4 gap-1.5 py-2">' + rows_data.map(r =>
|
||||
'<div class="card px-2 py-1.5 text-center"><div class="text-xs text-slate-400">' + r[0] + '</div><div class="text-sm font-semibold text-slate-800">' + r[1] + '</div></div>').join('') + '</div>';
|
||||
const cardHtml = (r, i) => '<div class="card px-2 py-1.5 text-center flex-1" style="background:' + (['#eff6ff','#eff6ff','#eff6ff','#fef3c7','#fef3c7','#fef3c7','#fef9c3','#fef9c3','#fef9c3'][i] || '#fff') + '"><div class="text-xs text-slate-600">' + r[0] + '</div><div class="text-sm font-semibold text-slate-800">' + r[1] + '</div></div>';
|
||||
const fs = '<div class="flex gap-1 items-stretch py-2">' + cardHtml(rows_data[0],0) + cardHtml(rows_data[1],1) + cardHtml(rows_data[2],2) + '<span class="w-px bg-slate-200 mx-1 self-stretch"></span>' + cardHtml(rows_data[3],3) + cardHtml(rows_data[4],4) + cardHtml(rows_data[5],5) + '<span class="w-px bg-slate-200 mx-1 self-stretch"></span>' + cardHtml(rows_data[6],6) + cardHtml(rows_data[7],7) + cardHtml(rows_data[8],8) + '</div>';
|
||||
const thead = isUnsigned ? buildThead(UNSIGNED_COLS, 'uns') : buildThead(SIGNED_COLS, 'sig');
|
||||
const theadCols = isUnsigned ? 9 : 14;
|
||||
const tbody = rows.length ? `<tbody style="line-height:1.37">${rows.join("")}</tbody>` : `<tr><td colspan="${theadCols}" class="p-6 text-center text-slate-400">${emptyText}</td></tr>`;
|
||||
const theadCols = isUnsigned ? 9 : 15;
|
||||
const tbody = rows.length ? `<tbody style="line-height:1.37">${rows.join("")}</tbody>` : `<tr><td colspan="${theadCols}" class="p-6 text-center text-slate-700">${emptyText}</td></tr>`;
|
||||
const subtitle = isUnsigned
|
||||
? '合同 → 毛利 → 成本 → 项目利润'
|
||||
: '合同 → 确收 → 毛利 → 成本 → 项目利润 → 回款 → 支出 → 现金流';
|
||||
return `${isUnsigned ? '' : `<div class="mb-1.5">${card(`<div class="flex justify-between items-center"><div class="flex items-center gap-2">${finHeaderBase}</div>${finAddBtn}</div>`, "py-2 px-4")}</div>`}` +
|
||||
card(fs +
|
||||
`<h3 class="text-sm font-bold text-slate-700 mt-2">项目财务明细</h3><p class="text-xs text-slate-400 mb-3">${subtitle}</p><div class="overflow-x-auto"><table class="w-full text-sm">${thead}${tbody}</table></div>`, "p-4");
|
||||
card(fs, "px-4 py-2") +
|
||||
card(`<h3 class="text-sm font-bold text-slate-700">项目财务明细</h3><p class="text-xs text-slate-600 mb-3">${subtitle}</p><div class="overflow-x-auto"><table class="w-full text-sm">${thead}${tbody}</table></div>`, "p-4");
|
||||
};
|
||||
|
||||
const fmtNoUnit = (v) => v ? `<span class="font-medium">${Number(v||0).toLocaleString('zh-CN')}</span>` : '<span class="text-slate-300">—</span>';
|
||||
@@ -365,8 +370,11 @@ 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';
|
||||
const payable = cost - paid;
|
||||
const payableCls = payable > 0 ? 'text-rose-600' : payable < 0 ? 'text-green-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-400';
|
||||
const pendingCls = pending > 0 ? 'text-red-600' : pending < 0 ? 'text-green-600' : 'text-slate-700';
|
||||
const pendingVal = pending ? (pending > 0 ? '+' : '') + money(pending) : '<span class="text-slate-300">—</span>';
|
||||
var budgetArr = [];
|
||||
var expenseArr = [];
|
||||
@@ -388,9 +396,9 @@ function renderPlan() {
|
||||
var bRev = parseFloat(b.rev || 0), bGross = parseFloat(b.gross || 0), bPay = parseFloat(b.payment || 0);
|
||||
var eCost = parseFloat(exp.cost || 0), ePaid = parseFloat(exp.paid || 0);
|
||||
var bCf = bPay - ePaid;
|
||||
return '<tr class="bg-slate-50"><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-slate-400">' + m + '</td><td class="px-2 py-px text-center text-xs text-blue-600">' + (bRev ? money(bRev) : '—') + '</td><td class="px-2 py-px text-center text-xs text-green-600">' + (bGross ? money(bGross) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-amber-600">' + (bPay ? money(bPay) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-rose-600">' + (eCost ? money(eCost) : '—') + '</td><td class="px-2 py-px text-center text-xs text-purple-600">' + (ePaid ? money(ePaid) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs ' + (bCf >= 0 ? 'text-green-600' : 'text-red-600') + '">' + (bCf ? money(bCf) : '—') + '</td><td class="px-2 py-px text-center text-xs ' + (bRev - bPay > 0 ? 'text-red-600' : bRev - bPay < 0 ? 'text-green-600' : 'text-slate-300') + '">' + (bRev - bPay ? money(bRev - bPay) : '—') + '</td></tr>';
|
||||
return '<tr class="bg-slate-50"><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-slate-600">' + m + '</td><td class="px-2 py-px text-center text-xs text-blue-600">' + (bRev ? money(bRev) : '—') + '</td><td class="px-2 py-px text-center text-xs text-green-600">' + (bGross ? money(bGross) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-amber-600">' + (bPay ? money(bPay) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-rose-600">' + (eCost ? money(eCost) : '—') + '</td><td class="px-2 py-px text-center text-xs text-purple-600">' + (ePaid ? money(ePaid) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs ' + (bCf >= 0 ? 'text-green-600' : 'text-red-600') + '">' + (bCf ? money(bCf) : '—') + '</td><td class="px-2 py-px text-center text-xs ' + (bRev - bPay > 0 ? 'text-red-600' : bRev - bPay < 0 ? 'text-green-600' : 'text-slate-300') + '">' + (bRev - bPay ? money(bRev - bPay) : '—') + '</td></tr>';
|
||||
}).join('');
|
||||
return `<tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="planOpenPfEditModal(${pf.id})"><td class="px-2 py-px text-center align-middle text-sm" onclick="event.stopPropagation();planToggleExpand(${pf.id})"><i data-lucide="chevron-right" style="width:14px;height:14px;color:#94a3b8" id="plan_expand_icon_${pf.id}"></i></td><td class="px-2 py-px text-center align-middle text-sm truncate" style="max-width:100px" title="${esc(pf.client_name || '')}">${esc(pf.client_name || "")}</td><td class="px-2 py-px 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="px-2 py-px 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="px-2 py-px text-center align-middle text-sm" onclick="event.stopPropagation()">${planInlineSelect('ptype', pf.id, 'project_type', pf.project_type || '待签约', ['待签约','已签约'])}</td><td class="px-2 py-px text-center align-middle text-sm font-medium">${money(pf.sign_amount)}</td><td class="px-2 py-px text-center text-blue-700 align-middle">${fmtNoUnit(rev)}</td><td class="px-2 py-px text-center align-middle text-sm font-medium">${fmtNoUnit(gross)}</td><td class="px-2 py-px text-center align-middle text-sm">${grossR}</td><td class="px-2 py-px text-center text-amber-700 align-middle">${fmtNoUnit(payment)}</td><td class="px-2 py-px text-center align-middle text-sm font-semibold ${pendingCls}">${pendingVal}</td><td class="px-2 py-px text-center align-middle text-sm">${payR}</td><td class="px-2 py-px text-center text-rose-700 align-middle">${fmtNoUnit(cost)}</td><td class="px-2 py-px text-center text-purple-700 align-middle">${fmtNoUnit(paid)}</td><td class="px-2 py-px text-center font-semibold align-middle ${cfCls}">${cf}</td></tr><tr class="hidden" id="plan_expand_${pf.id}"><td colspan="15"><table class="w-full"><thead><tr class="border-b border-slate-200"><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">月份</th><th class="px-2 py-1 text-center text-xs text-slate-400">确收</th><th class="px-2 py-1 text-center text-xs text-slate-400">毛利</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">回款</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">当期成本</th><th class="px-2 py-1 text-center text-xs text-slate-400">当期流出</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">现金流</th><th class="px-2 py-1 text-center text-xs text-slate-400">待回款</th></tr></thead><tbody>${detailRows}</tbody></table></td></tr>`;
|
||||
return `<tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="planOpenPfEditModal(${pf.id})"><td class="px-2 py-px text-center align-middle text-sm" onclick="event.stopPropagation();planToggleExpand(${pf.id})"><i data-lucide="chevron-right" style="width:14px;height:14px;color:#94a3b8" id="plan_expand_icon_${pf.id}"></i></td><td class="px-2 py-px text-center align-middle text-sm truncate" style="max-width:100px" title="${esc(pf.client_name || '')}">${esc(pf.client_name || "")}</td><td class="px-2 py-px 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="px-2 py-px 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="px-2 py-px text-center align-middle text-sm" onclick="event.stopPropagation()">${planInlineSelect('ptype', pf.id, 'project_type', pf.project_type || '待签约', ['待签约','已签约'])}</td><td class="px-2 py-px text-center align-middle text-sm font-medium">${money(pf.sign_amount)}</td><td class="px-2 py-px text-center text-blue-700 align-middle">${fmtNoUnit(rev)}</td><td class="px-2 py-px text-center align-middle text-sm font-medium">${fmtNoUnit(gross)}</td><td class="px-2 py-px text-center align-middle text-sm">${grossR}</td><td class="px-2 py-px text-center text-amber-700 align-middle">${fmtNoUnit(payment)}</td><td class="px-2 py-px text-center align-middle text-sm font-semibold ${pendingCls}">${pendingVal}</td><td class="px-2 py-px text-center align-middle text-sm">${payR}</td><td class="px-2 py-px text-center text-rose-700 align-middle">${fmtNoUnit(cost)}</td><td class="px-2 py-px text-center text-purple-700 align-middle">${fmtNoUnit(paid)}</td><td class="px-2 py-px text-center align-middle text-sm font-semibold ${payableCls}">${payableVal}</td><td class="px-2 py-px text-center font-semibold align-middle ${cfCls}">${cf}</td></tr><tr class="hidden" id="plan_expand_${pf.id}"><td colspan="16"><table class="w-full"><thead><tr class="border-b border-slate-200"><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-600">月份</th><th class="px-2 py-1 text-center text-xs text-slate-600">确收</th><th class="px-2 py-1 text-center text-xs text-slate-600">毛利</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-600">回款</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-600">当期成本</th><th class="px-2 py-1 text-center text-xs text-slate-600">当期流出</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-600">现金流</th><th class="px-2 py-1 text-center text-xs text-slate-600">应收</th></tr></thead><tbody>${detailRows}</tbody></table></td></tr>`;
|
||||
};
|
||||
// 待签约简版行
|
||||
const tdRowUnsigned = (pf, cost, gross) => {
|
||||
@@ -657,7 +665,7 @@ window.planAddTaskRow = (taskMonth = '', taskType = '', taskCount = '', executed
|
||||
const isPreset = TASK_TYPES.includes(taskType);
|
||||
const typeCell = isPreset || !taskType
|
||||
? `<select name="task_type[]" class="form-ctrl form-ctrl-sm w-full" onchange="planOnTaskTypeChange(this)"><option value="">选择</option>${TASK_TYPES.map(t => `<option ${t === taskType ? 'selected' : ''}>${t}</option>`).join("")}<option value="__custom__" ${!isPreset && taskType ? 'selected' : ''}>自定义...</option></select>`
|
||||
: `<input name="task_type[]" class="form-ctrl form-ctrl-sm w-full" value="${esc(taskType)}" placeholder="输入自定义类型"><button type="button" class="btn btn-ghost btn-sm text-slate-400 p-0 w-5 h-5 ml-1" onclick="planRevertTaskType(this)" title="返回选择"><i data-lucide="rotate-ccw" style="width:12px;height:12px"></i></button>`;
|
||||
: `<input name="task_type[]" class="form-ctrl form-ctrl-sm w-full" value="${esc(taskType)}" placeholder="输入自定义类型"><button type="button" class="btn btn-ghost btn-sm text-slate-700 p-0 w-5 h-5 ml-1" onclick="planRevertTaskType(this)" title="返回选择"><i data-lucide="rotate-ccw" style="width:12px;height:12px"></i></button>`;
|
||||
row.innerHTML = `<td><select name="task_month[]" class="form-ctrl form-ctrl-sm w-full">${planTaskMonthOptions(taskMonth || defaultMonth)}</select></td>
|
||||
<td class="flex items-center">${typeCell}</td>
|
||||
<td><input name="task_count[]" type="number" step="1" min="0" class="form-ctrl form-ctrl-sm text-right" style="width:100px" placeholder="0" value="${taskCount}" oninput="planUpdateTaskDiff(this)"></td>
|
||||
@@ -719,7 +727,7 @@ window.planOnTaskTypeChange = (sel) => {
|
||||
if (sel.value !== '__custom__') return;
|
||||
const td = sel.parentElement;
|
||||
const oldVal = sel.value;
|
||||
td.innerHTML = `<input name="task_type[]" class="form-ctrl form-ctrl-sm w-full" placeholder="输入自定义类型" autofocus><button type="button" class="btn btn-ghost btn-sm text-slate-400 p-0 w-5 h-5 ml-1" onclick="planRevertTaskType(this)" title="返回选择"><i data-lucide="rotate-ccw" style="width:12px;height:12px"></i></button>`;
|
||||
td.innerHTML = `<input name="task_type[]" class="form-ctrl form-ctrl-sm w-full" placeholder="输入自定义类型" autofocus><button type="button" class="btn btn-ghost btn-sm text-slate-700 p-0 w-5 h-5 ml-1" onclick="planRevertTaskType(this)" title="返回选择"><i data-lucide="rotate-ccw" style="width:12px;height:12px"></i></button>`;
|
||||
if (window.lucide) window.lucide.createIcons();
|
||||
td.querySelector('input').focus();
|
||||
};
|
||||
@@ -783,7 +791,7 @@ async function planLoadFollowups(pfId) {
|
||||
const fups = await api(`/api/followups/project_finance/${pfId}`);
|
||||
list.innerHTML = fups.length
|
||||
? fups.map(f => `<div class="activity-item"><div class="activity-icon"><i data-lucide="message-square"></i></div><div class="min-w-0 flex-1"><div class="flex justify-between gap-3 text-[12px] text-slate-500"><span>${esc(f.follower)} · ${esc(f.follow_up_method)}</span><span>${esc(f.followed_at)}</span></div><div class="mt-1 leading-5 text-slate-800 rich-content" data-html="${encodeURIComponent(f.content || '')}"></div>${f.next_action ? `<p class="mt-1 leading-5 text-blue-700">下一步:${text(f.next_action)}</p>` : ""}</div><button class="activity-delete" type="button" onclick="planDeleteFollowup(event, ${f.id})" title="删除评论"><i data-lucide="trash-2"></i></button></div>`).join("")
|
||||
: '<p class="text-sm text-slate-400 py-4 text-center">暂无跟进记录</p>';
|
||||
: '<p class="text-sm text-slate-700 py-4 text-center">暂无跟进记录</p>';
|
||||
if (window.lucide) window.lucide.createIcons();
|
||||
list.querySelectorAll(".rich-content").forEach(el => {
|
||||
const html = el.dataset.html;
|
||||
@@ -1173,9 +1181,9 @@ function renderPlanOverview() {
|
||||
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-400">2026</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-400">2026</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-400">2026</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">2026</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">2026</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">2026</span></div><div style="position:relative;height:200px"><canvas id="planChartCash"></canvas></div>', 'p-4') +
|
||||
'</div></div>';
|
||||
|
||||
if (window.lucide) window.lucide.createIcons();
|
||||
@@ -1219,9 +1227,9 @@ function renderPlanQuarterlyOverview() {
|
||||
}
|
||||
var thead = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
|
||||
thead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">Q1</th>';
|
||||
thead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">Q2</th><th class="py-1.5 text-right font-semibold text-slate-400 whitespace-nowrap">环比</th>';
|
||||
thead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">Q3</th><th class="py-1.5 text-right font-semibold text-slate-400 whitespace-nowrap">环比</th>';
|
||||
thead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">Q4</th><th class="py-1.5 text-right font-semibold text-slate-400 whitespace-nowrap">环比</th>';
|
||||
thead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">Q2</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">环比</th>';
|
||||
thead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">Q3</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">环比</th>';
|
||||
thead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">Q4</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">环比</th>';
|
||||
thead += '</tr>';
|
||||
var qoq = function(cur, prev) { if (!prev) return '<span class="text-slate-300">—</span>'; var pct = Math.round((cur - prev) / prev * 100); var cls = pct >= 0 ? 'text-green-600' : 'text-red-600'; var sign = pct >= 0 ? '+' : ''; return '<span class="' + cls + '">' + sign + pct + '%</span>'; };
|
||||
var tbody = '';
|
||||
@@ -1251,7 +1259,7 @@ function renderPlanQuarterlyOverview() {
|
||||
var grossQ = [qData[2][0], qData[2][1], qData[2][2], qData[2][3]];
|
||||
var grossAnnual = annualSums2.gross;
|
||||
var deptThead = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
|
||||
for (var qi = 0; qi < 4; qi++) { deptThead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">Q' + (qi+1) + '</th>'; if (qi > 0) deptThead += '<th class="py-1.5 text-right font-semibold text-slate-400 whitespace-nowrap">环比</th>'; }
|
||||
for (var qi = 0; qi < 4; qi++) { deptThead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">Q' + (qi+1) + '</th>'; if (qi > 0) deptThead += '<th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">环比</th>'; }
|
||||
deptThead += '</tr>';
|
||||
var deptRows = [
|
||||
{ label: '项目毛利', vals: grossQ, annual: grossAnnual },
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
<header class="topbar border-b border-slate-200 bg-white px-8 py-5">
|
||||
<div class="flex items-center gap-3 w-full">
|
||||
<div class="flex-1">
|
||||
<p class="eyebrow text-xs font-semibold uppercase tracking-[0.18em] text-blue-700">OPC Manager <span class="ml-2 text-xs font-normal text-slate-400 tracking-normal">v1.2.0.34</span></p>
|
||||
<p class="eyebrow text-xs font-semibold uppercase tracking-[0.18em] text-blue-700">OPC Manager <span class="ml-2 text-xs font-normal text-slate-400 tracking-normal">v1.2.0.38</span></p>
|
||||
<div class="flex items-center gap-4 mt-1">
|
||||
<h1 class="text-2xl font-semibold" id="workspaceTitle">科普 OPC 工作台</h1>
|
||||
<div class="hidden sm:flex items-center gap-1.5 bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-100 rounded-full px-4 py-1.5">
|
||||
|
||||
Reference in New Issue
Block a user