From c0fb382d78367d92aaf530cf5f3b14bbe46e56fb Mon Sep 17 00:00:00 2001 From: mac Date: Wed, 8 Jul 2026 20:39:44 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A4=A7=E7=89=88=E6=9C=AC=E6=9B=B4?= =?UTF-8?q?=E6=96=B0-=E5=B7=A5=E4=BD=9C=E5=8F=B0=E6=94=B9=E7=89=88+?= =?UTF-8?q?=E6=80=BB=E6=8A=A5=E5=AE=8C=E5=96=84+UI=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 工作台改版: - 学会→学术, 总工作台→总览(数据库迁移+session清理) - 侧边栏工作台从下拉菜单改为竖向平铺+独立图标 - 5个工作台去除部门模块, 只有总览显示 - 工作台顺序: 总览→学术→MCN→科普→医患→科研 经营月报+季度月报: - 月度/季度总览改名(月度总览→经营月报, 季度总览→季度月报) - 财务概览增加回款/项目现金流行(7行) - 新增业务线经营情况卡片(毛利/平台费用/利润) - 新增业务线现金流卡片(项目现金流/平台费用已付/业务线现金流) - 季度月报完善为三卡片结构, 与经营月报一致 - 计划模块总览独立数据源(computePlanMonthly) UI优化: - 汇总卡片单行显示+5x2网格+去外层卡片 - 项目表格行高压缩(py-px+line-height:0.95) - 表头不压缩, 只压缩tbody - 加权/项目类型内联下拉去原生箭头+自定义箭头 - 加权/项目类型表头去除排序箭头 - 实际模块已付→支出, 已回款→回款, 成本→成本(不含平台费用) - 现金流注释(回款-支出) - 卡片间距/筛选区/tip高度压缩 - 实际模块增加执行提示tip 修复: - routes.py planFinances client_name列去重 - plan_expense.js savePlanExpense改用api()+load() - plan.js createPlanFinance拆分try-catch - 版本号 v1.0.0.35 --- backend/migrations/__init__.py | 4 +- backend/migrations/data_fixes.py | 43 +++++++ backend/migrations/seed.py | 2 +- backend/migrations/seed_data.py | 4 +- backend/routes.py | 25 ++-- static/app.js | 2 +- static/modules/finance.js | 191 ++++++++++++++++++++--------- static/modules/home.js | 2 +- static/modules/plan.js | 201 ++++++++++++++++++++----------- static/modules/projects.js | 28 ++++- static/modules/utils.js | 13 +- templates/index.html | 10 +- 12 files changed, 365 insertions(+), 160 deletions(-) diff --git a/backend/migrations/__init__.py b/backend/migrations/__init__.py index 0efdece..db13c5b 100644 --- a/backend/migrations/__init__.py +++ b/backend/migrations/__init__.py @@ -17,7 +17,7 @@ def run_migrations(): """ from migrations.tables import migrate_create_tables from migrations.columns import migrate_add_columns - from migrations.data_fixes import migrate_fix_task_status, migrate_rename_tenant, migrate_drop_product_fields, migrate_fix_service_fee_standard, migrate_fix_expense_status + from migrations.data_fixes import migrate_fix_task_status, migrate_rename_tenant, migrate_drop_product_fields, migrate_fix_service_fee_standard, migrate_fix_expense_status, migrate_rename_xuehui_to_xueshu, migrate_rename_zong_to_overview from migrations.data_split import migrate_data_split from migrations.seed import migrate_seed_users, migrate_seed_demo_data @@ -28,6 +28,8 @@ def run_migrations(): migrate_drop_product_fields() migrate_fix_service_fee_standard() migrate_fix_expense_status() + migrate_rename_xuehui_to_xueshu() + migrate_rename_zong_to_overview() migrate_data_split() migrate_seed_users() migrate_seed_demo_data() diff --git a/backend/migrations/data_fixes.py b/backend/migrations/data_fixes.py index f1c10e3..003a5bb 100644 --- a/backend/migrations/data_fixes.py +++ b/backend/migrations/data_fixes.py @@ -114,3 +114,46 @@ def migrate_fix_expense_status(): conn.close() + +def migrate_rename_xuehui_to_xueshu(): + """工作台重命名:学会·无界 → 学术·无界""" + from db import db, _exec, mysql + conn = db() + try: + tables = ["project_finances", "plan_finances", "expense_records", "plan_expense_records", + "sales_leads", "business_proposals", "product_versions", "operation_projects", + "follow_up_records", "project_tasks", "finance_records", "users", "user_tenants"] + for table in tables: + try: + cur = _exec(conn, f"UPDATE {table} SET tenant='学术·无界' WHERE tenant='学会·无界'") + affected = cur.rowcount + cur.close() + if affected: + conn.commit() + print(f"[migrate] {table}: {affected} 条记录 tenant 改为 '学术·无界'") + except mysql.connector.Error: + pass + finally: + conn.close() + + +def migrate_rename_zong_to_overview(): + """工作台重命名:总工作台 → 总览""" + from db import db, _exec, mysql + conn = db() + try: + tables = ["project_finances", "plan_finances", "expense_records", "plan_expense_records", + "sales_leads", "business_proposals", "product_versions", "operation_projects", + "follow_up_records", "project_tasks", "finance_records", "users", "user_tenants"] + for table in tables: + try: + cur = _exec(conn, f"UPDATE {table} SET tenant='总览' WHERE tenant='总工作台'") + affected = cur.rowcount + cur.close() + if affected: + conn.commit() + print(f"[migrate] {table}: {affected} 条记录 tenant 改为 '总览'") + except mysql.connector.Error: + pass + finally: + conn.close() diff --git a/backend/migrations/seed.py b/backend/migrations/seed.py index 5b00467..19ca2f7 100644 --- a/backend/migrations/seed.py +++ b/backend/migrations/seed.py @@ -28,7 +28,7 @@ def migrate_seed_users(): # 绑定工作台 tenant_map = [ ("kepu", "科普·无界"), ("keyan", "科研·无界"), ("yihuan", "医患·无界"), - ("mcn", "MCN·无界"), ("wuji", "学会·无界"), + ("mcn", "MCN·无界"), ("wuji", "学术·无界"), ] for uname, tenant in tenant_map: u = one(conn, "SELECT id FROM users WHERE username=?", (uname,)) diff --git a/backend/migrations/seed_data.py b/backend/migrations/seed_data.py index 20f2517..d9c7311 100644 --- a/backend/migrations/seed_data.py +++ b/backend/migrations/seed_data.py @@ -8,7 +8,7 @@ random.seed(42) from datetime import date from db import db, _exec, one -TENANTS = ["科普·无界", "科研·无界", "医患·无界", "MCN·无界", "学会·无界"] +TENANTS = ["科普·无界", "科研·无界", "医患·无界", "MCN·无界", "学术·无界"] # 每个工作台的差异化客户和项目模板 TENANT_CUSTOMERS = { @@ -40,7 +40,7 @@ TENANT_CUSTOMERS = { ("B站知识区", ["深度科普纪录片", "医学动画制作"]), ("微信视频号", ["医生科普直播运营", "企业微信科普推送"]), ], - "学会·无界": [ + "学术·无界": [ ("中华医学会", ["学术年会数字化平台", "继续教育学分系统", "基层医生培训"]), ("中国医师协会", ["医师定考管理系统", "专科医师培训平台"]), ("中国抗癌协会", ["肿瘤科普万里行", "CACA指南推广"]), diff --git a/backend/routes.py b/backend/routes.py index b90682d..c8712e0 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,10 +75,13 @@ 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] + # 清理旧名称 + session["tenants"] = [t if t != "总工作台" else "总览" for t in session["tenants"]] + session["tenants"] = [t if t != "学会·无界" else "学术·无界" for t in session["tenants"]] return jsonify({ "ok": True, "user": {"id": user["id"], "username": user["username"], "display_name": user["display_name"], "role": user["role"]}, @@ -123,8 +126,10 @@ def auth_me(): if "user_id" not in session: return jsonify({"logged_in": False}) tenants = session.get("tenants", []) - if "总工作台" not in tenants: - tenants = ["总工作台"] + tenants + tenants = [t if t != "总工作台" else "总览" for t in tenants] + tenants = [t if t != "学会·无界" else "学术·无界" for t in 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"]}, @@ -257,15 +262,17 @@ def bootstrap(): _year = date.today().year tenant = request.args.get("tenant", session.get("tenants", ["科普·无界"])[0]) allowed = session.get("tenants", []) - if "总工作台" not in allowed: - allowed = ["总工作台"] + allowed + allowed = [t if t != "总工作台" else "总览" for t in allowed] + allowed = [t if t != "学会·无界" else "学术·无界" for t in allowed] + 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 != "总工作台"] + if tenant == "总览": + real_tenants = [t for t in allowed if t != "总览"] all_metrics = [] all_monthly = [] all_recent = [] @@ -423,7 +430,7 @@ def bootstrap(): m[field] = sum(tl[i][field] if i < len(tl) else 0 for tl in all_monthly) merged_monthly.append(m) summary = { - "project_name": "总工作台", + "project_name": "总览", "metrics": agg, "recent": sorted(all_recent, key=lambda x: x.get("id", 0), reverse=True)[:8], "risks": [], diff --git a/static/app.js b/static/app.js index 195bc3f..6951af8 100644 --- a/static/app.js +++ b/static/app.js @@ -15,7 +15,7 @@ const savedTab = localStorage.getItem("opc-active-tab"); applyUserTenants(); updateSidebarTabs(); load().then(() => { - if (state.tenant === "总工作台") { + if (state.tenant === "总览") { switchTab("home"); } else if (savedTab && savedTab !== "home") { switchTab(savedTab); diff --git a/static/modules/finance.js b/static/modules/finance.js index 746ad27..6d54955 100644 --- a/static/modules/finance.js +++ b/static/modules/finance.js @@ -78,27 +78,27 @@ function renderFinance() { if (isRevView) { const rev = b.rev || 0; const gross = b.gross || 0; - return `${rev ? money(rev) : '—'}
${gross ? money(gross) : '—'}`; + return `${rev ? money(rev) : '—'}
${gross ? money(gross) : '—'}`; } else { const payment = b.payment || 0; const cost = b.cost || 0; - return `${payment ? money(payment) : '—'}
${cost ? money(cost) : '—'}`; + return `${payment ? money(payment) : '—'}
${cost ? money(cost) : '—'}`; } }).join(""); const totalCol = (() => { if (isRevView) { const totalRev = pf.total_rev || 0; const totalGross = pf.total_gross || 0; - return `${totalRev ? money(totalRev) : '—'}
${totalGross ? money(totalGross) : '—'}`; + return `${totalRev ? money(totalRev) : '—'}
${totalGross ? money(totalGross) : '—'}`; } else { let totalPayment = 0, totalCost = 0; try { JSON.parse(pf.budget_data || "[]").forEach(b => { totalPayment += parseFloat(b.payment||0)||0; totalCost += parseFloat(b.cost||0)||0; }); } catch (e) {} - return `${totalPayment ? money(totalPayment) : '—'}
${totalCost ? money(totalCost) : '—'}`; + return `${totalPayment ? money(totalPayment) : '—'}
${totalCost ? money(totalCost) : '—'}`; } })(); const sm = pf.sign_month || ""; - const signMonthCell = `${sm || '—'}`; - return `${esc(pf.client_name || "")}${esc(pf.customer_name)}${signMonthCell}${money(pf.sign_amount)}${mCols}${totalCol}`; + const signMonthCell = `${sm || '—'}`; + return `${esc(pf.client_name || "")}${esc(pf.customer_name)}${signMonthCell}${money(pf.sign_amount)}${mCols}${totalCol}`; }; const now2 = new Date(); @@ -110,14 +110,18 @@ function renderFinance() { const nowD = new Date(); const activeQ = state.finQuarter !== undefined ? state.finQuarter : Math.floor(nowD.getMonth() / 3); const activeMonths = qMonths[activeQ]; - const finHeaderBase = `
|按季度:${qLabels.map((q,i) => ``).join('')}|按月度:${activeMonths.map(m => { const ms = `2026-${String(m).padStart(2,'0')}`; return ``; }).join('')}
`; + const finHeaderBase = `
|按季度:${qLabels.map((q,i) => ``).join('')}|按月度:${activeMonths.map(m => { const ms = `2026-${String(m).padStart(2,'0')}`; return ``; }).join('')}
`; const finAddBtn = ``; const finFilterTabs = `
`; - document.querySelector("#finance").innerHTML = `
+ document.querySelector("#finance").innerHTML = `
${finFilterTabs} - ${state.finFilter !== 'expense' && state.finFilter !== 'overview' && state.finFilter !== 'quarterlyOverview' ? card(`
${finHeaderBase}
${finAddBtn}
`, "p-4") : ''} +
+ + 执行提示:先关注确收,毛利确保业务运营,再关注回款,回款率,最后逐步优化成本与支出 +
+ ${state.finFilter !== 'expense' && state.finFilter !== 'overview' && state.finFilter !== 'quarterlyOverview' ? card(`
${finHeaderBase}
${finAddBtn}
`, "py-2 px-4") : ''}