feat(auth): 实现用户认证状态持久化及权限控制优化

- 新增 pinia-plugin-persistedstate 实现用户状态本地存储
- 重构 API 响应类型为统一模块管理
- 优化路由守卫逻辑,支持 token 自动登录
- 修复权限检查逻辑错误,调整 AI 聊天页访问权限
- 新增用户信息获取接口及类型定义
This commit is contained in:
2026-01-07 20:14:40 +08:00
parent 8e5558d9a2
commit fad13e00e5
13 changed files with 2810 additions and 21 deletions

View File

@@ -8,6 +8,7 @@
"@arco-design/web-vue": "^2.57.0",
"axios": "^1.13.2",
"pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
"vue": "^3.5.24",
"vue-router": "4",
},
@@ -205,6 +206,8 @@
"dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="],
"defu": ["defu@6.1.4", "https://registry.npmmirror.com/defu/-/defu-6.1.4.tgz", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
@@ -333,6 +336,8 @@
"pinia": ["pinia@3.0.4", "", { "dependencies": { "@vue/devtools-api": "^7.7.7" }, "peerDependencies": { "typescript": ">=4.5.0", "vue": "^3.5.11" }, "optionalPeers": ["typescript"] }, "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw=="],
"pinia-plugin-persistedstate": ["pinia-plugin-persistedstate@4.7.1", "https://registry.npmmirror.com/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-4.7.1.tgz", { "dependencies": { "defu": "^6.1.4" }, "peerDependencies": { "@nuxt/kit": ">=3.0.0", "@pinia/nuxt": ">=0.10.0", "pinia": ">=3.0.0" }, "optionalPeers": ["@nuxt/kit", "@pinia/nuxt", "pinia"] }, "sha512-WHOqh2esDlR3eAaknPbqXrkkj0D24h8shrDPqysgCFR6ghqP/fpFfJmMPJp0gETHsvrh9YNNg6dQfo2OEtDnIQ=="],
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],

2691
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -17,6 +17,7 @@
"@arco-design/web-vue": "^2.57.0",
"axios": "^1.13.2",
"pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
"vue": "^3.5.24",
"vue-router": "4"
},

View File

@@ -20,7 +20,7 @@ const checkAccess = (
}
// 如果用户登录才能访问
if (needAccess === ACCESS_ENUM.USER) {
if (loginUserAccess !== ACCESS_ENUM.NOT_LOGIN) {
if (loginUserAccess === ACCESS_ENUM.NOT_LOGIN) {
return false;
}
}
@@ -31,7 +31,7 @@ const checkAccess = (
return false;
}
}
// 如果说啥都不符合,那还说啥了,直接拒了
return false;
// 权限通过
return true;
};
export default checkAccess;

View File

@@ -5,8 +5,8 @@ import type { LoginUesr } from "../store/types";
import { useUserStore } from "../store/user";
import ACCESS_ENUM from "./accessEnum";
import checkAccess from "./checkAccess";
import { getAccessToken } from "@/utils/token";
const userStore = useUserStore();
/**
* 检查是否需要权限访问
* @param to 要访问的路由
@@ -39,12 +39,17 @@ const redirectWithAccess = (
};
// 这里接收异步函数,是因为下面要调用 userStore.getLoginUser()
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore(); // 必须在守卫内部获取 store确保 pinia 已安装
console.log("登陆用户信息", userStore.loginUser);
let loginUser = userStore.loginUser;
// 如果之前没登陆过,自动登录
if (!loginUser || !loginUser.userRole) {
await userStore.getLoginUser();
loginUser = userStore.loginUser;
// 如果之前没登陆过,且有 token尝试自动登录
// 没有 token 时不调用 API避免未登录状态重复弹出错误提示
if (!loginUser || !loginUser.userRole || loginUser.userRole === ACCESS_ENUM.NOT_LOGIN) {
const token = getAccessToken();
if (token) {
await userStore.getLoginUser();
loginUser = userStore.loginUser;
}
}
// 检查是否需要权限访问

View File

@@ -1,15 +1,11 @@
import request from "@/plugins/axios";
import type { ApiResponse } from "../response";
/**
* 认证服务 API 类型定义
*/
// 统一响应格式
export interface ApiResponse<T = any> {
success: boolean;
message: string;
data: T;
}
// 登录请求参数
export interface LoginRequest {

View File

@@ -9,5 +9,4 @@ export {
type LoginRequest,
type LoginResponse,
type RefreshTokenResponse,
type ApiResponse,
} from './auth'

70
src/api/auth/user.ts Normal file
View File

@@ -0,0 +1,70 @@
import request from "@/plugins/axios";
import type { ApiResponse } from "../response";
/**
*
*/
/**
* 用户信息
*/
export interface UserInfo {
/**
* id
*/
id: number;
/**
* 用户账号
*/
userAccount: string;
/**
* 开放平台id
*/
unionId?: string;
/**
* 公众号openId
*/
mpOpenId?: string;
/**
* 用户昵称
*/
userName: string;
/**
* 用户头像
*/
userAvatar?: string;
/**
* 用户简介
*/
userProfile?: string;
/**
* 用户角色user/admin/ban
*/
userRole: 'user' | 'admin' | 'ban';
/**
* 创建时间
*/
createTime: string;
/**
* 更新时间
*/
updateTime: string;
}
/**
* 获取当前登录用户信息
* @returns 当前登录用户信息
*/
export const getUserInfoByToken = () => {
return request<ApiResponse<UserInfo>>({
url: "/v1/auth/getUserInfo",
method: "GET",
})
}

3
src/api/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export {
type ApiResponse,
} from './response'

6
src/api/response.ts Normal file
View File

@@ -0,0 +1,6 @@
// 统一响应格式
export interface ApiResponse<T = any> {
success: boolean;
message: string;
data: T;
}

View File

@@ -4,11 +4,15 @@ import "@arco-design/web-vue/dist/arco.css";
import "./style.css";
import App from "./App.vue";
import router from "./router/router";
import {createPinia} from "pinia";
import { createPinia } from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
import "./access";
const app = createApp(App);
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
app.use(pinia);
app.use(router);
app.use(ArcoVue);
app.use(pinia)
app.mount("#app");
app.mount("#app");

View File

@@ -25,7 +25,7 @@ export const routes: Array<RouteRecordRaw> = [
path: "/ai/chat",
name: "AiChatView",
component: AiChatView,
meta: { hideInMenu: false, access: ACCESS_ENUM.NOT_LOGIN },
meta: { hideInMenu: false, access: ACCESS_ENUM.USER },
},
{
path: "/user/login",

View File

@@ -1,6 +1,7 @@
import { defineStore } from "pinia";
import ACCESS_ENUM from "../access/accessEnum";
import type { LoginUesr } from "../store/types";
import { getUserInfoByToken } from "@/api/auth/user";
/**
*
*/
@@ -11,12 +12,20 @@ export const useUserStore = defineStore("user", {
userRole: ACCESS_ENUM.NOT_LOGIN,
} as LoginUesr,
}),
persist: {
key: "user-store",
storage: localStorage,
pick: ["loginUser"], // 只持久化 loginUser
},
actions: {
// 获取登录用户
async getLoginUser() {
try {
// 从后端获取当前登录用户信息
const res = await getUserInfoByToken();
console.log("获取登录用户成功", res);
if(res.data.success === true){
this.updateUserLoginStatus(res.data.data);
}
}catch(e) {
console.error("获取登录用户失败", e);
// 网络错误情况也视为未登录