refactor(projects): refactor useTable

This commit is contained in:
Soybean
2024-03-18 08:36:17 +08:00
parent 51d7758903
commit c3efa1b6e0
7 changed files with 639 additions and 6 deletions

View File

@ -1,13 +1,12 @@
<script setup lang="ts" generic="T extends Record<string, unknown>, K = never">
import { VueDraggable } from 'vue-draggable-plus';
import type { FilteredColumn } from '@/hooks/common/table';
import { $t } from '@/locales';
defineOptions({
name: 'TableColumnSetting'
});
const columns = defineModel<FilteredColumn[]>('columns', {
const columns = defineModel<NaiveUI.TableColumnCheck[]>('columns', {
required: true
});
</script>

View File

@ -1,6 +1,4 @@
<script setup lang="ts">
import type { FilteredColumn } from '@/hooks/common/table';
defineOptions({
name: 'TableHeaderOperation'
});
@ -21,7 +19,7 @@ interface Emits {
const emit = defineEmits<Emits>();
const columns = defineModel<FilteredColumn[]>('columns', {
const columns = defineModel<NaiveUI.TableColumnCheck[]>('columns', {
default: () => []
});

View File

@ -0,0 +1,232 @@
import { computed, effectScope, onScopeDispose, reactive, ref, watch } from 'vue';
import type { Ref } from 'vue';
import type { PaginationProps } from 'naive-ui';
import { useBoolean, useTable } from '@sa/hooks';
import { useAppStore } from '@/store/modules/app';
import { $t } from '@/locales';
type TableData = NaiveUI.TableData;
export function useNaiveTable<T extends TableData = TableData, A extends NaiveUI.TableApiFn<T> = NaiveUI.TableApiFn<T>>(
config: NaiveUI.NaiveTableConfig<T, A>
) {
const scope = effectScope();
const appStore = useAppStore();
const { apiFn, apiParams, immediate } = config;
const SELECTION_KEY = '__selection__';
const {
loading,
empty,
data,
columns,
columnChecks,
reloadColumns,
getData,
searchParams,
updateSearchParams,
resetSearchParams
} = useTable<A, T, NaiveUI.TableColumn<T>>({
apiFn,
apiParams,
columns: config.columns,
transformer: res => {
const { records = [], current = 1, size = 10, total = 0 } = res.data || {};
return {
data: records,
pageNum: current,
pageSize: size,
total
};
},
getColumnChecks: cols => {
const checks: NaiveUI.TableColumnCheck[] = [];
cols.forEach(column => {
if (isTableColumnHasKey(column)) {
checks.push({
key: column.key as string,
title: column.title as string,
checked: true
});
} else if (column.type === 'selection') {
checks.push({
key: SELECTION_KEY,
title: $t('common.check'),
checked: true
});
}
});
return checks;
},
getColumns: (cols, checks) => {
const columnMap = new Map<string, NaiveUI.TableColumn<T>>();
cols.forEach(column => {
if (isTableColumnHasKey(column)) {
columnMap.set(column.key as string, column);
} else if (column.type === 'selection') {
columnMap.set(SELECTION_KEY, column);
}
});
const filteredColumns = checks
.filter(item => item.checked)
.map(check => columnMap.get(check.key) as NaiveUI.TableColumn<T>);
return filteredColumns;
},
onFetched: async transformed => {
const { pageNum, pageSize, total } = transformed;
updatePagination({
page: pageNum,
pageSize,
itemCount: total
});
},
immediate
});
const pagination: PaginationProps = reactive({
page: 1,
pageSize: 10,
showSizePicker: true,
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();
}
});
// this is for mobile, if the system does not support mobile, you can use `pagination` directly
const mobilePagination = computed(() => {
const p: PaginationProps = {
...pagination,
pageSlot: appStore.isMobile ? 3 : 9
};
return p;
});
function updatePagination(update: Partial<PaginationProps>) {
Object.assign(pagination, update);
}
scope.run(() => {
watch(
() => appStore.locale,
() => {
reloadColumns();
}
);
});
onScopeDispose(() => {
scope.stop();
});
return {
loading,
empty,
data,
columns,
columnChecks,
reloadColumns,
pagination,
mobilePagination,
updatePagination,
getData,
searchParams,
updateSearchParams,
resetSearchParams
};
}
export function useNaiveTableOperate<T extends TableData = TableData>(data: Ref<T[]>, getData: () => Promise<void>) {
const { bool: drawerVisible, setTrue: openDrawer, setFalse: closeDrawer } = useBoolean();
const operateType = ref<NaiveUI.TableOperateType>('add');
function handleAdd() {
operateType.value = 'add';
openDrawer();
}
/** the editing row data */
const editingData: Ref<T | null> = ref(null);
function handleEdit(id: number) {
operateType.value = 'edit';
editingData.value = data.value.find(item => item.id === id) || null;
openDrawer();
}
const checkedRowKeys = ref<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,
operateType,
handleAdd,
editingData,
handleEdit,
checkedRowKeys,
onBatchDeleted,
onDeleted,
closeDrawer
};
}
export function getNaiveTableIndex(pagination: PaginationProps, index: number) {
const { page = 1, pageSize = 10 } = pagination;
return (page - 1) * pageSize + index + 1;
}
export function getNaiveTableRowKey<T extends TableData>(row: T) {
return row.id;
}
function isTableColumnHasKey<T extends TableData>(
column: NaiveUI.TableColumn<T>
): column is NaiveUI.TableColumnWithKey<T> {
return Boolean((column as NaiveUI.TableColumnWithKey<T>).key);
}

View File

@ -1,4 +1,44 @@
declare namespace NaiveUI {
type ThemeColor = 'default' | 'error' | 'primary' | 'info' | 'success' | 'warning';
type Align = 'stretch' | 'baseline' | 'start' | 'end' | 'center' | 'flex-end' | 'flex-start';
type DataTableBaseColumn<T> = import('naive-ui').DataTableBaseColumn<T>;
type DataTableExpandColumn<T> = import('naive-ui').DataTableExpandColumn<T>;
type DataTableSelectionColumn<T> = import('naive-ui').DataTableSelectionColumn<T>;
type TableColumnGroup<T> = import('naive-ui/es/data-table/src/interface').TableColumnGroup<T>;
type PaginationProps = import('naive-ui').PaginationProps;
type TableColumnCheck = import('@sa/hooks').TableColumnCheck;
type FlatResponseData<T> = import('@sa/axios').FlatResponseData<T>;
type CustomColumnKey = 'index' | 'operate';
type SetTableColumnKey<C, T> = Omit<C, 'key'> & { key: keyof T | CustomColumnKey };
type TableData = Api.Common.CommonRecord<object>;
type TableColumnWithKey<T extends TableData = TableData> =
| SetTableColumnKey<DataTableBaseColumn<T>, T>
| SetTableColumnKey<TableColumnGroup<T>, T>;
type TableColumn<T extends TableData = TableData> =
| TableColumnWithKey<T>
| DataTableSelectionColumn<T>
| DataTableExpandColumn<T>;
type TableApiFn<T extends TableData = TableData> = (
params: Api.SystemManage.CommonSearchParams
) => Promise<FlatResponseData<Api.Common.PaginatingQueryRecord<T>>>;
/**
* the type of table operation
*
* - add: add table item
* - edit: edit table item
*/
type TableOperateType = 'add' | 'edit';
type NaiveTableConfig<T extends TableData = TableData, A extends TableApiFn<T> = TableApiFn<T>> = Pick<
import('@sa/hooks').TableConfig<A, T, TableColumn<T>>,
'apiFn' | 'apiParams' | 'columns' | 'immediate'
>;
}

View File

@ -0,0 +1,198 @@
<script setup lang="tsx">
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import { NButton, NPopconfirm, NTag } from 'naive-ui';
import { fetchGetUserList } from '@/service/api';
import { $t } from '@/locales';
import { useAppStore } from '@/store/modules/app';
import { enableStatusRecord, userGenderRecord } from '@/constants/business';
import {
getNaiveTableIndex,
getNaiveTableRowKey,
useNaiveTable,
useNaiveTableOperate
} from '@/hooks/common/naive-table';
import UserOperateDrawer from './modules/user-operate-drawer.vue';
import UserSearch from './modules/user-search.vue';
const appStore = useAppStore();
const { columns, columnChecks, data, getData, loading, pagination, mobilePagination, searchParams, resetSearchParams } =
useNaiveTable({
apiFn: fetchGetUserList,
apiParams: {
current: 1,
size: 10,
// if you want to use the searchParams in Form, you need to define the following properties, and the value is null
// the value can not be undefined, otherwise the property in Form will not be reactive
status: null,
userName: null,
userGender: null,
nickName: null,
userPhone: null,
userEmail: null
},
columns: () => [
{
type: 'selection',
align: 'center',
width: 48
},
{
key: 'index',
title: $t('common.index'),
render: (_, index): number => getNaiveTableIndex(pagination, index),
align: 'center',
width: 64
},
{
key: 'userName',
title: $t('page.manage.user.userName'),
align: 'center',
minWidth: 100
},
{
key: 'userGender',
title: $t('page.manage.user.userGender'),
align: 'center',
width: 100,
render: row => {
if (row.userGender === null) {
return null;
}
const tagMap: Record<Api.SystemManage.UserGender, NaiveUI.ThemeColor> = {
1: 'primary',
2: 'error'
};
const label = $t(userGenderRecord[row.userGender]);
return <NTag type={tagMap[row.userGender]}>{label}</NTag>;
}
},
{
key: 'nickName',
title: $t('page.manage.user.nickName'),
align: 'center',
minWidth: 100
},
{
key: 'userPhone',
title: $t('page.manage.user.userPhone'),
align: 'center',
width: 120
},
{
key: 'userEmail',
title: $t('page.manage.user.userEmail'),
align: 'center',
minWidth: 200
},
{
key: 'status',
title: $t('page.manage.user.userStatus'),
align: 'center',
width: 100,
render: row => {
if (row.status === null) {
return null;
}
const tagMap: Record<Api.Common.EnableStatus, NaiveUI.ThemeColor> = {
1: 'success',
2: 'warning'
};
const label = $t(enableStatusRecord[row.status]);
return <NTag type={tagMap[row.status]}>{label}</NTag>;
}
},
{
key: 'operate',
title: $t('common.operate'),
align: 'center',
width: 130,
render: row => (
<div class="flex-center gap-8px">
<NButton type="primary" ghost size="small" onClick={() => edit(row.id)}>
{$t('common.edit')}
</NButton>
<NPopconfirm onPositiveClick={() => handleDelete(row.id)}>
{{
default: () => $t('common.confirmDelete'),
trigger: () => (
<NButton type="error" ghost size="small">
{$t('common.delete')}
</NButton>
)
}}
</NPopconfirm>
</div>
)
}
]
});
const { drawerVisible, operateType, editingData, handleAdd, handleEdit, checkedRowKeys, onBatchDeleted, onDeleted } =
useNaiveTableOperate(data, getData);
async function handleBatchDelete() {
// request
console.log(checkedRowKeys.value);
onBatchDeleted();
}
function handleDelete(id: number) {
// request
console.log(id);
onDeleted();
}
function edit(id: number) {
handleEdit(id);
}
</script>
<template>
<div class="min-h-500px flex-vertical-stretch gap-16px overflow-hidden <sm:overflow-auto">
<UserSearch v-model:model="searchParams" @reset="resetSearchParams" @search="getData" />
<NCard :title="$t('page.manage.user.title')" :bordered="false" size="small" class="sm:flex-1-hidden card-wrapper">
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"
:disabled-delete="checkedRowKeys.length === 0"
:loading="loading"
@add="handleAdd"
@delete="handleBatchDelete"
@refresh="getData"
/>
</template>
<NDataTable
v-model:checked-row-keys="checkedRowKeys"
:columns="columns"
:data="data"
size="small"
:flex-height="!appStore.isMobile"
:scroll-x="962"
:loading="loading"
remote
:row-key="getNaiveTableRowKey"
:pagination="mobilePagination"
class="sm:h-full"
/>
<UserOperateDrawer
v-model:visible="drawerVisible"
:operate-type="operateType"
:row-data="editingData"
@submitted="getData"
/>
</NCard>
</div>
</template>
<style scoped></style>