Files
KCGL/inventory-web/src/stores/user.ts
2026-05-20 09:09:33 +08:00

211 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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] 登录成功,重新初始化 DifyToken 变化时 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
}
})