Merge branch 'soybeanjs:main' into Jin

This commit is contained in:
金街水 2024-03-22 03:11:56 +08:00 committed by GitHub
commit 9254d22665
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 1218 additions and 725 deletions

View File

@ -13,5 +13,6 @@
"i18n-ally.keystyle": "nested", "i18n-ally.keystyle": "nested",
"i18n-ally.localesPaths": ["src/locales/langs"], "i18n-ally.localesPaths": ["src/locales/langs"],
"prettier.enable": false, "prettier.enable": false,
"unocss.root": ["./"] "unocss.root": ["./"],
"typescript.tsdk": "node_modules/typescript/lib"
} }

View File

@ -65,12 +65,12 @@
}, },
"devDependencies": { "devDependencies": {
"@elegant-router/vue": "0.3.6", "@elegant-router/vue": "0.3.6",
"@iconify/json": "2.2.192", "@iconify/json": "2.2.194",
"@sa/scripts": "workspace:*", "@sa/scripts": "workspace:*",
"@sa/uno-preset": "workspace:*", "@sa/uno-preset": "workspace:*",
"@soybeanjs/eslint-config": "1.2.5", "@soybeanjs/eslint-config": "1.2.5",
"@types/lodash-es": "4.17.12", "@types/lodash-es": "4.17.12",
"@types/node": "20.11.28", "@types/node": "20.11.30",
"@types/nprogress": "0.2.3", "@types/nprogress": "0.2.3",
"@unocss/eslint-config": "0.58.6", "@unocss/eslint-config": "0.58.6",
"@unocss/preset-icons": "0.58.6", "@unocss/preset-icons": "0.58.6",
@ -86,15 +86,15 @@
"sass": "1.72.0", "sass": "1.72.0",
"simple-git-hooks": "2.11.0", "simple-git-hooks": "2.11.0",
"tsx": "4.7.1", "tsx": "4.7.1",
"typescript": "5.4.2", "typescript": "5.4.3",
"unplugin-icons": "0.18.5", "unplugin-icons": "0.18.5",
"unplugin-vue-components": "0.26.0", "unplugin-vue-components": "0.26.0",
"vite": "5.1.6", "vite": "5.2.2",
"vite-plugin-progress": "0.0.7", "vite-plugin-progress": "0.0.7",
"vite-plugin-svg-icons": "2.0.1", "vite-plugin-svg-icons": "2.0.1",
"vite-plugin-vue-devtools": "7.0.18", "vite-plugin-vue-devtools": "7.0.20",
"vue-eslint-parser": "9.4.2", "vue-eslint-parser": "9.4.2",
"vue-tsc": "2.0.6" "vue-tsc": "2.0.7"
}, },
"simple-git-hooks": { "simple-git-hooks": {
"commit-msg": "pnpm sa git-commit-verify", "commit-msg": "pnpm sa git-commit-verify",

View File

@ -12,10 +12,10 @@
"dependencies": { "dependencies": {
"@sa/utils": "workspace:*", "@sa/utils": "workspace:*",
"axios": "1.6.8", "axios": "1.6.8",
"axios-retry": "4.0.0", "axios-retry": "4.1.0",
"qs": "6.12.0" "qs": "6.12.0"
}, },
"devDependencies": { "devDependencies": {
"@types/qs": "6.9.12" "@types/qs": "6.9.14"
} }
} }

View File

@ -2,5 +2,8 @@ import useBoolean from './use-boolean';
import useLoading from './use-loading'; import useLoading from './use-loading';
import useContext from './use-context'; import useContext from './use-context';
import useSvgIconRender from './use-svg-icon-render'; import useSvgIconRender from './use-svg-icon-render';
import useHookTable from './use-table';
export { useBoolean, useLoading, useContext, useSvgIconRender }; export { useBoolean, useLoading, useContext, useSvgIconRender, useHookTable };
export * from './use-table';

View File

