# Booking Page - Quick Reference & Code Snippets ## 🚀 Quick Start: Understanding the Flow ### Where Slots Come From ```typescript // 1. Store calls API packages/app/src/stores/booking.ts:17-27 async function fetchSlots(date: string) { loadingSlots.value = true try { // GET /time-slot/available?date=2026-04-05 slots.value = await get( '/time-slot/available', { date } ) } catch (err) { console.error('Fetch slots failed:', err) slots.value = [] // ⚠️ Clears on error! } finally { loadingSlots.value = false } } ``` ### Where Time Periods Are Defined ```typescript // packages/shared/src/constants.ts:11-15 export const TIME_PERIODS = { MORNING: { label: '上午', start: '06:00', end: '12:00' }, AFTERNOON: { label: '下午', start: '12:00', end: '18:00' }, EVENING: { label: '晚上', start: '18:00', end: '22:00' }, } as const ``` ### Where Filtering Happens ```typescript // pages/booking/index.vue:94-103 const filteredSlots = computed(() => { const slots = bookingStore.slots as TimeSlotWithBookingStatus[] if (!selectedPeriod.value) return [...slots] const period = TIME_PERIODS[selectedPeriod.value] return slots.filter((slot) => { const t = slot.startTime // "09:00", "10:00", etc return t >= period.start && t < period.end }) }) ``` ### Slot Rendering ```vue ``` --- ## 🔍 Finding Specific Things ### Q: Where do the time slot types come from? **A:** `packages/shared/src/types/time-slot.ts` ```typescript interface TimeSlotWithBookingStatus extends TimeSlot { readonly isBookedByMe: boolean // true if user booked it readonly myBookingId: string | null // needed for cancellation } interface TimeSlot { readonly id: string // UUID readonly date: string // "2026-04-05" readonly startTime: string // "09:00" readonly endTime: string // "10:00" readonly capacity: number // 1 (for private lessons) readonly bookedCount: number // 0 or 1 readonly status: TimeSlotStatus // OPEN|FULL|CLOSED readonly source: TimeSlotSource // TEMPLATE|MANUAL readonly templateId: string | null readonly createdAt: string readonly updatedAt: string } ``` ### Q: Where is the membership selection happening? **A:** `components/BookingConfirmPopup.vue:136-147` ```typescript const selectedMembershipId = ref('') watch( [() => props.visible, () => props.memberships], ([visible, memberships]) => { if (visible && memberships.length > 0) { selectedMembershipId.value = memberships[0].id // Auto-select first } }, { immediate: true }, ) ``` ### Q: Where are the button states determined? **A:** `components/SlotCard.vue:15-45` ```vue ``` ### Q: Where is the API request actually made? **A:** `utils/request.ts:22-59` ```typescript export function request(options: RequestOptions): Promise { return new Promise((resolve, reject) => { const token = uni.getStorageSync('token') as string uni.request({ url: `${BASE_URL}${options.url}`, // BASE_URL = http://localhost:3000/api method: options.method || 'GET', data: options.data, header: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), ...options.header, }, success: (res) => { if (res.statusCode === 401) { uni.removeStorageSync('token') uni.showToast({ title: '请重新登录', icon: 'none' }) reject(new Error('Unauthorized')) return } if (res.statusCode >= 400) { const body = res.data as ApiResponse reject(new Error(body?.message || `请求失败 (${res.statusCode})`)) return } const body = res.data as ApiResponse if (body.success) { resolve(body.data as T) // ← Extract data from ApiResponse } else { reject(new Error(body.message || '请求失败')) } }, fail: (err) => { reject(new Error(err.errMsg || '网络请求失败')) }, }) }) } ``` --- ## 🐛 Debugging Tips ### Tip 1: Check what's in the store ```typescript // In browser console while in booking page: console.log('Slots:', JSON.stringify(uni.$u.pinia.state.value.booking.slots, null, 2)) console.log('Selected period:', uni.$u.pinia.state.value.booking.selectedPeriod) ``` ### Tip 2: Log slot filtering ```typescript // Add to pages/booking/index.vue filteredSlots computed: const filteredSlots = computed(() => { const slots = bookingStore.slots as TimeSlotWithBookingStatus[] if (!selectedPeriod.value) { console.log('No period filter, showing all slots:', slots.length) return [...slots] } const period = TIME_PERIODS[selectedPeriod.value] console.log(`Filtering by ${selectedPeriod.value}:`, period) console.log('All slot times:', slots.map(s => s.startTime)) const filtered = slots.filter((slot) => { const t = slot.startTime const matches = t >= period.start && t < period.end if (!matches) console.log(`${t} not in [${period.start}, ${period.end})`) return matches }) console.log('Filtered result:', filtered.length) return filtered }) ``` ### Tip 3: Verify API response ```typescript // In stores/booking.ts fetchSlots(): async function fetchSlots(date: string) { loadingSlots.value = true try { console.log('Fetching slots for date:', date) slots.value = await get( '/time-slot/available', { date } ) console.log('Received slots:', slots.value) console.log('Slot count:', slots.value.length) if (slots.value.length > 0) { console.log('First slot:', JSON.stringify(slots.value[0], null, 2)) } } catch (err) { console.error('Fetch slots failed:', err) slots.value = [] } finally { loadingSlots.value = false } } ``` ### Tip 4: Check network requests ```typescript // Open WeChat DevTools → Network tab // Look for GET request to /time-slot/available // Check: // ✓ URL has ?date=YYYY-MM-DD // ✓ Authorization header exists // ✓ Response status 200 // ✓ Response body has "success": true ``` --- ## ❌ Common Issues & Solutions ### Issue 1: Slots not loading **Symptoms:** - Page shows "当日暂无可约时段" (no slots) - No error message **Check list:** ```typescript // 1. Is API endpoint correct? // Check: /time-slot/available?date=2026-04-05 // Should return TimeSlotWithBookingStatus[] // 2. Is date format correct? // Page sends: formatDate(new Date()) → "2026-04-05" // API expects: "YYYY-MM-DD" console.log(formatDate(new Date())) // Should output: "2026-04-05" // 3. Is authentication working? console.log('Token:', uni.getStorageSync('token')) // 4. Check for errors in console // If fetchSlots fails, slots.value becomes [] ``` **Solution:** ```typescript // In bookingStore.fetchSlots(), add error state: const error = ref(null) async function fetchSlots(date: string) { loadingSlots.value = true error.value = null // Clear previous error try { slots.value = await get( '/time-slot/available', { date } ) } catch (err) { console.error('Fetch slots failed:', err) error.value = err instanceof Error ? err.message : '加载失败' slots.value = [] } finally { loadingSlots.value = false } } // Then in page template: {{ error }} 重试 ``` ### Issue 2: Time period filtering not working **Symptoms:** - Select "上午" (morning) but all slots still show - Or vice versa **Check:** ```typescript // 1. Verify TIME_PERIODS constant console.log('TIME_PERIODS:', TIME_PERIODS) // 2. Check selectedPeriod value console.log('Selected period:', selectedPeriod.value) // 3. Verify slot.startTime format // Should be "HH:MM" like "09:00", not "09:00:00" bookingStore.slots.forEach(slot => { console.log('Slot time:', slot.startTime, 'format ok?', /^\d{2}:\d{2}$/.test(slot.startTime)) }) // 4. Test filtering manually const slot = bookingStore.slots[0] const period = TIME_PERIODS.MORNING console.log(`${slot.startTime} >= ${period.start}?`, slot.startTime >= period.start) console.log(`${slot.startTime} < ${period.end}?`, slot.startTime < period.end) ``` **Solution:** ```typescript // If time format is "09:00:00", slice it: const filteredSlots = computed(() => { const slots = bookingStore.slots as TimeSlotWithBookingStatus[] if (!selectedPeriod.value) return [...slots] const period = TIME_PERIODS[selectedPeriod.value] return slots.filter((slot) => { // Ensure HH:MM format const t = slot.startTime.slice(0, 5) // "09:00:00" → "09:00" return t >= period.start && t < period.end }) }) ``` ### Issue 3: Booking button not responding **Symptoms:** - Click "可预约" but nothing happens - No modal appears **Check:** ```typescript // 1. Is slot.status correct? console.log('Slot status:', slot.status) // Should be "OPEN" to show book button // 2. Is isBookedByMe false? console.log('Is booked by me?', slot.isBookedByMe) // Should be false to show book button // 3. Is onBookTap being called? // Add to pages/booking/index.vue: async function onBookTap(slot: TimeSlotWithBookingStatus) { console.log('Book tapped for slot:', slot) // ← Should log // Rest of code... } // 4. Is userStore.loggedIn true? console.log('Logged in?', userStore.loggedIn) ``` ### Issue 4: Membership not showing in popup **Symptoms:** - Booking popup appears but no membership card shown - "暂无可用会员卡" displayed **Check:** ```typescript // 1. Are memberships loaded? console.log('Memberships:', userStore.memberships) // 2. Are any memberships ACTIVE? console.log('Active memberships:', userStore.activeMemberships) console.log('Has valid membership?', userStore.hasValidMembership) // 3. Are memberships passed to popup? // In pages/booking/index.vue: console.log('Popup passed memberships:', userStore.activeMemberships) ``` **Solution:** ```typescript // In onMounted: onMounted(async () => { if (userStore.loggedIn && userStore.activeMemberships.length === 0) { console.log('Fetching memberships...') try { await userStore.fetchMemberships() console.log('Memberships loaded:', userStore.activeMemberships) } catch (err) { console.error('Failed to fetch memberships:', err) uni.showToast({ title: '加载会员卡失败', icon: 'none' }) } } await loadSlots(selectedDate.value) }) ``` --- ## 📊 Capacity Display Logic ### How Capacity Color is Determined ```typescript // components/SlotCard.vue:69-81 const capacityLabel = computed(() => { const { bookedCount, capacity, status } = props.slot if (status === TimeSlotStatus.CLOSED) return '已关闭' return `${bookedCount}/${capacity} 人` }) const capacityClass = computed(() => { const { bookedCount, capacity, status } = props.slot if (status === TimeSlotStatus.CLOSED) return 'cap-closed' if (status === TimeSlotStatus.FULL) return 'cap-full' if (bookedCount >= capacity * 0.8) return 'cap-almost' return 'cap-open' }) // Color mapping in styles: // cap-open: #f0faf3 bg, #4caf50 text (green) - <80% booked // cap-almost: #fff8ed bg, #f59e0b text (orange) - ≥80% booked // cap-full: #fef0f0 bg, #ef4444 text (red) - status: FULL // cap-closed: #f5f5f5 bg, #999 text (gray) - status: CLOSED ``` ### Example Calculations ```typescript // Slot 1: capacity=1, bookedCount=0, status=OPEN // 0/1 人 in green badge (0% booked) // Slot 2: capacity=1, bookedCount=1, status=OPEN // 1/1 人 in red badge (100% booked ≥ 80%) // Slot 3: capacity=5, bookedCount=4, status=OPEN // 4/5 人 in orange badge (80% booked ≥ 80%) // Slot 4: capacity=5, bookedCount=3, status=OPEN // 3/5 人 in green badge (60% booked < 80%) ``` --- ## 🔗 API Contract Summary ### GET /time-slot/available **Request:** ``` GET /api/time-slot/available?date=2026-04-05 Authorization: Bearer ``` **Response (200 OK):** ```json { "success": true, "data": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "date": "2026-04-05", "startTime": "09:00", "endTime": "10:00", "capacity": 1, "bookedCount": 0, "status": "OPEN", "source": "MANUAL", "templateId": null, "createdAt": "2026-04-01T10:00:00Z", "updatedAt": "2026-04-05T09:00:00Z", "isBookedByMe": false, "myBookingId": null } ], "message": null } ``` **Error (400):** ```json { "success": false, "data": null, "message": "Invalid date format" } ``` ### POST /booking **Request:** ```json POST /api/booking { "timeSlotId": "550e8400-e29b-41d4-a716-446655440000", "membershipId": "220e8400-e29b-41d4-a716-446655440111" } ``` **Response (201):** ```json { "success": true, "data": { "id": "550e8400-e29b-41d4-a716-446655440222", "userId": "user-123", "timeSlotId": "550e8400-e29b-41d4-a716-446655440000", "membershipId": "220e8400-e29b-41d4-a716-446655440111", "status": "CONFIRMED", "bookedAt": "2026-04-05T10:30:00Z", "courseDate": "2026-04-05", "courseTime": "09:00", "instructorName": "instructor name", "isCompleted": false }, "message": null } ``` ### PUT /booking/:id/cancel **Request:** ``` PUT /api/booking/550e8400-e29b-41d4-a716-446655440222/cancel ``` **Response (200):** ```json { "success": true, "data": { "id": "550e8400-e29b-41d4-a716-446655440222", "status": "CANCELLED", "cancelledAt": "2026-04-05T10:35:00Z" }, "message": null } ``` --- ## 🎯 Next Steps for Debugging 1. **Verify API Endpoint** - Open DevTools → Network - Check `/time-slot/available?date=...` request - Confirm response has `"success": true` - Confirm data array is not empty 2. **Check Store State** - Add console.logs to bookingStore.fetchSlots() - Verify slots are set correctly - Check loadingSlots toggle 3. **Verify Computed Properties** - Log filteredSlots in component - Check if filtering logic works - Verify slot.startTime format 4. **Test User Interaction** - Click date item → verify onDateSelect fires - Click period tab → verify onPeriodChange fires - Click book button → verify onBookTap fires - Check modals appear 5. **Check Mobile-Specific Issues** - Test in WeChat DevTools - Check rpx calculations - Verify touch events work