mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
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:
@@ -9,13 +9,32 @@ import type {
|
||||
} from './types'
|
||||
|
||||
/**
|
||||
* 获取 Providers 摘要(包含 Endpoints 统计)
|
||||
* 获取 Providers 摘要(分页)
|
||||
*/
|
||||
export async function getProvidersSummary(): Promise<ProviderWithEndpointsSummary[]> {
|
||||
return dedupedRequest('providers:summary', async () => {
|
||||
const response = await client.get<ProviderWithEndpointsSummary[]>('/api/admin/providers/summary')
|
||||
return response.data
|
||||
})
|
||||
export interface ProviderSummaryQuery {
|
||||
page?: number
|
||||
page_size?: number
|
||||
search?: string
|
||||
status?: string
|
||||
api_format?: string
|
||||
model_id?: string
|
||||
}
|
||||
|
||||
export interface ProviderSummaryPageResponse {
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
items: ProviderWithEndpointsSummary[]
|
||||
}
|
||||
|
||||
export async function getProvidersSummary(
|
||||
params: ProviderSummaryQuery = {},
|
||||
): Promise<ProviderSummaryPageResponse> {
|
||||
const response = await client.get<ProviderSummaryPageResponse>(
|
||||
'/api/admin/providers/summary',
|
||||
{ params },
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1168,7 +1168,7 @@ async function ensureProviderOptions() {
|
||||
providerOptionsRequest = (async () => {
|
||||
try {
|
||||
loadingProviderOptions.value = true
|
||||
providerOptions.value = await getProvidersSummary()
|
||||
providerOptions.value = (await getProvidersSummary({ page_size: 9999 })).items
|
||||
} catch (err: unknown) {
|
||||
const message = parseApiError(err, '加载 Provider 列表失败')
|
||||
showError(message, '错误')
|
||||
@@ -1494,7 +1494,7 @@ async function refreshData() {
|
||||
async function loadProviders() {
|
||||
const requestId = ++providersRequestId
|
||||
try {
|
||||
const nextProviders = await getProvidersSummary()
|
||||
const nextProviders = (await getProvidersSummary({ page_size: 9999 })).items
|
||||
if (requestId !== providersRequestId) return
|
||||
providers.value = nextProviders
|
||||
} catch (err: unknown) {
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div
|
||||
v-else-if="filteredProviders.length === 0"
|
||||
v-else-if="providers.length === 0"
|
||||
class="flex flex-col items-center justify-center py-16 text-center"
|
||||
>
|
||||
<div class="text-muted-foreground mb-2">
|
||||
@@ -87,7 +87,7 @@
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<ProviderTableRow
|
||||
v-for="provider in paginatedProviders"
|
||||
v-for="provider in providers"
|
||||
:key="provider.id"
|
||||
:provider="provider"
|
||||
:editing-description-id="editingDescriptionId"
|
||||
@@ -118,11 +118,11 @@
|
||||
|
||||
<!-- 移动端卡片列表 -->
|
||||
<div
|
||||
v-if="!loading && filteredProviders.length > 0"
|
||||
v-if="!loading && providers.length > 0"
|
||||
class="xl:hidden divide-y divide-border/40"
|
||||
>
|
||||
<ProviderMobileCard
|
||||
v-for="provider in paginatedProviders"
|
||||
v-for="provider in providers"
|
||||
:key="provider.id"
|
||||
:provider="provider"
|
||||
:editing-description-id="editingDescriptionId"
|
||||
@@ -146,9 +146,9 @@
|
||||
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
v-if="!loading && filteredProviders.length > 0"
|
||||
v-if="!loading && total > 0"
|
||||
:current="currentPage"
|
||||
:total="filteredProviders.length"
|
||||
:total="total"
|
||||
:page-size="pageSize"
|
||||
cache-key="provider-management-page-size"
|
||||
@update:current="currentPage = $event"
|
||||
@@ -168,7 +168,6 @@
|
||||
|
||||
<PriorityManagementDialog
|
||||
v-model="priorityDialogOpen"
|
||||
:providers="providers"
|
||||
@saved="handlePrioritySaved"
|
||||
/>
|
||||
|
||||
@@ -190,7 +189,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
||||
import Button from '@/components/ui/button.vue'
|
||||
import Card from '@/components/ui/card.vue'
|
||||
import Table from '@/components/ui/table.vue'
|
||||
@@ -247,13 +246,12 @@ const {
|
||||
apiFormatFilters,
|
||||
modelFilters,
|
||||
hasActiveFilters,
|
||||
filteredProviders,
|
||||
currentPage,
|
||||
pageSize,
|
||||
paginatedProviders,
|
||||
total,
|
||||
queryParams,
|
||||
resetFilters,
|
||||
} = useProviderFilters(
|
||||
() => providers.value,
|
||||
() => globalModels.value,
|
||||
)
|
||||
|
||||
@@ -349,14 +347,15 @@ async function loadGlobalModelList() {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载提供商列表
|
||||
// 加载提供商列表(服务端分页)
|
||||
async function loadProviders() {
|
||||
const requestId = ++providersRequestId
|
||||
loading.value = true
|
||||
try {
|
||||
const nextProviders = await getProvidersSummary()
|
||||
const response = await getProvidersSummary(queryParams.value)
|
||||
if (requestId !== providersRequestId) return
|
||||
providers.value = nextProviders
|
||||
providers.value = response.items
|
||||
total.value = response.total
|
||||
// 异步加载配置了 ops 的 provider 的余额数据
|
||||
loadBalances(providers.value)
|
||||
} catch (err: unknown) {
|
||||
@@ -369,6 +368,24 @@ async function loadProviders() {
|
||||
}
|
||||
}
|
||||
|
||||
// 分页/筛选/搜索变化时重新加载
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
watch(queryParams, (newParams, oldParams) => {
|
||||
if (debounceTimer) clearTimeout(debounceTimer)
|
||||
// 搜索输入 debounce 300ms,其他变化立即执行
|
||||
const isSearchOnly = newParams.search !== oldParams?.search &&
|
||||
newParams.page === oldParams?.page &&
|
||||
newParams.page_size === oldParams?.page_size &&
|
||||
newParams.status === oldParams?.status &&
|
||||
newParams.api_format === oldParams?.api_format &&
|
||||
newParams.model_id === oldParams?.model_id
|
||||
if (isSearchOnly) {
|
||||
debounceTimer = setTimeout(loadProviders, 300)
|
||||
} else {
|
||||
loadProviders()
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
// 使用复用的行点击逻辑
|
||||
const { handleMouseDown, shouldTriggerRowClick } = useRowClick()
|
||||
|
||||
@@ -508,6 +525,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (debounceTimer) clearTimeout(debounceTimer)
|
||||
document.removeEventListener('click', handleGlobalClick, true)
|
||||
stopTick()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user