feat: add subpackage @sa/alova, refactor all example pages with @sa/alova

This commit is contained in:
胡镇 2024-10-10 10:24:44 +08:00
parent 3fbf2e2a5f
commit 77e17b9bd0
27 changed files with 378 additions and 566 deletions

View File

@ -51,7 +51,7 @@
"@antv/g2": "5.2.5", "@antv/g2": "5.2.5",
"@better-scroll/core": "2.5.1", "@better-scroll/core": "2.5.1",
"@iconify/vue": "4.1.2", "@iconify/vue": "4.1.2",
"@sa/axios": "workspace:*", "@sa/alova": "workspace:*",
"@sa/color": "workspace:*", "@sa/color": "workspace:*",
"@sa/hooks": "workspace:*", "@sa/hooks": "workspace:*",
"@sa/materials": "workspace:*", "@sa/materials": "workspace:*",

View File

@ -1,12 +1,13 @@
import { computed, reactive, ref } from 'vue'; import { computed, reactive, ref } from 'vue';
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import { jsonClone } from '@sa/utils'; import { jsonClone } from '@sa/utils';
import { usePagination } from '@sa/alova/client';
import type { AlovaGenerics, Method } from '@sa/alova';
import useBoolean from './use-boolean'; import useBoolean from './use-boolean';
import useLoading from './use-loading';
export type MaybePromise<T> = T | Promise<T>; export type MaybePromise<T> = T | Promise<T>;
export type ApiFn = (args: any) => Promise<unknown>; export type ApiFn = (args: any) => Method<AlovaGenerics>;
export type TableColumnCheck = { export type TableColumnCheck = {
key: string; key: string;
@ -16,14 +17,7 @@ export type TableColumnCheck = {
export type TableDataWithIndex<T> = T & { index: number }; export type TableDataWithIndex<T> = T & { index: number };
export type TransformedData<T> = { export type Transformer<T, Response> = (response: Response) => TableDataWithIndex<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> = { export type TableConfig<A extends ApiFn, T, C> = {
/** api function to get table data */ /** api function to get table data */
@ -31,7 +25,7 @@ export type TableConfig<A extends ApiFn, T, C> = {
/** api params */ /** api params */
apiParams?: Parameters<A>[0]; apiParams?: Parameters<A>[0];
/** transform api response to table data */ /** transform api response to table data */
transformer: Transformer<T, Awaited<ReturnType<A>>>; transformer: Transformer<T, Awaited<ReturnType<ReturnType<A>['send']>>>;
/** columns factory */ /** columns factory */
columns: () => C[]; columns: () => C[];
/** /**
@ -46,12 +40,6 @@ export type TableConfig<A extends ApiFn, T, C> = {
* @param columns * @param columns
*/ */
getColumns: (columns: C[], checks: TableColumnCheck[]) => C[]; 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 * whether to get data immediately
* *
@ -61,7 +49,6 @@ export type TableConfig<A extends ApiFn, T, C> = {
}; };
export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<A, T, C>) { export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<A, T, C>) {
const { loading, startLoading, endLoading } = useLoading();
const { bool: empty, setBool: setEmpty } = useBoolean(); const { bool: empty, setBool: setEmpty } = useBoolean();
const { apiFn, apiParams, transformer, immediate = true, getColumnChecks, getColumns } = config; const { apiFn, apiParams, transformer, immediate = true, getColumnChecks, getColumns } = config;
@ -70,12 +57,22 @@ export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<
const allColumns = ref(config.columns()) as Ref<C[]>; const allColumns = ref(config.columns()) as Ref<C[]>;
const data: Ref<TableDataWithIndex<T>[]> = ref([]);
const columnChecks: Ref<TableColumnCheck[]> = ref(getColumnChecks(config.columns())); const columnChecks: Ref<TableColumnCheck[]> = ref(getColumnChecks(config.columns()));
const columns = computed(() => getColumns(allColumns.value, columnChecks.value)); const columns = computed(() => getColumns(allColumns.value, columnChecks.value));
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.total
}
).onSuccess(({ data }) => {
setEmpty(data.length === 0);
});
Reflect.deleteProperty(states, 'uploading');
function reloadColumns() { function reloadColumns() {
allColumns.value = config.columns(); allColumns.value = config.columns();
@ -89,33 +86,13 @@ export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<
})); }));
} }
async function getData() {
startLoading();
const formattedParams = formatSearchParams(searchParams);
const response = await apiFn(formattedParams);
const transformed = transformer(response as Awaited<ReturnType<A>>);
data.value = transformed.data;
setEmpty(transformed.data.length === 0);
await config.onFetched?.(transformed);
endLoading();
}
function formatSearchParams(params: Record<string, unknown>) { function formatSearchParams(params: Record<string, unknown>) {
const formattedParams: Record<string, unknown> = {}; const formattedParams: Record<string, unknown> = {};
Object.entries(params).forEach(([key, value]) => { Object.entries(params).forEach(([key, value]) => {
if (value !== null && value !== undefined) { if (value !== null && value !== undefined) {
formattedParams[key] = value; formattedParams[key] = value;
} }
}); });
return formattedParams; return formattedParams;
} }
@ -133,18 +110,13 @@ export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<
Object.assign(searchParams, jsonClone(apiParams)); Object.assign(searchParams, jsonClone(apiParams));
} }
if (immediate) {
getData();
}
return { return {
loading, ...states,
empty, empty,
data,
columns, columns,
columnChecks, columnChecks,
reloadColumns, reloadColumns,
getData, getData: states.send,
searchParams, searchParams,
updateSearchParams, updateSearchParams,
resetSearchParams resetSearchParams

View File

@ -20,9 +20,9 @@ importers:
'@iconify/vue': '@iconify/vue':
specifier: 4.1.2 specifier: 4.1.2
version: 4.1.2(vue@3.5.11(typescript@5.6.3)) version: 4.1.2(vue@3.5.11(typescript@5.6.3))
'@sa/axios': '@sa/alova':
specifier: workspace:* specifier: workspace:*
version: link:packages/axios version: link:packages/alova
'@sa/color': '@sa/color':
specifier: workspace:* specifier: workspace:*
version: link:packages/color version: link:packages/color

View File

@ -7,10 +7,10 @@ import { useAppStore } from '@/store/modules/app';
import { $t } from '@/locales'; import { $t } from '@/locales';
type TableData = NaiveUI.TableData; 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>; 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 scope = effectScope();
const appStore = useAppStore(); const appStore = useAppStore();
@ -21,41 +21,24 @@ export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTabl
const SELECTION_KEY = '__selection__'; const SELECTION_KEY = '__selection__';
const EXPAND_KEY = '__expand__'; const EXPAND_KEY = '__expand__';
const { reloadColumns, page, pageSize, total, getData, update, ...rest } = useHookTable<
const { A,
loading, GetTableData<A>,
empty, TableColumn<NaiveUI.TableDataWithIndex<GetTableData<A>>>
data, >({
columns,
columnChecks,
reloadColumns,
getData,
searchParams,
updateSearchParams,
resetSearchParams
} = useHookTable<A, GetTableData<A>, TableColumn<NaiveUI.TableDataWithIndex<GetTableData<A>>>>({
apiFn, apiFn,
apiParams, apiParams,
columns: config.columns, columns: config.columns,
transformer: res => { transformer: res => {
const { records = [], current = 1, size = 10, total = 0 } = 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. // 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 pageSizeValue = size <= 0 ? 10 : size;
const recordsWithIndex = records.map((item, index) => { return records.map((item, index) => ({
return {
...item, ...item,
index: (current - 1) * pageSize + index + 1 index: (current - 1) * pageSizeValue + index + 1
}; }));
});
return {
data: recordsWithIndex,
pageNum: current,
pageSize,
total
};
}, },
getColumnChecks: cols => { getColumnChecks: cols => {
const checks: NaiveUI.TableColumnCheck[] = []; const checks: NaiveUI.TableColumnCheck[] = [];
@ -103,64 +86,56 @@ export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTabl
return filteredColumns; return filteredColumns;
}, },
onFetched: async transformed => {
const { pageNum, pageSize, total } = transformed;
updatePagination({
page: pageNum,
pageSize,
itemCount: total
});
},
immediate immediate
}); });
const pagination: PaginationProps = reactive({ const paginationBase: PaginationProps = reactive({
page: 1,
pageSize: 10,
showSizePicker: true, showSizePicker: true,
pageSizes: [10, 15, 20, 25, 30], pageSizes: [10, 15, 20, 25, 30],
onUpdatePage: async (page: number) => { onUpdatePage: async pageValue => {
pagination.page = page; page.value = pageValue;
updateSearchParams({
current: page,
size: pagination.pageSize!
});
getData();
}, },
onUpdatePageSize: async (pageSize: number) => { onUpdatePageSize: async pageSizeValue => {
pagination.pageSize = pageSize; pageSize.value = pageSizeValue;
pagination.page = 1;
updateSearchParams({
current: pagination.page,
size: pageSize
});
getData();
}, },
...(showTotal ...(showTotal
? { ? {
prefix: page => $t('datatable.itemCount', { total: page.itemCount }) prefix: pageProps => $t('datatable.itemCount', { total: pageProps.itemCount })
} }
: {}) : {})
}); });
const pagination = computed(
() =>
<PaginationProps>{
...paginationBase,
page: page.value,
pageSize: pageSize.value,
itemCount: total.value
}
);
// this is for mobile, if the system does not support mobile, you can use `pagination` directly // this is for mobile, if the system does not support mobile, you can use `pagination` directly
const mobilePagination = computed(() => { const mobilePagination = computed(() => {
const p: PaginationProps = { const p: PaginationProps = {
...pagination, ...pagination.value,
pageSlot: isMobile.value ? 3 : 9, pageSlot: isMobile.value ? 3 : 9,
prefix: !isMobile.value && showTotal ? pagination.prefix : undefined prefix: !isMobile.value && showTotal ? pagination.value.prefix : undefined
}; };
return p; return p;
}); });
function updatePagination(update: Partial<PaginationProps>) { function updatePagination(updateProps: Partial<PaginationProps>) {
Object.assign(pagination, update); const innerPageStates = ['page', 'pageSize', 'itemCount'] as const;
innerPageStates.forEach(key => {
if (updateProps[key]) {
update({
[key]: updateProps[key]
});
}
});
Object.assign(paginationBase, updateProps);
} }
/** /**
@ -169,16 +144,8 @@ export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTabl
* @param pageNum the page number. default is 1 * @param pageNum the page number. default is 1
*/ */
async function getDataByPage(pageNum: number = 1) { async function getDataByPage(pageNum: number = 1) {
updatePagination({ page.value = pageNum;
page: pageNum return getData();
});
updateSearchParams({
current: pageNum,
size: pagination.pageSize!
});
await getData();
} }
scope.run(() => { scope.run(() => {
@ -195,24 +162,17 @@ export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTabl
}); });
return { return {
loading, ...rest,
empty, getData,
data,
columns,
columnChecks,
reloadColumns, reloadColumns,
pagination, pagination,
mobilePagination, mobilePagination,
updatePagination, updatePagination,
getData, getDataByPage
getDataByPage,
searchParams,
updateSearchParams,
resetSearchParams
}; };
} }
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 { bool: drawerVisible, setTrue: openDrawer, setFalse: closeDrawer } = useBoolean();
const operateType = ref<NaiveUI.TableOperateType>('add'); const operateType = ref<NaiveUI.TableOperateType>('add');

