mirror of
https://gitee.com/honghuangdc/soybean-admin-element-plus.git
synced 2025-12-30 10:22:25 +00:00
refactor(hooks): ♻️ sync refactor useTable and enhance type definitions.
This commit is contained in:
@@ -13,11 +13,12 @@ import type {
|
||||
ResponseType
|
||||
} from './type';
|
||||
|
||||
function createCommonRequest<ResponseData = any>(
|
||||
axiosConfig?: CreateAxiosDefaults,
|
||||
options?: Partial<RequestOption<ResponseData>>
|
||||
) {
|
||||
const opts = createDefaultOptions<ResponseData>(options);
|
||||
function createCommonRequest<
|
||||
ResponseData,
|
||||
ApiData = ResponseData,
|
||||
State extends Record<string, unknown> = Record<string, unknown>
|
||||
>(axiosConfig?: CreateAxiosDefaults, options?: Partial<RequestOption<ResponseData, ApiData, State>>) {
|
||||
const opts = createDefaultOptions<ResponseData, ApiData, State>(options);
|
||||
|
||||
const axiosConf = createAxiosConfig(axiosConfig);
|
||||
const instance = axios.create(axiosConf);
|
||||
@@ -80,14 +81,6 @@ function createCommonRequest<ResponseData = any>(
|
||||
}
|
||||
);
|
||||
|
||||
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<ResponseData = any>(
|
||||
return {
|
||||
instance,
|
||||
opts,
|
||||
cancelRequest,
|
||||
cancelAllRequest
|
||||
};
|
||||
}
|
||||
@@ -109,27 +101,27 @@ function createCommonRequest<ResponseData = any>(
|
||||
* @param axiosConfig axios config
|
||||
* @param options request options
|
||||
*/
|
||||
export function createRequest<ResponseData = any, State = Record<string, unknown>>(
|
||||
export function createRequest<ResponseData, ApiData, State extends Record<string, unknown>>(
|
||||
axiosConfig?: CreateAxiosDefaults,
|
||||
options?: Partial<RequestOption<ResponseData>>
|
||||
options?: Partial<RequestOption<ResponseData, ApiData, State>>
|
||||
) {
|
||||
const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest<ResponseData>(axiosConfig, options);
|
||||
const { instance, opts, cancelAllRequest } = createCommonRequest<ResponseData, ApiData, State>(axiosConfig, options);
|
||||
|
||||
const request: RequestInstance<State> = async function request<T = any, R extends ResponseType = 'json'>(
|
||||
config: CustomAxiosRequestConfig
|
||||
) {
|
||||
const request: RequestInstance<ApiData, State> = async function request<
|
||||
T extends ApiData = ApiData,
|
||||
R extends ResponseType = 'json'
|
||||
>(config: CustomAxiosRequestConfig) {
|
||||
const response: AxiosResponse<ResponseData> = 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<R, T>;
|
||||
} as RequestInstance<State>;
|
||||
} as RequestInstance<ApiData, State>;
|
||||
|
||||
request.cancelRequest = cancelRequest;
|
||||
request.cancelAllRequest = cancelAllRequest;
|
||||
request.state = {} as State;
|
||||
|
||||
@@ -144,14 +136,14 @@ export function createRequest<ResponseData = any, State = Record<string, unknown
|
||||
* @param axiosConfig axios config
|
||||
* @param options request options
|
||||
*/
|
||||
export function createFlatRequest<ResponseData = any, State = Record<string, unknown>>(
|
||||
export function createFlatRequest<ResponseData, ApiData, State extends Record<string, unknown>>(
|
||||
axiosConfig?: CreateAxiosDefaults,
|
||||
options?: Partial<RequestOption<ResponseData>>
|
||||
options?: Partial<RequestOption<ResponseData, ApiData, State>>
|
||||
) {
|
||||
const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest<ResponseData>(axiosConfig, options);
|
||||
const { instance, opts, cancelAllRequest } = createCommonRequest<ResponseData, ApiData, State>(axiosConfig, options);
|
||||
|
||||
const flatRequest: FlatRequestInstance<State, ResponseData> = async function flatRequest<
|
||||
T = any,
|
||||
const flatRequest: FlatRequestInstance<ResponseData, ApiData, State> = async function flatRequest<
|
||||
T extends ApiData = ApiData,
|
||||
R extends ResponseType = 'json'
|
||||
>(config: CustomAxiosRequestConfig) {
|
||||
try {
|
||||
@@ -160,20 +152,21 @@ export function createFlatRequest<ResponseData = any, State = Record<string, unk
|
||||
const responseType = response.config?.responseType || 'json';
|
||||
|
||||
if (responseType === 'json') {
|
||||
const data = opts.transformBackendResponse(response);
|
||||
const data = await opts.transform(response);
|
||||
|
||||
return { data, error: null, response };
|
||||
}
|
||||
|
||||
return { data: response.data as MappedType<R, T>, error: null };
|
||||
return { data: response.data as MappedType<R, T>, error: null, response };
|
||||
} catch (error) {
|
||||
return { data: null, error, response: (error as AxiosError<ResponseData>).response };
|
||||
}
|
||||
} as FlatRequestInstance<State, ResponseData>;
|
||||
} as FlatRequestInstance<ResponseData, ApiData, State>;
|
||||
|
||||
flatRequest.cancelRequest = cancelRequest;
|
||||
flatRequest.cancelAllRequest = cancelAllRequest;
|
||||
flatRequest.state = {} as State;
|
||||
flatRequest.state = {
|
||||
...opts.defaultState
|
||||
} as State;
|
||||
|
||||
return flatRequest;
|
||||
}
|
||||
|
||||
@@ -4,15 +4,27 @@ import { stringify } from 'qs';
|
||||
import { isHttpSuccess } from './shared';
|
||||
import type { RequestOption } from './type';
|
||||
|
||||
export function createDefaultOptions<ResponseData = any>(options?: Partial<RequestOption<ResponseData>>) {
|
||||
const opts: RequestOption<ResponseData> = {
|
||||
export function createDefaultOptions<
|
||||
ResponseData,
|
||||
ApiData = ResponseData,
|
||||
State extends Record<string, unknown> = Record<string, unknown>
|
||||
>(options?: Partial<RequestOption<ResponseData, ApiData, State>>) {
|
||||
const opts: RequestOption<ResponseData, ApiData, State> = {
|
||||
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;
|
||||
|
||||
@@ -8,7 +8,30 @@ export type ContentType =
|
||||
| 'application/x-www-form-urlencoded'
|
||||
| 'application/octet-stream';
|
||||
|
||||
export interface RequestOption<ResponseData = any> {
|
||||
export type ResponseTransform<Input = any, Output = any> = (input: Input) => Output | Promise<Output>;
|
||||
|
||||
export interface RequestOption<
|
||||
ResponseData,
|
||||
ApiData = ResponseData,
|
||||
State extends Record<string, unknown> = Record<string, unknown>
|
||||
> {
|
||||
/**
|
||||
* The default state
|
||||
*/
|
||||
defaultState?: State;
|
||||
/**
|
||||
* transform the response data to the api data
|
||||
*
|
||||
* @param response Axios response
|
||||
*/
|
||||
transform: ResponseTransform<AxiosResponse<ResponseData>, 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<AxiosResponse<ResponseData>, ApiData>;
|
||||
/**
|
||||
* The hook before request
|
||||
*
|
||||
@@ -35,12 +58,6 @@ export interface RequestOption<ResponseData = any> {
|
||||
response: AxiosResponse<ResponseData>,
|
||||
instance: AxiosInstance
|
||||
) => Promise<AxiosResponse | null> | Promise<void>;
|
||||
/**
|
||||
* transform backend response when the responseType is json
|
||||
*
|
||||
* @param response Axios response
|
||||
*/
|
||||
transformBackendResponse(response: AxiosResponse<ResponseData>): any | Promise<any>;
|
||||
/**
|
||||
* The hook to handle error
|
||||
*
|
||||
@@ -68,15 +85,7 @@ export type CustomAxiosRequestConfig<R extends ResponseType = 'json'> = Omit<Axi
|
||||
responseType?: R;
|
||||
};
|
||||
|
||||
export interface RequestInstanceCommon<T> {
|
||||
/**
|
||||
* 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<State extends Record<string, unknown>> {
|
||||
/**
|
||||
* cancel all request
|
||||
*
|
||||
@@ -84,32 +93,35 @@ export interface RequestInstanceCommon<T> {
|
||||
*/
|
||||
cancelAllRequest: () => void;
|
||||
/** you can set custom state in the request instance */
|
||||
state: T;
|
||||
state: State;
|
||||
}
|
||||
|
||||
/** The request instance */
|
||||
export interface RequestInstance<S = Record<string, unknown>> extends RequestInstanceCommon<S> {
|
||||
<T = any, R extends ResponseType = 'json'>(config: CustomAxiosRequestConfig<R>): Promise<MappedType<R, T>>;
|
||||
export interface RequestInstance<ApiData, State extends Record<string, unknown>> extends RequestInstanceCommon<State> {
|
||||
<T extends ApiData = ApiData, R extends ResponseType = 'json'>(
|
||||
config: CustomAxiosRequestConfig<R>
|
||||
): Promise<MappedType<R, T>>;
|
||||
}
|
||||
|
||||
export type FlatResponseSuccessData<T = any, ResponseData = any> = {
|
||||
data: T;
|
||||
export type FlatResponseSuccessData<ResponseData, ApiData> = {
|
||||
data: ApiData;
|
||||
error: null;
|
||||
response: AxiosResponse<ResponseData>;
|
||||
};
|
||||
|
||||
export type FlatResponseFailData<ResponseData = any> = {
|
||||
export type FlatResponseFailData<ResponseData> = {
|
||||
data: null;
|
||||
error: AxiosError<ResponseData>;
|
||||
response: AxiosResponse<ResponseData>;
|
||||
};
|
||||
|
||||
export type FlatResponseData<T = any, ResponseData = any> =
|
||||
| FlatResponseSuccessData<T, ResponseData>
|
||||
export type FlatResponseData<ResponseData, ApiData> =
|
||||
| FlatResponseSuccessData<ResponseData, ApiData>
|
||||
| FlatResponseFailData<ResponseData>;
|
||||
|
||||
export interface FlatRequestInstance<S = Record<string, unknown>, ResponseData = any> extends RequestInstanceCommon<S> {
|
||||
<T = any, R extends ResponseType = 'json'>(
|
||||
export interface FlatRequestInstance<ResponseData, ApiData, State extends Record<string, unknown>>
|
||||
extends RequestInstanceCommon<State> {
|
||||
<T extends ApiData = ApiData, R extends ResponseType = 'json'>(
|
||||
config: CustomAxiosRequestConfig<R>
|
||||
): Promise<FlatResponseData<MappedType<R, T>, ResponseData>>;
|
||||
): Promise<FlatResponseData<ResponseData, MappedType<R, T>>>;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -6,31 +6,31 @@ import type {
|
||||
CreateAxiosDefaults,
|
||||
CustomAxiosRequestConfig,
|
||||
MappedType,
|
||||
RequestInstanceCommon,
|
||||
RequestOption,
|
||||
ResponseType
|
||||
} from '@sa/axios';
|
||||
import useLoading from './use-loading';
|
||||
|
||||
export type HookRequestInstanceResponseSuccessData<T = any> = {
|
||||
data: Ref<T>;
|
||||
export type HookRequestInstanceResponseSuccessData<ApiData> = {
|
||||
data: Ref<ApiData>;
|
||||
error: Ref<null>;
|
||||
};
|
||||
|
||||
export type HookRequestInstanceResponseFailData<ResponseData = any> = {
|
||||
export type HookRequestInstanceResponseFailData<ResponseData> = {
|
||||
data: Ref<null>;
|
||||
error: Ref<AxiosError<ResponseData>>;
|
||||
};
|
||||
|
||||
export type HookRequestInstanceResponseData<T = any, ResponseData = any> = {
|
||||
export type HookRequestInstanceResponseData<ResponseData, ApiData> = {
|
||||
loading: Ref<boolean>;
|
||||
} & (HookRequestInstanceResponseSuccessData<T> | HookRequestInstanceResponseFailData<ResponseData>);
|
||||
} & (HookRequestInstanceResponseSuccessData<ApiData> | HookRequestInstanceResponseFailData<ResponseData>);
|
||||
|
||||
export interface HookRequestInstance<ResponseData = any> {
|
||||
<T = any, R extends ResponseType = 'json'>(
|
||||
export interface HookRequestInstance<ResponseData, ApiData, State extends Record<string, unknown>>
|
||||
extends RequestInstanceCommon<State> {
|
||||
<T extends ApiData = ApiData, R extends ResponseType = 'json'>(
|
||||
config: CustomAxiosRequestConfig
|
||||
): HookRequestInstanceResponseData<MappedType<R, T>, ResponseData>;
|
||||
cancelRequest: (requestId: string) => void;
|
||||
cancelAllRequest: () => void;
|
||||
): HookRequestInstanceResponseData<ResponseData, MappedType<R, T>>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,25 +39,26 @@ export interface HookRequestInstance<ResponseData = any> {
|
||||
* @param axiosConfig
|
||||
* @param options
|
||||
*/
|
||||
export default function createHookRequest<ResponseData = any>(
|
||||
export default function createHookRequest<ResponseData, ApiData, State extends Record<string, unknown>>(
|
||||
axiosConfig?: CreateAxiosDefaults,
|
||||
options?: Partial<RequestOption<ResponseData>>
|
||||
options?: Partial<RequestOption<ResponseData, ApiData, State>>
|
||||
) {
|
||||
const request = createFlatRequest<ResponseData>(axiosConfig, options);
|
||||
const request = createFlatRequest<ResponseData, ApiData, State>(axiosConfig, options);
|
||||
|
||||
const hookRequest: HookRequestInstance<ResponseData> = function hookRequest<T = any, R extends ResponseType = 'json'>(
|
||||
config: CustomAxiosRequestConfig
|
||||
) {
|
||||
const hookRequest: HookRequestInstance<ResponseData, ApiData, State> = function hookRequest<
|
||||
T extends ApiData = ApiData,
|
||||
R extends ResponseType = 'json'
|
||||
>(config: CustomAxiosRequestConfig) {
|
||||
const { loading, startLoading, endLoading } = useLoading();
|
||||
|
||||
const data = ref<MappedType<R, T> | null>(null) as Ref<MappedType<R, T>>;
|
||||
const error = ref<AxiosError<ResponseData> | null>(null) as Ref<AxiosError<ResponseData> | null>;
|
||||
const data = ref(null) as Ref<MappedType<R, T>>;
|
||||
const error = ref(null) as Ref<AxiosError<ResponseData> | null>;
|
||||
|
||||
startLoading();
|
||||
|
||||
request(config).then(res => {
|
||||
if (res.data) {
|
||||
data.value = res.data;
|
||||
data.value = res.data as MappedType<R, T>;
|
||||
} else {
|
||||
error.value = res.error;
|
||||
}
|
||||
@@ -70,9 +71,8 @@ export default function createHookRequest<ResponseData = any>(
|
||||
data,
|
||||
error
|
||||
};
|
||||
} as HookRequestInstance<ResponseData>;
|
||||
} as HookRequestInstance<ResponseData, ApiData, State>;
|
||||
|
||||
hookRequest.cancelRequest = request.cancelRequest;
|
||||
hookRequest.cancelAllRequest = request.cancelAllRequest;
|
||||
|
||||
return hookRequest;
|
||||
|
||||
@@ -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> = T | Promise<T>;
|
||||
|
||||
export type ApiFn = (args: any) => Promise<unknown>;
|
||||
|
||||
export type TableColumnCheck = {
|
||||
prop: string;
|
||||
label: string;
|
||||
checked: boolean;
|
||||
};
|
||||
|
||||
export type TableDataWithIndex<T> = T & { index: number };
|
||||
|
||||
export type TransformedData<T> = {
|
||||
data: TableDataWithIndex<T>[];
|
||||
export interface PaginationData<T> {
|
||||
data: T[];
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
type GetApiData<ApiData, Pagination extends boolean> = Pagination extends true ? PaginationData<ApiData> : ApiData[];
|
||||
|
||||
type Transform<ResponseData, ApiData, Pagination extends boolean> = (
|
||||
response: ResponseData
|
||||
) => GetApiData<ApiData, Pagination>;
|
||||
|
||||
export type TableColumnCheckTitle = string | ((...args: any) => VNodeChild);
|
||||
|
||||
export type TableColumnCheck = {
|
||||
prop: string;
|
||||
label: TableColumnCheckTitle;
|
||||
checked: boolean;
|
||||
visible: boolean;
|
||||
};
|
||||
|
||||
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[];
|
||||
export interface UseTableOptions<ResponseData, ApiData, Column, Pagination extends boolean> {
|
||||
/**
|
||||
* api function to get table data
|
||||
*/
|
||||
api: () => Promise<ResponseData>;
|
||||
/**
|
||||
* whether to enable pagination
|
||||
*/
|
||||
pagination?: Pagination;
|
||||
/**
|
||||
* transform api response to table data
|
||||
*/
|
||||
transform: Transform<ResponseData, ApiData, Pagination>;
|
||||
/**
|
||||
* 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<T>) => MaybePromise<void>;
|
||||
onFetched?: (data: GetApiData<ApiData, Pagination>) => void | Promise<void>;
|
||||
/**
|
||||
* whether to get data immediately
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
immediate?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<A, T, C>) {
|
||||
export default function useTable<ResponseData, ApiData, Column, Pagination extends boolean>(
|
||||
options: UseTableOptions<ResponseData, ApiData, Column, Pagination>
|
||||
) {
|
||||
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<Parameters<A>[0]> = reactive(jsonClone({ ...apiParams }));
|
||||
const data = ref([]) as Ref<ApiData[]>;
|
||||
|
||||
const allColumns = ref(config.columns()) as Ref<C[]>;
|
||||
const columnChecks = ref(getColumnChecks(columns())) as Ref<TableColumnCheck[]>;
|
||||
|
||||
const data: Ref<TableDataWithIndex<T>[]> = ref([]);
|
||||
|
||||
const columnChecks: Ref<TableColumnCheck[]> = 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<A extends ApiFn, T, C>(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<ReturnType<A>>);
|
||||
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<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, jsonClone(apiParams));
|
||||
await onFetched?.(transformed);
|
||||
} finally {
|
||||
endLoading();
|
||||
}
|
||||
}
|
||||
|
||||
if (immediate) {
|
||||
@@ -141,12 +113,20 @@ export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<
|
||||
loading,
|
||||
empty,
|
||||
data,
|
||||
columns,
|
||||
columns: $columns,
|
||||
columnChecks,
|
||||
reloadColumns,
|
||||
getData,
|
||||
searchParams,
|
||||
updateSearchParams,
|
||||
resetSearchParams
|
||||
getData
|
||||
};
|
||||
}
|
||||
|
||||
function getTableData<ApiData, Pagination extends boolean>(
|
||||
data: GetApiData<ApiData, Pagination>,
|
||||
pagination?: Pagination
|
||||
) {
|
||||
if (pagination) {
|
||||
return (data as PaginationData<ApiData>).data;
|
||||
}
|
||||
|
||||
return data as ApiData[];
|
||||
}
|
||||
|
||||
@@ -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<T> = {
|
||||
-readonly [key in keyof T]: T[key];
|
||||
};
|
||||
|
||||
type TableData = UI.TableData;
|
||||
type GetTableData<A extends UI.TableApiFn> = UI.GetTableData<A>;
|
||||
type TableColumn<T> = UI.TableColumn<T>;
|
||||
export type UseUITableOptions<ResponseData, ApiData, Pagination extends boolean> = Omit<
|
||||
UseTableOptions<ResponseData, ApiData, UI.TableColumn<ApiData>, Pagination>,
|
||||
'pagination' | 'getColumnChecks' | 'getColumns'
|
||||
> & {
|
||||
/**
|
||||
* get column visible
|
||||
*
|
||||
* @param column
|
||||
*
|
||||
* @default true
|
||||
*
|
||||
* @returns true if the column is visible, false otherwise
|
||||
*/
|
||||
getColumnVisible?: (column: UI.TableColumn<ApiData>) => boolean;
|
||||
};
|
||||
|
||||
export function useTable<A extends UI.TableApiFn>(config: UI.NaiveTableConfig<A>) {
|
||||
const SELECTION_KEY = '__selection__';
|
||||
|
||||
const EXPAND_KEY = '__expand__';
|
||||
|
||||
const INDEX_KEY = '__index__';
|
||||
|
||||
export function useUITable<ResponseData, ApiData>(options: UseUITableOptions<ResponseData, ApiData, false>) {
|
||||
const scope = effectScope();
|
||||
const appStore = useAppStore();
|
||||
|
||||
const result = useTable<ResponseData, ApiData, UI.TableColumn<ApiData>, false>({
|
||||
...options,
|
||||
getColumnChecks: cols => getColumnChecks(cols, options.getColumnVisible),
|
||||
getColumns
|
||||
});
|
||||
|
||||
// calculate the total width of the table this is used for horizontal scrolling
|
||||
const scrollX = computed(() => {
|
||||
return result.columns.value.reduce((acc, column) => {
|
||||
return acc + Number(column.width ?? column.minWidth ?? 120);
|
||||
}, 0);
|
||||
});
|
||||
|
||||
scope.run(() => {
|
||||
watch(
|
||||
() => appStore.locale,
|
||||
() => {
|
||||
result.reloadColumns();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
onScopeDispose(() => {
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
scrollX
|
||||
};
|
||||
}
|
||||
|
||||
type PaginationParams = Pick<PaginationProps, 'currentPage' | 'pageSize'>;
|
||||
|
||||
type UseUIPaginatedTableOptions<ResponseData, ApiData> = UseUITableOptions<ResponseData, ApiData, true> & {
|
||||
paginationProps?: Omit<PaginationProps, 'currentPage' | 'pageSize' | 'total'>;
|
||||
/**
|
||||
* whether to show the total count of the table
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
showTotal?: boolean;
|
||||
onPaginationParamsChange?: (params: PaginationParams) => void | Promise<void>;
|
||||
};
|
||||
|
||||
export function useUIPaginatedTable<ResponseData, ApiData>(options: UseUIPaginatedTableOptions<ResponseData, ApiData>) {
|
||||
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<A, GetTableData<A>, TableColumn<UI.TableDataWithIndex<GetTableData<A>>>>({
|
||||
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<string, TableColumn<GetTableData<A>>>();
|
||||
|
||||
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<GetTableData<A>>);
|
||||
|
||||
return filteredColumns;
|
||||
},
|
||||
onFetched: async transformed => {
|
||||
const { pageNum, pageSize, total } = transformed;
|
||||
|
||||
updatePagination({
|
||||
currentPage: pageNum,
|
||||
pageSize,
|
||||
total
|
||||
});
|
||||
},
|
||||
immediate
|
||||
});
|
||||
|
||||
const pagination: Partial<RemoveReadonly<PaginationProps & PaginationEmits>> = 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<A extends UI.TableApiFn>(config: UI.NaiveTableConfig<A>
|
||||
return p;
|
||||
});
|
||||
|
||||
function updatePagination(update: Partial<PaginationProps>) {
|
||||
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<ResponseData, ApiData, UI.TableColumn<ApiData>, 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<A extends UI.TableApiFn>(config: UI.NaiveTableConfig<A>
|
||||
});
|
||||
|
||||
return {
|
||||
loading,
|
||||
empty,
|
||||
data,
|
||||
columns,
|
||||
columnChecks,
|
||||
reloadColumns,
|
||||
pagination,
|
||||
mobilePagination,
|
||||
updatePagination,
|
||||
getData,
|
||||
...result,
|
||||
getDataByPage,
|
||||
searchParams,
|
||||
updateSearchParams,
|
||||
resetSearchParams
|
||||
pagination,
|
||||
mobilePagination
|
||||
};
|
||||
}
|
||||
|
||||
export function useTableOperate<T extends TableData = TableData>(data: Ref<T[]>, getData: () => Promise<void>) {
|
||||
export function useTableOperate<TableData>(
|
||||
data: Ref<TableData[]>,
|
||||
idKey: keyof TableData,
|
||||
getData: () => Promise<void>
|
||||
) {
|
||||
const { bool: drawerVisible, setTrue: openDrawer, setFalse: closeDrawer } = useBoolean();
|
||||
|
||||
const operateType = ref<UI.TableOperateType>('add');
|
||||
const operateType = shallowRef<UI.TableOperateType>('add');
|
||||
|
||||
function handleAdd() {
|
||||
operateType.value = 'add';
|
||||
@@ -226,18 +189,18 @@ export function useTableOperate<T extends TableData = TableData>(data: Ref<T[]>,
|
||||
}
|
||||
|
||||
/** the editing row data */
|
||||
const editingData: Ref<T | null> = ref(null);
|
||||
const editingData = shallowRef<TableData | null>(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<string[]>([]);
|
||||
const checkedRowKeys = shallowRef<string[]>([]);
|
||||
|
||||
/** the hook after the batch delete operation is completed */
|
||||
async function onBatchDeleted() {
|
||||
@@ -268,3 +231,88 @@ export function useTableOperate<T extends TableData = TableData>(data: Ref<T[]>,
|
||||
onDeleted
|
||||
};
|
||||
}
|
||||
|
||||
export function defaultTransform<ApiData>(
|
||||
response: FlatResponseData<any, Api.Common.PaginatingQueryRecord<ApiData>>
|
||||
): PaginationData<ApiData> {
|
||||
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<Column extends UI.TableColumn<any>>(
|
||||
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<Column extends UI.TableColumn<any>>(cols: Column[], checks: TableColumnCheck[]) {
|
||||
const columnMap = new Map<string, Column>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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<App.Service.Response, RequestInstanceState>(
|
||||
export const request = createFlatRequest(
|
||||
{
|
||||
baseURL,
|
||||
headers: {
|
||||
@@ -18,6 +18,13 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
|
||||
}
|
||||
},
|
||||
{
|
||||
defaultState: {
|
||||
errMsgStack: [],
|
||||
refreshTokenPromise: null
|
||||
} as RequestInstanceState,
|
||||
transform(response: AxiosResponse<App.Service.Response<any>>) {
|
||||
return response.data.data;
|
||||
},
|
||||
async onRequest(config) {
|
||||
const Authorization = getAuthorization();
|
||||
Object.assign(config.headers, { Authorization });
|
||||
@@ -70,19 +77,6 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
|
||||
.then(() => {
|
||||
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<App.Service.Response, RequestInstanceSt
|
||||
|
||||
return null;
|
||||
},
|
||||
transformBackendResponse(response) {
|
||||
return response.data.data;
|
||||
},
|
||||
onError(error) {
|
||||
// when the request is fail, you can show error message
|
||||
|
||||
@@ -134,11 +125,14 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
|
||||
}
|
||||
);
|
||||
|
||||
export const demoRequest = createRequest<App.Service.DemoResponse>(
|
||||
export const demoRequest = createRequest(
|
||||
{
|
||||
baseURL: otherBaseURL.demo
|
||||
},
|
||||
{
|
||||
transform(response: AxiosResponse<App.Service.DemoResponse>) {
|
||||
return response.data.result;
|
||||
},
|
||||
async onRequest(config) {
|
||||
const { headers } = config;
|
||||
|
||||
@@ -158,9 +152,6 @@ export const demoRequest = createRequest<App.Service.DemoResponse>(
|
||||
// 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
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export interface RequestInstanceState {
|
||||
/** whether the request is refreshing token */
|
||||
refreshTokenFn: Promise<boolean> | null;
|
||||
/** the promise of refreshing token */
|
||||
refreshTokenPromise: Promise<boolean> | null;
|
||||
/** the request error message stack */
|
||||
errMsgStack: string[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
20
src/typings/api/auth.d.ts
vendored
Normal file
20
src/typings/api/auth.d.ts
vendored
Normal file
@@ -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[];
|
||||
}
|
||||
}
|
||||
}
|
||||
50
src/typings/api/common.d.ts
vendored
Normal file
50
src/typings/api/common.d.ts
vendored
Normal file
@@ -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<T = any> extends PaginatingCommonParams {
|
||||
records: T[];
|
||||
}
|
||||
|
||||
/** common search params of table */
|
||||
type CommonSearchParams = Pick<Common.PaginatingCommonParams, 'current' | 'size'>;
|
||||
|
||||
/**
|
||||
* enable status
|
||||
*
|
||||
* - "1": enabled
|
||||
* - "2": disabled
|
||||
*/
|
||||
type EnableStatus = '1' | '2';
|
||||
|
||||
/** common record */
|
||||
type CommonRecord<T = any> = {
|
||||
/** 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;
|
||||
}
|
||||
}
|
||||
19
src/typings/api/route.d.ts
vendored
Normal file
19
src/typings/api/route.d.ts
vendored
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<T = any> extends PaginatingCommonParams {
|
||||
records: T[];
|
||||
}
|
||||
|
||||
/** common search params of table */
|
||||
type CommonSearchParams = Pick<Common.PaginatingCommonParams, 'current' | 'size'>;
|
||||
|
||||
/**
|
||||
* enable status
|
||||
*
|
||||
* - "1": enabled
|
||||
* - "2": disabled
|
||||
*/
|
||||
type EnableStatus = '1' | '2';
|
||||
|
||||
/** common record */
|
||||
type CommonRecord<T = any> = {
|
||||
/** 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
|
||||
*
|
||||
37
src/typings/ui.d.ts
vendored
37
src/typings/ui.d.ts
vendored
@@ -1,28 +1,15 @@
|
||||
declare namespace UI {
|
||||
type ThemeColor = 'danger' | 'primary' | 'info' | 'success' | 'warning';
|
||||
|
||||
type DataTableBaseColumn<T> = Partial<import('element-plus').TableColumnCtx<T extends object ? T : never>>;
|
||||
|
||||
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, 'prop'> & { prop?: keyof T | (string & {}) };
|
||||
|
||||
type SetTableColumnKey<C, T> = Omit<C, 'key'> & { key: keyof T | `CustomColumnKey` };
|
||||
type TableColumnWithKey<T> = SetTableColumnKey<DataTableBaseColumn<T>, T>;
|
||||
|
||||
type TableData = Api.Common.CommonRecord<object>;
|
||||
|
||||
type TableColumnWithKey<T> = Partial<import('element-plus').TableColumnCtx<T>>;
|
||||
|
||||
type TableColumn<T> = TableColumnWithKey<T>;
|
||||
|
||||
type TableApiFn<T = any, R = Api.Common.CommonSearchParams> = (
|
||||
params: R
|
||||
) => Promise<FlatResponseData<Api.Common.PaginatingQueryRecord<T>>>;
|
||||
type TableColumn<T> = DataTableBaseColumn<T>;
|
||||
|
||||
/**
|
||||
* 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> = A extends TableApiFn<infer T> ? T : never;
|
||||
|
||||
type NaiveTableConfig<A extends TableApiFn> = Pick<
|
||||
import('@sa/hooks').TableConfig<A, GetTableData<A>, TableColumn<TableDataWithIndex<GetTableData<A>>>>,
|
||||
'apiFn' | 'apiParams' | 'columns' | 'immediate'
|
||||
> & {
|
||||
/**
|
||||
* whether to display the total items count
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
showTotal?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// ======================================== element-plus ========================================
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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<object>;
|
||||
interface Operations<T> {
|
||||
delete?: (row: T) => Promise<void>;
|
||||
batchDelete?: (rows: T[]) => Promise<void>;
|
||||
|
||||
@@ -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<HTMLElement | null>(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<OperateType>('add');
|
||||
|
||||
|
||||
@@ -1,34 +1,31 @@
|
||||
<script setup lang="tsx">
|
||||
import { reactive } from 'vue';
|
||||
import { ElButton, ElPopconfirm, ElTag } from 'element-plus';
|
||||
import { enableStatusRecord } from '@/constants/business';
|
||||
import { fetchGetRoleList } from '@/service/api';
|
||||
import { useTable, useTableOperate } from '@/hooks/common/table';
|
||||
import { defaultTransform, useTableOperate, useUIPaginatedTable } from '@/hooks/common/table';
|
||||
import { $t } from '@/locales';
|
||||
import RoleOperateDrawer from './modules/role-operate-drawer.vue';
|
||||
import RoleSearch from './modules/role-search.vue';
|
||||
|
||||
const {
|
||||
columns,
|
||||
columnChecks,
|
||||
data,
|
||||
loading,
|
||||
getData,
|
||||
getDataByPage,
|
||||
mobilePagination,
|
||||
searchParams,
|
||||
resetSearchParams
|
||||
} = useTable({
|
||||
apiFn: fetchGetRoleList,
|
||||
apiParams: {
|
||||
current: 1,
|
||||
size: 10,
|
||||
status: undefined,
|
||||
roleName: undefined,
|
||||
roleCode: undefined
|
||||
const searchParams: Api.SystemManage.RoleSearchParams = reactive({
|
||||
current: 1,
|
||||
size: 10,
|
||||
status: undefined,
|
||||
roleName: undefined,
|
||||
roleCode: undefined
|
||||
});
|
||||
|
||||
const { columns, columnChecks, data, loading, getData, getDataByPage, mobilePagination } = useUIPaginatedTable({
|
||||
api: () => fetchGetRoleList(searchParams),
|
||||
transform: response => defaultTransform(response),
|
||||
onPaginationParamsChange: params => {
|
||||
searchParams.current = params.currentPage;
|
||||
searchParams.size = params.pageSize;
|
||||
},
|
||||
columns: () => [
|
||||
{ type: 'selection', width: 48 },
|
||||
{ prop: 'index', label: $t('common.index'), width: 64 },
|
||||
{ type: 'index', label: $t('common.index'), width: 64 },
|
||||
{ prop: 'roleName', label: $t('page.manage.role.roleName'), minWidth: 120 },
|
||||
{ prop: 'roleCode', label: $t('page.manage.role.roleCode'), minWidth: 120 },
|
||||
{ prop: 'roleDesc', label: $t('page.manage.role.roleDesc'), minWidth: 120 },
|
||||
@@ -85,7 +82,7 @@ const {
|
||||
onBatchDeleted,
|
||||
onDeleted
|
||||
// closeDrawer
|
||||
} = useTableOperate(data, getData);
|
||||
} = useTableOperate(data, 'id', getData);
|
||||
|
||||
async function handleBatchDelete() {
|
||||
// eslint-disable-next-line no-console
|
||||
@@ -104,6 +101,14 @@ function handleDelete(id: number) {
|
||||
onDeleted();
|
||||
}
|
||||
|
||||
function resetSearchParams() {
|
||||
searchParams.current = 1;
|
||||
searchParams.size = 10;
|
||||
searchParams.status = undefined;
|
||||
searchParams.roleName = undefined;
|
||||
searchParams.roleCode = undefined;
|
||||
}
|
||||
|
||||
function edit(id: number) {
|
||||
handleEdit(id);
|
||||
}
|
||||
|
||||
@@ -1,40 +1,36 @@
|
||||
<script setup lang="tsx">
|
||||
import { reactive } from 'vue';
|
||||
import { ElButton, ElPopconfirm, ElTag } from 'element-plus';
|
||||
import { enableStatusRecord, userGenderRecord } from '@/constants/business';
|
||||
import { fetchGetUserList } from '@/service/api';
|
||||
import { useTable, useTableOperate } from '@/hooks/common/table';
|
||||
import { defaultTransform, useTableOperate, useUIPaginatedTable } from '@/hooks/common/table';
|
||||
import { $t } from '@/locales';
|
||||
import UserOperateDrawer from './modules/user-operate-drawer.vue';
|
||||
import UserSearch from './modules/user-search.vue';
|
||||
|
||||
defineOptions({ name: 'UserManage' });
|
||||
|
||||
const {
|
||||
columns,
|
||||
columnChecks,
|
||||
data,
|
||||
getData,
|
||||
getDataByPage,
|
||||
loading,
|
||||
mobilePagination,
|
||||
searchParams,
|
||||
resetSearchParams
|
||||
} = useTable({
|
||||
apiFn: fetchGetUserList,
|
||||
showTotal: true,
|
||||
apiParams: {
|
||||
current: 1,
|
||||
size: 10,
|
||||
status: undefined,
|
||||
userName: undefined,
|
||||
userGender: undefined,
|
||||
nickName: undefined,
|
||||
userPhone: undefined,
|
||||
userEmail: undefined
|
||||
const searchParams: Api.SystemManage.UserSearchParams = reactive({
|
||||
current: 1,
|
||||
size: 10,
|
||||
status: undefined,
|
||||
userName: undefined,
|
||||
userGender: undefined,
|
||||
nickName: undefined,
|
||||
userPhone: undefined,
|
||||
userEmail: undefined
|
||||
});
|
||||
|
||||
const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagination } = useUIPaginatedTable({
|
||||
api: () => fetchGetUserList(searchParams),
|
||||
transform: response => defaultTransform(response),
|
||||
onPaginationParamsChange: params => {
|
||||
searchParams.current = params.currentPage;
|
||||
searchParams.size = params.pageSize;
|
||||
},
|
||||
columns: () => [
|
||||
{ type: 'selection', width: 48 },
|
||||
{ prop: 'index', label: $t('common.index'), width: 64 },
|
||||
{ type: 'index', label: $t('common.index'), width: 64 },
|
||||
{ prop: 'userName', label: $t('page.manage.user.userName'), minWidth: 100 },
|
||||
{
|
||||
prop: 'userGender',
|
||||
@@ -111,7 +107,7 @@ const {
|
||||
onBatchDeleted,
|
||||
onDeleted
|
||||
// closeDrawer
|
||||
} = useTableOperate(data, getData);
|
||||
} = useTableOperate(data, 'id', getData);
|
||||
|
||||
async function handleBatchDelete() {
|
||||
// eslint-disable-next-line no-console
|
||||
@@ -129,6 +125,17 @@ function handleDelete(id: number) {
|
||||
onDeleted();
|
||||
}
|
||||
|
||||
function resetSearchParams() {
|
||||
searchParams.current = 1;
|
||||
searchParams.size = 10;
|
||||
searchParams.status = undefined;
|
||||
searchParams.userName = undefined;
|
||||
searchParams.userGender = undefined;
|
||||
searchParams.nickName = undefined;
|
||||
searchParams.userPhone = undefined;
|
||||
searchParams.userEmail = undefined;
|
||||
}
|
||||
|
||||
function edit(id: number) {
|
||||
handleEdit(id);
|
||||
}
|
||||
|
||||
@@ -1,29 +1,35 @@
|
||||
<script setup lang="tsx">
|
||||
import { reactive } from 'vue';
|
||||
import { ElButton, ElTag } from 'element-plus';
|
||||
import { utils, writeFile } from 'xlsx';
|
||||
import { enableStatusRecord, userGenderRecord } from '@/constants/business';
|
||||
import { fetchGetUserList } from '@/service/api';
|
||||
import { useTable } from '@/hooks/common/table';
|
||||
import { defaultTransform, useUIPaginatedTable } from '@/hooks/common/table';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({ name: 'ExcelPage' });
|
||||
|
||||
const { columns, data, loading } = useTable({
|
||||
apiFn: fetchGetUserList,
|
||||
showTotal: true,
|
||||
apiParams: {
|
||||
current: 1,
|
||||
size: 999,
|
||||
status: undefined,
|
||||
userName: undefined,
|
||||
userGender: undefined,
|
||||
nickName: undefined,
|
||||
userPhone: undefined,
|
||||
userEmail: undefined
|
||||
const searchParams: Api.SystemManage.UserSearchParams = reactive({
|
||||
current: 1,
|
||||
size: 10,
|
||||
status: undefined,
|
||||
userName: undefined,
|
||||
userGender: undefined,
|
||||
nickName: undefined,
|
||||
userPhone: undefined,
|
||||
userEmail: undefined
|
||||
});
|
||||
|
||||
const { columns, data, loading } = useUIPaginatedTable({
|
||||
api: () => fetchGetUserList(searchParams),
|
||||
transform: response => defaultTransform(response),
|
||||
onPaginationParamsChange: params => {
|
||||
searchParams.current = params.currentPage;
|
||||
searchParams.size = params.pageSize;
|
||||
},
|
||||
columns: () => [
|
||||
{ type: 'selection', width: 48 },
|
||||
{ prop: 'index', label: $t('common.index'), width: 64 },
|
||||
{ type: 'index', label: $t('common.index'), width: 64 },
|
||||
{ prop: 'userName', label: $t('page.manage.user.userName'), minWidth: 100 },
|
||||
{
|
||||
prop: 'userGender',
|
||||
@@ -91,10 +97,7 @@ function exportExcel() {
|
||||
writeFile(workBook, '用户数据.xlsx');
|
||||
}
|
||||
|
||||
function getTableValue(
|
||||
col: UI.TableColumn<UI.TableDataWithIndex<Api.SystemManage.User>>,
|
||||
item: UI.TableDataWithIndex<Api.SystemManage.User>
|
||||
) {
|
||||
function getTableValue(col: UI.TableColumn<Api.SystemManage.User>, item: Api.SystemManage.User) {
|
||||
if (!isTableColumnHasKey(col)) {
|
||||
return '';
|
||||
}
|
||||
@@ -118,19 +121,17 @@ function getTableValue(
|
||||
}
|
||||
|
||||
if (prop in item) {
|
||||
return item[prop as keyof UI.TableDataWithIndex<Api.SystemManage.User>];
|
||||
return item[prop as keyof Api.SystemManage.User];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function isTableColumnHasKey<T>(column: UI.TableColumn<T>): column is UI.TableColumnWithKey<T> {
|
||||
function isTableColumnHasKey<T>(column: UI.TableColumn<T>): boolean {
|
||||
return Boolean((column as UI.TableColumnWithKey<T>).prop);
|
||||
}
|
||||
|
||||
function isTableColumnHasTitle<T>(column: UI.TableColumn<T>): column is UI.TableColumnWithKey<T> & {
|
||||
label: string;
|
||||
} {
|
||||
function isTableColumnHasTitle<T>(column: UI.TableColumn<T>): boolean {
|
||||
return Boolean((column as UI.TableColumnWithKey<T>).label);
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user