feat: Provider 摘要 API 改为服务端分页,支持搜索和筛选

- 后端 /summary 接口新增 page/page_size/search/status/api_format/model_id 参数
- 新增 ProviderSummaryPageResponse 分页响应模型
- 前端 useProviderFilters 从客户端筛选改为构建服务端查询参数
- ProviderManagement 通过 watch queryParams 实现分页/筛选联动,搜索 debounce 300ms
- PriorityManagementDialog 改为对话框打开时自行加载全量 providers
- 其他使用方(StandaloneKeyFormDialog/UserFormDialog/ReplayDialog/ModelManagement)适配新接口
This commit is contained in:
fawney19
2026-03-09 12:45:54 +08:00
parent 40736a8334
commit 0046123e22
10 changed files with 182 additions and 151 deletions

View File

@@ -401,12 +401,12 @@ const { isEditMode, handleDialogUpdate, handleCancel } = useFormDialog({
// 加载选项数据
async function loadAccessRestrictionOptions() {
try {
const [providersData, modelsData, formatsData] = await Promise.all([
getProvidersSummary(),
const [providersResponse, modelsData, formatsData] = await Promise.all([
getProvidersSummary({ page_size: 9999 }),
getGlobalModels({ limit: 1000, is_active: true }),
adminApi.getApiFormats()
])
providers.value = providersData
providers.value = providersResponse.items
globalModels.value = modelsData.models || []
allApiFormats.value = formatsData.formats?.map((f: { value: string }) => f.value) || []
} catch (err) {

View File

@@ -472,7 +472,7 @@ import Badge from '@/components/ui/badge.vue'
import { useToast } from '@/composables/useToast'
import { parseApiError } from '@/utils/errorParser'
import { updateProvider, updateProviderKey } from '@/api/endpoints'
import type { ProviderWithEndpointsSummary } from '@/api/endpoints'
import { getProvidersSummary, type ProviderWithEndpointsSummary } from '@/api/endpoints'
import { adminApi } from '@/api/admin'
import { batchQueryBalance, type ActionResultResponse, type BalanceInfo } from '@/api/providerOps'
import { API_FORMAT_SHORT } from '@/api/endpoints/types'
@@ -508,7 +508,6 @@ interface KeyWithMeta {
const props = defineProps<{
modelValue: boolean
providers: ProviderWithEndpointsSummary[]
}>()
const emit = defineEmits<{
@@ -663,7 +662,7 @@ function normalizeRateMultipliers(
const providerById = computed(() => {
const map = new Map<string, ProviderWithEndpointsSummary>()
props.providers.forEach((provider) => {
sortedProviders.value.forEach((provider) => {
map.set(provider.id, provider)
})
return map
@@ -671,7 +670,7 @@ const providerById = computed(() => {
const providerIdByName = computed(() => {
const map = new Map<string, string>()
props.providers.forEach((provider) => {
sortedProviders.value.forEach((provider) => {
if (!map.has(provider.name)) {
map.set(provider.name, provider.id)
}
@@ -686,7 +685,7 @@ function resolveProviderId(key: Pick<KeyWithMeta, 'provider_id' | 'provider_name
const poolProviderIds = computed(() => {
const set = new Set<string>()
props.providers.forEach((provider) => {
sortedProviders.value.forEach((provider) => {
if (provider.pool_advanced) {
set.add(provider.id)
}
@@ -835,16 +834,10 @@ function sortProvidersByActiveAndPriority(providers: ProviderWithEndpointsSummar
})
}
// 监听 props.providers 变化
watch(() => props.providers, (newProviders) => {
if (newProviders) {
sortedProviders.value = sortProvidersByActiveAndPriority(newProviders)
}
}, { immediate: true })
// 监听对话框打开
watch(internalOpen, async (open) => {
if (open) {
await loadAllProviders()
await loadCurrentPriorityMode()
await loadKeysByFormat()
// 异步加载余额数据
@@ -852,6 +845,16 @@ watch(internalOpen, async (open) => {
}
})
// 加载全量 providers优先级管理需要完整列表
async function loadAllProviders() {
try {
const response = await getProvidersSummary({ page: 1, page_size: 9999 })
sortedProviders.value = sortProvidersByActiveAndPriority(response.items)
} catch {
sortedProviders.value = []
}
}
// 加载当前的优先级模式配置
async function loadCurrentPriorityMode() {
try {

View File

@@ -1,5 +1,5 @@
import { ref, computed, watch } from 'vue'
import type { ProviderWithEndpointsSummary } from '@/api/endpoints'
import type { ProviderSummaryQuery } from '@/api/endpoints'
export interface FilterOption {
value: string
@@ -7,7 +7,6 @@ export interface FilterOption {
}
export function useProviderFilters(
providers: () => ProviderWithEndpointsSummary[],
globalModels: () => { id: string; name: string }[],
) {
// 搜索与筛选
@@ -33,11 +32,8 @@ export function useProviderFilters(
{ value: 'gemini:cli', label: 'Gemini CLI' },
]
// 动态计算模型筛选选项:只展示当前提供商列表中实际关联的全局模型
const modelFilters = computed<FilterOption[]>(() => {
const usedIds = new Set(providers().flatMap(p => p.global_model_ids || []))
const items = globalModels()
.filter(m => usedIds.has(m.id))
.map(m => ({ value: m.id, label: m.name }))
.sort((a, b) => a.label.localeCompare(b.label))
return [{ value: 'all', label: '全部模型' }, ...items]
@@ -52,68 +48,22 @@ export function useProviderFilters(
)
})
// 筛选后的提供商列表
const filteredProviders = computed(() => {
let result = [...providers()]
// 搜索筛选(支持空格分隔的多关键词 AND 搜索)
if (searchQuery.value.trim()) {
const keywords = searchQuery.value
.toLowerCase()
.split(/\s+/)
.filter(k => k.length > 0)
result = result.filter(p => {
const searchableText = `${p.name}`.toLowerCase()
return keywords.every(keyword => searchableText.includes(keyword))
})
}
// 状态筛选
if (filterStatus.value !== 'all') {
const isActive = filterStatus.value === 'active'
result = result.filter(p => p.is_active === isActive)
}
// API 格式筛选
if (filterApiFormat.value !== 'all') {
result = result.filter(
p => p.api_formats && p.api_formats.includes(filterApiFormat.value),
)
}
// 模型筛选
if (filterModel.value !== 'all') {
result = result.filter(
p => p.global_model_ids && p.global_model_ids.includes(filterModel.value),
)
}
// 排序
return result.sort((a, b) => {
// 1. 优先显示活跃的提供商
if (a.is_active !== b.is_active) {
return a.is_active ? -1 : 1
}
// 2. 按优先级排序
if (a.provider_priority !== b.provider_priority) {
return a.provider_priority - b.provider_priority
}
// 3. 按名称排序
return a.name.localeCompare(b.name)
})
})
// 分页
const currentPage = ref(1)
const pageSize = ref(20)
const total = ref(0)
const paginatedProviders = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return filteredProviders.value.slice(start, end)
})
// 服务端分页查询参数
const queryParams = computed<ProviderSummaryQuery>(() => ({
page: currentPage.value,
page_size: pageSize.value,
search: searchQuery.value.trim() || undefined,
status: filterStatus.value !== 'all' ? filterStatus.value : undefined,
api_format: filterApiFormat.value !== 'all' ? filterApiFormat.value : undefined,
model_id: filterModel.value !== 'all' ? filterModel.value : undefined,
}))
// 搜索/筛选时重置分页
// 搜索/筛选变化时重置分页到第1页
watch([searchQuery, filterStatus, filterApiFormat, filterModel], () => {
currentPage.value = 1
})
@@ -134,10 +84,10 @@ export function useProviderFilters(
apiFormatFilters,
modelFilters,
hasActiveFilters,
filteredProviders,
currentPage,
pageSize,
paginatedProviders,
total,
queryParams,
resetFilters,
}
}

View File

@@ -388,8 +388,8 @@ watch(() => props.isOpen, async (isOpen) => {
showRequestHeaders.value = false
showResponseHeaders.value = false
try {
const summary = await getProvidersSummary()
providers.value = summary
const response = await getProvidersSummary({ page_size: 9999 })
providers.value = response.items
.filter(p => p.is_active && p.active_endpoints > 0)
.map(p => ({ id: p.id, name: p.name }))
} catch (e) {

View File

@@ -536,12 +536,12 @@ const isFormValid = computed(() => {
// 加载访问控制选项
async function loadAccessControlOptions(): Promise<void> {
try {
const [providersData, modelsData, formatsData] = await Promise.all([
getProvidersSummary(),
const [providersResponse, modelsData, formatsData] = await Promise.all([
getProvidersSummary({ page_size: 9999 }),
getGlobalModels({ limit: 1000, is_active: true }),
adminApi.getApiFormats()
])
providers.value = providersData
providers.value = providersResponse.items
globalModels.value = modelsData.models || []
apiFormats.value = formatsData.formats || []
} catch (err) {