Compare commits

..

73 Commits

Author SHA1 Message Date
mac
7c33bc05f2 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-09 18:18:03 +08:00
mac
153835b184 fix: 非总览工作台强制跳过renderHome
- skip条件从state.active判断改为仅判断state.tenant!==总览
- 修复state.active默认为home导致renderHome仍被调用
- 版本号 v1.2.0.3
2026-07-09 18:18:03 +08:00
mac
9de0a63be2 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 13s
2026-07-09 17:41:01 +08:00
mac
2f759b42cd fix: 预算模块tab选中态+平台费用字段改名
- switchPlanTab选择器修复(.plan-tab→#planModal .finance-tab)
- 平台费用计划: 金额→费用计提, 已发生金额→现金支出
- 版本号 v1.2.0.2
2026-07-09 17:41:01 +08:00
mac
f3c94c57d6 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-09 16:52:35 +08:00
mac
b076cabb83 feat: renderHome跳过+卡片排序+模块改名
- 非总览工作台跳过renderHome(省Chart.js渲染)
- 计划→预算, 实际→执行
- 经营月报+季报卡片顺序统一: 业务线经营→业务线现金流→财务概览
- 版本号 v1.0.0.40
2026-07-09 16:52:35 +08:00
mac
9db627770b Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 16s
2026-07-09 16:33:49 +08:00
mac
c6c282537c feat: 加载浮层+工作台改回下拉菜单
- load()增加模态加载浮层: 7步进度展示+进度条+逐步渲染
- 工作台切换从竖向平铺改回下拉菜单(带图标)
- 版本号 v1.0.0.39
2026-07-09 16:33:49 +08:00
mac
75b988c77a Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-09 16:20:21 +08:00
mac
c26cbb3cf4 perf: 全项目批量优化-16处load()改为轻量刷新
- plan.js: createPlanFinance/deletePlanItem/planInlineUpdate
- finance.js: createFinance/deleteFinanceItem
- drawer.js: deleteDrawerItem/submitComment/deleteActivity
- products.js: createResource/submitProductDrawer
- projects.js: duplicateProject/submitTaskForm
- proposals.js: ensureStandardProposals/submitStandardComment/submitProposal
- plan_expense.js: savePlanExpense/deletePlanExpItem(上版本已修)
- 所有保存/删除/编辑后改为/api/<resource>/list+单模块render
- 仅保留app.js初始加载和switchTenant切换工作台的全量load()
- 版本号 v1.0.0.38
2026-07-09 16:20:21 +08:00
mac
2b7f67436e Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-09 16:03:30 +08:00
mac
296917456c perf: 计划费用保存/删除性能优化
- 后端新增/api/<resource>/list轻量级列表接口
- plan_expense.js保存/删除后改为轻量刷新,不再调load()
- 避免全量bootstrap+7模块渲染,只刷新费用列表
- 版本号 v1.0.0.37
2026-07-09 16:03:30 +08:00
mac
ed5455f309 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-08 20:48:52 +08:00
mac
452d410843 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-08 18:55:27 +08:00
mac
c95373b85e Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 20s
2026-07-08 17:45:48 +08:00
mac
6be10481d5 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 14s
2026-07-08 17:04:47 +08:00
mac
4ce0e66d90 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-08 16:49:11 +08:00
mac
0f9e1b6cf2 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-08 16:29:30 +08:00
mac
dc8d3d59d2 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-08 16:14:51 +08:00
mac
1e5c5b5706 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-08 16:05:45 +08:00
mac
b68144ebb0 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-08 15:52:45 +08:00
mac
3cae1d85cf Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 15s
2026-07-08 15:11:26 +08:00
mac
ca0cd9eab1 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-08 14:50:14 +08:00
mac
2bdc0e0779 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-08 14:16:31 +08:00
mac
93dca56c3f Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-08 13:46:10 +08:00
mac
8c4f50fc52 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-08 13:21:54 +08:00
mac
13864f1968 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 13s
2026-07-08 13:04:51 +08:00
mac
d11e8f9313 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-08 13:00:17 +08:00
mac
02a12a208c Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 14s
2026-07-08 12:23:32 +08:00
mac
b977627895 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-08 12:22:02 +08:00
mac
edf8e6179c Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-08 12:13:07 +08:00
mac
3148af05b3 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-08 11:42:07 +08:00
mac
4029933ada Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-08 11:34:00 +08:00
mac
95d3f9809c Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-07 19:22:08 +08:00
mac
8b040489e2 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-07 19:19:46 +08:00
mac
db9700c8ee Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-07 19:14:07 +08:00
mac
11e30f33b9 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-07 19:11:42 +08:00
mac
95f52b61d0 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-07 19:05:20 +08:00
mac
a4012f2189 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-07 19:03:00 +08:00
mac
e1c5e65c1e Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-07 19:00:53 +08:00
mac
a3d78b3ca0 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-07 18:44:10 +08:00
mac
9dc5631574 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-07 18:39:11 +08:00
mac
c5a59508d2 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-07 18:34:47 +08:00
mac
ab1d57492c Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-07 18:01:04 +08:00
mac
9a81a30feb Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 18s
2026-07-07 17:02:47 +08:00
mac
d6d21b03dd Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 13s
2026-07-07 15:56:13 +08:00
mac
b9a05e8732 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-07 13:15:54 +08:00
mac
440707e19a Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-07 12:56:27 +08:00
mac
e0dc1a56b2 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-06 21:42:24 +08:00
mac
f45afd954d Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-06 21:34:48 +08:00
mac
664da22329 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-06 20:58:19 +08:00
mac
73a6ac126c Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-06 19:34:24 +08:00
mac
878e26d4ca Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-06 19:19:41 +08:00
mac
e12ed0936b Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-06 19:10:18 +08:00
mac
8edb74f173 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-06 18:20:38 +08:00
mac
1a42c6e6f3 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-06 17:31:40 +08:00
mac
d21497bce3 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-06 15:39:56 +08:00
mac
574e3c5700 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-06 13:05:01 +08:00
mac
c70293b447 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-03 19:06:57 +08:00
mac
adeff08827 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-02 20:10:45 +08:00
mac
8bd40de41d Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 13s
2026-07-02 18:30:24 +08:00
mac
ac6eacea82 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-02 17:55:40 +08:00
mac
83c2a5ca94 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-02 14:50:45 +08:00
mac
fbd2290d29 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-02 14:31:06 +08:00
mac
97fcf88c61 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-06-29 17:02:55 +08:00
mac
a01afef599 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-06-29 16:52:37 +08:00
mac
28fa244fe5 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-06-29 16:45:21 +08:00
mac
ad3885e0be Merge branch 'dev'
Some checks failed
Deploy / deploy (push) Failing after 23s
2026-06-29 16:02:10 +08:00
mac
0fb7ee2992 Merge branch 'dev'
Some checks failed
Deploy / deploy (push) Failing after 14m52s
2026-06-26 12:21:02 +08:00
mac
f6792cad39 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 10s
2026-06-24 12:42:56 +08:00
mac
bed6e9192a Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-06-23 23:12:03 +08:00
mac
aaa213a765 fix(deploy): 修复 data 目录不存在导致 ln 软链失败
All checks were successful
Deploy / deploy (push) Successful in 10s
rsync 排除了 data/uploads 但没保留 data/ 空目录,
导致 ln -sfn shared/uploads data/uploads 时父目录不存在而失败。
在 ln 之前加 mkdir -p 确保目录存在。
2026-06-23 23:07:25 +08:00
Deploy Test
207629a9bb test: trigger workflow debug
Some checks failed
Deploy / deploy (push) Failing after 0s
2026-06-23 23:03:22 +08:00
11 changed files with 187 additions and 43 deletions

View File

@@ -53,3 +53,4 @@ curl http://127.0.0.1:5177/api/health
- 产品:慰心斋产品路线图中的 5 个产品版本 - 产品:慰心斋产品路线图中的 5 个产品版本
- 财务:首版财务样例和原财务 manager 合并方向 - 财务:首版财务样例和原财务 manager 合并方向
# test trigger

View File

@@ -666,6 +666,26 @@ def bootstrap():
# ---------- 通用 CRUD ---------- # ---------- 通用 CRUD ----------
@bp.route("/api/<resource>/list", methods=["GET"])
@login_required
def list_resource(resource):
"""轻量级列表接口,只返回指定 tenant 的记录"""
if resource not in TABLES:
return jsonify({"error": "unknown resource"}), 404
table, _ = TABLES[resource]
tenant = request.args.get("tenant", session.get("tenants", [""])[0])
allowed = session.get("tenants", [])
allowed = [t if t != "总工作台" else "总览" for t in allowed]
allowed = [t if t != "学会·无界" else "学术·无界" for t in allowed]
if tenant not in allowed:
tenant = allowed[0] if allowed else ""
conn = db()
try:
result = rows(conn, f"SELECT * FROM {table} WHERE tenant=? ORDER BY id DESC", [tenant])
return jsonify(result)
finally:
conn.close()
@bp.route("/api/<resource>", methods=["POST"]) @bp.route("/api/<resource>", methods=["POST"])
@login_required @login_required
def create_resource(resource): def create_resource(resource):

View File

@@ -175,7 +175,9 @@ window.deleteDrawerItem = async (resource, id) => {
logActivity(resType, id, "删除了「" + name + "」"); logActivity(resType, id, "删除了「" + name + "」");
} }
closeDrawer(); closeDrawer();
await load(); var rk = resource === 'projectFinances' ? 'projectFinances' : resource === 'planFinances' ? 'planFinances' : null;
if (rk) { try { state.data[rk] = await api("/api/" + rk + "/list?tenant=" + encodeURIComponent(state.tenant)); } catch(e) {} }
if (resource === 'projectFinances') renderFinance(); else if (resource === 'planFinances') renderPlan();
} catch (error) { } catch (error) {
toast("删除失败:" + error.message, "error"); toast("删除失败:" + error.message, "error");
} }
@@ -216,21 +218,26 @@ window.submitComment = async (event, targetType, targetId, resource) => {
button.disabled = true; button.disabled = true;
button.textContent = "发送中…"; button.textContent = "发送中…";
await api(`/api/followups/${targetType}/${targetId}`, { method: "POST", body: JSON.stringify({ data: { content } }) }); await api(`/api/followups/${targetType}/${targetId}`, { method: "POST", body: JSON.stringify({ data: { content } }) });
await load(); var ck = resource === 'projectFinances' ? 'projectFinances' : resource === 'planFinances' ? 'planFinances' : null;
if (ck) { try { state.data[ck] = await api("/api/" + ck + "/list?tenant=" + encodeURIComponent(state.tenant)); } catch(e) {} }
openDrawer(resource, targetId); openDrawer(resource, targetId);
}; };
window.deleteActivity = async (id) => { window.deleteActivity = async (id) => {
if (!confirm("确认删除这条动态?")) return; if (!confirm("确认删除这条动态?")) return;
await api(`/api/followups/${id}`, { method: "DELETE" }); await api(`/api/followups/${id}`, { method: "DELETE" });
await load(); var dk = resource === 'projectFinances' ? 'projectFinances' : resource === 'planFinances' ? 'planFinances' : null;
if (dk) { try { state.data[dk] = await api("/api/" + dk + "/list?tenant=" + encodeURIComponent(state.tenant)); } catch(e) {} }
if (resource === 'projectFinances') renderFinance(); else if (resource === 'planFinances') renderPlan();
}; };
window.deleteFollowup = async (event, followupId, resource, targetId) => { window.deleteFollowup = async (event, followupId, resource, targetId) => {
event.stopPropagation(); event.stopPropagation();
if (!confirm("确认删除这条评论?")) return; if (!confirm("确认删除这条评论?")) return;
await api(`/api/followups/${followupId}`, { method: "DELETE" }); await api(`/api/followups/${followupId}`, { method: "DELETE" });
await load(); var fkey2 = resource === 'projectFinances' ? 'projectFinances' : resource === 'planFinances' ? 'planFinances' : null;
if (fkey2) { try { state.data[fkey2] = await api("/api/" + fkey2 + "/list?tenant=" + encodeURIComponent(state.tenant)); } catch(e) {} }
if (resource === 'projectFinances') renderFinance(); else if (resource === 'planFinances') renderPlan();
openDrawer(resource, targetId); openDrawer(resource, targetId);
}; };

