309 lines
9.8 KiB
JavaScript
309 lines
9.8 KiB
JavaScript
import {
|
|
memberCenterMock,
|
|
userInfoMock,
|
|
memberCardMock,
|
|
bookingMock
|
|
} from './mockData.js'
|
|
import { formatMemberCenterPhone, normalizePhoneForStore } from './format.js'
|
|
import {
|
|
getDefaultBodyTestState,
|
|
mergeBodyTestState,
|
|
getLatestBodyTestRecord,
|
|
buildBodyReportSummary
|
|
} from './bodyTestStore.js'
|
|
import {
|
|
getDefaultModuleState,
|
|
mergeModuleState,
|
|
finalizeModules
|
|
} from './moduleStore.js'
|
|
import {
|
|
getDefaultCourseCatalog,
|
|
mergeCourseCatalog,
|
|
canCancelBooking
|
|
} from './bookingStore.js'
|
|
|
|
const STORAGE_KEY = 'gym_member_info_v1'
|
|
|
|
function clone(value) {
|
|
return JSON.parse(JSON.stringify(value))
|
|
}
|
|
|
|
export function buildCardTip(remainingDays) {
|
|
return `距离下次到期还有${remainingDays}天,请及时续费`
|
|
}
|
|
|
|
function applyCardInfo(store) {
|
|
const days = computeRemainingDays(store.card.validityEnd)
|
|
store.card.remainingDays = days
|
|
store.cardInfo.remainingDays = days
|
|
store.cardInfo.tip = buildCardTip(days)
|
|
return store
|
|
}
|
|
|
|
export function syncStats(store) {
|
|
store.stats = {
|
|
...store.stats,
|
|
pointsBalance: store.stats.pointsBalance ?? 1250
|
|
}
|
|
return store
|
|
}
|
|
|
|
function finalizeStore(store) {
|
|
syncStats(store)
|
|
applyCardInfo(store)
|
|
if (store.profile?.avatar) {
|
|
store.memberProfile.avatar = store.profile.avatar
|
|
}
|
|
if (store.profile?.phone) {
|
|
store.memberProfile.phone = formatMemberCenterPhone(store.profile.phone)
|
|
}
|
|
const latestBodyTest = getLatestBodyTestRecord(store)
|
|
if (latestBodyTest) {
|
|
const previous = store.bodyTest.records[1]
|
|
store.bodyReport = buildBodyReportSummary(latestBodyTest, previous)
|
|
}
|
|
if (store.modules) {
|
|
finalizeModules(store)
|
|
}
|
|
return store
|
|
}
|
|
|
|
function getDefaultStore() {
|
|
return finalizeStore({
|
|
profile: { ...userInfoMock, avatar: memberCenterMock.userInfo.avatar },
|
|
memberProfile: { ...memberCenterMock.userInfo },
|
|
stats: { ...memberCenterMock.stats },
|
|
cardInfo: { ...memberCenterMock.cardInfo },
|
|
card: { ...memberCardMock.card },
|
|
records: clone(memberCardMock.records),
|
|
ongoingBookings: clone(bookingMock.ongoing),
|
|
historyBookings: clone(bookingMock.history),
|
|
checkIns: clone(memberCenterMock.checkIns),
|
|
bodyReport: { ...memberCenterMock.bodyReport },
|
|
bodyTest: getDefaultBodyTestState(),
|
|
modules: getDefaultModuleState(),
|
|
courseCatalog: getDefaultCourseCatalog(),
|
|
couponPoints: { ...memberCenterMock.couponPoints },
|
|
referral: { ...memberCenterMock.referral }
|
|
})
|
|
}
|
|
|
|
function mergeDefaults(saved) {
|
|
const defaults = getDefaultStore()
|
|
return finalizeStore({
|
|
profile: { ...defaults.profile, ...(saved.profile || {}) },
|
|
memberProfile: { ...defaults.memberProfile, ...(saved.memberProfile || {}) },
|
|
stats: { ...defaults.stats, ...(saved.stats || {}) },
|
|
cardInfo: { ...defaults.cardInfo, ...(saved.cardInfo || {}) },
|
|
card: { ...defaults.card, ...(saved.card || {}) },
|
|
records: saved.records?.length ? saved.records : defaults.records,
|
|
ongoingBookings: saved.ongoingBookings ?? defaults.ongoingBookings,
|
|
historyBookings: saved.historyBookings ?? defaults.historyBookings,
|
|
checkIns: saved.checkIns?.length ? saved.checkIns : defaults.checkIns,
|
|
bodyReport: { ...defaults.bodyReport, ...(saved.bodyReport || {}) },
|
|
bodyTest: mergeBodyTestState(saved.bodyTest),
|
|
modules: mergeModuleState(saved.modules),
|
|
courseCatalog: mergeCourseCatalog(saved.courseCatalog),
|
|
couponPoints: { ...defaults.couponPoints, ...(saved.couponPoints || {}) },
|
|
referral: { ...defaults.referral, ...(saved.referral || {}) }
|
|
})
|
|
}
|
|
|
|
export function loadMemberStore() {
|
|
try {
|
|
const saved = uni.getStorageSync(STORAGE_KEY)
|
|
if (saved && typeof saved === 'object') {
|
|
return mergeDefaults(saved)
|
|
}
|
|
} catch (e) {
|
|
console.warn('[memberStore] load failed', e)
|
|
}
|
|
return getDefaultStore()
|
|
}
|
|
|
|
export function saveMemberStore(store) {
|
|
uni.setStorageSync(STORAGE_KEY, store)
|
|
}
|
|
|
|
/** 解析为本地 0 点,避免 ISO 字符串时区偏差 */
|
|
export function parseLocalDate(dateStr) {
|
|
if (!dateStr) return null
|
|
const str = String(dateStr).trim()
|
|
const iso = str.match(/^(\d{4})-(\d{2})-(\d{2})$/)
|
|
if (iso) {
|
|
return new Date(Number(iso[1]), Number(iso[2]) - 1, Number(iso[3]))
|
|
}
|
|
const cn = str.match(/(\d{4})年(\d{2})月(\d{2})日/)
|
|
if (cn) {
|
|
return new Date(Number(cn[1]), Number(cn[2]) - 1, Number(cn[3]))
|
|
}
|
|
const parsed = new Date(str.replace(/-/g, '/'))
|
|
return Number.isNaN(parsed.getTime()) ? null : parsed
|
|
}
|
|
|
|
function formatIsoDate(date) {
|
|
const y = date.getFullYear()
|
|
const m = String(date.getMonth() + 1).padStart(2, '0')
|
|
const d = String(date.getDate()).padStart(2, '0')
|
|
return `${y}-${m}-${d}`
|
|
}
|
|
|
|
function formatChineseDate(date) {
|
|
const y = date.getFullYear()
|
|
const m = String(date.getMonth() + 1).padStart(2, '0')
|
|
const day = String(date.getDate()).padStart(2, '0')
|
|
return `${y}年${m}月${day}日`
|
|
}
|
|
|
|
function formatRecordTime(date) {
|
|
const y = date.getFullYear()
|
|
const m = String(date.getMonth() + 1).padStart(2, '0')
|
|
const d = String(date.getDate()).padStart(2, '0')
|
|
const h = String(date.getHours()).padStart(2, '0')
|
|
const min = String(date.getMinutes()).padStart(2, '0')
|
|
return `${y}-${m}-${d} ${h}:${min}`
|
|
}
|
|
|
|
function nextRecordId(records) {
|
|
return (records || []).reduce((max, item) => Math.max(max, item.id || 0), 0) + 1
|
|
}
|
|
|
|
export function computeRemainingDays(endDateStr) {
|
|
const end = parseLocalDate(endDateStr)
|
|
if (!end) return 0
|
|
const now = new Date()
|
|
now.setHours(0, 0, 0, 0)
|
|
end.setHours(0, 0, 0, 0)
|
|
const diff = Math.ceil((end - now) / 86400000)
|
|
return Math.max(0, diff)
|
|
}
|
|
|
|
export function formatUpcomingAlert(booking) {
|
|
if (!booking) return ''
|
|
const timePart = booking.timeRange || booking.schedule?.split(' ')[1] || ''
|
|
return `明天 ${timePart} 有一堂${booking.title},请提前 30 分钟到场`
|
|
}
|
|
|
|
export function toBookingPreviewItem(item) {
|
|
return {
|
|
id: item.id,
|
|
dateDay: item.dateDay,
|
|
dateMonth: item.dateMonth,
|
|
desc: `${item.title} · ${item.timeRange}`,
|
|
coach: item.coachShort || item.coach.replace('教练', ''),
|
|
location: item.location ? `地点:${item.location}` : '',
|
|
status: item.status,
|
|
statusLabel: item.statusLabel
|
|
}
|
|
}
|
|
|
|
export function getBookingPreview(store, limit = 2) {
|
|
return store.ongoingBookings.slice(0, limit).map(toBookingPreviewItem)
|
|
}
|
|
|
|
export function getCenterPageData(store) {
|
|
return {
|
|
userInfo: { ...store.memberProfile },
|
|
stats: { ...store.stats },
|
|
cardInfo: { ...store.cardInfo },
|
|
bookingPreview: getBookingPreview(store),
|
|
checkIns: store.checkIns.map((item) => ({ ...item })),
|
|
bodyReport: {
|
|
...store.bodyReport,
|
|
weight: store.profile.weight || store.bodyReport.weight
|
|
},
|
|
couponPoints: {
|
|
...store.couponPoints,
|
|
points: store.stats.pointsBalance
|
|
},
|
|
referral: { ...store.referral }
|
|
}
|
|
}
|
|
|
|
export function cancelOngoingBooking(store, id) {
|
|
const index = store.ongoingBookings.findIndex((b) => b.id === id)
|
|
if (index < 0) return { ok: false, message: '预约不存在' }
|
|
|
|
const item = store.ongoingBookings[index]
|
|
if (!canCancelBooking(item)) {
|
|
return { ok: false, message: '距开课不足2小时,无法取消' }
|
|
}
|
|
|
|
const [removed] = store.ongoingBookings.splice(index, 1)
|
|
if (removed.courseId) {
|
|
const course = store.courseCatalog.find((c) => c.id === removed.courseId)
|
|
if (course && course.enrolled > 0) course.enrolled -= 1
|
|
}
|
|
store.historyBookings.unshift({
|
|
...removed,
|
|
status: 'cancelled',
|
|
statusLabel: '已取消',
|
|
footerText: '用户主动取消',
|
|
canCancel: false
|
|
})
|
|
finalizeStore(store)
|
|
saveMemberStore(store)
|
|
return { ok: true, message: '已取消' }
|
|
}
|
|
|
|
export function renewMemberCard(store, addDays = 90) {
|
|
const now = new Date()
|
|
now.setHours(0, 0, 0, 0)
|
|
|
|
let base = parseLocalDate(store.card.validityEnd) || new Date(now)
|
|
base.setHours(0, 0, 0, 0)
|
|
// 已过期:从今天起续费;未过期:从当前到期日起顺延
|
|
if (base < now) {
|
|
base = new Date(now)
|
|
}
|
|
|
|
const end = new Date(base)
|
|
end.setDate(end.getDate() + addDays)
|
|
|
|
const validityEnd = formatIsoDate(end)
|
|
const validityEndCn = formatChineseDate(end)
|
|
store.card.validityEnd = validityEnd
|
|
store.card.validity = store.card.validityStart
|
|
? `${store.card.validityStart} - ${validityEndCn}`
|
|
: `2024年01月01日 - ${validityEndCn}`
|
|
store.cardInfo.expireDate = `有效期至 ${validityEndCn}`
|
|
|
|
store.records.unshift({
|
|
id: nextRecordId(store.records),
|
|
type: 'consume',
|
|
title: '会员卡续费',
|
|
time: formatRecordTime(new Date()),
|
|
value: `+${addDays}天`,
|
|
valueType: 'positive',
|
|
icon: '/static/images/pluscircle.png',
|
|
iconTheme: 'orange'
|
|
})
|
|
|
|
finalizeStore(store)
|
|
saveMemberStore(store)
|
|
return store
|
|
}
|
|
|
|
export function saveUserProfile(store, profile) {
|
|
const phone = normalizePhoneForStore(profile.phone ?? store.profile.phone)
|
|
store.profile = { ...store.profile, ...profile, phone }
|
|
store.memberProfile = {
|
|
...store.memberProfile,
|
|
name: store.profile.name,
|
|
phone: formatMemberCenterPhone(store.profile.phone),
|
|
avatar: store.profile.avatar || store.memberProfile.avatar
|
|
}
|
|
if (store.profile.weight) {
|
|
store.bodyReport.weight = store.profile.weight
|
|
}
|
|
finalizeStore(store)
|
|
saveMemberStore(store)
|
|
return store
|
|
}
|
|
|
|
export function persistMemberStore(store) {
|
|
finalizeStore(store)
|
|
saveMemberStore(store)
|
|
return store
|
|
}
|