@ -0,0 +1,151 @@
import { computed, reactive, ref } from 'vue';
import type { Ref } from 'vue';
import useBoolean from './use-boolean';
import useLoading from './use-loading';
export type MaybePromise<T> = T | Promise<T>;
export type ApiFn = (args: any) => Promise<unknown>;
export type TableColumnCheck = {
key: string;
title: string;
checked: boolean;
};
export type TableDataWithIndex<T> = T & { index: number };
export type TransformedData<T> = {
data: TableDataWithIndex<T>[];
pageNum: number;
pageSize: number;
total: number;
};
export type Transformer<T, Response> = (response: Response) => TransformedData<T>;
export type TableConfig<A extends ApiFn, T, C> = {
/** api function to get table data */
apiFn: A;
/** api params */
apiParams?: Parameters<A>[0];
/** transform api response to table data */
transformer: Transformer<T, Awaited<ReturnType<A>>>;
/** columns factory */
columns: () => C[];
/**
* get column checks
*
* @param columns
*/
getColumnChecks: (columns: C[]) => TableColumnCheck[];
/**
* get columns
*
* @param columns
*/
getColumns: (columns: C[], checks: TableColumnCheck[]) => C[];
/**
* callback when response fetched
*
* @param transformed transformed data
*/
onFetched?: (transformed: TransformedData<T>) => MaybePromise<void>;
/**
* whether to get data immediately
*
* @default true
*/
immediate?: boolean;
};
export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<A, T, C>) {
const { loading, startLoading, endLoading } = useLoading();
const { bool: empty, setBool: setEmpty } = useBoolean();
const { apiFn, apiParams, transformer, immediate = true, getColumnChecks, getColumns } = config;
const searchParams: NonNullable<Parameters<A>[0]> = reactive({ ...apiParams });
const allColumns = ref(config.columns()) as Ref<C[]>;
const data: Ref<T[]> = ref([]);
const columnChecks: Ref<TableColumnCheck[]> = ref(getColumnChecks(config.columns()));
const columns = computed(() => getColumns(allColumns.value, columnChecks.value));
function reloadColumns() {
allColumns.value = config.columns();
const checkMap = new Map(columnChecks.value.map(col => [col.key, col.checked]));
const defaultChecks = getColumnChecks(allColumns.value);
columnChecks.value = defaultChecks.map(col => ({
...col,
checked: checkMap.get(col.key) ?? col.checked
}));
}
async function getData() {
startLoading();
const formattedParams = formatSearchParams(searchParams);
const response = await apiFn(formattedParams);
const transformed = transformer(response as Awaited<ReturnType<A>>);
data.value = transformed.data;
setEmpty(transformed.data.length === 0);
await config.onFetched?.(transformed);
endLoading();
}
function formatSearchParams(params: Record<string, unknown>) {
const formattedParams: Record<string, unknown> = {};
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<Parameters<A>[0]>) {
Object.assign(searchParams, params);
}
/** reset search params */
function resetSearchParams() {
Object.assign(searchParams, apiParams);
}
if (immediate) {
getData();
}
return {
loading,
empty,
data,
columns,
columnChecks,
reloadColumns,
getData,
searchParams,
updateSearchParams,
resetSearchParams
};
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -1,153 +1,146 @@
import { computed, effectScope, onScopeDispose, reactive, ref, watch } from 'vue'; import { computed, effectScope, onScopeDispose, reactive, ref, watch } from 'vue';
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import type { DataTableBaseColumn, DataTableExpandColumn, DataTableSelectionColumn, PaginationProps } from 'naive-ui'; import type { PaginationProps } from 'naive-ui';
import type { TableColumnGroup } from 'naive-ui/es/data-table/src/interface'; import { useBoolean, useHookTable } from '@sa/hooks';
import { useBoolean, useLoading } from '@sa/hooks';
import { useAppStore } from '@/store/modules/app'; import { useAppStore } from '@/store/modules/app';
import { $t } from '@/locales'; import { $t } from '@/locales';
type BaseData = Record<string, unknown>; type TableData = NaiveUI.TableData;
type GetTableData<A extends NaiveUI.TableApiFn> = NaiveUI.GetTableData<A>;
type TableColumn<T> = NaiveUI.TableColumn<T>;
type ApiFn = (args: any) => Promise<unknown>; export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTableConfig<A>) {
export type TableColumn<T extends BaseData = BaseData, CustomColumnKey = never> =
| (Omit<TableColumnGroup<T>, 'key'> & { key: keyof T | CustomColumnKey })
| (Omit<DataTableBaseColumn<T>, 'key'> & { key: keyof T | CustomColumnKey })
| DataTableSelectionColumn<T>
| DataTableExpandColumn<T>;
export type TransformedData<TableData extends BaseData = BaseData> = {
data: TableData[];
pageNum: number;
pageSize: number;
total: number;
};
/** transform api response to table data */
type Transformer<TableData extends BaseData = BaseData, Response = NonNullable<unknown>> = (
response: Response
) => TransformedData<TableData>;
/** table config */
export type TableConfig<TableData extends BaseData = BaseData, Fn extends ApiFn = ApiFn, CustomColumnKey = never> = {
/** api function to get table data */
apiFn: Fn;
/** api params */
apiParams?: Parameters<Fn>[0];
/** transform api response to table data */
transformer: Transformer<TableData, Awaited<ReturnType<Fn>>>;
/** pagination */
pagination?: PaginationProps;
/**
* callback when pagination changed
*
* @param pagination
*/
onPaginationChanged?: (pagination: PaginationProps) => void | Promise<void>;
/**
* whether to get data immediately
*
* @default true
*/
immediate?: boolean;
/** columns factory */
columns: () => TableColumn<TableData, CustomColumnKey>[];
};
/** filter columns */
export type FilteredColumn = {
key: string;
title: string;
checked: boolean;
};
export function useTable<TableData extends BaseData, Fn extends ApiFn, CustomColumnKey = never>(
config: TableConfig<TableData, Fn, CustomColumnKey>
) {
const scope = effectScope(); const scope = effectScope();
const appStore = useAppStore(); const appStore = useAppStore();
const { loading, startLoading, endLoading } = useLoading(); const { apiFn, apiParams, immediate } = config;
const { bool: empty, setBool: setEmpty } = useBoolean();
const { apiFn, apiParams, transformer, onPaginationChanged, immediate = true } = config; const SELECTION_KEY = '__selection__';
const searchParams: NonNullable<Parameters<Fn>[0]> = reactive({ ...apiParams }); const {
loading,
empty,
data,
columns,
columnChecks,
reloadColumns,
getData,
searchParams,
updateSearchParams,
resetSearchParams
} = useHookTable<A, GetTableData<A>, TableColumn<NaiveUI.TableDataWithIndex<GetTableData<A>>>>({
apiFn,
apiParams,
columns: config.columns,
transformer: res => {
const { records = [], current = 1, size = 10, total = 0 } = res.data || {};
const { columns, filteredColumns, reloadColumns } = useTableColumn(config.columns); const recordsWithIndex = records.map((item, index) => {
return {
...item,
index: (current - 1) * size + index + 1
};
});
const data: Ref<TableData[]> = ref([]); return {
data: recordsWithIndex,
pageNum: current,
pageSize: size,
total
};
},
getColumnChecks: cols => {
const checks: NaiveUI.TableColumnCheck[] = [];
const pagination = reactive({ 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, TableColumn<GetTableData<A>>>();
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 TableColumn<GetTableData<A>>);
return filteredColumns;
},
onFetched: async transformed => {
const { pageNum, pageSize, total } = transformed;
updatePagination({
page: pageNum,
pageSize,
itemCount: total
});
},
immediate
});
const pagination: PaginationProps = reactive({
page: 1, page: 1,
pageSize: 10, pageSize: 10,
showSizePicker: true, showSizePicker: true,
pageSizes: [10, 15, 20, 25, 30], pageSizes: [10, 15, 20, 25, 30],
// Fix Naive Pagination's outdated API
onUpdatePage: async (page: number) => { onUpdatePage: async (page: number) => {
pagination.page = page; pagination.page = page;
await onPaginationChanged?.(pagination); updateSearchParams({
current: page,
size: pagination.pageSize!
});
getData();
}, },
onUpdatePageSize: async (pageSize: number) => { onUpdatePageSize: async (pageSize: number) => {
pagination.pageSize = pageSize; pagination.pageSize = pageSize;
pagination.page = 1; pagination.page = 1;
await onPaginationChanged?.(pagination); updateSearchParams({
}, current: pagination.page,
...config.pagination size: pageSize
}) as PaginationProps; });
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>) { function updatePagination(update: Partial<PaginationProps>) {
Object.assign(pagination, update); Object.assign(pagination, update);
} }
async function getData() {
startLoading();
const formattedParams = formatSearchParams(searchParams);
const response = await apiFn(formattedParams);
const { data: tableData, pageNum, pageSize, total } = transformer(response as Awaited<ReturnType<Fn>>);
data.value = tableData;
setEmpty(tableData.length === 0);
updatePagination({ page: pageNum, pageSize, itemCount: total });
endLoading();
}
function formatSearchParams(params: Record<string, unknown>) {
const formattedParams: Record<string, unknown> = {};
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<Parameters<Fn>[0]>) {
Object.assign(searchParams, params);
}
/** reset search params */
function resetSearchParams() {
Object.assign(searchParams, apiParams);
}
if (immediate) {
getData();
}
scope.run(() => { scope.run(() => {
watch( watch(
() => appStore.locale, () => appStore.locale,
@ -166,9 +159,10 @@ export function useTable<TableData extends BaseData, Fn extends ApiFn, CustomCol
empty, empty,
data, data,
columns, columns,
filteredColumns, columnChecks,
reloadColumns, reloadColumns,
pagination, pagination,
mobilePagination,
updatePagination, updatePagination,
getData, getData,
searchParams, searchParams,
@ -177,62 +171,59 @@ export function useTable<TableData extends BaseData, Fn extends ApiFn, CustomCol
}; };
} }
function useTableColumn<TableData extends BaseData, CustomColumnKey = never>( export function useTableOperate<T extends TableData = TableData>(data: Ref<T[]>, getData: () => Promise<void>) {
factory: () => TableColumn<TableData, CustomColumnKey>[] const { bool: drawerVisible, setTrue: openDrawer, setFalse: closeDrawer } = useBoolean();
) {
const SELECTION_KEY = '__selection__';
const allColumns = ref(factory()) as Ref<TableColumn<TableData, CustomColumnKey>[]>; const operateType = ref<NaiveUI.TableOperateType>('add');
const filteredColumns: Ref<FilteredColumn[]> = ref(getFilteredColumns(factory())); function handleAdd() {
operateType.value = 'add';
const columns = computed(() => getColumns()); openDrawer();
function reloadColumns() {
allColumns.value = factory();
filteredColumns.value = getFilteredColumns(factory());
} }
function getFilteredColumns(aColumns: TableColumn<TableData, CustomColumnKey>[]) { /** the editing row data */
const cols: FilteredColumn[] = []; const editingData: Ref<T | null> = ref(null);
aColumns.forEach(column => { function handleEdit(id: T['id']) {
if (column.type === undefined) { operateType.value = 'edit';
cols.push({ editingData.value = data.value.find(item => item.id === id) || null;
key: column.key as string,
title: column.title as string,
checked: true
});
}
if (column.type === 'selection') { openDrawer();
cols.push({
key: SELECTION_KEY,
title: $t('common.check'),
checked: true
});
}
});
return cols;
} }
function getColumns() { /** the checked row keys of table */
const cols = filteredColumns.value const checkedRowKeys = ref<string[]>([]);
.filter(column => column.checked)
.map(column => {
if (column.key === SELECTION_KEY) {
return allColumns.value.find(col => col.type === 'selection');
}
return allColumns.value.find(col => (col as DataTableBaseColumn).key === column.key);
});
return cols as TableColumn<TableData, CustomColumnKey>[]; /** 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 { return {
columns, drawerVisible,
reloadColumns, openDrawer,
filteredColumns closeDrawer,
operateType,
handleAdd,
editingData,
handleEdit,
checkedRowKeys,
onBatchDeleted,
onDeleted
}; };
} }
function isTableColumnHasKey<T>(column: TableColumn<T>): column is NaiveUI.TableColumnWithKey<T> {
return Boolean((column as NaiveUI.TableColumnWithKey<T>).key);
}

View File

@ -319,8 +319,9 @@ const local: App.I18n.Schema = {
menuName: 'Menu Name', menuName: 'Menu Name',
routeName: 'Route Name', routeName: 'Route Name',
routePath: 'Route Path', routePath: 'Route Path',
page: 'Page Component', routeParams: 'Route Params',
layout: 'Layout Component', layout: 'Layout Component',
page: 'Page Component',
i18nKey: 'I18n Key', i18nKey: 'I18n Key',
icon: 'Icon', icon: 'Icon',
localIcon: 'Local Icon', localIcon: 'Local Icon',

View File

@ -319,8 +319,9 @@ const local: App.I18n.Schema = {
menuName: '菜单名称', menuName: '菜单名称',
routeName: '路由名称', routeName: '路由名称',
routePath: '路由路径', routePath: '路由路径',
page: '页面组件', routeParams: '路由参数',
layout: '布局', layout: '布局',
page: '页面组件',
i18nKey: '国际化key', i18nKey: '国际化key',
icon: '图标', icon: '图标',
localIcon: '本地图标', localIcon: '本地图标',

View File

@ -30,10 +30,30 @@ export function fetchGetUserList(params?: Api.SystemManage.UserSearchParams) {
}); });
} }
/** get menu list */ /**
export function fetchGetMenuList() { * get menu list
*
* @deprecated this will removed in next version 1.1.0
*/
export function fetchGetMenuListV1() {
return request<Api.SystemManage.Menu[]>({ return request<Api.SystemManage.Menu[]>({
url: '/systemManage/getMenuList', url: '/systemManage/getMenuList',
method: 'get' method: 'get'
}); });
} }
/** get menu list */
export function fetchGetMenuList() {
return request<Api.SystemManage.MenuList>({
url: '/systemManage/getMenuList/v2',
method: 'get'
});
}
/** get all pages */
export function fetchGetAllPages() {
return request<string[]>({
url: '/systemManage/getAllPages',
method: 'get'
});
}

View File

@ -225,7 +225,8 @@ export function getNaiveTheme(colors: App.Theme.ThemeColor) {
const theme: GlobalThemeOverrides = { const theme: GlobalThemeOverrides = {
common: { common: {
...getNaiveThemeColors(colors) ...getNaiveThemeColors(colors),
borderRadius: '6px'
}, },
LoadingBar: { LoadingBar: {
colorLoading colorLoading

View File

@ -16,7 +16,7 @@ declare namespace Api {
} }
/** common params of paginating query list data */ /** common params of paginating query list data */
interface PaginatingQueryRecord<T extends NonNullable<unknown>> extends PaginatingCommonParams { interface PaginatingQueryRecord<T = any> extends PaginatingCommonParams {
records: T[]; records: T[];
} }
@ -29,7 +29,7 @@ declare namespace Api {
type EnableStatus = '1' | '2'; type EnableStatus = '1' | '2';
/** common record */ /** common record */
type CommonRecord<T extends NonNullable<unknown>> = { type CommonRecord<T = any> = {
/** record id */ /** record id */
id: number; id: number;
/** record creator */ /** record creator */
@ -219,5 +219,8 @@ declare namespace Api {
/** children menu */ /** children menu */
children?: Menu[]; children?: Menu[];
}>; }>;
/** menu list */
type MenuList = Common.PaginatingQueryRecord<Menu>;
} }
} }

View File

@ -502,8 +502,9 @@ declare namespace App {
menuName: string; menuName: string;
routeName: string; routeName: string;
routePath: string; routePath: string;
page: string; routeParams: string;
layout: string; layout: string;
page: string;
i18nKey: string; i18nKey: string;
icon: string; icon: string;
localIcon: string; localIcon: string;
@ -524,8 +525,8 @@ declare namespace App {
menuName: string; menuName: string;
routeName: string; routeName: string;
routePath: string; routePath: string;
page: string;
layout: string; layout: string;
page: string;
i18nKey: string; i18nKey: string;
icon: string; icon: string;
localIcon: string; localIcon: string;

View File

@ -14,7 +14,7 @@ declare namespace CommonType {
* @property value: The option value * @property value: The option value
* @property label: The option label * @property label: The option label
*/ */
type Option<K> = { value: K; label: string }; type Option<K = string> = { value: K; label: string };
type YesOrNo = 'Y' | 'N'; type YesOrNo = 'Y' | 'N';

View File

@ -1,4 +1,47 @@
declare namespace NaiveUI { declare namespace NaiveUI {
type ThemeColor = 'default' | 'error' | 'primary' | 'info' | 'success' | 'warning'; type ThemeColor = 'default' | 'error' | 'primary' | 'info' | 'success' | 'warning';
type Align = 'stretch' | 'baseline' | 'start' | 'end' | 'center' | 'flex-end' | 'flex-start'; 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 TableDataWithIndex<T> = import('@sa/hooks').TableDataWithIndex<T>;
type FlatResponseData<T> = import('@sa/axios').FlatResponseData<T>;
/**
* 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<C, T> = Omit<C, 'key'> & { key: keyof T | CustomColumnKey };
type TableData = Api.Common.CommonRecord<object>;
type TableColumnWithKey<T> = SetTableColumnKey<DataTableBaseColumn<T>, T> | SetTableColumnKey<TableColumnGroup<T>, T>;
type TableColumn<T> = TableColumnWithKey<T> | DataTableSelectionColumn<T> | DataTableExpandColumn<T>;
type TableApiFn<T = any, R = Api.SystemManage.CommonSearchParams> = (
params: R
) => Promise<FlatResponseData<Api.Common.PaginatingQueryRecord<T>>>;
/**
* the type of table operation
*
* - add: add table item
* - edit: edit table item
*/
type TableOperateType = 'add' | 'edit';
type GetTableData<A extends TableApiFn> = A extends TableApiFn<infer T> ? T : never;
type NaiveTableConfig<A extends TableApiFn> = Pick<
import('@sa/hooks').TableConfig<A, GetTableData<A>, TableColumn<NaiveUI.TableDataWithIndex<GetTableData<A>>>>,
'apiFn' | 'apiParams' | 'columns' | 'immediate'
>;
} }

View File

@ -1,10 +1,11 @@
<script setup lang="tsx"> <script setup lang="tsx">
import { ref } from 'vue'; import { ref } from 'vue';
import type { Ref } from 'vue';
import { NButton, NPopconfirm, NTag } from 'naive-ui'; import { NButton, NPopconfirm, NTag } from 'naive-ui';
import { useBoolean } from '@sa/hooks'; import { useBoolean } from '@sa/hooks';
import { fetchGetMenuList } from '@/service/api'; import { fetchGetAllPages, fetchGetMenuList } from '@/service/api';
import { useAppStore } from '@/store/modules/app'; import { useAppStore } from '@/store/modules/app';
import { useTable } from '@/hooks/common/table'; import { useTable, useTableOperate } from '@/hooks/common/table';
import { $t } from '@/locales'; import { $t } from '@/locales';
import { yesOrNoRecord } from '@/constants/common'; import { yesOrNoRecord } from '@/constants/common';
import { enableStatusRecord, menuTypeRecord } from '@/constants/business'; import { enableStatusRecord, menuTypeRecord } from '@/constants/business';
@ -12,26 +13,13 @@ import SvgIcon from '@/components/custom/svg-icon.vue';
import MenuOperateDrawer, { type OperateType } from './modules/menu-operate-drawer.vue'; import MenuOperateDrawer, { type OperateType } from './modules/menu-operate-drawer.vue';
const appStore = useAppStore(); const appStore = useAppStore();
const { bool: drawerVisible, setTrue: openDrawer } = useBoolean();
const { bool: drawerVisible, setTrue: openDrawer, setFalse: _closeDrawer } = useBoolean();
const wrapperRef = ref<HTMLElement | null>(null); const wrapperRef = ref<HTMLElement | null>(null);
const { columns, filteredColumns, data, loading, pagination, getData } = useTable< const { columns, columnChecks, data, loading, pagination, getData } = useTable({
Api.SystemManage.Menu,
typeof fetchGetMenuList,
'index' | 'operate'
>({
apiFn: fetchGetMenuList, apiFn: fetchGetMenuList,
transformer: res => {
const menus = res.data || [];
return {
data: menus,
pageNum: 1,
pageSize: 10,
total: menus.length
};
},
columns: () => [ columns: () => [
{ {
type: 'selection', type: 'selection',
@ -159,11 +147,11 @@ const { columns, filteredColumns, data, loading, pagination, getData } = useTabl
render: row => ( render: row => (
<div class="flex-center justify-end gap-8px"> <div class="flex-center justify-end gap-8px">
{row.menuType === '1' && ( {row.menuType === '1' && (
<NButton type="primary" ghost size="small" onClick={() => handleAddChildMenu(row.id)}> <NButton type="primary" ghost size="small" onClick={() => handleAddChildMenu(row)}>
{$t('page.manage.menu.addChildMenu')} {$t('page.manage.menu.addChildMenu')}
</NButton> </NButton>
)} )}
<NButton type="primary" ghost size="small" onClick={() => handleEdit(row.id)}> <NButton type="primary" ghost size="small" onClick={() => handleEdit(row)}>
{$t('common.edit')} {$t('common.edit')}
</NButton> </NButton>
<NPopconfirm onPositiveClick={() => handleDelete(row.id)}> <NPopconfirm onPositiveClick={() => handleDelete(row.id)}>
@ -182,6 +170,8 @@ const { columns, filteredColumns, data, loading, pagination, getData } = useTabl
] ]
}); });
const { checkedRowKeys, onBatchDeleted, onDeleted } = useTableOperate(data, getData);
const operateType = ref<OperateType>('add'); const operateType = ref<OperateType>('add');
function handleAdd() { function handleAdd() {
@ -189,44 +179,51 @@ function handleAdd() {
openDrawer(); openDrawer();
} }
const checkedRowKeys = ref<string[]>([]);
async function handleBatchDelete() { async function handleBatchDelete() {
// request // request
console.log(checkedRowKeys.value); console.log(checkedRowKeys.value);
window.$message?.success($t('common.deleteSuccess'));
checkedRowKeys.value = []; onBatchDeleted();
getData();
} }
function handleAddChildMenu(id: number) { function handleDelete(id: number) {
console.log('id: ', id);
operateType.value = 'add';
openDrawer();
}
/** the editing row data */
const editingData = ref<Api.SystemManage.Menu | null>(null);
function handleEdit(id: number) {
operateType.value = 'edit';
editingData.value = data.value.find(item => item.id === id) || null;
openDrawer();
}
async function handleDelete(id: number) {
// request // request
console.log(id); console.log(id);
window.$message?.success($t('common.deleteSuccess'));
getData(); onDeleted();
} }
function getRowKey(row: Api.SystemManage.Menu) { /** the edit menu data or the parent menu data when adding a child menu */
return row.id; const editingData: Ref<Api.SystemManage.Menu | null> = ref(null);
function handleEdit(item: Api.SystemManage.Menu) {
operateType.value = 'edit';
editingData.value = { ...item };
openDrawer();
} }
function handleAddChildMenu(item: Api.SystemManage.Menu) {
operateType.value = 'addChild';
editingData.value = { ...item };
openDrawer();
}
const allPages = ref<string[]>([]);
async function getAllPages() {
const { data: pages } = await fetchGetAllPages();
allPages.value = pages || [];
}
function init() {
getAllPages();
}
// init
init();
</script> </script>
<template> <template>
@ -234,7 +231,7 @@ function getRowKey(row: Api.SystemManage.Menu) {
<NCard :title="$t('page.manage.menu.title')" :bordered="false" size="small" class="sm:flex-1-hidden card-wrapper"> <NCard :title="$t('page.manage.menu.title')" :bordered="false" size="small" class="sm:flex-1-hidden card-wrapper">
<template #header-extra> <template #header-extra>
<TableHeaderOperation <TableHeaderOperation
v-model:columns="filteredColumns" v-model:columns="columnChecks"
:disabled-delete="checkedRowKeys.length === 0" :disabled-delete="checkedRowKeys.length === 0"
:loading="loading" :loading="loading"
@add="handleAdd" @add="handleAdd"
@ -250,7 +247,7 @@ function getRowKey(row: Api.SystemManage.Menu) {
:flex-height="!appStore.isMobile" :flex-height="!appStore.isMobile"
:scroll-x="1088" :scroll-x="1088"
:loading="loading" :loading="loading"
:row-key="getRowKey" :row-key="row => row.id"
remote remote
:pagination="pagination" :pagination="pagination"
class="sm:h-full" class="sm:h-full"
@ -259,6 +256,7 @@ function getRowKey(row: Api.SystemManage.Menu) {
v-model:visible="drawerVisible" v-model:visible="drawerVisible"
:operate-type="operateType" :operate-type="operateType"
:row-data="editingData" :row-data="editingData"
:all-pages="allPages"
@submitted="getData" @submitted="getData"
/> />
</NCard> </NCard>

View File

@ -6,24 +6,21 @@ import { $t } from '@/locales';
import { enableStatusOptions, menuIconTypeOptions, menuTypeOptions } from '@/constants/business'; import { enableStatusOptions, menuIconTypeOptions, menuTypeOptions } from '@/constants/business';
import SvgIcon from '@/components/custom/svg-icon.vue'; import SvgIcon from '@/components/custom/svg-icon.vue';
import { getLocalIcons } from '@/utils/icon'; import { getLocalIcons } from '@/utils/icon';
import { getLayoutAndPage, transformLayoutAndPageToComponent } from './shared';
defineOptions({ defineOptions({
name: 'MenuOperateDrawer' name: 'MenuOperateDrawer'
}); });
/** export type OperateType = NaiveUI.TableOperateType | 'addChild';
* the type of operation
*
* - add: add user
* - edit: edit user
*/
export type OperateType = 'add' | 'edit';
interface Props { interface Props {
/** the type of operation */ /** the type of operation */
operateType: OperateType; operateType: OperateType;
/** the edit row data */ /** the edit menu data or the parent menu data when adding a child menu */
rowData?: Api.SystemManage.Menu | null; rowData?: Api.SystemManage.Menu | null;
/** all pages */
allPages: string[];
} }
const props = defineProps<Props>(); const props = defineProps<Props>();
@ -44,6 +41,7 @@ const { defaultRequiredRule } = useFormRules();
const title = computed(() => { const title = computed(() => {
const titles: Record<OperateType, string> = { const titles: Record<OperateType, string> = {
add: $t('page.manage.menu.addMenu'), add: $t('page.manage.menu.addMenu'),
addChild: $t('page.manage.menu.addChildMenu'),
edit: $t('page.manage.menu.editMenu') edit: $t('page.manage.menu.editMenu')
}; };
return titles[props.operateType]; return titles[props.operateType];
@ -51,8 +49,21 @@ const title = computed(() => {
type Model = Pick< type Model = Pick<
Api.SystemManage.Menu, Api.SystemManage.Menu,
'menuType' | 'menuName' | 'icon' | 'iconType' | 'routeName' | 'routePath' | 'status' | 'hideInMenu' | 'order' | 'menuType'
>; | 'menuName'
| 'icon'
| 'iconType'
| 'routeName'
| 'routePath'
| 'component'
| 'status'
| 'hideInMenu'
| 'order'
| 'parentId'
> & {
layout: string;
page: string;
};
const model: Model = reactive(createDefaultModel()); const model: Model = reactive(createDefaultModel());
@ -64,9 +75,12 @@ function createDefaultModel(): Model {
iconType: '1', iconType: '1',
routeName: '', routeName: '',
routePath: '', routePath: '',
layout: '',
page: '',
status: null, status: null,
hideInMenu: false, hideInMenu: false,
order: 0 order: 0,
parentId: 0
}; };
} }
@ -77,6 +91,8 @@ const rules: Record<RuleKey, App.Global.FormRule> = {
userStatus: defaultRequiredRule userStatus: defaultRequiredRule
}; };
const disabledMenuType = computed(() => props.operateType === 'edit');
const localIcons = getLocalIcons(); const localIcons = getLocalIcons();
const localIconOptions = localIcons.map<SelectOption>(item => ({ const localIconOptions = localIcons.map<SelectOption>(item => ({
label: () => ( label: () => (
@ -88,14 +104,55 @@ const localIconOptions = localIcons.map<SelectOption>(item => ({
value: item value: item
})); }));
function handleUpdateModelWhenEdit() { const showLayout = computed(() => model.parentId === 0);
const showPage = computed(() => model.menuType === '2');
const pageOptions = computed(() => {
const allPages = [...props.allPages];
if (model.routeName && !allPages.includes(model.routeName)) {
allPages.unshift(model.routeName);
}
const opts: CommonType.Option[] = allPages.map(page => ({
label: page,
value: page
}));
return opts;
});
const layoutOptions: CommonType.Option[] = [
{
label: 'base',
value: 'base'
},
{
label: 'blank',
value: 'blank'
}
];
function handleUpdateModel() {
if (props.operateType === 'add') { if (props.operateType === 'add') {
Object.assign(model, createDefaultModel()); Object.assign(model, createDefaultModel());
return; return;
} }
if (props.operateType === 'addChild' && props.rowData) {
const { id } = props.rowData;
Object.assign(model, createDefaultModel(), { parentId: id });
}
if (props.operateType === 'edit' && props.rowData) { if (props.operateType === 'edit' && props.rowData) {
Object.assign(model, props.rowData); const { component, ...rest } = props.rowData;
const { layout, page } = getLayoutAndPage(component);
Object.assign(model, rest, { layout, page });
} }
} }
@ -105,6 +162,9 @@ function closeDrawer() {
async function handleSubmit() { async function handleSubmit() {
await validate(); await validate();
model.component = transformLayoutAndPageToComponent(model.layout, model.page);
// request // request
window.$message?.success($t('common.updateSuccess')); window.$message?.success($t('common.updateSuccess'));
closeDrawer(); closeDrawer();
@ -113,7 +173,7 @@ async function handleSubmit() {
watch(visible, () => { watch(visible, () => {
if (visible.value) { if (visible.value) {
handleUpdateModelWhenEdit(); handleUpdateModel();
restoreValidation(); restoreValidation();
} }
}); });
@ -124,7 +184,7 @@ watch(visible, () => {
<NDrawerContent :title="title" :native-scrollbar="false" closable> <NDrawerContent :title="title" :native-scrollbar="false" closable>
<NForm ref="formRef" :model="model" :rules="rules" label-placement="left" :label-width="80"> <NForm ref="formRef" :model="model" :rules="rules" label-placement="left" :label-width="80">
<NFormItem :label="$t('page.manage.menu.menuType')" path="menuType"> <NFormItem :label="$t('page.manage.menu.menuType')" path="menuType">
<NRadioGroup v-model:value="model.menuType"> <NRadioGroup v-model:value="model.menuType" :disabled="disabledMenuType">
<NRadio v-for="item in menuTypeOptions" :key="item.value" :value="item.value" :label="$t(item.label)" /> <NRadio v-for="item in menuTypeOptions" :key="item.value" :value="item.value" :label="$t(item.label)" />
</NRadioGroup> </NRadioGroup>
</NFormItem> </NFormItem>
@ -158,6 +218,16 @@ watch(visible, () => {
<NFormItem :label="$t('page.manage.menu.routePath')" path="routePath"> <NFormItem :label="$t('page.manage.menu.routePath')" path="routePath">
<NInput v-model:value="model.routePath" :placeholder="$t('page.manage.menu.form.routePath')" /> <NInput v-model:value="model.routePath" :placeholder="$t('page.manage.menu.form.routePath')" />
</NFormItem> </NFormItem>
<NFormItem v-if="showLayout" :label="$t('page.manage.menu.layout')" path="layout">
<NSelect
v-model:value="model.layout"
:options="layoutOptions"
:placeholder="$t('page.manage.menu.form.layout')"
/>
</NFormItem>
<NFormItem v-if="showPage" :label="$t('page.manage.menu.page')" path="page">
<NSelect v-model:value="model.page" :options="pageOptions" :placeholder="$t('page.manage.menu.form.page')" />
</NFormItem>
<NFormItem :label="$t('page.manage.menu.menuStatus')" path="status"> <NFormItem :label="$t('page.manage.menu.menuStatus')" path="status">
<NRadioGroup v-model:value="model.status"> <NRadioGroup v-model:value="model.status">
<NRadio v-for="item in enableStatusOptions" :key="item.value" :value="item.value" :label="$t(item.label)" /> <NRadio v-for="item in enableStatusOptions" :key="item.value" :value="item.value" :label="$t(item.label)" />

View File

@ -0,0 +1,42 @@
const LAYOUT_PREFIX = 'layout.';
const VIEW_PREFIX = 'view.';
const FIRST_LEVEL_ROUTE_COMPONENT_SPLIT = '$';
export function getLayoutAndPage(component?: string | null) {
let layout = '';
let page = '';
const [layoutOrPage, pageItem] = component?.split(FIRST_LEVEL_ROUTE_COMPONENT_SPLIT) || [];
layout = getLayout(layoutOrPage);
page = getPage(pageItem || layoutOrPage);
return { layout, page };
}
function getLayout(layout: string) {
return layout.startsWith(LAYOUT_PREFIX) ? layout.replace(LAYOUT_PREFIX, '') : '';
}
function getPage(page: string) {
return page.startsWith(VIEW_PREFIX) ? page.replace(VIEW_PREFIX, '') : '';
}
export function transformLayoutAndPageToComponent(layout: string, page: string) {
const hasLayout = Boolean(layout);
const hasPage = Boolean(page);
if (hasLayout && hasPage) {
return `${LAYOUT_PREFIX}${layout}${FIRST_LEVEL_ROUTE_COMPONENT_SPLIT}${VIEW_PREFIX}${page}`;
}
if (hasLayout) {
return `${LAYOUT_PREFIX}${layout}`;
}
if (hasPage) {
return `${VIEW_PREFIX}${page}`;
}
return '';
}

View File

@ -1,30 +1,16 @@
<script setup lang="tsx"> <script setup lang="tsx">
import { computed, ref } from 'vue';
import { NButton, NPopconfirm, NTag } from 'naive-ui'; import { NButton, NPopconfirm, NTag } from 'naive-ui';
import type { PaginationProps } from 'naive-ui';
import { useBoolean } from '@sa/hooks';
import { fetchGetRoleList } from '@/service/api'; import { fetchGetRoleList } from '@/service/api';
import { useAppStore } from '@/store/modules/app'; import { useAppStore } from '@/store/modules/app';
import { useTable } from '@/hooks/common/table'; import { useTable, useTableOperate } from '@/hooks/common/table';
import { $t } from '@/locales'; import { $t } from '@/locales';
import { enableStatusRecord } from '@/constants/business'; import { enableStatusRecord } from '@/constants/business';
import RoleOperateDrawer, { type OperateType } from './modules/role-operate-drawer.vue'; import RoleOperateDrawer from './modules/role-operate-drawer.vue';
import RoleSearch from './modules/role-search.vue'; import RoleSearch from './modules/role-search.vue';
const appStore = useAppStore(); const appStore = useAppStore();
const { bool: drawerVisible, setTrue: openDrawer } = useBoolean();
const { const { columns, columnChecks, data, loading, getData, mobilePagination, searchParams, resetSearchParams } = useTable({
columns,
filteredColumns,
data,
loading,
pagination,
getData,
searchParams,
updateSearchParams,
resetSearchParams
} = useTable<Api.SystemManage.Role, typeof fetchGetRoleList, 'index' | 'operate'>({
apiFn: fetchGetRoleList, apiFn: fetchGetRoleList,
apiParams: { apiParams: {
current: 1, current: 1,
@ -35,26 +21,6 @@ const {
roleName: null, roleName: null,
roleCode: null roleCode: null
}, },
transformer: res => {
const { records = [], current = 1, size = 10, total = 0 } = res.data || {};
return {
data: records,
pageNum: current,
pageSize: size,
total
};
},
onPaginationChanged(pg) {
const { page, pageSize } = pg;
updateSearchParams({
current: page,
size: pageSize
});
getData();
},
columns: () => [ columns: () => [
{ {
type: 'selection', type: 'selection',
@ -64,7 +30,6 @@ const {
{ {
key: 'index', key: 'index',
title: $t('common.index'), title: $t('common.index'),
render: (_, index): string => getIndex(index),
width: 64, width: 64,
align: 'center' align: 'center'
}, },
@ -112,7 +77,7 @@ const {
width: 130, width: 130,
render: row => ( render: row => (
<div class="flex-center gap-8px"> <div class="flex-center gap-8px">
<NButton type="primary" ghost size="small" onClick={() => handleEdit(row.id)}> <NButton type="primary" ghost size="small" onClick={() => edit(row.id)}>
{$t('common.edit')} {$t('common.edit')}
</NButton> </NButton>
<NPopconfirm onPositiveClick={() => handleDelete(row.id)}> <NPopconfirm onPositiveClick={() => handleDelete(row.id)}>
@ -131,60 +96,34 @@ const {
] ]
}); });
// this is for mobile, if the system does not support mobile, you can use `pagination` directly const {
const mobilePagination = computed(() => { drawerVisible,
const p: PaginationProps = { operateType,
...pagination, editingData,
pageSlot: appStore.isMobile ? 3 : 9 handleAdd,
}; handleEdit,
checkedRowKeys,
return p; onBatchDeleted,
}); onDeleted
// closeDrawer
const operateType = ref<OperateType>('add'); } = useTableOperate(data, getData);
function handleAdd() {
operateType.value = 'add';
openDrawer();
}
const checkedRowKeys = ref<string[]>([]);
async function handleBatchDelete() { async function handleBatchDelete() {
// request // request
console.log(checkedRowKeys.value); console.log(checkedRowKeys.value);
window.$message?.success($t('common.deleteSuccess'));
checkedRowKeys.value = []; onBatchDeleted();
getData();
} }
/** the editing row data */ function handleDelete(id: number) {
const editingData = ref<Api.SystemManage.Role | null>(null);
function handleEdit(id: number) {
operateType.value = 'edit';
editingData.value = data.value.find(item => item.id === id) || null;
openDrawer();
}
async function handleDelete(id: number) {
// request // request
console.log(id); console.log(id);
window.$message?.success($t('common.deleteSuccess'));
getData(); onDeleted();
} }
function getIndex(index: number) { function edit(id: number) {
const { page = 0, pageSize = 10 } = pagination; handleEdit(id);
return String((page - 1) * pageSize + index + 1);
}
function getRowKey(row: Api.SystemManage.User) {
return row.id;
} }
</script> </script>
@ -194,7 +133,7 @@ function getRowKey(row: Api.SystemManage.User) {
<NCard :title="$t('page.manage.role.title')" :bordered="false" size="small" class="sm:flex-1-hidden card-wrapper"> <NCard :title="$t('page.manage.role.title')" :bordered="false" size="small" class="sm:flex-1-hidden card-wrapper">
<template #header-extra> <template #header-extra>
<TableHeaderOperation <TableHeaderOperation
v-model:columns="filteredColumns" v-model:columns="columnChecks"
:disabled-delete="checkedRowKeys.length === 0" :disabled-delete="checkedRowKeys.length === 0"
:loading="loading" :loading="loading"
@add="handleAdd" @add="handleAdd"
@ -211,7 +150,7 @@ function getRowKey(row: Api.SystemManage.User) {
:scroll-x="702" :scroll-x="702"
:loading="loading" :loading="loading"
remote remote
:row-key="getRowKey" :row-key="row => row.id"
:pagination="mobilePagination" :pagination="mobilePagination"
class="sm:h-full" class="sm:h-full"
/> />

View File

@ -8,17 +8,9 @@ defineOptions({
name: 'RoleOperateDrawer' name: 'RoleOperateDrawer'
}); });
/**
* the type of operation
*
* - add: add role
* - edit: edit role
*/
export type OperateType = 'add' | 'edit';
interface Props { interface Props {
/** the type of operation */ /** the type of operation */
operateType: OperateType; operateType: NaiveUI.TableOperateType;
/** the edit row data */ /** the edit row data */
rowData?: Api.SystemManage.Role | null; rowData?: Api.SystemManage.Role | null;
} }
@ -39,7 +31,7 @@ const { formRef, validate, restoreValidation } = useNaiveForm();
const { defaultRequiredRule } = useFormRules(); const { defaultRequiredRule } = useFormRules();
const title = computed(() => { const title = computed(() => {
const titles: Record<OperateType, string> = { const titles: Record<NaiveUI.TableOperateType, string> = {
add: $t('page.manage.role.addRole'), add: $t('page.manage.role.addRole'),
edit: $t('page.manage.role.editRole') edit: $t('page.manage.role.editRole')
}; };

View File

@ -1,30 +1,16 @@
<script setup lang="tsx"> <script setup lang="tsx">
import { computed, ref } from 'vue';
import { NButton, NPopconfirm, NTag } from 'naive-ui'; import { NButton, NPopconfirm, NTag } from 'naive-ui';
import type { PaginationProps } from 'naive-ui';
import { useBoolean } from '@sa/hooks';
import { fetchGetUserList } from '@/service/api'; import { fetchGetUserList } from '@/service/api';
import { useAppStore } from '@/store/modules/app';
import { useTable } from '@/hooks/common/table';
import { $t } from '@/locales'; import { $t } from '@/locales';
import { useAppStore } from '@/store/modules/app';
import { enableStatusRecord, userGenderRecord } from '@/constants/business'; import { enableStatusRecord, userGenderRecord } from '@/constants/business';
import UserOperateDrawer, { type OperateType } from './modules/user-operate-drawer.vue'; import { useTable, useTableOperate } from '@/hooks/common/table';
import UserOperateDrawer from './modules/user-operate-drawer.vue';
import UserSearch from './modules/user-search.vue'; import UserSearch from './modules/user-search.vue';
const appStore = useAppStore(); const appStore = useAppStore();
const { bool: drawerVisible, setTrue: openDrawer } = useBoolean();
const { const { columns, columnChecks, data, getData, loading, mobilePagination, searchParams, resetSearchParams } = useTable({
columns,
filteredColumns,
data,
loading,
pagination,
getData,
searchParams,
updateSearchParams,
resetSearchParams
} = useTable<Api.SystemManage.User, typeof fetchGetUserList, 'index' | 'operate'>({
apiFn: fetchGetUserList, apiFn: fetchGetUserList,
apiParams: { apiParams: {
current: 1, current: 1,
@ -38,26 +24,6 @@ const {
userPhone: null, userPhone: null,
userEmail: null userEmail: null
}, },
transformer: res => {
const { records = [], current = 1, size = 10, total = 0 } = res.data || {};
return {
data: records,
pageNum: current,
pageSize: size,
total
};
},
onPaginationChanged(pg) {
const { page, pageSize } = pg;
updateSearchParams({
current: page,
size: pageSize
});
getData();
},
columns: () => [ columns: () => [
{ {
type: 'selection', type: 'selection',
@ -67,7 +33,6 @@ const {
{ {
key: 'index', key: 'index',
title: $t('common.index'), title: $t('common.index'),
render: (_, index): string => getIndex(index),
align: 'center', align: 'center',
width: 64 width: 64
}, },
@ -142,7 +107,7 @@ const {
width: 130, width: 130,
render: row => ( render: row => (
<div class="flex-center gap-8px"> <div class="flex-center gap-8px">
<NButton type="primary" ghost size="small" onClick={() => handleEdit(row.id)}> <NButton type="primary" ghost size="small" onClick={() => edit(row.id)}>
{$t('common.edit')} {$t('common.edit')}
</NButton> </NButton>
<NPopconfirm onPositiveClick={() => handleDelete(row.id)}> <NPopconfirm onPositiveClick={() => handleDelete(row.id)}>
@ -161,60 +126,34 @@ const {
] ]
}); });
// this is for mobile, if the system does not support mobile, you can use `pagination` directly const {
const mobilePagination = computed(() => { drawerVisible,
const p: PaginationProps = { operateType,
...pagination, editingData,
pageSlot: appStore.isMobile ? 3 : 9 handleAdd,
}; handleEdit,
checkedRowKeys,
return p; onBatchDeleted,
}); onDeleted
// closeDrawer
const operateType = ref<OperateType>('add'); } = useTableOperate(data, getData);
function handleAdd() {
operateType.value = 'add';
openDrawer();
}
const checkedRowKeys = ref<string[]>([]);
async function handleBatchDelete() { async function handleBatchDelete() {
// request // request
console.log(checkedRowKeys.value); console.log(checkedRowKeys.value);
window.$message?.success($t('common.deleteSuccess'));
checkedRowKeys.value = []; onBatchDeleted();
getData();
} }
/** the editing row data */ function handleDelete(id: number) {
const editingData = ref<Api.SystemManage.User | null>(null);
function handleEdit(id: number) {
operateType.value = 'edit';
editingData.value = data.value.find(item => item.id === id) || null;
openDrawer();
}
async function handleDelete(id: number) {
// request // request
console.log(id); console.log(id);
window.$message?.success($t('common.deleteSuccess'));
getData(); onDeleted();
} }
function getIndex(index: number) { function edit(id: number) {
const { page = 0, pageSize = 10 } = pagination; handleEdit(id);
return String((page - 1) * pageSize + index + 1);
}
function getRowKey(row: Api.SystemManage.User) {
return row.id;
} }
</script> </script>
@ -224,7 +163,7 @@ function getRowKey(row: Api.SystemManage.User) {
<NCard :title="$t('page.manage.user.title')" :bordered="false" size="small" class="sm:flex-1-hidden card-wrapper"> <NCard :title="$t('page.manage.user.title')" :bordered="false" size="small" class="sm:flex-1-hidden card-wrapper">
<template #header-extra> <template #header-extra>
<TableHeaderOperation <TableHeaderOperation
v-model:columns="filteredColumns" v-model:columns="columnChecks"
:disabled-delete="checkedRowKeys.length === 0" :disabled-delete="checkedRowKeys.length === 0"
:loading="loading" :loading="loading"
@add="handleAdd" @add="handleAdd"
@ -241,7 +180,7 @@ function getRowKey(row: Api.SystemManage.User) {
:scroll-x="962" :scroll-x="962"
:loading="loading" :loading="loading"
remote remote
:row-key="getRowKey" :row-key="row => row.id"
:pagination="mobilePagination" :pagination="mobilePagination"
class="sm:h-full" class="sm:h-full"
/> />

View File

@ -9,17 +9,9 @@ defineOptions({
name: 'UserOperateDrawer' name: 'UserOperateDrawer'
}); });
/**
* the type of operation
*
* - add: add user
* - edit: edit user
*/
export type OperateType = 'add' | 'edit';
interface Props { interface Props {
/** the type of operation */ /** the type of operation */
operateType: OperateType; operateType: NaiveUI.TableOperateType;
/** the edit row data */ /** the edit row data */
rowData?: Api.SystemManage.User | null; rowData?: Api.SystemManage.User | null;
} }
@ -40,7 +32,7 @@ const { formRef, validate, restoreValidation } = useNaiveForm();
const { defaultRequiredRule } = useFormRules(); const { defaultRequiredRule } = useFormRules();
const title = computed(() => { const title = computed(() => {
const titles: Record<OperateType, string> = { const titles: Record<NaiveUI.TableOperateType, string> = {
add: $t('page.manage.user.addUser'), add: $t('page.manage.user.addUser'),
edit: $t('page.manage.user.editUser') edit: $t('page.manage.user.editUser')
}; };