Skip to content

Commit 4abb79f

Browse files
committed
feat: 服务状态监控和主题系统优化 v1.0.4
- 新增服务状态监控页面,实时显示多模型、多线路状态 - 优化重置按钮 UI,蓝色主题更醒目 - 修复主题切换失败和网站主题同步问题 - 修复 Storage 保存失败问题
1 parent 1bcb33d commit 4abb79f

18 files changed

+1250
-256
lines changed

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,22 @@
1313
- 套餐使用统计图表
1414
- 通知提醒功能
1515

16+
## [1.0.4] - 2025-02-10
17+
18+
### 新增
19+
- ✨ 服务状态监控页面,实时显示多模型、多线路的服务状态和可用性
20+
- ✨ 分组倍率配置显示(如 1.7x, 2x, 5x 等)
21+
22+
### 优化
23+
- 🎨 重置按钮 UI 优化,蓝色主题更醒目,添加加载动画
24+
- 🎨 主题切换简化为明亮/深色两种模式,每次打开自动同步官网主题
25+
26+
### 修复
27+
- 🐛 修复主题切换按钮无响应问题
28+
- 🐛 修复网站主题同步,正确读取 `vueuse-color-scheme` key
29+
- 🐛 修复 Token 读取,只读取 `authToken` key
30+
- 🐛 修复 Storage 保存失败问题
31+
1632
## [1.0.3] - 2025-12-10
1733

1834
### 新增

components/ServiceStatusButton.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* 服务状态按钮组件
3+
*/
4+
5+
import React from "react"
6+
import { Activity } from "lucide-react"
7+
8+
interface ServiceStatusButtonProps {
9+
onClick: () => void
10+
}
11+
12+
export function ServiceStatusButton({ onClick }: ServiceStatusButtonProps) {
13+
return (
14+
<button
15+
onClick={onClick}
16+
className="flex items-center justify-center rounded-md p-1.5 text-gray-700 transition-colors hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
17+
aria-label="服务状态"
18+
title="服务状态"
19+
>
20+
<Activity className="h-5 w-5" />
21+
</button>
22+
)
23+
}

