Compare commits

...

7 Commits

Author SHA1 Message Date
mac
63ddffb12a fix: 补全finance/plan筛选器控制函数
- 新增setFinYear/setPlanYear等年份/季度/月度/视图切换函数
- 新增switchFinFilter/switchPlanFilter tab切换函数
- 新增setPlanStatusFilter状态筛选函数
- 版本号 v1.2.0.31
2026-07-11 15:27:12 +08:00
mac
2c7cd21095 fix: 待回款列移至回款后面,正数显示+前缀
- finance.js/plan.js SIGNED_COLS: 待回款列移到回款与回款率之间
- 行渲染同步调序, 补回意外删除的cost/paid/cf列定义
- 正数待回款显示为+金额
- 版本号 v1.2.0.30
2026-07-11 15:23:16 +08:00
mac
9273be3143 fix: 发生总览损益/现金流详细报表补上零值月份跳过逻辑
- renderFinanceDeptMatrixData和renderFinanceCfMatrixData添加isAllZero检查
- 与预算总览三个详细报表行为对齐
- 版本号 v1.2.0.29
2026-07-11 15:10:16 +08:00
mac
2aab73a86a fix: 项目经营详细报表零值月份检查排除签约行
- isAllZero跳过sign(合同金额)行, 仅其他指标全零时隐藏月份
- 发生总览和预算总览均已同步
- 版本号 v1.2.0.28
2026-07-11 15:06:23 +08:00
mac
e0000c2abb fix: 总览工作台首次加载跳过提案初始化避免卡顿
- 总览为虚拟聚合视图无proposal数据, 首次加载时missingCount=7触发ensureStandardProposals
- 新增state.tenant!=='总览'条件跳过提案初始化
- 版本号 v1.2.0.27
2026-07-11 14:54:28 +08:00
mac
356c129668 feat: 总览工作台新增损益/现金流/项目经营详细报表
- 预算总览和发生总览各新增3个tab: 损益详细报表、现金流详细报表、项目经营详细报表
- 12个月卡片布局, 按部门(学术/科普/科研/医患/MCN)展示矩阵
- 后端新增6个聚合API: dept-matrix/cf-matrix/project-matrix x 2
- tab改为finance-tab风格, 与经营月报一致
- 修复项目报表函数未注册导致的加载失败
- 版本号 v1.2.0.26
2026-07-11 14:36:00 +08:00
mac
94b9f25938 fix: 总览工作台tab显示问题
- 移除hidden class避免CSS覆盖style.display
- 总览模式下隐藏部门和预算/已发生tab
- 只显示预算总览和发生总览
- 版本号 v1.2.0.25
2026-07-11 12:31:50 +08:00
7 changed files with 774 additions and 48 deletions

View File

