2025-12-10 20:52:44 +08:00
|
|
|
<template>
|
|
|
|
|
<Dialog
|
|
|
|
|
:model-value="open"
|
2026-01-11 19:33:16 +08:00
|
|
|
:title="providerName ? `批量管理模型 - ${providerName}` : '批量管理模型'"
|
|
|
|
|
description="选中的模型将被关联到提供商,取消选中将移除关联"
|
2025-12-10 20:52:44 +08:00
|
|
|
:icon="Layers"
|
2026-01-11 19:33:16 +08:00
|
|
|
size="2xl"
|
|
|
|
|
@update:model-value="handleDialogUpdate"
|
2025-12-10 20:52:44 +08:00
|
|
|
>
|
|
|
|
|
<template #default>
|
|
|
|
|
<div class="space-y-4">
|
2026-01-11 19:33:16 +08:00
|
|
|
<!-- 搜索栏 -->
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
<div class="flex-1 relative">
|
|
|
|
|
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
|
|
|
|
<Input
|
|
|
|
|
v-model="searchQuery"
|
|
|
|
|
placeholder="搜索模型..."
|
|
|
|
|
class="pl-8 h-9"
|
|
|
|
|
/>
|
2025-12-10 20:52:44 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-02-27 13:54:46 +08:00
|
|
|
<!-- 模型列表 -->
|
2026-01-11 19:33:16 +08:00
|
|
|
<div class="border rounded-lg overflow-hidden">
|
|
|
|
|
<div class="max-h-96 overflow-y-auto">
|
|
|
|
|
<div
|
|
|
|
|
v-if="loadingGlobalModels"
|
|
|
|
|
class="flex items-center justify-center py-12"
|
|
|
|
|
>
|
|
|
|
|
<Loader2 class="w-6 h-6 animate-spin text-primary" />
|
2025-12-10 20:52:44 +08:00
|
|
|
</div>
|
2026-01-11 19:33:16 +08:00
|
|
|
|
|
|
|
|
<template v-else>
|
2026-02-27 13:54:46 +08:00
|
|
|
<!-- 全局模型列表 -->
|
|
|
|
|
<div v-if="filteredGlobalModels.length > 0">
|
2025-12-10 20:52:44 +08:00
|
|
|
<div
|
2026-02-27 13:54:46 +08:00
|
|
|
class="flex items-center justify-between px-3 py-2 bg-muted sticky top-0 z-10"
|
2025-12-10 20:52:44 +08:00
|
|
|
>
|
2026-01-11 19:33:16 +08:00
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
<span class="text-xs font-medium">全局模型</span>
|
|
|
|
|
<span class="text-xs text-muted-foreground">({{ filteredGlobalModels.length }})</span>
|
2025-12-10 20:52:44 +08:00
|
|
|
</div>
|
2026-01-11 19:33:16 +08:00
|
|
|
<button
|
|
|
|
|
v-if="filteredGlobalModels.length > 0"
|
|
|
|
|
type="button"
|
|
|
|
|
class="text-xs text-primary hover:underline shrink-0"
|
|
|
|
|
@click.stop="toggleAllGlobalModels"
|
|
|
|
|
>
|
|
|
|
|
{{ isAllGlobalModelsSelected ? '取消全选' : '全选' }}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
2026-02-27 13:54:46 +08:00
|
|
|
<div class="space-y-1 p-2">
|
2026-01-11 19:33:16 +08:00
|
|
|
<div
|
|
|
|
|
v-for="model in filteredGlobalModels"
|
|
|
|
|
:key="model.id"
|
|
|
|
|
class="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-muted cursor-pointer"
|
|
|
|
|
@click="toggleGlobalModelSelection(model.id)"
|
2025-12-10 20:52:44 +08:00
|
|
|
>
|
2025-12-20 02:01:17 +08:00
|
|
|
<div
|
2026-01-11 19:33:16 +08:00
|
|
|
class="w-4 h-4 border rounded flex items-center justify-center shrink-0"
|
|
|
|
|
:class="isGlobalModelSelected(model.id) ? 'bg-primary border-primary' : ''"
|
2025-12-20 02:01:17 +08:00
|
|
|
>
|
2026-01-11 19:33:16 +08:00
|
|
|
<Check
|
|
|
|
|
v-if="isGlobalModelSelected(model.id)"
|
|
|
|
|
class="w-3 h-3 text-primary-foreground"
|
2025-12-20 02:01:17 +08:00
|
|
|
/>
|
2026-01-11 19:33:16 +08:00
|
|
|
</div>
|
|
|
|
|
<div class="flex-1 min-w-0">
|
2026-01-12 23:28:37 +08:00
|
|
|
<p class="text-sm font-medium truncate">
|
|
|
|
|
{{ model.display_name }}
|
|
|
|
|
</p>
|
|
|
|
|
<p class="text-xs text-muted-foreground truncate font-mono">
|
|
|
|
|
{{ model.name }}
|
|
|
|
|
</p>
|
2025-12-20 02:01:17 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-01-11 19:33:16 +08:00
|
|
|
</div>
|
2025-12-20 02:01:17 +08:00
|
|
|
|
2026-01-11 19:33:16 +08:00
|
|
|
<!-- 空状态 -->
|
2025-12-12 16:15:36 +08:00
|
|
|
<div
|
2026-02-27 13:54:46 +08:00
|
|
|
v-if="filteredGlobalModels.length === 0"
|
2026-01-11 19:33:16 +08:00
|
|
|
class="flex flex-col items-center justify-center py-12 text-muted-foreground"
|
2025-12-12 16:15:36 +08:00
|
|
|
>
|
2025-12-10 20:52:44 +08:00
|
|
|
<Layers class="w-10 h-10 mb-2 opacity-30" />
|
2026-01-12 23:28:37 +08:00
|
|
|
<p class="text-sm">
|
2026-02-27 13:54:46 +08:00
|
|
|
{{ searchQuery ? '无匹配结果' : '暂无可用全局模型' }}
|
2026-01-12 23:28:37 +08:00
|
|
|
</p>
|
2026-02-27 13:54:46 +08:00
|
|
|
<p class="text-xs mt-1">
|
|
|
|
|
请先前往"模型目录"页面创建全局模型
|
2026-01-11 19:33:16 +08:00
|
|
|
</p>
|
2025-12-10 20:52:44 +08:00
|
|
|
</div>
|
2026-01-11 19:33:16 +08:00
|
|
|
</template>
|
2025-12-10 20:52:44 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<template #footer>
|
2026-01-11 19:33:16 +08:00
|
|
|
<div class="flex items-center justify-between w-full">
|
|
|
|
|
<p class="text-xs text-muted-foreground">
|
|
|
|
|
{{ hasChanges ? `${pendingChangesCount} 项更改待保存` : '' }}
|
|
|
|
|
</p>
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
<Button
|
|
|
|
|
:disabled="!hasChanges || saving"
|
|
|
|
|
@click="handleSave"
|
|
|
|
|
>
|
|
|
|
|
<Loader2
|
|
|
|
|
v-if="saving"
|
|
|
|
|
class="w-4 h-4 mr-1 animate-spin"
|
|
|
|
|
/>
|
|
|
|
|
{{ saving ? '保存中...' : '保存' }}
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
@click="handleClose"
|
|
|
|
|
>
|
|
|
|
|
关闭
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-12-10 20:52:44 +08:00
|
|
|
</template>
|
|
|
|
|
</Dialog>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { ref, computed, watch } from 'vue'
|
2026-02-27 13:54:46 +08:00
|
|
|
import { Layers, Loader2, Search, Check } from 'lucide-vue-next'
|
2025-12-10 20:52:44 +08:00
|
|
|
import Dialog from '@/components/ui/dialog/Dialog.vue'
|
|
|
|
|
import Button from '@/components/ui/button.vue'
|
2025-12-20 02:01:17 +08:00
|
|
|
import Input from '@/components/ui/input.vue'
|
2025-12-10 20:52:44 +08:00
|
|
|
import { useToast } from '@/composables/useToast'
|
2026-01-11 19:33:16 +08:00
|
|
|
import { useConfirm } from '@/composables/useConfirm'
|
2025-12-10 20:52:44 +08:00
|
|
|
import { parseApiError } from '@/utils/errorParser'
|
|
|
|
|
import {
|
|
|
|
|
getGlobalModels,
|
|
|
|
|
type GlobalModelResponse
|
|
|
|
|
} from '@/api/endpoints/global-models'
|
|
|
|
|
import {
|
|
|
|
|
getProviderModels,
|
|
|
|
|
batchAssignModelsToProvider,
|
|
|
|
|
deleteModel,
|
|
|
|
|
type Model
|
|
|
|
|
} from '@/api/endpoints'
|
2025-12-20 02:01:17 +08:00
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
const props = defineProps<{
|
|
|
|
|
open: boolean
|
|
|
|
|
providerId: string
|
2026-01-11 19:33:16 +08:00
|
|
|
providerName?: string
|
2025-12-10 20:52:44 +08:00
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
'update:open': [value: boolean]
|
|
|
|
|
'changed': []
|
|
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
const { error: showError, success } = useToast()
|
2026-01-11 19:33:16 +08:00
|
|
|
const { confirmWarning } = useConfirm()
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
// 状态
|
|
|
|
|
const loadingGlobalModels = ref(false)
|
2026-01-11 19:33:16 +08:00
|
|
|
const saving = ref(false)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
// 数据
|
|
|
|
|
const allGlobalModels = ref<GlobalModelResponse[]>([])
|
|
|
|
|
const existingModels = ref<Model[]>([])
|
|
|
|
|
|
2026-01-11 19:33:16 +08:00
|
|
|
// 选择状态(本地状态,保存时才提交)
|
|
|
|
|
const selectedGlobalModelIds = ref<Set<string>>(new Set())
|
|
|
|
|
|
|
|
|
|
// 初始状态(用于计算变更)
|
|
|
|
|
const initialGlobalModelIds = ref<Set<string>>(new Set())
|
2025-12-20 02:01:17 +08:00
|
|
|
|
|
|
|
|
// 搜索状态
|
|
|
|
|
const searchQuery = ref('')
|
|
|
|
|
|
2026-01-11 19:33:16 +08:00
|
|
|
// 已关联的全局模型 ID 集合(从已有数据计算)
|
|
|
|
|
const existingGlobalModelIds = computed(() => {
|
|
|
|
|
return new Set(
|
2025-12-10 20:52:44 +08:00
|
|
|
existingModels.value
|
|
|
|
|
.map(m => m.global_model_id)
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
|
2026-01-11 19:33:16 +08:00
|
|
|
// 过滤后的全局模型
|
|
|
|
|
const filteredGlobalModels = computed(() => {
|
|
|
|
|
const query = searchQuery.value.toLowerCase().trim()
|
|
|
|
|
return allGlobalModels.value.filter(m => {
|
|
|
|
|
if (query && !m.name.toLowerCase().includes(query) && !m.display_name.toLowerCase().includes(query)) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
})
|
2025-12-20 02:01:17 +08:00
|
|
|
})
|
|
|
|
|
|
2026-02-27 13:54:46 +08:00
|
|
|
// 全局模型是否全选
|
|
|
|
|
const isAllGlobalModelsSelected = computed(() => {
|
|
|
|
|
if (filteredGlobalModels.value.length === 0) return false
|
|
|
|
|
return filteredGlobalModels.value.every(m => isGlobalModelSelected(m.id))
|
2026-01-29 22:58:37 +08:00
|
|
|
})
|
|
|
|
|
|
2026-01-11 19:33:16 +08:00
|
|
|
// 检查全局模型是否已选中
|
|
|
|
|
function isGlobalModelSelected(globalModelId: string): boolean {
|
|
|
|
|
return selectedGlobalModelIds.value.has(globalModelId)
|
|
|
|
|
}
|
2025-12-20 02:01:17 +08:00
|
|
|
|
2026-01-11 19:33:16 +08:00
|
|
|
// 计算待添加的全局模型
|
|
|
|
|
const globalModelsToAdd = computed(() => {
|
|
|
|
|
const toAdd: string[] = []
|
|
|
|
|
for (const id of selectedGlobalModelIds.value) {
|
|
|
|
|
if (!initialGlobalModelIds.value.has(id)) {
|
|
|
|
|
toAdd.push(id)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return toAdd
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 计算待移除的全局模型
|
|
|
|
|
const globalModelsToRemove = computed(() => {
|
|
|
|
|
const toRemove: string[] = []
|
|
|
|
|
for (const id of initialGlobalModelIds.value) {
|
|
|
|
|
if (!selectedGlobalModelIds.value.has(id)) {
|
|
|
|
|
toRemove.push(id)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return toRemove
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 是否有变更
|
|
|
|
|
const hasChanges = computed(() => {
|
|
|
|
|
return globalModelsToAdd.value.length > 0 ||
|
2026-02-27 13:54:46 +08:00
|
|
|
globalModelsToRemove.value.length > 0
|
2026-01-11 19:33:16 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 待变更数量
|
|
|
|
|
const pendingChangesCount = computed(() => {
|
|
|
|
|
return globalModelsToAdd.value.length +
|
2026-02-27 13:54:46 +08:00
|
|
|
globalModelsToRemove.value.length
|
2026-01-11 19:33:16 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 切换全局模型选择
|
|
|
|
|
function toggleGlobalModelSelection(id: string) {
|
|
|
|
|
if (selectedGlobalModelIds.value.has(id)) {
|
|
|
|
|
selectedGlobalModelIds.value.delete(id)
|
|
|
|
|
} else {
|
|
|
|
|
selectedGlobalModelIds.value.add(id)
|
|
|
|
|
}
|
|
|
|
|
selectedGlobalModelIds.value = new Set(selectedGlobalModelIds.value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 全选/取消全选全局模型
|
|
|
|
|
function toggleAllGlobalModels() {
|
|
|
|
|
const allIds = filteredGlobalModels.value.map(m => m.id)
|
|
|
|
|
if (isAllGlobalModelsSelected.value) {
|
|
|
|
|
for (const id of allIds) {
|
|
|
|
|
selectedGlobalModelIds.value.delete(id)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
for (const id of allIds) {
|
|
|
|
|
selectedGlobalModelIds.value.add(id)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
selectedGlobalModelIds.value = new Set(selectedGlobalModelIds.value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理关闭
|
|
|
|
|
async function handleClose() {
|
|
|
|
|
if (hasChanges.value) {
|
|
|
|
|
const confirmed = await confirmWarning('有未保存的更改,确定要关闭吗?', '放弃更改')
|
|
|
|
|
if (!confirmed) return
|
|
|
|
|
}
|
|
|
|
|
emit('update:open', false)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理对话框状态变更
|
|
|
|
|
async function handleDialogUpdate(value: boolean) {
|
|
|
|
|
if (!value && hasChanges.value) {
|
|
|
|
|
const confirmed = await confirmWarning('有未保存的更改,确定要关闭吗?', '放弃更改')
|
|
|
|
|
if (!confirmed) return
|
|
|
|
|
}
|
|
|
|
|
emit('update:open', value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 保存变更
|
|
|
|
|
async function handleSave() {
|
|
|
|
|
if (!hasChanges.value || saving.value) return
|
|
|
|
|
|
|
|
|
|
saving.value = true
|
2026-02-11 03:09:26 +08:00
|
|
|
let hasAnyOperation = false
|
2026-01-11 19:33:16 +08:00
|
|
|
try {
|
|
|
|
|
let totalSuccess = 0
|
|
|
|
|
const allErrors: string[] = []
|
|
|
|
|
|
|
|
|
|
// 移除全局模型
|
|
|
|
|
for (const globalModelId of globalModelsToRemove.value) {
|
|
|
|
|
const existingModel = existingModels.value.find(m => m.global_model_id === globalModelId)
|
|
|
|
|
if (existingModel) {
|
2026-02-11 03:09:26 +08:00
|
|
|
hasAnyOperation = true
|
2026-01-11 19:33:16 +08:00
|
|
|
try {
|
|
|
|
|
await deleteModel(props.providerId, existingModel.id)
|
|
|
|
|
totalSuccess++
|
2026-02-22 00:43:41 +08:00
|
|
|
} catch (err: unknown) {
|
2026-01-11 19:33:16 +08:00
|
|
|
allErrors.push(parseApiError(err, '移除失败'))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 添加全局模型
|
|
|
|
|
if (globalModelsToAdd.value.length > 0) {
|
2026-02-11 03:09:26 +08:00
|
|
|
hasAnyOperation = true
|
|
|
|
|
try {
|
|
|
|
|
const result = await batchAssignModelsToProvider(props.providerId, globalModelsToAdd.value)
|
|
|
|
|
totalSuccess += result.success.length
|
|
|
|
|
if (result.errors.length > 0) {
|
|
|
|
|
allErrors.push(...result.errors.map(e => e.error))
|
|
|
|
|
}
|
2026-02-22 00:43:41 +08:00
|
|
|
} catch (err: unknown) {
|
2026-02-11 03:09:26 +08:00
|
|
|
allErrors.push(parseApiError(err, '批量添加全局模型失败'))
|
2026-01-11 19:33:16 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (totalSuccess > 0) {
|
|
|
|
|
success(`成功处理 ${totalSuccess} 个模型`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (allErrors.length > 0) {
|
|
|
|
|
showError(`部分操作失败: ${allErrors.slice(0, 3).join(', ')}${allErrors.length > 3 ? '...' : ''}`, '警告')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
emit('changed')
|
|
|
|
|
emit('update:open', false)
|
2026-02-22 00:43:41 +08:00
|
|
|
} catch (err: unknown) {
|
2026-01-11 19:33:16 +08:00
|
|
|
showError(parseApiError(err, '保存失败'), '错误')
|
2026-02-11 03:09:26 +08:00
|
|
|
if (hasAnyOperation) {
|
|
|
|
|
emit('changed')
|
|
|
|
|
}
|
2026-01-11 19:33:16 +08:00
|
|
|
} finally {
|
|
|
|
|
saving.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-27 13:54:46 +08:00
|
|
|
// 从已有数据同步选择状态
|
2026-01-11 19:33:16 +08:00
|
|
|
function syncGlobalModelSelection() {
|
|
|
|
|
const globalIds = [...existingGlobalModelIds.value].filter((id): id is string => id !== undefined)
|
|
|
|
|
selectedGlobalModelIds.value = new Set(globalIds)
|
|
|
|
|
initialGlobalModelIds.value = new Set(globalIds)
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
// 监听打开状态
|
|
|
|
|
watch(() => props.open, async (isOpen) => {
|
|
|
|
|
if (isOpen && props.providerId) {
|
|
|
|
|
await loadData()
|
|
|
|
|
} else {
|
2025-12-20 02:01:17 +08:00
|
|
|
searchQuery.value = ''
|
2026-01-11 19:33:16 +08:00
|
|
|
selectedGlobalModelIds.value = new Set()
|
|
|
|
|
initialGlobalModelIds.value = new Set()
|
2025-12-10 20:52:44 +08:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 加载数据
|
|
|
|
|
async function loadData() {
|
|
|
|
|
await Promise.all([loadGlobalModels(), loadExistingModels()])
|
2026-01-11 19:33:16 +08:00
|
|
|
syncGlobalModelSelection()
|
2025-12-10 20:52:44 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 加载全局模型列表
|
|
|
|
|
async function loadGlobalModels() {
|
|
|
|
|
try {
|
|
|
|
|
loadingGlobalModels.value = true
|
|
|
|
|
const response = await getGlobalModels({ limit: 1000 })
|
|
|
|
|
allGlobalModels.value = response.models
|
2026-02-22 00:43:41 +08:00
|
|
|
} catch (err: unknown) {
|
2025-12-10 20:52:44 +08:00
|
|
|
showError(parseApiError(err, '加载全局模型失败'), '错误')
|
|
|
|
|
} finally {
|
|
|
|
|
loadingGlobalModels.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 加载已关联的模型
|
|
|
|
|
async function loadExistingModels() {
|
|
|
|
|
try {
|
|
|
|
|
existingModels.value = await getProviderModels(props.providerId)
|
2026-02-22 00:43:41 +08:00
|
|
|
} catch (err: unknown) {
|
2025-12-10 20:52:44 +08:00
|
|
|
showError(parseApiError(err, '加载已关联模型失败'), '错误')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</script>
|