View File

@ -14,7 +14,7 @@ import FirstLevelMenu from '../components/first-level-menu.vue';
import GlobalLogo from '../../global-logo/index.vue'; import GlobalLogo from '../../global-logo/index.vue';
defineOptions({ defineOptions({
name: 'VerticalMixMenu' name: 'VerticalMenuMix'
}); });
const route = useRoute(); const route = useRoute();

View File

@ -1,4 +1,4 @@
import { request } from '../request'; import { alova } from '../request';
/** /**
* Login * Login
@ -7,19 +7,12 @@ import { request } from '../request';
* @param password Password * @param password Password
*/ */
export function fetchLogin(userName: string, password: string) { export function fetchLogin(userName: string, password: string) {
return request<Api.Auth.LoginToken>({ return alova.Post<Api.Auth.LoginToken>('/auth/login', { userName, password });
url: '/auth/login',
method: 'post',
data: {
userName,
password
}
});
} }
/** Get user info */ /** Get user info */
export function fetchGetUserInfo() { export function fetchGetUserInfo() {
return request<Api.Auth.UserInfo>({ url: '/auth/getUserInfo' }); return alova.Get<Api.Auth.UserInfo>('/auth/getUserInfo');
} }
/** /**
@ -28,13 +21,15 @@ export function fetchGetUserInfo() {
* @param refreshToken Refresh token * @param refreshToken Refresh token
*/ */
export function fetchRefreshToken(refreshToken: string) { export function fetchRefreshToken(refreshToken: string) {
return request<Api.Auth.LoginToken>({ return alova.Post<Api.Auth.LoginToken>(
url: '/auth/refreshToken', '/auth/refreshToken',
method: 'post', { refreshToken },
data: { {
refreshToken meta: {
authRole: 'refreshToken'
} }
}); }
);
} }
/** /**
@ -44,5 +39,8 @@ export function fetchRefreshToken(refreshToken: string) {
* @param msg error message * @param msg error message
*/ */
export function fetchCustomBackendError(code: string, msg: string) { export function fetchCustomBackendError(code: string, msg: string) {
return request({ url: '/auth/error', params: { code, msg } }); return alova.Get('/auth/error', {
params: { code, msg },
shareRequest: false
});
} }