components/ServiceStatusPanel.tsx

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
/**
2+
* 服务状态监控面板
3+
* 展示多模型、多线路的服务状态和可用性
4+
*/
5+
6+
import React, { useMemo } from "react"
7+
import type { ServiceProvider, ServiceStatusTimeline, GroupRatioConfig } from "~/types"
8+
import { Card, CardContent, CardHeader } from "~/components/ui/card"
9+
import { Badge } from "~/components/ui/badge"
10+
import {
11+
Tooltip,
12+
TooltipContent,
13+
TooltipProvider,
14+
TooltipTrigger
15+
} from "~/components/ui/tooltip"
16+
17+
interface ServiceStatusPanelProps {
18+
providers: ServiceProvider[]
19+
groupRatioConfig: GroupRatioConfig | null
20+
loading: boolean
21+
}
22+
23+
// Provider ID 到配置 key 的映射
24+
const providerIdToConfigKey: Record<string, string> = {
25+
"88code-cc-kiro": "reverse_kiro",
26+
"88code-cc-antigravity": "reverse_antig",
27+
"88code-openai-team": "codex_team",
28+
"88code-cc-special": "reverse_special",
29+
"88code-cc-max": "claude_max"
30+
// 以下渠道未配置,默认 1x(显示):
31+
// 88code-cc-v5, 88code-cc-v3, 88code-openai-v5, 88code-openai-v3
32+
}
33+
34+
// 获取 provider 的倍率
35+
const getProviderRatio = (providerId: string, groupRatioConfig: GroupRatioConfig | null): number => {
36+
const configKey = providerIdToConfigKey[providerId]
37+
if (configKey && groupRatioConfig && groupRatioConfig[configKey]) {
38+
return groupRatioConfig[configKey]
39+
}
40+
return 1 // 默认 1x
41+
}
42+
43+
export function ServiceStatusPanel({
44+
providers,
45+
groupRatioConfig,
46+
loading
47+
}: ServiceStatusPanelProps) {
48+
if (loading) {
49+
return <ServiceStatusPanelSkeleton />
50+
}
51+
52+
if (!providers || providers.length === 0) {
53+
return null
54+
}
55+
56+
// 按 display_group 分组
57+
const groupedProviders = useMemo(() => {
58+
const groups = new Map<string, ServiceProvider[]>()
59+
providers.forEach((provider) => {
60+
const group = provider.display_group
61+
if (!groups.has(group)) {
62+
groups.set(group, [])
63+
}
64+
groups.get(group)!.push(provider)
65+
})
66+
return Array.from(groups.entries())
67+
}, [providers])
68+
69+
return (
70+
<div className="space-y-3">
71+
<div className="space-y-3">
72+
{groupedProviders.map(([groupName, groupProviders]) => (
73+
<Card key={groupName} className="overflow-hidden">
74+
<CardHeader className="p-4 pb-3 border-b bg-card">
75+
<div className="flex items-center justify-between">
76+
<div className="flex items-center gap-2">
77+
<h3 className="text-base font-semibold text-card-foreground">{groupName}</h3>
78+
</div>
79+
<div className="flex items-center gap-2 text-xs text-muted-foreground">
80+
<span className="flex items-center gap-1">
81+
<span className="inline-block w-2 h-2 rounded-full bg-green-500"></span>
82+
正常
83+
</span>
84+
<span className="flex items-center gap-1">
85+
<span className="inline-block w-2 h-2 rounded-full bg-yellow-500"></span>
86+
降级
87+
</span>
88+
<span className="flex items-center gap-1">
89+
<span className="inline-block w-2 h-2 rounded-full bg-red-500"></span>
90+
异常
91+
</span>
92+
</div>
93+
</div>
94+
</CardHeader>
95+
<CardContent className="p-4 space-y-3 bg-card">
96+
{groupProviders.map((provider) => (
97+
<ServiceProviderRow
98+
key={provider.id}
99+
provider={provider}
100+
groupRatioConfig={groupRatioConfig}
101+
/>
102+
))}
103+
</CardContent>
104+
</Card>
105+
))}
106+
</div>
107+
</div>
108+
)
109+
}
110+
111+
interface ServiceProviderRowProps {
112+
provider: ServiceProvider
113+
groupRatioConfig: GroupRatioConfig | null
114+
}
115+
116+
function ServiceProviderRow({ provider, groupRatioConfig }: ServiceProviderRowProps) {
117+
const { name, latest, statistics, timeline } = provider
118+
119+
// 获取该 provider 的倍率
120+
const ratio = getProviderRatio(provider.id, groupRatioConfig)
121+
122+
// 计算可用性百分比
123+
const uptimePercent = statistics.success_rate
124+
125+
// 获取状态颜色
126+
const getStatusColor = (status: string) => {
127+
switch (status) {
128+
case "operational":
129+
return "bg-green-500 dark:bg-green-600"
130+
case "degraded":
131+
return "bg-yellow-500 dark:bg-yellow-600"
132+
case "error":
133+
return "bg-red-500 dark:bg-red-600"
134+
default:
135+
return "bg-gray-400 dark:bg-gray-600"
136+
}
137+
}
138+
139+
// 获取状态文本
140+
const getStatusText = (status: string) => {
141+
switch (status) {
142+
case "operational":
143+
return "正常"
144+
case "degraded":
145+
return "降级"
146+
case "error":
147+
return "异常"
148+
default:
149+
return "未知"
150+
}
151+
}
152+
153+
// 格式化延迟
154+
const formatLatency = (latency: number | null) => {
155+
if (latency === null) return "N/A"
156+
return `${latency}ms`
157+
}
158+
159+
// 格式化时间
160+
const formatTime = (timestamp: string) => {
161+
const date = new Date(timestamp)
162+
return date.toLocaleTimeString("zh-CN", {
163+
hour: "2-digit",
164+
minute: "2-digit"
165+
})
166+
}
167+
168+
// 只显示最近 60 个状态点(最近 1 小时)
169+
const recentTimeline = timeline.slice(0, 60).reverse()
170+
171+
return (
172+
<div className="space-y-2">
173+
{/* 服务名称和状态 */}
174+
<div className="flex items-center justify-between">
175+
<div className="flex items-center gap-2">
176+
<span className="text-sm font-medium text-foreground">{name}</span>
177+
<Badge
178+
variant={
179+
latest.status === "operational"
180+
? "success"
181+
: latest.status === "degraded"
182+
? "warning"
183+
: "destructive"
184+
}
185+
className="text-xs"
186+
>
187+
{getStatusText(latest.status)}
188+
</Badge>
189+
{/* 显示倍率,包括默认的 1x */}
190+
<span className="text-xs px-2 py-0.5 rounded-full bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300 font-medium">
191+
{ratio}x
192+
</span>
193+
</div>
194+
<div className="flex items-center gap-3 text-xs text-muted-foreground">
195+
<span>延迟: {formatLatency(latest.latency_ms)}</span>
196+
<span className="font-semibold text-foreground">
197+
{uptimePercent.toFixed(1)}% 可用
198+
</span>
199+
</div>
200+
</div>
201+
202+
{/* 时间轴状态点 */}
203+
<div className="flex items-center gap-1">
204+
<TooltipProvider>
205+
{recentTimeline.map((point, index) => (
206+
<Tooltip key={index}>
207+
<TooltipTrigger asChild>
208+
<div
209+
className={`w-1.5 h-6 rounded-sm cursor-pointer transition-opacity hover:opacity-80 ${getStatusColor(
210+
point.status
211+
)}`}
212+
/>
213+
</TooltipTrigger>
214+
<TooltipContent side="top" className="text-xs">
215+
<div className="space-y-1">
216+
<p className="font-semibold">
217+
{getStatusText(point.status)}
218+
</p>
219+
<p>时间: {formatTime(point.checked_at)}</p>
220+
<p>延迟: {formatLatency(point.latency_ms)}</p>
221+
{point.message && point.status !== "operational" && (
222+
<p className="text-red-400 max-w-xs break-words">
223+
{point.message.length > 100
224+
? point.message.substring(0, 100) + "..."
225+
: point.message}
226+
</p>
227+
)}
228+
</div>
229+
</TooltipContent>
230+
</Tooltip>
231+
))}
232+
</TooltipProvider>
233+
</div>
234+
235+
{/* 统计信息 */}
236+
<div className="flex items-center gap-4 text-xs text-muted-foreground">
237+
<span>
238+
检查次数: {statistics.total_checks}
239+
</span>
240+
<span>
241+
成功: {statistics.operational_count}
242+
</span>
243+
{statistics.degraded_count > 0 && (
244+
<span className="text-yellow-600 dark:text-yellow-400">
245+
降级: {statistics.degraded_count}
246+
</span>
247+
)}
248+
{statistics.failed_count > 0 && (
249+
<span className="text-red-600 dark:text-red-400">
250+
失败: {statistics.failed_count}
251+
</span>
252+
)}
253+
{statistics.avg_latency_ms !== null && (
254+
<span>
255+
平均延迟: {statistics.avg_latency_ms.toFixed(0)}ms
256+
</span>
257+
)}
258+
</div>
259+
</div>
260+
)
261+
}
262+
263+
/**
264+
* 骨架屏组件
265+
*/
266+
export function ServiceStatusPanelSkeleton() {
267+
return (
268+
<div className="space-y-3">
269+
<div className="h-6 w-24 bg-muted rounded animate-pulse"></div>
270+
<div className="space-y-3">
271+
{[1, 2].map((i) => (
272+
<Card key={i} className="animate-pulse">
273+
<CardHeader className="p-4 pb-3 border-b">
274+
<div className="flex items-center justify-between">
275+
<div className="h-5 w-32 bg-muted rounded"></div>
276+
<div className="flex items-center gap-2">
277+
<div className="h-4 w-16 bg-muted rounded"></div>
278+
<div className="h-4 w-16 bg-muted rounded"></div>
279+
<div className="h-4 w-16 bg-muted rounded"></div>
280+
</div>
281+
</div>
282+
</CardHeader>
283+
<CardContent className="p-4 space-y-3">
284+
{[1, 2, 3].map((j) => (
285+
<div key={j} className="space-y-2">
286+
<div className="flex items-center justify-between">
287+
<div className="h-4 w-40 bg-muted rounded"></div>
288+
<div className="h-4 w-32 bg-muted rounded"></div>
289+
</div>
290+
<div className="h-6 w-full bg-muted rounded"></div>
291+
<div className="flex items-center gap-4">
292+
<div className="h-3 w-20 bg-muted rounded"></div>
293+
<div className="h-3 w-16 bg-muted rounded"></div>
294+
<div className="h-3 w-24 bg-muted rounded"></div>
295+
</div>
296+
</div>
297+
))}
298+
</CardContent>
299+
</Card>
300+
))}
301+
</div>
302+
</div>
303+
)
304+
}

0 commit comments

Comments
 (0)