Files
mp-pilates/packages/app/src/stores/user.ts
2026-09-09 11:27:03 +08:00

201 lines
6.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 { useInviteStore } from './invite'
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type {
UserProfileResponse,
UserStatsResponse,
MembershipWithCardType,
} from '@mp-pilates/shared'
import {
UserRole,
MembershipStatus,
getMembershipRenewalHint,
} from '@mp-pilates/shared'
import { wxLogin, isLoggedIn, logout as authLogout } from '../utils/auth'
import { setUnauthorizedHandler } from '../utils/session'
import { get, put } from '../utils/request'
import { ROUTES } from '../utils/routes'
import { cacheSubscriptionMessageTemplateConfig, resetSubscriptionMessageTemplateCache } from '../utils/wechat-subscription'
function syncSubscriptionTemplates(profile?: Pick<UserProfileResponse, 'subscriptionMessageTemplates'> | null) {
cacheSubscriptionMessageTemplateConfig(profile?.subscriptionMessageTemplates)
}
export const useUserStore = defineStore('user', () => {
// State
const user = ref<UserProfileResponse | null>(null)
const stats = ref<UserStatsResponse | null>(null)
const memberships = ref<readonly MembershipWithCardType[]>([])
const membershipsLoading = ref(false)
const membershipsLoaded = ref(false)
const membershipsError = ref(false)
const statsLoading = ref(false)
const statsError = ref(false)
let membershipRequestId = 0
let statsRequestId = 0
const token = ref<string>(uni.getStorageSync('token') as string || '')
// Getters
// loggedIn: 是否已认证token 存在),与 user 数据是否已加载无关
const loggedIn = computed(() => !!token.value)
// hasProfile: user 数据是否已加载(用于 UI 展示判断)
const hasProfile = computed(() => !!user.value)
const isAdmin = computed(() => user.value?.role === UserRole.ADMIN)
const activeMemberships = computed(() =>
memberships.value.filter((m) => m.status === MembershipStatus.ACTIVE),
)
const hasValidMembership = computed(() => activeMemberships.value.length > 0)
const renewalHint = computed(() => getMembershipRenewalHint(memberships.value))
const inviteShareEligible = computed(() => !!user.value?.inviteShareEligible)
// Actions
async function login() {
try {
const result = await wxLogin()
token.value = result.token
user.value = result.user
await useInviteStore().refresh().catch(() => {})
syncSubscriptionTemplates(result.user)
return { user: result.user, isNewUser: result.isNewUser }
} catch (err) {
console.error('Login failed:', err)
throw err
}
}
/**
* Login, redirect new users to complete profile, and fetch post-login data.
* Returns isNewUser so callers know whether navigation was intercepted.
*/
async function loginWithSetup(): Promise<{ isNewUser: boolean }> {
const { isNewUser } = await login()
if (isNewUser) {
uni.navigateTo({ url: ROUTES.PROFILE_INFO_FIRST_LOGIN })
return { isNewUser: true }
}
await Promise.all([fetchProfile(), fetchMemberships()])
return { isNewUser: false }
}
async function fetchProfile() {
if (!isLoggedIn()) return
try {
user.value = await get<UserProfileResponse>('/user/profile')
await useInviteStore().refresh().catch(() => {})
syncSubscriptionTemplates(user.value)
return user.value
} catch (err) {
console.error('Fetch profile failed:', err)
}
}
async function fetchStats(): Promise<boolean> {
if (!isLoggedIn()) return false
const id = ++statsRequestId
const session = token.value
statsLoading.value = true
statsError.value = false
try {
const result = await get<UserStatsResponse>('/user/stats')
if (id !== statsRequestId || session !== token.value) return false
stats.value = result
return true
} catch {
if (id === statsRequestId && session === token.value) statsError.value = true
return false
} finally {
if (id === statsRequestId && session === token.value) statsLoading.value = false
}
}
async function fetchMemberships(): Promise<boolean> {
if (!isLoggedIn()) return false
const id = ++membershipRequestId
const session = token.value
membershipsLoading.value = true
membershipsError.value = false
try {
const result = await get<MembershipWithCardType[]>('/membership/my')
if (id !== membershipRequestId || session !== token.value) return false
memberships.value = [...result]
membershipsLoaded.value = true
return true
} catch {
if (id === membershipRequestId && session === token.value) membershipsError.value = true
return false
} finally {
if (id === membershipRequestId && session === token.value) membershipsLoading.value = false
}
}
async function updateProfile(data: { nickname?: string; avatarUrl?: string }) {
const updated = await put<UserProfileResponse>('/user/profile', data)
user.value = updated
syncSubscriptionTemplates(updated)
return updated
}
function setProfile(profile: UserProfileResponse) {
user.value = profile
syncSubscriptionTemplates(profile)
}
function checkAuth() {
if (isLoggedIn()) {
fetchProfile()
fetchMemberships()
}
}
function clearSession() {
membershipRequestId++
statsRequestId++
membershipsLoading.value = false
membershipsLoaded.value = false
membershipsError.value = false
statsLoading.value = false
statsError.value = false
token.value = ''
useInviteStore().reset()
user.value = null
stats.value = null
memberships.value = []
resetSubscriptionMessageTemplateCache()
}
function logout() {
authLogout()
clearSession()
}
setUnauthorizedHandler(clearSession)
return {
membershipsLoading,
membershipsLoaded,
membershipsError,
statsLoading,
statsError,
user,
stats,
memberships,
token,
loggedIn,
hasProfile,
isAdmin,
activeMemberships,
hasValidMembership,
renewalHint,
inviteShareEligible,
login,
loginWithSetup,
fetchProfile,
fetchStats,
fetchMemberships,
updateProfile,
setProfile,
checkAuth,
logout,
}
})