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:
44
packages/app/src/utils/auth.ts
Normal file
44
packages/app/src/utils/auth.ts
Normal 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')
|
||||
}
|
||||
46
packages/app/src/utils/format.ts
Normal file
46
packages/app/src/utils/format.ts
Normal 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
|
||||
}
|
||||
69
packages/app/src/utils/request.ts
Normal file
69
packages/app/src/utils/request.ts
Normal 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 })
|
||||
}
|
||||
Reference in New Issue
Block a user