- 阶段 A: 增加 KeepAlive 缓存高频列表页,改用 onActivated 进行后台静默刷新,优化 tab 切换与 HomeEntry 闪屏 - 阶段 B: beforeEach 守卫在 TTL 内走同步快速路径,api 请求拦截器缓存 token 避免重复解析 JWT - 阶段 C: 引入 unplugin 插件启用 Element Plus 组件按需加载,清理 App.vue 中 960+ 行冗余暗色主题 CSS - i18n 拆包: 将三语文案包提取为独立懒加载 chunks,主包体积从 209KB 减少至 99KB (降低 53%)
149 lines
4.1 KiB
TypeScript
149 lines
4.1 KiB
TypeScript
import axios from 'axios';
|
||
import router from './router';
|
||
import { clearStaffSession, reconcileStaffSessionFromToken, useAuthStore } from './stores/auth';
|
||
import { ensureStaffSession, resetStaffSessionHydration } from './utils/session-hydrate';
|
||
import { ADMIN_LOCALE_STORAGE_KEY } from './i18n';
|
||
|
||
const api = axios.create({ baseURL: '/api' });
|
||
|
||
let handlingAuthInvalid = false;
|
||
let handling403Portal = false;
|
||
|
||
const PORTAL_MISMATCH_CODES = new Set(['ADMIN_ACCESS_ONLY', 'AGENT_ACCESS_ONLY']);
|
||
|
||
function requestPath(config: { url?: string; baseURL?: string } | undefined): string {
|
||
if (!config?.url) return '';
|
||
const base = config.baseURL ?? '';
|
||
return `${base}${config.url}`;
|
||
}
|
||
|
||
function isLoginRequest(config: { url?: string; baseURL?: string } | undefined): boolean {
|
||
return requestPath(config).includes('/auth/login');
|
||
}
|
||
|
||
function isInvalidCredentialsResponse(data: unknown): boolean {
|
||
return (
|
||
typeof data === 'object' &&
|
||
data !== null &&
|
||
'code' in data &&
|
||
(data as { code?: unknown }).code === 'INVALID_CREDENTIALS'
|
||
);
|
||
}
|
||
|
||
async function redirectToLoginForInvalidSession() {
|
||
if (handlingAuthInvalid) return;
|
||
handlingAuthInvalid = true;
|
||
try {
|
||
clearStaffSession();
|
||
resetStaffSessionHydration();
|
||
if (router.currentRoute.value.path !== '/login') {
|
||
await router.replace('/login');
|
||
}
|
||
} finally {
|
||
handlingAuthInvalid = false;
|
||
}
|
||
}
|
||
|
||
let _lastReconciledToken: string | null = null;
|
||
|
||
api.interceptors.request.use((config) => {
|
||
const t = localStorage.getItem('manage_token');
|
||
if (t) config.headers.Authorization = `Bearer ${t}`;
|
||
|
||
const locale = localStorage.getItem(ADMIN_LOCALE_STORAGE_KEY) || 'zh-CN';
|
||
config.headers['X-Locale'] = locale;
|
||
|
||
// 只在 token 变更时才重新 decode JWT + reconcile,避免并发请求时多次执行同步开销
|
||
if (t !== _lastReconciledToken) {
|
||
reconcileStaffSessionFromToken();
|
||
_lastReconciledToken = t;
|
||
}
|
||
const auth = useAuthStore();
|
||
const path = requestPath(config);
|
||
|
||
if (path.includes('/admin/') && auth.isAgent.value) {
|
||
return Promise.reject(
|
||
Object.assign(new Error('Agent portal blocked admin API'), {
|
||
isPortalMismatch: true,
|
||
blockedPath: path,
|
||
}),
|
||
);
|
||
}
|
||
if (path.includes('/agent/') && auth.isAdmin.value) {
|
||
return Promise.reject(
|
||
Object.assign(new Error('Admin portal blocked agent API'), {
|
||
isPortalMismatch: true,
|
||
blockedPath: path,
|
||
}),
|
||
);
|
||
}
|
||
|
||
return config;
|
||
});
|
||
|
||
api.interceptors.response.use(
|
||
async (res) => {
|
||
if (!isLoginRequest(res.config) && isInvalidCredentialsResponse(res.data)) {
|
||
await redirectToLoginForInvalidSession();
|
||
return Promise.reject(
|
||
Object.assign(new Error((res.data as { error?: string }).error || 'Invalid credentials'), {
|
||
response: res,
|
||
config: res.config,
|
||
}),
|
||
);
|
||
}
|
||
return res;
|
||
},
|
||
async (err) => {
|
||
if (err.isPortalMismatch) {
|
||
await ensureStaffSession();
|
||
if (router.currentRoute.value.path !== '/login') {
|
||
await router.replace('/');
|
||
}
|
||
return Promise.reject(err);
|
||
}
|
||
|
||
if (
|
||
!isLoginRequest(err.config) &&
|
||
(err.response?.status === 401 || isInvalidCredentialsResponse(err.response?.data))
|
||
) {
|
||
await redirectToLoginForInvalidSession();
|
||
}
|
||
|
||
if (
|
||
err.response?.status === 403 &&
|
||
!handling403Portal &&
|
||
PORTAL_MISMATCH_CODES.has(err.response?.data?.code)
|
||
) {
|
||
handling403Portal = true;
|
||
await ensureStaffSession();
|
||
handling403Portal = false;
|
||
|
||
const auth = useAuthStore();
|
||
const path = requestPath(err.config);
|
||
const isAdminApi = path.includes('/admin/');
|
||
const isAgentApi = path.includes('/agent/');
|
||
|
||
if ((isAdminApi && auth.isAgent.value) || (isAgentApi && auth.isAdmin.value)) {
|
||
if (router.currentRoute.value.path !== '/login') {
|
||
await router.replace('/');
|
||
}
|
||
return Promise.reject(err);
|
||
}
|
||
|
||
if (err.config) {
|
||
return api.request(err.config);
|
||
}
|
||
|
||
clearStaffSession();
|
||
if (router.currentRoute.value.path !== '/login') {
|
||
await router.replace('/login');
|
||
}
|
||
}
|
||
|
||
return Promise.reject(err);
|
||
},
|
||
);
|
||
|
||
export default api;
|