mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-13 04:29:50 +08:00
feat(frontend): improve provider detail drawer and pool actions
This commit is contained in:
@@ -8,7 +8,7 @@ vi.mock('@/api/client', () => ({
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
import { getProviderMappingPreview, getProvidersSummary } from '@/api/endpoints/providers'
|
import { getProvider, getProviderMappingPreview, getProvidersSummary } from '@/api/endpoints/providers'
|
||||||
|
|
||||||
const provider = {
|
const provider = {
|
||||||
id: 'provider-1',
|
id: 'provider-1',
|
||||||
@@ -45,6 +45,30 @@ describe('getProvidersSummary', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('getProvider', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
getMock.mockReset()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deduplicates concurrent detail requests without caching the settled result', async () => {
|
||||||
|
let resolveRequest: ((value: { data: typeof provider }) => void) | undefined
|
||||||
|
getMock.mockImplementation(() => new Promise((resolve) => {
|
||||||
|
resolveRequest = resolve
|
||||||
|
}))
|
||||||
|
|
||||||
|
const firstRequest = getProvider('provider-1')
|
||||||
|
const secondRequest = getProvider('provider-1')
|
||||||
|
|
||||||
|
expect(getMock).toHaveBeenCalledTimes(1)
|
||||||
|
resolveRequest?.({ data: provider })
|
||||||
|
await expect(Promise.all([firstRequest, secondRequest])).resolves.toHaveLength(2)
|
||||||
|
|
||||||
|
getMock.mockResolvedValue({ data: provider })
|
||||||
|
await getProvider('provider-1')
|
||||||
|
expect(getMock).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('getProviderMappingPreview', () => {
|
describe('getProviderMappingPreview', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
getMock.mockReset()
|
getMock.mockReset()
|
||||||
|
|||||||
@@ -94,8 +94,10 @@ export async function getProvidersSummary(
|
|||||||
* 获取单个 Provider 的详细信息
|
* 获取单个 Provider 的详细信息
|
||||||
*/
|
*/
|
||||||
export async function getProvider(providerId: string): Promise<ProviderWithEndpointsSummary> {
|
export async function getProvider(providerId: string): Promise<ProviderWithEndpointsSummary> {
|
||||||
const response = await client.get<ProviderWithEndpointsSummary>(`/api/admin/providers/${providerId}/summary`)
|
return dedupedRequest(`providers:detail:${providerId}`, async () => {
|
||||||
return normalizeProviderSummary(response.data)
|
const response = await client.get<ProviderWithEndpointsSummary>(`/api/admin/providers/${providerId}/summary`)
|
||||||
|
return normalizeProviderSummary(response.data)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -90,6 +90,8 @@
|
|||||||
size="icon"
|
size="icon"
|
||||||
class="h-8 w-8 shrink-0"
|
class="h-8 w-8 shrink-0"
|
||||||
:title="action.title"
|
:title="action.title"
|
||||||
|
@pointerenter="action.key === 'viewProvider' && emit('prefetchProvider')"
|
||||||
|
@focus="action.key === 'viewProvider' && emit('prefetchProvider')"
|
||||||
@click="emit(action.event)"
|
@click="emit(action.event)"
|
||||||
>
|
>
|
||||||
<component
|
<component
|
||||||
@@ -249,6 +251,8 @@
|
|||||||
class="h-8 w-8"
|
class="h-8 w-8"
|
||||||
:data-testid="action.key === 'demandMetrics' ? 'pool-demand-metrics-button' : undefined"
|
:data-testid="action.key === 'demandMetrics' ? 'pool-demand-metrics-button' : undefined"
|
||||||
:title="action.title"
|
:title="action.title"
|
||||||
|
@pointerenter="action.key === 'viewProvider' && emit('prefetchProvider')"
|
||||||
|
@focus="action.key === 'viewProvider' && emit('prefetchProvider')"
|
||||||
@click="emit(action.event)"
|
@click="emit(action.event)"
|
||||||
>
|
>
|
||||||
<component
|
<component
|
||||||
@@ -329,6 +333,7 @@ import {
|
|||||||
Settings2,
|
Settings2,
|
||||||
SlidersHorizontal,
|
SlidersHorizontal,
|
||||||
SquareCheckBig,
|
SquareCheckBig,
|
||||||
|
Upload,
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -352,12 +357,14 @@ import {
|
|||||||
} from '@/features/pool/utils/poolBatchActions'
|
} from '@/features/pool/utils/poolBatchActions'
|
||||||
|
|
||||||
type HeaderActionEvent =
|
type HeaderActionEvent =
|
||||||
|
| 'import'
|
||||||
| 'scheduling'
|
| 'scheduling'
|
||||||
| 'viewProvider'
|
| 'viewProvider'
|
||||||
| 'demandMetrics'
|
| 'demandMetrics'
|
||||||
| 'advanced'
|
| 'advanced'
|
||||||
|
|
||||||
type HeaderActionKey =
|
type HeaderActionKey =
|
||||||
|
| 'import'
|
||||||
| 'scheduling'
|
| 'scheduling'
|
||||||
| 'viewProvider'
|
| 'viewProvider'
|
||||||
| 'demandMetrics'
|
| 'demandMetrics'
|
||||||
@@ -395,6 +402,8 @@ const emit = defineEmits<{
|
|||||||
'update:providerId': [value: string]
|
'update:providerId': [value: string]
|
||||||
'update:status': [value: string]
|
'update:status': [value: string]
|
||||||
'update:search': [value: string]
|
'update:search': [value: string]
|
||||||
|
import: []
|
||||||
|
prefetchProvider: []
|
||||||
scheduling: []
|
scheduling: []
|
||||||
viewProvider: []
|
viewProvider: []
|
||||||
demandMetrics: []
|
demandMetrics: []
|
||||||
@@ -428,6 +437,7 @@ const selectedCountLabel = computed(() => legacyT(`已选 ${Math.max(0, props.se
|
|||||||
const mobileActions = computed<HeaderAction[]>(() => {
|
const mobileActions = computed<HeaderAction[]>(() => {
|
||||||
const actions: HeaderAction[] = [
|
const actions: HeaderAction[] = [
|
||||||
{ key: 'viewProvider', title: legacyT('查看详情'), event: 'viewProvider', icon: Eye },
|
{ key: 'viewProvider', title: legacyT('查看详情'), event: 'viewProvider', icon: Eye },
|
||||||
|
{ key: 'import', title: legacyT('导入账号'), event: 'import', icon: Upload },
|
||||||
{ key: 'scheduling', title: legacyT('号池调度'), event: 'scheduling', icon: SlidersHorizontal },
|
{ key: 'scheduling', title: legacyT('号池调度'), event: 'scheduling', icon: SlidersHorizontal },
|
||||||
]
|
]
|
||||||
if (props.showAdaptiveHotPoolMetricsButton) {
|
if (props.showAdaptiveHotPoolMetricsButton) {
|
||||||
@@ -442,6 +452,7 @@ const mobileActions = computed<HeaderAction[]>(() => {
|
|||||||
const desktopActions = computed<HeaderAction[]>(() => {
|
const desktopActions = computed<HeaderAction[]>(() => {
|
||||||
const actions: HeaderAction[] = [
|
const actions: HeaderAction[] = [
|
||||||
{ key: 'viewProvider', title: legacyT('查看详情'), event: 'viewProvider', icon: Eye },
|
{ key: 'viewProvider', title: legacyT('查看详情'), event: 'viewProvider', icon: Eye },
|
||||||
|
{ key: 'import', title: legacyT('导入账号'), event: 'import', icon: Upload },
|
||||||
]
|
]
|
||||||
if (props.showAdaptiveHotPoolMetricsButton) {
|
if (props.showAdaptiveHotPoolMetricsButton) {
|
||||||
actions.push({ key: 'demandMetrics', title: legacyT('查看自适应热池指标'), event: 'demandMetrics', icon: Activity })
|
actions.push({ key: 'demandMetrics', title: legacyT('查看自适应热池指标'), event: 'demandMetrics', icon: Activity })
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ describe('PoolManagementHeader', () => {
|
|||||||
refreshLoading: false,
|
refreshLoading: false,
|
||||||
refreshTitle: '刷新',
|
refreshTitle: '刷新',
|
||||||
onViewProvider: () => events.push('viewProvider'),
|
onViewProvider: () => events.push('viewProvider'),
|
||||||
|
onImport: () => events.push('import'),
|
||||||
|
onPrefetchProvider: () => events.push('prefetchProvider'),
|
||||||
onScheduling: () => events.push('scheduling'),
|
onScheduling: () => events.push('scheduling'),
|
||||||
onDemandMetrics: () => events.push('demandMetrics'),
|
onDemandMetrics: () => events.push('demandMetrics'),
|
||||||
onAdvanced: () => events.push('advanced'),
|
onAdvanced: () => events.push('advanced'),
|
||||||
@@ -51,7 +53,10 @@ describe('PoolManagementHeader', () => {
|
|||||||
app.use(createI18n())
|
app.use(createI18n())
|
||||||
app.mount(root)
|
app.mount(root)
|
||||||
|
|
||||||
root.querySelector<HTMLButtonElement>('[title="查看详情"]')?.click()
|
const mobileViewProviderButton = root.querySelector<HTMLButtonElement>('[title="查看详情"]')
|
||||||
|
mobileViewProviderButton?.dispatchEvent(new Event('pointerenter'))
|
||||||
|
mobileViewProviderButton?.click()
|
||||||
|
root.querySelector<HTMLButtonElement>('[title="导入账号"]')?.click()
|
||||||
root.querySelector<HTMLButtonElement>('[title="点击调整号池调度"]')?.click()
|
root.querySelector<HTMLButtonElement>('[title="点击调整号池调度"]')?.click()
|
||||||
root.querySelector<HTMLButtonElement>('[title="查看自适应热池指标"]')?.click()
|
root.querySelector<HTMLButtonElement>('[title="查看自适应热池指标"]')?.click()
|
||||||
const desktopActions = root.querySelector('[data-testid="pool-header-actions"]')
|
const desktopActions = root.querySelector('[data-testid="pool-header-actions"]')
|
||||||
@@ -67,7 +72,9 @@ describe('PoolManagementHeader', () => {
|
|||||||
root.querySelector<HTMLButtonElement>('[title="刷新"]')?.click()
|
root.querySelector<HTMLButtonElement>('[title="刷新"]')?.click()
|
||||||
|
|
||||||
expect(events).toEqual([
|
expect(events).toEqual([
|
||||||
|
'prefetchProvider',
|
||||||
'viewProvider',
|
'viewProvider',
|
||||||
|
'import',
|
||||||
'scheduling',
|
'scheduling',
|
||||||
'demandMetrics',
|
'demandMetrics',
|
||||||
'advanced',
|
'advanced',
|
||||||
@@ -75,8 +82,11 @@ describe('PoolManagementHeader', () => {
|
|||||||
'batchAction:refresh_quota',
|
'batchAction:refresh_quota',
|
||||||
'refresh',
|
'refresh',
|
||||||
])
|
])
|
||||||
|
const viewProviderButton = desktopActions?.querySelector<HTMLButtonElement>('[title="查看详情"]')
|
||||||
|
const importButton = desktopActions?.querySelector<HTMLButtonElement>('[title="导入账号"]')
|
||||||
expect(selectAllButton?.textContent?.trim()).toBe('')
|
expect(selectAllButton?.textContent?.trim()).toBe('')
|
||||||
expect(selectAllButton?.getAttribute('title')).toBe('全选')
|
expect(selectAllButton?.getAttribute('title')).toBe('全选')
|
||||||
|
expect(viewProviderButton?.nextElementSibling).toBe(importButton)
|
||||||
expect(advancedButton?.nextElementSibling).toBe(selectAllButton)
|
expect(advancedButton?.nextElementSibling).toBe(selectAllButton)
|
||||||
expect(selectAllButton?.nextElementSibling).toBe(batchActionsButton)
|
expect(selectAllButton?.nextElementSibling).toBe(batchActionsButton)
|
||||||
expect(batchActionsButton?.getAttribute('title')).toBe('选择执行动作')
|
expect(batchActionsButton?.getAttribute('title')).toBe('选择执行动作')
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- 自定义抽屉 -->
|
<!-- 自定义抽屉 -->
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<Transition name="drawer">
|
<Transition
|
||||||
|
name="drawer"
|
||||||
|
appear
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-if="open && (loading || provider)"
|
v-if="open && (loading || provider)"
|
||||||
class="fixed inset-0 z-50 flex justify-end"
|
class="fixed inset-0 z-50 flex justify-end"
|
||||||
@@ -9,12 +12,12 @@
|
|||||||
>
|
>
|
||||||
<!-- 背景遮罩 -->
|
<!-- 背景遮罩 -->
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 bg-black/30 backdrop-blur-sm"
|
class="absolute inset-0 bg-black/30"
|
||||||
@click="handleBackdropClick"
|
@click="handleBackdropClick"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 抽屉内容 -->
|
<!-- 抽屉内容 -->
|
||||||
<Card class="relative h-full w-full sm:w-[700px] sm:max-w-[90vw] rounded-none shadow-2xl overflow-y-auto">
|
<Card class="drawer-panel relative h-full w-full sm:w-[700px] sm:max-w-[90vw] rounded-none shadow-2xl overflow-y-auto">
|
||||||
<!-- 加载状态 -->
|
<!-- 加载状态 -->
|
||||||
<div
|
<div
|
||||||
v-if="loading"
|
v-if="loading"
|
||||||
@@ -810,7 +813,7 @@
|
|||||||
|
|
||||||
<!-- 端点表单对话框(管理/编辑) -->
|
<!-- 端点表单对话框(管理/编辑) -->
|
||||||
<EndpointFormDialog
|
<EndpointFormDialog
|
||||||
v-if="provider && open"
|
v-if="provider && open && endpointDialogOpen"
|
||||||
v-model="endpointDialogOpen"
|
v-model="endpointDialogOpen"
|
||||||
:provider="provider"
|
:provider="provider"
|
||||||
:endpoints="endpoints"
|
:endpoints="endpoints"
|
||||||
@@ -822,7 +825,7 @@
|
|||||||
|
|
||||||
<!-- 密钥编辑对话框 -->
|
<!-- 密钥编辑对话框 -->
|
||||||
<KeyFormDialog
|
<KeyFormDialog
|
||||||
v-if="open"
|
v-if="open && keyFormDialogOpen"
|
||||||
:open="keyFormDialogOpen"
|
:open="keyFormDialogOpen"
|
||||||
:endpoint="currentEndpoint"
|
:endpoint="currentEndpoint"
|
||||||
:editing-key="editingKey"
|
:editing-key="editingKey"
|
||||||
@@ -834,7 +837,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<ProviderKeyBatchImportDialog
|
<ProviderKeyBatchImportDialog
|
||||||
v-if="open && provider?.provider_type === 'custom'"
|
v-if="open && keyBatchImportDialogOpen && provider?.provider_type === 'custom'"
|
||||||
:open="keyBatchImportDialogOpen"
|
:open="keyBatchImportDialogOpen"
|
||||||
:provider-id="provider.id"
|
:provider-id="provider.id"
|
||||||
:provider-name="provider.name"
|
:provider-name="provider.name"
|
||||||
@@ -845,7 +848,7 @@
|
|||||||
|
|
||||||
<!-- OAuth 账号对话框 -->
|
<!-- OAuth 账号对话框 -->
|
||||||
<OAuthAccountDialog
|
<OAuthAccountDialog
|
||||||
v-if="open && provider"
|
v-if="open && oauthAccountDialogOpen && provider"
|
||||||
:open="oauthAccountDialogOpen"
|
:open="oauthAccountDialogOpen"
|
||||||
:provider-id="provider.id"
|
:provider-id="provider.id"
|
||||||
:provider-type="provider.provider_type"
|
:provider-type="provider.provider_type"
|
||||||
@@ -855,7 +858,7 @@
|
|||||||
|
|
||||||
<!-- OAuth 密钥编辑对话框 -->
|
<!-- OAuth 密钥编辑对话框 -->
|
||||||
<OAuthKeyEditDialog
|
<OAuthKeyEditDialog
|
||||||
v-if="open"
|
v-if="open && oauthKeyEditDialogOpen"
|
||||||
:open="oauthKeyEditDialogOpen"
|
:open="oauthKeyEditDialogOpen"
|
||||||
:editing-key="editingKey"
|
:editing-key="editingKey"
|
||||||
@close="oauthKeyEditDialogOpen = false"
|
@close="oauthKeyEditDialogOpen = false"
|
||||||
@@ -864,7 +867,7 @@
|
|||||||
|
|
||||||
<!-- 模型权限对话框 -->
|
<!-- 模型权限对话框 -->
|
||||||
<KeyAllowedModelsEditDialog
|
<KeyAllowedModelsEditDialog
|
||||||
v-if="open"
|
v-if="open && keyPermissionsDialogOpen"
|
||||||
:open="keyPermissionsDialogOpen"
|
:open="keyPermissionsDialogOpen"
|
||||||
:api-key="editingKey"
|
:api-key="editingKey"
|
||||||
:provider-id="providerId || ''"
|
:provider-id="providerId || ''"
|
||||||
@@ -874,7 +877,7 @@
|
|||||||
|
|
||||||
<!-- 删除密钥确认对话框 -->
|
<!-- 删除密钥确认对话框 -->
|
||||||
<AlertDialog
|
<AlertDialog
|
||||||
v-if="open"
|
v-if="open && deleteKeyConfirmOpen"
|
||||||
:model-value="deleteKeyConfirmOpen"
|
:model-value="deleteKeyConfirmOpen"
|
||||||
:title="legacyT('删除密钥')"
|
:title="legacyT('删除密钥')"
|
||||||
:description="formatDeleteKeyConfirmDescription()"
|
:description="formatDeleteKeyConfirmDescription()"
|
||||||
@@ -888,7 +891,7 @@
|
|||||||
|
|
||||||
<!-- 添加/编辑模型对话框 -->
|
<!-- 添加/编辑模型对话框 -->
|
||||||
<ProviderModelFormDialog
|
<ProviderModelFormDialog
|
||||||
v-if="open && provider"
|
v-if="open && modelFormDialogOpen && provider"
|
||||||
:open="modelFormDialogOpen"
|
:open="modelFormDialogOpen"
|
||||||
:provider-id="provider.id"
|
:provider-id="provider.id"
|
||||||
:provider-name="provider.name"
|
:provider-name="provider.name"
|
||||||
@@ -899,7 +902,7 @@
|
|||||||
|
|
||||||
<!-- 批量关联模型对话框 -->
|
<!-- 批量关联模型对话框 -->
|
||||||
<BatchAssignModelsDialog
|
<BatchAssignModelsDialog
|
||||||
v-if="open && provider"
|
v-if="open && batchAssignDialogOpen && provider"
|
||||||
:open="batchAssignDialogOpen"
|
:open="batchAssignDialogOpen"
|
||||||
:provider-id="provider.id"
|
:provider-id="provider.id"
|
||||||
:provider-name="provider.name"
|
:provider-name="provider.name"
|
||||||
@@ -921,6 +924,7 @@
|
|||||||
|
|
||||||
<!-- 故障转移规则弹窗 -->
|
<!-- 故障转移规则弹窗 -->
|
||||||
<FailoverRulesDialog
|
<FailoverRulesDialog
|
||||||
|
v-if="open && failoverRulesDialogOpen"
|
||||||
:open="failoverRulesDialogOpen"
|
:open="failoverRulesDialogOpen"
|
||||||
:provider="provider ?? null"
|
:provider="provider ?? null"
|
||||||
@update:open="failoverRulesDialogOpen = $event"
|
@update:open="failoverRulesDialogOpen = $event"
|
||||||
@@ -3908,8 +3912,8 @@ useEscapeKey(() => {
|
|||||||
transition: opacity 0.3s ease;
|
transition: opacity 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawer-enter-active .relative,
|
.drawer-enter-active .drawer-panel,
|
||||||
.drawer-leave-active .relative {
|
.drawer-leave-active .drawer-panel {
|
||||||
transition: transform 0.3s ease;
|
transition: transform 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3918,16 +3922,16 @@ useEscapeKey(() => {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawer-enter-from .relative {
|
.drawer-enter-from .drawer-panel {
|
||||||
transform: translateX(100%);
|
transform: translateX(100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawer-leave-to .relative {
|
.drawer-leave-to .drawer-panel {
|
||||||
transform: translateX(100%);
|
transform: translateX(100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawer-enter-to .relative,
|
.drawer-enter-to .drawer-panel,
|
||||||
.drawer-leave-from .relative {
|
.drawer-leave-from .drawer-panel {
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+25
@@ -8,6 +8,18 @@ const source = readFileSync(
|
|||||||
)
|
)
|
||||||
|
|
||||||
describe('ProviderDetailDrawer loading priorities', () => {
|
describe('ProviderDetailDrawer loading priorities', () => {
|
||||||
|
it('plays the drawer transition when the lazily mounted component first appears', () => {
|
||||||
|
const transitionTemplate = source
|
||||||
|
.split('<Transition')[1]
|
||||||
|
?.split('>')[0]
|
||||||
|
|
||||||
|
expect(transitionTemplate).toContain('name="drawer"')
|
||||||
|
expect(transitionTemplate).toContain('appear')
|
||||||
|
expect(source).toContain('class="drawer-panel relative')
|
||||||
|
expect(source).toContain('.drawer-enter-from .drawer-panel')
|
||||||
|
expect(source).not.toContain('.drawer-enter-from .relative')
|
||||||
|
})
|
||||||
|
|
||||||
it('loads mapping preview after first-screen provider data', () => {
|
it('loads mapping preview after first-screen provider data', () => {
|
||||||
const openWatcher = source
|
const openWatcher = source
|
||||||
.split('// 合并监听 providerId 和 open')[1]
|
.split('// 合并监听 providerId 和 open')[1]
|
||||||
@@ -29,4 +41,17 @@ describe('ProviderDetailDrawer loading priorities', () => {
|
|||||||
expect(source).not.toContain(':loading="loadingProviderModels || loadingProviderKeys"')
|
expect(source).not.toContain(':loading="loadingProviderModels || loadingProviderKeys"')
|
||||||
expect(source).not.toContain(':loading="loadingProviderEndpoints || loadingProviderKeys || loadingProviderModels || loadingProviderMappingPreview"')
|
expect(source).not.toContain(':loading="loadingProviderEndpoints || loadingProviderKeys || loadingProviderModels || loadingProviderMappingPreview"')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('does not mount closed child dialogs or blur the page behind the drawer', () => {
|
||||||
|
expect(source).toContain('class="absolute inset-0 bg-black/30"')
|
||||||
|
expect(source).not.toContain('backdrop-blur-sm')
|
||||||
|
expect(source).toContain('v-if="provider && open && endpointDialogOpen"')
|
||||||
|
expect(source).toContain('v-if="open && keyFormDialogOpen"')
|
||||||
|
expect(source).toContain('v-if="open && oauthAccountDialogOpen && provider"')
|
||||||
|
expect(source).toContain('v-if="open && oauthKeyEditDialogOpen"')
|
||||||
|
expect(source).toContain('v-if="open && keyPermissionsDialogOpen"')
|
||||||
|
expect(source).toContain('v-if="open && modelFormDialogOpen && provider"')
|
||||||
|
expect(source).toContain('v-if="open && batchAssignDialogOpen && provider"')
|
||||||
|
expect(source).toContain('v-if="open && failoverRulesDialogOpen"')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
:refresh-loading="refreshCurrentPageLoading"
|
:refresh-loading="refreshCurrentPageLoading"
|
||||||
:refresh-title="refreshButtonTitle"
|
:refresh-title="refreshButtonTitle"
|
||||||
@view-provider="openProviderDrawer"
|
@view-provider="openProviderDrawer"
|
||||||
|
@prefetch-provider="prefetchProviderDetailDrawer"
|
||||||
|
@import="showImportDialog = true"
|
||||||
@scheduling="openSchedulingDialog"
|
@scheduling="openSchedulingDialog"
|
||||||
@demand-metrics="showDemandMetricsDialog = true"
|
@demand-metrics="showDemandMetricsDialog = true"
|
||||||
@advanced="showAdvancedDialog = true"
|
@advanced="showAdvancedDialog = true"
|
||||||
@@ -949,7 +951,7 @@
|
|||||||
:samples="providerDemandMetricSamples"
|
:samples="providerDemandMetricSamples"
|
||||||
/>
|
/>
|
||||||
<ProviderDetailDrawer
|
<ProviderDetailDrawer
|
||||||
v-if="providerDrawerOpen && selectedProviderId"
|
v-if="providerDrawerMounted && selectedProviderId"
|
||||||
:open="providerDrawerOpen"
|
:open="providerDrawerOpen"
|
||||||
:provider-id="selectedProviderId"
|
:provider-id="selectedProviderId"
|
||||||
:initial-provider="selectedProviderData"
|
:initial-provider="selectedProviderData"
|
||||||
@@ -1157,9 +1159,12 @@ import {
|
|||||||
getQuotaDisplayText,
|
getQuotaDisplayText,
|
||||||
} from '@/utils/providerKeyQuota'
|
} from '@/utils/providerKeyQuota'
|
||||||
|
|
||||||
const ProviderDetailDrawer = defineAsyncComponent(
|
const loadProviderDetailDrawer = () => import('@/features/providers/components/ProviderDetailDrawer.vue')
|
||||||
() => import('@/features/providers/components/ProviderDetailDrawer.vue'),
|
const ProviderDetailDrawer = defineAsyncComponent(loadProviderDetailDrawer)
|
||||||
)
|
|
||||||
|
function prefetchProviderDetailDrawer(): void {
|
||||||
|
void loadProviderDetailDrawer().catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
type PoolKeyScore = NonNullable<PoolKeyDetail['pool_score']>
|
type PoolKeyScore = NonNullable<PoolKeyDetail['pool_score']>
|
||||||
|
|
||||||
@@ -1193,6 +1198,8 @@ let selectProviderRequestId = 0
|
|||||||
let providerDataRequestId = 0
|
let providerDataRequestId = 0
|
||||||
let keysRequestId = 0
|
let keysRequestId = 0
|
||||||
let keysSearchDebounceTimer: number | null = null
|
let keysSearchDebounceTimer: number | null = null
|
||||||
|
let providerDetailDrawerPrefetchIdleId: number | null = null
|
||||||
|
let providerDetailDrawerPrefetchTimer: number | null = null
|
||||||
const keysSearchPending = ref(false)
|
const keysSearchPending = ref(false)
|
||||||
let demandMetricsPollingTimer: number | null = null
|
let demandMetricsPollingTimer: number | null = null
|
||||||
let demandMetricsRequestId = 0
|
let demandMetricsRequestId = 0
|
||||||
@@ -1204,6 +1211,38 @@ const POOL_SCHEDULING_PRESETS_CACHE_TTL_MS = 5 * 60 * 1000
|
|||||||
const POOL_DEMAND_METRICS_SAMPLES_LIMIT = 120
|
const POOL_DEMAND_METRICS_SAMPLES_LIMIT = 120
|
||||||
const POOL_DEMAND_METRICS_POLL_INTERVAL_MS = 10 * 1000
|
const POOL_DEMAND_METRICS_POLL_INTERVAL_MS = 10 * 1000
|
||||||
|
|
||||||
|
type IdleCallbackWindow = typeof window & {
|
||||||
|
requestIdleCallback?: (callback: () => void, options?: { timeout: number }) => number
|
||||||
|
cancelIdleCallback?: (handle: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleProviderDetailDrawerPrefetch(): void {
|
||||||
|
const idleWindow = window as IdleCallbackWindow
|
||||||
|
if (idleWindow.requestIdleCallback) {
|
||||||
|
providerDetailDrawerPrefetchIdleId = idleWindow.requestIdleCallback(() => {
|
||||||
|
providerDetailDrawerPrefetchIdleId = null
|
||||||
|
prefetchProviderDetailDrawer()
|
||||||
|
}, { timeout: 1500 })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
providerDetailDrawerPrefetchTimer = window.setTimeout(() => {
|
||||||
|
providerDetailDrawerPrefetchTimer = null
|
||||||
|
prefetchProviderDetailDrawer()
|
||||||
|
}, 800)
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelProviderDetailDrawerPrefetch(): void {
|
||||||
|
const idleWindow = window as IdleCallbackWindow
|
||||||
|
if (providerDetailDrawerPrefetchIdleId !== null) {
|
||||||
|
idleWindow.cancelIdleCallback?.(providerDetailDrawerPrefetchIdleId)
|
||||||
|
providerDetailDrawerPrefetchIdleId = null
|
||||||
|
}
|
||||||
|
if (providerDetailDrawerPrefetchTimer !== null) {
|
||||||
|
clearTimeout(providerDetailDrawerPrefetchTimer)
|
||||||
|
providerDetailDrawerPrefetchTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface PoolDemandMetricSample {
|
interface PoolDemandMetricSample {
|
||||||
providerId: string
|
providerId: string
|
||||||
sampledAt: number
|
sampledAt: number
|
||||||
@@ -2827,6 +2866,7 @@ const showImportDialog = ref(false)
|
|||||||
const showSchedulingDialog = ref(false)
|
const showSchedulingDialog = ref(false)
|
||||||
const showAdvancedDialog = ref(false)
|
const showAdvancedDialog = ref(false)
|
||||||
const providerDrawerOpen = ref(false)
|
const providerDrawerOpen = ref(false)
|
||||||
|
const providerDrawerMounted = ref(false)
|
||||||
const providerEditDialogOpen = ref(false)
|
const providerEditDialogOpen = ref(false)
|
||||||
const providerToEdit = ref<ProviderWithEndpointsSummary | null>(null)
|
const providerToEdit = ref<ProviderWithEndpointsSummary | null>(null)
|
||||||
const showAccountBatchDialog = ref(false)
|
const showAccountBatchDialog = ref(false)
|
||||||
@@ -2848,7 +2888,9 @@ watch(showAccountBatchDialog, (open) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function openProviderDrawer(): void {
|
function openProviderDrawer(): void {
|
||||||
|
prefetchProviderDetailDrawer()
|
||||||
if (!selectedProviderId.value) return
|
if (!selectedProviderId.value) return
|
||||||
|
providerDrawerMounted.value = true
|
||||||
providerDrawerOpen.value = true
|
providerDrawerOpen.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3914,9 +3956,11 @@ onMounted(() => {
|
|||||||
startCountdownTimer()
|
startCountdownTimer()
|
||||||
void loadSchedulingPresetMetas({ cacheTtlMs: POOL_SCHEDULING_PRESETS_CACHE_TTL_MS })
|
void loadSchedulingPresetMetas({ cacheTtlMs: POOL_SCHEDULING_PRESETS_CACHE_TTL_MS })
|
||||||
void loadOverview({ cacheTtlMs: POOL_OVERVIEW_CACHE_TTL_MS })
|
void loadOverview({ cacheTtlMs: POOL_OVERVIEW_CACHE_TTL_MS })
|
||||||
|
scheduleProviderDetailDrawerPrefetch()
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
cancelProviderDetailDrawerPrefetch()
|
||||||
stopDemandMetricsPolling()
|
stopDemandMetricsPolling()
|
||||||
if (keysSearchDebounceTimer !== null) {
|
if (keysSearchDebounceTimer !== null) {
|
||||||
clearTimeout(keysSearchDebounceTimer)
|
clearTimeout(keysSearchDebounceTimer)
|
||||||
|
|||||||
@@ -221,7 +221,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<ProviderDetailDrawer
|
<ProviderDetailDrawer
|
||||||
v-if="providerDrawerOpen"
|
v-if="providerDrawerMounted"
|
||||||
:open="providerDrawerOpen"
|
:open="providerDrawerOpen"
|
||||||
:provider-id="selectedProviderId"
|
:provider-id="selectedProviderId"
|
||||||
:initial-provider="selectedProvider"
|
:initial-provider="selectedProvider"
|
||||||
@@ -310,6 +310,7 @@ const providerToEdit = ref<ProviderWithEndpointsSummary | null>(null)
|
|||||||
const priorityDialogOpen = ref(false)
|
const priorityDialogOpen = ref(false)
|
||||||
const priorityMode = ref<'provider' | 'global_key'>('provider')
|
const priorityMode = ref<'provider' | 'global_key'>('provider')
|
||||||
const providerDrawerOpen = ref(false)
|
const providerDrawerOpen = ref(false)
|
||||||
|
const providerDrawerMounted = ref(false)
|
||||||
const selectedProviderId = ref<string | null>(null)
|
const selectedProviderId = ref<string | null>(null)
|
||||||
const selectedProvider = computed<ProviderWithEndpointsSummary | null>(() => {
|
const selectedProvider = computed<ProviderWithEndpointsSummary | null>(() => {
|
||||||
if (!selectedProviderId.value) return null
|
if (!selectedProviderId.value) return null
|
||||||
@@ -639,6 +640,7 @@ async function handleProviderBatchChanged() {
|
|||||||
// 打开提供商详情抽屉
|
// 打开提供商详情抽屉
|
||||||
function openProviderDrawer(providerId: string) {
|
function openProviderDrawer(providerId: string) {
|
||||||
selectedProviderId.value = providerId
|
selectedProviderId.value = providerId
|
||||||
|
providerDrawerMounted.value = true
|
||||||
providerDrawerOpen.value = true
|
providerDrawerOpen.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,13 +8,16 @@ const source = readFileSync(
|
|||||||
)
|
)
|
||||||
|
|
||||||
describe('PoolManagement provider detail drawer', () => {
|
describe('PoolManagement provider detail drawer', () => {
|
||||||
it('keeps the heavy drawer out of the initial route chunk', () => {
|
it('keeps the heavy drawer out of the initial route chunk and prefetches it when idle', () => {
|
||||||
expect(source).not.toContain(
|
expect(source).not.toContain(
|
||||||
"import ProviderDetailDrawer from '@/features/providers/components/ProviderDetailDrawer.vue'",
|
"import ProviderDetailDrawer from '@/features/providers/components/ProviderDetailDrawer.vue'",
|
||||||
)
|
)
|
||||||
expect(source).toContain(
|
expect(source).toContain(
|
||||||
"() => import('@/features/providers/components/ProviderDetailDrawer.vue')",
|
"const loadProviderDetailDrawer = () => import('@/features/providers/components/ProviderDetailDrawer.vue')",
|
||||||
)
|
)
|
||||||
|
expect(source).toContain('defineAsyncComponent(loadProviderDetailDrawer)')
|
||||||
|
expect(source).toContain('scheduleProviderDetailDrawerPrefetch()')
|
||||||
|
expect(source).toContain('@prefetch-provider="prefetchProviderDetailDrawer"')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('only mounts the drawer after the provider detail action is opened', () => {
|
it('only mounts the drawer after the provider detail action is opened', () => {
|
||||||
@@ -23,7 +26,8 @@ describe('PoolManagement provider detail drawer', () => {
|
|||||||
?.split('/>')[0]
|
?.split('/>')[0]
|
||||||
|
|
||||||
expect(drawerTemplate).toBeTruthy()
|
expect(drawerTemplate).toBeTruthy()
|
||||||
expect(drawerTemplate).toContain('v-if="providerDrawerOpen && selectedProviderId"')
|
expect(drawerTemplate).toContain('v-if="providerDrawerMounted && selectedProviderId"')
|
||||||
expect(source).toContain('@view-provider="openProviderDrawer"')
|
expect(source).toContain('@view-provider="openProviderDrawer"')
|
||||||
|
expect(source).toContain('providerDrawerMounted.value = true')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ describe('ProviderManagement detail drawer loading', () => {
|
|||||||
?.split('/>')[0]
|
?.split('/>')[0]
|
||||||
|
|
||||||
expect(drawerTemplate).toBeTruthy()
|
expect(drawerTemplate).toBeTruthy()
|
||||||
expect(drawerTemplate).toContain('v-if="providerDrawerOpen"')
|
expect(drawerTemplate).toContain('v-if="providerDrawerMounted"')
|
||||||
|
expect(source).toContain('providerDrawerMounted.value = true')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user