Files
zkxw/web-admin/src/hooks/core/useTable.ts

763 lines
23 KiB
TypeScript

/**
* useTable - 企业级表格数据管理方案
*
* 功能完整的表格数据管理解决方案,专为后台管理系统设计。
* 封装了表格开发中的所有常见需求,让你专注于业务逻辑。
*
* ## 主要功能
*
* 1. 数据管理 - 自动处理 API 请求、响应转换、加载状态和错误处理
* 2. 分页控制 - 自动同步分页状态、移动端适配、智能页码边界处理
* 3. 搜索功能 - 防抖搜索优化、参数管理、一键重置、参数过滤
* 4. 缓存系统 - 智能请求缓存、多种清理策略、自动过期管理、统计信息
* 5. 刷新策略 - 提供 5 种刷新方法适配不同业务场景(新增/更新/删除/手动/定时)
* 6. 列配置管理 - 动态显示/隐藏列、列排序、配置持久化、批量操作(可选)
*
* @module useTable
* @author Art Design Pro Team
*/
import { ref, reactive, computed, onMounted, onUnmounted, nextTick, readonly } from 'vue'
import { useWindowSize } from '@vueuse/core'
import { useTableColumns } from './useTableColumns'
import type { ColumnOption } from '@/types/component'
import {
TableCache,
CacheInvalidationStrategy,
type ApiResponse
} from '../../utils/table/tableCache'
import {
type TableError,
defaultResponseAdapter,
extractTableData,
updatePaginationFromResponse,
createSmartDebounce,
createErrorHandler
} from '../../utils/table/tableUtils'
import { tableConfig } from '../../utils/table/tableConfig'
// 类型推导工具类型
type InferApiParams<T> = T extends (params: infer P) => any ? P : never
type InferApiResponse<T> = T extends (params: any) => Promise<infer R> ? R : never
type InferRecordType<T> = T extends Api.Common.PaginatedResponse<infer U> ? U : never
// 优化的配置接口 - 支持自动类型推导
export interface UseTableConfig<
TApiFn extends (params: any) => Promise<any> = (params: any) => Promise<any>,
TRecord = InferRecordType<InferApiResponse<TApiFn>>,
TParams = InferApiParams<TApiFn>,
TResponse = InferApiResponse<TApiFn>
> {
// 核心配置
core: {
/** API 请求函数 */
apiFn: TApiFn
/** 默认请求参数 */
apiParams?: Partial<TParams>
/** 排除 apiParams 中的属性 */
excludeParams?: string[]
/** 是否立即加载数据 */
immediate?: boolean
/** 列配置工厂函数 */
columnsFactory?: () => ColumnOption<TRecord>[]
/** 自定义分页字段映射 */
paginationKey?: {
/** 当前页码字段名,默认为 'current' */
current?: string
/** 每页条数字段名,默认为 'size' */
size?: string
}
}
// 数据处理
transform?: {
/** 数据转换函数 */
dataTransformer?: (data: TRecord[]) => TRecord[]
/** 响应数据适配器 */
responseAdapter?: (response: TResponse) => ApiResponse<TRecord>
}
// 性能优化
performance?: {
/** 是否启用缓存 */
enableCache?: boolean
/** 缓存时间(毫秒) */
cacheTime?: number
/** 防抖延迟时间(毫秒) */
debounceTime?: number
/** 最大缓存条数限制 */
maxCacheSize?: number
}
// 生命周期钩子
hooks?: {
/** 数据加载成功回调(仅网络请求成功时触发) */
onSuccess?: (data: TRecord[], response: ApiResponse<TRecord>) => void
/** 错误处理回调 */
onError?: (error: TableError) => void
/** 缓存命中回调(从缓存获取数据时触发) */
onCacheHit?: (data: TRecord[], response: ApiResponse<TRecord>) => void
/** 加载状态变化回调 */
onLoading?: (loading: boolean) => void
/** 重置表单回调函数 */
resetFormCallback?: () => void
}
// 调试配置
debug?: {
/** 是否启用日志输出 */
enableLog?: boolean
/** 日志级别 */
logLevel?: 'info' | 'warn' | 'error'
}
}
export function useTable<TApiFn extends (params: any) => Promise<any>>(
config: UseTableConfig<TApiFn>
) {
return useTableImpl(config)
}
/**
* useTable 的核心实现 - 强大的表格数据管理 Hook
*
* 提供完整的表格解决方案,包括:
* - 数据获取与缓存
* - 分页控制
* - 搜索功能
* - 智能刷新策略
* - 错误处理
* - 列配置管理
*/
function useTableImpl<TApiFn extends (params: any) => Promise<any>>(
config: UseTableConfig<TApiFn>
) {
type TRecord = InferRecordType<InferApiResponse<TApiFn>>
type TParams = InferApiParams<TApiFn>
const {
core: {
apiFn,
apiParams = {} as Partial<TParams>,
excludeParams = [],
immediate = true,
columnsFactory,
paginationKey
},
transform: { dataTransformer, responseAdapter = defaultResponseAdapter } = {},
performance: {
enableCache = false,
cacheTime = 5 * 60 * 1000,
debounceTime = 300,
maxCacheSize = 50
} = {},
hooks: { onSuccess, onError, onCacheHit, resetFormCallback } = {},
debug: { enableLog = false } = {}
} = config
// 分页字段名配置:优先使用传入的配置,否则使用全局配置
const pageKey = paginationKey?.current || tableConfig.paginationKey.current
const sizeKey = paginationKey?.size || tableConfig.paginationKey.size
// 响应式触发器,用于手动更新缓存统计信息
const cacheUpdateTrigger = ref(0)
// 日志工具函数
const logger = {
log: (message: string, ...args: unknown[]) => {
if (enableLog) {
console.log(`[useTable] ${message}`, ...args)
}
},
warn: (message: string, ...args: unknown[]) => {
if (enableLog) {
console.warn(`[useTable] ${message}`, ...args)
}
},
error: (message: string, ...args: unknown[]) => {
if (enableLog) {
console.error(`[useTable] ${message}`, ...args)
}
}
}
// 缓存实例
const cache = enableCache ? new TableCache<TRecord>(cacheTime, maxCacheSize, enableLog) : null
// 加载状态机
type LoadingState = 'idle' | 'loading' | 'success' | 'error'
const loadingState = ref<LoadingState>('idle')
const loading = computed(() => loadingState.value === 'loading')
// 错误状态
const error = ref<TableError | null>(null)
// 表格数据
const data = ref<TRecord[]>([])
// 请求取消控制器
let abortController: AbortController | null = null
// 缓存清理定时器
let cacheCleanupTimer: NodeJS.Timeout | null = null
// 搜索参数
const searchParams = reactive(
Object.assign(
{
[pageKey]: 1,
[sizeKey]: 10
},
apiParams || {}
) as TParams
)
// 分页配置
const pagination = reactive<Api.Common.PaginationParams>({
current: ((searchParams as Record<string, unknown>)[pageKey] as number) || 1,
size: ((searchParams as Record<string, unknown>)[sizeKey] as number) || 10,
total: 0
})
// 移动端分页 (响应式)
const { width } = useWindowSize()
const mobilePagination = computed(() => ({
...pagination,
small: width.value < 768
}))
// 列配置
const columnConfig = columnsFactory ? useTableColumns<TRecord>(columnsFactory) : null
const columns = columnConfig?.columns
const columnChecks = columnConfig?.columnChecks
// 是否有数据
const hasData = computed(() => data.value.length > 0)
// 缓存统计信息
const cacheInfo = computed(() => {
// 依赖触发器,确保缓存变化时重新计算
void cacheUpdateTrigger.value
if (!cache) return { total: 0, size: '0KB', hitRate: '0 avg hits' }
return cache.getStats()
})
// 错误处理函数
const handleError = createErrorHandler(onError, enableLog)
// 清理缓存,根据不同的业务场景选择性地清理缓存
const clearCache = (strategy: CacheInvalidationStrategy, context?: string): void => {
if (!cache) return
let clearedCount = 0
switch (strategy) {
case CacheInvalidationStrategy.CLEAR_ALL:
cache.clear()
logger.log(`清空所有缓存 - ${context || ''}`)
break
case CacheInvalidationStrategy.CLEAR_CURRENT:
clearedCount = cache.clearCurrentSearch(searchParams)
logger.log(`清空当前搜索缓存 ${clearedCount} 条 - ${context || ''}`)
break
case CacheInvalidationStrategy.CLEAR_PAGINATION:
clearedCount = cache.clearPagination()
logger.log(`清空分页缓存 ${clearedCount} 条 - ${context || ''}`)
break
case CacheInvalidationStrategy.KEEP_ALL:
default:
logger.log(`保持缓存不变 - ${context || ''}`)
break
}
// 手动触发缓存状态更新
cacheUpdateTrigger.value++
}
// 获取数据的核心方法
const fetchData = async (
params?: Partial<TParams>,
useCache = enableCache
): Promise<ApiResponse<TRecord>> => {
// 取消上一个请求
if (abortController) {
abortController.abort()
}
// 创建新的取消控制器
const currentController = new AbortController()
abortController = currentController
// 状态机:进入 loading 状态
loadingState.value = 'loading'
error.value = null
try {
let requestParams = Object.assign(
{},
searchParams,
{
[pageKey]: pagination.current,
[sizeKey]: pagination.size
},
params || {}
) as TParams
// 剔除不需要的参数
if (excludeParams.length > 0) {
const filteredParams = { ...requestParams }
excludeParams.forEach((key) => {
delete (filteredParams as Record<string, unknown>)[key]
})
requestParams = filteredParams as TParams
}
// 检查缓存
if (useCache && cache) {
const cachedItem = cache.get(requestParams)
if (cachedItem) {
data.value = cachedItem.data
updatePaginationFromResponse(pagination, cachedItem.response)
// 修复:避免重复设置相同的值,防止响应式循环更新
const paramsRecord = searchParams as Record<string, unknown>
if (paramsRecord[pageKey] !== pagination.current) {
paramsRecord[pageKey] = pagination.current
}
if (paramsRecord[sizeKey] !== pagination.size) {
paramsRecord[sizeKey] = pagination.size
}
// 状态机:缓存命中,进入 success 状态
loadingState.value = 'success'
// 缓存命中时触发专门的回调,而不是 onSuccess
if (onCacheHit) {
onCacheHit(cachedItem.data, cachedItem.response)
}
logger.log(`缓存命中`)
return cachedItem.response
}
}
const response = await apiFn(requestParams)
// 检查请求是否被取消
if (currentController.signal.aborted) {
throw new Error('请求已取消')
}
// 使用响应适配器转换为标准格式
const standardResponse = responseAdapter(response)
// 处理响应数据
let tableData = extractTableData(standardResponse)
// 应用数据转换函数
if (dataTransformer) {
tableData = dataTransformer(tableData)
}
// 更新状态
data.value = tableData
updatePaginationFromResponse(pagination, standardResponse)
// 修复:避免重复设置相同的值,防止响应式循环更新
const paramsRecord = searchParams as Record<string, unknown>
if (paramsRecord[pageKey] !== pagination.current) {
paramsRecord[pageKey] = pagination.current
}
if (paramsRecord[sizeKey] !== pagination.size) {
paramsRecord[sizeKey] = pagination.size
}
// 缓存数据
if (useCache && cache) {
cache.set(requestParams, tableData, standardResponse)
// 手动触发缓存状态更新
cacheUpdateTrigger.value++
logger.log(`数据已缓存`)
}
// 状态机:请求成功,进入 success 状态
loadingState.value = 'success'
// 成功回调
if (onSuccess) {
onSuccess(tableData, standardResponse)
}
return standardResponse
} catch (err) {
if (err instanceof Error && err.message === '请求已取消') {
// 请求被取消,回到 idle 状态
loadingState.value = 'idle'
return { records: [], total: 0, current: 1, size: 10 }
}
// 状态机:请求失败,进入 error 状态
loadingState.value = 'error'
data.value = []
const tableError = handleError(err, '获取表格数据失败')
throw tableError
} finally {
// 只有当前控制器是活跃的才清空
if (abortController === currentController) {
abortController = null
}
}
}
// 获取数据 (保持当前页)
const getData = async (params?: Partial<TParams>): Promise<ApiResponse<TRecord> | void> => {
try {
return await fetchData(params)
} catch {
// 错误已在 fetchData 中处理
return Promise.resolve()
}
}
// 分页获取数据 (重置到第一页) - 专门用于搜索场景
const getDataByPage = async (params?: Partial<TParams>): Promise<ApiResponse<TRecord> | void> => {
pagination.current = 1
;(searchParams as Record<string, unknown>)[pageKey] = 1
// 搜索时清空当前搜索条件的缓存,确保获取最新数据
clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '搜索数据')
try {
return await fetchData(params, false) // 搜索时不使用缓存
} catch {
// 错误已在 fetchData 中处理
return Promise.resolve()
}
}
// 智能防抖搜索函数
const debouncedGetDataByPage = createSmartDebounce(getDataByPage, debounceTime)
// 重置搜索参数
const resetSearchParams = async (): Promise<void> => {
// 取消防抖的搜索
debouncedGetDataByPage.cancel()
// 保存分页相关的默认值
const paramsRecord = searchParams as Record<string, unknown>
const defaultPagination = {
[pageKey]: 1,
[sizeKey]: (paramsRecord[sizeKey] as number) || 10
}
// 清空所有搜索参数
Object.keys(searchParams).forEach((key) => {
delete paramsRecord[key]
})
// 重新设置默认参数
Object.assign(searchParams, apiParams || {}, defaultPagination)
// 重置分页
pagination.current = 1
pagination.size = defaultPagination[sizeKey] as number
// 清空错误状态
error.value = null
// 清空缓存
clearCache(CacheInvalidationStrategy.CLEAR_ALL, '重置搜索')
// 重新获取数据
await getData()
// 执行重置回调
if (resetFormCallback) {
await nextTick()
resetFormCallback()
}
}
// 替换搜索参数:适用于表单查询,避免旧字段残留
const replaceSearchParams = (params?: Partial<TParams>): void => {
const paramsRecord = searchParams as Record<string, unknown>
const currentSize = pagination.size || ((paramsRecord[sizeKey] as number) ?? 10)
Object.keys(searchParams).forEach((key) => {
if (key !== pageKey && key !== sizeKey) {
delete paramsRecord[key]
}
})
Object.assign(
searchParams,
{
[pageKey]: 1,
[sizeKey]: currentSize
},
params || {}
)
pagination.current = 1
pagination.size = currentSize
}
// 防重复调用的标志
let isCurrentChanging = false
// 处理分页大小变化
const handleSizeChange = async (newSize: number): Promise<void> => {
if (newSize <= 0) return
debouncedGetDataByPage.cancel()
const paramsRecord = searchParams as Record<string, unknown>
pagination.size = newSize
pagination.current = 1
paramsRecord[sizeKey] = newSize
paramsRecord[pageKey] = 1
clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '分页大小变化')
await getData()
}
// 处理当前页变化
const handleCurrentChange = async (newCurrent: number): Promise<void> => {
if (newCurrent <= 0) return
// 修复:防止重复调用
if (isCurrentChanging) {
return
}
// 修复:如果当前页没有变化,不需要重新请求
if (pagination.current === newCurrent) {
logger.log('分页页码未变化,跳过请求')
return
}
try {
isCurrentChanging = true
// 修复:只更新必要的状态
const paramsRecord = searchParams as Record<string, unknown>
pagination.current = newCurrent
// 只有当 searchParams 的分页字段与新值不同时才更新
if (paramsRecord[pageKey] !== newCurrent) {
paramsRecord[pageKey] = newCurrent
}
await getData()
} finally {
isCurrentChanging = false
}
}
// 针对不同业务场景的刷新方法
// 新增后刷新:回到第一页并清空分页缓存(适用于新增数据后)
const refreshCreate = async (): Promise<void> => {
debouncedGetDataByPage.cancel()
pagination.current = 1
;(searchParams as Record<string, unknown>)[pageKey] = 1
clearCache(CacheInvalidationStrategy.CLEAR_PAGINATION, '新增数据')
await getData()
}
// 更新后刷新:保持当前页,仅清空当前搜索缓存(适用于更新数据后)
const refreshUpdate = async (): Promise<void> => {
clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '编辑数据')
await getData()
}
// 删除后刷新:智能处理页码,避免空页面(适用于删除数据后)
const refreshRemove = async (): Promise<void> => {
const { current } = pagination
// 清除缓存并获取最新数据
clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '删除数据')
await getData()
// 如果当前页为空且不是第一页,回到上一页
if (data.value.length === 0 && current > 1) {
pagination.current = current - 1
;(searchParams as Record<string, unknown>)[pageKey] = current - 1
await getData()
}
}
// 全量刷新:清空所有缓存,重新获取数据(适用于手动刷新按钮)
const refreshData = async (): Promise<void> => {
debouncedGetDataByPage.cancel()
clearCache(CacheInvalidationStrategy.CLEAR_ALL, '手动刷新')
await getData()
}
// 轻量刷新:仅清空当前搜索条件的缓存,保持分页状态(适用于定时刷新)
const refreshSoft = async (): Promise<void> => {
clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '软刷新')
await getData()
}
// 取消当前请求
const cancelRequest = (): void => {
if (abortController) {
abortController.abort()
}
debouncedGetDataByPage.cancel()
}
// 清空数据
const clearData = (): void => {
data.value = []
error.value = null
clearCache(CacheInvalidationStrategy.CLEAR_ALL, '清空数据')
}
// 清理已过期的缓存条目,释放内存空间
const clearExpiredCache = (): number => {
if (!cache) return 0
const cleanedCount = cache.cleanupExpired()
if (cleanedCount > 0) {
// 手动触发缓存状态更新
cacheUpdateTrigger.value++
}
return cleanedCount
}
// 设置定期清理过期缓存
if (enableCache && cache) {
cacheCleanupTimer = setInterval(() => {
const cleanedCount = cache.cleanupExpired()
if (cleanedCount > 0) {
logger.log(`自动清理 ${cleanedCount} 条过期缓存`)
// 手动触发缓存状态更新
cacheUpdateTrigger.value++
}
}, cacheTime / 2) // 每半个缓存周期清理一次
}
// 挂载时自动加载数据
if (immediate) {
onMounted(async () => {
await getData()
})
}
// 组件卸载时彻底清理
onUnmounted(() => {
cancelRequest()
if (cache) {
cache.clear()
}
if (cacheCleanupTimer) {
clearInterval(cacheCleanupTimer)
}
})
// 优化的返回值结构
return {
// 数据相关
/** 表格数据 */
data,
/** 数据加载状态 */
loading: readonly(loading),
/** 错误状态 */
error: readonly(error),
/** 数据是否为空 */
isEmpty: computed(() => data.value.length === 0),
/** 是否有数据 */
hasData,
// 分页相关
/** 分页状态信息 */
pagination: readonly(pagination),
/** 移动端分页配置 */
paginationMobile: mobilePagination,
/** 页面大小变化处理 */
handleSizeChange,
/** 当前页变化处理 */
handleCurrentChange,
// 搜索相关 - 统一前缀
/** 搜索参数 */
searchParams,
/** 替换搜索参数(适用于表单查询,避免旧字段残留) */
replaceSearchParams,
/** 重置搜索参数 */
resetSearchParams,
// 数据操作 - 更明确的操作意图
/** 加载数据 */
fetchData: getData,
/** 获取数据 */
getData: getDataByPage,
/** 获取数据(防抖) */
getDataDebounced: debouncedGetDataByPage,
/** 清空数据 */
clearData,
// 刷新策略
/** 全量刷新:清空所有缓存,重新获取数据(适用于手动刷新按钮) */
refreshData,
/** 轻量刷新:仅清空当前搜索条件的缓存,保持分页状态(适用于定时刷新) */
refreshSoft,
/** 新增后刷新:回到第一页并清空分页缓存(适用于新增数据后) */
refreshCreate,
/** 更新后刷新:保持当前页,仅清空当前搜索缓存(适用于更新数据后) */
refreshUpdate,
/** 删除后刷新:智能处理页码,避免空页面(适用于删除数据后) */
refreshRemove,
// 缓存控制
/** 缓存统计信息 */
cacheInfo,
/** 清除缓存,根据不同的业务场景选择性地清理缓存: */
clearCache,
// 支持4种清理策略
// clearCache(CacheInvalidationStrategy.CLEAR_ALL, '手动刷新') // 清空所有缓存
// clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '搜索数据') // 只清空当前搜索条件的缓存
// clearCache(CacheInvalidationStrategy.CLEAR_PAGINATION, '新增数据') // 清空分页相关缓存
// clearCache(CacheInvalidationStrategy.KEEP_ALL, '保持缓存') // 不清理任何缓存
/** 清理已过期的缓存条目,释放内存空间 */
clearExpiredCache,
// 请求控制
/** 取消当前请求 */
cancelRequest,
// 列配置 (如果提供了 columnsFactory)
...(columnConfig && {
/** 表格列配置 */
columns,
/** 列显示控制 */
columnChecks,
/** 新增列 */
addColumn: columnConfig.addColumn,
/** 删除列 */
removeColumn: columnConfig.removeColumn,
/** 切换列显示状态 */
toggleColumn: columnConfig.toggleColumn,
/** 更新列配置 */
updateColumn: columnConfig.updateColumn,
/** 批量更新列配置 */
batchUpdateColumns: columnConfig.batchUpdateColumns,
/** 重新排序列 */
reorderColumns: columnConfig.reorderColumns,
/** 获取指定列配置 */
getColumnConfig: columnConfig.getColumnConfig,
/** 获取所有列配置 */
getAllColumns: columnConfig.getAllColumns,
/** 重置所有列配置到默认状态 */
resetColumns: columnConfig.resetColumns
})
}
}
// 重新导出类型和枚举,方便使用
export { CacheInvalidationStrategy } from '../../utils/table/tableCache'
export type { ApiResponse, CacheItem } from '../../utils/table/tableCache'
export type { BaseRequestParams, TableError } from '../../utils/table/tableUtils'