Merge pull request #868 from AAEE86/feat/batch-assign-create-model

feat(providers): 批量管理模型支持创建统一模型并刷新列表
This commit is contained in:
ZheFox
2026-09-29 14:37:34 +08:00
committed by GitHub
3 changed files with 124 additions and 1 deletions
@@ -5,6 +5,7 @@
:description="isEditMode ? '修改模型配置和价格信息' : ''" :description="isEditMode ? '修改模型配置和价格信息' : ''"
:icon="isEditMode ? SquarePen : Layers" :icon="isEditMode ? SquarePen : Layers"
:size="isEditMode ? '4xl' : '3xl'" :size="isEditMode ? '4xl' : '3xl'"
:z-index="zIndex"
@update:model-value="handleDialogUpdate" @update:model-value="handleDialogUpdate"
> >
<div <div
@@ -869,6 +870,8 @@ import {
const props = defineProps<{ const props = defineProps<{
open: boolean open: boolean
model?: GlobalModelResponse | null model?: GlobalModelResponse | null
// 嵌套在其他弹窗内部时使用更高的层级,避免被父弹窗遮挡
zIndex?: number
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
@@ -62,6 +62,16 @@
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
<Button
variant="outline"
size="sm"
class="shrink-0 gap-1.5"
data-testid="batch-assign-create-model"
@click="openCreateModelDialog"
>
<Plus class="w-4 h-4" />
创建模型
</Button>
</div> </div>
<!-- 模型列表 --> <!-- 模型列表 -->
@@ -139,6 +149,15 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 创建统一模型对话框(嵌套,创建成功后刷新下方模型列表)
注意:不使用 v-if 挂载,GlobalModelFormDialog 依赖 open 变化来加载模型目录 -->
<GlobalModelFormDialog
:open="createModelDialogOpen"
:z-index="70"
@update:open="createModelDialogOpen = $event"
@success="handleGlobalModelCreated"
/>
</template> </template>
<template #footer> <template #footer>
<div class="flex items-center justify-between w-full"> <div class="flex items-center justify-between w-full">
@@ -170,10 +189,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch } from 'vue' import { ref, computed, watch } from 'vue'
import { Layers, Loader2, Search, Check, ListChecks } from 'lucide-vue-next' import { Layers, Loader2, Search, Check, ListChecks, Plus } from 'lucide-vue-next'
import Dialog from '@/components/ui/dialog/Dialog.vue' import Dialog from '@/components/ui/dialog/Dialog.vue'
import Button from '@/components/ui/button.vue' import Button from '@/components/ui/button.vue'
import Input from '@/components/ui/input.vue' import Input from '@/components/ui/input.vue'
import GlobalModelFormDialog from '@/features/models/components/GlobalModelFormDialog.vue'
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuTrigger, DropdownMenuTrigger,
@@ -227,6 +247,8 @@ const loadingGlobalModels = ref(false)
const loadingProviderKeys = ref(false) const loadingProviderKeys = ref(false)
const saving = ref(false) const saving = ref(false)
const fetchingAutoMatchedModels = ref(false) const fetchingAutoMatchedModels = ref(false)
// 创建统一模型对话框是否打开(嵌套在本弹窗内)
const createModelDialogOpen = ref(false)
// 数据 // 数据
const allGlobalModels = ref<GlobalModelResponse[]>([]) const allGlobalModels = ref<GlobalModelResponse[]>([])
@@ -422,6 +444,16 @@ async function applyAutoMatchFromKey(key: AutoMatchKey) {
} }
} }
// 打开"创建统一模型"对话框
function openCreateModelDialog() {
createModelDialogOpen.value = true
}
// 统一模型创建成功:刷新下方全局模型列表,便于直接勾选新模型
async function handleGlobalModelCreated() {
await loadGlobalModels()
}
// 处理关闭 // 处理关闭
async function handleClose() { async function handleClose() {
if (hasChanges.value) { if (hasChanges.value) {
@@ -517,6 +549,7 @@ watch(
initialGlobalModelIds.value = new Set() initialGlobalModelIds.value = new Set()
providerKeys.value = [] providerKeys.value = []
fetchingAutoMatchedModels.value = false fetchingAutoMatchedModels.value = false
createModelDialogOpen.value = false
} }
}, },
{ immediate: true }, { immediate: true },
@@ -33,6 +33,32 @@ vi.mock('@/features/providers/composables/useUpstreamModelsCache', () => ({
fetchModels: vi.fn(), fetchModels: vi.fn(),
}), }),
})) }))
vi.mock('@/features/models/components/GlobalModelFormDialog.vue', async () => {
const { defineComponent, h } = await import('vue')
return {
default: defineComponent({
name: 'GlobalModelFormDialogStub',
props: {
open: { type: Boolean, default: false },
model: { type: Object, default: null },
zIndex: { type: Number, default: undefined },
},
emits: ['update:open', 'success'],
setup(props, { emit }) {
return () => {
if (!props.open) return null
return h('div', { 'data-testid': 'global-model-form-dialog-stub' }, [
h('button', {
type: 'button',
'data-testid': 'global-model-form-dialog-stub-success',
onClick: () => emit('success'),
}, 'submit'),
])
}
},
}),
}
})
vi.mock('@/components/ui/dialog/Dialog.vue', async () => { vi.mock('@/components/ui/dialog/Dialog.vue', async () => {
const { defineComponent, h } = await import('vue') const { defineComponent, h } = await import('vue')
return { return {
@@ -203,3 +229,64 @@ describe('BatchAssignModelsDialog loading', () => {
expect(visibleModelIds(root)).toEqual(['gm-beta', 'gm-alpha']) expect(visibleModelIds(root)).toEqual(['gm-beta', 'gm-alpha'])
}) })
}) })
describe('BatchAssignModelsDialog create model entry', () => {
async function mountDialog() {
const root = document.createElement('div')
document.body.appendChild(root)
const app = createApp(defineComponent({
setup() {
return () => h(BatchAssignModelsDialog, {
open: true,
providerId: 'provider-1',
})
},
}))
app.mount(root)
mountedApps.push({ app, root })
await settle()
return root
}
it('opens the unified model dialog from the create button', async () => {
const root = await mountDialog()
expect(root.querySelector('[data-testid="global-model-form-dialog-stub"]')).toBeNull()
const createButton = root.querySelector('[data-testid="batch-assign-create-model"]') as HTMLButtonElement
expect(createButton).toBeTruthy()
createButton.click()
await settle()
expect(root.querySelector('[data-testid="global-model-form-dialog-stub"]')).not.toBeNull()
})
it('refreshes the global model list after a model is created', async () => {
globalModelMocks.getGlobalModels
.mockResolvedValueOnce({
models: [createGlobalModel('gm-old', 'old-model', 'Old Model')],
total: 1,
})
.mockResolvedValueOnce({
models: [
createGlobalModel('gm-old', 'old-model', 'Old Model'),
createGlobalModel('gm-new', 'new-model', 'New Model'),
],
total: 2,
})
const root = await mountDialog()
expect(visibleModelIds(root)).toEqual(['gm-old'])
const createButton = root.querySelector('[data-testid="batch-assign-create-model"]') as HTMLButtonElement
createButton.click()
await settle()
const successButton = root.querySelector('[data-testid="global-model-form-dialog-stub-success"]') as HTMLButtonElement
expect(successButton).toBeTruthy()
successButton.click()
await settle()
expect(globalModelMocks.getGlobalModels).toHaveBeenCalledTimes(2)
expect(visibleModelIds(root)).toEqual(['gm-new', 'gm-old'])
})
})