View File

@@ -908,7 +908,8 @@ window.createFinance = async (event) => {
document.querySelector("#pf-id-input").value = ""; document.querySelector("#pf-id-input").value = "";
document.querySelector("#financeModalTitle").textContent = "新增项目财务"; document.querySelector("#financeModalTitle").textContent = "新增项目财务";
closeFinanceModal(); closeFinanceModal();
await load(); state.data.projectFinances = await api("/api/projectFinances/list?tenant=" + encodeURIComponent(state.tenant));
renderFinance();
} catch (error) { } catch (error) {
toast("保存失败:" + error.message, "error"); toast("保存失败:" + error.message, "error");
} }
@@ -923,7 +924,8 @@ window.deleteFinanceItem = async () => {
try { try {
await api(`/api/projectFinances/${pfId}`, { method: "DELETE" }); await api(`/api/projectFinances/${pfId}`, { method: "DELETE" });
closeFinanceModal(); closeFinanceModal();
await load(); state.data.projectFinances = await api("/api/projectFinances/list?tenant=" + encodeURIComponent(state.tenant));
renderFinance();
toast("已删除", "success"); toast("已删除", "success");
} catch (error) { } catch (error) {
toast("删除失败:" + error.message, "error"); toast("删除失败:" + error.message, "error");
@@ -1033,9 +1035,9 @@ function renderFinanceOverview() {
var cfCard = card('<h3 class="text-sm font-bold text-slate-700">业务线现金流</h3><p class="text-xs text-slate-400 mb-3">项目现金流 - 平台费用(支出)= 业务线现金流</p><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + cfTbody + '</tbody></table></div>', 'p-4'); var cfCard = card('<h3 class="text-sm font-bold text-slate-700">业务线现金流</h3><p class="text-xs text-slate-400 mb-3">项目现金流 - 平台费用(支出)= 业务线现金流</p><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + cfTbody + '</tbody></table></div>', 'p-4');
document.querySelector("#financeOverview").innerHTML = '<div class="grid gap-2.5">' + document.querySelector("#financeOverview").innerHTML = '<div class="grid gap-2.5">' +
card('<h3 class="text-sm font-bold text-slate-700">业务(项目)财务月度概览</h3><p class="text-xs text-slate-400 mb-3">合同 → 确收 → 毛利 → 成本 → 支出</p><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + thead + '</thead><tbody>' + tbody + '</tbody></table></div>', 'p-4') +
deptCard + deptCard +
cfCard + cfCard +
card('<h3 class="text-sm font-bold text-slate-700">业务(项目)财务月度概览</h3><p class="text-xs text-slate-400 mb-3">合同 → 确收 → 毛利 → 成本 → 支出</p><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + thead + '</thead><tbody>' + tbody + '</tbody></table></div>', 'p-4') +
'<div class="grid grid-cols-3 gap-2.5">' + '<div class="grid grid-cols-3 gap-2.5">' +
card('<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度签约趋势</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="finChartSign"></canvas></div>', 'p-4') + card('<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度签约趋势</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="finChartSign"></canvas></div>', 'p-4') +
card('<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度确收与毛利</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="finChartRev"></canvas></div>', 'p-4') + card('<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度确收与毛利</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="finChartRev"></canvas></div>', 'p-4') +

View File

@@ -718,7 +718,7 @@ window.planEditSignMonth = (event, pfId) => {
}; };
window.switchPlanTab = (tab) => { window.switchPlanTab = (tab) => {
document.querySelectorAll(".plan-tab").forEach(b => b.classList.toggle("active", b.dataset.tab === tab)); document.querySelectorAll("#planModal .finance-tab").forEach(b => b.classList.toggle("active", b.dataset.tab === tab));
document.querySelector("#plan_financeTabInfo").classList.toggle("hidden", tab !== "info"); document.querySelector("#plan_financeTabInfo").classList.toggle("hidden", tab !== "info");
document.querySelector("#plan_financeTabRevpay").classList.toggle("hidden", tab !== "revpay"); document.querySelector("#plan_financeTabRevpay").classList.toggle("hidden", tab !== "revpay");
document.querySelector("#plan_financeTabCost").classList.toggle("hidden", tab !== "cost"); document.querySelector("#plan_financeTabCost").classList.toggle("hidden", tab !== "cost");
@@ -945,7 +945,8 @@ window.createPlanFinance = async (event) => {
document.querySelector("#plan_pf-id-input").value = ""; document.querySelector("#plan_pf-id-input").value = "";
document.querySelector("#planModalTitle").textContent = "新增项目财务"; document.querySelector("#planModalTitle").textContent = "新增项目财务";
closePlanModal(); closePlanModal();
await load(); state.data.planFinances = await api("/api/planFinances/list?tenant=" + encodeURIComponent(state.tenant));
renderPlan();
} catch (error) { } catch (error) {
toast("刷新失败:" + error.message, "error"); toast("刷新失败:" + error.message, "error");
} }
@@ -960,7 +961,8 @@ window.deletePlanItem = async () => {
try { try {
await api(`/api/planFinances/${pfId}`, { method: "DELETE" }); await api(`/api/planFinances/${pfId}`, { method: "DELETE" });
closePlanModal(); closePlanModal();
await load(); state.data.planFinances = await api("/api/planFinances/list?tenant=" + encodeURIComponent(state.tenant));
renderPlan();
toast("已删除", "success"); toast("已删除", "success");
} catch (error) { } catch (error) {
toast("删除失败:" + error.message, "error"); toast("删除失败:" + error.message, "error");
@@ -1109,9 +1111,9 @@ function renderPlanOverview() {
var cfCard = card('<h3 class="text-sm font-bold text-slate-700">业务线现金流</h3><p class="text-xs text-slate-400 mb-3">项目现金流 - 平台费用(已付)= 业务线现金流</p><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + cfTbody + '</tbody></table></div>', 'p-4'); var cfCard = card('<h3 class="text-sm font-bold text-slate-700">业务线现金流</h3><p class="text-xs text-slate-400 mb-3">项目现金流 - 平台费用(已付)= 业务线现金流</p><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + cfTbody + '</tbody></table></div>', 'p-4');
document.querySelector("#planOverview").innerHTML = '<div class="grid gap-2.5">' + document.querySelector("#planOverview").innerHTML = '<div class="grid gap-2.5">' +
card('<h3 class="text-sm font-bold text-slate-700">业务(项目)财务概览</h3><p class="text-xs text-slate-400 mb-3">合同 → 确收 → 毛利 → 成本 → 已付</p><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + thead + '</thead><tbody>' + tbody + '</tbody></table></div>', 'p-4') +
deptCard + deptCard +
cfCard + cfCard +
card('<h3 class="text-sm font-bold text-slate-700">业务(项目)财务概览</h3><p class="text-xs text-slate-400 mb-3">合同 → 确收 → 毛利 → 成本 → 已付</p><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + thead + '</thead><tbody>' + tbody + '</tbody></table></div>', 'p-4') +
'<div class="grid grid-cols-3 gap-2.5">' + '<div class="grid grid-cols-3 gap-2.5">' +
card('<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度签约趋势</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="planChartSign"></canvas></div>', 'p-4') + card('<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度签约趋势</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="planChartSign"></canvas></div>', 'p-4') +
card('<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度确收与毛利</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="planChartRev"></canvas></div>', 'p-4') + card('<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度确收与毛利</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="planChartRev"></canvas></div>', 'p-4') +
@@ -1244,9 +1246,9 @@ function renderPlanQuarterlyOverview() {
} }
document.querySelector("#planQuarterlyOverview").innerHTML = '<div class="grid gap-2.5">' + document.querySelector("#planQuarterlyOverview").innerHTML = '<div class="grid gap-2.5">' +
card('<h3 class="text-sm font-bold text-slate-700">业务(项目)财务季度概览</h3><p class="text-xs text-slate-400 mb-3">合同 → 确收 → 毛利 → 成本 → 支出 → 回款 → 项目现金流</p><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + thead + '</thead><tbody>' + tbody + '</tbody></table></div>', 'p-4') +
card('<h3 class="text-sm font-bold text-slate-700">业务线经营情况</h3><p class="text-xs text-slate-400 mb-3">毛利 - 平台费用 = 利润</p><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + deptTbody + '</tbody></table></div>', 'p-4') + card('<h3 class="text-sm font-bold text-slate-700">业务线经营情况</h3><p class="text-xs text-slate-400 mb-3">毛利 - 平台费用 = 利润</p><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + deptTbody + '</tbody></table></div>', 'p-4') +
card('<h3 class="text-sm font-bold text-slate-700">业务线现金流</h3><p class="text-xs text-slate-400 mb-3">项目现金流 - 平台费用(已付)= 业务线现金流</p><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + cfTbody + '</tbody></table></div>', 'p-4') + card('<h3 class="text-sm font-bold text-slate-700">业务线现金流</h3><p class="text-xs text-slate-400 mb-3">项目现金流 - 平台费用(已付)= 业务线现金流</p><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + cfTbody + '</tbody></table></div>', 'p-4') +
card('<h3 class="text-sm font-bold text-slate-700">业务(项目)财务季度概览</h3><p class="text-xs text-slate-400 mb-3">合同 → 确收 → 毛利 → 成本 → 支出 → 回款 → 项目现金流</p><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + thead + '</thead><tbody>' + tbody + '</tbody></table></div>', 'p-4') +
'</div>'; '</div>';
if (window.lucide) window.lucide.createIcons(); if (window.lucide) window.lucide.createIcons();
} }
@@ -1285,7 +1287,8 @@ function planInlineSelect(name, pfId, field, currentVal, options) {
window.planInlineUpdate = async (pfId, field, value) => { window.planInlineUpdate = async (pfId, field, value) => {
try { try {
await api("/api/planFinances/" + pfId, { method: "PUT", body: JSON.stringify({ data: { [field]: value } }) }); await api("/api/planFinances/" + pfId, { method: "PUT", body: JSON.stringify({ data: { [field]: value } }) });
await load(); state.data.planFinances = await api("/api/planFinances/list?tenant=" + encodeURIComponent(state.tenant));
renderPlan();
toast("已更新", "success"); toast("已更新", "success");
} catch (error) { } catch (error) {
toast("更新失败:" + error.message, "error"); toast("更新失败:" + error.message, "error");

View File

@@ -114,7 +114,7 @@ function renderPlanExpense() {
var modal = '<div id="planExpenseModal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/40" onclick="closePlanExpModal()"><div class="bg-white rounded-2xl shadow-2xl w-full max-w-lg mx-4" onclick="event.stopPropagation()"><div class="sticky top-0 z-10 bg-white/95 backdrop-blur border-b border-slate-100 px-6 py-4 flex items-center justify-between"><h3 class="text-lg font-bold text-slate-800" id="planExpenseModalTitle">新增费用</h3><div class="flex items-center gap-2"><button class="btn btn-ghost btn-sm text-red-600 hidden" id="planExpDeleteBtn" onclick="deletePlanExpItem()">删除</button><button class="btn btn-ghost btn-sm rounded-full w-8 h-8 p-0" onclick="closePlanExpModal()"><i data-lucide="x"></i></button></div></div><form onsubmit="savePlanExpense(event)" class="p-6 grid gap-4" novalidate><input type="hidden" name="expense_id" value=""><div class="grid grid-cols-2 gap-3"><label class="block"><span class="text-sm text-slate-500 mb-1 block">费用类型 <span class="text-red-500">*</span></span><select name="expense_type" required class="form-ctrl bg-white"><option value="">请选择</option>'; var modal = '<div id="planExpenseModal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/40" onclick="closePlanExpModal()"><div class="bg-white rounded-2xl shadow-2xl w-full max-w-lg mx-4" onclick="event.stopPropagation()"><div class="sticky top-0 z-10 bg-white/95 backdrop-blur border-b border-slate-100 px-6 py-4 flex items-center justify-between"><h3 class="text-lg font-bold text-slate-800" id="planExpenseModalTitle">新增费用</h3><div class="flex items-center gap-2"><button class="btn btn-ghost btn-sm text-red-600 hidden" id="planExpDeleteBtn" onclick="deletePlanExpItem()">删除</button><button class="btn btn-ghost btn-sm rounded-full w-8 h-8 p-0" onclick="closePlanExpModal()"><i data-lucide="x"></i></button></div></div><form onsubmit="savePlanExpense(event)" class="p-6 grid gap-4" novalidate><input type="hidden" name="expense_id" value=""><div class="grid grid-cols-2 gap-3"><label class="block"><span class="text-sm text-slate-500 mb-1 block">费用类型 <span class="text-red-500">*</span></span><select name="expense_type" required class="form-ctrl bg-white"><option value="">请选择</option>';
EXPENSE_TYPES.forEach(function(t) { modal += '<option>' + t + '</option>'; }); EXPENSE_TYPES.forEach(function(t) { modal += '<option>' + t + '</option>'; });
modal += '</select></label><label class="block"><span class="text-sm text-slate-500 mb-1 block">月份 <span class="text-red-500">*</span></span><select name="expense_month" required class="form-ctrl bg-white">' + modalMonthOpts + '</select></label></div><div class="grid grid-cols-2 gap-3"><label class="block"><span class="text-sm text-slate-500 mb-1 block">金额(元)</span><input name="amount" type="number" step="0.01" class="form-ctrl" placeholder="0.00"></label><label class="block"><span class="text-sm text-slate-500 mb-1 block">已发生金额(元)</span><input name="incurred_amount" type="number" step="0.01" class="form-ctrl" placeholder="0.00"></label></div><label class="block"><span class="text-sm text-slate-500 mb-1 block">状态</span><select name="status" class="form-ctrl bg-white">' + statusOpts + '</select></label><label class="block"><span class="text-sm text-slate-500 mb-1 block">费用说明</span><textarea name="notes" class="form-ctrl" rows="3" placeholder="备注信息"></textarea></label><div class="flex justify-end gap-3 pt-2"><button type="button" class="btn btn-ghost btn-sm px-6" onclick="closePlanExpModal()">取消</button><button type="submit" class="btn btn-primary btn-sm px-8">保存</button></div></form></div></div>'; modal += '</select></label><label class="block"><span class="text-sm text-slate-500 mb-1 block">月份 <span class="text-red-500">*</span></span><select name="expense_month" required class="form-ctrl bg-white">' + modalMonthOpts + '</select></label></div><div class="grid grid-cols-2 gap-3"><label class="block"><span class="text-sm text-slate-500 mb-1 block">费用计提(元)</span><input name="amount" type="number" step="0.01" class="form-ctrl" placeholder="0.00"></label><label class="block"><span class="text-sm text-slate-500 mb-1 block">已发生费用计提(元)</span><input name="incurred_amount" type="number" step="0.01" class="form-ctrl" placeholder="0.00"></label></div><label class="block"><span class="text-sm text-slate-500 mb-1 block">状态</span><select name="status" class="form-ctrl bg-white">' + statusOpts + '</select></label><label class="block"><span class="text-sm text-slate-500 mb-1 block">费用说明</span><textarea name="notes" class="form-ctrl" rows="3" placeholder="备注信息"></textarea></label><div class="flex justify-end gap-3 pt-2"><button type="button" class="btn btn-ghost btn-sm px-6" onclick="closePlanExpModal()">取消</button><button type="submit" class="btn btn-primary btn-sm px-8">保存</button></div></form></div></div>';
var target = document.querySelector("#planExpenseModule") || document.querySelector("#planExpenseModule") || document.querySelector("#planExpenseContent"); var target = document.querySelector("#planExpenseModule") || document.querySelector("#planExpenseModule") || document.querySelector("#planExpenseContent");
if (!target) return; if (!target) return;
@@ -123,10 +123,10 @@ function renderPlanExpense() {
'<div class="grid grid-cols-3 gap-3">' + '<div class="grid grid-cols-3 gap-3">' +
'<div class="metric-card p-3"><span class="flex items-center gap-2 text-sm text-slate-500">费用记录</span><strong class="mt-2 block text-xl">' + filtered.length + '</strong></div>' + '<div class="metric-card p-3"><span class="flex items-center gap-2 text-sm text-slate-500">费用记录</span><strong class="mt-2 block text-xl">' + filtered.length + '</strong></div>' +
'<div class="metric-card p-3"><span class="flex items-center gap-2 text-sm text-slate-500">总费用</span><strong class="mt-2 block text-xl">' + money(totalAmount) + '</strong></div>' + '<div class="metric-card p-3"><span class="flex items-center gap-2 text-sm text-slate-500">总费用</span><strong class="mt-2 block text-xl">' + money(totalAmount) + '</strong></div>' +
'<div class="metric-card p-3"><span class="flex items-center gap-2 text-sm text-slate-500">已发生金额</span><strong class="mt-2 block text-xl">' + money(totalIncurred) + '</strong></div>' + '<div class="metric-card p-3"><span class="flex items-center gap-2 text-sm text-slate-500">现金支出</span><strong class="mt-2 block text-xl">' + money(totalIncurred) + '</strong></div>' +
'</div>' + '</div>' +
modal + modal +
'<div class="card p-4"><div class="overflow-x-auto"><table class="w-full text-sm"><thead><tr class="bg-slate-50 border-b border-slate-200"><th class="p-2 text-center font-semibold cursor-pointer select-none" onclick="togglePlanExpSort(\'expense_month\')">月份' + sortArrow('expense_month') + '</th><th class="p-2 text-center font-semibold cursor-pointer select-none" onclick="togglePlanExpSort(\'expense_type\')">费用类型' + sortArrow('expense_type') + '</th><th class="p-2 text-center font-semibold cursor-pointer select-none" onclick="togglePlanExpSort(\'status\')">状态' + sortArrow('status') + '</th><th class="p-2 text-center font-semibold cursor-pointer select-none" onclick="togglePlanExpSort(\'amount\')">金额' + sortArrow('amount') + '</th><th class="p-2 text-center font-semibold cursor-pointer select-none" onclick="togglePlanExpSort(\'incurred_amount\')">已发生金额' + sortArrow('incurred_amount') + '</th><th class="p-2 text-center font-semibold">费用说明</th></tr></thead><tbody>' + tableRows + '</tbody></table></div></div>' + '<div class="card p-4"><div class="overflow-x-auto"><table class="w-full text-sm"><thead><tr class="bg-slate-50 border-b border-slate-200"><th class="p-2 text-center font-semibold cursor-pointer select-none" onclick="togglePlanExpSort(\'expense_month\')">月份' + sortArrow('expense_month') + '</th><th class="p-2 text-center font-semibold cursor-pointer select-none" onclick="togglePlanExpSort(\'expense_type\')">费用类型' + sortArrow('expense_type') + '</th><th class="p-2 text-center font-semibold cursor-pointer select-none" onclick="togglePlanExpSort(\'status\')">状态' + sortArrow('status') + '</th><th class="p-2 text-center font-semibold cursor-pointer select-none" onclick="togglePlanExpSort(\'amount\')">金额' + sortArrow('amount') + '</th><th class="p-2 text-center font-semibold cursor-pointer select-none" onclick="togglePlanExpSort(\'incurred_amount\')">现金支出' + sortArrow('incurred_amount') + '</th><th class="p-2 text-center font-semibold">费用说明</th></tr></thead><tbody>' + tableRows + '</tbody></table></div></div>' +
'</div>'; '</div>';
if (window.lucide) window.lucide.createIcons(); if (window.lucide) window.lucide.createIcons();
@@ -199,8 +199,10 @@ window.savePlanExpense = async function(event) {
return; return;
} }
closePlanExpModal(); closePlanExpModal();
await load(); try {
renderPlanExpense(); state.data.planExpense = await api("/api/planExpense/list?tenant=" + encodeURIComponent(state.tenant));
renderPlanExpense();
} catch (e) { /* non-critical */ }
}; };
window.deletePlanExpItem = async function() { window.deletePlanExpItem = async function() {
@@ -213,6 +215,8 @@ window.deletePlanExpItem = async function() {
return; return;
} }
closePlanExpModal(); closePlanExpModal();
await load(); try {
renderPlanExpense(); state.data.planExpense = await api("/api/planExpense/list?tenant=" + encodeURIComponent(state.tenant));
renderPlanExpense();
} catch (e) { /* non-critical */ }
}; };

View File

@@ -19,7 +19,7 @@ async function createResource(event, resource) {
const name = data.project_name || data.target_customer || data.customer_or_project_name || data.product_name || ""; const name = data.project_name || data.target_customer || data.customer_or_project_name || data.product_name || "";
if (result.id && name) logActivity(resType, result.id, "创建了" + name); if (result.id && name) logActivity(resType, result.id, "创建了" + name);
form.reset(); form.reset();
await load(); try { state.data.products = await api("/api/products/list?tenant=" + encodeURIComponent(state.tenant)); renderProducts(); } catch(e) {}
} catch (error) { } catch (error) {
toast("创建失败:" + error.message, "error"); toast("创建失败:" + error.message, "error");
} }
@@ -141,7 +141,7 @@ window.submitProductDrawer = async (event) => {
form.reset(); form.reset();
closeProductDrawer(); closeProductDrawer();
if (result.id) logActivity("product", result.id, "创建了产品版本「" + data.product_name + " " + data.version + "」"); if (result.id) logActivity("product", result.id, "创建了产品版本「" + data.product_name + " " + data.version + "」");
await load(); try { state.data.products = await api("/api/products/list?tenant=" + encodeURIComponent(state.tenant)); renderProducts(); } catch(e) {}
} catch (error) { } catch (error) {
toast("创建失败:" + error.message, "error"); toast("创建失败:" + error.message, "error");
} }

View File

@@ -67,19 +67,58 @@ function updateTenantLabel() {
}; };
const container = document.querySelector("#tenantList"); const container = document.querySelector("#tenantList");
if (container) { if (container) {
container.innerHTML = tenants.map(t => { var currentShort = (state.tenant || '').replace("·无界", "");
const shortName = t.replace("·无界", ""); var currentIcon = tenantIcons[state.tenant] || 'circle';
const isActive = t === state.tenant; container.innerHTML = '<div class="flex flex-col items-center cursor-pointer hover:bg-slate-800 rounded-lg py-2 px-1 w-14 transition-colors" onclick="toggleTenantMenu(event)" title="切换工作台">' +
var icon = tenantIcons[t] || 'circle'; '<i data-lucide="' + currentIcon + '" style="width:20px;height:20px;color:#60a5fa"></i>' +
return '<div class="flex flex-col items-center cursor-pointer rounded-lg py-1.5 px-1 w-14 transition-colors ' + (isActive ? 'bg-blue-600' : 'hover:bg-slate-800') + '" onclick="switchTenantFromMenu(\'' + t.replace(/'/g, "\\'") + '\')" title="' + esc(t) + '">' + '<span class="text-[10px] mt-1 max-w-[56px] truncate text-center text-slate-300">' + esc(currentShort) + '</span>' +
'<i data-lucide="' + icon + '" style="width:20px;height:20px;color:' + (isActive ? '#fff' : '#94a3b8') + '"></i>' + '<i data-lucide="chevron-down" style="width:12px;height:12px;color:#64748b;margin-top:2px"></i>' +
'<span class="text-[10px] mt-1 max-w-[56px] truncate text-center ' + (isActive ? 'text-white' : 'text-slate-400') + '">' + esc(shortName) + '</span>' + '</div>';
'</div>';
}).join('');
if (window.lucide) window.lucide.createIcons(); if (window.lucide) window.lucide.createIcons();
} }
} }
window.toggleTenantMenu = (event) => {
event.stopPropagation();
let menu = document.getElementById("tenantMenu");
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; });
var tenantIcons = {
'学术·无界': 'graduation-cap',
'MCN·无界': 'video',
'科普·无界': 'flask-conical',
'医患·无界': 'stethoscope',
'科研·无界': 'microscope',
'总览': 'layout-dashboard'
};
menu = document.createElement("div");
menu.id = "tenantMenu";
menu.className = "fixed bg-white rounded-lg shadow-xl border border-slate-200 py-1 min-w-[180px] z-[9999]";
menu.style.left = Math.min(rect.left - 8, window.innerWidth - 200) + "px";
menu.style.top = rect.bottom + 6 + "px";
menu.innerHTML = '<div class="px-4 py-2 border-b border-slate-100"><p class="text-xs font-semibold text-slate-500 uppercase tracking-wider">切换工作台</p></div>' +
tenants.map(t => {
const shortName = t.replace("·无界", "");
const isActive = t === state.tenant;
var icon = tenantIcons[t] || 'circle';
return '<button class="w-full text-left px-4 py-2.5 text-sm hover:bg-slate-50 transition-colors flex items-center gap-3 ' + (isActive ? 'text-blue-600 font-medium bg-blue-50' : 'text-slate-700') + '" onclick="switchTenantFromMenu(\'' + t.replace(/'/g, "\\'") + '\')">' +
'<i data-lucide="' + icon + '" style="width:16px;height:16px;color:' + (isActive ? '#2563eb' : '#94a3b8') + '"></i>' +
'<span class="flex-1">' + esc(shortName) + '</span>' +
(isActive ? '<i data-lucide="check" style="width:14px;height:14px;color:#2563eb"></i>' : '') +
'</button>';
}).join('');
document.body.appendChild(menu);
if (window.lucide) window.lucide.createIcons();
setTimeout(() => {
document.addEventListener("click", function closeMenu() {
menu.remove();
document.removeEventListener("click", closeMenu);
}, { once: true });
}, 10);
};
window.toggleUserMenu = (user) => { window.toggleUserMenu = (user) => {
let menu = document.getElementById("userMenu"); let menu = document.getElementById("userMenu");
if (menu) { menu.remove(); return; } if (menu) { menu.remove(); return; }
@@ -226,7 +265,7 @@ window.duplicateProject = async () => {
}); });
} }
toast("已创建副本", "success"); toast("已创建副本", "success");
await load(); try { state.data.operations = await api("/api/operations/list?tenant=" + encodeURIComponent(state.tenant)); renderProjects(); } catch(e) {}
} catch (error) { } catch (error) {
toast("创建副本失败:" + error.message, "error"); toast("创建副本失败:" + error.message, "error");
} }
@@ -463,7 +502,7 @@ window.submitTaskForm = async (event, projectId) => {
const result = await api("/api/tasks", { method: "POST", body: JSON.stringify({ data }) }); const result = await api("/api/tasks", { method: "POST", body: JSON.stringify({ data }) });
if (result.id && data.task) logActivity("task", result.id, "创建了任务「" + data.task + "」"); if (result.id && data.task) logActivity("task", result.id, "创建了任务「" + data.task + "」");
closeTaskDrawer(projectId); closeTaskDrawer(projectId);
await load(); try { state.data.tasks = await api("/api/tasks/list?tenant=" + encodeURIComponent(state.tenant)); } catch(e) {}
} }
} catch (error) { } catch (error) {
toast("保存失败:" + error.message, "error"); toast("保存失败:" + error.message, "error");

View File

@@ -32,7 +32,8 @@ async function ensureStandardProposals() {
}); });
} catch (e) { /* ignore */ } } catch (e) { /* ignore */ }
} }
await load(); state.data.proposals = await api("/api/proposals/list?tenant=" + encodeURIComponent(state.tenant));
renderProposals();
} }
window.switchProposalTab = (tab) => { window.switchProposalTab = (tab) => {
@@ -183,7 +184,7 @@ window.submitStandardComment = async (event, targetId) => {
button.disabled = true; button.disabled = true;
button.textContent = "发送中…"; button.textContent = "发送中…";
await api(`/api/followups/proposal/${targetId}`, { method: "POST", body: JSON.stringify({ data: { content } }) }); await api(`/api/followups/proposal/${targetId}`, { method: "POST", body: JSON.stringify({ data: { content } }) });
await load(); try { state.data.proposals = await api("/api/proposals/list?tenant=" + encodeURIComponent(state.tenant)); renderProposals(); } catch(e) {}
openStandardProposalDrawer(targetId); openStandardProposalDrawer(targetId);
}; };
@@ -207,7 +208,7 @@ window.submitProposal = async (event) => {
if (result.id && data.customer_or_project_name) logActivity("proposal", result.id, "创建了方案「" + data.customer_or_project_name + "」"); if (result.id && data.customer_or_project_name) logActivity("proposal", result.id, "创建了方案「" + data.customer_or_project_name + "」");
form.reset(); form.reset();
closeProposalModal(); closeProposalModal();
await load(); try { state.data.proposals = await api("/api/proposals/list?tenant=" + encodeURIComponent(state.tenant)); renderProposals(); } catch(e) {}
} catch (error) { } catch (error) {
toast("保存失败:" + error.message, "error"); toast("保存失败:" + error.message, "error");
} }

