mirror of
https://github.com/soybeanjs/soybean-admin.git
synced 2025-11-14 12:43:42 +08:00
Merge branch 'main' into example
This commit is contained in:
@@ -93,11 +93,15 @@ export function useRouterPush(inSetup = true) {
|
||||
return routerPushByKey('login', { query, params: { module } });
|
||||
}
|
||||
|
||||
/** Redirect from login */
|
||||
async function redirectFromLogin() {
|
||||
/**
|
||||
* Redirect from login
|
||||
*
|
||||
* @param [needRedirect=true] Whether to redirect after login. Default is `true`
|
||||
*/
|
||||
async function redirectFromLogin(needRedirect = true) {
|
||||
const redirect = route.value.query?.redirect as string;
|
||||
|
||||
if (redirect) {
|
||||
if (needRedirect && redirect) {
|
||||
routerPush(redirect);
|
||||
} else {
|
||||
toHome();
|
||||
|
||||
@@ -8,6 +8,7 @@ export const { setupStore: setupMixMenuContext, useStore: useMixMenuContext } =
|
||||
function useMixMenu() {
|
||||
const route = useRoute();
|
||||
const routeStore = useRouteStore();
|
||||
const { selectedKey } = useMenu();
|
||||
|
||||
const activeFirstLevelMenuKey = ref('');
|
||||
|
||||
@@ -16,12 +17,7 @@ function useMixMenu() {
|
||||
}
|
||||
|
||||
function getActiveFirstLevelMenuKey() {
|
||||
const { hideInMenu, activeMenu } = route.meta;
|
||||
const name = route.name as string;
|
||||
|
||||
const routeName = (hideInMenu ? activeMenu : name) || name;
|
||||
|
||||
const [firstLevelRouteName] = routeName.split('_');
|
||||
const [firstLevelRouteName] = selectedKey.value.split('_');
|
||||
|
||||
setActiveFirstLevelMenuKey(firstLevelRouteName);
|
||||
}
|
||||
@@ -68,3 +64,20 @@ function useMixMenu() {
|
||||
getActiveFirstLevelMenuKey
|
||||
};
|
||||
}
|
||||
|
||||
export function useMenu() {
|
||||
const route = useRoute();
|
||||
|
||||
const selectedKey = computed(() => {
|
||||
const { hideInMenu, activeMenu } = route.meta;
|
||||
const name = route.name as string;
|
||||
|
||||
const routeName = (hideInMenu ? activeMenu : name) || name;
|
||||
|
||||
return routeName;
|
||||
});
|
||||
|
||||
return {
|
||||
selectedKey
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,26 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { GLOBAL_HEADER_MENU_ID } from '@/constants/app';
|
||||
import { useRouteStore } from '@/store/modules/route';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { useMenu } from '../../../context';
|
||||
|
||||
defineOptions({
|
||||
name: 'HorizontalMenu'
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const routeStore = useRouteStore();
|
||||
const { routerPushByKeyWithMetaQuery } = useRouterPush();
|
||||
|
||||
const selectedKey = computed(() => {
|
||||
const { hideInMenu, activeMenu } = route.meta;
|
||||
const name = route.name as string;
|
||||
|
||||
const routeName = (hideInMenu ? activeMenu : name) || name;
|
||||
|
||||
return routeName;
|
||||
});
|
||||
const { selectedKey } = useMenu();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,31 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { GLOBAL_HEADER_MENU_ID, GLOBAL_SIDER_MENU_ID } from '@/constants/app';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import FirstLevelMenu from '../components/first-level-menu.vue';
|
||||
import { useMixMenuContext } from '../../../context';
|
||||
import { useMenu, useMixMenuContext } from '../../../context';
|
||||
|
||||
defineOptions({
|
||||
name: 'HorizontalMixMenu'
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const appStore = useAppStore();
|
||||
const themeStore = useThemeStore();
|
||||
const { allMenus, childLevelMenus, activeFirstLevelMenuKey, setActiveFirstLevelMenuKey } = useMixMenuContext();
|
||||
const { routerPushByKeyWithMetaQuery } = useRouterPush();
|
||||
|
||||
const selectedKey = computed(() => {
|
||||
const { hideInMenu, activeMenu } = route.meta;
|
||||
const name = route.name as string;
|
||||
|
||||
const routeName = (hideInMenu ? activeMenu : name) || name;
|
||||
|
||||
return routeName;
|
||||
});
|
||||
const { allMenus, childLevelMenus, activeFirstLevelMenuKey, setActiveFirstLevelMenuKey } = useMixMenuContext();
|
||||
const { selectedKey } = useMenu();
|
||||
|
||||
function handleSelectMixMenu(menu: App.Global.Menu) {
|
||||
setActiveFirstLevelMenuKey(menu.key);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import type { RouteKey } from '@elegant-router/types';
|
||||
import { SimpleScrollbar } from '@sa/materials';
|
||||
@@ -8,7 +8,7 @@ import { useAppStore } from '@/store/modules/app';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { useRouteStore } from '@/store/modules/route';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { useMixMenuContext } from '../../../context';
|
||||
import { useMenu, useMixMenuContext } from '../../../context';
|
||||
|
||||
defineOptions({
|
||||
name: 'ReversedHorizontalMixMenu'
|
||||
@@ -18,6 +18,7 @@ const route = useRoute();
|
||||
const appStore = useAppStore();
|
||||
const themeStore = useThemeStore();
|
||||
const routeStore = useRouteStore();
|
||||
const { routerPushByKeyWithMetaQuery } = useRouterPush();
|
||||
const {
|
||||
firstLevelMenus,
|
||||
childLevelMenus,
|
||||
@@ -25,16 +26,7 @@ const {
|
||||
setActiveFirstLevelMenuKey,
|
||||
isActiveFirstLevelMenuHasChildren
|
||||
} = useMixMenuContext();
|
||||
const { routerPushByKeyWithMetaQuery } = useRouterPush();
|
||||
|
||||
const selectedKey = computed(() => {
|
||||
const { hideInMenu, activeMenu } = route.meta;
|
||||
const name = route.name as string;
|
||||
|
||||
const routeName = (hideInMenu ? activeMenu : name) || name;
|
||||
|
||||
return routeName;
|
||||
});
|
||||
const { selectedKey } = useMenu();
|
||||
|
||||
function handleSelectMixMenu(key: RouteKey) {
|
||||
setActiveFirstLevelMenuKey(key);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useThemeStore } from '@/store/modules/theme';
|
||||
import { useRouteStore } from '@/store/modules/route';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { GLOBAL_SIDER_MENU_ID } from '@/constants/app';
|
||||
import { useMenu } from '../../../context';
|
||||
|
||||
defineOptions({
|
||||
name: 'VerticalMenu'
|
||||
@@ -17,18 +18,10 @@ const appStore = useAppStore();
|
||||
const themeStore = useThemeStore();
|
||||
const routeStore = useRouteStore();
|
||||
const { routerPushByKeyWithMetaQuery } = useRouterPush();
|
||||
const { selectedKey } = useMenu();
|
||||
|
||||
const inverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted);
|
||||
|
||||
const selectedKey = computed(() => {
|
||||
const { hideInMenu, activeMenu } = route.meta;
|
||||
const name = route.name as string;
|
||||
|
||||
const routeName = (hideInMenu ? activeMenu : name) || name;
|
||||
|
||||
return routeName;
|
||||
});
|
||||
|
||||
const expandedKeys = ref<string[]>([]);
|
||||
|
||||
function updateExpandedKeys() {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useRouteStore } from '@/store/modules/route';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { $t } from '@/locales';
|
||||
import { GLOBAL_SIDER_MENU_ID } from '@/constants/app';
|
||||
import { useMixMenuContext } from '../../../context';
|
||||
import { useMenu, useMixMenuContext } from '../../../context';
|
||||
import FirstLevelMenu from '../components/first-level-menu.vue';
|
||||
import GlobalLogo from '../../global-logo/index.vue';
|
||||
|
||||
@@ -31,6 +31,7 @@ const {
|
||||
getActiveFirstLevelMenuKey
|
||||
//
|
||||
} = useMixMenuContext();
|
||||
const { selectedKey } = useMenu();
|
||||
|
||||
const inverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted);
|
||||
|
||||
@@ -56,15 +57,6 @@ function handleResetActiveMenu() {
|
||||
}
|
||||
}
|
||||
|
||||
const selectedKey = computed(() => {
|
||||
const { hideInMenu, activeMenu } = route.meta;
|
||||
const name = route.name as string;
|
||||
|
||||
const routeName = (hideInMenu ? activeMenu : name) || name;
|
||||
|
||||
return routeName;
|
||||
});
|
||||
|
||||
const expandedKeys = ref<string[]>([]);
|
||||
|
||||
function updateExpandedKeys() {
|
||||
@@ -122,9 +114,6 @@ watch(
|
||||
mode="vertical"
|
||||
:value="selectedKey"
|
||||
:options="childLevelMenus"
|
||||
:collapsed="appStore.siderCollapse"
|
||||
:collapsed-width="themeStore.sider.collapsedWidth"
|
||||
:collapsed-icon-size="22"
|
||||
:inverted="inverted"
|
||||
:indent="18"
|
||||
@update:value="routerPushByKeyWithMetaQuery"
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
import { h } from 'vue';
|
||||
import type { App } from 'vue';
|
||||
import { NButton } from 'naive-ui';
|
||||
import { $t } from '../locales';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
export function setupAppErrorHandle(app: App) {
|
||||
app.config.errorHandler = (err, vm, info) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err, vm, info);
|
||||
};
|
||||
}
|
||||
|
||||
export function setupAppVersionNotification() {
|
||||
const canAutoUpdateApp = import.meta.env.VITE_AUTOMATICALLY_DETECT_UPDATE === 'Y';
|
||||
|
||||
if (!canAutoUpdateApp) return;
|
||||
|
||||
let isShow = false;
|
||||
|
||||
document.addEventListener('visibilitychange', async () => {
|
||||
@@ -52,9 +64,7 @@ export function setupAppVersionNotification() {
|
||||
}
|
||||
|
||||
async function getHtmlBuildTime() {
|
||||
const baseURL = import.meta.env.VITE_BASE_URL;
|
||||
|
||||
const res = await fetch(`${baseURL}index.html`);
|
||||
const res = await fetch(`/index.html?time=${Date.now()}`);
|
||||
|
||||
const html = await res.text();
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useAuthStore } from '@/store/modules/auth';
|
||||
import { $t } from '@/locales';
|
||||
import { localStg } from '@/utils/storage';
|
||||
import { getServiceBaseURL } from '@/utils/service';
|
||||
import { handleRefreshToken, showErrorMsg } from './shared';
|
||||
import { getAuthorization, handleExpiredRequest, showErrorMsg } from './shared';
|
||||
import type { RequestInstanceState } from './type';
|
||||
|
||||
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
|
||||
@@ -19,12 +19,8 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
|
||||
},
|
||||
{
|
||||
async onRequest(config) {
|
||||
const { headers } = config;
|
||||
|
||||
// set token
|
||||
const token = localStg.get('token');
|
||||
const Authorization = token ? `Bearer ${token}` : null;
|
||||
Object.assign(headers, { Authorization });
|
||||
const Authorization = getAuthorization();
|
||||
Object.assign(config.headers, { Authorization });
|
||||
|
||||
return config;
|
||||
},
|
||||
@@ -83,15 +79,13 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
|
||||
// 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) && !request.state.isRefreshingToken) {
|
||||
request.state.isRefreshingToken = true;
|
||||
if (expiredTokenCodes.includes(responseCode)) {
|
||||
const success = await handleExpiredRequest(request.state);
|
||||
if (success) {
|
||||
const Authorization = getAuthorization();
|
||||
Object.assign(response.config.headers, { Authorization });
|
||||
|
||||
const refreshConfig = await handleRefreshToken(response.config);
|
||||
|
||||
request.state.isRefreshingToken = false;
|
||||
|
||||
if (refreshConfig) {
|
||||
return instance.request(refreshConfig) as Promise<AxiosResponse>;
|
||||
return instance.request(response.config) as Promise<AxiosResponse>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +1,44 @@
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
import { useAuthStore } from '@/store/modules/auth';
|
||||
import { localStg } from '@/utils/storage';
|
||||
import { fetchRefreshToken } from '../api';
|
||||
import type { RequestInstanceState } from './type';
|
||||
|
||||
/**
|
||||
* refresh token
|
||||
*
|
||||
* @param axiosConfig - request config when the token is expired
|
||||
*/
|
||||
export async function handleRefreshToken(axiosConfig: AxiosRequestConfig) {
|
||||
export function getAuthorization() {
|
||||
const token = localStg.get('token');
|
||||
const Authorization = token ? `Bearer ${token}` : null;
|
||||
|
||||
return Authorization;
|
||||
}
|
||||
|
||||
/** refresh token */
|
||||
async function handleRefreshToken() {
|
||||
const { resetStore } = useAuthStore();
|
||||
|
||||
const refreshToken = localStg.get('refreshToken') || '';
|
||||
const { error, data } = await fetchRefreshToken(refreshToken);
|
||||
const rToken = localStg.get('refreshToken') || '';
|
||||
const { error, data } = await fetchRefreshToken(rToken);
|
||||
if (!error) {
|
||||
localStg.set('token', data.token);
|
||||
localStg.set('refreshToken', data.refreshToken);
|
||||
|
||||
const config = { ...axiosConfig };
|
||||
if (config.headers) {
|
||||
config.headers.Authorization = data.token;
|
||||
}
|
||||
|
||||
return config;
|
||||
return true;
|
||||
}
|
||||
|
||||
resetStore();
|
||||
|
||||
return null;
|
||||
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,6 @@
|
||||
export interface RequestInstanceState {
|
||||
/** whether the request is refreshing token */
|
||||
isRefreshingToken: boolean;
|
||||
refreshTokenFn: Promise<boolean> | null;
|
||||
/** the request error message stack */
|
||||
errMsgStack: string[];
|
||||
}
|
||||
|
||||
@@ -71,9 +71,7 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
||||
if (pass) {
|
||||
await routeStore.initAuthRoute();
|
||||
|
||||
if (redirect) {
|
||||
await redirectFromLogin();
|
||||
}
|
||||
await redirectFromLogin(redirect);
|
||||
|
||||
if (routeStore.isInitAuthRoute) {
|
||||
window.$notification?.success({
|
||||
|
||||
@@ -281,15 +281,25 @@ export function getBreadcrumbsByRoute(
|
||||
const key = route.name as string;
|
||||
const activeKey = route.meta?.activeMenu;
|
||||
|
||||
const menuKey = activeKey || key;
|
||||
|
||||
for (const menu of menus) {
|
||||
if (menu.key === menuKey) {
|
||||
const breadcrumbMenu = menuKey !== activeKey ? menu : getGlobalMenuByBaseRoute(route);
|
||||
|
||||
if (menu.key === key) {
|
||||
const breadcrumbMenu = menu;
|
||||
return [transformMenuToBreadcrumb(breadcrumbMenu)];
|
||||
}
|
||||
|
||||
if (menu.key === activeKey) {
|
||||
const ROUTE_DEGREE_SPLITTER = '_';
|
||||
|
||||
const parentKey = key.split(ROUTE_DEGREE_SPLITTER).slice(0, -1).join(ROUTE_DEGREE_SPLITTER);
|
||||
|
||||
const breadcrumbMenu = getGlobalMenuByBaseRoute(route);
|
||||
if (parentKey !== activeKey) {
|
||||
return [transformMenuToBreadcrumb(breadcrumbMenu)];
|
||||
}
|
||||
|
||||
return [transformMenuToBreadcrumb(menu), transformMenuToBreadcrumb(breadcrumbMenu)];
|
||||
}
|
||||
|
||||
if (menu.children?.length) {
|
||||
const result = getBreadcrumbsByRoute(route, menu.children);
|
||||
if (result.length > 0) {
|
||||
|
||||
3
src/typings/api.d.ts
vendored
3
src/typings/api.d.ts
vendored
@@ -20,6 +20,9 @@ declare namespace Api {
|
||||
records: T[];
|
||||
}
|
||||
|
||||
/** common search params of table */
|
||||
type CommonSearchParams = Pick<Common.PaginatingCommonParams, 'current' | 'size'>;
|
||||
|
||||
/**
|
||||
* enable status
|
||||
*
|
||||
|
||||
2
src/typings/env.d.ts
vendored
2
src/typings/env.d.ts
vendored
@@ -103,6 +103,8 @@ declare namespace Env {
|
||||
readonly VITE_ICONIFY_URL?: string;
|
||||
/** Used to differentiate storage across different domains */
|
||||
readonly VITE_STORAGE_PREFIX?: string;
|
||||
/** Whether to automatically detect updates after configuring application packaging */
|
||||
readonly VITE_AUTOMATICALLY_DETECT_UPDATE?: CommonType.YesOrNo;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
2
src/typings/naive-ui.d.ts
vendored
2
src/typings/naive-ui.d.ts
vendored
@@ -26,7 +26,7 @@ declare namespace NaiveUI {
|
||||
|
||||
type TableColumn<T> = TableColumnWithKey<T> | DataTableSelectionColumn<T> | DataTableExpandColumn<T>;
|
||||
|
||||
type TableApiFn<T = any, R = Api.SystemManage.CommonSearchParams> = (
|
||||
type TableApiFn<T = any, R = Api.Common.CommonSearchParams> = (
|
||||
params: R
|
||||
) => Promise<FlatResponseData<Api.Common.PaginatingQueryRecord<T>>>;
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import json5 from 'json5';
|
||||
|
||||
/**
|
||||
* Create service config by current env
|
||||
*
|
||||
@@ -8,10 +10,10 @@ export function createServiceConfig(env: Env.ImportMeta) {
|
||||
|
||||
let other = {} as Record<App.Service.OtherBaseURLKey, string>;
|
||||
try {
|
||||
other = JSON.parse(VITE_OTHER_SERVICE_BASE_URL);
|
||||
other = json5.parse(VITE_OTHER_SERVICE_BASE_URL);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('VITE_OTHER_SERVICE_BASE_URL is not a valid JSON string');
|
||||
console.error('VITE_OTHER_SERVICE_BASE_URL is not a valid json5 string');
|
||||
}
|
||||
|
||||
const httpConfig: App.Service.SimpleServiceConfig = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="tsx">
|
||||
import { onMounted, shallowRef } from 'vue';
|
||||
import { gantt } from 'dhtmlx-gantt';
|
||||
import type { GanttConfigOptions, ZoomLevels } from 'dhtmlx-gantt';
|
||||
import type { GanttConfigOptions, ZoomLevel } from 'dhtmlx-gantt';
|
||||
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css';
|
||||
import { ganttTasks } from './data';
|
||||
|
||||
@@ -72,7 +72,7 @@ function initGantt() {
|
||||
gantt.init(ganttRef.value);
|
||||
gantt.parse({ data: ganttTasks });
|
||||
|
||||
const zoomLevels: ZoomLevels[] = [
|
||||
const zoomLevels: ZoomLevel[] = [
|
||||
{
|
||||
name: 'day',
|
||||
scale_height: 60,
|
||||
|
||||
Reference in New Issue
Block a user