mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat(ui): 重构端点健康状态为进度条可视化
- 将端点标签改为网格布局的进度条样式 - 显示 API 格式缩写和健康分数百分比 - 优化端点提示区分禁用、无可用密钥和未配置密钥状态 - 优化优先级管理对话框布局以显示更多内容 - 后端添加 total_keys 字段支持密钥状态区分 Closes #113 Co-authored-by: AAEE86 <ppk0227@hotmail.com>
This commit is contained in:
@@ -221,6 +221,7 @@ export interface EndpointHealthDetail {
|
||||
api_format: string
|
||||
health_score: number
|
||||
is_active: boolean
|
||||
total_keys?: number
|
||||
active_keys?: number
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
title="优先级管理"
|
||||
description="调整提供商和 API Key 的优先级顺序,保存后自动切换对应的调度策略"
|
||||
:icon="ListOrdered"
|
||||
size="3xl"
|
||||
size="2xl"
|
||||
@update:model-value="handleDialogUpdate"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
@@ -39,7 +39,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<div class="min-h-[420px]">
|
||||
<div class="min-h-[70vh]">
|
||||
<!-- 提供商优先级 -->
|
||||
<div
|
||||
v-show="activeMainTab === 'provider'"
|
||||
@@ -63,12 +63,12 @@
|
||||
<!-- 提供商列表 -->
|
||||
<div
|
||||
v-else
|
||||
class="space-y-2 max-h-[380px] overflow-y-auto pr-1"
|
||||
class="space-y-1 max-h-[65vh] overflow-y-auto pr-1"
|
||||
>
|
||||
<div
|
||||
v-for="(provider, index) in sortedProviders"
|
||||
:key="provider.id"
|
||||
class="group flex items-center gap-3 px-3 py-2.5 rounded-lg border transition-all duration-200"
|
||||
class="group flex items-center gap-3 px-3 py-1.5 rounded-lg border transition-all duration-200"
|
||||
:class="[
|
||||
draggedProvider === index
|
||||
? 'border-primary/50 bg-primary/5 shadow-md scale-[1.01]'
|
||||
@@ -122,26 +122,30 @@
|
||||
停用
|
||||
</Badge>
|
||||
</div>
|
||||
<!-- API 格式标签 -->
|
||||
<div
|
||||
v-if="provider.api_formats?.length"
|
||||
class="flex items-center gap-1 shrink-0"
|
||||
>
|
||||
<span
|
||||
v-for="fmt in provider.api_formats"
|
||||
:key="fmt"
|
||||
class="text-[10px] px-1.5 py-0.5 rounded bg-muted text-muted-foreground"
|
||||
>
|
||||
{{ API_FORMAT_SHORT[fmt] || fmt }}
|
||||
</span>
|
||||
<div class="flex items-center gap-3 shrink-0 ml-2">
|
||||
<!-- API 格式标签 (自适应宽度) -->
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<template v-if="provider.api_formats?.length">
|
||||
<span
|
||||
v-for="fmt in provider.api_formats"
|
||||
:key="fmt"
|
||||
class="text-[10px] px-1.5 py-0.5 rounded bg-muted text-muted-foreground whitespace-nowrap"
|
||||
>
|
||||
{{ API_FORMAT_SHORT[fmt] || fmt }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 余额显示 (表格对齐) -->
|
||||
<div class="min-w-[4rem] text-right">
|
||||
<span
|
||||
v-if="formatBalanceDisplay(provider.id)"
|
||||
class="text-xs font-semibold text-foreground/90 tabular-nums"
|
||||
>
|
||||
{{ formatBalanceDisplay(provider.id) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 余额显示-->
|
||||
<span
|
||||
v-if="formatBalanceDisplay(provider.id)"
|
||||
class="text-xs shrink-0"
|
||||
>
|
||||
<span class="font-semibold text-foreground/90">{{ formatBalanceDisplay(provider.id) }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -209,7 +213,7 @@
|
||||
>
|
||||
<div
|
||||
v-if="keysByFormat[format]?.length > 0"
|
||||
class="space-y-2 max-h-[380px] overflow-y-auto pr-1"
|
||||
class="space-y-2 max-h-[65vh] overflow-y-auto pr-1"
|
||||
>
|
||||
<div
|
||||
v-for="(key, index) in keysByFormat[format]"
|
||||
|
||||
@@ -101,19 +101,19 @@
|
||||
<TableHead class="w-[18%] min-w-[140px] h-11 font-medium text-foreground/80">
|
||||
提供商信息
|
||||
</TableHead>
|
||||
<TableHead class="w-[16%] min-w-[180px] h-11 font-medium text-foreground/80">
|
||||
<TableHead class="w-[20%] min-w-[180px] h-11 font-medium text-foreground/80">
|
||||
余额监控
|
||||
</TableHead>
|
||||
<TableHead class="w-[12%] min-w-[100px] h-11 font-medium text-foreground/80 text-center">
|
||||
资源统计
|
||||
</TableHead>
|
||||
<TableHead class="w-[30%] min-w-[200px] h-11 font-medium text-foreground/80">
|
||||
<TableHead class="w-[24%] min-w-[260px] h-11 font-medium text-foreground/80">
|
||||
端点健康
|
||||
</TableHead>
|
||||
<TableHead class="w-[8%] min-w-[60px] h-11 font-medium text-foreground/80 text-center">
|
||||
状态
|
||||
</TableHead>
|
||||
<TableHead class="w-[16%] min-w-[120px] h-11 font-medium text-foreground/80 text-center">
|
||||
<TableHead class="w-[18%] min-w-[120px] h-11 font-medium text-foreground/80 text-center">
|
||||
操作
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
@@ -265,21 +265,33 @@
|
||||
<TableCell class="py-3.5 align-middle">
|
||||
<div
|
||||
v-if="provider.endpoint_health_details && provider.endpoint_health_details.length > 0"
|
||||
class="flex flex-wrap gap-1.5 max-w-[280px]"
|
||||
class="grid grid-cols-3 gap-x-3 gap-y-2 max-w-[240px]"
|
||||
>
|
||||
<span
|
||||
<div
|
||||
v-for="endpoint in sortEndpoints(provider.endpoint_health_details)"
|
||||
:key="endpoint.api_format"
|
||||
class="inline-flex items-center gap-1 px-2 py-0.5 rounded-md border text-[10px] font-medium tracking-wide uppercase leading-none"
|
||||
:class="getEndpointTagClass(endpoint, provider)"
|
||||
class="flex flex-col gap-1.5"
|
||||
:title="getEndpointTooltip(endpoint, provider)"
|
||||
>
|
||||
<span
|
||||
class="w-1.5 h-1.5 rounded-full"
|
||||
:class="getEndpointDotColor(endpoint, provider)"
|
||||
/>
|
||||
{{ endpoint.api_format }}
|
||||
</span>
|
||||
<!-- 上排:缩写 + 百分比 -->
|
||||
<div class="flex items-center justify-between text-[10px] leading-none">
|
||||
<span class="font-medium text-muted-foreground/80">
|
||||
{{ API_FORMAT_SHORT[endpoint.api_format] || endpoint.api_format.substring(0,2) }}
|
||||
</span>
|
||||
<span class="font-medium text-muted-foreground/80">
|
||||
{{ isEndpointAvailable(endpoint, provider) ? `${(endpoint.health_score * 100).toFixed(0)}%` : '-' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 下排:进度条 -->
|
||||
<div class="h-1.5 w-full bg-muted/40 rounded-full overflow-hidden">
|
||||
<div
|
||||
class="h-full rounded-full transition-all duration-300"
|
||||
:class="getEndpointDotColor(endpoint, provider)"
|
||||
:style="{ width: isEndpointAvailable(endpoint, provider) ? `${Math.max(endpoint.health_score * 100, 5)}%` : '100%' }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
v-else
|
||||
@@ -489,20 +501,33 @@
|
||||
<!-- 第三行:端点健康 -->
|
||||
<div
|
||||
v-if="provider.endpoint_health_details && provider.endpoint_health_details.length > 0"
|
||||
class="flex flex-wrap gap-1.5"
|
||||
class="grid grid-cols-3 gap-x-3 gap-y-2 max-w-[240px]"
|
||||
>
|
||||
<span
|
||||
<div
|
||||
v-for="endpoint in sortEndpoints(provider.endpoint_health_details)"
|
||||
:key="endpoint.api_format"
|
||||
class="inline-flex items-center gap-1 px-2 py-0.5 rounded-md border text-[10px] font-medium tracking-wide uppercase leading-none"
|
||||
:class="getEndpointTagClass(endpoint, provider)"
|
||||
class="flex flex-col gap-1.5"
|
||||
:title="getEndpointTooltip(endpoint, provider)"
|
||||
>
|
||||
<span
|
||||
class="w-1.5 h-1.5 rounded-full"
|
||||
:class="getEndpointDotColor(endpoint, provider)"
|
||||
/>
|
||||
{{ endpoint.api_format }}
|
||||
</span>
|
||||
<!-- 上排:缩写 + 百分比 -->
|
||||
<div class="flex items-center justify-between text-[10px] leading-none">
|
||||
<span class="font-medium text-muted-foreground/80">
|
||||
{{ API_FORMAT_SHORT[endpoint.api_format] || endpoint.api_format.substring(0,2) }}
|
||||
</span>
|
||||
<span class="font-medium text-muted-foreground/80">
|
||||
{{ isEndpointAvailable(endpoint, provider) ? `${(endpoint.health_score * 100).toFixed(0)}%` : '-' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 下排:进度条 -->
|
||||
<div class="h-1.5 w-full bg-muted/40 rounded-full overflow-hidden">
|
||||
<div
|
||||
class="h-full rounded-full transition-all duration-300"
|
||||
:class="getEndpointDotColor(endpoint, provider)"
|
||||
:style="{ width: isEndpointAvailable(endpoint, provider) ? `${Math.max(endpoint.health_score * 100, 5)}%` : '100%' }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -584,7 +609,8 @@ import {
|
||||
getProvidersSummary,
|
||||
deleteProvider,
|
||||
updateProvider,
|
||||
type ProviderWithEndpointsSummary
|
||||
type ProviderWithEndpointsSummary,
|
||||
API_FORMAT_SHORT
|
||||
} from '@/api/endpoints'
|
||||
import { adminApi } from '@/api/admin'
|
||||
import { batchQueryBalance, type ActionResultResponse } from '@/api/providerOps'
|
||||
@@ -902,10 +928,10 @@ function getEndpointTagClass(endpoint: any, provider: ProviderWithEndpointsSumma
|
||||
return 'border-border/40 bg-muted/20 text-foreground/70'
|
||||
}
|
||||
|
||||
// 端点圆点颜色
|
||||
// 进度条颜色
|
||||
function getEndpointDotColor(endpoint: any, provider: ProviderWithEndpointsSummary): string {
|
||||
if (!isEndpointAvailable(endpoint, provider)) {
|
||||
return 'bg-red-400'
|
||||
return 'bg-red-400/50'
|
||||
}
|
||||
const score = endpoint.health_score
|
||||
if (score === undefined || score === null) {
|
||||
@@ -922,8 +948,15 @@ function getEndpointDotColor(endpoint: any, provider: ProviderWithEndpointsSumma
|
||||
|
||||
// 端点提示文本
|
||||
function getEndpointTooltip(endpoint: any, provider: ProviderWithEndpointsSummary): string {
|
||||
if (provider.active_keys === 0) {
|
||||
return `${endpoint.api_format}: 无可用密钥`
|
||||
if (endpoint.is_active === false) {
|
||||
return `${endpoint.api_format}: 端点已禁用`
|
||||
}
|
||||
if (endpoint.active_keys === 0) {
|
||||
// 区分:有密钥但全部禁用 vs 未配置任何密钥
|
||||
if ((endpoint.total_keys ?? 0) > 0) {
|
||||
return `${endpoint.api_format}: 无可用密钥`
|
||||
}
|
||||
return `${endpoint.api_format}: 未配置密钥`
|
||||
}
|
||||
const score = endpoint.health_score
|
||||
if (score === undefined || score === null) {
|
||||
|
||||
Reference in New Issue
Block a user