mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-12 04:28:28 +08:00
refactor(ui): 重构批量模型管理对话框为单列勾选模式
- BatchAssignModelsDialog: 从左右双栏改为单列勾选,支持统一保存 - KeyAllowedModelsEditDialog: 添加分组折叠功能,优化布局 - Dialog: 调整移动端内边距适配 - ProviderDetailDrawer: 密钥列表改为两行显示 - KeyFormDialog: 缩小对话框尺寸 - ModelsTab: 调整表格列宽
This commit is contained in:
@@ -22,7 +22,7 @@
|
|||||||
/>
|
/>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
||||||
<div class="relative flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0 pointer-events-none">
|
<div class="relative flex min-h-full items-end justify-center px-3 py-4 text-center sm:items-center sm:p-0 pointer-events-none">
|
||||||
<!-- 对话框内容 -->
|
<!-- 对话框内容 -->
|
||||||
<Transition
|
<Transition
|
||||||
enter-active-class="duration-300 ease-out"
|
enter-active-class="duration-300 ease-out"
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="isOpen"
|
v-if="isOpen"
|
||||||
class="relative transform rounded-lg bg-background text-left shadow-2xl transition-all sm:my-8 sm:w-full border border-border pointer-events-auto"
|
class="relative transform rounded-lg bg-background text-left shadow-2xl transition-all w-full sm:my-8 sm:w-full border border-border pointer-events-auto"
|
||||||
:style="{ zIndex: contentZIndex }"
|
:style="{ zIndex: contentZIndex }"
|
||||||
:class="maxWidthClass"
|
:class="maxWidthClass"
|
||||||
@click.stop
|
@click.stop
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog
|
<Dialog
|
||||||
:model-value="isOpen"
|
:model-value="isOpen"
|
||||||
title="模型权限"
|
:title="props.apiKey?.name ? `模型权限 - ${props.apiKey.name}` : '模型权限'"
|
||||||
:description="`管理密钥 ${props.apiKey?.name || ''} 可访问的模型`"
|
description="选中的模型将被允许访问,不选择则允许全部"
|
||||||
:icon="Shield"
|
:icon="Shield"
|
||||||
size="lg"
|
size="2xl"
|
||||||
@update:model-value="handleDialogUpdate"
|
@update:model-value="handleDialogUpdate"
|
||||||
>
|
>
|
||||||
<template #default>
|
<template #default>
|
||||||
@@ -20,12 +20,6 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 已选数量提示 -->
|
|
||||||
<div class="text-sm text-muted-foreground">
|
|
||||||
<span v-if="selectedModels.length === 0">允许访问全部模型</span>
|
|
||||||
<span v-else>已选择 {{ selectedModels.length }} 个模型</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 常驻选择面板 -->
|
<!-- 常驻选择面板 -->
|
||||||
<div class="border rounded-lg overflow-hidden">
|
<div class="border rounded-lg overflow-hidden">
|
||||||
<!-- 搜索 + 操作栏 -->
|
<!-- 搜索 + 操作栏 -->
|
||||||
@@ -38,6 +32,19 @@
|
|||||||
class="pl-8 h-8 text-sm"
|
class="pl-8 h-8 text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 已选数量徽章 -->
|
||||||
|
<span
|
||||||
|
v-if="selectedModels.length === 0"
|
||||||
|
class="h-6 px-2 text-xs rounded flex items-center bg-muted text-muted-foreground shrink-0"
|
||||||
|
>
|
||||||
|
全部模型
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="h-6 px-2 text-xs rounded flex items-center bg-primary/10 text-primary shrink-0"
|
||||||
|
>
|
||||||
|
已选 {{ selectedModels.length }} 个
|
||||||
|
</span>
|
||||||
<button
|
<button
|
||||||
v-if="upstreamModelsLoaded"
|
v-if="upstreamModelsLoaded"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -56,7 +63,7 @@
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
class="h-8"
|
class="h-8"
|
||||||
title="从提供商获取模型"
|
title="从提供<EFBFBD><EFBFBD><EFBFBD>获取模型"
|
||||||
@click="fetchUpstreamModels()"
|
@click="fetchUpstreamModels()"
|
||||||
>
|
>
|
||||||
<Zap class="w-4 h-4" />
|
<Zap class="w-4 h-4" />
|
||||||
@@ -97,10 +104,23 @@
|
|||||||
|
|
||||||
<!-- 自定义模型(手动添加的,始终显示全部,搜索命中的排前面) -->
|
<!-- 自定义模型(手动添加的,始终显示全部,搜索命中的排前面) -->
|
||||||
<div v-if="customModels.length > 0">
|
<div v-if="customModels.length > 0">
|
||||||
<div class="flex items-center justify-between px-3 py-1.5 bg-muted/50">
|
<div
|
||||||
<span class="text-xs font-medium text-muted-foreground">自定义模型</span>
|
class="flex items-center justify-between px-3 py-2 bg-muted sticky top-0 z-10 cursor-pointer hover:bg-muted/80 transition-colors"
|
||||||
|
@click="toggleGroupCollapse('custom')"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<ChevronDown
|
||||||
|
class="w-4 h-4 transition-transform shrink-0"
|
||||||
|
:class="collapsedGroups.has('custom') ? '-rotate-90' : ''"
|
||||||
|
/>
|
||||||
|
<span class="text-xs font-medium">自定义模型</span>
|
||||||
|
<span class="text-xs text-muted-foreground">({{ customModels.length }})</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-1 p-2">
|
<div
|
||||||
|
v-show="!collapsedGroups.has('custom')"
|
||||||
|
class="space-y-1 p-2"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-for="model in sortedCustomModels"
|
v-for="model in sortedCustomModels"
|
||||||
:key="model"
|
:key="model"
|
||||||
@@ -113,24 +133,37 @@
|
|||||||
>
|
>
|
||||||
<Check v-if="selectedModels.includes(model)" class="w-3 h-3 text-primary-foreground" />
|
<Check v-if="selectedModels.includes(model)" class="w-3 h-3 text-primary-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<span class="text-xs font-mono truncate">{{ model }}</span>
|
<span class="text-sm font-mono truncate">{{ model }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 全局模型 -->
|
<!-- 全局模型 -->
|
||||||
<div v-if="filteredGlobalModels.length > 0">
|
<div v-if="filteredGlobalModels.length > 0">
|
||||||
<div class="flex items-center justify-between px-3 py-1.5 bg-muted/50">
|
<div
|
||||||
<span class="text-xs font-medium text-muted-foreground">全局模型</span>
|
class="flex items-center justify-between px-3 py-2 bg-muted sticky top-0 z-10 cursor-pointer hover:bg-muted/80 transition-colors"
|
||||||
|
@click="toggleGroupCollapse('global')"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<ChevronDown
|
||||||
|
class="w-4 h-4 transition-transform shrink-0"
|
||||||
|
:class="collapsedGroups.has('global') ? '-rotate-90' : ''"
|
||||||
|
/>
|
||||||
|
<span class="text-xs font-medium">全局模型</span>
|
||||||
|
<span class="text-xs text-muted-foreground">({{ filteredGlobalModels.length }})</span>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="text-xs text-primary hover:underline"
|
class="text-xs text-primary hover:underline"
|
||||||
@click="toggleAllGlobalModels"
|
@click.stop="toggleAllGlobalModels"
|
||||||
>
|
>
|
||||||
{{ isAllGlobalModelsSelected ? '取消全选' : '全选' }}
|
{{ isAllGlobalModelsSelected ? '取消全选' : '全选' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-1 p-2">
|
<div
|
||||||
|
v-show="!collapsedGroups.has('global')"
|
||||||
|
class="space-y-1 p-2"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-for="model in filteredGlobalModels"
|
v-for="model in filteredGlobalModels"
|
||||||
:key="model.name"
|
:key="model.name"
|
||||||
@@ -143,26 +176,42 @@
|
|||||||
>
|
>
|
||||||
<Check v-if="selectedModels.includes(model.name)" class="w-3 h-3 text-primary-foreground" />
|
<Check v-if="selectedModels.includes(model.name)" class="w-3 h-3 text-primary-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<span class="text-xs font-mono truncate">{{ model.name }}</span>
|
<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 font-mono">{{ model.name }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 上游模型组 -->
|
<!-- 上游模型组 -->
|
||||||
<div v-for="group in filteredUpstreamGroups" :key="group.api_format">
|
<div v-for="group in filteredUpstreamGroups" :key="group.api_format">
|
||||||
<div class="flex items-center justify-between px-3 py-1.5 bg-muted/50">
|
<div
|
||||||
<span class="text-xs font-medium text-muted-foreground">
|
class="flex items-center justify-between px-3 py-2 bg-muted sticky top-0 z-10 cursor-pointer hover:bg-muted/80 transition-colors"
|
||||||
{{ API_FORMAT_LABELS[group.api_format] || group.api_format }}
|
@click="toggleGroupCollapse(group.api_format)"
|
||||||
</span>
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<ChevronDown
|
||||||
|
class="w-4 h-4 transition-transform shrink-0"
|
||||||
|
:class="collapsedGroups.has(group.api_format) ? '-rotate-90' : ''"
|
||||||
|
/>
|
||||||
|
<span class="text-xs font-medium">
|
||||||
|
{{ API_FORMAT_LABELS[group.api_format] || group.api_format }}
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-muted-foreground">({{ group.models.length }})</span>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="text-xs text-primary hover:underline"
|
class="text-xs text-primary hover:underline"
|
||||||
@click="toggleAllUpstreamGroup(group.api_format)"
|
@click.stop="toggleAllUpstreamGroup(group.api_format)"
|
||||||
>
|
>
|
||||||
{{ isUpstreamGroupAllSelected(group.api_format) ? '取消全选' : '全选' }}
|
{{ isUpstreamGroupAllSelected(group.api_format) ? '取消全选' : '全选' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-1 p-2">
|
<div
|
||||||
|
v-show="!collapsedGroups.has(group.api_format)"
|
||||||
|
class="space-y-1 p-2"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-for="model in group.models"
|
v-for="model in group.models"
|
||||||
:key="model.id"
|
:key="model.id"
|
||||||
@@ -175,7 +224,7 @@
|
|||||||
>
|
>
|
||||||
<Check v-if="selectedModels.includes(model.id)" class="w-3 h-3 text-primary-foreground" />
|
<Check v-if="selectedModels.includes(model.id)" class="w-3 h-3 text-primary-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<span class="text-xs font-mono truncate">{{ model.id }}</span>
|
<span class="text-sm font-mono truncate">{{ model.id }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -201,10 +250,10 @@
|
|||||||
{{ hasChanges ? '有未保存的更改' : '' }}
|
{{ hasChanges ? '有未保存的更改' : '' }}
|
||||||
</p>
|
</p>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Button variant="outline" @click="handleCancel">取消</Button>
|
|
||||||
<Button :disabled="saving || !hasChanges" @click="handleSave">
|
<Button :disabled="saving || !hasChanges" @click="handleSave">
|
||||||
{{ saving ? '保存中...' : '保存' }}
|
{{ saving ? '保存中...' : '保存' }}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button variant="outline" @click="handleCancel">取消</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -220,10 +269,12 @@ import {
|
|||||||
Loader2,
|
Loader2,
|
||||||
Zap,
|
Zap,
|
||||||
Plus,
|
Plus,
|
||||||
Check
|
Check,
|
||||||
|
ChevronDown
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import { Dialog, Button, Input } from '@/components/ui'
|
import { Dialog, Button, Input } from '@/components/ui'
|
||||||
import { useToast } from '@/composables/useToast'
|
import { useToast } from '@/composables/useToast'
|
||||||
|
import { useConfirm } from '@/composables/useConfirm'
|
||||||
import { parseApiError, parseUpstreamModelError } from '@/utils/errorParser'
|
import { parseApiError, parseUpstreamModelError } from '@/utils/errorParser'
|
||||||
import {
|
import {
|
||||||
updateProviderKey,
|
updateProviderKey,
|
||||||
@@ -252,6 +303,7 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { success, error: showError } = useToast()
|
const { success, error: showError } = useToast()
|
||||||
|
const { confirmWarning } = useConfirm()
|
||||||
|
|
||||||
const isOpen = computed(() => props.open)
|
const isOpen = computed(() => props.open)
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
@@ -280,6 +332,9 @@ const allCustomModels = ref<string[]>([])
|
|||||||
// 是否为字典模式(按 API 格式区分)
|
// 是否为字典模式(按 API 格式区分)
|
||||||
const isDictMode = ref(false)
|
const isDictMode = ref(false)
|
||||||
|
|
||||||
|
// 折叠状态
|
||||||
|
const collapsedGroups = ref<Set<string>>(new Set())
|
||||||
|
|
||||||
// 是否有更改
|
// 是否有更改
|
||||||
const hasChanges = computed(() => {
|
const hasChanges = computed(() => {
|
||||||
if (selectedModels.value.length !== initialSelectedModels.value.length) return true
|
if (selectedModels.value.length !== initialSelectedModels.value.length) return true
|
||||||
@@ -444,6 +499,16 @@ function toggleAllUpstreamGroup(apiFormat: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 切换折叠状态
|
||||||
|
function toggleGroupCollapse(group: string) {
|
||||||
|
if (collapsedGroups.value.has(group)) {
|
||||||
|
collapsedGroups.value.delete(group)
|
||||||
|
} else {
|
||||||
|
collapsedGroups.value.add(group)
|
||||||
|
}
|
||||||
|
collapsedGroups.value = new Set(collapsedGroups.value)
|
||||||
|
}
|
||||||
|
|
||||||
// 加载全局模型
|
// 加载全局模型
|
||||||
async function loadGlobalModels() {
|
async function loadGlobalModels() {
|
||||||
loadingGlobalModels.value = true
|
loadingGlobalModels.value = true
|
||||||
@@ -537,11 +602,19 @@ onUnmounted(() => {
|
|||||||
loadingCancelled = true
|
loadingCancelled = true
|
||||||
})
|
})
|
||||||
|
|
||||||
function handleDialogUpdate(value: boolean) {
|
async function handleDialogUpdate(value: boolean) {
|
||||||
|
if (!value && hasChanges.value) {
|
||||||
|
const confirmed = await confirmWarning('有未保存的更改,确定要关闭吗?', '放弃更改')
|
||||||
|
if (!confirmed) return
|
||||||
|
}
|
||||||
if (!value) emit('close')
|
if (!value) emit('close')
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCancel() {
|
async function handleCancel() {
|
||||||
|
if (hasChanges.value) {
|
||||||
|
const confirmed = await confirmWarning('有未保存的更改,确定要关闭吗?', '放弃更改')
|
||||||
|
if (!confirmed) return
|
||||||
|
}
|
||||||
emit('close')
|
emit('close')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
:title="isEditMode ? '编辑密钥' : '添加密钥'"
|
:title="isEditMode ? '编辑密钥' : '添加密钥'"
|
||||||
:description="isEditMode ? '修改 API 密钥配置' : '为提供商添加新的 API 密钥'"
|
:description="isEditMode ? '修改 API 密钥配置' : '为提供商添加新的 API 密钥'"
|
||||||
:icon="isEditMode ? SquarePen : Key"
|
:icon="isEditMode ? SquarePen : Key"
|
||||||
size="2xl"
|
size="xl"
|
||||||
@update:model-value="handleDialogUpdate"
|
@update:model-value="handleDialogUpdate"
|
||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
<!-- API 格式选择 -->
|
<!-- API 格式选择 -->
|
||||||
<div v-if="sortedApiFormats.length > 0">
|
<div v-if="sortedApiFormats.length > 0">
|
||||||
<Label class="mb-1.5 block">支持的 API 格式 *</Label>
|
<Label class="mb-1.5 block">支持的 API 格式 *</Label>
|
||||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
<div class="grid grid-cols-2 gap-2">
|
||||||
<div
|
<div
|
||||||
v-for="format in sortedApiFormats"
|
v-for="format in sortedApiFormats"
|
||||||
:key="format"
|
:key="format"
|
||||||
|
|||||||
@@ -189,19 +189,23 @@
|
|||||||
<div class="cursor-grab active:cursor-grabbing text-muted-foreground/30 group-hover/item:text-muted-foreground transition-colors shrink-0">
|
<div class="cursor-grab active:cursor-grabbing text-muted-foreground/30 group-hover/item:text-muted-foreground transition-colors shrink-0">
|
||||||
<GripVertical class="w-4 h-4" />
|
<GripVertical class="w-4 h-4" />
|
||||||
</div>
|
</div>
|
||||||
<span class="text-sm font-medium truncate">{{ key.name || '未命名密钥' }}</span>
|
<div class="flex flex-col min-w-0">
|
||||||
<span class="text-xs font-mono text-muted-foreground">
|
<span class="text-sm font-medium truncate">{{ key.name || '未命名密钥' }}</span>
|
||||||
{{ key.api_key_masked }}
|
<div class="flex items-center gap-1">
|
||||||
</span>
|
<span class="text-[11px] font-mono text-muted-foreground">
|
||||||
<Button
|
{{ key.api_key_masked }}
|
||||||
variant="ghost"
|
</span>
|
||||||
size="icon"
|
<Button
|
||||||
class="h-5 w-5 shrink-0"
|
variant="ghost"
|
||||||
title="复制密钥"
|
size="icon"
|
||||||
@click.stop="copyFullKey(key)"
|
class="h-4 w-4 shrink-0"
|
||||||
>
|
title="复制密钥"
|
||||||
<Copy class="w-3 h-3" />
|
@click.stop="copyFullKey(key)"
|
||||||
</Button>
|
>
|
||||||
|
<Copy class="w-2.5 h-2.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<Badge
|
<Badge
|
||||||
v-if="!key.is_active"
|
v-if="!key.is_active"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
|||||||
@@ -34,13 +34,13 @@
|
|||||||
<table class="w-full text-sm table-fixed">
|
<table class="w-full text-sm table-fixed">
|
||||||
<thead class="bg-muted/50 text-xs uppercase tracking-wide text-muted-foreground">
|
<thead class="bg-muted/50 text-xs uppercase tracking-wide text-muted-foreground">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="text-left px-4 py-3 font-semibold w-[50%]">
|
<th class="text-left px-4 py-3 font-semibold w-[45%]">
|
||||||
模型
|
模型
|
||||||
</th>
|
</th>
|
||||||
<th class="text-left px-4 py-3 font-semibold w-[30%]">
|
<th class="text-left px-4 py-3 font-semibold w-[30%]">
|
||||||
价格 ($/M)
|
价格 ($/M)
|
||||||
</th>
|
</th>
|
||||||
<th class="text-center px-4 py-3 font-semibold w-[20%]">
|
<th class="text-center px-4 py-3 font-semibold w-[25%]">
|
||||||
操作
|
操作
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
${{ formatPrice(model.effective_price_per_request ?? model.price_per_request) }}/次
|
${{ formatPrice(model.effective_price_per_request ?? model.price_per_request) }}/次
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<!-- 无计费配置 -->
|
<!-- 无计<EFBFBD><EFBFBD>配置 -->
|
||||||
<template v-if="!hasTokenPricing(model) && !hasRequestPricing(model)">
|
<template v-if="!hasTokenPricing(model) && !hasRequestPricing(model)">
|
||||||
<span class="text-muted-foreground">—</span>
|
<span class="text-muted-foreground">—</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -305,7 +305,7 @@ function getStatusTitle(model: Model): string {
|
|||||||
return '活跃但不可用'
|
return '活跃但不可用'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 编辑模型
|
// <EFBFBD><EFBFBD>辑模型
|
||||||
function editModel(model: Model) {
|
function editModel(model: Model) {
|
||||||
emit('editModel', model)
|
emit('editModel', model)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user