feat: 新增全局模型批量管理功能,优化提供商更新接口

- 模型管理页面新增批量管理对话框,支持搜索、快捷筛选和批量删除
- 快捷筛选支持:无提供商、无活跃提供商、已禁用、未调用、无价格
- 提供商 PATCH 接口返回完整的 ProviderWithEndpointsSummary
- 修复系统级格式转换开关的 tooltip 文案和按钮高亮样式
This commit is contained in:
fawney19
2026-02-28 00:01:15 +08:00
parent 5bfaee47cc
commit 2a0c684e88
3 changed files with 278 additions and 10 deletions

View File

@@ -39,11 +39,11 @@
</Badge>
</div>
<div class="flex items-center gap-1 shrink-0">
<span :title="systemFormatConversionEnabled ? '请先关闭系统级开关' : (provider.enable_format_conversion ? '已启用格式转换(点击关闭)' : '启用格式转换')">
<span :title="systemFormatConversionEnabled ? '系统级格式转换已启用' : (provider.enable_format_conversion ? '已启用格式转换(点击关闭)' : '启用格式转换')">
<Button
variant="ghost"
size="icon"
:class="`${provider.enable_format_conversion ? 'text-primary' : ''} ${systemFormatConversionEnabled ? 'opacity-50' : ''}`"
:class="(provider.enable_format_conversion || systemFormatConversionEnabled) ? 'text-primary' : ''"
:disabled="systemFormatConversionEnabled"
@click="toggleFormatConversion"
>

View File

