galaxy-opc-plugin 0.2.2 → 0.2.3
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.
- package/README.md +207 -10
- package/index.ts +106 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/api/companies.ts +4 -0
- package/src/api/dashboard.ts +368 -16
- package/src/api/routes.ts +2 -2
- package/src/db/migrations.ts +146 -0
- package/src/db/schema.ts +104 -3
- package/src/db/sqlite-adapter.ts +39 -2
- package/src/opc/accounting-parser.ts +178 -0
- package/src/opc/daily-brief.ts +529 -0
- package/src/opc/onboarding-flow.ts +332 -0
- package/src/opc/proactive-service.ts +290 -3
- package/src/tools/finance-tool.test-payment.ts +326 -0
- package/src/tools/finance-tool.ts +653 -1
- package/src/tools/onboarding-tool.ts +233 -0
- package/src/tools/order-tool.ts +481 -0
- package/src/tools/smart-accounting-tool.ts +144 -0
- package/src/web/DASHBOARD_INTEGRATION_GUIDE.md +478 -0
- package/src/web/config-ui-patches.ts +389 -0
- package/src/web/config-ui.ts +4162 -3809
- package/src/web/dashboard-ui.ts +582 -0
- package/src/web/landing-page.ts +56 -6
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 星环OPC中心 — Dashboard 监控中心 UI
|
|
3
|
+
*
|
|
4
|
+
* 生成 Dashboard 页面的 HTML/CSS,包含:
|
|
5
|
+
* - 4个关键指标卡片(本月收入、本月利润、现金余额、应收账款)
|
|
6
|
+
* - 风险预警列表
|
|
7
|
+
* - 今日待办列表
|
|
8
|
+
* - AI 建议区域
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface DashboardMetrics {
|
|
12
|
+
monthlyIncome: number;
|
|
13
|
+
monthlyIncomeChange: number; // 同比变化百分比
|
|
14
|
+
monthlyProfit: number;
|
|
15
|
+
monthlyProfitChange: number;
|
|
16
|
+
cashBalance: number;
|
|
17
|
+
monthsOfRunway: number; // 可撑月数
|
|
18
|
+
receivables: number;
|
|
19
|
+
overdueReceivables: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface DashboardAlert {
|
|
23
|
+
id: string;
|
|
24
|
+
title: string;
|
|
25
|
+
severity: 'critical' | 'warning' | 'info';
|
|
26
|
+
category: string;
|
|
27
|
+
message: string;
|
|
28
|
+
created_at: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DashboardTodo {
|
|
32
|
+
id: string;
|
|
33
|
+
title: string;
|
|
34
|
+
priority: 'urgent' | 'high' | 'normal';
|
|
35
|
+
category: string;
|
|
36
|
+
due_date?: string;
|
|
37
|
+
description?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface DashboardSuggestion {
|
|
41
|
+
id: string;
|
|
42
|
+
title: string;
|
|
43
|
+
description: string;
|
|
44
|
+
action?: {
|
|
45
|
+
label: string;
|
|
46
|
+
url?: string;
|
|
47
|
+
onclick?: string;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface DashboardData {
|
|
52
|
+
metrics: DashboardMetrics;
|
|
53
|
+
alerts: DashboardAlert[];
|
|
54
|
+
todos: DashboardTodo[];
|
|
55
|
+
suggestions: DashboardSuggestion[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 生成 Dashboard HTML
|
|
60
|
+
*/
|
|
61
|
+
export function generateDashboardHtml(data: DashboardData): string {
|
|
62
|
+
return `
|
|
63
|
+
<div class="dashboard-container">
|
|
64
|
+
${generateMetricsSection(data.metrics)}
|
|
65
|
+
<div class="dashboard-two-column">
|
|
66
|
+
<div class="dashboard-left-column">
|
|
67
|
+
${generateAlertsSection(data.alerts)}
|
|
68
|
+
${generateTodosSection(data.todos)}
|
|
69
|
+
</div>
|
|
70
|
+
<div class="dashboard-right-column">
|
|
71
|
+
${generateSuggestionsSection(data.suggestions)}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 生成关键指标卡片区域
|
|
80
|
+
*/
|
|
81
|
+
function generateMetricsSection(metrics: DashboardMetrics): string {
|
|
82
|
+
const formatMoney = (amount: number) => {
|
|
83
|
+
if (amount >= 10000) {
|
|
84
|
+
return (amount / 10000).toFixed(1) + '万';
|
|
85
|
+
}
|
|
86
|
+
return amount.toFixed(0);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const formatChange = (change: number) => {
|
|
90
|
+
const sign = change >= 0 ? '+' : '';
|
|
91
|
+
const className = change >= 0 ? 'trend-up' : 'trend-down';
|
|
92
|
+
const arrow = change >= 0 ? '↑' : '↓';
|
|
93
|
+
return `<span class="${className}">${arrow} ${sign}${change.toFixed(1)}%</span>`;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return `
|
|
97
|
+
<div class="stats-grid dashboard-metrics">
|
|
98
|
+
<div class="stat-card">
|
|
99
|
+
<div class="label">
|
|
100
|
+
<span>本月收入</span>
|
|
101
|
+
${formatChange(metrics.monthlyIncomeChange)}
|
|
102
|
+
</div>
|
|
103
|
+
<div class="value">
|
|
104
|
+
¥${formatMoney(metrics.monthlyIncome)}
|
|
105
|
+
<span class="unit">元</span>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<div class="stat-card">
|
|
110
|
+
<div class="label">
|
|
111
|
+
<span>本月利润</span>
|
|
112
|
+
${formatChange(metrics.monthlyProfitChange)}
|
|
113
|
+
</div>
|
|
114
|
+
<div class="value">
|
|
115
|
+
¥${formatMoney(metrics.monthlyProfit)}
|
|
116
|
+
<span class="unit">元</span>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<div class="stat-card ${metrics.monthsOfRunway < 2 ? 'stat-card-warning' : ''}">
|
|
121
|
+
<div class="label">
|
|
122
|
+
<span>现金余额</span>
|
|
123
|
+
${metrics.monthsOfRunway < 2 ? '<span class="badge badge-critical">预警</span>' : ''}
|
|
124
|
+
</div>
|
|
125
|
+
<div class="value">
|
|
126
|
+
¥${formatMoney(metrics.cashBalance)}
|
|
127
|
+
<span class="unit">元</span>
|
|
128
|
+
</div>
|
|
129
|
+
<div class="stat-card-footer">
|
|
130
|
+
可撑 ${metrics.monthsOfRunway.toFixed(1)} 个月
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<div class="stat-card ${metrics.overdueReceivables > 0 ? 'stat-card-warning' : ''}">
|
|
135
|
+
<div class="label">
|
|
136
|
+
<span>应收账款</span>
|
|
137
|
+
${metrics.overdueReceivables > 0 ? '<span class="badge badge-warning">逾期</span>' : ''}
|
|
138
|
+
</div>
|
|
139
|
+
<div class="value">
|
|
140
|
+
¥${formatMoney(metrics.receivables)}
|
|
141
|
+
<span class="unit">元</span>
|
|
142
|
+
</div>
|
|
143
|
+
${metrics.overdueReceivables > 0 ? `
|
|
144
|
+
<div class="stat-card-footer stat-card-footer-warning">
|
|
145
|
+
逾期金额: ¥${formatMoney(metrics.overdueReceivables)}
|
|
146
|
+
</div>
|
|
147
|
+
` : ''}
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 生成风险预警列表
|
|
155
|
+
*/
|
|
156
|
+
function generateAlertsSection(alerts: DashboardAlert[]): string {
|
|
157
|
+
if (alerts.length === 0) {
|
|
158
|
+
return `
|
|
159
|
+
<div class="card dashboard-section">
|
|
160
|
+
<h2>风险预警</h2>
|
|
161
|
+
<div class="dashboard-empty-state">
|
|
162
|
+
<div class="dashboard-empty-icon">✓</div>
|
|
163
|
+
<p>暂无风险预警</p>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const alertItems = alerts.map(alert => {
|
|
170
|
+
const severityClass = `alert-${alert.severity}`;
|
|
171
|
+
const severityIcon = alert.severity === 'critical' ? '⚠️' :
|
|
172
|
+
alert.severity === 'warning' ? '⚡' : 'ℹ️';
|
|
173
|
+
|
|
174
|
+
return `
|
|
175
|
+
<div class="alert-banner ${severityClass}" data-alert-id="${alert.id}">
|
|
176
|
+
<span class="alert-icon">${severityIcon}</span>
|
|
177
|
+
<div class="alert-content">
|
|
178
|
+
<div class="alert-title">${alert.title}</div>
|
|
179
|
+
<div class="alert-message">${alert.message}</div>
|
|
180
|
+
</div>
|
|
181
|
+
<button class="alert-dismiss" onclick="dismissAlert('${alert.id}')" title="忽略">×</button>
|
|
182
|
+
</div>
|
|
183
|
+
`;
|
|
184
|
+
}).join('');
|
|
185
|
+
|
|
186
|
+
return `
|
|
187
|
+
<div class="card dashboard-section">
|
|
188
|
+
<h2>风险预警 <span class="badge badge-warning">${alerts.length}</span></h2>
|
|
189
|
+
<div class="dashboard-alerts">
|
|
190
|
+
${alertItems}
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 生成今日待办列表
|
|
198
|
+
*/
|
|
199
|
+
function generateTodosSection(todos: DashboardTodo[]): string {
|
|
200
|
+
if (todos.length === 0) {
|
|
201
|
+
return `
|
|
202
|
+
<div class="card dashboard-section">
|
|
203
|
+
<h2>今日待办</h2>
|
|
204
|
+
<div class="dashboard-empty-state">
|
|
205
|
+
<div class="dashboard-empty-icon">✓</div>
|
|
206
|
+
<p>太棒了!今天没有待办事项</p>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 按优先级排序
|
|
213
|
+
const sortedTodos = [...todos].sort((a, b) => {
|
|
214
|
+
const priorityOrder = { urgent: 0, high: 1, normal: 2 };
|
|
215
|
+
return priorityOrder[a.priority] - priorityOrder[b.priority];
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const todoItems = sortedTodos.map(todo => {
|
|
219
|
+
const priorityBadge = todo.priority === 'urgent' ? 'badge-critical' :
|
|
220
|
+
todo.priority === 'high' ? 'badge-warning' : 'badge-info';
|
|
221
|
+
const priorityLabel = todo.priority === 'urgent' ? '紧急' :
|
|
222
|
+
todo.priority === 'high' ? '重要' : '一般';
|
|
223
|
+
|
|
224
|
+
return `
|
|
225
|
+
<div class="dashboard-todo-item">
|
|
226
|
+
<div class="dashboard-todo-header">
|
|
227
|
+
<span class="badge ${priorityBadge}">${priorityLabel}</span>
|
|
228
|
+
<span class="dashboard-todo-category">${todo.category}</span>
|
|
229
|
+
</div>
|
|
230
|
+
<div class="dashboard-todo-title">${todo.title}</div>
|
|
231
|
+
${todo.description ? `<div class="dashboard-todo-desc">${todo.description}</div>` : ''}
|
|
232
|
+
${todo.due_date ? `<div class="dashboard-todo-due">截止: ${todo.due_date}</div>` : ''}
|
|
233
|
+
</div>
|
|
234
|
+
`;
|
|
235
|
+
}).join('');
|
|
236
|
+
|
|
237
|
+
return `
|
|
238
|
+
<div class="card dashboard-section">
|
|
239
|
+
<h2>今日待办 <span class="badge badge-info">${todos.length}</span></h2>
|
|
240
|
+
<div class="dashboard-todos">
|
|
241
|
+
${todoItems}
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 生成 AI 建议区域
|
|
249
|
+
*/
|
|
250
|
+
function generateSuggestionsSection(suggestions: DashboardSuggestion[]): string {
|
|
251
|
+
if (suggestions.length === 0) {
|
|
252
|
+
return `
|
|
253
|
+
<div class="card dashboard-section">
|
|
254
|
+
<h2>AI 建议</h2>
|
|
255
|
+
<div class="dashboard-empty-state">
|
|
256
|
+
<div class="dashboard-empty-icon">💡</div>
|
|
257
|
+
<p>暂无建议</p>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const suggestionItems = suggestions.map(suggestion => {
|
|
264
|
+
const actionButton = suggestion.action ? `
|
|
265
|
+
<button
|
|
266
|
+
class="dashboard-suggestion-action"
|
|
267
|
+
${suggestion.action.onclick ? `onclick="${suggestion.action.onclick}"` : ''}
|
|
268
|
+
${suggestion.action.url ? `onclick="window.location.href='${suggestion.action.url}'"` : ''}
|
|
269
|
+
>
|
|
270
|
+
${suggestion.action.label}
|
|
271
|
+
</button>
|
|
272
|
+
` : '';
|
|
273
|
+
|
|
274
|
+
return `
|
|
275
|
+
<div class="dashboard-suggestion-item">
|
|
276
|
+
<div class="dashboard-suggestion-title">${suggestion.title}</div>
|
|
277
|
+
<div class="dashboard-suggestion-desc">${suggestion.description}</div>
|
|
278
|
+
${actionButton}
|
|
279
|
+
</div>
|
|
280
|
+
`;
|
|
281
|
+
}).join('');
|
|
282
|
+
|
|
283
|
+
return `
|
|
284
|
+
<div class="card dashboard-section">
|
|
285
|
+
<h2>💡 AI 建议</h2>
|
|
286
|
+
<div class="dashboard-suggestions">
|
|
287
|
+
${suggestionItems}
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Dashboard 专用 CSS
|
|
295
|
+
*/
|
|
296
|
+
export function getDashboardCss(): string {
|
|
297
|
+
return `
|
|
298
|
+
/* Dashboard Container */
|
|
299
|
+
.dashboard-container {
|
|
300
|
+
animation: fadeIn 0.3s ease;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.dashboard-two-column {
|
|
304
|
+
display: grid;
|
|
305
|
+
grid-template-columns: 1fr 400px;
|
|
306
|
+
gap: 20px;
|
|
307
|
+
margin-top: 20px;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
@media (max-width: 1200px) {
|
|
311
|
+
.dashboard-two-column {
|
|
312
|
+
grid-template-columns: 1fr;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/* Metrics Cards */
|
|
317
|
+
.dashboard-metrics {
|
|
318
|
+
margin-bottom: 28px;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.stat-card-warning {
|
|
322
|
+
border-color: #fde68a;
|
|
323
|
+
background: linear-gradient(135deg, #fffbeb 0%, #ffffff 100%);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.stat-card-footer {
|
|
327
|
+
margin-top: 12px;
|
|
328
|
+
font-size: 12px;
|
|
329
|
+
color: var(--tx3);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.stat-card-footer-warning {
|
|
333
|
+
color: #92400e;
|
|
334
|
+
font-weight: 500;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/* Dashboard Sections */
|
|
338
|
+
.dashboard-section {
|
|
339
|
+
margin-bottom: 20px;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/* Empty State */
|
|
343
|
+
.dashboard-empty-state {
|
|
344
|
+
text-align: center;
|
|
345
|
+
padding: 40px 20px;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.dashboard-empty-icon {
|
|
349
|
+
font-size: 48px;
|
|
350
|
+
margin-bottom: 12px;
|
|
351
|
+
opacity: 0.3;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.dashboard-empty-state p {
|
|
355
|
+
color: var(--tx3);
|
|
356
|
+
font-size: 14px;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/* Alerts */
|
|
360
|
+
.dashboard-alerts {
|
|
361
|
+
display: flex;
|
|
362
|
+
flex-direction: column;
|
|
363
|
+
gap: 8px;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.alert-banner {
|
|
367
|
+
position: relative;
|
|
368
|
+
padding-right: 40px;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.alert-icon {
|
|
372
|
+
font-size: 18px;
|
|
373
|
+
line-height: 1;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.alert-content {
|
|
377
|
+
flex: 1;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.alert-title {
|
|
381
|
+
font-weight: 600;
|
|
382
|
+
margin-bottom: 2px;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.alert-message {
|
|
386
|
+
font-size: 12px;
|
|
387
|
+
opacity: 0.9;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.alert-dismiss {
|
|
391
|
+
position: absolute;
|
|
392
|
+
top: 8px;
|
|
393
|
+
right: 8px;
|
|
394
|
+
width: 24px;
|
|
395
|
+
height: 24px;
|
|
396
|
+
border: none;
|
|
397
|
+
background: rgba(0, 0, 0, 0.05);
|
|
398
|
+
border-radius: 4px;
|
|
399
|
+
cursor: pointer;
|
|
400
|
+
font-size: 18px;
|
|
401
|
+
line-height: 1;
|
|
402
|
+
color: currentColor;
|
|
403
|
+
opacity: 0.5;
|
|
404
|
+
transition: all 0.15s;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.alert-dismiss:hover {
|
|
408
|
+
opacity: 1;
|
|
409
|
+
background: rgba(0, 0, 0, 0.1);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/* Todos */
|
|
413
|
+
.dashboard-todos {
|
|
414
|
+
display: flex;
|
|
415
|
+
flex-direction: column;
|
|
416
|
+
gap: 12px;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.dashboard-todo-item {
|
|
420
|
+
padding: 14px;
|
|
421
|
+
background: #f9fafb;
|
|
422
|
+
border-radius: 6px;
|
|
423
|
+
border-left: 3px solid var(--bd);
|
|
424
|
+
transition: all 0.15s;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.dashboard-todo-item:hover {
|
|
428
|
+
background: #f3f4f6;
|
|
429
|
+
border-left-color: var(--tx);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.dashboard-todo-header {
|
|
433
|
+
display: flex;
|
|
434
|
+
align-items: center;
|
|
435
|
+
gap: 8px;
|
|
436
|
+
margin-bottom: 6px;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
.dashboard-todo-category {
|
|
440
|
+
font-size: 11px;
|
|
441
|
+
color: var(--tx3);
|
|
442
|
+
text-transform: uppercase;
|
|
443
|
+
letter-spacing: 0.05em;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.dashboard-todo-title {
|
|
447
|
+
font-weight: 600;
|
|
448
|
+
font-size: 14px;
|
|
449
|
+
color: var(--tx);
|
|
450
|
+
margin-bottom: 4px;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.dashboard-todo-desc {
|
|
454
|
+
font-size: 12px;
|
|
455
|
+
color: var(--tx2);
|
|
456
|
+
margin-bottom: 6px;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
.dashboard-todo-due {
|
|
460
|
+
font-size: 11px;
|
|
461
|
+
color: var(--tx3);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/* Suggestions */
|
|
465
|
+
.dashboard-suggestions {
|
|
466
|
+
display: flex;
|
|
467
|
+
flex-direction: column;
|
|
468
|
+
gap: 16px;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.dashboard-suggestion-item {
|
|
472
|
+
padding: 16px;
|
|
473
|
+
background: linear-gradient(135deg, #f0f9ff 0%, #ffffff 100%);
|
|
474
|
+
border-radius: 8px;
|
|
475
|
+
border: 1px solid #bae6fd;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
.dashboard-suggestion-title {
|
|
479
|
+
font-weight: 600;
|
|
480
|
+
font-size: 14px;
|
|
481
|
+
color: var(--tx);
|
|
482
|
+
margin-bottom: 6px;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.dashboard-suggestion-desc {
|
|
486
|
+
font-size: 13px;
|
|
487
|
+
color: var(--tx2);
|
|
488
|
+
margin-bottom: 12px;
|
|
489
|
+
line-height: 1.5;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.dashboard-suggestion-action {
|
|
493
|
+
padding: 8px 16px;
|
|
494
|
+
background: var(--tx);
|
|
495
|
+
color: white;
|
|
496
|
+
border: none;
|
|
497
|
+
border-radius: 6px;
|
|
498
|
+
font-size: 13px;
|
|
499
|
+
font-weight: 500;
|
|
500
|
+
cursor: pointer;
|
|
501
|
+
transition: all 0.15s;
|
|
502
|
+
font-family: var(--font);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.dashboard-suggestion-action:hover {
|
|
506
|
+
background: var(--pri-l);
|
|
507
|
+
transform: translateY(-1px);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/* Mobile Responsive */
|
|
511
|
+
@media (max-width: 768px) {
|
|
512
|
+
.stats-grid {
|
|
513
|
+
grid-template-columns: 1fr;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
.dashboard-two-column {
|
|
517
|
+
grid-template-columns: 1fr;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
.main {
|
|
521
|
+
padding: 20px 16px;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
`;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Dashboard 专用 JavaScript
|
|
529
|
+
*/
|
|
530
|
+
export function getDashboardJs(): string {
|
|
531
|
+
return `
|
|
532
|
+
// 忽略预警
|
|
533
|
+
async function dismissAlert(alertId) {
|
|
534
|
+
if (!confirm('确定忽略此预警?')) return;
|
|
535
|
+
|
|
536
|
+
try {
|
|
537
|
+
const res = await fetch('/opc/admin/api/alerts/' + alertId + '/dismiss', {
|
|
538
|
+
method: 'POST',
|
|
539
|
+
headers: { 'Content-Type': 'application/json' }
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
if (res.ok) {
|
|
543
|
+
showToast('预警已忽略', 'ok');
|
|
544
|
+
// 移除元素
|
|
545
|
+
const alertEl = document.querySelector('[data-alert-id="' + alertId + '"]');
|
|
546
|
+
if (alertEl) {
|
|
547
|
+
alertEl.style.opacity = '0';
|
|
548
|
+
setTimeout(() => alertEl.remove(), 200);
|
|
549
|
+
}
|
|
550
|
+
} else {
|
|
551
|
+
showToast('操作失败', 'err');
|
|
552
|
+
}
|
|
553
|
+
} catch (err) {
|
|
554
|
+
showToast('网络错误', 'err');
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// 加载 Dashboard 数据
|
|
559
|
+
async function loadDashboard() {
|
|
560
|
+
showLoading();
|
|
561
|
+
|
|
562
|
+
try {
|
|
563
|
+
const res = await fetch('/opc/admin/api/dashboard');
|
|
564
|
+
if (!res.ok) throw new Error('加载失败');
|
|
565
|
+
|
|
566
|
+
const data = await res.json();
|
|
567
|
+
renderDashboard(data);
|
|
568
|
+
} catch (err) {
|
|
569
|
+
document.getElementById('app').innerHTML =
|
|
570
|
+
'<div class="card"><p style="color:var(--err);">加载失败: ' + err.message + '</p></div>';
|
|
571
|
+
} finally {
|
|
572
|
+
hideLoading();
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// 渲染 Dashboard
|
|
577
|
+
function renderDashboard(data) {
|
|
578
|
+
// Dashboard HTML 已在服务端生成,这里只需要绑定事件
|
|
579
|
+
// 实际渲染逻辑在 config-ui.ts 的 renderDashboard 函数中
|
|
580
|
+
}
|
|
581
|
+
`;
|
|
582
|
+
}
|
package/src/web/landing-page.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 星环OPC中心 — 产品官网/文档页
|
|
3
3
|
*
|
|
4
|
-
* 路由: /
|
|
4
|
+
* 路由: / (根路径)
|
|
5
5
|
* 纯静态 HTML 单页,展示产品功能和安装指南
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
8
11
|
import type { ServerResponse } from "node:http";
|
|
9
12
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
10
13
|
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = path.dirname(__filename);
|
|
16
|
+
|
|
11
17
|
function sendHtml(res: ServerResponse, html: string): void {
|
|
12
18
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
13
19
|
res.end(html);
|
|
@@ -208,7 +214,7 @@ function buildLandingHtml(): string {
|
|
|
208
214
|
http://localhost:18789/opc/admin
|
|
209
215
|
|
|
210
216
|
# 访问产品官网
|
|
211
|
-
http://localhost:18789
|
|
217
|
+
http://localhost:18789/</pre>
|
|
212
218
|
</div>
|
|
213
219
|
</div>
|
|
214
220
|
</section>
|
|
@@ -252,18 +258,62 @@ http://localhost:18789/opc/home</pre>
|
|
|
252
258
|
}
|
|
253
259
|
|
|
254
260
|
export function registerLandingPage(api: OpenClawPluginApi): void {
|
|
261
|
+
// opc-homepage 前端页面路径(项目根目录下)
|
|
262
|
+
const homepageDir = path.resolve(__dirname, "../../../../opc-homepage");
|
|
263
|
+
const homepageIndexPath = path.join(homepageDir, "index.html");
|
|
264
|
+
|
|
255
265
|
api.registerHttpHandler(async (req, res) => {
|
|
256
266
|
const rawUrl = req.url ?? "";
|
|
257
267
|
const urlObj = new URL(rawUrl, "http://localhost");
|
|
258
268
|
const pathname = urlObj.pathname;
|
|
259
269
|
|
|
260
|
-
|
|
270
|
+
// 1. 根路径 `/` → 返回 opc-homepage/index.html
|
|
271
|
+
if (pathname === "/" || pathname === "") {
|
|
272
|
+
try {
|
|
273
|
+
if (fs.existsSync(homepageIndexPath)) {
|
|
274
|
+
const html = fs.readFileSync(homepageIndexPath, "utf-8");
|
|
275
|
+
sendHtml(res, html);
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
} catch (err) {
|
|
279
|
+
api.logger.warn(`opc: 无法读取主页文件: ${err instanceof Error ? err.message : String(err)}`);
|
|
280
|
+
}
|
|
281
|
+
// 降级到内置页面
|
|
282
|
+
sendHtml(res, buildLandingHtml());
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// 2. 静态资源 `/galaxy.png` 等 → 返回 opc-homepage/ 下的文件
|
|
287
|
+
if (pathname.match(/^\/(galaxy.*\.png|.*\.jpg|.*\.svg|.*\.css|.*\.js)$/)) {
|
|
288
|
+
try {
|
|
289
|
+
const filename = path.basename(pathname);
|
|
290
|
+
const filePath = path.join(homepageDir, filename);
|
|
291
|
+
|
|
292
|
+
if (fs.existsSync(filePath)) {
|
|
293
|
+
const ext = path.extname(filename).toLowerCase();
|
|
294
|
+
const mimeTypes: Record<string, string> = {
|
|
295
|
+
".png": "image/png",
|
|
296
|
+
".jpg": "image/jpeg",
|
|
297
|
+
".jpeg": "image/jpeg",
|
|
298
|
+
".svg": "image/svg+xml",
|
|
299
|
+
".css": "text/css",
|
|
300
|
+
".js": "application/javascript",
|
|
301
|
+
};
|
|
302
|
+
const contentType = mimeTypes[ext] || "application/octet-stream";
|
|
303
|
+
|
|
304
|
+
const fileContent = fs.readFileSync(filePath);
|
|
305
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
306
|
+
res.end(fileContent);
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
} catch (err) {
|
|
310
|
+
api.logger.warn(`opc: 无法读取静态资源: ${err instanceof Error ? err.message : String(err)}`);
|
|
311
|
+
}
|
|
261
312
|
return false;
|
|
262
313
|
}
|
|
263
314
|
|
|
264
|
-
|
|
265
|
-
return true;
|
|
315
|
+
return false;
|
|
266
316
|
});
|
|
267
317
|
|
|
268
|
-
api.logger.info("opc: 已注册产品官网 (/
|
|
318
|
+
api.logger.info("opc: 已注册产品官网 (/)");
|
|
269
319
|
}
|