Compare commits

..

112 Commits

Author SHA1 Message Date
mac
3930451ce8 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-15 10:03:12 +08:00
mac
39513c0a7b feat: 版本号自动同步 + 五色标记 + 财务指标颜色优化
- 左上角版本号从 git tag 自动读取(flask_app.py → Jinja2 注入)
- 项目管理右键菜单新增标黄/蓝/绿/紫四种颜色标记 + 取消标记
- 应收/应付列正数绿负数红(finance+plan 双模块)
- 经营月报利润行负数标红
- 预算模块移除状态筛选标签,添加年份选择器
- 年份下拉框 pr-6 修复箭头重叠
2026-07-15 10:03:12 +08:00
mac
e2a421cde7 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-14 15:25:38 +08:00
mac
aa3ec75179 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 13s
2026-07-13 16:34:57 +08:00
mac
4ad19d438f Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-13 14:57:49 +08:00
mac
30b1a50490 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 13s
2026-07-13 13:47:56 +08:00
mac
01718e9acc Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-13 13:41:48 +08:00
mac
87f0abbbd8 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-13 09:15:18 +08:00
mac
c2ed20caeb Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-12 21:48:12 +08:00
mac
a3f269046d Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-12 17:23:28 +08:00
mac
ec062fb2ec Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 14s
2026-07-12 17:13:06 +08:00
mac
c4af452115 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 13s
2026-07-12 17:05:03 +08:00
mac
8c318fb3ad Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 15s
2026-07-12 16:47:25 +08:00
mac
9c2beee51e Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-11 15:43:43 +08:00
mac
6a582c65b8 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-11 15:39:35 +08:00
mac
3fff80425b Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-11 15:27:12 +08:00
mac
02abe41793 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-11 15:23:16 +08:00
mac
f30d40fe70 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-11 15:10:16 +08:00
mac
eb300731a3 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-11 15:06:23 +08:00
mac
67bc00314c Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-11 14:54:28 +08:00
mac
d34e52eff4 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 17s
2026-07-11 14:36:00 +08:00
mac
8e38f50c99 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-11 12:31:50 +08:00
mac
3a40e78c3e Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 13s
2026-07-11 12:26:29 +08:00
mac
a1a4f41d79 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-11 11:57:42 +08:00
mac
d4889d56ec Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 17s
2026-07-11 11:50:18 +08:00
mac
35706f123d Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-11 11:44:05 +08:00
mac
e4d1cd59f3 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-10 23:47:54 +08:00
mac
1673603b02 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-10 23:22:12 +08:00
mac
db93eb8d31 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-10 23:02:48 +08:00
mac
94ad7aed55 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 16s
2026-07-10 22:32:27 +08:00
mac
043938670a Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 13s
2026-07-10 21:43:19 +08:00
mac
9862d9fe56 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-10 21:19:53 +08:00
mac
150e627811 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-10 20:14:53 +08:00
mac
295bf5a81c Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-10 20:06:03 +08:00
mac
e8a26059d5 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-10 19:52:00 +08:00
mac
543d620384 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-10 19:49:11 +08:00
mac
975034c589 feat: 新增测试工作台+tip样式调整+tip/tab位置对调
- 新增测试·无界工作台(所有人可见)
- 为所有opc_owner用户添加测试工作台权限
- 预算模块tip改淡黄色+新文案
- 预算/已发生模块tip和tab位置对调(tip在上)
- 版本号 v1.2.0.12
2026-07-10 19:49:11 +08:00
mac
71e90bd55f Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 17s
2026-07-10 19:32:00 +08:00
mac
3042791a90 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 13s
2026-07-10 19:11:32 +08:00
mac
a8243acc5a Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 21s
2026-07-10 18:22:54 +08:00
mac
cb0e3fdac2 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 13s
2026-07-10 17:33:05 +08:00
mac
f5c92d09af Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 13s
2026-07-10 13:38:11 +08:00
mac
e18626105b Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 16s
2026-07-10 13:27:22 +08:00
mac
b587530437 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-10 13:12:42 +08:00
mac
9a1260db68 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-10 13:10:04 +08:00
mac
7c33bc05f2 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
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
f3c94c57d6 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
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
75b988c77a Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
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
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
5 changed files with 78 additions and 37 deletions

View File

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

View File

