237 lines
6.2 KiB
JavaScript
237 lines
6.2 KiB
JavaScript
/**
|
||
* 用户状态管理 Store
|
||
*
|
||
* 管理用户认证状态和个人信息,提供:
|
||
* - 登录/注册/登出操作
|
||
* - Token 自动管理(存储 + 刷新)
|
||
* - 用户信息缓存与更新
|
||
* - 持久化存储(通过 pinia-plugin-persistedstate,H5 用 localStorage,小程序用 uni 存储)
|
||
*
|
||
* 对应后端 API:/api/v1/auth/*
|
||
* 接口文档见:docs/api/frontend/auth.md
|
||
*/
|
||
|
||
import { defineStore } from 'pinia'
|
||
import { ref, computed } from 'vue'
|
||
import authApi from '@/api/auth'
|
||
import { useWebSocketStore } from '@/store/websocket'
|
||
import {
|
||
saveToken,
|
||
getToken,
|
||
getRefreshToken,
|
||
removeToken,
|
||
saveUserInfo,
|
||
getUserInfo,
|
||
removeUserInfo,
|
||
clearAll
|
||
} from '@/utils/storage'
|
||
|
||
/**
|
||
* 用户 Store(Composition API 风格)
|
||
*
|
||
* state:
|
||
* - token: Access Token
|
||
* - userInfo: 用户信息对象
|
||
*
|
||
* getters:
|
||
* - isLoggedIn: 是否已登录
|
||
* - username: 用户名快捷访问
|
||
* - roles: 用户角色列表
|
||
*
|
||
* actions:
|
||
* - login: 用户登录
|
||
* - register: 用户注册
|
||
* - logout: 退出登录
|
||
* - getProfile: 获取/刷新用户信息
|
||
* - updateProfile: 更新个人信息
|
||
* - changePassword: 修改密码
|
||
* - refreshUserToken: 刷新 Token
|
||
*/
|
||
export const useUserStore = defineStore('user', () => {
|
||
// ==================== State ====================
|
||
|
||
/** Access Token,用于 API 认证 */
|
||
const token = ref(getToken() || '')
|
||
|
||
/** 用户信息对象,与后端 dto.UserInfo 结构一致 */
|
||
const userInfo = ref(getUserInfo() || null)
|
||
|
||
// ==================== Getters ====================
|
||
|
||
/** 是否已登录(Token 存在视为已登录) */
|
||
const isLoggedIn = computed(() => !!token.value)
|
||
|
||
/** 用户名快捷访问 */
|
||
const username = computed(() => userInfo.value?.username || '')
|
||
|
||
/** 用户角色列表 */
|
||
const roles = computed(() => userInfo.value?.roles || [])
|
||
|
||
// ==================== Actions ====================
|
||
|
||
/**
|
||
* 处理登录/注册成功后的通用逻辑
|
||
* 保存 Token 和用户信息到 Store 和本地存储
|
||
*
|
||
* @param {Object} data - 后端返回的登录响应 { token, refresh_token, expires_in, user }
|
||
*/
|
||
const _handleAuthSuccess = (data) => {
|
||
token.value = data.token
|
||
userInfo.value = data.user
|
||
saveToken(data.token, data.refresh_token, data.expires_in)
|
||
saveUserInfo(data.user)
|
||
}
|
||
|
||
/**
|
||
* 用户登录
|
||
*
|
||
* @param {Object} loginData - { account, password }
|
||
* @returns {Promise<Object>} 登录响应数据
|
||
*/
|
||
const login = async (loginData) => {
|
||
const res = await authApi.login(loginData)
|
||
_handleAuthSuccess(res.data)
|
||
return res.data
|
||
}
|
||
|
||
/**
|
||
* 用户注册(注册成功后自动登录)
|
||
*
|
||
* @param {Object} registerData - { username, email, password, nickname? }
|
||
* @returns {Promise<Object>} 注册响应数据
|
||
*/
|
||
const register = async (registerData) => {
|
||
const res = await authApi.register(registerData)
|
||
_handleAuthSuccess(res.data)
|
||
return res.data
|
||
}
|
||
|
||
const applyOAuthLogin = (data) => {
|
||
_handleAuthSuccess(data)
|
||
return data
|
||
}
|
||
|
||
/**
|
||
* 退出登录
|
||
* 1. 调用后端 API 从 Redis 删除 Token(使 Token 立即失效)
|
||
* 2. 清除本地存储的 Token 和用户信息
|
||
* 3. 重置 Store 状态
|
||
*/
|
||
const logout = async () => {
|
||
const wsStore = useWebSocketStore()
|
||
wsStore.disconnect()
|
||
try {
|
||
await authApi.logout()
|
||
} catch (e) {
|
||
console.warn('登出 API 调用失败,仍然清除本地状态', e)
|
||
}
|
||
// 清理 chat store 中的私人视图态(语音已播放缓存按 userId 隔离存在 storage,
|
||
// 此处只重置运行时 state,下次登录会按新 userId 加载对应缓存)
|
||
try {
|
||
const { useChatStore } = await import('@/store/chat')
|
||
useChatStore().resetVoicePlayedState()
|
||
} catch (_) { /* ignore */ }
|
||
// 清空通知中心缓存,避免账号切换后看到前一个用户的通知
|
||
try {
|
||
const { useNotifyStore } = await import('@/store/notify')
|
||
useNotifyStore().reset()
|
||
} catch (_) { /* ignore */ }
|
||
token.value = ''
|
||
userInfo.value = null
|
||
clearAll()
|
||
}
|
||
|
||
/**
|
||
* 获取/刷新用户信息
|
||
* 从后端获取最新的用户资料并更新 Store 和本地缓存
|
||
*
|
||
* @returns {Promise<Object>} 用户信息
|
||
*/
|
||
const getProfile = async () => {
|
||
const res = await authApi.getProfile()
|
||
userInfo.value = res.data
|
||
saveUserInfo(res.data)
|
||
return res.data
|
||
}
|
||
|
||
/**
|
||
* 更新个人信息
|
||
*
|
||
* @param {Object} data - 需要更新的字段 { nickname?, avatar?, gender?, phone? }
|
||
* @returns {Promise<Object>} 更新后的用户信息
|
||
*/
|
||
const updateProfile = async (data) => {
|
||
const res = await authApi.updateProfile(data)
|
||
userInfo.value = res.data
|
||
saveUserInfo(res.data)
|
||
return res.data
|
||
}
|
||
|
||
/**
|
||
* 修改密码
|
||
*
|
||
* @param {Object} data - { old_password, new_password }
|
||
* @returns {Promise<Object>}
|
||
*/
|
||
const changePassword = async (data) => {
|
||
return await authApi.changePassword(data)
|
||
}
|
||
|
||
/**
|
||
* 刷新 Token
|
||
* 使用 Refresh Token 获取新的 Access Token
|
||
* 新 Token 会覆盖 Redis 中的旧值(实现单设备登录)
|
||
*
|
||
* @returns {Promise<boolean>} 刷新是否成功
|
||
*/
|
||
const refreshUserToken = async () => {
|
||
const refreshTokenValue = getRefreshToken()
|
||
if (!refreshTokenValue) return false
|
||
|
||
try {
|
||
const res = await authApi.refreshToken(refreshTokenValue)
|
||
_handleAuthSuccess(res.data)
|
||
return true
|
||
} catch (e) {
|
||
// Refresh Token 也过期或无效,需要重新登录
|
||
token.value = ''
|
||
userInfo.value = null
|
||
clearAll()
|
||
return false
|
||
}
|
||
}
|
||
|
||
return {
|
||
// state
|
||
token,
|
||
userInfo,
|
||
// getters
|
||
isLoggedIn,
|
||
username,
|
||
roles,
|
||
// actions
|
||
login,
|
||
register,
|
||
applyOAuthLogin,
|
||
logout,
|
||
getProfile,
|
||
updateProfile,
|
||
changePassword,
|
||
refreshUserToken
|
||
}
|
||
}, {
|
||
/**
|
||
* Pinia 持久化配置
|
||
* H5 环境使用 localStorage,小程序/App 使用 uni 存储
|
||
* 仅持久化 token 和 userInfo,避免存储过多无关数据
|
||
*/
|
||
persist: {
|
||
key: 'echo-user-store',
|
||
storage: {
|
||
getItem: (key) => uni.getStorageSync(key),
|
||
setItem: (key, value) => uni.setStorageSync(key, value)
|
||
},
|
||
paths: ['token', 'userInfo']
|
||
}
|
||
})
|