feat(service): 完善服务层和状态管理

- 更新API服务配置(api/login.ts)
- 优化HTTP服务配置(http/alova.ts, config.ts, handle.ts)
- 完善认证状态管理(store/auth.ts)
- 优化路由状态管理(store/router/)

加强服务层架构和状态管理机制
This commit is contained in:
Leo 2025-07-06 00:59:18 +08:00
parent c239a15840
commit 567e68234b
7 changed files with 253 additions and 54 deletions

View File

@ -1,25 +1,84 @@
import { request } from '../http' import { request } from '../http'
interface Ilogin { interface LoginRequest {
userName: string loginName: string
password: string password: string
codeKey: string
securityCode: string
rememberMe?: boolean
} }
export function fetchLogin(data: Ilogin) { interface RegisterRequest {
const methodInstance = request.Post<Service.ResponseResult<Api.Login.Info>>('/login', data) loginName: string
password: string
userName: string
codeKey: string
securityCode: string
}
interface LoginResponse {
tokenName: string
tokenValue: string
}
interface CaptchaResponse {
codeKey: string
captchaPicture: string
captchaText?: string
}
export function fetchLogin(data: LoginRequest) {
const methodInstance = request.Post<Service.ResponseResult<LoginResponse>>('/auth/login', data)
methodInstance.meta = { methodInstance.meta = {
authRole: null, authRole: null,
} }
return methodInstance return methodInstance
} }
export function fetchLogout() {
return request.Get<Service.ResponseResult<string>>('/auth/logout')
}
export function fetchRegister(data: RegisterRequest) {
const methodInstance = request.Post<Service.ResponseResult<string>>('/auth/register', data)
methodInstance.meta = {
authRole: null,
}
return methodInstance
}
export function fetchUpdateToken(data: any) { export function fetchUpdateToken(data: any) {
const method = request.Post<Service.ResponseResult<Api.Login.Info>>('/updateToken', data) const method = request.Post<Service.ResponseResult<LoginResponse>>('/updateToken', data)
method.meta = { method.meta = {
authRole: 'refreshToken', authRole: 'refreshToken',
} }
return method return method
} }
export function fetchUserRoutes(params: { id: number }) { export function fetchUserRoutesOld(params: { id: number }) {
return request.Get<Service.ResponseResult<AppRoute.RowRoute[]>>('/getUserRoutes', { params }) return request.Get<Service.ResponseResult<AppRoute.RowRoute[]>>('/getUserRoutes', { params })
} }
export function fetchLoginUserInfo() {
return request.Get<Service.ResponseResult<Api.Login.Info>>('/coder/sysLoginUser/getLoginUserInformation')
}
// 验证码相关接口
// 获取PNG格式验证码
export function fetchCaptchaPng() {
const methodInstance = request.Get<Service.ResponseResult<CaptchaResponse>>('/captcha/png')
methodInstance.meta = {
authRole: null,
}
return methodInstance
}
// 获取GIF格式验证码
export function fetchCaptchaGif() {
const methodInstance = request.Get<Service.ResponseResult<CaptchaResponse>>('/captcha/gif')
methodInstance.meta = {
authRole: null,
}
return methodInstance
}

View File

