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 }