mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-06 01:17:46 +08:00
fix(frontend): preserve provider order across pagination
This commit is contained in:
@@ -34,6 +34,8 @@ export function useProviderDisplayOrder<Provider extends SortableProvider>(
|
||||
? [...new Set(savedOrder.value.filter((providerId): providerId is string => typeof providerId === 'string'))]
|
||||
: [])
|
||||
|
||||
const hasCustomOrder = computed(() => normalizedOrder.value.length > 0)
|
||||
|
||||
const orderedProviders = computed(() => {
|
||||
const ranks = new Map(normalizedOrder.value.map((providerId, index) => [providerId, index]))
|
||||
return [...providers()].sort((first, second) => (
|
||||
@@ -212,6 +214,7 @@ export function useProviderDisplayOrder<Provider extends SortableProvider>(
|
||||
|
||||
return {
|
||||
orderedProviders,
|
||||
hasCustomOrder,
|
||||
draggingProvider,
|
||||
dragPreviewStyle,
|
||||
announcement,
|
||||
|
||||
@@ -576,7 +576,8 @@ function sortProvidersByActiveAndPriority(items: ProviderWithEndpointsSummary[])
|
||||
|
||||
const providerListRef = ref<HTMLElement | null>(null)
|
||||
const {
|
||||
orderedProviders: displayedProviders,
|
||||
orderedProviders,
|
||||
hasCustomOrder,
|
||||
draggingProvider,
|
||||
dragPreviewStyle,
|
||||
announcement,
|
||||
@@ -587,6 +588,16 @@ const {
|
||||
sortItemClass,
|
||||
} = useProviderDisplayOrder(() => sortProvidersByActiveAndPriority(providers.value), providerListRef)
|
||||
|
||||
// 自定义展示顺序需要作用于整个列表,因此改为一次性拉取全量并在前端分页,
|
||||
// 否则服务端分页切片会把拖到前面的供应商排除在当前页之外(换每页条数后顺序回退)。
|
||||
const DISPLAY_ORDER_FETCH_PAGE_SIZE = 10_000
|
||||
const localPaging = computed(() => hasCustomOrder.value)
|
||||
const displayedProviders = computed(() => {
|
||||
if (!localPaging.value) return orderedProviders.value
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
return orderedProviders.value.slice(start, start + pageSize.value)
|
||||
})
|
||||
|
||||
watch([loading, cardView, queryParams], cancelDrag)
|
||||
|
||||
function startEditDescription(_event: Event, provider: ProviderWithEndpointsSummary) {
|
||||
@@ -645,7 +656,10 @@ async function loadProviders(options: { cacheTtlMs?: number } = {}) {
|
||||
const requestId = ++providersRequestId
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await getProvidersSummary(queryParams.value, {
|
||||
const requestParams = localPaging.value
|
||||
? { ...queryParams.value, page: 1, page_size: DISPLAY_ORDER_FETCH_PAGE_SIZE }
|
||||
: queryParams.value
|
||||
const response = await getProvidersSummary(requestParams, {
|
||||
cacheTtlMs: options.cacheTtlMs ?? 0,
|
||||
})
|
||||
if (requestId !== providersRequestId) return
|
||||
@@ -656,7 +670,8 @@ async function loadProviders(options: { cacheTtlMs?: number } = {}) {
|
||||
Object.assign(existing, item)
|
||||
return existing
|
||||
})
|
||||
total.value = response.total
|
||||
// 前端分页模式下总数以实际拉取到的条目为准,避免切片越界
|
||||
total.value = localPaging.value ? response.items.length : response.total
|
||||
// 异步加载配置了 ops 的 provider 的余额数据
|
||||
loadBalances(providers.value)
|
||||
} catch (err: unknown) {
|
||||
@@ -669,10 +684,26 @@ async function loadProviders(options: { cacheTtlMs?: number } = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
// 首次建立自定义顺序时,当前页可能只是全量的一部分,需要补拉全量以支持前端分页
|
||||
watch(hasCustomOrder, (enabled) => {
|
||||
if (enabled && providers.value.length < total.value) {
|
||||
void loadProviders({ cacheTtlMs: PROVIDER_SUMMARY_CACHE_TTL_MS })
|
||||
}
|
||||
})
|
||||
|
||||
// 分页/筛选/搜索变化时重新加载
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
watch(queryParams, (newParams, oldParams) => {
|
||||
if (debounceTimer) clearTimeout(debounceTimer)
|
||||
// 前端分页模式下翻页/换每页条数只影响本地切片,无需重新请求
|
||||
const isPagingOnly = (newParams.page !== oldParams?.page || newParams.page_size !== oldParams?.page_size) &&
|
||||
newParams.search === oldParams?.search &&
|
||||
newParams.status === oldParams?.status &&
|
||||
newParams.api_format === oldParams?.api_format &&
|
||||
newParams.model_id === oldParams?.model_id
|
||||
if (localPaging.value && isPagingOnly) {
|
||||
return
|
||||
}
|
||||
// 搜索输入 debounce 300ms,其他变化立即执行
|
||||
const isSearchOnly = newParams.search !== oldParams?.search &&
|
||||
newParams.page === oldParams?.page &&
|
||||
|
||||
@@ -528,32 +528,45 @@ describe('ProviderManagement shared display order', () => {
|
||||
expect(providerOrder(root)).toEqual(['provider-1', 'provider-2', 'provider-3', 'provider-4'])
|
||||
})
|
||||
|
||||
it('preserves saved ordering on other pages when reordering the current page', async () => {
|
||||
const providers = mockSortableProviders()
|
||||
apiMocks.getProvidersSummary.mockImplementation(async ({ page }: { page: number }) => ({
|
||||
items: page === 1 ? providers.slice(0, 2) : providers.slice(2),
|
||||
total: 40,
|
||||
it('keeps the saved ordering across local pages after reordering the current page', async () => {
|
||||
const providers = Array.from({ length: 12 }, (_, index) => createProvider({
|
||||
id: `provider-${index + 1}`,
|
||||
name: `Provider ${index + 1}`,
|
||||
provider_priority: (index + 1) * 10,
|
||||
}))
|
||||
apiMocks.getProvidersSummary.mockImplementation(async (query: { page?: number, page_size?: number } = {}) => {
|
||||
const page = query.page ?? 1
|
||||
const pageSize = query.page_size ?? 20
|
||||
return {
|
||||
items: providers.slice((page - 1) * pageSize, page * pageSize),
|
||||
total: providers.length,
|
||||
}
|
||||
})
|
||||
localStorage.setItem('provider-management-page-size', '10')
|
||||
const root = await mountView()
|
||||
const firstDrag = startProviderDrag(root, 'provider-1', 'provider-2')
|
||||
await dropProvider(firstDrag.handle)
|
||||
|
||||
const firstDrag = startProviderDrag(root, 'provider-2', 'provider-1')
|
||||
await dropProvider(firstDrag.handle)
|
||||
expect(providerOrder(root).slice(0, 2)).toEqual(['provider-2', 'provider-1'])
|
||||
|
||||
const requestsBeforePaging = apiMocks.getProvidersSummary.mock.calls.length
|
||||
const secondPage = [...root.querySelectorAll<HTMLButtonElement>('button')]
|
||||
.find(button => button.textContent?.trim() === '2')!
|
||||
secondPage.click()
|
||||
await settle()
|
||||
expect(providerOrder(root)).toEqual(['provider-3', 'provider-4'])
|
||||
const secondDrag = startProviderDrag(root, 'provider-4', 'provider-3')
|
||||
await dropProvider(secondDrag.handle)
|
||||
expect(providerOrder(root)).toEqual(['provider-4', 'provider-3'])
|
||||
expect(providerOrder(root)).toEqual(['provider-11', 'provider-12'])
|
||||
expect(apiMocks.getProvidersSummary).toHaveBeenCalledTimes(requestsBeforePaging)
|
||||
|
||||
const firstPage = [...root.querySelectorAll<HTMLButtonElement>('button')]
|
||||
.find(button => button.textContent?.trim() === '1')!
|
||||
firstPage.click()
|
||||
await settle()
|
||||
expect(providerOrder(root)).toEqual(['provider-2', 'provider-1'])
|
||||
expect(providerOrder(root).slice(0, 2)).toEqual(['provider-2', 'provider-1'])
|
||||
expect(JSON.parse(localStorage.getItem('aether-provider-display-order')!))
|
||||
.toEqual(['provider-2', 'provider-1', 'provider-4', 'provider-3'])
|
||||
.toEqual([
|
||||
'provider-2', 'provider-1', 'provider-3', 'provider-4', 'provider-5', 'provider-6',
|
||||
'provider-7', 'provider-8', 'provider-9', 'provider-10', 'provider-11', 'provider-12',
|
||||
])
|
||||
})
|
||||
|
||||
it('ignores stale IDs and appends providers that are not in the saved order', async () => {
|
||||
@@ -562,4 +575,36 @@ describe('ProviderManagement shared display order', () => {
|
||||
const root = await mountView()
|
||||
expect(providerOrder(root)).toEqual(['provider-3', 'provider-1', 'provider-2', 'provider-4'])
|
||||
})
|
||||
|
||||
it('keeps the dragged provider first after switching to a smaller page size', async () => {
|
||||
const providers = Array.from({ length: 12 }, (_, index) => createProvider({
|
||||
id: `provider-${index + 1}`,
|
||||
name: `Provider ${index + 1}`,
|
||||
provider_priority: (index + 1) * 10,
|
||||
}))
|
||||
apiMocks.getProvidersSummary.mockImplementation(async (query: { page?: number, page_size?: number } = {}) => {
|
||||
const page = query.page ?? 1
|
||||
const pageSize = query.page_size ?? 20
|
||||
return {
|
||||
items: providers.slice((page - 1) * pageSize, page * pageSize),
|
||||
total: providers.length,
|
||||
}
|
||||
})
|
||||
|
||||
localStorage.setItem('provider-management-page-size', '50')
|
||||
let root = await mountView()
|
||||
const { handle } = startProviderDrag(root, 'provider-12', 'provider-1')
|
||||
await dropProvider(handle)
|
||||
expect(providerOrder(root)[0]).toBe('provider-12')
|
||||
|
||||
unmountView()
|
||||
localStorage.setItem('provider-management-page-size', '10')
|
||||
root = await mountView()
|
||||
|
||||
expect(providerOrder(root)).toEqual([
|
||||
'provider-12',
|
||||
'provider-1', 'provider-2', 'provider-3', 'provider-4', 'provider-5',
|
||||
'provider-6', 'provider-7', 'provider-8', 'provider-9',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user