diff --git a/miniprogram/src/components/ice/StatePanel.vue b/miniprogram/src/components/ice/StatePanel.vue
new file mode 100644
index 0000000..45c517e
--- /dev/null
+++ b/miniprogram/src/components/ice/StatePanel.vue
@@ -0,0 +1,210 @@
+
+
+
+ {{ markText }}
+
+
+
+ {{ title }}
+ {{ description }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/miniprogram/src/modules/ice/api/booking.js b/miniprogram/src/modules/ice/api/booking.js
new file mode 100644
index 0000000..6f6a3d3
--- /dev/null
+++ b/miniprogram/src/modules/ice/api/booking.js
@@ -0,0 +1,63 @@
+import { request } from './request'
+
+const toNumber = (value) => {
+ const numberValue = Number(value)
+ return Number.isFinite(numberValue) ? numberValue : 0
+}
+
+const toBookingCourse = (backendCourse = {}) => ({
+ id: backendCourse.id ?? null,
+ title: backendCourse.title || '',
+ start_time: backendCourse.start_time || '',
+ end_time: backendCourse.end_time || '',
+ coach_name: backendCourse.coach_name || '',
+ capacity: toNumber(backendCourse.capacity),
+ booked_count: toNumber(backendCourse.booked_count)
+})
+
+const toBookingCard = (backendCard) => {
+ if (!backendCard) return null
+ return {
+ id: backendCard.id ?? null,
+ card_name: backendCard.card_name || '',
+ remained_detail: backendCard.remained_detail || ''
+ }
+}
+
+const toBookingViewModel = (backendBooking = {}) => ({
+ id: backendBooking.id,
+ status: backendBooking.status || 'booked',
+ checked_in_at: backendBooking.checked_in_at || null,
+ cancelled_at: backendBooking.cancelled_at || null,
+ created_at: backendBooking.created_at || '',
+ course: toBookingCourse(backendBooking.course),
+ card: toBookingCard(backendBooking.card)
+})
+
+const toPaginatedBookings = (backendData = {}) => ({
+ items: Array.isArray(backendData.items) ? backendData.items.map(toBookingViewModel) : [],
+ total: toNumber(backendData.total),
+ page: toNumber(backendData.page) || 1,
+ page_size: toNumber(backendData.page_size) || 20
+})
+
+const toQuery = (params = {}) => ({
+ status: params.status || 'all',
+ page: params.page || 1,
+ page_size: params.pageSize || 20
+})
+
+export const bookingApi = {
+ getBookings(params = {}) {
+ return request({
+ url: '/api/bookings',
+ data: toQuery(params)
+ }).then(toPaginatedBookings)
+ },
+
+ getBookingDetail(id) {
+ return request({
+ url: `/api/bookings/${id}`
+ }).then(toBookingViewModel)
+ }
+}
diff --git a/miniprogram/src/modules/ice/api/card.js b/miniprogram/src/modules/ice/api/card.js
new file mode 100644
index 0000000..ed02889
--- /dev/null
+++ b/miniprogram/src/modules/ice/api/card.js
@@ -0,0 +1,61 @@
+import { request } from './request'
+
+const toNumber = (value) => {
+ const numberValue = Number(value)
+ return Number.isFinite(numberValue) ? numberValue : 0
+}
+
+const toMemberCard = (backendCard = {}) => ({
+ id: backendCard.id,
+ card_no: backendCard.card_no || '',
+ card_name: backendCard.card_name || '',
+ card_type: backendCard.card_type || 'times',
+ backend_card_type: backendCard.backend_card_type || '',
+ status: backendCard.status || 'active',
+ total_times: toNumber(backendCard.total_times),
+ remained_times: toNumber(backendCard.remained_times),
+ original_balance: backendCard.original_balance || '0.00',
+ gift_balance: backendCard.gift_balance || '0.00',
+ balance: backendCard.balance || '0.00',
+ start_date: backendCard.start_date || '',
+ expire_date: backendCard.expire_date || '',
+ remained_detail: backendCard.remained_detail || ''
+})
+
+const toCardChange = (backendChange = {}) => ({
+ id: backendChange.id,
+ change_type: backendChange.change_type || 'adjustment',
+ backend_change_type: backendChange.backend_change_type || '',
+ delta: backendChange.delta || '0',
+ post_times: toNumber(backendChange.post_times),
+ post_balance: backendChange.post_balance || '0.00',
+ memo: backendChange.memo || '',
+ created_at: backendChange.created_at || ''
+})
+
+const toCardHistory = (backendData = {}) => ({
+ card: toMemberCard(backendData.card),
+ items: Array.isArray(backendData.items) ? backendData.items.map(toCardChange) : [],
+ total: toNumber(backendData.total),
+ page: toNumber(backendData.page) || 1,
+ page_size: toNumber(backendData.page_size) || 20
+})
+
+export const cardApi = {
+ getCards(status = 'all') {
+ return request({
+ url: '/api/members/me/cards',
+ data: { status }
+ }).then((data = {}) => (Array.isArray(data.items) ? data.items.map(toMemberCard) : []))
+ },
+
+ getCardChanges(cardId, params = {}) {
+ return request({
+ url: `/api/members/me/cards/${cardId}/changes`,
+ data: {
+ page: params.page || 1,
+ page_size: params.pageSize || 20
+ }
+ }).then(toCardHistory)
+ }
+}
diff --git a/miniprogram/src/modules/ice/api/content.js b/miniprogram/src/modules/ice/api/content.js
new file mode 100644
index 0000000..ff8fc7a
--- /dev/null
+++ b/miniprogram/src/modules/ice/api/content.js
@@ -0,0 +1,17 @@
+import { request } from './request'
+
+const toActivityDetail = (backendActivity = {}) => ({
+ id: backendActivity.id,
+ title: backendActivity.title || '',
+ cover: backendActivity.cover || '',
+ created_at: backendActivity.created_at || '',
+ content_html: backendActivity.content_html || ''
+})
+
+export const contentApi = {
+ getActivityDetail(id) {
+ return request({
+ url: `/api/contents/activities/${id}`
+ }).then(toActivityDetail)
+ }
+}
diff --git a/miniprogram/src/modules/ice/api/course.js b/miniprogram/src/modules/ice/api/course.js
new file mode 100644
index 0000000..45238ef
--- /dev/null
+++ b/miniprogram/src/modules/ice/api/course.js
@@ -0,0 +1,68 @@
+import { request } from './request'
+
+const toNumber = (value) => {
+ const numberValue = Number(value)
+ return Number.isFinite(numberValue) ? numberValue : 0
+}
+
+const toCoach = (backendCoach = {}) => ({
+ id: backendCoach.id ?? null,
+ name: backendCoach.name || ''
+})
+
+export const toCourseViewModel = (backendCourse = {}) => {
+ const coach = toCoach(backendCourse.coach)
+ return {
+ id: backendCourse.id,
+ title: backendCourse.title || '',
+ start_time: backendCourse.start_time || '',
+ end_time: backendCourse.end_time || '',
+ coach,
+ coach_name: backendCourse.coach_name || coach.name,
+ capacity: toNumber(backendCourse.capacity),
+ booked_count: toNumber(backendCourse.booked_count),
+ available_count: toNumber(backendCourse.available_count),
+ is_booked: backendCourse.is_booked === true,
+ my_booking_id: backendCourse.my_booking_id ?? null,
+ cover: backendCourse.cover || '',
+ detail_html: backendCourse.detail_html || '',
+ }
+}
+
+const toPaginatedCourses = (backendData = {}) => ({
+ items: Array.isArray(backendData.items) ? backendData.items.map(toCourseViewModel) : [],
+ total: toNumber(backendData.total),
+ page: toNumber(backendData.page) || 1,
+ page_size: toNumber(backendData.page_size) || 20
+})
+
+const toQuery = (params = {}) => {
+ const data = {}
+ if (params.date) data.date = params.date
+ if (params.coachId) data.coach_id = params.coachId
+ if (params.page) data.page = params.page
+ if (params.pageSize) data.page_size = params.pageSize
+ return data
+}
+
+export const courseApi = {
+ getCoaches(status = 'active') {
+ return request({
+ url: '/api/coaches',
+ data: { status }
+ }).then((data = {}) => (Array.isArray(data.items) ? data.items.map(toCoach) : []))
+ },
+
+ getCourses(params = {}) {
+ return request({
+ url: '/api/courses',
+ data: toQuery(params)
+ }).then(toPaginatedCourses)
+ },
+
+ getCourseDetail(id) {
+ return request({
+ url: `/api/courses/${id}`
+ }).then(toCourseViewModel)
+ }
+}
diff --git a/miniprogram/src/modules/ice/api/home.js b/miniprogram/src/modules/ice/api/home.js
new file mode 100644
index 0000000..84b4f8b
--- /dev/null
+++ b/miniprogram/src/modules/ice/api/home.js
@@ -0,0 +1,52 @@
+import { request } from './request'
+import { toCourseViewModel } from './course'
+
+const toNumber = (value) => {
+ const numberValue = Number(value)
+ return Number.isFinite(numberValue) ? numberValue : 0
+}
+
+const toBanner = (backendBanner = {}) => ({
+ id: backendBanner.id,
+ title: backendBanner.title || '',
+ image: backendBanner.image || '',
+ link_type: backendBanner.link_type || 'none',
+ link_value: backendBanner.link_value || '',
+ sort_order: toNumber(backendBanner.sort_order)
+})
+
+const toActivitySummary = (backendActivity = {}) => ({
+ id: backendActivity.id,
+ title: backendActivity.title || '',
+ cover: backendActivity.cover || '',
+ created_at: backendActivity.created_at || ''
+})
+
+const toHomeViewModel = (backendData = {}) => ({
+ banners: Array.isArray(backendData.banners) ? backendData.banners.map(toBanner) : [],
+ upcomingCourses: Array.isArray(backendData.upcoming_courses)
+ ? backendData.upcoming_courses.map(toCourseViewModel)
+ : [],
+ activities: Array.isArray(backendData.activities) ? backendData.activities.map(toActivitySummary) : [],
+ venue: {
+ name: backendData.venue?.name || '奥林匹克滑冰中心',
+ }
+})
+
+export const homeApi = {
+ getHome(options = {}) {
+ const data = {
+ course_limit: options.courseLimit || 2,
+ banner_limit: options.bannerLimit || 5
+ }
+ if (options.activityLimit !== undefined) {
+ data.activity_limit = options.activityLimit
+ }
+ if (options.courseDate) data.course_date = options.courseDate
+
+ return request({
+ url: '/api/contents/home',
+ data
+ }).then(toHomeViewModel)
+ }
+}
diff --git a/miniprogram/src/modules/ice/api/member.js b/miniprogram/src/modules/ice/api/member.js
new file mode 100644
index 0000000..b73d7cb
--- /dev/null
+++ b/miniprogram/src/modules/ice/api/member.js
@@ -0,0 +1,151 @@
+import { authStorage, request } from './request'
+
+// 防腐层:小程序端只在本文件感知后端字段名,页面层统一使用下方 mapper 输出的前端模型。
+export const memberBackendContract = Object.freeze({
+ wechatLoginResponse: [
+ 'access_token',
+ 'token_type',
+ 'expires_in',
+ 'member_bound',
+ 'member'
+ ],
+ registerOrBindRequest: [
+ 'phone_code',
+ 'phone',
+ 'name',
+ 'gender',
+ 'birthday'
+ ],
+ registerOrBindResponse: [
+ 'created',
+ 'bound',
+ 'member'
+ ],
+ memberSummary: [
+ 'id',
+ 'name',
+ 'phone',
+ 'gender',
+ 'birthday',
+ 'joined_at'
+ ],
+ memberProfile: [
+ 'id',
+ 'name',
+ 'phone',
+ 'gender',
+ 'gender_label',
+ 'birthday',
+ 'joined_at',
+ 'sales.id',
+ 'sales.name',
+ 'card_summary.total',
+ 'card_summary.active',
+ 'card_summary.disabled',
+ 'card_summary.expired'
+ ]
+})
+
+const GENDER_LABELS = Object.freeze({
+ male: '男',
+ female: '女',
+ unknown: '未设置'
+})
+
+const emptyCardSummary = () => ({
+ total: 0,
+ active: 0,
+ disabled: 0,
+ expired: 0
+})
+
+const toNumber = (value) => {
+ const numberValue = Number(value)
+ return Number.isFinite(numberValue) ? numberValue : 0
+}
+
+const toMemberCardSummary = (backendCardSummary = {}) => ({
+ total: toNumber(backendCardSummary.total),
+ active: toNumber(backendCardSummary.active),
+ disabled: toNumber(backendCardSummary.disabled),
+ expired: toNumber(backendCardSummary.expired)
+})
+
+const toMemberViewModel = (backendMember, options = {}) => {
+ if (!backendMember || !backendMember.id) return null
+
+ const cardSummary = options.withProfileFields
+ ? toMemberCardSummary(backendMember.card_summary)
+ : emptyCardSummary()
+
+ return {
+ id: backendMember.id,
+ name: backendMember.name || '',
+ phone: backendMember.phone || '',
+ gender: backendMember.gender || 'unknown',
+ genderLabel: backendMember.gender_label || GENDER_LABELS[backendMember.gender] || GENDER_LABELS.unknown,
+ birthday: backendMember.birthday || '',
+ joinDate: backendMember.joined_at || '',
+ salesId: backendMember.sales?.id || null,
+ salesName: backendMember.sales?.name || '无',
+ cardSummary,
+ cardCount: cardSummary.total
+ }
+}
+
+const toWechatLoginViewModel = (backendData = {}) => ({
+ accessToken: backendData.access_token || '',
+ tokenType: backendData.token_type || 'Bearer',
+ expiresIn: toNumber(backendData.expires_in),
+ memberBound: backendData.member_bound === true,
+ member: toMemberViewModel(backendData.member)
+})
+
+const toRegisterOrBindPayload = (formData = {}) => ({
+ phone_code: formData.phoneCode || null,
+ phone: formData.phone || null,
+ name: formData.name || null,
+ gender: formData.gender || 'unknown',
+ birthday: formData.birthday || null
+})
+
+const toRegisterOrBindViewModel = (backendData = {}) => ({
+ created: backendData.created === true,
+ bound: backendData.bound === true,
+ member: toMemberViewModel(backendData.member)
+})
+
+const toMemberProfileViewModel = (backendData = {}) => {
+ return toMemberViewModel(backendData, { withProfileFields: true })
+}
+
+// 登录注册与绑定 API 接口组
+export const api = {
+ // 1. 微信小程序登录 (用 code 换 token)
+ wechatLogin(code) {
+ return request({
+ url: '/api/auth/wechat-login',
+ method: 'POST',
+ data: { code }
+ }).then(toWechatLoginViewModel)
+ },
+
+ // 2. 绑定或注册会员
+ registerOrBind(data) {
+ return request({
+ url: '/api/auth/register-or-bind',
+ method: 'POST',
+ data: toRegisterOrBindPayload(data)
+ }).then(toRegisterOrBindViewModel)
+ },
+
+ // 3. 获取当前登录会员信息
+ getMemberMe() {
+ return request({
+ url: '/api/members/me',
+ method: 'GET'
+ }).then(toMemberProfileViewModel)
+ }
+}
+
+export { authStorage, request }
diff --git a/miniprogram/src/modules/ice/api/request.js b/miniprogram/src/modules/ice/api/request.js
new file mode 100644
index 0000000..a79c13d
--- /dev/null
+++ b/miniprogram/src/modules/ice/api/request.js
@@ -0,0 +1,134 @@
+const KEY_TOKEN = 'ICE_MEMBER_TOKEN'
+const KEY_TOKEN_EXPIRES_AT = 'ICE_MEMBER_TOKEN_EXPIRES_AT'
+
+const API_BASE_URL = import.meta.env.VITE_ICE_API_BASE_URL || 'http://127.0.0.1:8000'
+
+export const authStorage = {
+ setToken(token, expiresIn) {
+ uni.setStorageSync(KEY_TOKEN, token)
+ if (expiresIn) {
+ uni.setStorageSync(KEY_TOKEN_EXPIRES_AT, Date.now() + Number(expiresIn) * 1000)
+ }
+ },
+ getToken() {
+ return uni.getStorageSync(KEY_TOKEN)
+ },
+ isTokenExpired() {
+ const expiresAt = Number(uni.getStorageSync(KEY_TOKEN_EXPIRES_AT) || 0)
+ return expiresAt > 0 && Date.now() >= expiresAt
+ },
+ clear() {
+ uni.removeStorageSync(KEY_TOKEN)
+ uni.removeStorageSync(KEY_TOKEN_EXPIRES_AT)
+ }
+}
+
+export const normalizeApiError = (rawError = {}) => {
+ const statusCode = rawError.statusCode
+ const body = rawError.data || rawError
+ const code = body?.code
+ const backendMessage = body?.message || ''
+
+ if (statusCode === 401 || code === 40001) {
+ return {
+ type: 'auth_expired',
+ code,
+ statusCode,
+ message: '登录状态已过期,请重新登录',
+ raw: rawError
+ }
+ }
+
+ if (code === 40002) {
+ return {
+ type: 'profile_required',
+ code,
+ statusCode,
+ message: '请先完善会员资料',
+ raw: rawError
+ }
+ }
+
+ if (code === 40003) {
+ return {
+ type: 'bind_conflict',
+ code,
+ statusCode,
+ message: backendMessage || '该手机号已绑定其他微信账号,请联系场馆前台处理',
+ raw: rawError
+ }
+ }
+
+ if (!statusCode) {
+ return {
+ type: 'network_error',
+ code,
+ statusCode,
+ message: '网络连接失败,请检查后重试',
+ raw: rawError
+ }
+ }
+
+ if (statusCode === 404 || code === 40400) {
+ return {
+ type: 'not_found',
+ code,
+ statusCode,
+ message: backendMessage || '未找到对应信息或无访问权限',
+ raw: rawError
+ }
+ }
+
+ return {
+ type: 'api_error',
+ code,
+ statusCode,
+ message: backendMessage || '服务暂时不可用,请稍后重试',
+ raw: rawError
+ }
+}
+
+export function request(options = {}) {
+ const token = authStorage.getToken()
+ const header = {
+ 'Content-Type': 'application/json',
+ ...(options.header || {})
+ }
+
+ if (token) {
+ header.Authorization = `Bearer ${token}`
+ }
+
+ return new Promise((resolve, reject) => {
+ uni.request({
+ url: options.url.startsWith('http') ? options.url : `${API_BASE_URL}${options.url}`,
+ method: options.method || 'GET',
+ data: options.data,
+ header,
+ success: (res) => {
+ const body = res.data
+
+ if (res.statusCode >= 200 && res.statusCode < 300) {
+ if (body && body.code === 0) {
+ resolve(body.data)
+ return
+ }
+
+ reject(normalizeApiError({
+ statusCode: res.statusCode,
+ data: body
+ }))
+ return
+ }
+
+ reject(normalizeApiError({
+ statusCode: res.statusCode,
+ data: body
+ }))
+ },
+ fail: (err) => {
+ reject(normalizeApiError(err))
+ }
+ })
+ })
+}
diff --git a/miniprogram/src/modules/ice/api/session.js b/miniprogram/src/modules/ice/api/session.js
new file mode 100644
index 0000000..6dbc9d9
--- /dev/null
+++ b/miniprogram/src/modules/ice/api/session.js
@@ -0,0 +1,108 @@
+import { api, authStorage } from './member'
+
+export const SESSION_STATUS = Object.freeze({
+ CHECKING: 'checking',
+ GUEST: 'guest',
+ EXPIRED: 'expired',
+ PROFILE_REQUIRED: 'profile_required',
+ MEMBER: 'member',
+ NETWORK_ERROR: 'network_error'
+})
+
+const createLoginCode = () => new Promise((resolve, reject) => {
+ uni.login({
+ provider: 'weixin',
+ success: (loginRes) => {
+ if (loginRes.code) {
+ resolve(loginRes.code)
+ return
+ }
+ reject({
+ type: 'login_error',
+ message: '微信登录没有返回有效凭证,请重新尝试'
+ })
+ },
+ fail: () => {
+ reject({
+ type: 'login_error',
+ message: '微信登录失败,请稍后重试'
+ })
+ }
+ })
+})
+
+export const loginAndStoreToken = async () => {
+ const code = await createLoginCode()
+ const data = await api.wechatLogin(code)
+ if (!data?.accessToken) {
+ throw {
+ type: 'login_error',
+ message: '登录失败,请稍后重试'
+ }
+ }
+ authStorage.setToken(data.accessToken, data.expiresIn)
+ return data
+}
+
+export const getMemberSession = async () => {
+ const token = authStorage.getToken()
+ if (!token) {
+ return { status: SESSION_STATUS.GUEST }
+ }
+
+ if (authStorage.isTokenExpired()) {
+ authStorage.clear()
+ return {
+ status: SESSION_STATUS.EXPIRED,
+ message: '登录状态已过期,请重新登录'
+ }
+ }
+
+ try {
+ const member = await api.getMemberMe()
+ return {
+ status: SESSION_STATUS.MEMBER,
+ member
+ }
+ } catch (err) {
+ if (err?.type === 'profile_required' || err?.code === 40002) {
+ return {
+ status: SESSION_STATUS.PROFILE_REQUIRED,
+ message: '请先完善会员资料'
+ }
+ }
+
+ if (err?.type === 'auth_expired' || err?.code === 40001) {
+ authStorage.clear()
+ return {
+ status: SESSION_STATUS.EXPIRED,
+ message: '登录状态已过期,请重新登录'
+ }
+ }
+
+ return {
+ status: SESSION_STATUS.NETWORK_ERROR,
+ message: err?.message || '暂时无法连接服务,请稍后重试',
+ error: err
+ }
+ }
+}
+
+export const isSessionBlockingError = (err) => {
+ return ['auth_expired', 'profile_required'].includes(err?.type) || [40001, 40002].includes(err?.code)
+}
+
+export const sessionStatusFromError = (err) => {
+ if (err?.type === 'auth_expired' || err?.code === 40001) {
+ authStorage.clear()
+ return SESSION_STATUS.EXPIRED
+ }
+ if (err?.type === 'profile_required' || err?.code === 40002) {
+ return SESSION_STATUS.PROFILE_REQUIRED
+ }
+ return ''
+}
+
+export const errorMessage = (err, fallback = '操作失败,请稍后重试') => {
+ return err?.message || fallback
+}
diff --git a/miniprogram/src/pages.json b/miniprogram/src/pages.json
index 80a18e6..12c4bd6 100644
--- a/miniprogram/src/pages.json
+++ b/miniprogram/src/pages.json
@@ -136,6 +136,71 @@
"style": {
"navigationBarTitleText": "个人信息"
}
+ },
+ {
+ "path": "pages/ice/home/index",
+ "style": {
+ "navigationBarTitleText": "首页",
+ "enablePullDownRefresh": true
+ }
+ },
+ {
+ "path": "pages/ice/courses/index",
+ "style": {
+ "navigationBarTitleText": "近期课程",
+ "enablePullDownRefresh": true
+ }
+ },
+ {
+ "path": "pages/ice/bookings/index",
+ "style": {
+ "navigationBarTitleText": "我的预约",
+ "enablePullDownRefresh": true
+ }
+ },
+ {
+ "path": "pages/ice/profile/index",
+ "style": {
+ "navigationBarTitleText": "会员中心"
+ }
+ },
+ {
+ "path": "pages/ice/bookings/detail",
+ "style": {
+ "navigationBarTitleText": "预约详情"
+ }
+ },
+ {
+ "path": "pages/ice/courses/detail",
+ "style": {
+ "navigationBarTitleText": "课程详情"
+ }
+ },
+ {
+ "path": "pages/ice/profile/cards",
+ "style": {
+ "navigationBarTitleText": "我的会员卡",
+ "enablePullDownRefresh": true
+ }
+ },
+ {
+ "path": "pages/ice/profile/card-history",
+ "style": {
+ "navigationBarTitleText": "会员卡使用流水",
+ "enablePullDownRefresh": true
+ }
+ },
+ {
+ "path": "pages/ice/profile/detail",
+ "style": {
+ "navigationBarTitleText": "个人信息"
+ }
+ },
+ {
+ "path": "pages/ice/activities/detail",
+ "style": {
+ "navigationBarTitleText": "活动详情"
+ }
}
],
"globalStyle": {
diff --git a/miniprogram/src/pages/ice/activities/detail.vue b/miniprogram/src/pages/ice/activities/detail.vue
new file mode 100644
index 0000000..130cb2e
--- /dev/null
+++ b/miniprogram/src/pages/ice/activities/detail.vue
@@ -0,0 +1,157 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/miniprogram/src/pages/ice/bookings/detail.vue b/miniprogram/src/pages/ice/bookings/detail.vue
new file mode 100644
index 0000000..807198f
--- /dev/null
+++ b/miniprogram/src/pages/ice/bookings/detail.vue
@@ -0,0 +1,511 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatStatus(booking.status) }}
+
+
+
+
+
+ 关联课程
+
+ {{ booking.course?.title }}
+ →
+
+
+
+
+
+
+
+ 上课时段
+ 时间:{{ formatDateTimeRange(booking.course?.start_time, booking.course?.end_time) }}
+
+
+ 预约人数
+ 已约 {{ booking.course?.booked_count }}/{{ booking.course?.capacity }} 人
+
+
+
+
+
+
+
+ 使用卡项
+
+ {{ booking.card.card_name }}
+ →
+
+
+
+
+
+
+ 时间节点
+
+
+
+
+ 预约时间
+ {{ formatFullDateTime(booking.created_at) }}
+
+
+
+
+
+ 核销时间
+ {{ formatFullDateTime(booking.checked_in_at) }}
+
+
+
+
+
+ 取消时间
+ {{ formatFullDateTime(booking.cancelled_at) }}
+
+
+
+
+
+
+
+ 行程变动须知
+ 所有的上课行程均由场馆统一排定展示。如需取消或改约,请提前联系场馆前台处理。
+
+
+
+
+
+
+
+
diff --git a/miniprogram/src/pages/ice/bookings/index.vue b/miniprogram/src/pages/ice/bookings/index.vue
new file mode 100644
index 0000000..8fd1a6e
--- /dev/null
+++ b/miniprogram/src/pages/ice/bookings/index.vue
@@ -0,0 +1,544 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ booking.course.title }}
+ {{ formatStatus(booking.status) }}
+
+
+
+ 时间:{{ formatDateTimeRange(booking.course.start_time, booking.course.end_time) }}
+ • 人数:已约 {{ booking.course.booked_count }}/{{ booking.course.capacity }}人
+
+
+
+
+
+
+ 加载中...
+
+
+ — 已加载全部 —
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/miniprogram/src/pages/ice/courses/detail.vue b/miniprogram/src/pages/ice/courses/detail.vue
new file mode 100644
index 0000000..9674a42
--- /dev/null
+++ b/miniprogram/src/pages/ice/courses/detail.vue
@@ -0,0 +1,258 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ course.title }}
+ 我已约此课
+
+
+
+
+
+
+ 上课时段
+ 时间:{{ formatDateTimeRange(course.start_time, course.end_time) }}
+
+
+ 执教教练
+ {{ course.coach?.name }}
+
+
+ 预约人数
+ 已约 {{ course.booked_count }}/{{ course.capacity }} 人
+
+
+
+
+
+
+ 课程详情
+
+
+
+
+
+
+
+
+
+
diff --git a/miniprogram/src/pages/ice/courses/index.vue b/miniprogram/src/pages/ice/courses/index.vue
new file mode 100644
index 0000000..8f1b757
--- /dev/null
+++ b/miniprogram/src/pages/ice/courses/index.vue
@@ -0,0 +1,419 @@
+
+
+
+
+
+
+
+ 日期:
+ {{ selectedDate ? formatDateDisplay(selectedDate) : '选择日期' }}
+ ▼
+
+
+
+
+
+
+
+
+ 教练:
+ {{ selectedCoachName }}
+ ▼
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ course.title }}
+ 已预约
+
+
+
+ 时间:{{ formatTimeRange(course.start_time, course.end_time) }}
+
+ •教练:{{ course.coach.name }}
+ •人数:已约 {{ course.booked_count }}/{{ course.capacity }}人
+
+
+
+
+
+
+
+
+ 加载中...
+
+
+ — 已加载全部 —
+
+
+
+
+
+
+
+
+
+
+
diff --git a/miniprogram/src/pages/ice/home/index.vue b/miniprogram/src/pages/ice/home/index.vue
new file mode 100644
index 0000000..eb56f28
--- /dev/null
+++ b/miniprogram/src/pages/ice/home/index.vue
@@ -0,0 +1,474 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ banner.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ course.title }}
+ 时间:{{ formatCourseDate(course.start_time, course.end_time) }}
+
+ • 教练:{{ course.coach_name }}
+ • 人数:已约 {{ course.booked_count }}/{{ course.capacity }}人
+
+
+
+
+ 今日暂无课程排期
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ activity.title }}
+ {{ formatDate(activity.created_at) }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/miniprogram/src/pages/ice/profile/card-history.vue b/miniprogram/src/pages/ice/profile/card-history.vue
new file mode 100644
index 0000000..d5b082a
--- /dev/null
+++ b/miniprogram/src/pages/ice/profile/card-history.vue
@@ -0,0 +1,516 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ card.card_name }}
+ 正常使用中
+
+
+ 卡号: {{ card.card_no }}
+ 截止有效期: {{ formatDate(card.expire_date) }}
+
+
+
+
+
+
+ 当前剩余
+ {{ card.remained_times }} 次
+
+
+ 当前余额
+ ¥ {{ card.balance }}
+
+
+ 卡项类型
+ 无限通滑年卡
+
+
+
+
+
+ 累计变动
+ {{ historyList.length }} 次变动记录
+
+
+
+
+
+
+ 流水明细日志
+
+
+
+
+
+
+
+
+ {{ formatChangeType(log.change_type) }}
+ {{ log.memo }}
+ {{ formatFullDateTime(log.created_at) }}
+
+
+
+
+
+
+ {{ Number(log.delta) > 0 ? '+' : '' }}{{ log.delta }}{{ card.card_type === 'times' ? '次' : '元' }}
+
+
+ 余 {{ log.post_times }} 次
+ 余 ¥{{ log.post_balance }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/miniprogram/src/pages/ice/profile/cards.vue b/miniprogram/src/pages/ice/profile/cards.vue
new file mode 100644
index 0000000..fc99cb6
--- /dev/null
+++ b/miniprogram/src/pages/ice/profile/cards.vue
@@ -0,0 +1,430 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ card.card_no }}
+
+
+ {{ formatCardType(card.card_type) }}
+
+
+
+
+ {{ card.card_name }}
+
+
+
+
+
+ 剩余次数
+ {{ card.remained_times }} 次
+
+
+
+ 剩余额度
+ ¥ {{ card.balance }}
+
+
+
+ 专属权益
+ 不限次月卡
+
+
+
+ {{ card.start_date }} 至 {{ card.expire_date }}
+
+
+
+
+
+
+
+ 总次数: {{ card.total_times }}次
+ 含赠送: ¥{{ card.gift_balance }}
+ 有效期内无限通滑
+
+
+
+ 使用流水
+ →
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/miniprogram/src/pages/ice/profile/detail.vue b/miniprogram/src/pages/ice/profile/detail.vue
new file mode 100644
index 0000000..c15241e
--- /dev/null
+++ b/miniprogram/src/pages/ice/profile/detail.vue
@@ -0,0 +1,271 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 基本资料
+
+ 姓名
+ {{ member.name || '-' }}
+
+
+ 手机号
+ {{ member.phone || '-' }}
+
+
+ 性别
+ {{ formatGender(member.gender) }}
+
+
+ 生日
+ {{ member.birthday || '-' }}
+
+
+
+
+
+ 会员档案
+
+ 入会日期
+ {{ formatDate(member.joinDate) }}
+
+
+
+
+
+
+
+
+
diff --git a/miniprogram/src/pages/ice/profile/index.vue b/miniprogram/src/pages/ice/profile/index.vue
new file mode 100644
index 0000000..6d51470
--- /dev/null
+++ b/miniprogram/src/pages/ice/profile/index.vue
@@ -0,0 +1,1175 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 还差一步
+ 完成资料绑定后,就能查看你在场馆登记的会员卡、预约行程和使用流水。
+
+
+
+
+
+ 真实姓名
+
+ {{ formErrors.name }}
+
+
+
+
+ 手机号码
+
+
+
+
+ {{ formErrors.phone }}
+
+
+
+
+ 性别
+
+
+
+ {{ selectedGenderLabel }}
+
+ ▼
+
+
+
+
+
+
+ 生日
+
+
+
+ {{ bindForm.birthday || '请选择出生日期' }}
+
+ ▼
+
+
+
+
+
+
+ {{ bindError }}
+
+
+ 姓名和手机号仅用于匹配场馆会员档案。
+
+
+
+
+
+
+
+
+
+
+ {{ member.name }}
+
+ {{ member.phone }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/miniprogram/src/static/ice/tab/booking-active.png b/miniprogram/src/static/ice/tab/booking-active.png
new file mode 100644
index 0000000..eacef65
Binary files /dev/null and b/miniprogram/src/static/ice/tab/booking-active.png differ
diff --git a/miniprogram/src/static/ice/tab/booking.png b/miniprogram/src/static/ice/tab/booking.png
new file mode 100644
index 0000000..51593c6
Binary files /dev/null and b/miniprogram/src/static/ice/tab/booking.png differ
diff --git a/miniprogram/src/static/ice/tab/course-active.png b/miniprogram/src/static/ice/tab/course-active.png
new file mode 100644
index 0000000..1f57376
Binary files /dev/null and b/miniprogram/src/static/ice/tab/course-active.png differ
diff --git a/miniprogram/src/static/ice/tab/course.png b/miniprogram/src/static/ice/tab/course.png
new file mode 100644
index 0000000..21f7bda
Binary files /dev/null and b/miniprogram/src/static/ice/tab/course.png differ
diff --git a/miniprogram/src/static/ice/tab/home-active.png b/miniprogram/src/static/ice/tab/home-active.png
new file mode 100644
index 0000000..118a460
Binary files /dev/null and b/miniprogram/src/static/ice/tab/home-active.png differ
diff --git a/miniprogram/src/static/ice/tab/home.png b/miniprogram/src/static/ice/tab/home.png
new file mode 100644
index 0000000..711ddfd
Binary files /dev/null and b/miniprogram/src/static/ice/tab/home.png differ
diff --git a/miniprogram/src/static/ice/tab/mine-active.png b/miniprogram/src/static/ice/tab/mine-active.png
new file mode 100644
index 0000000..5a19691
Binary files /dev/null and b/miniprogram/src/static/ice/tab/mine-active.png differ
diff --git a/miniprogram/src/static/ice/tab/mine.png b/miniprogram/src/static/ice/tab/mine.png
new file mode 100644
index 0000000..4f3c125
Binary files /dev/null and b/miniprogram/src/static/ice/tab/mine.png differ