211 lines
7.5 KiB
TypeScript
211 lines
7.5 KiB
TypeScript
import { defineStore } from 'pinia'
|
||
import { login } from '@/api/auth'
|
||
import { getRolePermissions } from '@/api/system/permission'
|
||
import { ref } from 'vue'
|
||
import axios from 'axios'
|
||
|
||
// JWT 解码函数(手写实现,不依赖外部库)
|
||
function parseJWT(token: string): any {
|
||
if (!token) return null
|
||
try {
|
||
const base64Url = token.split('.')[1]
|
||
if (!base64Url) return null
|
||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
|
||
const jsonPayload = decodeURIComponent(
|
||
atob(base64)
|
||
.split('')
|
||
.map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
|
||
.join('')
|
||
)
|
||
return JSON.parse(jsonPayload)
|
||
} catch (e) {
|
||
console.error('JWT parse error:', e)
|
||
return null
|
||
}
|
||
}
|
||
|
||
// 获取 Token 过期时间戳
|
||
function getTokenExp(token: string): number | null {
|
||
const decoded = parseJWT(token)
|
||
return decoded?.exp ? decoded.exp * 1000 : null // 转换为毫秒
|
||
}
|
||
|
||
export const useUserStore = defineStore('user', () => {
|
||
// 1. State: 初始化时优先从 localStorage 获取,防止刷新丢失
|
||
const token = ref(localStorage.getItem('access_token') || localStorage.getItem('token') || '')
|
||
const refreshToken = ref(localStorage.getItem('refresh_token') || '')
|
||
const role = ref(localStorage.getItem('role') || '')
|
||
const username = ref(localStorage.getItem('username') || '')
|
||
const permissions = ref<string[]>(JSON.parse(localStorage.getItem('permissions') || '[]'))
|
||
|
||
// 兼容旧版 localStorage key
|
||
if (localStorage.getItem('token') && !localStorage.getItem('access_token')) {
|
||
localStorage.setItem('access_token', localStorage.getItem('token') || '')
|
||
}
|
||
|
||
// 2. Actions
|
||
// 登录逻辑
|
||
const handleLogin = async (loginForm: any) => {
|
||
const res = await login(loginForm)
|
||
|
||
// [调试日志] 查看实际返回的数据结构
|
||
console.log('Login API Response:', res)
|
||
|
||
// ============================================================
|
||
// [关键修复] 兼容 Axios 拦截器的不同处理方式
|
||
// 如果拦截器已经返回了 response.data,那么 res 本身就是数据对象
|
||
// ============================================================
|
||
const data = res.data || res
|
||
|
||
// 安全检查:确保 data 存在且包含 access_token
|
||
if (!data || !data.access_token) {
|
||
console.error('Login Error: 响应数据中缺少 access_token', data)
|
||
throw new Error('登录失败: 响应数据异常')
|
||
}
|
||
|
||
// 更新 Pinia 状态 (内存)
|
||
token.value = data.access_token
|
||
refreshToken.value = data.refresh_token || ''
|
||
|
||
// 处理用户信息 (确保后端返回结构中有 user 字段)
|
||
if (data.user) {
|
||
const rawRole = data.user.role || 'user'
|
||
role.value = rawRole.toUpperCase() // 角色统一转换为大写
|
||
username.value = data.user.username || '用户'
|
||
|
||
// 持久化存储用户信息
|
||
localStorage.setItem('role', role.value)
|
||
localStorage.setItem('username', username.value)
|
||
}
|
||
|
||
// 持久化存储双 Token
|
||
localStorage.setItem('access_token', data.access_token)
|
||
if (data.refresh_token) {
|
||
localStorage.setItem('refresh_token', data.refresh_token)
|
||
}
|
||
|
||
// [Dify] 登录成功,重新初始化 Dify(Token 变化时 Dify 会开辟新会话,解决会话串号问题)
|
||
if (typeof window.initDifyChatbot === 'function') {
|
||
window.initDifyChatbot()
|
||
}
|
||
|
||
// 登录成功后,根据角色获取权限
|
||
if (role.value) {
|
||
try {
|
||
const permRes = await getRolePermissions(role.value)
|
||
const permData = permRes.data || permRes
|
||
// 合并 menus 和 elements 两个数组
|
||
const allPerms = [
|
||
...(permData.menus || []),
|
||
...(permData.elements || [])
|
||
]
|
||
permissions.value = allPerms
|
||
localStorage.setItem('permissions', JSON.stringify(allPerms))
|
||
} catch (error) {
|
||
console.error('获取权限失败:', error)
|
||
permissions.value = []
|
||
localStorage.setItem('permissions', '[]')
|
||
}
|
||
}
|
||
|
||
return true // 返回 true 表示登录成功
|
||
}
|
||
|
||
// 更新 Access Token(无感刷新时调用)
|
||
const setToken = (newToken: string) => {
|
||
token.value = newToken
|
||
localStorage.setItem('access_token', newToken)
|
||
|
||
// [Dify] Token 刷新后,重新初始化 Dify 以更新用户会话
|
||
if (typeof window.initDifyChatbot === 'function') {
|
||
window.initDifyChatbot()
|
||
}
|
||
}
|
||
|
||
// 退出逻辑
|
||
const logout = () => {
|
||
// 1. 清空 Pinia 状态 (内存)
|
||
token.value = ''
|
||
refreshToken.value = ''
|
||
role.value = ''
|
||
username.value = ''
|
||
permissions.value = []
|
||
|
||
// 2. 清空 LocalStorage (硬盘)
|
||
localStorage.removeItem('access_token')
|
||
|
||
// [Dify] 退出登录时,彻底销毁桌面上的 Dify 聊天窗口,防止信息泄露或报错
|
||
document.querySelectorAll('[id^="dify-chatbot-"]').forEach(el => el.remove())
|
||
|
||
// 清空其他本地存储
|
||
localStorage.removeItem('refresh_token')
|
||
localStorage.removeItem('token')
|
||
localStorage.removeItem('role')
|
||
localStorage.removeItem('username')
|
||
localStorage.removeItem('permissions')
|
||
}
|
||
|
||
// 刷新用户权限(不重新登录)
|
||
const refreshUserPermissions = async () => {
|
||
if (!token.value || !role.value) {
|
||
console.warn('无法刷新权限:用户未登录')
|
||
return
|
||
}
|
||
try {
|
||
const permRes = await getRolePermissions(role.value)
|
||
const permData = permRes.data || permRes
|
||
// 合并 menus 和 elements 两个数组
|
||
const allPerms = [
|
||
...(permData.menus || []),
|
||
...(permData.elements || [])
|
||
]
|
||
permissions.value = allPerms
|
||
localStorage.setItem('permissions', JSON.stringify(allPerms))
|
||
console.log('用户权限已刷新')
|
||
} catch (error) {
|
||
console.error('刷新权限失败:', error)
|
||
// 可选:保留原有权限
|
||
}
|
||
}
|
||
|
||
// 检查 refresh_token 是否即将过期(30分钟)
|
||
const isRefreshTokenExpiringSoon = (): boolean => {
|
||
if (!refreshToken.value) return true
|
||
const expTime = getTokenExp(refreshToken.value)
|
||
if (!expTime) return true
|
||
const now = Date.now()
|
||
const thirtyMinutes = 30 * 60 * 1000
|
||
return expTime - now < thirtyMinutes
|
||
}
|
||
|
||
// 3. Getters / Helpers
|
||
// 判断当前用户是否拥有某些角色
|
||
const hasRole = (roles: string[]) => {
|
||
return roles.includes(role.value)
|
||
}
|
||
|
||
// 判断当前用户是否拥有某个权限(菜单或元素)
|
||
const hasPermission = (code: string) => {
|
||
// 超级管理员拥有所有权限
|
||
if (role.value && role.value.toUpperCase() === 'SUPER_ADMIN') {
|
||
return true
|
||
}
|
||
return permissions.value.includes(code)
|
||
}
|
||
|
||
return {
|
||
token,
|
||
refreshToken,
|
||
role,
|
||
username,
|
||
permissions,
|
||
handleLogin,
|
||
setToken,
|
||
logout,
|
||
refreshUserPermissions,
|
||
isRefreshTokenExpiringSoon,
|
||
hasRole,
|
||
hasPermission
|
||
}
|
||
})
|