2026-03-15 21:08:51 +08:00

235 lines
7.0 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 { ref, computed } from 'vue'
import { defineStore } from 'pinia'
import { getUsdcBalance, formatUsdcBalance, getUserInfo } from '@/api/user'
import { getUserWsUrl, BASE_URL } from '@/api/request'
import { jsonInBlacklist } from '@/api/jwt'
import { UserSdk, type BalanceData, type PositionData } from '../../sdk/userSocket'
export interface UserInfo {
/** 用户 IDAPI 可能返回 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 {
//
}
}
/** 从 API 返回的 user 对象解析 id/ID兼容 number 与 string */
function parseUserId(raw: { id?: number | string; ID?: number } | null | undefined): {
id: number | string | undefined
numId: number | undefined
} {
const rawId = raw?.ID ?? raw?.id
const numId =
typeof rawId === 'number' ? rawId : rawId != null ? parseInt(String(rawId), 10) : undefined
return { id: rawId ?? numId, numId: Number.isFinite(numId) ? numId : undefined }
}
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(() => {
const img = user.value?.headerImg
if (!img) return ''
if (img.startsWith('http') || img.startsWith('data:')) return img
// 处理相对路径,拼接 BASE_URL
const baseUrl = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
const path = img.startsWith('/') ? img : `/${img}`
return `${baseUrl}${path}`
})
/** 钱包余额显示,如 "0.00",可从接口或 UserSocket 推送更新 */
const balance = ref<string>('0.00')
let userSdkRef: UserSdk | null = null
const positionUpdateCallbacks: ((data: PositionData & Record<string, unknown>) => void)[] = []
// 若从 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) => {
if ((data.tokenType ?? '').toUpperCase() !== 'USDC') return
const avail = data.available ?? data.amount
if (avail != null) {
balance.value = formatUsdcBalance(String(avail))
}
})
sdk.onPositionUpdate((data) => {
positionUpdateCallbacks.forEach((cb) => cb(data as PositionData & Record<string, unknown>))
})
sdk.onConnect(() => {})
sdk.onDisconnect(() => {})
sdk.onError((e) => {
console.error('[UserStore] UserSocket 错误:', e)
})
userSdkRef = sdk
sdk.connect()
}
function disconnectUserSocket() {
if (userSdkRef) {
userSdkRef.disconnect()
userSdkRef = null
}
}
/** 订阅 position_update 推送,返回取消订阅函数 */
function onPositionUpdate(
cb: (data: PositionData & Record<string, unknown>) => void,
): () => void {
positionUpdateCallbacks.push(cb)
return () => {
const i = positionUpdateCallbacks.indexOf(cb)
if (i >= 0) positionUpdateCallbacks.splice(i, 1)
}
}
function setUser(loginData: { token?: string; user?: UserInfo }) {
const t = loginData.token ?? ''
const raw = loginData.user ?? null
token.value = t
if (raw) {
const { id, numId } = parseUserId(raw)
user.value = { ...raw, id, ID: numId ?? raw.ID }
} else {
user.value = null
}
if (t && user.value) {
saveToStorage(t, user.value)
connectUserSocket()
} else {
clearStorage()
disconnectUserSocket()
}
}
async function logout() {
const headers = getAuthHeaders()
if (headers) {
try {
await jsonInBlacklist({ headers })
} catch {
// 忽略失败,仍执行本地登出
}
}
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 与 balance */
async function fetchUserInfo() {
const headers = getAuthHeaders()
if (!headers) return
try {
const res = await getUserInfo(headers)
const data = res.data as Record<string, unknown> | undefined
if (res.code !== 0 && res.code !== 200) return
// 更新余额data.balance.available
const bal = data?.balance as { available?: string } | undefined
if (bal?.available != null) {
balance.value = formatUsdcBalance(String(bal.available))
}
// 更新用户信息data.userInfo
const u = (data?.userInfo ?? data?.user ?? data) as Record<string, unknown> | undefined
if (!u) return
const { id, numId } = parseUserId(u as { id?: number | string; ID?: number })
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,
ID: numId,
} 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,
onPositionUpdate,
}
})