perf: 优化登录进系统链路,减轻首屏与进系统阻塞
登录页延后重型依赖与角色动画,i18n 拆登录轻量包;进系统只等待动态路由与完整语言包,EP/VXE 后台预热。兼容登录响应带回 userInfo。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
c4979a1fb4
commit
d3e1c62043
20
src/App.vue
20
src/App.vue
@ -1,16 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<a-config-provider :locale="locale">
|
<a-config-provider :locale="locale">
|
||||||
<router-view />
|
<router-view />
|
||||||
<GlobalSetting />
|
<GlobalSetting v-if="showGlobalSetting" />
|
||||||
</a-config-provider>
|
</a-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed, defineAsyncComponent } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
import enUS from '@arco-design/web-vue/es/locale/lang/en-us';
|
import enUS from '@arco-design/web-vue/es/locale/lang/en-us';
|
||||||
import zhCN from '@arco-design/web-vue/es/locale/lang/zh-cn';
|
import zhCN from '@arco-design/web-vue/es/locale/lang/zh-cn';
|
||||||
import GlobalSetting from '@/components/global-setting/index.vue';
|
|
||||||
import useLocale from '@/hooks/locale';
|
import useLocale from '@/hooks/locale';
|
||||||
|
import { isLoginRouteName } from '@/utils/login-portal';
|
||||||
|
|
||||||
|
/** Settings drawer not needed on login; skip load to shrink first paint */
|
||||||
|
const GlobalSetting = defineAsyncComponent(
|
||||||
|
() => import('@/components/global-setting/index.vue'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
// route.name can be empty on first tick — do not load settings until we know it's not login
|
||||||
|
const showGlobalSetting = computed(() => {
|
||||||
|
const name = route.name ? String(route.name) : '';
|
||||||
|
if (!name) return false;
|
||||||
|
return !isLoginRouteName(name);
|
||||||
|
});
|
||||||
|
|
||||||
const { currentLocale } = useLocale();
|
const { currentLocale } = useLocale();
|
||||||
const locale = computed(() => {
|
const locale = computed(() => {
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
/* istanbul ignore file */
|
/* istanbul ignore file */
|
||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
import type { UserInfoDto } from './UserInfoDto';
|
||||||
/**
|
/**
|
||||||
* 登录 / 刷新令牌响应
|
* 登录 / 刷新令牌响应
|
||||||
*/
|
*/
|
||||||
@ -11,4 +12,8 @@ export type LoginResultDto = {
|
|||||||
tokenType?: string | null;
|
tokenType?: string | null;
|
||||||
expiresIn?: number;
|
expiresIn?: number;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
|
/**
|
||||||
|
* 可选:登录接口直接带回用户信息(含 menus)时可跳过二次 get-current-user
|
||||||
|
*/
|
||||||
|
userInfo?: UserInfoDto | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,16 +1,21 @@
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { Message } from '@arco-design/web-vue';
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
import { ensureLocaleMessages } from '@/locale';
|
||||||
|
|
||||||
export default function useLocale() {
|
export default function useLocale() {
|
||||||
const i18 = useI18n();
|
const i18 = useI18n();
|
||||||
const currentLocale = computed(() => {
|
const currentLocale = computed(() => {
|
||||||
return i18.locale.value;
|
return i18.locale.value;
|
||||||
});
|
});
|
||||||
const changeLocale = (value: string, options?: { silent?: boolean }) => {
|
const changeLocale = async (
|
||||||
|
value: string,
|
||||||
|
options?: { silent?: boolean },
|
||||||
|
) => {
|
||||||
if (i18.locale.value === value) {
|
if (i18.locale.value === value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
await ensureLocaleMessages(value);
|
||||||
i18.locale.value = value;
|
i18.locale.value = value;
|
||||||
localStorage.setItem('arco-locale', value);
|
localStorage.setItem('arco-locale', value);
|
||||||
if (!options?.silent) {
|
if (!options?.silent) {
|
||||||
|
|||||||
10
src/locale/bootstrap-en-US.ts
Normal file
10
src/locale/bootstrap-en-US.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import localeLogin from '@/views/login/locale/en-US';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录页轻量英文包(仅在切换到英文或偏好英文时加载)
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
'app.language': 'Language',
|
||||||
|
'navbar.action.locale': 'Switch to English',
|
||||||
|
...localeLogin,
|
||||||
|
};
|
||||||
10
src/locale/bootstrap-zh-CN.ts
Normal file
10
src/locale/bootstrap-zh-CN.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import localeLogin from '@/views/login/locale/zh-CN';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录页轻量语言包(首屏只加载这份,进系统后再合并完整 zh-CN)
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
'app.language': '语言',
|
||||||
|
'navbar.action.locale': '切换为中文',
|
||||||
|
...localeLogin,
|
||||||
|
};
|
||||||
@ -1,25 +1,76 @@
|
|||||||
import { createI18n } from 'vue-i18n';
|
import { createI18n } from 'vue-i18n';
|
||||||
import en from './en-US';
|
import bootstrapZh from './bootstrap-zh-CN';
|
||||||
import cn from './zh-CN';
|
|
||||||
|
|
||||||
export const LOCALE_OPTIONS = [
|
export const LOCALE_OPTIONS = [
|
||||||
{ label: '中文', value: 'zh-CN' },
|
{ label: '中文', value: 'zh-CN' },
|
||||||
{ label: 'English', value: 'en-US' },
|
{ label: 'English', value: 'en-US' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const defaultLocale = localStorage.getItem('arco-locale') || 'zh-CN';
|
const defaultLocale = localStorage.getItem('arco-locale') || 'zh-CN';
|
||||||
|
|
||||||
|
const fullLocaleLoaded = new Set<string>(['zh-CN-bootstrap']);
|
||||||
|
|
||||||
const i18n = createI18n({
|
const i18n = createI18n({
|
||||||
locale: defaultLocale,
|
locale: defaultLocale === 'en-US' ? 'zh-CN' : defaultLocale,
|
||||||
fallbackLocale: 'en-US',
|
fallbackLocale: 'zh-CN',
|
||||||
legacy: false,
|
legacy: false,
|
||||||
allowComposition: true,
|
allowComposition: true,
|
||||||
messages: {
|
messages: {
|
||||||
'en-US': en,
|
'zh-CN': bootstrapZh,
|
||||||
'zh-CN': cn,
|
|
||||||
},
|
},
|
||||||
silentTranslationWarn: true,
|
silentTranslationWarn: true,
|
||||||
missingWarn: false,
|
missingWarn: false,
|
||||||
fallbackWarn: false,
|
fallbackWarn: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function loadBootstrapEn() {
|
||||||
|
if (fullLocaleLoaded.has('en-US-bootstrap')) return;
|
||||||
|
const mod = await import('./bootstrap-en-US');
|
||||||
|
i18n.global.setLocaleMessage('en-US', mod.default);
|
||||||
|
fullLocaleLoaded.add('en-US-bootstrap');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按需加载完整业务语言包(进系统前调用) */
|
||||||
|
export async function ensureFullLocaleMessages(
|
||||||
|
locale: string = i18n.global.locale.value,
|
||||||
|
): Promise<void> {
|
||||||
|
const key = `${locale}-full`;
|
||||||
|
if (fullLocaleLoaded.has(key)) return;
|
||||||
|
|
||||||
|
if (locale === 'zh-CN') {
|
||||||
|
const mod = await import('./zh-CN');
|
||||||
|
i18n.global.setLocaleMessage('zh-CN', mod.default);
|
||||||
|
fullLocaleLoaded.add(key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (locale === 'en-US') {
|
||||||
|
await loadBootstrapEn();
|
||||||
|
const mod = await import('./en-US');
|
||||||
|
i18n.global.setLocaleMessage('en-US', mod.default);
|
||||||
|
fullLocaleLoaded.add(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 登录页切换语言:优先轻量包,后台补全量 */
|
||||||
|
export async function ensureLocaleMessages(locale: string): Promise<void> {
|
||||||
|
if (locale === 'zh-CN') {
|
||||||
|
// 已有 bootstrap;若已进过系统则已是 full
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (locale === 'en-US') {
|
||||||
|
if (fullLocaleLoaded.has('en-US-full')) return;
|
||||||
|
await loadBootstrapEn();
|
||||||
|
// 后台拉全量,不堵切换
|
||||||
|
void ensureFullLocaleMessages('en-US');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 启动时若偏好英文,先加载英文轻量包再切语言 */
|
||||||
|
export async function initPreferredLocale(): Promise<void> {
|
||||||
|
if (defaultLocale !== 'en-US') return;
|
||||||
|
await loadBootstrapEn();
|
||||||
|
i18n.global.locale.value = 'en-US';
|
||||||
|
}
|
||||||
|
|
||||||
export default i18n;
|
export default i18n;
|
||||||
|
|||||||
40
src/main.ts
40
src/main.ts
@ -5,7 +5,7 @@ import ArcoVueIcon from '@arco-design/web-vue/es/icon';
|
|||||||
import 'uno.css';
|
import 'uno.css';
|
||||||
import router from './router';
|
import router from './router';
|
||||||
import store, { useAppStore } from './store';
|
import store, { useAppStore } from './store';
|
||||||
import i18n from './locale';
|
import i18n, { initPreferredLocale, ensureFullLocaleMessages } from './locale';
|
||||||
import directive from './directive';
|
import directive from './directive';
|
||||||
import globalProperties from './plugins/globalProperties';
|
import globalProperties from './plugins/globalProperties';
|
||||||
import {
|
import {
|
||||||
@ -22,7 +22,7 @@ if (import.meta.env.DEV) {
|
|||||||
void import('./mock');
|
void import('./mock');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 登录/公开页:不预拉 Element Plus / VXE */
|
/** Login / public pages: no EP/VXE on critical path */
|
||||||
const LIGHT_ENTRY_PATHS = [
|
const LIGHT_ENTRY_PATHS = [
|
||||||
'/',
|
'/',
|
||||||
'/admin',
|
'/admin',
|
||||||
@ -32,6 +32,11 @@ const LIGHT_ENTRY_PATHS = [
|
|||||||
'/wechat/bind',
|
'/wechat/bind',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/** Home uses Arco (+ optional echarts); do not block on EP/VXE */
|
||||||
|
function isHomePath(path: string) {
|
||||||
|
return path === '/home' || path.startsWith('/home/');
|
||||||
|
}
|
||||||
|
|
||||||
function isLightEntry(path: string) {
|
function isLightEntry(path: string) {
|
||||||
const normalized = path.split('?')[0] || '/';
|
const normalized = path.split('?')[0] || '/';
|
||||||
return LIGHT_ENTRY_PATHS.some(
|
return LIGHT_ENTRY_PATHS.some(
|
||||||
@ -39,6 +44,14 @@ function isLightEntry(path: string) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function routeNeedsHeavyPlugins(path: string) {
|
||||||
|
const normalized = path.split('?')[0] || '/';
|
||||||
|
if (isLightEntry(normalized) || isHomePath(normalized)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
||||||
app.use(ArcoVue, {});
|
app.use(ArcoVue, {});
|
||||||
@ -59,9 +72,11 @@ if (rbacAccessToken) {
|
|||||||
setAuthToken(rbacAccessToken);
|
setAuthToken(rbacAccessToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A/B:保留守卫 await(登录时已预热则几乎瞬过)
|
|
||||||
router.beforeEach(async (to) => {
|
router.beforeEach(async (to) => {
|
||||||
if (!isLightEntry(to.path)) {
|
if (!isLightEntry(to.path)) {
|
||||||
|
void ensureFullLocaleMessages();
|
||||||
|
}
|
||||||
|
if (routeNeedsHeavyPlugins(to.path)) {
|
||||||
await ensureHeavyPlugins();
|
await ensureHeavyPlugins();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -69,21 +84,24 @@ router.beforeEach(async (to) => {
|
|||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const entryPath =
|
const entryPath =
|
||||||
typeof window !== 'undefined' ? window.location.pathname : '/';
|
typeof window !== 'undefined' ? window.location.pathname : '/';
|
||||||
if (!isLightEntry(entryPath)) {
|
const light = isLightEntry(entryPath);
|
||||||
|
|
||||||
|
await initPreferredLocale();
|
||||||
|
|
||||||
|
// Business / home refresh: need full i18n before paint
|
||||||
|
if (!light) {
|
||||||
|
await ensureFullLocaleMessages();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (routeNeedsHeavyPlugins(entryPath)) {
|
||||||
await ensureHeavyPlugins();
|
await ensureHeavyPlugins();
|
||||||
}
|
}
|
||||||
|
|
||||||
app.mount('#app');
|
app.mount('#app');
|
||||||
initAuthKeepAlive();
|
initAuthKeepAlive();
|
||||||
|
|
||||||
if (isLightEntry(entryPath) && typeof window !== 'undefined') {
|
if (light || isHomePath(entryPath)) {
|
||||||
const ric =
|
|
||||||
window.requestIdleCallback ||
|
|
||||||
((cb: () => void) =>
|
|
||||||
window.setTimeout(cb, 2500) as unknown as number);
|
|
||||||
ric(() => {
|
|
||||||
void ensureHeavyPlugins();
|
void ensureHeavyPlugins();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -94,7 +94,7 @@ export function bindHeavyPluginsContext(app: App, i18n: AppI18n) {
|
|||||||
i18nRef = i18n;
|
i18nRef = i18n;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 业务页重型 UI(Element Plus / VXE);登录成功时可提前预热 */
|
/** 业务页重型 UI(Element Plus / VXE);登录页后台预热,进系统时 await */
|
||||||
export function ensureHeavyPlugins(): Promise<void> {
|
export function ensureHeavyPlugins(): Promise<void> {
|
||||||
if (!heavyPluginsReady) {
|
if (!heavyPluginsReady) {
|
||||||
heavyPluginsReady = (async () => {
|
heavyPluginsReady = (async () => {
|
||||||
@ -108,6 +108,8 @@ export function ensureHeavyPlugins(): Promise<void> {
|
|||||||
import('element-plus/dist/index.css'),
|
import('element-plus/dist/index.css'),
|
||||||
import('vxe-table/lib/style.css'),
|
import('vxe-table/lib/style.css'),
|
||||||
import('font-awesome/css/font-awesome.min.css'),
|
import('font-awesome/css/font-awesome.min.css'),
|
||||||
|
import('handsontable/styles/handsontable.min.css'),
|
||||||
|
import('handsontable/styles/ht-theme-classic.min.css'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
appRef.use(ElementPlus);
|
appRef.use(ElementPlus);
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
/** 动态路由是否已完成 addRoute(登录编排与权限守卫共用) */
|
/** 动态路由是否已完成 addRoute(登录编排与权限守卫共用) */
|
||||||
let dynamicRouterInitialized = false;
|
let dynamicRouterInitialized = false;
|
||||||
let isInitializingDynamicRouter = false;
|
let isInitializingDynamicRouter = false;
|
||||||
|
let bootstrapPromise: Promise<void> | null = null;
|
||||||
|
|
||||||
export function isDynamicRouterInitialized() {
|
export function isDynamicRouterInitialized() {
|
||||||
return dynamicRouterInitialized;
|
return dynamicRouterInitialized;
|
||||||
@ -8,6 +9,10 @@ export function isDynamicRouterInitialized() {
|
|||||||
|
|
||||||
export function markDynamicRouterInitialized(value = true) {
|
export function markDynamicRouterInitialized(value = true) {
|
||||||
dynamicRouterInitialized = value;
|
dynamicRouterInitialized = value;
|
||||||
|
if (!value) {
|
||||||
|
bootstrapPromise = null;
|
||||||
|
isInitializingDynamicRouter = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isInitializingDynamicRouterFlag() {
|
export function isInitializingDynamicRouterFlag() {
|
||||||
@ -17,3 +22,14 @@ export function isInitializingDynamicRouterFlag() {
|
|||||||
export function setInitializingDynamicRouter(value: boolean) {
|
export function setInitializingDynamicRouter(value: boolean) {
|
||||||
isInitializingDynamicRouter = value;
|
isInitializingDynamicRouter = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单例:登录进系统与权限守卫共用同一 in-flight Promise,避免重复 init。
|
||||||
|
*/
|
||||||
|
export function getDynamicRouterBootstrapPromise() {
|
||||||
|
return bootstrapPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setDynamicRouterBootstrapPromise(p: Promise<void> | null) {
|
||||||
|
bootstrapPromise = p;
|
||||||
|
}
|
||||||
|
|||||||
@ -28,10 +28,14 @@ export const clearDynamicRoutes = () => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 初始化动态路由
|
* @description 初始化动态路由
|
||||||
|
* @param options.forceRefresh 为 true 时清空菜单缓存后重建;登录进系统应传 false 以复用 getCurrentUser 已带的 menus
|
||||||
*/
|
*/
|
||||||
export const initDynamicRouter = async () => {
|
export const initDynamicRouter = async (options?: {
|
||||||
|
forceRefresh?: boolean;
|
||||||
|
}) => {
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
|
const rbacUserStore = useRbacUserStore();
|
||||||
|
|
||||||
/** 路由初始化错误 */
|
/** 路由初始化错误 */
|
||||||
const routerError = (isNotice: boolean = true, title: string = '无权限访问', message: string = '当前账号无任何菜单权限,请联系系统管理员!') => {
|
const routerError = (isNotice: boolean = true, title: string = '无权限访问', message: string = '当前账号无任何菜单权限,请联系系统管理员!') => {
|
||||||
@ -60,9 +64,12 @@ export const initDynamicRouter = async () => {
|
|||||||
// 如果后续需要,可以在这里设置:userStore.setModule(1);
|
// 如果后续需要,可以在这里设置:userStore.setModule(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1.获取菜单列表 && 按钮权限列表(强制刷新,不使用缓存)
|
// 1.获取菜单列表 && 按钮权限列表
|
||||||
|
// 登录后 getCurrentUser 通常已带 menus,默认不 forceRefresh,避免白等
|
||||||
|
const hasCachedMenus = (rbacUserStore.menus?.length ?? 0) > 0;
|
||||||
|
const forceRefresh = options?.forceRefresh ?? !hasCachedMenus;
|
||||||
try {
|
try {
|
||||||
await authStore.getAuthMenuList(chooseModule, true);
|
await authStore.getAuthMenuList(chooseModule, forceRefresh);
|
||||||
} catch (menuError: any) {
|
} catch (menuError: any) {
|
||||||
console.error('获取菜单列表失败:', menuError);
|
console.error('获取菜单列表失败:', menuError);
|
||||||
const errorMsg = menuError?.response?.data?.msg || menuError?.message || '获取菜单列表失败';
|
const errorMsg = menuError?.response?.data?.msg || menuError?.message || '获取菜单列表失败';
|
||||||
|
|||||||
48
src/router/ensure-dynamic-router.ts
Normal file
48
src/router/ensure-dynamic-router.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { initDynamicRouter } from './dynamicRouter';
|
||||||
|
import {
|
||||||
|
isDynamicRouterInitialized,
|
||||||
|
markDynamicRouterInitialized,
|
||||||
|
getDynamicRouterBootstrapPromise,
|
||||||
|
setDynamicRouterBootstrapPromise,
|
||||||
|
setInitializingDynamicRouter,
|
||||||
|
} from './dynamic-router-state';
|
||||||
|
|
||||||
|
export type EnsureDynamicRouterOptions = {
|
||||||
|
/** 默认:登录已带 menus 时不强制清空重建 */
|
||||||
|
forceRefresh?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保证动态路由已 addRoute。多处调用共享同一 Promise。
|
||||||
|
*/
|
||||||
|
export function ensureDynamicRouterReady(
|
||||||
|
options: EnsureDynamicRouterOptions = {},
|
||||||
|
): Promise<void> {
|
||||||
|
if (isDynamicRouterInitialized()) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = getDynamicRouterBootstrapPromise();
|
||||||
|
if (existing) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
setInitializingDynamicRouter(true);
|
||||||
|
const promise = initDynamicRouter({
|
||||||
|
forceRefresh: options.forceRefresh ?? false,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
markDynamicRouterInitialized(true);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setDynamicRouterBootstrapPromise(null);
|
||||||
|
markDynamicRouterInitialized(false);
|
||||||
|
throw err;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setInitializingDynamicRouter(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
setDynamicRouterBootstrapPromise(promise);
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
@ -4,18 +4,15 @@ import NProgress from 'nprogress'; // progress bar
|
|||||||
import usePermission from '@/hooks/permission';
|
import usePermission from '@/hooks/permission';
|
||||||
import { useUserStore, useAppStore, useTabBarStore } from '@/store';
|
import { useUserStore, useAppStore, useTabBarStore } from '@/store';
|
||||||
import { useRbacUserStore } from '@/store/modules/rbac-user';
|
import { useRbacUserStore } from '@/store/modules/rbac-user';
|
||||||
import { useAuthStore } from '@/store';
|
|
||||||
import { hasPermission } from '@/utils/rbac-permission';
|
import { hasPermission } from '@/utils/rbac-permission';
|
||||||
import { appRoutes } from '../routes';
|
import { appRoutes } from '../routes';
|
||||||
import { WHITE_LIST, NOT_FOUND } from '../constants';
|
import { WHITE_LIST, NOT_FOUND } from '../constants';
|
||||||
import { Message } from '@arco-design/web-vue';
|
import { Message } from '@arco-design/web-vue';
|
||||||
import { initDynamicRouter } from '../dynamicRouter';
|
|
||||||
import {
|
import {
|
||||||
isDynamicRouterInitialized,
|
isDynamicRouterInitialized,
|
||||||
markDynamicRouterInitialized,
|
markDynamicRouterInitialized,
|
||||||
isInitializingDynamicRouterFlag,
|
|
||||||
setInitializingDynamicRouter,
|
|
||||||
} from '../dynamic-router-state';
|
} from '../dynamic-router-state';
|
||||||
|
import { ensureDynamicRouterReady } from '../ensure-dynamic-router';
|
||||||
import { isLoginRouteName, getDefaultHomePathByType, canAccessCustomerPortalRoute } from '@/utils/login-portal';
|
import { isLoginRouteName, getDefaultHomePathByType, canAccessCustomerPortalRoute } from '@/utils/login-portal';
|
||||||
|
|
||||||
export default function setupPermissionGuard(router: Router) {
|
export default function setupPermissionGuard(router: Router) {
|
||||||
@ -23,7 +20,6 @@ export default function setupPermissionGuard(router: Router) {
|
|||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const rbacUserStore = useRbacUserStore();
|
const rbacUserStore = useRbacUserStore();
|
||||||
const authStore = useAuthStore();
|
|
||||||
const tabBarStore = useTabBarStore();
|
const tabBarStore = useTabBarStore();
|
||||||
const Permission = usePermission();
|
const Permission = usePermission();
|
||||||
const permissionsAllow = Permission.accessRouter(to);
|
const permissionsAllow = Permission.accessRouter(to);
|
||||||
@ -48,7 +44,7 @@ export default function setupPermissionGuard(router: Router) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是登录页,直接放行
|
// 如果是登录页,直接放行并重置动态路由状态(下次进系统重新编排)
|
||||||
if (isLoginRouteName(to.name as string)) {
|
if (isLoginRouteName(to.name as string)) {
|
||||||
markDynamicRouterInitialized(false);
|
markDynamicRouterInitialized(false);
|
||||||
next();
|
next();
|
||||||
@ -71,25 +67,16 @@ export default function setupPermissionGuard(router: Router) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否需要初始化动态路由
|
// 动态路由未就绪:与 enterSystemAfterLogin 共用单例,避免重复 init
|
||||||
if (
|
if (
|
||||||
!isDynamicRouterInitialized() &&
|
!isDynamicRouterInitialized() &&
|
||||||
!isInitializingDynamicRouterFlag() &&
|
|
||||||
(userStore.name || userStore.accountId)
|
(userStore.name || userStore.accountId)
|
||||||
) {
|
) {
|
||||||
const chooseModule = userStore.chooseModuleGet;
|
if (!userStore.chooseModuleGet) {
|
||||||
const hasNoMenus = !authStore.authMenuListGet || authStore.authMenuListGet.length === 0;
|
|
||||||
|
|
||||||
if (hasNoMenus) {
|
|
||||||
if (!chooseModule) {
|
|
||||||
userStore.setModule(1);
|
userStore.setModule(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
setInitializingDynamicRouter(true);
|
|
||||||
try {
|
try {
|
||||||
await initDynamicRouter();
|
await ensureDynamicRouterReady({ forceRefresh: false });
|
||||||
markDynamicRouterInitialized(true);
|
|
||||||
// 首页等静态路由已匹配则一次放行;仅未匹配时 rematch
|
|
||||||
if (to.matched.length === 0) {
|
if (to.matched.length === 0) {
|
||||||
next({ path: to.fullPath, replace: true });
|
next({ path: to.fullPath, replace: true });
|
||||||
return;
|
return;
|
||||||
@ -98,20 +85,17 @@ export default function setupPermissionGuard(router: Router) {
|
|||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('初始化动态路由失败:', error);
|
console.error('初始化动态路由失败:', error);
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage =
|
||||||
if (errorMessage.includes('模块') || errorMessage.includes('请先选择模块')) {
|
error instanceof Error ? error.message : String(error);
|
||||||
|
if (
|
||||||
|
errorMessage.includes('模块') ||
|
||||||
|
errorMessage.includes('请先选择模块')
|
||||||
|
) {
|
||||||
markDynamicRouterInitialized(true);
|
markDynamicRouterInitialized(true);
|
||||||
} else {
|
} else {
|
||||||
setInitializingDynamicRouter(false);
|
|
||||||
next({ name: rbacUserStore.getLoginRouteName() });
|
next({ name: rbacUserStore.getLoginRouteName() });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
setInitializingDynamicRouter(false);
|
|
||||||
}
|
|
||||||
} else if (authStore.authMenuListGet && authStore.authMenuListGet.length > 0) {
|
|
||||||
// 登录编排已 init:菜单在、路由已 add
|
|
||||||
markDynamicRouterInitialized(true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -203,7 +203,7 @@ export const useRbacUserStore = defineStore('rbacUser', () => {
|
|||||||
})
|
})
|
||||||
saveTokens(result)
|
saveTokens(result)
|
||||||
setLoginPortalType(loginType)
|
setLoginPortalType(loginType)
|
||||||
await getCurrentUser()
|
await hydrateUserAfterLogin(result)
|
||||||
return true
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleApiError(error)
|
handleApiError(error)
|
||||||
@ -219,7 +219,7 @@ export const useRbacUserStore = defineStore('rbacUser', () => {
|
|||||||
}
|
}
|
||||||
saveTokens(result)
|
saveTokens(result)
|
||||||
setLoginPortalType(OmsLoginType.Customer)
|
setLoginPortalType(OmsLoginType.Customer)
|
||||||
await getCurrentUser()
|
await hydrateUserAfterLogin(result)
|
||||||
return true
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleApiError(error)
|
handleApiError(error)
|
||||||
@ -235,7 +235,7 @@ export const useRbacUserStore = defineStore('rbacUser', () => {
|
|||||||
}
|
}
|
||||||
saveTokens(result, { sessionOnly: true })
|
saveTokens(result, { sessionOnly: true })
|
||||||
setLoginPortalType(OmsLoginType.Customer, { sessionOnly: true })
|
setLoginPortalType(OmsLoginType.Customer, { sessionOnly: true })
|
||||||
await getCurrentUser()
|
await hydrateUserAfterLogin(result)
|
||||||
return true
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleApiError(error)
|
handleApiError(error)
|
||||||
@ -243,6 +243,17 @@ export const useRbacUserStore = defineStore('rbacUser', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 登录响应若已带 userInfo 则直接用,否则再打 get-current-user */
|
||||||
|
async function hydrateUserAfterLogin(result: LoginResultDto) {
|
||||||
|
const embedded = result.userInfo
|
||||||
|
if (embedded?.id) {
|
||||||
|
userInfo.value = embedded
|
||||||
|
getAuthStorage().setItem(USER_INFO_KEY, JSON.stringify(embedded))
|
||||||
|
return embedded
|
||||||
|
}
|
||||||
|
return getCurrentUser()
|
||||||
|
}
|
||||||
|
|
||||||
// 获取当前用户信息(基于 Token,无需 userId 参数)
|
// 获取当前用户信息(基于 Token,无需 userId 参数)
|
||||||
async function getCurrentUser() {
|
async function getCurrentUser() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -2,13 +2,12 @@ import type { RouteLocationRaw } from 'vue-router';
|
|||||||
import { isNavigationFailure } from 'vue-router';
|
import { isNavigationFailure } from 'vue-router';
|
||||||
import router from '@/router';
|
import router from '@/router';
|
||||||
import { useUserStore } from '@/store';
|
import { useUserStore } from '@/store';
|
||||||
import { initDynamicRouter } from '@/router/dynamicRouter';
|
import { ensureDynamicRouterReady } from '@/router/ensure-dynamic-router';
|
||||||
import {
|
|
||||||
isDynamicRouterInitialized,
|
|
||||||
markDynamicRouterInitialized,
|
|
||||||
} from '@/router/dynamic-router-state';
|
|
||||||
import { ensureHeavyPlugins } from '@/plugins/heavy-plugins';
|
import { ensureHeavyPlugins } from '@/plugins/heavy-plugins';
|
||||||
|
import { ensureFullLocaleMessages } from '@/locale';
|
||||||
import { skipNextPageTransition } from '@/utils/page-transition-flag';
|
import { skipNextPageTransition } from '@/utils/page-transition-flag';
|
||||||
|
import type { UserInfoDto } from '@/api/rbac';
|
||||||
|
import { resolveAvatarUrl } from '@/utils/avatar';
|
||||||
|
|
||||||
export type EnterSystemTarget = {
|
export type EnterSystemTarget = {
|
||||||
path?: string;
|
path?: string;
|
||||||
@ -16,10 +15,34 @@ export type EnterSystemTarget = {
|
|||||||
query?: Record<string, unknown>;
|
query?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 将 RBAC 用户写入 userStore(密码/微信/SSO 共用) */
|
||||||
|
export function applyRbacUserToUserStore(
|
||||||
|
userInfoData: UserInfoDto,
|
||||||
|
fallbackAccount = '',
|
||||||
|
) {
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const displayName =
|
||||||
|
userInfoData.name ||
|
||||||
|
userInfoData.account ||
|
||||||
|
fallbackAccount ||
|
||||||
|
String(userInfoData.id ?? '');
|
||||||
|
userStore.setInfo({
|
||||||
|
account: userInfoData.account || fallbackAccount,
|
||||||
|
name: displayName,
|
||||||
|
avatar: resolveAvatarUrl(userInfoData.avatar),
|
||||||
|
email: userInfoData.email,
|
||||||
|
id: userInfoData.id,
|
||||||
|
accountId: userInfoData.id,
|
||||||
|
defaultModule: undefined,
|
||||||
|
moduleList: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A+B:登录成功后进入系统
|
* 登录成功后进入系统(客户端 / 管理端共用)
|
||||||
* - 退场动画 ∥ 重插件预热 ∥ 动态路由
|
*
|
||||||
* - 一次 replace(守卫仍保留 beforeEach await,预热后几乎不堵)
|
* 关键路径只等:完整语言包 + 动态路由就绪 → replace
|
||||||
|
* 退场动画、重型插件:后台进行,不堵跳转
|
||||||
*/
|
*/
|
||||||
export async function enterSystemAfterLogin(options: {
|
export async function enterSystemAfterLogin(options: {
|
||||||
target: EnterSystemTarget;
|
target: EnterSystemTarget;
|
||||||
@ -31,17 +54,13 @@ export async function enterSystemAfterLogin(options: {
|
|||||||
userStore.setModule(1);
|
userStore.setModule(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const warmPlugins = ensureHeavyPlugins();
|
void options.playExit?.();
|
||||||
|
void ensureHeavyPlugins();
|
||||||
|
|
||||||
const bootstrapRoutes = (async () => {
|
await Promise.all([
|
||||||
if (isDynamicRouterInitialized()) return;
|
ensureFullLocaleMessages(),
|
||||||
await initDynamicRouter();
|
ensureDynamicRouterReady({ forceRefresh: false }),
|
||||||
markDynamicRouterInitialized(true);
|
]);
|
||||||
})();
|
|
||||||
|
|
||||||
const exit = options.playExit?.() ?? Promise.resolve();
|
|
||||||
|
|
||||||
await Promise.all([warmPlugins, bootstrapRoutes, exit]);
|
|
||||||
|
|
||||||
skipNextPageTransition();
|
skipNextPageTransition();
|
||||||
|
|
||||||
|
|||||||
@ -29,7 +29,7 @@ const tooltip = computed(() =>
|
|||||||
|
|
||||||
function toggleLocale() {
|
function toggleLocale() {
|
||||||
const next = locale.value === 'zh-CN' ? 'en-US' : 'zh-CN';
|
const next = locale.value === 'zh-CN' ? 'en-US' : 'zh-CN';
|
||||||
changeLocale(next, { silent: true });
|
void changeLocale(next, { silent: true });
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -121,10 +121,9 @@ import {
|
|||||||
import { OmsLoginType } from '@/api/rbac';
|
import { OmsLoginType } from '@/api/rbac';
|
||||||
import { useRbacUserStore } from '@/store/modules/rbac-user';
|
import { useRbacUserStore } from '@/store/modules/rbac-user';
|
||||||
import { setToken } from '@/utils/auth';
|
import { setToken } from '@/utils/auth';
|
||||||
import { useUserStore } from '@/store';
|
|
||||||
import { getDefaultHomePathByType } from '@/utils/login-portal';
|
import { getDefaultHomePathByType } from '@/utils/login-portal';
|
||||||
import { resolveAvatarUrl } from '@/utils/avatar';
|
import { enterSystemAfterLogin, applyRbacUserToUserStore } from '@/utils/enter-system';
|
||||||
import { enterSystemAfterLogin } from '@/utils/enter-system';
|
import { ensureHeavyPlugins } from '@/plugins/heavy-plugins';
|
||||||
import { LOGIN_CHARACTERS_KEY, type LoginCharactersApi } from '../../composables/useLoginCharacters';
|
import { LOGIN_CHARACTERS_KEY, type LoginCharactersApi } from '../../composables/useLoginCharacters';
|
||||||
import { LOGIN_EXIT_KEY, type LoginExitApi } from '../../composables/useLoginExit';
|
import { LOGIN_EXIT_KEY, type LoginExitApi } from '../../composables/useLoginExit';
|
||||||
import LoginLockLogo from '../LoginLockLogo.vue';
|
import LoginLockLogo from '../LoginLockLogo.vue';
|
||||||
@ -146,7 +145,6 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const userStore = useUserStore();
|
|
||||||
const rbacUserStore = useRbacUserStore();
|
const rbacUserStore = useRbacUserStore();
|
||||||
|
|
||||||
const passwordInputRef = ref<HTMLInputElement | null>(null);
|
const passwordInputRef = ref<HTMLInputElement | null>(null);
|
||||||
@ -273,6 +271,7 @@ function togglePasswordVisible() {
|
|||||||
function onAccountFocus() {
|
function onAccountFocus() {
|
||||||
accountError.value = '';
|
accountError.value = '';
|
||||||
loginCharacters.value?.onAccountFocus();
|
loginCharacters.value?.onAccountFocus();
|
||||||
|
void ensureHeavyPlugins();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onAccountBlur() {
|
function onAccountBlur() {
|
||||||
@ -282,6 +281,7 @@ function onAccountBlur() {
|
|||||||
function onPasswordFocus() {
|
function onPasswordFocus() {
|
||||||
passwordError.value = '';
|
passwordError.value = '';
|
||||||
loginCharacters.value?.onPasswordFocus();
|
loginCharacters.value?.onPasswordFocus();
|
||||||
|
void ensureHeavyPlugins();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPasswordBlur() {
|
function onPasswordBlur() {
|
||||||
@ -341,48 +341,31 @@ async function handleSubmit() {
|
|||||||
setToken(rbacUserStore.token);
|
setToken(rbacUserStore.token);
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayName =
|
applyRbacUserToUserStore(userInfoData, fallbackAccount);
|
||||||
userInfoData.name || userInfoData.account || fallbackAccount || String(userInfoData.id ?? '');
|
|
||||||
const userInfo: any = {
|
|
||||||
account: userInfoData.account || fallbackAccount,
|
|
||||||
name: displayName,
|
|
||||||
avatar: resolveAvatarUrl(userInfoData.avatar),
|
|
||||||
email: userInfoData.email,
|
|
||||||
id: userInfoData.id,
|
|
||||||
accountId: userInfoData.id,
|
|
||||||
defaultModule: undefined,
|
|
||||||
moduleList: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
userStore.setInfo(userInfo);
|
|
||||||
|
|
||||||
const { redirect, ...othersQuery } = router.currentRoute.value.query;
|
const { redirect, ...othersQuery } = router.currentRoute.value.query;
|
||||||
const redirectStr = redirect as string | undefined;
|
const redirectStr = redirect as string | undefined;
|
||||||
const defaultHomePath = getDefaultHomePathByType(props.loginType);
|
const defaultHomePath = getDefaultHomePathByType(props.loginType);
|
||||||
const defaultTarget = { path: defaultHomePath };
|
|
||||||
|
|
||||||
const redirectParams: any = {
|
const redirectParams: Record<string, unknown> = {
|
||||||
query: { ...othersQuery },
|
query: { ...othersQuery },
|
||||||
};
|
};
|
||||||
|
|
||||||
if (redirectStr) {
|
if (redirectStr) {
|
||||||
if (redirectStr.startsWith('/')) {
|
if (redirectStr.startsWith('/')) {
|
||||||
redirectParams.path = redirectStr;
|
redirectParams.path = redirectStr;
|
||||||
delete redirectParams.name;
|
|
||||||
} else {
|
} else {
|
||||||
redirectParams.name = redirectStr;
|
redirectParams.name = redirectStr;
|
||||||
delete redirectParams.path;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
redirectParams.path = defaultTarget.path;
|
redirectParams.path = defaultHomePath;
|
||||||
delete redirectParams.name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Message.success(t('login.form.login.success'));
|
Message.success(t('login.form.login.success'));
|
||||||
await nextTick();
|
await nextTick();
|
||||||
await enterSystemAfterLogin({
|
await enterSystemAfterLogin({
|
||||||
target: redirectParams,
|
target: redirectParams,
|
||||||
fallbackPath: defaultTarget.path,
|
fallbackPath: defaultHomePath,
|
||||||
playExit: () => loginExit?.playExitAnimation() ?? Promise.resolve(),
|
playExit: () => loginExit?.playExitAnimation() ?? Promise.resolve(),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -64,11 +64,9 @@ import { IconUser } from '@arco-design/web-vue/es/icon';
|
|||||||
import { OmsLoginType } from '@/api/rbac';
|
import { OmsLoginType } from '@/api/rbac';
|
||||||
import { getLoginQrcode, getWxLoginStatus, isWxLoginPendingError, wxLogin } from '@/api/wechat';
|
import { getLoginQrcode, getWxLoginStatus, isWxLoginPendingError, wxLogin } from '@/api/wechat';
|
||||||
import { useRbacUserStore } from '@/store/modules/rbac-user';
|
import { useRbacUserStore } from '@/store/modules/rbac-user';
|
||||||
import { useUserStore } from '@/store';
|
|
||||||
import { setToken } from '@/utils/auth';
|
import { setToken } from '@/utils/auth';
|
||||||
import { resolveAvatarUrl } from '@/utils/avatar';
|
|
||||||
import { getDefaultHomePathByType } from '@/utils/login-portal';
|
import { getDefaultHomePathByType } from '@/utils/login-portal';
|
||||||
import { enterSystemAfterLogin } from '@/utils/enter-system';
|
import { enterSystemAfterLogin, applyRbacUserToUserStore } from '@/utils/enter-system';
|
||||||
import { LOGIN_EXIT_KEY, type LoginExitApi } from '../composables/useLoginExit';
|
import { LOGIN_EXIT_KEY, type LoginExitApi } from '../composables/useLoginExit';
|
||||||
|
|
||||||
const QR_EXPIRE_MS = 600_000;
|
const QR_EXPIRE_MS = 600_000;
|
||||||
@ -78,7 +76,6 @@ const POLL_INTERVAL_MS = 1800;
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const rbacUserStore = useRbacUserStore();
|
const rbacUserStore = useRbacUserStore();
|
||||||
const userStore = useUserStore();
|
|
||||||
const loginExit = inject<LoginExitApi | null>(LOGIN_EXIT_KEY, null);
|
const loginExit = inject<LoginExitApi | null>(LOGIN_EXIT_KEY, null);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@ -145,16 +142,7 @@ async function navigateAfterLogin() {
|
|||||||
setToken(rbacUserStore.token);
|
setToken(rbacUserStore.token);
|
||||||
}
|
}
|
||||||
|
|
||||||
userStore.setInfo({
|
applyRbacUserToUserStore(userInfoData);
|
||||||
account: userInfoData.account,
|
|
||||||
name: userInfoData.name || userInfoData.account || String(userInfoData.id ?? ''),
|
|
||||||
avatar: resolveAvatarUrl(userInfoData.avatar),
|
|
||||||
email: userInfoData.email,
|
|
||||||
id: userInfoData.id,
|
|
||||||
accountId: userInfoData.id,
|
|
||||||
defaultModule: undefined,
|
|
||||||
moduleList: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const { redirect, ...othersQuery } = router.currentRoute.value.query;
|
const { redirect, ...othersQuery } = router.currentRoute.value.query;
|
||||||
const redirectStr = redirect as string | undefined;
|
const redirectStr = redirect as string | undefined;
|
||||||
|
|||||||
@ -17,7 +17,7 @@
|
|||||||
<LoginHero :light="isLightMode" />
|
<LoginHero :light="isLightMode" />
|
||||||
</aside>
|
</aside>
|
||||||
<div class="login-characters-panel">
|
<div class="login-characters-panel">
|
||||||
<LoginCharacters @register="registerLoginCharacters" />
|
<LoginCharacters v-if="showCharacters" @register="registerLoginCharacters" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -64,15 +64,21 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed, onMounted, onBeforeUnmount, provide, shallowRef } from 'vue';
|
import {
|
||||||
|
ref,
|
||||||
|
computed,
|
||||||
|
onMounted,
|
||||||
|
onBeforeUnmount,
|
||||||
|
provide,
|
||||||
|
shallowRef,
|
||||||
|
defineAsyncComponent,
|
||||||
|
} from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { OmsLoginType } from '@/api/rbac';
|
import { OmsLoginType } from '@/api/rbac';
|
||||||
import { getLoginTypeFromRouteName } from '@/utils/login-portal';
|
import { getLoginTypeFromRouteName } from '@/utils/login-portal';
|
||||||
import { IconSunFill, IconMoonFill } from '@arco-design/web-vue/es/icon';
|
import { IconSunFill, IconMoonFill } from '@arco-design/web-vue/es/icon';
|
||||||
import LoginForm from './components/pwd-login/index.vue';
|
import LoginForm from './components/pwd-login/index.vue';
|
||||||
import WechatLogin from './components/wechat-login.vue';
|
|
||||||
import LoginCharacters from './components/LoginCharacters.vue';
|
|
||||||
import LoginHero from './components/LoginHero.vue';
|
import LoginHero from './components/LoginHero.vue';
|
||||||
import LoginLangToggle from './components/LoginLangToggle.vue';
|
import LoginLangToggle from './components/LoginLangToggle.vue';
|
||||||
import {
|
import {
|
||||||
@ -80,7 +86,15 @@ import {
|
|||||||
type LoginCharactersApi,
|
type LoginCharactersApi,
|
||||||
} from './composables/useLoginCharacters';
|
} from './composables/useLoginCharacters';
|
||||||
import { LOGIN_EXIT_KEY, type LoginExitApi } from './composables/useLoginExit';
|
import { LOGIN_EXIT_KEY, type LoginExitApi } from './composables/useLoginExit';
|
||||||
import gsap from 'gsap';
|
|
||||||
|
/** Admin login never mounts WeChat; keep it out of the critical path */
|
||||||
|
const WechatLogin = defineAsyncComponent(
|
||||||
|
() => import('./components/wechat-login.vue'),
|
||||||
|
);
|
||||||
|
/** Characters + gsap are decorative; async so /admin first paint stays light */
|
||||||
|
const LoginCharacters = defineAsyncComponent(
|
||||||
|
() => import('./components/LoginCharacters.vue'),
|
||||||
|
);
|
||||||
|
|
||||||
const LOGIN_APPEARANCE_KEY = 'login-appearance';
|
const LOGIN_APPEARANCE_KEY = 'login-appearance';
|
||||||
const LOGIN_PAGE_SCROLL_LOCK_CLASS = 'login-page-no-scroll';
|
const LOGIN_PAGE_SCROLL_LOCK_CLASS = 'login-page-no-scroll';
|
||||||
@ -91,9 +105,11 @@ const { t } = useI18n();
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const loginType = computed(() => getLoginTypeFromRouteName(route.name as string));
|
const loginType = computed(() => getLoginTypeFromRouteName(route.name as string));
|
||||||
const isCustomerLogin = computed(() => loginType.value === OmsLoginType.Customer);
|
const isCustomerLogin = computed(() => loginType.value === OmsLoginType.Customer);
|
||||||
|
/** 角色动画偏重,首屏延后一帧再挂,管理端也挂但异步分包 */
|
||||||
|
const showCharacters = ref(false);
|
||||||
const activeKey = ref<'pwd' | 'wechat'>('pwd');
|
const activeKey = ref<'pwd' | 'wechat'>('pwd');
|
||||||
const wechatMounted = ref(false);
|
const wechatMounted = ref(false);
|
||||||
const wechatLoginRef = ref<InstanceType<typeof WechatLogin> | null>(null);
|
const wechatLoginRef = ref<{ prefetch?: () => void } | null>(null);
|
||||||
const shellReady = ref(false);
|
const shellReady = ref(false);
|
||||||
const isExiting = ref(false);
|
const isExiting = ref(false);
|
||||||
const loginCharactersApi = shallowRef<LoginCharactersApi | null>(null);
|
const loginCharactersApi = shallowRef<LoginCharactersApi | null>(null);
|
||||||
@ -128,7 +144,12 @@ provide(LOGIN_EXIT_KEY, {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
gsap.set([...leftTargets, ...rightTargets], { x: 0, opacity: 1, force3D: true });
|
void import('gsap').then(({ default: gsap }) => {
|
||||||
|
gsap.set([...leftTargets, ...rightTargets], {
|
||||||
|
x: 0,
|
||||||
|
opacity: 1,
|
||||||
|
force3D: true,
|
||||||
|
});
|
||||||
|
|
||||||
const tl = gsap.timeline({
|
const tl = gsap.timeline({
|
||||||
defaults: { ease: 'power2.inOut' },
|
defaults: { ease: 'power2.inOut' },
|
||||||
@ -140,7 +161,7 @@ provide(LOGIN_EXIT_KEY, {
|
|||||||
formEl,
|
formEl,
|
||||||
{
|
{
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
duration: 0.35,
|
duration: 0.22,
|
||||||
ease: 'power1.out',
|
ease: 'power1.out',
|
||||||
},
|
},
|
||||||
0,
|
0,
|
||||||
@ -152,10 +173,10 @@ provide(LOGIN_EXIT_KEY, {
|
|||||||
{
|
{
|
||||||
x: '-100%',
|
x: '-100%',
|
||||||
opacity: 0.96,
|
opacity: 0.96,
|
||||||
duration: 0.55,
|
duration: 0.32,
|
||||||
ease: 'power3.inOut',
|
ease: 'power3.inOut',
|
||||||
},
|
},
|
||||||
0.05,
|
0.02,
|
||||||
);
|
);
|
||||||
|
|
||||||
tl.to(
|
tl.to(
|
||||||
@ -163,10 +184,10 @@ provide(LOGIN_EXIT_KEY, {
|
|||||||
{
|
{
|
||||||
x: '100%',
|
x: '100%',
|
||||||
opacity: 0.96,
|
opacity: 0.96,
|
||||||
duration: 0.55,
|
duration: 0.32,
|
||||||
ease: 'power3.inOut',
|
ease: 'power3.inOut',
|
||||||
},
|
},
|
||||||
0.08,
|
0.04,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (containerRef.value) {
|
if (containerRef.value) {
|
||||||
@ -174,13 +195,14 @@ provide(LOGIN_EXIT_KEY, {
|
|||||||
containerRef.value,
|
containerRef.value,
|
||||||
{
|
{
|
||||||
backgroundColor: '#f0f3f4',
|
backgroundColor: '#f0f3f4',
|
||||||
duration: 0.45,
|
duration: 0.28,
|
||||||
ease: 'power1.inOut',
|
ease: 'power1.inOut',
|
||||||
},
|
},
|
||||||
0.04,
|
0.02,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
} satisfies LoginExitApi);
|
} satisfies LoginExitApi);
|
||||||
|
|
||||||
@ -228,8 +250,14 @@ function prefetchWechatQr() {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
document.documentElement.classList.add(LOGIN_PAGE_SCROLL_LOCK_CLASS);
|
document.documentElement.classList.add(LOGIN_PAGE_SCROLL_LOCK_CLASS);
|
||||||
requestAnimationFrame(() => {
|
|
||||||
shellReady.value = true;
|
shellReady.value = true;
|
||||||
|
// 首屏先出表单,下一帧再挂角色动画分包
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
showCharacters.value = true;
|
||||||
|
});
|
||||||
|
// 后台预拉完整语言包,进系统时几乎不等
|
||||||
|
void import('@/locale').then(({ ensureFullLocaleMessages }) => {
|
||||||
|
void ensureFullLocaleMessages();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -416,9 +444,8 @@ onBeforeUnmount(() => {
|
|||||||
box-shadow: 0 16px 40px rgba(30, 41, 59, 0.08);
|
box-shadow: 0 16px 40px rgba(30, 41, 59, 0.08);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
opacity: 0;
|
opacity: 1;
|
||||||
transform: translateY(16px);
|
transform: none;
|
||||||
transition: opacity 0.28s ease-out, transform 0.28s ease-out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-form:hover {
|
.login-form:hover {
|
||||||
@ -428,7 +455,7 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
.login-form--ready {
|
.login-form--ready {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-content {
|
.login-content {
|
||||||
|
|||||||
@ -13,7 +13,10 @@ import configCompressPlugin from './config/plugin/compress';
|
|||||||
|
|
||||||
export default defineConfig(({ mode }) => {
|
export default defineConfig(({ mode }) => {
|
||||||
const env = loadEnv(mode, process.cwd(), '');
|
const env = loadEnv(mode, process.cwd(), '');
|
||||||
const omsAdminTarget = (env.VITE_OMS_ADMIN_URL || 'http://api.nanafa.com/admin').replace(/\/$/, '');
|
// Admin API 目标:由 VITE_OMS_ADMIN_URL 决定(开发默认线上)
|
||||||
|
const omsAdminTarget = (
|
||||||
|
env.VITE_OMS_ADMIN_URL || 'http://api.nanafa.com/admin'
|
||||||
|
).replace(/\/$/, '');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
server: {
|
server: {
|
||||||
@ -33,15 +36,15 @@ export default defineConfig(({ mode }) => {
|
|||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
proxyTimeout: 30000,
|
proxyTimeout: 30000,
|
||||||
},
|
},
|
||||||
// 权限(admin)43.139.210.35:5281 -> http://api.nanafa.com/admin(路径保持 /rbac/xxx -> .../admin/rbac/xxx)
|
// Admin API:目标由 VITE_OMS_ADMIN_URL 决定(默认 http://api.nanafa.com/admin)
|
||||||
'/rbac': {
|
'/rbac': {
|
||||||
target: 'http://api.nanafa.com/admin',
|
target: omsAdminTarget,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
},
|
},
|
||||||
// OMS 订单等 admin 接口:/order/list、/order/detail(与 Swagger OMS-Order 一致)
|
// OMS 订单等 admin 接口:/order/list、/order/detail(与 Swagger OMS-Order 一致)
|
||||||
'/order': {
|
'/order': {
|
||||||
target: 'http://api.nanafa.com/admin',
|
target: omsAdminTarget,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
bypass(req) {
|
bypass(req) {
|
||||||
@ -65,7 +68,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
},
|
},
|
||||||
// OMS 仓储 admin 接口:/warehouse/sku/list 等(与 Swagger OMS-Warehouse 一致)
|
// OMS 仓储 admin 接口:/warehouse/sku/list 等(与 Swagger OMS-Warehouse 一致)
|
||||||
'/warehouse': {
|
'/warehouse': {
|
||||||
target: 'http://api.nanafa.com/admin',
|
target: omsAdminTarget,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
bypass(req) {
|
bypass(req) {
|
||||||
@ -86,7 +89,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
'/plugin': {
|
'/plugin': {
|
||||||
target: 'http://api.nanafa.com/admin',
|
target: omsAdminTarget,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
bypass(req) {
|
bypass(req) {
|
||||||
@ -97,7 +100,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
'/Plugin': {
|
'/Plugin': {
|
||||||
target: 'http://api.nanafa.com/admin',
|
target: omsAdminTarget,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
bypass(req) {
|
bypass(req) {
|
||||||
@ -108,17 +111,17 @@ export default defineConfig(({ mode }) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
'/monitor': {
|
'/monitor': {
|
||||||
target: 'http://api.nanafa.com/admin',
|
target: omsAdminTarget,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
},
|
},
|
||||||
'/customer': {
|
'/customer': {
|
||||||
target: 'http://api.nanafa.com/admin',
|
target: omsAdminTarget,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
},
|
},
|
||||||
'/wechat': {
|
'/wechat': {
|
||||||
target: 'http://api.nanafa.com/admin',
|
target: omsAdminTarget,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
// H5 页面路由与 API 同前缀:刷新 /wechat/register、/wechat/bind 须回退 index.html
|
// H5 页面路由与 API 同前缀:刷新 /wechat/register、/wechat/bind 须回退 index.html
|
||||||
@ -134,17 +137,17 @@ export default defineConfig(({ mode }) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
'/recharge': {
|
'/recharge': {
|
||||||
target: 'http://api.nanafa.com/admin',
|
target: omsAdminTarget,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
},
|
},
|
||||||
'/channel': {
|
'/channel': {
|
||||||
target: 'http://api.nanafa.com/admin',
|
target: omsAdminTarget,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
},
|
},
|
||||||
'/operation': {
|
'/operation': {
|
||||||
target: 'http://api.nanafa.com/admin',
|
target: omsAdminTarget,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
// 与页面同前缀的 API 代理:必须在代理层 bypass,否则刷新会先于/绕过 unshift 中间件仍被转发到后端 404
|
// 与页面同前缀的 API 代理:必须在代理层 bypass,否则刷新会先于/绕过 unshift 中间件仍被转发到后端 404
|
||||||
@ -165,7 +168,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
'/system': {
|
'/system': {
|
||||||
target: 'http://api.nanafa.com/admin',
|
target: omsAdminTarget,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
bypass(req) {
|
bypass(req) {
|
||||||
@ -197,7 +200,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
},
|
},
|
||||||
// 微信公众号服务器回调(兼容 WMS 路径 /api/apiwechat/verification)
|
// 微信公众号服务器回调(兼容 WMS 路径 /api/apiwechat/verification)
|
||||||
'/api/apiwechat': {
|
'/api/apiwechat': {
|
||||||
target: 'http://api.nanafa.com/admin',
|
target: omsAdminTarget,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user