feat(app): initialize uni-app with routing, stores, and infrastructure

- Vue 3 + TypeScript + Pinia + SCSS
- 3-tab navigation (home, booking, profile) + 11 sub-pages
- HTTP client with JWT auth, request interceptors
- Pinia stores: user (auth, profile, memberships), studio, booking
- Utility functions: price formatting, date helpers
- WeChat login helper
- All pages as stubs ready for implementation
This commit is contained in:
richarjiang
2026-04-02 12:51:28 +08:00
parent b9d55c9e9f
commit 554fc30954
36 changed files with 5438 additions and 53 deletions

View File

@@ -0,0 +1,71 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type {
TimeSlotWithBookingStatus,
BookingWithDetails,
CreateBookingDto,
} from '@mp-pilates/shared'
import { get, post, put } from '../utils/request'
export const useBookingStore = defineStore('booking', () => {
const slots = ref<readonly TimeSlotWithBookingStatus[]>([])
const myBookings = ref<readonly BookingWithDetails[]>([])
const upcomingBookings = ref<readonly BookingWithDetails[]>([])
const loadingSlots = ref(false)
const loadingBookings = ref(false)
async function fetchSlots(date: string) {
loadingSlots.value = true
try {
slots.value = await get<TimeSlotWithBookingStatus[]>('/time-slot/available', { date })
} catch (err) {
console.error('Fetch slots failed:', err)
slots.value = []
} finally {
loadingSlots.value = false
}
}
async function createBooking(dto: CreateBookingDto) {
const result = await post<BookingWithDetails>('/booking', dto as unknown as Record<string, unknown>)
return result
}
async function cancelBooking(bookingId: string) {
const result = await put<BookingWithDetails>(`/booking/${bookingId}/cancel`)
return result
}
async function fetchMyBookings(status?: string) {
loadingBookings.value = true
try {
const params = status ? { status } : {}
myBookings.value = await get<BookingWithDetails[]>('/booking/my', params)
} catch (err) {
console.error('Fetch bookings failed:', err)
} finally {
loadingBookings.value = false
}
}
async function fetchUpcomingBookings() {
try {
upcomingBookings.value = await get<BookingWithDetails[]>('/booking/my/upcoming')
} catch (err) {
console.error('Fetch upcoming bookings failed:', err)
}
}
return {
slots,
myBookings,
upcomingBookings,
loadingSlots,
loadingBookings,
fetchSlots,
createBooking,
cancelBooking,
fetchMyBookings,
fetchUpcomingBookings,
}
})

View File

@@ -0,0 +1,27 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { StudioConfig } from '@mp-pilates/shared'
import { get } from '../utils/request'
export const useStudioStore = defineStore('studio', () => {
const studioInfo = ref<StudioConfig | null>(null)
const loading = ref(false)
async function fetchStudioInfo() {
if (loading.value) return
loading.value = true
try {
studioInfo.value = await get<StudioConfig>('/studio/info')
} catch (err) {
console.error('Fetch studio info failed:', err)
} finally {
loading.value = false
}
}
return {
studioInfo,
loading,
fetchStudioInfo,
}
})

View File

@@ -0,0 +1,105 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type {
UserProfileResponse,
UserStatsResponse,
MembershipWithCardType,
} from '@mp-pilates/shared'
import { UserRole, MembershipStatus } from '@mp-pilates/shared'
import { wxLogin, isLoggedIn, logout as authLogout } from '../utils/auth'
import { get, put } from '../utils/request'
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
const loggedIn = computed(() => !!token.value && !!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)
// Actions
async function login() {
try {
const result = await wxLogin()
token.value = result.token
user.value = result.user
return result.user
} catch (err) {
console.error('Login failed:', err)
throw err
}
}
async function fetchProfile() {
if (!isLoggedIn()) return
try {
user.value = await get<UserProfileResponse>('/user/profile')
} 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
return updated
}
function checkAuth() {
if (isLoggedIn()) {
fetchProfile()
fetchMemberships()
}
}
function logout() {
authLogout()
token.value = ''
user.value = null
stats.value = null
memberships.value = []
}
return {
user,
stats,
memberships,
token,
loggedIn,
isAdmin,
activeMemberships,
hasValidMembership,
login,
fetchProfile,
fetchStats,
fetchMemberships,
updateProfile,
checkAuth,
logout,
}
})