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>
|
||||
|
||||
<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
|
||||
enter-active-class="duration-300 ease-out"
|
||||
@@ -34,7 +34,7 @@
|
||||
>
|
||||
<div
|
||||
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 }"
|
||||
:class="maxWidthClass"
|
||||
@click.stop
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<Dialog
|
||||
:model-value="isOpen"
|
||||
title="模型权限"
|
||||
:description="`管理密钥 ${props.apiKey?.name || ''} 可访问的模型`"
|
||||
:title="props.apiKey?.name ? `模型权限 - ${props.apiKey.name}` : '模型权限'"
|
||||
description="选中的模型将被允许访问,不选择则允许全部"
|
||||
:icon="Shield"
|
||||
size="lg"
|
||||
size="2xl"
|
||||
@update:model-value="handleDialogUpdate"
|
||||
>
|
||||
<template #default>
|
||||
@@ -20,12 +20,6 @@
|
||||
</p>
|
||||
</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">
|
||||
<!-- 搜索 + 操作栏 -->
|
||||
@@ -38,6 +32,19 @@
|
||||
class="pl-8 h-8 text-sm"
|
||||
/>
|
||||
</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
|
||||
v-if="upstreamModelsLoaded"
|
||||
type="button"
|
||||
@@ -56,7 +63,7 @@
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-8"
|
||||
title="从提供商获取模型"
|
||||
title="从提供<EFBFBD><EFBFBD><EFBFBD>获取模型"
|
||||
@click="fetchUpstreamModels()"
|
||||
>
|
||||
<Zap class="w-4 h-4" />
|
||||
@@ -97,10 +104,23 @@
|
||||
|
||||
<!-- 自定义模型(手动添加的,始终显示全部,搜索命中的排前面) -->
|
||||
<div v-if="customModels.length > 0">
|
||||
<div class="flex items-center justify-between px-3 py-1.5 bg-muted/50">
|
||||
<span class="text-xs font-medium text-muted-foreground">自定义模型</span>
|
||||
<div
|
||||
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 class="space-y-1 p-2">
|
||||
<div
|
||||
v-show="!collapsedGroups.has('custom')"
|
||||
class="space-y-1 p-2"
|
||||
>
|
||||
<div
|
||||
v-for="model in sortedCustomModels"
|
||||
:key="model"
|
||||
@@ -113,24 +133,37 @@
|
||||
>
|
||||
<Check v-if="selectedModels.includes(model)" class="w-3 h-3 text-primary-foreground" />
|
||||
</div>
|
||||
<span class="text-xs font-mono truncate">{{ model }}</span>
|
||||
<span class="text-sm font-mono truncate">{{ model }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 全局模型 -->
|
||||
<div v-if="filteredGlobalModels.length > 0">
|
||||
<div class="flex items-center justify-between px-3 py-1.5 bg-muted/50">
|
||||
<span class="text-xs font-medium text-muted-foreground">全局模型</span>
|
||||
<div
|
||||
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
|
||||
type="button"
|
||||
class="text-xs text-primary hover:underline"
|
||||
@click="toggleAllGlobalModels"
|
||||
@click.stop="toggleAllGlobalModels"
|
||||
>
|
||||
{{ isAllGlobalModelsSelected ? '取消全选' : '全选' }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="space-y-1 p-2">
|
||||
<div
|
||||
v-show="!collapsedGroups.has('global')"
|
||||
class="space-y-1 p-2"
|
||||
>
|
||||
<div
|
||||
v-for="model in filteredGlobalModels"
|
||||
:key="model.name"
|
||||
@@ -143,26 +176,42 @@
|
||||
>
|
||||
<Check v-if="selectedModels.includes(model.name)" class="w-3 h-3 text-primary-foreground" />
|
||||
</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 v-for="group in filteredUpstreamGroups" :key="group.api_format">
|
||||
<div class="flex items-center justify-between px-3 py-1.5 bg-muted/50">
|
||||
<span class="text-xs font-medium text-muted-foreground">
|
||||
{{ API_FORMAT_LABELS[group.api_format] || group.api_format }}
|
||||
</span>
|
||||
<div
|
||||
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(group.api_format)"
|
||||
>
|
||||
<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
|
||||
type="button"
|
||||
class="text-xs text-primary hover:underline"
|
||||
@click="toggleAllUpstreamGroup(group.api_format)"
|
||||
@click.stop="toggleAllUpstreamGroup(group.api_format)"
|
||||
>
|
||||
{{ isUpstreamGroupAllSelected(group.api_format) ? '取消全选' : '全选' }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="space-y-1 p-2">
|
||||
<div
|
||||
v-show="!collapsedGroups.has(group.api_format)"
|
||||
class="space-y-1 p-2"
|
||||
>
|
||||
<div
|
||||
v-for="model in group.models"
|
||||
:key="model.id"
|
||||
@@ -175,7 +224,7 @@
|
||||
>
|
||||
<Check v-if="selectedModels.includes(model.id)" class="w-3 h-3 text-primary-foreground" />
|
||||
</div>
|
||||
<span class="text-xs font-mono truncate">{{ model.id }}</span>
|
||||
<span class="text-sm font-mono truncate">{{ model.id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -201,10 +250,10 @@
|
||||
{{ hasChanges ? '有未保存的更改' : '' }}
|
||||
</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button variant="outline" @click="handleCancel">取消</Button>
|
||||
<Button :disabled="saving || !hasChanges" @click="handleSave">
|
||||
{{ saving ? '保存中...' : '保存' }}
|
||||
</Button>
|
||||
<Button variant="outline" @click="handleCancel">取消</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -220,10 +269,12 @@ import {
|
||||
Loader2,
|
||||
Zap,
|
||||
Plus,
|
||||
Check
|
||||
Check,
|
||||
ChevronDown
|
||||
} from 'lucide-vue-next'
|
||||
import { Dialog, Button, Input } from '@/components/ui'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import { parseApiError, parseUpstreamModelError } from '@/utils/errorParser'
|
||||
import {
|
||||
updateProviderKey,
|
||||
@@ -252,6 +303,7 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const { success, error: showError } = useToast()
|
||||
const { confirmWarning } = useConfirm()
|
||||
|
||||
const isOpen = computed(() => props.open)
|
||||
const saving = ref(false)
|
||||
@@ -280,6 +332,9 @@ const allCustomModels = ref<string[]>([])
|
||||
// 是否为字典模式(按 API 格式区分)
|
||||
const isDictMode = ref(false)
|
||||
|
||||
// 折叠状态
|
||||
const collapsedGroups = ref<Set<string>>(new Set())
|
||||
|
||||
// 是否有更改
|
||||
const hasChanges = computed(() => {
|
||||
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() {
|
||||
loadingGlobalModels.value = true
|
||||
@@ -537,11 +602,19 @@ onUnmounted(() => {
|
||||
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')
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
async function handleCancel() {
|
||||
if (hasChanges.value) {
|
||||
const confirmed = await confirmWarning('有未保存的更改,确定要关闭吗?', '放弃更改')
|
||||
if (!confirmed) return
|
||||
}
|
||||
emit('close')
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
:title="isEditMode ? '编辑密钥' : '添加密钥'"
|
||||
:description="isEditMode ? '修改 API 密钥配置' : '为提供商添加新的 API 密钥'"
|
||||
:icon="isEditMode ? SquarePen : Key"
|
||||
size="2xl"
|
||||
size="xl"
|
||||
@update:model-value="handleDialogUpdate"
|
||||
>
|
||||
<form
|
||||
@@ -80,7 +80,7 @@
|
||||
<!-- API 格式选择 -->
|
||||
<div v-if="sortedApiFormats.length > 0">
|
||||
<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
|
||||
v-for="format in sortedApiFormats"
|
||||
: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">
|
||||
<GripVertical class="w-4 h-4" />
|
||||
</div>
|
||||
<span class="text-sm font-medium truncate">{{ key.name || '未命名密钥' }}</span>
|
||||
<span class="text-xs font-mono text-muted-foreground">
|
||||
{{ key.api_key_masked }}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-5 w-5 shrink-0"
|
||||
title="复制密钥"
|
||||
@click.stop="copyFullKey(key)"
|
||||
>
|
||||
<Copy class="w-3 h-3" />
|
||||
</Button>
|
||||
<div class="flex flex-col min-w-0">
|
||||
<span class="text-sm font-medium truncate">{{ key.name || '未命名密钥' }}</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-[11px] font-mono text-muted-foreground">
|
||||
{{ key.api_key_masked }}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-4 w-4 shrink-0"
|
||||
title="复制密钥"
|
||||
@click.stop="copyFullKey(key)"
|
||||
>
|
||||
<Copy class="w-2.5 h-2.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Badge
|
||||
v-if="!key.is_active"
|
||||
variant="secondary"
|
||||
|
||||
@@ -34,13 +34,13 @@
|
||||
<table class="w-full text-sm table-fixed">
|
||||
<thead class="bg-muted/50 text-xs uppercase tracking-wide text-muted-foreground">
|
||||
<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 class="text-left px-4 py-3 font-semibold w-[30%]">
|
||||
价格 ($/M)
|
||||
</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>
|
||||
</tr>
|
||||
@@ -109,7 +109,7 @@
|
||||
${{ formatPrice(model.effective_price_per_request ?? model.price_per_request) }}/次
|
||||
</span>
|
||||
</template>
|
||||
<!-- 无计费配置 -->
|
||||
<!-- 无计<EFBFBD><EFBFBD>配置 -->
|
||||
<template v-if="!hasTokenPricing(model) && !hasRequestPricing(model)">
|
||||
<span class="text-muted-foreground">—</span>
|
||||
</template>
|
||||
@@ -305,7 +305,7 @@ function getStatusTitle(model: Model): string {
|
||||
return '活跃但不可用'
|
||||
}
|
||||
|
||||
// 编辑模型
|
||||
// <EFBFBD><EFBFBD>辑模型
|
||||
function editModel(model: Model) {
|
||||
emit('editModel', model)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user