@@ -27,6 +27,15 @@
</div>
<!-- 操作按钮 -->
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
title="批量管理"
@click="openBatchManageDialog"
>
<ListChecks class="w-3.5 h-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
@@ -488,6 +497,148 @@
@update:open="handleEditProviderDialogUpdate"
@saved="handleEditProviderSaved"
/>
<!-- 批量管理全局模型对话框 -->
<Dialog
:model-value="batchManageDialogOpen"
title="批量管理模型"
description="选择要删除的全局模型"
:icon="Trash2"
icon-class="bg-destructive/10"
size="2xl"
@update:model-value="batchManageDialogOpen = $event"
>
<template #default>
<div class="space-y-4">
<!-- 搜索栏 -->
<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="batchManageSearchQuery"
placeholder="搜索模型..."
class="pl-8 h-9"
/>
</div>
</div>
<!-- 快捷选中 -->
<div class="flex flex-wrap items-center gap-1.5">
<span class="text-xs text-muted-foreground mr-0.5">快捷选中:</span>
<button
v-for="shortcut in batchManageShortcuts"
:key="shortcut.label"
type="button"
class="text-xs px-2 py-1 rounded-md border border-border/60 hover:bg-muted transition-colors"
:title="shortcut.description"
@click="applyBatchManageShortcut(shortcut.filter)"
>
{{ shortcut.label }} ({{ shortcut.count }})
</button>
</div>
<!-- 模型列表 -->
<div class="border rounded-lg overflow-hidden">
<div class="max-h-96 overflow-y-auto">
<template v-if="filteredBatchManageModels.length > 0">
<div
class="flex items-center justify-between px-3 py-2 bg-muted sticky top-0 z-10"
>
<div class="flex items-center gap-2">
<span class="text-xs font-medium">模型</span>
<span class="text-xs text-muted-foreground">({{ filteredBatchManageModels.length }})</span>
</div>
<button
type="button"
class="text-xs text-primary hover:underline shrink-0"
@click="toggleAllBatchManageModels"
>
{{ isAllBatchManageModelsSelected ? '取消全选' : '全选' }}
</button>
</div>
<div class="space-y-1 p-2">
<div
v-for="model in filteredBatchManageModels"
:key="model.id"
class="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-muted cursor-pointer"
@click="toggleBatchManageModelSelection(model.id)"
>
<div
class="w-4 h-4 border rounded flex items-center justify-center shrink-0"
:class="selectedBatchManageModelIds.has(model.id) ? 'bg-primary border-primary' : ''"
>
<Check
v-if="selectedBatchManageModelIds.has(model.id)"
class="w-3 h-3 text-primary-foreground"
/>
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium truncate">
{{ model.display_name }}
</p>
<p class="text-xs text-muted-foreground truncate">
{{ model.name }}
</p>
</div>
<div class="flex items-center gap-2 shrink-0">
<Badge
variant="secondary"
class="text-xs"
>
{{ model.active_provider_count || 0 }}/{{ model.provider_count || 0 }}
</Badge>
<Badge
:variant="model.is_active ? 'outline' : 'secondary'"
:class="model.is_active ? 'text-green-600 border-green-500/60' : ''"
class="text-xs"
>
{{ model.is_active ? '活跃' : '停用' }}
</Badge>
</div>
</div>
</div>
</template>
<!-- 空状态 -->
<div
v-if="filteredBatchManageModels.length === 0"
class="flex flex-col items-center justify-center py-12 text-muted-foreground"
>
<p class="text-sm">
{{ batchManageSearchQuery ? '无匹配结果' : '暂无模型' }}
</p>
</div>
</div>
</div>
</div>
</template>
<template #footer>
<div class="flex items-center justify-between w-full">
<p class="text-xs text-muted-foreground">
{{ selectedBatchManageModelIds.size > 0 ? `已选择 ${selectedBatchManageModelIds.size} 个模型` : '' }}
</p>
<div class="flex items-center gap-2">
<Button
variant="destructive"
:disabled="selectedBatchManageModelIds.size === 0 || submittingBatchManage"
@click="confirmBatchDeleteModels"
>
<Loader2
v-if="submittingBatchManage"
class="w-4 h-4 mr-1 animate-spin"
/>
{{ submittingBatchManage ? '删除中...' : '删除选中' }}
</Button>
<Button
variant="outline"
@click="batchManageDialogOpen = false"
>
关闭
</Button>
</div>
</div>
</template>
</Dialog>
</div>
</template>
@@ -505,6 +656,7 @@ import {
Copy,
Server,
Check,
ListChecks,
} from 'lucide-vue-next'
import ModelDetailDrawer from '@/features/models/components/ModelDetailDrawer.vue'
import GlobalModelFormDialog from '@/features/models/components/GlobalModelFormDialog.vue'
@@ -606,6 +758,12 @@ const initialBatchProviderIds = ref<Set<string>>(new Set())
const editProviderDialogOpen = ref(false)
const editingProvider = ref<ModelProviderDisplay | null>(null)
// 批量管理全局模型
const batchManageDialogOpen = ref(false)
const batchManageSearchQuery = ref('')
const selectedBatchManageModelIds = ref<Set<string>>(new Set())
const submittingBatchManage = ref(false)
// 将 provider 数据转换为 Model 类型供 ProviderModelFormDialog 使用
const editingProviderModel = computed<Model | null>(() => {
if (!editingProvider.value) return null
@@ -700,7 +858,8 @@ function getVideoPricingTooltip(model: GlobalModelResponse): string {
const hasBlockingDialogOpen = computed(() =>
createModelDialogOpen.value ||
batchAddProvidersDialogOpen.value ||
editProviderDialogOpen.value
editProviderDialogOpen.value ||
batchManageDialogOpen.value
)
// 能力筛选
@@ -1074,6 +1233,117 @@ function closeBatchAddProvidersDialog() {
submittingBatchProviders.value = false
}
// 批量管理全局模型 - 过滤
const filteredBatchManageModels = computed(() => {
const query = batchManageSearchQuery.value.toLowerCase().trim()
if (!query) return globalModels.value
return globalModels.value.filter(m => {
const searchableText = `${m.name} ${m.display_name || ''}`.toLowerCase()
return searchableText.includes(query)
})
})
// 批量管理 - 快捷筛选定义
function hasNoPrice(m: GlobalModelResponse): boolean {
return !getFirstTierPrice(m, 'input') && !getFirstTierPrice(m, 'output')
&& !m.default_price_per_request
}
const batchManageShortcuts = computed(() => {
const models = globalModels.value
const defs: { label: string; description: string; filter: (m: GlobalModelResponse) => boolean }[] = [
{ label: '无提供商', description: '没有关联任何提供商的模型', filter: m => (m.provider_count || 0) === 0 },
{ label: '无活跃提供商', description: '有提供商但没有活跃提供商的模型', filter: m => (m.active_provider_count || 0) === 0 && (m.provider_count || 0) > 0 },
{ label: '已禁用', description: '被禁用的模型', filter: m => !m.is_active },
{ label: '未调用', description: '没有调用记录的模型', filter: m => (m.usage_count || 0) === 0 },
{ label: '无价格', description: '没有配置任何价格的模型', filter: m => hasNoPrice(m) },
]
return defs.map(d => ({ ...d, count: models.filter(d.filter).length }))
})
// 批量管理 - 应用快捷选中
function applyBatchManageShortcut(filter: (m: GlobalModelResponse) => boolean) {
const matchedIds = globalModels.value.filter(filter).map(m => m.id)
selectedBatchManageModelIds.value = new Set(matchedIds)
}
// 批量管理 - 是否全选
const isAllBatchManageModelsSelected = computed(() => {
if (filteredBatchManageModels.value.length === 0) return false
return filteredBatchManageModels.value.every(m => selectedBatchManageModelIds.value.has(m.id))
})
// 批量管理 - 切换单个选择
function toggleBatchManageModelSelection(modelId: string) {
if (selectedBatchManageModelIds.value.has(modelId)) {
selectedBatchManageModelIds.value.delete(modelId)
} else {
selectedBatchManageModelIds.value.add(modelId)
}
selectedBatchManageModelIds.value = new Set(selectedBatchManageModelIds.value)
}
// 批量管理 - 全选/取消全选
function toggleAllBatchManageModels() {
const allIds = filteredBatchManageModels.value.map(m => m.id)
if (isAllBatchManageModelsSelected.value) {
for (const id of allIds) {
selectedBatchManageModelIds.value.delete(id)
}
} else {
for (const id of allIds) {
selectedBatchManageModelIds.value.add(id)
}
}
selectedBatchManageModelIds.value = new Set(selectedBatchManageModelIds.value)
}
// 打开批量管理对话框
function openBatchManageDialog() {
batchManageSearchQuery.value = ''
selectedBatchManageModelIds.value = new Set()
batchManageDialogOpen.value = true
}
// 确认批量删除模型
async function confirmBatchDeleteModels() {
const count = selectedBatchManageModelIds.value.size
if (count === 0) return
const confirmed = await confirmDanger(
`确定删除选中的 ${count} 个模型吗?\n\n此操作不可撤销。`,
'批量删除模型'
)
if (!confirmed) return
submittingBatchManage.value = true
try {
const ids = Array.from(selectedBatchManageModelIds.value)
const results = await Promise.allSettled(ids.map(id => deleteGlobalModel(id)))
const successCount = results.filter(r => r.status === 'fulfilled').length
const failCount = results.filter(r => r.status === 'rejected').length
if (successCount > 0) {
success(`成功删除 ${successCount} 个模型`)
}
if (failCount > 0) {
showError(`${failCount} 个模型删除失败`, '部分失败')
}
// 清除选中的已删除模型
if (selectedModel.value && selectedBatchManageModelIds.value.has(selectedModel.value.id)) {
selectedModel.value = null
}
selectedBatchManageModelIds.value = new Set()
await loadGlobalModels()
} catch (err: unknown) {
showError(parseApiError(err, '批量删除失败'), '错误')
} finally {
submittingBatchManage.value = false
}
}
// 抽屉控制函数
function handleDrawerOpenChange(value: boolean) {
if (!value && !hasBlockingDialogOpen.value) {