feat: 按需加载+年份筛选+行展开明细+UI优化
后端: - bootstrap精简为轻量版(只返回summary+financeMonthly) 0.028s - 按需加载: 切tab时通过/api/<resource>/list获取数据 - 修复risk_projects语法错误 前端: - 项目管理tab改名(项目计划/项目→项目管理) - 筛选区增加年份下拉框(2024-2027) - 季度筛选增加年份过滤 - 项目行增加展开箭头,显示月度确收回款明细 - 自动过滤全0月份记录 - 平台费用: 金额→费用计提, 已发生金额→现金支出 - 计划→预算, 实际→执行 - 经营月报/季报卡片顺序调整 - 非总览工作台跳过renderHome - 版本号 v1.2.0.4
This commit is contained in:
@@ -12,6 +12,8 @@ const state = {
|
||||
finView: "overview",
|
||||
finSort: null, // { key: 'col', asc: true }
|
||||
planFilter: "projects",
|
||||
planYear: new Date().getFullYear(),
|
||||
finYear: new Date().getFullYear(),
|
||||
planView: "overview",
|
||||
planSort: null,
|
||||
proposalTab: "standard",
|
||||
@@ -167,8 +169,22 @@ function hideLoadingOverlay() {
|
||||
}
|
||||
|
||||
async function load() {
|
||||
showLoadingOverlay(_loadingSteps);
|
||||
var allSteps = [{ label: '正在加载工作台数据(查询10+张表+计算财务汇总)' }].concat(_loadingSteps);
|
||||
showLoadingOverlay(allSteps);
|
||||
state.data = await api(`/api/bootstrap?tenant=${encodeURIComponent(state.tenant)}`);
|
||||
// 初始化空数据,按需加载
|
||||
if (!state.data.sales) state.data.sales = [];
|
||||
if (!state.data.proposals) state.data.proposals = [];
|
||||
if (!state.data.operations) state.data.operations = [];
|
||||
if (!state.data.products) state.data.products = [];
|
||||
if (!state.data.finance) state.data.finance = [];
|
||||
if (!state.data.tasks) state.data.tasks = [];
|
||||
if (!state.data.projectFinances) state.data.projectFinances = [];
|
||||
if (!state.data.expense) state.data.expense = [];
|
||||
if (!state.data.planFinances) state.data.planFinances = [];
|
||||
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 === "标准资料");
|
||||
@@ -183,19 +199,51 @@ async function load() {
|
||||
for (var i = 0; i < _loadingSteps.length; i++) {
|
||||
var step = _loadingSteps[i];
|
||||
if (step.skip && step.skip()) {
|
||||
updateLoadingStep(i, _loadingSteps.length);
|
||||
updateLoadingStep(i + 1, allSteps.length);
|
||||
continue;
|
||||
}
|
||||
if (typeof window[step.fn] === 'function') {
|
||||
window[step.fn]();
|
||||
}
|
||||
updateLoadingStep(i, _loadingSteps.length);
|
||||
updateLoadingStep(i + 1, allSteps.length);
|
||||
await new Promise(function(r) { setTimeout(r, 50); });
|
||||
}
|
||||
if (window.lucide) window.lucide.createIcons();
|
||||
setTimeout(hideLoadingOverlay, 300);
|
||||
}
|
||||
|
||||
var _tabResourceMap = {
|
||||
projects: 'operations',
|
||||
proposals: 'proposals',
|
||||
products: 'products',
|
||||
finance: 'projectFinances',
|
||||
plan: 'planFinances',
|
||||
};
|
||||
|
||||
var _tabLoaded = {};
|
||||
|
||||
async function ensureTabData(tab) {
|
||||
if (tab === 'home' || tab === 'performance') return;
|
||||
var resource = _tabResourceMap[tab];
|
||||
if (!resource) return;
|
||||
var dataKey = resource;
|
||||
if (_tabLoaded[dataKey + '_' + state.tenant]) return;
|
||||
_tabLoaded[dataKey + '_' + state.tenant] = true;
|
||||
try {
|
||||
state.data[dataKey] = await api("/api/" + resource + "/list?tenant=" + encodeURIComponent(state.tenant));
|
||||
} catch(e) { /* non-critical */ }
|
||||
// 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) {}
|
||||
} else if (tab === 'plan') {
|
||||
try { state.data.planExpense = await api("/api/planExpense/list?tenant=" + encodeURIComponent(state.tenant)); } catch(e) {}
|
||||
}
|
||||
}
|
||||
|
||||
function clearTabCache() {
|
||||
_tabLoaded = {};
|
||||
}
|
||||
|
||||
function switchTab(tab) {
|
||||
state.active = tab;
|
||||
localStorage.setItem("opc-active-tab", tab);
|
||||
@@ -203,11 +251,15 @@ function switchTab(tab) {
|
||||
document.querySelectorAll(".panel").forEach((panel) => panel.classList.toggle("active", panel.id === tab));
|
||||
|
||||
// 更新顶部标题
|
||||
const titles = { home: 'OPC 工作台', finance: '实际管理', plan: '计划管理', projects: '重点工作台账', proposals: '业务方案', products: '版本与运营', performance: '季度绩效考核' };
|
||||
const titles = { home: 'OPC 工作台', finance: '执行管理', plan: '预算管理', projects: '重点工作台账', proposals: '业务方案', products: '版本与运营', performance: '季度绩效考核' };
|
||||
const titleEl = document.querySelector('#workspaceTitle');
|
||||
if (titleEl) titleEl.textContent = titles[tab] || state.tenant + ' OPC 工作台';
|
||||
|
||||
render();
|
||||
// 按需加载 tab 数据
|
||||
ensureTabData(tab).then(function() {
|
||||
renderActive();
|
||||
if (window.lucide) window.lucide.createIcons();
|
||||
});
|
||||
}
|
||||
|
||||
function updateSidebarTabs() {
|
||||
@@ -273,6 +325,7 @@ window.setFinView = (view) => {
|
||||
};
|
||||
window.setFinQuarter = (q) => { state.finQuarter = q; state.finView = 'quarterly'; renderFinance(); };
|
||||
window.setFinMonth = (month) => { state.finMonth = month; state.finView = 'monthly'; renderFinance(); };
|
||||
window.setFinYear = (year) => { state.finYear = parseInt(year); renderFinance(); };
|
||||
window.switchFinFilter = (filter) => {
|
||||
state.finFilter = filter;
|
||||
state.finSort = null;
|
||||
@@ -291,6 +344,7 @@ window.setPlanView = (view) => {
|
||||
};
|
||||
window.setPlanQuarter = (q) => { state.planQuarter = q; state.planView = 'quarterly'; renderPlan(); };
|
||||
window.setPlanMonth = (month) => { state.planMonth = month; state.planView = 'monthly'; renderPlan(); };
|
||||
window.setPlanYear = (year) => { state.planYear = parseInt(year); renderPlan(); };
|
||||
window.switchPlanFilter = (filter) => {
|
||||
state.planFilter = filter;
|
||||
state.planSort = null;
|
||||
@@ -312,6 +366,15 @@ window.switchTenant = (tenant) => {
|
||||
if (tenant === "总览") { state.planFilter = 'overview'; state.finFilter = 'overview'; switchTab("home"); }
|
||||
load();
|
||||
};
|
||||
|
||||
// 轻量刷新辅助函数
|
||||
async function refreshResource(resource, renderFn) {
|
||||
try {
|
||||
var dataKey = resource;
|
||||
state.data[dataKey] = await api("/api/" + resource + "/list?tenant=" + encodeURIComponent(state.tenant));
|
||||
if (renderFn && typeof window[renderFn] === 'function') window[renderFn]();
|
||||
} catch(e) { /* non-critical */ }
|
||||
}
|
||||
window.doLogout = async () => {
|
||||
await api("/api/auth/logout", { method: "POST" });
|
||||
location.href = "/login";
|
||||
|
||||
Reference in New Issue
Block a user