修复的问题: 1. **搜索和筛选防抖不同步**(AccountsView.vue) - 问题:筛选器使用 reload(立即),搜索使用 debouncedReload(300ms延迟) - 修复:统一使用 debouncedReload,避免多余的API调用 2. **useTableLoader 竞态条件**(useTableLoader.ts) - 问题:finally 块检查 signal.aborted 而不是 controller 实例 - 修复:检查 abortController === currentController 3. **改进错误处理**(UsersView.vue) - 添加详细错误消息:error.response?.data?.detail || error.message - 用户可以看到具体的错误原因而不是通用消息 4. **分页边界检查**(useTableLoader.ts, UsersView.vue) - 添加页码有效性检查:Math.max(1, Math.min(page, pagination.pages || 1)) - 防止分页越界导致显示空表 影响范围: - frontend/src/composables/useTableLoader.ts - frontend/src/views/admin/AccountsView.vue - frontend/src/views/admin/UsersView.vue 测试:✓ 前端构建测试通过
109 lines
2.6 KiB
TypeScript
109 lines
2.6 KiB
TypeScript
import { ref, reactive, onUnmounted, toRaw } from 'vue'
|
|
import { useDebounceFn } from '@vueuse/core'
|
|
import type { BasePaginationResponse, FetchOptions } from '@/types'
|
|
|
|
interface PaginationState {
|
|
page: number
|
|
page_size: number
|
|
total: number
|
|
pages: number
|
|
}
|
|
|
|
interface TableLoaderOptions<T, P> {
|
|
fetchFn: (page: number, pageSize: number, params: P, options?: FetchOptions) => Promise<BasePaginationResponse<T>>
|
|
initialParams?: P
|
|
pageSize?: number
|
|
debounceMs?: number
|
|
}
|
|
|
|
/**
|
|
* 通用表格数据加载 Composable
|
|
* 统一处理分页、筛选、搜索防抖和请求取消
|
|
*/
|
|
export function useTableLoader<T, P extends Record<string, any>>(options: TableLoaderOptions<T, P>) {
|
|
const { fetchFn, initialParams, pageSize = 20, debounceMs = 300 } = options
|
|
|
|
const items = ref<T[]>([])
|
|
const loading = ref(false)
|
|
const params = reactive<P>({ ...(initialParams || {}) } as P)
|
|
const pagination = reactive<PaginationState>({
|
|
page: 1,
|
|
page_size: pageSize,
|
|
total: 0,
|
|
pages: 0
|
|
})
|
|
|
|
let abortController: AbortController | null = null
|
|
|
|
const isAbortError = (error: any) => {
|
|
return error?.name === 'AbortError' || error?.code === 'ERR_CANCELED' || error?.name === 'CanceledError'
|
|
}
|
|
|
|
const load = async () => {
|
|
if (abortController) {
|
|
abortController.abort()
|
|
}
|
|
const currentController = new AbortController()
|
|
abortController = currentController
|
|
loading.value = true
|
|
|
|
try {
|
|
const response = await fetchFn(
|
|
pagination.page,
|
|
pagination.page_size,
|
|
toRaw(params) as P,
|
|
{ signal: currentController.signal }
|
|
)
|
|
|
|
items.value = response.items || []
|
|
pagination.total = response.total || 0
|
|
pagination.pages = response.pages || 0
|
|
} catch (error) {
|
|
if (!isAbortError(error)) {
|
|
console.error('Table load error:', error)
|
|
throw error
|
|
}
|
|
} finally {
|
|
if (abortController === currentController) {
|
|
loading.value = false
|
|
}
|
|
}
|
|
}
|
|
|
|
const reload = () => {
|
|
pagination.page = 1
|
|
return load()
|
|
}
|
|
|
|
const debouncedReload = useDebounceFn(reload, debounceMs)
|
|
|
|
const handlePageChange = (page: number) => {
|
|
// 确保页码在有效范围内
|
|
const validPage = Math.max(1, Math.min(page, pagination.pages || 1))
|
|
pagination.page = validPage
|
|
load()
|
|
}
|
|
|
|
const handlePageSizeChange = (size: number) => {
|
|
pagination.page_size = size
|
|
pagination.page = 1
|
|
load()
|
|
}
|
|
|
|
onUnmounted(() => {
|
|
abortController?.abort()
|
|
})
|
|
|
|
return {
|
|
items,
|
|
loading,
|
|
params,
|
|
pagination,
|
|
load,
|
|
reload,
|
|
debouncedReload,
|
|
handlePageChange,
|
|
handlePageSizeChange
|
|
}
|
|
}
|