mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-08 10:27:46 +08:00
213 lines
5.6 KiB
Vue
213 lines
5.6 KiB
Vue
<template>
|
|
<div class="flex items-center gap-1 shrink-0">
|
|
<Badge
|
|
v-if="apiKey.circuit_breaker_open"
|
|
variant="destructive"
|
|
class="text-[10px] px-1.5 py-0 shrink-0"
|
|
:title="circuitBreakerTitle"
|
|
data-testid="provider-key-circuit-badge"
|
|
>
|
|
{{ legacyT('熔断') }}{{ circuitProbeCountdown }}
|
|
</Badge>
|
|
|
|
<div
|
|
v-if="apiKey.health_score !== undefined"
|
|
class="flex items-center gap-1 mr-1"
|
|
data-testid="provider-key-health"
|
|
>
|
|
<div class="w-10 h-1.5 bg-border rounded-full overflow-hidden">
|
|
<div
|
|
class="h-full transition-all duration-300"
|
|
:class="healthScoreBarClass"
|
|
:style="{ width: `${healthScorePercent}%` }"
|
|
/>
|
|
</div>
|
|
<span
|
|
class="text-[10px] font-medium tabular-nums"
|
|
:class="healthScoreTextClass"
|
|
>
|
|
{{ healthScorePercent.toFixed(0) }}%
|
|
</span>
|
|
</div>
|
|
|
|
<Button
|
|
v-if="recoverable"
|
|
variant="ghost"
|
|
size="icon"
|
|
class="h-7 w-7 text-green-600"
|
|
:title="recoverTitle"
|
|
@click="$emit('recover')"
|
|
>
|
|
<RefreshCw class="w-3.5 h-3.5" />
|
|
</Button>
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
class="h-7 w-7"
|
|
:title="legacyT('模型权限')"
|
|
@click="$emit('permissions')"
|
|
>
|
|
<Shield class="w-3.5 h-3.5" />
|
|
</Button>
|
|
|
|
<Popover
|
|
:open="proxyPopoverOpen"
|
|
@update:open="$emit('update:proxyPopoverOpen', $event)"
|
|
>
|
|
<PopoverTrigger as-child>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
class="h-7 w-7"
|
|
:class="apiKey.proxy?.node_id ? 'text-blue-500' : ''"
|
|
:disabled="savingProxy"
|
|
:title="apiKey.proxy?.node_id ? `${legacyT('代理')}: ${proxyNodeName}` : legacyT('设置代理节点')"
|
|
@click.stop
|
|
>
|
|
<Globe class="w-3.5 h-3.5" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent
|
|
class="w-72 p-3"
|
|
side="bottom"
|
|
align="end"
|
|
>
|
|
<div class="space-y-2">
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-xs font-medium">{{ legacyT('代理节点') }}</span>
|
|
<Button
|
|
v-if="apiKey.proxy?.node_id"
|
|
variant="ghost"
|
|
size="sm"
|
|
class="h-6 px-2 text-[10px] text-muted-foreground"
|
|
:disabled="savingProxy"
|
|
@click="$emit('clearProxy')"
|
|
>
|
|
{{ legacyT('清除') }}
|
|
</Button>
|
|
</div>
|
|
<ProxyNodeSelect
|
|
:model-value="apiKey.proxy?.node_id || ''"
|
|
trigger-class="h-8"
|
|
@update:model-value="$emit('setProxy', $event)"
|
|
/>
|
|
<p class="text-[10px] text-muted-foreground">
|
|
{{ legacyT(apiKey.proxy?.node_id ? '当前使用独立代理' : '未设置,使用提供商级别代理') }}
|
|
</p>
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
class="h-7 w-7"
|
|
:title="legacyT('编辑密钥')"
|
|
@click="$emit('edit')"
|
|
>
|
|
<Edit class="w-3.5 h-3.5" />
|
|
</Button>
|
|
|
|
<Button
|
|
v-if="providerType === 'antigravity'"
|
|
variant="ghost"
|
|
size="icon"
|
|
class="h-7 w-7"
|
|
:title="legacyT('配额详情')"
|
|
@click="$emit('openAntigravityQuota')"
|
|
>
|
|
<BarChart3 class="w-3.5 h-3.5" />
|
|
</Button>
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
class="h-7 w-7"
|
|
:disabled="toggling"
|
|
:title="legacyT(apiKey.is_active ? '点击停用' : '点击启用')"
|
|
:aria-label="legacyT(apiKey.is_active ? '点击停用' : '点击启用')"
|
|
data-testid="provider-key-toggle-active"
|
|
@click="$emit('toggleActive')"
|
|
>
|
|
<Power class="w-3.5 h-3.5" />
|
|
</Button>
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
class="h-7 w-7"
|
|
:title="legacyT('删除密钥')"
|
|
@click="$emit('delete')"
|
|
>
|
|
<Trash2 class="w-3.5 h-3.5" />
|
|
</Button>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
import {
|
|
BarChart3,
|
|
Edit,
|
|
Globe,
|
|
Power,
|
|
RefreshCw,
|
|
Shield,
|
|
Trash2,
|
|
} from 'lucide-vue-next'
|
|
import Button from '@/components/ui/button.vue'
|
|
import Badge from '@/components/ui/badge.vue'
|
|
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui'
|
|
import { useI18n } from '@/i18n'
|
|
import type { EndpointAPIKey } from '@/api/endpoints'
|
|
import ProxyNodeSelect from '@/features/providers/components/ProxyNodeSelect.vue'
|
|
|
|
const props = withDefaults(defineProps<{
|
|
apiKey: EndpointAPIKey
|
|
providerType?: string | null
|
|
recoverable?: boolean
|
|
recoverTitle?: string
|
|
circuitBreakerTitle?: string
|
|
circuitProbeCountdown?: string
|
|
healthScoreBarClass?: string
|
|
healthScoreTextClass?: string
|
|
proxyPopoverOpen?: boolean
|
|
proxyNodeName?: string
|
|
savingProxy?: boolean
|
|
toggling?: boolean
|
|
}>(), {
|
|
providerType: null,
|
|
recoverable: false,
|
|
recoverTitle: '',
|
|
circuitBreakerTitle: '',
|
|
circuitProbeCountdown: '',
|
|
healthScoreBarClass: '',
|
|
healthScoreTextClass: '',
|
|
proxyPopoverOpen: false,
|
|
proxyNodeName: '',
|
|
savingProxy: false,
|
|
toggling: false,
|
|
})
|
|
|
|
defineEmits<{
|
|
(e: 'recover'): void
|
|
(e: 'permissions'): void
|
|
(e: 'update:proxyPopoverOpen', value: boolean): void
|
|
(e: 'clearProxy'): void
|
|
(e: 'setProxy', nodeId: string): void
|
|
(e: 'edit'): void
|
|
(e: 'openAntigravityQuota'): void
|
|
(e: 'toggleActive'): void
|
|
(e: 'delete'): void
|
|
}>()
|
|
|
|
const { legacyT } = useI18n()
|
|
|
|
const healthScorePercent = computed(() => {
|
|
const score = Number(props.apiKey.health_score ?? 0)
|
|
if (!Number.isFinite(score)) return 0
|
|
return Math.min(Math.max(score * 100, 0), 100)
|
|
})
|
|
</script>
|