import { ref, reactive, computed, onMounted, onUnmounted, nextTick, readonly } from 'vue'
|
import { useWindowSize } from '@vueuse/core'
|
import { useTableColumns } from './useTableColumns'
|
import { TableCache, CacheInvalidationStrategy } from '../../utils/table/tableCache'
|
import {
|
defaultResponseAdapter,
|
extractTableData,
|
updatePaginationFromResponse,
|
createSmartDebounce,
|
createErrorHandler
|
} from '../../utils/table/tableUtils'
|
import { tableConfig } from '../../utils/table/tableConfig'
|
function useTable(config) {
|
return useTableImpl(config)
|
}
|
function useTableImpl(config) {
|
const {
|
core: {
|
apiFn,
|
apiParams = {},
|
excludeParams = [],
|
immediate = true,
|
columnsFactory,
|
paginationKey
|
},
|
transform: { dataTransformer, responseAdapter = defaultResponseAdapter } = {},
|
performance: {
|
enableCache = false,
|
cacheTime = 5 * 60 * 1e3,
|
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, ...args) => {
|
if (enableLog) {
|
console.log(`[useTable] ${message}`, ...args)
|
}
|
},
|
warn: (message, ...args) => {
|
if (enableLog) {
|
console.warn(`[useTable] ${message}`, ...args)
|
}
|
},
|
error: (message, ...args) => {
|
if (enableLog) {
|
console.error(`[useTable] ${message}`, ...args)
|
}
|
}
|
}
|
const cache = enableCache ? new TableCache(cacheTime, maxCacheSize, enableLog) : null
|
const loadingState = ref('idle')
|
const loading = computed(() => loadingState.value === 'loading')
|
const error = ref(null)
|
const data = ref([])
|
let abortController = null
|
let cacheCleanupTimer = null
|
const searchParams = reactive(
|
Object.assign(
|
{
|
[pageKey]: 1,
|
[sizeKey]: 10
|
},
|
apiParams || {}
|
)
|
)
|
const pagination = reactive({
|
current: searchParams[pageKey] || 1,
|
size: searchParams[sizeKey] || 10,
|
total: 0
|
})
|
const { width } = useWindowSize()
|
const mobilePagination = computed(() => ({
|
...pagination,
|
small: width.value < 768
|
}))
|
const columnConfig = columnsFactory ? useTableColumns(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, context) => {
|
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, useCache = enableCache) => {
|
if (abortController) {
|
abortController.abort()
|
}
|
const currentController = new AbortController()
|
abortController = currentController
|
loadingState.value = 'loading'
|
error.value = null
|
try {
|
let requestParams = Object.assign(
|
{},
|
searchParams,
|
{
|
[pageKey]: pagination.current,
|
[sizeKey]: pagination.size
|
},
|
params || {}
|
)
|
if (excludeParams.length > 0) {
|
const filteredParams = { ...requestParams }
|
excludeParams.forEach((key) => {
|
delete filteredParams[key]
|
})
|
requestParams = filteredParams
|
}
|
if (useCache && cache) {
|
const cachedItem = cache.get(requestParams)
|
if (cachedItem) {
|
data.value = cachedItem.data
|
updatePaginationFromResponse(pagination, cachedItem.response)
|
const paramsRecord2 = searchParams
|
if (paramsRecord2[pageKey] !== pagination.current) {
|
paramsRecord2[pageKey] = pagination.current
|
}
|
if (paramsRecord2[sizeKey] !== pagination.size) {
|
paramsRecord2[sizeKey] = pagination.size
|
}
|
loadingState.value = 'success'
|
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
|
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(`数据已缓存`)
|
}
|
loadingState.value = 'success'
|
if (onSuccess) {
|
onSuccess(tableData, standardResponse)
|
}
|
return standardResponse
|
} catch (err) {
|
if (err instanceof Error && err.message === '请求已取消') {
|
loadingState.value = 'idle'
|
return { records: [], total: 0, current: 1, size: 10 }
|
}
|
loadingState.value = 'error'
|
data.value = []
|
const tableError = handleError(err, '获取表格数据失败')
|
throw tableError
|
} finally {
|
if (abortController === currentController) {
|
abortController = null
|
}
|
}
|
}
|
const getData = async (params) => {
|
try {
|
return await fetchData(params)
|
} catch {
|
return Promise.resolve()
|
}
|
}
|
const getDataByPage = async (params) => {
|
pagination.current = 1
|
searchParams[pageKey] = 1
|
clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '搜索数据')
|
try {
|
return await fetchData(params, false)
|
} catch {
|
return Promise.resolve()
|
}
|
}
|
const debouncedGetDataByPage = createSmartDebounce(getDataByPage, debounceTime)
|
const resetSearchParams = async () => {
|
debouncedGetDataByPage.cancel()
|
const paramsRecord = searchParams
|
const defaultPagination = {
|
[pageKey]: 1,
|
[sizeKey]: paramsRecord[sizeKey] || 10
|
}
|
Object.keys(searchParams).forEach((key) => {
|
delete paramsRecord[key]
|
})
|
Object.assign(searchParams, apiParams || {}, defaultPagination)
|
pagination.current = 1
|
pagination.size = defaultPagination[sizeKey]
|
error.value = null
|
clearCache(CacheInvalidationStrategy.CLEAR_ALL, '重置搜索')
|
await getData()
|
if (resetFormCallback) {
|
await nextTick()
|
resetFormCallback()
|
}
|
}
|
const replaceSearchParams = (params) => {
|
const paramsRecord = searchParams
|
const currentSize = pagination.size || (paramsRecord[sizeKey] ?? 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) => {
|
if (newSize <= 0) return
|
debouncedGetDataByPage.cancel()
|
const paramsRecord = searchParams
|
pagination.size = newSize
|
pagination.current = 1
|
paramsRecord[sizeKey] = newSize
|
paramsRecord[pageKey] = 1
|
clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '分页大小变化')
|
await getData()
|
}
|
const handleCurrentChange = async (newCurrent) => {
|
if (newCurrent <= 0) return
|
if (isCurrentChanging) {
|
return
|
}
|
if (pagination.current === newCurrent) {
|
logger.log('分页页码未变化,跳过请求')
|
return
|
}
|
try {
|
isCurrentChanging = true
|
const paramsRecord = searchParams
|
pagination.current = newCurrent
|
if (paramsRecord[pageKey] !== newCurrent) {
|
paramsRecord[pageKey] = newCurrent
|
}
|
await getData()
|
} finally {
|
isCurrentChanging = false
|
}
|
}
|
const refreshCreate = async () => {
|
debouncedGetDataByPage.cancel()
|
pagination.current = 1
|
searchParams[pageKey] = 1
|
clearCache(CacheInvalidationStrategy.CLEAR_PAGINATION, '新增数据')
|
await getData()
|
}
|
const refreshUpdate = async () => {
|
clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '编辑数据')
|
await getData()
|
}
|
const refreshRemove = async () => {
|
const { current } = pagination
|
clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '删除数据')
|
await getData()
|
if (data.value.length === 0 && current > 1) {
|
pagination.current = current - 1
|
searchParams[pageKey] = current - 1
|
await getData()
|
}
|
}
|
const refreshData = async () => {
|
debouncedGetDataByPage.cancel()
|
clearCache(CacheInvalidationStrategy.CLEAR_ALL, '手动刷新')
|
await getData()
|
}
|
const refreshSoft = async () => {
|
clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '软刷新')
|
await getData()
|
}
|
const cancelRequest = () => {
|
if (abortController) {
|
abortController.abort()
|
}
|
debouncedGetDataByPage.cancel()
|
}
|
const clearData = () => {
|
data.value = []
|
error.value = null
|
clearCache(CacheInvalidationStrategy.CLEAR_ALL, '清空数据')
|
}
|
const clearExpiredCache = () => {
|
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
|
})
|
}
|
}
|
import { CacheInvalidationStrategy as CacheInvalidationStrategy2 } from '../../utils/table/tableCache'
|
export { CacheInvalidationStrategy2 as CacheInvalidationStrategy, useTable }
|