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,44 @@
import { post } from './request'
import type { UserProfileResponse } from '@mp-pilates/shared'
interface LoginResponse {
readonly token: string
readonly user: UserProfileResponse
}
export async function wxLogin(): Promise<LoginResponse> {
return new Promise((resolve, reject) => {
uni.login({
provider: 'weixin',
success: async (loginRes) => {
try {
const result = await post<LoginResponse>('/auth/login', {
code: loginRes.code,
})
uni.setStorageSync('token', result.token)
resolve(result)
} catch (err) {
reject(err)
}
},
fail: (err) => {
reject(new Error(err.errMsg || '微信登录失败'))
},
})
})
}
export async function wxBindPhone(e: {
readonly detail: { readonly encryptedData: string; readonly iv: string }
}): Promise<UserProfileResponse> {
const { encryptedData, iv } = e.detail
return post<UserProfileResponse>('/auth/phone', { encryptedData, iv })
}
export function isLoggedIn(): boolean {
return !!uni.getStorageSync('token')
}
export function logout(): void {
uni.removeStorageSync('token')
}

View File

@@ -0,0 +1,46 @@
/** 格式化金额:分 → 元 */
export function formatPrice(cents: number): string {
return (cents / 100).toFixed(2)
}
/** 格式化日期为 YYYY-MM-DD */
export function formatDate(date: Date | string): string {
const d = typeof date === 'string' ? new Date(date) : date
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
/** 获取星期几中文 */
export function getWeekdayLabel(date: Date | string): string {
const d = typeof date === 'string' ? new Date(date) : date
const labels = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
return labels[d.getDay()]
}
/** 判断是否是今天 */
export function isToday(date: Date | string): boolean {
const d = typeof date === 'string' ? new Date(date) : date
const today = new Date()
return (
d.getFullYear() === today.getFullYear() &&
d.getMonth() === today.getMonth() &&
d.getDate() === today.getDate()
)
}
/** 生成未来 N 天的日期列表 */
export function getDateRange(days: number): ReadonlyArray<{ readonly date: string; readonly weekday: string; readonly isToday: boolean }> {
const result = []
const now = new Date()
for (let i = 0; i < days; i++) {
const d = new Date(now.getTime() + i * 86400000)
result.push({
date: formatDate(d),
weekday: getWeekdayLabel(d),
isToday: i === 0,
})
}
return result
}

View File

@@ -0,0 +1,69 @@
import type { ApiResponse, PaginatedData } from '@mp-pilates/shared'
const BASE_URL = 'http://localhost:3000/api'
interface RequestOptions {
readonly url: string
readonly method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
readonly data?: Record<string, unknown>
readonly header?: Record<string, string>
}
export function request<T>(options: RequestOptions): Promise<T> {
return new Promise((resolve, reject) => {
const token = uni.getStorageSync('token') as string
uni.request({
url: `${BASE_URL}${options.url}`,
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<unknown>
reject(new Error(body?.message || `请求失败 (${res.statusCode})`))
return
}
const body = res.data as ApiResponse<T>
if (body.success) {
resolve(body.data as T)
} else {
reject(new Error(body.message || '请求失败'))
}
},
fail: (err) => {
reject(new Error(err.errMsg || '网络请求失败'))
},
})
})
}
export function get<T>(url: string, data?: Record<string, unknown>): Promise<T> {
return request<T>({ url, method: 'GET', data })
}
export function post<T>(url: string, data?: Record<string, unknown>): Promise<T> {
return request<T>({ url, method: 'POST', data })
}
export function put<T>(url: string, data?: Record<string, unknown>): Promise<T> {
return request<T>({ url, method: 'PUT', data })
}
export function del<T>(url: string, data?: Record<string, unknown>): Promise<T> {
return request<T>({ url, method: 'DELETE', data })
}
export function getPaginated<T>(url: string, params?: Record<string, unknown>): Promise<PaginatedData<T>> {
return request<PaginatedData<T>>({ url, method: 'GET', data: params })
}