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

@@ -1,384 +0,0 @@
<template>
<Dialog
:model-value="open"
:title="dialogTitle"
:description="dialogDescription"
:icon="dialogIcon"
size="md"
@update:model-value="handleDialogUpdate"
>
<form
class="space-y-4"
@submit.prevent="handleSubmit"
>
<!-- 模式选择仅创建时显示 -->
<div
v-if="!isEditMode"
class="space-y-2"
>
<Label>创建类型 *</Label>
<div class="grid grid-cols-2 gap-3">
<button
type="button"
class="p-3 rounded-lg border-2 text-left transition-all"
:class="[
form.mapping_type === 'alias'
? 'border-primary bg-primary/5'
: 'border-border hover:border-primary/50'
]"
@click="form.mapping_type = 'alias'"
>
<div class="font-medium text-sm">
别名
</div>
<div class="text-xs text-muted-foreground mt-1">
名称简写按目标模型计费
</div>
</button>
<button
type="button"
class="p-3 rounded-lg border-2 text-left transition-all"
:class="[
form.mapping_type === 'mapping'
? 'border-primary bg-primary/5'
: 'border-border hover:border-primary/50'
]"
@click="form.mapping_type = 'mapping'"
>
<div class="font-medium text-sm">
映射
</div>
<div class="text-xs text-muted-foreground mt-1">
模型降级按源模型计费
</div>
</button>
</div>
</div>
<!-- 模式说明 -->
<div class="rounded-lg border border-border bg-muted/50 p-3 text-sm">
<p class="text-foreground font-medium mb-1">
{{ form.mapping_type === 'alias' ? '别名模式' : '映射模式' }}
</p>
<p class="text-muted-foreground text-xs">
{{ form.mapping_type === 'alias'
? '用户请求此别名时,会路由到目标模型,并按目标模型价格计费。'
: '将源模型的请求转发到目标模型处理,按源模型价格计费。' }}
</p>
</div>
<!-- Provider 选择/作用范围 -->
<div
v-if="showProviderSelect"
class="space-y-2"
>
<Label>作用范围</Label>
<!-- 固定 Provider 时显示只读 -->
<div
v-if="fixedProvider"
class="px-3 py-2 border rounded-md bg-muted/50 text-sm"
>
{{ fixedProvider.display_name || fixedProvider.name }}
</div>
<!-- 否则显示可选择的下拉 -->
<Select
v-else
v-model:open="providerSelectOpen"
:model-value="form.provider_id || 'global'"
@update:model-value="handleProviderChange"
>
<SelectTrigger class="w-full">
<SelectValue placeholder="选择作用范围" />
</SelectTrigger>
<SelectContent>
<SelectItem value="global">
全局所有 Provider
</SelectItem>
<SelectItem
v-for="p in providers"
:key="p.id"
:value="p.id"
>
{{ p.display_name || p.name }}
</SelectItem>
</SelectContent>
</Select>
</div>
<!-- 别名模式别名名称 -->
<div
v-if="form.mapping_type === 'alias'"
class="space-y-2"
>
<Label for="alias-name">别名名称 *</Label>
<Input
id="alias-name"
v-model="form.alias"
placeholder="如sonnet, opus"
:disabled="isEditMode"
required
/>
<p class="text-xs text-muted-foreground">
{{ isEditMode ? '创建后不可修改' : '用户将使用此名称请求模型' }}
</p>
</div>
<!-- 映射模式选择源模型 -->
<div
v-else
class="space-y-2"
>
<Label>源模型 (用户请求的模型) *</Label>
<Select
v-model:open="sourceModelSelectOpen"
:model-value="form.alias"
:disabled="isEditMode"
@update:model-value="form.alias = $event"
>
<SelectTrigger
class="w-full"
:class="{ 'opacity-50': isEditMode }"
>
<SelectValue placeholder="请选择源模型" />
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="model in availableSourceModels"
:key="model.id"
:value="model.name"
>
{{ model.display_name }} ({{ model.name }})
</SelectItem>
</SelectContent>
</Select>
<p class="text-xs text-muted-foreground">
{{ isEditMode ? '创建后不可修改' : '选择要被映射的源模型,计费将按此模型价格' }}
</p>
</div>
<!-- 目标模型选择 -->
<div class="space-y-2">
<Label>
{{ form.mapping_type === 'alias' ? '目标模型 *' : '目标模型 (实际处理请求) *' }}
</Label>
<!-- 固定目标模型时显示只读信息 -->
<div
v-if="fixedTargetModel"
class="px-3 py-2 border rounded-md bg-muted/50"
>
<span class="font-medium">{{ fixedTargetModel.display_name }}</span>
<span class="text-muted-foreground ml-1">({{ fixedTargetModel.name }})</span>
</div>
<!-- 否则显示下拉选择 -->
<Select
v-else
v-model:open="targetModelSelectOpen"
:model-value="form.global_model_id"
@update:model-value="form.global_model_id = $event"
>
<SelectTrigger class="w-full">
<SelectValue placeholder="请选择模型" />
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="model in availableTargetModels"
:key="model.id"
:value="model.id"
>
{{ model.display_name }} ({{ model.name }})
</SelectItem>
</SelectContent>
</Select>
</div>
</form>
<template #footer>
<Button
type="button"
variant="outline"
@click="handleCancel"
>
取消
</Button>
<Button
:disabled="submitting"
@click="handleSubmit"
>
<Loader2
v-if="submitting"
class="w-4 h-4 mr-2 animate-spin"
/>
{{ isEditMode ? '保存' : '创建' }}
</Button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { Loader2, Tag, SquarePen } from 'lucide-vue-next'
import { Dialog, Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui'
import Button from '@/components/ui/button.vue'
import Input from '@/components/ui/input.vue'
import Label from '@/components/ui/label.vue'
import { useToast } from '@/composables/useToast'
import { useFormDialog } from '@/composables/useFormDialog'
import type { ModelAlias, CreateModelAliasRequest, UpdateModelAliasRequest } from '@/api/endpoints/aliases'
import type { GlobalModelResponse } from '@/api/global-models'
export interface ProviderOption {
id: string
name: string
display_name?: string
}
interface AliasFormData {
alias: string
global_model_id: string
provider_id: string | null
mapping_type: 'alias' | 'mapping'
is_active: boolean
}
const props = withDefaults(defineProps<{
open: boolean
editingAlias?: ModelAlias | null
globalModels: GlobalModelResponse[]
providers?: ProviderOption[]
fixedTargetModel?: GlobalModelResponse | null // 用于从模型详情抽屉打开时固定目标模型
fixedProvider?: ProviderOption | null // 用于 Provider 特定别名固定 Provider
showProviderSelect?: boolean // 是否显示 Provider 选择(默认 true
}>(), {
editingAlias: null,
providers: () => [],
fixedTargetModel: null,
fixedProvider: null,
showProviderSelect: true
})
const emit = defineEmits<{
'update:open': [value: boolean]
'submit': [data: CreateModelAliasRequest | UpdateModelAliasRequest, isEdit: boolean]
}>()
const { error: showError } = useToast()
// 状态
const submitting = ref(false)
const providerSelectOpen = ref(false)
const sourceModelSelectOpen = ref(false)
const targetModelSelectOpen = ref(false)
const form = ref<AliasFormData>({
alias: '',
global_model_id: '',
provider_id: null,
mapping_type: 'alias',
is_active: true,
})
// 处理 Provider 选择变化
function handleProviderChange(value: string) {
form.value.provider_id = value === 'global' ? null : value
}
// 重置表单
function resetForm() {
form.value = {
alias: '',
global_model_id: props.fixedTargetModel?.id || '',
provider_id: props.fixedProvider?.id || null,
mapping_type: 'alias',
is_active: true,
}
}
// 加载别名数据(编辑模式)
function loadAliasData() {
if (!props.editingAlias) return
form.value = {
alias: props.editingAlias.alias,
global_model_id: props.editingAlias.global_model_id,
provider_id: props.editingAlias.provider_id,
mapping_type: props.editingAlias.mapping_type || 'alias',
is_active: props.editingAlias.is_active,
}
}
// 使用 useFormDialog 统一处理对话框逻辑
const { isEditMode, handleDialogUpdate, handleCancel } = useFormDialog({
isOpen: () => props.open,
entity: () => props.editingAlias,
isLoading: submitting,
onClose: () => emit('update:open', false),
loadData: loadAliasData,
resetForm,
})
// 对话框标题
const dialogTitle = computed(() => {
if (isEditMode.value) {
return form.value.mapping_type === 'mapping' ? '编辑映射' : '编辑别名'
}
if (props.fixedProvider) {
return `创建 ${props.fixedProvider.display_name || props.fixedProvider.name} 特定别名/映射`
}
return '创建别名/映射'
})
// 对话框描述
const dialogDescription = computed(() => {
if (isEditMode.value) {
return form.value.mapping_type === 'mapping' ? '修改模型映射配置' : '修改别名设置'
}
return '为模型创建别名或映射规则'
})
// 对话框图标
const dialogIcon = computed(() => isEditMode.value ? SquarePen : Tag)
// 映射模式下可选的源模型(排除已选择的目标模型)
const availableSourceModels = computed(() => {
return props.globalModels.filter(m => m.id !== form.value.global_model_id)
})
// 可选的目标模型(映射模式下排除已选择的源模型)
const availableTargetModels = computed(() => {
if (form.value.mapping_type === 'mapping' && form.value.alias) {
// 找到源模型对应的 GlobalModel
const sourceModel = props.globalModels.find(m => m.name === form.value.alias)
if (sourceModel) {
return props.globalModels.filter(m => m.id !== sourceModel.id)
}
}
return props.globalModels
})
// 提交表单
async function handleSubmit() {
if (!form.value.alias) {
showError(form.value.mapping_type === 'alias' ? '请输入别名名称' : '请选择源模型', '错误')
return
}
const targetModelId = props.fixedTargetModel?.id || form.value.global_model_id
if (!targetModelId) {
showError('请选择目标模型', '错误')
return
}
submitting.value = true
try {
const data: CreateModelAliasRequest | UpdateModelAliasRequest = {
alias: form.value.alias,
global_model_id: targetModelId,
provider_id: props.fixedProvider?.id || form.value.provider_id,
mapping_type: form.value.mapping_type,
is_active: form.value.is_active,
}
emit('submit', data, !!props.editingAlias)
} finally {
submitting.value = false
}
}
</script>

View File

@@ -104,19 +104,6 @@
<span class="hidden sm:inline">关联提供商</span>
<span class="sm:hidden">提供商</span>
</button>
<button
type="button"
class="flex-1 px-2 sm:px-4 py-2 text-xs sm:text-sm font-medium rounded-md transition-all duration-200"
:class="[
detailTab === 'aliases'
? 'bg-primary text-primary-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground hover:bg-background/50'
]"
@click="detailTab = 'aliases'"
>
<span class="hidden sm:inline">别名/映射</span>
<span class="sm:hidden">别名</span>
</button>
</div>
<!-- Tab 内容 -->
@@ -684,236 +671,6 @@
</div>
</Card>
</div>
<!-- Tab 3: 别名 -->
<div v-show="detailTab === 'aliases'">
<Card class="overflow-hidden">
<!-- 标题栏 -->
<div class="px-4 py-3 border-b border-border/60">
<div class="flex items-center justify-between gap-4">
<div>
<h4 class="text-sm font-semibold">
别名与映射
</h4>
</div>
<div class="flex items-center gap-2">
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
title="添加别名/映射"
@click="$emit('addAlias')"
>
<Plus class="w-3.5 h-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
title="刷新"
@click="$emit('refreshAliases')"
>
<RefreshCw
class="w-3.5 h-3.5"
:class="loadingAliases ? 'animate-spin' : ''"
/>
</Button>
</div>
</div>
</div>
<!-- 表格内容 -->
<div
v-if="loadingAliases"
class="flex items-center justify-center py-12"
>
<Loader2 class="w-6 h-6 animate-spin text-primary" />
</div>
<template v-else-if="aliases.length > 0">
<!-- 桌面端表格 -->
<Table class="hidden sm:table">
<TableHeader>
<TableRow class="border-b border-border/60 hover:bg-transparent">
<TableHead class="h-10 font-semibold">
别名
</TableHead>
<TableHead class="w-[80px] h-10 font-semibold">
类型
</TableHead>
<TableHead class="w-[100px] h-10 font-semibold">
作用域
</TableHead>
<TableHead class="w-[100px] h-10 font-semibold text-center">
操作
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow
v-for="alias in aliases"
:key="alias.id"
class="border-b border-border/40 hover:bg-muted/30 transition-colors"
>
<TableCell class="py-3">
<div class="flex items-center gap-2">
<span
class="w-2 h-2 rounded-full shrink-0"
:class="alias.is_active ? 'bg-green-500' : 'bg-gray-300'"
:title="alias.is_active ? '活跃' : '停用'"
/>
<code class="text-sm font-medium bg-muted px-1.5 py-0.5 rounded">{{ alias.alias }}</code>
</div>
</TableCell>
<TableCell class="py-3">
<Badge
variant="secondary"
class="text-xs"
>
{{ alias.mapping_type === 'mapping' ? '映射' : '别名' }}
</Badge>
</TableCell>
<TableCell class="py-3">
<Badge
v-if="alias.provider_id"
variant="outline"
class="text-xs truncate max-w-[90px]"
:title="alias.provider_name || 'Provider'"
>
{{ alias.provider_name || 'Provider' }}
</Badge>
<Badge
v-else
variant="default"
class="text-xs"
>
全局
</Badge>
</TableCell>
<TableCell class="py-3 text-center">
<div class="flex items-center justify-center gap-0.5">
<Button
variant="ghost"
size="icon"
class="h-7 w-7"
title="编辑"
@click="$emit('editAlias', alias)"
>
<Edit class="w-3.5 h-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
class="h-7 w-7"
:title="alias.is_active ? '停用' : '启用'"
@click="$emit('toggleAliasStatus', alias)"
>
<Power class="w-3.5 h-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
class="h-7 w-7"
title="删除"
@click="$emit('deleteAlias', alias)"
>
<Trash2 class="w-3.5 h-3.5" />
</Button>
</div>
</TableCell>
</TableRow>
</TableBody>
</Table>
<!-- 移动端卡片列表 -->
<div class="sm:hidden divide-y divide-border/40">
<div
v-for="alias in aliases"
:key="alias.id"
class="p-4 space-y-2"
>
<div class="flex items-start justify-between gap-3">
<div class="flex items-center gap-2 min-w-0 flex-1">
<span
class="w-2 h-2 rounded-full shrink-0"
:class="alias.is_active ? 'bg-green-500' : 'bg-gray-300'"
/>
<code class="text-sm font-medium bg-muted px-1.5 py-0.5 rounded truncate">{{ alias.alias }}</code>
</div>
<div class="flex items-center gap-1 shrink-0">
<Button
variant="ghost"
size="icon"
class="h-7 w-7"
@click="$emit('editAlias', alias)"
>
<Edit class="w-3.5 h-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
class="h-7 w-7"
@click="$emit('toggleAliasStatus', alias)"
>
<Power class="w-3.5 h-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
class="h-7 w-7"
@click="$emit('deleteAlias', alias)"
>
<Trash2 class="w-3.5 h-3.5" />
</Button>
</div>
</div>
<div class="flex items-center gap-2">
<Badge
variant="secondary"
class="text-xs"
>
{{ alias.mapping_type === 'mapping' ? '映射' : '别名' }}
</Badge>
<Badge
v-if="alias.provider_id"
variant="outline"
class="text-xs truncate max-w-[120px]"
>
{{ alias.provider_name || 'Provider' }}
</Badge>
<Badge
v-else
variant="default"
class="text-xs"
>
全局
</Badge>
</div>
</div>
</div>
</template>
<div
v-else
class="text-center py-12"
>
<!-- 空状态 -->
<Tag class="w-12 h-12 mx-auto text-muted-foreground/30 mb-3" />
<p class="text-sm text-muted-foreground">
暂无别名或映射
</p>
<Button
size="sm"
variant="outline"
class="mt-4"
@click="$emit('addAlias')"
>
<Plus class="w-4 h-4 mr-1" />
添加别名/映射
</Button>
</div>
</Card>
</div>
</div>
</Card>
</div>
@@ -931,7 +688,6 @@ import {
Zap,
Image,
Building2,
Tag,
Plus,
Edit,
Trash2,
@@ -955,13 +711,11 @@ import TableCell from '@/components/ui/table-cell.vue'
// 使用外部类型定义
import type { GlobalModelResponse } from '@/api/global-models'
import type { ModelAlias } from '@/api/endpoints/aliases'
import type { TieredPricingConfig, PricingTier } from '@/api/endpoints/types'
import type { CapabilityDefinition } from '@/api/endpoints'
const props = withDefaults(defineProps<Props>(), {
loadingProviders: false,
loadingAliases: false,
hasBlockingDialogOpen: false,
})
const emit = defineEmits<{
@@ -973,11 +727,6 @@ const emit = defineEmits<{
'deleteProvider': [provider: any]
'toggleProviderStatus': [provider: any]
'refreshProviders': []
'addAlias': []
'editAlias': [alias: ModelAlias]
'toggleAliasStatus': [alias: ModelAlias]
'deleteAlias': [alias: ModelAlias]
'refreshAliases': []
}>()
const { success: showSuccess, error: showError } = useToast()
@@ -985,9 +734,7 @@ interface Props {
model: GlobalModelResponse | null
open: boolean
providers: any[]
aliases: ModelAlias[]
loadingProviders?: boolean
loadingAliases?: boolean
hasBlockingDialogOpen?: boolean
capabilities?: CapabilityDefinition[]
}

View File

@@ -1,4 +1,3 @@
export { default as GlobalModelFormDialog } from './GlobalModelFormDialog.vue'
export { default as AliasDialog } from './AliasDialog.vue'
export { default as ModelDetailDrawer } from './ModelDetailDrawer.vue'
export { default as TieredPricingEditor } from './TieredPricingEditor.vue'