mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 18:07:47 +08:00
feat(frontend): improve provider detail drawer and pool actions
This commit is contained in:
@@ -90,6 +90,8 @@
|
||||
size="icon"
|
||||
class="h-8 w-8 shrink-0"
|
||||
:title="action.title"
|
||||
@pointerenter="action.key === 'viewProvider' && emit('prefetchProvider')"
|
||||
@focus="action.key === 'viewProvider' && emit('prefetchProvider')"
|
||||
@click="emit(action.event)"
|
||||
>
|
||||
<component
|
||||
@@ -249,6 +251,8 @@
|
||||
class="h-8 w-8"
|
||||
:data-testid="action.key === 'demandMetrics' ? 'pool-demand-metrics-button' : undefined"
|
||||
:title="action.title"
|
||||
@pointerenter="action.key === 'viewProvider' && emit('prefetchProvider')"
|
||||
@focus="action.key === 'viewProvider' && emit('prefetchProvider')"
|
||||
@click="emit(action.event)"
|
||||
>
|
||||
<component
|
||||
@@ -329,6 +333,7 @@ import {
|
||||
Settings2,
|
||||
SlidersHorizontal,
|
||||
SquareCheckBig,
|
||||
Upload,
|
||||
} from 'lucide-vue-next'
|
||||
import {
|
||||
Button,
|
||||
@@ -352,12 +357,14 @@ import {
|
||||
} from '@/features/pool/utils/poolBatchActions'
|
||||
|
||||
type HeaderActionEvent =
|
||||
| 'import'
|
||||
| 'scheduling'
|
||||
| 'viewProvider'
|
||||
| 'demandMetrics'
|
||||
| 'advanced'
|
||||
|
||||
type HeaderActionKey =
|
||||
| 'import'
|
||||
| 'scheduling'
|
||||
| 'viewProvider'
|
||||
| 'demandMetrics'
|
||||
@@ -395,6 +402,8 @@ const emit = defineEmits<{
|
||||
'update:providerId': [value: string]
|
||||
'update:status': [value: string]
|
||||
'update:search': [value: string]
|
||||
import: []
|
||||
prefetchProvider: []
|
||||
scheduling: []
|
||||
viewProvider: []
|
||||
demandMetrics: []
|
||||
@@ -428,6 +437,7 @@ const selectedCountLabel = computed(() => legacyT(`已选 ${Math.max(0, props.se
|
||||
const mobileActions = computed<HeaderAction[]>(() => {
|
||||
const actions: HeaderAction[] = [
|
||||
{ key: 'viewProvider', title: legacyT('查看详情'), event: 'viewProvider', icon: Eye },
|
||||
{ key: 'import', title: legacyT('导入账号'), event: 'import', icon: Upload },
|
||||
{ key: 'scheduling', title: legacyT('号池调度'), event: 'scheduling', icon: SlidersHorizontal },
|
||||
]
|
||||
if (props.showAdaptiveHotPoolMetricsButton) {
|
||||
@@ -442,6 +452,7 @@ const mobileActions = computed<HeaderAction[]>(() => {
|
||||
const desktopActions = computed<HeaderAction[]>(() => {
|
||||
const actions: HeaderAction[] = [
|
||||
{ key: 'viewProvider', title: legacyT('查看详情'), event: 'viewProvider', icon: Eye },
|
||||
{ key: 'import', title: legacyT('导入账号'), event: 'import', icon: Upload },
|
||||
]
|
||||
if (props.showAdaptiveHotPoolMetricsButton) {
|
||||
actions.push({ key: 'demandMetrics', title: legacyT('查看自适应热池指标'), event: 'demandMetrics', icon: Activity })
|
||||
|
||||
@@ -35,6 +35,8 @@ describe('PoolManagementHeader', () => {
|
||||
refreshLoading: false,
|
||||
refreshTitle: '刷新',
|
||||
onViewProvider: () => events.push('viewProvider'),
|
||||
onImport: () => events.push('import'),
|
||||
onPrefetchProvider: () => events.push('prefetchProvider'),
|
||||
onScheduling: () => events.push('scheduling'),
|
||||
onDemandMetrics: () => events.push('demandMetrics'),
|
||||
onAdvanced: () => events.push('advanced'),
|
||||
@@ -51,7 +53,10 @@ describe('PoolManagementHeader', () => {
|
||||
app.use(createI18n())
|
||||
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()
|
||||
const desktopActions = root.querySelector('[data-testid="pool-header-actions"]')
|
||||
@@ -67,7 +72,9 @@ describe('PoolManagementHeader', () => {
|
||||
root.querySelector<HTMLButtonElement>('[title="刷新"]')?.click()
|
||||
|
||||
expect(events).toEqual([
|
||||
'prefetchProvider',
|
||||
'viewProvider',
|
||||
'import',
|
||||
'scheduling',
|
||||
'demandMetrics',
|
||||
'advanced',
|
||||
@@ -75,8 +82,11 @@ describe('PoolManagementHeader', () => {
|
||||
'batchAction:refresh_quota',
|
||||
'refresh',
|
||||
])
|
||||
const viewProviderButton = desktopActions?.querySelector<HTMLButtonElement>('[title="查看详情"]')
|
||||
const importButton = desktopActions?.querySelector<HTMLButtonElement>('[title="导入账号"]')
|
||||
expect(selectAllButton?.textContent?.trim()).toBe('')
|
||||
expect(selectAllButton?.getAttribute('title')).toBe('全选')
|
||||
expect(viewProviderButton?.nextElementSibling).toBe(importButton)
|
||||
expect(advancedButton?.nextElementSibling).toBe(selectAllButton)
|
||||
expect(selectAllButton?.nextElementSibling).toBe(batchActionsButton)
|
||||
expect(batchActionsButton?.getAttribute('title')).toBe('选择执行动作')
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<template>
|
||||
<!-- 自定义抽屉 -->
|
||||
<Teleport to="body">
|
||||
<Transition name="drawer">
|
||||
<Transition
|
||||
name="drawer"
|
||||
appear
|
||||
>
|
||||
<div
|
||||
v-if="open && (loading || provider)"
|
||||
class="fixed inset-0 z-50 flex justify-end"
|
||||
@@ -9,12 +12,12 @@
|
||||
>
|
||||
<!-- 背景遮罩 -->
|
||||
<div
|
||||
class="absolute inset-0 bg-black/30 backdrop-blur-sm"
|
||||
class="absolute inset-0 bg-black/30"
|
||||
@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
|
||||
v-if="loading"
|
||||
@@ -810,7 +813,7 @@
|
||||
|
||||
<!-- 端点表单对话框(管理/编辑) -->
|
||||
<EndpointFormDialog
|
||||
v-if="provider && open"
|
||||
v-if="provider && open && endpointDialogOpen"
|
||||
v-model="endpointDialogOpen"
|
||||
:provider="provider"
|
||||
:endpoints="endpoints"
|
||||
@@ -822,7 +825,7 @@
|
||||
|
||||
<!-- 密钥编辑对话框 -->
|
||||
<KeyFormDialog
|
||||
v-if="open"
|
||||
v-if="open && keyFormDialogOpen"
|
||||
:open="keyFormDialogOpen"
|
||||
:endpoint="currentEndpoint"
|
||||
:editing-key="editingKey"
|
||||
@@ -834,7 +837,7 @@
|
||||
/>
|
||||
|
||||
<ProviderKeyBatchImportDialog
|
||||
v-if="open && provider?.provider_type === 'custom'"
|
||||
v-if="open && keyBatchImportDialogOpen && provider?.provider_type === 'custom'"
|
||||
:open="keyBatchImportDialogOpen"
|
||||
:provider-id="provider.id"
|
||||
:provider-name="provider.name"
|
||||
@@ -845,7 +848,7 @@
|
||||
|
||||
<!-- OAuth 账号对话框 -->
|
||||
<OAuthAccountDialog
|
||||
v-if="open && provider"
|
||||
v-if="open && oauthAccountDialogOpen && provider"
|
||||
:open="oauthAccountDialogOpen"
|
||||
:provider-id="provider.id"
|
||||
:provider-type="provider.provider_type"
|
||||
@@ -855,7 +858,7 @@
|
||||
|
||||
<!-- OAuth 密钥编辑对话框 -->
|
||||
<OAuthKeyEditDialog
|
||||
v-if="open"
|
||||
v-if="open && oauthKeyEditDialogOpen"
|
||||
:open="oauthKeyEditDialogOpen"
|
||||
:editing-key="editingKey"
|
||||
@close="oauthKeyEditDialogOpen = false"
|
||||
@@ -864,7 +867,7 @@
|
||||
|
||||
<!-- 模型权限对话框 -->
|
||||
<KeyAllowedModelsEditDialog
|
||||
v-if="open"
|
||||
v-if="open && keyPermissionsDialogOpen"
|
||||
:open="keyPermissionsDialogOpen"
|
||||
:api-key="editingKey"
|
||||
:provider-id="providerId || ''"
|
||||
@@ -874,7 +877,7 @@
|
||||
|
||||
<!-- 删除密钥确认对话框 -->
|
||||
<AlertDialog
|
||||
v-if="open"
|
||||
v-if="open && deleteKeyConfirmOpen"
|
||||
:model-value="deleteKeyConfirmOpen"
|
||||
:title="legacyT('删除密钥')"
|
||||
:description="formatDeleteKeyConfirmDescription()"
|
||||
@@ -888,7 +891,7 @@
|
||||
|
||||
<!-- 添加/编辑模型对话框 -->
|
||||
<ProviderModelFormDialog
|
||||
v-if="open && provider"
|
||||
v-if="open && modelFormDialogOpen && provider"
|
||||
:open="modelFormDialogOpen"
|
||||
:provider-id="provider.id"
|
||||
:provider-name="provider.name"
|
||||
@@ -899,7 +902,7 @@
|
||||
|
||||
<!-- 批量关联模型对话框 -->
|
||||
<BatchAssignModelsDialog
|
||||
v-if="open && provider"
|
||||
v-if="open && batchAssignDialogOpen && provider"
|
||||
:open="batchAssignDialogOpen"
|
||||
:provider-id="provider.id"
|
||||
:provider-name="provider.name"
|
||||
@@ -921,6 +924,7 @@
|
||||
|
||||
<!-- 故障转移规则弹窗 -->
|
||||
<FailoverRulesDialog
|
||||
v-if="open && failoverRulesDialogOpen"
|
||||
:open="failoverRulesDialogOpen"
|
||||
:provider="provider ?? null"
|
||||
@update:open="failoverRulesDialogOpen = $event"
|
||||
@@ -3908,8 +3912,8 @@ useEscapeKey(() => {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.drawer-enter-active .relative,
|
||||
.drawer-leave-active .relative {
|
||||
.drawer-enter-active .drawer-panel,
|
||||
.drawer-leave-active .drawer-panel {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
@@ -3918,16 +3922,16 @@ useEscapeKey(() => {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.drawer-enter-from .relative {
|
||||
.drawer-enter-from .drawer-panel {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.drawer-leave-to .relative {
|
||||
.drawer-leave-to .drawer-panel {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.drawer-enter-to .relative,
|
||||
.drawer-leave-from .relative {
|
||||
.drawer-enter-to .drawer-panel,
|
||||
.drawer-leave-from .drawer-panel {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
|
||||
+25
@@ -8,6 +8,18 @@ const source = readFileSync(
|
||||
)
|
||||
|
||||
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', () => {
|
||||
const openWatcher = source
|
||||
.split('// 合并监听 providerId 和 open')[1]
|
||||
@@ -29,4 +41,17 @@ describe('ProviderDetailDrawer loading priorities', () => {
|
||||
expect(source).not.toContain(':loading="loadingProviderModels || loadingProviderKeys"')
|
||||
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"')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user