Files
EchoChat/frontend/src/store/user.js
2026-05-26 22:41:53 +08:00

237 lines
6.2 KiB
JavaScript
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.

/**
* 用户状态管理 Store
*
* 管理用户认证状态和个人信息,提供:
* - 登录/注册/登出操作
* - Token 自动管理(存储 + 刷新)
* - 用户信息缓存与更新
* - 持久化存储(通过 pinia-plugin-persistedstateH5 用 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'
/**
* 用户 StoreComposition 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']
}
})