/** * 用户状态管理 composable * * 管理:token、用户信息、登录状态、登录弹窗显隐 * Token 持久化:Cookie(SSR 安全,Nuxt useCookie 自动同步服务端/客户端) */ export function useUser() { const tokenCookie = useCookie('token', { maxAge: 7 * 24 * 3600, path: '/', }) /** * 登录态用 useState 全局共享。 * 注意:不能直接把 useCookie 的 ref 当共享状态——每次 useCookie('token') 都会返回 * 一个独立的 ref,页面在登录前创建的 ref 不会感知到登录后的写入, * 会导致请求不带 token、后端返回 1009。 */ const token = useState('auth_token', () => tokenCookie.value || '') /** 同时写入共享状态与 Cookie */ function setToken(value) { token.value = value || '' tokenCookie.value = value || null } const userInfo = useState('userInfo', () => ({ user_id: null, user_name: '', avatar: '', nickname: '', open_id: '', vip_type: 0, balance: 0, })) /** * 只以 token 判断登录态(对齐 V2)。 * 不能附加 userInfo.user_id 条件:刷新页面时用户信息是异步拉取的, * 否则页面首次渲染会被判为未登录,导致该拉的数据不拉、还误弹登录框。 */ const isLogin = computed(() => !!token.value) // 登录弹窗全局状态(任何组件都可以触发) const showLogin = useState('showLogin', () => false) /** * 设置登录态 */ function setLogin(data) { setToken(data.token) userInfo.value = { user_id: data.user_id, user_name: data.user_name || data.nickname || '', avatar: data.avatar || '', nickname: data.nickname || data.user_name || '', open_id: data.open_id || '', vip_type: data.vip_type || 0, balance: data.balance || 0, } // 同步写入 open_id(共享状态 + Cookie,供 useApi 读取 uid) if (data.open_id) { const openIdCookie = useCookie('open_id', { path: '/' }) openIdCookie.value = data.open_id useState('auth_open_id', () => '').value = data.open_id } } /** * 退出登录 */ function logout() { setToken('') userInfo.value = { user_id: null, user_name: '', avatar: '', nickname: '', open_id: '', vip_type: 0, balance: 0, } // 清除 open_id const openIdCookie = useCookie('open_id', { path: '/' }) openIdCookie.value = null useState('auth_open_id', () => '').value = '' } /** * 刷新用户信息 */ async function refreshUserInfo() { if (!token.value) return try { const { post } = useApi() const res = await post('/user/getuserinfo') if (res.code === 0 && res.data) { userInfo.value = { ...userInfo.value, ...res.data, } } } catch (e) { // 获取失败不中断 } } /** * 刷新点数余额。 * 注意:/user/getuserinfo 不返回点数,必须单独查 /point/balance, * 否则 header 会一直显示登录时的旧值(通常是 0)。 */ async function refreshBalance() { if (!token.value) return 0 try { const { post } = useApi() const res = await post('/point/balance') const points = res?.data?.points ?? res?.data?.point_balance ?? 0 userInfo.value = { ...userInfo.value, balance: Number(points) || 0 } return userInfo.value.balance } catch { return userInfo.value.balance || 0 } } /** * 初始化:如果有 token 但无 userInfo,自动刷新 */ async function init() { if (token.value && !userInfo.value.user_id) { await refreshUserInfo() } } /** * 处理 1009 鉴权失效(由 useApi 调用) */ function handleAuthError() { if (import.meta.client) { logout() showLogin.value = true } } // 客户端初始化 if (import.meta.client) { init() } return { token, setToken, userInfo, isLogin, showLogin, setLogin, logout, refreshUserInfo, refreshBalance, handleAuthError, init, } }