| import { ref, reactive, onUnmounted, toRaw } from 'vue' |
| import { useDebounceFn } from '@vueuse/core' |
| import type { BasePaginationResponse, FetchOptions } from '@/types' |
| import { getPersistedPageSize, setPersistedPageSize } from './usePersistedPageSize' |
|
|
| 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 |
| } |
|
|
| |
| |
| |
| |
| export function useTableLoader<T, P extends Record<string, any>>(options: TableLoaderOptions<T, P>) { |
| const { fetchFn, initialParams, pageSize, 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 ?? getPersistedPageSize(), |
| 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 |
| setPersistedPageSize(size) |
| load() |
| } |
|
|
| onUnmounted(() => { |
| abortController?.abort() |
| }) |
|
|
| return { |
| items, |
| loading, |
| params, |
| pagination, |
| load, |
| reload, |
| debouncedReload, |
| handlePageChange, |
| handlePageSizeChange |
| } |
| } |
|
|