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:
71
packages/app/src/stores/booking.ts
Normal file
71
packages/app/src/stores/booking.ts
Normal 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,
|
||||
}
|
||||
})
|
||||
27
packages/app/src/stores/studio.ts
Normal file
27
packages/app/src/stores/studio.ts
Normal 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,
|
||||
}
|
||||
})
|
||||
105
packages/app/src/stores/user.ts
Normal file
105
packages/app/src/stores/user.ts
Normal 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,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user