mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
refactor(pool): 移除调度多维评分详情,简化为 reasons 摘要
移除 scheduling_score、candidate_eligible、scheduling_dimensions 等字段, 前后端统一使用 scheduling_reasons 展示调度状态,精简号池列表信息密度。
This commit is contained in:
@@ -142,11 +142,6 @@ export interface PoolKeyDetail {
|
||||
| string
|
||||
scheduling_label?: string
|
||||
scheduling_reasons?: PoolSchedulingReason[]
|
||||
scheduling_score?: number
|
||||
candidate_eligible?: boolean
|
||||
scheduling_blocked_count?: number
|
||||
scheduling_degraded_count?: number
|
||||
scheduling_dimensions?: PoolSchedulingDimension[]
|
||||
}
|
||||
|
||||
export interface PoolSchedulingReason {
|
||||
@@ -158,18 +153,6 @@ export interface PoolSchedulingReason {
|
||||
detail?: string | null
|
||||
}
|
||||
|
||||
export interface PoolSchedulingDimension {
|
||||
code: string
|
||||
label: string
|
||||
status: 'ok' | 'degraded' | 'blocked' | string
|
||||
blocking: boolean
|
||||
source: 'manual' | 'pool' | 'health' | 'policy' | string
|
||||
weight: number
|
||||
score: number
|
||||
ttl_seconds?: number | null
|
||||
detail?: string | null
|
||||
}
|
||||
|
||||
export interface PoolKeysPageResponse {
|
||||
total: number
|
||||
page: number
|
||||
|
||||
@@ -291,9 +291,6 @@
|
||||
>
|
||||
配额
|
||||
</TableHead>
|
||||
<TableHead class="w-[180px] font-semibold whitespace-nowrap">
|
||||
调度
|
||||
</TableHead>
|
||||
<TableHead class="w-24 font-semibold whitespace-nowrap">
|
||||
状态
|
||||
</TableHead>
|
||||
@@ -439,95 +436,6 @@
|
||||
class="text-xs text-muted-foreground"
|
||||
>-</span>
|
||||
</TableCell>
|
||||
<TableCell class="py-3">
|
||||
<div
|
||||
class="grid grid-rows-[16px_16px] gap-1 w-20"
|
||||
:title="getSchedulingDimensionTitle(key)"
|
||||
>
|
||||
<div class="h-4 flex items-center justify-between gap-1">
|
||||
<span
|
||||
class="text-[11px] font-normal leading-none tabular-nums"
|
||||
:class="isCandidateEligible(key) ? 'text-foreground' : 'text-destructive'"
|
||||
>
|
||||
{{ getSchedulingScore(key).toFixed(1) }}
|
||||
</span>
|
||||
<Popover
|
||||
:open="schedulingDetailDesktopPopoverOpenKeyId === key.key_id"
|
||||
@update:open="(v: boolean) => handleSchedulingDetailDesktopPopoverToggle(key.key_id, v)"
|
||||
>
|
||||
<PopoverTrigger as-child>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-4 w-4 items-center justify-center text-muted-foreground hover:text-foreground"
|
||||
title="查看计算详情"
|
||||
@click.stop
|
||||
>
|
||||
<CircleHelp class="w-3 h-3" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
class="w-72 p-3"
|
||||
side="bottom"
|
||||
align="start"
|
||||
>
|
||||
<div class="space-y-2">
|
||||
<div class="text-xs font-medium">
|
||||
调度分计算详情
|
||||
</div>
|
||||
<div class="max-h-56 overflow-y-auto space-y-2 pr-1">
|
||||
<div
|
||||
v-for="item in getSchedulingRuleEntries(key)"
|
||||
:key="`${key.key_id}-score-detail-${item.code}`"
|
||||
class="rounded-md border border-border/60 p-2"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-[11px] font-medium">
|
||||
{{ item.label }}
|
||||
</span>
|
||||
<Badge
|
||||
:variant="getSchedulingDimensionStatusVariant(item.status)"
|
||||
class="text-[10px]"
|
||||
>
|
||||
{{ getSchedulingDimensionStatusLabel(item.status) }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div
|
||||
v-if="item.weight != null || item.score != null"
|
||||
class="text-[10px] text-muted-foreground mt-1"
|
||||
>
|
||||
<span v-if="item.weight != null">权重 {{ item.weight }}</span>
|
||||
<span v-if="item.weight != null && item.score != null"> · </span>
|
||||
<span v-if="item.score != null">分值 {{ Math.round(item.score * 100) }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="item.detail"
|
||||
class="text-[10px] text-muted-foreground mt-1 break-all"
|
||||
>
|
||||
{{ item.detail }}
|
||||
</div>
|
||||
<div
|
||||
v-if="item.ttl_seconds && item.ttl_seconds > 0"
|
||||
class="text-[10px] text-muted-foreground mt-1"
|
||||
>
|
||||
剩余 {{ formatTTL(item.ttl_seconds) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<div class="h-4 flex items-center">
|
||||
<div class="w-full h-1.5 bg-border rounded-full overflow-hidden">
|
||||
<div
|
||||
class="h-full transition-all duration-300"
|
||||
:class="getSchedulingScoreBarColor(getSchedulingScore(key))"
|
||||
:style="{ width: `${Math.max(Math.min(getSchedulingScore(key), 100), 0)}%` }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="py-3">
|
||||
<Badge
|
||||
:variant="getSchedulingBadgeVariant(key)"
|
||||
@@ -896,94 +804,8 @@
|
||||
</div>
|
||||
<div
|
||||
class="mt-2.5 grid gap-2"
|
||||
:class="showAccountQuotaColumn ? 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-5' : 'grid-cols-2 sm:grid-cols-4'"
|
||||
:class="showAccountQuotaColumn ? 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-4' : 'grid-cols-2 sm:grid-cols-3'"
|
||||
>
|
||||
<div class="p-2 bg-muted/50 rounded-lg text-xs">
|
||||
<div class="text-muted-foreground mb-0.5">
|
||||
调度
|
||||
</div>
|
||||
<div class="w-24 flex items-center justify-between gap-1">
|
||||
<div
|
||||
class="font-normal tabular-nums text-[10px] leading-none"
|
||||
:class="isCandidateEligible(key) ? 'text-foreground' : 'text-destructive'"
|
||||
:title="getSchedulingDimensionTitle(key)"
|
||||
>
|
||||
{{ getSchedulingScore(key).toFixed(1) }}
|
||||
</div>
|
||||
<Popover
|
||||
:open="schedulingDetailMobilePopoverOpenKeyId === key.key_id"
|
||||
@update:open="(v: boolean) => handleSchedulingDetailMobilePopoverToggle(key.key_id, v)"
|
||||
>
|
||||
<PopoverTrigger as-child>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-4 w-4 items-center justify-center text-muted-foreground hover:text-foreground"
|
||||
title="查看计算详情"
|
||||
@click.stop
|
||||
>
|
||||
<CircleHelp class="w-3 h-3" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
class="w-72 p-3"
|
||||
side="bottom"
|
||||
align="start"
|
||||
>
|
||||
<div class="space-y-2">
|
||||
<div class="text-xs font-medium">
|
||||
调度分计算详情
|
||||
</div>
|
||||
<div class="max-h-56 overflow-y-auto space-y-2 pr-1">
|
||||
<div
|
||||
v-for="item in getSchedulingRuleEntries(key)"
|
||||
:key="`${key.key_id}-mobile-score-detail-${item.code}`"
|
||||
class="rounded-md border border-border/60 p-2"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-[11px] font-medium">
|
||||
{{ item.label }}
|
||||
</span>
|
||||
<Badge
|
||||
:variant="getSchedulingDimensionStatusVariant(item.status)"
|
||||
class="text-[10px]"
|
||||
>
|
||||
{{ getSchedulingDimensionStatusLabel(item.status) }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div
|
||||
v-if="item.weight != null || item.score != null"
|
||||
class="text-[10px] text-muted-foreground mt-1"
|
||||
>
|
||||
<span v-if="item.weight != null">权重 {{ item.weight }}</span>
|
||||
<span v-if="item.weight != null && item.score != null"> · </span>
|
||||
<span v-if="item.score != null">分值 {{ Math.round(item.score * 100) }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="item.detail"
|
||||
class="text-[10px] text-muted-foreground mt-1 break-all"
|
||||
>
|
||||
{{ item.detail }}
|
||||
</div>
|
||||
<div
|
||||
v-if="item.ttl_seconds && item.ttl_seconds > 0"
|
||||
class="text-[10px] text-muted-foreground mt-1"
|
||||
>
|
||||
剩余 {{ formatTTL(item.ttl_seconds) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<div class="w-24 h-1.5 bg-border rounded-full overflow-hidden mt-1">
|
||||
<div
|
||||
class="h-full transition-all duration-300"
|
||||
:class="getSchedulingScoreBarColor(getSchedulingScore(key))"
|
||||
:style="{ width: `${Math.max(Math.min(getSchedulingScore(key), 100), 0)}%` }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-2 bg-muted/50 rounded-lg text-xs">
|
||||
<div class="text-muted-foreground mb-0.5">
|
||||
最后使用
|
||||
@@ -1162,7 +984,6 @@ import {
|
||||
Globe,
|
||||
SquarePen,
|
||||
Trash2,
|
||||
CircleHelp,
|
||||
Ban,
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
@@ -1392,8 +1213,6 @@ async function selectProvider(id: string) {
|
||||
oauthKeyEditDialogOpen.value = false
|
||||
proxyDesktopPopoverOpenKeyId.value = null
|
||||
proxyMobilePopoverOpenKeyId.value = null
|
||||
schedulingDetailDesktopPopoverOpenKeyId.value = null
|
||||
schedulingDetailMobilePopoverOpenKeyId.value = null
|
||||
suppressFiltersWatch = true
|
||||
currentPage.value = 1
|
||||
searchQuery.value = ''
|
||||
@@ -1438,8 +1257,6 @@ const recoveringHealthKeyId = ref<string | null>(null)
|
||||
const savingProxyKeyId = ref<string | null>(null)
|
||||
const proxyDesktopPopoverOpenKeyId = ref<string | null>(null)
|
||||
const proxyMobilePopoverOpenKeyId = ref<string | null>(null)
|
||||
const schedulingDetailDesktopPopoverOpenKeyId = ref<string | null>(null)
|
||||
const schedulingDetailMobilePopoverOpenKeyId = ref<string | null>(null)
|
||||
const deletingKeyId = ref<string | null>(null)
|
||||
const togglingKeyId = ref<string | null>(null)
|
||||
|
||||
@@ -1753,20 +1570,6 @@ function handleProxyMobilePopoverToggle(keyId: string, open: boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleSchedulingDetailDesktopPopoverToggle(keyId: string, open: boolean) {
|
||||
schedulingDetailDesktopPopoverOpenKeyId.value = open ? keyId : null
|
||||
if (open) {
|
||||
schedulingDetailMobilePopoverOpenKeyId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function handleSchedulingDetailMobilePopoverToggle(keyId: string, open: boolean) {
|
||||
schedulingDetailMobilePopoverOpenKeyId.value = open ? keyId : null
|
||||
if (open) {
|
||||
schedulingDetailDesktopPopoverOpenKeyId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function setKeyProxy(key: PoolKeyDetail, nodeId: string) {
|
||||
savingProxyKeyId.value = key.key_id
|
||||
try {
|
||||
@@ -2050,14 +1853,6 @@ function getSchedulingTitle(key: PoolKeyDetail): string {
|
||||
const accountAlertTitle = getAccountAlertTitle(key)
|
||||
if (accountAlertTitle) return accountAlertTitle
|
||||
|
||||
if (key.scheduling_dimensions && key.scheduling_dimensions.length > 0) {
|
||||
return key.scheduling_dimensions.map((item) => {
|
||||
const ttl = item.ttl_seconds && item.ttl_seconds > 0 ? ` (${formatTTL(item.ttl_seconds)})` : ''
|
||||
const detail = item.detail ? ` - ${item.detail}` : ''
|
||||
return `${item.label}: ${item.status}${ttl}${detail}`
|
||||
}).join('\n')
|
||||
}
|
||||
|
||||
const reasons = key.scheduling_reasons ?? []
|
||||
if (reasons.length > 0) {
|
||||
return reasons.map((item) => {
|
||||
@@ -2074,89 +1869,6 @@ function getSchedulingTitle(key: PoolKeyDetail): string {
|
||||
return getSchedulingBadgeLabel(key)
|
||||
}
|
||||
|
||||
function getSchedulingScore(key: PoolKeyDetail): number {
|
||||
const raw = key.scheduling_score
|
||||
if (typeof raw === 'number' && Number.isFinite(raw)) {
|
||||
return Math.max(Math.min(raw, 100), 0)
|
||||
}
|
||||
const status = getSchedulingStatus(key)
|
||||
if (status === 'blocked') return 35
|
||||
if (status === 'degraded') return 68
|
||||
return 100
|
||||
}
|
||||
|
||||
function getSchedulingScoreBarColor(score: number): string {
|
||||
if (score >= 80) return 'bg-green-500 dark:bg-green-400'
|
||||
if (score >= 50) return 'bg-yellow-500 dark:bg-yellow-400'
|
||||
return 'bg-red-500 dark:bg-red-400'
|
||||
}
|
||||
|
||||
function isCandidateEligible(key: PoolKeyDetail): boolean {
|
||||
if (typeof key.candidate_eligible === 'boolean') return key.candidate_eligible
|
||||
return getSchedulingStatus(key) !== 'blocked'
|
||||
}
|
||||
|
||||
function getSchedulingDimensionTitle(key: PoolKeyDetail): string {
|
||||
const dimensions = key.scheduling_dimensions ?? []
|
||||
if (!dimensions.length) return ''
|
||||
return dimensions.map((item) => {
|
||||
const ttl = item.ttl_seconds && item.ttl_seconds > 0 ? ` (${formatTTL(item.ttl_seconds)})` : ''
|
||||
const detail = item.detail ? ` - ${item.detail}` : ''
|
||||
return `${item.label}: ${item.status}${ttl}${detail}`
|
||||
}).join('\n')
|
||||
}
|
||||
|
||||
function getSchedulingRuleEntries(
|
||||
key: PoolKeyDetail,
|
||||
): NonNullable<PoolKeyDetail['scheduling_dimensions']> {
|
||||
const dimensions = key.scheduling_dimensions ?? []
|
||||
if (dimensions.length > 0) {
|
||||
return dimensions
|
||||
}
|
||||
|
||||
const reasons = key.scheduling_reasons ?? []
|
||||
if (reasons.length > 0) {
|
||||
return reasons.map((reason) => ({
|
||||
code: reason.code,
|
||||
label: reason.label,
|
||||
status: reason.blocking ? 'blocked' : 'degraded',
|
||||
blocking: reason.blocking,
|
||||
source: reason.source,
|
||||
weight: 0,
|
||||
score: 0,
|
||||
ttl_seconds: reason.ttl_seconds ?? null,
|
||||
detail: reason.detail ?? null,
|
||||
}))
|
||||
}
|
||||
|
||||
const fallbackStatus = getSchedulingStatus(key)
|
||||
return [
|
||||
{
|
||||
code: key.scheduling_reason ?? 'available',
|
||||
label: getSchedulingBadgeLabel(key),
|
||||
status: fallbackStatus === 'available' ? 'ok' : fallbackStatus,
|
||||
blocking: fallbackStatus === 'blocked',
|
||||
source: 'pool',
|
||||
weight: 0,
|
||||
score: 0,
|
||||
ttl_seconds: null,
|
||||
detail: null,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function getSchedulingDimensionStatusLabel(status: string): string {
|
||||
if (status === 'blocked') return '阻塞'
|
||||
if (status === 'degraded') return '降级'
|
||||
return '可用'
|
||||
}
|
||||
|
||||
function getSchedulingDimensionStatusVariant(status: string): PoolStatusVariant {
|
||||
if (status === 'blocked') return 'destructive'
|
||||
if (status === 'degraded') return 'warning'
|
||||
return 'default'
|
||||
}
|
||||
|
||||
function formatTTL(seconds: number): string {
|
||||
if (seconds <= 0) return ''
|
||||
const m = Math.floor(seconds / 60)
|
||||
|
||||
Reference in New Issue
Block a user