From 536b165a9ba8835f1881a47b4e4d29f5ad13d3b2 Mon Sep 17 00:00:00 2001 From: mac Date: Sat, 11 Jul 2026 12:26:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E6=80=BB=E8=A7=88?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E5=8F=B0(=E4=BB=85=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=91=98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增总览工作台,聚合学术/科普/科研/医患/MCN数据 - 4个聚合API: 预算/发生月报季报 - 新增预算总览/发生总览侧边栏tab - 月报/季报视图切换 - 总览仅管理员可见 - 版本号 v1.2.0.24 --- backend/routes.py | 180 ++++++++++++++++++++++++++++- static/modules/overview_finance.js | 112 ++++++++++++++++++ static/modules/overview_plan.js | 112 ++++++++++++++++++ static/modules/projects.js | 6 +- static/modules/utils.js | 28 ++++- templates/index.html | 14 ++- 6 files changed, 438 insertions(+), 14 deletions(-) create mode 100644 static/modules/overview_finance.js create mode 100644 static/modules/overview_plan.js diff --git a/backend/routes.py b/backend/routes.py index 69e6e22..c8991b8 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -14,7 +14,7 @@ from helpers import add_file_index, attach_common, monthly_finance bp = Blueprint("api", __name__) -ALL_TENANTS = ["科普·无界", "科研·无界", "医患·无界", "MCN·无界", "学术·无界", "测试·无界"] +ALL_TENANTS = ["总览", "科普·无界", "科研·无界", "医患·无界", "MCN·无界", "学术·无界", "测试·无界"] TABLES = { "sales": ("sales_leads", ["target_customer", "priority", "status", "tenant"]), @@ -75,7 +75,7 @@ def auth_login(): session["display_name"] = user["display_name"] session["role"] = user["role"] if user["role"] == "admin": - session["tenants"] = ["科普·无界", "科研·无界", "医患·无界", "MCN·无界", "学术·无界", "测试·无界"] + session["tenants"] = ["总览", "科普·无界", "科研·无界", "医患·无界", "MCN·无界", "学术·无界", "测试·无界"] else: ut = rows(conn, "SELECT tenant FROM user_tenants WHERE user_id=?", (user["id"],)) session["tenants"] = [x["tenant"] for x in ut] @@ -128,7 +128,6 @@ def auth_me(): tenants = session.get("tenants", []) tenants = [t if t != "总工作台" else "科普·无界" for t in tenants] tenants = [t if t != "学会·无界" else "学术·无界" for t in tenants] - tenants = [t for t in tenants if t != "总览" and t != "总工作台"] return jsonify({ "logged_in": True, "user": {"id": session["user_id"], "username": session["username"], "display_name": session["display_name"], "role": session["role"]}, @@ -264,7 +263,6 @@ def bootstrap(): allowed = session.get("tenants", []) allowed = [t if t != "总工作台" else "科普·无界" for t in allowed] allowed = [t if t != "学会·无界" else "学术·无界" for t in allowed] - allowed = [t for t in allowed if t != "总览" and t != "总工作台"] if tenant not in allowed: tenant = allowed[0] if allowed else "" conn = db() @@ -731,3 +729,177 @@ def delete_file(file_id): @bp.route("/api/health") def health(): return jsonify({"ok": True, "service": "opc-manager"}) + +# ---------- 总览聚合 API ---------- + +OVERVIEW_TENANTS = ["学术·无界", "科普·无界", "科研·无界", "医患·无界", "MCN·无界"] + + +def _aggregate_monthly_from_json(conn, table, tenants): + """聚合多个 tenant 的表 (含 budget_data/expense_data JSON) 到 12 个月数组""" + _year = date.today().year + months = [f"{_year}-{m:02d}" for m in range(1, 13)] + rev, gross, payment, cost, paid = [0]*12, [0]*12, [0]*12, [0]*12, [0]*12 + sign_total = 0 + sign_cnt = 0 + + for tenant in tenants: + pfs = rows(conn, f"SELECT sign_amount, sign_month, budget_data, expense_data FROM {table} WHERE tenant=?", [tenant]) + for pf in pfs: + sm = pf.get("sign_month") or "" + if sm.startswith(str(_year)): + sign_total += float(pf.get("sign_amount") or 0) + sign_cnt += 1 + try: + bd = json.loads(pf.get("budget_data") or "[]") + except: + bd = [] + try: + ed = json.loads(pf.get("expense_data") or "[]") + except: + ed = [] + for b in bd: + m = (b.get("month") or "")[:7] + if m in months: + i = months.index(m) + rev[i] += float(b.get("rev") or 0) + gross[i] += float(b.get("gross") or 0) + payment[i] += float(b.get("payment") or 0) + for e in ed: + m = (e.get("month") or "")[:7] + if m in months: + i = months.index(m) + cost[i] += float(e.get("cost") or 0) + paid[i] += float(e.get("paid") or 0) + + return { + "months": months, + "rev": rev, "gross": gross, "payment": payment, "cost": cost, "paid": paid, + "cashflow": [payment[i] - paid[i] for i in range(12)], + "sign_total": sign_total, "sign_cnt": sign_cnt, + } + + +def _aggregate_expenses(conn, table, tenants): + """聚合多个 tenant 的平台费用到 12 个月""" + _year = date.today().year + months = [f"{_year}-{m:02d}" for m in range(1, 13)] + amount = [0]*12 + incurred = [0]*12 + for tenant in tenants: + exps = rows(conn, f"SELECT expense_month, amount, incurred_amount FROM {table} WHERE tenant=? AND expense_month LIKE ?", [tenant, f"{_year}-%"]) + for e in exps: + m = (e.get("expense_month") or "")[:7] + if m in months: + i = months.index(m) + amount[i] += float(e.get("amount") or 0) + incurred[i] += float(e.get("incurred_amount") or 0) + return amount, incurred + + +def _build_overview_result(agg, exp_amount, exp_incurred): + """根据聚合数据构建前端使用的 overview JSON""" + months = agg["months"] + rev = agg["rev"] + gross = agg["gross"] + payment = agg["payment"] + cost = agg["cost"] + paid = agg["paid"] + cf = agg["cashflow"] + + # 部门损益情况 + dept_profit = [gross[i] - exp_amount[i] for i in range(12)] + dept = { + "project_gross": gross, + "dept_expense": exp_amount, + "profit": dept_profit, + "annual": [sum(gross), sum(exp_amount), sum(dept_profit)], + } + + # 部门现金流 + bl_cf = [cf[i] - exp_incurred[i] for i in range(12)] + dept_cf = { + "project_cf": cf, + "dept_cf_item": exp_incurred, + "bl_cf": bl_cf, + "annual": [sum(cf), sum(exp_incurred), sum(bl_cf)], + } + + # 项目经营详情 + project = { + "sign": agg["sign_total"], + "revenue": rev, + "gross": gross, + "cost": cost, + "paid": paid, + "payment": payment, + "cashflow": cf, + "annual": [agg["sign_total"], sum(rev), sum(gross), sum(cost), sum(paid), sum(payment), sum(cf)], + } + + return {"dept": dept, "dept_cf": dept_cf, "project": project, "months": months} + + +def _quarterly_from_monthly(arr12): + """将 12 个月数组转换为 4 个季度""" + q_ranges = [(0,1,2),(3,4,5),(6,7,8),(9,10,11)] + return [arr12[q[0]]+arr12[q[1]]+arr12[q[2]] for q in q_ranges] + + +@bp.route("/api/overview/plan-monthly") +@login_required +def overview_plan_monthly(): + conn = db() + agg = _aggregate_monthly_from_json(conn, "plan_finances", OVERVIEW_TENANTS) + exp_amount, exp_incurred = _aggregate_expenses(conn, "plan_expense_records", OVERVIEW_TENANTS) + conn.close() + return jsonify(_build_overview_result(agg, exp_amount, exp_incurred)) + + +@bp.route("/api/overview/plan-quarterly") +@login_required +def overview_plan_quarterly(): + conn = db() + agg = _aggregate_monthly_from_json(conn, "plan_finances", OVERVIEW_TENANTS) + exp_amount, exp_incurred = _aggregate_expenses(conn, "plan_expense_records", OVERVIEW_TENANTS) + conn.close() + # Convert monthly to quarterly + result = _build_overview_result(agg, exp_amount, exp_incurred) + for key in ["revenue","gross","payment","cost","paid","cashflow"]: + result["project"][key] = _quarterly_from_monthly(result["project"][key]) + result["dept"]["project_gross"] = _quarterly_from_monthly(result["dept"]["project_gross"]) + result["dept"]["dept_expense"] = _quarterly_from_monthly(result["dept"]["dept_expense"]) + result["dept"]["profit"] = _quarterly_from_monthly(result["dept"]["profit"]) + result["dept_cf"]["project_cf"] = _quarterly_from_monthly(result["dept_cf"]["project_cf"]) + result["dept_cf"]["dept_cf_item"] = _quarterly_from_monthly(result["dept_cf"]["dept_cf_item"]) + result["dept_cf"]["bl_cf"] = _quarterly_from_monthly(result["dept_cf"]["bl_cf"]) + return jsonify(result) + + +@bp.route("/api/overview/finance-monthly") +@login_required +def overview_finance_monthly(): + conn = db() + agg = _aggregate_monthly_from_json(conn, "project_finances", OVERVIEW_TENANTS) + exp_amount, exp_incurred = _aggregate_expenses(conn, "expense_records", OVERVIEW_TENANTS) + conn.close() + return jsonify(_build_overview_result(agg, exp_amount, exp_incurred)) + + +@bp.route("/api/overview/finance-quarterly") +@login_required +def overview_finance_quarterly(): + conn = db() + agg = _aggregate_monthly_from_json(conn, "project_finances", OVERVIEW_TENANTS) + exp_amount, exp_incurred = _aggregate_expenses(conn, "expense_records", OVERVIEW_TENANTS) + conn.close() + result = _build_overview_result(agg, exp_amount, exp_incurred) + for key in ["revenue","gross","payment","cost","paid","cashflow"]: + result["project"][key] = _quarterly_from_monthly(result["project"][key]) + result["dept"]["project_gross"] = _quarterly_from_monthly(result["dept"]["project_gross"]) + result["dept"]["dept_expense"] = _quarterly_from_monthly(result["dept"]["dept_expense"]) + result["dept"]["profit"] = _quarterly_from_monthly(result["dept"]["profit"]) + result["dept_cf"]["project_cf"] = _quarterly_from_monthly(result["dept_cf"]["project_cf"]) + result["dept_cf"]["dept_cf_item"] = _quarterly_from_monthly(result["dept_cf"]["dept_cf_item"]) + result["dept_cf"]["bl_cf"] = _quarterly_from_monthly(result["dept_cf"]["bl_cf"]) + return jsonify(result) diff --git a/static/modules/overview_finance.js b/static/modules/overview_finance.js new file mode 100644 index 0000000..3f21807 --- /dev/null +++ b/static/modules/overview_finance.js @@ -0,0 +1,112 @@ +// overview_finance.js — 发生总览模块(5 个工作台数据聚合) + +function renderOverviewFinance() { + const view = state.overviewView || "monthly"; + const api = view === "quarterly" ? "/api/overview/finance-quarterly" : "/api/overview/finance-monthly"; + const container = document.querySelector("#overview_finance"); + if (!container) return; + container.innerHTML = '
加载中...
'; + + fetch(api).then(r => r.json()).then(data => { + renderOverviewFinanceData(data, view); + }).catch(() => { + container.innerHTML = '
加载失败
'; + }); +} + +function renderOverviewFinanceData(data, view) { + const container = document.querySelector("#overview_finance"); + if (!container) return; + + const moneyIntL = (v) => Math.round(Number(v || 0)).toLocaleString("zh-CN"); + const card = (body, cls) => '
' + body + '
'; + + const isQuarterly = view === "quarterly"; + const headers = isQuarterly ? ['Q1','Q2','Q3','Q4'] : ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月']; + const cols = isQuarterly ? 4 : 12; + + const d = data.dept; + const cf = data.dept_cf; + const p = data.project; + + // 视图切换按钮 + var viewBtns = '
'; + + // ====== 部门损益情况 ====== + var deptThead = '指标年度累计'; + for (var hi = 0; hi < cols; hi++) { deptThead += '' + headers[hi] + ''; } + deptThead += ''; + + var deptRows = [ + { label: '项目毛利', vals: d.project_gross, annual: d.annual[0] }, + { label: '部门费用', vals: d.dept_expense, annual: d.annual[1] }, + { label: '利润', vals: d.profit, annual: d.annual[2] } + ]; + var deptTbody = ''; + for (var dri = 0; dri < 3; dri++) { + var dr = deptRows[dri]; + var rowCls = dri === 2 ? 'font-semibold' : ''; + var rowHtml = '' + dr.label + ''; + var annualCls = dri === 2 ? (dr.annual >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-700'; + rowHtml += '' + moneyIntL(dr.annual) + ''; + for (var i = 0; i < cols; i++) { + var cellCls = dri === 2 ? (dr.vals[i] >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-700'; + rowHtml += '' + moneyIntL(dr.vals[i]) + ''; + } + deptTbody += rowHtml + ''; + } + var deptCard = card('

部门损益情况 部门利润 = 所有项目产生的毛利 - 部门 1 级产生的人力成本,研发成本,差旅和其他支出

' + deptThead + '' + deptTbody + '
', 'p-4'); + + // ====== 部门现金流 ====== + var cfRows = [ + { label: '项目现金流', vals: cf.project_cf, annual: cf.annual[0] }, + { label: '部门现金流', vals: cf.dept_cf_item, annual: cf.annual[1] }, + { label: '经营现金流', vals: cf.bl_cf, annual: cf.annual[2] } + ]; + var cfTbody = ''; + for (var cfri = 0; cfri < 3; cfri++) { + var cfr = cfRows[cfri]; + var cfRowCls = cfri === 2 ? 'font-semibold' : ''; + var rowHtml = '' + cfr.label + ''; + var cAnnualCls = cfri === 2 ? (cfr.annual >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-700'; + rowHtml += '' + moneyIntL(cfr.annual) + ''; + for (var i = 0; i < cols; i++) { + var cCellCls = cfri === 2 ? (cfr.vals[i] >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-700'; + rowHtml += '' + moneyIntL(cfr.vals[i]) + ''; + } + cfTbody += rowHtml + ''; + } + var cfCard = card('

部门现金流 部门现金流=所有项目产生的现金流 - 部门一级因费用产生的现金流出或流入

' + deptThead + '' + cfTbody + '
', 'p-4'); + + // ====== 项目经营详情 ====== + var LABELS = ['合同金额','确收金额','确收毛利','成本','支出','回款','项目现金流']; + var projVals = [p.revenue, p.gross, p.cost, p.paid, p.payment, p.cashflow]; + var projAnnual = p.annual; + + var projThead = '指标年度累计'; + for (var phi = 0; phi < cols; phi++) { projThead += '' + headers[phi] + ''; } + projThead += ''; + + var projTbody = ''; + for (var ri = 0; ri < 7; ri++) { + var rowHtml = '' + LABELS[ri] + ''; + rowHtml += '' + (ri === 0 ? moneyIntL(p.sign) : moneyIntL(projAnnual[ri])) + ''; + for (var i = 0; i < cols; i++) { + var val = ri === 0 ? 0 : (projVals[ri - 1] ? projVals[ri - 1][i] : 0); + var cls = 'text-slate-700'; + if (ri === 2) cls = 'text-green-600'; + if (ri === 6) cls = val >= 0 ? 'text-green-600' : 'text-red-600'; + rowHtml += '' + moneyIntL(val) + ''; + } + projTbody += rowHtml + ''; + } + var projCard = card('

项目经营详情 所有项目产生的签单,确收,毛利,回款,当期成本,当期流出,现金流

' + projThead + '' + projTbody + '
', 'p-4'); + + container.innerHTML = '
' + viewBtns + deptCard + cfCard + projCard + '
'; + if (window.lucide) window.lucide.createIcons(); +} + +window.setOverviewFinanceView = (v) => { + state.overviewView = v; + renderOverviewFinance(); +}; diff --git a/static/modules/overview_plan.js b/static/modules/overview_plan.js new file mode 100644 index 0000000..35d0c3e --- /dev/null +++ b/static/modules/overview_plan.js @@ -0,0 +1,112 @@ +// overview_plan.js — 预算总览模块(5 个工作台数据聚合) + +function renderOverviewPlan() { + const view = state.overviewView || "monthly"; + const api = view === "quarterly" ? "/api/overview/plan-quarterly" : "/api/overview/plan-monthly"; + const container = document.querySelector("#overview_plan"); + if (!container) return; + container.innerHTML = '
加载中...
'; + + fetch(api).then(r => r.json()).then(data => { + renderOverviewPlanData(data, view); + }).catch(() => { + container.innerHTML = '
加载失败
'; + }); +} + +function renderOverviewPlanData(data, view) { + const container = document.querySelector("#overview_plan"); + if (!container) return; + + const moneyIntL = (v) => Math.round(Number(v || 0)).toLocaleString("zh-CN"); + const card = (body, cls) => '
' + body + '
'; + + const isQuarterly = view === "quarterly"; + const headers = isQuarterly ? ['Q1','Q2','Q3','Q4'] : ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月']; + const cols = isQuarterly ? 4 : 12; + + const d = data.dept; + const cf = data.dept_cf; + const p = data.project; + + // ====== 部门损益情况 ====== + var deptThead = '指标年度累计'; + for (var hi = 0; hi < cols; hi++) { deptThead += '' + headers[hi] + ''; } + deptThead += ''; + + var deptRows = [ + { label: '项目毛利', vals: d.project_gross, annual: d.annual[0] }, + { label: '部门费用', vals: d.dept_expense, annual: d.annual[1] }, + { label: '利润', vals: d.profit, annual: d.annual[2] } + ]; + var deptTbody = ''; + for (var dri = 0; dri < 3; dri++) { + var dr = deptRows[dri]; + var rowCls = dri === 2 ? 'font-semibold' : ''; + var rowHtml = '' + dr.label + ''; + var annualCls = dri === 2 ? (dr.annual >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-700'; + rowHtml += '' + moneyIntL(dr.annual) + ''; + for (var i = 0; i < cols; i++) { + var cellCls = dri === 2 ? (dr.vals[i] >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-700'; + rowHtml += '' + moneyIntL(dr.vals[i]) + ''; + } + deptTbody += rowHtml + ''; + } + var deptCard = card('

部门损益情况 部门利润 = 所有项目产生的毛利 - 部门 1 级产生的人力成本,研发成本,差旅和其他支出

' + deptThead + '' + deptTbody + '
', 'p-4'); + + // ====== 部门现金流 ====== + var cfRows = [ + { label: '项目现金流', vals: cf.project_cf, annual: cf.annual[0] }, + { label: '部门现金流', vals: cf.dept_cf_item, annual: cf.annual[1] }, + { label: '经营现金流', vals: cf.bl_cf, annual: cf.annual[2] } + ]; + var cfTbody = ''; + for (var cfri = 0; cfri < 3; cfri++) { + var cfr = cfRows[cfri]; + var cfRowCls = cfri === 2 ? 'font-semibold' : ''; + var rowHtml = '' + cfr.label + ''; + var cAnnualCls = cfri === 2 ? (cfr.annual >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-700'; + rowHtml += '' + moneyIntL(cfr.annual) + ''; + for (var i = 0; i < cols; i++) { + var cCellCls = cfri === 2 ? (cfr.vals[i] >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-700'; + rowHtml += '' + moneyIntL(cfr.vals[i]) + ''; + } + cfTbody += rowHtml + ''; + } + var cfCard = card('

部门现金流 部门现金流=所有项目产生的现金流 - 部门一级因费用产生的现金流出或流入

' + deptThead + '' + cfTbody + '
', 'p-4'); + + // ====== 项目经营详情 ====== + var LABELS = ['合同金额','确收金额','确收毛利','成本','支出','回款','项目现金流']; + var projVals = [p.revenue, p.gross, p.cost, p.paid, p.payment, p.cashflow]; + var projAnnual = p.annual; // [sign, revenue_sum, gross_sum, cost_sum, paid_sum, payment_sum, cf_sum] + + var projThead = '指标年度累计'; + for (var phi = 0; phi < cols; phi++) { projThead += '' + headers[phi] + ''; } + projThead += ''; + + var projTbody = ''; + for (var ri = 0; ri < 7; ri++) { + var rowHtml = '' + LABELS[ri] + ''; + rowHtml += '' + (ri === 0 ? moneyIntL(p.sign) : moneyIntL(projAnnual[ri])) + ''; + for (var i = 0; i < cols; i++) { + var val = ri === 0 ? 0 : (projVals[ri - 1] ? projVals[ri - 1][i] : 0); + var cls = 'text-slate-700'; + if (ri === 2) cls = 'text-green-600'; + if (ri === 6) cls = val >= 0 ? 'text-green-600' : 'text-red-600'; + rowHtml += '' + moneyIntL(val) + ''; + } + projTbody += rowHtml + ''; + } + var projCard = card('

项目经营详情 所有项目产生的签单,确收,毛利,回款,当期成本,当期流出,现金流

' + projThead + '' + projTbody + '
', 'p-4'); + + // 视图切换按钮 + var viewBtns = '
'; + + container.innerHTML = '
' + viewBtns + deptCard + cfCard + projCard + '
'; + if (window.lucide) window.lucide.createIcons(); +} + +window.setOverviewView = (v) => { + state.overviewView = v; + renderOverviewPlan(); +}; diff --git a/static/modules/projects.js b/static/modules/projects.js index 28c6247..9e56f1d 100644 --- a/static/modules/projects.js +++ b/static/modules/projects.js @@ -32,7 +32,7 @@ window.toggleTenantMenu = (event) => { 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; }); + 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; }); menu = document.createElement("div"); menu.id = "tenantMenu"; menu.className = "fixed bg-white rounded-lg shadow-xl border border-slate-200 py-1 min-w-[160px] z-[9999]"; @@ -63,7 +63,7 @@ window.switchTenantFromMenu = (tenant) => { }; function updateTenantLabel() { - 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; }); + 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', @@ -92,7 +92,7 @@ window.toggleTenantMenu = (event) => { 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; }); + 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', diff --git a/static/modules/utils.js b/static/modules/utils.js index 037730a..f5d98ca 100644 --- a/static/modules/utils.js +++ b/static/modules/utils.js @@ -17,6 +17,8 @@ const state = { planStatusFilter: "all", planView: "overview", planSort: null, + overviewView: "monthly", // 总览视图: monthly | quarterly + overviewFilter: "plan", // 总览筛选: plan | finance proposalTab: "standard", chart: null, chart2: null, @@ -120,6 +122,8 @@ function renderTable(headers, rows, rowClicks) { var _loadingSteps = [ { fn: 'renderHome', label: '正在加载首页仪表盘(指标卡片+图表)', skip: function() { return state.tenant !== '总览'; } }, + { fn: 'renderOverviewPlan', label: '正在加载预算总览', skip: function() { return state.tenant !== '总览'; } }, + { fn: 'renderOverviewFinance', label: '正在加载发生总览', skip: function() { return state.tenant !== '总览'; } }, { fn: 'renderProjects', label: '正在加载业务机会列表' }, { fn: 'renderProposals', label: '正在加载业务方案列表' }, { fn: 'renderProducts', label: '正在加载产品迭代列表' }, @@ -232,7 +236,7 @@ var _tabLoaded = {}; var _proposalsInitialized = false; async function ensureTabData(tab) { - if (tab === 'home' || tab === 'performance') return; + if (tab === 'home' || tab === 'performance' || tab === 'overview_plan' || tab === 'overview_finance') return; var resource = _tabResourceMap[tab]; if (!resource) return; var dataKey = resource; @@ -262,7 +266,7 @@ 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: '季度绩效考核', overview_plan: '预算总览', overview_finance: '发生总览' }; const titleEl = document.querySelector('#workspaceTitle'); if (titleEl) titleEl.textContent = titles[tab] || state.tenant + ' OPC 工作台'; @@ -278,9 +282,9 @@ function updateSidebarTabs() { document.querySelectorAll(".sidebar-tab").forEach((btn) => { const tab = btn.dataset.tab; if (isOverview) { - btn.style.display = (tab === "home" || tab === "plan" || tab === "finance") ? "" : "none"; + btn.style.display = (tab === "home" || tab === "overview_plan" || tab === "overview_finance") ? "" : "none"; } else { - btn.style.display = tab === "home" ? "none" : ""; + btn.style.display = (tab === "overview_plan" || tab === "overview_finance" || tab === "home") ? "none" : ""; } }); } @@ -301,6 +305,8 @@ function renderActive() { if (!state.data) return; const tab = state.active; if (tab === "home") renderHome(); + else if (tab === "overview_plan") renderOverviewPlan(); + else if (tab === "overview_finance") renderOverviewFinance(); else if (tab === "projects") renderProjects(); else if (tab === "proposals") renderProposals(); else if (tab === "products") renderProducts(); @@ -388,12 +394,22 @@ window.switchTenant = async (tenant) => { state.tenant = tenant; state.selectedProject = null; localStorage.setItem("opc-active-tenant", tenant); - document.querySelector("#workspaceTitle").textContent = tenant.replace("·无界", "") + " OPC 工作台"; + document.querySelector("#workspaceTitle").textContent = tenant === "总览" ? "总览 OPC 工作台" : tenant.replace("·无界", "") + " OPC 工作台"; if (typeof updateTenantLabel === "function") updateTenantLabel(); state.planFilter = 'overview'; state.finFilter = 'overview'; updateSidebarTabs(); - 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")); } + if (tenant === "总览") { + state.active = "overview_plan"; + localStorage.setItem("opc-active-tab", "overview_plan"); + document.querySelectorAll(".sidebar-tab").forEach((btn) => btn.classList.toggle("active", btn.dataset.tab === "overview_plan")); + document.querySelectorAll(".panel").forEach((panel) => panel.classList.toggle("active", panel.id === "overview_plan")); + } else if (state.active === 'home' || state.active === 'overview_plan' || state.active === 'overview_finance') { + 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(); }; diff --git a/templates/index.html b/templates/index.html index d5b99e7..eccf9bc 100644 --- a/templates/index.html +++ b/templates/index.html @@ -53,6 +53,14 @@ 已发生 + +