feat: 上游模型列表显示 API 格式标签 (#108)

- KeyAllowedModelsEditDialog 按模型 ID 聚合上游模型
- 每个模型显示其支持的所有 API 格式缩写标签
- 移除按 Key API 格式过滤的限制,展示全部模型
This commit is contained in:
AAEE86
2026-01-19 19:41:45 +08:00
committed by GitHub
parent 427f173b38
commit 6b8c1fcf47

View File

@@ -254,7 +254,7 @@
> >
<div <div
v-for="model in filteredUpstreamModels" v-for="model in filteredUpstreamModels"
:key="model" :key="model.id"
class="flex items-center gap-2 px-2 py-1.5 rounded" class="flex items-center gap-2 px-2 py-1.5 rounded"
:class="isAutoFetchMode ? 'opacity-80' : 'hover:bg-muted cursor-pointer'" :class="isAutoFetchMode ? 'opacity-80' : 'hover:bg-muted cursor-pointer'"
@click="handleUpstreamModelClick(model)" @click="handleUpstreamModelClick(model)"
@@ -262,25 +262,35 @@
<div <div
class="w-4 h-4 border rounded flex items-center justify-center shrink-0" class="w-4 h-4 border rounded flex items-center justify-center shrink-0"
:class="[ :class="[
selectedModels.includes(model) ? 'bg-primary border-primary' : '', selectedModels.includes(model.id) ? 'bg-primary border-primary' : '',
isAutoFetchMode ? 'opacity-50' : '' isAutoFetchMode ? 'opacity-50' : ''
]" ]"
> >
<Check <Check
v-if="selectedModels.includes(model)" v-if="selectedModels.includes(model.id)"
class="w-3 h-3 text-primary-foreground" class="w-3 h-3 text-primary-foreground"
/> />
</div> </div>
<span class="text-sm font-mono truncate flex-1">{{ model }}</span> <span class="text-sm font-mono truncate flex-1">{{ model.id }}</span>
<!-- API 格式标签 -->
<div class="flex items-center gap-1 shrink-0">
<span
v-for="fmt in model.api_formats"
:key="fmt"
class="text-[10px] px-1.5 py-0.5 rounded bg-muted text-muted-foreground"
>
{{ API_FORMAT_SHORT[fmt] || fmt }}
</span>
</div>
<button <button
v-if="selectedModels.includes(model)" v-if="selectedModels.includes(model.id)"
type="button" type="button"
class="p-1 rounded hover:bg-muted-foreground/10 transition-colors shrink-0 text-muted-foreground" class="p-1 rounded hover:bg-muted-foreground/10 transition-colors shrink-0 text-muted-foreground"
:title="isLocked(model) ? '已锁定 - 点击解锁' : '点击锁定(刷新时不会被删除)'" :title="isLocked(model.id) ? '已锁定 - 点击解锁' : '点击锁定(刷新时不会被删除)'"
@click="toggleLock(model, $event)" @click="toggleLock(model.id, $event)"
> >
<Lock <Lock
v-if="isLocked(model)" v-if="isLocked(model.id)"
class="w-3.5 h-3.5" class="w-3.5 h-3.5"
/> />
<LockOpen <LockOpen
@@ -356,7 +366,7 @@ import {
} from '@/api/endpoints' } from '@/api/endpoints'
import { getGlobalModels, type GlobalModelResponse } from '@/api/global-models' import { getGlobalModels, type GlobalModelResponse } from '@/api/global-models'
import { useUpstreamModelsCache } from '../composables/useUpstreamModelsCache' import { useUpstreamModelsCache } from '../composables/useUpstreamModelsCache'
import type { UpstreamModel } from '@/api/endpoints/types' import { API_FORMAT_SHORT, type UpstreamModel } from '@/api/endpoints/types'
interface AvailableModel { interface AvailableModel {
name: string name: string
@@ -452,37 +462,49 @@ function isGlobalModel(modelId: string): boolean {
return globalModelNamesSet.value.has(modelId) return globalModelNamesSet.value.has(modelId)
} }
// Key 支持的 API 格式 // 上游模型信息(包含 api_format
const keyApiFormats = computed(() => props.apiKey?.api_formats ?? []) interface UpstreamModelInfo {
id: string
api_formats: string[] // 该模型支持的所有 API 格式
}
// 上游模型名称列表(去重后 // 上游模型列表(按 id 聚合,包含所有 api_format
const upstreamModelNames = computed(() => { const upstreamModelList = computed(() => {
const names = new Set<string>() const modelMap = new Map<string, Set<string>>()
upstreamModels.value.forEach(m => { upstreamModels.value.forEach(m => {
// 只包含 Key 支持的 API 格式的模型 if (!modelMap.has(m.id)) {
if (!m.api_format || keyApiFormats.value.includes(m.api_format)) { modelMap.set(m.id, new Set())
names.add(m.id) }
if (m.api_format) {
modelMap.get(m.id)!.add(m.api_format)
} }
}) })
return Array.from(names).sort() const result: UpstreamModelInfo[] = []
modelMap.forEach((formats, id) => {
result.push({ id, api_formats: Array.from(formats).sort() })
})
return result.sort((a, b) => a.id.localeCompare(b.id))
}) })
// 上游模型名称列表(用于计数和全选判断)
const upstreamModelNames = computed(() => upstreamModelList.value.map(m => m.id))
// 过滤后的上游模型 // 过滤后的上游模型
const filteredUpstreamModels = computed(() => { const filteredUpstreamModels = computed(() => {
if (!searchQuery.value.trim()) return upstreamModelNames.value if (!searchQuery.value.trim()) return upstreamModelList.value
const query = searchQuery.value.toLowerCase() const query = searchQuery.value.toLowerCase()
return upstreamModelNames.value.filter(m => m.toLowerCase().includes(query)) return upstreamModelList.value.filter(m => m.id.toLowerCase().includes(query))
}) })
// 上游模型是否全选 // 上游模型是否全选
const isAllUpstreamModelsSelected = computed(() => { const isAllUpstreamModelsSelected = computed(() => {
if (filteredUpstreamModels.value.length === 0) return false if (filteredUpstreamModels.value.length === 0) return false
return filteredUpstreamModels.value.every(m => selectedModels.value.includes(m)) return filteredUpstreamModels.value.every(m => selectedModels.value.includes(m.id))
}) })
// 全选/取消全选上游模型 // 全选/取消全选上游模型
function toggleAllUpstreamModels() { function toggleAllUpstreamModels() {
const allIds = filteredUpstreamModels.value const allIds = filteredUpstreamModels.value.map(m => m.id)
if (isAllUpstreamModelsSelected.value) { if (isAllUpstreamModelsSelected.value) {
selectedModels.value = selectedModels.value.filter(id => !allIds.includes(id)) selectedModels.value = selectedModels.value.filter(id => !allIds.includes(id))
} else { } else {
@@ -495,9 +517,9 @@ function toggleAllUpstreamModels() {
} }
// 处理上游模型点击(自动同步模式下禁用) // 处理上游模型点击(自动同步模式下禁用)
function handleUpstreamModelClick(model: string) { function handleUpstreamModelClick(model: UpstreamModelInfo) {
if (!isAutoFetchMode.value) { if (!isAutoFetchMode.value) {
toggleModel(model) toggleModel(model.id)
} }
} }