@zhin.js/client 1.0.3 → 1.0.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.
- package/README.md +117 -3
- package/{src → client}/index.ts +0 -1
- package/{src → client}/store/index.ts +22 -1
- package/client/store/reducers/config.ts +135 -0
- package/{src → client}/store/reducers/index.ts +4 -1
- package/client/websocket/hooks.ts +280 -0
- package/client/websocket/index.ts +48 -0
- package/client/websocket/instance.ts +46 -0
- package/client/websocket/manager.ts +412 -0
- package/client/websocket/messageHandler.ts +166 -0
- package/client/websocket/types.ts +208 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -0
- package/dist/router/index.d.ts +25 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/index.js +49 -0
- package/dist/router/index.js.map +1 -0
- package/dist/store/index.d.ts +19 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +67 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/reducers/config.d.ts +54 -0
- package/dist/store/reducers/config.d.ts.map +1 -0
- package/dist/store/reducers/config.js +78 -0
- package/dist/store/reducers/config.js.map +1 -0
- package/dist/store/reducers/index.d.ts +13 -0
- package/dist/store/reducers/index.d.ts.map +1 -0
- package/dist/store/reducers/index.js +11 -0
- package/dist/store/reducers/index.js.map +1 -0
- package/dist/store/reducers/route.d.ts +37 -0
- package/dist/store/reducers/route.d.ts.map +1 -0
- package/dist/store/reducers/route.js +85 -0
- package/dist/store/reducers/route.js.map +1 -0
- package/dist/store/reducers/script.d.ts +17 -0
- package/dist/store/reducers/script.d.ts.map +1 -0
- package/dist/store/reducers/script.js +74 -0
- package/dist/store/reducers/script.js.map +1 -0
- package/dist/store/reducers/ui.d.ts +14 -0
- package/dist/store/reducers/ui.d.ts.map +1 -0
- package/dist/store/reducers/ui.js +23 -0
- package/dist/store/reducers/ui.js.map +1 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/websocket/hooks.d.ts +55 -0
- package/dist/websocket/hooks.d.ts.map +1 -0
- package/dist/websocket/hooks.js +225 -0
- package/dist/websocket/hooks.js.map +1 -0
- package/dist/websocket/index.d.ts +13 -0
- package/dist/websocket/index.d.ts.map +1 -0
- package/dist/websocket/index.js +31 -0
- package/dist/websocket/index.js.map +1 -0
- package/dist/websocket/instance.d.ts +18 -0
- package/dist/websocket/instance.d.ts.map +1 -0
- package/dist/websocket/instance.js +39 -0
- package/dist/websocket/instance.js.map +1 -0
- package/dist/websocket/manager.d.ts +110 -0
- package/dist/websocket/manager.d.ts.map +1 -0
- package/dist/websocket/manager.js +341 -0
- package/dist/websocket/manager.js.map +1 -0
- package/dist/websocket/messageHandler.d.ts +48 -0
- package/dist/websocket/messageHandler.d.ts.map +1 -0
- package/dist/websocket/messageHandler.js +140 -0
- package/dist/websocket/messageHandler.js.map +1 -0
- package/dist/websocket/types.d.ts +125 -0
- package/dist/websocket/types.d.ts.map +1 -0
- package/dist/websocket/types.js +43 -0
- package/dist/websocket/types.js.map +1 -0
- package/package.json +8 -18
- package/app/index.html +0 -13
- package/app/postcss.config.js +0 -5
- package/app/src/components/ThemeToggle.tsx +0 -21
- package/app/src/hooks/useTheme.ts +0 -17
- package/app/src/layouts/dashboard.tsx +0 -259
- package/app/src/main.tsx +0 -121
- package/app/src/pages/dashboard-bots.tsx +0 -198
- package/app/src/pages/dashboard-home.tsx +0 -301
- package/app/src/pages/dashboard-logs.tsx +0 -298
- package/app/src/pages/dashboard-plugin-detail.tsx +0 -349
- package/app/src/pages/dashboard-plugins.tsx +0 -166
- package/app/src/style.css +0 -1105
- package/app/src/theme/index.ts +0 -92
- package/app/tailwind.config.js +0 -70
- package/app/tsconfig.json +0 -16
- package/src/websocket/index.ts +0 -193
- package/src/websocket/useWebSocket.ts +0 -42
- /package/{src → client}/router/index.tsx +0 -0
- /package/{src → client}/store/reducers/route.ts +0 -0
- /package/{src → client}/store/reducers/script.ts +0 -0
- /package/{src → client}/store/reducers/ui.ts +0 -0
- /package/{src → client}/types.ts +0 -0
|
@@ -1,301 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react'
|
|
2
|
-
import { useNavigate } from 'react-router'
|
|
3
|
-
import {Flex,Box,Spinner,Text,Callout,Heading,Badge,Grid,Card,Button} from '@radix-ui/themes'
|
|
4
|
-
import { Bot, AlertCircle, Activity, Package, Clock, Cpu, MemoryStick, FileText, TrendingUp } from 'lucide-react'
|
|
5
|
-
|
|
6
|
-
interface Stats {
|
|
7
|
-
plugins: { total: number; active: number }
|
|
8
|
-
bots: { total: number; online: number }
|
|
9
|
-
commands: number
|
|
10
|
-
components: number
|
|
11
|
-
uptime: number
|
|
12
|
-
memory: number
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
interface SystemStatus {
|
|
16
|
-
uptime: number
|
|
17
|
-
memory: {
|
|
18
|
-
rss: number
|
|
19
|
-
heapTotal: number
|
|
20
|
-
heapUsed: number
|
|
21
|
-
external: number
|
|
22
|
-
}
|
|
23
|
-
cpu: {
|
|
24
|
-
user: number
|
|
25
|
-
system: number
|
|
26
|
-
}
|
|
27
|
-
platform: string
|
|
28
|
-
nodeVersion: string
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export default function DashboardHome() {
|
|
32
|
-
const navigate = useNavigate()
|
|
33
|
-
const [stats, setStats] = useState<Stats | null>(null)
|
|
34
|
-
const [systemStatus, setSystemStatus] = useState<SystemStatus | null>(null)
|
|
35
|
-
const [loading, setLoading] = useState(true)
|
|
36
|
-
const [error, setError] = useState<string | null>(null)
|
|
37
|
-
|
|
38
|
-
useEffect(() => {
|
|
39
|
-
fetchData()
|
|
40
|
-
const interval = setInterval(fetchData, 5000)
|
|
41
|
-
return () => clearInterval(interval)
|
|
42
|
-
}, [])
|
|
43
|
-
|
|
44
|
-
const fetchData = async () => {
|
|
45
|
-
try {
|
|
46
|
-
const [statsRes, statusRes] = await Promise.all([
|
|
47
|
-
fetch('/api/stats', { credentials: 'include' }),
|
|
48
|
-
fetch('/api/system/status', { credentials: 'include' })
|
|
49
|
-
])
|
|
50
|
-
|
|
51
|
-
if (!statsRes.ok || !statusRes.ok) throw new Error('API 请求失败')
|
|
52
|
-
|
|
53
|
-
const statsData = await statsRes.json()
|
|
54
|
-
const statusData = await statusRes.json()
|
|
55
|
-
|
|
56
|
-
if (statsData.success && statusData.success) {
|
|
57
|
-
setStats(statsData.data)
|
|
58
|
-
setSystemStatus(statusData.data)
|
|
59
|
-
setError(null)
|
|
60
|
-
}
|
|
61
|
-
} catch (err) {
|
|
62
|
-
setError((err as Error).message)
|
|
63
|
-
} finally {
|
|
64
|
-
setLoading(false)
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const formatUptime = (seconds: number) => {
|
|
69
|
-
const days = Math.floor(seconds / 86400)
|
|
70
|
-
const hours = Math.floor((seconds % 86400) / 3600)
|
|
71
|
-
const minutes = Math.floor((seconds % 3600) / 60)
|
|
72
|
-
return `${days}天 ${hours}小时 ${minutes}分钟`
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const formatMemory = (bytes: number) => {
|
|
76
|
-
return `${(bytes / 1024 / 1024).toFixed(2)} MB`
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (loading) {
|
|
80
|
-
return (
|
|
81
|
-
<Flex align="center" justify="center" style={{ height: '100%' }}>
|
|
82
|
-
<Box>
|
|
83
|
-
<Spinner size="3" />
|
|
84
|
-
<Text size="2" color="gray" style={{ marginTop: '8px' }}>加载中...</Text>
|
|
85
|
-
</Box>
|
|
86
|
-
</Flex>
|
|
87
|
-
)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (error) {
|
|
91
|
-
return (
|
|
92
|
-
<Flex align="center" justify="center" style={{ height: '100%' }}>
|
|
93
|
-
<Callout.Root color="red">
|
|
94
|
-
<Callout.Icon>
|
|
95
|
-
<AlertCircle />
|
|
96
|
-
</Callout.Icon>
|
|
97
|
-
<Callout.Text>
|
|
98
|
-
加载失败: {error}
|
|
99
|
-
</Callout.Text>
|
|
100
|
-
</Callout.Root>
|
|
101
|
-
</Flex>
|
|
102
|
-
)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return (
|
|
106
|
-
<Box>
|
|
107
|
-
{/* 页面标题 */}
|
|
108
|
-
<Flex direction="column" gap="2" mb="6">
|
|
109
|
-
<Heading size="8">系统概览</Heading>
|
|
110
|
-
<Text color="gray">实时监控您的机器人框架运行状态</Text>
|
|
111
|
-
</Flex>
|
|
112
|
-
|
|
113
|
-
{/* 统计卡片 */}
|
|
114
|
-
<Grid columns={{ initial: '1', sm: '2', lg: '4' }} gap="4" mb="6">
|
|
115
|
-
<Card>
|
|
116
|
-
<Flex direction="column" gap="2">
|
|
117
|
-
<Flex justify="between" align="center">
|
|
118
|
-
<Text size="2" weight="medium" color="gray">插件总数</Text>
|
|
119
|
-
<Box p="2" style={{ borderRadius: '8px', backgroundColor: 'var(--purple-3)' }}>
|
|
120
|
-
<Package size={16} color="var(--purple-9)" />
|
|
121
|
-
</Box>
|
|
122
|
-
</Flex>
|
|
123
|
-
<Heading size="7">{stats?.plugins.total || 0}</Heading>
|
|
124
|
-
<Flex align="center" gap="1">
|
|
125
|
-
<Badge color="blue">{stats?.plugins.active || 0}</Badge>
|
|
126
|
-
<Text size="1" color="gray">个活跃</Text>
|
|
127
|
-
</Flex>
|
|
128
|
-
</Flex>
|
|
129
|
-
</Card>
|
|
130
|
-
|
|
131
|
-
<Card>
|
|
132
|
-
<Flex direction="column" gap="2">
|
|
133
|
-
<Flex justify="between" align="center">
|
|
134
|
-
<Text size="2" weight="medium" color="gray">机器人</Text>
|
|
135
|
-
<Bot size={16} color="var(--gray-9)" />
|
|
136
|
-
</Flex>
|
|
137
|
-
<Heading size="7">{stats?.bots.total || 0}</Heading>
|
|
138
|
-
<Text size="1" color="green">
|
|
139
|
-
{stats?.bots.online || 0} 个在线
|
|
140
|
-
</Text>
|
|
141
|
-
</Flex>
|
|
142
|
-
</Card>
|
|
143
|
-
|
|
144
|
-
<Card>
|
|
145
|
-
<Flex direction="column" gap="2">
|
|
146
|
-
<Flex justify="between" align="center">
|
|
147
|
-
<Text size="2" weight="medium" color="gray">命令数量</Text>
|
|
148
|
-
<Activity size={16} color="var(--gray-9)" />
|
|
149
|
-
</Flex>
|
|
150
|
-
<Heading size="7">{stats?.commands || 0}</Heading>
|
|
151
|
-
<Text size="1" color="gray">可用命令</Text>
|
|
152
|
-
</Flex>
|
|
153
|
-
</Card>
|
|
154
|
-
|
|
155
|
-
<Card>
|
|
156
|
-
<Flex direction="column" gap="2">
|
|
157
|
-
<Flex justify="between" align="center">
|
|
158
|
-
<Text size="2" weight="medium" color="gray">组件数量</Text>
|
|
159
|
-
<TrendingUp size={16} color="var(--gray-9)" />
|
|
160
|
-
</Flex>
|
|
161
|
-
<Heading size="7">{stats?.components || 0}</Heading>
|
|
162
|
-
<Text size="1" color="gray">已注册组件</Text>
|
|
163
|
-
</Flex>
|
|
164
|
-
</Card>
|
|
165
|
-
</Grid>
|
|
166
|
-
|
|
167
|
-
{/* 系统状态 */}
|
|
168
|
-
<Grid columns={{ initial: '1', md: '2' }} gap="4" mb="6">
|
|
169
|
-
<Card>
|
|
170
|
-
<Flex direction="column" gap="4">
|
|
171
|
-
<Box>
|
|
172
|
-
<Heading size="5" mb="1">系统信息</Heading>
|
|
173
|
-
<Text size="2" color="gray">服务器运行状态</Text>
|
|
174
|
-
</Box>
|
|
175
|
-
|
|
176
|
-
<Flex direction="column" gap="3">
|
|
177
|
-
<Flex justify="between" align="center">
|
|
178
|
-
<Flex align="center" gap="2">
|
|
179
|
-
<Clock size={16} color="var(--gray-9)" />
|
|
180
|
-
<Text size="2">运行时间</Text>
|
|
181
|
-
</Flex>
|
|
182
|
-
<Badge variant="soft">
|
|
183
|
-
{systemStatus ? formatUptime(systemStatus.uptime) : '-'}
|
|
184
|
-
</Badge>
|
|
185
|
-
</Flex>
|
|
186
|
-
|
|
187
|
-
<Flex justify="between" align="center">
|
|
188
|
-
<Flex align="center" gap="2">
|
|
189
|
-
<Cpu size={16} color="var(--gray-9)" />
|
|
190
|
-
<Text size="2">平台</Text>
|
|
191
|
-
</Flex>
|
|
192
|
-
<Badge variant="soft">
|
|
193
|
-
{systemStatus?.platform || '-'}
|
|
194
|
-
</Badge>
|
|
195
|
-
</Flex>
|
|
196
|
-
|
|
197
|
-
<Flex justify="between" align="center">
|
|
198
|
-
<Flex align="center" gap="2">
|
|
199
|
-
<Activity size={16} color="var(--gray-9)" />
|
|
200
|
-
<Text size="2">Node 版本</Text>
|
|
201
|
-
</Flex>
|
|
202
|
-
<Badge variant="soft">
|
|
203
|
-
{systemStatus?.nodeVersion || '-'}
|
|
204
|
-
</Badge>
|
|
205
|
-
</Flex>
|
|
206
|
-
</Flex>
|
|
207
|
-
</Flex>
|
|
208
|
-
</Card>
|
|
209
|
-
|
|
210
|
-
<Card>
|
|
211
|
-
<Flex direction="column" gap="4">
|
|
212
|
-
<Box>
|
|
213
|
-
<Heading size="5" mb="1">资源使用</Heading>
|
|
214
|
-
<Text size="2" color="gray">内存使用情况</Text>
|
|
215
|
-
</Box>
|
|
216
|
-
|
|
217
|
-
<Flex direction="column" gap="3">
|
|
218
|
-
<Flex justify="between" align="center">
|
|
219
|
-
<Flex align="center" gap="2">
|
|
220
|
-
<MemoryStick size={16} color="var(--gray-9)" />
|
|
221
|
-
<Text size="2">堆内存使用</Text>
|
|
222
|
-
</Flex>
|
|
223
|
-
<Badge variant="soft">
|
|
224
|
-
{stats ? `${stats.memory.toFixed(2)} MB` : '-'}
|
|
225
|
-
</Badge>
|
|
226
|
-
</Flex>
|
|
227
|
-
|
|
228
|
-
<Flex justify="between" align="center">
|
|
229
|
-
<Flex align="center" gap="2">
|
|
230
|
-
<MemoryStick size={16} color="var(--gray-9)" />
|
|
231
|
-
<Text size="2">总堆内存</Text>
|
|
232
|
-
</Flex>
|
|
233
|
-
<Badge variant="soft">
|
|
234
|
-
{systemStatus ? formatMemory(systemStatus.memory.heapTotal) : '-'}
|
|
235
|
-
</Badge>
|
|
236
|
-
</Flex>
|
|
237
|
-
|
|
238
|
-
<Flex justify="between" align="center">
|
|
239
|
-
<Flex align="center" gap="2">
|
|
240
|
-
<MemoryStick size={16} color="var(--gray-9)" />
|
|
241
|
-
<Text size="2">RSS</Text>
|
|
242
|
-
</Flex>
|
|
243
|
-
<Badge variant="soft">
|
|
244
|
-
{systemStatus ? formatMemory(systemStatus.memory.rss) : '-'}
|
|
245
|
-
</Badge>
|
|
246
|
-
</Flex>
|
|
247
|
-
</Flex>
|
|
248
|
-
</Flex>
|
|
249
|
-
</Card>
|
|
250
|
-
</Grid>
|
|
251
|
-
|
|
252
|
-
{/* 快速操作 */}
|
|
253
|
-
<Card>
|
|
254
|
-
<Flex direction="column" gap="4">
|
|
255
|
-
<Box>
|
|
256
|
-
<Heading size="5" mb="1">快速操作</Heading>
|
|
257
|
-
<Text size="2" color="gray">常用功能快捷入口</Text>
|
|
258
|
-
</Box>
|
|
259
|
-
|
|
260
|
-
<Grid columns={{ initial: '1', md: '3' }} gap="3">
|
|
261
|
-
<Button
|
|
262
|
-
variant="outline"
|
|
263
|
-
onClick={() => navigate('/plugins')}
|
|
264
|
-
style={{ height: 'auto', padding: '16px', cursor: 'pointer' }}
|
|
265
|
-
>
|
|
266
|
-
<Flex direction="column" align="start" gap="2" style={{ width: '100%' }}>
|
|
267
|
-
<Package size={20} color="var(--blue-9)" />
|
|
268
|
-
<Text weight="medium">插件管理</Text>
|
|
269
|
-
<Text size="1" color="gray">查看和管理插件</Text>
|
|
270
|
-
</Flex>
|
|
271
|
-
</Button>
|
|
272
|
-
|
|
273
|
-
<Button
|
|
274
|
-
variant="outline"
|
|
275
|
-
onClick={() => navigate('/bots')}
|
|
276
|
-
style={{ height: 'auto', padding: '16px', cursor: 'pointer' }}
|
|
277
|
-
>
|
|
278
|
-
<Flex direction="column" align="start" gap="2" style={{ width: '100%' }}>
|
|
279
|
-
<Bot size={20} color="var(--green-9)" />
|
|
280
|
-
<Text weight="medium">机器人状态</Text>
|
|
281
|
-
<Text size="1" color="gray">监控机器人运行</Text>
|
|
282
|
-
</Flex>
|
|
283
|
-
</Button>
|
|
284
|
-
|
|
285
|
-
<Button
|
|
286
|
-
variant="outline"
|
|
287
|
-
onClick={() => navigate('/logs')}
|
|
288
|
-
style={{ height: 'auto', padding: '16px', cursor: 'pointer' }}
|
|
289
|
-
>
|
|
290
|
-
<Flex direction="column" align="start" gap="2" style={{ width: '100%' }}>
|
|
291
|
-
<FileText size={20} color="var(--purple-9)" />
|
|
292
|
-
<Text weight="medium">系统日志</Text>
|
|
293
|
-
<Text size="1" color="gray">查看运行日志</Text>
|
|
294
|
-
</Flex>
|
|
295
|
-
</Button>
|
|
296
|
-
</Grid>
|
|
297
|
-
</Flex>
|
|
298
|
-
</Card>
|
|
299
|
-
</Box>
|
|
300
|
-
)
|
|
301
|
-
}
|
|
@@ -1,298 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState, useRef } from 'react'
|
|
2
|
-
import { Flex, Box, Spinner, Text, Callout, Heading, Badge, Grid, Card, Button, Select, Checkbox } from '@radix-ui/themes'
|
|
3
|
-
import { Info, AlertTriangle, XCircle, Circle, Trash2, RefreshCw, FileText, AlertCircle } from 'lucide-react'
|
|
4
|
-
|
|
5
|
-
interface LogEntry {
|
|
6
|
-
level: 'info' | 'warn' | 'error'
|
|
7
|
-
message: string
|
|
8
|
-
timestamp: string
|
|
9
|
-
source: string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
interface LogStats {
|
|
13
|
-
total: number
|
|
14
|
-
byLevel: Record<string, number>
|
|
15
|
-
oldestTimestamp: string | null
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export default function DashboardLogs() {
|
|
19
|
-
const [logs, setLogs] = useState<LogEntry[]>([])
|
|
20
|
-
const [stats, setStats] = useState<LogStats | null>(null)
|
|
21
|
-
const [loading, setLoading] = useState(true)
|
|
22
|
-
const [error, setError] = useState<string | null>(null)
|
|
23
|
-
const [levelFilter, setLevelFilter] = useState<string>('all')
|
|
24
|
-
const [autoScroll, setAutoScroll] = useState(true)
|
|
25
|
-
const logsEndRef = useRef<HTMLDivElement>(null)
|
|
26
|
-
const [prevLogCount, setPrevLogCount] = useState(0)
|
|
27
|
-
|
|
28
|
-
useEffect(() => {
|
|
29
|
-
fetchLogs()
|
|
30
|
-
fetchStats()
|
|
31
|
-
const interval = setInterval(() => {
|
|
32
|
-
fetchLogs()
|
|
33
|
-
fetchStats()
|
|
34
|
-
}, 3000)
|
|
35
|
-
return () => clearInterval(interval)
|
|
36
|
-
}, [levelFilter])
|
|
37
|
-
|
|
38
|
-
useEffect(() => {
|
|
39
|
-
if (autoScroll && logs.length > prevLogCount) {
|
|
40
|
-
logsEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
|
41
|
-
}
|
|
42
|
-
setPrevLogCount(logs.length)
|
|
43
|
-
}, [logs, autoScroll])
|
|
44
|
-
|
|
45
|
-
const fetchLogs = async () => {
|
|
46
|
-
try {
|
|
47
|
-
const url = levelFilter === 'all'
|
|
48
|
-
? '/api/logs?limit=100'
|
|
49
|
-
: `/api/logs?limit=100&level=${levelFilter}`
|
|
50
|
-
|
|
51
|
-
const res = await fetch(url, { credentials: 'include' })
|
|
52
|
-
if (!res.ok) throw new Error('API 请求失败')
|
|
53
|
-
|
|
54
|
-
const data = await res.json()
|
|
55
|
-
if (data.success && Array.isArray(data.data)) {
|
|
56
|
-
setLogs(data.data.reverse())
|
|
57
|
-
setError(null)
|
|
58
|
-
}
|
|
59
|
-
} catch (err) {
|
|
60
|
-
setError((err as Error).message)
|
|
61
|
-
} finally {
|
|
62
|
-
setLoading(false)
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const fetchStats = async () => {
|
|
67
|
-
try {
|
|
68
|
-
const res = await fetch('/api/logs/stats', { credentials: 'include' })
|
|
69
|
-
if (!res.ok) return
|
|
70
|
-
|
|
71
|
-
const data = await res.json()
|
|
72
|
-
if (data.success) {
|
|
73
|
-
setStats(data.data)
|
|
74
|
-
}
|
|
75
|
-
} catch (err) {
|
|
76
|
-
console.error('Failed to fetch stats:', err)
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const handleCleanup = async (days?: number, maxRecords?: number) => {
|
|
81
|
-
const message = days
|
|
82
|
-
? `确定清理 ${days} 天前的日志吗?`
|
|
83
|
-
: `确定只保留最近 ${maxRecords} 条日志吗?`
|
|
84
|
-
|
|
85
|
-
if (!confirm(message)) return
|
|
86
|
-
|
|
87
|
-
try {
|
|
88
|
-
const res = await fetch('/api/logs/cleanup', {
|
|
89
|
-
method: 'POST',
|
|
90
|
-
headers: { 'Content-Type': 'application/json' },
|
|
91
|
-
body: JSON.stringify({ days, maxRecords }),
|
|
92
|
-
credentials: 'include'
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
if (!res.ok) throw new Error('清理失败')
|
|
96
|
-
|
|
97
|
-
const data = await res.json()
|
|
98
|
-
if (data.success) {
|
|
99
|
-
alert(`成功清理 ${data.data.deletedCount} 条日志`)
|
|
100
|
-
fetchLogs()
|
|
101
|
-
fetchStats()
|
|
102
|
-
}
|
|
103
|
-
} catch (err) {
|
|
104
|
-
alert((err as Error).message)
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const getLevelColor = (level: string): 'blue' | 'amber' | 'red' | 'gray' => {
|
|
109
|
-
switch (level) {
|
|
110
|
-
case 'info': return 'blue'
|
|
111
|
-
case 'warn': return 'amber'
|
|
112
|
-
case 'error': return 'red'
|
|
113
|
-
default: return 'gray'
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const getLevelIcon = (level: string) => {
|
|
118
|
-
switch (level) {
|
|
119
|
-
case 'info': return <Info size={14} />
|
|
120
|
-
case 'warn': return <AlertTriangle size={14} />
|
|
121
|
-
case 'error': return <XCircle size={14} />
|
|
122
|
-
default: return <Circle size={14} />
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (loading) {
|
|
127
|
-
return (
|
|
128
|
-
<Flex align="center" justify="center" style={{ height: '100%' }}>
|
|
129
|
-
<Box>
|
|
130
|
-
<Spinner size="3" />
|
|
131
|
-
<Text size="2" color="gray" style={{ marginTop: '8px' }}>加载中...</Text>
|
|
132
|
-
</Box>
|
|
133
|
-
</Flex>
|
|
134
|
-
)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return (
|
|
138
|
-
<Box>
|
|
139
|
-
{/* 页面标题 */}
|
|
140
|
-
<Flex direction="column" gap="2" mb="4">
|
|
141
|
-
<Heading size="8">系统日志</Heading>
|
|
142
|
-
<Text color="gray">实时查看系统运行日志</Text>
|
|
143
|
-
</Flex>
|
|
144
|
-
|
|
145
|
-
{/* 日志统计 */}
|
|
146
|
-
{stats && (
|
|
147
|
-
<Grid columns={{ initial: '2', sm: '4' }} gap="3" mb="4">
|
|
148
|
-
<Card>
|
|
149
|
-
<Flex direction="column" align="center" gap="1" p="3">
|
|
150
|
-
<Text size="5" weight="bold">{stats.total}</Text>
|
|
151
|
-
<Text size="1" color="gray">总日志数</Text>
|
|
152
|
-
</Flex>
|
|
153
|
-
</Card>
|
|
154
|
-
<Card>
|
|
155
|
-
<Flex direction="column" align="center" gap="1" p="3">
|
|
156
|
-
<Text size="5" weight="bold" color="blue">{stats.byLevel.info || 0}</Text>
|
|
157
|
-
<Text size="1" color="gray">Info</Text>
|
|
158
|
-
</Flex>
|
|
159
|
-
</Card>
|
|
160
|
-
<Card>
|
|
161
|
-
<Flex direction="column" align="center" gap="1" p="3">
|
|
162
|
-
<Text size="5" weight="bold" color="amber">{stats.byLevel.warn || 0}</Text>
|
|
163
|
-
<Text size="1" color="gray">Warn</Text>
|
|
164
|
-
</Flex>
|
|
165
|
-
</Card>
|
|
166
|
-
<Card>
|
|
167
|
-
<Flex direction="column" align="center" gap="1" p="3">
|
|
168
|
-
<Text size="5" weight="bold" color="red">{stats.byLevel.error || 0}</Text>
|
|
169
|
-
<Text size="1" color="gray">Error</Text>
|
|
170
|
-
</Flex>
|
|
171
|
-
</Card>
|
|
172
|
-
</Grid>
|
|
173
|
-
)}
|
|
174
|
-
|
|
175
|
-
{/* 工具栏 */}
|
|
176
|
-
<Card mb="4">
|
|
177
|
-
<Flex justify="between" align="center" p="3" wrap="wrap" gap="3">
|
|
178
|
-
<Flex align="center" gap="3" wrap="wrap">
|
|
179
|
-
<Select.Root value={levelFilter} onValueChange={setLevelFilter}>
|
|
180
|
-
<Select.Trigger style={{ minWidth: '120px' }} />
|
|
181
|
-
<Select.Content>
|
|
182
|
-
<Select.Item value="all">所有级别</Select.Item>
|
|
183
|
-
<Select.Item value="info">Info</Select.Item>
|
|
184
|
-
<Select.Item value="warn">Warn</Select.Item>
|
|
185
|
-
<Select.Item value="error">Error</Select.Item>
|
|
186
|
-
</Select.Content>
|
|
187
|
-
</Select.Root>
|
|
188
|
-
|
|
189
|
-
<Flex as="span">
|
|
190
|
-
<Checkbox
|
|
191
|
-
checked={autoScroll}
|
|
192
|
-
onCheckedChange={(checked) => setAutoScroll(checked as boolean)}
|
|
193
|
-
/>
|
|
194
|
-
<Text size="2">自动滚动</Text>
|
|
195
|
-
</Flex>
|
|
196
|
-
</Flex>
|
|
197
|
-
|
|
198
|
-
<Flex gap="2">
|
|
199
|
-
<Button
|
|
200
|
-
variant="soft"
|
|
201
|
-
onClick={() => handleCleanup(7)}
|
|
202
|
-
size="2"
|
|
203
|
-
>
|
|
204
|
-
<Trash2 size={14} />
|
|
205
|
-
清理7天前
|
|
206
|
-
</Button>
|
|
207
|
-
<Button
|
|
208
|
-
variant="soft"
|
|
209
|
-
onClick={() => handleCleanup(undefined, 5000)}
|
|
210
|
-
size="2"
|
|
211
|
-
>
|
|
212
|
-
<Trash2 size={14} />
|
|
213
|
-
保留5000条
|
|
214
|
-
</Button>
|
|
215
|
-
<Button
|
|
216
|
-
variant="soft"
|
|
217
|
-
onClick={() => fetchLogs()}
|
|
218
|
-
size="2"
|
|
219
|
-
>
|
|
220
|
-
<RefreshCw size={14} />
|
|
221
|
-
刷新
|
|
222
|
-
</Button>
|
|
223
|
-
</Flex>
|
|
224
|
-
</Flex>
|
|
225
|
-
</Card>
|
|
226
|
-
|
|
227
|
-
{/* 日志列表 */}
|
|
228
|
-
<Card>
|
|
229
|
-
<Box
|
|
230
|
-
p="4"
|
|
231
|
-
style={{
|
|
232
|
-
maxHeight: '600px',
|
|
233
|
-
overflowY: 'auto',
|
|
234
|
-
backgroundColor: 'var(--gray-1)'
|
|
235
|
-
}}
|
|
236
|
-
>
|
|
237
|
-
{error ? (
|
|
238
|
-
<Callout.Root color="red">
|
|
239
|
-
<Callout.Icon>
|
|
240
|
-
<AlertCircle />
|
|
241
|
-
</Callout.Icon>
|
|
242
|
-
<Callout.Text>
|
|
243
|
-
加载失败: {error}
|
|
244
|
-
</Callout.Text>
|
|
245
|
-
</Callout.Root>
|
|
246
|
-
) : logs.length === 0 ? (
|
|
247
|
-
<Flex direction="column" align="center" gap="3" py="9">
|
|
248
|
-
<FileText size={48} color="var(--gray-6)" />
|
|
249
|
-
<Text color="gray">暂无日志</Text>
|
|
250
|
-
</Flex>
|
|
251
|
-
) : (
|
|
252
|
-
<Flex direction="column" gap="2">
|
|
253
|
-
{logs.map((log, index) => (
|
|
254
|
-
<Box
|
|
255
|
-
key={`${log.timestamp}-${index}`}
|
|
256
|
-
p="3"
|
|
257
|
-
style={{
|
|
258
|
-
borderRadius: '6px',
|
|
259
|
-
backgroundColor: 'var(--gray-2)',
|
|
260
|
-
borderLeft: `3px solid var(--${getLevelColor(log.level)}-9)`
|
|
261
|
-
}}
|
|
262
|
-
>
|
|
263
|
-
<Flex direction="column" gap="1">
|
|
264
|
-
<Flex align="center" gap="2">
|
|
265
|
-
<Badge color={getLevelColor(log.level)} size="1">
|
|
266
|
-
<Flex align="center" gap="1">
|
|
267
|
-
{getLevelIcon(log.level)}
|
|
268
|
-
{log.level.toUpperCase()}
|
|
269
|
-
</Flex>
|
|
270
|
-
</Badge>
|
|
271
|
-
<Text size="1" color="gray">
|
|
272
|
-
{new Date(log.timestamp).toLocaleString()}
|
|
273
|
-
</Text>
|
|
274
|
-
{log.source && (
|
|
275
|
-
<Badge variant="soft" size="1">{log.source}</Badge>
|
|
276
|
-
)}
|
|
277
|
-
</Flex>
|
|
278
|
-
<Text
|
|
279
|
-
size="2"
|
|
280
|
-
style={{
|
|
281
|
-
fontFamily: 'monospace',
|
|
282
|
-
whiteSpace: 'pre-wrap',
|
|
283
|
-
wordBreak: 'break-word'
|
|
284
|
-
}}
|
|
285
|
-
>
|
|
286
|
-
{log.message}
|
|
287
|
-
</Text>
|
|
288
|
-
</Flex>
|
|
289
|
-
</Box>
|
|
290
|
-
))}
|
|
291
|
-
<div ref={logsEndRef} />
|
|
292
|
-
</Flex>
|
|
293
|
-
)}
|
|
294
|
-
</Box>
|
|
295
|
-
</Card>
|
|
296
|
-
</Box>
|
|
297
|
-
)
|
|
298
|
-
}
|