' +
'
费用记录' + filtered.length + '
' +
diff --git a/static/modules/finance.js b/static/modules/finance.js
index a1e7968..1a429c7 100644
--- a/static/modules/finance.js
+++ b/static/modules/finance.js
@@ -116,11 +116,11 @@ function renderFinance() {
const finHeaderBase = `
视图:${toolDateSelect}`;
const finAddBtn = `
`;
- const finFilterTabs = `
`;
+ const finFilterTabs = `
`;
document.querySelector("#finance").innerHTML = `
${finFilterTabs}
- ${card(`
${finHeaderBase}
${finAddBtn}
`, "p-4")}
+ ${state.finFilter !== 'expense' ? card(`
${finHeaderBase}
${finAddBtn}
`, "p-4") : ''}
@@ -237,6 +237,10 @@ function renderFinance() {
${(() => {
+ if (state.finFilter === 'expense') {
+ setTimeout(() => renderExpense(), 10);
+ return `
`;
+ }
const isUnsigned = state.finFilter === '待签约';
const METRIC_CARDS = [
{ label: '签约金额', value: ctx => moneyWan(ctx.signTotal), color: 'text-slate-700' },
diff --git a/static/modules/performance.js b/static/modules/performance.js
new file mode 100644
index 0000000..31cd249
--- /dev/null
+++ b/static/modules/performance.js
@@ -0,0 +1,124 @@
+// performance.js — 绩效模块
+window.renderPerformance = () => {
+ const data = state.data;
+ if (!data) return;
+
+ if (state.perfQuarter === undefined) state.perfQuarter = Math.floor(new Date().getMonth() / 3);
+ const q = state.perfQuarter;
+ const qLabel = ['Q1 (1-3月)', 'Q2 (4-6月)', 'Q3 (7-9月)', 'Q4 (10-12月)'];
+ const qRanges = [[1,2,3],[4,5,6],[7,8,9],[10,11,12]];
+ const qRange = qRanges[q];
+ const storageKey = 'opc-performance-Q' + (q + 1);
+
+ const pfs = (data.projectFinances || []).filter(p => p.status === '已签约');
+
+ const sumQ = (field) => {
+ let total = 0;
+ pfs.forEach(p => {
+ try { JSON.parse(p.budget_data || '[]').forEach(b => { const m = parseInt((b.month || '').substring(5)) || 0; if (qRange.includes(m)) total += parseFloat(b[field] || 0); }); } catch (e) {}
+ });
+ return Math.round(total);
+ };
+
+ const signTotal = Math.round(pfs.filter(x => { const m = parseInt((x.sign_month || '0').substring(5)) || 0; return qRange.includes(m); }).reduce((a, p) => a + (p.sign_amount || 0), 0));
+ const revTotal = sumQ('rev');
+ const grossTotal = sumQ('gross');
+ const paymentTotal = sumQ('payment');
+
+ const rows = [
+ { key: 'sign', label: '签约', complete: signTotal, icon: 'file-text', defWeight: 10 },
+ { key: 'rev', label: '收入', complete: revTotal, icon: 'dollar-sign', defWeight: 20 },
+ { key: 'gross', label: '毛利', complete: grossTotal, icon: 'trending-up', defWeight: 50 },
+ { key: 'payment', label: '回款', complete: paymentTotal, icon: 'coins', defWeight: 20 },
+ ];
+
+ const saved = JSON.parse(localStorage.getItem(storageKey) || '{}');
+
+ function val(key, field, def) {
+ const r = saved[key] || {};
+ return r[field] !== undefined && r[field] !== '' ? Number(r[field]) : def;
+ }
+
+ function moneyWan(v) {
+ if (v >= 10000) return (v / 10000).toFixed(1) + '万';
+ return v.toLocaleString();
+ }
+
+ let totalScore = 0;
+ const tableRows = rows.map(r => {
+ const targetWan = val(r.key, 'targetWan', 0);
+ const target = Math.round(targetWan * 10000);
+ const weight = val(r.key, 'weight', r.defWeight);
+ const score = target > 0 ? Math.round((r.complete / target) * weight * 10) / 10 : 0;
+ totalScore += score;
+
+ return `
+ | ${r.label} |
+
+
+
+ 万
+
+ |
+ ${moneyWan(r.complete)} |
+
+
+
+
+ |
+ ${score.toFixed(1)} |
+
`;
+ }).join('');
+
+ const quarterTabs = `
` + qLabel.map((l, i) => {
+ return ``;
+ }).join('') + `
`;
+
+ document.querySelector("#performance").innerHTML = `
+
+
+ ${quarterTabs}
+
+
+
+ | 指标 |
+ 目标(万) |
+ 完成情况 |
+ 权重 |
+ 评分 |
+
+ ${tableRows}
+
+ | 合计 |
+ — |
+ — |
+ 100 |
+ ${totalScore.toFixed(1)} |
+
+
+
+
`;
+
+ if (window.lucide) window.lucide.createIcons();
+};
+
+window.switchPerfQuarter = (q) => {
+ state.perfQuarter = q;
+ renderPerformance();
+};
+
+window.updatePerfScore = (key, field, value) => {
+ const q = state.perfQuarter || 0;
+ const storageKey = 'opc-performance-Q' + (q + 1);
+ const saved = JSON.parse(localStorage.getItem(storageKey) || '{}');
+ saved[key] = saved[key] || {};
+ saved[key][field] = value;
+ localStorage.setItem(storageKey, JSON.stringify(saved));
+ renderPerformance();
+};
diff --git a/static/modules/products.js b/static/modules/products.js
index 6e2cc82..3c36906 100644
--- a/static/modules/products.js
+++ b/static/modules/products.js
@@ -187,8 +187,13 @@ function renderProducts() {
return productSort.dir > 0 ? '
' : '
';
};
const sortTh = (f, label, extra='') => `
${label}${sortIcon(f)}${extra} | `;
+ const tab = state.productTab || 'version';
+ const tabs = `
`;
+
document.querySelector("#products").innerHTML = `
+ ${tabs}
+ ${tab === 'version' ? `
@@ -230,6 +235,7 @@ function renderProducts() {
`}
`;
if (window.lucide) window.lucide.createIcons();
}
@@ -342,3 +348,8 @@ window.cyclePriority = async (id) => {
renderProducts();
} catch (e) { toast("更新失败:" + e.message, "error"); }
};
+
+window.switchProductTab = (v) => {
+ state.productTab = v;
+ renderProducts();
+};
diff --git a/static/modules/proposals.js b/static/modules/proposals.js
index 3cc03f6..1ea68d9 100644
--- a/static/modules/proposals.js
+++ b/static/modules/proposals.js
@@ -47,13 +47,11 @@ function renderProposals() {
const isStandard = state.proposalTab === "standard";
document.querySelector("#proposals").innerHTML = `
-
-
-
-
-
- ${!isStandard ? `
` : ''}
+
+
+
+
${!isStandard ? `` : ''}
${isStandard ? `
这是每一条 OPC 线,必须要梳理清楚的 7 份资料,项目不可以删除,只可以更新附件,请大家将最新的材料上传
` : `
在这里新建,并且上传您希望与团队其他成员共享的资料
`}
${isStandard ? renderStandardTable(standardItems) : renderOtherTable(otherItems)}
diff --git a/static/modules/utils.js b/static/modules/utils.js
index be67084..64bf31b 100644
--- a/static/modules/utils.js
+++ b/static/modules/utils.js
@@ -130,6 +130,12 @@ function switchTab(tab) {
localStorage.setItem("opc-active-tab", tab);
document.querySelectorAll(".sidebar-tab").forEach((btn) => btn.classList.toggle("active", btn.dataset.tab === tab));
document.querySelectorAll(".panel").forEach((panel) => panel.classList.toggle("active", panel.id === tab));
+
+ // 更新顶部标题
+ const titles = { home: 'OPC 工作台', finance: '业务管理', projects: '重点工作台账', proposals: '业务方案', products: '版本与运营', performance: '季度绩效考核' };
+ const titleEl = document.querySelector('#workspaceTitle');
+ if (titleEl) titleEl.textContent = titles[tab] || state.tenant + ' OPC 工作台';
+
render();
}
@@ -151,9 +157,8 @@ function render() {
renderProjects();
renderProposals();
renderProducts();
- renderOperations();
+ renderPerformance();
renderFinance();
- renderExpense();
if (window.lucide) window.lucide.createIcons();
}
@@ -164,7 +169,7 @@ function renderActive() {
else if (tab === "projects") renderProjects();
else if (tab === "proposals") renderProposals();
else if (tab === "products") renderProducts();
- else if (tab === "operations") renderOperations();
+ else if (tab === "performance") renderPerformance();
else if (tab === "finance") renderFinance();
if (window.lucide) window.lucide.createIcons();
}
diff --git a/templates/index.html b/templates/index.html
index 95d5e65..b72d2d6 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -49,17 +49,13 @@
首页