View File

@ -1,13 +1,13 @@
import { request } from '../request'; import { alova } from '../request';
/** get constant routes */ /** get constant routes */
export function fetchGetConstantRoutes() { export function fetchGetConstantRoutes() {
return request<Api.Route.MenuRoute[]>({ url: '/route/getConstantRoutes' }); return alova.Get<Api.Route.MenuRoute[]>('/route/getConstantRoutes');
} }
/** get user routes */ /** get user routes */
export function fetchGetUserRoutes() { export function fetchGetUserRoutes() {
return request<Api.Route.UserRoute>({ url: '/route/getUserRoutes' }); return alova.Get<Api.Route.UserRoute>('/route/getUserRoutes');
} }
/** /**
@ -16,5 +16,5 @@ export function fetchGetUserRoutes() {
* @param routeName route name * @param routeName route name
*/ */
export function fetchIsRouteExist(routeName: string) { export function fetchIsRouteExist(routeName: string) {
return request<boolean>({ url: '/route/isRouteExist', params: { routeName } }); return alova.Get<boolean>('/route/isRouteExist', { params: { routeName } });
} }

View File

@ -1,12 +1,8 @@
import { request } from '../request'; import { alova } from '../request';
/** get role list */ /** get role list */
export function fetchGetRoleList(params?: Api.SystemManage.RoleSearchParams) { export function fetchGetRoleList(params?: Api.SystemManage.RoleSearchParams) {
return request<Api.SystemManage.RoleList>({ return alova.Get<Api.SystemManage.RoleList>('/systemManage/getRoleList', { params });
url: '/systemManage/getRoleList',
method: 'get',
params
});
} }
/** /**
@ -15,41 +11,25 @@ export function fetchGetRoleList(params?: Api.SystemManage.RoleSearchParams) {
* these roles are all enabled * these roles are all enabled
*/ */
export function fetchGetAllRoles() { export function fetchGetAllRoles() {
return request<Api.SystemManage.AllRole[]>({ return alova.Get<Api.SystemManage.AllRole[]>('/systemManage/getAllRoles');
url: '/systemManage/getAllRoles',
method: 'get'
});
} }
/** get user list */ /** get user list */
export function fetchGetUserList(params?: Api.SystemManage.UserSearchParams) { export function fetchGetUserList(params?: Api.SystemManage.UserSearchParams) {
return request<Api.SystemManage.UserList>({ return alova.Get<Api.SystemManage.UserList>('/systemManage/getUserList', { params });
url: '/systemManage/getUserList',
method: 'get',
params
});
} }
/** get menu list */ /** get menu list */
export function fetchGetMenuList() { export function fetchGetMenuList() {
return request<Api.SystemManage.MenuList>({ return alova.Get<Api.SystemManage.MenuList>('/systemManage/getMenuList/v2');
url: '/systemManage/getMenuList/v2',
method: 'get'
});
} }
/** get all pages */ /** get all pages */
export function fetchGetAllPages() { export function fetchGetAllPages() {
return request<string[]>({ return alova.Get<string[]>('/systemManage/getAllPages');
url: '/systemManage/getAllPages',
method: 'get'
});
} }
/** get menu tree */ /** get menu tree */
export function fetchGetMenuTree() { export function fetchGetMenuTree() {
return request<Api.SystemManage.MenuTree[]>({ return alova.Get<Api.SystemManage.MenuTree[]>('/systemManage/getMenuTree');
url: '/systemManage/getMenuTree',
method: 'get'
});
} }

