style:新增登录界面 完善api请求逻辑

This commit is contained in:
liangdong
2025-12-30 21:07:45 +08:00
parent 1cc15fc76d
commit bd2a300f55
9 changed files with 409 additions and 253 deletions

2
components.d.ts vendored
View File

@@ -14,6 +14,7 @@ declare module 'vue' {
CardItem: typeof import('./src/components/cardItem/index.vue')['default'] CardItem: typeof import('./src/components/cardItem/index.vue')['default']
ElAside: typeof import('element-plus/es')['ElAside'] ElAside: typeof import('element-plus/es')['ElAside']
ElButton: typeof import('element-plus/es')['ElButton'] ElButton: typeof import('element-plus/es')['ElButton']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElContainer: typeof import('element-plus/es')['ElContainer'] ElContainer: typeof import('element-plus/es')['ElContainer']
ElDatePick: typeof import('element-plus/es')['ElDatePick'] ElDatePick: typeof import('element-plus/es')['ElDatePick']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
@@ -27,6 +28,7 @@ declare module 'vue' {
ElHeader: typeof import('element-plus/es')['ElHeader'] ElHeader: typeof import('element-plus/es')['ElHeader']
ElIcon: typeof import('element-plus/es')['ElIcon'] ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput'] ElInput: typeof import('element-plus/es')['ElInput']
ElLink: typeof import('element-plus/es')['ElLink']
ElMain: typeof import('element-plus/es')['ElMain'] ElMain: typeof import('element-plus/es')['ElMain']
ElMenu: typeof import('element-plus/es')['ElMenu'] ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] ElMenuItem: typeof import('element-plus/es')['ElMenuItem']

View File

@@ -9,6 +9,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@micro-zoe/micro-app": "^1.0.0-rc.28", "@micro-zoe/micro-app": "^1.0.0-rc.28",
"element-plus": "^2.13.0", "element-plus": "^2.13.0",
"pinia": "^3.0.4", "pinia": "^3.0.4",

3
pnpm-lock.yaml generated
View File

@@ -8,6 +8,9 @@ importers:
.: .:
dependencies: dependencies:
'@element-plus/icons-vue':
specifier: ^2.3.2
version: 2.3.2(vue@3.5.26(typescript@5.9.3))
'@micro-zoe/micro-app': '@micro-zoe/micro-app':
specifier: ^1.0.0-rc.28 specifier: ^1.0.0-rc.28
version: 1.0.0-rc.28 version: 1.0.0-rc.28

View File

@@ -9,25 +9,19 @@
@select="handleMenuSelect" @select="handleMenuSelect"
router router
> >
<template v-for="item in menuList" :key="item.path"> <template v-for="item in menuList" :key="`${item.path}-${item.meta?.title}`">
<el-sub-menu v-if="item.children && item.children.length > 0"> <el-sub-menu v-if="item.children && item.children.length > 0">
<template #title> <template #title>
<el-icon> <el-icon v-if="item.meta?.icon"><component :is="item.meta.icon" /></el-icon>
<location />
</el-icon>
<span>{{ item.meta.title }}</span> <span>{{ item.meta.title }}</span>
</template> </template>
<el-menu-item :index="`${item.path}/${row.path}`" v-for="(row,key) in item.children" :key="key"> <el-menu-item :index="resolvePath(item.path, row.path)" v-for="(row,key) in item.children" :key="resolvePath(item.path, row.path)">
<el-icon> <el-icon v-if="row.meta?.icon"><component :is="row.meta.icon" /></el-icon>
<location />
</el-icon>
<template #title>{{ row.meta.title }}</template> <template #title>{{ row.meta.title }}</template>
</el-menu-item> </el-menu-item>
</el-sub-menu> </el-sub-menu>
<el-menu-item v-else :index="item.path"> <el-menu-item v-else :index="item.path">
<el-icon> <el-icon v-if="item.meta?.icon"><component :is="item.meta.icon" /></el-icon>
<location />
</el-icon>
<template #title>{{ item.meta.title }}</template> <template #title>{{ item.meta.title }}</template>
</el-menu-item> </el-menu-item>
</template> </template>
@@ -37,6 +31,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { Location } from '@element-plus/icons-vue' import { Location } from '@element-plus/icons-vue'
defineOptions({ name: "standardMenu" }) defineOptions({ name: "standardMenu" })
const route = useRoute();
const {mode="vertical",menuList,isCollapse,activeMenu} = defineProps<{ const {mode="vertical",menuList,isCollapse,activeMenu} = defineProps<{
mode?: 'vertical' | 'horizontal' mode?: 'vertical' | 'horizontal'
@@ -51,9 +46,30 @@ const emit = defineEmits<{
const activeIndex = computed(() => { const activeIndex = computed(() => {
// 如果传入了 activeMenu使用它否则使用默认值 // 如果传入了 activeMenu使用它否则使用默认值
return activeMenu || '1-1' return activeMenu || route.path
}) })
/**
* 智能拼接路径
* @param parentPath 父级路径
* @param childPath 子级路径
*/
const resolvePath = (parentPath: string, childPath: string) => {
// 1. 如果子路径是绝对路径(以 / 开头),直接返回子路径
if (childPath.startsWith('/')) {
return childPath;
}
// 2. 移除父路径末尾的斜杠
const parent = parentPath.endsWith('/') ? parentPath.slice(0, -1) : parentPath;
// 3. 移除子路径开头的斜杠(虽然第一步已经过滤了,但为了健壮性保留)
const child = childPath.startsWith('/') ? childPath.slice(1) : childPath;
// 4. 返回拼接后的路径
return `${parent}/${child}`;
};
// 处理菜单选中事件 // 处理菜单选中事件
const handleMenuSelect = (index: string) => { const handleMenuSelect = (index: string) => {
// 如果是水平模式(顶部菜单),触发选中事件 // 如果是水平模式(顶部菜单),触发选中事件

View File

@@ -9,6 +9,12 @@ import en from "element-plus/es/locale/lang/en";
import Directives from '@/utils/directives'; import Directives from '@/utils/directives';
import '@/styles/common.scss'; import '@/styles/common.scss';
// 全局导入element ui样式类
import 'element-plus/es/components/message/style/css'
import 'element-plus/es/components/notification/style/css'
import 'element-plus/es/components/message-box/style/css'
import 'element-plus/es/components/loading/style/css'
const pinia = createPinia(); const pinia = createPinia();
const app = createApp(App); const app = createApp(App);

View File

@@ -49,6 +49,7 @@ defineOptions({
}); });
const userStore = useUserStore(); const userStore = useUserStore();
const router = useRouter();
// 响应式断点(小屏阈值,小于此值视为小屏) // 响应式断点(小屏阈值,小于此值视为小屏)
const BREAKPOINT = 1024; const BREAKPOINT = 1024;
@@ -158,7 +159,16 @@ const handleSideMenuSelect = (menuPath: string) => {
// 初始化:默认选中第一个菜单 // 初始化:默认选中第一个菜单
onMounted(() => { onMounted(() => {
if (topLevelMenuList.value.length > 0) {
const currentRoutePath = router.currentRoute.value.path;
const matchedTopMenu = topLevelMenuList.value.find(menu =>
currentRoutePath.startsWith(`${menu.path}/`) || currentRoutePath === menu.path
);
if (matchedTopMenu && matchedTopMenu.path) {
selectedTopMenu.value = matchedTopMenu.path;
} else if (topLevelMenuList.value.length > 0) {
// 否则默认选中第一个菜单
const firstMenu = topLevelMenuList.value[0]; const firstMenu = topLevelMenuList.value[0];
if (firstMenu && firstMenu.path) { if (firstMenu && firstMenu.path) {
selectedTopMenu.value = firstMenu.path; selectedTopMenu.value = firstMenu.path;

View File

@@ -1,48 +1,89 @@
<template> <template>
<div class="mj-login">
<div class="login-container"> <div class="login-container">
<div class="left-section">
<div class="logo-wrapper">
<div class="logo-icon">M</div>
<span class="logo-text">M Version <span>SaaS</span></span>
</div>
<div class="content-wrapper">
<h1 class="main-title">专注业务增长</h1>
<h1 class="sub-title">智能高效管理</h1>
<p class="description">
为现代企业打造的一站式后台管理方案集成商机合同项目及招聘管理全方位提升团队效能
</p>
</div>
<div class="footer-users">
<div class="avatar-group">
<img
v-for="i in 4"
:key="i"
:src="`https://i.pravatar.cc/100?img=${i + 10}`"
alt="user"
/>
</div>
<span class="user-count">超过 5,000+ 行业领先企业正在使用</span>
</div>
<div class="bg-blur-circle"></div>
</div>
<div class="right-section">
<div class="login-box"> <div class="login-box">
<h2 class="login-title">系统登录</h2> <h2 class="welcome-title">欢迎回来</h2>
<p class="welcome-sub">请登录您的账号以继续</p>
<el-form <el-form
ref="loginFormRef"
:model="loginForm" :model="loginForm"
:rules="loginRules" label-position="top"
label-width="0"
class="login-form" class="login-form"
ref="loginFormRef"
> >
<el-form-item prop="username"> <el-form-item label="账号/邮箱" class="account-item">
<el-input <el-input
v-model="loginForm.username" v-model="loginForm.username"
placeholder="请输入用户名" placeholder="admin@figmamake.com"
size="large" :prefix-icon="Message"
prefix-icon="User"
clearable
/> />
</el-form-item> </el-form-item>
<el-form-item prop="password">
<el-form-item class="password-item">
<template #label>
<div class="password-label">
<span>密码</span>
<el-link type="primary" underline="never" class="forgot-pass"
>忘记密码</el-link
>
</div>
</template>
<el-input <el-input
v-model="loginForm.password" v-model="loginForm.password"
type="password" type="password"
placeholder="请输入密码"
size="large"
prefix-icon="Lock"
show-password show-password
clearable placeholder="••••••••"
@keyup.enter="handleLogin" :prefix-icon="Lock"
/> />
</el-form-item> </el-form-item>
<el-form-item>
<el-button <div class="form-options">
type="primary" <el-checkbox v-model="loginForm.remember">
size="large" <span class="checkbox-text">记住此设备</span>
:loading="loading" </el-checkbox>
class="login-button" </div>
@click="handleLogin"
> <el-button type="primary" class="login-btn" @click="handleLogin">
{{ loading ? '登录中...' : '登录' }} 登录系统 <el-icon class="el-icon--right"><Right /></el-icon>
</el-button> </el-button>
</el-form-item>
</el-form> </el-form>
<div class="form-footer">
<span>© 2025 智视界保留所有权利</span>
<div class="links">
<el-link underline="never">隐私政策</el-link>
<el-link underline="never">服务条款</el-link>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -52,6 +93,7 @@
import { reactive, ref } from "vue"; import { reactive, ref } from "vue";
import { useRouter, useRoute } from "vue-router"; import { useRouter, useRoute } from "vue-router";
import { ElMessage, type FormInstance, type FormRules } from "element-plus"; import { ElMessage, type FormInstance, type FormRules } from "element-plus";
import { Message, Lock, Right } from "@element-plus/icons-vue";
import { login } from "@/api"; import { login } from "@/api";
import { useUserStore } from "@/store"; import { useUserStore } from "@/store";
import useTokenRefresh from "@/hooks/useTokenRefresh"; import useTokenRefresh from "@/hooks/useTokenRefresh";
@@ -74,7 +116,12 @@ const loginForm = reactive({
const loginRules: FormRules = { const loginRules: FormRules = {
username: [ username: [
{ required: true, message: "请输入用户名", trigger: "blur" }, { required: true, message: "请输入用户名", trigger: "blur" },
{ min: 3, max: 20, message: "用户名长度在 3 到 20 个字符", trigger: "blur" }, {
min: 3,
max: 20,
message: "用户名长度在 3 到 20 个字符",
trigger: "blur",
},
], ],
password: [ password: [
{ required: true, message: "请输入密码", trigger: "blur" }, { required: true, message: "请输入密码", trigger: "blur" },
@@ -96,7 +143,8 @@ const handleLogin = async () => {
}); });
if (response.code === 0 && response.data) { if (response.code === 0 && response.data) {
const { accessToken, refreshToken, expiresIn, userInfo } = response.data; const { accessToken, refreshToken, expiresIn, userInfo } =
response.data;
// 保存 token // 保存 token
setTokens({ setTokens({
@@ -128,38 +176,189 @@ const handleLogin = async () => {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.mj-login { // 变量定义
width: 100%; $primary-color: #2b5cff;
$text-main: #1a1a1a;
$text-secondary: #999;
$bg-light: #f8faff;
.login-container {
display: flex;
height: 100vh; height: 100vh;
width: 100vw;
overflow: hidden;
}
// --- 左侧展示区 ---
.left-section {
flex: 1.2;
background-color: $bg-light;
position: relative;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 80px;
.logo-wrapper {
display: flex;
align-items: center;
gap: 12px;
z-index: 2;
.logo-icon {
width: 32px;
height: 32px;
background: $primary-color;
color: white;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 8px;
font-weight: bold;
}
.logo-text {
font-size: 20px;
font-weight: 700;
span {
color: $primary-color;
}
}
}
.login-container { .content-wrapper {
width: 100%; z-index: 2;
max-width: 400px; .main-title {
padding: 20px; font-size: 48px;
color: $text-main;
margin: 0;
}
.sub-title {
font-size: 48px;
color: $primary-color;
margin: 10px 0 0 0;
}
.description {
color: #666;
max-width: 460px;
line-height: 1.8;
margin-top: 30px;
}
}
.footer-users {
display: flex;
align-items: center;
z-index: 2;
.avatar-group img {
width: 36px;
height: 36px;
border-radius: 50%;
border: 2px solid white;
margin-right: -10px;
}
.user-count {
color: $text-secondary;
font-size: 14px;
margin-left: 20px;
}
}
.bg-blur-circle {
position: absolute;
width: 600px;
height: 600px;
background: radial-gradient(
circle,
rgba(92, 103, 255, 0.12) 0%,
rgba(255, 122, 250, 0.08) 50%,
transparent 100%
);
filter: blur(80px);
top: 15%;
left: 15%;
}
}
// --- 右侧登录区 ---
.right-section {
flex: 1;
background: white;
display: flex;
align-items: center;
justify-content: center;
.login-box { .login-box {
background: #fff; width: 100%;
border-radius: 8px; max-width: 440px;
padding: 40px; padding: 0 40px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
.login-title { .welcome-title {
text-align: center; font-size: 32px;
margin-bottom: 30px; margin-bottom: 8px;
font-size: 24px; }
font-weight: 600; .welcome-sub {
color: #333; color: $text-secondary;
margin-bottom: 40px;
} }
.login-form { .login-form {
.login-button { // 深度作用选择器统一处理 Element Plus 内部样式
width: 100%; :deep(.el-form-item__label) {
margin-top: 10px; font-weight: 500;
color: #666;
padding-bottom: 4px;
} }
:deep(.el-input__wrapper) {
height: 54px;
border-radius: 12px;
box-shadow: none;
transition: none;
background-color: #f5f7fa;
border: 1px solid transparent;
&.is-focus {
box-shadow: none;
background-color: #fff;
border-color: #dce4ff;
}
}
.password-label {
display: flex;
justify-content: space-between;
width: 100%;
.el-link {
font-size: 13px;
font-weight: bold;
}
}
.checkbox-label {
color: #333;
font-weight: 500;
}
.login-btn {
width: 100%;
height: 54px;
border-radius: 12px;
background-color: $primary-color;
margin-top: 25px;
font-size: 16px;
border: none;
box-shadow: 0 8px 16px rgba(43, 92, 255, 0.2);
}
}
.form-footer {
margin-top: 80px;
display: flex;
justify-content: space-between;
font-size: 13px;
color: $text-secondary;
.links {
display: flex;
gap: 20px;
} }
} }
} }

View File

@@ -3,9 +3,7 @@
<!-- 顶部的tabs菜单 --> <!-- 顶部的tabs菜单 -->
<div class="organization-tabs"> <div class="organization-tabs">
<stageBreadcrumbs title="组织管理"> <stageBreadcrumbs title="组织管理">
<template #content> <template #content> 内容占位 </template>
内容占位
</template>
<template #action> <template #action>
<el-button type="primary" :icon="Plus" plain>新增集团</el-button> <el-button type="primary" :icon="Plus" plain>新增集团</el-button>
</template> </template>
@@ -53,7 +51,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Plus, Search } from "@element-plus/icons-vue"; import { Plus, Search } from "@element-plus/icons-vue";
import stageBreadcrumbs from '@/components/stageBreadcrumbs/index.vue'; import stageBreadcrumbs from "@/components/stageBreadcrumbs/index.vue";
defineOptions({ name: "Organization" }); defineOptions({ name: "Organization" });
const addValue = ref(""); const addValue = ref("");
@@ -61,7 +59,6 @@ const search = ref("");
const activeName = ref("first"); const activeName = ref("first");
interface Tree { interface Tree {
label: string; label: string;
children?: Tree[]; children?: Tree[];
@@ -135,6 +132,7 @@ const defaultProps = {
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@use "sass:math";
.mj-organization { .mj-organization {
.mj-organization-card { .mj-organization-card {
border-radius: 16px; border-radius: 16px;
@@ -145,7 +143,6 @@ const defaultProps = {
} }
.organization-tabs { .organization-tabs {
} }
.organization-content { .organization-content {
@@ -168,7 +165,7 @@ const defaultProps = {
.org-tree-list { .org-tree-list {
flex: 1; flex: 1;
overflow: auto; overflow: auto;
padding: $mj-padding-standard/2 $mj-padding-standard; padding: math.div($mj-padding-standard, 2) $mj-padding-standard;
} }
.org-bottom-add { .org-bottom-add {
border-top: 1px solid #f1f5f9; border-top: 1px solid #f1f5f9;

View File

@@ -1,197 +1,125 @@
import axios from "axios"; import axios from "axios";
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import useTokenRefresh from "@/hooks/useTokenRefresh"; import { ElNotification } from 'element-plus';
import 'element-plus/es/components/notification/style/css'
import { ElNotification } from 'element-plus'
import { getMockData, shouldUseMock } from '@/mock' //mock数据信息 import { getMockData, shouldUseMock } from '@/mock' //mock数据信息
const baseUrl = import.meta.env.VITE_APP_BASE_API; const baseUrl = import.meta.env.VITE_APP_BASE_API;
// 定义响应数据类型 // 1. 锁和队列定义在类外部,确保全局唯一
interface ApiResponse<T = any> { let isRefreshing = false;
code: number; let requestsQueue: Array<(token: string) => void> = [];
data: T;
msg: string;
[key: string]: any;
}
// 定义错误类型
interface ApiError {
code: number;
msg: string;
[key: string]: any;
}
// 封装axios请求
class HttpRequest { class HttpRequest {
private baseUrl: string; private instance: AxiosInstance;
private instanceMap: Map<string, AxiosInstance> = new Map();
constructor(baseUrl?: string) { constructor(externalBaseUrl?: string) {
this.baseUrl = baseUrl || ""; this.instance = axios.create({
} baseURL: externalBaseUrl || baseUrl,
// 获取基础配置
private getBaseConfig(): AxiosRequestConfig {
return {
baseURL: this.baseUrl || baseUrl,
timeout: 50 * 1000, timeout: 50 * 1000,
headers: { headers: { "Content-Type": "application/json;charset=utf-8" },
"Content-Type": "application/json;charset=utf-8", });
},
}; this.setupInterceptors();
} }
// 创建 axios 实例 // --- Token 管理方法 ---
private createAxiosInstance(config: AxiosRequestConfig): AxiosInstance { private getAccessToken() { return localStorage.getItem("accessToken"); }
return axios.create(config); private getRefreshToken() { return localStorage.getItem("refreshToken"); }
private clearTokens() {
localStorage.removeItem("accessToken");
localStorage.removeItem("refreshToken");
// 这里可以触发跳转登录逻辑例如router.push('/login')
}
private setTokens(data: any) {
localStorage.setItem("accessToken", data.accessToken);
localStorage.setItem("refreshToken", data.refreshToken);
} }
// 设置请求和响应拦截器 private setupInterceptors() {
private setupInterceptors(instance: AxiosInstance, url: string) {
const { refreshToken, getAccessToken, clearTokens } = useTokenRefresh(this.baseUrl);
// 请求拦截器 // 请求拦截器
instance.interceptors.request.use( this.instance.interceptors.request.use(
(config: AxiosRequestConfig) => { (config) => {
// 添加 token const token = this.getAccessToken();
const token = getAccessToken(); if (token && config.headers) {
if (token) {
config.headers = config.headers || {};
config.headers.Authorization = `Bearer ${token}`; config.headers.Authorization = `Bearer ${token}`;
} }
return config; return config;
}, },
(error: any) => { (error) => Promise.reject(error)
console.error('Request error:', error);
return Promise.reject(error);
}
); );
// 响应拦截器 // 响应拦截器
instance.interceptors.response.use( this.instance.interceptors.response.use(
async (response: AxiosResponse<ApiResponse>) => { async (response: AxiosResponse) => {
const { data: responseData, status } = response; const { data: res, config: originalRequest } = response;
switch (responseData.code) { // 业务成功直接返回数据
case 0: if (res.code === 0) return res;
// 成功
return Promise.resolve(responseData);
case 302: // 重点401 未授权处理
// 重定向 if (res.code === 401) {
if (responseData.msg) {
window.location.href = responseData.msg; // 如果已经在刷新中了,将请求挂起并加入队列
if (isRefreshing) {
return new Promise((resolve) => {
requestsQueue.push((token: string) => {
originalRequest.headers!.Authorization = `Bearer ${token}`;
resolve(this.instance(originalRequest));
});
});
} }
return Promise.reject(responseData);
case 401: // 开始刷新流程
// 未授权需要刷新token isRefreshing = true;
if ( const rToken = this.getRefreshToken();
originalRequest &&
!originalRequest._retry && if (!rToken) {
!/\/auth\/refresh$/i.test(originalRequest.url || "") this.clearTokens();
) { return Promise.reject(res);
originalRequest._retry = true; }
try { try {
// 刷新 token // 使用一个干净的 axios 实例去发刷新请求,避免拦截器死循环
await refreshToken(); const refreshRes = await axios.post(`${this.instance.defaults.baseURL}/auth/refresh`, {
refreshToken: rToken
});
// 重试原请求 const newToken = refreshRes.data.data.accessToken;
const newToken = getAccessToken(); this.setTokens(refreshRes.data.data);
if (newToken) {
originalRequest.headers = originalRequest.headers || {}; // 刷新成功:释放队列
originalRequest.headers.Authorization = `Bearer ${newToken}`; requestsQueue.forEach(callback => callback(newToken));
requestsQueue = [];
isRefreshing = false;
// 重试本次请求
originalRequest.headers!.Authorization = `Bearer ${newToken}`;
return this.instance(originalRequest);
} catch (error) {
// 刷新失败:清理并彻底报错
isRefreshing = false;
requestsQueue = [];
this.clearTokens();
return Promise.reject(error);
}
} }
return instance(originalRequest); // 其它业务错误
} catch (refreshErr) { ElNotification.error({ title: '提示', message: res.msg || '服务异常' });
// 刷新失败,清理 token 并跳转登录 return Promise.reject(res);
clearTokens();
// 可以在这里触发跳转到登录页
// router.push('/login');
console.error('Token refresh failed:', refreshErr);
return Promise.reject(refreshErr);
}
}
return Promise.reject(responseData);
default:
// 其他业务错误
ElNotification({
title:'提示',
message: responseData.msg || '请求失败',
type: 'error'
})
return Promise.reject(responseData);
}
}, },
(error: any) => { (error) => {
// 网络错误或其他错误 // 网络层错误处理
console.error('Response error:', error); if (error.response?.status === 401) {
this.clearTokens();
// 判断错误类型
if (error.response) {
// 服务器返回错误状态码
const { status, data } = error.response;
if (status === 401) {
// 未授权,清理 token
const { clearTokens } = useTokenRefresh(this.baseUrl);
clearTokens();
} else if (status >= 500) {
// 服务器错误
return Promise.reject({
code: status,
msg: '服务器内部错误,请稍后重试',
} as ApiError);
} }
} else if (error.request) { return Promise.reject(error);
// 网络错误
return Promise.reject({
code: -1,
msg: '网络连接失败,请检查网络',
} as ApiError);
}
return Promise.reject({
code: -1,
msg: error.message || '请求失败',
} as ApiError);
} }
); );
} }
// 获取实例的缓存键
private getInstanceKey(baseURL: string, timeout: number): string {
return `${baseURL || this.baseUrl}__${timeout}`;
}
// 获取 axios 实例
public getInstance(config: AxiosRequestConfig = {}): AxiosInstance {
const baseURL = config.baseURL || this.baseUrl || baseUrl;
const timeout = config.timeout || 50 * 1000;
const key = this.getInstanceKey(baseURL, timeout);
if (this.instanceMap.has(key)) {
return this.instanceMap.get(key)!;
}
const instanceConfig = {
...this.getBaseConfig(),
...config,
};
const instance = this.createAxiosInstance(instanceConfig);
this.setupInterceptors(instance, baseURL);
this.instanceMap.set(key, instance);
return instance;
}
// 通用请求方法
public request<T = any>(config: AxiosRequestConfig): Promise<ApiResponse<T>> { public request<T = any>(config: AxiosRequestConfig): Promise<ApiResponse<T>> {
// 检查是否应该使用 Mock 数据 // 检查是否应该使用 Mock 数据
const requestUrl = config.url || ''; const requestUrl = config.url || '';
@@ -211,11 +139,7 @@ class HttpRequest {
} }
} }
const instance = this.getInstance(config); return this.instance(config) as unknown as Promise<ApiResponse<T>>;
return instance(config).catch((error) => {
// 统一错误处理
throw error;
}) as Promise<ApiResponse<T>>;
} }
// GET 请求 // GET 请求
@@ -258,7 +182,6 @@ class HttpRequest {
} }
} }
// 创建实例
const httpRequest = new HttpRequest(baseUrl); const httpRequest = new HttpRequest(baseUrl);
// 导出方法 // 导出方法
@@ -272,5 +195,4 @@ export const request = {
delete: <T = any>(url: string, config?: AxiosRequestConfig) => delete: <T = any>(url: string, config?: AxiosRequestConfig) =>
httpRequest.delete<T>(url, config), httpRequest.delete<T>(url, config),
}; };
export default httpRequest;
export default request;