mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-10 21:20:20 +08:00
refactor(frontend): extract provider key actions
This commit is contained in:
@@ -244,142 +244,29 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 并发 + 健康度 + 操作按钮 -->
|
||||
<div class="flex items-center gap-1 shrink-0">
|
||||
<!-- 熔断徽章 -->
|
||||
<Badge
|
||||
v-if="key.circuit_breaker_open"
|
||||
variant="destructive"
|
||||
class="text-[10px] px-1.5 py-0 shrink-0"
|
||||
:title="getKeyCircuitBreakerTitle(key)"
|
||||
>
|
||||
{{ legacyT('熔断') }}{{ getKeyCircuitProbeCountdown(key) }}
|
||||
</Badge>
|
||||
<!-- 健康度 -->
|
||||
<div
|
||||
v-if="key.health_score !== undefined"
|
||||
class="flex items-center gap-1 mr-1"
|
||||
>
|
||||
<div class="w-10 h-1.5 bg-border rounded-full overflow-hidden">
|
||||
<div
|
||||
class="h-full transition-all duration-300"
|
||||
:class="getHealthScoreBarColor(key.health_score || 0)"
|
||||
:style="{ width: `${(key.health_score || 0) * 100}%` }"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
class="text-[10px] font-medium tabular-nums"
|
||||
:class="getHealthScoreColor(key.health_score || 0)"
|
||||
>
|
||||
{{ ((key.health_score || 0) * 100).toFixed(0) }}%
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
v-if="isKeyRecoverable(key)"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7 text-green-600"
|
||||
:title="getRecoverKeyTitle(key)"
|
||||
@click="handleRecoverKey(key)"
|
||||
>
|
||||
<RefreshCw class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
:title="legacyT('模型权限')"
|
||||
@click="handleKeyPermissions(key)"
|
||||
>
|
||||
<Shield class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<!-- 代理节点配置 -->
|
||||
<Popover
|
||||
:open="proxyPopoverOpenKeyId === key.id"
|
||||
@update:open="(v: boolean) => handleProxyPopoverToggle(key.id, v)"
|
||||
>
|
||||
<PopoverTrigger as-child>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
:class="key.proxy?.node_id ? 'text-blue-500' : ''"
|
||||
:disabled="savingProxyKeyId === key.id"
|
||||
:title="key.proxy?.node_id ? `${legacyT('代理')}: ${getKeyProxyNodeName(key)}` : 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="key.proxy?.node_id"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-6 px-2 text-[10px] text-muted-foreground"
|
||||
:disabled="savingProxyKeyId === key.id"
|
||||
@click="clearKeyProxy(key)"
|
||||
>
|
||||
{{ legacyT('清除') }}
|
||||
</Button>
|
||||
</div>
|
||||
<ProxyNodeSelect
|
||||
:model-value="key.proxy?.node_id || ''"
|
||||
trigger-class="h-8"
|
||||
@update:model-value="(v: string) => setKeyProxy(key, v)"
|
||||
/>
|
||||
<p class="text-[10px] text-muted-foreground">
|
||||
{{ legacyT(key.proxy?.node_id ? '当前使用独立代理' : '未设置,使用提供商级别代理') }}
|
||||
</p>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
:title="legacyT('编辑密钥')"
|
||||
@click="handleEditKey(endpoint, key)"
|
||||
>
|
||||
<Edit class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
v-if="provider.provider_type === 'antigravity'"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
:title="legacyT('配额详情')"
|
||||
@click="openAntigravityQuotaDialog(key)"
|
||||
>
|
||||
<BarChart3 class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
:disabled="togglingKeyId === key.id"
|
||||
:title="legacyT(key.is_active ? '点击停用' : '点击启用')"
|
||||
@click="toggleKeyActive(key)"
|
||||
>
|
||||
<Power class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
:title="legacyT('删除密钥')"
|
||||
@click="handleDeleteKey(key)"
|
||||
>
|
||||
<Trash2 class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
<ProviderKeyActionCluster
|
||||
:api-key="key"
|
||||
:provider-type="provider.provider_type"
|
||||
:recoverable="isKeyRecoverable(key)"
|
||||
:recover-title="getRecoverKeyTitle(key)"
|
||||
:circuit-breaker-title="getKeyCircuitBreakerTitle(key)"
|
||||
:circuit-probe-countdown="getKeyCircuitProbeCountdown(key)"
|
||||
:health-score-bar-class="getHealthScoreBarColor(key.health_score || 0)"
|
||||
:health-score-text-class="getHealthScoreColor(key.health_score || 0)"
|
||||
:proxy-popover-open="proxyPopoverOpenKeyId === key.id"
|
||||
:proxy-node-name="getKeyProxyNodeName(key)"
|
||||
:saving-proxy="savingProxyKeyId === key.id"
|
||||
:toggling="togglingKeyId === key.id"
|
||||
@recover="handleRecoverKey(key)"
|
||||
@permissions="handleKeyPermissions(key)"
|
||||
@update:proxy-popover-open="(v: boolean) => handleProxyPopoverToggle(key.id, v)"
|
||||
@clear-proxy="clearKeyProxy(key)"
|
||||
@set-proxy="(v: string) => setKeyProxy(key, v)"
|
||||
@edit="handleEditKey(endpoint, key)"
|
||||
@open-antigravity-quota="openAntigravityQuotaDialog(key)"
|
||||
@toggle-active="toggleKeyActive(key)"
|
||||
@delete="handleDeleteKey(key)"
|
||||
/>
|
||||
</div>
|
||||
<!-- Codex 上游额度信息(仅当有元数据时显示) -->
|
||||
<div
|
||||
@@ -1080,17 +967,11 @@ import {
|
||||
Plus,
|
||||
Key,
|
||||
Loader2,
|
||||
Edit,
|
||||
Trash2,
|
||||
RefreshCw,
|
||||
Power,
|
||||
GripVertical,
|
||||
Copy,
|
||||
Download,
|
||||
Shield,
|
||||
BarChart3,
|
||||
ShieldX,
|
||||
Globe,
|
||||
} from 'lucide-vue-next'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { useEscapeKey } from '@/composables/useEscapeKey'
|
||||
@@ -1098,7 +979,6 @@ import { useI18n } from '@/i18n'
|
||||
import Button from '@/components/ui/button.vue'
|
||||
import Badge from '@/components/ui/badge.vue'
|
||||
import Card from '@/components/ui/card.vue'
|
||||
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import { useClipboard } from '@/composables/useClipboard'
|
||||
@@ -1128,10 +1008,10 @@ import AlertDialog from '@/components/common/AlertDialog.vue'
|
||||
import AntigravityQuotaDialog from '@/features/providers/components/AntigravityQuotaDialog.vue'
|
||||
import FailoverRulesDialog from '@/features/providers/components/FailoverRulesDialog.vue'
|
||||
import ProviderDetailHeader from '@/features/providers/components/ProviderDetailHeader.vue'
|
||||
import ProviderKeyActionCluster from '@/features/providers/components/ProviderKeyActionCluster.vue'
|
||||
import ProviderMonthlyQuotaCard from '@/features/providers/components/ProviderMonthlyQuotaCard.vue'
|
||||
import ProviderQuotaProgressRow from '@/features/providers/components/ProviderQuotaProgressRow.vue'
|
||||
import ProviderQuotaSectionHeader from '@/features/providers/components/ProviderQuotaSectionHeader.vue'
|
||||
import ProxyNodeSelect from '@/features/providers/components/ProxyNodeSelect.vue'
|
||||
import { useProxyNodesStore } from '@/stores/proxy-nodes'
|
||||
import {
|
||||
deleteEndpointKey,
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
<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 ? '点击停用' : '点击启用')"
|
||||
@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>
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createApp, defineComponent, h } from 'vue'
|
||||
|
||||
import ProviderKeyActionCluster from '@/features/providers/components/ProviderKeyActionCluster.vue'
|
||||
import type { EndpointAPIKey } from '@/api/endpoints'
|
||||
import { createI18n } from '@/i18n'
|
||||
|
||||
vi.mock('@/components/ui', async () => {
|
||||
const { defineComponent, h } = await import('vue')
|
||||
|
||||
const passthrough = (name: string) => defineComponent({
|
||||
name,
|
||||
setup(_, { slots }) {
|
||||
return () => h('div', slots.default?.())
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
Popover: passthrough('PopoverStub'),
|
||||
PopoverTrigger: passthrough('PopoverTriggerStub'),
|
||||
PopoverContent: passthrough('PopoverContentStub'),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/features/providers/components/ProxyNodeSelect.vue', async () => {
|
||||
const { defineComponent, h } = await import('vue')
|
||||
|
||||
return {
|
||||
default: defineComponent({
|
||||
name: 'ProxyNodeSelectStub',
|
||||
emits: ['update:modelValue'],
|
||||
setup(_, { emit }) {
|
||||
return () => h('button', {
|
||||
type: 'button',
|
||||
'data-testid': 'proxy-node-select',
|
||||
onClick: () => emit('update:modelValue', 'proxy-node-2'),
|
||||
}, 'select proxy')
|
||||
},
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('lucide-vue-next', async () => {
|
||||
const { defineComponent, h } = await import('vue')
|
||||
const Icon = defineComponent({
|
||||
name: 'IconStub',
|
||||
setup() {
|
||||
return () => h('span')
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
BarChart3: Icon,
|
||||
Edit: Icon,
|
||||
Globe: Icon,
|
||||
Power: Icon,
|
||||
RefreshCw: Icon,
|
||||
Shield: Icon,
|
||||
Trash2: 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: 'api_key',
|
||||
name: 'Primary key',
|
||||
internal_priority: 10,
|
||||
cache_ttl_minutes: 0,
|
||||
max_probe_interval_minutes: 5,
|
||||
health_score: 0.42,
|
||||
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(ProviderKeyActionCluster, props)
|
||||
},
|
||||
}))
|
||||
app.use(createI18n())
|
||||
app.mount(root)
|
||||
|
||||
return {
|
||||
root,
|
||||
unmount: () => {
|
||||
app.unmount()
|
||||
root.remove()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
describe('ProviderKeyActionCluster', () => {
|
||||
it('renders circuit, health, proxy and antigravity actions', () => {
|
||||
const { root, unmount } = mount({
|
||||
apiKey: createProviderKey({
|
||||
circuit_breaker_open: true,
|
||||
proxy: { node_id: 'proxy-node-1' },
|
||||
}),
|
||||
providerType: 'antigravity',
|
||||
recoverable: true,
|
||||
recoverTitle: 'Recover key',
|
||||
circuitBreakerTitle: 'Circuit is open',
|
||||
circuitProbeCountdown: ' 2m',
|
||||
healthScoreBarClass: 'bg-red-500',
|
||||
healthScoreTextClass: 'text-red-600',
|
||||
proxyPopoverOpen: true,
|
||||
proxyNodeName: 'Tokyo',
|
||||
})
|
||||
|
||||
expect(root.querySelector('[data-testid="provider-key-circuit-badge"]')?.textContent).toContain('熔断 2m')
|
||||
expect(root.querySelector('[data-testid="provider-key-health"]')?.textContent).toContain('42%')
|
||||
expect(root.querySelector('button[title="Recover key"]')).toBeTruthy()
|
||||
expect(root.querySelector('button[title="代理: Tokyo"]')).toBeTruthy()
|
||||
expect(root.querySelector('button[title="配额详情"]')).toBeTruthy()
|
||||
|
||||
unmount()
|
||||
})
|
||||
|
||||
it('emits operation and proxy events without owning business logic', () => {
|
||||
const onRecover = vi.fn()
|
||||
const onPermissions = vi.fn()
|
||||
const onEdit = vi.fn()
|
||||
const onOpenAntigravityQuota = vi.fn()
|
||||
const onToggleActive = vi.fn()
|
||||
const onDelete = vi.fn()
|
||||
const onClearProxy = vi.fn()
|
||||
const onSetProxy = vi.fn()
|
||||
|
||||
const { root, unmount } = mount({
|
||||
apiKey: createProviderKey({ proxy: { node_id: 'proxy-node-1' } }),
|
||||
providerType: 'antigravity',
|
||||
recoverable: true,
|
||||
recoverTitle: 'Recover key',
|
||||
proxyPopoverOpen: true,
|
||||
proxyNodeName: 'Tokyo',
|
||||
onRecover,
|
||||
onPermissions,
|
||||
onEdit,
|
||||
onOpenAntigravityQuota,
|
||||
onToggleActive,
|
||||
onDelete,
|
||||
onClearProxy,
|
||||
onSetProxy,
|
||||
})
|
||||
|
||||
;(root.querySelector('button[title="Recover key"]') as HTMLButtonElement).click()
|
||||
;(root.querySelector('button[title="模型权限"]') as HTMLButtonElement).click()
|
||||
;(root.querySelector('button[title="编辑密钥"]') as HTMLButtonElement).click()
|
||||
;(root.querySelector('button[title="配额详情"]') as HTMLButtonElement).click()
|
||||
;(root.querySelector('button[title="点击停用"]') as HTMLButtonElement).click()
|
||||
;(root.querySelector('button[title="删除密钥"]') as HTMLButtonElement).click()
|
||||
;(Array.from(root.querySelectorAll('button')).find(button => button.textContent?.includes('清除')) as HTMLButtonElement).click()
|
||||
;(root.querySelector('[data-testid="proxy-node-select"]') as HTMLButtonElement).click()
|
||||
|
||||
expect(onRecover).toHaveBeenCalledTimes(1)
|
||||
expect(onPermissions).toHaveBeenCalledTimes(1)
|
||||
expect(onEdit).toHaveBeenCalledTimes(1)
|
||||
expect(onOpenAntigravityQuota).toHaveBeenCalledTimes(1)
|
||||
expect(onToggleActive).toHaveBeenCalledTimes(1)
|
||||
expect(onDelete).toHaveBeenCalledTimes(1)
|
||||
expect(onClearProxy).toHaveBeenCalledTimes(1)
|
||||
expect(onSetProxy).toHaveBeenCalledWith('proxy-node-2')
|
||||
|
||||
unmount()
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,5 @@
|
||||
export { default as ProviderFormDialog } from './ProviderFormDialog.vue'
|
||||
export { default as ProviderKeyActionCluster } from './ProviderKeyActionCluster.vue'
|
||||
export { default as ProviderMonthlyQuotaCard } from './ProviderMonthlyQuotaCard.vue'
|
||||
export { default as ProviderQuotaProgressRow } from './ProviderQuotaProgressRow.vue'
|
||||
export { default as ProviderQuotaSectionHeader } from './ProviderQuotaSectionHeader.vue'
|
||||
|
||||
Reference in New Issue
Block a user