Compare commits

..

13 Commits

47 changed files with 1158 additions and 1876 deletions

View File

@ -85,8 +85,8 @@
"vditor": "3.11.1",
"vue": "3.5.17",
"vue-draggable-plus": "0.6.0",
"vue-i18n": "11.1.9",
"vue-pdf-embed": "2.1.2",
"vue-i18n": "11.1.10",
"vue-pdf-embed": "2.1.3",
"vue-router": "4.5.1",
"wangeditor": "4.7.15",
"xgplayer": "3.0.22",
@ -95,12 +95,12 @@
"devDependencies": {
"@amap/amap-jsapi-types": "0.0.15",
"@elegant-router/vue": "0.3.8",
"@iconify/json": "2.2.357",
"@iconify/json": "2.2.359",
"@sa/scripts": "workspace:*",
"@sa/uno-preset": "workspace:*",
"@soybeanjs/eslint-config": "1.7.1",
"@types/bmapgl": "0.0.7",
"@types/node": "24.0.13",
"@types/node": "24.0.15",
"@types/nprogress": "0.2.3",
"@unocss/eslint-config": "66.3.3",
"@unocss/preset-icons": "66.3.3",
@ -122,12 +122,12 @@
"typescript": "5.8.3",
"unplugin-icons": "22.1.0",
"unplugin-vue-components": "28.8.0",
"vite": "7.0.4",
"vite": "7.0.5",
"vite-plugin-progress": "0.0.7",
"vite-plugin-svg-icons": "2.0.1",
"vite-plugin-vue-devtools": "7.7.7",
"vue-eslint-parser": "10.2.0",
"vue-tsc": "3.0.1"
"vue-tsc": "3.0.3"
},
"simple-git-hooks": {
"commit-msg": "pnpm sa git-commit-verify",

View File

@ -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;
}

View File

@ -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;

View File

@ -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>>>;
}

View File

@ -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';

View File

