refactor(frontend): refactor model management with aliases, remove mappings UI

This commit is contained in:
fawney19
2025-12-15 14:30:42 +08:00
parent 743f23e640
commit 03ee6c16d9
14 changed files with 62 additions and 1369 deletions

View File

@@ -526,14 +526,7 @@
@edit-model="handleEditModel"
@delete-model="handleDeleteModel"
@batch-assign="handleBatchAssign"
/>
<!-- 模型映射 -->
<MappingsTab
v-if="provider"
:key="`mappings-${provider.id}`"
:provider="provider"
@refresh="handleRelatedDataRefresh"
@manage-alias="handleManageAlias"
/>
</div>
</template>
@@ -636,6 +629,16 @@
@update:open="batchAssignDialogOpen = $event"
@changed="handleBatchAssignChanged"
/>
<!-- 模型别名管理对话框 -->
<ModelAliasDialog
v-if="open && provider"
:open="aliasDialogOpen"
:provider-id="provider.id"
:model="aliasEditingModel"
@update:open="aliasDialogOpen = $event"
@saved="handleAliasSaved"
/>
</template>
<script setup lang="ts">
@@ -663,9 +666,9 @@ import { getProvider, getProviderEndpoints } from '@/api/endpoints'
import {
KeyFormDialog,
KeyAllowedModelsDialog,
MappingsTab,
ModelsTab,
BatchAssignModelsDialog
BatchAssignModelsDialog,
ModelAliasDialog
} from '@/features/providers/components'
import EndpointFormDialog from '@/features/providers/components/EndpointFormDialog.vue'
import ProviderModelFormDialog from '@/features/providers/components/ProviderModelFormDialog.vue'
@@ -734,6 +737,10 @@ const deleteModelConfirmOpen = ref(false)
const modelToDelete = ref<Model | null>(null)
const batchAssignDialogOpen = ref(false)
// 别名管理相关状态
const aliasDialogOpen = ref(false)
const aliasEditingModel = ref<Model | null>(null)
// 拖动排序相关状态
const dragState = ref({
isDragging: false,
@@ -755,7 +762,8 @@ const hasBlockingDialogOpen = computed(() =>
deleteKeyConfirmOpen.value ||
modelFormDialogOpen.value ||
deleteModelConfirmOpen.value ||
batchAssignDialogOpen.value
batchAssignDialogOpen.value ||
aliasDialogOpen.value
)
// 监听 providerId 变化
@@ -784,6 +792,7 @@ watch(() => props.open, (newOpen) => {
keyAllowedModelsDialogOpen.value = false
deleteKeyConfirmOpen.value = false
batchAssignDialogOpen.value = false
aliasDialogOpen.value = false
// 重置临时数据
endpointToEdit.value = null
@@ -1021,6 +1030,19 @@ async function handleBatchAssignChanged() {
emit('refresh')
}
// 处理管理别名 - 打开别名对话框
function handleManageAlias(model: Model) {
aliasEditingModel.value = model
aliasDialogOpen.value = true
}
// 处理别名保存完成
async function handleAliasSaved() {
aliasEditingModel.value = null
await loadProvider()
emit('refresh')
}
// 处理模型保存完成
async function handleModelSaved() {
editingModel.value = null

View File

@@ -7,6 +7,6 @@ export { default as ProviderModelFormDialog } from './ProviderModelFormDialog.vu
export { default as ProviderDetailDrawer } from './ProviderDetailDrawer.vue'
export { default as EndpointHealthTimeline } from './EndpointHealthTimeline.vue'
export { default as BatchAssignModelsDialog } from './BatchAssignModelsDialog.vue'
export { default as ModelAliasDialog } from './ModelAliasDialog.vue'
export { default as MappingsTab } from './provider-tabs/MappingsTab.vue'
export { default as ModelsTab } from './provider-tabs/ModelsTab.vue'

View File

@@ -1,310 +0,0 @@
<template>
<Card class="overflow-hidden">
<!-- 标题头部 -->
<div class="p-4 border-b border-border/60">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<h3 class="text-sm font-semibold leading-none">
别名与映射管理
</h3>
</div>
<Button
v-if="!hideAddButton"
variant="outline"
size="sm"
class="h-8"
@click="openCreateDialog"
>
<Plus class="w-3.5 h-3.5 mr-1.5" />
创建别名/映射
</Button>
</div>
</div>
<!-- 加载状态 -->
<div
v-if="loading"
class="flex items-center justify-center py-12"
>
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
</div>
<!-- 别名列表 -->
<div
v-else-if="mappings.length > 0"
class="overflow-x-auto"
>
<table class="w-full text-sm">
<thead class="bg-muted/50 text-xs uppercase tracking-wide text-muted-foreground">
<tr>
<th class="text-left px-4 py-3 font-semibold">
名称
</th>
<th class="text-left px-4 py-3 font-semibold w-24">
类型
</th>
<th class="text-left px-4 py-3 font-semibold">
指向模型
</th>
<th
v-if="!hideAddButton"
class="px-4 py-3 font-semibold w-28 text-center"
>
操作
</th>
</tr>
</thead>
<tbody>
<tr
v-for="mapping in mappings"
:key="mapping.id"
class="border-b border-border/40 last:border-b-0 hover:bg-muted/30 transition-colors"
>
<td class="px-4 py-3">
<div class="flex items-center gap-2">
<!-- 状态指示灯 -->
<span
class="w-2 h-2 rounded-full shrink-0"
:class="mapping.is_active ? 'bg-green-500' : 'bg-gray-300'"
:title="mapping.is_active ? '活跃' : '停用'"
/>
<span class="font-mono">{{ mapping.alias }}</span>
</div>
</td>
<td class="px-4 py-3">
<Badge
variant="secondary"
class="text-xs"
>
{{ mapping.mapping_type === 'mapping' ? '映射' : '别名' }}
</Badge>
</td>
<td class="px-4 py-3">
{{ mapping.global_model_display_name || mapping.global_model_name }}
</td>
<td
v-if="!hideAddButton"
class="px-4 py-3"
>
<div class="flex justify-center gap-1.5">
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
title="编辑"
@click="openEditDialog(mapping)"
>
<Edit class="w-3.5 h-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
:disabled="togglingId === mapping.id"
:title="mapping.is_active ? '点击停用' : '点击启用'"
@click="toggleActive(mapping)"
>
<Power class="w-3.5 h-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
class="h-8 w-8 text-destructive hover:text-destructive"
title="删除"
@click="confirmDelete(mapping)"
>
<Trash2 class="w-3.5 h-3.5" />
</Button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 空状态 -->
<div
v-else
class="p-8 text-center text-muted-foreground"
>
<ArrowLeftRight class="w-12 h-12 mx-auto mb-3 opacity-50" />
<p class="text-sm">
暂无特定别名/映射
</p>
<p class="text-xs mt-1">
点击上方按钮添加
</p>
</div>
</Card>
<!-- 使用共享的 AliasDialog 组件 -->
<AliasDialog
:open="dialogOpen"
:editing-alias="editingAlias"
:global-models="availableModels"
:fixed-provider="fixedProviderOption"
:show-provider-select="true"
@update:open="handleDialogVisibility"
@submit="handleAliasSubmit"
/>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ArrowLeftRight, Plus, Edit, Trash2, Power } from 'lucide-vue-next'
import Card from '@/components/ui/card.vue'
import Badge from '@/components/ui/badge.vue'
import Button from '@/components/ui/button.vue'
import AliasDialog from '@/features/models/components/AliasDialog.vue'
import { useToast } from '@/composables/useToast'
import {
getAliases,
createAlias,
updateAlias,
deleteAlias,
type ModelAlias,
type CreateModelAliasRequest,
type UpdateModelAliasRequest,
} from '@/api/endpoints/aliases'
import { listGlobalModels, type GlobalModelResponse } from '@/api/global-models'
const props = withDefaults(defineProps<{
provider: any
hideAddButton?: boolean
}>(), {
hideAddButton: false
})
const emit = defineEmits<{
refresh: []
}>()
const { success, error: showError } = useToast()
// 状态
const loading = ref(false)
const submitting = ref(false)
const togglingId = ref<string | null>(null)
const mappings = ref<ModelAlias[]>([])
const availableModels = ref<GlobalModelResponse[]>([])
const dialogOpen = ref(false)
const editingAlias = ref<ModelAlias | null>(null)
// 固定的 Provider 选项(传递给 AliasDialog
const fixedProviderOption = computed(() => ({
id: props.provider.id,
name: props.provider.name,
display_name: props.provider.display_name
}))
// 加载映射 (实际返回的是该 Provider 的别名列表)
async function loadMappings() {
try {
loading.value = true
mappings.value = await getAliases({ provider_id: props.provider.id })
} catch (err: any) {
showError(err.response?.data?.detail || '加载失败', '错误')
} finally {
loading.value = false
}
}
// 加载可用的 GlobalModel 列表
async function loadAvailableModels() {
try {
const response = await listGlobalModels({ limit: 1000, is_active: true })
availableModels.value = response.models || []
} catch (err: any) {
showError(err.response?.data?.detail || '加载模型列表失败', '错误')
}
}
// 打开创建对话框
function openCreateDialog() {
editingAlias.value = null
dialogOpen.value = true
}
// 打开编辑对话框
function openEditDialog(alias: ModelAlias) {
editingAlias.value = alias
dialogOpen.value = true
}
// 处理对话框可见性变化
function handleDialogVisibility(value: boolean) {
dialogOpen.value = value
if (!value) {
editingAlias.value = null
}
}
// 处理别名提交(来自 AliasDialog 组件)
async function handleAliasSubmit(data: CreateModelAliasRequest | UpdateModelAliasRequest, isEdit: boolean) {
submitting.value = true
try {
if (isEdit && editingAlias.value) {
// 更新
await updateAlias(editingAlias.value.id, data as UpdateModelAliasRequest)
success(data.mapping_type === 'mapping' ? '映射已更新' : '别名已更新')
} else {
// 创建 - 确保 provider_id 设置为当前 Provider
const createData = data as CreateModelAliasRequest
createData.provider_id = props.provider.id
await createAlias(createData)
success(data.mapping_type === 'mapping' ? '映射已创建' : '别名已创建')
}
dialogOpen.value = false
editingAlias.value = null
await loadMappings()
emit('refresh')
} catch (err: any) {
const detail = err.response?.data?.detail || err.message
let errorMessage = detail
if (detail === '映射已存在') {
errorMessage = '该名称已存在,请使用其他名称'
}
showError(errorMessage, isEdit ? '更新失败' : '创建失败')
} finally {
submitting.value = false
}
}
// 切换启用状态
async function toggleActive(alias: ModelAlias) {
if (togglingId.value) return
togglingId.value = alias.id
try {
const newStatus = !alias.is_active
await updateAlias(alias.id, { is_active: newStatus })
alias.is_active = newStatus
} catch (err: any) {
showError(err.response?.data?.detail || '操作失败', '错误')
} finally {
togglingId.value = null
}
}
// 确认删除
async function confirmDelete(alias: ModelAlias) {
const typeName = alias.mapping_type === 'mapping' ? '映射' : '别名'
if (!confirm(`确定要删除${typeName} "${alias.alias}" 吗?`)) {
return
}
try {
await deleteAlias(alias.id)
success(`${typeName}已删除`)
await loadMappings()
emit('refresh')
} catch (err: any) {
showError(err.response?.data?.detail || err.message, '删除失败')
}
}
onMounted(() => {
loadMappings()
loadAvailableModels()
})
</script>

View File

@@ -165,6 +165,15 @@
>
<Edit class="w-3.5 h-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
title="管理别名"
@click="openAliasDialog(model)"
>
<Tag class="w-3.5 h-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
@@ -209,7 +218,7 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { Box, Edit, Trash2, Layers, Eye, Wrench, Zap, Brain, Power, Copy, Image } from 'lucide-vue-next'
import { Box, Edit, Trash2, Layers, Eye, Wrench, Zap, Brain, Power, Copy, Image, Tag } from 'lucide-vue-next'
import Card from '@/components/ui/card.vue'
import Button from '@/components/ui/button.vue'
import { useToast } from '@/composables/useToast'
@@ -224,6 +233,7 @@ const emit = defineEmits<{
'editModel': [model: Model]
'deleteModel': [model: Model]
'batchAssign': []
'manageAlias': [model: Model]
}>()
const { error: showError, success: showSuccess } = useToast()
@@ -363,6 +373,11 @@ function openBatchAssignDialog() {
emit('batchAssign')
}
// 打开别名管理对话框
function openAliasDialog(model: Model) {
emit('manageAlias', model)
}
// 切换模型启用状态
async function toggleModelActive(model: Model) {
if (togglingModelId.value) return