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 @@
已发生
+
+