211 lines
5.8 KiB
TypeScript
211 lines
5.8 KiB
TypeScript
import { defineStore } from 'pinia'
|
||
import { ref } from 'vue'
|
||
import {
|
||
login as loginApi,
|
||
register as registerApi,
|
||
refresh as refreshApi,
|
||
type LoginRequest,
|
||
type RegisterRequest
|
||
} from '@/api/modules/account'
|
||
import {
|
||
getUserInfo,
|
||
updateUserInfo as updateUserInfoApi,
|
||
uploadAvatar as uploadAvatarApi,
|
||
deleteAvatar as deleteAvatarApi,
|
||
updateStatus as updateStatusApi,
|
||
type UserStatusType,
|
||
type UpdateUserInfoRequest
|
||
} from '@/api/modules/user'
|
||
|
||
/** 账户信息 */
|
||
export interface AccountState {
|
||
/** 用户唯一标识(UUID) */
|
||
userId?: string
|
||
/** 用户昵称 */
|
||
nickName?: string
|
||
/** 头像 */
|
||
avatar?: string | null
|
||
/** 个性签名 */
|
||
signature?: string | null
|
||
/** 年龄 */
|
||
age?: number | null
|
||
/** 性别:1=男,2=女 */
|
||
sex?: number | null
|
||
/** 生日 */
|
||
birthday?: string | null
|
||
/** 在线状态:ONLINE / AWAY / DND / INVISIBLE */
|
||
status?: UserStatusType
|
||
}
|
||
|
||
export const useAccountStore = defineStore(
|
||
'account',
|
||
() => {
|
||
/** 账户信息 */
|
||
const accountInfo = ref<AccountState | null>(null)
|
||
/** Access Token(30 分钟有效,每次请求通过 saToken 头携带) */
|
||
const accessToken = ref('')
|
||
/** Refresh Token(30 天有效,仅用于刷新 Access Token) */
|
||
const refreshToken = ref('')
|
||
/** token 是否已验证有效 */
|
||
const isTokenValid = ref(false)
|
||
/** 是否已完成初始验证(防止重复请求) */
|
||
const isInitialized = ref(false)
|
||
|
||
/**
|
||
* 用户登录
|
||
* 调用登录 API,成功后存储双 Token 并获取用户信息
|
||
* @param params - 登录请求参数(用户名、密码)
|
||
*/
|
||
const login = async (params: LoginRequest) => {
|
||
const res = await loginApi(params)
|
||
accessToken.value = res.data.accessToken
|
||
refreshToken.value = res.data.refreshToken
|
||
await fetchUserInfo()
|
||
isTokenValid.value = true
|
||
}
|
||
|
||
/**
|
||
* 用户注册
|
||
* 调用注册 API,成功后自动登录(存储双 Token 并获取用户信息)
|
||
* @param params - 注册请求参数(用户名、密码)
|
||
*/
|
||
const register = async (params: RegisterRequest) => {
|
||
const res = await registerApi(params)
|
||
accessToken.value = res.data.accessToken
|
||
refreshToken.value = res.data.refreshToken
|
||
await fetchUserInfo()
|
||
isTokenValid.value = true
|
||
}
|
||
|
||
/**
|
||
* 退出登录
|
||
* 清除令牌、账户信息和验证状态
|
||
*/
|
||
const logout = () => {
|
||
clearAuth()
|
||
}
|
||
|
||
/**
|
||
* 获取当前登录用户信息
|
||
* 需要先有 Access Token 才能调用
|
||
*/
|
||
const fetchUserInfo = async () => {
|
||
const res = await getUserInfo()
|
||
accountInfo.value = res.data
|
||
}
|
||
|
||
/**
|
||
* 刷新 Access Token(带并发锁)
|
||
*
|
||
* 由刷新拦截器的 doRefreshToken 和 SSE 的 onUnauthorized 调用。
|
||
* 返回新的 Access Token 字符串,方便调用方直接使用。
|
||
*
|
||
* 并发控制:若已有刷新正在进行中,返回同一个 Promise,
|
||
* 确保无论并发触发多少次,实际只发出一次 /account/refresh 请求。
|
||
*
|
||
* @returns 新的 Access Token
|
||
*/
|
||
/** 正在进行的刷新 Promise(null 表示无刷新进行中) */
|
||
let refreshPromise: Promise<string> | null = null
|
||
|
||
const refreshAccessToken = async (): Promise<string> => {
|
||
// 已有刷新进行中 → 复用同一个 Promise,避免并发重复请求
|
||
if (refreshPromise) {
|
||
return refreshPromise
|
||
}
|
||
|
||
if (!refreshToken.value) {
|
||
throw new Error('无 Refresh Token,无法刷新')
|
||
}
|
||
|
||
refreshPromise = (async () => {
|
||
try {
|
||
const res = await refreshApi({ refreshToken: refreshToken.value })
|
||
accessToken.value = res.data.accessToken
|
||
if (res.data.refreshToken) {
|
||
refreshToken.value = res.data.refreshToken
|
||
}
|
||
isTokenValid.value = true
|
||
return accessToken.value
|
||
} finally {
|
||
refreshPromise = null
|
||
}
|
||
})()
|
||
|
||
return refreshPromise
|
||
}
|
||
|
||
/**
|
||
* 更新当前用户的个人资料
|
||
* 调用后端接口持久化,并同步更新本地 accountInfo
|
||
* @param data - 要更新的字段(所有字段可选)
|
||
*/
|
||
const updateProfile = async (data: UpdateUserInfoRequest) => {
|
||
const res = await updateUserInfoApi(data)
|
||
accountInfo.value = res.data
|
||
}
|
||
|
||
/**
|
||
* 上传用户头像
|
||
* 调用后端接口上传文件并更新 DB,同步更新本地 accountInfo
|
||
* @param file - 头像文件
|
||
*/
|
||
const uploadAvatar = async (file: File) => {
|
||
const res = await uploadAvatarApi(file)
|
||
accountInfo.value = res.data
|
||
}
|
||
|
||
/**
|
||
* 删除用户头像
|
||
* 调用后端接口删除文件并清空 DB,同步更新本地 accountInfo
|
||
*/
|
||
const removeAvatar = async () => {
|
||
const res = await deleteAvatarApi()
|
||
accountInfo.value = res.data
|
||
}
|
||
|
||
/**
|
||
* 更新当前用户的在线状态
|
||
* 调用后端接口持久化,并同步更新本地 accountInfo
|
||
* @param status - 新状态值
|
||
*/
|
||
const updateUserStatus = async (status: UserStatusType) => {
|
||
await updateStatusApi({ status })
|
||
if (accountInfo.value) {
|
||
accountInfo.value = { ...accountInfo.value, status }
|
||
}
|
||
}
|
||
|
||
/** 清除所有认证状态 */
|
||
const clearAuth = () => {
|
||
accessToken.value = ''
|
||
refreshToken.value = ''
|
||
accountInfo.value = null
|
||
isTokenValid.value = false
|
||
isInitialized.value = false
|
||
}
|
||
|
||
console.log('useAccountStore', accessToken.value)
|
||
|
||
return {
|
||
accountInfo,
|
||
accessToken,
|
||
refreshToken,
|
||
login,
|
||
register,
|
||
logout,
|
||
refreshAccessToken,
|
||
fetchUserInfo,
|
||
updateProfile,
|
||
uploadAvatar,
|
||
removeAvatar,
|
||
updateUserStatus
|
||
}
|
||
},
|
||
{
|
||
persist: {
|
||
pick: ['accessToken', 'refreshToken']
|
||
}
|
||
}
|
||
)
|