galaxy-opc-plugin 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@
2
2
  "id": "galaxy-opc-plugin",
3
3
  "name": "OPC Platform",
4
4
  "description": "星环OPC中心 — 一人公司孵化与赋能平台",
5
- "version": "0.2.3",
5
+ "version": "0.2.5",
6
6
  "skills": ["./skills"],
7
7
  "configSchema": {
8
8
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "galaxy-opc-plugin",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "星环 Galaxy OPC — 一人公司孵化与赋能平台 OpenClaw 插件",
5
5
  "keywords": [
6
6
  "openclaw",
@@ -32,10 +32,17 @@ export class SqliteAdapter implements OpcDatabase {
32
32
  for (const sql of Object.values(OPC_TABLES)) {
33
33
  this.db.exec(sql);
34
34
  }
35
+ // IMPORTANT: run migrations before creating indexes.
36
+ // Existing DBs may miss columns introduced by migrations (e.g. quotation_id),
37
+ // and creating dependent indexes first would fail plugin startup.
38
+ runMigrations(this.db);
35
39
  for (const idx of OPC_INDEXES) {
36
- this.db.exec(idx);
40
+ try {
41
+ this.db.exec(idx);
42
+ } catch {
43
+ // Keep startup resilient for partially migrated historical databases.
44
+ }
37
45
  }
38
- runMigrations(this.db);
39
46
  }
40
47
 
41
48
  /** 通用查询方法,供 Phase 2 工具使用 */
@@ -3127,15 +3127,28 @@ export function registerConfigUi(api: OpenClawPluginApi, db: OpcDatabase, gatewa
3127
3127
  // 注册 Dashboard API 路由
3128
3128
  registerDashboardApiRoutes(api, db);
3129
3129
 
3130
- api.registerHttpHandler(async (req, res) => {
3130
+ api.registerHttpHandler(async (req, res) => {
3131
3131
  const rawUrl = req.url ?? "";
3132
3132
  const urlObj = new URL(rawUrl, "http://localhost");
3133
3133
  const pathname = urlObj.pathname;
3134
3134
  const method = req.method?.toUpperCase() ?? "GET";
3135
3135
 
3136
- if (!pathname.startsWith("/opc/admin")) {
3137
- return false;
3138
- }
3136
+ if (!pathname.startsWith("/opc/admin")) {
3137
+ return false;
3138
+ }
3139
+
3140
+ // Normalize accidental SPA deep-links (e.g. /opc/admin/chat) back to OPC admin root.
3141
+ // Without this, users may land on OpenClaw's default chat UI and think OPC admin failed to load.
3142
+ if (
3143
+ !pathname.startsWith("/opc/admin/api/")
3144
+ && pathname !== "/opc/admin"
3145
+ && pathname !== "/opc/admin/"
3146
+ ) {
3147
+ const tokenParam = gatewayToken ? `?token=${encodeURIComponent(gatewayToken)}` : "";
3148
+ res.writeHead(302, { Location: `/opc/admin${tokenParam}` });
3149
+ res.end();
3150
+ return true;
3151
+ }
3139
3152
 
3140
3153
  // API 端点需要认证
3141
3154
  if (pathname.startsWith("/opc/admin/api/") && gatewayToken) {
@@ -1,478 +0,0 @@
1
- # Dashboard 和收付款管理 UI 集成指南
2
-
3
- 本文档详细说明如何将 Dashboard 监控中心和收付款管理 UI 集成到 `config-ui.ts` 中。
4
-
5
- ## 1. Dashboard 监控中心集成
6
-
7
- ### 1.1 导入 Dashboard UI 模块
8
-
9
- 在 `config-ui.ts` 文件顶部添加导入:
10
-
11
- ```typescript
12
- import { generateDashboardHtml, getDashboardCss, getDashboardJs, type DashboardData } from './dashboard-ui.js';
13
- import { registerDashboardApiRoutes } from '../api/dashboard.js';
14
- ```
15
-
16
- ### 1.2 在 CSS 部分添加 Dashboard 样式
17
-
18
- 在 `getCss()` 函数的返回值中添加 Dashboard CSS:
19
-
20
- ```typescript
21
- function getCss(): string {
22
- return ":root{...}"
23
- + "\n*{margin:0;padding:0;box-sizing:border-box}"
24
- // ... 现有样式 ...
25
- + getDashboardCss() // 添加这一行
26
- // ... 继续现有样式 ...
27
- }
28
- ```
29
-
30
- ### 1.3 修改侧边栏菜单
31
-
32
- 当前已经有 "监控中心" 菜单项 (data-view="monitoring"),我们将其更新为 Dashboard:
33
-
34
- 在 `getBodyHtml()` 函数中,找到侧边栏部分并修改:
35
-
36
- ```typescript
37
- // 将原来的:
38
- + '<a data-view="monitoring"><span class="icon">...</span> 监控中心</a>'
39
-
40
- // 改为:
41
- + '<a data-view="dashboard-monitor"><span class="icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2" y="2" width="5" height="5" stroke="currentColor" stroke-width="1.5" rx="1"/><rect x="9" y="2" width="5" height="5" stroke="currentColor" stroke-width="1.5" rx="1"/><rect x="2" y="9" width="5" height="5" stroke="currentColor" stroke-width="1.5" rx="1"/><rect x="9" y="9" width="5" height="5" stroke="currentColor" stroke-width="1.5" rx="1"/></svg></span> 监控中心</a>'
42
- ```
43
-
44
- ### 1.4 添加 Dashboard 视图容器
45
-
46
- 在 main 内容区域添加 dashboard 视图:
47
-
48
- ```typescript
49
- // 在 <div class="main"> 内部添加:
50
- + '<div id="view-dashboard-monitor" class="view"><div class="page-header"><h1>监控中心</h1><p>一屏看清公司运营状态、风险预警、今日待办</p></div><div id="dashboard-monitor-content"><div class="skeleton" style="height:400px"></div></div></div>'
51
- ```
52
-
53
- ### 1.5 更新 showView 函数
54
-
55
- 在 JavaScript 部分的 `showView` 函数中添加:
56
-
57
- ```typescript
58
- + "\nfunction showView(name){currentView=name;document.querySelectorAll('.view').forEach(function(v){v.classList.remove('active')});document.querySelectorAll('.sidebar-nav a').forEach(function(a){a.classList.remove('active')});var el=document.getElementById('view-'+name);if(el)el.classList.add('active');var nav=document.querySelector('.sidebar-nav a[data-view=\"'+name+'\"]');if(nav)nav.classList.add('active');if(name==='dashboard')loadDashboard();if(name==='companies')loadCompanies();if(name==='finance')loadFinance();if(name==='monitoring')loadMonitoring();if(name==='dashboard-monitor')loadDashboardMonitor();if(name==='tools')loadConfig();if(name==='guide')loadGuide();if(name==='canvas')initCanvasView();if(name==='feishu')loadFeishu();}"
59
- ```
60
-
61
- ### 1.6 添加 loadDashboardMonitor 函数
62
-
63
- 在 JavaScript 部分添加新的加载函数:
64
-
65
- ```typescript
66
- + "\nfunction loadDashboardMonitor(){"
67
- + "var el=document.getElementById('dashboard-monitor-content');"
68
- + "el.innerHTML='<div class=\"skeleton\" style=\"height:400px\"></div>';"
69
- + "fetch('/opc/admin/api/dashboard').then(function(r){return r.json()}).then(function(data){"
70
- + "renderDashboardMonitor(data);"
71
- + "}).catch(function(e){el.innerHTML='<div class=\"card\"><div class=\"empty-state\"><p>加载失败: '+e.message+'</p></div></div>';});}"
72
-
73
- + "\nfunction renderDashboardMonitor(data){"
74
- + "var el=document.getElementById('dashboard-monitor-content');"
75
- + "var html=generateDashboardHtmlClient(data);" // 需要在客户端重新实现或传输 HTML
76
- + "el.innerHTML=html;"
77
- + "}"
78
- ```
79
-
80
- **注意**: 由于 `generateDashboardHtml` 在服务端生成,我们需要:
81
-
82
- 方案 A: 在服务端生成 HTML 并通过 API 返回
83
- 方案 B: 在客户端 JavaScript 中重新实现 `generateDashboardHtml` 逻辑
84
-
85
- 推荐**方案 A**,修改 `/opc/admin/api/dashboard` 端点:
86
-
87
- ```typescript
88
- // 在 src/api/dashboard.ts 中修改
89
- api.registerHttpRoute({
90
- path: "/opc/admin/api/dashboard",
91
- handler: (req, res) => {
92
- try {
93
- const data = getDashboardData(db);
94
- const html = generateDashboardHtml(data); // 生成 HTML
95
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
96
- res.end(JSON.stringify({ ...data, html })); // 同时返回数据和 HTML
97
- } catch (err) {
98
- res.writeHead(500, { "Content-Type": "application/json; charset=utf-8" });
99
- res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
100
- }
101
- },
102
- });
103
- ```
104
-
105
- 然后在客户端:
106
-
107
- ```typescript
108
- + "\nfunction renderDashboardMonitor(data){"
109
- + "var el=document.getElementById('dashboard-monitor-content');"
110
- + "el.innerHTML=data.html || '<div class=\"card\"><p>暂无数据</p></div>';"
111
- + "}"
112
- ```
113
-
114
- ### 1.7 添加 Dashboard JavaScript 函数
115
-
116
- 在 JavaScript 部分添加 Dashboard 专用函数:
117
-
118
- ```typescript
119
- + getDashboardJs() // 添加 dismissAlert 等函数
120
- ```
121
-
122
- ### 1.8 注册 Dashboard API 路由
123
-
124
- 在 `registerConfigUiRoutes` 函数中调用:
125
-
126
- ```typescript
127
- export function registerConfigUiRoutes(api: OpenClawPluginApi, db: OpcDatabase, gatewayToken?: string): void {
128
- // 注册 Dashboard API
129
- registerDashboardApiRoutes(api, db);
130
-
131
- // 现有路由注册代码...
132
- api.registerHttpRoute({
133
- path: /^\/opc\/admin/,
134
- handler: async (req, res) => {
135
- // ...
136
- }
137
- });
138
- }
139
- ```
140
-
141
- ---
142
-
143
- ## 2. 收付款管理 UI 集成
144
-
145
- ### 2.1 修改公司详情页 Tab 标签
146
-
147
- 在 `renderCompanyDetail` 函数中,找到 tabNames 数组并添加 "收付款" Tab:
148
-
149
- ```typescript
150
- // 原来的:
151
- + "var tabNames=[{k:'overview',l:'概览'},{k:'finance',l:'财务'},{k:'team',l:'团队'},{k:'projects',l:'项目'},{k:'contracts',l:'合同'},{k:'investment',l:'投融资'},{k:'timeline',l:'时间线'},{k:'staff',l:'AI员工'},{k:'media',l:'新媒体'},{k:'procurement',l:'采购'}];"
152
-
153
- // 改为:
154
- + "var tabNames=[{k:'overview',l:'概览'},{k:'finance',l:'财务'},{k:'payments',l:'收付款'},{k:'team',l:'团队'},{k:'projects',l:'项目'},{k:'contracts',l:'合同'},{k:'investment',l:'投融资'},{k:'timeline',l:'时间线'},{k:'staff',l:'AI员工'},{k:'media',l:'新媒体'},{k:'procurement',l:'采购'}];"
155
- ```
156
-
157
- ### 2.2 添加收付款 Tab 内容渲染逻辑
158
-
159
- 在 `renderCompanyDetail` 函数中,添加收付款 Tab 的渲染:
160
-
161
- ```typescript
162
- // 在 finance tab 后添加:
163
-
164
- // ── Tab: 收付款 ──
165
- + "if(activeTab==='payments'){"
166
- + "h+='<div class=\"tab-content\">';"
167
-
168
- // 汇总卡片
169
- + "h+='<div class=\"stats-grid\" style=\"grid-template-columns:repeat(3,1fr);margin-bottom:24px\">';"
170
- + "var receivableTotal=0,payableTotal=0,overdueCount=0;"
171
- + "if(d.payments){"
172
- + "d.payments.forEach(function(p){"
173
- + "var unpaid=p.amount-p.paid_amount;"
174
- + "if(p.direction==='receivable'&&p.status!=='paid'&&p.status!=='cancelled')receivableTotal+=unpaid;"
175
- + "if(p.direction==='payable'&&p.status!=='paid'&&p.status!=='cancelled')payableTotal+=unpaid;"
176
- + "if(p.status==='overdue')overdueCount++;"
177
- + "});}"
178
- + "h+='<div class=\"stat-card\" style=\"border-left:4px solid #10b981\"><div class=\"label\">待收总额</div><div class=\"value\">'+fmt(receivableTotal)+' <span class=\"unit\">元</span></div></div>';"
179
- + "h+='<div class=\"stat-card\" style=\"border-left:4px solid #ef4444\"><div class=\"label\">待付总额</div><div class=\"value\">'+fmt(payableTotal)+' <span class=\"unit\">元</span></div></div>';"
180
- + "h+='<div class=\"stat-card\" style=\"border-left:4px solid #f59e0b\"><div class=\"label\">逾期笔数</div><div class=\"value\">'+overdueCount+' <span class=\"unit\">笔</span></div></div>';"
181
- + "h+='</div>';"
182
-
183
- // 新增按钮
184
- + "h+='<div style=\"display:flex;justify-content:space-between;align-items:center;margin-bottom:16px\">';"
185
- + "h+='<h2 style=\"margin:0\">收付款列表</h2>';"
186
- + "h+='<button class=\"btn-primary\" onclick=\"openPaymentModal(\\''+esc(companyId)+'\\',null)\">+ 新增收付款</button>';"
187
- + "h+='</div>';"
188
-
189
- // 收付款表格
190
- + "if(d.payments&&d.payments.length){"
191
- + "h+='<table><thead><tr><th>方向</th><th>对方</th><th>金额</th><th>已付</th><th>状态</th><th>到期日</th><th>类别</th><th>操作</th></tr></thead><tbody>';"
192
- + "d.payments.forEach(function(p){"
193
- + "var directionBadge=p.direction==='receivable'?'<span class=\"badge badge-income\">应收</span>':'<span class=\"badge badge-expense\">应付</span>';"
194
- + "var statusBadge=p.status==='paid'?'<span class=\"badge badge-paid\">已付</span>':p.status==='partial'?'<span class=\"badge badge-warning\">部分</span>':p.status==='overdue'?'<span class=\"badge badge-critical\">逾期</span>':'<span class=\"badge badge-pending\">待付</span>';"
195
- + "h+='<tr>';"
196
- + "h+='<td>'+directionBadge+'</td>';"
197
- + "h+='<td>'+esc(p.counterparty)+'</td>';"
198
- + "h+='<td>'+fmt(p.amount)+' 元</td>';"
199
- + "h+='<td>'+fmt(p.paid_amount)+' 元</td>';"
200
- + "h+='<td>'+statusBadge+'</td>';"
201
- + "h+='<td>'+fmtDate(p.due_date)+'</td>';"
202
- + "h+='<td>'+esc(p.category||'--')+'</td>';"
203
- + "h+='<td><button class=\"btn-link\" onclick=\"openPaymentModal(\\''+esc(companyId)+'\\',\\''+esc(p.id)+'\\')\" style=\"margin-right:8px\">编辑</button><button class=\"btn-link\" onclick=\"recordPayment(\\''+esc(p.id)+'\\')\" style=\"color:var(--ok)\">记账</button></td>';"
204
- + "h+='</tr>';"
205
- + "});"
206
- + "h+='</tbody></table>';"
207
- + "}else{h+='<div class=\"empty-state\"><p>暂无收付款记录</p></div>';}"
208
-
209
- + "h+='</div>';" // 结束 tab-content
210
- + "}"
211
- ```
212
-
213
- ### 2.3 添加收付款数据获取
214
-
215
- 在 `handleCompanyDetail` 函数中,添加收付款数据查询:
216
-
217
- ```typescript
218
- // 在返回对象中添加:
219
- const payments = db.query(
220
- "SELECT * FROM opc_payments WHERE company_id = ? ORDER BY due_date DESC, created_at DESC",
221
- companyId,
222
- ) as PaymentRow[];
223
-
224
- return {
225
- company,
226
- finance: financeSummary,
227
- transactions,
228
- invoices,
229
- taxFilings,
230
- hrRecords,
231
- projects,
232
- tasks,
233
- contracts,
234
- rounds,
235
- investors,
236
- milestones,
237
- lifecycleEvents,
238
- alerts,
239
- contacts,
240
- employees,
241
- salarySum,
242
- staffConfig,
243
- mediaContent,
244
- procurementOrders,
245
- services,
246
- // ... 其他字段
247
- payments, // 添加这一行
248
- };
249
- ```
250
-
251
- 并添加 PaymentRow 接口定义:
252
-
253
- ```typescript
254
- interface PaymentRow {
255
- id: string;
256
- company_id: string;
257
- direction: string;
258
- counterparty: string;
259
- amount: number;
260
- paid_amount: number;
261
- status: string;
262
- due_date: string;
263
- paid_date: string;
264
- invoice_id: string;
265
- contract_id: string;
266
- category: string;
267
- payment_method: string;
268
- notes: string;
269
- created_at: string;
270
- updated_at: string;
271
- }
272
- ```
273
-
274
- ### 2.4 添加收付款弹窗 HTML
275
-
276
- 在 `getBodyHtml()` 函数的 `<body>` 内添加弹窗:
277
-
278
- ```typescript
279
- + '<div id="payment-modal" class="modal" style="display:none">'
280
- + '<div class="modal-content" style="max-width:600px">'
281
- + '<div class="modal-header">'
282
- + '<h3 id="payment-modal-title">新增收付款</h3>'
283
- + '<span class="modal-close" onclick="closePaymentModal()">&times;</span>'
284
- + '</div>'
285
- + '<div class="modal-body">'
286
- + '<form id="payment-form" onsubmit="savePayment(event)">'
287
- + '<input type="hidden" id="payment-id" name="payment_id">'
288
- + '<input type="hidden" id="payment-company-id" name="company_id">'
289
- + '<div class="form-group">'
290
- + '<label>方向 *</label>'
291
- + '<select name="direction" required><option value="receivable">应收</option><option value="payable">应付</option></select>'
292
- + '</div>'
293
- + '<div class="form-group">'
294
- + '<label>对方单位 *</label>'
295
- + '<input type="text" name="counterparty" required placeholder="客户/供应商名称">'
296
- + '</div>'
297
- + '<div class="form-group">'
298
- + '<label>金额(元)*</label>'
299
- + '<input type="number" name="amount" required step="0.01" min="0" placeholder="0.00">'
300
- + '</div>'
301
- + '<div class="form-group">'
302
- + '<label>到期日期 *</label>'
303
- + '<input type="date" name="due_date" required>'
304
- + '</div>'
305
- + '<div class="form-group">'
306
- + '<label>类别</label>'
307
- + '<select name="category"><option value="">请选择</option><option value="service">服务</option><option value="product">产品</option><option value="rent">租金</option><option value="salary">工资</option><option value="tax">税费</option></select>'
308
- + '</div>'
309
- + '<div class="form-group">'
310
- + '<label>支付方式</label>'
311
- + '<select name="payment_method"><option value="">请选择</option><option value="bank_transfer">银行转账</option><option value="alipay">支付宝</option><option value="wechat">微信</option><option value="cash">现金</option></select>'
312
- + '</div>'
313
- + '<div class="form-group">'
314
- + '<label>备注</label>'
315
- + '<textarea name="notes" rows="3" placeholder="可选"></textarea>'
316
- + '</div>'
317
- + '<div class="modal-actions">'
318
- + '<button type="button" class="btn-secondary" onclick="closePaymentModal()">取消</button>'
319
- + '<button type="submit" class="btn-primary">保存</button>'
320
- + '</div>'
321
- + '</form>'
322
- + '</div>'
323
- + '</div>'
324
- + '</div>'
325
- ```
326
-
327
- ### 2.5 添加收付款弹窗 CSS
328
-
329
- 在 `getCss()` 函数中添加:
330
-
331
- ```typescript
332
- // Modal styles
333
- + "\n.modal{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:1000;display:flex;align-items:center;justify-content:center;padding:20px}"
334
- + "\n.modal-content{background:var(--card);border-radius:var(--r);max-width:800px;width:100%;max-height:90vh;overflow:hidden;display:flex;flex-direction:column;box-shadow:0 20px 25px -5px rgba(0,0,0,0.1),0 10px 10px -5px rgba(0,0,0,0.04)}"
335
- + "\n.modal-header{padding:20px 24px;border-bottom:1px solid var(--bd);display:flex;justify-content:space-between;align-items:center}"
336
- + "\n.modal-header h3{font-size:18px;font-weight:600;margin:0}"
337
- + "\n.modal-close{font-size:28px;font-weight:300;line-height:1;cursor:pointer;color:var(--tx3);transition:color .15s}"
338
- + "\n.modal-close:hover{color:var(--tx)}"
339
- + "\n.modal-body{padding:24px;overflow-y:auto}"
340
- + "\n.modal-actions{display:flex;gap:12px;justify-content:flex-end;padding-top:16px;border-top:1px solid var(--bd);margin-top:16px}"
341
- + "\n.form-group{margin-bottom:16px}"
342
- + "\n.form-group label{display:block;font-size:13px;font-weight:500;margin-bottom:6px;color:var(--tx)}"
343
- + "\n.form-group input,.form-group select,.form-group textarea{width:100%;padding:9px 14px;border:1px solid var(--bd);border-radius:var(--r);font-size:13px;font-family:var(--font);background:var(--card);color:var(--tx)}"
344
- + "\n.form-group input:focus,.form-group select:focus,.form-group textarea:focus{outline:none;border-color:var(--tx3)}"
345
- + "\n.btn-link{background:none;border:none;color:var(--tx);cursor:pointer;font-size:13px;text-decoration:underline;padding:0}"
346
- + "\n.btn-link:hover{color:var(--tx2)}"
347
- + "\n.btn-primary{padding:9px 18px;background:var(--tx);color:white;border:none;border-radius:var(--r);font-size:13px;font-weight:500;cursor:pointer;font-family:var(--font);transition:all .15s}"
348
- + "\n.btn-primary:hover{background:var(--pri-l)}"
349
- + "\n.btn-secondary{padding:9px 18px;background:var(--bg);color:var(--tx);border:1px solid var(--bd);border-radius:var(--r);font-size:13px;font-weight:500;cursor:pointer;font-family:var(--font);transition:all .15s}"
350
- + "\n.btn-secondary:hover{background:#f3f4f6}"
351
- ```
352
-
353
- ### 2.6 添加收付款 JavaScript 函数
354
-
355
- 在 JavaScript 部分添加:
356
-
357
- ```typescript
358
- // 打开收付款弹窗
359
- + "\nfunction openPaymentModal(companyId,paymentId){"
360
- + "var modal=document.getElementById('payment-modal');"
361
- + "var form=document.getElementById('payment-form');"
362
- + "var title=document.getElementById('payment-modal-title');"
363
- + "document.getElementById('payment-company-id').value=companyId;"
364
- + "if(paymentId){"
365
- + "title.textContent='编辑收付款';"
366
- + "document.getElementById('payment-id').value=paymentId;"
367
- + "fetch('/opc/admin/api/payments/'+paymentId).then(function(r){return r.json()}).then(function(p){"
368
- + "form.direction.value=p.direction;"
369
- + "form.counterparty.value=p.counterparty;"
370
- + "form.amount.value=p.amount;"
371
- + "form.due_date.value=p.due_date;"
372
- + "form.category.value=p.category||'';"
373
- + "form.payment_method.value=p.payment_method||'';"
374
- + "form.notes.value=p.notes||'';"
375
- + "}).catch(function(e){showToast('加载失败','err');});"
376
- + "}else{"
377
- + "title.textContent='新增收付款';"
378
- + "document.getElementById('payment-id').value='';"
379
- + "form.reset();"
380
- + "}"
381
- + "modal.style.display='flex';"
382
- + "}"
383
-
384
- // 关闭弹窗
385
- + "\nfunction closePaymentModal(){"
386
- + "document.getElementById('payment-modal').style.display='none';"
387
- + "}"
388
-
389
- // 保存收付款
390
- + "\nfunction savePayment(event){"
391
- + "event.preventDefault();"
392
- + "var form=event.target;"
393
- + "var data=new FormData(form);"
394
- + "var json={};"
395
- + "data.forEach(function(value,key){json[key]=value;});"
396
- + "var url=json.payment_id?'/opc/admin/api/payments/'+json.payment_id:'/opc/admin/api/payments';"
397
- + "var method=json.payment_id?'PUT':'POST';"
398
- + "fetch(url,{method:method,headers:{'Content-Type':'application/json'},body:JSON.stringify(json)})"
399
- + ".then(function(r){return r.json()}).then(function(d){"
400
- + "if(d.ok||d.id){"
401
- + "showToast('保存成功','ok');"
402
- + "closePaymentModal();"
403
- + "loadCompanyDetail(json.company_id);"
404
- + "}else{showToast('保存失败','err');}"
405
- + "}).catch(function(e){showToast('网络错误','err');});"
406
- + "}"
407
-
408
- // 记账(更新已付金额)
409
- + "\nfunction recordPayment(paymentId){"
410
- + "var amount=prompt('请输入本次收/付款金额(元):');"
411
- + "if(!amount||isNaN(parseFloat(amount)))return;"
412
- + "fetch('/opc/admin/api/payments/'+paymentId+'/record',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({paid_amount:parseFloat(amount)})})"
413
- + ".then(function(r){return r.json()}).then(function(d){"
414
- + "if(d.ok){"
415
- + "showToast('记账成功','ok');"
416
- + "location.reload();"
417
- + "}else{showToast('记账失败','err');}"
418
- + "}).catch(function(e){showToast('网络错误','err');});"
419
- + "}"
420
- ```
421
-
422
- ### 2.7 添加收付款 API 端点
423
-
424
- 需要在服务端添加以下 API 端点:
425
-
426
- ```typescript
427
- // GET /opc/admin/api/payments/:id - 获取单个收付款
428
- // POST /opc/admin/api/payments - 创建收付款
429
- // PUT /opc/admin/api/payments/:id - 更新收付款
430
- // POST /opc/admin/api/payments/:id/record - 记账
431
- ```
432
-
433
- 这些端点可以通过调用 `finance-tool.ts` 中的相应 action 来实现。
434
-
435
- ---
436
-
437
- ## 3. 测试清单
438
-
439
- 完成集成后,请进行以下测试:
440
-
441
- ### Dashboard 监控中心
442
- - [ ] 访问 `/opc/admin`,点击左侧"监控中心"菜单
443
- - [ ] 验证 4 个关键指标卡片显示正确
444
- - [ ] 验证风险预警列表显示
445
- - [ ] 验证今日待办列表显示
446
- - [ ] 验证 AI 建议显示
447
- - [ ] 点击预警的忽略按钮,验证功能正常
448
- - [ ] 在不同屏幕尺寸下测试响应式布局
449
-
450
- ### 收付款管理
451
- - [ ] 进入公司详情页,点击"收付款" Tab
452
- - [ ] 验证汇总卡片显示正确
453
- - [ ] 验证收付款列表显示
454
- - [ ] 点击"新增收付款",填写表单并保存
455
- - [ ] 点击"编辑"按钮,修改收付款信息
456
- - [ ] 点击"记账"按钮,更新已付金额
457
- - [ ] 验证逾期状态自动标记
458
- - [ ] 验证部分付款状态显示
459
-
460
- ---
461
-
462
- ## 4. 注意事项
463
-
464
- 1. **文件太大**: `config-ui.ts` 文件已经超过 3800 行,建议考虑拆分成多个模块
465
- 2. **性能优化**: Dashboard 数据实时计算可能较慢,考虑添加缓存
466
- 3. **权限控制**: 如果启用了 `gatewayToken`,确保 API 端点有正确的权限检查
467
- 4. **错误处理**: 所有 fetch 调用都应有完善的错误处理
468
- 5. **深色模式**: 确保所有新增样式支持深色模式 CSS 变量
469
-
470
- ---
471
-
472
- ## 5. 后续优化建议
473
-
474
- 1. **Dashboard 刷新**: 添加自动刷新功能(每 5 分钟)
475
- 2. **收付款提醒**: 集成到每日简报中
476
- 3. **导出功能**: 添加收付款列表导出 Excel 功能
477
- 4. **批量操作**: 支持批量标记收付款状态
478
- 5. **图表可视化**: 在 Dashboard 中添加趋势图表