diff --git a/backend/flask_app.py b/backend/flask_app.py
index 8637b6f..1fcfc21 100644
--- a/backend/flask_app.py
+++ b/backend/flask_app.py
@@ -67,7 +67,7 @@ def admin_required(f):
return decorated
-ALL_TENANTS = ["科普·无界", "科研·无界", "医患·无界", "MCN·无界", "学会·无界"]
+ALL_TENANTS = ["总工作台", "科普·无界", "科研·无界", "医患·无界", "MCN·无界", "学会·无界"]
@app.route("/login")
def login_page():
@@ -90,7 +90,7 @@ def auth_login():
session["role"] = user["role"]
# 管理员可看所有工作台,OPC负责人看分配的工作台
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]
@@ -113,10 +113,14 @@ def auth_logout():
def auth_me():
if "user_id" not in session:
return jsonify({"logged_in": False})
+ tenants = session.get("tenants", [])
+ # 确保总工作台始终在列表最前
+ if "总工作台" not in tenants:
+ tenants = ["总工作台"] + tenants
return jsonify({
"logged_in": True,
"user": {"id": session["user_id"], "username": session["username"], "display_name": session["display_name"], "role": session["role"]},
- "tenants": session.get("tenants", []),
+ "tenants": tenants,
})
@@ -748,10 +752,88 @@ def bootstrap():
tenant = request.args.get("tenant", session.get("tenants", ["科普·无界"])[0])
# 验证用户是否有权限访问该 workbench
allowed = session.get("tenants", [])
+ if "总工作台" not in allowed:
+ allowed = ["总工作台"] + allowed
if tenant not in allowed:
tenant = allowed[0]
conn = db()
try:
+ # 总工作台:聚合所有工作台的首页数据
+ if tenant == "总工作台":
+ real_tenants = [t for t in allowed if t != "总工作台"]
+ all_metrics = []
+ all_monthly = []
+ all_recent = []
+ for t in real_tenants:
+ t_pfs = rows(conn, "SELECT * FROM project_finances WHERE tenant=? ORDER BY id DESC", [t])
+ t_ops = attach_common(conn, "operations", rows(conn, "SELECT * FROM operation_projects WHERE tenant=? ORDER BY id ASC", [t]))
+ t_sales = attach_common(conn, "sales", rows(conn, "SELECT * FROM sales_leads WHERE tenant=? ORDER BY id DESC", [t]))
+ t_products = attach_common(conn, "products", rows(conn, "SELECT * FROM product_versions WHERE tenant=? ORDER BY id DESC", [t]))
+ t_proposals = attach_common(conn, "proposals", rows(conn, "SELECT * FROM business_proposals WHERE tenant=? ORDER BY id DESC", [t]))
+ t_signed_pfs = [x for x in t_pfs if x["status"] == "已签约"]
+ def t_parse_budget(pf):
+ try:
+ budget = json.loads(pf.get("budget_data") or "[]")
+ except (json.JSONDecodeError, TypeError):
+ budget = []
+ return {(b.get("month") or "").replace("-", "_"): b for b in budget}
+ t_bm = {pf["id"]: t_parse_budget(pf) for pf in t_pfs}
+ def t_sum_budget(field, months_range):
+ total = 0
+ for pf in t_pfs:
+ bm = t_bm.get(pf["id"], {})
+ for m in months_range:
+ b = bm.get(f"2026_{m:02d}")
+ if b:
+ total += float(b.get(field) or 0)
+ return total
+ _now_month = date.today().month
+ _q_start = ((_now_month - 1) // 3) * 3 + 1
+ _q_range = range(_q_start, _q_start + 3)
+ _q_months = [f"2026-{m:02d}" for m in _q_range]
+ all_metrics.append({
+ "total_projects": len(t_signed_pfs),
+ "total_proposals": len(t_ops),
+ "total_products": len(t_proposals),
+ "upcoming_products": len(t_products),
+ "signed_amount": sum(x["sign_amount"] or 0 for x in t_pfs if x["status"] == "已签约"),
+ "signed_annual": sum(x["sign_amount"] or 0 for x in t_pfs if x["status"] == "已签约"),
+ "signed_q2": sum(x["sign_amount"] or 0 for x in t_pfs if x["status"] == "已签约" and (x.get("sign_month") or "")[:7] in _q_months),
+ "signed_month": sum(x["sign_amount"] or 0 for x in t_pfs if x["status"] == "已签约" and (x.get("sign_month") or "")[:7] == f"2026-{_now_month:02d}"),
+ "revenue_annual": t_sum_budget("rev", range(1, 13)),
+ "revenue_q2": t_sum_budget("rev", _q_range),
+ "monthly_revenue": t_sum_budget("rev", [_now_month]),
+ "gross_annual": t_sum_budget("gross", range(1, 13)),
+ "gross_q2": t_sum_budget("gross", _q_range),
+ "monthly_net_profit": t_sum_budget("gross", [_now_month]),
+ "payment_annual": t_sum_budget("payment", range(1, 13)),
+ "payment_q2": t_sum_budget("payment", _q_range),
+ "payment_month": t_sum_budget("payment", [_now_month]),
+ "cost_annual": t_sum_budget("cost", range(1, 13)),
+ "cost_q2": t_sum_budget("cost", _q_range),
+ "cost_month": t_sum_budget("cost", [_now_month]),
+ })
+ all_monthly.append(monthly_finance(conn, t))
+ all_recent.extend(rows(conn, "SELECT * FROM follow_up_records WHERE tenant=? ORDER BY id DESC LIMIT 4", [t]))
+ # 合并 metrics
+ agg = {}
+ for key in ["total_projects","total_proposals","total_products","upcoming_products","signed_amount","signed_annual","signed_q2","signed_month","revenue_annual","revenue_q2","monthly_revenue","gross_annual","gross_q2","monthly_net_profit","payment_annual","payment_q2","payment_month","cost_annual","cost_q2","cost_month"]:
+ agg[key] = sum(m.get(key, 0) for m in all_metrics)
+ # 合并 monthly finance(按月累加)
+ merged_monthly = []
+ for i in range(12):
+ m = {"month": all_monthly[0][i]["month"] if all_monthly and len(all_monthly[0]) > i else f"2026-{i+1:02d}"}
+ for field in ["revenue","gross","payment","cost","sign"]:
+ m[field] = sum(tl[i][field] if i < len(tl) else 0 for tl in all_monthly)
+ merged_monthly.append(m)
+ summary = {
+ "project_name": "总工作台",
+ "metrics": agg,
+ "recent": sorted(all_recent, key=lambda x: x.get("id", 0), reverse=True)[:8],
+ "risks": [],
+ }
+ return jsonify({"summary": summary, "sales": [], "proposals": [], "operations": [], "products": [], "finance": [], "projectFinances": [], "financeMonthly": merged_monthly, "tasks": [], "tenant": tenant, "tenants": allowed})
+
def q(sql, *args):
return rows(conn, sql, args)
sales = attach_common(conn, "sales", q("SELECT * FROM sales_leads WHERE tenant=? ORDER BY id DESC", tenant))
@@ -859,7 +941,7 @@ TABLES = {
"products": ("product_versions", ["product_name", "version", "version_goal", "priority", "start_date", "plan_date", "dev_done_date", "test_date", "launch_date", "status", "notes", "tenant"]),
"finance": ("finance_records", ["month", "project_name", "record_type", "category", "amount", "occurred_date", "notes", "tenant"]),
"tasks": ("project_tasks", ["project_id", "phase", "milestone", "task", "owner", "due_date", "blockers", "notes", "status", "sort_order", "priority", "tenant"]),
- "projectFinances": ("project_finances", ["project_id", "tenant", "business_type", "customer_name", "sign_amount", "sign_month", "status", "sales_person", "owner", "total_rev", "total_gross", "budget_data"]),
+ "projectFinances": ("project_finances", ["project_id", "tenant", "business_type", "customer_name", "sign_amount", "sign_month", "status", "sales_person", "owner", "total_rev", "total_gross", "total_payment", "total_cost", "total_paid", "budget_data"]),
}
diff --git a/backend/migrations/columns.py b/backend/migrations/columns.py
index e11b67c..8587db9 100644
--- a/backend/migrations/columns.py
+++ b/backend/migrations/columns.py
@@ -67,6 +67,14 @@ def migrate_add_columns():
_add_column_if_missing(conn, "project_finances", col,
f"ALTER TABLE project_finances ADD COLUMN {col} DOUBLE NOT NULL DEFAULT 0")
+ # project_finances 总视图字段:已回款 / 应付 / 已付
+ _add_column_if_missing(conn, "project_finances", "total_payment",
+ "ALTER TABLE project_finances ADD COLUMN total_payment DOUBLE NOT NULL DEFAULT 0")
+ _add_column_if_missing(conn, "project_finances", "total_cost",
+ "ALTER TABLE project_finances ADD COLUMN total_cost DOUBLE NOT NULL DEFAULT 0")
+ _add_column_if_missing(conn, "project_finances", "total_paid",
+ "ALTER TABLE project_finances ADD COLUMN total_paid DOUBLE NOT NULL DEFAULT 0")
+
conn.commit()
print("[migrate] 加列迁移完成")
finally:
diff --git a/static/app.js b/static/app.js
index 6bd8d5e..195bc3f 100644
--- a/static/app.js
+++ b/static/app.js
@@ -13,8 +13,13 @@ const savedTab = localStorage.getItem("opc-active-tab");
// 初始化
applyUserTenants();
+updateSidebarTabs();
load().then(() => {
- if (savedTab && savedTab !== "home") switchTab(savedTab);
+ if (state.tenant === "总工作台") {
+ switchTab("home");
+ } else if (savedTab && savedTab !== "home") {
+ switchTab(savedTab);
+ }
}).catch((error) => {
document.querySelector("main").innerHTML = `
填写项目财务信息与月度预算
总确收
¥0
@@ -157,18 +157,66 @@ function renderFinance() {¥0
总费用
+总应付
¥0
总已付
+¥0
+| 月份 | 确收 | 毛利 | 回款 | 费用 | ||
|---|---|---|---|---|---|---|
| 月份 | 确收 | 毛利 | 回款 | 应付 | 已付 |
| 项目名称 | 类型 | 状态 | 签约月份 | 签约金额 | ${monthLabels.map(l => `${l} ${state.finView !== 'cashflow' ? '确收/毛利' : '回款/费用'} | `).join("")}总计 ${state.finView !== 'cashflow' ? '确收/毛利' : '回款/费用'} | 商务负责人 | 经营负责人 |
|---|
| 项目名称 | 类型 | 状态 | 已确收 | 已回款 | 回款差额 | 应付 | 已付 | 应付差额 | 现金流 |
|---|---|---|---|---|---|---|---|---|---|
| 该月份暂无数据 | |||||||||
| 合计 | ${money(sumRev)} | ${money(sumPay)} | ${fmtDiff(sumRev - sumPay)} | ${money(sumCost)} | ${money(sumPaid)} | ${fmtDiff(sumCost - sumPaid)} | ${money(sumPay - sumPaid)} | ||
| 项目名称 | 类型 | 状态 | 已确收 | 已回款 | 回款差额 | 应付 | 已付 | 应付差额 | 现金流 |
|---|---|---|---|---|---|---|---|---|---|
| ${esc(pf.customer_name)} | ${esc(pf.business_type)} | ${pf.status === '已签约' ? badge('已签约') : pf.status === '流程中' ? badge('流程中','blue') : badge('待签约','amber')} | ${fmt(t.rev)} | ${fmt(t.payment)} | ${fmtDiff(payDiff)} | ${fmt(t.cost)} | ${fmt(t.paid)} | ${fmtDiff(costDiff)} | ${cashflow ? money(cashflow) : '—'} |
| 合计 | ${money(sumRev)} | ${money(sumPay)} | ${fmtDiff(sumRev - sumPay)} | ${money(sumCost)} | ${money(sumPaid)} | ${fmtDiff(sumCost - sumPaid)} | ${money(sumPay - sumPaid)} | ||
| ${label} | ${value} |