feat: API层与认证流(client拦截/守卫/登录改密实装/域API骨架/vitest)
- 自建轻量 axios client(src/api/client.ts):Bearer 注入、401 清会话跳登录
(login 请求除外)、409 精确匹配"必须先修改初始密码"跳强制改密(区别于并发
冲突 409)、429/网络错误统一 ElMessage、ApiError 标准化、showErrorMessage
可opt-out——模板 utils/http 的 {code,msg,data} 信封与后端裸 HTTP 语义不符故弃用
- Pinia auth store(src/store/modules/auth.ts):token/role/tenant_slug/
must_change 落地并持久化(all-kefu-auth key),login/logout/changePassword/
会话过期/强制改密跳转(动态 import 断循环)
- 路由守卫:token 事实源判定、must_change 全站门(除改密页)、UserInfo 由登录
快照派生(后端无用户信息端点);user store 残留自愈
- 登录页实装(去除角色调试开关)、强制改密页实装;用户菜单/锁屏对接真实 logout
- 域 API 骨架:tenants/config/conversations/stats/settings(类型+端点常量)
- mask 哨兵助手:__SET__ ↔ 已设置 表单语义
- vitest 25 例(client 11/store 8/mask 6);登录页拖拽验证 reset 空引用修复
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,7 @@ import { nextTick } from 'vue'
|
||||
import NProgress from 'nprogress'
|
||||
import { useSettingStore } from '@/store/modules/setting'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { useAuthStore } from '@/store/modules/auth'
|
||||
import { useMenuStore } from '@/store/modules/menu'
|
||||
import { setWorktab } from '@/utils/navigation'
|
||||
import { setPageTitle } from '@/utils/router'
|
||||
@@ -48,7 +49,6 @@ import { staticRoutes } from '../routes/staticRoutes'
|
||||
import { loadingService } from '@/utils/ui'
|
||||
import { useCommon } from '@/hooks/core/useCommon'
|
||||
import { useWorktabStore } from '@/store/modules/worktab'
|
||||
import { fetchGetUserInfo } from '@/api/auth'
|
||||
import { ApiStatus } from '@/utils/http/status'
|
||||
import { isHttpError } from '@/utils/http/error'
|
||||
import { RouteRegistry, MenuProcessor, IframeRouteManager, RoutePermissionValidator } from '../core'
|
||||
@@ -151,12 +151,18 @@ async function handleRouteGuard(
|
||||
NProgress.start()
|
||||
}
|
||||
|
||||
// 1. 检查登录状态
|
||||
// 1. 检查登录状态(会话事实源 = auth store 的 token)
|
||||
if (!handleLoginStatus(to, userStore, next)) {
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 检查路由初始化是否已失败(防止死循环)
|
||||
// 2. 强制改密门:会话有效但 must_change → 除改密页外一律重定向
|
||||
// (含登录页——已登录的引导流程不回登录;后端 409 语义的前端镜像)
|
||||
if (!handleMustChangePassword(to, next)) {
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 检查路由初始化是否已失败(防止死循环)
|
||||
if (routeInitFailed) {
|
||||
// 已经失败过,直接放行到错误页面,不再重试
|
||||
if (to.matched.length > 0) {
|
||||
@@ -168,7 +174,7 @@ async function handleRouteGuard(
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 处理动态路由注册
|
||||
// 4. 处理动态路由注册
|
||||
if (!routeRegistry?.isRegistered() && userStore.isLogin) {
|
||||
// 防止并发请求(快速连续导航场景)
|
||||
if (routeInitInProgress) {
|
||||
@@ -180,12 +186,12 @@ async function handleRouteGuard(
|
||||
return
|
||||
}
|
||||
|
||||
// 4. 处理根路径重定向
|
||||
// 5. 处理根路径重定向
|
||||
if (handleRootPathRedirect(to, next)) {
|
||||
return
|
||||
}
|
||||
|
||||
// 5. 处理已匹配的路由
|
||||
// 6. 处理已匹配的路由
|
||||
if (to.matched.length > 0) {
|
||||
setWorktab(to)
|
||||
setPageTitle(to)
|
||||
@@ -193,12 +199,40 @@ async function handleRouteGuard(
|
||||
return
|
||||
}
|
||||
|
||||
// 6. 未匹配到路由,跳转到 404
|
||||
// 7. 未匹配到路由,跳转到 404
|
||||
next({ name: 'Exception404' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制改密门
|
||||
*
|
||||
* 会话有效且 must_change_password=true 时,除强制改密页本身外
|
||||
* 一律重定向(含登录页与静态异常页——改完初始密码前不进后台)。
|
||||
* 后端同语义:受保护端点 409(HTTP client 全局跳转兜底)。
|
||||
*
|
||||
* @returns true 表示可以继续,false 表示已处理跳转
|
||||
*/
|
||||
function handleMustChangePassword(
|
||||
to: RouteLocationNormalized,
|
||||
next: NavigationGuardNext
|
||||
): boolean {
|
||||
const authStore = useAuthStore()
|
||||
if (!authStore.isAuthenticated || !authStore.mustChangePassword) {
|
||||
return true
|
||||
}
|
||||
if (to.path === RoutesAlias.ForceChangePassword) {
|
||||
next()
|
||||
return false
|
||||
}
|
||||
next({ path: RoutesAlias.ForceChangePassword, replace: true })
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理登录状态
|
||||
*
|
||||
* 会话事实源为 auth store 的 token(localStorage 持久化,刷新恢复);
|
||||
* 模板 user store 的 isLogin 由登录/登出同步维护(此处对遗留残留自愈)。
|
||||
* @returns true 表示可以继续,false 表示已处理跳转
|
||||
*/
|
||||
function handleLoginStatus(
|
||||
@@ -206,13 +240,26 @@ function handleLoginStatus(
|
||||
userStore: ReturnType<typeof useUserStore>,
|
||||
next: NavigationGuardNext
|
||||
): boolean {
|
||||
// 已登录或访问登录页或静态路由,直接放行
|
||||
if (userStore.isLogin || to.path === RoutesAlias.Login || isStaticRoute(to.path)) {
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 已认证:放行(自愈模板侧登录标记——如 Task 2 桩时代的残留状态)
|
||||
if (authStore.isAuthenticated) {
|
||||
if (!userStore.isLogin) {
|
||||
userStore.setLoginStatus(true)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 未认证:登录页/静态路由放行,但强制改密页需要会话——回登录页
|
||||
if (to.path === RoutesAlias.Login || isStaticRoute(to.path)) {
|
||||
if (to.path === RoutesAlias.ForceChangePassword) {
|
||||
next({ name: 'Login', replace: true })
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 未登录且访问需要权限的页面,跳转到登录页并携带 redirect 参数
|
||||
userStore.logOut()
|
||||
next({
|
||||
name: 'Login',
|
||||
query: { redirect: to.fullPath }
|
||||
@@ -266,8 +313,8 @@ async function handleDynamicRoutes(
|
||||
loadingService.showLoading()
|
||||
|
||||
try {
|
||||
// 1. 获取用户信息
|
||||
await fetchUserInfo()
|
||||
// 1. 派生用户信息(auth store 登录快照 → 模板 UserInfo)
|
||||
deriveUserInfo()
|
||||
|
||||
// 2. 获取菜单数据
|
||||
const menuList = await menuProcessor.getMenuList()
|
||||
@@ -365,12 +412,15 @@ async function handleDynamicRoutes(
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
* 派生用户信息
|
||||
*
|
||||
* 后端无用户信息端点——登录响应(role/username)已随 auth store 持久化,
|
||||
* 此处映射为模板消费形态(roles 供菜单角色过滤,见 MenuProcessor)。
|
||||
*/
|
||||
async function fetchUserInfo(): Promise<void> {
|
||||
function deriveUserInfo(): void {
|
||||
const authStore = useAuthStore()
|
||||
const userStore = useUserStore()
|
||||
const data = await fetchGetUserInfo()
|
||||
userStore.setUserInfo(data)
|
||||
userStore.setUserInfo(authStore.userInfo)
|
||||
// 检查并清理工作台标签页(如果是不同用户登录)
|
||||
userStore.checkAndClearWorktabs()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user