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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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