diff --git a/packages/axios/src/index.ts b/packages/axios/src/index.ts index c29d9d2..076d2ca 100644 --- a/packages/axios/src/index.ts +++ b/packages/axios/src/index.ts @@ -13,11 +13,12 @@ import type { ResponseType } from './type'; -function createCommonRequest( - axiosConfig?: CreateAxiosDefaults, - options?: Partial> -) { - const opts = createDefaultOptions(options); +function createCommonRequest< + ResponseData, + ApiData = ResponseData, + State extends Record = Record +>(axiosConfig?: CreateAxiosDefaults, options?: Partial>) { + const opts = createDefaultOptions(options); const axiosConf = createAxiosConfig(axiosConfig); const instance = axios.create(axiosConf); @@ -80,14 +81,6 @@ function createCommonRequest( } ); - function cancelRequest(requestId: string) { - const abortController = abortControllerMap.get(requestId); - if (abortController) { - abortController.abort(); - abortControllerMap.delete(requestId); - } - } - function cancelAllRequest() { abortControllerMap.forEach(abortController => { abortController.abort(); @@ -98,7 +91,6 @@ function createCommonRequest( return { instance, opts, - cancelRequest, cancelAllRequest }; } @@ -109,27 +101,27 @@ function createCommonRequest( * @param axiosConfig axios config * @param options request options */ -export function createRequest>( +export function createRequest>( axiosConfig?: CreateAxiosDefaults, - options?: Partial> + options?: Partial> ) { - const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest(axiosConfig, options); + const { instance, opts, cancelAllRequest } = createCommonRequest(axiosConfig, options); - const request: RequestInstance = async function request( - config: CustomAxiosRequestConfig - ) { + const request: RequestInstance = async function request< + T extends ApiData = ApiData, + R extends ResponseType = 'json' + >(config: CustomAxiosRequestConfig) { const response: AxiosResponse = await instance(config); const responseType = response.config?.responseType || 'json'; if (responseType === 'json') { - return opts.transformBackendResponse(response); + return opts.transform(response); } return response.data as MappedType; - } as RequestInstance; + } as RequestInstance; - request.cancelRequest = cancelRequest; request.cancelAllRequest = cancelAllRequest; request.state = {} as State; @@ -144,14 +136,14 @@ export function createRequest>( +export function createFlatRequest>( axiosConfig?: CreateAxiosDefaults, - options?: Partial> + options?: Partial> ) { - const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest(axiosConfig, options); + const { instance, opts, cancelAllRequest } = createCommonRequest(axiosConfig, options); - const flatRequest: FlatRequestInstance = async function flatRequest< - T = any, + const flatRequest: FlatRequestInstance = async function flatRequest< + T extends ApiData = ApiData, R extends ResponseType = 'json' >(config: CustomAxiosRequestConfig) { try { @@ -160,20 +152,21 @@ export function createFlatRequest, error: null }; + return { data: response.data as MappedType, error: null, response }; } catch (error) { return { data: null, error, response: (error as AxiosError).response }; } - } as FlatRequestInstance; + } as FlatRequestInstance; - flatRequest.cancelRequest = cancelRequest; flatRequest.cancelAllRequest = cancelAllRequest; - flatRequest.state = {} as State; + flatRequest.state = { + ...opts.defaultState + } as State; return flatRequest; } diff --git a/packages/axios/src/options.ts b/packages/axios/src/options.ts index 8b2b116..e786639 100644 --- a/packages/axios/src/options.ts +++ b/packages/axios/src/options.ts @@ -4,15 +4,27 @@ import { stringify } from 'qs'; import { isHttpSuccess } from './shared'; import type { RequestOption } from './type'; -export function createDefaultOptions(options?: Partial>) { - const opts: RequestOption = { +export function createDefaultOptions< + ResponseData, + ApiData = ResponseData, + State extends Record = Record +>(options?: Partial>) { + const opts: RequestOption = { + defaultState: {} as State, + transform: async response => response.data as unknown as ApiData, + transformBackendResponse: async response => response.data as unknown as ApiData, onRequest: async config => config, isBackendSuccess: _response => true, onBackendFail: async () => {}, - transformBackendResponse: async response => response.data, onError: async () => {} }; + if (options?.transform) { + opts.transform = options.transform; + } else { + opts.transform = options?.transformBackendResponse || opts.transform; + } + Object.assign(opts, options); return opts; diff --git a/packages/axios/src/type.ts b/packages/axios/src/type.ts index 644847f..846950c 100644 --- a/packages/axios/src/type.ts +++ b/packages/axios/src/type.ts @@ -8,7 +8,30 @@ export type ContentType = | 'application/x-www-form-urlencoded' | 'application/octet-stream'; -export interface RequestOption { +export type ResponseTransform = (input: Input) => Output | Promise; + +export interface RequestOption< + ResponseData, + ApiData = ResponseData, + State extends Record = Record +> { + /** + * The default state + */ + defaultState?: State; + /** + * transform the response data to the api data + * + * @param response Axios response + */ + transform: ResponseTransform, ApiData>; + /** + * transform the response data to the api data + * + * @deprecated use `transform` instead, will be removed in the next major version v3 + * @param response Axios response + */ + transformBackendResponse: ResponseTransform, ApiData>; /** * The hook before request * @@ -35,12 +58,6 @@ export interface RequestOption { response: AxiosResponse, instance: AxiosInstance ) => Promise | Promise; - /** - * transform backend response when the responseType is json - * - * @param response Axios response - */ - transformBackendResponse(response: AxiosResponse): any | Promise; /** * The hook to handle error * @@ -68,15 +85,7 @@ export type CustomAxiosRequestConfig = Omit { - /** - * cancel the request by request id - * - * if the request provide abort controller sign from config, it will not collect in the abort controller map - * - * @param requestId - */ - cancelRequest: (requestId: string) => void; +export interface RequestInstanceCommon> { /** * cancel all request * @@ -84,32 +93,35 @@ export interface RequestInstanceCommon { */ cancelAllRequest: () => void; /** you can set custom state in the request instance */ - state: T; + state: State; } /** The request instance */ -export interface RequestInstance> extends RequestInstanceCommon { - (config: CustomAxiosRequestConfig): Promise>; +export interface RequestInstance> extends RequestInstanceCommon { + ( + config: CustomAxiosRequestConfig + ): Promise>; } -export type FlatResponseSuccessData = { - data: T; +export type FlatResponseSuccessData = { + data: ApiData; error: null; response: AxiosResponse; }; -export type FlatResponseFailData = { +export type FlatResponseFailData = { data: null; error: AxiosError; response: AxiosResponse; }; -export type FlatResponseData = - | FlatResponseSuccessData +export type FlatResponseData = + | FlatResponseSuccessData | FlatResponseFailData; -export interface FlatRequestInstance, ResponseData = any> extends RequestInstanceCommon { - ( +export interface FlatRequestInstance> + extends RequestInstanceCommon { + ( config: CustomAxiosRequestConfig - ): Promise, ResponseData>>; + ): Promise>>; } diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts index a6a330b..f976351 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-request.ts b/packages/hooks/src/use-request.ts index a0a40e6..219ac07 100644 --- a/packages/hooks/src/use-request.ts +++ b/packages/hooks/src/use-request.ts @@ -6,31 +6,31 @@ import type { CreateAxiosDefaults, CustomAxiosRequestConfig, MappedType, + RequestInstanceCommon, RequestOption, ResponseType } from '@sa/axios'; import useLoading from './use-loading'; -export type HookRequestInstanceResponseSuccessData = { - data: Ref; +export type HookRequestInstanceResponseSuccessData = { + data: Ref; error: Ref; }; -export type HookRequestInstanceResponseFailData = { +export type HookRequestInstanceResponseFailData = { data: Ref; error: Ref>; }; -export type HookRequestInstanceResponseData = { +export type HookRequestInstanceResponseData = { loading: Ref; -} & (HookRequestInstanceResponseSuccessData | HookRequestInstanceResponseFailData); +} & (HookRequestInstanceResponseSuccessData | HookRequestInstanceResponseFailData); -export interface HookRequestInstance { - ( +export interface HookRequestInstance> + extends RequestInstanceCommon { + ( config: CustomAxiosRequestConfig - ): HookRequestInstanceResponseData, ResponseData>; - cancelRequest: (requestId: string) => void; - cancelAllRequest: () => void; + ): HookRequestInstanceResponseData>; } /** @@ -39,25 +39,26 @@ export interface HookRequestInstance { * @param axiosConfig * @param options */ -export default function createHookRequest( +export default function createHookRequest>( axiosConfig?: CreateAxiosDefaults, - options?: Partial> + options?: Partial> ) { - const request = createFlatRequest(axiosConfig, options); + const request = createFlatRequest(axiosConfig, options); - const hookRequest: HookRequestInstance = function hookRequest( - config: CustomAxiosRequestConfig - ) { + const hookRequest: HookRequestInstance = function hookRequest< + T extends ApiData = ApiData, + R extends ResponseType = 'json' + >(config: CustomAxiosRequestConfig) { const { loading, startLoading, endLoading } = useLoading(); - const data = ref | null>(null) as Ref>; - const error = ref | null>(null) as Ref | null>; + const data = ref(null) as Ref>; + const error = ref(null) as Ref | null>; startLoading(); request(config).then(res => { if (res.data) { - data.value = res.data; + data.value = res.data as MappedType; } else { error.value = res.error; } @@ -70,9 +71,8 @@ export default function createHookRequest( data, error }; - } as HookRequestInstance; + } as HookRequestInstance; - hookRequest.cancelRequest = request.cancelRequest; hookRequest.cancelAllRequest = request.cancelAllRequest; return hookRequest; diff --git a/packages/hooks/src/use-table.ts b/packages/hooks/src/use-table.ts index 175370b..7a01035 100644 --- a/packages/hooks/src/use-table.ts +++ b/packages/hooks/src/use-table.ts @@ -1,87 +1,85 @@ -import { computed, reactive, ref } from 'vue'; -import type { Ref } from 'vue'; -import { jsonClone } from '@sa/utils'; +import { computed, ref } from 'vue'; +import type { Ref, VNodeChild } from 'vue'; import useBoolean from './use-boolean'; import useLoading from './use-loading'; -export type MaybePromise = T | Promise; - -export type ApiFn = (args: any) => Promise; - -export type TableColumnCheck = { - prop: string; - label: string; - checked: boolean; -}; - -export type TableDataWithIndex = T & { index: number }; - -export type TransformedData = { - data: TableDataWithIndex[]; +export interface PaginationData { + data: T[]; pageNum: number; pageSize: number; total: number; +} + +type GetApiData = Pagination extends true ? PaginationData : ApiData[]; + +type Transform = ( + response: ResponseData +) => GetApiData; + +export type TableColumnCheckTitle = string | ((...args: any) => VNodeChild); + +export type TableColumnCheck = { + prop: string; + label: TableColumnCheckTitle; + checked: boolean; + visible: boolean; }; -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 columnChecks = ref(getColumnChecks(columns())) as Ref; - const data: Ref[]> = 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(); - const checkMap = new Map(columnChecks.value.map(col => [col.prop, col.checked])); - const defaultChecks = getColumnChecks(allColumns.value); + const defaultChecks = getColumnChecks(columns()); columnChecks.value = defaultChecks.map(col => ({ ...col, @@ -90,47 +88,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) { @@ -141,12 +113,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 d9a0a69..1613cb7 100644 --- a/src/hooks/common/table.ts +++ b/src/hooks/common/table.ts @@ -1,8 +1,10 @@ -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 { PaginationEmits, PaginationProps } from 'element-plus'; +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'; @@ -10,147 +12,101 @@ type RemoveReadonly = { -readonly [key in keyof T]: T[key]; }; -type TableData = UI.TableData; -type GetTableData = UI.GetTableData; -type TableColumn = UI.TableColumn; +export type UseUITableOptions = Omit< + UseTableOptions, Pagination>, + 'pagination' | 'getColumnChecks' | 'getColumns' +> & { + /** + * get column visible + * + * @param column + * + * @default true + * + * @returns true if the column is visible, false otherwise + */ + getColumnVisible?: (column: UI.TableColumn) => boolean; +}; -export function useTable(config: UI.NaiveTableConfig) { +const SELECTION_KEY = '__selection__'; + +const EXPAND_KEY = '__expand__'; + +const INDEX_KEY = '__index__'; + +export function useUITable(options: UseUITableOptions) { + const scope = effectScope(); + const appStore = useAppStore(); + + const result = useTable, 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; + +type UseUIPaginatedTableOptions = UseUITableOptions & { + paginationProps?: Omit; + /** + * whether to show the total count of the table + * + * @default true + */ + showTotal?: boolean; + onPaginationParamsChange?: (params: PaginationParams) => void | Promise; +}; + +export function useUIPaginatedTable(options: UseUIPaginatedTableOptions) { const scope = effectScope(); const appStore = useAppStore(); const isMobile = computed(() => appStore.isMobile); - const { apiFn, apiParams, immediate } = config; - - const SELECTION_KEY = '__selection__'; - - const EXPAND_KEY = '__expand__'; - - const INDEX_KEY = '__index__'; - - 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, index) => { - return { - ...item, - index: (current - 1) * pageSize + index + 1 - }; - }); - - return { - data: recordsWithIndex, - pageNum: current, - pageSize, - total - }; - }, - getColumnChecks: cols => { - const checks: UI.TableColumnCheck[] = []; - cols.forEach(column => { - if (column.type === 'selection') { - checks.push({ - prop: SELECTION_KEY, - label: $t('common.check'), - checked: true - }); - } else if (column.type === 'expand') { - checks.push({ - prop: EXPAND_KEY, - label: $t('common.expandColumn'), - checked: true - }); - } else if (column.type === 'index') { - checks.push({ - prop: INDEX_KEY, - label: $t('common.index'), - checked: true - }); - } else { - checks.push({ - prop: column.prop as string, - label: column.label as string, - checked: true - }); - } - }); - - return checks; - }, - getColumns: (cols, checks) => { - const columnMap = new Map>>(); - - cols.forEach(column => { - if (column.type === 'selection') { - columnMap.set(SELECTION_KEY, column); - } else if (column.type === 'expand') { - columnMap.set(EXPAND_KEY, column); - } else if (column.type === 'index') { - columnMap.set(INDEX_KEY, column); - } else { - columnMap.set(column.prop as string, column); - } - }); - - const filteredColumns = checks - .filter(item => item.checked) - .map(check => columnMap.get(check.prop) as TableColumn>); - - return filteredColumns; - }, - onFetched: async transformed => { - const { pageNum, pageSize, total } = transformed; - - updatePagination({ - currentPage: pageNum, - pageSize, - total - }); - }, - immediate - }); - const pagination: Partial> = reactive({ currentPage: 1, pageSize: 10, + total: 0, pageSizes: [10, 15, 20, 25, 30], 'current-change': (page: number) => { pagination.currentPage = page; - updateSearchParams({ current: page, size: pagination.pageSize! }); - - getData(); - return true; }, 'size-change': (pageSize: number) => { pagination.currentPage = 1; pagination.pageSize = pageSize; - updateSearchParams({ current: pagination.currentPage, size: pageSize }); - - getData(); return true; - } - }); + }, + ...options.paginationProps + }) as PaginationProps; // this is for mobile, if the system does not support mobile, you can use `pagination` directly const mobilePagination = computed(() => { @@ -162,35 +118,48 @@ export function useTable(config: UI.NaiveTableConfig return p; }); - function updatePagination(update: Partial) { - Object.assign(pagination, update); - } + const paginationParams = computed(() => { + const { currentPage, pageSize } = pagination; - /** - * get data by page number - * - * @param pageNum the page number. default is 1 - */ - async function getDataByPage(pageNum: number = 1) { - updatePagination({ - currentPage: pageNum - }); + return { + currentPage, + pageSize + }; + }); - updateSearchParams({ - current: pageNum, - size: pagination.pageSize! - }); + const result = useTable, true>({ + ...options, + pagination: true, + getColumnChecks: cols => getColumnChecks(cols, options.getColumnVisible), + getColumns, + onFetched: data => { + pagination.total = data.total; + } + }); - await getData(); + async function getDataByPage(page: number = 1) { + if (page !== pagination.currentPage) { + pagination.currentPage = page; + + return; + } + + await result.getData(); } scope.run(() => { watch( () => appStore.locale, () => { - reloadColumns(); + result.reloadColumns(); } ); + + watch(paginationParams, async newVal => { + await options.onPaginationParamsChange?.(newVal); + + await result.getData(); + }); }); onScopeDispose(() => { @@ -198,27 +167,21 @@ export function useTable(config: UI.NaiveTableConfig }); 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'; @@ -226,18 +189,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() { @@ -268,3 +231,88 @@ export function useTableOperate(data: Ref, onDeleted }; } + +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 => { + if (column.type === 'selection') { + checks.push({ + prop: SELECTION_KEY, + label: $t('common.check'), + checked: true, + visible: getColumnVisible?.(column) ?? false + }); + } else if (column.type === 'expand') { + checks.push({ + prop: EXPAND_KEY, + label: $t('common.expandColumn'), + checked: true, + visible: getColumnVisible?.(column) ?? false + }); + } else if (column.type === 'index') { + checks.push({ + prop: INDEX_KEY, + label: $t('common.index'), + checked: true, + visible: getColumnVisible?.(column) ?? false + }); + } else { + checks.push({ + prop: column.prop as string, + label: column.label as string, + checked: true, + visible: getColumnVisible?.(column) ?? true + }); + } + }); + + return checks; +} + +function getColumns>(cols: Column[], checks: TableColumnCheck[]) { + const columnMap = new Map(); + + cols.forEach(column => { + if (column.type === 'selection') { + columnMap.set(SELECTION_KEY, column); + } else if (column.type === 'expand') { + columnMap.set(EXPAND_KEY, column); + } else if (column.type === 'index') { + columnMap.set(INDEX_KEY, column); + } else { + columnMap.set(column.prop as string, column); + } + }); + + const filteredColumns = checks.filter(item => item.checked).map(check => columnMap.get(check.prop) as Column); + + return filteredColumns; +} diff --git a/src/service/request/index.ts b/src/service/request/index.ts index 1ed0ef6..d2582fb 100644 --- a/src/service/request/index.ts +++ b/src/service/request/index.ts @@ -10,7 +10,7 @@ import type { RequestInstanceState } from './type'; const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y'; const { baseURL, otherBaseURL } = getServiceBaseURL(import.meta.env, isHttpProxy); -export const request = createFlatRequest( +export const request = createFlatRequest( { baseURL, headers: { @@ -18,6 +18,13 @@ export const request = createFlatRequest>) { + return response.data.data; + }, async onRequest(config) { const Authorization = getAuthorization(); Object.assign(config.headers, { Authorization }); @@ -70,19 +77,6 @@ export const request = createFlatRequest { logoutAndCleanup(); }); - // window.$dialog?.error({ - // title: $t('common.error'), - // content: response.data.msg, - // positiveText: $t('common.confirm'), - // maskClosable: false, - // closeOnEsc: false, - // onPositiveClick() { - // logoutAndCleanup(); - // }, - // onClose() { - // logoutAndCleanup(); - // } - // }); return null; } @@ -102,9 +96,6 @@ export const request = createFlatRequest( +export const demoRequest = createRequest( { baseURL: otherBaseURL.demo }, { + transform(response: AxiosResponse) { + return response.data.result; + }, async onRequest(config) { const { headers } = config; @@ -158,9 +152,6 @@ export const demoRequest = createRequest( // when the backend response code is not "200", it means the request is fail // for example: the token is expired, refresh token and retry request }, - transformBackendResponse(response) { - return response.data.result; - }, onError(error) { // when the request is fail, you can show error message diff --git a/src/service/request/type.ts b/src/service/request/type.ts index 4e4a2f7..f72b58a 100644 --- a/src/service/request/type.ts +++ b/src/service/request/type.ts @@ -1,6 +1,7 @@ export interface RequestInstanceState { - /** whether the request is refreshing token */ - refreshTokenFn: Promise | null; + /** the promise of refreshing token */ + refreshTokenPromise: Promise | null; /** the request error message stack */ errMsgStack: string[]; + [key: string]: unknown; } diff --git a/src/typings/api/auth.d.ts b/src/typings/api/auth.d.ts new file mode 100644 index 0000000..1ff174b --- /dev/null +++ b/src/typings/api/auth.d.ts @@ -0,0 +1,20 @@ +declare namespace Api { + /** + * namespace Auth + * + * backend api module: "auth" + */ + namespace Auth { + interface LoginToken { + token: string; + refreshToken: string; + } + + interface UserInfo { + userId: string; + userName: string; + roles: string[]; + buttons: string[]; + } + } +} diff --git a/src/typings/api/common.d.ts b/src/typings/api/common.d.ts new file mode 100644 index 0000000..3005226 --- /dev/null +++ b/src/typings/api/common.d.ts @@ -0,0 +1,50 @@ +/** + * Namespace Api + * + * All backend api type + */ +declare namespace Api { + namespace Common { + /** common params of paginating */ + interface PaginatingCommonParams { + /** current page number */ + current: number; + /** page size */ + size: number; + /** total count */ + total: number; + } + + /** common params of paginating query list data */ + interface PaginatingQueryRecord extends PaginatingCommonParams { + records: T[]; + } + + /** common search params of table */ + type CommonSearchParams = Pick; + + /** + * enable status + * + * - "1": enabled + * - "2": disabled + */ + type EnableStatus = '1' | '2'; + + /** common record */ + type CommonRecord = { + /** record id */ + id: number; + /** record creator */ + createBy: string; + /** record create time */ + createTime: string; + /** record updater */ + updateBy: string; + /** record update time */ + updateTime: string; + /** record status */ + status: EnableStatus | undefined; + } & T; + } +} diff --git a/src/typings/api/route.d.ts b/src/typings/api/route.d.ts new file mode 100644 index 0000000..edb755e --- /dev/null +++ b/src/typings/api/route.d.ts @@ -0,0 +1,19 @@ +declare namespace Api { + /** + * namespace Route + * + * backend api module: "route" + */ + namespace Route { + type ElegantConstRoute = import('@elegant-router/types').ElegantConstRoute; + + interface MenuRoute extends ElegantConstRoute { + id: string; + } + + interface UserRoute { + routes: MenuRoute[]; + home: import('@elegant-router/types').LastLevelRouteKey; + } + } +} diff --git a/src/typings/api.d.ts b/src/typings/api/system-manage.d.ts similarity index 63% rename from src/typings/api.d.ts rename to src/typings/api/system-manage.d.ts index 9e5da3a..33b60d6 100644 --- a/src/typings/api.d.ts +++ b/src/typings/api/system-manage.d.ts @@ -1,90 +1,4 @@ -/** - * Namespace Api - * - * All backend api type - */ declare namespace Api { - namespace Common { - /** common params of paginating */ - interface PaginatingCommonParams { - /** current page number */ - current: number; - /** page size */ - size: number; - /** total count */ - total: number; - } - - /** common params of paginating query list data */ - interface PaginatingQueryRecord extends PaginatingCommonParams { - records: T[]; - } - - /** common search params of table */ - type CommonSearchParams = Pick; - - /** - * enable status - * - * - "1": enabled - * - "2": disabled - */ - type EnableStatus = '1' | '2'; - - /** common record */ - type CommonRecord = { - /** record id */ - id: number; - /** record creator */ - createBy: string; - /** record create time */ - createTime: string; - /** record updater */ - updateBy: string; - /** record update time */ - updateTime: string; - /** record status */ - status: EnableStatus | undefined; - } & T; - } - - /** - * namespace Auth - * - * backend api module: "auth" - */ - namespace Auth { - interface LoginToken { - token: string; - refreshToken: string; - } - - interface UserInfo { - userId: string; - userName: string; - roles: string[]; - buttons: string[]; - } - } - - /** - * namespace Route - * - * backend api module: "route" - */ - namespace Route { - type ElegantConstRoute = import('@elegant-router/types').ElegantConstRoute; - - interface MenuRoute extends ElegantConstRoute { - id: string; - } - - interface UserRoute { - routes: MenuRoute[]; - home: import('@elegant-router/types').LastLevelRouteKey; - } - } - /** * namespace SystemManage * diff --git a/src/typings/ui.d.ts b/src/typings/ui.d.ts index f36abb6..c402795 100644 --- a/src/typings/ui.d.ts +++ b/src/typings/ui.d.ts @@ -1,28 +1,15 @@ declare namespace UI { type ThemeColor = 'danger' | 'primary' | 'info' | 'success' | 'warning'; + type DataTableBaseColumn = Partial>; + 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 & { prop?: keyof T | (string & {}) }; - type SetTableColumnKey = Omit & { key: keyof T | `CustomColumnKey` }; + type TableColumnWithKey = SetTableColumnKey, T>; - type TableData = Api.Common.CommonRecord; - - type TableColumnWithKey = Partial>; - - type TableColumn = TableColumnWithKey; - - type TableApiFn = ( - params: R - ) => Promise>>; + type TableColumn = DataTableBaseColumn; /** * the type of table operation @@ -31,20 +18,6 @@ declare namespace UI { * - 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; - }; } // ======================================== element-plus ======================================== diff --git a/src/views/alova/user/hooks/use-checked-columns.ts b/src/views/alova/user/hooks/use-checked-columns.ts index e015836..8345f0b 100644 --- a/src/views/alova/user/hooks/use-checked-columns.ts +++ b/src/views/alova/user/hooks/use-checked-columns.ts @@ -28,25 +28,29 @@ export default function useCheckedColumns< checks.push({ prop: SELECTION_KEY, label: $t('common.check'), - checked: true + checked: true, + visible: true }); } else if (column.type === 'expand') { checks.push({ prop: EXPAND_KEY, label: $t('common.expandColumn'), - checked: true + checked: true, + visible: true }); } else if (column.type === 'index') { checks.push({ prop: INDEX_KEY, label: $t('common.index'), - checked: true + checked: true, + visible: true }); } else { checks.push({ prop: column.prop as string, label: column.label as string, - checked: true + checked: true, + visible: true }); } }); diff --git a/src/views/alova/user/hooks/use-table-operate.ts b/src/views/alova/user/hooks/use-table-operate.ts index 981fef7..ef542f3 100644 --- a/src/views/alova/user/hooks/use-table-operate.ts +++ b/src/views/alova/user/hooks/use-table-operate.ts @@ -4,7 +4,7 @@ import { useBoolean } from '@sa/hooks'; import { jsonClone } from '@sa/utils'; import { $t } from '@/locales'; -type TableData = UI.TableData; +type TableData = Api.Common.CommonRecord; interface Operations { delete?: (row: T) => Promise; batchDelete?: (rows: T[]) => Promise; diff --git a/src/views/manage/menu/index.vue b/src/views/manage/menu/index.vue index 7b9f09b..c0d0b25 100644 --- a/src/views/manage/menu/index.vue +++ b/src/views/manage/menu/index.vue @@ -6,7 +6,7 @@ import { useBoolean } from '@sa/hooks'; import { yesOrNoRecord } from '@/constants/common'; import { enableStatusRecord, menuTypeRecord } from '@/constants/business'; import { fetchGetAllPages, fetchGetMenuList } from '@/service/api'; -import { useTable, useTableOperate } from '@/hooks/common/table'; +import { defaultTransform, useTableOperate, useUIPaginatedTable } from '@/hooks/common/table'; import { $t } from '@/locales'; import SvgIcon from '@/components/custom/svg-icon.vue'; import MenuOperateModal, { type OperateType } from './modules/menu-operate-modal.vue'; @@ -15,8 +15,9 @@ const { bool: visible, setTrue: openModal } = useBoolean(); const wrapperRef = ref(null); -const { columns, columnChecks, data, loading, pagination, getData, getDataByPage } = useTable({ - apiFn: fetchGetMenuList, +const { columns, columnChecks, data, loading, pagination, getData, getDataByPage } = useUIPaginatedTable({ + api: () => fetchGetMenuList(), + transform: response => defaultTransform(response), columns: () => [ { type: 'selection', width: 48 }, { prop: 'id', label: $t('page.manage.menu.id') }, @@ -132,7 +133,7 @@ const { columns, columnChecks, data, loading, pagination, getData, getDataByPage ] }); -const { checkedRowKeys, onBatchDeleted, onDeleted } = useTableOperate(data, getData); +const { checkedRowKeys, onBatchDeleted, onDeleted } = useTableOperate(data, 'id', getData); const operateType = ref('add'); diff --git a/src/views/manage/role/index.vue b/src/views/manage/role/index.vue index 4638fed..f4f3201 100644 --- a/src/views/manage/role/index.vue +++ b/src/views/manage/role/index.vue @@ -1,34 +1,31 @@