View File

@@ -115,18 +115,85 @@ function renderTable(headers, rows, rowClicks) {
`); `);
} }
var _loadingSteps = [
{ fn: 'renderHome', label: '正在加载首页仪表盘(指标卡片+图表)', skip: function() { return state.tenant !== '总览'; } },
{ fn: 'renderProjects', label: '正在加载业务机会列表' },
{ fn: 'renderProposals', label: '正在加载业务方案列表' },
{ fn: 'renderProducts', label: '正在加载产品迭代列表' },
{ fn: 'renderPerformance', label: '正在加载绩效管理' },
{ fn: 'renderFinance', label: '正在加载执行模块(表格+总览+图表)' },
{ fn: 'renderPlan', label: '正在加载预算模块' },
];
function showLoadingOverlay(steps) {
var existing = document.getElementById('_loadingOverlay');
if (existing) existing.remove();
var stepsHtml = steps.map(function(s, i) {
return '<div id="_loadingStep_' + i + '" class="flex items-center gap-3 py-1.5 text-sm ' + (i === 0 ? 'text-blue-600 font-medium' : 'text-slate-400') + '">' +
'<span class="w-4 h-4 flex items-center justify-center">' + (i === 0 ? '<svg class="animate-spin" width="14" height="14" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" opacity="0.25"/><path d="M12 2a10 10 0 0 1 10 10" stroke="currentColor" stroke-width="3" stroke-linecap="round"/></svg>' : '<span class="w-2 h-2 rounded-full bg-slate-300"></span>') + '</span>' +
'<span>' + s.label + '</span>' +
'</div>';
}).join('');
var overlay = document.createElement('div');
overlay.id = '_loadingOverlay';
overlay.className = 'fixed inset-0 z-[99999] flex items-center justify-center bg-black/50';
overlay.innerHTML = '<div class="bg-white rounded-2xl shadow-2xl px-8 py-6 w-full max-w-md">' +
'<h3 class="text-base font-bold text-slate-800 mb-1">正在加载工作台</h3>' +
'<p class="text-xs text-slate-400 mb-4">共 ' + steps.length + ' 个模块,请稍候…</p>' +
'<div id="_loadingSteps">' + stepsHtml + '</div>' +
'<div class="mt-4 h-1 bg-slate-100 rounded-full overflow-hidden"><div id="_loadingProgress" class="h-full bg-blue-600 transition-all duration-300" style="width:0%"></div></div>' +
'</div>';
document.body.appendChild(overlay);
}
function updateLoadingStep(idx, total) {
var stepEl = document.getElementById('_loadingStep_' + idx);
if (stepEl) {
stepEl.className = 'flex items-center gap-3 py-1.5 text-sm text-green-600 font-medium';
stepEl.querySelector('span:first-child').innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>';
}
var nextEl = document.getElementById('_loadingStep_' + (idx + 1));
if (nextEl) {
nextEl.className = 'flex items-center gap-3 py-1.5 text-sm text-blue-600 font-medium';
nextEl.querySelector('span:first-child').innerHTML = '<svg class="animate-spin" width="14" height="14" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" opacity="0.25"/><path d="M12 2a10 10 0 0 1 10 10" stroke="currentColor" stroke-width="3" stroke-linecap="round"/></svg>';
}
var progress = document.getElementById('_loadingProgress');
if (progress) progress.style.width = Math.round((idx + 1) / total * 100) + '%';
}
function hideLoadingOverlay() {
var overlay = document.getElementById('_loadingOverlay');
if (overlay) overlay.remove();
}
async function load() { async function load() {
showLoadingOverlay(_loadingSteps);
state.data = await api(`/api/bootstrap?tenant=${encodeURIComponent(state.tenant)}`); state.data = await api(`/api/bootstrap?tenant=${encodeURIComponent(state.tenant)}`);
// 首次加载时确保标准资料库 7 项已初始化 // 首次加载时确保标准资料库 7 项已初始化
if (typeof ensureStandardProposals === "function") { if (typeof ensureStandardProposals === "function") {
const existing = (state.data.proposals || []).filter(p => p.proposal_type === "标准资料"); const existing = (state.data.proposals || []).filter(p => p.proposal_type === "标准资料");
const missingCount = 7 - existing.length; const missingCount = 7 - existing.length;
if (missingCount > 0) { if (missingCount > 0) {
hideLoadingOverlay();
await ensureStandardProposals(); await ensureStandardProposals();
return; // ensureStandardProposals 内部会再次 render return; // ensureStandardProposals 内部会再次 render
} }
} }
render(); // 逐步渲染
for (var i = 0; i < _loadingSteps.length; i++) {
var step = _loadingSteps[i];
if (step.skip && step.skip()) {
updateLoadingStep(i, _loadingSteps.length);
continue;
}
if (typeof window[step.fn] === 'function') {
window[step.fn]();
}
updateLoadingStep(i, _loadingSteps.length);
await new Promise(function(r) { setTimeout(r, 50); });
}
if (window.lucide) window.lucide.createIcons();
setTimeout(hideLoadingOverlay, 300);
} }
function switchTab(tab) { function switchTab(tab) {
@@ -157,7 +224,7 @@ function updateSidebarTabs() {
function render() { function render() {
if (!state.data) return; if (!state.data) return;
renderHome(); if (state.tenant === '总览') renderHome();
renderProjects(); renderProjects();
renderProposals(); renderProposals();
renderProducts(); renderProducts();

View File

@@ -45,13 +45,13 @@
<i data-lucide="trending-up" style="width:20px;height:20px"></i> <i data-lucide="trending-up" style="width:20px;height:20px"></i>
<span class="text-[10px] mt-1">部门</span> <span class="text-[10px] mt-1">部门</span>
</div> </div>
<div class="sidebar-tab" data-tab="plan" onclick="switchTab('plan')" title="计划"> <div class="sidebar-tab" data-tab="plan" onclick="switchTab('plan')" title="预算">
<i data-lucide="calendar-range" style="width:20px;height:20px"></i> <i data-lucide="calendar-range" style="width:20px;height:20px"></i>
<span class="text-[10px] mt-1">计划</span> <span class="text-[10px] mt-1">预算</span>
</div> </div>
<div class="sidebar-tab" data-tab="finance" onclick="switchTab('finance')" title="实际"> <div class="sidebar-tab" data-tab="finance" onclick="switchTab('finance')" title="执行">
<i data-lucide="folder-open-dot" style="width:20px;height:20px"></i> <i data-lucide="folder-open-dot" style="width:20px;height:20px"></i>
<span class="text-[10px] mt-1">实际</span> <span class="text-[10px] mt-1">执行</span>
</div> </div>
<div class="sidebar-tab" data-tab="projects" onclick="switchTab('projects')" title="Focus"> <div class="sidebar-tab" data-tab="projects" onclick="switchTab('projects')" title="Focus">
<i data-lucide="file-text" style="width:20px;height:20px"></i> <i data-lucide="file-text" style="width:20px;height:20px"></i>
@@ -76,7 +76,7 @@
<header class="topbar border-b border-slate-200 bg-white px-8 py-5"> <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 items-center gap-3 w-full">
<div class="flex-1"> <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.0.0.36</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.3</span></p>
<div class="flex items-center gap-4 mt-1"> <div class="flex items-center gap-4 mt-1">
<h1 class="text-2xl font-semibold" id="workspaceTitle">科普 OPC 工作台</h1> <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"> <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">