mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-12 04:09:48 +08:00
refactor(frontend): extract provider key identity block
This commit is contained in:
@@ -112,137 +112,30 @@
|
|||||||
<div class="cursor-grab active:cursor-grabbing text-muted-foreground/30 group-hover/item:text-muted-foreground transition-colors shrink-0">
|
<div class="cursor-grab active:cursor-grabbing text-muted-foreground/30 group-hover/item:text-muted-foreground transition-colors shrink-0">
|
||||||
<GripVertical class="w-4 h-4" />
|
<GripVertical class="w-4 h-4" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col min-w-0">
|
<ProviderKeyIdentityBlock
|
||||||
<div class="flex items-center gap-1.5">
|
:api-key="key"
|
||||||
<span
|
:masked-secret-label="getProviderMaskedSecretLabel(key, provider.provider_type)"
|
||||||
class="text-sm font-medium truncate"
|
:oauth-plan-label="key.oauth_plan_type ? formatOAuthPlanType(key.oauth_plan_type) : null"
|
||||||
:class="key.name ? 'cursor-pointer hover:text-primary transition-colors' : ''"
|
:oauth-plan-class="key.oauth_plan_type ? getOAuthPlanTypeClass(key.oauth_plan_type) : ''"
|
||||||
:title="key.name ? legacyT('点击复制') : ''"
|
:oauth-org-badge="getOAuthOrgBadge(key)"
|
||||||
@click.stop="key.name && copyToClipboard(key.name)"
|
:kiro-subscription-label="shouldShowKiroSubscriptionBadge(key) ? getKiroSubscriptionBadgeLabel(key) : null"
|
||||||
>{{ key.name || legacyT('未命名密钥') }}</span>
|
:kiro-subscription-class="shouldShowKiroSubscriptionBadge(key) ? getOAuthPlanTypeClass(getKiroSubscriptionBadgeLabel(key)) : ''"
|
||||||
<!-- OAuth 订阅类型标签 (Codex) -->
|
:can-export-credential="canExportOAuthCredential(key)"
|
||||||
<Badge
|
:show-o-auth-refresh-control="shouldShowOAuthRefreshControl(key, provider.provider_type)"
|
||||||
v-if="key.oauth_plan_type"
|
:account-level-block="isAccountLevelBlock(key)"
|
||||||
variant="outline"
|
:oauth-status="getKeyOAuthExpires(key)"
|
||||||
class="text-[10px] px-1.5 py-0 shrink-0"
|
:oauth-status-title="getOAuthStatusTitle(key)"
|
||||||
:class="getOAuthPlanTypeClass(key.oauth_plan_type)"
|
:oauth-refresh-button-title="getOAuthRefreshButtonTitle(key)"
|
||||||
>
|
:can-refresh-credential="canRefreshOAuthCredential(key)"
|
||||||
{{ formatOAuthPlanType(key.oauth_plan_type) }}
|
:clearing-o-auth-invalid="clearingOAuthInvalidKeyId === key.id"
|
||||||
</Badge>
|
:refreshing-o-auth="refreshingOAuthKeyId === key.id"
|
||||||
<Badge
|
:antigravity-inactive="provider.provider_type === 'antigravity' && key.is_active && isOAuthManagedCredential(key) && !hasAntigravityQuotaDisplayData(key)"
|
||||||
v-if="getOAuthOrgBadge(key)"
|
@copy-name="copyToClipboard"
|
||||||
variant="secondary"
|
@download-credential="downloadRefreshToken(key)"
|
||||||
class="text-[9px] px-1 py-0 h-4 shrink-0"
|
@copy-full-key="copyFullKey(key)"
|
||||||
:title="getOAuthOrgBadge(key)?.title"
|
@clear-o-auth-invalid="handleClearOAuthInvalid(key)"
|
||||||
>
|
@refresh-o-auth="handleRefreshOAuth(key)"
|
||||||
{{ getOAuthOrgBadge(key)?.label }}
|
/>
|
||||||
</Badge>
|
|
||||||
<!-- Kiro 订阅类型标签 -->
|
|
||||||
<Badge
|
|
||||||
v-if="shouldShowKiroSubscriptionBadge(key)"
|
|
||||||
variant="outline"
|
|
||||||
class="text-[10px] px-1.5 py-0 shrink-0"
|
|
||||||
:class="getOAuthPlanTypeClass(getKiroSubscriptionBadgeLabel(key))"
|
|
||||||
>
|
|
||||||
{{ getKiroSubscriptionBadgeLabel(key) }}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<span class="text-[11px] font-mono text-muted-foreground">
|
|
||||||
{{ getProviderMaskedSecretLabel(key, provider.provider_type) }}
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
v-if="canExportOAuthCredential(key)"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="h-4 w-4 shrink-0"
|
|
||||||
:title="legacyT('下载 OAuth 授权文件')"
|
|
||||||
@click.stop="downloadRefreshToken(key)"
|
|
||||||
>
|
|
||||||
<Download class="w-2.5 h-2.5" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
v-else
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="h-4 w-4 shrink-0"
|
|
||||||
:title="legacyT('复制密钥')"
|
|
||||||
@click.stop="copyFullKey(key)"
|
|
||||||
>
|
|
||||||
<Copy class="w-2.5 h-2.5" />
|
|
||||||
</Button>
|
|
||||||
<!-- OAuth 状态(失效/过期/倒计时)和刷新按钮 -->
|
|
||||||
<template v-if="shouldShowOAuthRefreshControl(key, provider.provider_type)">
|
|
||||||
<!-- 账号级别异常:醒目提示 + 清除按钮 -->
|
|
||||||
<template v-if="isAccountLevelBlock(key)">
|
|
||||||
<Badge
|
|
||||||
variant="destructive"
|
|
||||||
class="text-[10px] px-1.5 py-0 shrink-0 gap-0.5"
|
|
||||||
:title="getOAuthStatusTitle(key)"
|
|
||||||
>
|
|
||||||
<ShieldX class="w-2.5 h-2.5" />
|
|
||||||
{{ legacyT('账号异常') }}
|
|
||||||
</Badge>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="h-4 w-4 shrink-0 text-destructive hover:text-destructive"
|
|
||||||
:disabled="clearingOAuthInvalidKeyId === key.id"
|
|
||||||
:title="legacyT('清除异常标记(确认账号已完成验证后使用)')"
|
|
||||||
@click.stop="handleClearOAuthInvalid(key)"
|
|
||||||
>
|
|
||||||
<RefreshCw
|
|
||||||
class="w-2.5 h-2.5"
|
|
||||||
:class="{ 'animate-spin': clearingOAuthInvalidKeyId === key.id }"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</template>
|
|
||||||
<!-- 普通 OAuth 状态 -->
|
|
||||||
<template v-else>
|
|
||||||
<span
|
|
||||||
class="text-[10px]"
|
|
||||||
:class="{
|
|
||||||
'text-destructive': getKeyOAuthExpires(key)?.isInvalid || getKeyOAuthExpires(key)?.isExpired,
|
|
||||||
'text-warning': getKeyOAuthExpires(key)?.isExpiringSoon && !getKeyOAuthExpires(key)?.isExpired && !getKeyOAuthExpires(key)?.isInvalid,
|
|
||||||
'text-muted-foreground': !getKeyOAuthExpires(key)?.isExpired && !getKeyOAuthExpires(key)?.isExpiringSoon && !getKeyOAuthExpires(key)?.isInvalid
|
|
||||||
}"
|
|
||||||
:title="getOAuthStatusTitle(key)"
|
|
||||||
>
|
|
||||||
{{ getKeyOAuthExpires(key)?.text }}
|
|
||||||
</span>
|
|
||||||
<Badge
|
|
||||||
v-if="key.oauth_temporary"
|
|
||||||
variant="outline"
|
|
||||||
class="text-[10px] px-1.5 py-0 shrink-0"
|
|
||||||
:title="legacyT('仅通过 Access Token 导入,无法自动刷新,到期后需要重新导入')"
|
|
||||||
>
|
|
||||||
{{ legacyT('临时') }}
|
|
||||||
</Badge>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="h-4 w-4 shrink-0"
|
|
||||||
:disabled="refreshingOAuthKeyId === key.id || !canRefreshOAuthCredential(key)"
|
|
||||||
:title="getOAuthRefreshButtonTitle(key)"
|
|
||||||
@click.stop="handleRefreshOAuth(key)"
|
|
||||||
>
|
|
||||||
<RefreshCw
|
|
||||||
class="w-2.5 h-2.5"
|
|
||||||
:class="{ 'animate-spin': refreshingOAuthKeyId === key.id }"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
<!-- Antigravity 账号未激活提示 -->
|
|
||||||
<span
|
|
||||||
v-if="provider.provider_type === 'antigravity' && key.is_active && isOAuthManagedCredential(key) && !hasAntigravityQuotaDisplayData(key)"
|
|
||||||
class="text-[10px] text-orange-500 dark:text-orange-400"
|
|
||||||
:title="legacyT('该账号尚未完成 Gemini Code Assist 激活,无法获取配额和使用模型')"
|
|
||||||
>
|
|
||||||
{{ legacyT('账号未激活') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<ProviderKeyActionCluster
|
<ProviderKeyActionCluster
|
||||||
:api-key="key"
|
:api-key="key"
|
||||||
@@ -967,17 +860,13 @@ import {
|
|||||||
Plus,
|
Plus,
|
||||||
Key,
|
Key,
|
||||||
Loader2,
|
Loader2,
|
||||||
RefreshCw,
|
|
||||||
GripVertical,
|
GripVertical,
|
||||||
Copy,
|
|
||||||
Download,
|
|
||||||
ShieldX,
|
ShieldX,
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import { parseApiError } from '@/utils/errorParser'
|
import { parseApiError } from '@/utils/errorParser'
|
||||||
import { useEscapeKey } from '@/composables/useEscapeKey'
|
import { useEscapeKey } from '@/composables/useEscapeKey'
|
||||||
import { useI18n } from '@/i18n'
|
import { useI18n } from '@/i18n'
|
||||||
import Button from '@/components/ui/button.vue'
|
import Button from '@/components/ui/button.vue'
|
||||||
import Badge from '@/components/ui/badge.vue'
|
|
||||||
import Card from '@/components/ui/card.vue'
|
import Card from '@/components/ui/card.vue'
|
||||||
import { useToast } from '@/composables/useToast'
|
import { useToast } from '@/composables/useToast'
|
||||||
import { useConfirm } from '@/composables/useConfirm'
|
import { useConfirm } from '@/composables/useConfirm'
|
||||||
@@ -1009,6 +898,7 @@ import AntigravityQuotaDialog from '@/features/providers/components/AntigravityQ
|
|||||||
import FailoverRulesDialog from '@/features/providers/components/FailoverRulesDialog.vue'
|
import FailoverRulesDialog from '@/features/providers/components/FailoverRulesDialog.vue'
|
||||||
import ProviderDetailHeader from '@/features/providers/components/ProviderDetailHeader.vue'
|
import ProviderDetailHeader from '@/features/providers/components/ProviderDetailHeader.vue'
|
||||||
import ProviderKeyActionCluster from '@/features/providers/components/ProviderKeyActionCluster.vue'
|
import ProviderKeyActionCluster from '@/features/providers/components/ProviderKeyActionCluster.vue'
|
||||||
|
import ProviderKeyIdentityBlock from '@/features/providers/components/ProviderKeyIdentityBlock.vue'
|
||||||
import ProviderMonthlyQuotaCard from '@/features/providers/components/ProviderMonthlyQuotaCard.vue'
|
import ProviderMonthlyQuotaCard from '@/features/providers/components/ProviderMonthlyQuotaCard.vue'
|
||||||
import ProviderQuotaProgressRow from '@/features/providers/components/ProviderQuotaProgressRow.vue'
|
import ProviderQuotaProgressRow from '@/features/providers/components/ProviderQuotaProgressRow.vue'
|
||||||
import ProviderQuotaSectionHeader from '@/features/providers/components/ProviderQuotaSectionHeader.vue'
|
import ProviderQuotaSectionHeader from '@/features/providers/components/ProviderQuotaSectionHeader.vue'
|
||||||
|
|||||||
@@ -0,0 +1,211 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col min-w-0">
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
<span
|
||||||
|
class="text-sm font-medium truncate"
|
||||||
|
:class="apiKey.name ? 'cursor-pointer hover:text-primary transition-colors' : ''"
|
||||||
|
:title="apiKey.name ? legacyT('点击复制') : ''"
|
||||||
|
data-testid="provider-key-name"
|
||||||
|
@click.stop="apiKey.name && $emit('copyName', apiKey.name)"
|
||||||
|
>
|
||||||
|
{{ apiKey.name || legacyT('未命名密钥') }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<Badge
|
||||||
|
v-if="oauthPlanLabel"
|
||||||
|
variant="outline"
|
||||||
|
class="text-[10px] px-1.5 py-0 shrink-0"
|
||||||
|
:class="oauthPlanClass"
|
||||||
|
data-testid="provider-key-oauth-plan"
|
||||||
|
>
|
||||||
|
{{ oauthPlanLabel }}
|
||||||
|
</Badge>
|
||||||
|
|
||||||
|
<Badge
|
||||||
|
v-if="oauthOrgBadge"
|
||||||
|
variant="secondary"
|
||||||
|
class="text-[9px] px-1 py-0 h-4 shrink-0"
|
||||||
|
:title="oauthOrgBadge.title"
|
||||||
|
data-testid="provider-key-oauth-org"
|
||||||
|
>
|
||||||
|
{{ oauthOrgBadge.label }}
|
||||||
|
</Badge>
|
||||||
|
|
||||||
|
<Badge
|
||||||
|
v-if="kiroSubscriptionLabel"
|
||||||
|
variant="outline"
|
||||||
|
class="text-[10px] px-1.5 py-0 shrink-0"
|
||||||
|
:class="kiroSubscriptionClass"
|
||||||
|
data-testid="provider-key-kiro-plan"
|
||||||
|
>
|
||||||
|
{{ kiroSubscriptionLabel }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<span class="text-[11px] font-mono text-muted-foreground">
|
||||||
|
{{ maskedSecretLabel }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
v-if="canExportCredential"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-4 w-4 shrink-0"
|
||||||
|
:title="legacyT('下载 OAuth 授权文件')"
|
||||||
|
@click.stop="$emit('downloadCredential')"
|
||||||
|
>
|
||||||
|
<Download class="w-2.5 h-2.5" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
v-else
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-4 w-4 shrink-0"
|
||||||
|
:title="legacyT('复制密钥')"
|
||||||
|
@click.stop="$emit('copyFullKey')"
|
||||||
|
>
|
||||||
|
<Copy class="w-2.5 h-2.5" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<template v-if="showOAuthRefreshControl">
|
||||||
|
<template v-if="accountLevelBlock">
|
||||||
|
<Badge
|
||||||
|
variant="destructive"
|
||||||
|
class="text-[10px] px-1.5 py-0 shrink-0 gap-0.5"
|
||||||
|
:title="oauthStatusTitle"
|
||||||
|
data-testid="provider-key-account-block"
|
||||||
|
>
|
||||||
|
<ShieldX class="w-2.5 h-2.5" />
|
||||||
|
{{ legacyT('账号异常') }}
|
||||||
|
</Badge>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-4 w-4 shrink-0 text-destructive hover:text-destructive"
|
||||||
|
:disabled="clearingOAuthInvalid"
|
||||||
|
:title="legacyT('清除异常标记(确认账号已完成验证后使用)')"
|
||||||
|
@click.stop="$emit('clearOAuthInvalid')"
|
||||||
|
>
|
||||||
|
<RefreshCw
|
||||||
|
class="w-2.5 h-2.5"
|
||||||
|
:class="{ 'animate-spin': clearingOAuthInvalid }"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<span
|
||||||
|
class="text-[10px]"
|
||||||
|
:class="oauthStatusClass"
|
||||||
|
:title="oauthStatusTitle"
|
||||||
|
data-testid="provider-key-oauth-status"
|
||||||
|
>
|
||||||
|
{{ oauthStatus?.text }}
|
||||||
|
</span>
|
||||||
|
<Badge
|
||||||
|
v-if="apiKey.oauth_temporary"
|
||||||
|
variant="outline"
|
||||||
|
class="text-[10px] px-1.5 py-0 shrink-0"
|
||||||
|
:title="legacyT('仅通过 Access Token 导入,无法自动刷新,到期后需要重新导入')"
|
||||||
|
data-testid="provider-key-temporary"
|
||||||
|
>
|
||||||
|
{{ legacyT('临时') }}
|
||||||
|
</Badge>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-4 w-4 shrink-0"
|
||||||
|
:disabled="refreshingOAuth || !canRefreshCredential"
|
||||||
|
:title="oauthRefreshButtonTitle"
|
||||||
|
@click.stop="$emit('refreshOAuth')"
|
||||||
|
>
|
||||||
|
<RefreshCw
|
||||||
|
class="w-2.5 h-2.5"
|
||||||
|
:class="{ 'animate-spin': refreshingOAuth }"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="antigravityInactive"
|
||||||
|
class="text-[10px] text-orange-500 dark:text-orange-400"
|
||||||
|
:title="legacyT('该账号尚未完成 Gemini Code Assist 激活,无法获取配额和使用模型')"
|
||||||
|
data-testid="provider-key-antigravity-inactive"
|
||||||
|
>
|
||||||
|
{{ legacyT('账号未激活') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { Copy, Download, RefreshCw, ShieldX } from 'lucide-vue-next'
|
||||||
|
import Button from '@/components/ui/button.vue'
|
||||||
|
import Badge from '@/components/ui/badge.vue'
|
||||||
|
import { useI18n } from '@/i18n'
|
||||||
|
import type { EndpointAPIKey } from '@/api/endpoints'
|
||||||
|
import type { OAuthStatusInfo } from '@/composables/useCountdownTimer'
|
||||||
|
|
||||||
|
interface OAuthOrgBadgeDisplay {
|
||||||
|
label: string
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
apiKey: EndpointAPIKey
|
||||||
|
maskedSecretLabel: string
|
||||||
|
oauthPlanLabel?: string | null
|
||||||
|
oauthPlanClass?: string
|
||||||
|
oauthOrgBadge?: OAuthOrgBadgeDisplay | null
|
||||||
|
kiroSubscriptionLabel?: string | null
|
||||||
|
kiroSubscriptionClass?: string
|
||||||
|
canExportCredential?: boolean
|
||||||
|
showOAuthRefreshControl?: boolean
|
||||||
|
accountLevelBlock?: boolean
|
||||||
|
oauthStatus?: OAuthStatusInfo | null
|
||||||
|
oauthStatusTitle?: string
|
||||||
|
oauthRefreshButtonTitle?: string
|
||||||
|
canRefreshCredential?: boolean
|
||||||
|
clearingOAuthInvalid?: boolean
|
||||||
|
refreshingOAuth?: boolean
|
||||||
|
antigravityInactive?: boolean
|
||||||
|
}>(), {
|
||||||
|
oauthPlanLabel: null,
|
||||||
|
oauthPlanClass: '',
|
||||||
|
oauthOrgBadge: null,
|
||||||
|
kiroSubscriptionLabel: null,
|
||||||
|
kiroSubscriptionClass: '',
|
||||||
|
canExportCredential: false,
|
||||||
|
showOAuthRefreshControl: false,
|
||||||
|
accountLevelBlock: false,
|
||||||
|
oauthStatus: null,
|
||||||
|
oauthStatusTitle: '',
|
||||||
|
oauthRefreshButtonTitle: '',
|
||||||
|
canRefreshCredential: false,
|
||||||
|
clearingOAuthInvalid: false,
|
||||||
|
refreshingOAuth: false,
|
||||||
|
antigravityInactive: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
(e: 'copyName', name: string): void
|
||||||
|
(e: 'downloadCredential'): void
|
||||||
|
(e: 'copyFullKey'): void
|
||||||
|
(e: 'clearOAuthInvalid'): void
|
||||||
|
(e: 'refreshOAuth'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { legacyT } = useI18n()
|
||||||
|
|
||||||
|
const oauthStatusClass = computed(() => {
|
||||||
|
const status = props.oauthStatus
|
||||||
|
return {
|
||||||
|
'text-destructive': status?.isInvalid || status?.isExpired,
|
||||||
|
'text-warning': status?.isExpiringSoon && !status?.isExpired && !status?.isInvalid,
|
||||||
|
'text-muted-foreground': !status?.isExpired && !status?.isExpiringSoon && !status?.isInvalid,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
+143
@@ -0,0 +1,143 @@
|
|||||||
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
import { createApp, defineComponent, h } from 'vue'
|
||||||
|
|
||||||
|
import ProviderKeyIdentityBlock from '@/features/providers/components/ProviderKeyIdentityBlock.vue'
|
||||||
|
import type { EndpointAPIKey } from '@/api/endpoints'
|
||||||
|
import { createI18n } from '@/i18n'
|
||||||
|
|
||||||
|
vi.mock('lucide-vue-next', async () => {
|
||||||
|
const { defineComponent, h } = await import('vue')
|
||||||
|
const Icon = defineComponent({
|
||||||
|
name: 'IconStub',
|
||||||
|
setup() {
|
||||||
|
return () => h('span')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
Copy: Icon,
|
||||||
|
Download: Icon,
|
||||||
|
RefreshCw: Icon,
|
||||||
|
ShieldX: Icon,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function createProviderKey(overrides: Partial<EndpointAPIKey> = {}): EndpointAPIKey {
|
||||||
|
return {
|
||||||
|
id: 'provider-key-1',
|
||||||
|
provider_id: 'provider-1',
|
||||||
|
api_formats: ['openai:chat'],
|
||||||
|
api_key_masked: 'sk-***',
|
||||||
|
auth_type: 'oauth',
|
||||||
|
name: 'Primary key',
|
||||||
|
internal_priority: 10,
|
||||||
|
cache_ttl_minutes: 0,
|
||||||
|
max_probe_interval_minutes: 5,
|
||||||
|
health_score: 1,
|
||||||
|
consecutive_failures: 0,
|
||||||
|
request_count: 0,
|
||||||
|
success_count: 0,
|
||||||
|
error_count: 0,
|
||||||
|
success_rate: 1,
|
||||||
|
avg_response_time_ms: 0,
|
||||||
|
is_active: true,
|
||||||
|
created_at: '2026-01-01T00:00:00Z',
|
||||||
|
updated_at: '2026-01-01T00:00:00Z',
|
||||||
|
...overrides,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mount(props: Record<string, unknown>) {
|
||||||
|
const root = document.createElement('div')
|
||||||
|
document.body.appendChild(root)
|
||||||
|
const app = createApp(defineComponent({
|
||||||
|
setup() {
|
||||||
|
return () => h(ProviderKeyIdentityBlock, props)
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
app.use(createI18n())
|
||||||
|
app.mount(root)
|
||||||
|
|
||||||
|
return {
|
||||||
|
root,
|
||||||
|
unmount: () => {
|
||||||
|
app.unmount()
|
||||||
|
root.remove()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ProviderKeyIdentityBlock', () => {
|
||||||
|
it('renders identity badges and emits copy/download/refresh actions', () => {
|
||||||
|
const onCopyName = vi.fn()
|
||||||
|
const onDownloadCredential = vi.fn()
|
||||||
|
const onRefreshOAuth = vi.fn()
|
||||||
|
|
||||||
|
const { root, unmount } = mount({
|
||||||
|
apiKey: createProviderKey({
|
||||||
|
oauth_temporary: true,
|
||||||
|
}),
|
||||||
|
maskedSecretLabel: 'oauth:***',
|
||||||
|
oauthPlanLabel: 'Team',
|
||||||
|
oauthPlanClass: 'text-purple-600',
|
||||||
|
oauthOrgBadge: { label: 'org:demo', title: 'org_id: demo' },
|
||||||
|
kiroSubscriptionLabel: 'Pro',
|
||||||
|
kiroSubscriptionClass: 'text-blue-600',
|
||||||
|
canExportCredential: true,
|
||||||
|
showOAuthRefreshControl: true,
|
||||||
|
oauthStatus: { text: '12m', isExpired: false, isExpiringSoon: true, isInvalid: false },
|
||||||
|
oauthStatusTitle: 'OAuth expires soon',
|
||||||
|
oauthRefreshButtonTitle: 'Refresh OAuth',
|
||||||
|
canRefreshCredential: true,
|
||||||
|
onCopyName,
|
||||||
|
onDownloadCredential,
|
||||||
|
onRefreshOAuth,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(root.querySelector('[data-testid="provider-key-name"]')?.textContent).toContain('Primary key')
|
||||||
|
expect(root.querySelector('[data-testid="provider-key-oauth-plan"]')?.textContent).toContain('Team')
|
||||||
|
expect(root.querySelector('[data-testid="provider-key-oauth-org"]')?.textContent).toContain('org:demo')
|
||||||
|
expect(root.querySelector('[data-testid="provider-key-kiro-plan"]')?.textContent).toContain('Pro')
|
||||||
|
expect(root.querySelector('[data-testid="provider-key-oauth-status"]')?.textContent).toContain('12m')
|
||||||
|
expect(root.querySelector('[data-testid="provider-key-temporary"]')).toBeTruthy()
|
||||||
|
|
||||||
|
;(root.querySelector('[data-testid="provider-key-name"]') as HTMLElement).click()
|
||||||
|
;(root.querySelector('button[title="下载 OAuth 授权文件"]') as HTMLButtonElement).click()
|
||||||
|
;(root.querySelector('button[title="Refresh OAuth"]') as HTMLButtonElement).click()
|
||||||
|
|
||||||
|
expect(onCopyName).toHaveBeenCalledWith('Primary key')
|
||||||
|
expect(onDownloadCredential).toHaveBeenCalledTimes(1)
|
||||||
|
expect(onRefreshOAuth).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders account block and inactive account states', () => {
|
||||||
|
const onCopyFullKey = vi.fn()
|
||||||
|
const onClearOAuthInvalid = vi.fn()
|
||||||
|
|
||||||
|
const { root, unmount } = mount({
|
||||||
|
apiKey: createProviderKey({ name: '' }),
|
||||||
|
maskedSecretLabel: 'sk-***',
|
||||||
|
canExportCredential: false,
|
||||||
|
showOAuthRefreshControl: true,
|
||||||
|
accountLevelBlock: true,
|
||||||
|
oauthStatusTitle: 'Account blocked',
|
||||||
|
antigravityInactive: true,
|
||||||
|
onCopyFullKey,
|
||||||
|
onClearOAuthInvalid,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(root.querySelector('[data-testid="provider-key-name"]')?.textContent).toContain('未命名密钥')
|
||||||
|
expect(root.querySelector('[data-testid="provider-key-account-block"]')?.textContent).toContain('账号异常')
|
||||||
|
expect(root.querySelector('[data-testid="provider-key-antigravity-inactive"]')?.textContent).toContain('账号未激活')
|
||||||
|
|
||||||
|
;(root.querySelector('button[title="复制密钥"]') as HTMLButtonElement).click()
|
||||||
|
;(root.querySelector('button[title="清除异常标记(确认账号已完成验证后使用)"]') as HTMLButtonElement).click()
|
||||||
|
|
||||||
|
expect(onCopyFullKey).toHaveBeenCalledTimes(1)
|
||||||
|
expect(onClearOAuthInvalid).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
unmount()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
export { default as ProviderFormDialog } from './ProviderFormDialog.vue'
|
export { default as ProviderFormDialog } from './ProviderFormDialog.vue'
|
||||||
export { default as ProviderKeyActionCluster } from './ProviderKeyActionCluster.vue'
|
export { default as ProviderKeyActionCluster } from './ProviderKeyActionCluster.vue'
|
||||||
|
export { default as ProviderKeyIdentityBlock } from './ProviderKeyIdentityBlock.vue'
|
||||||
export { default as ProviderMonthlyQuotaCard } from './ProviderMonthlyQuotaCard.vue'
|
export { default as ProviderMonthlyQuotaCard } from './ProviderMonthlyQuotaCard.vue'
|
||||||
export { default as ProviderQuotaProgressRow } from './ProviderQuotaProgressRow.vue'
|
export { default as ProviderQuotaProgressRow } from './ProviderQuotaProgressRow.vue'
|
||||||
export { default as ProviderQuotaSectionHeader } from './ProviderQuotaSectionHeader.vue'
|
export { default as ProviderQuotaSectionHeader } from './ProviderQuotaSectionHeader.vue'
|
||||||
|
|||||||
Reference in New Issue
Block a user