mirror of
https://github.com/soybeanjs/soybean-admin.git
synced 2025-09-29 14:46:41 +08:00
feat: refactor all pages with @sa/alova
This commit is contained in:
parent
d8ec43bc6c
commit
8d6d1865f6
@ -52,13 +52,11 @@
|
||||
"@better-scroll/core": "2.5.1",
|
||||
"@iconify/vue": "4.1.2",
|
||||
"@sa/alova": "workspace:*",
|
||||
"@sa/axios": "workspace:*",
|
||||
"@sa/color": "workspace:*",
|
||||
"@sa/hooks": "workspace:*",
|
||||
"@sa/materials": "workspace:*",
|
||||
"@sa/utils": "workspace:*",
|
||||
"@vueuse/core": "11.0.3",
|
||||
"alova": "^3.0.16",
|
||||
"clipboard": "2.0.11",
|
||||
"dayjs": "1.11.13",
|
||||
"dhtmlx-gantt": "8.0.10",
|
||||
|
@ -12,6 +12,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@sa/utils": "workspace:*",
|
||||
"alova": "^3.0.16"
|
||||
"alova": "^3.0.19"
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,2 @@
|
||||
/** request id key */
|
||||
export const REQUEST_ID_KEY = 'X-Request-Id';
|
||||
|
||||
/** the backend error code key */
|
||||
export const BACKEND_ERROR_CODE = 'BACKEND_ERROR';
|
||||
|
@ -1,86 +1,76 @@
|
||||
import type { AlovaDefaultCacheAdapter, AlovaGenerics, AlovaGlobalCacheAdapter, AlovaRequestAdapter } from 'alova';
|
||||
import { createAlova } from 'alova';
|
||||
import VueHook from 'alova/vue';
|
||||
import type { FetchRequestInit } from 'alova/fetch';
|
||||
import adapterFetch from 'alova/fetch';
|
||||
import { createServerTokenAuthentication } from 'alova/client';
|
||||
import { BACKEND_ERROR_CODE, REQUEST_ID_KEY } from './constant';
|
||||
import { isJSON } from './shared';
|
||||
import { BACKEND_ERROR_CODE } from './constant';
|
||||
import type { CustomAlovaConfig, RequestOptions } from './type';
|
||||
|
||||
export const createAlovaRequest = (customConfig: CustomAlovaConfig<any>, options: RequestOptions<any>) => {
|
||||
const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthentication({
|
||||
export const createAlovaRequest = <
|
||||
RequestConfig = FetchRequestInit,
|
||||
ResponseType = Response,
|
||||
ResponseHeader = Headers,
|
||||
L1Cache extends AlovaGlobalCacheAdapter = AlovaDefaultCacheAdapter,
|
||||
L2Cache extends AlovaGlobalCacheAdapter = AlovaDefaultCacheAdapter
|
||||
>(
|
||||
customConfig: CustomAlovaConfig<
|
||||
AlovaGenerics<any, any, RequestConfig, ResponseType, ResponseHeader, L1Cache, L2Cache, any>
|
||||
>,
|
||||
options: RequestOptions<AlovaGenerics<any, any, RequestConfig, ResponseType, ResponseHeader, L1Cache, L2Cache, any>>
|
||||
) => {
|
||||
const { tokenRefresher } = options;
|
||||
const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthentication<
|
||||
typeof VueHook,
|
||||
AlovaRequestAdapter<RequestConfig, ResponseType, ResponseHeader>
|
||||
>({
|
||||
refreshTokenOnSuccess: {
|
||||
isExpired: async response => {
|
||||
const contentType = response.headers.get('Content-Type');
|
||||
if (isJSON(contentType ?? '')) {
|
||||
const resp = response.clone();
|
||||
const data = await resp.json();
|
||||
const responseCode = String(data.code);
|
||||
if (customConfig.expiredTokenCodes.includes(responseCode)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
handler: async () => {
|
||||
if (options.refreshTokenHandler) {
|
||||
await options.refreshTokenHandler();
|
||||
}
|
||||
}
|
||||
isExpired: (response, method) => tokenRefresher?.isExpired(response, method) || false,
|
||||
handler: async (response, method) => tokenRefresher?.handler(response, method)
|
||||
},
|
||||
refreshTokenOnError: {
|
||||
isExpired: async response => {
|
||||
const contentType = response.headers.get('Content-Type');
|
||||
if (isJSON(contentType ?? '')) {
|
||||
const resp = response.clone();
|
||||
const data = await resp.json();
|
||||
const responseCode = String(data.code);
|
||||
if (customConfig.expiredTokenCodes.includes(responseCode)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
handler: async () => {
|
||||
if (options.refreshTokenHandler) {
|
||||
await options.refreshTokenHandler();
|
||||
}
|
||||
}
|
||||
isExpired: (response, method) => tokenRefresher?.isExpired(response, method) || false,
|
||||
handler: async (response, method) => tokenRefresher?.handler(response, method)
|
||||
}
|
||||
});
|
||||
|
||||
const instance = createAlova({
|
||||
...customConfig,
|
||||
timeout: customConfig.timeout ?? 10 * 1000,
|
||||
requestAdapter: customConfig.requestAdapter ?? adapterFetch(),
|
||||
requestAdapter: (customConfig.requestAdapter as any) ?? adapterFetch(),
|
||||
statesHook: VueHook,
|
||||
beforeRequest: onAuthRequired(options.onRequest),
|
||||
beforeRequest: onAuthRequired(options.onRequest as any),
|
||||
responded: onResponseRefreshToken({
|
||||
onSuccess: async resp => {
|
||||
onSuccess: async (response, method) => {
|
||||
// check if http status is success
|
||||
if (resp.ok || resp.status === 304) {
|
||||
if (
|
||||
!isJSON(resp.headers.get('Content-Type') ?? '') ||
|
||||
(options.isBackendSuccess && (await options.isBackendSuccess(resp)))
|
||||
) {
|
||||
return options.transformBackendResponse ? await options.transformBackendResponse(resp) : resp;
|
||||
}
|
||||
if (options.onBackendFail) {
|
||||
const fail = await options.onBackendFail(resp);
|
||||
if (fail) {
|
||||
return fail;
|
||||
}
|
||||
let error: any = null;
|
||||
let transformedData: any = null;
|
||||
try {
|
||||
if (await options.isBackendSuccess(response)) {
|
||||
transformedData = await options.transformBackendResponse(response);
|
||||
} else {
|
||||
error = new Error('the backend request error');
|
||||
error.code = BACKEND_ERROR_CODE;
|
||||
}
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
throw new Error(resp.statusText);
|
||||
|
||||
if (error) {
|
||||
await options.onError?.(error, response, method);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return transformedData;
|
||||
},
|
||||
onComplete: options.onComplete,
|
||||
onError: options.onError
|
||||
onError: (error, method) => options.onError?.(error, null, method)
|
||||
})
|
||||
});
|
||||
|
||||
return instance;
|
||||
};
|
||||
|
||||
export { BACKEND_ERROR_CODE, REQUEST_ID_KEY };
|
||||
export { BACKEND_ERROR_CODE };
|
||||
export type * from './type';
|
||||
export type * from 'alova';
|
||||
|
@ -1,3 +0,0 @@
|
||||
export function isJSON(contentType: string) {
|
||||
return contentType.includes('application/json');
|
||||
}
|
@ -1,18 +1,9 @@
|
||||
import type {
|
||||
AlovaGenerics,
|
||||
AlovaOptions,
|
||||
AlovaRequestAdapter,
|
||||
Method,
|
||||
ResponseCompleteHandler,
|
||||
ResponseErrorHandler
|
||||
} from 'alova';
|
||||
import type { AlovaGenerics, AlovaOptions, AlovaRequestAdapter, Method, ResponseCompleteHandler } from 'alova';
|
||||
|
||||
export type CustomAlovaConfig<AG extends AlovaGenerics> = Omit<
|
||||
AlovaOptions<AG>,
|
||||
'statesHook' | 'beforeRequest' | 'responded' | 'requestAdapter'
|
||||
> & {
|
||||
/** expired token codes */
|
||||
expiredTokenCodes: string[];
|
||||
/** request adapter. all request of alova will be sent by it. */
|
||||
requestAdapter?: AlovaRequestAdapter<AG['RequestConfig'], AG['Response'], AG['ResponseHeader']>;
|
||||
};
|
||||
@ -25,26 +16,23 @@ export interface RequestOptions<AG extends AlovaGenerics> {
|
||||
*
|
||||
* @param method alova Method Instance
|
||||
*/
|
||||
onRequest?: (method: Method<AG>) => void | Promise<void>;
|
||||
onRequest?: AlovaOptions<AG>['beforeRequest'];
|
||||
/**
|
||||
* The hook to check backend response is success or not
|
||||
*
|
||||
* @param response Axios response
|
||||
* @param response alova response
|
||||
*/
|
||||
isBackendSuccess?: (response: Response) => Promise<boolean>;
|
||||
isBackendSuccess: (response: AG['Response']) => Promise<boolean>;
|
||||
|
||||
/** The hook to refresh token */
|
||||
refreshTokenHandler?: () => Promise<void>;
|
||||
/**
|
||||
* The hook after backend request fail
|
||||
*
|
||||
* For example: You can handle the expired token in this hook
|
||||
*
|
||||
* @param response Axios response
|
||||
* @param instance Axios instance
|
||||
*/
|
||||
onBackendFail?: (response: Response) => Promise<Response | null> | Promise<void>;
|
||||
/** The config to refresh token */
|
||||
tokenRefresher?: {
|
||||
/** detect the token is expired */
|
||||
isExpired(response: AG['Response'], Method: Method<AG>): Promise<boolean> | boolean;
|
||||
/** refhresh token handler */
|
||||
handler(response: AG['Response'], Method: Method<AG>): Promise<void>;
|
||||
};
|
||||
|
||||
/** The hook after backend request complete */
|
||||
onComplete?: ResponseCompleteHandler<AG>;
|
||||
|
||||
/**
|
||||
@ -54,11 +42,11 @@ export interface RequestOptions<AG extends AlovaGenerics> {
|
||||
*
|
||||
* @param error
|
||||
*/
|
||||
onError?: ResponseErrorHandler<AG>;
|
||||
onError?: (error: any, response: AG['Response'] | null, methodInstance: Method<AG>) => any | Promise<any>;
|
||||
/**
|
||||
* transform backend response when the responseType is json
|
||||
*
|
||||
* @param response Axios response
|
||||
* @param response alova response
|
||||
*/
|
||||
transformBackendResponse?: (response: AG['Response']) => any | Promise<any>;
|
||||
transformBackendResponse: (response: AG['Response']) => any;
|
||||
}
|
||||
|
@ -17,14 +17,7 @@ export type TableColumnCheck = {
|
||||
|
||||
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 Transformer<T, Response> = (response: Response) => TableDataWithIndex<T>[];
|
||||
|
||||
export type TableConfig<A extends ApiFn, T, C> = {
|
||||
/** api function to get table data */
|
||||
@ -32,7 +25,7 @@ export type TableConfig<A extends ApiFn, T, C> = {
|
||||
/** api params */
|
||||
apiParams?: Parameters<A>[0];
|
||||
/** transform api response to table data */
|
||||
transformer: Transformer<T, Awaited<ReturnType<A>>>;
|
||||
transformer: Transformer<T, Awaited<ReturnType<ReturnType<A>['send']>>>;
|
||||
/** columns factory */
|
||||
columns: () => C[];
|
||||
/**
|
||||
@ -47,12 +40,6 @@ export type TableConfig<A extends ApiFn, T, C> = {
|
||||
* @param columns
|
||||
*/
|
||||
getColumns: (columns: C[], checks: TableColumnCheck[]) => C[];
|
||||
/**
|
||||
* callback when response fetched
|
||||
*
|
||||
* @param transformed transformed data
|
||||
*/
|
||||
onFetched?: (transformed: TransformedData<T>) => MaybePromise<void>;
|
||||
/**
|
||||
* whether to get data immediately
|
||||
*
|
||||
@ -74,17 +61,17 @@ export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<
|
||||
|
||||
const columns = computed(() => getColumns(allColumns.value, columnChecks.value));
|
||||
|
||||
const states = usePagination(
|
||||
(page, pageSize) => apiFn({ ...formatSearchParams(searchParams), page, size: pageSize }),
|
||||
const states = usePagination<ReturnType<A> extends Method<infer AG> ? AG : never, ReturnType<typeof transformer>>(
|
||||
(page, size) => apiFn({ ...formatSearchParams(searchParams), page, size }) as any,
|
||||
{
|
||||
immediate,
|
||||
data: transformer,
|
||||
total: res => res.data.total
|
||||
total: res => res.total
|
||||
}
|
||||
).onSuccess(event => {
|
||||
setEmpty(event.data.length === 0);
|
||||
).onSuccess(({ data }) => {
|
||||
setEmpty(data.length === 0);
|
||||
});
|
||||
delete states.uploading;
|
||||
Reflect.deleteProperty(states, 'uploading');
|
||||
|
||||
function reloadColumns() {
|
||||
allColumns.value = config.columns();
|
||||
|
@ -23,9 +23,6 @@ importers:
|
||||
'@sa/alova':
|
||||
specifier: workspace:*
|
||||
version: link:packages/alova
|
||||
'@sa/axios':
|
||||
specifier: workspace:*
|
||||
version: link:packages/axios
|
||||
'@sa/color':
|
||||
specifier: workspace:*
|
||||
version: link:packages/color
|
||||
@ -41,9 +38,6 @@ importers:
|
||||
'@vueuse/core':
|
||||
specifier: 11.0.3
|
||||
version: 11.0.3(vue@3.5.3(typescript@5.5.4))
|
||||
alova:
|
||||
specifier: ^3.0.16
|
||||
version: 3.0.16
|
||||
clipboard:
|
||||
specifier: 2.0.11
|
||||
version: 2.0.11
|
||||
@ -223,8 +217,8 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: link:../utils
|
||||
alova:
|
||||
specifier: ^3.0.16
|
||||
version: 3.0.16
|
||||
specifier: ^3.0.19
|
||||
version: 3.0.19
|
||||
|
||||
packages/axios:
|
||||
dependencies:
|
||||
@ -1601,8 +1595,8 @@ packages:
|
||||
resolution: {integrity: sha512-GrTZLRpmp6wIC2ztrWW9MjjTgSKccffgFagbNDOX95/dcjEcYZibYTeaOntySQLcdw1ztBoFkviiUvTMbb9MYg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
alova@3.0.16:
|
||||
resolution: {integrity: sha512-iCMi/MFAyEU9ukt2Hy/htshT2gpDTa5L4b9IZzrtu/O1Oevj6zES/uC49hNEBuZWyBBky3M6tvynurMYusfFYA==}
|
||||
alova@3.0.19:
|
||||
resolution: {integrity: sha512-G8YEuGn06vwg/B8mvyfRfMtxq8S8t88TwdAPlncyvUKOG4Hz1rKc4aH++QF9H+9coCVTKQzDbE4pWrgl3I0kBw==}
|
||||
engines: {node: '>= 18.0.0'}
|
||||
|
||||
amdefine@1.0.1:
|
||||
@ -6469,7 +6463,7 @@ snapshots:
|
||||
longest: 1.0.1
|
||||
repeat-string: 1.6.1
|
||||
|
||||
alova@3.0.16:
|
||||
alova@3.0.19:
|
||||
dependencies:
|
||||
'@alova/shared': 1.0.5
|
||||
rate-limiter-flexible: 5.0.3
|
||||
|
@ -7,10 +7,10 @@ import { useAppStore } from '@/store/modules/app';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
type TableData = NaiveUI.TableData;
|
||||
type GetTableData<A extends NaiveUI.TableApiFn> = NaiveUI.GetTableData<A>;
|
||||
type GetTableData<A extends NaiveUI.TableAlovaApiFn> = NaiveUI.GetTableData<A>;
|
||||
type TableColumn<T> = NaiveUI.TableColumn<T>;
|
||||
|
||||
export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTableConfig<A>) {
|
||||
export function useTable<A extends NaiveUI.TableAlovaApiFn>(config: NaiveUI.NaiveTableConfig<A>) {
|
||||
const scope = effectScope();
|
||||
const appStore = useAppStore();
|
||||
|
||||
@ -21,8 +21,7 @@ export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTabl
|
||||
const SELECTION_KEY = '__selection__';
|
||||
|
||||
const EXPAND_KEY = '__expand__';
|
||||
|
||||
const { reloadColumns, page, pageSize, total, ...rest } = useHookTable<
|
||||
const { reloadColumns, page, pageSize, total, getData, update, ...rest } = useHookTable<
|
||||
A,
|
||||
GetTableData<A>,
|
||||
TableColumn<NaiveUI.TableDataWithIndex<GetTableData<A>>>
|
||||
@ -31,17 +30,15 @@ export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTabl
|
||||
apiParams,
|
||||
columns: config.columns,
|
||||
transformer: res => {
|
||||
const { records = [], current = 1, size = 10 } = res.data || {};
|
||||
const { records = [], current = 1, size = 10 } = res || {};
|
||||
|
||||
// Ensure that the size is greater than 0, If it is less than 0, it will cause paging calculation errors.
|
||||
const pageSizeValue = size <= 0 ? 10 : size;
|
||||
|
||||
return records.map((item, index) => {
|
||||
return {
|
||||
...item,
|
||||
index: (current - 1) * pageSizeValue + index + 1
|
||||
};
|
||||
});
|
||||
return records.map((item, index) => ({
|
||||
...item,
|
||||
index: (current - 1) * pageSizeValue + index + 1
|
||||
}));
|
||||
},
|
||||
getColumnChecks: cols => {
|
||||
const checks: NaiveUI.TableColumnCheck[] = [];
|
||||
@ -129,16 +126,16 @@ export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTabl
|
||||
return p;
|
||||
});
|
||||
|
||||
function updatePagination(update: Partial<PaginationProps>) {
|
||||
(['page', 'pageSize', 'itemCount'] as const).forEach(key => {
|
||||
update[key] &&
|
||||
({
|
||||
page,
|
||||
pageSize,
|
||||
itemCount: total
|
||||
}[key].value = update[key]);
|
||||
function updatePagination(updateProps: Partial<PaginationProps>) {
|
||||
const innerPageStates = ['page', 'pageSize', 'itemCount'] as const;
|
||||
innerPageStates.forEach(key => {
|
||||
if (updateProps[key]) {
|
||||
update({
|
||||
[key]: updateProps[key]
|
||||
});
|
||||
}
|
||||
});
|
||||
Object.assign(paginationBase, update);
|
||||
Object.assign(paginationBase, updateProps);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -148,6 +145,7 @@ export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTabl
|
||||
*/
|
||||
async function getDataByPage(pageNum: number = 1) {
|
||||
page.value = pageNum;
|
||||
return getData();
|
||||
}
|
||||
|
||||
scope.run(() => {
|
||||
@ -165,6 +163,7 @@ export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTabl
|
||||
|
||||
return {
|
||||
...rest,
|
||||
getData,
|
||||
reloadColumns,
|
||||
pagination,
|
||||
mobilePagination,
|
||||
@ -173,7 +172,7 @@ export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTabl
|
||||
};
|
||||
}
|
||||
|
||||
export function useTableOperate<T extends TableData = TableData>(data: Ref<T[]>, getData: () => Promise<void>) {
|
||||
export function useTableOperate<T extends TableData = TableData>(data: Ref<T[]>, getData: () => Promise<void> | void) {
|
||||
const { bool: drawerVisible, setTrue: openDrawer, setFalse: closeDrawer } = useBoolean();
|
||||
|
||||
const operateType = ref<NaiveUI.TableOperateType>('add');
|
||||
|
@ -39,5 +39,8 @@ export function fetchRefreshToken(refreshToken: string) {
|
||||
* @param msg error message
|
||||
*/
|
||||
export function fetchCustomBackendError(code: string, msg: string) {
|
||||
return alova.Get('/auth/error', { params: { code, msg } });
|
||||
return alova.Get('/auth/error', {
|
||||
params: { code, msg },
|
||||
shareRequest: false
|
||||
});
|
||||
}
|
||||
|
@ -1,175 +1,19 @@
|
||||
import type { AxiosResponse } from 'axios';
|
||||
import { BACKEND_ERROR_CODE, createFlatRequest, createRequest } from '@sa/axios';
|
||||
import { createAlovaRequest } from '@sa/alova';
|
||||
import { useAuthStore } from '@/store/modules/auth';
|
||||
import { $t } from '@/locales';
|
||||
import { localStg } from '@/utils/storage';
|
||||
import { getServiceBaseURL } from '@/utils/service';
|
||||
import { getAuthorization, handleExpiredRequest, handleRefreshToken, showErrorMsg } from './shared';
|
||||
import { getAuthorization, handleRefreshToken, showErrorMsg } from './shared';
|
||||
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>(
|
||||
{
|
||||
baseURL,
|
||||
headers: {
|
||||
apifoxToken: 'XL299LiMEDZ0H5h3A29PxwQXdMJqWyY2'
|
||||
}
|
||||
},
|
||||
{
|
||||
async onRequest(config) {
|
||||
const Authorization = getAuthorization();
|
||||
Object.assign(config.headers, { Authorization });
|
||||
|
||||
return config;
|
||||
},
|
||||
isBackendSuccess(response) {
|
||||
// when the backend response code is "0000"(default), it means the request is success
|
||||
// to change this logic by yourself, you can modify the `VITE_SERVICE_SUCCESS_CODE` in `.env` file
|
||||
return String(response.data.code) === import.meta.env.VITE_SERVICE_SUCCESS_CODE;
|
||||
},
|
||||
async onBackendFail(response, instance) {
|
||||
const authStore = useAuthStore();
|
||||
const responseCode = String(response.data.code);
|
||||
|
||||
function handleLogout() {
|
||||
authStore.resetStore();
|
||||
}
|
||||
|
||||
function logoutAndCleanup() {
|
||||
handleLogout();
|
||||
window.removeEventListener('beforeunload', handleLogout);
|
||||
|
||||
request.state.errMsgStack = request.state.errMsgStack.filter(msg => msg !== response.data.msg);
|
||||
}
|
||||
|
||||
// when the backend response code is in `logoutCodes`, it means the user will be logged out and redirected to login page
|
||||
const logoutCodes = import.meta.env.VITE_SERVICE_LOGOUT_CODES?.split(',') || [];
|
||||
if (logoutCodes.includes(responseCode)) {
|
||||
handleLogout();
|
||||
return null;
|
||||
}
|
||||
|
||||
// when the backend response code is in `modalLogoutCodes`, it means the user will be logged out by displaying a modal
|
||||
const modalLogoutCodes = import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES?.split(',') || [];
|
||||
if (modalLogoutCodes.includes(responseCode) && !request.state.errMsgStack?.includes(response.data.msg)) {
|
||||
request.state.errMsgStack = [...(request.state.errMsgStack || []), response.data.msg];
|
||||
|
||||
// prevent the user from refreshing the page
|
||||
window.addEventListener('beforeunload', handleLogout);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// when the backend response code is in `expiredTokenCodes`, it means the token is expired, and refresh token
|
||||
// the api `refreshToken` can not return error code in `expiredTokenCodes`, otherwise it will be a dead loop, should return `logoutCodes` or `modalLogoutCodes`
|
||||
const expiredTokenCodes = import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES?.split(',') || [];
|
||||
if (expiredTokenCodes.includes(responseCode)) {
|
||||
const success = await handleExpiredRequest(request.state);
|
||||
if (success) {
|
||||
const Authorization = getAuthorization();
|
||||
Object.assign(response.config.headers, { Authorization });
|
||||
|
||||
return instance.request(response.config) as Promise<AxiosResponse>;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
transformBackendResponse(response) {
|
||||
return response.data.data;
|
||||
},
|
||||
onError(error) {
|
||||
// when the request is fail, you can show error message
|
||||
|
||||
let message = error.message;
|
||||
let backendErrorCode = '';
|
||||
|
||||
// get backend error message and code
|
||||
if (error.code === BACKEND_ERROR_CODE) {
|
||||
message = error.response?.data?.msg || message;
|
||||
backendErrorCode = String(error.response?.data?.code || '');
|
||||
}
|
||||
|
||||
// the error message is displayed in the modal
|
||||
const modalLogoutCodes = import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES?.split(',') || [];
|
||||
if (modalLogoutCodes.includes(backendErrorCode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// when the token is expired, refresh token and retry request, so no need to show error message
|
||||
const expiredTokenCodes = import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES?.split(',') || [];
|
||||
if (expiredTokenCodes.includes(backendErrorCode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
showErrorMsg(request.state, message);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const demoRequest = createRequest<App.Service.DemoResponse>(
|
||||
{
|
||||
baseURL: otherBaseURL.demo
|
||||
},
|
||||
{
|
||||
async onRequest(config) {
|
||||
const { headers } = config;
|
||||
|
||||
// set token
|
||||
const token = localStg.get('token');
|
||||
const Authorization = token ? `Bearer ${token}` : null;
|
||||
Object.assign(headers, { Authorization });
|
||||
|
||||
return config;
|
||||
},
|
||||
isBackendSuccess(response) {
|
||||
// when the backend response code is "200", it means the request is success
|
||||
// you can change this logic by yourself
|
||||
return response.data.status === '200';
|
||||
},
|
||||
async onBackendFail(_response) {
|
||||
// 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
|
||||
|
||||
let message = error.message;
|
||||
|
||||
// show backend error message
|
||||
if (error.code === BACKEND_ERROR_CODE) {
|
||||
message = error.response?.data?.message || message;
|
||||
}
|
||||
|
||||
window.$message?.error(message);
|
||||
}
|
||||
}
|
||||
);
|
||||
const { baseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
|
||||
|
||||
const state: RequestInstanceState = {
|
||||
errMsgStack: []
|
||||
};
|
||||
export const alova = createAlovaRequest(
|
||||
{
|
||||
baseURL,
|
||||
expiredTokenCodes: import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES?.split(',') || []
|
||||
baseURL
|
||||
},
|
||||
{
|
||||
onRequest({ config }) {
|
||||
@ -177,8 +21,15 @@ export const alova = createAlovaRequest(
|
||||
config.headers.Authorization = Authorization;
|
||||
config.headers.apifoxToken = 'XL299LiMEDZ0H5h3A29PxwQXdMJqWyY2';
|
||||
},
|
||||
async refreshTokenHandler() {
|
||||
await handleRefreshToken();
|
||||
tokenRefresher: {
|
||||
async isExpired(response) {
|
||||
const expiredTokenCodes = import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES?.split(',') || [];
|
||||
const { code } = await response.clone().json();
|
||||
return expiredTokenCodes.includes(String(code));
|
||||
},
|
||||
async handler() {
|
||||
await handleRefreshToken();
|
||||
}
|
||||
},
|
||||
async isBackendSuccess(response) {
|
||||
// when the backend response code is "0000"(default), it means the request is success
|
||||
@ -187,42 +38,49 @@ export const alova = createAlovaRequest(
|
||||
const data = await resp.json();
|
||||
return String(data.code) === import.meta.env.VITE_SERVICE_SUCCESS_CODE;
|
||||
},
|
||||
async onBackendFail(response) {
|
||||
async transformBackendResponse(response) {
|
||||
return (await response.clone().json()).data;
|
||||
},
|
||||
async onError(error, response) {
|
||||
const authStore = useAuthStore();
|
||||
const resp = response.clone();
|
||||
const data = await resp.json();
|
||||
|
||||
const responseCode = String(data.code);
|
||||
let message = error.message;
|
||||
let responseCode = '';
|
||||
if (response) {
|
||||
const data = await response?.clone().json();
|
||||
message = data.msg;
|
||||
responseCode = String(data.code);
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
showErrorMsg(state, message);
|
||||
authStore.resetStore();
|
||||
}
|
||||
|
||||
function logoutAndCleanup() {
|
||||
handleLogout();
|
||||
window.removeEventListener('beforeunload', handleLogout);
|
||||
|
||||
request.state.errMsgStack = request.state.errMsgStack.filter(msg => msg !== data.msg);
|
||||
state.errMsgStack = state.errMsgStack.filter(msg => msg !== message);
|
||||
}
|
||||
|
||||
// when the backend response code is in `logoutCodes`, it means the user will be logged out and redirected to login page
|
||||
const logoutCodes = import.meta.env.VITE_SERVICE_LOGOUT_CODES?.split(',') || [];
|
||||
if (logoutCodes.includes(responseCode)) {
|
||||
handleLogout();
|
||||
return null;
|
||||
throw error;
|
||||
}
|
||||
|
||||
// when the backend response code is in `modalLogoutCodes`, it means the user will be logged out by displaying a modal
|
||||
const modalLogoutCodes = import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES?.split(',') || [];
|
||||
if (modalLogoutCodes.includes(responseCode) && !request.state.errMsgStack?.includes(data.msg)) {
|
||||
request.state.errMsgStack = [...(request.state.errMsgStack || []), data.msg];
|
||||
if (modalLogoutCodes.includes(responseCode) && !state.errMsgStack?.includes(message)) {
|
||||
state.errMsgStack = [...(state.errMsgStack || []), message];
|
||||
|
||||
// prevent the user from refreshing the page
|
||||
window.addEventListener('beforeunload', handleLogout);
|
||||
|
||||
window.$dialog?.error({
|
||||
title: $t('common.error'),
|
||||
content: data.msg,
|
||||
content: message,
|
||||
positiveText: $t('common.confirm'),
|
||||
maskClosable: false,
|
||||
closeOnEsc: false,
|
||||
@ -233,39 +91,10 @@ export const alova = createAlovaRequest(
|
||||
logoutAndCleanup();
|
||||
}
|
||||
});
|
||||
|
||||
return null;
|
||||
throw error;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
transformBackendResponse(response) {
|
||||
return response.json();
|
||||
},
|
||||
onError(error) {
|
||||
// when the request is fail, you can show error message
|
||||
|
||||
let message = error.message;
|
||||
let backendErrorCode = '';
|
||||
|
||||
// get backend error message and code
|
||||
if (error.code === BACKEND_ERROR_CODE) {
|
||||
message = error.response?.data?.msg || message;
|
||||
backendErrorCode = String(error.response?.data?.code || '');
|
||||
}
|
||||
|
||||
// the error message is displayed in the modal
|
||||
const modalLogoutCodes = import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES?.split(',') || [];
|
||||
if (modalLogoutCodes.includes(backendErrorCode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// when the token is expired, refresh token and retry request, so no need to show error message
|
||||
const expiredTokenCodes = import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES?.split(',') || [];
|
||||
if (expiredTokenCodes.includes(backendErrorCode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
showErrorMsg(request.state, message);
|
||||
showErrorMsg(state, message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -15,30 +15,19 @@ export async function handleRefreshToken() {
|
||||
const { resetStore } = useAuthStore();
|
||||
|
||||
const rToken = localStg.get('refreshToken') || '';
|
||||
const { error, data } = await fetchRefreshToken(rToken);
|
||||
if (!error) {
|
||||
const refreshTokenMethod = fetchRefreshToken(rToken);
|
||||
|
||||
// set the refreshToken role, so that the request will not be intercepted
|
||||
refreshTokenMethod.meta.authRole = 'refreshToken';
|
||||
|
||||
try {
|
||||
const data = await refreshTokenMethod;
|
||||
localStg.set('token', data.token);
|
||||
localStg.set('refreshToken', data.refreshToken);
|
||||
return true;
|
||||
} catch (error) {
|
||||
resetStore();
|
||||
throw error;
|
||||
}
|
||||
|
||||
resetStore();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function handleExpiredRequest(state: RequestInstanceState) {
|
||||
if (!state.refreshTokenFn) {
|
||||
state.refreshTokenFn = handleRefreshToken();
|
||||
}
|
||||
|
||||
const success = await state.refreshTokenFn;
|
||||
|
||||
setTimeout(() => {
|
||||
state.refreshTokenFn = null;
|
||||
}, 1000);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
export function showErrorMsg(state: RequestInstanceState, message: string) {
|
||||
|
@ -1,6 +1,4 @@
|
||||
export interface RequestInstanceState {
|
||||
/** whether the request is refreshing token */
|
||||
refreshTokenFn: Promise<boolean> | null;
|
||||
/** the request error message stack */
|
||||
errMsgStack: string[];
|
||||
}
|
||||
|
@ -63,9 +63,8 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
||||
async function login(userName: string, password: string, redirect = true) {
|
||||
startLoading();
|
||||
|
||||
const { data: loginToken, error } = await fetchLogin(userName, password);
|
||||
|
||||
if (!error) {
|
||||
try {
|
||||
const loginToken = await fetchLogin(userName, password);
|
||||
const pass = await loginByToken(loginToken);
|
||||
|
||||
if (pass) {
|
||||
@ -81,7 +80,7 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
} catch (err) {
|
||||
resetStore();
|
||||
}
|
||||
|
||||
@ -106,15 +105,13 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
||||
}
|
||||
|
||||
async function getUserInfo() {
|
||||
const { data: info, error } = await fetchGetUserInfo();
|
||||
|
||||
if (!error) {
|
||||
try {
|
||||
const info = await fetchGetUserInfo();
|
||||
// update store
|
||||
Object.assign(userInfo, info);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} catch (error) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
1
src/typings/components.d.ts
vendored
1
src/typings/components.d.ts
vendored
@ -87,6 +87,7 @@ declare module 'vue' {
|
||||
NSelect: typeof import('naive-ui')['NSelect']
|
||||
NSkeleton: typeof import('naive-ui')['NSkeleton']
|
||||
NSpace: typeof import('naive-ui')['NSpace']
|
||||
NSpin: typeof import('naive-ui')['NSpin']
|
||||
NStatistic: typeof import('naive-ui')['NStatistic']
|
||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
NTab: typeof import('naive-ui')['NTab']
|
||||
|
9
src/typings/naive-ui.d.ts
vendored
9
src/typings/naive-ui.d.ts
vendored
@ -9,7 +9,6 @@ declare namespace NaiveUI {
|
||||
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
|
||||
@ -26,9 +25,9 @@ declare namespace NaiveUI {
|
||||
|
||||
type TableColumn<T> = TableColumnWithKey<T> | DataTableSelectionColumn<T> | DataTableExpandColumn<T>;
|
||||
|
||||
type TableApiFn<T = any, R = Api.Common.CommonSearchParams> = (
|
||||
type TableAlovaApiFn<T = any, R = Api.Common.CommonSearchParams> = (
|
||||
params: R
|
||||
) => Promise<FlatResponseData<Api.Common.PaginatingQueryRecord<T>>>;
|
||||
) => import('@sa/alova').Method<import('@sa/alova').AlovaGenerics<Api.Common.PaginatingQueryRecord<T>>>;
|
||||
|
||||
/**
|
||||
* the type of table operation
|
||||
@ -38,9 +37,9 @@ declare namespace NaiveUI {
|
||||
*/
|
||||
type TableOperateType = 'add' | 'edit';
|
||||
|
||||
type GetTableData<A extends TableApiFn> = A extends TableApiFn<infer T> ? T : never;
|
||||
type GetTableData<A extends TableAlovaApiFn> = A extends TableAlovaApiFn<infer T> ? T : never;
|
||||
|
||||
type NaiveTableConfig<A extends TableApiFn> = Pick<
|
||||
type NaiveTableConfig<A extends TableAlovaApiFn> = Pick<
|
||||
import('@sa/hooks').TableConfig<A, GetTableData<A>, TableColumn<TableDataWithIndex<GetTableData<A>>>>,
|
||||
'apiFn' | 'apiParams' | 'columns' | 'immediate'
|
||||
> & {
|
||||
|
@ -3,7 +3,7 @@ import { ref } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import { NButton, NPopconfirm, NTag } from 'naive-ui';
|
||||
import { useBoolean } from '@sa/hooks';
|
||||
import { fetchGetAllPages, fetchGetMenuList } from '@/service/api';
|
||||
import { fetchGetMenuList } from '@/service/api';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useTable, useTableOperate } from '@/hooks/common/table';
|
||||
import { $t } from '@/locales';
|
||||
@ -18,7 +18,7 @@ const { bool: visible, setTrue: openModal } = useBoolean();
|
||||
|
||||
const wrapperRef = ref<HTMLElement | null>(null);
|
||||
|
||||
const { columns, columnChecks, data, loading, pagination, getData, getDataByPage } = useTable({
|
||||
const { columns, columnChecks, data, loading, pagination, refresh, reload, getDataByPage } = useTable({
|
||||
apiFn: fetchGetMenuList,
|
||||
columns: () => [
|
||||
{
|
||||
@ -170,7 +170,7 @@ const { columns, columnChecks, data, loading, pagination, getData, getDataByPage
|
||||
]
|
||||
});
|
||||
|
||||
const { checkedRowKeys, onBatchDeleted, onDeleted } = useTableOperate(data, getData);
|
||||
const { checkedRowKeys, onBatchDeleted, onDeleted } = useTableOperate(data, reload);
|
||||
|
||||
const operateType = ref<OperateType>('add');
|
||||
|
||||
@ -210,20 +210,6 @@ function handleAddChildMenu(item: Api.SystemManage.Menu) {
|
||||
|
||||
openModal();
|
||||
}
|
||||
|
||||
const allPages = ref<string[]>([]);
|
||||
|
||||
async function getAllPages() {
|
||||
const { data: pages } = await fetchGetAllPages();
|
||||
allPages.value = pages || [];
|
||||
}
|
||||
|
||||
function init() {
|
||||
getAllPages();
|
||||
}
|
||||
|
||||
// init
|
||||
init();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -236,7 +222,7 @@ init();
|
||||
:loading="loading"
|
||||
@add="handleAdd"
|
||||
@delete="handleBatchDelete"
|
||||
@refresh="getData"
|
||||
@refresh="refresh"
|
||||
/>
|
||||
</template>
|
||||
<NDataTable
|
||||
@ -256,7 +242,6 @@ init();
|
||||
v-model:visible="visible"
|
||||
:operate-type="operateType"
|
||||
:row-data="editingData"
|
||||
:all-pages="allPages"
|
||||
@submitted="getDataByPage"
|
||||
/>
|
||||
</NCard>
|
||||
|
@ -1,12 +1,13 @@
|
||||
<script setup lang="tsx">
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { computed, reactive, watch } from 'vue';
|
||||
import type { SelectOption } from 'naive-ui';
|
||||
import { useWatcher } from '@sa/alova/client';
|
||||
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
|
||||
import { $t } from '@/locales';
|
||||
import { enableStatusOptions, menuIconTypeOptions, menuTypeOptions } from '@/constants/business';
|
||||
import SvgIcon from '@/components/custom/svg-icon.vue';
|
||||
import { getLocalIcons } from '@/utils/icon';
|
||||
import { fetchGetAllRoles } from '@/service/api';
|
||||
import { fetchGetAllPages } from '@/service/api';
|
||||
import {
|
||||
getLayoutAndPage,
|
||||
getPathParamFromRoutePath,
|
||||
@ -26,8 +27,6 @@ interface Props {
|
||||
operateType: OperateType;
|
||||
/** the edit menu data or the parent menu data when adding a child menu */
|
||||
rowData?: Api.SystemManage.Menu | null;
|
||||
/** all pages */
|
||||
allPages: string[];
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
@ -138,8 +137,14 @@ const showLayout = computed(() => model.parentId === 0);
|
||||
|
||||
const showPage = computed(() => model.menuType === '2');
|
||||
|
||||
const { data: allPagesRaw, loading: loadingPages } = useWatcher(fetchGetAllPages, [visible], {
|
||||
initialData: [],
|
||||
middleware(_, next) {
|
||||
return visible.value ? next() : undefined;
|
||||
}
|
||||
});
|
||||
const pageOptions = computed(() => {
|
||||
const allPages = [...props.allPages];
|
||||
const allPages = [...allPagesRaw.value];
|
||||
|
||||
if (model.routeName && !allPages.includes(model.routeName)) {
|
||||
allPages.unshift(model.routeName);
|
||||
@ -165,20 +170,18 @@ const layoutOptions: CommonType.Option[] = [
|
||||
];
|
||||
|
||||
/** the enabled role options */
|
||||
const roleOptions = ref<CommonType.Option<string>[]>([]);
|
||||
|
||||
async function getRoleOptions() {
|
||||
const { error, data } = await fetchGetAllRoles();
|
||||
|
||||
if (!error) {
|
||||
const options = data.map(item => ({
|
||||
label: item.roleName,
|
||||
value: item.roleCode
|
||||
}));
|
||||
|
||||
roleOptions.value = [...options];
|
||||
}
|
||||
}
|
||||
// const { data: roleOptionsRaw, loading } = useWatcher(fetchGetAllRoles, [visible], {
|
||||
// initialData: [],
|
||||
// middleware(_, next) {
|
||||
// return visible.value ? next() : undefined;
|
||||
// }
|
||||
// });
|
||||
// const roleOptions = computed<CommonType.Option<string>[]>(() => {
|
||||
// return roleOptionsRaw.value.map(item => ({
|
||||
// label: item.roleName,
|
||||
// value: item.roleCode
|
||||
// }));
|
||||
// });
|
||||
|
||||
function handleInitModel() {
|
||||
Object.assign(model, createDefaultModel());
|
||||
@ -266,7 +269,6 @@ watch(visible, () => {
|
||||
if (visible.value) {
|
||||
handleInitModel();
|
||||
restoreValidation();
|
||||
getRoleOptions();
|
||||
}
|
||||
});
|
||||
|
||||
@ -311,6 +313,7 @@ watch(
|
||||
<NFormItemGi v-if="showPage" span="24 m:12" :label="$t('page.manage.menu.page')" path="page">
|
||||
<NSelect
|
||||
v-model:value="model.page"
|
||||
:loading="loadingPages"
|
||||
:options="pageOptions"
|
||||
:placeholder="$t('page.manage.menu.form.page')"
|
||||
/>
|
||||
|
@ -15,7 +15,8 @@ const {
|
||||
columnChecks,
|
||||
data,
|
||||
loading,
|
||||
getData,
|
||||
reload,
|
||||
refresh,
|
||||
getDataByPage,
|
||||
mobilePagination,
|
||||
searchParams,
|
||||
@ -116,7 +117,7 @@ const {
|
||||
onBatchDeleted,
|
||||
onDeleted
|
||||
// closeDrawer
|
||||
} = useTableOperate(data, getData);
|
||||
} = useTableOperate(data, reload);
|
||||
|
||||
async function handleBatchDelete() {
|
||||
// request
|
||||
@ -148,7 +149,7 @@ function edit(id: number) {
|
||||
:loading="loading"
|
||||
@add="handleAdd"
|
||||
@delete="handleBatchDelete"
|
||||
@refresh="getData"
|
||||
@refresh="refresh"
|
||||
/>
|
||||
</template>
|
||||
<NDataTable
|
||||
|
@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, shallowRef, watch } from 'vue';
|
||||
import { useWatcher } from '@sa/alova/client';
|
||||
import { $t } from '@/locales';
|
||||
import { fetchGetAllPages, fetchGetMenuTree } from '@/service/api';
|
||||
|
||||
@ -38,16 +39,12 @@ async function updateHome(val: string) {
|
||||
home.value = val;
|
||||
}
|
||||
|
||||
const pages = shallowRef<string[]>([]);
|
||||
|
||||
async function getPages() {
|
||||
const { error, data } = await fetchGetAllPages();
|
||||
|
||||
if (!error) {
|
||||
pages.value = data;
|
||||
const { data: pages, loading: loadingPages } = useWatcher(fetchGetAllPages, [visible], {
|
||||
initialData: [],
|
||||
middleware(_, next) {
|
||||
return visible.value ? next() : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
const pageSelectOptions = computed(() => {
|
||||
const opts: CommonType.Option[] = pages.value.map(page => ({
|
||||
label: page,
|
||||
@ -57,15 +54,12 @@ const pageSelectOptions = computed(() => {
|
||||
return opts;
|
||||
});
|
||||
|
||||
const tree = shallowRef<Api.SystemManage.MenuTree[]>([]);
|
||||
|
||||
async function getTree() {
|
||||
const { error, data } = await fetchGetMenuTree();
|
||||
|
||||
if (!error) {
|
||||
tree.value = data;
|
||||
const { data: tree, loading: loadingTree } = useWatcher(fetchGetMenuTree, [visible], {
|
||||
initialData: [],
|
||||
middleware(_, next) {
|
||||
return visible.value ? next() : undefined;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const checks = shallowRef<number[]>([]);
|
||||
|
||||
@ -86,8 +80,6 @@ function handleSubmit() {
|
||||
|
||||
function init() {
|
||||
getHome();
|
||||
getPages();
|
||||
getTree();
|
||||
getChecks();
|
||||
}
|
||||
|
||||
@ -102,7 +94,14 @@ watch(visible, val => {
|
||||
<NModal v-model:show="visible" :title="title" preset="card" class="w-480px">
|
||||
<div class="flex-y-center gap-16px pb-12px">
|
||||
<div>{{ $t('page.manage.menu.home') }}</div>
|
||||
<NSelect :value="home" :options="pageSelectOptions" size="small" class="w-160px" @update:value="updateHome" />
|
||||
<NSelect
|
||||
:loading="loadingPages"
|
||||
:value="home"
|
||||
:options="pageSelectOptions"
|
||||
size="small"
|
||||
class="w-160px"
|
||||
@update:value="updateHome"
|
||||
/>
|
||||
</div>
|
||||
<NTree
|
||||
v-model:checked-keys="checks"
|
||||
@ -113,7 +112,11 @@ watch(visible, val => {
|
||||
virtual-scroll
|
||||
block-line
|
||||
class="h-280px"
|
||||
/>
|
||||
>
|
||||
<template v-if="loadingTree" #empty>
|
||||
<NSpin size="small"></NSpin>
|
||||
</template>
|
||||
</NTree>
|
||||
<template #footer>
|
||||
<NSpace justify="end">
|
||||
<NButton size="small" class="mt-16px" @click="closeModal">
|
||||
|
@ -10,7 +10,7 @@ import UserSearch from './modules/user-search.vue';
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
const { columns, columnChecks, data, getData, reload, loading, mobilePagination, searchParams, resetSearchParams } =
|
||||
const { columns, columnChecks, data, refresh, reload, loading, mobilePagination, searchParams, resetSearchParams } =
|
||||
useTable({
|
||||
apiFn: fetchGetUserList,
|
||||
showTotal: true,
|
||||
@ -136,7 +136,7 @@ const {
|
||||
onBatchDeleted,
|
||||
onDeleted
|
||||
// closeDrawer
|
||||
} = useTableOperate(data, getData);
|
||||
} = useTableOperate(data, reload);
|
||||
|
||||
async function handleBatchDelete() {
|
||||
// request
|
||||
@ -168,7 +168,7 @@ function edit(id: number) {
|
||||
:loading="loading"
|
||||
@add="handleAdd"
|
||||
@delete="handleBatchDelete"
|
||||
@refresh="getData"
|
||||
@refresh="refresh"
|
||||
/>
|
||||
</template>
|
||||
<NDataTable
|
||||
|
@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { computed, reactive, watch } from 'vue';
|
||||
import { useWatcher } from '@sa/alova/client';
|
||||
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
|
||||
import { fetchGetAllRoles } from '@/service/api';
|
||||
import { $t } from '@/locales';
|
||||
@ -66,28 +67,28 @@ const rules: Record<RuleKey, App.Global.FormRule> = {
|
||||
};
|
||||
|
||||
/** the enabled role options */
|
||||
const roleOptions = ref<CommonType.Option<string>[]>([]);
|
||||
|
||||
async function getRoleOptions() {
|
||||
const { error, data } = await fetchGetAllRoles();
|
||||
|
||||
if (!error) {
|
||||
const options = data.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 = model.userRoles.map(item => ({
|
||||
label: item,
|
||||
value: item
|
||||
}));
|
||||
// end
|
||||
|
||||
roleOptions.value = [...userRoleOptions, ...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 = model.userRoles.map(item => ({
|
||||
label: item,
|
||||
value: item
|
||||
}));
|
||||
// end
|
||||
|
||||
return [...userRoleOptions, ...options];
|
||||
});
|
||||
|
||||
function handleInitModel() {
|
||||
Object.assign(model, createDefaultModel());
|
||||
@ -113,7 +114,6 @@ watch(visible, () => {
|
||||
if (visible.value) {
|
||||
handleInitModel();
|
||||
restoreValidation();
|
||||
getRoleOptions();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -148,6 +148,7 @@ watch(visible, () => {
|
||||
<NSelect
|
||||
v-model:value="model.userRoles"
|
||||
multiple
|
||||
:loading="loading"
|
||||
:options="roleOptions"
|
||||
:placeholder="$t('page.manage.user.form.userRole')"
|
||||
/>
|
||||
|
Loading…
Reference in New Issue
Block a user