hikvision-web 1.0.0
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/index.html +12 -0
- package/package.json +27 -0
- package/src/App.css +48 -0
- package/src/App.css.new +48 -0
- package/src/App.tsx +157 -0
- package/src/App.tsx.new +132 -0
- package/src/index.css +39 -0
- package/src/main.tsx +14 -0
- package/src/pages/BindingPage.tsx +351 -0
- package/src/pages/CardForm.tsx +173 -0
- package/src/pages/CardList.tsx +193 -0
- package/src/pages/DashboardPage.tsx +300 -0
- package/src/pages/GroupPage.tsx +336 -0
- package/src/pages/OrgPage.tsx +198 -0
- package/src/pages/OrgTreePage.tsx +203 -0
- package/src/pages/PersonDetail.tsx +146 -0
- package/src/pages/PersonForm.tsx +199 -0
- package/src/pages/PersonList.tsx +174 -0
- package/src/pages/PersonTreePage.tsx +563 -0
- package/src/pages/SyncPage.tsx +29 -0
- package/src/pages/SystemPage.tsx +758 -0
- package/src/pages/VehicleForm.tsx +231 -0
- package/src/pages/VehicleList.tsx +199 -0
- package/src/services/api.ts +128 -0
- package/src/store/appStore.ts +159 -0
- package/src/utils/constants.ts +105 -0
- package/tsconfig.json +21 -0
- package/tsconfig.node.json +10 -0
- package/vite.config.ts +16 -0
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { Card, Button, Form, Input, message, Divider, Descriptions, Modal, InputNumber, Row, Col, Statistic, Tag } from 'antd';
|
|
3
|
+
import { SettingOutlined, SaveOutlined, ReloadOutlined, PlayCircleOutlined, StopOutlined, SyncOutlined, CheckCircleOutlined, CloseCircleOutlined, EyeInvisibleOutlined, EyeOutlined } from '@ant-design/icons';
|
|
4
|
+
import { saveApiKey, getApiKey, clearApiKey } from '../services/api';
|
|
5
|
+
|
|
6
|
+
// fetch 封装,自动携带 API Key
|
|
7
|
+
const apiFetch = async (url: string, options: RequestInit = {}) => {
|
|
8
|
+
const apiKey = getApiKey();
|
|
9
|
+
const headers: any = { ...options.headers };
|
|
10
|
+
if (apiKey) {
|
|
11
|
+
headers['x-api-key'] = apiKey;
|
|
12
|
+
}
|
|
13
|
+
return fetch(url, { ...options, headers });
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// 从当前页面 URL 动态获取 Manager URL(解决跨主机访问 localhost 问题)
|
|
17
|
+
// Manager 端口从 config.json 读取,默认 3001
|
|
18
|
+
const getManagerUrl = (managerPort: number = 3001) => {
|
|
19
|
+
const url = new URL(window.location.href);
|
|
20
|
+
return `${url.protocol}//${url.hostname}:${managerPort}`;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
let MANAGER_URL = getManagerUrl(3001); // 默认值,启动时从配置更新
|
|
24
|
+
|
|
25
|
+
interface ConfigData {
|
|
26
|
+
apiServer: { port: number };
|
|
27
|
+
web: { port: number };
|
|
28
|
+
manager?: { port: number };
|
|
29
|
+
hikvision: { host: string; appKey: string; appSecret: string };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface ServiceStatus {
|
|
33
|
+
isRunning: boolean;
|
|
34
|
+
pid: number | null;
|
|
35
|
+
port: number;
|
|
36
|
+
host: string;
|
|
37
|
+
uptime: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface ManagerStatus {
|
|
41
|
+
isRunning: boolean;
|
|
42
|
+
pid: number | null;
|
|
43
|
+
port: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface ManagerConnectionStatus {
|
|
47
|
+
connected: boolean;
|
|
48
|
+
port: number | null;
|
|
49
|
+
error?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default function SystemPage() {
|
|
53
|
+
const [form] = Form.useForm();
|
|
54
|
+
const [, setConfig] = useState<ConfigData | null>(null);
|
|
55
|
+
const [serviceStatus, setServiceStatus] = useState<ServiceStatus | null>(null);
|
|
56
|
+
const [_managerStatus, setManagerStatus] = useState<ManagerStatus | null>(null);
|
|
57
|
+
const [managerConnStatus, setManagerConnStatus] = useState<ManagerConnectionStatus>({ connected: false, port: null });
|
|
58
|
+
const [apiAvailable, setApiAvailable] = useState(true);
|
|
59
|
+
const [apiKeyInput, setApiKeyInput] = useState(getApiKey());
|
|
60
|
+
const [apiKeyStatus, setApiKeyStatus] = useState<'none' | 'saved' | 'error'>('none');
|
|
61
|
+
// 存储原始配置值,用于比较哪些字段被修改
|
|
62
|
+
const [originalConfig, setOriginalConfig] = useState<{
|
|
63
|
+
host: string;
|
|
64
|
+
appKey: string;
|
|
65
|
+
appSecret: string;
|
|
66
|
+
} | null>(null);
|
|
67
|
+
const [loading, setLoading] = useState(false);
|
|
68
|
+
const [saving, setSaving] = useState(false);
|
|
69
|
+
const [testing, setTesting] = useState(false);
|
|
70
|
+
|
|
71
|
+
// 尝试从 Manager 服务获取配置(当 API 不可用时)
|
|
72
|
+
const loadConfigFromManager = async (): Promise<ConfigData | null> => {
|
|
73
|
+
try {
|
|
74
|
+
const res = await fetch(`${MANAGER_URL}/api/config`);
|
|
75
|
+
const data = await res.json();
|
|
76
|
+
if (data.success && data.data?.fileConfig) {
|
|
77
|
+
return data.data.fileConfig;
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
// Manager 也不可用
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// 加载配置和服务状态
|
|
86
|
+
const loadAll = async () => {
|
|
87
|
+
try {
|
|
88
|
+
// 先尝试从 API Server 加载
|
|
89
|
+
let configData: any = null;
|
|
90
|
+
let statusData: any = null;
|
|
91
|
+
let apiOk = false;
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const [configRes, statusRes] = await Promise.all([
|
|
95
|
+
apiFetch('/api/config'),
|
|
96
|
+
apiFetch('/api/service/status')
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
if (configRes.ok) {
|
|
100
|
+
configData = await configRes.json();
|
|
101
|
+
apiOk = true;
|
|
102
|
+
}
|
|
103
|
+
if (statusRes.ok) {
|
|
104
|
+
statusData = await statusRes.json();
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
// API Server 不可用
|
|
108
|
+
setApiAvailable(false);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (apiOk && configData?.success) {
|
|
112
|
+
setApiAvailable(true);
|
|
113
|
+
setConfig(configData.data);
|
|
114
|
+
const fc = configData.data.fileConfig;
|
|
115
|
+
// 从配置中获取 Manager 端口并更新 MANAGER_URL
|
|
116
|
+
const mgrPort = fc?.manager?.port || 3001;
|
|
117
|
+
MANAGER_URL = getManagerUrl(mgrPort);
|
|
118
|
+
|
|
119
|
+
// 保存原始配置值,用于后续比较
|
|
120
|
+
const originalHik = {
|
|
121
|
+
host: fc?.hikvision?.host || '',
|
|
122
|
+
appKey: fc?.hikvision?.appKey || '',
|
|
123
|
+
appSecret: fc?.hikvision?.appSecret || '',
|
|
124
|
+
};
|
|
125
|
+
setOriginalConfig(originalHik);
|
|
126
|
+
|
|
127
|
+
// 显示实际内容
|
|
128
|
+
form.setFieldsValue({
|
|
129
|
+
host: originalHik.host,
|
|
130
|
+
appKey: originalHik.appKey,
|
|
131
|
+
appSecret: originalHik.appSecret,
|
|
132
|
+
apiPort: fc?.apiServer?.port || 3000,
|
|
133
|
+
webPort: fc?.web?.port || 3030,
|
|
134
|
+
});
|
|
135
|
+
// 验证 API Key 是否有效
|
|
136
|
+
const savedKey = getApiKey();
|
|
137
|
+
if (savedKey) {
|
|
138
|
+
try {
|
|
139
|
+
const testRes = await apiFetch('/api/service/status');
|
|
140
|
+
setApiKeyStatus(testRes.ok ? 'saved' : 'error');
|
|
141
|
+
} catch {
|
|
142
|
+
setApiKeyStatus('error');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} else if (statusData?.success) {
|
|
146
|
+
// API 不可用但有状态数据,说明部分可用
|
|
147
|
+
setApiAvailable(false);
|
|
148
|
+
} else {
|
|
149
|
+
// API 完全不可用,尝试从 Manager 获取配置
|
|
150
|
+
const fc = await loadConfigFromManager();
|
|
151
|
+
if (fc) {
|
|
152
|
+
setApiAvailable(false);
|
|
153
|
+
setConfig({ fileConfig: fc } as any);
|
|
154
|
+
// 从 Manager 配置中获取 Manager 端口并更新 MANAGER_URL
|
|
155
|
+
const mgrPort = fc?.manager?.port || 3001;
|
|
156
|
+
MANAGER_URL = getManagerUrl(mgrPort);
|
|
157
|
+
|
|
158
|
+
// 保存原始配置值,用于后续比较
|
|
159
|
+
const originalHik = {
|
|
160
|
+
host: fc?.hikvision?.host || '',
|
|
161
|
+
appKey: fc?.hikvision?.appKey || '',
|
|
162
|
+
appSecret: fc?.hikvision?.appSecret || '',
|
|
163
|
+
};
|
|
164
|
+
setOriginalConfig(originalHik);
|
|
165
|
+
|
|
166
|
+
// 显示实际内容
|
|
167
|
+
form.setFieldsValue({
|
|
168
|
+
host: originalHik.host,
|
|
169
|
+
appKey: originalHik.appKey,
|
|
170
|
+
appSecret: originalHik.appSecret,
|
|
171
|
+
apiPort: fc?.apiServer?.port || 3000,
|
|
172
|
+
webPort: fc?.web?.port || 3030,
|
|
173
|
+
});
|
|
174
|
+
} else {
|
|
175
|
+
setApiAvailable(false);
|
|
176
|
+
message.warning('无法连接 API 服务,请检查服务状态');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (statusData?.success) {
|
|
181
|
+
setServiceStatus(statusData.data);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 查询 Manager 状态
|
|
185
|
+
try {
|
|
186
|
+
const managerRes = await fetch(`${MANAGER_URL}/api/manager/status`);
|
|
187
|
+
if (managerRes.ok) {
|
|
188
|
+
const mdata = await managerRes.json();
|
|
189
|
+
if (mdata.success) {
|
|
190
|
+
setManagerStatus(mdata.data);
|
|
191
|
+
setManagerConnStatus({ connected: true, port: mdata.data?.port || 3001 });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
} catch (error: any) {
|
|
195
|
+
setManagerConnStatus({ connected: false, port: 3001, error: error.message });
|
|
196
|
+
}
|
|
197
|
+
} catch (error: any) {
|
|
198
|
+
message.error(`加载失败: ${error.message}`);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
loadAll();
|
|
204
|
+
// 定时刷新状态
|
|
205
|
+
const interval = setInterval(() => {
|
|
206
|
+
// 刷新 API 状态
|
|
207
|
+
fetch('/api/service/status')
|
|
208
|
+
.then(r => r.ok ? r.json() : null)
|
|
209
|
+
.then(data => {
|
|
210
|
+
if (data?.success) {
|
|
211
|
+
setApiAvailable(true);
|
|
212
|
+
setServiceStatus(data.data);
|
|
213
|
+
} else {
|
|
214
|
+
setApiAvailable(false);
|
|
215
|
+
}
|
|
216
|
+
})
|
|
217
|
+
.catch(() => {
|
|
218
|
+
setApiAvailable(false);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// 刷新 Manager 状态
|
|
222
|
+
fetch(`${MANAGER_URL}/api/manager/status`)
|
|
223
|
+
.then(r => r.ok ? r.json() : null)
|
|
224
|
+
.then(data => {
|
|
225
|
+
if (data?.success) {
|
|
226
|
+
setManagerStatus(data.data);
|
|
227
|
+
setManagerConnStatus({ connected: true, port: data.data?.port || 3001 });
|
|
228
|
+
// Manager 返回的 API Server 状态(用于 API 不可用时)
|
|
229
|
+
if (data.data) {
|
|
230
|
+
setServiceStatus(data.data);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
})
|
|
234
|
+
.catch((error: any) => {
|
|
235
|
+
setManagerConnStatus({ connected: false, port: 3001, error: error.message });
|
|
236
|
+
});
|
|
237
|
+
}, 5000); // 5秒刷新一次,更及时反映状态变化
|
|
238
|
+
return () => clearInterval(interval);
|
|
239
|
+
}, []);
|
|
240
|
+
|
|
241
|
+
const handleSave = async () => {
|
|
242
|
+
try {
|
|
243
|
+
setSaving(true);
|
|
244
|
+
const values = await form.validateFields();
|
|
245
|
+
|
|
246
|
+
// 只保存修改过的字段
|
|
247
|
+
const newHikvision: any = {};
|
|
248
|
+
|
|
249
|
+
if (!originalConfig || values.host !== originalConfig.host) {
|
|
250
|
+
newHikvision.host = values.host;
|
|
251
|
+
}
|
|
252
|
+
if (!originalConfig || values.appKey !== originalConfig.appKey) {
|
|
253
|
+
newHikvision.appKey = values.appKey;
|
|
254
|
+
}
|
|
255
|
+
if (!originalConfig || values.appSecret !== originalConfig.appSecret) {
|
|
256
|
+
newHikvision.appSecret = values.appSecret;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// 如果没有修改任何 hikvision 字段,只发送其他配置
|
|
260
|
+
if (Object.keys(newHikvision).length === 0) {
|
|
261
|
+
message.info('未检测到配置变更');
|
|
262
|
+
setSaving(false);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const newConfig: ConfigData = {
|
|
267
|
+
apiServer: { port: values.apiPort || 3000 },
|
|
268
|
+
web: { port: values.webPort || 3030 },
|
|
269
|
+
hikvision: newHikvision,
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const res = await apiFetch('/api/config', {
|
|
273
|
+
method: 'POST',
|
|
274
|
+
headers: { 'Content-Type': 'application/json' },
|
|
275
|
+
body: JSON.stringify(newConfig)
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const result = await res.json();
|
|
279
|
+
|
|
280
|
+
if (result.success) {
|
|
281
|
+
message.success('配置已更新,重启服务后生效');
|
|
282
|
+
loadAll();
|
|
283
|
+
} else {
|
|
284
|
+
message.error(result.message || '保存失败');
|
|
285
|
+
}
|
|
286
|
+
} catch (error: any) {
|
|
287
|
+
message.error(`保存失败: ${error.message || error}`);
|
|
288
|
+
} finally {
|
|
289
|
+
setSaving(false);
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const handleSaveApiKey = async () => {
|
|
294
|
+
if (apiKeyInput.trim()) {
|
|
295
|
+
saveApiKey(apiKeyInput.trim());
|
|
296
|
+
// 立即验证 API Key 是否有效
|
|
297
|
+
try {
|
|
298
|
+
const res = await apiFetch('/api/service/status');
|
|
299
|
+
if (res.ok) {
|
|
300
|
+
setApiKeyStatus('saved');
|
|
301
|
+
message.success('API Key 已保存并验证通过');
|
|
302
|
+
} else {
|
|
303
|
+
const data = await res.json();
|
|
304
|
+
if (data.message?.includes('未授权')) {
|
|
305
|
+
setApiKeyStatus('error');
|
|
306
|
+
message.error('API Key 无效,请检查配置');
|
|
307
|
+
} else {
|
|
308
|
+
setApiKeyStatus('saved'); // 其他错误(如服务未运行)也认为 key 有效
|
|
309
|
+
message.success('API Key 已保存');
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
} catch {
|
|
313
|
+
setApiKeyStatus('saved'); // 网络错误时也认为 key 已保存
|
|
314
|
+
message.success('API Key 已保存');
|
|
315
|
+
}
|
|
316
|
+
} else {
|
|
317
|
+
clearApiKey();
|
|
318
|
+
setApiKeyStatus('none');
|
|
319
|
+
message.info('API Key 已清除');
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const handleTestConnection = async () => {
|
|
324
|
+
setTesting(true);
|
|
325
|
+
try {
|
|
326
|
+
const values = await form.validateFields();
|
|
327
|
+
const res = await apiFetch('/api/config/test', {
|
|
328
|
+
method: 'POST',
|
|
329
|
+
headers: { 'Content-Type': 'application/json' },
|
|
330
|
+
body: JSON.stringify({
|
|
331
|
+
host: values.host,
|
|
332
|
+
appKey: values.appKey,
|
|
333
|
+
appSecret: values.appSecret,
|
|
334
|
+
})
|
|
335
|
+
});
|
|
336
|
+
const data = await res.json();
|
|
337
|
+
|
|
338
|
+
if (data.status === 'ok') {
|
|
339
|
+
message.success('连接测试成功!');
|
|
340
|
+
if (data.config) {
|
|
341
|
+
message.info(`平台: ${data.config.host}, App Key: ${data.config.appKeyConfigured ? '已配置' : '未配置'}`);
|
|
342
|
+
}
|
|
343
|
+
} else {
|
|
344
|
+
message.error(`连接测试失败: ${data.message || '请检查配置'}`);
|
|
345
|
+
}
|
|
346
|
+
} catch (error: any) {
|
|
347
|
+
message.error(`连接测试失败: ${error.message || '请检查配置'}`);
|
|
348
|
+
} finally {
|
|
349
|
+
setTesting(false);
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
// 通过 Manager 服务启动 API Server
|
|
354
|
+
const handleStartViaManager = async () => {
|
|
355
|
+
setLoading(true);
|
|
356
|
+
try {
|
|
357
|
+
const res = await apiFetch(`${MANAGER_URL}/api/manager/start`, { method: 'POST' });
|
|
358
|
+
const data = await res.json();
|
|
359
|
+
|
|
360
|
+
if (data.success) {
|
|
361
|
+
message.success(data.message);
|
|
362
|
+
// 立即更新状态为运行中
|
|
363
|
+
setServiceStatus({ isRunning: true, pid: data.pid || null, port: 3000, host: 'http://localhost:3000', uptime: 0 });
|
|
364
|
+
setApiAvailable(true);
|
|
365
|
+
// 从 Manager 获取最新状态
|
|
366
|
+
setTimeout(loadAll, 3000);
|
|
367
|
+
} else {
|
|
368
|
+
message.error(data.message || '启动失败');
|
|
369
|
+
}
|
|
370
|
+
} catch (error: any) {
|
|
371
|
+
message.error(`启动失败: ${error.message}`);
|
|
372
|
+
} finally {
|
|
373
|
+
setLoading(false);
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
// 通过 Manager 服务停止 API Server
|
|
379
|
+
const handleStopViaManager = async () => {
|
|
380
|
+
Modal.confirm({
|
|
381
|
+
title: '确认停止服务',
|
|
382
|
+
content: '停止服务后,前端页面将无法访问 API。确定继续?',
|
|
383
|
+
onOk: async () => {
|
|
384
|
+
setLoading(true);
|
|
385
|
+
try {
|
|
386
|
+
const res = await apiFetch(`${MANAGER_URL}/api/manager/stop`, { method: 'POST' });
|
|
387
|
+
const data = await res.json();
|
|
388
|
+
|
|
389
|
+
if (data.success) {
|
|
390
|
+
message.success(data.message);
|
|
391
|
+
// 立即更新状态为已停止
|
|
392
|
+
setApiAvailable(false);
|
|
393
|
+
setServiceStatus({ isRunning: false, pid: null, port: 3000, host: '', uptime: 0 });
|
|
394
|
+
setManagerStatus({ isRunning: true, pid: null, port: 3000 });
|
|
395
|
+
// 从 Manager 获取最新状态
|
|
396
|
+
setTimeout(loadAll, 1000);
|
|
397
|
+
} else {
|
|
398
|
+
message.error(data.message || '停止失败');
|
|
399
|
+
}
|
|
400
|
+
} catch (error: any) {
|
|
401
|
+
message.error(`停止失败: ${error.message}`);
|
|
402
|
+
} finally {
|
|
403
|
+
setLoading(false);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
// 通过 Manager 服务重启 API Server
|
|
410
|
+
const handleRestartViaManager = async () => {
|
|
411
|
+
Modal.confirm({
|
|
412
|
+
title: '确认重启服务',
|
|
413
|
+
content: '重启服务将短暂中断 API 访问。确定继续?',
|
|
414
|
+
onOk: async () => {
|
|
415
|
+
setLoading(true);
|
|
416
|
+
try {
|
|
417
|
+
const res = await apiFetch(`${MANAGER_URL}/api/manager/restart`, { method: 'POST' });
|
|
418
|
+
const data = await res.json();
|
|
419
|
+
|
|
420
|
+
if (data.success) {
|
|
421
|
+
message.success('服务已重启');
|
|
422
|
+
// 立即更新状态为运行中(PID 来自 Manager 响应)
|
|
423
|
+
setServiceStatus({ isRunning: true, pid: data.pid || null, port: 3000, host: 'http://localhost:3000', uptime: 0 });
|
|
424
|
+
setApiAvailable(true);
|
|
425
|
+
// 从 Manager 获取最新状态
|
|
426
|
+
setTimeout(loadAll, 3000);
|
|
427
|
+
} else {
|
|
428
|
+
message.error(data.message || '重启失败');
|
|
429
|
+
}
|
|
430
|
+
} catch (error: any) {
|
|
431
|
+
message.error(`重启失败: ${error.message}`);
|
|
432
|
+
} finally {
|
|
433
|
+
setLoading(false);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
// 通过 API Server 控制服务(服务运行时使用)
|
|
440
|
+
const handleStart = async () => {
|
|
441
|
+
setLoading(true);
|
|
442
|
+
try {
|
|
443
|
+
const res = await apiFetch('/api/service/start', { method: 'POST' });
|
|
444
|
+
const data = await res.json();
|
|
445
|
+
|
|
446
|
+
if (data.success) {
|
|
447
|
+
message.success(data.message);
|
|
448
|
+
// 立即更新状态为运行中
|
|
449
|
+
setServiceStatus({ isRunning: true, pid: data.pid || null, port: 3000, host: 'http://localhost:3000', uptime: 0 });
|
|
450
|
+
setApiAvailable(true);
|
|
451
|
+
setTimeout(loadAll, 3000);
|
|
452
|
+
} else {
|
|
453
|
+
message.error(data.message || '启动失败');
|
|
454
|
+
}
|
|
455
|
+
} catch (error: any) {
|
|
456
|
+
message.error(`启动失败: ${error.message}`);
|
|
457
|
+
} finally {
|
|
458
|
+
setLoading(false);
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
const handleStop = async () => {
|
|
464
|
+
Modal.confirm({
|
|
465
|
+
title: '确认停止服务',
|
|
466
|
+
content: '停止服务后,前端页面将无法访问 API。确定继续?',
|
|
467
|
+
onOk: async () => {
|
|
468
|
+
setLoading(true);
|
|
469
|
+
try {
|
|
470
|
+
const res = await apiFetch('/api/service/stop', { method: 'POST' });
|
|
471
|
+
const data = await res.json();
|
|
472
|
+
|
|
473
|
+
if (data.success) {
|
|
474
|
+
message.success(data.message);
|
|
475
|
+
// 立即更新状态为已停止
|
|
476
|
+
setApiAvailable(false);
|
|
477
|
+
setServiceStatus({ isRunning: false, pid: null, port: 3000, host: '', uptime: 0 });
|
|
478
|
+
setTimeout(loadAll, 1000);
|
|
479
|
+
} else {
|
|
480
|
+
message.error(data.message || '停止失败');
|
|
481
|
+
}
|
|
482
|
+
} catch (error: any) {
|
|
483
|
+
message.error(`停止失败: ${error.message}`);
|
|
484
|
+
} finally {
|
|
485
|
+
setLoading(false);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const handleRestart = async () => {
|
|
492
|
+
Modal.confirm({
|
|
493
|
+
title: '确认重启服务',
|
|
494
|
+
content: '重启服务将短暂中断 API 访问。确定继续?',
|
|
495
|
+
onOk: async () => {
|
|
496
|
+
setLoading(true);
|
|
497
|
+
try {
|
|
498
|
+
const res = await apiFetch('/api/service/restart', { method: 'POST' });
|
|
499
|
+
const data = await res.json();
|
|
500
|
+
|
|
501
|
+
if (data.success) {
|
|
502
|
+
message.success('服务已重启');
|
|
503
|
+
// 立即更新状态为运行中
|
|
504
|
+
setServiceStatus({ isRunning: true, pid: data.pid || null, port: 3000, host: 'http://localhost:3000', uptime: 0 });
|
|
505
|
+
setApiAvailable(true);
|
|
506
|
+
setTimeout(loadAll, 3000);
|
|
507
|
+
} else {
|
|
508
|
+
message.error(data.message || '重启失败');
|
|
509
|
+
}
|
|
510
|
+
} catch (error: any) {
|
|
511
|
+
message.error(`重启失败: ${error.message}`);
|
|
512
|
+
} finally {
|
|
513
|
+
setLoading(false);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const formatUptime = (seconds: number) => {
|
|
520
|
+
if (!seconds) return '-';
|
|
521
|
+
const h = Math.floor(seconds / 3600);
|
|
522
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
523
|
+
const s = Math.floor(seconds % 60);
|
|
524
|
+
return `${h}h ${m}m ${s}s`;
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
// 选择使用哪个控制接口
|
|
528
|
+
const useManager = !apiAvailable;
|
|
529
|
+
|
|
530
|
+
return (
|
|
531
|
+
<div>
|
|
532
|
+
<h2 className="page-title" style={{ marginBottom: 24 }}>系统设置</h2>
|
|
533
|
+
|
|
534
|
+
{/* ========== 服务状态卡片 ========== */}
|
|
535
|
+
<Card title="API Server 服务状态">
|
|
536
|
+
{useManager && (
|
|
537
|
+
<div style={{ marginBottom: 12, padding: 8, background: '#fffbe6', border: '1px solid #ffe58f', borderRadius: 4 }}>
|
|
538
|
+
<Tag color={managerConnStatus.connected ? 'green' : 'red'}>
|
|
539
|
+
{managerConnStatus.connected ? '✅ 已连接管理服务' : '❌ 无法连接管理服务'}
|
|
540
|
+
</Tag>
|
|
541
|
+
{managerConnStatus.connected ? (
|
|
542
|
+
<span style={{ marginLeft: 8, color: '#666' }}>管理服务端口: {managerConnStatus.port}</span>
|
|
543
|
+
) : (
|
|
544
|
+
<span style={{ marginLeft: 8, color: '#666' }}>管理服务 ({MANAGER_URL}) {managerConnStatus.error ? `- ${managerConnStatus.error}` : ''}</span>
|
|
545
|
+
)}
|
|
546
|
+
</div>
|
|
547
|
+
)}
|
|
548
|
+
{!useManager && (
|
|
549
|
+
<div style={{ marginBottom: 12, padding: 8, background: '#f6ffed', border: '1px solid #b7eb8f', borderRadius: 4 }}>
|
|
550
|
+
<Tag color="green">✅ API 服务正常</Tag>
|
|
551
|
+
<Tag color="blue" style={{ marginLeft: 8 }}>管理服务: {managerConnStatus.connected ? '已连接' : '未连接'}</Tag>
|
|
552
|
+
</div>
|
|
553
|
+
)}
|
|
554
|
+
<Row gutter={16}>
|
|
555
|
+
<Col span={6}>
|
|
556
|
+
<Statistic
|
|
557
|
+
title="状态"
|
|
558
|
+
value={serviceStatus?.isRunning ? '运行中' : '已停止'}
|
|
559
|
+
valueStyle={{
|
|
560
|
+
color: serviceStatus?.isRunning ? '#52c41a' : '#ff4d4f',
|
|
561
|
+
fontWeight: 'bold'
|
|
562
|
+
}}
|
|
563
|
+
prefix={serviceStatus?.isRunning ? <CheckCircleOutlined /> : <CloseCircleOutlined />}
|
|
564
|
+
/>
|
|
565
|
+
</Col>
|
|
566
|
+
<Col span={6}>
|
|
567
|
+
<Statistic title="端口" value={serviceStatus?.port || '-'} />
|
|
568
|
+
</Col>
|
|
569
|
+
<Col span={6}>
|
|
570
|
+
<Statistic title="PID" value={serviceStatus?.pid || '-'} />
|
|
571
|
+
</Col>
|
|
572
|
+
<Col span={6}>
|
|
573
|
+
<Statistic title="运行时间" value={formatUptime(serviceStatus?.uptime || 0)} />
|
|
574
|
+
</Col>
|
|
575
|
+
</Row>
|
|
576
|
+
|
|
577
|
+
<Divider />
|
|
578
|
+
|
|
579
|
+
<div style={{ display: 'flex', gap: 8 }}>
|
|
580
|
+
{useManager ? (
|
|
581
|
+
<>
|
|
582
|
+
<Button
|
|
583
|
+
type="primary"
|
|
584
|
+
icon={<PlayCircleOutlined />}
|
|
585
|
+
onClick={handleStartViaManager}
|
|
586
|
+
disabled={serviceStatus?.isRunning ?? false}
|
|
587
|
+
loading={loading}
|
|
588
|
+
>
|
|
589
|
+
启动
|
|
590
|
+
</Button>
|
|
591
|
+
|
|
592
|
+
<Button
|
|
593
|
+
danger
|
|
594
|
+
icon={<StopOutlined />}
|
|
595
|
+
onClick={handleStopViaManager}
|
|
596
|
+
disabled={!(serviceStatus?.isRunning ?? false)}
|
|
597
|
+
loading={loading}
|
|
598
|
+
>
|
|
599
|
+
停止
|
|
600
|
+
</Button>
|
|
601
|
+
|
|
602
|
+
<Button
|
|
603
|
+
icon={<SyncOutlined />}
|
|
604
|
+
onClick={handleRestartViaManager}
|
|
605
|
+
loading={loading}
|
|
606
|
+
>
|
|
607
|
+
重启
|
|
608
|
+
</Button>
|
|
609
|
+
</>
|
|
610
|
+
) : (
|
|
611
|
+
<>
|
|
612
|
+
<Button
|
|
613
|
+
type="primary"
|
|
614
|
+
icon={<PlayCircleOutlined />}
|
|
615
|
+
onClick={handleStart}
|
|
616
|
+
disabled={serviceStatus?.isRunning}
|
|
617
|
+
loading={loading}
|
|
618
|
+
>
|
|
619
|
+
启动
|
|
620
|
+
</Button>
|
|
621
|
+
|
|
622
|
+
<Button
|
|
623
|
+
danger
|
|
624
|
+
icon={<StopOutlined />}
|
|
625
|
+
onClick={handleStop}
|
|
626
|
+
disabled={!serviceStatus?.isRunning}
|
|
627
|
+
loading={loading}
|
|
628
|
+
>
|
|
629
|
+
停止
|
|
630
|
+
</Button>
|
|
631
|
+
|
|
632
|
+
<Button
|
|
633
|
+
icon={<SyncOutlined />}
|
|
634
|
+
onClick={handleRestart}
|
|
635
|
+
loading={loading}
|
|
636
|
+
>
|
|
637
|
+
重启
|
|
638
|
+
</Button>
|
|
639
|
+
</>
|
|
640
|
+
)}
|
|
641
|
+
|
|
642
|
+
<Button
|
|
643
|
+
icon={<ReloadOutlined />}
|
|
644
|
+
onClick={loadAll}
|
|
645
|
+
style={{ marginLeft: 'auto' }}
|
|
646
|
+
>
|
|
647
|
+
刷新
|
|
648
|
+
</Button>
|
|
649
|
+
</div>
|
|
650
|
+
</Card>
|
|
651
|
+
|
|
652
|
+
{/* ========== API 配置 ========== */}
|
|
653
|
+
<Card title="API 配置" style={{ marginTop: 24 }}>
|
|
654
|
+
<Form form={form} layout="vertical" onFinish={handleSave}>
|
|
655
|
+
<Row gutter={16}>
|
|
656
|
+
<Col span={12}>
|
|
657
|
+
<Form.Item name="host" label="平台主机地址" tooltip="海康平台 API 地址,如:127.0.0.1:18443" rules={[{ required: true, message: '请输入主机地址' }]}>
|
|
658
|
+
<Input placeholder="如:127.0.0.1:18443" />
|
|
659
|
+
</Form.Item>
|
|
660
|
+
</Col>
|
|
661
|
+
<Col span={12}>
|
|
662
|
+
<Form.Item name="apiPort" label="API Server 端口" tooltip="API 服务监听端口" rules={[{ required: true, type: 'number', min: 1, max: 65535 }]}>
|
|
663
|
+
<InputNumber min={1} max={65535} style={{ width: '100%' }} />
|
|
664
|
+
</Form.Item>
|
|
665
|
+
</Col>
|
|
666
|
+
</Row>
|
|
667
|
+
|
|
668
|
+
<Row gutter={16}>
|
|
669
|
+
<Col span={8}>
|
|
670
|
+
<Form.Item name="appKey" label="App Key" tooltip="在海康平台申请的 App Key" rules={[{ required: true, message: '请输入 App Key' }]}>
|
|
671
|
+
<Input placeholder="请输入 App Key" />
|
|
672
|
+
</Form.Item>
|
|
673
|
+
</Col>
|
|
674
|
+
<Col span={8}>
|
|
675
|
+
<Form.Item name="appSecret" label="App Secret" tooltip="在海康平台申请的 App Secret" rules={[{ required: true, message: '请输入 App Secret' }]}>
|
|
676
|
+
<Input.Password placeholder="请输入 App Secret" />
|
|
677
|
+
</Form.Item>
|
|
678
|
+
</Col>
|
|
679
|
+
<Col span={8}>
|
|
680
|
+
<Form.Item name="webPort" label="Web 前端端口" tooltip="前端开发服务器端口" rules={[{ required: true, type: 'number', min: 1, max: 65535 }]}>
|
|
681
|
+
<InputNumber min={1} max={65535} style={{ width: '100%' }} />
|
|
682
|
+
</Form.Item>
|
|
683
|
+
</Col>
|
|
684
|
+
</Row>
|
|
685
|
+
|
|
686
|
+
<Divider />
|
|
687
|
+
|
|
688
|
+
<Row gutter={16}>
|
|
689
|
+
<Col span={12}>
|
|
690
|
+
<Form.Item label="API Key" tooltip="用于前端访问 API Server 的认证密钥">
|
|
691
|
+
<Input.Password
|
|
692
|
+
value={apiKeyInput}
|
|
693
|
+
onChange={(e) => setApiKeyInput(e.target.value)}
|
|
694
|
+
placeholder="输入 API Key(从 config.json 的 auth.apiKey 获取)"
|
|
695
|
+
iconRender={(visible) => visible ? <EyeInvisibleOutlined /> : <EyeOutlined />}
|
|
696
|
+
/>
|
|
697
|
+
</Form.Item>
|
|
698
|
+
</Col>
|
|
699
|
+
<Col span={4}>
|
|
700
|
+
<Form.Item label=" ">
|
|
701
|
+
<Button
|
|
702
|
+
icon={<SaveOutlined />}
|
|
703
|
+
onClick={handleSaveApiKey}
|
|
704
|
+
style={{ marginTop: 4 }}
|
|
705
|
+
>
|
|
706
|
+
保存 API Key
|
|
707
|
+
</Button>
|
|
708
|
+
</Form.Item>
|
|
709
|
+
</Col>
|
|
710
|
+
<Col span={8}>
|
|
711
|
+
<Form.Item label="状态">
|
|
712
|
+
<Tag color={apiKeyStatus === 'saved' ? 'green' : apiKeyStatus === 'error' ? 'red' : 'default'}>
|
|
713
|
+
{apiKeyStatus === 'saved' ? '✅ 已配置' : apiKeyStatus === 'error' ? '❌ 验证失败' : '未配置'}
|
|
714
|
+
</Tag>
|
|
715
|
+
<span style={{ marginLeft: 8, color: '#666', fontSize: 12 }}>
|
|
716
|
+
{getApiKey() ? `已保存 ${getApiKey().slice(0, 8)}...` : '未保存'}
|
|
717
|
+
</span>
|
|
718
|
+
</Form.Item>
|
|
719
|
+
</Col>
|
|
720
|
+
</Row>
|
|
721
|
+
|
|
722
|
+
<Divider />
|
|
723
|
+
|
|
724
|
+
<Form.Item>
|
|
725
|
+
<Button type="primary" icon={<SaveOutlined />} onClick={handleSave} loading={saving}>保存配置</Button>
|
|
726
|
+
<Button icon={<ReloadOutlined />} onClick={loadAll} style={{ marginLeft: 8 }}>重置</Button>
|
|
727
|
+
<Button icon={<SettingOutlined />} onClick={handleTestConnection} loading={testing} style={{ marginLeft: 8 }}>测试连接</Button>
|
|
728
|
+
</Form.Item>
|
|
729
|
+
</Form>
|
|
730
|
+
</Card>
|
|
731
|
+
|
|
732
|
+
{/* ========== 配置说明 ========== */}
|
|
733
|
+
<Card title="配置说明" style={{ marginTop: 24 }}>
|
|
734
|
+
<Descriptions bordered column={1}>
|
|
735
|
+
<Descriptions.Item label="配置优先级">
|
|
736
|
+
<Tag color="red">环境变量</Tag>
|
|
737
|
+
>
|
|
738
|
+
<Tag color="blue">config.json</Tag>
|
|
739
|
+
>
|
|
740
|
+
<Tag color="default">默认值</Tag>
|
|
741
|
+
</Descriptions.Item>
|
|
742
|
+
<Descriptions.Item label="配置文件位置">
|
|
743
|
+
<code>packages/server/config.json</code>
|
|
744
|
+
</Descriptions.Item>
|
|
745
|
+
<Descriptions.Item label="环境变量文件">
|
|
746
|
+
<code>.env</code> 或 <code>/etc/openclaw/openclaw.env</code>
|
|
747
|
+
</Descriptions.Item>
|
|
748
|
+
<Descriptions.Item label="生效方式">
|
|
749
|
+
修改配置后需要重启 API Server 才能生效
|
|
750
|
+
</Descriptions.Item>
|
|
751
|
+
<Descriptions.Item label="管理服务">
|
|
752
|
+
独立运行在 <code>:3031</code>,即使 API Server 停止也可控制其启停
|
|
753
|
+
</Descriptions.Item>
|
|
754
|
+
</Descriptions>
|
|
755
|
+
</Card>
|
|
756
|
+
</div>
|
|
757
|
+
);
|
|
758
|
+
}
|