@@ -28,6 +28,19 @@ if not _secret_key:
)
app.secret_key = _secret_key
app.config["TEMPLATES_AUTO_RELOAD"] = True
# 自动读取 git tag 作为版本号,保持左上角与发布 tag 一致
import subprocess as _sp
def _git_version():
try:
tags = _sp.check_output(
["git", "tag", "--sort=-v:refname"],
cwd=_backend_dir, stderr=_sp.DEVNULL,
).decode().strip().split("\n")
return tags[0] if tags and tags[0] else "dev"
except Exception:
return "dev"
app.jinja_env.globals["app_version"] = _git_version()
app.register_blueprint(bp)
# 模块级执行迁移(保留 gunicorn --preload 行为:导入即触发)

View File

@@ -98,7 +98,7 @@ function renderFinance() {
})();
const sm = pf.sign_month || "";
const signMonthCell = `<td class="px-2 py-px text-center align-middle text-sm"><span class="pf-sm-text cursor-pointer hover:text-brand-600" id="pf-sm-${pf.id}" onclick="event.stopPropagation(); editPfSignMonth(event, ${pf.id})">${sm || '—'}</span></td>`;
return `<tr class="border-b ${state._fmMarks.red[pf.id] ? 'bg-red-50' : 'border-slate-100'} hover:bg-slate-50 cursor-pointer" oncontextmenu="finShowCtx(event, ${pf.id})" onclick="openPfEditModal(${pf.id})"><td class="px-2 py-px text-sm font-medium text-center align-middle">${esc(pf.customer_name)}</td>${signMonthCell}<td class="px-2 py-px text-center align-middle text-sm">${money(pf.sign_amount)}</td>${mCols}${totalCol}</tr>`;
return `<tr class="border-b ${fmMarkBg(pf.id) || 'border-slate-100'} hover:bg-slate-50 cursor-pointer" oncontextmenu="finShowCtx(event, ${pf.id})" onclick="openPfEditModal(${pf.id})"><td class="px-2 py-px text-sm font-medium text-center align-middle">${esc(pf.customer_name)}</td>${signMonthCell}<td class="px-2 py-px text-center align-middle text-sm">${money(pf.sign_amount)}</td>${mCols}${totalCol}</tr>`;
};
const now2 = new Date();
@@ -119,7 +119,7 @@ function renderFinance() {
const activeQ = state.finQuarter !== undefined ? state.finQuarter : Math.floor(nowD.getMonth() / 3);
const activeMonths = qMonths[activeQ];
const yearOpts = [2020,2021,2022,2023,2024,2025,2026,2027];
const finHeaderBase = `<div class="flex items-center gap-2"><span class="text-sm text-slate-500">年份:</span><select onchange="setFinYear(this.value)" class="px-2 py-0.5 rounded text-sm font-medium border border-slate-200 bg-white">${`<option value="0" ${state.finYear===0?'selected':''}>所有年份</option>`}${yearOpts.map(y => `<option value="${y}" ${state.finYear===y?'selected':''}>${y}年</option>`).join('')}</select><span class="text-slate-300 mx-2">|</span><button onclick="setFinView('annual')" class="px-3 py-0.5 rounded text-sm font-medium ${state.finView==='annual'?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">全年</button><span class="text-slate-300 mx-2">|</span><span class="text-sm text-slate-500">季度:</span>${qLabels.map((q,i) => `<button onclick="setFinQuarter(${i})" class="px-3 py-0.5 rounded text-sm font-medium ${state.finView==='quarterly'&&activeQ===i?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">${q}</button>`).join('')}<span class="text-slate-300 mx-2">|</span><span class="text-sm text-slate-500">月度:</span>${activeMonths.map(m => { const ms = `${state.finYear}-${String(m).padStart(2,'0')}`; return `<button onclick="setFinMonth('${ms}')" class="px-3 py-0.5 rounded text-sm font-medium ${state.finView==='monthly'&&state.finMonth===ms?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">${m}月</button>`; }).join('')}</div>`;
const finHeaderBase = `<div class="flex items-center gap-2"><span class="text-sm text-slate-500">年份:</span><select onchange="setFinYear(this.value)" class="px-2 py-0.5 pr-6 rounded text-sm font-medium border border-slate-200 bg-white">${`<option value="0" ${state.finYear===0?'selected':''}>所有年份</option>`}${yearOpts.map(y => `<option value="${y}" ${state.finYear===y?'selected':''}>${y}年</option>`).join('')}</select><span class="text-slate-300 mx-2">|</span><button onclick="setFinView('annual')" class="px-3 py-0.5 rounded text-sm font-medium ${state.finView==='annual'?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">全年</button><span class="text-slate-300 mx-2">|</span><span class="text-sm text-slate-500">季度:</span>${qLabels.map((q,i) => `<button onclick="setFinQuarter(${i})" class="px-3 py-0.5 rounded text-sm font-medium ${state.finView==='quarterly'&&activeQ===i?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">${q}</button>`).join('')}<span class="text-slate-300 mx-2">|</span><span class="text-sm text-slate-500">月度:</span>${activeMonths.map(m => { const ms = `${state.finYear}-${String(m).padStart(2,'0')}`; return `<button onclick="setFinMonth('${ms}')" class="px-3 py-0.5 rounded text-sm font-medium ${state.finView==='monthly'&&state.finMonth===ms?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">${m}月</button>`; }).join('')}</div>`;
const finAddBtn = `<button class="btn btn-primary btn-sm" onclick="openFinanceModal()">新增财务项目</button>`;
const isOverviewMode = false;
@@ -349,13 +349,13 @@ function renderFinance() {
const payR = payRVal !== null ? '<span class="' + payRCls + ' font-semibold">' + payRVal + '%</span>' : '<span class="text-slate-300">—</span>';
const paidR = cost && paid ? Math.round(paid / cost * 100) + '%' : '<span class="text-slate-300">—</span>';
const payable = cost - paid;
const payableCls = payable > 0 ? 'text-rose-600' : payable < 0 ? 'text-green-600' : 'text-slate-700';
const payableCls = payable > 0 ? 'text-green-600' : payable < 0 ? 'text-red-600' : 'text-slate-700';
const payableVal = payable ? (payable > 0 ? '+' : '') + money(payable) : '<span class="text-slate-300">—</span>';
const grossR = rev && gross ? Math.round(gross / rev * 100) + '%' : '<span class="text-slate-300">—</span>';
const cf = cashflow ? money(cashflow) : '<span class="text-slate-300">—</span>';
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-700';
const pendingCls = pending > 0 ? 'text-green-600' : pending < 0 ? 'text-red-600' : 'text-slate-700';
const pendingVal = pending ? (pending > 0 ? '+' : '') + money(pending) : '<span class="text-slate-300">—</span>';
var budgetArr = [];
var expenseArr = [];
@@ -379,11 +379,11 @@ 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-600">' + m + '</td><td class="px-2 py-px text-center text-xs text-brand-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 ${state._fmMarks.red[pf.id] ? 'bg-red-50' : 'border-slate-100'} hover:bg-slate-50 cursor-pointer" oncontextmenu="finShowCtx(event, ${pf.id})" 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-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-brand-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 font-semibold ${payableCls}">${payableVal}</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-600">月份</th><th class="px-2 py-1 text-center text-xs text-slate-600">确收</th><th class="px-2 py-1 text-center text-xs text-slate-600">毛利</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-600">回款</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-600">成本</th><th class="px-2 py-1 text-center text-xs text-slate-600">支出</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-600">现金流</th><th class="px-2 py-1 text-center text-xs text-slate-600">应收</th></tr></thead><tbody>${detailRows}</tbody></table></td></tr>`;
return `<tr class="border-b ${fmMarkBg(pf.id) || 'border-slate-100'} hover:bg-slate-50 cursor-pointer" oncontextmenu="finShowCtx(event, ${pf.id})" 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-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-brand-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 font-semibold ${payableCls}">${payableVal}</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-600">月份</th><th class="px-2 py-1 text-center text-xs text-slate-600">确收</th><th class="px-2 py-1 text-center text-xs text-slate-600">毛利</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-600">回款</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-600">成本</th><th class="px-2 py-1 text-center text-xs text-slate-600">支出</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-600">现金流</th><th class="px-2 py-1 text-center text-xs text-slate-600">应收</th></tr></thead><tbody>${detailRows}</tbody></table></td></tr>`;
};
// 待签约简版行
const tdRowUnsigned = (pf, cost, gross) => {
return `<tr class="border-b ${state._fmMarks.red[pf.id] ? 'bg-red-50' : 'border-slate-100'} hover:bg-slate-50 cursor-pointer" oncontextmenu="finShowCtx(event, ${pf.id})" onclick="openPfEditModal(${pf.id})"><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 align-middle text-sm">${pf.sign_month || '—'}</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 text-rose-700 align-middle">${fmtNoUnit(cost)}</td></tr>`;
return `<tr class="border-b ${fmMarkBg(pf.id) || 'border-slate-100'} hover:bg-slate-50 cursor-pointer" oncontextmenu="finShowCtx(event, ${pf.id})" onclick="openPfEditModal(${pf.id})"><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 align-middle text-sm">${pf.sign_month || '—'}</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 text-rose-700 align-middle">${fmtNoUnit(cost)}</td></tr>`;
};
if (state.finView === 'monthly') {
@@ -1104,11 +1104,12 @@ function renderFinanceOverview() {
for (var dri = 0; dri < 3; dri++) {
var dr = deptRows[dri];
var rowCls = dri === 2 ? 'font-semibold' : '';
var valCls = dri === 2 ? 'text-green-600' : 'text-slate-700';
var valCls = dri === 2 ? (dr.annual >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-700';
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>';
rowHtml += '<td class="py-1.5 text-right ' + rowCls + ' ' + valCls + ' whitespace-nowrap">' + moneyIntL(dr.annual) + '</td>';
for (var dmi = 0; dmi < 12; dmi++) {
rowHtml += '<td class="py-1.5 text-right text-sm whitespace-nowrap ' + valCls + '">' + moneyIntL(dr.vals[dmi]) + '</td>';
var cellCls = dri === 2 ? (dr.vals[dmi] >= 0 ? 'text-green-600' : 'text-red-600') : valCls;
rowHtml += '<td class="py-1.5 text-right text-sm whitespace-nowrap ' + cellCls + '">' + moneyIntL(dr.vals[dmi]) + '</td>';
}
deptTbody += rowHtml + '</tr>';
}
@@ -1151,7 +1152,7 @@ function renderFinanceOverview() {
}
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');
const yearSelectOv = `<div class="card px-4 py-2 mb-4"><div class="flex items-center gap-2"><span class="text-sm font-medium text-slate-500">📅 选择年份:</span><select onchange="setFinYear(this.value)" class="px-2 py-0.5 rounded text-sm font-medium border border-slate-200 bg-white"><option value="0"${state.finYear===0?' selected':''}>所有年份</option>${[2020,2021,2022,2023,2024,2025,2026,2027].map(y => '<option value="'+y+'"'+(state.finYear===y?' selected':'')+'>'+y+'年</option>').join('')}</select></div></div>`;
const yearSelectOv = `<div class="card px-4 py-2 mb-4"><div class="flex items-center gap-2"><span class="text-sm font-medium text-slate-500">📅 选择年份:</span><select onchange="setFinYear(this.value)" class="px-2 py-0.5 pr-6 rounded text-sm font-medium border border-slate-200 bg-white"><option value="0"${state.finYear===0?' selected':''}>所有年份</option>${[2020,2021,2022,2023,2024,2025,2026,2027].map(y => '<option value="'+y+'"'+(state.finYear===y?' selected':'')+'>'+y+'年</option>').join('')}</select></div></div>`;
document.querySelector("#financeOverview").innerHTML = yearSelectOv + '<div class="grid gap-2.5">' +
deptCard +
cfCard +
@@ -1292,7 +1293,7 @@ function renderFinanceQuarterlyOverview() {
cfTbody += rowHtml + '</tr>';
}
const yearSelectQ = `<div class="card px-4 py-2 mb-4"><div class="flex items-center gap-2"><span class="text-sm font-medium text-slate-500">📅 选择年份:</span><select onchange="setFinYear(this.value)" class="px-2 py-0.5 rounded text-sm font-medium border border-slate-200 bg-white"><option value="0"${state.finYear===0?' selected':''}>所有年份</option>${[2020,2021,2022,2023,2024,2025,2026,2027].map(y => '<option value="'+y+'"'+(state.finYear===y?' selected':'')+'>'+y+'年</option>').join('')}</select></div></div>`;
const yearSelectQ = `<div class="card px-4 py-2 mb-4"><div class="flex items-center gap-2"><span class="text-sm font-medium text-slate-500">📅 选择年份:</span><select onchange="setFinYear(this.value)" class="px-2 py-0.5 pr-6 rounded text-sm font-medium border border-slate-200 bg-white"><option value="0"${state.finYear===0?' selected':''}>所有年份</option>${[2020,2021,2022,2023,2024,2025,2026,2027].map(y => '<option value="'+y+'"'+(state.finYear===y?' selected':'')+'>'+y+'年</option>').join('')}</select></div></div>`;
document.querySelector("#financeQuarterlyOverview").innerHTML = yearSelectQ + '<div class="grid gap-2.5">' +
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') +
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') +
@@ -1343,7 +1344,21 @@ window.setFinMonth = (m) => { state.finView = 'monthly'; state.finMonth = m; loc
// ---- 项目标记上下文菜单 ----
if (!state._fmMarks) state._fmMarks = (function() { try { return JSON.parse(localStorage.getItem('opc_fm_marks')) || { red: {} }; } catch(e) { return { red: {} }; } })();
if (!state._fmMarks) state._fmMarks = (function() { try { return JSON.parse(localStorage.getItem('opc_fm_marks')) || { color: {} }; } catch(e) { return { color: {} }; } })();
if (!state._fmMarks.color) state._fmMarks.color = {};
const FM_COLORS = [
{ key: 'red', label: '标红', hex: '#ef4444', bg: 'bg-red-50' },
{ key: 'yellow', label: '标黄', hex: '#eab308', bg: 'bg-yellow-50' },
{ key: 'blue', label: '标蓝', hex: '#3b82f6', bg: 'bg-blue-50' },
{ key: 'green', label: '标绿', hex: '#22c55e', bg: 'bg-green-50' },
{ key: 'purple', label: '标紫', hex: '#a855f7', bg: 'bg-purple-50' },
];
const fmMarkBg = function(id) {
var c = (state._fmMarks.color||{})[id];
return c ? { red:'bg-red-50',yellow:'bg-yellow-50',blue:'bg-blue-50',green:'bg-green-50',purple:'bg-purple-50'}[c] || '' : '';
};
window.finShowCtx = function(e, pfId) {
e.preventDefault();
var menu = document.getElementById('finCtxMenu');
@@ -1352,12 +1367,27 @@ window.finShowCtx = function(e, pfId) {
menu.id = 'finCtxMenu';
menu.style.cssText = 'display:none;position:fixed;z-index:9999;background:#fff;border-radius:8px;box-shadow:0 10px 40px rgba(0,0,0,.15);border:1px solid #e2e8f0;padding:4px 0;min-width:130px';
menu.innerHTML = '<div style="padding:4px 12px;font-size:11px;font-weight:600;color:#94a3b8">项目标记</div>' +
'<div data-act="red" style="padding:6px 12px;font-size:13px;cursor:pointer;color:#334155"><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#ef4444;margin-right:8px"></span>标红</div>';
FM_COLORS.map(function(c) {
return '<div data-act="' + c.key + '" style="padding:6px 12px;font-size:13px;cursor:pointer;color:#334155;display:flex;align-items:center"><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:' + c.hex + ';margin-right:8px;flex-shrink:0"></span>' + c.label + '</div>';
}).join('') +
'<div style="border-top:1px solid #e2e8f0;margin:2px 0"></div>' +
'<div data-act="clear" style="padding:6px 12px;font-size:13px;cursor:pointer;color:#94a3b8">取消标记</div>';
menu.addEventListener('click', function(ev) {
var target = ev.target.closest('[data-act]');
if (!target) return;
ev.stopPropagation();
var act = target.getAttribute('data-act');
var id = menu.getAttribute('data-pfid');
menu.style.display = 'none';
if (state._fmMarks.red[id]) { delete state._fmMarks.red[id]; } else { state._fmMarks.red[id] = true; }
if (act === 'clear') {
delete state._fmMarks.color[id];
} else {
if (state._fmMarks.color[id] === act) {
delete state._fmMarks.color[id];
} else {
state._fmMarks.color[id] = act;
}
}
localStorage.setItem('opc_fm_marks', JSON.stringify(state._fmMarks));
renderFinance();
});
@@ -1366,15 +1396,6 @@ window.finShowCtx = function(e, pfId) {
menu.setAttribute('data-pfid', pfId);
menu.style.display = 'block';
menu.style.left = e.pageX + 'px';
menu.style.top = Math.min(e.pageY, window.innerHeight - 200) + 'px';
menu.style.top = Math.min(e.pageY, window.innerHeight - 280) + 'px';
};
document.addEventListener('click', function() { var m = document.getElementById('finCtxMenu'); if (m) m.style.display = 'none'; });
window.finCtxMenu_act = function(e, type) {
e.stopPropagation();
var pfId = document.getElementById("finCtxMenu").getAttribute("data-pfid");
if (type === "clear") { delete state._fmMarks.red[pfId]; delete state._fmMarks.pin[pfId]; }
else { if (state._fmMarks[type][pfId]) { delete state._fmMarks[type][pfId]; } else { state._fmMarks[type][pfId] = true; } }
localStorage.setItem('opc_fm_marks', JSON.stringify(state._fmMarks));
renderFinance();
};

View File

@@ -103,15 +103,15 @@ function renderPlan() {
const defaultMonth2 = now2.getFullYear() + "-" + String(now2.getMonth()+1).padStart(2,"0");
if (!state.planMonth) state.planMonth = defaultMonth2;
if (state.planQuarter === undefined) state.planQuarter = Math.floor(now2.getMonth() / 3);
if (!state.planYear) state.planYear = now2.getFullYear();
if (!state.planView) state.planView = 'annual';
if (state.planYear == null) state.planYear = now2.getFullYear();
if (!['annual','quarterly','monthly'].includes(state.planView)) state.planView = 'annual';
const qLabels = ['Q1','Q2','Q3','Q4'];
const qMonths = [[1,2,3],[4,5,6],[7,8,9],[10,11,12]];
const nowD = new Date();
const activeQ = state.planQuarter !== undefined ? state.planQuarter : Math.floor(nowD.getMonth() / 3);
const activeMonths = qMonths[activeQ];
const yearOpts = [2024,2025,2026,2027];
const finHeaderBase = `<div class="flex items-center gap-2"><button onclick="setPlanStatusFilter('all')" class="px-3 py-0.5 rounded text-sm font-medium ${state.planStatusFilter==='all'?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">全部</button><button onclick="setPlanStatusFilter('unsigned')" class="px-3 py-0.5 rounded text-sm font-medium ${state.planStatusFilter==='unsigned'?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">待签约</button><button onclick="setPlanStatusFilter('signed')" class="px-3 py-0.5 rounded text-sm font-medium ${state.planStatusFilter==='signed'?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">已签约</button><button onclick="setPlanStatusFilter('paymentonly')" class="px-3 py-0.5 rounded text-sm font-medium ${state.planStatusFilter==='paymentonly'?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">只付款</button><span class="text-slate-300 mx-2">|</span><span class="text-sm text-slate-500">年份</span><select onchange="setPlanYear(this.value)" class="px-2 py-0.5 rounded text-sm font-medium border border-slate-200 bg-white">${yearOpts.map(y => `<option value="${y}" ${state.planYear===y?'selected':''}>${y}</option>`).join('')}</select><span class="text-slate-300 mx-2">|</span><button onclick="setPlanView('annual')" class="px-3 py-0.5 rounded text-sm font-medium ${state.planView==='annual'?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">全年</button><span class="text-slate-300 mx-2">|</span><span class="text-sm text-slate-500">季度:</span>${qLabels.map((q,i) => `<button onclick="setPlanQuarter(${i})" class="px-3 py-0.5 rounded text-sm font-medium ${state.planView==='quarterly'&&activeQ===i?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">${q}</button>`).join('')}<span class="text-slate-300 mx-2">|</span><span class="text-sm text-slate-500">月度:</span>${activeMonths.map(m => { const ms = `${state.planYear}-${String(m).padStart(2,'0')}`; return `<button onclick="setPlanMonth('${ms}')" class="px-3 py-0.5 rounded text-sm font-medium ${state.planView==='monthly'&&state.planMonth===ms?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">${m}月</button>`; }).join('')}</div>`;
const yearOpts = [2020,2021,2022,2023,2024,2025,2026,2027];
const finHeaderBase = `<div class="flex items-center gap-2"><span class="text-sm text-slate-500">年份:</span><select onchange="setPlanYear(this.value)" class="px-2 py-0.5 pr-6 rounded text-sm font-medium border border-slate-200 bg-white"><option value="0"${state.planYear===0?' selected':''}>所有年份</option>${yearOpts.map(y => `<option value="${y}" ${state.planYear===y?'selected':''}>${y}</option>`).join('')}</select><span class="text-slate-300 mx-2">|</span><button onclick="setPlanView('annual')" class="px-3 py-0.5 rounded text-sm font-medium ${state.planView==='annual'?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">全年</button><span class="text-slate-300 mx-2">|</span><span class="text-sm text-slate-500">季度:</span>${qLabels.map((q,i) => `<button onclick="setPlanQuarter(${i})" class="px-3 py-0.5 rounded text-sm font-medium ${state.planView==='quarterly'&&activeQ===i?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">${q}</button>`).join('')}<span class="text-slate-300 mx-2">|</span><span class="text-sm text-slate-500">月度:</span>${activeMonths.map(m => { const ms = `${state.planYear}-${String(m).padStart(2,'0')}`; return `<button onclick="setPlanMonth('${ms}')" class="px-3 py-0.5 rounded text-sm font-medium ${state.planView==='monthly'&&state.planMonth===ms?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">${m}月</button>`; }).join('')}</div>`;
const finAddBtn = `<button class="btn btn-primary btn-sm" onclick="openPlanModal()">新增财务项目</button>`;
const isOverviewMode = false;
@@ -369,10 +369,10 @@ function renderPlan() {
const cf = cashflow ? money(cashflow) : '<span class="text-slate-300">—</span>';
const cfCls = cashflow >= 0 ? 'text-green-600' : 'text-red-600';
const payable = cost - paid;
const payableCls = payable > 0 ? 'text-rose-600' : payable < 0 ? 'text-green-600' : 'text-slate-400';
const payableCls = payable > 0 ? 'text-green-600' : payable < 0 ? 'text-red-600' : 'text-slate-400';
const payableVal = payable ? (payable > 0 ? '+' : '') + money(payable) : '<span class="text-slate-300">—</span>';
const pending = rev - payment;
const pendingCls = pending > 0 ? 'text-red-600' : pending < 0 ? 'text-green-600' : 'text-slate-700';
const pendingCls = pending > 0 ? 'text-green-600' : pending < 0 ? 'text-red-600' : 'text-slate-700';
const pendingVal = pending ? (pending > 0 ? '+' : '') + money(pending) : '<span class="text-slate-300">—</span>';
var budgetArr = [];
var expenseArr = [];
@@ -463,12 +463,12 @@ function renderPlan() {
const qRange = qRanges[selQ];
const sumBudget = (pf, field) => {
let total = 0;
try { JSON.parse(pf.budget_data || "[]").forEach(b => { const m = parseInt((b.month || "").substring(5)) || 0; const y = parseInt((b.month || "").substring(0, 4)) || 0; if (qRange.includes(m) && y === state.planYear) total += parseFloat(b[field] || 0); }); } catch (e) {}
try { JSON.parse(pf.budget_data || "[]").forEach(b => { const m = parseInt((b.month || "").substring(5)) || 0; const y = parseInt((b.month || "").substring(0, 4)) || 0; if (qRange.includes(m) && (y === state.planYear || state.planYear === 0)) total += parseFloat(b[field] || 0); }); } catch (e) {}
return total;
};
const sumExpense = (pf, field) => {
let total = 0;
try { JSON.parse(pf.expense_data || "[]").forEach(e => { const m = parseInt((e.month || "").substring(5)) || 0; const y = parseInt((e.month || "").substring(0, 4)) || 0; if (qRange.includes(m) && y === state.planYear) total += parseFloat(e[field] || 0); }); } catch (e) {}
try { JSON.parse(pf.expense_data || "[]").forEach(e => { const m = parseInt((e.month || "").substring(5)) || 0; const y = parseInt((e.month || "").substring(0, 4)) || 0; if (qRange.includes(m) && (y === state.planYear || state.planYear === 0)) total += parseFloat(e[field] || 0); }); } catch (e) {}
return total;
};
let dataItems = [];
@@ -1113,6 +1113,8 @@ function renderPlanOverview() {
for (var ei = 0; ei < planExp.length; ei++) {
var em = (planExp[ei].expense_month || '').substring(0, 7);
var mi = parseInt(em.substring(5)) - 1;
var emYear = parseInt(em.substring(0, 4)) || 0;
if (state.planYear !== 0 && emYear && emYear !== state.planYear) continue;
if (mi >= 0 && mi < 12) expMonthly[mi] += parseFloat(planExp[ei].amount || 0);
}
var expAnnual = 0; for (var ei = 0; ei < 12; ei++) expAnnual += expMonthly[ei];
@@ -1149,6 +1151,8 @@ function renderPlanOverview() {
for (var epi = 0; epi < planExp.length; epi++) {
var epm = (planExp[epi].expense_month || '').substring(0, 7);
var epmi = parseInt(epm.substring(5)) - 1;
var epmYear = parseInt(epm.substring(0, 4)) || 0;
if (state.planYear !== 0 && epmYear && epmYear !== state.planYear) continue;
if (epmi >= 0 && epmi < 12) expPaidMonthly[epmi] += parseFloat(planExp[epi].incurred_amount || 0);
}
var expPaidAnnual = 0; for (var epi = 0; epi < 12; epi++) expPaidAnnual += expPaidMonthly[epi];
@@ -1175,14 +1179,15 @@ function renderPlanOverview() {
}
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');
document.querySelector("#planOverview").innerHTML = '<div class="grid gap-2.5">' +
const yearSelectOv = `<div class="card px-4 py-2 mb-4"><div class="flex items-center gap-2"><span class="text-sm font-medium text-slate-500">📅 选择年份:</span><select onchange="setPlanYear(this.value)" class="px-2 py-0.5 pr-6 rounded text-sm font-medium border border-slate-200 bg-white"><option value="0"${state.planYear===0?' selected':''}>所有年份</option>${[2020,2021,2022,2023,2024,2025,2026,2027].map(y => '<option value="'+y+'"'+(state.planYear===y?' selected':'')+'>'+y+'年</option>').join('')}</select></div></div>`;
document.querySelector("#planOverview").innerHTML = yearSelectOv + '<div class="grid gap-2.5">' +
deptCard +
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>' + thead + '</thead><tbody>' + tbody + '</tbody></table></div>', 'p-4') +
'<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-600">' + state.planYear + '</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-600">' + state.planYear + '</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-600">' + state.planYear + '</span></div><div style="position:relative;height:200px"><canvas id="planChartCash"></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-600">' + (state.planYear===0 ? '所有年份' : state.planYear) + '</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-600">' + (state.planYear===0 ? '所有年份' : state.planYear) + '</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-600">' + (state.planYear===0 ? '所有年份' : state.planYear) + '</span></div><div style="position:relative;height:200px"><canvas id="planChartCash"></canvas></div>', 'p-4') +
'</div></div>';
if (window.lucide) window.lucide.createIcons();
@@ -1312,7 +1317,8 @@ function renderPlanQuarterlyOverview() {
cfTbody += rowHtml + '</tr>';
}
document.querySelector("#planQuarterlyOverview").innerHTML = '<div class="grid gap-2.5">' +
const yearSelectQ = `<div class="card px-4 py-2 mb-4"><div class="flex items-center gap-2"><span class="text-sm font-medium text-slate-500">📅 选择年份:</span><select onchange="setPlanYear(this.value)" class="px-2 py-0.5 pr-6 rounded text-sm font-medium border border-slate-200 bg-white"><option value="0"${state.planYear===0?' selected':''}>所有年份</option>${[2020,2021,2022,2023,2024,2025,2026,2027].map(y => '<option value="'+y+'"'+(state.planYear===y?' selected':'')+'>'+y+'年</option>').join('')}</select></div></div>`;
document.querySelector("#planQuarterlyOverview").innerHTML = yearSelectQ + '<div class="grid gap-2.5">' +
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') +
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') +
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>' + thead + '</thead><tbody>' + tbody + '</tbody></table></div>', 'p-4') +

View File

@@ -106,7 +106,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-brand-700">OPC Manager <span class="ml-2 text-xs font-normal text-slate-400 tracking-normal">v1.2.0.41</span></p>
<p class="eyebrow text-xs font-semibold uppercase tracking-[0.18em] text-brand-700">OPC Manager <span class="ml-2 text-xs font-normal text-slate-400 tracking-normal">{{ app_version }}</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-brand-50 to-indigo-50 border border-brand-100 rounded-full px-4 py-1.5">