Compare commits

...

2 Commits

Author SHA1 Message Date
mac
b076cabb83 feat: renderHome跳过+卡片排序+模块改名
- 非总览工作台跳过renderHome(省Chart.js渲染)
- 计划→预算, 实际→执行
- 经营月报+季报卡片顺序统一: 业务线经营→业务线现金流→财务概览
- 版本号 v1.0.0.40
2026-07-09 16:52:35 +08:00
mac
c6c282537c feat: 加载浮层+工作台改回下拉菜单
- load()增加模态加载浮层: 7步进度展示+进度条+逐步渲染
- 工作台切换从竖向平铺改回下拉菜单(带图标)
- 版本号 v1.0.0.39
2026-07-09 16:33:49 +08:00
5 changed files with 125 additions and 19 deletions

View File

@@ -1035,9 +1035,9 @@ function renderFinanceOverview() {
var cfCard = card('<h3 class="text-sm font-bold text-slate-700">业务线现金流</h3><p class="text-xs text-slate-400 mb-3">项目现金流 - 平台费用(支出)= 业务线现金流</p><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + cfTbody + '</tbody></table></div>', 'p-4');
document.querySelector("#financeOverview").innerHTML = '<div class="grid gap-2.5">' +
card('<h3 class="text-sm font-bold text-slate-700">业务(项目)财务月度概览</h3><p class="text-xs text-slate-400 mb-3">合同 → 确收 → 毛利 → 成本 → 支出</p><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + thead + '</thead><tbody>' + tbody + '</tbody></table></div>', 'p-4') +
deptCard +
cfCard +
card('<h3 class="text-sm font-bold text-slate-700">业务(项目)财务月度概览</h3><p class="text-xs text-slate-400 mb-3">合同 → 确收 → 毛利 → 成本 → 支出</p><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') +

View File

@@ -1111,9 +1111,9 @@ function renderPlanOverview() {
var cfCard = card('<h3 class="text-sm font-bold text-slate-700">业务线现金流</h3><p class="text-xs text-slate-400 mb-3">项目现金流 - 平台费用(已付)= 业务线现金流</p><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">' +
card('<h3 class="text-sm font-bold text-slate-700">业务(项目)财务概览</h3><p class="text-xs text-slate-400 mb-3">合同 → 确收 → 毛利 → 成本 → 已付</p><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + thead + '</thead><tbody>' + tbody + '</tbody></table></div>', 'p-4') +
deptCard +
cfCard +
card('<h3 class="text-sm font-bold text-slate-700">业务(项目)财务概览</h3><p class="text-xs text-slate-400 mb-3">合同 → 确收 → 毛利 → 成本 → 已付</p><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') +
@@ -1246,9 +1246,9 @@ function renderPlanQuarterlyOverview() {
}
document.querySelector("#planQuarterlyOverview").innerHTML = '<div class="grid gap-2.5">' +
card('<h3 class="text-sm font-bold text-slate-700">业务(项目)财务季度概览</h3><p class="text-xs text-slate-400 mb-3">合同 → 确收 → 毛利 → 成本 → 支出 → 回款 → 项目现金流</p><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + thead + '</thead><tbody>' + tbody + '</tbody></table></div>', 'p-4') +
card('<h3 class="text-sm font-bold text-slate-700">业务线经营情况</h3><p class="text-xs text-slate-400 mb-3">毛利 - 平台费用 = 利润</p><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-sm font-bold text-slate-700">业务线现金流</h3><p class="text-xs text-slate-400 mb-3">项目现金流 - 平台费用(已付)= 业务线现金流</p><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-sm font-bold text-slate-700">业务(项目)财务季度概览</h3><p class="text-xs text-slate-400 mb-3">合同 → 确收 → 毛利 → 成本 → 支出 → 回款 → 项目现金流</p><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + thead + '</thead><tbody>' + tbody + '</tbody></table></div>', 'p-4') +
'</div>';
if (window.lucide) window.lucide.createIcons();
}

View File

@@ -67,19 +67,58 @@ function updateTenantLabel() {
};
const container = document.querySelector("#tenantList");
if (container) {
container.innerHTML = tenants.map(t => {
const shortName = t.replace("·无界", "");
const isActive = t === state.tenant;
var icon = tenantIcons[t] || 'circle';
return '<div class="flex flex-col items-center cursor-pointer rounded-lg py-1.5 px-1 w-14 transition-colors ' + (isActive ? 'bg-blue-600' : 'hover:bg-slate-800') + '" onclick="switchTenantFromMenu(\'' + t.replace(/'/g, "\\'") + '\')" title="' + esc(t) + '">' +
'<i data-lucide="' + icon + '" style="width:20px;height:20px;color:' + (isActive ? '#fff' : '#94a3b8') + '"></i>' +
'<span class="text-[10px] mt-1 max-w-[56px] truncate text-center ' + (isActive ? 'text-white' : 'text-slate-400') + '">' + esc(shortName) + '</span>' +
'</div>';
}).join('');
var currentShort = (state.tenant || '').replace("·无界", "");
var currentIcon = tenantIcons[state.tenant] || 'circle';
container.innerHTML = '<div class="flex flex-col items-center cursor-pointer hover:bg-slate-800 rounded-lg py-2 px-1 w-14 transition-colors" onclick="toggleTenantMenu(event)" title="切换工作台">' +
'<i data-lucide="' + currentIcon + '" style="width:20px;height:20px;color:#60a5fa"></i>' +
'<span class="text-[10px] mt-1 max-w-[56px] truncate text-center text-slate-300">' + esc(currentShort) + '</span>' +
'<i data-lucide="chevron-down" style="width:12px;height:12px;color:#64748b;margin-top:2px"></i>' +
'</div>';
if (window.lucide) window.lucide.createIcons();
}
}
window.toggleTenantMenu = (event) => {
event.stopPropagation();
let menu = document.getElementById("tenantMenu");
if (menu) { menu.remove(); return; }
const btn = event.currentTarget;
const rect = btn.getBoundingClientRect();
const tenants = (state.allowedTenants || []).slice().sort(function(a, b) { var order = ['总览','学术·无界','MCN·无界','科普·无界','医患·无界','科研·无界']; var ai = order.indexOf(a); var bi = order.indexOf(b); if (ai < 0) ai = 99; if (bi < 0) bi = 99; return ai - bi; });
var tenantIcons = {
'学术·无界': 'graduation-cap',
'MCN·无界': 'video',
'科普·无界': 'flask-conical',
'医患·无界': 'stethoscope',
'科研·无界': 'microscope',
'总览': 'layout-dashboard'
};
menu = document.createElement("div");
menu.id = "tenantMenu";
menu.className = "fixed bg-white rounded-lg shadow-xl border border-slate-200 py-1 min-w-[180px] z-[9999]";
menu.style.left = Math.min(rect.left - 8, window.innerWidth - 200) + "px";
menu.style.top = rect.bottom + 6 + "px";
menu.innerHTML = '<div class="px-4 py-2 border-b border-slate-100"><p class="text-xs font-semibold text-slate-500 uppercase tracking-wider">切换工作台</p></div>' +
tenants.map(t => {
const shortName = t.replace("·无界", "");
const isActive = t === state.tenant;
var icon = tenantIcons[t] || 'circle';
return '<button class="w-full text-left px-4 py-2.5 text-sm hover:bg-slate-50 transition-colors flex items-center gap-3 ' + (isActive ? 'text-blue-600 font-medium bg-blue-50' : 'text-slate-700') + '" onclick="switchTenantFromMenu(\'' + t.replace(/'/g, "\\'") + '\')">' +
'<i data-lucide="' + icon + '" style="width:16px;height:16px;color:' + (isActive ? '#2563eb' : '#94a3b8') + '"></i>' +
'<span class="flex-1">' + esc(shortName) + '</span>' +
(isActive ? '<i data-lucide="check" style="width:14px;height:14px;color:#2563eb"></i>' : '') +
'</button>';
}).join('');
document.body.appendChild(menu);
if (window.lucide) window.lucide.createIcons();
setTimeout(() => {
document.addEventListener("click", function closeMenu() {
menu.remove();
document.removeEventListener("click", closeMenu);
}, { once: true });
}, 10);
};
window.toggleUserMenu = (user) => {
let menu = document.getElementById("userMenu");
if (menu) { menu.remove(); return; }

View File

@@ -115,18 +115,85 @@ function renderTable(headers, rows, rowClicks) {
`);
}
var _loadingSteps = [
{ fn: 'renderHome', label: '正在加载首页仪表盘(指标卡片+图表)', skip: function() { return state.tenant !== '总览' && state.active !== 'home'; } },
{ fn: 'renderProjects', label: '正在加载业务机会列表' },
{ fn: 'renderProposals', label: '正在加载业务方案列表' },
{ fn: 'renderProducts', label: '正在加载产品迭代列表' },
{ fn: 'renderPerformance', label: '正在加载绩效管理' },
{ fn: 'renderFinance', label: '正在加载执行模块(表格+总览+图表)' },
{ fn: 'renderPlan', label: '正在加载预算模块' },
];
function showLoadingOverlay(steps) {
var existing = document.getElementById('_loadingOverlay');
if (existing) existing.remove();
var stepsHtml = steps.map(function(s, i) {
return '<div id="_loadingStep_' + i + '" class="flex items-center gap-3 py-1.5 text-sm ' + (i === 0 ? 'text-blue-600 font-medium' : 'text-slate-400') + '">' +
'<span class="w-4 h-4 flex items-center justify-center">' + (i === 0 ? '<svg class="animate-spin" width="14" height="14" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" opacity="0.25"/><path d="M12 2a10 10 0 0 1 10 10" stroke="currentColor" stroke-width="3" stroke-linecap="round"/></svg>' : '<span class="w-2 h-2 rounded-full bg-slate-300"></span>') + '</span>' +
'<span>' + s.label + '</span>' +
'</div>';
}).join('');
var overlay = document.createElement('div');
overlay.id = '_loadingOverlay';
overlay.className = 'fixed inset-0 z-[99999] flex items-center justify-center bg-black/50';
overlay.innerHTML = '<div class="bg-white rounded-2xl shadow-2xl px-8 py-6 w-full max-w-md">' +
'<h3 class="text-base font-bold text-slate-800 mb-1">正在加载工作台</h3>' +
'<p class="text-xs text-slate-400 mb-4">共 ' + steps.length + ' 个模块,请稍候…</p>' +
'<div id="_loadingSteps">' + stepsHtml + '</div>' +
'<div class="mt-4 h-1 bg-slate-100 rounded-full overflow-hidden"><div id="_loadingProgress" class="h-full bg-blue-600 transition-all duration-300" style="width:0%"></div></div>' +
'</div>';
document.body.appendChild(overlay);
}
function updateLoadingStep(idx, total) {
var stepEl = document.getElementById('_loadingStep_' + idx);
if (stepEl) {
stepEl.className = 'flex items-center gap-3 py-1.5 text-sm text-green-600 font-medium';
stepEl.querySelector('span:first-child').innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>';
}
var nextEl = document.getElementById('_loadingStep_' + (idx + 1));
if (nextEl) {
nextEl.className = 'flex items-center gap-3 py-1.5 text-sm text-blue-600 font-medium';
nextEl.querySelector('span:first-child').innerHTML = '<svg class="animate-spin" width="14" height="14" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" opacity="0.25"/><path d="M12 2a10 10 0 0 1 10 10" stroke="currentColor" stroke-width="3" stroke-linecap="round"/></svg>';
}
var progress = document.getElementById('_loadingProgress');
if (progress) progress.style.width = Math.round((idx + 1) / total * 100) + '%';
}
function hideLoadingOverlay() {
var overlay = document.getElementById('_loadingOverlay');
if (overlay) overlay.remove();
}
async function load() {
showLoadingOverlay(_loadingSteps);
state.data = await api(`/api/bootstrap?tenant=${encodeURIComponent(state.tenant)}`);
// 首次加载时确保标准资料库 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
}
}
render();
// 逐步渲染
for (var i = 0; i < _loadingSteps.length; i++) {
var step = _loadingSteps[i];
if (step.skip && step.skip()) {
updateLoadingStep(i, _loadingSteps.length);
continue;
}
if (typeof window[step.fn] === 'function') {
window[step.fn]();
}
updateLoadingStep(i, _loadingSteps.length);
await new Promise(function(r) { setTimeout(r, 50); });
}
if (window.lucide) window.lucide.createIcons();
setTimeout(hideLoadingOverlay, 300);
}
function switchTab(tab) {
@@ -157,7 +224,7 @@ function updateSidebarTabs() {
function render() {
if (!state.data) return;
renderHome();
if (state.tenant === '总览' || state.active === 'home') renderHome();
renderProjects();
renderProposals();
renderProducts();

View File

@@ -45,13 +45,13 @@
<i data-lucide="trending-up" style="width:20px;height:20px"></i>
<span class="text-[10px] mt-1">部门</span>
</div>
<div class="sidebar-tab" data-tab="plan" onclick="switchTab('plan')" title="计划">
<div class="sidebar-tab" data-tab="plan" onclick="switchTab('plan')" title="预算">
<i data-lucide="calendar-range" style="width:20px;height:20px"></i>
<span class="text-[10px] mt-1">计划</span>
<span class="text-[10px] mt-1">预算</span>
</div>
<div class="sidebar-tab" data-tab="finance" onclick="switchTab('finance')" title="实际">
<div class="sidebar-tab" data-tab="finance" onclick="switchTab('finance')" title="执行">
<i data-lucide="folder-open-dot" style="width:20px;height:20px"></i>
<span class="text-[10px] mt-1">实际</span>
<span class="text-[10px] mt-1">执行</span>
</div>
<div class="sidebar-tab" data-tab="projects" onclick="switchTab('projects')" title="Focus">
<i data-lucide="file-text" style="width:20px;height:20px"></i>
@@ -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.0.0.38</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.0.0.40</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">