支付成功后将次数或有效期叠加到已有会员卡,首页与详情页引导续卡;接口 401 时同步清掉本地登录态。 Co-authored-by: Cursor <cursoragent@cursor.com>
157 lines
4.4 KiB
TypeScript
157 lines
4.4 KiB
TypeScript
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 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
|
||
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')
|
||
syncSubscriptionTemplates(user.value)
|
||
return user.value
|
||
} catch (err) {
|
||
console.error('Fetch profile failed:', err)
|
||
}
|
||
}
|
||
|
||
async function fetchStats() {
|
||
if (!isLoggedIn()) return
|
||
try {
|
||
stats.value = await get<UserStatsResponse>('/user/stats')
|
||
} catch (err) {
|
||
console.error('Fetch stats failed:', err)
|
||
}
|
||
}
|
||
|
||
async function fetchMemberships() {
|
||
if (!isLoggedIn()) return
|
||
try {
|
||
memberships.value = await get<MembershipWithCardType[]>('/membership/my')
|
||
} catch (err) {
|
||
console.error('Fetch memberships failed:', err)
|
||
}
|
||
}
|
||
|
||
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() {
|
||
token.value = ''
|
||
user.value = null
|
||
stats.value = null
|
||
memberships.value = []
|
||
resetSubscriptionMessageTemplateCache()
|
||
}
|
||
|
||
function logout() {
|
||
authLogout()
|
||
clearSession()
|
||
}
|
||
|
||
setUnauthorizedHandler(clearSession)
|
||
|
||
return {
|
||
user,
|
||
stats,
|
||
memberships,
|
||
token,
|
||
loggedIn,
|
||
hasProfile,
|
||
isAdmin,
|
||
activeMemberships,
|
||
hasValidMembership,
|
||
renewalHint,
|
||
inviteShareEligible,
|
||
login,
|
||
loginWithSetup,
|
||
fetchProfile,
|
||
fetchStats,
|
||
fetchMemberships,
|
||
updateProfile,
|
||
setProfile,
|
||
checkAuth,
|
||
logout,
|
||
}
|
||
})
|