refactor: 优化提供商管理界面交互与术语,增强 OAuth 编辑体验

- 模型映射对话框统一术语为"客户端模型/提供商模型",修复编辑时已有映射不可见的问题
- 密钥表单包含/排除规则改为双列布局
- OAuth 密钥编辑对话框增加 dirty 状态跟踪,关闭时提示未保存更改
- 调整密钥操作按钮顺序,代理节点配置移至编辑按钮前
- preset_models.py 添加 ModelsFetcherFunc 类型别名
This commit is contained in:
fawney19
2026-02-09 13:24:42 +08:00
parent 57f3ed71a1
commit 0da34f729e
5 changed files with 127 additions and 68 deletions

View File

@@ -272,21 +272,23 @@
v-if="form.auto_fetch_models" v-if="form.auto_fetch_models"
class="space-y-2 pt-2 border-t border-border/40" class="space-y-2 pt-2 border-t border-border/40"
> >
<div> <div class="grid grid-cols-2 gap-3">
<Label class="text-xs">包含规则</Label> <div>
<Input <Label class="text-xs">包含规则</Label>
v-model="form.model_include_patterns_text" <Input
placeholder="gpt-*, claude-*, 留空包含全部" v-model="form.model_include_patterns_text"
class="h-8 text-sm" placeholder="gpt-*, claude-*, 留空包含全部"
/> class="h-8 text-sm"
</div> />
<div> </div>
<Label class="text-xs">排除规则</Label> <div>
<Input <Label class="text-xs">排除规则</Label>
v-model="form.model_exclude_patterns_text" <Input
placeholder="*-preview, *-beta" v-model="form.model_exclude_patterns_text"
class="h-8 text-sm" placeholder="*-preview, *-beta"
/> class="h-8 text-sm"
/>
</div>
</div> </div>
<p class="text-xs text-muted-foreground"> <p class="text-xs text-muted-foreground">
逗号分隔,支持 * ? 通配符,不区分大小写 逗号分隔,支持 * ? 通配符,不区分大小写

View File

