import { computed, effectScope, onScopeDispose, reactive, ref, 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 { useAppStore } from '@/store/modules/app';
|
import { handleTree } from '@/utils/common';
|
import { $t } from '@/locales';
|
|
export type UseNaiveTableOptions<ResponseData, ApiData, Pagination extends boolean> = Omit<
|
UseTableOptions<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, Pagination>,
|
'pagination' | 'getColumnChecks' | 'getColumns'
|
> & {
|
/**
|
* get column visible
|
*
|
* @param column
|
*
|
* @default true
|
*
|
* @returns true if the column is visible, false otherwise
|
*/
|
getColumnVisible?: (column: NaiveUI.TableColumn<ApiData>) => boolean;
|
};
|
|
const SELECTION_KEY = '__selection__';
|
|
const EXPAND_KEY = '__expand__';
|
|
export function useNaiveTable<ResponseData, ApiData>(options: UseNaiveTableOptions<ResponseData, ApiData, false>) {
|
const scope = effectScope();
|
const appStore = useAppStore();
|
|
const result = useTable<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, false>({
|
...options,
|
getColumnChecks: cols => getColumnChecks(cols, options.getColumnVisible),
|
getColumns
|
});
|
|
// calculate the total width of the table this is used for horizontal scrolling
|
const scrollX = computed(() => {
|
return result.columns.value.reduce((acc, column) => {
|
return acc + Number(column.width ?? column.minWidth ?? 120);
|
}, 0);
|
});
|
|
scope.run(() => {
|
watch(
|
() => appStore.locale,
|
() => {
|
result.reloadColumns();
|
}
|
);
|
});
|
|
onScopeDispose(() => {
|
scope.stop();
|
});
|
|
return {
|
...result,
|
scrollX
|
};
|
}
|
|
type PaginationParams = Pick<PaginationProps, 'page' | 'pageSize'>;
|
|
type UseNaivePaginatedTableOptions<ResponseData, ApiData> = UseNaiveTableOptions<ResponseData, ApiData, true> & {
|
paginationProps?: Omit<PaginationProps, 'page' | 'pageSize' | 'itemCount'>;
|
/**
|
* whether to show the total count of the table
|
*
|
* @default true
|
*/
|
showTotal?: boolean;
|
onPaginationParamsChange?: (params: PaginationParams) => void | Promise<void>;
|
};
|
|
export function useNaivePaginatedTable<ResponseData, ApiData>(
|
options: UseNaivePaginatedTableOptions<ResponseData, ApiData>
|
) {
|
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<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, true>({
|
...options,
|
pagination: true,
|
getColumnChecks: cols => getColumnChecks(cols, options.getColumnVisible),
|
getColumns,
|
onFetched: data => {
|
pagination.itemCount = data.total;
|
}
|
});
|
|
// calculate the total width of the table this is used for horizontal scrolling
|
const scrollX = computed(() => {
|
return result.columns.value.reduce((acc, column) => {
|
return acc + Number(column.width ?? column.minWidth ?? 120);
|
}, 0);
|
});
|
|
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 {
|
...result,
|
scrollX,
|
getDataByPage,
|
pagination,
|
mobilePagination
|
};
|
}
|
|
export function useTableOperate<TableData>(
|
data: Ref<TableData[]>,
|
idKey: keyof TableData,
|
getData: () => Promise<void>
|
) {
|
const { bool: drawerVisible, setTrue: openDrawer, setFalse: closeDrawer } = useBoolean();
|
|
const operateType = shallowRef<NaiveUI.TableOperateType>('add');
|
|
function handleAdd() {
|
operateType.value = 'add';
|
openDrawer();
|
}
|
|
/** the editing row data */
|
const editingData = shallowRef<TableData | null>(null);
|
|
function handleEdit(id: TableData[keyof TableData]) {
|
operateType.value = 'edit';
|
const findItem = data.value.find(item => item[idKey] === id) || null;
|
editingData.value = jsonClone(findItem);
|
|
openDrawer();
|
}
|
|
/** the checked row keys of table */
|
const checkedRowKeys = shallowRef<CommonType.IdType[]>([]);
|
|
/** the hook after the batch delete operation is completed */
|
async function onBatchDeleted() {
|
window.$message?.success($t('common.deleteSuccess'));
|
|
checkedRowKeys.value = [];
|
|
await getData();
|
}
|
|
/** the hook after the delete operation is completed */
|
async function onDeleted() {
|
window.$message?.success($t('common.deleteSuccess'));
|
|
await getData();
|
}
|
|
return {
|
drawerVisible,
|
openDrawer,
|
closeDrawer,
|
operateType,
|
handleAdd,
|
editingData,
|
handleEdit,
|
checkedRowKeys,
|
onBatchDeleted,
|
onDeleted
|
};
|
}
|
|
export function defaultTransform<ApiData>(
|
response: FlatResponseData<any, Api.Common.PaginatingQueryRecord<ApiData>>
|
): PaginationData<ApiData> {
|
const { data, error } = response;
|
|
if (error) {
|
return {
|
data: [],
|
pageNum: 1,
|
total: 0
|
};
|
}
|
|
const { rows: records, pageNum: current, total } = data;
|
|
return {
|
data: records,
|
pageNum: current,
|
total
|
};
|
}
|
|
type TreeTableTransformResult<ApiData> = {
|
/** tree data for display */
|
tree: ApiData[];
|
/** flat data for operations */
|
flatData: ApiData[];
|
};
|
|
type UseNaiveTreeTableOptions<ResponseData, ApiData> = Omit<
|
UseNaiveTableOptions<ResponseData, ApiData, false>,
|
'transform'
|
> & {
|
keyField: keyof ApiData;
|
defaultExpandAll?: boolean;
|
/**
|
* transform api response to tree table data
|
*/
|
transform: (response: ResponseData) => TreeTableTransformResult<ApiData>;
|
};
|
|
export function useNaiveTreeTable<ResponseData, ApiData>(options: UseNaiveTreeTableOptions<ResponseData, ApiData>) {
|
const scope = effectScope();
|
const appStore = useAppStore();
|
const rows: Ref<ApiData[]> = ref([]);
|
|
const result = useTable<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, false>({
|
...options,
|
pagination: false,
|
transform: response => {
|
const transformed = options.transform(response);
|
// save flat data for operations
|
rows.value = transformed.flatData;
|
// return tree data for display
|
return transformed.tree;
|
},
|
getColumnChecks: cols => getColumnChecks(cols, options.getColumnVisible),
|
getColumns
|
});
|
|
// calculate the total width of the table this is used for horizontal scrolling
|
const scrollX = computed(() => {
|
return result.columns.value.reduce((acc, column) => {
|
return acc + Number(column.width ?? column.minWidth ?? 120);
|
}, 0);
|
});
|
|
const { keyField = 'id', defaultExpandAll = false } = options;
|
|
const expandedRowKeys = ref<ApiData[keyof ApiData][]>([]);
|
const { bool: isCollapse, toggle: toggleCollapse } = useBoolean(defaultExpandAll);
|
|
/** expand all nodes */
|
function expandAll() {
|
toggleCollapse();
|
expandedRowKeys.value = rows.value.map(item => item[keyField as keyof ApiData]);
|
}
|
|
/** collapse all nodes */
|
function collapseAll() {
|
toggleCollapse();
|
expandedRowKeys.value = [];
|
}
|
|
scope.run(() => {
|
watch(
|
() => appStore.locale,
|
() => {
|
result.reloadColumns();
|
}
|
);
|
});
|
|
onScopeDispose(() => {
|
scope.stop();
|
});
|
|
return {
|
...result,
|
scrollX,
|
rows,
|
isCollapse,
|
expandedRowKeys,
|
expandAll,
|
collapseAll
|
};
|
}
|
|
export function useTreeTableOperate<ApiData>(data: Ref<ApiData[]>, idKey: keyof ApiData, getData: () => Promise<void>) {
|
const { bool: drawerVisible, setTrue: openDrawer, setFalse: closeDrawer } = useBoolean();
|
|
const operateType = shallowRef<NaiveUI.TableOperateType>('add');
|
|
function handleAdd() {
|
operateType.value = 'add';
|
openDrawer();
|
}
|
|
/** the editing row data */
|
const editingData = shallowRef<ApiData | null>(null);
|
|
function handleEdit(id: ApiData[keyof ApiData]) {
|
operateType.value = 'edit';
|
const findItem = data.value.find(item => item[idKey] === id) || null;
|
editingData.value = jsonClone(findItem);
|
|
openDrawer();
|
}
|
|
/** the checked row keys of table */
|
const checkedRowKeys = shallowRef<string[]>([]);
|
|
/** the hook after the batch delete operation is completed */
|
async function onBatchDeleted() {
|
window.$message?.success($t('common.deleteSuccess'));
|
|
checkedRowKeys.value = [];
|
|
await getData();
|
}
|
|
/** the hook after the delete operation is completed */
|
async function onDeleted() {
|
window.$message?.success($t('common.deleteSuccess'));
|
|
await getData();
|
}
|
|
return {
|
drawerVisible,
|
openDrawer,
|
closeDrawer,
|
operateType,
|
handleAdd,
|
editingData,
|
handleEdit,
|
checkedRowKeys,
|
onBatchDeleted,
|
onDeleted
|
};
|
}
|
|
export function treeTransform<ApiData>(
|
response: FlatResponseData<any, ApiData[]>,
|
options: CommonType.TreeConfig<ApiData> = {}
|
): TreeTableTransformResult<ApiData> {
|
const { data, error } = response;
|
|
if (!error) {
|
return handleTree(data, options);
|
}
|
|
return {
|
tree: [],
|
flatData: []
|
};
|
}
|
|
function getColumnChecks<Column extends NaiveUI.TableColumn<any>>(
|
cols: Column[],
|
getColumnVisible?: (column: Column) => boolean
|
) {
|
const checks: TableColumnCheck[] = [];
|
|
cols.forEach(column => {
|
if (isTableColumnHasKey(column)) {
|
checks.push({
|
key: column.key as string,
|
title: column.title!,
|
checked: true,
|
visible: getColumnVisible?.(column) ?? true
|
});
|
} else if (column.type === 'selection') {
|
checks.push({
|
key: SELECTION_KEY,
|
title: $t('common.check'),
|
checked: true,
|
visible: getColumnVisible?.(column) ?? false
|
});
|
} else if (column.type === 'expand') {
|
checks.push({
|
key: EXPAND_KEY,
|
title: $t('common.expandColumn'),
|
checked: true,
|
visible: getColumnVisible?.(column) ?? false
|
});
|
}
|
});
|
|
return checks;
|
}
|
|
function getColumns<Column extends NaiveUI.TableColumn<any>>(cols: Column[], checks: TableColumnCheck[]) {
|
const columnMap = new Map<string, Column>();
|
|
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);
|
}
|
});
|
|
// Filter out any checks that do not have a corresponding column (can happen when column definitions change).
|
const filteredColumns = checks
|
.filter(item => item.checked)
|
.map(check => columnMap.get(check.key))
|
.filter((col): col is Column => Boolean(col));
|
|
return filteredColumns;
|
}
|
|
export function isTableColumnHasKey<T>(column: NaiveUI.TableColumn<T>): column is NaiveUI.TableColumnWithKey<T> {
|
return Boolean((column as NaiveUI.TableColumnWithKey<T>).key);
|
}
|