init
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
"nprogress": "^0.2.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"pinia": "^2.2.4",
|
||||
"pinia-plugin-persistedstate": "^3.2.3",
|
||||
"vue": "^3.5.12",
|
||||
"vue-router": "^4.4.5"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import request from '@/utils/request'
|
||||
import type { LoginForm, LoginResult, UserInfo } from '@/types/auth'
|
||||
import type { RouteVO } from '@/types/router'
|
||||
|
||||
const BASE_URL = '/auth'
|
||||
|
||||
export const AuthAPI = {
|
||||
/** 登录 */
|
||||
login(data: LoginForm) {
|
||||
return request<LoginResult>({ url: `${BASE_URL}/login`, method: 'post', data })
|
||||
},
|
||||
|
||||
/** 登出 */
|
||||
logout() {
|
||||
return request<boolean>({ url: `${BASE_URL}/logout`, method: 'post' })
|
||||
},
|
||||
|
||||
/** 获取当前登录用户信息(含角色与权限) */
|
||||
getMe() {
|
||||
return request<UserInfo>({ url: `${BASE_URL}/me`, method: 'get' })
|
||||
},
|
||||
|
||||
/** 获取当前用户可访问的菜单路由 */
|
||||
getRoutes() {
|
||||
return request<RouteVO[]>({ url: `${BASE_URL}/routes`, method: 'get' })
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import request from '@/utils/request'
|
||||
import type { PageResult } from '@/types/common'
|
||||
import type { UserForm, UserQuery, UserVO } from '@/types/system/user'
|
||||
|
||||
const BASE_URL = '/users'
|
||||
|
||||
export const UserAPI = {
|
||||
/** 分页查询用户列表 */
|
||||
query(params: UserQuery) {
|
||||
return request<PageResult<UserVO>>({ url: `${BASE_URL}/page`, method: 'get', params })
|
||||
},
|
||||
|
||||
/** 用户详情 */
|
||||
detail(id: number) {
|
||||
return request<UserVO>({ url: `${BASE_URL}/${id}`, method: 'get' })
|
||||
},
|
||||
|
||||
/** 新增用户 */
|
||||
create(data: UserForm) {
|
||||
return request<number>({ url: BASE_URL, method: 'post', data })
|
||||
},
|
||||
|
||||
/** 修改用户 */
|
||||
update(id: number, data: UserForm) {
|
||||
return request<boolean>({ url: `${BASE_URL}/${id}`, method: 'put', data })
|
||||
},
|
||||
|
||||
/** 删除用户,ids 为逗号分隔 */
|
||||
delete(ids: string) {
|
||||
return request<boolean>({ url: `${BASE_URL}/${ids}`, method: 'delete' })
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { App } from 'vue'
|
||||
import { hasPerm, hasRole } from './permission'
|
||||
|
||||
/** 全局指令注册入口 */
|
||||
export function setupDirectives(app: App): void {
|
||||
app.directive('hasPerm', hasPerm)
|
||||
app.directive('hasRole', hasRole)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { Directive, DirectiveBinding } from 'vue'
|
||||
import { useUserStore } from '@/store'
|
||||
|
||||
type AuthValue = string | string[]
|
||||
|
||||
function normalize(binding: DirectiveBinding<AuthValue>): string[] {
|
||||
const value = binding.value
|
||||
if (Array.isArray(value)) return value
|
||||
return value ? [value] : []
|
||||
}
|
||||
|
||||
/** 校验不通过时直接移除 DOM 节点,而非仅禁用 */
|
||||
function check(el: HTMLElement, binding: DirectiveBinding<AuthValue>, checker: (code: string) => boolean) {
|
||||
const codes = normalize(binding)
|
||||
if (!codes.length) return
|
||||
|
||||
const passed = codes.some(checker)
|
||||
if (!passed) {
|
||||
el.remove()
|
||||
}
|
||||
}
|
||||
|
||||
/** 按钮级权限:v-hasPerm="'sys:user:add'" 或 v-hasPerm="['sys:user:add']" */
|
||||
export const hasPerm: Directive<HTMLElement, AuthValue> = {
|
||||
mounted(el, binding) {
|
||||
check(el, binding, (code) => useUserStore().hasPermission(code))
|
||||
},
|
||||
}
|
||||
|
||||
/** 角色级权限:v-hasRole="'ADMIN'" */
|
||||
export const hasRole: Directive<HTMLElement, AuthValue> = {
|
||||
mounted(el, binding) {
|
||||
check(el, binding, (code) => useUserStore().hasRole(code))
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { Option } from '@/types/common'
|
||||
import type { UserStatus } from '@/types/system/user'
|
||||
|
||||
/** 用户状态枚举 */
|
||||
export const UserStatusEnum = {
|
||||
ENABLED: 1,
|
||||
DISABLED: 0,
|
||||
} as const
|
||||
|
||||
/** 用户状态下拉选项 */
|
||||
export const USER_STATUS_OPTIONS: Option<UserStatus>[] = [
|
||||
{ label: '启用', value: UserStatusEnum.ENABLED },
|
||||
{ label: '禁用', value: UserStatusEnum.DISABLED },
|
||||
]
|
||||
@@ -0,0 +1,88 @@
|
||||
import NProgress from 'nprogress'
|
||||
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
|
||||
import { usePermissionStore, useUserStore } from '@/store'
|
||||
|
||||
const DEFAULT_TITLE = import.meta.env.VITE_APP_TITLE || 'Portal 管理后台'
|
||||
|
||||
/** 静态路由:登录页与首页骨架,不依赖后端返回 */
|
||||
export const constantRoutes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('@/views/login/index.vue'),
|
||||
meta: { hidden: true },
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('@/layouts/index.vue'),
|
||||
redirect: '/dashboard',
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('@/views/dashboard/index.vue'),
|
||||
meta: { title: '仪表盘', icon: 'Odometer' },
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
/** 兜底 404,必须在动态路由之后注册 */
|
||||
export const notFoundRoute: RouteRecordRaw = {
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'NotFound',
|
||||
component: () => import('@/views/error/404.vue'),
|
||||
meta: { hidden: true },
|
||||
}
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.VITE_APP_BASE_URL),
|
||||
routes: constantRoutes,
|
||||
scrollBehavior: () => ({ top: 0 }),
|
||||
})
|
||||
|
||||
router.beforeEach(async (to, _from, next) => {
|
||||
NProgress.start()
|
||||
|
||||
const title = (to.meta.title as string | undefined) ?? ''
|
||||
document.title = title ? `${title} - ${DEFAULT_TITLE}` : DEFAULT_TITLE
|
||||
|
||||
const userStore = useUserStore()
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
// 已登录访问登录页 → 回首页
|
||||
if (to.path === '/login') {
|
||||
next(userStore.token ? { path: '/' } : undefined)
|
||||
return
|
||||
}
|
||||
|
||||
// 未登录 → 跳转登录并携带回跳地址
|
||||
if (!userStore.token) {
|
||||
next({ path: '/login', query: { redirect: to.fullPath } })
|
||||
return
|
||||
}
|
||||
|
||||
// 首次进入:拉取用户信息与动态路由
|
||||
if (!permissionStore.isLoaded) {
|
||||
try {
|
||||
await userStore.getUserInfo()
|
||||
const dynamicRoutes = await permissionStore.generateRoutes()
|
||||
dynamicRoutes.forEach((route) => router.addRoute(route))
|
||||
router.addRoute(notFoundRoute)
|
||||
next({ ...to, replace: true })
|
||||
} catch {
|
||||
userStore.resetToken()
|
||||
permissionStore.resetRoutes()
|
||||
next({ path: '/login', query: { redirect: to.fullPath } })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
router.afterEach(() => {
|
||||
NProgress.done()
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,12 @@
|
||||
import { createPinia } from 'pinia'
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||
|
||||
const pinia = createPinia()
|
||||
pinia.use(piniaPluginPersistedstate)
|
||||
|
||||
export default pinia
|
||||
|
||||
export * from './modules/app'
|
||||
export * from './modules/permission'
|
||||
export * from './modules/settings'
|
||||
export * from './modules/user'
|
||||
@@ -0,0 +1,24 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export type DeviceType = 'desktop' | 'mobile'
|
||||
|
||||
export const useAppStore = defineStore(
|
||||
'app',
|
||||
() => {
|
||||
const sidebarCollapsed = ref(false)
|
||||
const device = ref<DeviceType>('desktop')
|
||||
|
||||
function toggleSidebar(collapsed?: boolean) {
|
||||
sidebarCollapsed.value = collapsed ?? !sidebarCollapsed.value
|
||||
}
|
||||
|
||||
function setDevice(value: DeviceType) {
|
||||
device.value = value
|
||||
}
|
||||
|
||||
return { sidebarCollapsed, device, toggleSidebar, setDevice }
|
||||
},
|
||||
{
|
||||
persist: { paths: ['sidebarCollapsed'] },
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,81 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { AuthAPI } from '@/api/auth'
|
||||
import type { RouteVO } from '@/types/router'
|
||||
|
||||
/** views 下所有页面组件,用于后端路由动态映射 */
|
||||
const viewModules = import.meta.glob('../../views/**/*.vue')
|
||||
const Layout = () => import('@/layouts/index.vue')
|
||||
|
||||
function resolveView(component?: string) {
|
||||
if (!component) return undefined
|
||||
if (component === 'Layout') return Layout
|
||||
|
||||
const loader = viewModules[`../../views/${component}.vue`]
|
||||
if (!loader) {
|
||||
console.warn(`[router] 未找到组件文件:views/${component}.vue`)
|
||||
return undefined
|
||||
}
|
||||
return loader
|
||||
}
|
||||
|
||||
function joinPath(parentPath: string, path: string): string {
|
||||
if (path.startsWith('/')) return path
|
||||
return `${parentPath}/${path}`.replace(/\/{2,}/g, '/')
|
||||
}
|
||||
|
||||
/** 将后端返回的路由树转换为 vue-router 可识别的路由记录 */
|
||||
function transformRoutes(routes: RouteVO[], parentPath = ''): RouteRecordRaw[] {
|
||||
const result: RouteRecordRaw[] = []
|
||||
|
||||
routes.forEach((route) => {
|
||||
if (route.meta?.hidden) return
|
||||
|
||||
const fullPath = joinPath(parentPath, route.path)
|
||||
const children = route.children?.length ? transformRoutes(route.children, fullPath) : []
|
||||
|
||||
// 布局容器节点
|
||||
if (route.component === 'Layout') {
|
||||
result.push({
|
||||
path: fullPath,
|
||||
component: Layout,
|
||||
redirect: route.redirect,
|
||||
meta: route.meta ?? {},
|
||||
children,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const component = resolveView(route.component)
|
||||
if (!component) return
|
||||
|
||||
result.push({
|
||||
path: fullPath,
|
||||
name: route.name,
|
||||
component,
|
||||
meta: route.meta ?? {},
|
||||
})
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export const usePermissionStore = defineStore('permission', () => {
|
||||
/** 后端返回的动态路由 */
|
||||
const routes = ref<RouteRecordRaw[]>([])
|
||||
const isLoaded = ref(false)
|
||||
|
||||
async function generateRoutes(): Promise<RouteRecordRaw[]> {
|
||||
const data = await AuthAPI.getRoutes()
|
||||
routes.value = transformRoutes(data)
|
||||
isLoaded.value = true
|
||||
return routes.value
|
||||
}
|
||||
|
||||
function resetRoutes() {
|
||||
routes.value = []
|
||||
isLoaded.value = false
|
||||
}
|
||||
|
||||
return { routes, isLoaded, generateRoutes, resetRoutes }
|
||||
})
|
||||
@@ -0,0 +1,26 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const DEFAULT_THEME_COLOR = '#409eff'
|
||||
|
||||
export const useSettingsStore = defineStore(
|
||||
'settings',
|
||||
() => {
|
||||
const themeColor = ref(DEFAULT_THEME_COLOR)
|
||||
const showLogo = ref(true)
|
||||
|
||||
/** 通过 CSS 变量覆盖 Element Plus 主色,避免使用 !important 打补丁 */
|
||||
function setThemeColor(color: string) {
|
||||
themeColor.value = color
|
||||
document.documentElement.style.setProperty('--el-color-primary', color)
|
||||
}
|
||||
|
||||
function toggleLogo(visible?: boolean) {
|
||||
showLogo.value = visible ?? !showLogo.value
|
||||
}
|
||||
|
||||
return { themeColor, showLogo, setThemeColor, toggleLogo }
|
||||
},
|
||||
{
|
||||
persist: { paths: ['themeColor', 'showLogo'] },
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,70 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { AuthAPI } from '@/api/auth'
|
||||
import type { LoginForm, UserInfo } from '@/types/auth'
|
||||
import { getToken, removeToken, setToken } from '@/utils/storage'
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
/** Token 初始从本地恢复,避免刷新后丢失登录态 */
|
||||
const token = ref(getToken())
|
||||
const userInfo = ref<UserInfo | null>(null)
|
||||
const roles = ref<string[]>([])
|
||||
const permissions = ref<string[]>([])
|
||||
|
||||
const nickname = computed(() => userInfo.value?.nickname ?? '')
|
||||
|
||||
/** 登录 */
|
||||
async function login(data: LoginForm) {
|
||||
const result = await AuthAPI.login(data)
|
||||
token.value = result.accessToken
|
||||
setToken(result.accessToken)
|
||||
}
|
||||
|
||||
/** 拉取用户信息(角色 + 权限码) */
|
||||
async function getUserInfo() {
|
||||
const data = await AuthAPI.getMe()
|
||||
userInfo.value = data
|
||||
roles.value = data.roles ?? []
|
||||
permissions.value = data.permissions ?? []
|
||||
return data
|
||||
}
|
||||
|
||||
/** 登出 */
|
||||
async function logout() {
|
||||
try {
|
||||
await AuthAPI.logout()
|
||||
} finally {
|
||||
resetToken()
|
||||
}
|
||||
}
|
||||
|
||||
/** 清空登录态 */
|
||||
function resetToken() {
|
||||
token.value = ''
|
||||
userInfo.value = null
|
||||
roles.value = []
|
||||
permissions.value = []
|
||||
removeToken()
|
||||
}
|
||||
|
||||
function hasPermission(code: string): boolean {
|
||||
return permissions.value.includes(code)
|
||||
}
|
||||
|
||||
function hasRole(role: string): boolean {
|
||||
return roles.value.includes(role)
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
userInfo,
|
||||
roles,
|
||||
permissions,
|
||||
nickname,
|
||||
login,
|
||||
getUserInfo,
|
||||
logout,
|
||||
resetToken,
|
||||
hasPermission,
|
||||
hasRole,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,44 @@
|
||||
// 该文件由 Vite scss additionalData 自动注入,请只放置变量与 mixin,不要输出任何样式
|
||||
|
||||
// 布局尺寸
|
||||
$sidebar-width: 220px;
|
||||
$sidebar-width-collapsed: 64px;
|
||||
$navbar-height: 50px;
|
||||
$container-padding: 16px;
|
||||
|
||||
// 主题色(Element Plus 主色通过 CSS 变量覆盖,见 styles/element/index.scss)
|
||||
$color-primary: #409eff;
|
||||
$color-success: #67c23a;
|
||||
$color-warning: #e6a23c;
|
||||
$color-danger: #f56c6c;
|
||||
$color-info: #909399;
|
||||
|
||||
// 文字
|
||||
$text-color-primary: #303133;
|
||||
$text-color-regular: #606266;
|
||||
$text-color-secondary: #909399;
|
||||
|
||||
// 背景与边框
|
||||
$bg-page: #f5f7fa;
|
||||
$bg-sidebar: #ffffff;
|
||||
$border-color: #e4e7ed;
|
||||
$border-color-light: #ebeef5;
|
||||
|
||||
// 层级
|
||||
$z-index-sidebar: 1001;
|
||||
$z-index-navbar: 1002;
|
||||
|
||||
// 过渡
|
||||
$transition-duration: 0.28s;
|
||||
|
||||
@mixin flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@mixin text-ellipsis {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { RouteVO } from './router'
|
||||
|
||||
/** 登录表单 */
|
||||
export interface LoginForm {
|
||||
username: string
|
||||
password: string
|
||||
captchaKey?: string
|
||||
captchaCode?: string
|
||||
}
|
||||
|
||||
/** 登录响应 */
|
||||
export interface LoginResult {
|
||||
accessToken: string
|
||||
refreshToken?: string
|
||||
tokenType: string
|
||||
expiresIn?: number
|
||||
}
|
||||
|
||||
/** 当前登录用户 */
|
||||
export interface UserInfo {
|
||||
userId: number
|
||||
username: string
|
||||
nickname: string
|
||||
avatar: string
|
||||
roles: string[]
|
||||
permissions: string[]
|
||||
}
|
||||
|
||||
/** 登录接口返回的完整信息(含菜单) */
|
||||
export type UserSession = UserInfo & { routes: RouteVO[] }
|
||||
@@ -0,0 +1,25 @@
|
||||
/** 后端统一响应结构 */
|
||||
export interface ApiResponse<T = unknown> {
|
||||
/** 业务状态码,00000 表示成功 */
|
||||
code: string
|
||||
data: T
|
||||
msg: string
|
||||
}
|
||||
|
||||
/** 分页查询基础参数 */
|
||||
export interface PageQuery {
|
||||
pageNum: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
/** 分页响应结构 */
|
||||
export interface PageResult<T> {
|
||||
list: T[]
|
||||
total: number
|
||||
}
|
||||
|
||||
/** 下拉选项 */
|
||||
export interface Option<T = number> {
|
||||
label: string
|
||||
value: T
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/** 路由元信息(与后端菜单字段保持一致) */
|
||||
export interface RouteMetaVO {
|
||||
title?: string
|
||||
icon?: string
|
||||
hidden?: boolean
|
||||
/** 是否缓存该页面 */
|
||||
keepAlive?: boolean
|
||||
/** 只有一个子路由时是否始终展示父级菜单 */
|
||||
alwaysShow?: boolean
|
||||
roles?: string[]
|
||||
}
|
||||
|
||||
/** 后端返回的路由/菜单结构,component 为 views 下的相对路径 */
|
||||
export interface RouteVO {
|
||||
path: string
|
||||
name?: string
|
||||
component?: string
|
||||
redirect?: string
|
||||
meta?: RouteMetaVO
|
||||
children?: RouteVO[]
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { PageQuery } from '@/types/common'
|
||||
|
||||
/** 用户状态:1 启用 / 0 禁用 */
|
||||
export type UserStatus = 0 | 1
|
||||
|
||||
/** 用户列表查询参数 */
|
||||
export interface UserQuery extends PageQuery {
|
||||
username?: string
|
||||
status?: UserStatus
|
||||
deptId?: number
|
||||
}
|
||||
|
||||
/** 用户列表实体(后端返回) */
|
||||
export interface UserVO {
|
||||
id: number
|
||||
username: string
|
||||
nickname: string
|
||||
mobile: string
|
||||
email: string
|
||||
status: UserStatus
|
||||
deptName: string
|
||||
roleNames: string[]
|
||||
createTime: string
|
||||
}
|
||||
|
||||
/** 新增/编辑表单 */
|
||||
export interface UserForm {
|
||||
id?: number
|
||||
username: string
|
||||
nickname: string
|
||||
mobile: string
|
||||
email: string
|
||||
status: UserStatus
|
||||
roleNames: string[]
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import axios, {
|
||||
AxiosError,
|
||||
type AxiosInstance,
|
||||
type AxiosRequestConfig,
|
||||
type AxiosResponse,
|
||||
} from 'axios'
|
||||
import router from '@/router'
|
||||
import type { ApiResponse } from '@/types/common'
|
||||
import { getToken, removeToken } from '@/utils/storage'
|
||||
|
||||
/** 业务成功状态码 */
|
||||
export const SUCCESS_CODE = '00000'
|
||||
/** Token 失效 / 未授权 */
|
||||
export const TOKEN_INVALID_CODE = 'A0230'
|
||||
|
||||
const service: AxiosInstance = axios.create({
|
||||
baseURL: import.meta.env.VITE_APP_BASE_API,
|
||||
timeout: 50000,
|
||||
headers: { 'Content-Type': 'application/json;charset=utf-8' },
|
||||
})
|
||||
|
||||
/** 防止并发请求重复触发登录过期弹窗 */
|
||||
let unauthorizedHandling = false
|
||||
|
||||
function handleUnauthorized(msg: string): void {
|
||||
removeToken()
|
||||
if (unauthorizedHandling) return
|
||||
unauthorizedHandling = true
|
||||
|
||||
ElMessageBox.alert(msg || '登录状态已过期,请重新登录', '提示', { type: 'warning' })
|
||||
.catch(() => undefined)
|
||||
.finally(() => {
|
||||
unauthorizedHandling = false
|
||||
const redirect = encodeURIComponent(router.currentRoute.value.fullPath)
|
||||
void router.replace(`/login?redirect=${redirect}`)
|
||||
})
|
||||
}
|
||||
|
||||
// 请求拦截器
|
||||
service.interceptors.request.use((config) => {
|
||||
const token = getToken()
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
// 响应拦截器
|
||||
service.interceptors.response.use(
|
||||
(response: AxiosResponse<ApiResponse>): unknown => {
|
||||
// 文件流直接透传,不做业务码判断
|
||||
if (response.config.responseType === 'blob') {
|
||||
return response.data
|
||||
}
|
||||
|
||||
const { code, data, msg } = response.data
|
||||
|
||||
if (code === SUCCESS_CODE) {
|
||||
return data
|
||||
}
|
||||
|
||||
if (code === TOKEN_INVALID_CODE) {
|
||||
handleUnauthorized(msg)
|
||||
return Promise.reject(new Error(msg))
|
||||
}
|
||||
|
||||
ElMessage.error(msg || '请求失败,请稍后重试')
|
||||
return Promise.reject(new Error(msg || '请求失败'))
|
||||
},
|
||||
(error: AxiosError<ApiResponse>) => {
|
||||
const status = error.response?.status
|
||||
const msg = error.response?.data?.msg || error.message
|
||||
|
||||
if (status === 401) {
|
||||
handleUnauthorized(msg)
|
||||
} else if (status === 403) {
|
||||
ElMessage.error('没有权限访问该资源')
|
||||
} else {
|
||||
ElMessage.error(msg || '网络异常,请稍后重试')
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
},
|
||||
)
|
||||
|
||||
/**
|
||||
* 统一请求入口
|
||||
* @returns 直接返回业务数据(已在响应拦截器中剥离外层结构)
|
||||
*/
|
||||
export default function request<T>(config: AxiosRequestConfig): Promise<T> {
|
||||
return service.request<unknown, T>(config)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
const TOKEN_KEY = 'portal_token'
|
||||
|
||||
/** 读取 Token(SSR / 隐私模式下 localStorage 可能不可用,需容错) */
|
||||
export function getToken(): string {
|
||||
return localStorage.getItem(TOKEN_KEY) ?? ''
|
||||
}
|
||||
|
||||
export function setToken(token: string): void {
|
||||
localStorage.setItem(TOKEN_KEY, token)
|
||||
}
|
||||
|
||||
export function removeToken(): void {
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
}
|
||||
Reference in New Issue
Block a user