From 384c80e34d5b1bf36927cad5ec359a3129f49f0b Mon Sep 17 00:00:00 2001 From: mac Date: Thu, 30 Jul 2026 17:27:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E3=80=8C=E6=88=91?= =?UTF-8?q?=E7=9A=84=E3=80=8D=E6=A8=A1=E5=9D=97=20=E2=80=94=20=E7=BB=A9?= =?UTF-8?q?=E6=95=88+=E9=A2=84=E7=AE=97=E6=9C=88=E6=8A=A5+=E5=B7=B2?= =?UTF-8?q?=E5=8F=91=E7=94=9F=E6=9C=88=E6=8A=A5+=E9=87=8D=E7=82=B9?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 侧边栏新增「我的」tab,排序第一,默认首页 - 模块内容:绩效 → 预算月报 → 已发生月报 → 重点项目 - 绩效:自动统计目标/完成/完成率/评分,年/季/月筛选 - 预算月报/已发生月报:12 月汇总表(7 项核心指标) - 重点项目:展示 Focus 项目清单,点击跳转定位 - 创建 my.js 模块文件,替换原 performance.js 入口 - 总览工作台不显示「我的」入口 --- static/app.js | 2 +- static/modules/my.js | 221 ++++++++++++++++++++++++++++++++++++++++ static/modules/utils.js | 8 +- templates/index.html | 14 +-- 4 files changed, 234 insertions(+), 11 deletions(-) create mode 100644 static/modules/my.js diff --git a/static/app.js b/static/app.js index f2c89bb..4cf0f2d 100644 --- a/static/app.js +++ b/static/app.js @@ -6,7 +6,7 @@ applyUserTenants().then(() => { return load(); }).then(() => { const savedTab = localStorage.getItem("opc-active-tab"); - const validTabs = ["plan", "finance", "projects", "proposals", "products", "performance", "overview_plan", "overview_finance"]; + const validTabs = ["plan", "finance", "projects", "proposals", "products", "my", "performance", "overview_plan", "overview_finance"]; if (savedTab && validTabs.includes(savedTab) && document.getElementById(savedTab)) { switchTab(savedTab); } else { diff --git a/static/modules/my.js b/static/modules/my.js new file mode 100644 index 0000000..e8be1fa --- /dev/null +++ b/static/modules/my.js @@ -0,0 +1,221 @@ +// my.js — 我的模块(绩效 + 预算月报 + 已发生月报) +window.renderMy = () => { + const data = state.data; + if (!data) return; + + // 确保数据已加载 + if (!data._myDataLoaded) { + loadMyData().then(() => { data._myDataLoaded = true; renderMy(); }); + return; + } + + // ── 状态初始化 ── + if (state.planYear == null) state.planYear = new Date().getFullYear(); + const planYear = state.planYear; + if (!['annual','quarterly','monthly'].includes(state.perfView)) state.perfView = 'quarterly'; + if (state.perfQuarter === undefined) state.perfQuarter = Math.floor(new Date().getMonth() / 3); + const nowD = new Date(); + const activeQ = state.perfQuarter; + const qRanges = [[1,2,3],[4,5,6],[7,8,9],[10,11,12]]; + const activeMonths = qRanges[activeQ]; + const qLabels = ['Q1','Q2','Q3','Q4']; + + let aggMonths = []; + if (state.perfView === 'annual') { for (var mi = 1; mi <= 12; mi++) aggMonths.push(mi); } + else if (state.perfView === 'quarterly') { aggMonths = qRanges[activeQ]; } + else if (state.perfView === 'monthly') { const m = state.perfMonth; aggMonths = m ? [parseInt(m.substring(5))] : []; } + + if (state.perfView === 'monthly' && !state.perfMonth) { + state.perfMonth = planYear + '-' + String(nowD.getMonth() + 1).padStart(2, '0'); + } + + const pfs = (data.projectFinances || []); + const planPfs = (data.planFinances || []); + + // ── 1. 绩效 ── + function sumByMonths(items, field, useSign, months) { + let total = 0; + items.forEach(p => { + if (useSign) { + const sm = p.sign_month || ''; + const smY = parseInt(sm.substring(0, 4)) || 0; + const smM = parseInt(sm.substring(5)) || 0; + if (!months.includes(smM)) return; + if (planYear !== 0 && smY && smY !== planYear) return; + total += parseFloat(p.sign_amount || 0); + } else { + let bd = []; + try { bd = JSON.parse(p.budget_data || '[]'); } catch (e) {} + bd.forEach(b => { + const m = parseInt((b.month || '').substring(5)) || 0; + const y = parseInt((b.month || '').substring(0, 4)) || 0; + if (!months.includes(m)) return; + if (planYear !== 0 && y && y !== planYear) return; + total += parseFloat(b[field] || 0); + }); + } + }); + return Math.round(total); + } + + const perfRows = [ + { key: 'sign', label: '签约', complete: sumByMonths(pfs, '', true, aggMonths), plan: sumByMonths(planPfs, '', true, aggMonths), icon: 'file-text', defWeight: 10 }, + { key: 'rev', label: '收入', complete: sumByMonths(pfs, 'rev', false, aggMonths), plan: sumByMonths(planPfs, 'rev', false, aggMonths), icon: 'dollar-sign', defWeight: 20 }, + { key: 'gross', label: '毛利', complete: sumByMonths(pfs, 'gross', false, aggMonths), plan: sumByMonths(planPfs, 'gross', false, aggMonths), icon: 'trending-up', defWeight: 50 }, + { key: 'payment', label: '回款', complete: sumByMonths(pfs, 'payment', false, aggMonths), plan: sumByMonths(planPfs, 'payment', false, aggMonths), icon: 'coins', defWeight: 20 }, + ]; + + let storageKey = 'opc-perf-t'; + if (state.perfView === 'quarterly') storageKey = 'opc-perf-Q' + (activeQ + 1); + else if (state.perfView === 'monthly') storageKey = 'opc-perf-M' + (state.perfMonth || ''); + 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 (Math.abs(v) >= 10000) return (v / 10000).toFixed(1) + '万'; return v.toLocaleString(); } + function moneyIntL(v) { return Math.round(Number(v || 0)).toLocaleString('zh-CN'); } + + let totalScore = 0; + const perfHTML = perfRows.map(r => { + const target = r.plan; const weight = val(r.key, 'weight', r.defWeight); + const score = target > 0 ? Math.round((r.complete / target) * weight * 10) / 10 : 0; + const rate = target > 0 ? Math.round((r.complete / target) * 100) : 0; + totalScore += score; + return ` + ${r.label} + ${moneyWan(target)} + ${moneyWan(r.complete)} + ${rate}% +
+ ${score.toFixed(1)}`; + }).join(''); + + // ── 绩效 Header ── + const yearOpts = [2020,2021,2022,2023,2024,2025,2026,2027]; + const yearSelect = `年份:`; + const viewBtns = ``; + const quarterBtns = qLabels.map((ql,i) => ``).join(''); + const monthBtns = activeMonths.map(m => { const ms = planYear+'-'+String(m).padStart(2,'0'); return ``; }).join(''); + const perfHeader = `
${yearSelect}|${viewBtns}|季度:${quarterBtns}|月度:${monthBtns}
`; + + // ── 2. 预算月报 ── + const planFm = computePlanMonthly(planPfs, planYear); + const mLbl = ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月']; + const pLabels = ['合同金额','确收金额','确收毛利','成本','支出','回款','项目现金流']; + const pFields = ['sign','revenue','gross','cost','paid','payment',null]; + var pAnnual = { sign:0, revenue:0, gross:0, cost:0, paid:0, payment:0 }; + for (var i = 0; i < planFm.length; i++) { pAnnual.sign += planFm[i].sign; pAnnual.revenue += planFm[i].revenue; pAnnual.gross += planFm[i].gross; pAnnual.cost += planFm[i].cost; pAnnual.paid += planFm[i].paid; pAnnual.payment += planFm[i].payment; } + var planThead = '指标年度累计'; + for (var mi2 = 0; mi2 < 12; mi2++) { planThead += '' + mLbl[mi2] + ''; } + planThead += ''; + var planTbody = ''; + for (var ri = 0; ri < pLabels.length; ri++) { + var label = pLabels[ri]; + var fKey = pFields[ri]; + var annualVal = fKey ? pAnnual[fKey] : pAnnual.payment - pAnnual.paid; + planTbody += '' + label + '' + moneyIntL(annualVal) + ''; + for (var mi3 = 0; mi3 < 12; mi3++) { + if (fKey) { planTbody += '' + moneyIntL(planFm[mi3][fKey]) + ''; } + else { planTbody += '' + moneyIntL(planFm[mi3].payment - planFm[mi3].paid) + ''; } + } + planTbody += ''; + } + var planOverviewHTML = `
${planThead}${planTbody}
`; + + // ── 3. 已发生月报 ── + const finPfs = (data.projectFinances || []); + const finFm = computePlanMonthly(finPfs, planYear); + var fAnnual = { sign:0, revenue:0, gross:0, cost:0, paid:0, payment:0 }; + for (var fi = 0; fi < finFm.length; fi++) { fAnnual.sign += finFm[fi].sign; fAnnual.revenue += finFm[fi].revenue; fAnnual.gross += finFm[fi].gross; fAnnual.cost += finFm[fi].cost; fAnnual.paid += finFm[fi].paid; fAnnual.payment += finFm[fi].payment; } + var finThead = '指标年度累计'; + for (var mi4 = 0; mi4 < 12; mi4++) { finThead += '' + mLbl[mi4] + ''; } + finThead += ''; + var finTbody = ''; + var fLabels2 = ['合同金额','确收金额','确收毛利','成本','支出','回款','项目现金流']; + for (var ri2 = 0; ri2 < fLabels2.length; ri2++) { + var fKey2 = pFields[ri2]; + var aVal = fKey2 ? fAnnual[fKey2] : fAnnual.payment - fAnnual.paid; + finTbody += '' + fLabels2[ri2] + '' + moneyIntL(aVal) + ''; + for (var mi5 = 0; mi5 < 12; mi5++) { + if (fKey2) { finTbody += '' + moneyIntL(finFm[mi5][fKey2]) + ''; } + else { finTbody += '' + moneyIntL(finFm[mi5].payment - finFm[mi5].paid) + ''; } + } + finTbody += ''; + } + var finOverviewHTML = `
${finThead}${finTbody}
`; + + // ── 组装页面 ── + document.querySelector("#my").innerHTML = ` + +
+

绩效

+ ${perfHeader} +
+ + + + + + + + + + ${perfHTML} + + + +
指标目标(万)完成情况完成率权重评分
合计100${totalScore.toFixed(1)}
+
+

预算月报

+ ${planOverviewHTML} +

已发生月报

+ ${finOverviewHTML} +

重点项目

+
+ ${renderMyProjects()} +
+
`; + +// ── 重点项目列表 ── +function renderMyProjects() { + const ops = state.data.operations || []; + if (!ops.length) return '

暂无项目

'; + return '
' + ops.map(o => { + return ` + ${esc(o.project_name || '')} + `; + }).join('') + '
'; +} + + if (window.lucide) window.lucide.createIcons(); +}; + +// ── 筛选切换 ── +window.setMyYear = (y) => { state.planYear = parseInt(y); renderMy(); }; +window.setMyView = (v) => { state.perfView = v; renderMy(); }; +window.setMyQuarter = (q) => { state.perfView = 'quarterly'; state.perfQuarter = q; renderMy(); }; +window.setMyMonth = (m) => { state.perfView = 'monthly'; state.perfMonth = m; renderMy(); }; +window.updateMyScore = (key, field, value) => { + let storageKey = 'opc-perf-t'; + if (state.perfView === 'quarterly') storageKey = 'opc-perf-Q' + ((state.perfQuarter || 0) + 1); + else if (state.perfView === 'monthly') storageKey = 'opc-perf-M' + (state.perfMonth || ''); + const saved = JSON.parse(localStorage.getItem(storageKey) || '{}'); + saved[key] = saved[key] || {}; + saved[key][field] = value; + localStorage.setItem(storageKey, JSON.stringify(saved)); + renderMy(); +}; + +// ── 数据加载 ── +async function loadMyData() { + for (var i = 0; i < 3; i++) { + var r = i === 0 ? 'planFinances' : i === 1 ? 'projectFinances' : 'operations'; + try { state.data[r] = await api("/api/" + r + "/list?tenant=" + encodeURIComponent(state.tenant)); } + catch(e) { console.error('loadMyData failed:', r, e.message); } + } +} diff --git a/static/modules/utils.js b/static/modules/utils.js index bc45bc0..b5dc6a6 100644 --- a/static/modules/utils.js +++ b/static/modules/utils.js @@ -1,7 +1,7 @@ // utils.js — 全局工具函数与共享状态 const state = { - active: "plan", + active: "my", data: null, tenant: localStorage.getItem("opc-active-tenant") || "科普·无界", opFilter: "all", @@ -126,6 +126,7 @@ var _loadingSteps = [ { fn: 'renderProposals', label: '正在加载业务方案列表' }, { fn: 'renderProducts', label: '正在加载产品迭代列表' }, { fn: 'renderPerformance', label: '正在加载绩效管理' }, + { fn: 'renderMy', label: '正在加载我的模块' }, { fn: 'renderFinance', label: '正在加载已发生模块(表格+总览+图表)' }, { fn: 'renderPlan', label: '正在加载预算模块' }, ]; @@ -234,7 +235,7 @@ var _tabLoaded = {}; var _proposalsInitialized = false; async function ensureTabData(tab) { - if (tab === 'performance' || tab === 'overview_plan' || tab === 'overview_finance') return; + if (tab === 'my' || tab === 'performance' || tab === 'overview_plan' || tab === 'overview_finance') return; var resource = _tabResourceMap[tab]; if (!resource) return; var dataKey = resource; @@ -266,7 +267,7 @@ function switchTab(tab) { document.querySelectorAll(".panel").forEach((panel) => panel.classList.toggle("active", panel.id === tab)); // 更新顶部标题 - const titles = { finance: '执行管理', plan: '预算管理', projects: '重点工作台账', proposals: 'wiki', products: '版本与运营', performance: '季度绩效考核', overview_plan: '预算总览', overview_finance: '发生总览' }; + const titles = { finance: '执行管理', plan: '预算管理', projects: '重点工作台账', proposals: 'wiki', products: '版本与运营', performance: '季度绩效考核', my: '我的', overview_plan: '预算总览', overview_finance: '发生总览' }; const titleEl = document.querySelector('#workspaceTitle'); if (titleEl) titleEl.textContent = titles[tab] || state.tenant + ' OPC 工作台'; @@ -298,6 +299,7 @@ function renderActive() { else if (tab === "proposals") renderProposals(); else if (tab === "products") renderProducts(); else if (tab === "performance") renderPerformance(); + else if (tab === "my") renderMy(); else if (tab === "finance") renderFinance(); else if (tab === "plan") renderPlan(); if (window.lucide) window.lucide.createIcons(); diff --git a/templates/index.html b/templates/index.html index 4e0e5e9..4514c34 100644 --- a/templates/index.html +++ b/templates/index.html @@ -63,6 +63,10 @@
+ -
@@ -152,7 +152,7 @@ - +