fix(frontend): highlight selected models missing upstream (#528)

* fix(frontend): highlight unavailable selected models

* fix(frontend): refine model catalog warnings and refresh control

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
蓝芷怡
2026-10-26 18:39:08 +08:00
committed by GitHub
co-authored by Codex Zyy
parent 51c5caed0e
commit c43a8872db
4 changed files with 52 additions and 21 deletions
@@ -195,6 +195,7 @@
"searchOrEnterIdAndPressEnter": "Search or enter ID and press Enter to add",
"selectModel": "Select model",
"notInCurrentModelCatalog": "{id} (not in the current model catalog)",
"modelCatalogMissingHint": "The current upstream catalog does not list this model, your selection has been kept",
"loadingModel": "Loading model",
"noMatchingModels": "No matching models",
"noModels": "No models",
@@ -195,6 +195,7 @@
"searchOrEnterIdAndPressEnter": "搜索或输入 ID,回车添加",
"selectModel": "选择模型",
"notInCurrentModelCatalog": "{id}(当前目录未返回)",
"modelCatalogMissingHint": "当前上游目录未返回此模型,已保留你的选择",
"loadingModel": "加载模型中",
"noMatchingModels": "无匹配模型",
"noModels": "暂无模型",
@@ -195,6 +195,7 @@
"searchOrEnterIdAndPressEnter": "搜索或輸入 ID,回車添加",
"selectModel": "選擇模型",
"notInCurrentModelCatalog": "{id}(當前目錄未返回)",
"modelCatalogMissingHint": "當前上游目錄未返回此模型,已保留你的選擇",
"loadingModel": "加載模型中",
"noMatchingModels": "無匹配模型",
"noModels": "暫無模型",
@@ -1,9 +1,9 @@
<script setup lang="ts">
import type { AccountModelAccess } from '@/api'
import { ZCheckbox, ZEmpty, ZFormItem, ZIconButton, ZInput, ZScrollbar, ZSegmented } from '@codex-proxy/ui'
import { ZCheckbox, ZEmpty, ZFormItem, ZIconButton, ZInput, ZPopover, ZScrollbar, ZSegmented } from '@codex-proxy/ui'
import { RefreshCw, Search } from '@lucide/vue'
import { RefreshCw, Search, TriangleAlert } from '@lucide/vue'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { getAccountModels, refreshAccountModels } from '@/api'
@@ -19,7 +19,8 @@ const props = withDefaults(defineProps<{
const { t } = useI18n({ useScope: 'global' })
const model = defineModel<AccountModelAccess | undefined>({ required: true })
const catalog = ref<Array<{ id: string, label: string }>>([])
// 未取得目录时不能判断缺失,成功返回空目录则可以
const catalog = ref<Array<{ id: string, label: string }>>()
const search = ref('')
const inputError = ref('')
const request = useRequestState()
@@ -67,13 +68,14 @@ const mode = computed({
const restricted = computed(() => mode.value === 'allowlist' || mode.value === 'denylist')
const canAdd = computed(() => Boolean(search.value.trim()) && !model.value?.models.includes(search.value.trim()))
const models = computed(() => {
const entries = new Map(catalog.value.map(item => [
const catalogReady = catalog.value !== undefined && !loading.value && !error.value
const entries = new Map((catalog.value ?? []).map(item => [
item.id,
{ ...item, unavailable: false },
]))
for (const id of model.value?.models ?? []) {
if (!entries.has(id))
entries.set(id, { id, label: id, unavailable: true })
entries.set(id, { id, label: id, unavailable: catalogReady })
}
const query = search.value.trim().toLowerCase()
return [
@@ -137,7 +139,7 @@ watch([
restricted,
], () => {
request.invalidate()
catalog.value = []
catalog.value = undefined
search.value = ''
inputError.value = ''
error.value = ''
@@ -152,15 +154,15 @@ watch(search, () => {
<template>
<div class="grid gap-3">
<ZFormItem :label="t('accounts.modelAccess.modelLimitations')">
<template v-if="restricted && accountId" #extra>
<ZIconButton :aria-label="t('accounts.modelAccess.refreshModel')" size="small" :loading="loading" :disabled="disabled" @click="load(true)">
<div class="flex min-w-0 items-center justify-between gap-2">
<ZSegmented v-model="mode" class="w-80 min-w-0 max-w-full" :aria-label="t('accounts.modelAccess.modelRestrictionMode')" :options="modes" :disabled="disabled" />
<ZIconButton v-if="restricted && accountId" :aria-label="t('accounts.modelAccess.refreshModel')" variant="plain" :loading="loading" :disabled="disabled" @click="load(true)">
<template #loading>
<RefreshCw class="size-3.5 animate-spin motion-reduce:animate-none" />
</template>
<RefreshCw class="size-3.5" />
</ZIconButton>
</template>
<ZSegmented v-model="mode" class="w-80 max-w-full" :aria-label="t('accounts.modelAccess.modelRestrictionMode')" :options="modes" :disabled="disabled" />
</div>
</ZFormItem>
<template v-if="restricted">
<ZInput v-model="search" :aria-label="t('accounts.modelAccess.searchOrAddModels')" :placeholder="t('accounts.modelAccess.searchOrEnterIdAndPressEnter')" :disabled="disabled" :aria-invalid="Boolean(inputError)" @keydown.enter.prevent="addModel">
@@ -173,21 +175,47 @@ watch(search, () => {
</p>
<ZScrollbar v-if="models.length" max-height="15rem" class="min-w-0 -m-1">
<div class="grid grid-cols-2 gap-2 p-1 sm:grid-cols-3" role="group" :aria-label="t('accounts.modelAccess.selectModel')" :aria-busy="loading || undefined">
<ZCheckbox
<div
v-for="item in models"
:key="item.id"
class="min-h-11 min-w-0 rounded-cp px-3 py-2.5 transition-colors duration-150 motion-reduce:transition-none"
class="flex min-w-0 items-center rounded-cp transition-colors duration-150 motion-reduce:transition-none"
:class="model?.models.includes(item.id) ? 'bg-cp-primary-container text-cp-primary-on-container' : 'bg-cp-fill-quaternary text-cp-text-secondary hover:bg-cp-fill-tertiary hover:text-cp-text'"
:model-value="model?.models.includes(item.id) ?? false"
:label="item.id"
:title="item.unavailable && accountId ? t('accounts.modelAccess.notInCurrentModelCatalog', { id: item.id }) : item.id"
:disabled="disabled"
@update:model-value="select(item.id, $event)"
>
<template #label>
<span class="block truncate">{{ item.id }}</span>
</template>
</ZCheckbox>
<ZCheckbox
class="min-h-11 min-w-0 flex-1 px-3 py-2.5"
:model-value="model?.models.includes(item.id) ?? false"
:label="item.id"
:title="item.id"
:disabled="disabled"
@update:model-value="select(item.id, $event)"
>
<template #label>
<span class="block truncate">{{ item.id }}</span>
</template>
</ZCheckbox>
<ZPopover v-if="item.unavailable" class="mr-1 shrink-0" trigger="hover-click" placement="top" role="tooltip">
<template #reference="{ open, id }">
<ZIconButton
class="bg-transparent! hover:bg-transparent! active:bg-transparent!"
size="small"
:aria-label="t('accounts.modelAccess.notInCurrentModelCatalog', { id: item.id })"
:aria-describedby="open ? id : undefined"
:aria-expanded="open"
:title="undefined"
>
<TriangleAlert class="size-3.5 text-cp-warning-text" aria-hidden="true" />
</ZIconButton>
</template>
<div class="max-w-64 space-y-1 px-3 py-2 text-cp-sm wrap-anywhere">
<p class="m-0 font-emphasis text-cp-text">
{{ item.id }}
</p>
<p class="m-0 text-cp-text-secondary">
{{ t('accounts.modelAccess.modelCatalogMissingHint') }}
</p>
</div>
</ZPopover>
</div>
</div>
</ZScrollbar>
<p v-else-if="loading" class="m-0 py-4 text-center text-cp-sm text-cp-text-tertiary" role="status">