diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts index a6a330bd..f976351f 100644 --- a/packages/hooks/src/index.ts +++ b/packages/hooks/src/index.ts @@ -3,9 +3,9 @@ import useLoading from './use-loading'; import useCountDown from './use-count-down'; import useContext from './use-context'; import useSvgIconRender from './use-svg-icon-render'; -import useHookTable from './use-table'; +import useTable from './use-table'; -export { useBoolean, useLoading, useCountDown, useContext, useSvgIconRender, useHookTable }; +export { useBoolean, useLoading, useCountDown, useContext, useSvgIconRender, useTable }; export * from './use-signal'; -export * from './use-table'; +export type * from './use-table'; diff --git a/packages/hooks/src/use-table.ts b/packages/hooks/src/use-table.ts index 46bcf520..fa971b51 100644 --- a/packages/hooks/src/use-table.ts +++ b/packages/hooks/src/use-table.ts @@ -1,12 +1,20 @@ -import { computed, reactive, ref } from 'vue'; +import { computed, ref } from 'vue'; import type { Ref, VNodeChild } from 'vue'; -import { jsonClone } from '@sa/utils'; import useBoolean from './use-boolean'; import useLoading from './use-loading'; -export type MaybePromise = T | Promise; +export interface PaginationData { + data: T[]; + pageNum: number; + pageSize: number; + total: number; +} -export type ApiFn = (args: any) => Promise; +type GetApiData = Pagination extends true ? PaginationData : ApiData[]; + +type Transform = ( + response: ResponseData +) => GetApiData; export type TableColumnCheckTitle = string | ((...args: any) => VNodeChild); @@ -14,72 +22,64 @@ export type TableColumnCheck = { key: string; title: TableColumnCheckTitle; checked: boolean; + visible: boolean; }; -export type TableDataWithIndex = T & { index: number }; - -export type TransformedData = { - data: TableDataWithIndex[]; - pageNum: number; - pageSize: number; - total: number; -}; - -export type Transformer = (response: Response) => TransformedData; - -export type TableConfig = { - /** api function to get table data */ - apiFn: A; - /** api params */ - apiParams?: Parameters[0]; - /** transform api response to table data */ - transformer: Transformer>>; - /** columns factory */ - columns: () => C[]; +export interface UseTableOptions { + /** + * api function to get table data + */ + api: () => Promise; + /** + * whether to enable pagination + */ + pagination?: Pagination; + /** + * transform api response to table data + */ + transform: Transform; + /** + * columns factory + */ + columns: () => Column[]; /** * get column checks - * - * @param columns */ - getColumnChecks: (columns: C[]) => TableColumnCheck[]; + getColumnChecks: (columns: Column[]) => TableColumnCheck[]; /** * get columns - * - * @param columns */ - getColumns: (columns: C[], checks: TableColumnCheck[]) => C[]; + getColumns: (columns: Column[], checks: TableColumnCheck[]) => Column[]; /** * callback when response fetched - * - * @param transformed transformed data */ - onFetched?: (transformed: TransformedData) => MaybePromise; + onFetched?: (data: GetApiData) => void | Promise; /** * whether to get data immediately * * @default true */ immediate?: boolean; -}; +} -export default function useHookTable(config: TableConfig) { +export default function useTable( + options: UseTableOptions +) { const { loading, startLoading, endLoading } = useLoading(); const { bool: empty, setBool: setEmpty } = useBoolean(); - const { apiFn, apiParams, transformer, immediate = true, getColumnChecks, getColumns } = config; + const { api, pagination, transform, columns, getColumnChecks, getColumns, onFetched, immediate = true } = options; - const searchParams: NonNullable[0]> = reactive(jsonClone({ ...apiParams })); + const data = ref([]) as Ref; - const allColumns = ref(config.columns()) as Ref; + const allColumns = ref(columns()) as Ref; - const data: Ref[]> = ref([]); + const columnChecks = ref(getColumnChecks(columns())) as Ref; - const columnChecks: Ref = ref(getColumnChecks(config.columns())); - - const columns = computed(() => getColumns(allColumns.value, columnChecks.value)); + const $columns = computed(() => getColumns(columns(), columnChecks.value)); function reloadColumns() { - allColumns.value = config.columns(); + allColumns.value = columns(); const checkMap = new Map(columnChecks.value.map(col => [col.key, col.checked])); @@ -92,47 +92,21 @@ export default function useHookTable(config: TableConfig< } async function getData() { - startLoading(); + try { + startLoading(); - const formattedParams = formatSearchParams(searchParams); + const response = await api(); - const response = await apiFn(formattedParams); + const transformed = transform(response); - const transformed = transformer(response as Awaited>); + data.value = getTableData(transformed, pagination); - data.value = transformed.data; + setEmpty(data.value.length === 0); - setEmpty(transformed.data.length === 0); - - await config.onFetched?.(transformed); - - endLoading(); - } - - function formatSearchParams(params: Record) { - const formattedParams: Record = {}; - - Object.entries(params).forEach(([key, value]) => { - if (value !== null && value !== undefined) { - formattedParams[key] = value; - } - }); - - return formattedParams; - } - - /** - * update search params - * - * @param params - */ - function updateSearchParams(params: Partial[0]>) { - Object.assign(searchParams, params); - } - - /** reset search params */ - function resetSearchParams() { - Object.assign(searchParams, jsonClone(apiParams)); + await onFetched?.(transformed); + } finally { + endLoading(); + } } if (immediate) { @@ -143,12 +117,20 @@ export default function useHookTable(config: TableConfig< loading, empty, data, - columns, + columns: $columns, columnChecks, reloadColumns, - getData, - searchParams, - updateSearchParams, - resetSearchParams + getData }; } + +function getTableData( + data: GetApiData, + pagination?: Pagination +) { + if (pagination) { + return (data as PaginationData).data; + } + + return data as ApiData[]; +} diff --git a/src/hooks/common/table.ts b/src/hooks/common/table.ts index cee916a1..287973fb 100644 --- a/src/hooks/common/table.ts +++ b/src/hooks/common/table.ts @@ -1,192 +1,48 @@ -import { computed, effectScope, onScopeDispose, reactive, ref, watch } from 'vue'; +import { computed, effectScope, onScopeDispose, reactive, shallowRef, watch } from 'vue'; import type { Ref } from 'vue'; import type { PaginationProps } from 'naive-ui'; +import { useBoolean, useTable } from '@sa/hooks'; +import type { PaginationData, TableColumnCheck, UseTableOptions } from '@sa/hooks'; +import type { FlatResponseData } from '@sa/axios'; import { jsonClone } from '@sa/utils'; -import { useBoolean, useHookTable } from '@sa/hooks'; import { useAppStore } from '@/store/modules/app'; import { $t } from '@/locales'; -type TableData = NaiveUI.TableData; -type GetTableData = NaiveUI.GetTableData; -type TableColumn = NaiveUI.TableColumn; +export type UseNaiveTableOptions = Omit< + UseTableOptions, Pagination>, + 'pagination' | 'getColumnChecks' | 'getColumns' +> & { + /** + * get column visible + * + * @param column + * + * @default true + * + * @returns true if the column is visible, false otherwise + */ + getColumnVisible?: (column: NaiveUI.TableColumn) => boolean; +}; -export function useTable(config: NaiveUI.NaiveTableConfig) { +const SELECTION_KEY = '__selection__'; + +const EXPAND_KEY = '__expand__'; + +export function useNaiveTable(options: UseNaiveTableOptions) { const scope = effectScope(); const appStore = useAppStore(); - const isMobile = computed(() => appStore.isMobile); - - const { apiFn, apiParams, immediate, showTotal } = config; - - const SELECTION_KEY = '__selection__'; - - const EXPAND_KEY = '__expand__'; - - const { - loading, - empty, - data, - columns, - columnChecks, - reloadColumns, - getData, - searchParams, - updateSearchParams, - resetSearchParams - } = useHookTable, TableColumn>>>({ - apiFn, - apiParams, - columns: config.columns, - transformer: res => { - const { records = [], current = 1, size = 10, total = 0 } = res.data || {}; - - // Ensure that the size is greater than 0, If it is less than 0, it will cause paging calculation errors. - const pageSize = size <= 0 ? 10 : size; - - const recordsWithIndex = records.map((item: GetTableData, index: number) => { - return { - ...item, - index: (current - 1) * pageSize + index + 1 - }; - }); - - return { - data: recordsWithIndex, - pageNum: current, - pageSize, - total - }; - }, - getColumnChecks: cols => { - const checks: NaiveUI.TableColumnCheck[] = []; - - cols.forEach(column => { - if (isTableColumnHasKey(column)) { - checks.push({ - key: column.key as string, - title: column.title!, - checked: true - }); - } else if (column.type === 'selection') { - checks.push({ - key: SELECTION_KEY, - title: $t('common.check'), - checked: true - }); - } else if (column.type === 'expand') { - checks.push({ - key: EXPAND_KEY, - title: $t('common.expandColumn'), - checked: true - }); - } - }); - - return checks; - }, - getColumns: (cols, checks) => { - const columnMap = new Map>>(); - - cols.forEach(column => { - if (isTableColumnHasKey(column)) { - columnMap.set(column.key as string, column); - } else if (column.type === 'selection') { - columnMap.set(SELECTION_KEY, column); - } else if (column.type === 'expand') { - columnMap.set(EXPAND_KEY, column); - } - }); - - const filteredColumns = checks - .filter(item => item.checked) - .map(check => columnMap.get(check.key) as TableColumn>); - - return filteredColumns; - }, - onFetched: async transformed => { - const { pageNum, pageSize, total } = transformed; - - updatePagination({ - page: pageNum, - pageSize, - itemCount: total - }); - }, - immediate + const result = useTable, false>({ + ...options, + getColumnChecks: cols => getColumnChecks(cols, options.getColumnVisible), + getColumns }); - const pagination: PaginationProps = reactive({ - page: 1, - pageSize: 10, - showSizePicker: true, - itemCount: 0, - pageSizes: [10, 15, 20, 25, 30], - onUpdatePage: async (page: number) => { - pagination.page = page; - - updateSearchParams({ - current: page, - size: pagination.pageSize! - }); - - getData(); - }, - onUpdatePageSize: async (pageSize: number) => { - pagination.pageSize = pageSize; - pagination.page = 1; - - updateSearchParams({ - current: pagination.page, - size: pageSize - }); - - getData(); - }, - ...(showTotal - ? { - prefix: page => $t('datatable.itemCount', { total: page.itemCount }) - } - : {}) - }); - - // this is for mobile, if the system does not support mobile, you can use `pagination` directly - const mobilePagination = computed(() => { - const p: PaginationProps = { - ...pagination, - pageSlot: isMobile.value ? 3 : 9, - prefix: !isMobile.value && showTotal ? pagination.prefix : undefined - }; - - return p; - }); - - function updatePagination(update: Partial) { - Object.assign(pagination, update); - } - - /** - * get data by page number - * - * @param pageNum the page number. default is 1 - */ - async function getDataByPage(pageNum: number = 1) { - updatePagination({ - page: pageNum - }); - - updateSearchParams({ - current: pageNum, - size: pagination.pageSize! - }); - - await getData(); - } - scope.run(() => { watch( () => appStore.locale, () => { - reloadColumns(); + result.reloadColumns(); } ); }); @@ -195,28 +51,124 @@ export function useTable(config: NaiveUI.NaiveTabl scope.stop(); }); + return result; +} + +type PaginationParams = Pick; + +type UseNaivePaginatedTableOptions = UseNaiveTableOptions & { + paginationProps?: Omit; + /** + * whether to show the total count of the table + * + * @default true + */ + showTotal?: boolean; + onPaginationParamsChange?: (params: PaginationParams) => void | Promise; +}; + +export function useNaivePaginatedTable( + options: UseNaivePaginatedTableOptions +) { + const scope = effectScope(); + const appStore = useAppStore(); + + const isMobile = computed(() => appStore.isMobile); + + const showTotal = computed(() => options.showTotal ?? true); + + const pagination = reactive({ + page: 1, + pageSize: 10, + itemCount: 0, + showSizePicker: true, + pageSizes: [10, 15, 20, 25, 30], + prefix: showTotal.value ? page => $t('datatable.itemCount', { total: page.itemCount }) : undefined, + onUpdatePage(page) { + pagination.page = page; + }, + onUpdatePageSize(pageSize) { + pagination.pageSize = pageSize; + pagination.page = 1; + }, + ...options.paginationProps + }) as PaginationProps; + + // this is for mobile, if the system does not support mobile, you can use `pagination` directly + const mobilePagination = computed(() => { + const p: PaginationProps = { + ...pagination, + pageSlot: isMobile.value ? 3 : 9, + prefix: !isMobile.value && showTotal.value ? pagination.prefix : undefined + }; + + return p; + }); + + const paginationParams = computed(() => { + const { page, pageSize } = pagination; + + return { + page, + pageSize + }; + }); + + const result = useTable, true>({ + ...options, + pagination: true, + getColumnChecks: cols => getColumnChecks(cols, options.getColumnVisible), + getColumns, + onFetched: data => { + pagination.itemCount = data.total; + } + }); + + async function getDataByPage(page: number = 1) { + if (page !== pagination.page) { + pagination.page = page; + + return; + } + + await result.getData(); + } + + scope.run(() => { + watch( + () => appStore.locale, + () => { + result.reloadColumns(); + } + ); + + watch(paginationParams, async newVal => { + await options.onPaginationParamsChange?.(newVal); + + await result.getData(); + }); + }); + + onScopeDispose(() => { + scope.stop(); + }); + return { - loading, - empty, - data, - columns, - columnChecks, - reloadColumns, - pagination, - mobilePagination, - updatePagination, - getData, + ...result, getDataByPage, - searchParams, - updateSearchParams, - resetSearchParams + pagination, + mobilePagination }; } -export function useTableOperate(data: Ref, getData: () => Promise) { +export function useTableOperate( + data: Ref, + idKey: keyof TableData, + getData: () => Promise +) { const { bool: drawerVisible, setTrue: openDrawer, setFalse: closeDrawer } = useBoolean(); - const operateType = ref('add'); + const operateType = shallowRef('add'); function handleAdd() { operateType.value = 'add'; @@ -224,18 +176,18 @@ export function useTableOperate(data: Ref, } /** the editing row data */ - const editingData: Ref = ref(null); + const editingData = shallowRef(null); - function handleEdit(id: T['id']) { + function handleEdit(id: TableData[keyof TableData]) { operateType.value = 'edit'; - const findItem = data.value.find(item => item.id === id) || null; + const findItem = data.value.find(item => item[idKey] === id) || null; editingData.value = jsonClone(findItem); openDrawer(); } /** the checked row keys of table */ - const checkedRowKeys = ref([]); + const checkedRowKeys = shallowRef([]); /** the hook after the batch delete operation is completed */ async function onBatchDeleted() { @@ -267,6 +219,84 @@ export function useTableOperate(data: Ref, }; } -function isTableColumnHasKey(column: TableColumn): column is NaiveUI.TableColumnWithKey { +export function defaultTransform( + response: FlatResponseData> +): PaginationData { + const { data, error } = response; + + if (!error) { + const { records, current, size, total } = data; + + return { + data: records, + pageNum: current, + pageSize: size, + total + }; + } + + return { + data: [], + pageNum: 1, + pageSize: 10, + total: 0 + }; +} + +function getColumnChecks>( + cols: Column[], + getColumnVisible?: (column: Column) => boolean +) { + const checks: TableColumnCheck[] = []; + + cols.forEach(column => { + const visible = getColumnVisible?.(column) ?? true; + + if (isTableColumnHasKey(column)) { + checks.push({ + key: column.key as string, + title: column.title!, + checked: true, + visible + }); + } else if (column.type === 'selection') { + checks.push({ + key: SELECTION_KEY, + title: $t('common.check'), + checked: true, + visible + }); + } else if (column.type === 'expand') { + checks.push({ + key: EXPAND_KEY, + title: $t('common.expandColumn'), + checked: true, + visible + }); + } + }); + + return checks; +} + +function getColumns>(cols: Column[], checks: TableColumnCheck[]) { + const columnMap = new Map(); + + cols.forEach(column => { + if (isTableColumnHasKey(column)) { + columnMap.set(column.key as string, column); + } else if (column.type === 'selection') { + columnMap.set(SELECTION_KEY, column); + } else if (column.type === 'expand') { + columnMap.set(EXPAND_KEY, column); + } + }); + + const filteredColumns = checks.filter(item => item.checked).map(check => columnMap.get(check.key) as Column); + + return filteredColumns; +} + +export function isTableColumnHasKey(column: NaiveUI.TableColumn): column is NaiveUI.TableColumnWithKey { return Boolean((column as NaiveUI.TableColumnWithKey).key); } diff --git a/src/typings/naive-ui.d.ts b/src/typings/naive-ui.d.ts index d9ffb778..30e55c6f 100644 --- a/src/typings/naive-ui.d.ts +++ b/src/typings/naive-ui.d.ts @@ -6,30 +6,14 @@ declare namespace NaiveUI { type DataTableExpandColumn = import('naive-ui').DataTableExpandColumn; type DataTableSelectionColumn = import('naive-ui').DataTableSelectionColumn; type TableColumnGroup = import('naive-ui/es/data-table/src/interface').TableColumnGroup; - type PaginationProps = import('naive-ui').PaginationProps; type TableColumnCheck = import('@sa/hooks').TableColumnCheck; - type TableDataWithIndex = import('@sa/hooks').TableDataWithIndex; - type FlatResponseData = import('@sa/axios').FlatResponseData; - /** - * the custom column key - * - * if you want to add a custom column, you should add a key to this type - */ - type CustomColumnKey = 'operate'; - - type SetTableColumnKey = Omit & { key: keyof T | CustomColumnKey }; - - type TableData = Api.Common.CommonRecord; + type SetTableColumnKey = Omit & { key: keyof T | (string & {}) }; type TableColumnWithKey = SetTableColumnKey, T> | SetTableColumnKey, T>; type TableColumn = TableColumnWithKey | DataTableSelectionColumn | DataTableExpandColumn; - type TableApiFn = ( - params: R - ) => Promise>>; - /** * the type of table operation * @@ -37,18 +21,4 @@ declare namespace NaiveUI { * - edit: edit table item */ type TableOperateType = 'add' | 'edit'; - - type GetTableData = A extends TableApiFn ? T : never; - - type NaiveTableConfig = Pick< - import('@sa/hooks').TableConfig, TableColumn>>>, - 'apiFn' | 'apiParams' | 'columns' | 'immediate' - > & { - /** - * whether to display the total items count - * - * @default false - */ - showTotal?: boolean; - }; }