Compare commits

...

3 Commits

Author SHA1 Message Date
mac
7bf30a7cc2 feat: 执行模块项目管理增加行展开明细
- 项目财务明细表格最左侧增加展开箭头
- 点击展开月度确收回款明细(同预算模块)
- 自动过滤全0月份
- 版本号 v1.2.0.7
2026-07-10 13:38:11 +08:00
mac
44f313050e fix: 切换工作台后数据不刷新
- 根因: ensureStandardProposals每次触发return跳过ensureTabData
- 修复: ensureTabData移到ensureStandardProposals之前
- 增加_proposalsInitialized一次性flag避免重复检查
- switchTenant改为async+await load()
- 版本号 v1.2.0.6
2026-07-10 13:27:22 +08:00
mac
fad6a0e2fb fix: 切换工作台后数据不刷新
- load()渲染前增加ensureTabData(state.active)加载当前tab数据
- 版本号 v1.2.0.5
2026-07-10 13:12:42 +08:00
3 changed files with 56 additions and 14 deletions

View File

@@ -290,6 +290,7 @@ function renderFinance() {
return h;
};
const SIGNED_COLS = [
{ 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: 'pay_rate', label: '回款率' },
@@ -331,7 +332,24 @@ function renderFinance() {
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="openPfEditModal(${pf.id})"><td class="px-2 py-px text-center align-middle text-sm">${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">${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="p-2 text-center font-semibold align-middle ${cfCls}">${cf}</td></tr>`;
var budgetArr = [];
var expenseArr = [];
try { budgetArr = JSON.parse(pf.budget_data || "[]"); } catch(e) {}
try { expenseArr = JSON.parse(pf.expense_data || "[]"); } catch(e) {}
var detailRows = budgetArr.filter(function(b) {
var exp = expenseArr.find(function(e) { return e.month === (b.month || ''); }) || {};
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);
return bRev || bGross || bPay || eCost || ePaid;
}).map(function(b) {
var m = b.month || '';
var exp = expenseArr.find(function(e) { return e.month === m; }) || {};
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></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">${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">${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="p-2 text-center font-semibold align-middle ${cfCls}">${cf}</td></tr><tr class="hidden" id="fin_expand_${pf.id}"><td colspan="13"><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></tr></thead><tbody>${detailRows}</tbody></table></td></tr>`;
};
// 待签约简版行
const tdRowUnsigned = (pf, cost, gross) => {
@@ -752,6 +770,19 @@ window.deleteFinFollowup = async (event, followupId) => {
if (pfId) await loadFinFollowups(pfId);
};
window.finToggleExpand = (pfId) => {
var row = document.getElementById('fin_expand_' + pfId);
var icon = document.getElementById('fin_expand_icon_' + pfId);
if (!row) return;
if (row.classList.contains('hidden')) {
row.classList.remove('hidden');
if (icon) { icon.setAttribute('data-lucide', 'chevron-down'); if (window.lucide) window.lucide.createIcons(); }
} else {
row.classList.add('hidden');
if (icon) { icon.setAttribute('data-lucide', 'chevron-right'); if (window.lucide) window.lucide.createIcons(); }
}
};
window.openPfEditModal = (pfId) => {
const pf = (state.data.projectFinances || []).find(x => x.id === pfId);
if (!pf) return;

View File

@@ -185,15 +185,22 @@ async function load() {
if (!state.data.planExpense) state.data.planExpense = [];
clearTabCache();
updateLoadingStep(0, allSteps.length);
// 首次加载时确保标准资料库 7 项已初始化
if (typeof ensureStandardProposals === "function") {
const existing = (state.data.proposals || []).filter(p => p.proposal_type === "标准资料");
const missingCount = 7 - existing.length;
if (missingCount > 0) {
hideLoadingOverlay();
await ensureStandardProposals();
return; // ensureStandardProposals 内部会再次 render
}
// 按需加载当前 tab 的数据(必须在 ensureStandardProposals 之前,否则会被 return 跳过)
await ensureTabData(state.active);
// 首次加载时确保标准资料库 7 项已初始化(仅首次,避免每次切换工作台都触发)
if (!_proposalsInitialized && typeof ensureStandardProposals === "function") {
_proposalsInitialized = true;
try {
const proposalsData = await api("/api/proposals/list?tenant=" + encodeURIComponent(state.tenant));
state.data.proposals = proposalsData;
const existing = (proposalsData || []).filter(p => p.proposal_type === "标准资料");
const missingCount = 7 - existing.length;
if (missingCount > 0) {
hideLoadingOverlay();
await ensureStandardProposals();
return;
}
} catch(e) { /* non-critical */ }
}
// 逐步渲染
for (var i = 0; i < _loadingSteps.length; i++) {
@@ -221,6 +228,7 @@ var _tabResourceMap = {
};
var _tabLoaded = {};
var _proposalsInitialized = false;
async function ensureTabData(tab) {
if (tab === 'home' || tab === 'performance') return;
@@ -231,10 +239,12 @@ async function ensureTabData(tab) {
_tabLoaded[dataKey + '_' + state.tenant] = true;
try {
state.data[dataKey] = await api("/api/" + resource + "/list?tenant=" + encodeURIComponent(state.tenant));
} catch(e) { /* non-critical */ }
} catch(e) { console.error('ensureTabData failed:', resource, e.message); }
// Load related expense data for finance/plan tabs
if (tab === 'finance') {
try { state.data.expense = await api("/api/expense/list?tenant=" + encodeURIComponent(state.tenant)); } catch(e) {}
try { state.data.finance = await api("/api/finance/list?tenant=" + encodeURIComponent(state.tenant)); } catch(e) {}
try { state.data.tasks = await api("/api/tasks/list?tenant=" + encodeURIComponent(state.tenant)); } catch(e) {}
} else if (tab === 'plan') {
try { state.data.planExpense = await api("/api/planExpense/list?tenant=" + encodeURIComponent(state.tenant)); } catch(e) {}
}
@@ -356,7 +366,7 @@ window.setPlanSort = (key) => {
else { state.planSort.key = key; state.planSort.asc = true; }
renderPlan();
};
window.switchTenant = (tenant) => {
window.switchTenant = async (tenant) => {
state.tenant = tenant;
state.selectedProject = null;
localStorage.setItem("opc-active-tenant", tenant);
@@ -364,7 +374,8 @@ window.switchTenant = (tenant) => {
if (typeof updateTenantLabel === "function") updateTenantLabel();
updateSidebarTabs();
if (tenant === "总览") { state.planFilter = 'overview'; state.finFilter = 'overview'; switchTab("home"); }
load();
else if (state.active === 'home') { state.active = 'plan'; localStorage.setItem("opc-active-tab", "plan"); document.querySelectorAll(".sidebar-tab").forEach((btn) => btn.classList.toggle("active", btn.dataset.tab === "plan")); document.querySelectorAll(".panel").forEach((panel) => panel.classList.toggle("active", panel.id === "plan")); }
await load();
};
// 轻量刷新辅助函数

View File

@@ -76,7 +76,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.4</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.7</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">