View File

@ -1,67 +1,86 @@
import type { AxiosResponse } from 'axios'; import { createAlovaRequest } from '@sa/alova';
import { BACKEND_ERROR_CODE, createFlatRequest, createRequest } from '@sa/axios';
import { useAuthStore } from '@/store/modules/auth'; import { useAuthStore } from '@/store/modules/auth';
import { $t } from '@/locales'; import { $t } from '@/locales';
import { localStg } from '@/utils/storage';
import { getServiceBaseURL } from '@/utils/service'; import { getServiceBaseURL } from '@/utils/service';
import { getAuthorization, handleExpiredRequest, showErrorMsg } from './shared'; import { getAuthorization, handleRefreshToken, showErrorMsg } from './shared';
import type { RequestInstanceState } from './type'; import type { RequestInstanceState } from './type';
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y'; const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
const { baseURL, otherBaseURL } = getServiceBaseURL(import.meta.env, isHttpProxy); const { baseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
export const request = createFlatRequest<App.Service.Response, RequestInstanceState>( const state: RequestInstanceState = {
errMsgStack: []
};
export const alova = createAlovaRequest(
{ {
baseURL, baseURL
headers: { },
apifoxToken: 'XL299LiMEDZ0H5h3A29PxwQXdMJqWyY2' {
onRequest({ config }) {
const Authorization = getAuthorization();
config.headers.Authorization = Authorization;
config.headers.apifoxToken = 'XL299LiMEDZ0H5h3A29PxwQXdMJqWyY2';
},
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) {
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 // 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 // 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; const resp = response.clone();
const data = await resp.json();
return String(data.code) === import.meta.env.VITE_SERVICE_SUCCESS_CODE;
}, },
async onBackendFail(response, instance) { async transformBackendResponse(response) {
return (await response.clone().json()).data;
},
async onError(error, response) {
const authStore = useAuthStore(); const authStore = useAuthStore();
const responseCode = String(response.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() { function handleLogout() {
showErrorMsg(state, message);
authStore.resetStore(); authStore.resetStore();
} }
function logoutAndCleanup() { function logoutAndCleanup() {
handleLogout(); handleLogout();
window.removeEventListener('beforeunload', handleLogout); window.removeEventListener('beforeunload', handleLogout);
state.errMsgStack = state.errMsgStack.filter(msg => msg !== message);
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 // 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(',') || []; const logoutCodes = import.meta.env.VITE_SERVICE_LOGOUT_CODES?.split(',') || [];
if (logoutCodes.includes(responseCode)) { if (logoutCodes.includes(responseCode)) {
handleLogout(); 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 // 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(',') || []; const modalLogoutCodes = import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES?.split(',') || [];
if (modalLogoutCodes.includes(responseCode) && !request.state.errMsgStack?.includes(response.data.msg)) { if (modalLogoutCodes.includes(responseCode) && !state.errMsgStack?.includes(message)) {
request.state.errMsgStack = [...(request.state.errMsgStack || []), response.data.msg]; state.errMsgStack = [...(state.errMsgStack || []), message];
// prevent the user from refreshing the page // prevent the user from refreshing the page
window.addEventListener('beforeunload', handleLogout); window.addEventListener('beforeunload', handleLogout);
window.$dialog?.error({ window.$dialog?.error({
title: $t('common.error'), title: $t('common.error'),
content: response.data.msg, content: message,
positiveText: $t('common.confirm'), positiveText: $t('common.confirm'),
maskClosable: false, maskClosable: false,
closeOnEsc: false, closeOnEsc: false,
@ -72,95 +91,10 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
logoutAndCleanup(); logoutAndCleanup();
} }
}); });
throw error;
return null;
} }
showErrorMsg(state, message);
// when the backend response code is in `expiredTokenCodes`, it means the token is expired, and refresh token throw error;
// 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);
} }
} }
); );

View File

@ -11,34 +11,23 @@ export function getAuthorization() {
} }
/** refresh token */ /** refresh token */
async function handleRefreshToken() { export async function handleRefreshToken() {
const { resetStore } = useAuthStore(); const { resetStore } = useAuthStore();
const rToken = localStg.get('refreshToken') || ''; const rToken = localStg.get('refreshToken') || '';
const { error, data } = await fetchRefreshToken(rToken); const refreshTokenMethod = fetchRefreshToken(rToken);
if (!error) {
// 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('token', data.token);
localStg.set('refreshToken', data.refreshToken); localStg.set('refreshToken', data.refreshToken);
return true; } catch (error) {
}
resetStore(); resetStore();
throw error;
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) { export function showErrorMsg(state: RequestInstanceState, message: string) {

View File

@ -1,6 +1,4 @@
export interface RequestInstanceState { export interface RequestInstanceState {
/** whether the request is refreshing token */
refreshTokenFn: Promise<boolean> | null;
/** the request error message stack */ /** the request error message stack */
errMsgStack: string[]; errMsgStack: string[];
} }

View File

@ -63,9 +63,8 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
async function login(userName: string, password: string, redirect = true) { async function login(userName: string, password: string, redirect = true) {
startLoading(); startLoading();
const { data: loginToken, error } = await fetchLogin(userName, password); try {
const loginToken = await fetchLogin(userName, password);
if (!error) {
const pass = await loginByToken(loginToken); const pass = await loginByToken(loginToken);
if (pass) { if (pass) {
@ -81,7 +80,7 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
}); });
} }
} }
} else { } catch {
resetStore(); resetStore();
} }
@ -106,17 +105,16 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
} }
async function getUserInfo() { async function getUserInfo() {
const { data: info, error } = await fetchGetUserInfo(); try {
const info = await fetchGetUserInfo();
if (!error) {
// update store // update store
Object.assign(userInfo, info); Object.assign(userInfo, info);
return true; return true;
} } catch {
return false; return false;
} }
}
async function initUserInfo() { async function initUserInfo() {
const hasToken = getToken(); const hasToken = getToken();

View File

@ -201,11 +201,10 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
if (authRouteMode.value === 'static') { if (authRouteMode.value === 'static') {
addConstantRoutes(staticRoute.constantRoutes); addConstantRoutes(staticRoute.constantRoutes);
} else { } else {
const { data, error } = await fetchGetConstantRoutes(); try {
const data = await fetchGetConstantRoutes();
if (!error) {
addConstantRoutes(data); addConstantRoutes(data);
} else { } catch {
// if fetch constant routes failed, use static constant routes // if fetch constant routes failed, use static constant routes
addConstantRoutes(staticRoute.constantRoutes); addConstantRoutes(staticRoute.constantRoutes);
} }
@ -246,9 +245,9 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
/** Init dynamic auth route */ /** Init dynamic auth route */
async function initDynamicAuthRoute() { async function initDynamicAuthRoute() {
const { data, error } = await fetchGetUserRoutes(); try {
const data = await fetchGetUserRoutes();
if (!error) {
const { routes, home } = data; const { routes, home } = data;
addAuthRoutes(routes); addAuthRoutes(routes);
@ -260,7 +259,7 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
handleUpdateRootRouteRedirect(home); handleUpdateRootRouteRedirect(home);
setIsInitAuthRoute(true); setIsInitAuthRoute(true);
} else { } catch {
// if fetch user routes failed, reset store // if fetch user routes failed, reset store
authStore.resetStore(); authStore.resetStore();
} }
@ -340,9 +339,7 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
return isRouteExistByRouteName(routeName, staticAuthRoutes); return isRouteExistByRouteName(routeName, staticAuthRoutes);
} }
const { data } = await fetchIsRouteExist(routeName); return fetchIsRouteExist(routeName);
return data;
} }
/** /**

View File

@ -19,7 +19,7 @@ export function filterAuthRoutesByRoles(routes: ElegantConstRoute[], roles: stri
* @param route Auth route * @param route Auth route
* @param roles Roles * @param roles Roles
*/ */
function filterAuthRouteByRoles(route: ElegantConstRoute, roles: string[]): ElegantConstRoute[] { function filterAuthRouteByRoles(route: ElegantConstRoute, roles: string[]) {
const routeRoles = (route.meta && route.meta.roles) || []; const routeRoles = (route.meta && route.meta.roles) || [];
// if the route's "roles" is empty, then it is allowed to access // if the route's "roles" is empty, then it is allowed to access
@ -34,11 +34,6 @@ function filterAuthRouteByRoles(route: ElegantConstRoute, roles: string[]): Eleg
filterRoute.children = filterRoute.children.flatMap(item => filterAuthRouteByRoles(item, roles)); filterRoute.children = filterRoute.children.flatMap(item => filterAuthRouteByRoles(item, roles));
} }
// Exclude the route if it has no children after filtering
if (filterRoute.children?.length === 0) {
return [];
}
return hasPermission || isEmptyRoles ? [filterRoute] : []; return hasPermission || isEmptyRoles ? [filterRoute] : [];
} }
@ -288,7 +283,8 @@ export function getBreadcrumbsByRoute(
for (const menu of menus) { for (const menu of menus) {
if (menu.key === key) { if (menu.key === key) {
return [transformMenuToBreadcrumb(menu)]; const breadcrumbMenu = menu;
return [transformMenuToBreadcrumb(breadcrumbMenu)];
} }
if (menu.key === activeKey) { if (menu.key === activeKey) {

View File

@ -87,6 +87,7 @@ declare module 'vue' {
NSelect: typeof import('naive-ui')['NSelect'] NSelect: typeof import('naive-ui')['NSelect']
NSkeleton: typeof import('naive-ui')['NSkeleton'] NSkeleton: typeof import('naive-ui')['NSkeleton']
NSpace: typeof import('naive-ui')['NSpace'] NSpace: typeof import('naive-ui')['NSpace']
NSpin: typeof import('naive-ui')['NSpin']
NStatistic: typeof import('naive-ui')['NStatistic'] NStatistic: typeof import('naive-ui')['NStatistic']
NSwitch: typeof import('naive-ui')['NSwitch'] NSwitch: typeof import('naive-ui')['NSwitch']
NTab: typeof import('naive-ui')['NTab'] NTab: typeof import('naive-ui')['NTab']

View File

@ -14,6 +14,14 @@ declare global {
$notification?: import('naive-ui').NotificationProviderInst; $notification?: import('naive-ui').NotificationProviderInst;
} }
interface ViewTransition {
ready: Promise<void>;
}
export interface Document {
startViewTransition?: (callback: () => Promise<void> | void) => ViewTransition;
}
/** Build time of the project */ /** Build time of the project */
export const BUILD_TIME: string; export const BUILD_TIME: string;
} }

View File

@ -9,7 +9,6 @@ declare namespace NaiveUI {
type PaginationProps = import('naive-ui').PaginationProps; type PaginationProps = import('naive-ui').PaginationProps;
type TableColumnCheck = import('@sa/hooks').TableColumnCheck; type TableColumnCheck = import('@sa/hooks').TableColumnCheck;
type TableDataWithIndex<T> = import('@sa/hooks').TableDataWithIndex<T>; type TableDataWithIndex<T> = import('@sa/hooks').TableDataWithIndex<T>;
type FlatResponseData<T> = import('@sa/axios').FlatResponseData<T>;
/** /**
* the custom column key * the custom column key
@ -26,9 +25,9 @@ declare namespace NaiveUI {
type TableColumn<T> = TableColumnWithKey<T> | DataTableSelectionColumn<T> | DataTableExpandColumn<T>; 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 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 * the type of table operation
@ -38,9 +37,9 @@ declare namespace NaiveUI {
*/ */
type TableOperateType = 'add' | 'edit'; 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>>>>, import('@sa/hooks').TableConfig<A, GetTableData<A>, TableColumn<TableDataWithIndex<GetTableData<A>>>>,
'apiFn' | 'apiParams' | 'columns' | 'immediate' 'apiFn' | 'apiParams' | 'columns' | 'immediate'
> & { > & {

View File

@ -40,7 +40,7 @@ async function handleSubmit() {
</script> </script>
<template> <template>
<NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false" @keyup.enter="handleSubmit"> <NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
<NFormItem path="phone"> <NFormItem path="phone">
<NInput v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" /> <NInput v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" />
</NFormItem> </NFormItem>

View File

@ -75,7 +75,7 @@ async function handleAccountLogin(account: Account) {
</script> </script>
<template> <template>
<NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false" @keyup.enter="handleSubmit"> <NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
<NFormItem path="userName"> <NFormItem path="userName">
<NInput v-model:value="model.userName" :placeholder="$t('page.login.common.userNamePlaceholder')" /> <NInput v-model:value="model.userName" :placeholder="$t('page.login.common.userNamePlaceholder')" />
</NFormItem> </NFormItem>

View File

@ -46,7 +46,7 @@ async function handleSubmit() {
</script> </script>
<template> <template>
<NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false" @keyup.enter="handleSubmit"> <NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
<NFormItem path="phone"> <NFormItem path="phone">
<NInput v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" /> <NInput v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" />
</NFormItem> </NFormItem>

View File

@ -45,7 +45,7 @@ async function handleSubmit() {
</script> </script>
<template> <template>
<NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false" @keyup.enter="handleSubmit"> <NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
<NFormItem path="phone"> <NFormItem path="phone">
<NInput v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" /> <NInput v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" />
</NFormItem> </NFormItem>

View File

@ -3,7 +3,7 @@ import { ref } from 'vue';
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import { NButton, NPopconfirm, NTag } from 'naive-ui'; import { NButton, NPopconfirm, NTag } from 'naive-ui';
import { useBoolean } from '@sa/hooks'; import { useBoolean } from '@sa/hooks';
import { fetchGetAllPages, fetchGetMenuList } from '@/service/api'; import { fetchGetMenuList } from '@/service/api';
import { useAppStore } from '@/store/modules/app'; import { useAppStore } from '@/store/modules/app';
import { useTable, useTableOperate } from '@/hooks/common/table'; import { useTable, useTableOperate } from '@/hooks/common/table';
import { $t } from '@/locales'; import { $t } from '@/locales';
@ -18,7 +18,7 @@ const { bool: visible, setTrue: openModal } = useBoolean();
const wrapperRef = ref<HTMLElement | null>(null); 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, apiFn: fetchGetMenuList,
columns: () => [ 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'); const operateType = ref<OperateType>('add');
@ -210,20 +210,6 @@ function handleAddChildMenu(item: Api.SystemManage.Menu) {
openModal(); openModal();
} }
const allPages = ref<string[]>([]);
async function getAllPages() {
const { data: pages } = await fetchGetAllPages();
allPages.value = pages || [];
}
function init() {
getAllPages();
}
// init
init();
</script> </script>
<template> <template>
@ -236,7 +222,7 @@ init();
:loading="loading" :loading="loading"
@add="handleAdd" @add="handleAdd"
@delete="handleBatchDelete" @delete="handleBatchDelete"
@refresh="getData" @refresh="refresh"
/> />
</template> </template>
<NDataTable <NDataTable
@ -256,7 +242,6 @@ init();
v-model:visible="visible" v-model:visible="visible"
:operate-type="operateType" :operate-type="operateType"
:row-data="editingData" :row-data="editingData"
:all-pages="allPages"
@submitted="getDataByPage" @submitted="getDataByPage"
/> />
</NCard> </NCard>

View File

@ -1,12 +1,13 @@
<script setup lang="tsx"> <script setup lang="tsx">
import { computed, reactive, ref, watch } from 'vue'; import { computed, reactive, watch } from 'vue';
import type { SelectOption } from 'naive-ui'; import type { SelectOption } from 'naive-ui';
import { useWatcher } from '@sa/alova/client';
import { useFormRules, useNaiveForm } from '@/hooks/common/form'; import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { $t } from '@/locales'; import { $t } from '@/locales';
import { enableStatusOptions, menuIconTypeOptions, menuTypeOptions } from '@/constants/business'; import { enableStatusOptions, menuIconTypeOptions, menuTypeOptions } from '@/constants/business';
import SvgIcon from '@/components/custom/svg-icon.vue'; import SvgIcon from '@/components/custom/svg-icon.vue';
import { getLocalIcons } from '@/utils/icon'; import { getLocalIcons } from '@/utils/icon';
import { fetchGetAllRoles } from '@/service/api'; import { fetchGetAllPages } from '@/service/api';
import { import {
getLayoutAndPage, getLayoutAndPage,
getPathParamFromRoutePath, getPathParamFromRoutePath,
@ -26,8 +27,6 @@ interface Props {
operateType: OperateType; operateType: OperateType;
/** the edit menu data or the parent menu data when adding a child menu */ /** the edit menu data or the parent menu data when adding a child menu */
rowData?: Api.SystemManage.Menu | null; rowData?: Api.SystemManage.Menu | null;
/** all pages */
allPages: string[];
} }
const props = defineProps<Props>(); const props = defineProps<Props>();
@ -138,8 +137,14 @@ const showLayout = computed(() => model.parentId === 0);
const showPage = computed(() => model.menuType === '2'); 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 pageOptions = computed(() => {
const allPages = [...props.allPages]; const allPages = [...allPagesRaw.value];
if (model.routeName && !allPages.includes(model.routeName)) { if (model.routeName && !allPages.includes(model.routeName)) {
allPages.unshift(model.routeName); allPages.unshift(model.routeName);
@ -165,20 +170,18 @@ const layoutOptions: CommonType.Option[] = [
]; ];
/** the enabled role options */ /** the enabled role options */
const roleOptions = ref<CommonType.Option<string>[]>([]); // const { data: roleOptionsRaw, loading } = useWatcher(fetchGetAllRoles, [visible], {
// initialData: [],
async function getRoleOptions() { // middleware(_, next) {
const { error, data } = await fetchGetAllRoles(); // return visible.value ? next() : undefined;
// }
if (!error) { // });
const options = data.map(item => ({ // const roleOptions = computed<CommonType.Option<string>[]>(() => {
label: item.roleName, // return roleOptionsRaw.value.map(item => ({
value: item.roleCode // label: item.roleName,
})); // value: item.roleCode
// }));
roleOptions.value = [...options]; // });
}
}
function handleInitModel() { function handleInitModel() {
Object.assign(model, createDefaultModel()); Object.assign(model, createDefaultModel());
@ -266,7 +269,6 @@ watch(visible, () => {
if (visible.value) { if (visible.value) {
handleInitModel(); handleInitModel();
restoreValidation(); restoreValidation();
getRoleOptions();
} }
}); });
@ -311,6 +313,7 @@ watch(
<NFormItemGi v-if="showPage" span="24 m:12" :label="$t('page.manage.menu.page')" path="page"> <NFormItemGi v-if="showPage" span="24 m:12" :label="$t('page.manage.menu.page')" path="page">
<NSelect <NSelect
v-model:value="model.page" v-model:value="model.page"
:loading="loadingPages"
:options="pageOptions" :options="pageOptions"
:placeholder="$t('page.manage.menu.form.page')" :placeholder="$t('page.manage.menu.form.page')"
/> />

View File

@ -15,7 +15,8 @@ const {
columnChecks, columnChecks,
data, data,
loading, loading,
getData, reload,
refresh,
getDataByPage, getDataByPage,
mobilePagination, mobilePagination,
searchParams, searchParams,
@ -116,7 +117,7 @@ const {
onBatchDeleted, onBatchDeleted,
onDeleted onDeleted
// closeDrawer // closeDrawer
} = useTableOperate(data, getData); } = useTableOperate(data, reload);
async function handleBatchDelete() { async function handleBatchDelete() {
// request // request
@ -148,7 +149,7 @@ function edit(id: number) {
:loading="loading" :loading="loading"
@add="handleAdd" @add="handleAdd"
@delete="handleBatchDelete" @delete="handleBatchDelete"
@refresh="getData" @refresh="refresh"
/> />
</template> </template>
<NDataTable <NDataTable

View File

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, shallowRef, watch } from 'vue'; import { computed, shallowRef, watch } from 'vue';
import { useWatcher } from '@sa/alova/client';
import { $t } from '@/locales'; import { $t } from '@/locales';
import { fetchGetAllPages, fetchGetMenuTree } from '@/service/api'; import { fetchGetAllPages, fetchGetMenuTree } from '@/service/api';
@ -38,16 +39,12 @@ async function updateHome(val: string) {
home.value = val; home.value = val;
} }
const pages = shallowRef<string[]>([]); const { data: pages, loading: loadingPages } = useWatcher(fetchGetAllPages, [visible], {
initialData: [],
async function getPages() { middleware(_, next) {
const { error, data } = await fetchGetAllPages(); return visible.value ? next() : undefined;
if (!error) {
pages.value = data;
} }
} });
const pageSelectOptions = computed(() => { const pageSelectOptions = computed(() => {
const opts: CommonType.Option[] = pages.value.map(page => ({ const opts: CommonType.Option[] = pages.value.map(page => ({
label: page, label: page,
@ -57,15 +54,12 @@ const pageSelectOptions = computed(() => {
return opts; return opts;
}); });
const tree = shallowRef<Api.SystemManage.MenuTree[]>([]); const { data: tree, loading: loadingTree } = useWatcher(fetchGetMenuTree, [visible], {
initialData: [],
async function getTree() { middleware(_, next) {
const { error, data } = await fetchGetMenuTree(); return visible.value ? next() : undefined;
if (!error) {
tree.value = data;
} }
} });
const checks = shallowRef<number[]>([]); const checks = shallowRef<number[]>([]);
@ -86,8 +80,6 @@ function handleSubmit() {
function init() { function init() {
getHome(); getHome();
getPages();
getTree();
getChecks(); getChecks();
} }
@ -102,7 +94,14 @@ watch(visible, val => {
<NModal v-model:show="visible" :title="title" preset="card" class="w-480px"> <NModal v-model:show="visible" :title="title" preset="card" class="w-480px">
<div class="flex-y-center gap-16px pb-12px"> <div class="flex-y-center gap-16px pb-12px">
<div>{{ $t('page.manage.menu.home') }}</div> <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> </div>
<NTree <NTree
v-model:checked-keys="checks" v-model:checked-keys="checks"
@ -113,7 +112,11 @@ watch(visible, val => {
virtual-scroll virtual-scroll
block-line block-line
class="h-280px" class="h-280px"
/> >
<template v-if="loadingTree" #empty>
<NSpin size="small"></NSpin>
</template>
</NTree>
<template #footer> <template #footer>
<NSpace justify="end"> <NSpace justify="end">
<NButton size="small" class="mt-16px" @click="closeModal"> <NButton size="small" class="mt-16px" @click="closeModal">

View File

@ -10,22 +10,11 @@ import UserSearch from './modules/user-search.vue';
const appStore = useAppStore(); const appStore = useAppStore();
const { const { columns, columnChecks, data, refresh, reload, loading, mobilePagination, searchParams, resetSearchParams } =
columns, useTable({
columnChecks,
data,
getData,
getDataByPage,
loading,
mobilePagination,
searchParams,
resetSearchParams
} = useTable({
apiFn: fetchGetUserList, apiFn: fetchGetUserList,
showTotal: true, showTotal: true,
apiParams: { 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 // 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 // the value can not be undefined, otherwise the property in Form will not be reactive
status: null, status: null,
@ -135,7 +124,7 @@ const {
) )
} }
] ]
}); });
const { const {
drawerVisible, drawerVisible,
@ -147,7 +136,7 @@ const {
onBatchDeleted, onBatchDeleted,
onDeleted onDeleted
// closeDrawer // closeDrawer
} = useTableOperate(data, getData); } = useTableOperate(data, reload);
async function handleBatchDelete() { async function handleBatchDelete() {
// request // request
@ -170,7 +159,7 @@ function edit(id: number) {
<template> <template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto"> <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" @reset="resetSearchParams" @search="reload" />
<NCard :title="$t('page.manage.user.title')" :bordered="false" size="small" class="sm:flex-1-hidden card-wrapper"> <NCard :title="$t('page.manage.user.title')" :bordered="false" size="small" class="sm:flex-1-hidden card-wrapper">
<template #header-extra> <template #header-extra>
<TableHeaderOperation <TableHeaderOperation
@ -179,7 +168,7 @@ function edit(id: number) {
:loading="loading" :loading="loading"
@add="handleAdd" @add="handleAdd"
@delete="handleBatchDelete" @delete="handleBatchDelete"
@refresh="getData" @refresh="refresh"
/> />
</template> </template>
<NDataTable <NDataTable
@ -199,7 +188,7 @@ function edit(id: number) {
v-model:visible="drawerVisible" v-model:visible="drawerVisible"
:operate-type="operateType" :operate-type="operateType"
:row-data="editingData" :row-data="editingData"
@submitted="getDataByPage" @submitted="reload"
/> />
</NCard> </NCard>
</div> </div>

View File

@ -1,5 +1,6 @@
<script setup lang="ts"> <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 { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { fetchGetAllRoles } from '@/service/api'; import { fetchGetAllRoles } from '@/service/api';
import { $t } from '@/locales'; import { $t } from '@/locales';
@ -66,13 +67,14 @@ const rules: Record<RuleKey, App.Global.FormRule> = {
}; };
/** the enabled role options */ /** the enabled role options */
const roleOptions = ref<CommonType.Option<string>[]>([]); const { data: roleOptionsRaw, loading } = useWatcher(fetchGetAllRoles, [visible], {
initialData: [],
async function getRoleOptions() { middleware(_, next) {
const { error, data } = await fetchGetAllRoles(); return visible.value ? next() : undefined;
}
if (!error) { });
const options = data.map(item => ({ const roleOptions = computed<CommonType.Option<string>[]>(() => {
const options = roleOptionsRaw.value.map(item => ({
label: item.roleName, label: item.roleName,
value: item.roleCode value: item.roleCode
})); }));
@ -85,9 +87,8 @@ async function getRoleOptions() {
})); }));
// end // end
roleOptions.value = [...userRoleOptions, ...options]; return [...userRoleOptions, ...options];
} });
}
function handleInitModel() { function handleInitModel() {
Object.assign(model, createDefaultModel()); Object.assign(model, createDefaultModel());
@ -113,7 +114,6 @@ watch(visible, () => {
if (visible.value) { if (visible.value) {
handleInitModel(); handleInitModel();
restoreValidation(); restoreValidation();
getRoleOptions();
} }
}); });
</script> </script>
@ -148,6 +148,7 @@ watch(visible, () => {
<NSelect <NSelect
v-model:value="model.userRoles" v-model:value="model.userRoles"
multiple multiple
:loading="loading"
:options="roleOptions" :options="roleOptions"
:placeholder="$t('page.manage.user.form.userRole')" :placeholder="$t('page.manage.user.form.userRole')"
/> />