user.js 6.38 KB
/**
 * 用户状态管理
 *
 * @description 管理用户登录状态、用户信息等
 * @module stores/user
 */

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import Taro from '@tarojs/taro'
import { loginStatusAPI, loginAPI, getProfileAPI, logoutAPI } from '@/api/user'
import { ensureOpenidAuthorized } from '@/utils/openid'
import { isFeatureEnabled, getFeatureConfig } from '@/config/features'

export const useUserStore = defineStore('user', () => {
  // ========== 状态 ==========
  /** 用户信息 */
  const userInfo = ref(null)

  /** 是否已授权(openid) */
  const isOpenid = ref(false)

  /** 是否已登录 */
  const isLoggedIn = ref(false)

  /** 加载状态 */
  const loading = ref(false)

  /** 上次获取用户信息的时间戳(用于防抖) */
  let lastFetchTime = 0

  /** 防抖时间间隔(毫秒) */
  const FETCH_DEBOUNCE_TIME = 5000

  // ========== 方法 ==========

  /**
   * 检查登录状态
   * @description 小程序启动时检查 openid 和登录状态
   * - 只触发微信授权,不跳转登录页
   * - 401 由接口拦截器统一处理
   * @throws {Error} 检查失败时抛出错误
   *
   * @example
   * await userStore.checkLoginStatus()
   */
  async function checkLoginStatus() {
    loading.value = true

    try {
      // 1. 确保 openid 已授权并尝试自动登录
      const user = await ensureOpenidAuthorized()

      if (user) {
        // miniProgramAuthAPI 返回了用户信息,说明已自动登录
        userInfo.value = user
        isOpenid.value = true
        isLoggedIn.value = true
        return
      }

      // 2. 查询登录状态
      const res = await loginStatusAPI()

      if (res.code === 1) {
        isOpenid.value = res.data.is_openid
        isLoggedIn.value = res.data.is_login

        // 3. 如果已登录,获取用户信息
        if (isLoggedIn.value) {
          await fetchUserInfo()
        }
        // 注意:这里不跳转登录页,让用户可以浏览小程序
        // 当用户操作触发接口返回 401 时,会自动跳转登录页
      } else {
        throw new Error(res.msg || '查询登录状态失败')
      }
    } catch (err) {
      console.error('检查登录状态失败:', err)
      throw err
    } finally {
      loading.value = false
    }
  }

  /**
   * 获取用户信息
   * @description 调用 getProfileAPI 获取用户信息,带防抖机制(5秒内不重复请求)
   * @param {boolean} force - 是否强制刷新(忽略防抖)
   * @throws {Error} 获取失败时抛出错误
   *
   * @example
   * await userStore.fetchUserInfo()
   * await userStore.fetchUserInfo(true) // 强制刷新
   */
  async function fetchUserInfo(force = false) {
    // 防抖检查:如果不是强制刷新,且距离上次请求不足 5 秒,则跳过
    if (!force) {
      const now = Date.now()
      if (now - lastFetchTime < FETCH_DEBOUNCE_TIME) {
        console.log('[UserStore] 跳过频繁的用户信息请求')
        return
      }
    }

    try {
      loading.value = true
      const res = await getProfileAPI()

      if (res.code === 1) {
        userInfo.value = res.data.user
        // 更新最后请求时间
        lastFetchTime = Date.now()
      } else {
        throw new Error(res.msg || '获取用户信息失败')
      }
    } catch (err) {
      console.error('获取用户信息失败:', err)
      throw err
    } finally {
      loading.value = false
    }
  }

  /**
   * 用户登录
   * @description 调用 loginAPI 进行账号密码登录
   * @param {Object} loginData 登录数据
   * @param {string} loginData.uuid 账号
   * @param {string} loginData.password 密码
   * @returns {{success: boolean, message?: string}} 登录结果
   *
   * @example
   * const result = await userStore.login({
   *   uuid: '13800138000',
   *   password: '123456'
   * })
   * if (result.success) {
   *   console.log('登录成功')
   * }
   */
  async function login(loginData) {
    loading.value = true

    try {
      const res = await loginAPI(loginData)

      if (res.code === 1) {
        // 登录成功,获取用户信息
        await fetchUserInfo()

        isLoggedIn.value = true

        return { success: true }
      } else {
        throw new Error(res.msg || '登录失败')
      }
    } catch (err) {
      console.error('登录失败:', err)
      return { success: false, message: err.message }
    } finally {
      loading.value = false
    }
  }

  /**
   * 用户登出
   * @description 调用 logoutAPI 并清除本地状态
   *
   * @example
   * await userStore.logout()
   */
  async function logout() {
    try {
      // 调用登出接口
      await logoutAPI()

      // 清除本地状态
      userInfo.value = null
      isOpenid.value = false
      isLoggedIn.value = false
    } catch (err) {
      console.error('登出失败:', err)
    }
  }

  /**
   * TabBar 红点状态
   *
   * @description 根据 userInfo 中的字段计算是否显示红点
   * - 只在功能开关启用时生效
   * - 只处理 'me' 按钮的红点
   * - 根据 unread_count 字段判断(可配置)
   *
   * @returns {string[]} 需要显示红点的 tab key 数组
   *
   * @example
   * // 返回 ['me'] 表示在 '我的' 按钮显示红点
   * // 返回 [] 表示不显示红点
   */
  const tabBarBadges = computed(() => {
    // 1. 检查功能开关
    if (!isFeatureEnabled('tabbarBadge')) {
      return []
    }

    // 2. 检查用户信息是否存在
    if (!userInfo.value) {
      return []
    }

    // 3. 获取配置的字段名和阈值
    const fieldName = getFeatureConfig('tabbarBadgeField') // 'unread_count'
    const threshold = getFeatureConfig('tabbarBadgeThreshold') // 1

    // 4. 读取字段值
    const fieldValue = userInfo.value[fieldName]

    // 5. 判断是否显示红点
    const badges = []

    // 处理数字类型(如 unread_count: 5)
    if (typeof fieldValue === 'number') {
      if (fieldValue >= threshold) {
        badges.push('me')
      }
    }
    // 处理布尔类型(如 has_notification: true)
    else if (typeof fieldValue === 'boolean') {
      if (fieldValue) {
        badges.push('me')
      }
    }

    return badges
  })

  // ========== 返回 ==========
  return {
    // 状态
    userInfo,
    isOpenid,
    isLoggedIn,
    loading,

    // 计算属性
    tabBarBadges,

    // 方法
    checkLoginStatus,
    fetchUserInfo,
    login,
    logout
  }
})