@ -1,4 +1,5 @@
import { local } from '@/utils' import { local } from '@/utils'
import { coiMsgWarning } from '@/utils/coi'
import { createAlova } from 'alova' import { createAlova } from 'alova'
import { createServerTokenAuthentication } from 'alova/client' import { createServerTokenAuthentication } from 'alova/client'
import adapterFetch from 'alova/fetch' import adapterFetch from 'alova/fetch'
@ -89,7 +90,7 @@ export function createAlovaInstance(
}, },
onError: (error, method) => { onError: (error, method) => {
const tip = `[${method.type}] - [${method.url}] - ${error.message}` const tip = `[${method.type}] - [${method.url}] - ${error.message}`
window.$message?.warning(tip) coiMsgWarning(tip)
}, },
onComplete: async (_method) => { onComplete: async (_method) => {

View File

@ -10,8 +10,8 @@ export const DEFAULT_ALOVA_OPTIONS = {
export const DEFAULT_BACKEND_OPTIONS = { export const DEFAULT_BACKEND_OPTIONS = {
codeKey: 'code', codeKey: 'code',
dataKey: 'data', dataKey: 'data',
msgKey: 'message', msgKey: 'msg',
successCode: 200, successCode: 1,
} }
/** 请求不成功各种状态的错误 */ /** 请求不成功各种状态的错误 */

View File

@ -1,6 +1,7 @@
import { fetchUpdateToken } from '@/service' import { fetchUpdateToken } from '@/service'
import { useAuthStore } from '@/store' import { useAuthStore } from '@/store'
import { local } from '@/utils' import { local } from '@/utils'
import { coiMsgError } from '@/utils/coi'
import { import {
ERROR_NO_TIP_STATUS, ERROR_NO_TIP_STATUS,
ERROR_STATUS, ERROR_STATUS,
@ -94,5 +95,5 @@ export function showError(error: Service.RequestError) {
if (ERROR_NO_TIP_STATUS.includes(code)) if (ERROR_NO_TIP_STATUS.includes(code))
return return
window.$message.error(error.message) coiMsgError(error.message)
} }

View File

@ -1,5 +1,5 @@
import { router } from '@/router' import { router } from '@/router'
import { fetchLogin } from '@/service' import { fetchLogin, fetchLoginUserInfo } from '@/service'
import { local } from '@/utils' import { local } from '@/utils'
import { useRouteStore } from './router' import { useRouteStore } from './router'
import { useTabStore } from './tab' import { useTabStore } from './tab'
@ -52,14 +52,28 @@ export const useAuthStore = defineStore('auth-store', {
}, },
/* 用户登录 */ /* 用户登录 */
async login(userName: string, password: string) { async login(loginName: string, password: string, codeKey: string, securityCode: string, rememberMe = false) {
try { try {
const { isSuccess, data } = await fetchLogin({ userName, password }) const { isSuccess, data } = await fetchLogin({ loginName, password, codeKey, securityCode, rememberMe })
if (!isSuccess) if (!isSuccess)
return return
// 保存Token
local.set('accessToken', data.tokenValue)
this.token = data.tokenValue
// 获取用户信息
const userInfoResult = await fetchLoginUserInfo()
if (!userInfoResult.isSuccess)
return
// 处理登录信息 // 处理登录信息
await this.handleLoginInfo(data) const userInfo = {
...userInfoResult.data,
accessToken: data.tokenValue,
refreshToken: data.tokenValue, // 如果后端没有单独的refreshToken暂时使用相同值
}
await this.handleLoginInfo(userInfo as Api.Login.Info)
} }
catch (e) { catch (e) {
console.warn('[Login Error]:', e) console.warn('[Login Error]:', e)

View File

@ -1,14 +1,36 @@
import type { MenuOption } from 'naive-ui' import type { MenuOption } from 'naive-ui'
import type { RouteRecordRaw } from 'vue-router' import type { RouteRecordRaw } from 'vue-router'
import { h } from 'vue'
import { usePermission } from '@/hooks' import { usePermission } from '@/hooks'
import Layout from '@/layouts/index.vue' import Layout from '@/layouts/index.vue'
import { $t, arrayToTree, renderIcon } from '@/utils' import { arrayToTree, renderIcon } from '@/utils'
import { clone, min, omit, pick } from 'radash' import { clone, min, omit, pick } from 'radash'
import { RouterLink } from 'vue-router' import { RouterLink } from 'vue-router'
const metaFields: AppRoute.MetaKeys[] const metaFields: AppRoute.MetaKeys[]
= ['title', 'icon', 'requiresAuth', 'roles', 'keepAlive', 'hide', 'order', 'href', 'activeMenu', 'withoutTab', 'pinTab', 'menuType'] = ['title', 'icon', 'requiresAuth', 'roles', 'keepAlive', 'hide', 'order', 'href', 'activeMenu', 'withoutTab', 'pinTab', 'menuType']
// 将后端菜单数据转换为前端路由数据
function transformBackendToRoute(backendRoute: AppRoute.BackendRoute): AppRoute.RowRoute {
return {
id: backendRoute.menuId,
pid: backendRoute.parentId === 0 ? null : backendRoute.parentId,
name: backendRoute.name,
path: backendRoute.path,
componentPath: backendRoute.component || null,
redirect: backendRoute.redirect || undefined,
title: backendRoute.menuName,
icon: backendRoute.icon,
requiresAuth: true, // 动态路由都需要认证
hide: backendRoute.isHide === '0', // 0-隐藏 1-显示
keepAlive: backendRoute.isKeepAlive === '0', // 0-是 1-否
pinTab: backendRoute.isAffix === '0', // 0-是 1-否
activeMenu: backendRoute.activeMenu || undefined,
menuType: backendRoute.menuType as AppRoute.MenuType,
href: backendRoute.isLink === '0' ? backendRoute.path : undefined, // 如果是外链
}
}
function standardizedRoutes(route: AppRoute.RowRoute[]) { function standardizedRoutes(route: AppRoute.RowRoute[]) {
return clone(route).map((i) => { return clone(route).map((i) => {
const route = omit(i, metaFields) const route = omit(i, metaFields)
@ -18,9 +40,20 @@ function standardizedRoutes(route: AppRoute.RowRoute[]) {
}) as AppRoute.Route[] }) as AppRoute.Route[]
} }
export function createRoutes(routes: AppRoute.RowRoute[]) { // 处理路由数据的主函数 - 支持动态和静态路由以及混合模式
export function createRoutes(routeData: (AppRoute.BackendRoute | AppRoute.RowRoute)[]) {
const { hasPermission } = usePermission() const { hasPermission } = usePermission()
// 处理混合数据:分别处理后端数据和前端数据
const backendRoutes = routeData.filter(item => 'menuId' in item) as AppRoute.BackendRoute[]
const frontendRoutes = routeData.filter(item => !('menuId' in item)) as AppRoute.RowRoute[]
// 转换后端路由数据
const transformedBackendRoutes = backendRoutes.map(transformBackendToRoute)
// 合并所有路由
const routes = [...frontendRoutes, ...transformedBackendRoutes]
// Structure the meta field // Structure the meta field
let resultRouter = standardizedRoutes(routes) let resultRouter = standardizedRoutes(routes)
@ -30,8 +63,43 @@ export function createRoutes(routes: AppRoute.RowRoute[]) {
// Generate routes, no need to import files for those with redirect // Generate routes, no need to import files for those with redirect
const modules = import.meta.glob('@/views/**/*.vue') const modules = import.meta.glob('@/views/**/*.vue')
resultRouter = resultRouter.map((item: AppRoute.Route) => { resultRouter = resultRouter.map((item: AppRoute.Route) => {
if (item.componentPath && !item.redirect) if (item.componentPath && !item.redirect) {
item.component = modules[`/src/views${item.componentPath}`] // 对于动态路由,只有菜单类型才需要组件;对于静态路由,都需要组件
const needComponent = item.meta.menuType === '2' || !item.meta.menuType
if (needComponent) {
// 处理组件路径,确保正确的路径格式
let componentPath = item.componentPath
// 确保路径以 / 开头
if (!componentPath.startsWith('/')) {
componentPath = `/${componentPath}`
}
// 确保路径以 .vue 结尾
if (!componentPath.endsWith('.vue')) {
componentPath = `${componentPath}.vue`
}
const fullPath = `/src/views${componentPath}`
item.component = modules[fullPath]
// 如果组件未找到,输出调试信息并提供默认组件
if (!item.component) {
console.warn(`组件未找到: ${fullPath}`)
console.warn('可用组件路径:', Object.keys(modules).slice(0, 10)) // 只显示前10个避免日志过长
// 为找不到组件的页面提供一个默认的空页面组件
item.component = () => h('div', { class: 'p-4' }, [
h('div', { class: 'text-center text-gray-500' }, [
h('h3', '页面开发中'),
h('p', `组件路径: ${fullPath}`),
h('p', '请联系开发人员创建对应的页面组件'),
]),
])
}
}
else if (item.meta.menuType === '1') {
// 目录类型不需要组件但需要确保有children
item.component = undefined
}
}
return item return item
}) })
@ -41,7 +109,7 @@ export function createRoutes(routes: AppRoute.RowRoute[]) {
const appRootRoute: RouteRecordRaw = { const appRootRoute: RouteRecordRaw = {
path: '/appRoot', path: '/appRoot',
name: 'appRoot', name: 'appRoot',
redirect: import.meta.env.VITE_HOME_PATH, redirect: import.meta.env.VITE_HOME_PATH || '/dashboard/monitor',
component: Layout, component: Layout,
meta: { meta: {
title: '', title: '',
@ -91,11 +159,21 @@ function setRedirect(routes: AppRoute.Route[]) {
} }
/* 生成侧边菜单的数据 */ /* 生成侧边菜单的数据 */
export function createMenus(userRoutes: AppRoute.RowRoute[]) { export function createMenus(routeData: (AppRoute.BackendRoute | AppRoute.RowRoute)[]) {
// 处理混合数据:分别处理后端数据和前端数据
const backendRoutes = routeData.filter(item => 'menuId' in item) as AppRoute.BackendRoute[]
const frontendRoutes = routeData.filter(item => !('menuId' in item)) as AppRoute.RowRoute[]
// 转换后端路由数据
const transformedBackendRoutes = backendRoutes.map(transformBackendToRoute)
// 合并所有路由
const userRoutes = [...frontendRoutes, ...transformedBackendRoutes]
const resultMenus = standardizedRoutes(userRoutes) const resultMenus = standardizedRoutes(userRoutes)
// filter menus that do not need to be displayed // filter menus that do not need to be displayed
const visibleMenus = resultMenus.filter(route => !route.meta.hide) const visibleMenus = resultMenus.filter(route => !route.meta.hide && route.meta.menuType !== '3') // 过滤按钮类型
// generate side menu // generate side menu
return arrayToTree(transformAuthRoutesToMenus(visibleMenus)) return arrayToTree(transformAuthRoutesToMenus(visibleMenus))
@ -123,7 +201,7 @@ function transformAuthRoutesToMenus(userRoutes: AppRoute.Route[]) {
id: item.id, id: item.id,
pid: item.pid, pid: item.pid,
label: label:
(!item.meta.menuType || item.meta.menuType === 'page') (!item.meta.menuType || item.meta.menuType === '2')
? () => ? () =>
h( h(
RouterLink, RouterLink,
@ -132,9 +210,9 @@ function transformAuthRoutesToMenus(userRoutes: AppRoute.Route[]) {
path: item.path, path: item.path,
}, },
}, },
{ default: () => $t(`route.${String(item.name)}`, item.meta.title) }, { default: () => item.meta.title },
) )
: () => $t(`route.${String(item.name)}`, item.meta.title), : () => item.meta.title,
key: item.path, key: item.path,
icon: item.meta.icon ? renderIcon(item.meta.icon) : undefined, icon: item.meta.icon ? renderIcon(item.meta.icon) : undefined,
} }

View File

@ -2,14 +2,15 @@ import type { MenuOption } from 'naive-ui'
import { router } from '@/router' import { router } from '@/router'
import { staticRoutes } from '@/router/routes.static' import { staticRoutes } from '@/router/routes.static'
import { fetchUserRoutes } from '@/service' import { fetchUserRoutes } from '@/service'
import { useAuthStore } from '@/store/auth' import { $t } from '@/utils'
import { $t, local } from '@/utils' import { coiMsgError } from '@/utils/coi'
import { createMenus, createRoutes, generateCacheRoutes } from './helper' import { createMenus, createRoutes, generateCacheRoutes } from './helper'
interface RoutesStatus { interface RoutesStatus {
isInitAuthRoute: boolean isInitAuthRoute: boolean
menus: MenuOption[] menus: MenuOption[]
rowRoutes: AppRoute.RowRoute[] rowRoutes: AppRoute.RowRoute[]
backendRoutes: AppRoute.BackendRoute[]
activeMenu: string | null activeMenu: string | null
cacheRoutes: string[] cacheRoutes: string[]
} }
@ -20,6 +21,7 @@ export const useRouteStore = defineStore('route-store', {
activeMenu: null, activeMenu: null,
menus: [], menus: [],
rowRoutes: [], rowRoutes: [],
backendRoutes: [],
cacheRoutes: [], cacheRoutes: [],
} }
}, },
@ -38,50 +40,94 @@ export const useRouteStore = defineStore('route-store', {
}, },
async initRouteInfo() { async initRouteInfo() {
// 始终加载静态路由(仪表盘等基础路由)
const allRoutes = [...staticRoutes]
if (import.meta.env.VITE_ROUTE_LOAD_MODE === 'dynamic') { if (import.meta.env.VITE_ROUTE_LOAD_MODE === 'dynamic') {
const userInfo = local.get('userInfo') try {
// 获取动态路由并合并
const { isSuccess, data } = await fetchUserRoutes()
if (!userInfo || !userInfo.id) { if (isSuccess && data) {
const authStore = useAuthStore() // 将动态路由添加到静态路由中
authStore.logout() const dynamicRoutes = Array.isArray(data) ? data : []
return console.warn('成功获取动态路由:', dynamicRoutes.length, '个')
// 保持动态路由的原始ID关系不需要修改ID因为静态路由和动态路由可以共存
const processedDynamicRoutes = dynamicRoutes
return [...allRoutes, ...processedDynamicRoutes]
}
else {
console.warn('动态路由获取失败,只使用静态路由')
}
}
catch (error) {
console.error('动态路由获取异常,只使用静态路由:', error)
} }
// Get user's route
const { data } = await fetchUserRoutes({
id: userInfo.id,
})
if (!data)
return
return data
}
else {
this.rowRoutes = staticRoutes
return staticRoutes
} }
return allRoutes
}, },
async initAuthRoute() { async initAuthRoute() {
this.isInitAuthRoute = false this.isInitAuthRoute = false
// Initialize route information // Initialize route information
const rowRoutes = await this.initRouteInfo() const routeData = await this.initRouteInfo()
if (!rowRoutes) { if (!routeData) {
window.$message.error($t(`app.getRouteError`)) coiMsgError($t(`app.getRouteError`))
return return
} }
this.rowRoutes = rowRoutes
// 检查是否包含动态路由数据通过是否有menuId字段判断
const hasDynamicRoutes = routeData.some(item => 'menuId' in item)
if (hasDynamicRoutes) {
// 混合模式:分离静态路由和动态路由
const staticRouteData = routeData.filter(item => !('menuId' in item)) as AppRoute.RowRoute[]
const dynamicRouteData = routeData.filter(item => 'menuId' in item) as AppRoute.BackendRoute[]
// 保存动态路由原始数据
this.backendRoutes = dynamicRouteData
// 转换动态路由为前端格式
const transformedDynamicRoutes = dynamicRouteData.map(item => ({
id: item.menuId,
pid: item.parentId === 0 ? null : item.parentId,
name: item.name,
path: item.path,
componentPath: item.component || null,
redirect: item.redirect || undefined,
title: item.menuName,
icon: item.icon,
requiresAuth: true,
hide: item.isHide === '0',
keepAlive: item.isKeepAlive === '0',
pinTab: item.isAffix === '0',
activeMenu: item.activeMenu || undefined,
menuType: item.menuType as AppRoute.MenuType,
href: item.isLink === '0' ? item.path : undefined,
} as AppRoute.RowRoute))
// 合并静态路由和转换后的动态路由
this.rowRoutes = [...staticRouteData, ...transformedDynamicRoutes]
this.cacheRoutes = generateCacheRoutes(this.rowRoutes)
console.warn('混合路由模式 - 静态路由:', staticRouteData.length, '动态路由:', transformedDynamicRoutes.length)
}
else {
// 纯静态路由模式
this.rowRoutes = routeData as AppRoute.RowRoute[]
this.cacheRoutes = generateCacheRoutes(this.rowRoutes)
console.warn('静态路由模式 - 路由数量:', this.rowRoutes.length)
}
// Generate actual route and insert // Generate actual route and insert
const routes = createRoutes(rowRoutes) const routes = createRoutes(routeData)
router.addRoute(routes) router.addRoute(routes)
// Generate side menu // Generate side menu
this.menus = createMenus(rowRoutes) this.menus = createMenus(routeData)
// Generate the route cache
this.cacheRoutes = generateCacheRoutes(rowRoutes)
this.isInitAuthRoute = true this.isInitAuthRoute = true
}, },