@@ -724,6 +724,359 @@ def delete_file(file_id):
conn.close()
# ---------- 总览部门损益矩阵 API ----------
@bp.route("/api/overview/dept-matrix")
@login_required
def overview_dept_matrix():
"""返回各工作台的损益数据矩阵(按年/季/月筛选)"""
conn = db()
_year = int(request.args.get("year", date.today().year))
view = request.args.get("view", "monthly")
# 12个月数组
months = [f"{_year}-{m:02d}" for m in range(1, 13)]
tenants = ["学术·无界", "科普·无界", "科研·无界", "医患·无界", "MCN·无界"]
result = {"tenants": tenants, "months": months, "data": {}, "total": {"project_gross": [0]*12, "dept_expense": [0]*12, "profit": [0]*12}}
for tenant in tenants:
# 项目毛利(从 budget_data JSON
pfs = rows(conn, "SELECT budget_data FROM plan_finances WHERE tenant=?", [tenant])
gross = [0]*12
for pf in pfs:
try:
bd = json.loads(pf.get("budget_data") or "[]")
except:
bd = []
for b in bd:
m = (b.get("month") or "")[:7]
if m in months:
i = months.index(m)
gross[i] += float(b.get("gross") or 0)
# 部门费用(从 plan_expense_records
expense = [0]*12
exps = rows(conn, "SELECT expense_month, amount FROM plan_expense_records WHERE tenant=? AND expense_month LIKE %s", [tenant, f"{_year}-%"])
for e in exps:
m = (e.get("expense_month") or "")[:7]
if m in months:
i = months.index(m)
expense[i] += float(e.get("amount") or 0)
profit = [gross[i] - expense[i] for i in range(12)]
result["data"][tenant] = {
"project_gross": gross,
"dept_expense": expense,
"profit": profit
}
for i in range(12):
result["total"]["project_gross"][i] += gross[i]
result["total"]["dept_expense"][i] += expense[i]
result["total"]["profit"][i] += profit[i]
# 按视图聚合
if view == "quarterly":
def to_quarterly(arr12):
qr = [(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 qr]
result["months"] = ["Q1","Q2","Q3","Q4"]
for t in tenants:
for key in ["project_gross","dept_expense","profit"]:
result["data"][t][key] = to_quarterly(result["data"][t][key])
for key in ["project_gross","dept_expense","profit"]:
result["total"][key] = to_quarterly(result["total"][key])
elif view == "monthly":
sel_month = request.args.get("month", "")
if sel_month and sel_month in months:
mi = months.index(sel_month)
result["months"] = [sel_month]
for t in tenants:
for key in ["project_gross","dept_expense","profit"]:
result["data"][t][key] = [result["data"][t][key][mi]]
for key in ["project_gross","dept_expense","profit"]:
result["total"][key] = [result["total"][key][mi]]
# else: yearly (default) — keep all 12 months
# Compute annual totals
result["annual"] = {}
for t in tenants:
d = result["data"][t]
result["annual"][t] = {
"project_gross": sum(d["project_gross"]),
"dept_expense": sum(d["dept_expense"]),
"profit": sum(d["profit"])
}
result["annual"]["total"] = {
"project_gross": sum(result["total"]["project_gross"]),
"dept_expense": sum(result["total"]["dept_expense"]),
"profit": sum(result["total"]["profit"])
}
conn.close()
return jsonify(result)
@bp.route("/api/overview/finance-dept-matrix")
@login_required
def overview_finance_dept_matrix():
"""返回各工作台已发生损益数据矩阵"""
conn = db()
_year = date.today().year
months = [f"{_year}-{m:02d}" for m in range(1, 13)]
tenants = ["学术·无界", "科普·无界", "科研·无界", "医患·无界", "MCN·无界"]
result = {"tenants": tenants, "months": months, "data": {}, "total": {"project_gross": [0]*12, "dept_expense": [0]*12, "profit": [0]*12}}
for tenant in tenants:
pfs = rows(conn, "SELECT budget_data FROM project_finances WHERE tenant=?", [tenant])
gross = [0]*12
for pf in pfs:
try:
bd = json.loads(pf.get("budget_data") or "[]")
except:
bd = []
for b in bd:
m = (b.get("month") or "")[:7]
if m in months:
i = months.index(m)
gross[i] += float(b.get("gross") or 0)
expense = [0]*12
exps = rows(conn, "SELECT expense_month, amount FROM expense_records WHERE tenant=? AND expense_month LIKE %s", [tenant, f"{_year}-%"])
for e in exps:
m = (e.get("expense_month") or "")[:7]
if m in months:
i = months.index(m)
expense[i] += float(e.get("amount") or 0)
profit = [gross[i] - expense[i] for i in range(12)]
result["data"][tenant] = {"project_gross": gross, "dept_expense": expense, "profit": profit}
for i in range(12):
result["total"]["project_gross"][i] += gross[i]
result["total"]["dept_expense"][i] += expense[i]
result["total"]["profit"][i] += profit[i]
result["annual"] = {}
for t in tenants:
d = result["data"][t]
result["annual"][t] = {"project_gross": sum(d["project_gross"]), "dept_expense": sum(d["dept_expense"]), "profit": sum(d["profit"])}
result["annual"]["total"] = {"project_gross": sum(result["total"]["project_gross"]), "dept_expense": sum(result["total"]["dept_expense"]), "profit": sum(result["total"]["profit"])}
conn.close()
return jsonify(result)
@bp.route("/api/overview/dept-cf-matrix")
@login_required
def overview_dept_cf_matrix():
"""返回各工作台预算现金流数据矩阵"""
conn = db()
_year = date.today().year
months = [f"{_year}-{m:02d}" for m in range(1, 13)]
tenants = ["学术·无界", "科普·无界", "科研·无界", "医患·无界", "MCN·无界"]
result = {"tenants": tenants, "months": months, "data": {}, "total": {"project_cf": [0]*12, "dept_cf": [0]*12, "bl_cf": [0]*12}}
for tenant in tenants:
pfs = rows(conn, "SELECT budget_data, expense_data FROM plan_finances WHERE tenant=?", [tenant])
project_cf = [0]*12
for pf in pfs:
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)
project_cf[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)
project_cf[i] -= float(e.get("paid") or 0)
dept_cf = [0]*12
exps = rows(conn, "SELECT expense_month, incurred_amount FROM plan_expense_records WHERE tenant=? AND expense_month LIKE %s", [tenant, f"{_year}-%"])
for e in exps:
m = (e.get("expense_month") or "")[:7]
if m in months:
i = months.index(m)
dept_cf[i] += float(e.get("incurred_amount") or 0)
bl_cf = [project_cf[i] - dept_cf[i] for i in range(12)]
result["data"][tenant] = {"project_cf": project_cf, "dept_cf": dept_cf, "bl_cf": bl_cf}
for i in range(12):
result["total"]["project_cf"][i] += project_cf[i]
result["total"]["dept_cf"][i] += dept_cf[i]
result["total"]["bl_cf"][i] += bl_cf[i]
result["annual"] = {}
for t in tenants:
d = result["data"][t]
result["annual"][t] = {"project_cf": sum(d["project_cf"]), "dept_cf": sum(d["dept_cf"]), "bl_cf": sum(d["bl_cf"])}
result["annual"]["total"] = {"project_cf": sum(result["total"]["project_cf"]), "dept_cf": sum(result["total"]["dept_cf"]), "bl_cf": sum(result["total"]["bl_cf"])}
conn.close()
return jsonify(result)
@bp.route("/api/overview/finance-dept-cf-matrix")
@login_required
def overview_finance_dept_cf_matrix():
"""返回各工作台已发生现金流数据矩阵"""
conn = db()
_year = date.today().year
months = [f"{_year}-{m:02d}" for m in range(1, 13)]
tenants = ["学术·无界", "科普·无界", "科研·无界", "医患·无界", "MCN·无界"]
result = {"tenants": tenants, "months": months, "data": {}, "total": {"project_cf": [0]*12, "dept_cf": [0]*12, "bl_cf": [0]*12}}
for tenant in tenants:
pfs = rows(conn, "SELECT budget_data, expense_data FROM project_finances WHERE tenant=?", [tenant])
project_cf = [0]*12
for pf in pfs:
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)
project_cf[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)
project_cf[i] -= float(e.get("paid") or 0)
dept_cf = [0]*12
exps = rows(conn, "SELECT expense_month, incurred_amount FROM expense_records WHERE tenant=? AND expense_month LIKE %s", [tenant, f"{_year}-%"])
for e in exps:
m = (e.get("expense_month") or "")[:7]
if m in months:
i = months.index(m)
dept_cf[i] += float(e.get("incurred_amount") or 0)
bl_cf = [project_cf[i] - dept_cf[i] for i in range(12)]
result["data"][tenant] = {"project_cf": project_cf, "dept_cf": dept_cf, "bl_cf": bl_cf}
for i in range(12):
result["total"]["project_cf"][i] += project_cf[i]
result["total"]["dept_cf"][i] += dept_cf[i]
result["total"]["bl_cf"][i] += bl_cf[i]
result["annual"] = {}
for t in tenants:
d = result["data"][t]
result["annual"][t] = {"project_cf": sum(d["project_cf"]), "dept_cf": sum(d["dept_cf"]), "bl_cf": sum(d["bl_cf"])}
result["annual"]["total"] = {"project_cf": sum(result["total"]["project_cf"]), "dept_cf": sum(result["total"]["dept_cf"]), "bl_cf": sum(result["total"]["bl_cf"])}
conn.close()
return jsonify(result)
@bp.route("/api/overview/dept-project-matrix")
@login_required
def overview_dept_project_matrix():
"""返回各工作台预算项目经营详情矩阵"""
conn = db()
_year = date.today().year
months = [f"{_year}-{m:02d}" for m in range(1, 13)]
tenants = ["学术·无界", "科普·无界", "科研·无界", "医患·无界", "MCN·无界"]
keys = ["sign","revenue","gross","cost","paid","payment","cashflow"]
result = {"tenants": tenants, "months": months, "data": {}, "total": {k: [0]*12 for k in keys}}
for tenant in tenants:
tdata = {k: [0]*12 for k in keys}
pfs = rows(conn, "SELECT sign_amount, sign_month, budget_data, expense_data FROM plan_finances WHERE tenant=?", [tenant])
for pf in pfs:
sm = (pf.get("sign_month") or "")[:7]
if sm in months:
i = months.index(sm)
tdata["sign"][i] += float(pf.get("sign_amount") or 0)
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)
tdata["revenue"][i] += float(b.get("rev") or 0)
tdata["gross"][i] += float(b.get("gross") or 0)
tdata["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)
tdata["cost"][i] += float(e.get("cost") or 0)
tdata["paid"][i] += float(e.get("paid") or 0)
for i in range(12):
tdata["cashflow"][i] = tdata["payment"][i] - tdata["paid"][i]
result["data"][tenant] = tdata
for k in keys:
for i in range(12):
result["total"][k][i] += tdata[k][i]
result["annual"] = {}
for t in tenants:
result["annual"][t] = {k: sum(result["data"][t][k]) for k in keys}
result["annual"]["total"] = {k: sum(result["total"][k]) for k in keys}
conn.close()
return jsonify(result)
@bp.route("/api/overview/finance-dept-project-matrix")
@login_required
def overview_finance_dept_project_matrix():
"""返回各工作台已发生项目经营详情矩阵"""
conn = db()
_year = date.today().year
months = [f"{_year}-{m:02d}" for m in range(1, 13)]
tenants = ["学术·无界", "科普·无界", "科研·无界", "医患·无界", "MCN·无界"]
keys = ["sign","revenue","gross","cost","paid","payment","cashflow"]
result = {"tenants": tenants, "months": months, "data": {}, "total": {k: [0]*12 for k in keys}}
for tenant in tenants:
tdata = {k: [0]*12 for k in keys}
pfs = rows(conn, "SELECT sign_amount, sign_month, budget_data, expense_data FROM project_finances WHERE tenant=?", [tenant])
for pf in pfs:
sm = (pf.get("sign_month") or "")[:7]
if sm in months:
i = months.index(sm)
tdata["sign"][i] += float(pf.get("sign_amount") or 0)
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)
tdata["revenue"][i] += float(b.get("rev") or 0)
tdata["gross"][i] += float(b.get("gross") or 0)
tdata["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)
tdata["cost"][i] += float(e.get("cost") or 0)
tdata["paid"][i] += float(e.get("paid") or 0)
for i in range(12):
tdata["cashflow"][i] = tdata["payment"][i] - tdata["paid"][i]
result["data"][tenant] = tdata
for k in keys:
for i in range(12):
result["total"][k][i] += tdata[k][i]
result["annual"] = {}
for t in tenants:
result["annual"][t] = {k: sum(result["data"][t][k]) for k in keys}
result["annual"]["total"] = {k: sum(result["total"][k]) for k in keys}
conn.close()
return jsonify(result)
# ---------- health ----------
@bp.route("/api/health")

View File

@@ -297,10 +297,9 @@ function renderFinance() {
{ key: '_expand', label: '' },
{ key: 'client_name', label: '客户' }, { key: 'customer_name', label: '项目名称' }, { key: 'sign_amount', label: '签约金额' },
{ key: 'rev', label: '已确收' }, { key: 'gross', label: '毛利' }, { key: 'gross_rate', label: '毛利率' },
{ key: 'payment', label: '回款' }, { key: 'pay_rate', label: '回款率' },
{ key: 'payment', label: '回款' }, { key: 'pending', label: '待回款' }, { key: 'pay_rate', label: '回款率' },
{ key: 'cost', label: '成本' }, { key: 'paid', label: '支出' }, { key: 'paid_rate', label: '付款率' },
{ key: 'cashflow', label: '现金流' },
{ key: 'pending', label: '待回款' },
];
const UNSIGNED_COLS = [
{ key: 'client_name', label: '客户' }, { key: 'customer_name', label: '项目名称' }, { key: 'sign_amount', label: '签约金额' },
@@ -340,7 +339,7 @@ function renderFinance() {
const cfCls = cashflow >= 0 ? 'text-green-600' : 'text-red-600';
const pending = rev - payment;
const pendingCls = pending > 0 ? 'text-red-600' : pending < 0 ? 'text-green-600' : 'text-slate-400';
const pendingVal = pending ? money(pending) : '<span class="text-slate-300">—</span>';
const pendingVal = pending ? (pending > 0 ? '+' : '') + money(pending) : '<span class="text-slate-300">—</span>';
var budgetArr = [];
var expenseArr = [];
try { budgetArr = JSON.parse(pf.budget_data || "[]"); } catch(e) {}
@@ -363,7 +362,7 @@ function renderFinance() {
var bCf = bPay - ePaid;
return '<tr class="bg-slate-50"><td class="px-2 py-px"></td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-slate-400">' + m + '</td><td class="px-2 py-px text-center text-xs text-blue-600">' + (bRev ? money(bRev) : '—') + '</td><td class="px-2 py-px text-center text-xs text-green-600">' + (bGross ? money(bGross) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-amber-600">' + (bPay ? money(bPay) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-rose-600">' + (eCost ? money(eCost) : '—') + '</td><td class="px-2 py-px text-center text-xs text-purple-600">' + (ePaid ? money(ePaid) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs ' + (bCf >= 0 ? 'text-green-600' : 'text-red-600') + '">' + (bCf ? money(bCf) : '—') + '</td><td class="px-2 py-px text-center text-xs ' + (bRev - bPay > 0 ? 'text-red-600' : bRev - bPay < 0 ? 'text-green-600' : 'text-slate-300') + '">' + (bRev - bPay ? money(bRev - bPay) : '—') + '</td></tr>';
}).join('');
return `<tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="openPfEditModal(${pf.id})"><td class="px-2 py-px text-center align-middle text-sm" onclick="event.stopPropagation();finToggleExpand(${pf.id})"><i data-lucide="chevron-right" style="width:14px;height:14px;color:#94a3b8" id="fin_expand_icon_${pf.id}"></i></td><td class="px-2 py-px text-center align-middle text-sm truncate" style="max-width:100px" title="${esc(pf.client_name || '')}">${esc(pf.client_name || "")}</td><td class="px-2 py-px text-sm font-medium text-center align-middle truncate" style="min-width:300px;max-width:300px;width:300px" title="${esc(pf.customer_name)}">${esc(pf.customer_name)}</td><td class="px-2 py-px text-center align-middle text-sm font-medium">${money(pf.sign_amount)}</td><td class="px-2 py-px text-center text-blue-700 align-middle">${fmtNoUnit(rev)}</td><td class="px-2 py-px text-center align-middle text-sm font-medium">${fmtNoUnit(gross)}</td><td class="px-2 py-px text-center align-middle text-sm">${grossR}</td><td class="px-2 py-px text-center text-amber-700 align-middle">${fmtNoUnit(payment)}</td><td class="px-2 py-px text-center align-middle text-sm">${payR}</td><td class="px-2 py-px text-center text-rose-700 align-middle">${fmtNoUnit(cost)}</td><td class="px-2 py-px text-center text-purple-700 align-middle">${fmtNoUnit(paid)}</td><td class="px-2 py-px text-center align-middle text-sm">${paidR}</td><td class="px-2 py-px text-center font-semibold align-middle ${cfCls}">${cf}</td><td class="px-2 py-px text-center align-middle text-sm font-semibold ${pendingCls}">${pendingVal}</td></tr><tr class="hidden" id="fin_expand_${pf.id}"><td colspan="14"><table class="w-full"><thead><tr class="border-b border-slate-200"><th class="px-2 py-1"></th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">月份</th><th class="px-2 py-1 text-center text-xs text-slate-400">确收</th><th class="px-2 py-1 text-center text-xs text-slate-400">毛利</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">回款</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">成本</th><th class="px-2 py-1 text-center text-xs text-slate-400">支出</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">现金流</th><th class="px-2 py-1 text-center text-xs text-slate-400">待回款</th></tr></thead><tbody>${detailRows}</tbody></table></td></tr>`;
return `<tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="openPfEditModal(${pf.id})"><td class="px-2 py-px text-center align-middle text-sm" onclick="event.stopPropagation();finToggleExpand(${pf.id})"><i data-lucide="chevron-right" style="width:14px;height:14px;color:#94a3b8" id="fin_expand_icon_${pf.id}"></i></td><td class="px-2 py-px text-center align-middle text-sm truncate" style="max-width:100px" title="${esc(pf.client_name || '')}">${esc(pf.client_name || "")}</td><td class="px-2 py-px text-sm font-medium text-center align-middle truncate" style="min-width:300px;max-width:300px;width:300px" title="${esc(pf.customer_name)}">${esc(pf.customer_name)}</td><td class="px-2 py-px text-center align-middle text-sm font-medium">${money(pf.sign_amount)}</td><td class="px-2 py-px text-center text-blue-700 align-middle">${fmtNoUnit(rev)}</td><td class="px-2 py-px text-center align-middle text-sm font-medium">${fmtNoUnit(gross)}</td><td class="px-2 py-px text-center align-middle text-sm">${grossR}</td><td class="px-2 py-px text-center text-amber-700 align-middle">${fmtNoUnit(payment)}</td><td class="px-2 py-px text-center align-middle text-sm font-semibold ${pendingCls}">${pendingVal}</td><td class="px-2 py-px text-center align-middle text-sm">${payR}</td><td class="px-2 py-px text-center text-rose-700 align-middle">${fmtNoUnit(cost)}</td><td class="px-2 py-px text-center text-purple-700 align-middle">${fmtNoUnit(paid)}</td><td class="px-2 py-px text-center align-middle text-sm">${paidR}</td><td class="px-2 py-px text-center font-semibold align-middle ${cfCls}">${cf}</td></tr><tr class="hidden" id="fin_expand_${pf.id}"><td colspan="14"><table class="w-full"><thead><tr class="border-b border-slate-200"><th class="px-2 py-1"></th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">月份</th><th class="px-2 py-1 text-center text-xs text-slate-400">确收</th><th class="px-2 py-1 text-center text-xs text-slate-400">毛利</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">回款</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">成本</th><th class="px-2 py-1 text-center text-xs text-slate-400">支出</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">现金流</th><th class="px-2 py-1 text-center text-xs text-slate-400">待回款</th></tr></thead><tbody>${detailRows}</tbody></table></td></tr>`;
};
// 待签约简版行
const tdRowUnsigned = (pf, cost, gross) => {
@@ -1249,3 +1248,10 @@ function renderOverviewCharts(data) {
state.finChart4 = new Chart(c4, Object.assign({}, iconf, { data: { labels: labels, datasets: [{ label: "利润", data: data.map(function(x) { return x.gross || 0; }), borderColor: "#6366f1", backgroundColor: "rgba(99,102,241,0.06)", fill: true, tension: 0.3 }] } }));
}
};
// ====== 筛选器控制函数 ======
window.switchFinFilter = (f) => { state.finFilter = f; renderFinance(); };
window.setFinView = (v) => { state.finView = v; renderFinance(); };
window.setFinYear = (y) => { state.finYear = parseInt(y); renderFinance(); };
window.setFinQuarter = (q) => { state.finView = 'quarterly'; state.finQuarter = q; renderFinance(); };
window.setFinMonth = (m) => { state.finView = 'monthly'; state.finMonth = m; renderFinance(); };

View File

@@ -1,38 +1,123 @@
// 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");
var view = state.overviewView || "monthly";
if (view === 'matrix') { renderFinanceDeptMatrix(); return; }
if (view === 'cf_matrix') { renderFinanceCfMatrix(); return; }
if (view === 'project_matrix') { renderFinanceProjectMatrix(); return; }
var api = view === "quarterly" ? "/api/overview/finance-quarterly" : "/api/overview/finance-monthly";
var container = document.querySelector("#overview_finance");
if (!container) return;
container.innerHTML = '<div class="p-4 text-center text-slate-400">加载中...</div>';
fetch(api).then(r => r.json()).then(data => {
if (state.overviewView !== view) return;
renderOverviewFinanceData(data, view);
}).catch(() => {
if (state.overviewView !== view) return;
container.innerHTML = '<div class="p-4 text-center text-slate-400">加载失败</div>';
});
}
function isAllZero(data, tenants, keys, mi) {
for (var ti = 0; ti < tenants.length; ti++) {
for (var ki = 0; ki < keys.length; ki++) {
if ((data.data[tenants[ti]][keys[ki]][mi] || 0) !== 0) return false;
}
}
return true;
}
// ====== 项目经营详情报表(顶部定义,避免调用时未注册) ======
function renderFinanceProjectMatrix() {
var c = document.querySelector("#overview_finance");
if (!c) return;
c.innerHTML = '<div class="p-3 text-center text-slate-400 text-sm">加载项目经营详情矩阵...</div>';
fetch('/api/overview/finance-dept-project-matrix').then(function(r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
}).then(function(data) {
renderFinanceProjectMatrixData(data, c);
}).catch(function(e) {
c.innerHTML = '<div class="p-3 text-center text-red-500 text-sm">加载失败: ' + (e.message || '未知错误') + '</div>';
});
}
function renderFinanceProjectMatrixData(data, container) {
try {
var mf = function(v) { return Math.round(Number(v || 0)).toLocaleString("zh-CN"); };
var tenants = data.tenants;
var sn = tenants.map(function(t) { return t.replace('·无界', ''); });
var mL = ['01月','02月','03月','04月','05月','06月','07月','08月','09月','10月','11月','12月'];
var keys = ["sign","revenue","gross","cost","paid","payment","cashflow"];
var labels = ["合同金额","确收金额","确收毛利","当期成本","当期流出","回款","项目现金流"];
var vBtns = financeBtnBar('project_matrix');
var cards = '';
for (var mi = 11; mi >= 0; mi--) {
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
for (var ti = 0; ti < tenants.length; ti++) { th += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + sn[ti] + '</th>'; }
th += '</tr>';
if (isAllZero(data, tenants, keys.filter(function(k) { return k !== 'sign'; }), mi)) continue;
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
for (var ti = 0; ti < tenants.length; ti++) { th += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + sn[ti] + '</th>'; }
th += '</tr>';
var tb = financeProjectMatrixTbody(data, tenants, mi, keys, labels, mf);
cards += '<div class="flex items-center gap-3"><div class="text-5xl font-bold text-blue-200" style="width:120px;text-align:center;white-space:nowrap">' + mL[mi] + '</div><div class="card p-4 flex-1"><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + th + '</thead><tbody>' + tb + '</tbody></table></div></div></div>';
}
container.innerHTML = '<div class="grid gap-2.5">' + vBtns + '<div class="flex flex-col gap-2">' + cards + '</div></div>';
if (window.lucide) window.lucide.createIcons();
} catch(e) { container.innerHTML = '<div class="p-3 text-center text-red-500 text-sm">渲染失败: ' + (e.message || '未知错误') + '</div>'; }
}
function financeProjectMatrixTbody(data, tenants, mi, keys, labels, mf) {
var tb = '';
for (var ri = 0; ri < keys.length; ri++) {
var rowCls = ri === keys.length - 1 ? 'font-semibold' : '';
var ts = 0;
var vals = [];
for (var ti = 0; ti < tenants.length; ti++) {
var v = data.data[tenants[ti]][keys[ri]][mi] || 0;
ts += v;
vals.push(v);
}
var tCls = 'text-slate-700';
if (ri === 2) tCls = 'text-green-600';
if (ri === keys.length - 1) tCls = ts >= 0 ? 'text-green-600' : 'text-red-600';
var rh = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + rowCls + ' text-slate-700 whitespace-nowrap">' + labels[ri] + '</td><td class="py-1.5 text-right font-semibold text-sm whitespace-nowrap ' + tCls + '">' + mf(ts) + '</td>';
for (var ti = 0; ti < tenants.length; ti++) {
var cls = 'text-slate-700';
if (ri === 2) cls = 'text-green-600';
if (ri === keys.length - 1) cls = vals[ti] >= 0 ? 'text-green-600' : 'text-red-600';
rh += '<td class="py-1.5 text-right text-sm whitespace-nowrap ' + cls + '">' + mf(vals[ti]) + '</td>';
}
rh += '</tr>';
tb += rh;
}
return tb;
}
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) => '<div class="card ' + (cls || 'p-4') + '">' + body + '</div>';
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, cf = data.dept_cf, p = data.project;
const d = data.dept;
const cf = data.dept_cf;
const p = data.project;
var btns = [];
btns.push('<button onclick="setOverviewFinanceView(\'monthly\')" class="finance-tab' + (view === 'monthly' ? ' active' : '') + '" style="font-size:14px;font-weight:600">月报总览</button>');
btns.push('<button onclick="setOverviewFinanceView(\'quarterly\')" class="finance-tab' + (view === 'quarterly' ? ' active' : '') + '" style="font-size:14px;font-weight:600">季报总览</button>');
btns.push('<button onclick="setOverviewFinanceView(\'matrix\')" class="finance-tab' + (view === 'matrix' ? ' active' : '') + '" style="font-size:14px;font-weight:600">损益详细报表</button>');
btns.push('<button onclick="setOverviewFinanceView(\'cf_matrix\')" class="finance-tab' + (view === 'cf_matrix' ? ' active' : '') + '" style="font-size:14px;font-weight:600">现金流详细报表</button>');
btns.push('<button onclick="setOverviewFinanceView(\'project_matrix\')" class="finance-tab' + (view === 'project_matrix' ? ' active' : '') + '" style="font-size:14px;font-weight:600">项目经营详细报表</button>');
var viewBtns = '<div class="finance-tabs" style="padding:0;border-bottom:1px solid #e2e8f0;margin-bottom:12px">' + btns.join('') + '</div>';
// 视图切换按钮
var viewBtns = '<div class="flex items-center gap-2 mb-2"><button onclick="setOverviewFinanceView(\'monthly\')" class="px-3 py-0.5 rounded text-sm font-medium ' + (view === 'monthly' ? 'bg-blue-600 text-white' : 'bg-slate-100 text-slate-600 hover:bg-slate-200') + '">月报总览</button><button onclick="setOverviewFinanceView(\'quarterly\')" class="px-3 py-0.5 rounded text-sm font-medium ' + (view === 'quarterly' ? 'bg-blue-600 text-white' : 'bg-slate-100 text-slate-600 hover:bg-slate-200') + '">季报总览</button></div>';
// ====== 部门损益情况 ======
var deptThead = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
for (var hi = 0; hi < cols; hi++) { deptThead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + headers[hi] + '</th>'; }
deptThead += '</tr>';
@@ -44,8 +129,7 @@ function renderOverviewFinanceData(data, view) {
];
var deptTbody = '';
for (var dri = 0; dri < 3; dri++) {
var dr = deptRows[dri];
var rowCls = dri === 2 ? 'font-semibold' : '';
var dr = deptRows[dri], rowCls = dri === 2 ? 'font-semibold' : '';
var rowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + rowCls + ' text-slate-700 whitespace-nowrap">' + dr.label + '</td>';
var annualCls = dri === 2 ? (dr.annual >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-700';
rowHtml += '<td class="py-1.5 text-right ' + rowCls + ' ' + annualCls + ' whitespace-nowrap">' + moneyIntL(dr.annual) + '</td>';
@@ -57,7 +141,6 @@ function renderOverviewFinanceData(data, view) {
}
var deptCard = card('<h3 class="text-base font-bold text-slate-700 mb-3">部门损益情况 <span class="text-sm text-slate-500 font-normal ml-3">部门利润 = 所有项目产生的毛利 - 部门 1 级产生的人力成本,研发成本,差旅和其他支出</span></h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + deptTbody + '</tbody></table></div>', 'p-4');
// ====== 部门现金流 ======
var cfRows = [
{ label: '项目现金流', vals: cf.project_cf, annual: cf.annual[0] },
{ label: '部门现金流', vals: cf.dept_cf_item, annual: cf.annual[1] },
@@ -65,8 +148,7 @@ function renderOverviewFinanceData(data, view) {
];
var cfTbody = '';
for (var cfri = 0; cfri < 3; cfri++) {
var cfr = cfRows[cfri];
var cfRowCls = cfri === 2 ? 'font-semibold' : '';
var cfr = cfRows[cfri], cfRowCls = cfri === 2 ? 'font-semibold' : '';
var rowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + cfRowCls + ' text-slate-700 whitespace-nowrap">' + cfr.label + '</td>';
var cAnnualCls = cfri === 2 ? (cfr.annual >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-700';
rowHtml += '<td class="py-1.5 text-right ' + cfRowCls + ' ' + cAnnualCls + ' whitespace-nowrap">' + moneyIntL(cfr.annual) + '</td>';
@@ -78,15 +160,12 @@ function renderOverviewFinanceData(data, view) {
}
var cfCard = card('<h3 class="text-base font-bold text-slate-700 mb-3">部门现金流 <span class="text-sm text-slate-500 font-normal ml-3">部门现金流=所有项目产生的现金流 - 部门一级因费用产生的现金流出或流入</span></h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + cfTbody + '</tbody></table></div>', 'p-4');
// ====== 项目经营详情 ======
var LABELS = ['合同金额','确收金额','确收毛利','成本','支出','回款','项目现金流'];
var projVals = [p.revenue, p.gross, p.cost, p.paid, p.payment, p.cashflow];
var projAnnual = p.annual;
var projThead = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
for (var phi = 0; phi < cols; phi++) { projThead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + headers[phi] + '</th>'; }
projThead += '</tr>';
var projTbody = '';
for (var ri = 0; ri < 7; ri++) {
var rowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left font-semibold text-slate-700 whitespace-nowrap">' + LABELS[ri] + '</td>';
@@ -106,7 +185,97 @@ function renderOverviewFinanceData(data, view) {
if (window.lucide) window.lucide.createIcons();
}
window.setOverviewFinanceView = (v) => {
state.overviewView = v;
renderOverviewFinance();
};
window.setOverviewFinanceView = (v) => { state.overviewView = v; renderOverviewFinance(); };
// ====== 损益报表 ======
function renderFinanceDeptMatrix() {
var c = document.querySelector("#overview_finance");
if (!c) return;
c.innerHTML = '<div class="p-3 text-center text-slate-400 text-sm">加载损益矩阵...</div>';
fetch('/api/overview/finance-dept-matrix').then(r => r.json()).then(function(data) { renderFinanceDeptMatrixData(data, c); })
.catch(function() { c.innerHTML = '<div class="p-3 text-center text-slate-400 text-sm">加载失败</div>'; });
}
function renderFinanceDeptMatrixData(data, container) {
var mf = function(v) { return Math.round(Number(v || 0)).toLocaleString("zh-CN"); };
var tenants = data.tenants;
var sn = tenants.map(function(t) { return t.replace('·无界', ''); });
var mL = ['01月','02月','03月','04月','05月','06月','07月','08月','09月','10月','11月','12月'];
var vBtns = financeBtnBar('matrix');
var cards = '';
for (var mi = 11; mi >= 0; mi--) {
if (isAllZero(data, tenants, ['project_gross','dept_expense','profit'], mi)) continue;
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
for (var ti = 0; ti < tenants.length; ti++) { th += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + sn[ti] + '</th>'; }
th += '</tr>';
var tb = financeMatrixTbody(data, tenants, mi, ['project_gross','dept_expense','profit'], ['项目毛利','部门费用','利润'], mf);
cards += '<div class="flex items-center gap-3"><div class="text-5xl font-bold text-blue-200" style="width:120px;text-align:center;white-space:nowrap">' + mL[mi] + '</div><div class="card p-4 flex-1"><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + th + '</thead><tbody>' + tb + '</tbody></table></div></div></div>';
}
container.innerHTML = '<div class="grid gap-2.5">' + vBtns + '<div class="flex flex-col gap-2">' + cards + '</div></div>';
if (window.lucide) window.lucide.createIcons();
}
// ====== 现金流报表 ======
function renderFinanceCfMatrix() {
var c = document.querySelector("#overview_finance");
if (!c) return;
c.innerHTML = '<div class="p-3 text-center text-slate-400 text-sm">加载现金流矩阵...</div>';
fetch('/api/overview/finance-dept-cf-matrix').then(r => r.json()).then(function(data) { renderFinanceCfMatrixData(data, c); })
.catch(function() { c.innerHTML = '<div class="p-3 text-center text-slate-400 text-sm">加载失败</div>'; });
}
function renderFinanceCfMatrixData(data, container) {
var mf = function(v) { return Math.round(Number(v || 0)).toLocaleString("zh-CN"); };
var tenants = data.tenants;
var sn = tenants.map(function(t) { return t.replace('·无界', ''); });
var mL = ['01月','02月','03月','04月','05月','06月','07月','08月','09月','10月','11月','12月'];
var vBtns = financeBtnBar('cf_matrix');
var cards = '';
for (var mi = 11; mi >= 0; mi--) {
if (isAllZero(data, tenants, ['project_cf','dept_cf','bl_cf'], mi)) continue;
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
for (var ti = 0; ti < tenants.length; ti++) { th += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + sn[ti] + '</th>'; }
th += '</tr>';
var tb = financeMatrixTbody(data, tenants, mi, ['project_cf','dept_cf','bl_cf'], ['项目现金流','部门现金流','经营现金流'], mf);
cards += '<div class="flex items-center gap-3"><div class="text-5xl font-bold text-blue-200" style="width:120px;text-align:center;white-space:nowrap">' + mL[mi] + '</div><div class="card p-4 flex-1"><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + th + '</thead><tbody>' + tb + '</tbody></table></div></div></div>';
}
container.innerHTML = '<div class="grid gap-2.5">' + vBtns + '<div class="flex flex-col gap-2">' + cards + '</div></div>';
if (window.lucide) window.lucide.createIcons();
}
// ====== 共用工具 ======
function financeBtnBar(active) {
var btns = [];
btns.push('<button onclick="setOverviewFinanceView(\'monthly\')" class="finance-tab' + (active === 'monthly' ? ' active' : '') + '" style="font-size:14px;font-weight:600">月报总览</button>');
btns.push('<button onclick="setOverviewFinanceView(\'quarterly\')" class="finance-tab' + (active === 'quarterly' ? ' active' : '') + '" style="font-size:14px;font-weight:600">季报总览</button>');
btns.push('<button onclick="setOverviewFinanceView(\'matrix\')" class="finance-tab' + (active === 'matrix' ? ' active' : '') + '" style="font-size:14px;font-weight:600">损益详细报表</button>');
btns.push('<button onclick="setOverviewFinanceView(\'cf_matrix\')" class="finance-tab' + (active === 'cf_matrix' ? ' active' : '') + '" style="font-size:14px;font-weight:600">现金流详细报表</button>');
btns.push('<button onclick="setOverviewFinanceView(\'project_matrix\')" class="finance-tab' + (active === 'project_matrix' ? ' active' : '') + '" style="font-size:14px;font-weight:600">项目经营详细报表</button>');
return '<div class="finance-tabs" style="padding:0;border-bottom:1px solid #e2e8f0;margin-bottom:12px">' + btns.join('') + '</div>';
}
function financeMatrixTbody(data, tenants, mi, keys, labels, mf) {
var tb = '';
for (var ri = 0; ri < keys.length; ri++) {
var rowCls = ri === keys.length - 1 ? 'font-semibold' : '';
var ts = 0;
var vals = [];
for (var ti = 0; ti < tenants.length; ti++) {
var v = data.data[tenants[ti]][keys[ri]][mi] || 0;
ts += v;
vals.push(v);
}
var tCls = ts >= 0 ? 'text-green-600' : 'text-red-600';
var rh = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + rowCls + ' text-slate-700 whitespace-nowrap">' + labels[ri] + '</td><td class="py-1.5 text-right font-semibold text-sm whitespace-nowrap ' + tCls + '">' + mf(ts) + '</td>';
for (var ti = 0; ti < tenants.length; ti++) {
var cls = vals[ti] >= 0 ? 'text-green-600' : 'text-red-600';
rh += '<td class="py-1.5 text-right text-sm whitespace-nowrap ' + cls + '">' + mf(vals[ti]) + '</td>';
}
rh += '</tr>';
tb += rh;
}
return tb;
}

View File

@@ -1,18 +1,93 @@
// 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");
var view = state.overviewView || "monthly";
var container = document.querySelector("#overview_plan");
if (!container) return;
container.innerHTML = '<div class="p-4 text-center text-slate-400">加载中...</div>';
if (view === 'matrix') { renderDeptMatrix(); return; }
if (view === 'cf_matrix') { renderCfMatrix(); return; }
if (view === 'project_matrix') { renderProjectMatrix(); return; }
var api = view === "quarterly" ? "/api/overview/plan-quarterly" : "/api/overview/plan-monthly";
fetch(api).then(r => r.json()).then(data => {
if (state.overviewView !== view) return;
renderOverviewPlanData(data, view);
}).catch(() => {
if (state.overviewView !== view) return;
container.innerHTML = '<div class="p-4 text-center text-slate-400">加载失败</div>';
});
}
function renderProjectMatrix() {
var c = document.querySelector("#overview_plan");
if (!c) return;
c.innerHTML = '<div class="p-3 text-center text-slate-400 text-sm">加载项目经营详情矩阵...</div>';
fetch('/api/overview/dept-project-matrix').then(function(r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
}).then(function(data) {
renderProjectMatrixData(data, c);
}).catch(function(e) {
c.innerHTML = '<div class="p-3 text-center text-red-500 text-sm">加载失败: ' + (e.message || '未知错误') + '</div>';
});
}
function renderProjectMatrixData(data, container) {
try {
var mf = function(v) { return Math.round(Number(v || 0)).toLocaleString("zh-CN"); };
var tenants = data.tenants;
var sn = tenants.map(function(t) { return t.replace('·无界', ''); });
var mL = ['01月','02月','03月','04月','05月','06月','07月','08月','09月','10月','11月','12月'];
var keys = ["sign","revenue","gross","cost","paid","payment","cashflow"];
var labels = ["合同金额","确收金额","确收毛利","当期成本","当期流出","回款","项目现金流"];
var vBtns = btnBar('project_matrix');
var cards = '';
for (var mi = 11; mi >= 0; mi--) {
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
for (var ti = 0; ti < tenants.length; ti++) { th += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + sn[ti] + '</th>'; }
th += '</tr>';
if (isAllZero(data, tenants, keys.filter(function(k) { return k !== 'sign'; }), mi)) continue;
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
for (var ti = 0; ti < tenants.length; ti++) { th += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + sn[ti] + '</th>'; }
th += '</tr>';
var tb = projectMatrixTbody(data, tenants, mi, keys, labels, mf);
cards += '<div class="flex items-center gap-3"><div class="text-5xl font-bold text-blue-200" style="width:120px;text-align:center;white-space:nowrap">' + mL[mi] + '</div><div class="card p-4 flex-1"><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + th + '</thead><tbody>' + tb + '</tbody></table></div></div></div>';
}
container.innerHTML = '<div class="grid gap-2.5">' + vBtns + '<div class="flex flex-col gap-2">' + cards + '</div></div>';
if (window.lucide) window.lucide.createIcons();
} catch(e) { container.innerHTML = '<div class="p-3 text-center text-red-500 text-sm">渲染失败: ' + (e.message || '未知错误') + '</div>'; }
}
function projectMatrixTbody(data, tenants, mi, keys, labels, mf) {
var tb = '';
for (var ri = 0; ri < keys.length; ri++) {
var rowCls = ri === keys.length - 1 ? 'font-semibold' : '';
var ts = 0;
var vals = [];
for (var ti = 0; ti < tenants.length; ti++) {
var v = data.data[tenants[ti]][keys[ri]][mi] || 0;
ts += v;
vals.push(v);
}
var tCls = 'text-slate-700';
if (ri === 2) tCls = 'text-green-600';
if (ri === keys.length - 1) tCls = ts >= 0 ? 'text-green-600' : 'text-red-600';
var rh = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + rowCls + ' text-slate-700 whitespace-nowrap">' + labels[ri] + '</td><td class="py-1.5 text-right font-semibold text-sm whitespace-nowrap ' + tCls + '">' + mf(ts) + '</td>';
for (var ti = 0; ti < tenants.length; ti++) {
var cls = 'text-slate-700';
if (ri === 2) cls = 'text-green-600';
if (ri === keys.length - 1) cls = vals[ti] >= 0 ? 'text-green-600' : 'text-red-600';
rh += '<td class="py-1.5 text-right text-sm whitespace-nowrap ' + cls + '">' + mf(vals[ti]) + '</td>';
}
rh += '</tr>';
tb += rh;
}
return tb;
}
function renderOverviewPlanData(data, view) {
const container = document.querySelector("#overview_plan");
@@ -29,6 +104,14 @@ function renderOverviewPlanData(data, view) {
const cf = data.dept_cf;
const p = data.project;
// 视图切换按钮
var btns = [];
btns.push('<button onclick="setOverviewView(\'monthly\')" class="finance-tab' + (view === 'monthly' ? ' active' : '') + '" style="font-size:14px;font-weight:600">月报总览</button>');
btns.push('<button onclick="setOverviewView(\'quarterly\')" class="finance-tab' + (view === 'quarterly' ? ' active' : '') + '" style="font-size:14px;font-weight:600">季报总览</button>');
btns.push('<button onclick="setOverviewView(\'matrix\')" class="finance-tab' + (view === 'matrix' ? ' active' : '') + '" style="font-size:14px;font-weight:600">损益详细报表</button>');
btns.push('<button onclick="setOverviewView(\'cf_matrix\')" class="finance-tab' + (view === 'cf_matrix' ? ' active' : '') + '" style="font-size:14px;font-weight:600">现金流详细报表</button>'); btns.push('<button onclick="setOverviewView(\'project_matrix\')" class="finance-tab' + (view === 'project_matrix' ? ' active' : '') + '" style="font-size:14px;font-weight:600">项目经营详细报表</button>');
var viewBtns = '<div class="finance-tabs" style="padding:0;border-bottom:1px solid #e2e8f0;margin-bottom:12px">' + btns.join('') + '</div>';
// ====== 部门损益情况 ======
var deptThead = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
for (var hi = 0; hi < cols; hi++) { deptThead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + headers[hi] + '</th>'; }
@@ -78,7 +161,7 @@ function renderOverviewPlanData(data, view) {
// ====== 项目经营详情 ======
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 projAnnual = p.annual;
var projThead = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
for (var phi = 0; phi < cols; phi++) { projThead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + headers[phi] + '</th>'; }
@@ -99,14 +182,121 @@ function renderOverviewPlanData(data, view) {
}
var projCard = card('<h3 class="text-base font-bold text-slate-700 mb-3">项目经营详情 <span class="text-sm text-slate-500 font-normal ml-3">所有项目产生的签单,确收,毛利,回款,当期成本,当期流出,现金流</span></h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + projThead + '</thead><tbody>' + projTbody + '</tbody></table></div>', 'p-4');
// 视图切换按钮
var viewBtns = '<div class="flex items-center gap-2 mb-2"><button onclick="setOverviewView(\'monthly\')" class="px-3 py-0.5 rounded text-sm font-medium ' + (view === 'monthly' ? 'bg-blue-600 text-white' : 'bg-slate-100 text-slate-600 hover:bg-slate-200') + '">月报总览</button><button onclick="setOverviewView(\'quarterly\')" class="px-3 py-0.5 rounded text-sm font-medium ' + (view === 'quarterly' ? 'bg-blue-600 text-white' : 'bg-slate-100 text-slate-600 hover:bg-slate-200') + '">季报总览</button></div>';
container.innerHTML = '<div class="grid gap-2.5">' + viewBtns + deptCard + cfCard + projCard + '</div>';
if (window.lucide) window.lucide.createIcons();
}
window.setOverviewView = (v) => {
state.overviewView = v;
renderOverviewPlan();
};
window.setOverviewView = (v) => { state.overviewView = v; renderOverviewPlan(); };
// ====== 损益报表 ======
function renderDeptMatrix() {
var c = document.querySelector("#overview_plan");
if (!c) return;
c.innerHTML = '<div class="p-3 text-center text-slate-400 text-sm">加载损益矩阵...</div>';
fetch('/api/overview/dept-matrix').then(r => r.json()).then(function(data) { renderDeptMatrixData(data, c); })
.catch(function() { c.innerHTML = '<div class="p-3 text-center text-slate-400 text-sm">加载失败</div>'; });
}
function renderDeptMatrixData(data, container) {
var mf = function(v) { return Math.round(Number(v || 0)).toLocaleString("zh-CN"); };
var tenants = data.tenants;
var sn = tenants.map(function(t) { return t.replace('·无界', ''); });
var mL = ['01月','02月','03月','04月','05月','06月','07月','08月','09月','10月','11月','12月'];
var vBtns = btnBar('matrix');
var cards = '';
for (var mi = 11; mi >= 0; mi--) {
if (isAllZero(data, tenants, ['project_gross','dept_expense','profit'], mi)) continue;
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
for (var ti = 0; ti < tenants.length; ti++) { th += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + sn[ti] + '</th>'; }
th += '</tr>';
var tb = matrixTbody(data, tenants, mi, ['project_gross','dept_expense','profit'], ['项目毛利','部门费用','利润'], mf);
cards += '<div class="flex items-center gap-3"><div class="text-5xl font-bold text-blue-200" style="width:120px;text-align:center;white-space:nowrap">' + mL[mi] + '</div><div class="card p-4 flex-1"><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + th + '</thead><tbody>' + tb + '</tbody></table></div></div></div>';
}
container.innerHTML = '<div class="grid gap-2.5">' + vBtns + '<div class="flex flex-col gap-2">' + cards + '</div></div>';
if (window.lucide) window.lucide.createIcons();
}
// ====== 现金流报表 ======
function renderCfMatrix() {
var c = document.querySelector("#overview_plan");
if (!c) return;
c.innerHTML = '<div class="p-3 text-center text-slate-400 text-sm">加载现金流矩阵...</div>';
fetch('/api/overview/dept-cf-matrix').then(r => r.json()).then(function(data) { renderCfMatrixData(data, c); })
.catch(function() { c.innerHTML = '<div class="p-3 text-center text-slate-400 text-sm">加载失败</div>'; });
}
function renderCfMatrixData(data, container) {
var mf = function(v) { return Math.round(Number(v || 0)).toLocaleString("zh-CN"); };
var tenants = data.tenants;
var sn = tenants.map(function(t) { return t.replace('·无界', ''); });
var mL = ['01月','02月','03月','04月','05月','06月','07月','08月','09月','10月','11月','12月'];
var vBtns = btnBar('cf_matrix');
var cards = '';
for (var mi = 11; mi >= 0; mi--) {
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
for (var ti = 0; ti < tenants.length; ti++) { th += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + sn[ti] + '</th>'; }
th += '</tr>';
if (isAllZero(data, tenants, ['project_cf','dept_cf','bl_cf'], mi)) continue;
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
for (var ti = 0; ti < tenants.length; ti++) { th += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + sn[ti] + '</th>'; }
th += '</tr>';
var tb = matrixTbody(data, tenants, mi, ['project_cf','dept_cf','bl_cf'], ['项目现金流','部门现金流','经营现金流'], mf);
cards += '<div class="flex items-center gap-3"><div class="text-5xl font-bold text-blue-200" style="width:120px;text-align:center;white-space:nowrap">' + mL[mi] + '</div><div class="card p-4 flex-1"><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + th + '</thead><tbody>' + tb + '</tbody></table></div></div></div>';
}
container.innerHTML = '<div class="grid gap-2.5">' + vBtns + '<div class="flex flex-col gap-2">' + cards + '</div></div>';
if (window.lucide) window.lucide.createIcons();
}
// ====== 共用工具 ======
function btnBar(active) {
var btns = [];
btns.push('<button onclick="setOverviewView(\'monthly\')" class="finance-tab' + (active === 'monthly' ? ' active' : '') + '" style="font-size:14px;font-weight:600">月报总览</button>');
btns.push('<button onclick="setOverviewView(\'quarterly\')" class="finance-tab' + (active === 'quarterly' ? ' active' : '') + '" style="font-size:14px;font-weight:600">季报总览</button>');
btns.push('<button onclick="setOverviewView(\'matrix\')" class="finance-tab' + (active === 'matrix' ? ' active' : '') + '" style="font-size:14px;font-weight:600">损益详细报表</button>');
btns.push('<button onclick="setOverviewView(\'cf_matrix\')" class="finance-tab' + (active === 'cf_matrix' ? ' active' : '') + '" style="font-size:14px;font-weight:600">现金流详细报表</button>'); btns.push('<button onclick="setOverviewView(\'project_matrix\')" class="finance-tab' + (active === 'project_matrix' ? ' active' : '') + '" style="font-size:14px;font-weight:600">项目经营详细报表</button>');
return '<div class="finance-tabs" style="padding:0;border-bottom:1px solid #e2e8f0;margin-bottom:12px">' + btns.join('') + '</div>';
}
function matrixTbody(data, tenants, mi, keys, labels, mf) {
var tb = '';
for (var ri = 0; ri < keys.length; ri++) {
var rowCls = ri === keys.length - 1 ? 'font-semibold' : '';
var ts = 0;
var vals = [];
for (var ti = 0; ti < tenants.length; ti++) {
var v = data.data[tenants[ti]][keys[ri]][mi] || 0;
ts += v;
vals.push(v);
}
var tCls = ts >= 0 ? 'text-green-600' : 'text-red-600';
var rh = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + rowCls + ' text-slate-700 whitespace-nowrap">' + labels[ri] + '</td><td class="py-1.5 text-right font-semibold text-sm whitespace-nowrap ' + tCls + '">' + mf(ts) + '</td>';
for (var ti = 0; ti < tenants.length; ti++) {
var cls = vals[ti] >= 0 ? 'text-green-600' : 'text-red-600';
rh += '<td class="py-1.5 text-right text-sm whitespace-nowrap ' + cls + '">' + mf(vals[ti]) + '</td>';
}
rh += '</tr>';
tb += rh;
}
return tb;
function isAllZero(data, tenants, keys, mi) {
for (var ti = 0; ti < tenants.length; ti++) {
for (var ki = 0; ki < keys.length; ki++) {
if ((data.data[tenants[ti]][keys[ki]][mi] || 0) !== 0) return false;
}
}
return true;
}
// ====== 项目经营详情报表 ======
}

View File

@@ -306,11 +306,11 @@ function renderPlan() {
{ key: 'gross', label: '毛利' },
{ key: 'gross_rate', label: '毛利率' },
{ key: 'payment', label: '回款' },
{ key: 'pending', label: '待回款' },
{ key: 'pay_rate', label: '回款率' },
{ key: 'cost', label: '当期成本' },
{ key: 'paid', label: '当期流出' },
{ key: 'cashflow', label: '现金流' },
{ key: 'pending', label: '待回款' },
];
const UNSIGNED_COLS = [
{ key: 'client_name', label: '客户' },
@@ -369,7 +369,7 @@ function renderPlan() {
const cfCls = cashflow >= 0 ? 'text-green-600' : 'text-red-600';
const pending = rev - payment;
const pendingCls = pending > 0 ? 'text-red-600' : pending < 0 ? 'text-green-600' : 'text-slate-400';
const pendingVal = pending ? money(pending) : '<span class="text-slate-300">—</span>';
const pendingVal = pending ? (pending > 0 ? '+' : '') + money(pending) : '<span class="text-slate-300">—</span>';
var budgetArr = [];
var expenseArr = [];
try { budgetArr = JSON.parse(pf.budget_data || "[]"); } catch(e) {}
@@ -392,7 +392,7 @@ function renderPlan() {
var bCf = bPay - ePaid;
return '<tr class="bg-slate-50"><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-slate-400">' + m + '</td><td class="px-2 py-px text-center text-xs text-blue-600">' + (bRev ? money(bRev) : '—') + '</td><td class="px-2 py-px text-center text-xs text-green-600">' + (bGross ? money(bGross) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-amber-600">' + (bPay ? money(bPay) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-rose-600">' + (eCost ? money(eCost) : '—') + '</td><td class="px-2 py-px text-center text-xs text-purple-600">' + (ePaid ? money(ePaid) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs ' + (bCf >= 0 ? 'text-green-600' : 'text-red-600') + '">' + (bCf ? money(bCf) : '—') + '</td><td class="px-2 py-px text-center text-xs ' + (bRev - bPay > 0 ? 'text-red-600' : bRev - bPay < 0 ? 'text-green-600' : 'text-slate-300') + '">' + (bRev - bPay ? money(bRev - bPay) : '—') + '</td></tr>';
}).join('');
return `<tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="planOpenPfEditModal(${pf.id})"><td class="px-2 py-px text-center align-middle text-sm" onclick="event.stopPropagation();planToggleExpand(${pf.id})"><i data-lucide="chevron-right" style="width:14px;height:14px;color:#94a3b8" id="plan_expand_icon_${pf.id}"></i></td><td class="px-2 py-px text-center align-middle text-sm truncate" style="max-width:100px" title="${esc(pf.client_name || '')}">${esc(pf.client_name || "")}</td><td class="px-2 py-px text-sm font-medium text-center align-middle truncate" style="min-width:300px;max-width:300px;width:300px" title="${esc(pf.customer_name)}">${esc(pf.customer_name)}</td><td class="px-2 py-px text-center align-middle text-sm" onclick="event.stopPropagation()">${planInlineSelect('weight', pf.id, 'weight', pf.weight || '20%', ['20%','40%','60%','80%','100%'])}</td><td class="px-2 py-px text-center align-middle text-sm" onclick="event.stopPropagation()">${planInlineSelect('ptype', pf.id, 'project_type', pf.project_type || '待签约', ['待签约','已签约'])}</td><td class="px-2 py-px text-center align-middle text-sm font-medium">${money(pf.sign_amount)}</td><td class="px-2 py-px text-center text-blue-700 align-middle">${fmtNoUnit(rev)}</td><td class="px-2 py-px text-center align-middle text-sm font-medium">${fmtNoUnit(gross)}</td><td class="px-2 py-px text-center align-middle text-sm">${grossR}</td><td class="px-2 py-px text-center text-amber-700 align-middle">${fmtNoUnit(payment)}</td><td class="px-2 py-px text-center align-middle text-sm">${payR}</td><td class="px-2 py-px text-center text-rose-700 align-middle">${fmtNoUnit(cost)}</td><td class="px-2 py-px text-center text-purple-700 align-middle">${fmtNoUnit(paid)}</td><td class="px-2 py-px text-center font-semibold align-middle ${cfCls}">${cf}</td><td class="px-2 py-px text-center align-middle text-sm font-semibold ${pendingCls}">${pendingVal}</td></tr><tr class="hidden" id="plan_expand_${pf.id}"><td colspan="15"><table class="w-full"><thead><tr class="border-b border-slate-200"><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">月份</th><th class="px-2 py-1 text-center text-xs text-slate-400">确收</th><th class="px-2 py-1 text-center text-xs text-slate-400">毛利</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">回款</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">当期成本</th><th class="px-2 py-1 text-center text-xs text-slate-400">当期流出</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">现金流</th><th class="px-2 py-1 text-center text-xs text-slate-400">待回款</th></tr></thead><tbody>${detailRows}</tbody></table></td></tr>`;
return `<tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="planOpenPfEditModal(${pf.id})"><td class="px-2 py-px text-center align-middle text-sm" onclick="event.stopPropagation();planToggleExpand(${pf.id})"><i data-lucide="chevron-right" style="width:14px;height:14px;color:#94a3b8" id="plan_expand_icon_${pf.id}"></i></td><td class="px-2 py-px text-center align-middle text-sm truncate" style="max-width:100px" title="${esc(pf.client_name || '')}">${esc(pf.client_name || "")}</td><td class="px-2 py-px text-sm font-medium text-center align-middle truncate" style="min-width:300px;max-width:300px;width:300px" title="${esc(pf.customer_name)}">${esc(pf.customer_name)}</td><td class="px-2 py-px text-center align-middle text-sm" onclick="event.stopPropagation()">${planInlineSelect('weight', pf.id, 'weight', pf.weight || '20%', ['20%','40%','60%','80%','100%'])}</td><td class="px-2 py-px text-center align-middle text-sm" onclick="event.stopPropagation()">${planInlineSelect('ptype', pf.id, 'project_type', pf.project_type || '待签约', ['待签约','已签约'])}</td><td class="px-2 py-px text-center align-middle text-sm font-medium">${money(pf.sign_amount)}</td><td class="px-2 py-px text-center text-blue-700 align-middle">${fmtNoUnit(rev)}</td><td class="px-2 py-px text-center align-middle text-sm font-medium">${fmtNoUnit(gross)}</td><td class="px-2 py-px text-center align-middle text-sm">${grossR}</td><td class="px-2 py-px text-center text-amber-700 align-middle">${fmtNoUnit(payment)}</td><td class="px-2 py-px text-center align-middle text-sm font-semibold ${pendingCls}">${pendingVal}</td><td class="px-2 py-px text-center align-middle text-sm">${payR}</td><td class="px-2 py-px text-center text-rose-700 align-middle">${fmtNoUnit(cost)}</td><td class="px-2 py-px text-center text-purple-700 align-middle">${fmtNoUnit(paid)}</td><td class="px-2 py-px text-center font-semibold align-middle ${cfCls}">${cf}</td></tr><tr class="hidden" id="plan_expand_${pf.id}"><td colspan="15"><table class="w-full"><thead><tr class="border-b border-slate-200"><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">月份</th><th class="px-2 py-1 text-center text-xs text-slate-400">确收</th><th class="px-2 py-1 text-center text-xs text-slate-400">毛利</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">回款</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">当期成本</th><th class="px-2 py-1 text-center text-xs text-slate-400">当期流出</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">现金流</th><th class="px-2 py-1 text-center text-xs text-slate-400">待回款</th></tr></thead><tbody>${detailRows}</tbody></table></td></tr>`;
};
// 待签约简版行
const tdRowUnsigned = (pf, cost, gross) => {
@@ -1356,3 +1356,11 @@ window.planInlineUpdate = async (pfId, field, value) => {
toast("更新失败:" + error.message, "error");
}
};
// ====== 筛选器控制函数 ======
window.switchPlanFilter = (f) => { state.planFilter = f; renderPlan(); };
window.setPlanView = (v) => { state.planView = v; renderPlan(); };
window.setPlanYear = (y) => { state.planYear = parseInt(y); renderPlan(); };
window.setPlanQuarter = (q) => { state.planView = 'quarterly'; state.planQuarter = q; renderPlan(); };
window.setPlanMonth = (m) => { state.planView = 'monthly'; state.planMonth = m; renderPlan(); };
window.setPlanStatusFilter = (f) => { state.planStatusFilter = f; renderPlan(); };

View File

@@ -121,7 +121,7 @@ function renderTable(headers, rows, rowClicks) {
}
var _loadingSteps = [
{ fn: 'renderHome', label: '正在加载首页仪表盘(指标卡片+图表)', skip: function() { return state.tenant !== '总览'; } },
{ fn: 'renderHome', label: '正在加载首页仪表盘(指标卡片+图表)', skip: function() { return true; } },
{ fn: 'renderOverviewPlan', label: '正在加载预算总览', skip: function() { return state.tenant !== '总览'; } },
{ fn: 'renderOverviewFinance', label: '正在加载发生总览', skip: function() { return state.tenant !== '总览'; } },
{ fn: 'renderProjects', label: '正在加载业务机会列表' },
@@ -193,7 +193,7 @@ async function load() {
// 按需加载当前 tab 的数据(必须在 ensureStandardProposals 之前,否则会被 return 跳过)
await ensureTabData(state.active);
// 首次加载时确保标准资料库 7 项已初始化(仅首次,避免每次切换工作台都触发)
if (!_proposalsInitialized && typeof ensureStandardProposals === "function") {
if (!_proposalsInitialized && typeof ensureStandardProposals === "function" && state.tenant !== '总览') {
_proposalsInitialized = true;
try {
const proposalsData = await api("/api/proposals/list?tenant=" + encodeURIComponent(state.tenant));
@@ -282,7 +282,7 @@ function updateSidebarTabs() {
document.querySelectorAll(".sidebar-tab").forEach((btn) => {
const tab = btn.dataset.tab;
if (isOverview) {
btn.style.display = (tab === "home" || tab === "overview_plan" || tab === "overview_finance") ? "" : "none";
btn.style.display = (tab === "overview_plan" || tab === "overview_finance") ? "" : "none";
} else {
btn.style.display = (tab === "overview_plan" || tab === "overview_finance" || tab === "home") ? "none" : "";
}

View File

@@ -53,11 +53,11 @@
<i data-lucide="folder-open-dot" style="width:20px;height:20px"></i>
<span class="text-[10px] mt-1">已发生</span>
</div>
<div class="sidebar-tab hidden" data-tab="overview_plan" onclick="switchTab('overview_plan')" title="预算总览">
<div class="sidebar-tab" data-tab="overview_plan" onclick="switchTab('overview_plan')" title="预算总览">
<i data-lucide="bar-chart-horizontal" style="width:20px;height:20px"></i>
<span class="text-[10px] mt-1">预算总览</span>
</div>
<div class="sidebar-tab hidden" data-tab="overview_finance" onclick="switchTab('overview_finance')" title="发生总览">
<div class="sidebar-tab" data-tab="overview_finance" onclick="switchTab('overview_finance')" title="发生总览">
<i data-lucide="pie-chart" style="width:20px;height:20px"></i>
<span class="text-[10px] mt-1">发生总览</span>
</div>
@@ -84,7 +84,7 @@
<header class="topbar border-b border-slate-200 bg-white px-8 py-5">
<div class="flex items-center gap-3 w-full">
<div class="flex-1">
<p class="eyebrow text-xs font-semibold uppercase tracking-[0.18em] text-blue-700">OPC Manager <span class="ml-2 text-xs font-normal text-slate-400 tracking-normal">v1.2.0.24</span></p>
<p class="eyebrow text-xs font-semibold uppercase tracking-[0.18em] text-blue-700">OPC Manager <span class="ml-2 text-xs font-normal text-slate-400 tracking-normal">v1.2.0.31</span></p>
<div class="flex items-center gap-4 mt-1">
<h1 class="text-2xl font-semibold" id="workspaceTitle">科普 OPC 工作台</h1>
<div class="hidden sm:flex items-center gap-1.5 bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-100 rounded-full px-4 py-1.5">