209 lines
5.8 KiB
TypeScript
209 lines
5.8 KiB
TypeScript
import { ref, computed } from 'vue'
|
||
import { defineStore } from 'pinia'
|
||
import { getUsdcBalance, formatUsdcBalance, getUserInfo } from '@/api/user'
|
||
import { getUserWsUrl } from '@/api/request'
|
||
import { UserSdk, type BalanceData } from '../../sdk/userSocket'
|
||
|
||
export interface UserInfo {
|
||
/** 用户 ID(API 可能返回 id 或 ID) */
|
||
id?: number | string
|
||
ID?: number
|
||
headerImg?: string
|
||
nickName?: string
|
||
userName?: string
|
||
[key: string]: unknown
|
||
}
|
||
|
||
const STORAGE_KEY = 'poly-user'
|
||
|
||
function loadStored(): { token: string; user: UserInfo } | null {
|
||
try {
|
||
const raw = localStorage.getItem(STORAGE_KEY)
|
||
if (!raw) return null
|
||
return JSON.parse(raw) as { token: string; user: UserInfo }
|
||
} catch {
|
||
return null
|
||
}
|
||
}
|
||
|
||
function saveToStorage(token: string, user: UserInfo) {
|
||
try {
|
||
localStorage.setItem(STORAGE_KEY, JSON.stringify({ token, user }))
|
||
} catch {
|
||
//
|
||
}
|
||
}
|
||
|
||
function clearStorage() {
|
||
try {
|
||
localStorage.removeItem(STORAGE_KEY)
|
||
} catch {
|
||
//
|
||
}
|
||
}
|
||
|
||
export const useUserStore = defineStore('user', () => {
|
||
const stored = loadStored()
|
||
const token = ref<string>(stored?.token ?? '')
|
||
const user = ref<UserInfo | null>(stored?.user ?? null)
|
||
|
||
const isLoggedIn = computed(() => !!token.value && !!user.value)
|
||
const avatarUrl = computed(() => user.value?.headerImg ?? '')
|
||
/** 钱包余额显示,如 "0.00",可从接口或 UserSocket 推送更新 */
|
||
const balance = ref<string>('0.00')
|
||
|
||
let userSdkRef: UserSdk | null = null
|
||
|
||
// 若从 storage 恢复登录态,自动连接 UserSocket
|
||
if (stored?.token && stored?.user) {
|
||
// 延迟到 nextTick 后连接,避免 store 未完全初始化
|
||
Promise.resolve().then(() => connectUserSocket())
|
||
}
|
||
|
||
function connectUserSocket() {
|
||
if (!token.value) return
|
||
disconnectUserSocket()
|
||
const sdk = new UserSdk({
|
||
url: getUserWsUrl(),
|
||
token: token.value,
|
||
autoReconnect: true,
|
||
reconnectInterval: 2000,
|
||
})
|
||
sdk.onBalanceUpdate((data: BalanceData) => {
|
||
const avail = data.available ?? data.amount
|
||
if (avail != null) {
|
||
balance.value = formatUsdcBalance(String(avail))
|
||
}
|
||
})
|
||
sdk.onConnect(() => {
|
||
console.log('[UserStore] UserSocket 已连接')
|
||
})
|
||
sdk.onDisconnect(() => {
|
||
console.log('[UserStore] UserSocket 已断开')
|
||
})
|
||
sdk.onError((e) => {
|
||
console.error('[UserStore] UserSocket 错误:', e)
|
||
})
|
||
userSdkRef = sdk
|
||
sdk.connect()
|
||
}
|
||
|
||
function disconnectUserSocket() {
|
||
if (userSdkRef) {
|
||
userSdkRef.disconnect()
|
||
userSdkRef = null
|
||
}
|
||
}
|
||
|
||
function setUser(loginData: { token?: string; user?: UserInfo }) {
|
||
const t = loginData.token ?? ''
|
||
const raw = loginData.user ?? null
|
||
token.value = t
|
||
if (raw) {
|
||
const rawId = raw.ID ?? raw.id
|
||
const numId =
|
||
typeof rawId === 'number'
|
||
? rawId
|
||
: rawId != null
|
||
? parseInt(String(rawId), 10)
|
||
: undefined
|
||
user.value = {
|
||
...raw,
|
||
id: rawId ?? numId,
|
||
ID: Number.isFinite(numId) ? numId : raw.ID,
|
||
}
|
||
} else {
|
||
user.value = null
|
||
}
|
||
if (t && user.value) {
|
||
saveToStorage(t, user.value)
|
||
connectUserSocket()
|
||
} else {
|
||
clearStorage()
|
||
disconnectUserSocket()
|
||
}
|
||
}
|
||
|
||
function logout() {
|
||
disconnectUserSocket()
|
||
token.value = ''
|
||
user.value = null
|
||
clearStorage()
|
||
}
|
||
|
||
/** 鉴权请求头:x-token 与 x-user-id,未登录时返回 undefined */
|
||
function getAuthHeaders(): Record<string, string> | undefined {
|
||
if (!token.value) return undefined
|
||
const uid = user.value?.id ?? user.value?.ID
|
||
return {
|
||
'x-token': token.value,
|
||
...(uid != null && uid !== '' && { 'x-user-id': String(uid) }),
|
||
}
|
||
}
|
||
|
||
/** 请求 USDC 余额(需已登录),amount/available 除以 1000000 后更新余额显示 */
|
||
async function fetchUsdcBalance() {
|
||
const headers = getAuthHeaders()
|
||
if (!headers) return
|
||
try {
|
||
const res = await getUsdcBalance(headers)
|
||
if (res.code === 0 && res.data) {
|
||
balance.value = formatUsdcBalance(res.data.available)
|
||
}
|
||
} catch (e) {
|
||
console.error('[fetchUsdcBalance] 请求失败:', e)
|
||
}
|
||
}
|
||
|
||
/** 请求用户信息(需已登录),更新 store 中的 user */
|
||
async function fetchUserInfo() {
|
||
const headers = getAuthHeaders()
|
||
if (!headers) return
|
||
try {
|
||
const res = await getUserInfo(headers)
|
||
console.log('[fetchUserInfo] 接口响应:', JSON.stringify(res, null, 2))
|
||
const data = res.data as Record<string, unknown> | undefined
|
||
// 接口返回 data.userInfo 或 data.user,取实际用户对象;若仍含 userInfo 则再取一层
|
||
let u = (data?.userInfo ?? data?.user ?? data) as Record<string, unknown>
|
||
if (u?.userInfo && (u.ID == null && u.id == null)) {
|
||
u = u.userInfo as Record<string, unknown>
|
||
}
|
||
if ((res.code === 0 || res.code === 200) && u) {
|
||
const rawId = u.ID ?? u.id
|
||
const numId =
|
||
typeof rawId === 'number'
|
||
? rawId
|
||
: rawId != null
|
||
? parseInt(String(rawId), 10)
|
||
: undefined
|
||
user.value = {
|
||
...u,
|
||
userName: (u.userName ?? u.username) as string | undefined,
|
||
nickName: (u.nickName ?? u.nickname) as string | undefined,
|
||
headerImg: (u.headerImg ?? u.avatar ?? u.avatarUrl) as string | undefined,
|
||
id: (rawId ?? numId) as number | string | undefined,
|
||
ID: Number.isFinite(numId) ? numId : undefined,
|
||
} as UserInfo
|
||
if (token.value && user.value) saveToStorage(token.value, user.value)
|
||
}
|
||
} catch (e) {
|
||
console.error('[fetchUserInfo] 请求失败:', e)
|
||
}
|
||
}
|
||
|
||
return {
|
||
token,
|
||
user,
|
||
isLoggedIn,
|
||
avatarUrl,
|
||
balance,
|
||
setUser,
|
||
logout,
|
||
getAuthHeaders,
|
||
fetchUsdcBalance,
|
||
fetchUserInfo,
|
||
connectUserSocket,
|
||
disconnectUserSocket,
|
||
}
|
||
})
|