@ -1,5 +1,4 @@
import { inject, provide } from 'vue';
import type { InjectionKey } from 'vue';
/**
* Use context
@ -12,7 +11,7 @@ import type { InjectionKey } from 'vue';
* import { ref } from 'vue';
* import { useContext } from '@sa/hooks';
*
* export const { setupStore, useStore } = useContext('demo', () => {
* export const [provideDemoContext, useDemoContext] = useContext('demo', () => {
* const count = ref(0);
*
* function increment() {
@ -35,10 +34,10 @@ import type { InjectionKey } from 'vue';
* <div>A</div>
* </template>
* <script setup lang="ts">
* import { setupStore } from './context';
* import { provideDemoContext } from './context';
*
* setupStore();
* // const { increment } = setupStore(); // also can control the store in the parent component
* provideDemoContext();
* // const { increment } = provideDemoContext(); // also can control the store in the parent component
* </script>
* ``` // B.vue
* ```vue
@ -46,9 +45,9 @@ import type { InjectionKey } from 'vue';
* <div>B</div>
* </template>
* <script setup lang="ts">
* import { useStore } from './context';
* import { useDemoContext } from './context';
*
* const { count, increment } = useStore();
* const { count, increment } = useDemoContext();
* </script>
* ```;
*
@ -57,40 +56,41 @@ import type { InjectionKey } from 'vue';
* @param contextName Context name
* @param fn Context function
*/
export default function useContext<T extends (...args: any[]) => any>(contextName: string, fn: T) {
type Context = ReturnType<T>;
export default function useContext<Arguments extends Array<any>, T>(
contextName: string,
composable: (...args: Arguments) => T
) {
const key = Symbol(contextName);
const { useProvide, useInject: useStore } = createContext<Context>(contextName);
/**
* Injects the context value.
*
* @param consumerName - The name of the component that is consuming the context. If provided, the component must be
* used within the context provider.
* @param defaultValue - The default value to return if the context is not provided.
* @returns The context value.
*/
const useInject = <N extends string | null | undefined = undefined>(
consumerName?: N,
defaultValue?: T
): N extends null | undefined ? T | null : T => {
const value = inject(key, defaultValue);
function setupStore(...args: Parameters<T>) {
const context: Context = fn(...args);
return useProvide(context);
}
if (consumerName && !value) {
throw new Error(`\`${consumerName}\` must be used within \`${contextName}\``);
}
return {
/** Setup store in the parent component */
setupStore,
/** Use store in the child component */
useStore
// @ts-expect-error - we want to return null if the value is undefined or null
return value || null;
};
}
/** Create context */
function createContext<T>(contextName: string) {
const injectKey: InjectionKey<T> = Symbol(contextName);
const useProvide = (...args: Arguments) => {
const value = composable(...args);
function useProvide(context: T) {
provide(injectKey, context);
provide(key, value);
return context;
}
function useInject() {
return inject(injectKey) as T;
}
return {
useProvide,
useInject
return value;
};
return [useProvide, useInject] as const;
}

View File

@ -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;

View File

@ -1,12 +1,20 @@
import { computed, reactive, ref } from 'vue';
import { computed, ref } from 'vue';
import type { Ref, VNodeChild } from 'vue';
import { jsonClone } from '@sa/utils';
import useBoolean from './use-boolean';
import useLoading from './use-loading';
export type MaybePromise<T> = T | Promise<T>;
export interface PaginationData<T> {
data: T[];
pageNum: number;
pageSize: number;
total: number;
}
export type ApiFn = (args: any) => Promise<unknown>;
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);
@ -14,72 +22,64 @@ export type TableColumnCheck = {
key: string;
title: TableColumnCheckTitle;
checked: boolean;
visible: boolean;
};
export type TableDataWithIndex<T> = T & { index: number };
export type TransformedData<T> = {
data: TableDataWithIndex<T>[];
pageNum: number;
pageSize: number;
total: number;
};
export type Transformer<T, Response> = (response: Response) => TransformedData<T>;
export type TableConfig<A extends ApiFn, T, C> = {
/** api function to get table data */
apiFn: A;
/** api params */
apiParams?: Parameters<A>[0];
/** transform api response to table data */
transformer: Transformer<T, Awaited<ReturnType<A>>>;
/** columns factory */
columns: () => C[];
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 allColumns = ref(columns()) as Ref<Column[]>;
const data: Ref<TableDataWithIndex<T>[]> = ref([]);
const columnChecks = ref(getColumnChecks(columns())) as Ref<TableColumnCheck[]>;
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();
allColumns.value = columns();
const checkMap = new Map(columnChecks.value.map(col => [col.key, col.checked]));
@ -92,47 +92,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) {
@ -143,12 +117,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[];
}

View File

@ -1,15 +0,0 @@
{
"name": "@sa/fetch",
"version": "1.3.15",
"exports": {
".": "./src/index.ts"
},
"typesVersions": {
"*": {
"*": ["./src/*"]
}
},
"dependencies": {
"ofetch": "1.4.1"
}
}

View File

@ -1,10 +0,0 @@
import { ofetch } from 'ofetch';
import type { FetchOptions } from 'ofetch';
export function createRequest(options: FetchOptions) {
const request = ofetch.create(options);
return request;
}
export default createRequest;

View File

@ -1,20 +0,0 @@
{
"compilerOptions": {
"target": "ESNext",
"jsx": "preserve",
"lib": ["DOM", "ESNext"],
"baseUrl": ".",
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"types": ["node"],
"strict": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@ -15,7 +15,7 @@
"devDependencies": {
"@soybeanjs/changelog": "0.3.24",
"bumpp": "10.2.0",
"c12": "3.0.4",
"c12": "3.1.0",
"cac": "6.7.14",
"consola": "3.4.2",
"enquirer": "2.4.1",

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
import { computed, effectScope, nextTick, onScopeDispose, ref, watch } from 'vue';
import { computed, effectScope, nextTick, onScopeDispose, shallowRef, watch } from 'vue';
import { useElementSize } from '@vueuse/core';
import * as echarts from 'echarts/core';
import { BarChart, GaugeChart, LineChart, PictorialBarChart, PieChart, RadarChart, ScatterChart } from 'echarts/charts';
@ -86,11 +86,11 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
const themeStore = useThemeStore();
const darkMode = computed(() => themeStore.darkMode);
const domRef = ref<HTMLElement | null>(null);
const domRef = shallowRef<HTMLElement | null>(null);
const initialSize = { width: 0, height: 0 };
const { width, height } = useElementSize(domRef, initialSize);
let chart: echarts.ECharts | null = null;
const chart = shallowRef<echarts.ECharts | null>(null);
const chartOptions: T = optionsFactory();
const {
@ -111,18 +111,9 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
onDestroy
} = hooks;
/**
* whether can render chart
*
* when domRef is ready and initialSize is valid
*/
function canRender() {
return domRef.value && initialSize.width > 0 && initialSize.height > 0;
}
/** is chart rendered */
function isRendered() {
return Boolean(domRef.value && chart);
return Boolean(domRef.value && chart.value);
}
/**
@ -131,59 +122,59 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
* @param callback callback function
*/
async function updateOptions(callback: (opts: T, optsFactory: () => T) => ECOption = () => chartOptions) {
if (!isRendered()) return;
const updatedOpts = callback(chartOptions, optionsFactory);
Object.assign(chartOptions, updatedOpts);
await nextTick();
if (!isRendered()) return;
if (isRendered()) {
chart?.clear();
chart.value?.clear();
}
chart?.setOption({ ...updatedOpts, backgroundColor: 'transparent' });
chart.value?.setOption({ ...updatedOpts, backgroundColor: 'transparent' });
await onUpdated?.(chart!);
await onUpdated?.(chart.value!);
}
function setOptions(options: T) {
chart?.setOption(options);
chart.value?.setOption(options);
}
/** render chart */
async function render() {
if (!isRendered()) {
const chartTheme = darkMode.value ? 'dark' : 'light';
if (isRendered()) return;
await nextTick();
const chartTheme = darkMode.value ? 'dark' : 'light';
chart = echarts.init(domRef.value, chartTheme);
chart.value = echarts.init(domRef.value, chartTheme);
chart.setOption({ ...chartOptions, backgroundColor: 'transparent' });
chart.value?.setOption({ ...chartOptions, backgroundColor: 'transparent' });
await onRender?.(chart);
}
await onRender?.(chart.value!);
}
/** resize chart */
function resize() {
chart?.resize();
chart.value?.resize();
}
/** destroy chart */
async function destroy() {
if (!chart) return;
if (!chart.value) return;
await onDestroy?.(chart);
chart?.dispose();
chart = null;
await onDestroy?.(chart.value);
chart.value?.dispose();
chart.value = null;
}
/** change chart theme */
async function changeTheme() {
await destroy();
await render();
await onUpdated?.(chart!);
await onUpdated?.(chart.value!);
}
/**
@ -196,30 +187,29 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
initialSize.width = w;
initialSize.height = h;
// size is abnormal, destroy chart
if (!canRender()) {
await destroy();
return;
}
// resize chart
if (isRendered()) {
resize();
return;
}
// render chart
await render();
if (chart) {
await onUpdated?.(chart);
if (chart.value) {
await onUpdated?.(chart.value);
}
}
scope.run(() => {
watch([width, height], ([newWidth, newHeight]) => {
renderChartBySize(newWidth, newHeight);
});
watch(
[width, height],
([newWidth, newHeight]) => {
renderChartBySize(newWidth, newHeight);
},
{ flush: 'post' }
);
watch(darkMode, () => {
changeTheme();
@ -233,6 +223,7 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
return {
domRef,
chart,
updateOptions,
setOptions
};

View File

@ -1,192 +1,48 @@
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 { PaginationProps } from 'naive-ui';
import { useBoolean, useTable } from '@sa/hooks';
import type { PaginationData, TableColumnCheck, UseTableOptions } from '@sa/hooks';
import type { FlatResponseData } from '@sa/axios';
import { jsonClone } from '@sa/utils';
import { useBoolean, useHookTable } from '@sa/hooks';
import { useAppStore } from '@/store/modules/app';
import { $t } from '@/locales';
type TableData = NaiveUI.TableData;
type GetTableData<A extends NaiveUI.TableApiFn> = NaiveUI.GetTableData<A>;
type TableColumn<T> = NaiveUI.TableColumn<T>;
export type UseNaiveTableOptions<ResponseData, ApiData, Pagination extends boolean> = Omit<
UseTableOptions<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, Pagination>,
'pagination' | 'getColumnChecks' | 'getColumns'
> & {
/**
* get column visible
*
* @param column
*
* @default true
*
* @returns true if the column is visible, false otherwise
*/
getColumnVisible?: (column: NaiveUI.TableColumn<ApiData>) => boolean;
};
export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTableConfig<A>) {
const SELECTION_KEY = '__selection__';
const EXPAND_KEY = '__expand__';
export function useNaiveTable<ResponseData, ApiData>(options: UseNaiveTableOptions<ResponseData, ApiData, false>) {
const scope = effectScope();
const appStore = useAppStore();
const isMobile = computed(() => appStore.isMobile);
const { apiFn, apiParams, immediate, showTotal } = config;
const SELECTION_KEY = '__selection__';
const EXPAND_KEY = '__expand__';
const {
loading,
empty,
data,
columns,
columnChecks,
reloadColumns,
getData,
searchParams,
updateSearchParams,
resetSearchParams
} = useHookTable<A, GetTableData<A>, TableColumn<NaiveUI.TableDataWithIndex<GetTableData<A>>>>({
apiFn,
apiParams,
columns: config.columns,
transformer: res => {
const { records = [], current = 1, size = 10, total = 0 } = res.data || {};
// 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: NaiveUI.TableColumnCheck[] = [];
cols.forEach(column => {
if (isTableColumnHasKey(column)) {
checks.push({
key: column.key as string,
title: column.title!,
checked: true
});
} else if (column.type === 'selection') {
checks.push({
key: SELECTION_KEY,
title: $t('common.check'),
checked: true
});
} else if (column.type === 'expand') {
checks.push({
key: EXPAND_KEY,
title: $t('common.expandColumn'),
checked: true
});
}
});
return checks;
},
getColumns: (cols, checks) => {
const columnMap = new Map<string, TableColumn<GetTableData<A>>>();
cols.forEach(column => {
if (isTableColumnHasKey(column)) {
columnMap.set(column.key as string, column);
} else if (column.type === 'selection') {
columnMap.set(SELECTION_KEY, column);
} else if (column.type === 'expand') {
columnMap.set(EXPAND_KEY, column);
}
});
const filteredColumns = checks
.filter(item => item.checked)
.map(check => columnMap.get(check.key) as TableColumn<GetTableData<A>>);
return filteredColumns;
},
onFetched: async transformed => {
const { pageNum, pageSize, total } = transformed;
updatePagination({
page: pageNum,
pageSize,
itemCount: total
});
},
immediate
const result = useTable<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, false>({
...options,
getColumnChecks: cols => getColumnChecks(cols, options.getColumnVisible),
getColumns
});
const pagination: PaginationProps = reactive({
page: 1,
pageSize: 10,
showSizePicker: true,
itemCount: 0,
pageSizes: [10, 15, 20, 25, 30],
onUpdatePage: async (page: number) => {
pagination.page = page;
updateSearchParams({
current: page,
size: pagination.pageSize!
});
getData();
},
onUpdatePageSize: async (pageSize: number) => {
pagination.pageSize = pageSize;
pagination.page = 1;
updateSearchParams({
current: pagination.page,
size: pageSize
});
getData();
},
...(showTotal
? {
prefix: page => $t('datatable.itemCount', { total: page.itemCount })
}
: {})
});
// this is for mobile, if the system does not support mobile, you can use `pagination` directly
const mobilePagination = computed(() => {
const p: PaginationProps = {
...pagination,
pageSlot: isMobile.value ? 3 : 9,
prefix: !isMobile.value && showTotal ? pagination.prefix : undefined
};
return p;
});
function updatePagination(update: Partial<PaginationProps>) {
Object.assign(pagination, update);
}
/**
* get data by page number
*
* @param pageNum the page number. default is 1
*/
async function getDataByPage(pageNum: number = 1) {
updatePagination({
page: pageNum
});
updateSearchParams({
current: pageNum,
size: pagination.pageSize!
});
await getData();
}
scope.run(() => {
watch(
() => appStore.locale,
() => {
reloadColumns();
result.reloadColumns();
}
);
});
@ -195,28 +51,124 @@ export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTabl
scope.stop();
});
return result;
}
type PaginationParams = Pick<PaginationProps, 'page' | 'pageSize'>;
type UseNaivePaginatedTableOptions<ResponseData, ApiData> = UseNaiveTableOptions<ResponseData, ApiData, true> & {
paginationProps?: Omit<PaginationProps, 'page' | 'pageSize' | 'itemCount'>;
/**
* whether to show the total count of the table
*
* @default true
*/
showTotal?: boolean;
onPaginationParamsChange?: (params: PaginationParams) => void | Promise<void>;
};
export function useNaivePaginatedTable<ResponseData, ApiData>(
options: UseNaivePaginatedTableOptions<ResponseData, ApiData>
) {
const scope = effectScope();
const appStore = useAppStore();
const isMobile = computed(() => appStore.isMobile);
const showTotal = computed(() => options.showTotal ?? true);
const pagination = reactive({
page: 1,
pageSize: 10,
itemCount: 0,
showSizePicker: true,
pageSizes: [10, 15, 20, 25, 30],
prefix: showTotal.value ? page => $t('datatable.itemCount', { total: page.itemCount }) : undefined,
onUpdatePage(page) {
pagination.page = page;
},
onUpdatePageSize(pageSize) {
pagination.pageSize = pageSize;
pagination.page = 1;
},
...options.paginationProps
}) as PaginationProps;
// this is for mobile, if the system does not support mobile, you can use `pagination` directly
const mobilePagination = computed(() => {
const p: PaginationProps = {
...pagination,
pageSlot: isMobile.value ? 3 : 9,
prefix: !isMobile.value && showTotal.value ? pagination.prefix : undefined
};
return p;
});
const paginationParams = computed(() => {
const { page, pageSize } = pagination;
return {
page,
pageSize
};
});
const result = useTable<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, true>({
...options,
pagination: true,
getColumnChecks: cols => getColumnChecks(cols, options.getColumnVisible),
getColumns,
onFetched: data => {
pagination.itemCount = data.total;
}
});
async function getDataByPage(page: number = 1) {
if (page !== pagination.page) {
pagination.page = page;
return;
}
await result.getData();
}
scope.run(() => {
watch(
() => appStore.locale,
() => {
result.reloadColumns();
}
);
watch(paginationParams, async newVal => {
await options.onPaginationParamsChange?.(newVal);
await result.getData();
});
});
onScopeDispose(() => {
scope.stop();
});
return {
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<NaiveUI.TableOperateType>('add');
const operateType = shallowRef<NaiveUI.TableOperateType>('add');
function handleAdd() {
operateType.value = 'add';
@ -224,18 +176,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() {
@ -267,6 +219,84 @@ export function useTableOperate<T extends TableData = TableData>(data: Ref<T[]>,
};
}
function isTableColumnHasKey<T>(column: TableColumn<T>): column is NaiveUI.TableColumnWithKey<T> {
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 NaiveUI.TableColumn<any>>(
cols: Column[],
getColumnVisible?: (column: Column) => boolean
) {
const checks: TableColumnCheck[] = [];
cols.forEach(column => {
const visible = getColumnVisible?.(column) ?? true;
if (isTableColumnHasKey(column)) {
checks.push({
key: column.key as string,
title: column.title!,
checked: true,
visible
});
} else if (column.type === 'selection') {
checks.push({
key: SELECTION_KEY,
title: $t('common.check'),
checked: true,
visible
});
} else if (column.type === 'expand') {
checks.push({
key: EXPAND_KEY,
title: $t('common.expandColumn'),
checked: true,
visible
});
}
});
return checks;
}
function getColumns<Column extends NaiveUI.TableColumn<any>>(cols: Column[], checks: TableColumnCheck[]) {
const columnMap = new Map<string, Column>();
cols.forEach(column => {
if (isTableColumnHasKey(column)) {
columnMap.set(column.key as string, column);
} else if (column.type === 'selection') {
columnMap.set(SELECTION_KEY, column);
} else if (column.type === 'expand') {
columnMap.set(EXPAND_KEY, column);
}
});
const filteredColumns = checks.filter(item => item.checked).map(check => columnMap.get(check.key) as Column);
return filteredColumns;
}
export function isTableColumnHasKey<T>(column: NaiveUI.TableColumn<T>): column is NaiveUI.TableColumnWithKey<T> {
return Boolean((column as NaiveUI.TableColumnWithKey<T>).key);
}

View File

@ -10,7 +10,7 @@ import GlobalTab from '../modules/global-tab/index.vue';
import GlobalContent from '../modules/global-content/index.vue';
import GlobalFooter from '../modules/global-footer/index.vue';
import ThemeDrawer from '../modules/theme-drawer/index.vue';
import { setupMixMenuContext } from '../context';
import { provideMixMenuContext } from '../modules/global-menu/context';
defineOptions({
name: 'BaseLayout'
@ -18,7 +18,7 @@ defineOptions({
const appStore = useAppStore();
const themeStore = useThemeStore();
const { childLevelMenus, isActiveFirstLevelMenuHasChildren } = setupMixMenuContext();
const { childLevelMenus, isActiveFirstLevelMenuHasChildren } = provideMixMenuContext();
const GlobalMenu = defineAsyncComponent(() => import('../modules/global-menu/index.vue'));

View File

@ -5,7 +5,7 @@ import type { RouteKey } from '@elegant-router/types';
import { useRouteStore } from '@/store/modules/route';
import { useRouterPush } from '@/hooks/common/router';
export const { setupStore: setupMixMenuContext, useStore: useMixMenuContext } = useContext('mix-menu', useMixMenu);
export const [provideMixMenuContext, useMixMenuContext] = useContext('MixMenu', useMixMenu);
function useMixMenu() {
const route = useRoute();

View File

@ -2,7 +2,7 @@
import { GLOBAL_HEADER_MENU_ID } from '@/constants/app';
import { useRouteStore } from '@/store/modules/route';
import { useRouterPush } from '@/hooks/common/router';
import { useMenu } from '../../../context';
import { useMenu } from '../context';
defineOptions({
name: 'HorizontalMenu'

View File

@ -7,7 +7,7 @@ import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
import { useRouteStore } from '@/store/modules/route';
import { useRouterPush } from '@/hooks/common/router';
import { useMenu, useMixMenuContext } from '../../../context';
import { useMenu, useMixMenuContext } from '../context';
defineOptions({
name: 'TopHybridHeaderFirst'
@ -18,7 +18,8 @@ const appStore = useAppStore();
const themeStore = useThemeStore();
const routeStore = useRouteStore();
const { routerPushByKeyWithMetaQuery } = useRouterPush();
const { firstLevelMenus, secondLevelMenus, activeFirstLevelMenuKey, handleSelectFirstLevelMenu } = useMixMenuContext();
const { firstLevelMenus, secondLevelMenus, activeFirstLevelMenuKey, handleSelectFirstLevelMenu } =
useMixMenuContext('TopHybridHeaderFirst');
const { selectedKey } = useMenu();
const expandedKeys = ref<string[]>([]);

View File

@ -4,7 +4,7 @@ import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
import { useRouterPush } from '@/hooks/common/router';
import FirstLevelMenu from '../components/first-level-menu.vue';
import { useMenu, useMixMenuContext } from '../../../context';
import { useMenu, useMixMenuContext } from '../context';
defineOptions({
name: 'TopHybridSidebarFirst'
@ -13,7 +13,8 @@ defineOptions({
const appStore = useAppStore();
const themeStore = useThemeStore();
const { routerPushByKeyWithMetaQuery } = useRouterPush();
const { firstLevelMenus, secondLevelMenus, activeFirstLevelMenuKey, handleSelectFirstLevelMenu } = useMixMenuContext();
const { firstLevelMenus, secondLevelMenus, activeFirstLevelMenuKey, handleSelectFirstLevelMenu } =
useMixMenuContext('TopHybridSidebarFirst');
const { selectedKey } = useMenu();
</script>

View File

@ -9,7 +9,7 @@ import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
import { useRouteStore } from '@/store/modules/route';
import { useRouterPush } from '@/hooks/common/router';
import { useMenu, useMixMenuContext } from '../../../context';
import { useMenu, useMixMenuContext } from '../context';
import FirstLevelMenu from '../components/first-level-menu.vue';
import GlobalLogo from '../../global-logo/index.vue';
@ -34,7 +34,7 @@ const {
handleSelectSecondLevelMenu,
getActiveSecondLevelMenuKey,
childLevelMenus
} = useMixMenuContext();
} = useMixMenuContext('VerticalHybridHeaderFirst');
const { selectedKey } = useMenu();
const inverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted);

View File

@ -7,7 +7,7 @@ import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
import { useRouteStore } from '@/store/modules/route';
import { useRouterPush } from '@/hooks/common/router';
import { useMenu } from '../../../context';
import { useMenu } from '../context';
defineOptions({
name: 'VerticalMenu'

View File

@ -10,7 +10,7 @@ import { useThemeStore } from '@/store/modules/theme';
import { useRouteStore } from '@/store/modules/route';
import { useRouterPush } from '@/hooks/common/router';
import { $t } from '@/locales';
import { useMenu, useMixMenuContext } from '../../../context';
import { useMenu, useMixMenuContext } from '../context';
import FirstLevelMenu from '../components/first-level-menu.vue';
import GlobalLogo from '../../global-logo/index.vue';
@ -31,7 +31,7 @@ const {
isActiveFirstLevelMenuHasChildren,
getActiveFirstLevelMenuKey,
handleSelectFirstLevelMenu
} = useMixMenuContext();
} = useMixMenuContext('VerticalMixMenu');
const { selectedKey } = useMenu();
const inverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted);

View File

@ -217,7 +217,6 @@ const local: App.I18n.Schema = {
function: 'System Function',
alova: 'Alova Example',
alova_request: 'Alova Request',
alova_user: 'User List',
alova_scenes: 'Scenario Request',
'pro-naive': 'Pro Naive Example',
'pro-naive_form': 'Form',

View File

@ -214,7 +214,6 @@ const local: App.I18n.Schema = {
function: '系统功能',
alova: 'alova示例',
alova_request: 'alova请求',
alova_user: '用户列表',
alova_scenes: '场景化请求',
'pro-naive': 'Pro Naive UI 示例',
'pro-naive_form': '表单',

View File

@ -23,7 +23,6 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
about: () => import("@/views/about/index.vue"),
alova_request: () => import("@/views/alova/request/index.vue"),
alova_scenes: () => import("@/views/alova/scenes/index.vue"),
alova_user: () => import("@/views/alova/user/index.vue"),
"function_hide-child_one": () => import("@/views/function/hide-child/one/index.vue"),
"function_hide-child_three": () => import("@/views/function/hide-child/three/index.vue"),
"function_hide-child_two": () => import("@/views/function/hide-child/two/index.vue"),

View File

@ -81,17 +81,6 @@ export const generatedRoutes: GeneratedRoute[] = [
icon: 'cbi:scene-dynamic',
order: 3
}
},
{
name: 'alova_user',
path: '/alova/user',
component: 'view.alova_user',
meta: {
title: 'alova_user',
i18nKey: 'route.alova_user',
icon: 'carbon:user-multiple',
order: 2
}
}
]
},

View File

@ -185,7 +185,6 @@ const routeMap: RouteMap = {
"alova": "/alova",
"alova_request": "/alova/request",
"alova_scenes": "/alova/scenes",
"alova_user": "/alova/user",
"function": "/function",
"function_hide-child": "/function/hide-child",
"function_hide-child_one": "/function/hide-child/one",

View File

@ -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 });
@ -91,9 +98,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
@ -123,11 +127,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;
@ -147,9 +154,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

View File

@ -28,14 +28,14 @@ async function handleRefreshToken() {
}
export async function handleExpiredRequest(state: RequestInstanceState) {
if (!state.refreshTokenFn) {
state.refreshTokenFn = handleRefreshToken();
if (!state.refreshTokenPromise) {
state.refreshTokenPromise = handleRefreshToken();
}
const success = await state.refreshTokenFn;
const success = await state.refreshTokenPromise;
setTimeout(() => {
state.refreshTokenFn = null;
state.refreshTokenPromise = null;
}, 1000);
return success;

View File

@ -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;
}

View File

@ -318,7 +318,7 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
}
async function onRouteSwitchWhenLoggedIn() {
await authStore.initUserInfo();
// some global init logic when logged in and switch route
}
async function onRouteSwitchWhenNotLoggedIn() {

View File

@ -38,8 +38,6 @@ declare module 'vue' {
IconIcRoundRemove: typeof import('~icons/ic/round-remove')['default']
IconIcRoundSearch: typeof import('~icons/ic/round-search')['default']
IconLocalActivity: typeof import('~icons/local/activity')['default']
IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen')['default']
IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default']
IconLocalBanner: typeof import('~icons/local/banner')['default']
IconLocalCast: typeof import('~icons/local/cast')['default']
IconLocalLogo: typeof import('~icons/local/logo')['default']
@ -47,6 +45,7 @@ declare module 'vue' {
'IconMdi:printer': typeof import('~icons/mdi/printer')['default']
IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default']
IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default']
IconMdiDrag: typeof import('~icons/mdi/drag')['default']
IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default']
IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
@ -66,15 +65,20 @@ declare module 'vue' {
NCollapse: typeof import('naive-ui')['NCollapse']
NCollapseItem: typeof import('naive-ui')['NCollapseItem']
NColorPicker: typeof import('naive-ui')['NColorPicker']
NDataTable: typeof import('naive-ui')['NDataTable']
NDescriptions: typeof import('naive-ui')['NDescriptions']
NDescriptionsItem: typeof import('naive-ui')['NDescriptionsItem']
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
NDivider: typeof import('naive-ui')['NDivider']
NDrawer: typeof import('naive-ui')['NDrawer']
NDrawerContent: typeof import('naive-ui')['NDrawerContent']
NDropdown: typeof import('naive-ui')['NDropdown']
NDynamicInput: typeof import('naive-ui')['NDynamicInput']
NEmpty: typeof import('naive-ui')['NEmpty']
NFlex: typeof import('naive-ui')['NFlex']
NForm: typeof import('naive-ui')['NForm']
NFormItem: typeof import('naive-ui')['NFormItem']
NFormItemGi: typeof import('naive-ui')['NFormItemGi']
NGi: typeof import('naive-ui')['NGi']
NGrid: typeof import('naive-ui')['NGrid']
NInput: typeof import('naive-ui')['NInput']
@ -104,8 +108,10 @@ declare module 'vue' {
NTab: typeof import('naive-ui')['NTab']
NTabPane: typeof import('naive-ui')['NTabPane']
NTabs: typeof import('naive-ui')['NTabs']
NTag: typeof import('naive-ui')['NTag']
NThing: typeof import('naive-ui')['NThing']
NTooltip: typeof import('naive-ui')['NTooltip']
NTree: typeof import('naive-ui')['NTree']
NWatermark: typeof import('naive-ui')['NWatermark']
PinToggler: typeof import('./../components/common/pin-toggler.vue')['default']
ProCard: typeof import('pro-naive-ui')['ProCard']

View File

@ -39,7 +39,6 @@ declare module "@elegant-router/types" {
"alova": "/alova";
"alova_request": "/alova/request";
"alova_scenes": "/alova/scenes";
"alova_user": "/alova/user";
"function": "/function";
"function_hide-child": "/function/hide-child";
"function_hide-child_one": "/function/hide-child/one";
@ -183,7 +182,6 @@ declare module "@elegant-router/types" {
| "about"
| "alova_request"
| "alova_scenes"
| "alova_user"
| "function_hide-child_one"
| "function_hide-child_three"
| "function_hide-child_two"

View File

@ -6,30 +6,14 @@ declare namespace NaiveUI {
type DataTableExpandColumn<T> = import('naive-ui').DataTableExpandColumn<T>;
type DataTableSelectionColumn<T> = import('naive-ui').DataTableSelectionColumn<T>;
type TableColumnGroup<T> = import('naive-ui/es/data-table/src/interface').TableColumnGroup<T>;
type PaginationProps = import('naive-ui').PaginationProps;
type TableColumnCheck = import('@sa/hooks').TableColumnCheck;
type TableDataWithIndex<T> = import('@sa/hooks').TableDataWithIndex<T>;
type FlatResponseData<T> = import('@sa/axios').FlatResponseData<T>;
/**
* the custom column key
*
* if you want to add a custom column, you should add a key to this type
*/
type CustomColumnKey = 'operate';
type SetTableColumnKey<C, T> = Omit<C, 'key'> & { key: keyof T | CustomColumnKey };
type TableData = Api.Common.CommonRecord<object>;
type SetTableColumnKey<C, T> = Omit<C, 'key'> & { key: keyof T | (string & {}) };
type TableColumnWithKey<T> = SetTableColumnKey<DataTableBaseColumn<T>, T> | SetTableColumnKey<TableColumnGroup<T>, T>;
type TableColumn<T> = TableColumnWithKey<T> | DataTableSelectionColumn<T> | DataTableExpandColumn<T>;
type TableApiFn<T = any, R = Api.Common.CommonSearchParams> = (
params: R
) => Promise<FlatResponseData<Api.Common.PaginatingQueryRecord<T>>>;
/**
* the type of table operation
*
@ -37,18 +21,4 @@ declare namespace NaiveUI {
* - 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;
};
}

View File

@ -1,78 +0,0 @@
import { computed, ref } from 'vue';
import type { DataTableBaseColumn, DataTableColumn } from 'naive-ui';
import type { TableColumnCheck } from '@sa/hooks';
import { $t } from '@/locales';
import type { AlovaGenerics, Method } from '~/packages/alova/src';
function isTableColumnHasKey<T>(column: DataTableColumn<T>): column is DataTableBaseColumn<T> {
return Boolean((column as NaiveUI.TableColumnWithKey<T>).key);
}
type TableAlovaApiFn<T = any, R = Api.Common.CommonSearchParams> = (
params: R
) => Method<AlovaGenerics<Api.Common.PaginatingQueryRecord<T>>>;
// this hook is used to manage table columns
// if you choose alova, you can move this hook to the `src/hooks` to handle all list page in your project
export default function useCheckedColumns<A extends TableAlovaApiFn, T = Awaited<ReturnType<A>>['records'][number]>(
getColumns: () => DataTableColumn<T>[]
) {
const SELECTION_KEY = '__selection__';
const EXPAND_KEY = '__expand__';
const getColumnChecks = (cols: DataTableColumn<T>[]) => {
const checks: NaiveUI.TableColumnCheck[] = [];
cols.forEach(column => {
if (isTableColumnHasKey(column)) {
checks.push({
key: column.key as string,
title: column.title as string,
checked: true
});
} else if (column.type === 'selection') {
checks.push({
key: SELECTION_KEY,
title: $t('common.check'),
checked: true
});
} else if (column.type === 'expand') {
checks.push({
key: EXPAND_KEY,
title: $t('common.expandColumn'),
checked: true
});
}
});
return checks;
};
const columnChecks = ref<TableColumnCheck[]>(getColumnChecks(getColumns()));
const columns = computed(() => {
const cols = getColumns();
const columnMap = new Map<string, DataTableColumn<T>>();
cols.forEach(column => {
if (isTableColumnHasKey(column)) {
columnMap.set(column.key as string, column);
} else if (column.type === 'selection') {
columnMap.set(SELECTION_KEY, column);
} else if (column.type === 'expand') {
columnMap.set(EXPAND_KEY, column);
}
});
const filteredColumns = columnChecks.value
.filter(item => item.checked)
.map(check => columnMap.get(check.key) as NaiveUI.TableColumn<T>);
return filteredColumns;
});
return {
columnChecks,
columns
};
}

View File

@ -1,83 +0,0 @@
import type { Ref } from 'vue';
import { ref } from 'vue';
import { useBoolean } from '@sa/hooks';
import { jsonClone } from '@sa/utils';
import { $t } from '@/locales';
type TableData = NaiveUI.TableData;
interface Operations<T> {
delete?: (row: T) => Promise<void>;
batchDelete?: (rows: T[]) => Promise<void>;
}
// this hook is used to handle the table operations
// if you choose alova, you can move this hook to the `src/hooks` to handle all list page in your project
export default function useTableOperate<T extends TableData = TableData>(data: Ref<T[]>, operations: Operations<T>) {
const { bool: drawerVisible, setTrue: openDrawer, setFalse: closeDrawer } = useBoolean();
const { bool: deleting, setTrue: deletify, setFalse: antiDelete } = useBoolean();
const { bool: batchDeleting, setTrue: batchDeletify, setFalse: antiBatchDelete } = useBoolean();
const operateType = ref<NaiveUI.TableOperateType>('add');
const getRowByDataId = (id: T['id']) => data.value.find(item => item.id === id) || null;
function handleAdd() {
operateType.value = 'add';
openDrawer();
}
/** the editing row data */
const editingData: Ref<T | null> = ref(null);
function handleEdit(id: T['id']) {
operateType.value = 'edit';
editingData.value = jsonClone(getRowByDataId(id));
openDrawer();
}
/** the checked row keys of table */
const checkedRowKeys = ref<T['id'][]>([]);
/** handler to batch delete rows */
async function handleBatchDelete() {
batchDeletify();
try {
const rows = checkedRowKeys.value.map(id => getRowByDataId(id)).filter(Boolean);
await operations.batchDelete?.(rows as T[]);
window.$message?.success($t('common.deleteSuccess'));
checkedRowKeys.value = [];
} finally {
antiBatchDelete();
}
}
/** handler to delete row */
async function handleDelete(id: T['id']) {
deletify();
const row = getRowByDataId(id);
if (!row) return;
try {
await operations.delete?.(row);
window.$message?.success($t('common.deleteSuccess'));
checkedRowKeys.value = [];
} finally {
antiDelete();
}
}
return {
drawerVisible,
openDrawer,
closeDrawer,
operateType,
handleAdd,
editingData,
handleEdit,
checkedRowKeys,
deleting,
handleDelete,
batchDeleting,
handleBatchDelete
};
}

View File

@ -1,226 +0,0 @@
<script setup lang="tsx">
import { reactive } from 'vue';
import { NButton, NPopconfirm, NTag } from 'naive-ui';
import { usePagination } from '@sa/alova/client';
import { enableStatusRecord, userGenderRecord } from '@/constants/business';
import { useAppStore } from '@/store/modules/app';
import { batchDeleteUser, deleteUser, fetchGetUserList } from '@/service-alova/api';
import { $t } from '@/locales';
import useCheckedColumns from './hooks/use-checked-columns';
import useTableOperate from './hooks/use-table-operate';
import UserOperateDrawer from './modules/user-operate-drawer.vue';
import UserSearch from './modules/user-search.vue';
const appStore = useAppStore();
const searchParams = reactive({
// if you want to use the searchParams in Form, you need to define the following properties, and the value is null
// the value can not be undefined, otherwise the property in Form will not be reactive
status: null,
userName: null,
userGender: null,
nickName: null,
userPhone: null,
userEmail: null
});
const { loading, data, refresh, reload, page, pageSize, pageCount, send, remove } = usePagination(
(pageNum, size) =>
fetchGetUserList({
...searchParams,
current: pageNum,
size
}),
{
data: ({ records }) => records,
total: ({ total }) => total,
// trigger reload when states in `searchParams` changed
watchingStates: [searchParams],
// debounce of `searchParams`
debounce: [1000]
}
);
const getDataByPage = (newPage = 1) => {
page.value = newPage;
send(page.value, pageSize.value);
};
const {
drawerVisible,
operateType,
editingData,
handleAdd,
handleEdit,
handleDelete,
handleBatchDelete,
checkedRowKeys,
deleting
// batchDeleting
// closeDrawer
} = useTableOperate(data, {
async delete(row) {
await deleteUser(row.id);
remove(row);
},
async batchDelete(rows) {
await batchDeleteUser(rows.map(({ id }) => id));
remove(...rows);
}
});
function edit(id: number) {
handleEdit(id);
}
const { columnChecks, columns } = useCheckedColumns<typeof fetchGetUserList>(() => [
{
type: 'selection',
align: 'center',
width: 48
},
{
key: 'userName',
title: $t('page.manage.user.userName'),
align: 'center',
minWidth: 100
},
{
key: 'userGender',
title: $t('page.manage.user.userGender'),
align: 'center',
width: 100,
render: row => {
if (row.userGender === null) {
return null;
}
const tagMap: Record<Api.SystemManage.UserGender, NaiveUI.ThemeColor> = {
1: 'primary',
2: 'error'
};
const label = $t(userGenderRecord[row.userGender]);
return <NTag type={tagMap[row.userGender]}>{label}</NTag>;
}
},
{
key: 'nickName',
title: $t('page.manage.user.nickName'),
align: 'center',
minWidth: 100
},
{
key: 'userPhone',
title: $t('page.manage.user.userPhone'),
align: 'center',
width: 120
},
{
key: 'userEmail',
title: $t('page.manage.user.userEmail'),
align: 'center',
minWidth: 200
},
{
key: 'status',
title: $t('page.manage.user.userStatus'),
align: 'center',
width: 100,
render: row => {
if (row.status === null) {
return null;
}
const tagMap: Record<Api.Common.EnableStatus, NaiveUI.ThemeColor> = {
1: 'success',
2: 'warning'
};
const label = $t(enableStatusRecord[row.status]);
return <NTag type={tagMap[row.status]}>{label}</NTag>;
}
},
{
key: 'operate',
title: $t('common.operate'),
align: 'center',
width: 130,
render: row => (
<div class="flex-center gap-8px">
<NButton type="primary" ghost size="small" onClick={() => edit(row.id)}>
{$t('common.edit')}
</NButton>
<NPopconfirm
onPositiveClick={() => handleDelete(row.id)}
positiveButtonProps={{
loading: deleting.value
}}
>
{{
default: () => $t('common.confirmDelete'),
trigger: () => (
<NButton type="error" ghost size="small">
{$t('common.delete')}
</NButton>
)
}}
</NPopconfirm>
</div>
)
}
]);
</script>
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<UserSearch v-model:model="searchParams" @search="getDataByPage" />
<NCard :title="$t('page.manage.user.title')" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"
:disabled-delete="checkedRowKeys.length === 0"
:loading="loading"
@add="handleAdd"
@delete="handleBatchDelete"
@refresh="refresh"
/>
</template>
<NDataTable
v-model:checked-row-keys="checkedRowKeys"
:columns="columns"
:data="data"
size="small"
:flex-height="!appStore.isMobile"
:scroll-x="962"
:loading="loading"
remote
:row-key="row => row.id"
:pagination="{
page,
pageSize,
showSizePicker: true,
pageCount,
pageSizes: [10, 15, 20, 25, 30],
onUpdatePage(value) {
page = value;
},
onUpdatePageSize(value) {
pageSize = value;
}
}"
class="sm:h-full"
/>
<UserOperateDrawer
v-model:visible="drawerVisible"
:operate-type="operateType"
:row-data="editingData"
@submitted="reload"
/>
</NCard>
</div>
</template>
<style scoped></style>

View File

@ -1,169 +0,0 @@
<script setup lang="ts">
import { computed, watch } from 'vue';
import { useForm, useWatcher } from '@sa/alova/client';
import { enableStatusOptions, userGenderOptions } from '@/constants/business';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import type { UserModel } from '@/service-alova/api';
import { addUser, fetchGetAllRoles, updateUser } from '@/service-alova/api';
import { $t } from '@/locales';
defineOptions({
name: 'UserOperateDrawer'
});
interface Props {
/** the type of operation */
operateType: NaiveUI.TableOperateType;
/** the edit row data */
rowData?: Api.SystemManage.User | null;
}
const props = defineProps<Props>();
interface Emits {
(e: 'submitted'): void;
}
const emit = defineEmits<Emits>();
const visible = defineModel<boolean>('visible', {
default: false
});
const { formRef, validate, restoreValidation } = useNaiveForm();
const { defaultRequiredRule } = useFormRules();
const title = computed(() => {
const titles: Record<NaiveUI.TableOperateType, string> = {
add: $t('page.manage.user.addUser'),
edit: $t('page.manage.user.editUser')
};
return titles[props.operateType];
});
const {
loading: submiting,
reset,
send: submit,
form,
updateForm
} = useForm(formData => (props.operateType === 'add' ? addUser(formData) : updateUser(formData)), {
initialForm: {
userName: '',
userGender: null,
nickName: '',
userPhone: '',
userEmail: '',
userRoles: [],
status: null
} as UserModel,
resetAfterSubmiting: true
});
type RuleKey = Extract<keyof UserModel, 'userName' | 'status'>;
const rules: Record<RuleKey, App.Global.FormRule> = {
userName: defaultRequiredRule,
status: defaultRequiredRule
};
/** the enabled role options */
const { data: roleOptionsRaw, loading } = useWatcher(fetchGetAllRoles, [visible], {
initialData: [],
middleware(_, next) {
return visible.value ? next() : undefined;
}
});
const roleOptions = computed<CommonType.Option<string>[]>(() => {
const options = roleOptionsRaw.value.map(item => ({
label: item.roleName,
value: item.roleCode
}));
// the mock data does not have the roleCode, so fill it
// if the real request, remove the following code
const userRoleOptions = form.value.userRoles.map(item => ({
label: item,
value: item
}));
// end
return [...userRoleOptions, ...options];
});
function handleInitModel() {
if (props.operateType === 'edit' && props.rowData) {
updateForm(props.rowData);
} else if (props.operateType === 'add') {
reset();
}
}
function closeDrawer() {
visible.value = false;
}
async function handleSubmit() {
await validate();
// request
await submit();
window.$message?.success($t('common.updateSuccess'));
closeDrawer();
emit('submitted');
}
watch(visible, () => {
if (visible.value) {
restoreValidation();
handleInitModel();
}
});
</script>
<template>
<NDrawer v-model:show="visible" display-directive="show" :width="360">
<NDrawerContent :title="title" :native-scrollbar="false" closable>
<NForm ref="formRef" :model="form" :rules="rules">
<NFormItem :label="$t('page.manage.user.userName')" path="userName">
<NInput v-model:value="form.userName" :placeholder="$t('page.manage.user.form.userName')" />
</NFormItem>
<NFormItem :label="$t('page.manage.user.userGender')" path="userGender">
<NRadioGroup v-model:value="form.userGender">
<NRadio v-for="item in userGenderOptions" :key="item.value" :value="item.value" :label="$t(item.label)" />
</NRadioGroup>
</NFormItem>
<NFormItem :label="$t('page.manage.user.nickName')" path="nickName">
<NInput v-model:value="form.nickName" :placeholder="$t('page.manage.user.form.nickName')" />
</NFormItem>
<NFormItem :label="$t('page.manage.user.userPhone')" path="userPhone">
<NInput v-model:value="form.userPhone" :placeholder="$t('page.manage.user.form.userPhone')" />
</NFormItem>
<NFormItem :label="$t('page.manage.user.userEmail')" path="email">
<NInput v-model:value="form.userEmail" :placeholder="$t('page.manage.user.form.userEmail')" />
</NFormItem>
<NFormItem :label="$t('page.manage.user.userStatus')" path="status">
<NRadioGroup v-model:value="form.status">
<NRadio v-for="item in enableStatusOptions" :key="item.value" :value="item.value" :label="$t(item.label)" />
</NRadioGroup>
</NFormItem>
<NFormItem :label="$t('page.manage.user.userRole')" path="roles">
<NSelect
v-model:value="form.userRoles"
multiple
:loading="loading"
:options="roleOptions"
:placeholder="$t('page.manage.user.form.userRole')"
/>
</NFormItem>
</NForm>
<template #footer>
<NSpace :size="16">
<NButton @click="closeDrawer">{{ $t('common.cancel') }}</NButton>
<NButton type="primary" :loading="submiting" @click="handleSubmit">{{ $t('common.confirm') }}</NButton>
</NSpace>
</template>
</NDrawerContent>
</NDrawer>
</template>
<style scoped></style>

View File

@ -1,113 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue';
import { enableStatusOptions, userGenderOptions } from '@/constants/business';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { translateOptions } from '@/utils/common';
import { $t } from '@/locales';
defineOptions({
name: 'UserSearch'
});
interface Emits {
(e: 'search'): void;
}
const emit = defineEmits<Emits>();
const { formRef, validate, restoreValidation } = useNaiveForm();
const model = defineModel<Api.SystemManage.UserSearchParams>('model', { required: true });
const initialParams = { ...model.value };
type RuleKey = Extract<keyof Api.SystemManage.UserSearchParams, 'userEmail' | 'userPhone'>;
const rules = computed<Record<RuleKey, App.Global.FormRule>>(() => {
const { patternRules } = useFormRules(); // inside computed to make locale reactive
return {
userEmail: patternRules.email,
userPhone: patternRules.phone
};
});
async function reset() {
await restoreValidation();
Object.assign(model.value, initialParams);
}
async function search() {
await validate();
emit('search');
}
</script>
<template>
<NCard :bordered="false" size="small" class="card-wrapper">
<NCollapse :default-expanded-names="['user-search']">
<NCollapseItem :title="$t('common.search')" name="user-search">
<NForm ref="formRef" :model="model" :rules="rules" label-placement="left" :label-width="80">
<NGrid responsive="screen" item-responsive>
<NFormItemGi span="24 s:12 m:6" :label="$t('page.manage.user.userName')" path="userName" class="pr-24px">
<NInput v-model:value="model.userName" :placeholder="$t('page.manage.user.form.userName')" />
</NFormItemGi>
<NFormItemGi
span="24 s:12 m:6"
:label="$t('page.manage.user.userGender')"
path="userGender"
class="pr-24px"
>
<NSelect
v-model:value="model.userGender"
:placeholder="$t('page.manage.user.form.userGender')"
:options="translateOptions(userGenderOptions)"
clearable
/>
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" :label="$t('page.manage.user.nickName')" path="nickName" class="pr-24px">
<NInput v-model:value="model.nickName" :placeholder="$t('page.manage.user.form.nickName')" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" :label="$t('page.manage.user.userPhone')" path="userPhone" class="pr-24px">
<NInput v-model:value="model.userPhone" :placeholder="$t('page.manage.user.form.userPhone')" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" :label="$t('page.manage.user.userEmail')" path="userEmail" class="pr-24px">
<NInput v-model:value="model.userEmail" :placeholder="$t('page.manage.user.form.userEmail')" />
</NFormItemGi>
<NFormItemGi
span="24 s:12 m:6"
:label="$t('page.manage.user.userStatus')"
path="userStatus"
class="pr-24px"
>
<NSelect
v-model:value="model.status"
:placeholder="$t('page.manage.user.form.userStatus')"
:options="translateOptions(enableStatusOptions)"
clearable
/>
</NFormItemGi>
<NFormItemGi span="24 m:12" class="pr-24px">
<NSpace class="w-full" justify="end">
<NButton @click="reset">
<template #icon>
<icon-ic-round-refresh class="text-icon" />
</template>
{{ $t('common.reset') }}
</NButton>
<NButton type="primary" ghost @click="search">
<template #icon>
<icon-ic-round-search class="text-icon" />
</template>
{{ $t('common.search') }}
</NButton>
</NSpace>
</NFormItemGi>
</NGrid>
</NForm>
</NCollapseItem>
</NCollapse>
</NCard>
</template>
<style scoped></style>

View File

@ -7,7 +7,7 @@ import { yesOrNoRecord } from '@/constants/common';
import { enableStatusRecord, menuTypeRecord } from '@/constants/business';
import { fetchGetAllPages, fetchGetMenuList } from '@/service/api';
import { useAppStore } from '@/store/modules/app';
import { useTable, useTableOperate } from '@/hooks/common/table';
import { defaultTransform, useNaivePaginatedTable, useTableOperate } 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';
@ -18,8 +18,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 } = useNaivePaginatedTable({
api: () => fetchGetMenuList(),
transform: response => defaultTransform(response),
columns: () => [
{
type: 'selection',
@ -170,7 +171,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');

View File

@ -1,35 +1,30 @@
<script setup lang="tsx">
import { reactive } from 'vue';
import { NButton, NPopconfirm, NTag } from 'naive-ui';
import { enableStatusRecord } from '@/constants/business';
import { fetchGetRoleList } from '@/service/api';
import { useAppStore } from '@/store/modules/app';
import { useTable, useTableOperate } from '@/hooks/common/table';
import { defaultTransform, useNaivePaginatedTable, useTableOperate } from '@/hooks/common/table';
import { $t } from '@/locales';
import RoleOperateDrawer from './modules/role-operate-drawer.vue';
import RoleSearch from './modules/role-search.vue';
const appStore = useAppStore();
const {
columns,
columnChecks,
data,
loading,
getData,
getDataByPage,
mobilePagination,
searchParams,
resetSearchParams
} = useTable({
apiFn: fetchGetRoleList,
apiParams: {
current: 1,
size: 10,
// if you want to use the searchParams in Form, you need to define the following properties, and the value is null
// the value can not be undefined, otherwise the property in Form will not be reactive
status: null,
roleName: null,
roleCode: null
const searchParams: Api.SystemManage.RoleSearchParams = reactive({
current: 1,
size: 10,
roleName: null,
roleCode: null,
status: null
});
const { columns, columnChecks, data, loading, getData, getDataByPage, mobilePagination } = useNaivePaginatedTable({
api: () => fetchGetRoleList(searchParams),
transform: response => defaultTransform(response),
onPaginationParamsChange: params => {
searchParams.current = params.page;
searchParams.size = params.pageSize;
},
columns: () => [
{
@ -41,7 +36,8 @@ const {
key: 'index',
title: $t('common.index'),
width: 64,
align: 'center'
align: 'center',
render: (_, index) => index + 1
},
{
key: 'roleName',
@ -116,7 +112,7 @@ const {
onBatchDeleted,
onDeleted
// closeDrawer
} = useTableOperate(data, getData);
} = useTableOperate(data, 'id', getData);
async function handleBatchDelete() {
// request
@ -139,7 +135,7 @@ function edit(id: number) {
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<RoleSearch v-model:model="searchParams" @reset="resetSearchParams" @search="getDataByPage" />
<RoleSearch v-model:model="searchParams" @search="getDataByPage" />
<NCard :title="$t('page.manage.role.title')" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
<template #header-extra>
<TableHeaderOperation

View File

@ -8,7 +8,6 @@ defineOptions({
});
interface Emits {
(e: 'reset'): void;
(e: 'search'): void;
}
@ -16,8 +15,14 @@ const emit = defineEmits<Emits>();
const model = defineModel<Api.SystemManage.RoleSearchParams>('model', { required: true });
function reset() {
emit('reset');
function resetModel() {
model.value = {
current: 1,
size: 10,
roleName: null,
roleCode: null,
status: null
};
}
function search() {
@ -47,7 +52,7 @@ function search() {
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6">
<NSpace class="w-full" justify="end">
<NButton @click="reset">
<NButton @click="resetModel">
<template #icon>
<icon-ic-round-refresh class="text-icon" />
</template>

View File

@ -1,39 +1,33 @@
<script setup lang="tsx">
import { reactive } from 'vue';
import { NButton, NPopconfirm, NTag } from 'naive-ui';
import { enableStatusRecord, userGenderRecord } from '@/constants/business';
import { fetchGetUserList } from '@/service/api';
import { useAppStore } from '@/store/modules/app';
import { useTable, useTableOperate } from '@/hooks/common/table';
import { defaultTransform, useNaivePaginatedTable, useTableOperate } from '@/hooks/common/table';
import { $t } from '@/locales';
import UserOperateDrawer from './modules/user-operate-drawer.vue';
import UserSearch from './modules/user-search.vue';
const appStore = useAppStore();
const {
columns,
columnChecks,
data,
getData,
getDataByPage,
loading,
mobilePagination,
searchParams,
resetSearchParams
} = useTable({
apiFn: fetchGetUserList,
showTotal: true,
apiParams: {
current: 1,
size: 10,
// if you want to use the searchParams in Form, you need to define the following properties, and the value is null
// the value can not be undefined, otherwise the property in Form will not be reactive
status: null,
userName: null,
userGender: null,
nickName: null,
userPhone: null,
userEmail: null
const searchParams: Api.SystemManage.UserSearchParams = reactive({
current: 1,
size: 10,
status: null,
userName: null,
userGender: null,
nickName: null,
userPhone: null,
userEmail: null
});
const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagination } = useNaivePaginatedTable({
api: () => fetchGetUserList(searchParams),
transform: response => defaultTransform(response),
onPaginationParamsChange: params => {
searchParams.current = params.page;
searchParams.size = params.pageSize;
},
columns: () => [
{
@ -45,7 +39,8 @@ const {
key: 'index',
title: $t('common.index'),
align: 'center',
width: 64
width: 64,
render: (_, index) => index + 1
},
{
key: 'userName',
@ -147,7 +142,7 @@ const {
onBatchDeleted,
onDeleted
// closeDrawer
} = useTableOperate(data, getData);
} = useTableOperate(data, 'id', getData);
async function handleBatchDelete() {
// request
@ -170,7 +165,7 @@ function edit(id: number) {
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<UserSearch v-model:model="searchParams" @reset="resetSearchParams" @search="getDataByPage" />
<UserSearch v-model:model="searchParams" @search="getDataByPage" />
<NCard :title="$t('page.manage.user.title')" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
<template #header-extra>
<TableHeaderOperation

View File

@ -10,7 +10,6 @@ defineOptions({
});
interface Emits {
(e: 'reset'): void;
(e: 'search'): void;
}
@ -31,9 +30,22 @@ const rules = computed<Record<RuleKey, App.Global.FormRule>>(() => {
};
});
function resetModel() {
model.value = {
current: 1,
size: 10,
status: null,
userName: null,
userGender: null,
nickName: null,
userPhone: null,
userEmail: null
};
}
async function reset() {
await restoreValidation();
emit('reset');
resetModel();
}
async function search() {

View File

@ -1,28 +1,36 @@
<script setup lang="tsx">
import { reactive } from 'vue';
import { NButton, NTag } from 'naive-ui';
import { utils, writeFile } from 'xlsx';
import { enableStatusRecord, userGenderRecord } from '@/constants/business';
import { fetchGetUserList } from '@/service/api';
import { useAppStore } from '@/store/modules/app';
import { useTable } from '@/hooks/common/table';
import { isTableColumnHasKey, useNaiveTable } from '@/hooks/common/table';
import { $t } from '@/locales';
const appStore = useAppStore();
const { columns, data, loading } = useTable({
apiFn: fetchGetUserList,
showTotal: true,
apiParams: {
current: 1,
size: 999,
// if you want to use the searchParams in Form, you need to define the following properties, and the value is null
// the value can not be undefined, otherwise the property in Form will not be reactive
status: null,
userName: null,
userGender: null,
nickName: null,
userPhone: null,
userEmail: null
const searchParams: Api.SystemManage.UserSearchParams = reactive({
current: 1,
size: 999,
status: null,
userName: null,
userGender: null,
nickName: null,
userPhone: null,
userEmail: null
});
const { columns, data, loading } = useNaiveTable({
api: () => fetchGetUserList(searchParams),
transform: response => {
const { data: list, error } = response;
if (!error) {
return list.records;
}
return [];
},
columns: () => [
{
@ -34,7 +42,8 @@ const { columns, data, loading } = useTable({
key: 'index',
title: $t('common.index'),
align: 'center',
width: 64
width: 64,
render: (_, index) => index + 1
},
{
key: 'userName',
@ -125,20 +134,13 @@ function exportExcel() {
writeFile(workBook, '用户数据.xlsx');
}
function getTableValue(
col: NaiveUI.TableColumn<NaiveUI.TableDataWithIndex<Api.SystemManage.User>>,
item: NaiveUI.TableDataWithIndex<Api.SystemManage.User>
) {
function getTableValue(col: NaiveUI.TableColumn<Api.SystemManage.User>, item: Api.SystemManage.User) {
if (!isTableColumnHasKey(col)) {
return null;
}
const { key } = col;
if (key === 'operate') {
return null;
}
if (key === 'userRoles') {
return item.userRoles.map(role => role).join(',');
}
@ -151,11 +153,8 @@ function getTableValue(
return (item.userGender && $t(userGenderRecord[item.userGender])) || null;
}
return item[key];
}
function isTableColumnHasKey<T>(column: NaiveUI.TableColumn<T>): column is NaiveUI.TableColumnWithKey<T> {
return Boolean((column as NaiveUI.TableColumnWithKey<T>).key);
// @ts-expect-error the key is not in the type of Api.SystemManage.User
return item[key] || null;
}
function isTableColumnHasTitle<T>(column: NaiveUI.TableColumn<T>): column is NaiveUI.TableColumnWithKey<T> & {