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.
@@ -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
+ &gt;
738
+ <Tag color="blue">config.json</Tag>
739
+ &gt;
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
+ }