298 lines
8.1 KiB
TypeScript
298 lines
8.1 KiB
TypeScript
/**
|
|
* 表格工具函数模块
|
|
*
|
|
* 提供表格数据处理和请求管理的核心工具函数
|
|
*
|
|
* ## 主要功能
|
|
*
|
|
* - 多格式 API 响应自动适配和标准化
|
|
* - 表格数据提取和转换
|
|
* - 分页信息自动更新和校验
|
|
* - 智能防抖函数(支持取消和立即执行)
|
|
* - 统一的错误处理机制
|
|
* - 嵌套数据结构解析
|
|
*
|
|
* ## 使用场景
|
|
*
|
|
* - useTable 组合式函数的底层工具
|
|
* - 适配各种后端接口响应格式
|
|
* - 表格数据的标准化处理
|
|
* - 请求防抖和性能优化
|
|
* - 错误统一处理和日志记录
|
|
*
|
|
* ## 支持的响应格式
|
|
*
|
|
* 1. 直接数组: [item1, item2, ...]
|
|
* 2. 标准对象: { records: [], total: 100 }
|
|
* 3. 嵌套data: { data: { list: [], total: 100 } }
|
|
* 4. 多种字段名: list/data/records/items/result/rows
|
|
*
|
|
* ## 核心功能
|
|
*
|
|
* - defaultResponseAdapter: 智能识别和转换响应格式
|
|
* - extractTableData: 提取表格数据数组
|
|
* - updatePaginationFromResponse: 更新分页信息
|
|
* - createSmartDebounce: 创建可控的防抖函数
|
|
* - createErrorHandler: 生成错误处理器
|
|
*
|
|
* @module utils/table/tableUtils
|
|
* @author Art Design Pro Team
|
|
*/
|
|
|
|
import type { ApiResponse } from './tableCache'
|
|
import { tableConfig } from './tableConfig'
|
|
|
|
// 请求参数基础接口,扩展分页参数
|
|
export interface BaseRequestParams extends Api.Common.PaginationParams {
|
|
[key: string]: unknown
|
|
}
|
|
|
|
// 错误处理接口
|
|
export interface TableError {
|
|
code: string
|
|
message: string
|
|
details?: unknown
|
|
}
|
|
|
|
// 辅助函数:从对象中提取记录数组
|
|
function extractRecords<T>(obj: Record<string, unknown>, fields: string[]): T[] {
|
|
for (const field of fields) {
|
|
if (field in obj && Array.isArray(obj[field])) {
|
|
return obj[field] as T[]
|
|
}
|
|
}
|
|
return []
|
|
}
|
|
|
|
// 辅助函数:从对象中提取总数
|
|
function extractTotal(obj: Record<string, unknown>, records: unknown[], fields: string[]): number {
|
|
for (const field of fields) {
|
|
if (field in obj && typeof obj[field] === 'number') {
|
|
return obj[field] as number
|
|
}
|
|
}
|
|
return records.length
|
|
}
|
|
|
|
// 辅助函数:提取分页参数
|
|
function extractPagination(
|
|
obj: Record<string, unknown>,
|
|
data?: Record<string, unknown>
|
|
): Pick<ApiResponse<unknown>, 'current' | 'size'> | undefined {
|
|
const result: Partial<Pick<ApiResponse<unknown>, 'current' | 'size'>> = {}
|
|
const sources = [obj, data ?? {}]
|
|
|
|
const currentFields = tableConfig.currentFields
|
|
for (const src of sources) {
|
|
for (const field of currentFields) {
|
|
if (field in src && typeof src[field] === 'number') {
|
|
result.current = src[field] as number
|
|
break
|
|
}
|
|
}
|
|
if (result.current !== undefined) break
|
|
}
|
|
|
|
const sizeFields = tableConfig.sizeFields
|
|
for (const src of sources) {
|
|
for (const field of sizeFields) {
|
|
if (field in src && typeof src[field] === 'number') {
|
|
result.size = src[field] as number
|
|
break
|
|
}
|
|
}
|
|
if (result.size !== undefined) break
|
|
}
|
|
|
|
if (result.current === undefined && result.size === undefined) return undefined
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* 默认响应适配器 - 支持多种常见的API响应格式
|
|
*/
|
|
export const defaultResponseAdapter = <T>(response: unknown): ApiResponse<T> => {
|
|
// 定义支持的字段
|
|
const recordFields = tableConfig.recordFields
|
|
|
|
if (!response) {
|
|
return { records: [], total: 0 }
|
|
}
|
|
|
|
if (Array.isArray(response)) {
|
|
return { records: response, total: response.length }
|
|
}
|
|
|
|
if (typeof response !== 'object') {
|
|
console.warn(
|
|
'[tableUtils] 无法识别的响应格式,支持的格式包括: 数组、包含' +
|
|
recordFields.join('/') +
|
|
'字段的对象、嵌套data对象。当前格式:',
|
|
response
|
|
)
|
|
return { records: [], total: 0 }
|
|
}
|
|
|
|
const res = response as Record<string, unknown>
|
|
let records: T[] = []
|
|
let total = 0
|
|
let pagination: Pick<ApiResponse<unknown>, 'current' | 'size'> | undefined
|
|
|
|
// 处理标准格式或直接列表
|
|
records = extractRecords(res, recordFields)
|
|
total = extractTotal(res, records, tableConfig.totalFields)
|
|
pagination = extractPagination(res)
|
|
|
|
// 如果没有找到,检查嵌套data
|
|
if (records.length === 0 && 'data' in res && typeof res.data === 'object') {
|
|
const data = res.data as Record<string, unknown>
|
|
records = extractRecords(data, ['list', 'records', 'items'])
|
|
total = extractTotal(data, records, tableConfig.totalFields)
|
|
pagination = extractPagination(res, data)
|
|
|
|
if (Array.isArray(res.data)) {
|
|
records = res.data as T[]
|
|
total = records.length
|
|
}
|
|
}
|
|
|
|
if (!recordFields.some((field) => field in res) && records.length === 0) {
|
|
console.warn('[tableUtils] 无法识别的响应格式')
|
|
console.warn('支持的字段包括: ' + recordFields.join('、'), response)
|
|
console.warn('扩展字段请到 utils/table/tableConfig 文件配置')
|
|
}
|
|
|
|
const result: ApiResponse<T> = { records, total }
|
|
if (pagination) {
|
|
Object.assign(result, pagination)
|
|
}
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* 从标准化的API响应中提取表格数据
|
|
*/
|
|
export const extractTableData = <T>(response: ApiResponse<T>): T[] => {
|
|
const data = response.records || response.data || []
|
|
return Array.isArray(data) ? data : []
|
|
}
|
|
|
|
/**
|
|
* 根据API响应更新分页信息
|
|
*/
|
|
export const updatePaginationFromResponse = <T>(
|
|
pagination: Api.Common.PaginationParams,
|
|
response: ApiResponse<T>
|
|
): void => {
|
|
pagination.total = response.total ?? pagination.total ?? 0
|
|
|
|
if (response.current !== undefined) {
|
|
pagination.current = response.current
|
|
}
|
|
|
|
const maxPage = Math.max(1, Math.ceil(pagination.total / (pagination.size || 1)))
|
|
if (pagination.current > maxPage) {
|
|
pagination.current = maxPage
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 创建智能防抖函数 - 支持取消和立即执行
|
|
*/
|
|
export const createSmartDebounce = <T extends (...args: any[]) => Promise<any>>(
|
|
fn: T,
|
|
delay: number
|
|
): T & { cancel: () => void; flush: () => Promise<any> } => {
|
|
let timeoutId: NodeJS.Timeout | null = null
|
|
let lastArgs: Parameters<T> | null = null
|
|
let lastResolve: ((value: any) => void) | null = null
|
|
let lastReject: ((reason: any) => void) | null = null
|
|
|
|
const debouncedFn = (...args: Parameters<T>): Promise<any> => {
|
|
return new Promise((resolve, reject) => {
|
|
if (timeoutId) clearTimeout(timeoutId)
|
|
lastArgs = args
|
|
lastResolve = resolve
|
|
lastReject = reject
|
|
timeoutId = setTimeout(async () => {
|
|
try {
|
|
const result = await fn(...args)
|
|
resolve(result)
|
|
} catch (error) {
|
|
reject(error)
|
|
} finally {
|
|
timeoutId = null
|
|
lastArgs = null
|
|
lastResolve = null
|
|
lastReject = null
|
|
}
|
|
}, delay)
|
|
})
|
|
}
|
|
|
|
debouncedFn.cancel = () => {
|
|
if (timeoutId) clearTimeout(timeoutId)
|
|
timeoutId = null
|
|
lastArgs = null
|
|
lastResolve = null
|
|
lastReject = null
|
|
}
|
|
|
|
debouncedFn.flush = async () => {
|
|
if (timeoutId && lastArgs && lastResolve && lastReject) {
|
|
clearTimeout(timeoutId)
|
|
timeoutId = null
|
|
const args = lastArgs
|
|
const resolve = lastResolve
|
|
const reject = lastReject
|
|
lastArgs = null
|
|
lastResolve = null
|
|
lastReject = null
|
|
try {
|
|
const result = await fn(...args)
|
|
resolve(result)
|
|
return result
|
|
} catch (error) {
|
|
reject(error)
|
|
throw error
|
|
}
|
|
}
|
|
return Promise.resolve()
|
|
}
|
|
|
|
return debouncedFn as any
|
|
}
|
|
|
|
/**
|
|
* 生成错误处理函数
|
|
*/
|
|
export const createErrorHandler = (
|
|
onError?: (error: TableError) => void,
|
|
enableLog: boolean = false
|
|
) => {
|
|
const logger = {
|
|
error: (message: string, ...args: any[]) => {
|
|
if (enableLog) console.error(`[useTable] ${message}`, ...args)
|
|
}
|
|
}
|
|
|
|
return (err: unknown, context: string): TableError => {
|
|
const tableError: TableError = {
|
|
code: 'UNKNOWN_ERROR',
|
|
message: '未知错误',
|
|
details: err
|
|
}
|
|
|
|
if (err instanceof Error) {
|
|
tableError.message = err.message
|
|
tableError.code = err.name
|
|
} else if (typeof err === 'string') {
|
|
tableError.message = err
|
|
}
|
|
|
|
logger.error(`${context}:`, err)
|
|
onError?.(tableError)
|
|
return tableError
|
|
}
|
|
}
|