@@ -2,7 +2,7 @@
<Dialog <Dialog
:model-value="open" :model-value="open"
:title="editingGroup ? '编辑模型映射' : '添加模型映射'" :title="editingGroup ? '编辑模型映射' : '添加模型映射'"
:description="editingGroup ? '修改映射配置' : '为模型添加名称映射'" :description="editingGroup ? '修改映射配置' : '将提供商模型映射到客户端模型'"
:icon="Tag" :icon="Tag"
size="lg" size="lg"
@update:model-value="$emit('update:open', $event)" @update:model-value="$emit('update:open', $event)"
@@ -10,14 +10,14 @@
<div class="space-y-4"> <div class="space-y-4">
<!-- 目标模型选择 --> <!-- 目标模型选择 -->
<div class="space-y-1.5"> <div class="space-y-1.5">
<Label class="text-xs">目标模型</Label> <Label class="text-xs">客户端模型</Label>
<Select <Select
:model-value="formData.modelId" :model-value="formData.modelId"
:disabled="!!editingGroup" :disabled="!!editingGroup"
@update:model-value="handleModelChange" @update:model-value="handleModelChange"
> >
<SelectTrigger class="h-9"> <SelectTrigger class="h-9">
<SelectValue placeholder="请选择目标模型" /> <SelectValue placeholder="请选择客户端模型" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem <SelectItem
@@ -33,20 +33,20 @@
</SelectContent> </SelectContent>
</Select> </Select>
<p class="text-xs text-muted-foreground"> <p class="text-xs text-muted-foreground">
选中的映射名称将指向此模型 客户端请求此模型时将路由到选中的提供商模型
</p> </p>
</div> </div>
<!-- 映射名称选择面板 --> <!-- 映射名称选择面板 -->
<div class="space-y-1.5"> <div class="space-y-1.5">
<Label class="text-xs">映射名称</Label> <Label class="text-xs">提供商模型</Label>
<!-- 搜索栏 --> <!-- 搜索栏 -->
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="flex-1 relative"> <div class="flex-1 relative">
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" /> <Search class="absolute left-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input <Input
v-model="searchQuery" v-model="searchQuery"
placeholder="搜索或添加自定义映射名称..." placeholder="搜索或添加自定义提供商模型..."
class="pl-8 h-9" class="pl-8 h-9"
/> />
</div> </div>
@@ -94,7 +94,7 @@
<!-- 模型列表 --> <!-- 模型列表 -->
<div class="border rounded-lg overflow-hidden"> <div class="border rounded-lg overflow-hidden">
<div class="max-h-80 overflow-y-auto"> <div class="min-h-60 max-h-80 overflow-y-auto">
<!-- 加载中 --> <!-- 加载中 -->
<div <div
v-if="loadingModels" v-if="loadingModels"
@@ -117,7 +117,7 @@
<Plus class="w-4 h-4 text-muted-foreground" /> <Plus class="w-4 h-4 text-muted-foreground" />
<span class="text-sm font-mono">{{ searchQuery }}</span> <span class="text-sm font-mono">{{ searchQuery }}</span>
</div> </div>
<span class="text-xs text-muted-foreground">添加自定义模型</span> <span class="text-xs text-muted-foreground">添加自定义提供商模型</span>
</div> </div>
</div> </div>
@@ -132,7 +132,7 @@
class="w-4 h-4 transition-transform shrink-0" class="w-4 h-4 transition-transform shrink-0"
:class="collapsedGroups.has('custom') ? '-rotate-90' : ''" :class="collapsedGroups.has('custom') ? '-rotate-90' : ''"
/> />
<span class="text-xs font-medium">自定义映射</span> <span class="text-xs font-medium">自定义模型</span>
<span class="text-xs text-muted-foreground">({{ customNames.length }})</span> <span class="text-xs text-muted-foreground">({{ customNames.length }})</span>
</div> </div>
</div> </div>
@@ -216,7 +216,7 @@
{{ searchQuery ? '无匹配结果' : '暂无可选模型' }} {{ searchQuery ? '无匹配结果' : '暂无可选模型' }}
</p> </p>
<p class="text-xs mt-1"> <p class="text-xs mt-1">
输入名称后点击添加自定义映射 输入模型名称后点击添加自定义提供商模型
</p> </p>
</div> </div>
</template> </template>
@@ -486,8 +486,10 @@ function initForm() {
formData.value = { formData.value = {
modelId: props.editingGroup.model.id modelId: props.editingGroup.model.id
} }
selectedNames.value = props.editingGroup.aliases.map(a => a.name) const existingNames = props.editingGroup.aliases.map(a => a.name)
allCustomNames.value = [] selectedNames.value = [...existingNames]
// 将已有映射名称添加到自定义列表,使其在列表中可见(可取消选中来移除映射)
allCustomNames.value = [...existingNames]
} else { } else {
formData.value = { formData.value = {
modelId: props.preselectedModelId || '' modelId: props.preselectedModelId || ''

View File

@@ -135,21 +135,23 @@
v-if="form.auto_fetch_models" v-if="form.auto_fetch_models"
class="space-y-2 pt-2 border-t border-border/40" class="space-y-2 pt-2 border-t border-border/40"
> >
<div> <div class="grid grid-cols-2 gap-3">
<Label class="text-xs">包含规则</Label> <div>
<Input <Label class="text-xs">包含规则</Label>
v-model="form.model_include_patterns_text" <Input
placeholder="gpt-*, claude-*, 留空包含全部" v-model="form.model_include_patterns_text"
class="h-8 text-sm" placeholder="gpt-*, claude-*, 留空包含全部"
/> class="h-8 text-sm"
</div> />
<div> </div>
<Label class="text-xs">排除规则</Label> <div>
<Input <Label class="text-xs">排除规则</Label>
v-model="form.model_exclude_patterns_text" <Input
placeholder="*-preview, *-beta" v-model="form.model_exclude_patterns_text"
class="h-8 text-sm" placeholder="*-preview, *-beta"
/> class="h-8 text-sm"
/>
</div>
</div> </div>
<p class="text-xs text-muted-foreground"> <p class="text-xs text-muted-foreground">
逗号分隔,支持 * ? 通配符,不区分大小写 逗号分隔,支持 * ? 通配符,不区分大小写
@@ -176,10 +178,11 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue' import { ref, computed, watch } from 'vue'
import { Dialog, Button, Input, Label, Switch } from '@/components/ui' import { Dialog, Button, Input, Label, Switch } from '@/components/ui'
import { SquarePen } from 'lucide-vue-next' import { SquarePen } from 'lucide-vue-next'
import { useToast } from '@/composables/useToast' import { useToast } from '@/composables/useToast'
import { useConfirm } from '@/composables/useConfirm'
import { useFormDialog } from '@/composables/useFormDialog' import { useFormDialog } from '@/composables/useFormDialog'
import { parseApiError } from '@/utils/errorParser' import { parseApiError } from '@/utils/errorParser'
import { parseNumberInput, parseNullableNumberInput } from '@/utils/form' import { parseNumberInput, parseNullableNumberInput } from '@/utils/form'
@@ -200,6 +203,7 @@ const emit = defineEmits<{
}>() }>()
const { success, error: showError } = useToast() const { success, error: showError } = useToast()
const { confirmWarning } = useConfirm()
// 显示自动获取模型警告:编辑模式下,原本未启用但现在启用,且已有 allowed_models // 显示自动获取模型警告:编辑模式下,原本未启用但现在启用,且已有 allowed_models
const showAutoFetchWarning = computed(() => { const showAutoFetchWarning = computed(() => {
@@ -238,6 +242,27 @@ const form = ref({
model_exclude_patterns_text: '' model_exclude_patterns_text: ''
}) })
// ---------------------------------------------------------------------------
// Dirty 状态跟踪:通过快照比较判断表单是否被修改
// ---------------------------------------------------------------------------
const formSnapshot = ref('')
function takeSnapshot() {
formSnapshot.value = JSON.stringify(form.value)
}
const isDirty = computed(() => {
if (!formSnapshot.value) return false
return JSON.stringify(form.value) !== formSnapshot.value
})
// 对话框关闭时清除快照(快照在 loadKeyData 中拍摄)
watch(isOpen, (val) => {
if (!val) {
formSnapshot.value = ''
}
})
// 重置表单 // 重置表单
function resetForm() { function resetForm() {
form.value = { form.value = {
@@ -251,6 +276,7 @@ function resetForm() {
model_include_patterns_text: '', model_include_patterns_text: '',
model_exclude_patterns_text: '' model_exclude_patterns_text: ''
} }
formSnapshot.value = ''
} }
// 加载密钥数据 // 加载密钥数据
@@ -267,10 +293,15 @@ function loadKeyData() {
model_include_patterns_text: (props.editingKey.model_include_patterns || []).join(', '), model_include_patterns_text: (props.editingKey.model_include_patterns || []).join(', '),
model_exclude_patterns_text: (props.editingKey.model_exclude_patterns || []).join(', ') model_exclude_patterns_text: (props.editingKey.model_exclude_patterns || []).join(', ')
} }
// 数据加载完成后拍快照,作为 dirty 判断的基准
takeSnapshot()
} }
// 使用 useFormDialog 统一处理对话框逻辑 // 使用 useFormDialog 统一处理对话框逻辑
const { handleDialogUpdate, handleCancel } = useFormDialog({ const {
handleDialogUpdate: _baseHandleDialogUpdate,
handleCancel: _baseHandleCancel,
} = useFormDialog({
isOpen: () => props.open, isOpen: () => props.open,
entity: () => props.editingKey, entity: () => props.editingKey,
isLoading: saving, isLoading: saving,
@@ -279,6 +310,23 @@ const { handleDialogUpdate, handleCancel } = useFormDialog({
resetForm, resetForm,
}) })
// 包装关闭逻辑:有未保存更改时弹出确认
async function handleDialogUpdate(value: boolean) {
if (!value && isDirty.value) {
const confirmed = await confirmWarning('有未保存的更改,确定要关闭吗?', '放弃更改')
if (!confirmed) return
}
_baseHandleDialogUpdate(value)
}
async function handleCancel() {
if (isDirty.value) {
const confirmed = await confirmWarning('有未保存的更改,确定要关闭吗?', '放弃更改')
if (!confirmed) return
}
_baseHandleCancel()
}
// 将逗号分隔的文本解析为数组(去空、去重) // 将逗号分隔的文本解析为数组(去空、去重)
function parsePatternText(text: string): string[] { function parsePatternText(text: string): string[] {
if (!text.trim()) return [] if (!text.trim()) return []

View File

@@ -391,25 +391,6 @@
> >
<Shield class="w-3.5 h-3.5" /> <Shield class="w-3.5 h-3.5" />
</Button> </Button>
<Button
variant="ghost"
size="icon"
class="h-7 w-7"
title="编辑密钥"
@click="handleEditKey(endpoint, key)"
>
<Edit class="w-3.5 h-3.5" />
</Button>
<Button
v-if="provider.provider_type === 'antigravity'"
variant="ghost"
size="icon"
class="h-7 w-7"
title="配额详情"
@click="openAntigravityQuotaDialog(key)"
>
<BarChart3 class="w-3.5 h-3.5" />
</Button>
<!-- 代理节点配置仅非 custom 类型显示 --> <!-- 代理节点配置仅非 custom 类型显示 -->
<Popover <Popover
v-if="provider.provider_type !== 'custom'" v-if="provider.provider_type !== 'custom'"
@@ -459,6 +440,25 @@
</div> </div>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
<Button
variant="ghost"
size="icon"
class="h-7 w-7"
title="编辑密钥"
@click="handleEditKey(endpoint, key)"
>
<Edit class="w-3.5 h-3.5" />
</Button>
<Button
v-if="provider.provider_type === 'antigravity'"
variant="ghost"
size="icon"
class="h-7 w-7"
title="配额详情"
@click="openAntigravityQuotaDialog(key)"
>
<BarChart3 class="w-3.5 h-3.5" />
</Button>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"

View File

@@ -10,8 +10,16 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Awaitable, Callable
from typing import Any from typing import Any
# Fetcher 函数签名:与 UpstreamModelsFetcherRegistry 中的 _ModelsFetcher 保持一致
# (ctx, timeout_seconds) -> (models, errors, has_success, upstream_metadata)
ModelsFetcherFunc = Callable[
[Any, float],
Awaitable[tuple[list[dict], list[str], bool, dict[str, Any] | None]],
]
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 预设模型定义 # 预设模型定义
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -124,7 +132,7 @@ def get_preset_models(provider_type: str) -> list[dict[str, Any]]:
def create_preset_models_fetcher( def create_preset_models_fetcher(
provider_type: str, provider_type: str,
) -> Any: ) -> ModelsFetcherFunc:
"""创建一个返回预设模型列表的 fetcher 函数。 """创建一个返回预设模型列表的 fetcher 函数。
Args: Args:
@@ -136,22 +144,21 @@ def create_preset_models_fetcher(
models = get_preset_models(provider_type) models = get_preset_models(provider_type)
async def fetch_preset_models( async def fetch_preset_models(
ctx: Any, _ctx: Any,
timeout_seconds: float, # noqa: ARG001 _timeout_seconds: float,
) -> tuple[list[dict], list[str], bool, dict[str, Any] | None]: ) -> tuple[list[dict], list[str], bool, dict[str, Any] | None]:
"""Return preset model catalog. """Return preset model catalog.
This provider does not expose a /v1/models endpoint, so we skip the This provider does not expose a /v1/models endpoint, so we skip the
HTTP call entirely and return a hardcoded list. HTTP call entirely and return a hardcoded list.
""" """
_ = ctx
_ = timeout_seconds
return list(models), [], True, None return list(models), [], True, None
return fetch_preset_models return fetch_preset_models
__all__ = [ __all__ = [
"ModelsFetcherFunc",
"PRESET_MODELS", "PRESET_MODELS",
"create_preset_models_fetcher", "create_preset_models_fetcher",
"get_preset_models", "get_preset_models",