整合api请求、添加购买会员卡页面、登陆页面
This commit is contained in:
+29
-27
@@ -4,29 +4,12 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app'
|
||||||
import GlobalLoading from '@/components/global/GlobalLoading.vue'
|
import GlobalLoading from '@/components/global/GlobalLoading.vue'
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
GlobalLoading
|
|
||||||
},
|
|
||||||
onLaunch: function() {
|
|
||||||
console.log('App Launch')
|
|
||||||
this.preloadTabData()
|
|
||||||
},
|
|
||||||
onShow: function() {
|
|
||||||
console.log('App Show')
|
|
||||||
// #ifdef APP-PLUS
|
|
||||||
this.hideNativeTabBar()
|
|
||||||
// #endif
|
|
||||||
},
|
|
||||||
onHide: function() {
|
|
||||||
console.log('App Hide')
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
// 隐藏原生 TabBar - 这里是核心修复
|
// 隐藏原生 TabBar - 这里是核心修复
|
||||||
hideNativeTabBar() {
|
const hideNativeTabBar = () => {
|
||||||
// 尝试多次执行,确保执行成功
|
// 尝试多次执行,确保执行成功
|
||||||
const tryHide = (times = 0) => {
|
const tryHide = (times = 0) => {
|
||||||
if (times > 10) return // 最多尝试10次
|
if (times > 10) return // 最多尝试10次
|
||||||
@@ -36,7 +19,7 @@ export default {
|
|||||||
success: () => {
|
success: () => {
|
||||||
console.log('✅ 原生TabBar隐藏成功')
|
console.log('✅ 原生TabBar隐藏成功')
|
||||||
// 强制 CSS 覆盖(双重保险)
|
// 强制 CSS 覆盖(双重保险)
|
||||||
this.forceCSSHide()
|
forceCSSHide()
|
||||||
},
|
},
|
||||||
fail: (err) => {
|
fail: (err) => {
|
||||||
console.log(`❌ 第${times+1}次隐藏失败,1秒后重试`, err)
|
console.log(`❌ 第${times+1}次隐藏失败,1秒后重试`, err)
|
||||||
@@ -47,10 +30,10 @@ export default {
|
|||||||
|
|
||||||
// 延迟 300ms 执行,确保页面挂载完成
|
// 延迟 300ms 执行,确保页面挂载完成
|
||||||
setTimeout(() => tryHide(), 300)
|
setTimeout(() => tryHide(), 300)
|
||||||
},
|
}
|
||||||
|
|
||||||
// 强制 CSS 覆盖(最终保险)
|
// 强制 CSS 覆盖(最终保险)
|
||||||
forceCSSHide() {
|
const forceCSSHide = () => {
|
||||||
// #ifdef APP-PLUS
|
// #ifdef APP-PLUS
|
||||||
const style = document.createElement('style')
|
const style = document.createElement('style')
|
||||||
style.innerHTML = `
|
style.innerHTML = `
|
||||||
@@ -69,15 +52,34 @@ export default {
|
|||||||
document.head.appendChild(style)
|
document.head.appendChild(style)
|
||||||
console.log('✅ CSS 强制覆盖已注入')
|
console.log('✅ CSS 强制覆盖已注入')
|
||||||
// #endif
|
// #endif
|
||||||
},
|
}
|
||||||
|
|
||||||
// 预加载所有 Tab 页面的核心数据
|
// 预加载所有 Tab 页面的核心数据
|
||||||
preloadTabData() {
|
const preloadTabData = () => {
|
||||||
// 延迟执行,不阻塞首屏
|
// 延迟执行,不阻塞首屏
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// 预加载课程数据等...
|
// 预加载课程数据等...
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
onLaunch(() => {
|
||||||
|
console.log('App Launch')
|
||||||
|
preloadTabData()
|
||||||
|
})
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
console.log('App Show')
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
hideNativeTabBar()
|
||||||
|
// #endif
|
||||||
|
})
|
||||||
|
|
||||||
|
onHide(() => {
|
||||||
|
console.log('App Hide')
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
/* 注意要写在第一行,同时给style标签加入lang="scss"属性 */
|
||||||
|
@import "@/uni.scss";
|
||||||
|
</style>
|
||||||
@@ -4,6 +4,14 @@ export function login(params) {
|
|||||||
return request.post('/member/auth/miniapp/login', params)
|
return request.post('/member/auth/miniapp/login', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sendCode(params) {
|
||||||
|
return request.post('/member/auth/send-code', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loginWithPhone(params) {
|
||||||
|
return request.post('/member/auth/phone-login', params)
|
||||||
|
}
|
||||||
|
|
||||||
export function logout() {
|
export function logout() {
|
||||||
return request.post('/member/auth/logout')
|
return request.post('/member/auth/logout')
|
||||||
}
|
}
|
||||||
@@ -16,14 +24,156 @@ export function checkIn(params) {
|
|||||||
return request.post('/checkIn/scan', params)
|
return request.post('/checkIn/scan', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCheckInRecords(params = {}, options = {}) {
|
||||||
|
const { page = 0, size = 5 } = params
|
||||||
|
return request.post('/checkIn/page', { page, size }, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUnreadMessageCount(userId, options = {}) {
|
||||||
|
return request.get(`/messages/user/${userId}/unread`, {}, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 消息相关API ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户消息列表(支持分页)
|
||||||
|
* @param {number} userId - 用户ID
|
||||||
|
* @param {object} params - 查询参数
|
||||||
|
* @param {number} params.page - 页码(从0开始)
|
||||||
|
* @param {number} params.size - 每页条数
|
||||||
|
* @param {object} options - 请求选项
|
||||||
|
* @returns {Promise} 消息列表
|
||||||
|
*/
|
||||||
|
export function getUserMessages(userId, params = {}, options = {}) {
|
||||||
|
const { page = 0, size = 10 } = params
|
||||||
|
return request.get(`/messages/user/${userId}/page`, { page, size }, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取未读消息列表(支持分页)
|
||||||
|
* @param {number} userId - 用户ID
|
||||||
|
* @param {object} params - 查询参数
|
||||||
|
* @param {number} params.page - 页码(从0开始)
|
||||||
|
* @param {number} params.size - 每页条数
|
||||||
|
* @param {object} options - 请求选项
|
||||||
|
* @returns {Promise} 未读消息列表
|
||||||
|
*/
|
||||||
|
export function getUnreadMessages(userId, params = {}, options = {}) {
|
||||||
|
const { page = 0, size = 10 } = params
|
||||||
|
return request.get(`/messages/user/${userId}/unread/page`, { page, size }, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记消息为已读
|
||||||
|
* @param {number|string} id - 消息ID
|
||||||
|
* @param {object} options - 请求选项
|
||||||
|
* @returns {Promise} 操作结果
|
||||||
|
*/
|
||||||
|
export function markMessageAsRead(id, options = {}) {
|
||||||
|
return request.put(`/messages/${id}/read`, {}, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记所有消息为已读
|
||||||
|
* @param {number} userId - 用户ID
|
||||||
|
* @param {object} options - 请求选项
|
||||||
|
* @returns {Promise} 操作结果
|
||||||
|
*/
|
||||||
|
export function markAllMessagesAsRead(userId, options = {}) {
|
||||||
|
return request.put(`/messages/user/${userId}/read`, {}, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除消息
|
||||||
|
* @param {number|string} id - 消息ID
|
||||||
|
* @param {object} options - 请求选项
|
||||||
|
* @returns {Promise} 操作结果
|
||||||
|
*/
|
||||||
|
export function deleteMessage(id, options = {}) {
|
||||||
|
return request.delete(`/messages/${id}`, {}, options)
|
||||||
|
}
|
||||||
|
|
||||||
export function getUserInfo(options = { cache: true, cacheTime: 30 * 60 * 1000 }) {
|
export function getUserInfo(options = { cache: true, cacheTime: 30 * 60 * 1000 }) {
|
||||||
return request.get('/member/info', {}, options)
|
return request.get('/member/info', {}, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取会员详细信息(包含会员卡、积分等)
|
||||||
|
*/
|
||||||
|
export function getMemberDetail(options = { cache: false }) {
|
||||||
|
return request.get('/member/info', {}, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取签到统计
|
||||||
|
* @param {object} params - 查询参数
|
||||||
|
* @param {string} params.startDate - 开始日期(yyyy-MM-dd)
|
||||||
|
* @param {string} params.endDate - 结束日期(yyyy-MM-dd)
|
||||||
|
*/
|
||||||
|
export function getCheckInStats(params = {}, options = { cache: false }) {
|
||||||
|
return request.get('/checkIn/statistics', params, options)
|
||||||
|
}
|
||||||
|
|
||||||
export function updateUserInfo(params) {
|
export function updateUserInfo(params) {
|
||||||
return request.put('/member/info', params)
|
return request.put('/member/info', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 购买会员卡
|
||||||
|
* @param {object} params - 购买参数
|
||||||
|
* @param {number} params.memberId - 会员ID
|
||||||
|
* @param {number} params.memberCardId - 会员卡类型ID
|
||||||
|
* @param {number} [params.sourceOrderId] - 来源订单ID
|
||||||
|
*/
|
||||||
|
export function purchaseMemberCard(params) {
|
||||||
|
return request.post('/member-card-records/purchase', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMyMemberCards(memberId, options = { cache: false }) {
|
||||||
|
return request.get(`/member-card-records/my-cards/${memberId}`, {}, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 支付相关API ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建支付订单
|
||||||
|
* @param {object} params - 支付参数
|
||||||
|
* @param {number} params.memberId - 会员ID
|
||||||
|
* @param {string} params.orderType - 订单类型 (MEMBER_CARD-会员卡, GROUP_COURSE-团课, GOODS-商品)
|
||||||
|
* @param {string} params.goodsDesc - 商品描述
|
||||||
|
* @param {string} params.transAmt - 交易金额(单位:分)
|
||||||
|
* @param {string} params.tradeType - 交易类型 (WECHAT-微信, ALIPAY-支付宝)
|
||||||
|
* @param {string} [params.remark] - 备注
|
||||||
|
* @param {string} [params.acctId] - 账户号
|
||||||
|
* @param {string} [params.timeExpire] - 过期时间 (yyyyMMddHHmmss)
|
||||||
|
* @param {string} [params.delayAcctFlag] - 延迟入账标识
|
||||||
|
* @param {number} [params.feeFlag] - 手续费标识
|
||||||
|
* @param {string} [params.limitPayType] - 禁用支付方式
|
||||||
|
* @param {string} [params.channelNo] - 渠道号
|
||||||
|
* @param {string} [params.payScene] - 支付场景
|
||||||
|
* @param {string} [params.notifyUrl] - 回调通知URL
|
||||||
|
*/
|
||||||
|
export function createPayment(params) {
|
||||||
|
return request.post('/payment/create', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询支付状态
|
||||||
|
* @param {string} orderId - 订单ID
|
||||||
|
*/
|
||||||
|
export function getPaymentStatus(orderId) {
|
||||||
|
return request.get(`/payment/${orderId}`, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 申请退款
|
||||||
|
* @param {string} orderId - 订单ID
|
||||||
|
* @param {string} refundAmt - 退款金额(单位:分)
|
||||||
|
*/
|
||||||
|
export function refundPayment(orderId, refundAmt) {
|
||||||
|
return request.post(`/payment/${orderId}/refund`, { refundAmt })
|
||||||
|
}
|
||||||
|
|
||||||
export function getRecommendCourses(options = { cache: true, cacheTime: 10 * 60 * 1000 }) {
|
export function getRecommendCourses(options = { cache: true, cacheTime: 10 * 60 * 1000 }) {
|
||||||
return request.get('/course/recommend', {}, options)
|
return request.get('/course/recommend', {}, options)
|
||||||
}
|
}
|
||||||
@@ -37,14 +187,81 @@ export function getGroupCoursePage(params = {}, options = { cache: true, cacheTi
|
|||||||
return request.post('/groupCourse/page', { page, size, sort, order, keyword }, options)
|
return request.post('/groupCourse/page', { page, size, sort, order, keyword }, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 团课预约相关API ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预约团课
|
||||||
|
* @param {object} params - 预约参数
|
||||||
|
* @param {number} params.courseId - 团课ID
|
||||||
|
* @param {number} params.memberId - 会员ID
|
||||||
|
* @param {number} params.memberCardRecordId - 会员卡记录ID
|
||||||
|
*/
|
||||||
|
export function bookGroupCourse(params) {
|
||||||
|
return request.post('/groupCourse/book', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消预约
|
||||||
|
* @param {number} bookingId - 预约ID
|
||||||
|
*/
|
||||||
|
export function cancelBooking(bookingId) {
|
||||||
|
return request.delete(`/groupCourse/book/${bookingId}`, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 团课签到
|
||||||
|
* @param {number} memberId - 会员ID
|
||||||
|
* @param {number} courseId - 团课ID
|
||||||
|
*/
|
||||||
|
export function signinGroupCourse(memberId, courseId) {
|
||||||
|
return request.post(`/groupCourse/signin/${memberId}`, { courseId })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询会员预约记录
|
||||||
|
* @param {number} memberId - 会员ID
|
||||||
|
*/
|
||||||
|
export function getMemberBookings(memberId, options = { cache: false }) {
|
||||||
|
return request.get(`/groupCourse/bookings/member/${memberId}`, {}, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询课程预约记录
|
||||||
|
* @param {number} courseId - 团课ID
|
||||||
|
*/
|
||||||
|
export function getCourseBookings(courseId, options = { cache: false }) {
|
||||||
|
return request.get(`/groupCourse/bookings/course/${courseId}`, {}, options)
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
login,
|
login,
|
||||||
|
sendCode,
|
||||||
|
loginWithPhone,
|
||||||
logout,
|
logout,
|
||||||
getQRCode,
|
getQRCode,
|
||||||
checkIn,
|
checkIn,
|
||||||
|
getCheckInRecords,
|
||||||
|
getCheckInStats,
|
||||||
|
getUnreadMessageCount,
|
||||||
|
getUserMessages,
|
||||||
|
getUnreadMessages,
|
||||||
|
markMessageAsRead,
|
||||||
|
markAllMessagesAsRead,
|
||||||
|
deleteMessage,
|
||||||
getUserInfo,
|
getUserInfo,
|
||||||
|
getMemberDetail,
|
||||||
updateUserInfo,
|
updateUserInfo,
|
||||||
|
purchaseMemberCard,
|
||||||
|
getMyMemberCards,
|
||||||
|
createPayment,
|
||||||
|
getPaymentStatus,
|
||||||
|
refundPayment,
|
||||||
getRecommendCourses,
|
getRecommendCourses,
|
||||||
getCourseDetail,
|
getCourseDetail,
|
||||||
getGroupCoursePage
|
getGroupCoursePage,
|
||||||
|
bookGroupCourse,
|
||||||
|
cancelBooking,
|
||||||
|
signinGroupCourse,
|
||||||
|
getMemberBookings,
|
||||||
|
getCourseBookings
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ export const PAGE = {
|
|||||||
POINTS: '/pages/memberInfo/points',
|
POINTS: '/pages/memberInfo/points',
|
||||||
REFERRAL: '/pages/memberInfo/referral',
|
REFERRAL: '/pages/memberInfo/referral',
|
||||||
MY_COURSES: '/pages/memberInfo/myCourses',
|
MY_COURSES: '/pages/memberInfo/myCourses',
|
||||||
CHECK_IN_HISTORY: '/pages/memberInfo/checkInHistory'
|
CHECK_IN_HISTORY: '/pages/memberInfo/checkInHistory',
|
||||||
|
PURCHASE_CARD: '/pages/memberInfo/purchaseCard'
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 底部 TabBar 页面路径,顺序与 TabBar.vue 一致 */
|
/** 底部 TabBar 页面路径,顺序与 TabBar.vue 一致 */
|
||||||
|
|||||||
@@ -1,9 +1,3 @@
|
|||||||
import { bodyTestMock } from './mockData.js'
|
|
||||||
|
|
||||||
function clone(value) {
|
|
||||||
return JSON.parse(JSON.stringify(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatRecordTime(date) {
|
function formatRecordTime(date) {
|
||||||
const y = date.getFullYear()
|
const y = date.getFullYear()
|
||||||
const m = String(date.getMonth() + 1).padStart(2, '0')
|
const m = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
@@ -28,9 +22,9 @@ function formatTime(date) {
|
|||||||
|
|
||||||
export function getDefaultBodyTestState() {
|
export function getDefaultBodyTestState() {
|
||||||
return {
|
return {
|
||||||
settings: { ...bodyTestMock.settings },
|
settings: {},
|
||||||
device: { ...bodyTestMock.device },
|
device: { connected: false, battery: 80 },
|
||||||
records: clone(bodyTestMock.records)
|
records: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,14 +161,26 @@ export function getBodyTestTrendData(store, metricKey, limit = 6) {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getMetricDefs() {
|
||||||
|
return [
|
||||||
|
{ key: 'weight', label: '体重' },
|
||||||
|
{ key: 'bmi', label: 'BMI' },
|
||||||
|
{ key: 'bodyFat', label: '体脂率' },
|
||||||
|
{ key: 'muscleMass', label: '肌肉量' },
|
||||||
|
{ key: 'visceralFat', label: '内脏脂肪' },
|
||||||
|
{ key: 'bmr', label: '基础代谢' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
export function getCompareData(store, idA, idB) {
|
export function getCompareData(store, idA, idB) {
|
||||||
const a = getBodyTestRecordById(store, idA)
|
const a = getBodyTestRecordById(store, idA)
|
||||||
const b = getBodyTestRecordById(store, idB)
|
const b = getBodyTestRecordById(store, idB)
|
||||||
if (!a || !b) return null
|
if (!a || !b) return null
|
||||||
|
const metricDefs = getMetricDefs()
|
||||||
const keys = ['weight', 'bmi', 'bodyFat', 'muscleMass', 'visceralFat', 'bmr']
|
const keys = ['weight', 'bmi', 'bodyFat', 'muscleMass', 'visceralFat', 'bmr']
|
||||||
const metrics = keys.map((key) => ({
|
const metrics = keys.map((key) => ({
|
||||||
key,
|
key,
|
||||||
label: bodyTestMock.metricDefs.find((m) => m.key === key)?.label || key,
|
label: metricDefs.find((m) => m.key === key)?.label || key,
|
||||||
valueA: a.metrics[key],
|
valueA: a.metrics[key],
|
||||||
valueB: b.metrics[key],
|
valueB: b.metrics[key],
|
||||||
diff: Math.round((a.metrics[key] - b.metrics[key]) * 10) / 10
|
diff: Math.round((a.metrics[key] - b.metrics[key]) * 10) / 10
|
||||||
@@ -183,8 +189,7 @@ export function getCompareData(store, idA, idB) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getRecommendedCourses(record) {
|
export function getRecommendedCourses(record) {
|
||||||
const ids = record?.recommendedCourseIds || []
|
return []
|
||||||
return bodyTestMock.recommendedCourses.filter((c) => ids.includes(c.id))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateBodyTestSettings(store, patch) {
|
export function updateBodyTestSettings(store, patch) {
|
||||||
@@ -243,9 +248,9 @@ export function saveSimulatedBodyTestRecord(store, finalMetrics) {
|
|||||||
status,
|
status,
|
||||||
metrics,
|
metrics,
|
||||||
radar,
|
radar,
|
||||||
bodySegments: clone(bodyTestMock.records[0].bodySegments),
|
bodySegments: [],
|
||||||
advice: clone(bodyTestMock.records[0].advice),
|
advice: [],
|
||||||
recommendedCourseIds: [1, 2]
|
recommendedCourseIds: []
|
||||||
}
|
}
|
||||||
|
|
||||||
if (previous) {
|
if (previous) {
|
||||||
@@ -279,5 +284,3 @@ export function interpolateMeasuringMetrics(progress, profile) {
|
|||||||
bodyWater: Math.round((51 + (target.bodyWater - 51) * ratio) * 10) / 10
|
bodyWater: Math.round((51 + (target.bodyWater - 51) * ratio) * 10) / 10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { bodyTestMock }
|
|
||||||
|
|||||||
@@ -57,6 +57,14 @@ export function canCancelBooking(item) {
|
|||||||
return diff >= 2 * 3600000
|
return diff >= 2 * 3600000
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function canSigninCourse(item) {
|
||||||
|
if (!item?.courseDate || !item?.startTime) return false
|
||||||
|
if (item.status !== 'booked' && item.status !== 0) return false
|
||||||
|
const start = new Date(`${item.courseDate} ${item.startTime}`.replace(/-/g, '/'))
|
||||||
|
const diff = start - Date.now()
|
||||||
|
return diff <= 2 * 3600000 && diff >= -2 * 3600000
|
||||||
|
}
|
||||||
|
|
||||||
export function bookCourse(store, courseId) {
|
export function bookCourse(store, courseId) {
|
||||||
const course = store.courseCatalog.find((c) => c.id === Number(courseId))
|
const course = store.courseCatalog.find((c) => c.id === Number(courseId))
|
||||||
if (!course) return { ok: false, message: '课程不存在' }
|
if (!course) return { ok: false, message: '课程不存在' }
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
export { memberCenterMock, userInfoMock, fitnessGoalOptions, bookingMock, memberCardMock, bodyTestMock, moduleMock, courseCatalogMock } from './mockData.js'
|
|
||||||
export { statusBarTimeMixin, subPageMixin } from './mixins.js'
|
export { statusBarTimeMixin, subPageMixin } from './mixins.js'
|
||||||
export {
|
export {
|
||||||
loadMemberStore,
|
loadMemberStore,
|
||||||
@@ -31,8 +30,7 @@ export {
|
|||||||
connectBodyTestDevice,
|
connectBodyTestDevice,
|
||||||
disconnectBodyTestDevice,
|
disconnectBodyTestDevice,
|
||||||
saveSimulatedBodyTestRecord,
|
saveSimulatedBodyTestRecord,
|
||||||
interpolateMeasuringMetrics,
|
interpolateMeasuringMetrics
|
||||||
bodyTestMock
|
|
||||||
} from './bodyTestStore.js'
|
} from './bodyTestStore.js'
|
||||||
export {
|
export {
|
||||||
getTrainingReportData,
|
getTrainingReportData,
|
||||||
@@ -52,8 +50,7 @@ export {
|
|||||||
getMyCoursesByTab,
|
getMyCoursesByTab,
|
||||||
getOnlineCourseById,
|
getOnlineCourseById,
|
||||||
updateOnlineProgress,
|
updateOnlineProgress,
|
||||||
getCheckInHistory,
|
getCheckInHistory
|
||||||
moduleMock
|
|
||||||
} from './moduleStore.js'
|
} from './moduleStore.js'
|
||||||
export {
|
export {
|
||||||
filterCourses,
|
filterCourses,
|
||||||
@@ -61,8 +58,7 @@ export {
|
|||||||
bookCourse,
|
bookCourse,
|
||||||
canCancelBooking,
|
canCancelBooking,
|
||||||
enrichCourseForDisplay,
|
enrichCourseForDisplay,
|
||||||
getWeekDates,
|
getWeekDates
|
||||||
courseCatalogMock
|
|
||||||
} from './bookingStore.js'
|
} from './bookingStore.js'
|
||||||
export { previewImage, persistChosenImage, isLocalFilePath } from './media.js'
|
export { previewImage, persistChosenImage, isLocalFilePath } from './media.js'
|
||||||
export { maskPhone, formatMemberCenterPhone, normalizePhoneForStore } from './format.js'
|
export { maskPhone, formatMemberCenterPhone, normalizePhoneForStore } from './format.js'
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
/** 个人中心模块 mock 数据(后续可替换为 API) */
|
/** 个人中心模块 mock 数据(后续可替换为 API) */
|
||||||
|
|
||||||
export const memberCenterMock = {
|
// Mock 数据开关 - 设为 false 可关闭所有 mock 数据
|
||||||
|
export const MOCK_ENABLED = false
|
||||||
|
|
||||||
|
export const memberCenterMock = MOCK_ENABLED ? {
|
||||||
userInfo: {
|
userInfo: {
|
||||||
name: '张小芳',
|
name: '张小芳',
|
||||||
phone: '13812345678 已绑定微信',
|
phone: '13812345678 已绑定微信',
|
||||||
@@ -65,7 +68,7 @@ export const memberCenterMock = {
|
|||||||
registered: 3,
|
registered: 3,
|
||||||
purchased: 2
|
purchased: 2
|
||||||
}
|
}
|
||||||
}
|
} : null
|
||||||
|
|
||||||
export const userInfoMock = {
|
export const userInfoMock = {
|
||||||
name: '张小芳',
|
name: '张小芳',
|
||||||
|
|||||||
@@ -1,9 +1,3 @@
|
|||||||
import {
|
|
||||||
memberCenterMock,
|
|
||||||
userInfoMock,
|
|
||||||
memberCardMock,
|
|
||||||
bookingMock
|
|
||||||
} from './mockData.js'
|
|
||||||
import { formatMemberCenterPhone, normalizePhoneForStore } from './format.js'
|
import { formatMemberCenterPhone, normalizePhoneForStore } from './format.js'
|
||||||
import {
|
import {
|
||||||
getDefaultBodyTestState,
|
getDefaultBodyTestState,
|
||||||
@@ -24,10 +18,6 @@ import {
|
|||||||
|
|
||||||
const STORAGE_KEY = 'gym_member_info_v1'
|
const STORAGE_KEY = 'gym_member_info_v1'
|
||||||
|
|
||||||
function clone(value) {
|
|
||||||
return JSON.parse(JSON.stringify(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildCardTip(remainingDays) {
|
export function buildCardTip(remainingDays) {
|
||||||
return `距离下次到期还有${remainingDays}天,请及时续费`
|
return `距离下次到期还有${remainingDays}天,请及时续费`
|
||||||
}
|
}
|
||||||
@@ -70,24 +60,26 @@ function finalizeStore(store) {
|
|||||||
|
|
||||||
function getDefaultStore() {
|
function getDefaultStore() {
|
||||||
return finalizeStore({
|
return finalizeStore({
|
||||||
profile: { ...userInfoMock, avatar: memberCenterMock.userInfo.avatar },
|
profile: {},
|
||||||
memberProfile: { ...memberCenterMock.userInfo },
|
memberProfile: {},
|
||||||
stats: { ...memberCenterMock.stats },
|
stats: {},
|
||||||
cardInfo: { ...memberCenterMock.cardInfo },
|
cardInfo: {},
|
||||||
card: { ...memberCardMock.card },
|
card: {},
|
||||||
records: clone(memberCardMock.records),
|
records: [],
|
||||||
ongoingBookings: clone(bookingMock.ongoing),
|
ongoingBookings: [],
|
||||||
historyBookings: clone(bookingMock.history),
|
historyBookings: [],
|
||||||
checkIns: clone(memberCenterMock.checkIns),
|
checkIns: [],
|
||||||
bodyReport: { ...memberCenterMock.bodyReport },
|
bodyReport: {},
|
||||||
bodyTest: getDefaultBodyTestState(),
|
bodyTest: getDefaultBodyTestState(),
|
||||||
modules: getDefaultModuleState(),
|
modules: getDefaultModuleState(),
|
||||||
courseCatalog: getDefaultCourseCatalog(),
|
courseCatalog: getDefaultCourseCatalog(),
|
||||||
couponPoints: { ...memberCenterMock.couponPoints },
|
couponPoints: {},
|
||||||
referral: { ...memberCenterMock.referral }
|
referral: {}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { getDefaultStore }
|
||||||
|
|
||||||
function mergeDefaults(saved) {
|
function mergeDefaults(saved) {
|
||||||
const defaults = getDefaultStore()
|
const defaults = getDefaultStore()
|
||||||
return finalizeStore({
|
return finalizeStore({
|
||||||
@@ -204,7 +196,11 @@ export function getBookingPreview(store, limit = 2) {
|
|||||||
export function getCenterPageData(store) {
|
export function getCenterPageData(store) {
|
||||||
return {
|
return {
|
||||||
userInfo: { ...store.memberProfile },
|
userInfo: { ...store.memberProfile },
|
||||||
stats: { ...store.stats },
|
stats: {
|
||||||
|
checkInCount: store.stats?.checkInCount ?? 0,
|
||||||
|
trainingHours: store.stats?.trainingHours ?? 0,
|
||||||
|
pointsBalance: store.stats?.pointsBalance ?? 0
|
||||||
|
},
|
||||||
cardInfo: { ...store.cardInfo },
|
cardInfo: { ...store.cardInfo },
|
||||||
bookingPreview: getBookingPreview(store),
|
bookingPreview: getBookingPreview(store),
|
||||||
checkIns: store.checkIns.map((item) => ({ ...item })),
|
checkIns: store.checkIns.map((item) => ({ ...item })),
|
||||||
@@ -234,6 +230,15 @@ export function cancelOngoingBooking(store, id) {
|
|||||||
const course = store.courseCatalog.find((c) => c.id === removed.courseId)
|
const course = store.courseCatalog.find((c) => c.id === removed.courseId)
|
||||||
if (course && course.enrolled > 0) course.enrolled -= 1
|
if (course && course.enrolled > 0) course.enrolled -= 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 同步从 myCourses.group.ongoing 中移除
|
||||||
|
if (store.modules?.myCourses?.group?.ongoing) {
|
||||||
|
const myCoursesIndex = store.modules.myCourses.group.ongoing.findIndex((c) => c.id === id)
|
||||||
|
if (myCoursesIndex >= 0) {
|
||||||
|
store.modules.myCourses.group.ongoing.splice(myCoursesIndex, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
store.historyBookings.unshift({
|
store.historyBookings.unshift({
|
||||||
...removed,
|
...removed,
|
||||||
status: 'cancelled',
|
status: 'cancelled',
|
||||||
|
|||||||
@@ -269,3 +269,63 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
background-color: rgba(255, 255, 255, 0.31);
|
background-color: rgba(255, 255, 255, 0.31);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profile-header__user-inner--guest {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header__avatar-wrap--guest {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.5);
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header__avatar--guest {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header__user-meta--guest {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header__name--guest {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: black;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header__phone--guest {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #999999;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header__user-inner--guest {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: none;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|||||||
@@ -239,6 +239,29 @@
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bk-card__actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-card__signin {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--accent-orange, #FF6B35);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-card__signin-text {
|
||||||
|
font-size: var(--font-size-base, 14px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-inverse, #ffffff);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.bk-card__cancel {
|
.bk-card__cancel {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
padding: 6px 14px;
|
padding: 6px 14px;
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<template>
|
||||||
|
<view class="message-skeleton">
|
||||||
|
<view v-for="i in 6" :key="i" class="skeleton-item">
|
||||||
|
<view class="skeleton-icon skeleton-shimmer"></view>
|
||||||
|
<view class="skeleton-content">
|
||||||
|
<view class="skeleton-title skeleton-shimmer"></view>
|
||||||
|
<view class="skeleton-desc skeleton-shimmer"></view>
|
||||||
|
<view class="skeleton-footer">
|
||||||
|
<view class="skeleton-tag skeleton-shimmer"></view>
|
||||||
|
<view class="skeleton-time skeleton-shimmer"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.message-skeleton {
|
||||||
|
padding: 8rpx 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 24rpx 0;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
background: var(--bg-white);
|
||||||
|
border-radius: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-icon {
|
||||||
|
width: 80rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-title {
|
||||||
|
height: 32rpx;
|
||||||
|
width: 60%;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-desc {
|
||||||
|
height: 28rpx;
|
||||||
|
width: 85%;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-tag {
|
||||||
|
height: 24rpx;
|
||||||
|
width: 80rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-time {
|
||||||
|
height: 24rpx;
|
||||||
|
width: 100rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.skeleton-shimmer) {
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% { background-position: 200% 0; }
|
||||||
|
100% { background-position: -200% 0; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="tab-page__header">
|
<view class="tab-page__header" :style="{ padding: `${statusBarHeight}rpx`, height: `${statusBarHeight + 80}rpx` }">
|
||||||
<view v-if="showBack" :class="['tab-page__back-btn', { 'tab-page__back-btn--animate': isAnimating }]" @tap="goBack">
|
<view v-if="showBack" :class="['tab-page__back-btn', { 'tab-page__back-btn--animate': isAnimating }]" @tap="goBack">
|
||||||
<text class="tab-page__back-icon">‹</text>
|
<text class="tab-page__back-icon">‹</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -29,8 +29,11 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const isAnimating = ref(false)
|
const isAnimating = ref(false)
|
||||||
|
const statusBarHeight = ref(20)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
const sysInfo = uni.getSystemInfoSync()
|
||||||
|
statusBarHeight.value = sysInfo.statusBarHeight || 20
|
||||||
if (props.showBack) {
|
if (props.showBack) {
|
||||||
isAnimating.value = true
|
isAnimating.value = true
|
||||||
}
|
}
|
||||||
@@ -49,7 +52,7 @@ function goBack() {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.tab-page__header {
|
.tab-page__header {
|
||||||
padding: 48rpx 32rpx 16rpx;
|
padding: 0 32rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -89,7 +92,7 @@ function goBack() {
|
|||||||
font-size: 36rpx;
|
font-size: 36rpx;
|
||||||
color: $text-dark;
|
color: $text-dark;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
line-height: 1;
|
line-height: 28rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-page__title-wrap {
|
.tab-page__title-wrap {
|
||||||
|
|||||||
@@ -4,24 +4,243 @@
|
|||||||
v-for="(item, index) in entries"
|
v-for="(item, index) in entries"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="entry-item"
|
class="entry-item"
|
||||||
@tap="QEClick(item.path)"
|
@tap="handleEntryClick(item)"
|
||||||
>
|
>
|
||||||
<view :class="['entry-icon', { accent: item.accent }]">
|
<view :class="['entry-icon', { accent: item.accent }]">
|
||||||
<image :src="item.icon" mode="aspectFit" class="icon-img" />
|
<image :src="item.icon" mode="aspectFit" class="icon-img" />
|
||||||
|
<view v-if="item.title === '消息' && hasUnread" class="badge-dot"></view>
|
||||||
</view>
|
</view>
|
||||||
<text class="entry-title">{{ item.title }}</text>
|
<text class="entry-title">{{ item.title }}</text>
|
||||||
<text class="entry-desc">{{ item.desc }}</text>
|
<text class="entry-desc">{{ item.desc }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view v-if="showCheckInMenu" class="checkin-menu-overlay" @tap="showCheckInMenu = false">
|
||||||
|
<view class="checkin-menu" @tap.stop>
|
||||||
|
<view class="menu-item" @tap="handleStoreCheckIn">
|
||||||
|
<image src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/checkIn.png" mode="aspectFit" class="menu-icon" />
|
||||||
|
<text class="menu-text">到店签到</text>
|
||||||
|
</view>
|
||||||
|
<view class="menu-item" @tap="handleGroupCourseCheckIn">
|
||||||
|
<image src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/course.png" mode="aspectFit" class="menu-icon" />
|
||||||
|
<text class="menu-text">团课签到</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { getUnreadMessageCount } from '@/api/main.js'
|
||||||
|
|
||||||
const QEClick = path => {
|
const unreadCount = ref(0)
|
||||||
|
const showCheckInMenu = ref(false)
|
||||||
|
const TEST_MODE = false
|
||||||
|
|
||||||
|
// 是否有未读消息(用于显示红点)
|
||||||
|
const hasUnread = computed(() => unreadCount.value > 0)
|
||||||
|
|
||||||
|
const loadUnreadCount = async () => {
|
||||||
|
if (TEST_MODE) {
|
||||||
|
unreadCount.value = 3
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = uni.getStorageSync('token')
|
||||||
|
if (!token) {
|
||||||
|
console.log('[QuickEntry] token 不存在,跳过请求')
|
||||||
|
unreadCount.value = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userId = uni.getStorageSync('userId') || 1
|
||||||
|
console.log('[QuickEntry] 请求未读消息数量, userId:', userId)
|
||||||
|
const res = await getUnreadMessageCount(userId)
|
||||||
|
console.log('[QuickEntry] 未读消息数量响应数据:', res)
|
||||||
|
console.log('[QuickEntry] 响应完整类型:', typeof res, Object.prototype.toString.call(res))
|
||||||
|
|
||||||
|
let count = 0
|
||||||
|
// 兼容多种响应格式
|
||||||
|
if (typeof res === 'number') {
|
||||||
|
count = res
|
||||||
|
console.log('[QuickEntry] 使用直接数字格式')
|
||||||
|
} else if (typeof res === 'string' && res.trim() !== '') {
|
||||||
|
// 处理字符串数字,如 "20"
|
||||||
|
count = parseInt(res, 10)
|
||||||
|
console.log('[QuickEntry] 使用字符串转数字格式:', count)
|
||||||
|
} else if (res !== null && typeof res === 'object') {
|
||||||
|
if (typeof res.count === 'number') {
|
||||||
|
count = res.count
|
||||||
|
console.log('[QuickEntry] 使用 res.count')
|
||||||
|
} else if (res.data !== undefined) {
|
||||||
|
if (typeof res.data === 'number') {
|
||||||
|
count = res.data
|
||||||
|
console.log('[QuickEntry] 使用 res.data')
|
||||||
|
} else if (typeof res.data?.count === 'number') {
|
||||||
|
count = res.data.count
|
||||||
|
console.log('[QuickEntry] 使用 res.data.count')
|
||||||
|
} else if (Array.isArray(res.data?.content)) {
|
||||||
|
count = res.data.content.length
|
||||||
|
console.log('[QuickEntry] 使用 res.data.content.length')
|
||||||
|
} else if (Array.isArray(res.content)) {
|
||||||
|
count = res.content.length
|
||||||
|
console.log('[QuickEntry] 使用 res.content.length')
|
||||||
|
} else if (typeof res.data?.total === 'number') {
|
||||||
|
count = res.data.total
|
||||||
|
console.log('[QuickEntry] 使用 res.data.total')
|
||||||
|
} else if (typeof res.total === 'number') {
|
||||||
|
count = res.total
|
||||||
|
console.log('[QuickEntry] 使用 res.total')
|
||||||
|
}
|
||||||
|
} else if (Array.isArray(res.content)) {
|
||||||
|
count = res.content.length
|
||||||
|
console.log('[QuickEntry] 使用 res.content.length')
|
||||||
|
} else if (typeof res.total === 'number') {
|
||||||
|
count = res.total
|
||||||
|
console.log('[QuickEntry] 使用 res.total')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('[QuickEntry] 无法解析响应格式,res:', res)
|
||||||
|
}
|
||||||
|
|
||||||
|
unreadCount.value = count
|
||||||
|
uni.setStorageSync('unreadMessageCount', count)
|
||||||
|
console.log('[QuickEntry] 最终未读数量:', count, 'hasUnread:', hasUnread.value)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[QuickEntry] 获取未读消息数失败:', e)
|
||||||
|
try {
|
||||||
|
const count = uni.getStorageSync('unreadMessageCount')
|
||||||
|
unreadCount.value = count || 0
|
||||||
|
} catch (e2) {
|
||||||
|
unreadCount.value = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEntryClick = (item) => {
|
||||||
|
if (item.title === '消息' || item.title === '健身数据') {
|
||||||
|
if (TEST_MODE) {
|
||||||
|
uni.showLoading({
|
||||||
|
title: '加载中...',
|
||||||
|
mask: true
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.hideLoading()
|
||||||
|
if (item.isTabBar) {
|
||||||
|
uni.switchTab({ url: item.path })
|
||||||
|
} else {
|
||||||
|
uni.navigateTo({ url: item.path })
|
||||||
|
}
|
||||||
|
}, 300)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = uni.getStorageSync('token')
|
||||||
|
if (!token) {
|
||||||
|
uni.showModal({
|
||||||
|
title: '请登录',
|
||||||
|
content: '是否跳转到登录页面?',
|
||||||
|
confirmText: '确定',
|
||||||
|
cancelText: '取消',
|
||||||
|
confirmColor: '#FF6B35',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url:path
|
url: '/pages/login/login'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.showLoading({
|
||||||
|
title: '加载中...',
|
||||||
|
mask: true
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.hideLoading()
|
||||||
|
if (item.title === '消息') {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `${item.path}?unreadCount=${unreadCount.value}`
|
||||||
|
})
|
||||||
|
} else if (item.isTabBar) {
|
||||||
|
uni.switchTab({ url: item.path })
|
||||||
|
} else {
|
||||||
|
uni.navigateTo({ url: item.path })
|
||||||
|
}
|
||||||
|
}, 300)
|
||||||
|
} else if (item.title === '签到') {
|
||||||
|
const token = uni.getStorageSync('token')
|
||||||
|
if (!token) {
|
||||||
|
uni.showModal({
|
||||||
|
title: '请登录',
|
||||||
|
content: '是否跳转到登录页面?',
|
||||||
|
confirmText: '确定',
|
||||||
|
cancelText: '取消',
|
||||||
|
confirmColor: '#FF6B35',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/login/login'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
showCheckInMenu.value = !showCheckInMenu.value
|
||||||
|
} else if (item.path) {
|
||||||
|
// tabbar 页面使用 switchTab
|
||||||
|
if (item.isTabBar) {
|
||||||
|
uni.switchTab({
|
||||||
|
url: item.path
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: item.path
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStoreCheckIn = () => {
|
||||||
|
showCheckInMenu.value = false
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/checkIn/checkIn'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleGroupCourseCheckIn = () => {
|
||||||
|
showCheckInMenu.value = false
|
||||||
|
uni.scanCode({
|
||||||
|
onlyFromCamera: true,
|
||||||
|
success: (res) => {
|
||||||
|
console.log('扫码结果:', res)
|
||||||
|
uni.showToast({
|
||||||
|
title: '扫码成功',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
console.error('扫码失败:', err)
|
||||||
|
uni.showToast({
|
||||||
|
title: '扫码失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadUnreadCount()
|
||||||
|
})
|
||||||
|
|
||||||
|
uni.$on('pageShow', () => {
|
||||||
|
loadUnreadCount()
|
||||||
|
})
|
||||||
|
|
||||||
const entries = [
|
const entries = [
|
||||||
{
|
{
|
||||||
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/course.png',
|
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/course.png',
|
||||||
@@ -34,19 +253,23 @@ const entries = [
|
|||||||
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/plan.png',
|
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/plan.png',
|
||||||
title: '训练计划',
|
title: '训练计划',
|
||||||
desc: '个性定制',
|
desc: '个性定制',
|
||||||
accent: true
|
accent: true,
|
||||||
|
path: "/pages/train/index",
|
||||||
|
isTabBar: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/data.png',
|
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/data.png',
|
||||||
title: '健身数据',
|
title: '健身数据',
|
||||||
desc: '记录分析',
|
desc: '记录分析',
|
||||||
accent: false
|
accent: false,
|
||||||
|
path: "/pages/memberInfo/bodyTestTrend"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/message.png',
|
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/message.png',
|
||||||
title: '消息',
|
title: '消息',
|
||||||
desc: '通知消息',
|
desc: '通知消息',
|
||||||
accent: true
|
accent: true,
|
||||||
|
path: "/pages/message/index"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/checkIn.png',
|
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/checkIn.png',
|
||||||
@@ -91,6 +314,20 @@ const entries = [
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-bottom: 16rpx;
|
margin-bottom: 16rpx;
|
||||||
box-shadow: 0 6rpx 20rpx rgba(130, 220, 130, 0.35);
|
box-shadow: 0 6rpx 20rpx rgba(130, 220, 130, 0.35);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-dot {
|
||||||
|
position: absolute;
|
||||||
|
top: 8rpx;
|
||||||
|
right: 8rpx;
|
||||||
|
width: 16rpx;
|
||||||
|
height: 16rpx;
|
||||||
|
background: linear-gradient(135deg, #FF4757, #FF2D55);
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(255, 71, 87, 0.5);
|
||||||
|
z-index: 99;
|
||||||
|
animation: pulse 2s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-img {
|
.icon-img {
|
||||||
@@ -113,4 +350,93 @@ const entries = [
|
|||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
color: var(--tabbar-text-inactive);
|
color: var(--tabbar-text-inactive);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.checkin-menu-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: 280rpx;
|
||||||
|
padding-right: 60rpx;
|
||||||
|
animation: fadeIn 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkin-menu {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 140rpx;
|
||||||
|
background: white;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.15);
|
||||||
|
padding: 16rpx;
|
||||||
|
min-width: 320rpx;
|
||||||
|
animation: slideDown 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -24rpx;
|
||||||
|
right: 60rpx;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 24rpx solid transparent;
|
||||||
|
border-right: 24rpx solid transparent;
|
||||||
|
border-bottom: 24rpx solid white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-40rpx) scale(0.95);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 24rpx 32rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: rgba(0, 0, 0, 0.04);
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
& + .menu-item {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
width: 48rpx;
|
||||||
|
height: 48rpx;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-text {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2D4A5A;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<!-- 区域标题 -->
|
<!-- 区域标题 -->
|
||||||
<text class="section-title">推荐课程</text>
|
<text class="section-title">推荐课程</text>
|
||||||
<!-- 查看更多按钮 -->
|
<!-- 查看更多按钮 -->
|
||||||
<view class="view-more" @tap="goMore">
|
<view class="view-more" @click="handleViewMore">
|
||||||
<text>查看更多</text>
|
<text>查看更多</text>
|
||||||
<text class="arrow">
|
<text class="arrow">
|
||||||
<uni-icons type="right" size="20" color="#8CA0B0"/>
|
<uni-icons type="right" size="20" color="#8CA0B0"/>
|
||||||
@@ -19,28 +19,68 @@
|
|||||||
<!-- 课程列表 -->
|
<!-- 课程列表 -->
|
||||||
<view class="courses-list">
|
<view class="courses-list">
|
||||||
<!-- 课程卡片 -->
|
<!-- 课程卡片 -->
|
||||||
<CourseCard
|
<view
|
||||||
v-for="(course, index) in displayCourses"
|
v-for="(course, index) in courses"
|
||||||
:key="course.id || index"
|
:key="course.id || index"
|
||||||
:course="course"
|
class="course-card"
|
||||||
@join="handleJoinCourse"
|
>
|
||||||
/>
|
<!-- 课程图片区域 -->
|
||||||
|
<view class="course-image">
|
||||||
|
<!-- 课程封面图片 -->
|
||||||
|
<image :src="course.image" mode="aspectFill" class="img" />
|
||||||
|
<!-- 图片渐变遮罩 -->
|
||||||
|
<view class="course-overlay"></view>
|
||||||
|
<!-- 差几人提示(右上角) -->
|
||||||
|
<text v-if="course.remainingSlots > 0" class="remaining-slots">差{{ course.remainingSlots }}人</text>
|
||||||
|
<!-- 课程信息区域 -->
|
||||||
|
<view class="course-info">
|
||||||
|
<!-- 课程名称 -->
|
||||||
|
<text class="course-name">{{ course.name }}</text>
|
||||||
|
<!-- 课程元信息(时长、难度) -->
|
||||||
|
<view class="course-meta">
|
||||||
|
<view class="meta-left">
|
||||||
|
<!-- 时长信息 -->
|
||||||
|
<view class="meta-item time-tag">
|
||||||
|
<image src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/time.png" class="meta-icon" />
|
||||||
|
<text>{{ course.duration }}</text>
|
||||||
|
</view>
|
||||||
|
<!-- 难度信息 -->
|
||||||
|
<view class="meta-item level-tag">
|
||||||
|
<image src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/intensity.png" class="meta-icon" />
|
||||||
|
<text>{{ course.level }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- 课程标签 -->
|
||||||
|
<view class="meta-item course-type-tag">
|
||||||
|
<text :class="['tag', course.tagType]">{{ course.tag }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- 课程底部区域 -->
|
||||||
|
<view class="course-footer">
|
||||||
|
<!-- 参与人数信息 -->
|
||||||
|
<view class="participants">
|
||||||
|
<image src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/hot.png" class="fire-icon" />
|
||||||
|
<text>{{ course.participants }}人参与</text>
|
||||||
|
</view>
|
||||||
|
<!-- 去参与按钮 -->
|
||||||
|
<view class="join-btn" @click="handleJoinCourse(course)">
|
||||||
|
<text>去参与</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import CourseCard from './CourseCard.vue'
|
import { getGroupCoursePage } from '@/api/main.js'
|
||||||
|
|
||||||
// 接收父组件传递的数据
|
// 推荐课程数据列表
|
||||||
const props = defineProps({
|
const courses = ref([])
|
||||||
data: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({ list: [] })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 课程类型映射(用于显示标签)
|
// 课程类型映射(用于显示标签)
|
||||||
const getCourseTypeName = (type) => {
|
const getCourseTypeName = (type) => {
|
||||||
@@ -54,16 +94,12 @@ const getCourseTypeName = (type) => {
|
|||||||
|
|
||||||
// 根据课程信息获取标签文本
|
// 根据课程信息获取标签文本
|
||||||
const getTag = (course) => {
|
const getTag = (course) => {
|
||||||
if (course.currentMembers >= course.maxMembers) return '已满员'
|
|
||||||
if (course.status === '2') return '已结束'
|
|
||||||
if (course.currentMembers / course.maxMembers >= 0.8) return '热门'
|
if (course.currentMembers / course.maxMembers >= 0.8) return '热门'
|
||||||
return getCourseTypeName(course.courseType)
|
return getCourseTypeName(course.courseType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据课程信息获取标签样式类型
|
// 根据课程信息获取标签样式类型
|
||||||
const getTagType = (course) => {
|
const getTagType = (course) => {
|
||||||
if (course.currentMembers >= course.maxMembers) return 'full'
|
|
||||||
if (course.status === '2') return 'ended'
|
|
||||||
if (course.currentMembers / course.maxMembers >= 0.8) return 'hot'
|
if (course.currentMembers / course.maxMembers >= 0.8) return 'hot'
|
||||||
return 'default'
|
return 'default'
|
||||||
}
|
}
|
||||||
@@ -73,7 +109,9 @@ const calculateDuration = (startTime, endTime) => {
|
|||||||
if (!startTime || !endTime) return '60分钟'
|
if (!startTime || !endTime) return '60分钟'
|
||||||
const start = new Date(startTime)
|
const start = new Date(startTime)
|
||||||
const end = new Date(endTime)
|
const end = new Date(endTime)
|
||||||
const durationMinutes = Math.floor((end - start) / (1000 * 60))
|
const diffMs = end - start
|
||||||
|
if (diffMs <= 0) return ''
|
||||||
|
const durationMinutes = Math.floor(diffMs / (1000 * 60))
|
||||||
return `${durationMinutes}分钟`
|
return `${durationMinutes}分钟`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,10 +130,22 @@ const getImageUrl = (coverImage) => {
|
|||||||
return `https://your-domain.com${coverImage}`
|
return `https://your-domain.com${coverImage}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将原始课程数据转换为卡片需要的格式
|
// 获取推荐课程
|
||||||
const displayCourses = computed(() => {
|
const fetchRecommendCourses = async () => {
|
||||||
const list = props.data?.list || []
|
try {
|
||||||
return list.map(course => ({
|
const res = await getGroupCoursePage({
|
||||||
|
page: 0, size: 10, sort: 'current_members', order: 'desc'
|
||||||
|
}, { cache: true, cacheTime: 5 * 60 * 1000 })
|
||||||
|
if (res && res.content && Array.isArray(res.content)) {
|
||||||
|
// 筛选掉已结束和已满员的课程
|
||||||
|
const filteredCourses = res.content.filter(course => {
|
||||||
|
return course.status !== '2' && course.currentMembers < course.maxMembers
|
||||||
|
})
|
||||||
|
|
||||||
|
// 只取前5个符合条件的课程
|
||||||
|
courses.value = filteredCourses.slice(0, 5).map(course => {
|
||||||
|
const remainingSlots = course.maxMembers - course.currentMembers
|
||||||
|
return {
|
||||||
id: course.id,
|
id: course.id,
|
||||||
image: getImageUrl(course.coverImage),
|
image: getImageUrl(course.coverImage),
|
||||||
tag: getTag(course),
|
tag: getTag(course),
|
||||||
@@ -104,19 +154,27 @@ const displayCourses = computed(() => {
|
|||||||
duration: calculateDuration(course.startTime, course.endTime),
|
duration: calculateDuration(course.startTime, course.endTime),
|
||||||
level: getCourseLevel(course),
|
level: getCourseLevel(course),
|
||||||
participants: course.currentMembers || 0,
|
participants: course.currentMembers || 0,
|
||||||
|
remainingSlots: remainingSlots,
|
||||||
rawData: course
|
rawData: course
|
||||||
}))
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取推荐课程失败:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleJoinCourse = (course) => {
|
const handleJoinCourse = (course) => {
|
||||||
if (course.rawData.status === '2') { uni.showToast({ title: '课程已结束', icon: 'none' }); return }
|
if (course.rawData.status === '2') { uni.showToast({ title: '课程已结束', icon: 'none' }); return }
|
||||||
if (course.rawData.currentMembers >= course.rawData.maxMembers) { uni.showToast({ title: '课程已满员', icon: 'none' }); return }
|
if (course.rawData.currentMembers >= course.rawData.maxMembers) { uni.showToast({ title: '课程已满员', icon: 'none' }); return }
|
||||||
uni.navigateTo({ url: `/pages/course/detail?id=${course.id}` })
|
uni.navigateTo({ url: `/pages/groupCourse/detail?id=${course.id}` })
|
||||||
}
|
}
|
||||||
|
|
||||||
function goMore(){
|
const handleViewMore = () => {
|
||||||
uni.navigateTo({ url: '/pages/recommendCourses/index' })
|
uni.navigateTo({ url: '/pages/recommendCourses/index' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => { fetchRecommendCourses() })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@@ -158,6 +216,190 @@ function goMore(){
|
|||||||
|
|
||||||
.courses-list {
|
.courses-list {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
gap: 48rpx;
|
gap: 24rpx;
|
||||||
|
padding-right: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-card {
|
||||||
|
width: 338rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
border-radius: 24rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 8rpx 28rpx var(--shadow-blue-light);
|
||||||
|
border: 1rpx solid rgba(255, 255, 255, 0.6);
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-image {
|
||||||
|
height: 260rpx;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-overlay {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(to top, rgba(45, 74, 90, 0.7) 0%, transparent 60%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.remaining-slots {
|
||||||
|
position: absolute;
|
||||||
|
top: 16rpx;
|
||||||
|
right: 16rpx;
|
||||||
|
padding: 6rpx 12rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
background: linear-gradient(135deg, #FF6B35, #FF8F66);
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-tag {
|
||||||
|
position: absolute;
|
||||||
|
top: 16rpx;
|
||||||
|
right: 16rpx;
|
||||||
|
padding: 6rpx 12rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
background: linear-gradient(135deg, #7AB5CC, #9CCFDF);
|
||||||
|
z-index: 2;
|
||||||
|
&.hot {
|
||||||
|
background: linear-gradient(135deg, #6BA8C0, #8CC5D5);
|
||||||
|
}
|
||||||
|
&.new {
|
||||||
|
background: linear-gradient(135deg, #6DB5C8, #90CEDD);
|
||||||
|
}
|
||||||
|
&.free {
|
||||||
|
background: linear-gradient(135deg, #7AB5CC, #9CCFDF);
|
||||||
|
}
|
||||||
|
&.full {
|
||||||
|
background: linear-gradient(135deg, #A0B8C8, #B8CCD8);
|
||||||
|
}
|
||||||
|
&.ended {
|
||||||
|
background: linear-gradient(135deg, #B0C0CC, #C4D2DC);
|
||||||
|
}
|
||||||
|
&.default {
|
||||||
|
background: linear-gradient(135deg, #7AB5CC, #9CCFDF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-info {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
padding: 16rpx 20rpx;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-name {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-meta {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-left {
|
||||||
|
display: flex;
|
||||||
|
gap: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6rpx;
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
padding: 6rpx 10rpx;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
line-height: 1;
|
||||||
|
.meta-icon {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-tag {
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-tag {
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-type-tag {
|
||||||
|
background: transparent !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
.tag {
|
||||||
|
padding: 4rpx 10rpx;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
background: linear-gradient(135deg, #FF8F66, #FFB088);
|
||||||
|
color: #ffffff;
|
||||||
|
&.hot {
|
||||||
|
background: linear-gradient(135deg, #FF6B35, #FF8F66);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-icon {
|
||||||
|
width: 22rpx;
|
||||||
|
height: 22rpx;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-footer {
|
||||||
|
padding: 16rpx 20rpx;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.participants {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: var(--tabbar-text-inactive);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fire-icon {
|
||||||
|
width: 32rpx;
|
||||||
|
height: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.join-btn {
|
||||||
|
padding: 12rpx 28rpx;
|
||||||
|
background: rgba(130, 220, 130, 0.9);
|
||||||
|
border: none;
|
||||||
|
border-radius: 9999rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
box-shadow: 0 6rpx 16rpx rgba(130, 220, 130, 0.35);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<!-- 区域标题 -->
|
<!-- 区域标题 -->
|
||||||
<text class="section-title">今日推荐</text>
|
<text class="section-title">今日推荐</text>
|
||||||
<!-- 查看更多按钮 -->
|
<!-- 查看更多按钮 -->
|
||||||
<view class="view-more">
|
<view class="view-more" @click="handleViewMore">
|
||||||
<text>查看更多</text>
|
<text>查看更多</text>
|
||||||
<text class="arrow">
|
<text class="arrow">
|
||||||
<uni-icons type="right" size="20" color="#8CA0B0"></uni-icons>
|
<uni-icons type="right" size="20" color="#8CA0B0"></uni-icons>
|
||||||
@@ -50,6 +50,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
const handleViewMore = () => {
|
||||||
|
uni.navigateTo({ url: '/pages/discover/index' })
|
||||||
|
}
|
||||||
|
|
||||||
// 今日推荐数据列表
|
// 今日推荐数据列表
|
||||||
const recommends = [
|
const recommends = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,62 +10,50 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref, watch, onMounted, nextTick } from 'vue'
|
||||||
import { drawRadarChart } from '@/common/memberInfo/bodyTestChart.js'
|
import { drawRadarChart } from '@/common/memberInfo/bodyTestChart.js'
|
||||||
|
|
||||||
export default {
|
const props = defineProps({
|
||||||
options: {
|
|
||||||
virtualHost: false,
|
|
||||||
styleIsolation: 'apply-shared'
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
labels: { type: Array, default: () => [] },
|
labels: { type: Array, default: () => [] },
|
||||||
values: { type: Array, default: () => [] },
|
values: { type: Array, default: () => [] },
|
||||||
width: { type: Number, default: 280 },
|
width: { type: Number, default: 280 },
|
||||||
height: { type: Number, default: 240 }
|
height: { type: Number, default: 240 }
|
||||||
},
|
})
|
||||||
data() {
|
|
||||||
return {
|
const canvasId = ref(`bt-radar-${Math.random().toString(36).slice(2, 9)}`)
|
||||||
canvasId: `bt-radar-${Math.random().toString(36).slice(2, 9)}`
|
|
||||||
}
|
function renderChart() {
|
||||||
},
|
nextTick(() => {
|
||||||
watch: {
|
const query = uni.createSelectorQuery().in(getCurrentInstance())
|
||||||
values: {
|
|
||||||
deep: true,
|
|
||||||
handler() {
|
|
||||||
this.renderChart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.renderChart()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
renderChart() {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
const query = uni.createSelectorQuery().in(this)
|
|
||||||
query
|
query
|
||||||
.select(`#${this.canvasId}`)
|
.select(`#${canvasId.value}`)
|
||||||
.fields({ node: true, size: true })
|
.fields({ node: true, size: true })
|
||||||
.exec((res) => {
|
.exec((res) => {
|
||||||
const node = res?.[0]?.node
|
const node = res?.[0]?.node
|
||||||
if (!node) return
|
if (!node) return
|
||||||
const dpr = uni.getSystemInfoSync().pixelRatio || 1
|
const dpr = uni.getSystemInfoSync().pixelRatio || 1
|
||||||
drawRadarChart(node, {
|
drawRadarChart(node, {
|
||||||
width: this.width,
|
width: props.width,
|
||||||
height: this.height,
|
height: props.height,
|
||||||
labels: this.labels,
|
labels: props.labels,
|
||||||
values: this.values,
|
values: props.values,
|
||||||
dpr
|
dpr
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
watch(() => props.values, () => {
|
||||||
|
renderChart()
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
renderChart()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
.bt-radar {
|
.bt-radar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -10,62 +10,50 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref, watch, onMounted, nextTick } from 'vue'
|
||||||
import { drawTrendChart } from '@/common/memberInfo/bodyTestChart.js'
|
import { drawTrendChart } from '@/common/memberInfo/bodyTestChart.js'
|
||||||
|
|
||||||
export default {
|
const props = defineProps({
|
||||||
options: {
|
|
||||||
virtualHost: false,
|
|
||||||
styleIsolation: 'apply-shared'
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
points: { type: Array, default: () => [] },
|
points: { type: Array, default: () => [] },
|
||||||
unit: { type: String, default: '' },
|
unit: { type: String, default: '' },
|
||||||
width: { type: Number, default: 300 },
|
width: { type: Number, default: 300 },
|
||||||
height: { type: Number, default: 160 }
|
height: { type: Number, default: 160 }
|
||||||
},
|
})
|
||||||
data() {
|
|
||||||
return {
|
const canvasId = ref(`bt-trend-${Math.random().toString(36).slice(2, 9)}`)
|
||||||
canvasId: `bt-trend-${Math.random().toString(36).slice(2, 9)}`
|
|
||||||
}
|
function renderChart() {
|
||||||
},
|
nextTick(() => {
|
||||||
watch: {
|
const query = uni.createSelectorQuery().in(getCurrentInstance())
|
||||||
points: {
|
|
||||||
deep: true,
|
|
||||||
handler() {
|
|
||||||
this.renderChart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.renderChart()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
renderChart() {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
const query = uni.createSelectorQuery().in(this)
|
|
||||||
query
|
query
|
||||||
.select(`#${this.canvasId}`)
|
.select(`#${canvasId.value}`)
|
||||||
.fields({ node: true, size: true })
|
.fields({ node: true, size: true })
|
||||||
.exec((res) => {
|
.exec((res) => {
|
||||||
const node = res?.[0]?.node
|
const node = res?.[0]?.node
|
||||||
if (!node) return
|
if (!node) return
|
||||||
const dpr = uni.getSystemInfoSync().pixelRatio || 1
|
const dpr = uni.getSystemInfoSync().pixelRatio || 1
|
||||||
drawTrendChart(node, {
|
drawTrendChart(node, {
|
||||||
width: this.width,
|
width: props.width,
|
||||||
height: this.height,
|
height: props.height,
|
||||||
points: this.points,
|
points: props.points,
|
||||||
unit: this.unit,
|
unit: props.unit,
|
||||||
dpr
|
dpr
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
watch(() => props.points, () => {
|
||||||
|
renderChart()
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
renderChart()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
.bt-trend {
|
.bt-trend {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -102,13 +102,8 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
defineProps({
|
||||||
options: {
|
|
||||||
virtualHost: false,
|
|
||||||
styleIsolation: 'apply-shared'
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
report: {
|
report: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({
|
default: () => ({
|
||||||
@@ -121,12 +116,12 @@ export default {
|
|||||||
change: '-1.2kg'
|
change: '-1.2kg'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
emits: ['view-history', 'view-report']
|
|
||||||
}
|
defineEmits(['view-history', 'view-report'])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||||
@import '@/common/style/memberInfo/member-info-body-report.css';
|
@import '@/common/style/memberInfo/member-info-body-report.css';
|
||||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||||
|
|||||||
@@ -77,28 +77,24 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
import { computed } from 'vue'
|
||||||
options: {
|
|
||||||
virtualHost: false,
|
const props = defineProps({
|
||||||
styleIsolation: 'apply-shared'
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
items: {
|
items: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
emits: ['view-all', 'item-tap'],
|
|
||||||
computed: {
|
defineEmits(['view-all', 'item-tap'])
|
||||||
previewItems() {
|
|
||||||
return this.items
|
const previewItems = computed(() => {
|
||||||
}
|
return props.items
|
||||||
}
|
})
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||||
@import '@/common/style/memberInfo/member-info-booking-list.css';
|
@import '@/common/style/memberInfo/member-info-booking-list.css';
|
||||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||||
|
|||||||
@@ -64,23 +64,18 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
defineProps({
|
||||||
options: {
|
|
||||||
virtualHost: false,
|
|
||||||
styleIsolation: 'apply-shared'
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
items: {
|
items: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
emits: ['view-all', 'item-tap']
|
|
||||||
}
|
defineEmits(['view-all', 'item-tap'])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||||
@import '@/common/style/memberInfo/member-info-check-in-list.css';
|
@import '@/common/style/memberInfo/member-info-check-in-list.css';
|
||||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||||
|
|||||||
@@ -55,13 +55,8 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
defineProps({
|
||||||
options: {
|
|
||||||
virtualHost: false,
|
|
||||||
styleIsolation: 'apply-shared'
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
data: {
|
data: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({
|
default: () => ({
|
||||||
@@ -73,12 +68,12 @@ export default {
|
|||||||
pointsAction: '去兑换'
|
pointsAction: '去兑换'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
emits: ['view-all', 'use-coupon', 'redeem-points']
|
|
||||||
}
|
defineEmits(['view-all', 'use-coupon', 'redeem-points'])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||||
@import '@/common/style/memberInfo/member-info-coupon-points.css';
|
@import '@/common/style/memberInfo/member-info-coupon-points.css';
|
||||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||||
|
|||||||
@@ -22,8 +22,8 @@
|
|||||||
<view class="profile-header__toolbar-spacer" :style="toolbarSpacerStyle"></view>
|
<view class="profile-header__toolbar-spacer" :style="toolbarSpacerStyle"></view>
|
||||||
<!-- 用户信息区:深蓝渐变 -->
|
<!-- 用户信息区:深蓝渐变 -->
|
||||||
<view class="profile-header__hero">
|
<view class="profile-header__hero">
|
||||||
<view class="profile-header__inner">
|
<view class="profile-header__inner" v-if="isLogin">
|
||||||
<view class="profile-header__user" hover-class="mi-tap--hover" :hover-stay-time="150" @tap="$emit('user-info')">
|
<view class="profile-header__user" hover-class="mi-tap--hover" :hover-stay-time="150" @tap="handleUserTap">
|
||||||
<view class="profile-header__user-inner">
|
<view class="profile-header__user-inner">
|
||||||
<view class="profile-header__avatar-wrap">
|
<view class="profile-header__avatar-wrap">
|
||||||
<view class="profile-header__avatar-ring">
|
<view class="profile-header__avatar-ring">
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
</view>
|
</view>
|
||||||
<view class="profile-header__user-meta">
|
<view class="profile-header__user-meta">
|
||||||
<view class="profile-header__user-meta-inner">
|
<view class="profile-header__user-meta-inner">
|
||||||
<text class="profile-header__name">{{ userInfo.name }}</text>
|
<text class="profile-header__name">{{ userInfo.name || "活氧舱用户" }}</text>
|
||||||
<text class="profile-header__phone">{{ userInfo.phone }}</text>
|
<text class="profile-header__phone">{{ userInfo.phone }}</text>
|
||||||
<view class="profile-header__badge">
|
<view class="profile-header__badge">
|
||||||
<image
|
<image
|
||||||
@@ -82,69 +82,95 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
</view>
|
||||||
|
<view class="profile-header__inner" v-else>
|
||||||
|
<view class="profile-header__user" hover-class="mi-tap--hover" :hover-stay-time="150" @tap="handleUserTap">
|
||||||
|
<view
|
||||||
|
class="profile-header__user-inner--guest"
|
||||||
|
@tap="handleGuestLogin"
|
||||||
|
>
|
||||||
|
<view class="profile-header__avatar-wrap--guest">
|
||||||
|
<image
|
||||||
|
class="profile-header__avatar--guest"
|
||||||
|
src="/static/OIP-C.png"
|
||||||
|
mode="aspectFill"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="profile-header__user-meta--guest">
|
||||||
|
<text class="profile-header__name--guest">小伙伴,点我登录</text>
|
||||||
|
<text class="profile-header__phone--guest">陪你遇见更好的自己 ~</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
import { ref, computed, onMounted } from 'vue'
|
||||||
options: {
|
|
||||||
virtualHost: false,
|
const props = defineProps({
|
||||||
styleIsolation: 'apply-shared'
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
userInfo: { type: Object, required: true },
|
userInfo: { type: Object, required: true },
|
||||||
stats: { type: Object, required: true }
|
stats: { type: Object, required: true },
|
||||||
},
|
isLogin: { type: Boolean }
|
||||||
emits: ['user-info'],
|
})
|
||||||
computed: {
|
|
||||||
displayAvatar() {
|
|
||||||
return this.userInfo.avatar || 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AvatarEditWrap.png'
|
const emit = defineEmits(['user-info', 'guest-login'])
|
||||||
|
|
||||||
|
const displayAvatar = computed(() => {
|
||||||
|
return props.userInfo.avatar || '/static/OC-default-headIamge.png'
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleUserTap() {
|
||||||
|
if (props.isLogin) {
|
||||||
|
emit('user-info')
|
||||||
}
|
}
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
toolbarStyle: {},
|
|
||||||
toolbarSpacerStyle: {},
|
|
||||||
navRightStyle: {}
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
mounted() {
|
function handleGuestLogin() {
|
||||||
this.syncNavSafeArea()
|
emit('guest-login')
|
||||||
},
|
}
|
||||||
methods: {
|
|
||||||
syncNavSafeArea() {
|
const toolbarStyle = ref({})
|
||||||
|
const toolbarSpacerStyle = ref({})
|
||||||
|
const navRightStyle = ref({})
|
||||||
|
|
||||||
|
function syncNavSafeArea() {
|
||||||
try {
|
try {
|
||||||
const sys = uni.getSystemInfoSync()
|
const sys = uni.getSystemInfoSync()
|
||||||
const statusBarHeight = sys.statusBarHeight || 0
|
const statusBarHeight = sys.statusBarHeight || 0
|
||||||
const navHeight = 44
|
const navHeight = 44
|
||||||
const menu = uni.getMenuButtonBoundingClientRect?.()
|
const menu = uni.getMenuButtonBoundingClientRect?.()
|
||||||
|
|
||||||
this.toolbarStyle = {
|
toolbarStyle.value = {
|
||||||
paddingTop: `${statusBarHeight}px`
|
paddingTop: `${statusBarHeight}px`
|
||||||
}
|
}
|
||||||
|
|
||||||
this.toolbarSpacerStyle = {
|
toolbarSpacerStyle.value = {
|
||||||
height: `${statusBarHeight + navHeight}px`
|
height: `${statusBarHeight + navHeight}px`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (menu && menu.width) {
|
if (menu && menu.width) {
|
||||||
const capsuleGap = sys.windowWidth - menu.left + 8
|
const capsuleGap = sys.windowWidth - menu.left + 8
|
||||||
this.navRightStyle = {
|
navRightStyle.value = {
|
||||||
width: `${capsuleGap}px`,
|
width: `${capsuleGap}px`,
|
||||||
minWidth: `${capsuleGap}px`
|
minWidth: `${capsuleGap}px`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.toolbarSpacerStyle = { height: '44px' }
|
toolbarSpacerStyle.value = { height: '44px' }
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
syncNavSafeArea()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||||
@import '@/common/style/memberInfo/member-info-header.css';
|
@import '@/common/style/memberInfo/member-info-header.css';
|
||||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||||
|
|||||||
@@ -16,16 +16,10 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
defineEmits(['logout'])
|
||||||
options: {
|
|
||||||
virtualHost: false,
|
|
||||||
styleIsolation: 'apply-shared'
|
|
||||||
},
|
|
||||||
emits: ['logout']
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
@import '@/common/style/memberInfo/member-info-logout.css';
|
@import '@/common/style/memberInfo/member-info-logout.css';
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -94,6 +94,18 @@
|
|||||||
续费
|
续费
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
|
<view
|
||||||
|
class="member-card-preview__purchase"
|
||||||
|
hover-class="mi-tap-btn--hover"
|
||||||
|
:hover-stay-time="150"
|
||||||
|
@tap.stop="$emit('purchase')"
|
||||||
|
>
|
||||||
|
<text
|
||||||
|
class="member-card-preview__purchase-text"
|
||||||
|
>
|
||||||
|
购买新卡
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -115,20 +127,15 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
defineProps({
|
||||||
options: {
|
|
||||||
virtualHost: false,
|
|
||||||
styleIsolation: 'apply-shared'
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
cardInfo: { type: Object, required: true }
|
cardInfo: { type: Object, required: true }
|
||||||
},
|
})
|
||||||
emits: ['view-all', 'renew'],
|
|
||||||
}
|
defineEmits(['view-all', 'renew', 'purchase'])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||||
@import '@/common/style/memberInfo/member-info-member-card.css';
|
@import '@/common/style/memberInfo/member-info-member-card.css';
|
||||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||||
|
|||||||
@@ -83,33 +83,27 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
import { ref } from 'vue'
|
||||||
options: {
|
|
||||||
virtualHost: false,
|
defineEmits(['action'])
|
||||||
styleIsolation: 'apply-shared'
|
|
||||||
},
|
const row1 = ref([
|
||||||
emits: ['action'],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
row1: [
|
|
||||||
{ key: 'booking', label: '预约课程', textClass: 'quick-actions__title', icon: '' },
|
{ key: 'booking', label: '预约课程', textClass: 'quick-actions__title', icon: '' },
|
||||||
{ key: 'bodyTest', label: '智能体测', textClass: 'quick-actions__title-2', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/mappin2.png' },
|
{ key: 'bodyTest', label: '智能体测', textClass: 'quick-actions__title-2', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/mappin2.png' },
|
||||||
{ key: 'bodyReport', label: '体测报告', textClass: 'quick-actions__title-3', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/activity.png' },
|
{ key: 'bodyReport', label: '体测报告', textClass: 'quick-actions__title-3', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/activity.png' },
|
||||||
{ key: 'trainReport', label: '训练报告', textClass: 'quick-actions__coach', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/usercheck.png' }
|
{ key: 'trainReport', label: '训练报告', textClass: 'quick-actions__coach', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/usercheck.png' }
|
||||||
],
|
])
|
||||||
row2: [
|
|
||||||
|
const row2 = ref([
|
||||||
{ key: 'coupon', label: '我的优惠券', textClass: 'quick-actions__text', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/ticket.png' },
|
{ key: 'coupon', label: '我的优惠券', textClass: 'quick-actions__text', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/ticket.png' },
|
||||||
{ key: 'points', label: '我的积分', textClass: 'quick-actions__points-desc', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/star.png' },
|
{ key: 'points', label: '我的积分', textClass: 'quick-actions__points-desc', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/star.png' },
|
||||||
{ key: 'referral', label: '邀请好友', textClass: 'quick-actions__title-4', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/share2.png' },
|
{ key: 'referral', label: '邀请好友', textClass: 'quick-actions__title-4', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/share2.png' },
|
||||||
{ key: 'course', label: '我的课程', textClass: 'quick-actions__text-2', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/play.png' }
|
{ key: 'course', label: '我的课程', textClass: 'quick-actions__text-2', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/play.png' }
|
||||||
]
|
])
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||||
@import '@/common/style/memberInfo/member-info-quick-actions.css';
|
@import '@/common/style/memberInfo/member-info-quick-actions.css';
|
||||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||||
|
|||||||
@@ -63,13 +63,10 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
import { ref } from 'vue'
|
||||||
options: {
|
|
||||||
virtualHost: false,
|
const props = defineProps({
|
||||||
styleIsolation: 'apply-shared'
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
data: {
|
data: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({
|
default: () => ({
|
||||||
@@ -79,22 +76,21 @@ export default {
|
|||||||
purchased: 2
|
purchased: 2
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
emits: ['view-rules'],
|
|
||||||
methods: {
|
defineEmits(['view-rules'])
|
||||||
copyCode() {
|
|
||||||
|
function copyCode() {
|
||||||
uni.setClipboardData({
|
uni.setClipboardData({
|
||||||
data: this.data.code,
|
data: props.data.code,
|
||||||
success: () => {
|
success: () => {
|
||||||
uni.showToast({ title: '已复制', icon: 'success' })
|
uni.showToast({ title: '已复制', icon: 'success' })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||||
@import '@/common/style/memberInfo/member-info-referral.css';
|
@import '@/common/style/memberInfo/member-info-referral.css';
|
||||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||||
|
|||||||
@@ -55,16 +55,12 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
import { ref } from 'vue'
|
||||||
options: {
|
|
||||||
virtualHost: false,
|
defineEmits(['setting'])
|
||||||
styleIsolation: 'apply-shared'
|
|
||||||
},
|
const items = ref([
|
||||||
emits: ['setting'],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
items: [
|
|
||||||
{
|
{
|
||||||
key: 'notify',
|
key: 'notify',
|
||||||
label: '通知设置',
|
label: '通知设置',
|
||||||
@@ -97,13 +93,10 @@ export default {
|
|||||||
iconWrapClass: 'settings-section__item-icon-wrap--red',
|
iconWrapClass: 'settings-section__item-icon-wrap--red',
|
||||||
labelClass: 'settings-section__item-label--danger'
|
labelClass: 'settings-section__item-label--danger'
|
||||||
}
|
}
|
||||||
]
|
])
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||||
@import '@/common/style/memberInfo/member-info-settings.css';
|
@import '@/common/style/memberInfo/member-info-settings.css';
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -7,14 +7,8 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
defineProps({
|
||||||
options: {
|
|
||||||
virtualHost: true,
|
|
||||||
styleIsolation: 'apply-shared'
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
statusBarTime: { type: String, default: '9:41' }
|
statusBarTime: { type: String, default: '9:41' }
|
||||||
},
|
})
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -27,80 +27,75 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
import { ref, onMounted, nextTick } from 'vue'
|
||||||
options: {
|
|
||||||
virtualHost: false,
|
const props = defineProps({
|
||||||
styleIsolation: 'apply-shared'
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
title: { type: String, required: true },
|
title: { type: String, required: true },
|
||||||
rightText: { type: String, default: '' },
|
rightText: { type: String, default: '' },
|
||||||
actionButton: { type: Boolean, default: false }
|
actionButton: { type: Boolean, default: false }
|
||||||
},
|
|
||||||
emits: ['back', 'right-action'],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
toolbarStyle: {},
|
|
||||||
toolbarSpacerStyle: {},
|
|
||||||
capsuleStyle: {},
|
|
||||||
isH5: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.syncNavSafeArea()
|
|
||||||
this.$nextTick(() => {
|
|
||||||
setTimeout(() => this.syncNavSafeArea(), 50)
|
|
||||||
})
|
})
|
||||||
},
|
|
||||||
methods: {
|
defineEmits(['back', 'right-action'])
|
||||||
syncNavSafeArea() {
|
|
||||||
|
const toolbarStyle = ref({})
|
||||||
|
const toolbarSpacerStyle = ref({})
|
||||||
|
const capsuleStyle = ref({})
|
||||||
|
const isH5 = ref(false)
|
||||||
|
|
||||||
|
function syncNavSafeArea() {
|
||||||
try {
|
try {
|
||||||
const sys = uni.getSystemInfoSync()
|
const sys = uni.getSystemInfoSync()
|
||||||
const statusBarHeight = sys.statusBarHeight || 0
|
const statusBarHeight = sys.statusBarHeight || 0
|
||||||
const navHeight = 44
|
const navHeight = 44
|
||||||
const extraGap = 4
|
const extraGap = 4
|
||||||
const menu = uni.getMenuButtonBoundingClientRect?.()
|
const menu = uni.getMenuButtonBoundingClientRect?.()
|
||||||
|
|
||||||
// #ifdef H5
|
// #ifdef H5
|
||||||
this.isH5 = true
|
isH5.value = true
|
||||||
// #endif
|
// #endif
|
||||||
// #ifndef H5
|
// #ifndef H5
|
||||||
this.isH5 = false
|
isH5.value = false
|
||||||
// #endif
|
// #endif
|
||||||
if (!this.isH5 && typeof window !== 'undefined' && !menu?.width) {
|
if (!isH5.value && typeof window !== 'undefined' && !menu?.width) {
|
||||||
this.isH5 = sys.uniPlatform === 'web' || sys.platform === 'web'
|
isH5.value = sys.uniPlatform === 'web' || sys.platform === 'web'
|
||||||
}
|
}
|
||||||
|
|
||||||
this.toolbarStyle = {
|
toolbarStyle.value = {
|
||||||
paddingTop: `${statusBarHeight}px`
|
paddingTop: `${statusBarHeight}px`
|
||||||
}
|
}
|
||||||
|
|
||||||
this.toolbarSpacerStyle = {
|
toolbarSpacerStyle.value = {
|
||||||
height: `${statusBarHeight + navHeight + extraGap}px`
|
height: `${statusBarHeight + navHeight + extraGap}px`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.isH5 && menu && menu.width) {
|
if (!isH5.value && menu && menu.width) {
|
||||||
const capsuleGap = sys.windowWidth - menu.left + 8
|
const capsuleGap = sys.windowWidth - menu.left + 8
|
||||||
this.capsuleStyle = {
|
capsuleStyle.value = {
|
||||||
width: `${capsuleGap}px`,
|
width: `${capsuleGap}px`,
|
||||||
minWidth: `${capsuleGap}px`
|
minWidth: `${capsuleGap}px`
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.capsuleStyle = {
|
capsuleStyle.value = {
|
||||||
width: '0px',
|
width: '0px',
|
||||||
minWidth: '0px'
|
minWidth: '0px'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.toolbarSpacerStyle = { height: '44px' }
|
toolbarSpacerStyle.value = { height: '44px' }
|
||||||
this.isH5 = true
|
isH5.value = true
|
||||||
this.capsuleStyle = { width: '0px', minWidth: '0px' }
|
capsuleStyle.value = { width: '0px', minWidth: '0px' }
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
syncNavSafeArea()
|
||||||
|
nextTick(() => {
|
||||||
|
setTimeout(() => syncNavSafeArea(), 50)
|
||||||
|
})
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
@import '@/common/style/memberInfo/member-info-sub-nav.css';
|
@import '@/common/style/memberInfo/member-info-sub-nav.css';
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -17,7 +17,9 @@
|
|||||||
"delay" : 0
|
"delay" : 0
|
||||||
},
|
},
|
||||||
/* 模块配置 */
|
/* 模块配置 */
|
||||||
"modules" : {},
|
"modules" : {
|
||||||
|
"OAuth" : {}
|
||||||
|
},
|
||||||
/* 应用发布信息 */
|
/* 应用发布信息 */
|
||||||
"distribute" : {
|
"distribute" : {
|
||||||
/* android打包配置 */
|
/* android打包配置 */
|
||||||
@@ -45,7 +47,14 @@
|
|||||||
"dSYMs" : false
|
"dSYMs" : false
|
||||||
},
|
},
|
||||||
/* SDK配置 */
|
/* SDK配置 */
|
||||||
"sdkConfigs" : {},
|
"sdkConfigs" : {
|
||||||
|
"oauth" : {
|
||||||
|
"weixin" : {
|
||||||
|
"appid" : "wx8f0d644d1d8985f6",
|
||||||
|
"UniversalLinks" : ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"icons" : {
|
"icons" : {
|
||||||
"android" : {
|
"android" : {
|
||||||
"hdpi" : "unpackage/res/icons/72x72.png",
|
"hdpi" : "unpackage/res/icons/72x72.png",
|
||||||
|
|||||||
@@ -53,6 +53,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/memberInfo/login",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "登录"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/memberInfo/memberCard",
|
"path": "pages/memberInfo/memberCard",
|
||||||
"style": {
|
"style": {
|
||||||
@@ -207,6 +214,13 @@
|
|||||||
"navigationBarTitleText": "签到记录"
|
"navigationBarTitleText": "签到记录"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/memberInfo/purchaseCard",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "购买会员卡"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/memberInfo/courseList",
|
"path": "pages/memberInfo/courseList",
|
||||||
"style": {
|
"style": {
|
||||||
@@ -280,6 +294,25 @@
|
|||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": ""
|
"navigationBarTitleText": ""
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/message/index",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/message/detail",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "消息详情"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/webview/webview",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"globalStyle": {
|
"globalStyle": {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
<image :src="image" mode="" :style="{width: Math.min(width, 500) + 'rpx',height: Math.min(height, 500) + 'rpx' } "></image>
|
<image :src="image" mode="" :style="{width: Math.min(width, 500) + 'rpx',height: Math.min(height, 500) + 'rpx' } "></image>
|
||||||
</view>
|
</view>
|
||||||
<!-- 加载动画区域 - 设置最小尺寸确保进入页面时可见 -->
|
<!-- 加载动画区域 - 设置最小尺寸确保进入页面时可见 -->
|
||||||
<view v-if="!image || STQRC" class="loadingBox" :style="{width: (width > 0 ? Math.min(width, 500) : 500) + 'rpx',height: (height > 0 ? Math.min(height, 500) : 500) + 'rpx' }">
|
<view v-if="(!image || STQRC) && isHaveCard" class="loadingBox" :style="{width: (width > 0 ? Math.min(width, 500) : 500) + 'rpx',height: (height > 0 ? Math.min(height, 500) : 500) + 'rpx' }">
|
||||||
<view class="loading-spinner">
|
<view class="loading-spinner">
|
||||||
<view v-if="!isCheckIn" class="spinner-circle"></view>
|
<view v-if="!isCheckIn" class="spinner-circle"></view>
|
||||||
<view v-else>
|
<view v-else>
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 二维码装饰边框 -->
|
<!-- 二维码装饰边框 -->
|
||||||
<view v-else class="qr-border" :style="{width: Math.min(width, 500) + 80 + 'rpx',height: Math.min(height, 500) + 80 + 'rpx' }">
|
<view v-else-if="image" class="qr-border" :style="{width: Math.min(width, 500) + 80 + 'rpx',height: Math.min(height, 500) + 80 + 'rpx' }">
|
||||||
<view class="corner top-left"></view>
|
<view class="corner top-left"></view>
|
||||||
<view class="corner top-right"></view>
|
<view class="corner top-right"></view>
|
||||||
<view class="corner bottom-left"></view>
|
<view class="corner bottom-left"></view>
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
|
|
||||||
<!-- 状态组件(传递状态和自定义错误文案)- 已签到时不显示 -->
|
<!-- 状态组件(传递状态和自定义错误文案)- 已签到时不显示 -->
|
||||||
<QrStatus
|
<QrStatus
|
||||||
v-if="!isCheckIn"
|
v-if="isHaveCard && !isCheckIn"
|
||||||
:status="status"
|
:status="status"
|
||||||
:errorText="errorText"
|
:errorText="errorText"
|
||||||
/>
|
/>
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 底部按钮(仅保留刷新二维码,隐藏签到和清除缓存按钮) -->
|
<!-- 底部按钮 -->
|
||||||
<view class="bottom-actions">
|
<view class="bottom-actions">
|
||||||
<button class="btn-refresh" @tap="refreshQR">
|
<button class="btn-refresh" @tap="refreshQR">
|
||||||
<uni-icons type="refresh" size="36rpx" color="#5E6F8D"></uni-icons>
|
<uni-icons type="refresh" size="36rpx" color="#5E6F8D"></uni-icons>
|
||||||
@@ -97,9 +97,7 @@ import { ref } from 'vue';
|
|||||||
import { onLoad, onUnload } from '@dcloudio/uni-app'
|
import { onLoad, onUnload } from '@dcloudio/uni-app'
|
||||||
// 引入状态组件(路径与你保持一致)
|
// 引入状态组件(路径与你保持一致)
|
||||||
import QrStatus from '@/components/QRCode/StatusCard.vue'
|
import QrStatus from '@/components/QRCode/StatusCard.vue'
|
||||||
|
import { getQRCode, getMyMemberCards } from '@/api/main.js'
|
||||||
// 测试模式配置
|
|
||||||
const TEST_MODE = true // 开启测试模式
|
|
||||||
|
|
||||||
let image = ref("")
|
let image = ref("")
|
||||||
let width = ref(0)
|
let width = ref(0)
|
||||||
@@ -110,6 +108,7 @@ import QrStatus from '@/components/QRCode/StatusCard.vue'
|
|||||||
const QRStatus = ref("生成中...")
|
const QRStatus = ref("生成中...")
|
||||||
const STQRC = ref(false)//是否扫码
|
const STQRC = ref(false)//是否扫码
|
||||||
const isCheckIn = ref(false)
|
const isCheckIn = ref(false)
|
||||||
|
const isHaveCard = ref(true)
|
||||||
const webSoketURL = "ws://localhost:8084/webSocket/checkIn"
|
const webSoketURL = "ws://localhost:8084/webSocket/checkIn"
|
||||||
|
|
||||||
const qrcode = ref("")
|
const qrcode = ref("")
|
||||||
@@ -204,90 +203,65 @@ import QrStatus from '@/components/QRCode/StatusCard.vue'
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 清除所有与签到相关的缓存(用于测试阶段)
|
|
||||||
*/
|
onLoad(async () => {
|
||||||
const clearQRCache = () => {
|
uni.showLoading({
|
||||||
|
title: '加载中...',
|
||||||
|
mask: true
|
||||||
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const keys = uni.getStorageInfoSync().keys || []
|
const memberInfo = uni.getStorageSync('memberInfo')
|
||||||
let clearedCount = 0
|
console.log(memberInfo)
|
||||||
for (const key of keys) {
|
const memberId = memberInfo?.id || memberInfo?.memberId
|
||||||
// 清除 QR_ 开头的缓存(页面内部缓存)
|
console.log('[签到页面] 会员ID:', memberId)
|
||||||
if (key.startsWith(CACHE_PREFIX)) {
|
|
||||||
uni.removeStorageSync(key)
|
if (!memberId) {
|
||||||
clearedCount++
|
throw new Error('未获取到会员ID')
|
||||||
}
|
|
||||||
// 清除 API_CACHE_ 开头的缓存(通过 utils/cache.js 缓存的接口数据)
|
|
||||||
if (key.startsWith('API_CACHE_')) {
|
|
||||||
// 只清除与签到相关的 API 缓存
|
|
||||||
if (key.includes('checkIn') || key.includes('qrcode')) {
|
|
||||||
uni.removeStorageSync(key)
|
|
||||||
clearedCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log(`已清除 ${clearedCount} 个签到相关缓存`)
|
|
||||||
return clearedCount
|
|
||||||
} catch (e) {
|
|
||||||
console.error('清除 QR 缓存失败:', e)
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const cardRes = await getMyMemberCards(memberId)
|
||||||
* 测试用:手动清除缓存按钮点击事件
|
console.log('[签到页面] 查询会员卡结果:', JSON.stringify(cardRes, null, 2))
|
||||||
*/
|
|
||||||
const handleClearCache = () => {
|
const hasCard = cardRes && cardRes.data && cardRes.data.length > 0
|
||||||
|
console.log('[签到页面] 查询会员卡 - hasCard:', hasCard)
|
||||||
|
|
||||||
|
if (!hasCard) {
|
||||||
|
isHaveCard.value = false
|
||||||
|
uni.hideLoading()
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '清除缓存',
|
title: '提示',
|
||||||
content: '确定要清除所有签到相关的缓存吗?(测试用)',
|
content: '您还没有会员卡,无法进行签到。是否去购买会员卡?',
|
||||||
confirmText: '确定',
|
confirmText: '去购买',
|
||||||
cancelText: '取消',
|
cancelText: '取消',
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (res.confirm) {
|
if (res.confirm) {
|
||||||
const clearedCount = clearQRCache()
|
uni.redirectTo({
|
||||||
// 重置页面状态
|
url: '/pages/memberInfo/purchaseCard'
|
||||||
image.value = ""
|
|
||||||
width.value = 0
|
|
||||||
height.value = 0
|
|
||||||
status.value = 'loading'
|
|
||||||
QRStatus.value = "生成中..."
|
|
||||||
STQRC.value = false
|
|
||||||
isCheckIn.value = false
|
|
||||||
|
|
||||||
uni.showToast({
|
|
||||||
title: `已清除 ${clearedCount} 个缓存`,
|
|
||||||
icon: 'success',
|
|
||||||
duration: 2000
|
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
// 重置页面状态后不再自动请求二维码
|
uni.switchTab({
|
||||||
uni.hideLoading()
|
url: '/pages/index/index'
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
uni.switchTab({
|
||||||
|
url: '/pages/index/index'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
onLoad(() => {
|
isHaveCard.value = true
|
||||||
// 测试模式下不显示全局loading,让页面内的加载动画显示
|
|
||||||
if (!TEST_MODE) {
|
|
||||||
uni.showLoading({
|
uni.showLoading({
|
||||||
title: '生成签到二维码...',
|
title: '生成签到二维码...',
|
||||||
mask: true
|
mask: true
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
// 测试模式:直接生成假二维码,内容为"欢迎来到活氧舱"
|
|
||||||
if (TEST_MODE) {
|
|
||||||
console.log("测试模式:生成假二维码")
|
|
||||||
generateTestQRCode()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取签到状态缓存(自动检查过期)
|
|
||||||
// isCheckIn 代表签到状态,从缓存读取
|
|
||||||
const cachedIsCheckIn = getCacheData("isCheckIn")
|
const cachedIsCheckIn = getCacheData("isCheckIn")
|
||||||
// checkInTime 代表具体签到时间,从缓存读取(显示在 loading-text 中)
|
|
||||||
const cachedCheckInTime = getCacheData("checkInTime")
|
const cachedCheckInTime = getCacheData("checkInTime")
|
||||||
|
|
||||||
if(cachedIsCheckIn != null) {
|
if(cachedIsCheckIn != null) {
|
||||||
@@ -296,106 +270,33 @@ import QrStatus from '@/components/QRCode/StatusCard.vue'
|
|||||||
STQRC.value = true
|
STQRC.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果已经签到成功,直接显示成功状态,不需要请求后端
|
|
||||||
if(isCheckIn.value) {
|
if(isCheckIn.value) {
|
||||||
console.log("已签到且有缓存,无需请求后端")
|
console.log("已签到且有缓存,无需请求后端")
|
||||||
// 读取二维码图片缓存用于显示
|
|
||||||
const cachedQRInfo = getCacheData("QRInfo")
|
const cachedQRInfo = getCacheData("QRInfo")
|
||||||
if(cachedQRInfo) {
|
if(cachedQRInfo) {
|
||||||
image.value = cachedQRInfo.qrCodeBase64
|
image.value = cachedQRInfo.qrCodeBase64
|
||||||
width.value = cachedQRInfo.width * 2
|
width.value = cachedQRInfo.width * 2
|
||||||
height.value = cachedQRInfo.height * 2
|
height.value = cachedQRInfo.height * 2
|
||||||
}
|
}
|
||||||
// QRStatus 显示具体签到时间(从缓存读取,显示在 loading-text 中)
|
|
||||||
QRStatus.value = cachedCheckInTime || "已完成签到"
|
QRStatus.value = cachedCheckInTime || "已完成签到"
|
||||||
uni.hideLoading()
|
uni.hideLoading()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 未签到或缓存失效,需要请求后端获取二维码
|
|
||||||
// QRStatus 重置为默认的请求状态
|
|
||||||
QRStatus.value = "生成中..."
|
QRStatus.value = "生成中..."
|
||||||
getStorage(null)
|
getStorage(null)
|
||||||
})
|
} catch (err) {
|
||||||
|
console.error('[签到页面] 查询会员卡失败:', err)
|
||||||
// 测试模式:生成假二维码(内容为"欢迎来到活氧舱")
|
|
||||||
// 不发送请求,使用缓存机制避免重复请求,二维码内容保持不变
|
|
||||||
// @param {boolean} isRefresh - 是否是刷新操作(true=点击刷新按钮,false=首次进入页面)
|
|
||||||
const generateTestQRCode = (isRefresh = false) => {
|
|
||||||
// 检查是否有缓存的二维码图片
|
|
||||||
const cachedQRImage = getCacheData("TestQRImage")
|
|
||||||
const qrContent = "欢迎来到活氧舱"
|
|
||||||
|
|
||||||
if (cachedQRImage) {
|
|
||||||
// 使用缓存的二维码图片,不发送请求
|
|
||||||
console.log("测试模式:使用缓存的二维码图片")
|
|
||||||
image.value = cachedQRImage.image
|
|
||||||
width.value = cachedQRImage.width
|
|
||||||
height.value = cachedQRImage.height
|
|
||||||
qrcode.value = qrContent
|
|
||||||
status.value = 'waiting'
|
|
||||||
QRStatus.value = '请出示二维码签到'
|
|
||||||
uni.hideLoading()
|
uni.hideLoading()
|
||||||
|
|
||||||
// 只有刷新操作才显示提示
|
|
||||||
if (isRefresh) {
|
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '二维码已刷新',
|
title: '获取会员卡信息失败',
|
||||||
icon: 'success',
|
icon: 'none'
|
||||||
duration: 1500
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 首次生成,发送一次请求并缓存
|
|
||||||
console.log("测试模式:首次生成二维码,发送请求并缓存")
|
|
||||||
setTimeout(() => {
|
|
||||||
qrcode.value = qrContent
|
|
||||||
|
|
||||||
const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${encodeURIComponent(qrContent)}`
|
|
||||||
|
|
||||||
uni.request({
|
|
||||||
url: qrUrl,
|
|
||||||
method: 'GET',
|
|
||||||
responseType: 'arraybuffer',
|
|
||||||
success: (res) => {
|
|
||||||
const base64 = uni.arrayBufferToBase64(res.data)
|
|
||||||
const qrImage = `data:image/png;base64,${base64}`
|
|
||||||
|
|
||||||
image.value = qrImage
|
|
||||||
width.value = 500
|
|
||||||
height.value = 500
|
|
||||||
status.value = 'waiting'
|
|
||||||
QRStatus.value = '请出示二维码签到'
|
|
||||||
|
|
||||||
// 缓存二维码图片,后续刷新不再请求
|
|
||||||
setCacheData("TestQRImage", {
|
|
||||||
image: qrImage,
|
|
||||||
width: 500,
|
|
||||||
height: 500,
|
|
||||||
content: qrContent
|
|
||||||
})
|
})
|
||||||
|
|
||||||
uni.hideLoading()
|
|
||||||
},
|
|
||||||
fail: () => {
|
|
||||||
image.value = ''
|
|
||||||
width.value = 500
|
|
||||||
height.value = 500
|
|
||||||
status.value = 'waiting'
|
|
||||||
QRStatus.value = '测试模式 - 欢迎来到活氧舱'
|
|
||||||
uni.hideLoading()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, 500)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 页面卸载时关闭WebSocket连接(不清除缓存,让缓存自然过期)
|
|
||||||
onUnload(() => {
|
onUnload(() => {
|
||||||
closeWebSocket()
|
closeWebSocket()
|
||||||
// 缓存会在当日23:59:59自动过期,页面卸载时不主动清除
|
|
||||||
// 如需测试,使用页面上的"清除缓存"按钮手动清除
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取二维码接口
|
// 获取二维码接口
|
||||||
@@ -439,16 +340,6 @@ import QrStatus from '@/components/QRCode/StatusCard.vue'
|
|||||||
image.value = ""
|
image.value = ""
|
||||||
QRStatus.value = "正在刷新二维码..."
|
QRStatus.value = "正在刷新二维码..."
|
||||||
|
|
||||||
// 测试模式:重新生成二维码,但不发送请求,内容不变
|
|
||||||
if (TEST_MODE) {
|
|
||||||
// 延迟显示,让用户看到刷新效果
|
|
||||||
setTimeout(() => {
|
|
||||||
generateTestQRCode(true) // 传递 isRefresh=true
|
|
||||||
}, 300)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 非测试模式:正常刷新逻辑
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
getStorage(null)
|
getStorage(null)
|
||||||
}, 500)
|
}, 500)
|
||||||
@@ -486,12 +377,6 @@ import QrStatus from '@/components/QRCode/StatusCard.vue'
|
|||||||
scanType: ['qrCode'],
|
scanType: ['qrCode'],
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
console.log('扫码结果:', res)
|
console.log('扫码结果:', res)
|
||||||
// 测试模式下,如果扫描的是我们生成的二维码内容,直接模拟签到成功
|
|
||||||
if (TEST_MODE && res.result === '欢迎来到活氧舱') {
|
|
||||||
console.log('测试模式:模拟签到成功')
|
|
||||||
handleTestModeCheckIn()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
checkIn(res.result)
|
checkIn(res.result)
|
||||||
},
|
},
|
||||||
fail: (err) => {
|
fail: (err) => {
|
||||||
@@ -504,28 +389,6 @@ import QrStatus from '@/components/QRCode/StatusCard.vue'
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 测试模式:模拟签到成功(不请求后端)
|
|
||||||
const handleTestModeCheckIn = () => {
|
|
||||||
closeWebSocket()
|
|
||||||
const now = new Date()
|
|
||||||
const dateTime = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
|
|
||||||
|
|
||||||
status.value = 'scanned'
|
|
||||||
errorText.value = ''
|
|
||||||
QRStatus.value = `${dateTime} 成功签到`
|
|
||||||
isCheckIn.value = true
|
|
||||||
STQRC.value = true
|
|
||||||
|
|
||||||
setCacheData("checkInTime", QRStatus.value)
|
|
||||||
setCacheData("isCheckIn", isCheckIn.value)
|
|
||||||
|
|
||||||
uni.showToast({
|
|
||||||
title: '签到成功!',
|
|
||||||
icon: 'success',
|
|
||||||
duration: 2000
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 手动签到接口
|
// 手动签到接口
|
||||||
const checkIn = (qrContent) => {
|
const checkIn = (qrContent) => {
|
||||||
console.log(qrContent)
|
console.log(qrContent)
|
||||||
@@ -690,6 +553,7 @@ import QrStatus from '@/components/QRCode/StatusCard.vue'
|
|||||||
socketTask = null
|
socketTask = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -1021,25 +885,6 @@ import QrStatus from '@/components/QRCode/StatusCard.vue'
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 测试用:清除缓存按钮 */
|
|
||||||
.btn-clear-cache {
|
|
||||||
width: calc(100% - 96rpx);
|
|
||||||
height: 88rpx;
|
|
||||||
background: #FEF2F2;
|
|
||||||
color: #EF4444;
|
|
||||||
border: 1rpx solid #FECACA;
|
|
||||||
border-radius: 999px;
|
|
||||||
font-size: 29rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 16rpx;
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background: #FEE2E2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 旋转动画 */
|
/* 旋转动画 */
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% { transform: rotate(0deg); }
|
0% { transform: rotate(0deg); }
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<!-- pages/course/index.vue -->
|
<!-- pages/course/index.vue -->
|
||||||
<template>
|
<template>
|
||||||
<!-- 滚动内容区域 -->
|
<view class="scroll-container">
|
||||||
<scroll-view scroll-y="false" class="scroll-container">
|
|
||||||
<view class="tab-page">
|
<view class="tab-page">
|
||||||
<PageHeader title="课程" subtitle="精品团课 · 私教 · 线上课" />
|
<PageHeader title="课程" subtitle="精品团课 · 私教 · 线上课" />
|
||||||
|
|
||||||
@@ -29,7 +28,7 @@
|
|||||||
|
|
||||||
<view class="bottom-placeholder"></view>
|
<view class="bottom-placeholder"></view>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</view>
|
||||||
|
|
||||||
<!-- 固定 TabBar -->
|
<!-- 固定 TabBar -->
|
||||||
<view class="tabbar-fixed">
|
<view class="tabbar-fixed">
|
||||||
@@ -251,10 +250,13 @@ function goMyCourses() {
|
|||||||
|
|
||||||
/* 滚动容器 */
|
/* 滚动容器 */
|
||||||
.scroll-container {
|
.scroll-container {
|
||||||
position: relative;
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
overflow: hidden;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
height: 100vh;
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 固定 TabBar */
|
/* 固定 TabBar */
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- 固定白色块(滚动时显示) -->
|
<!-- 固定白色块(滚动时显示) -->
|
||||||
<view class="hand" :style="{height : handHeight + 'rpx'}" v-show="isShow"></view>
|
<view class="hand" :style="{height : handHeight + 50 + 'rpx',lineHeight: handHeight + 70 + 'rpx'}" v-show="isShow">活氧舱</view>
|
||||||
|
|
||||||
<!-- 滚动内容区域 -->
|
<!-- 滚动内容区域 -->
|
||||||
<scroll-view
|
<scroll-view
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
import BannerSwiper from '@/components/index/BannerSwiper.vue'
|
import BannerSwiper from '@/components/index/BannerSwiper.vue'
|
||||||
import QuickEntry from '@/components/index/QuickEntry.vue'
|
import QuickEntry from '@/components/index/QuickEntry.vue'
|
||||||
import RecommendCourses from '@/components/index/RecommendCourses.vue'
|
import RecommendCourses from '@/components/index/RecommendCourses.vue'
|
||||||
@@ -95,7 +96,21 @@ const refreshData = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
// 页面显示时触发未读消息检查
|
||||||
|
uni.$emit('pageShow')
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
// 设置测试token和userId(实际项目中由登录接口返回)
|
||||||
|
const testUserId = 1
|
||||||
|
uni.setStorageSync('userId', testUserId)
|
||||||
|
|
||||||
|
// 延迟触发未读消息检查,等待子组件挂载完成
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.$emit('pageShow')
|
||||||
|
}, 500)
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}, 1500)
|
}, 1500)
|
||||||
@@ -160,6 +175,8 @@ onMounted(() => {
|
|||||||
z-index: 100;
|
z-index: 100;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 34rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 固定 TabBar */
|
/* 固定 TabBar */
|
||||||
|
|||||||
@@ -74,84 +74,83 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||||
import { PAGE, goBackOrTab } from '@/common/constants/routes.js'
|
import { PAGE, goBackOrTab } from '@/common/constants/routes.js'
|
||||||
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||||
import { getBodyTestHistory, getCompareData } from '@/common/memberInfo/bodyTestStore.js'
|
import { getBodyTestHistory, getCompareData } from '@/common/memberInfo/bodyTestStore.js'
|
||||||
|
|
||||||
export default {
|
const records = ref([])
|
||||||
components: { MemberInfoSubNav },
|
const recordA = ref(null)
|
||||||
data() {
|
const recordB = ref(null)
|
||||||
return {
|
const compareData = ref(null)
|
||||||
records: [],
|
|
||||||
recordA: null,
|
const scoreDiff = computed(() => {
|
||||||
recordB: null,
|
if (!compareData.value) return 0
|
||||||
compareData: null
|
return compareData.value.recordA.score - compareData.value.recordB.score
|
||||||
}
|
})
|
||||||
},
|
|
||||||
computed: {
|
function refreshFromStore() {
|
||||||
scoreDiff() {
|
|
||||||
if (!this.compareData) return 0
|
|
||||||
return this.compareData.recordA.score - this.compareData.recordB.score
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onShow() {
|
|
||||||
const store = loadMemberStore()
|
const store = loadMemberStore()
|
||||||
this.records = getBodyTestHistory(store)
|
records.value = getBodyTestHistory(store)
|
||||||
if (this.records.length >= 2 && !this.recordA) {
|
if (records.value.length >= 2 && !recordA.value) {
|
||||||
this.recordA = this.records[0]
|
recordA.value = records.value[0]
|
||||||
this.recordB = this.records[1]
|
recordB.value = records.value[1]
|
||||||
this.refreshCompare()
|
refreshCompare()
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
methods: {
|
|
||||||
onBack() {
|
function onBack() {
|
||||||
goBackOrTab(PAGE.BODY_TEST_HISTORY)
|
goBackOrTab(PAGE.BODY_TEST_HISTORY)
|
||||||
},
|
}
|
||||||
pickRecord(which) {
|
|
||||||
const labels = this.records.map(
|
function pickRecord(which) {
|
||||||
|
const labels = records.value.map(
|
||||||
(r) => `${r.date} · ${r.score}分 · ${r.gradeLabel}`
|
(r) => `${r.date} · ${r.score}分 · ${r.gradeLabel}`
|
||||||
)
|
)
|
||||||
uni.showActionSheet({
|
uni.showActionSheet({
|
||||||
itemList: labels,
|
itemList: labels,
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
const picked = this.records[res.tapIndex]
|
const picked = records.value[res.tapIndex]
|
||||||
if (which === 'a') this.recordA = picked
|
if (which === 'a') recordA.value = picked
|
||||||
else this.recordB = picked
|
else recordB.value = picked
|
||||||
this.refreshCompare()
|
refreshCompare()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
refreshCompare() {
|
|
||||||
if (!this.recordA || !this.recordB) {
|
function refreshCompare() {
|
||||||
this.compareData = null
|
if (!recordA.value || !recordB.value) {
|
||||||
|
compareData.value = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (this.recordA.id === this.recordB.id) {
|
if (recordA.value.id === recordB.value.id) {
|
||||||
uni.showToast({ title: '请选择两条不同记录', icon: 'none' })
|
uni.showToast({ title: '请选择两条不同记录', icon: 'none' })
|
||||||
this.compareData = null
|
compareData.value = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const store = loadMemberStore()
|
const store = loadMemberStore()
|
||||||
this.compareData = getCompareData(store, this.recordA.id, this.recordB.id)
|
compareData.value = getCompareData(store, recordA.value.id, recordB.value.id)
|
||||||
},
|
}
|
||||||
formatDiff(diff, key) {
|
|
||||||
|
function formatDiff(diff, key) {
|
||||||
const sign = diff > 0 ? '+' : ''
|
const sign = diff > 0 ? '+' : ''
|
||||||
const units = { bodyFat: '%', weight: '', bmi: '', muscleMass: '', visceralFat: '', bmr: '' }
|
const units = { bodyFat: '%', weight: '', bmi: '', muscleMass: '', visceralFat: '', bmr: '' }
|
||||||
return `${sign}${diff}${units[key] || ''}`
|
return `${sign}${diff}${units[key] || ''}`
|
||||||
},
|
}
|
||||||
diffColor(row) {
|
|
||||||
|
function diffColor(row) {
|
||||||
const lowerBetter = ['weight', 'bodyFat', 'visceralFat'].includes(row.key)
|
const lowerBetter = ['weight', 'bodyFat', 'visceralFat'].includes(row.key)
|
||||||
const good = lowerBetter ? row.diff < 0 : row.diff > 0
|
const good = lowerBetter ? row.diff < 0 : row.diff > 0
|
||||||
if (row.diff === 0) return '#8A99B4'
|
if (row.diff === 0) return '#8A99B4'
|
||||||
return good ? '#2ECC71' : '#F39C12'
|
return good ? '#2ECC71' : '#F39C12'
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
refreshFromStore()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
@import '@/common/style/base.css';
|
@import '@/common/style/base.css';
|
||||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||||
|
|||||||
@@ -73,7 +73,8 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||||
import { PAGE, navigateToPage, goBackOrTab } from '@/common/constants/routes.js'
|
import { PAGE, navigateToPage, goBackOrTab } from '@/common/constants/routes.js'
|
||||||
import {
|
import {
|
||||||
@@ -81,52 +82,52 @@ import {
|
|||||||
persistMemberStore
|
persistMemberStore
|
||||||
} from '@/common/memberInfo/store.js'
|
} from '@/common/memberInfo/store.js'
|
||||||
import {
|
import {
|
||||||
connectBodyTestDevice,
|
connectBodyTestDevice
|
||||||
bodyTestMock
|
|
||||||
} from '@/common/memberInfo/bodyTestStore.js'
|
} from '@/common/memberInfo/bodyTestStore.js'
|
||||||
|
|
||||||
export default {
|
const device = ref({})
|
||||||
components: { MemberInfoSubNav },
|
const steps = ref([
|
||||||
data() {
|
{ step: 1, text: '打开体测仪电源' },
|
||||||
return {
|
{ step: 2, text: '站在体测仪上' },
|
||||||
device: {},
|
{ step: 3, text: '等待测量完成' }
|
||||||
steps: bodyTestMock.connectSteps,
|
])
|
||||||
searching: false,
|
const searching = ref(false)
|
||||||
connected: false,
|
const connected = ref(false)
|
||||||
searchHint: '请保持手机蓝牙已开启'
|
const searchHint = ref('请保持手机蓝牙已开启')
|
||||||
}
|
|
||||||
},
|
function refreshFromStore() {
|
||||||
onShow() {
|
|
||||||
const store = loadMemberStore()
|
const store = loadMemberStore()
|
||||||
this.device = { ...store.bodyTest.device }
|
device.value = { ...store.bodyTest.device }
|
||||||
this.connected = store.bodyTest.device.connected
|
connected.value = store.bodyTest.device.connected
|
||||||
},
|
}
|
||||||
methods: {
|
|
||||||
onBack() {
|
function onBack() {
|
||||||
goBackOrTab(PAGE.BODY_TEST_HOME)
|
goBackOrTab(PAGE.BODY_TEST_HOME)
|
||||||
},
|
}
|
||||||
searchDevice() {
|
|
||||||
if (this.searching) return
|
function searchDevice() {
|
||||||
this.searching = true
|
if (searching.value) return
|
||||||
this.searchHint = '发现 InBody 270…'
|
searching.value = true
|
||||||
|
searchHint.value = '发现 InBody 270…'
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const store = loadMemberStore()
|
const store = loadMemberStore()
|
||||||
connectBodyTestDevice(store)
|
connectBodyTestDevice(store)
|
||||||
persistMemberStore(store)
|
persistMemberStore(store)
|
||||||
this.device = { ...store.bodyTest.device }
|
device.value = { ...store.bodyTest.device }
|
||||||
this.connected = true
|
connected.value = true
|
||||||
this.searching = false
|
searching.value = false
|
||||||
uni.showToast({ title: '设备已连接', icon: 'success' })
|
uni.showToast({ title: '设备已连接', icon: 'success' })
|
||||||
}, 1800)
|
}, 1800)
|
||||||
},
|
}
|
||||||
goMeasuring() {
|
|
||||||
|
function goMeasuring() {
|
||||||
navigateToPage(PAGE.BODY_TEST_MEASURING)
|
navigateToPage(PAGE.BODY_TEST_MEASURING)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
refreshFromStore()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
@import '@/common/style/base.css';
|
@import '@/common/style/base.css';
|
||||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||||
|
|||||||
@@ -59,22 +59,21 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||||
import { PAGE, navigateToPage, goBackOrTab } from '@/common/constants/routes.js'
|
import { PAGE, navigateToPage, goBackOrTab } from '@/common/constants/routes.js'
|
||||||
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||||
import { getBodyTestHistory, getBodyTestYears, getBodyTestChangeBadge } from '@/common/memberInfo/bodyTestStore.js'
|
import { getBodyTestHistory, getBodyTestYears, getBodyTestChangeBadge } from '@/common/memberInfo/bodyTestStore.js'
|
||||||
|
|
||||||
export default {
|
const activeYear = ref('all')
|
||||||
components: { MemberInfoSubNav },
|
const years = ref([])
|
||||||
data() {
|
const allRecords = ref([])
|
||||||
return { activeYear: 'all', years: [], allRecords: [] }
|
|
||||||
},
|
const records = computed(() => {
|
||||||
computed: {
|
const list = activeYear.value === 'all'
|
||||||
records() {
|
? allRecords.value
|
||||||
const list = this.activeYear === 'all'
|
: allRecords.value.filter((r) => r.date.startsWith(activeYear.value))
|
||||||
? this.allRecords
|
|
||||||
: this.allRecords.filter((r) => r.date.startsWith(this.activeYear))
|
|
||||||
return list.map((item, index) => {
|
return list.map((item, index) => {
|
||||||
const previous = list[index + 1]
|
const previous = list[index + 1]
|
||||||
return {
|
return {
|
||||||
@@ -82,30 +81,30 @@ export default {
|
|||||||
changeBadge: getBodyTestChangeBadge(item, previous)
|
changeBadge: getBodyTestChangeBadge(item, previous)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
})
|
||||||
},
|
|
||||||
watch: {
|
function loadList() {
|
||||||
activeYear() { this.loadList() }
|
|
||||||
},
|
|
||||||
onShow() { this.loadList() },
|
|
||||||
methods: {
|
|
||||||
loadList() {
|
|
||||||
const store = loadMemberStore()
|
const store = loadMemberStore()
|
||||||
this.years = getBodyTestYears(store)
|
years.value = getBodyTestYears(store)
|
||||||
this.allRecords = getBodyTestHistory(store)
|
allRecords.value = getBodyTestHistory(store)
|
||||||
},
|
}
|
||||||
onBack() { goBackOrTab(PAGE.MEMBER) },
|
|
||||||
viewReport(item) {
|
function onBack() { goBackOrTab(PAGE.MEMBER) }
|
||||||
|
|
||||||
|
function viewReport(item) {
|
||||||
navigateToPage(`${PAGE.BODY_TEST_REPORT}?id=${item.id}`)
|
navigateToPage(`${PAGE.BODY_TEST_REPORT}?id=${item.id}`)
|
||||||
},
|
}
|
||||||
goCompare() {
|
|
||||||
|
function goCompare() {
|
||||||
navigateToPage(PAGE.BODY_TEST_COMPARE)
|
navigateToPage(PAGE.BODY_TEST_COMPARE)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
watch(activeYear, () => { loadList() })
|
||||||
|
|
||||||
|
loadList()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
@import '@/common/style/base.css';
|
@import '@/common/style/base.css';
|
||||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||||
@@ -196,15 +195,7 @@ export default {
|
|||||||
background: rgba(46, 204, 113, 0.12);
|
background: rgba(46, 204, 113, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mi-timeline-card__badge--good text {
|
.mi-timeline-card__badge--warn {
|
||||||
font-size: 10px;
|
background: rgba(243, 156, 18, 0.12);
|
||||||
color: var(--success-green, #2ECC71);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mi-timeline-card__badge--warn text {
|
|
||||||
font-size: 10px;
|
|
||||||
color: var(--warning-amber, #F39C12);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,47 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="scroll-container theme-light">
|
<view class="scroll-container theme-light">
|
||||||
<view class="bt-page">
|
<view class="bt-page">
|
||||||
<MemberInfoSubNav title="智能体测" @back="goBack" />
|
<MemberInfoSubNav title="体测" @back="goBack" />
|
||||||
<view class="bt-page__action-bar bt-page__action-bar--end">
|
|
||||||
<text
|
|
||||||
class="bt-page__action-link"
|
|
||||||
hover-class="mi-tap--hover"
|
|
||||||
:hover-stay-time="150"
|
|
||||||
@tap="goSettings"
|
|
||||||
>
|
|
||||||
体测设置
|
|
||||||
</text>
|
|
||||||
</view>
|
|
||||||
<view class="bt-page__body">
|
<view class="bt-page__body">
|
||||||
<view class="bt-hero">
|
<view v-if="latest" class="bt-hero">
|
||||||
<view class="bt-hero__top">
|
<view class="bt-hero__time">
|
||||||
<text class="bt-hero__label">最新体测评分</text>
|
<image class="bt-hero__time-icon" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/clock.png" mode="aspectFit" />
|
||||||
<view class="bt-hero__badge">
|
<text class="bt-hero__time-text">{{ latest.testTime }}</text>
|
||||||
<text class="bt-hero__badge-text">{{ latest?.status || '暂无数据' }}</text>
|
</view>
|
||||||
|
<view class="bt-hero__metrics">
|
||||||
|
<view
|
||||||
|
v-for="m in previewMetrics"
|
||||||
|
:key="m.key"
|
||||||
|
class="bt-hero__metric"
|
||||||
|
>
|
||||||
|
<text class="bt-hero__metric-value">{{ m.value }}</text>
|
||||||
|
<text class="bt-hero__metric-label">{{ m.label }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="bt-hero__score-row">
|
|
||||||
<text class="bt-hero__score">{{ latest?.score ?? '--' }}</text>
|
|
||||||
<text class="bt-hero__grade">{{ latest?.grade ?? '' }} {{ latest?.gradeLabel ?? '' }}</text>
|
|
||||||
</view>
|
|
||||||
<text class="bt-hero__meta">
|
|
||||||
{{ latest ? `最近测量 · ${latest.date} ${latest.time}` : '完成首次体测,获取健康画像' }}
|
|
||||||
</text>
|
|
||||||
<view class="bt-hero__actions">
|
<view class="bt-hero__actions">
|
||||||
<view
|
<view
|
||||||
class="bt-btn bt-btn--primary"
|
class="bt-btn bt-btn--primary"
|
||||||
hover-class="mi-tap-btn--hover"
|
hover-class="mi-tap-btn--hover"
|
||||||
:hover-stay-time="150"
|
:hover-stay-time="150"
|
||||||
@tap="startMeasure"
|
|
||||||
>
|
|
||||||
<image class="bt-btn__icon" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/activity.png" mode="aspectFit" />
|
|
||||||
<text class="bt-btn__text">开始体测</text>
|
|
||||||
</view>
|
|
||||||
<view
|
|
||||||
v-if="latest"
|
|
||||||
class="bt-btn bt-btn--ghost"
|
|
||||||
hover-class="mi-tap-btn--hover"
|
|
||||||
:hover-stay-time="150"
|
|
||||||
@tap="viewLatestReport"
|
@tap="viewLatestReport"
|
||||||
>
|
>
|
||||||
<text class="bt-btn__text">查看报告</text>
|
<text class="bt-btn__text">查看报告</text>
|
||||||
@@ -50,142 +31,145 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="bt-card">
|
<view class="bt-card">
|
||||||
<text class="bt-card__title">设备状态</text>
|
<view class="bt-card__header">
|
||||||
<view class="bt-device">
|
<text class="bt-card__title">快捷操作</text>
|
||||||
<view class="bt-device__icon-wrap">
|
|
||||||
<image class="bt-device__icon" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/mappin2.png" mode="aspectFit" />
|
|
||||||
</view>
|
|
||||||
<view class="bt-device__info">
|
|
||||||
<text class="bt-device__name">{{ device.name }}</text>
|
|
||||||
<text
|
|
||||||
class="bt-device__status"
|
|
||||||
:class="{ 'bt-device__status--on': device.connected }"
|
|
||||||
>
|
|
||||||
{{ deviceStatusText }}
|
|
||||||
</text>
|
|
||||||
</view>
|
</view>
|
||||||
|
<view class="bt-quick-grid">
|
||||||
<view
|
<view
|
||||||
class="bt-device__dot"
|
v-for="link in quickLinks"
|
||||||
:class="{ 'bt-device__dot--on': device.connected }"
|
:key="link.key"
|
||||||
></view>
|
class="bt-quick-item"
|
||||||
|
hover-class="mi-tap--hover"
|
||||||
|
:hover-stay-time="150"
|
||||||
|
@tap="onQuickLink(link.key)"
|
||||||
|
>
|
||||||
|
<image class="bt-quick-item__icon" :src="link.icon" mode="aspectFit" />
|
||||||
|
<text class="bt-quick-item__label">{{ link.label }}</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="bt-card">
|
<view class="bt-card">
|
||||||
<text class="bt-card__title">快捷入口</text>
|
<view class="bt-card__header">
|
||||||
<view class="bt-grid">
|
<text class="bt-card__title">开始体测</text>
|
||||||
|
</view>
|
||||||
|
<view class="bt-device-status" @tap="startMeasure">
|
||||||
|
<view class="bt-device-status__icon-wrap">
|
||||||
|
<image
|
||||||
|
class="bt-device-status__icon"
|
||||||
|
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/bluetooth.png"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="bt-device-status__info">
|
||||||
|
<text class="bt-device-status__name">{{ device.name || '体脂秤' }}</text>
|
||||||
|
<text class="bt-device-status__text">{{ deviceStatusText }}</text>
|
||||||
|
</view>
|
||||||
|
<image
|
||||||
|
class="bt-device-status__arrow"
|
||||||
|
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/rightarrow.png"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="bt-card__actions">
|
||||||
<view
|
<view
|
||||||
v-for="item in quickLinks"
|
class="bt-btn bt-btn--primary"
|
||||||
:key="item.key"
|
hover-class="mi-tap-btn--hover"
|
||||||
class="bt-grid__item"
|
|
||||||
hover-class="mi-tap--hover"
|
|
||||||
:hover-stay-time="150"
|
:hover-stay-time="150"
|
||||||
@tap="onQuickLink(item.key)"
|
@tap="startMeasure"
|
||||||
>
|
>
|
||||||
<image class="bt-grid__icon" :src="item.icon" mode="aspectFit" />
|
<text class="bt-btn__text">{{ device.connected ? '开始测量' : '连接设备' }}</text>
|
||||||
<text class="bt-grid__label">{{ item.label }}</text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-if="latest" class="bt-card">
|
<view class="bt-settings-link" @tap="goSettings">
|
||||||
<text class="bt-card__title">核心指标概览</text>
|
<text class="bt-settings-link__text">体测设置</text>
|
||||||
<view class="bt-metrics">
|
<image class="bt-settings-link__arrow" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/rightarrow.png" mode="aspectFit" />
|
||||||
<view
|
|
||||||
v-for="m in previewMetrics"
|
|
||||||
:key="m.key"
|
|
||||||
class="bt-metric"
|
|
||||||
>
|
|
||||||
<text class="bt-metric__value">{{ m.value }}</text>
|
|
||||||
<text class="bt-metric__label">{{ m.label }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||||
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
|
import { PAGE, navigateToPage, backToMemberCenter } from '@/common/constants/routes.js'
|
||||||
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||||
import { getLatestBodyTestRecord } from '@/common/memberInfo/bodyTestStore.js'
|
import { getLatestBodyTestRecord } from '@/common/memberInfo/bodyTestStore.js'
|
||||||
import { subPageMixin } from '@/common/memberInfo/mixins.js'
|
|
||||||
|
|
||||||
export default {
|
const latest = ref(null)
|
||||||
components: { MemberInfoSubNav },
|
const device = ref({})
|
||||||
mixins: [subPageMixin],
|
const quickLinks = ref([
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
latest: null,
|
|
||||||
device: {},
|
|
||||||
quickLinks: [
|
|
||||||
{ key: 'history', label: '历史记录', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/clock.png' },
|
{ key: 'history', label: '历史记录', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/clock.png' },
|
||||||
{ key: 'compare', label: '历史对比', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/trendingdown.png' },
|
{ key: 'compare', label: '历史对比', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/trendingdown.png' },
|
||||||
{ key: 'trend', label: '趋势分析', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/activity.png' },
|
{ key: 'trend', label: '趋势分析', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/activity.png' },
|
||||||
{ key: 'report', label: '体测报告', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/filetext.png' }
|
{ key: 'report', label: '体测报告', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/filetext.png' }
|
||||||
]
|
])
|
||||||
}
|
|
||||||
},
|
const deviceStatusText = computed(() => {
|
||||||
computed: {
|
if (device.value.connected) {
|
||||||
deviceStatusText() {
|
return `已连接 · 电量 ${device.value.battery}%`
|
||||||
if (this.device.connected) {
|
|
||||||
return `已连接 · 电量 ${this.device.battery}%`
|
|
||||||
}
|
}
|
||||||
return '未连接 · 点击开始体测进行配对'
|
return '未连接 · 点击开始体测进行配对'
|
||||||
},
|
})
|
||||||
previewMetrics() {
|
|
||||||
if (!this.latest?.metrics) return []
|
const previewMetrics = computed(() => {
|
||||||
const m = this.latest.metrics
|
if (!latest.value?.metrics) return []
|
||||||
|
const m = latest.value.metrics
|
||||||
return [
|
return [
|
||||||
{ key: 'weight', label: '体重(kg)', value: m.weight },
|
{ key: 'weight', label: '体重(kg)', value: m.weight },
|
||||||
{ key: 'bmi', label: 'BMI', value: m.bmi },
|
{ key: 'bmi', label: 'BMI', value: m.bmi },
|
||||||
{ key: 'bodyFat', label: '体脂率(%)', value: m.bodyFat },
|
{ key: 'bodyFat', label: '体脂率(%)', value: m.bodyFat },
|
||||||
{ key: 'muscleMass', label: '肌肉量(kg)', value: m.muscleMass }
|
{ key: 'muscleMass', label: '肌肉量(kg)', value: m.muscleMass }
|
||||||
]
|
]
|
||||||
}
|
})
|
||||||
},
|
|
||||||
onShow() {
|
function refreshFromStore() {
|
||||||
this.refreshFromStore()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
refreshFromStore() {
|
|
||||||
const store = loadMemberStore()
|
const store = loadMemberStore()
|
||||||
this.latest = getLatestBodyTestRecord(store)
|
latest.value = getLatestBodyTestRecord(store)
|
||||||
this.device = { ...store.bodyTest.device }
|
device.value = { ...store.bodyTest.device }
|
||||||
},
|
}
|
||||||
startMeasure() {
|
|
||||||
|
function goBack() {
|
||||||
|
backToMemberCenter()
|
||||||
|
}
|
||||||
|
|
||||||
|
function startMeasure() {
|
||||||
const store = loadMemberStore()
|
const store = loadMemberStore()
|
||||||
if (store.bodyTest.device.connected) {
|
if (store.bodyTest.device.connected) {
|
||||||
navigateToPage(PAGE.BODY_TEST_MEASURING)
|
navigateToPage(PAGE.BODY_TEST_MEASURING)
|
||||||
} else {
|
} else {
|
||||||
navigateToPage(PAGE.BODY_TEST_CONNECT)
|
navigateToPage(PAGE.BODY_TEST_CONNECT)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
viewLatestReport() {
|
|
||||||
if (!this.latest) return
|
function viewLatestReport() {
|
||||||
navigateToPage(`${PAGE.BODY_TEST_REPORT}?id=${this.latest.id}`)
|
if (!latest.value) return
|
||||||
},
|
navigateToPage(`${PAGE.BODY_TEST_REPORT}?id=${latest.value.id}`)
|
||||||
goSettings() {
|
}
|
||||||
|
|
||||||
|
function goSettings() {
|
||||||
navigateToPage(PAGE.BODY_TEST_SETTINGS)
|
navigateToPage(PAGE.BODY_TEST_SETTINGS)
|
||||||
},
|
}
|
||||||
onQuickLink(key) {
|
|
||||||
|
function onQuickLink(key) {
|
||||||
const routes = {
|
const routes = {
|
||||||
history: PAGE.BODY_TEST_HISTORY,
|
history: PAGE.BODY_TEST_HISTORY,
|
||||||
compare: PAGE.BODY_TEST_COMPARE,
|
compare: PAGE.BODY_TEST_COMPARE,
|
||||||
trend: PAGE.BODY_TEST_TREND,
|
trend: PAGE.BODY_TEST_TREND,
|
||||||
report: this.latest
|
report: latest.value
|
||||||
? `${PAGE.BODY_TEST_REPORT}?id=${this.latest.id}`
|
? `${PAGE.BODY_TEST_REPORT}?id=${latest.value.id}`
|
||||||
: PAGE.BODY_TEST_HISTORY
|
: PAGE.BODY_TEST_HISTORY
|
||||||
}
|
}
|
||||||
navigateToPage(routes[key] || PAGE.BODY_TEST_HOME)
|
navigateToPage(routes[key] || PAGE.BODY_TEST_HOME)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
refreshFromStore()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
@import '@/common/style/base.css';
|
@import '@/common/style/base.css';
|
||||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||||
|
|||||||
@@ -38,7 +38,8 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref, computed, onUnmounted } from 'vue'
|
||||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||||
import { PAGE, navigateToPage, goBackOrTab } from '@/common/constants/routes.js'
|
import { PAGE, navigateToPage, goBackOrTab } from '@/common/constants/routes.js'
|
||||||
import {
|
import {
|
||||||
@@ -57,82 +58,66 @@ const PHASES = [
|
|||||||
{ until: 100, hint: '即将完成', text: '生成健康报告中…' }
|
{ until: 100, hint: '即将完成', text: '生成健康报告中…' }
|
||||||
]
|
]
|
||||||
|
|
||||||
export default {
|
const progress = ref(0)
|
||||||
components: { MemberInfoSubNav },
|
const liveMetrics = ref({})
|
||||||
data() {
|
let timer = null
|
||||||
return {
|
const finished = ref(false)
|
||||||
progress: 0,
|
|
||||||
liveMetrics: {},
|
const ringRotation = computed(() => {
|
||||||
timer: null,
|
return -90 + (progress.value / 100) * 360
|
||||||
finished: false
|
})
|
||||||
}
|
|
||||||
},
|
const phaseHint = computed(() => {
|
||||||
computed: {
|
const phase = PHASES.find((p) => progress.value <= p.until)
|
||||||
ringRotation() {
|
|
||||||
return -90 + (this.progress / 100) * 360
|
|
||||||
},
|
|
||||||
phaseHint() {
|
|
||||||
const phase = PHASES.find((p) => this.progress <= p.until)
|
|
||||||
return phase?.hint || '完成'
|
return phase?.hint || '完成'
|
||||||
},
|
})
|
||||||
statusText() {
|
|
||||||
const phase = PHASES.find((p) => this.progress <= p.until)
|
const statusText = computed(() => {
|
||||||
|
const phase = PHASES.find((p) => progress.value <= p.until)
|
||||||
return phase?.text || '测量完成'
|
return phase?.text || '测量完成'
|
||||||
},
|
})
|
||||||
liveDisplay() {
|
|
||||||
const m = this.liveMetrics
|
const liveDisplay = computed(() => {
|
||||||
|
const m = liveMetrics.value
|
||||||
return [
|
return [
|
||||||
{ key: 'weight', label: '体重(kg)', value: m.weight ?? '--' },
|
{ key: 'weight', label: '体重(kg)', value: m.weight ?? '--' },
|
||||||
{ key: 'bodyFat', label: '体脂率(%)', value: m.bodyFat ?? '--' },
|
{ key: 'bodyFat', label: '体脂率(%)', value: m.bodyFat ?? '--' },
|
||||||
{ key: 'muscleMass', label: '肌肉量(kg)', value: m.muscleMass ?? '--' },
|
{ key: 'muscleMass', label: '肌肉量(kg)', value: m.muscleMass ?? '--' },
|
||||||
{ key: 'bmr', label: '基础代谢', value: m.bmr ?? '--' }
|
{ key: 'bmr', label: '基础代谢', value: m.bmr ?? '--' }
|
||||||
]
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
function clearTimer() {
|
||||||
|
if (timer) {
|
||||||
|
clearInterval(timer)
|
||||||
|
timer = null
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
onLoad() {
|
|
||||||
|
function startMeasurement() {
|
||||||
const store = loadMemberStore()
|
const store = loadMemberStore()
|
||||||
if (!store.bodyTest.device.connected) {
|
liveMetrics.value = interpolateMeasuringMetrics(0, store.profile)
|
||||||
uni.showToast({ title: '请先连接设备', icon: 'none' })
|
timer = setInterval(() => {
|
||||||
setTimeout(() => {
|
if (progress.value >= 100) {
|
||||||
navigateToPage(PAGE.BODY_TEST_CONNECT)
|
completeMeasurement()
|
||||||
}, 800)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.startMeasurement()
|
progress.value = Math.min(100, progress.value + 2)
|
||||||
},
|
|
||||||
onUnload() {
|
|
||||||
this.clearTimer()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
clearTimer() {
|
|
||||||
if (this.timer) {
|
|
||||||
clearInterval(this.timer)
|
|
||||||
this.timer = null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
startMeasurement() {
|
|
||||||
const store = loadMemberStore()
|
|
||||||
this.liveMetrics = interpolateMeasuringMetrics(0, store.profile)
|
|
||||||
this.timer = setInterval(() => {
|
|
||||||
if (this.progress >= 100) {
|
|
||||||
this.completeMeasurement()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.progress = Math.min(100, this.progress + 2)
|
|
||||||
const s = loadMemberStore()
|
const s = loadMemberStore()
|
||||||
this.liveMetrics = interpolateMeasuringMetrics(this.progress, s.profile)
|
liveMetrics.value = interpolateMeasuringMetrics(progress.value, s.profile)
|
||||||
}, 120)
|
}, 120)
|
||||||
},
|
}
|
||||||
completeMeasurement() {
|
|
||||||
if (this.finished) return
|
function completeMeasurement() {
|
||||||
this.finished = true
|
if (finished.value) return
|
||||||
this.clearTimer()
|
finished.value = true
|
||||||
|
clearTimer()
|
||||||
const store = loadMemberStore()
|
const store = loadMemberStore()
|
||||||
const record = saveSimulatedBodyTestRecord(store, {
|
const record = saveSimulatedBodyTestRecord(store, {
|
||||||
...this.liveMetrics,
|
...liveMetrics.value,
|
||||||
visceralFat: 6,
|
visceralFat: 6,
|
||||||
boneMass: 2.42,
|
boneMass: 2.42,
|
||||||
bodyWater: this.liveMetrics.bodyWater || 52.8,
|
bodyWater: liveMetrics.value.bodyWater || 52.8,
|
||||||
protein: 16.4
|
protein: 16.4
|
||||||
})
|
})
|
||||||
persistMemberStore(store)
|
persistMemberStore(store)
|
||||||
@@ -140,25 +125,39 @@ export default {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
navigateToPage(`${PAGE.BODY_TEST_REPORT}?id=${record.id}&new=1`)
|
navigateToPage(`${PAGE.BODY_TEST_REPORT}?id=${record.id}&new=1`)
|
||||||
}, 600)
|
}, 600)
|
||||||
},
|
}
|
||||||
onCancel() {
|
|
||||||
if (this.finished) return
|
function onCancel() {
|
||||||
|
if (finished.value) return
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '取消测量',
|
title: '取消测量',
|
||||||
content: '确定要中断当前体测吗?',
|
content: '确定要中断当前体测吗?',
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (res.confirm) {
|
if (res.confirm) {
|
||||||
this.clearTimer()
|
clearTimer()
|
||||||
goBackOrTab(PAGE.BODY_TEST_HOME)
|
goBackOrTab(PAGE.BODY_TEST_HOME)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearTimer()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
const store = loadMemberStore()
|
||||||
|
if (!store.bodyTest.device.connected) {
|
||||||
|
uni.showToast({ title: '请先连接设备', icon: 'none' })
|
||||||
|
setTimeout(() => {
|
||||||
|
navigateToPage(PAGE.BODY_TEST_CONNECT)
|
||||||
|
}, 800)
|
||||||
|
} else {
|
||||||
|
startMeasurement()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
@import '@/common/style/base.css';
|
@import '@/common/style/base.css';
|
||||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||||
|
|||||||
@@ -169,7 +169,8 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||||
import BodyTestRadarChart from '@/components/memberInfo/BodyTestRadarChart.vue'
|
import BodyTestRadarChart from '@/components/memberInfo/BodyTestRadarChart.vue'
|
||||||
import BodyTestTrendChart from '@/components/memberInfo/BodyTestTrendChart.vue'
|
import BodyTestTrendChart from '@/components/memberInfo/BodyTestTrendChart.vue'
|
||||||
@@ -180,36 +181,43 @@ import {
|
|||||||
getBodyTestTrendData,
|
getBodyTestTrendData,
|
||||||
getRecommendedCourses,
|
getRecommendedCourses,
|
||||||
computeChanges,
|
computeChanges,
|
||||||
formatChangeValue,
|
formatChangeValue
|
||||||
bodyTestMock
|
|
||||||
} from '@/common/memberInfo/bodyTestStore.js'
|
} from '@/common/memberInfo/bodyTestStore.js'
|
||||||
|
|
||||||
export default {
|
const recordId = ref(null)
|
||||||
components: { MemberInfoSubNav, BodyTestRadarChart, BodyTestTrendChart },
|
const record = ref(null)
|
||||||
data() {
|
const previous = ref(null)
|
||||||
return {
|
const chartWidth = ref(300)
|
||||||
recordId: null,
|
|
||||||
record: null,
|
const radarLabels = computed(() => {
|
||||||
previous: null,
|
return ['体重', 'BMI', '体脂率', '肌肉量', '内脏脂肪', '基础代谢']
|
||||||
chartWidth: 300
|
})
|
||||||
}
|
|
||||||
},
|
const radarKeys = ['weight', 'bmi', 'bodyFat', 'muscleMass', 'visceralFat', 'bmr']
|
||||||
computed: {
|
|
||||||
radarLabels() {
|
const radarValues = computed(() => {
|
||||||
return bodyTestMock.radarLabels.map((l) => l.label)
|
if (!record.value?.radar) return []
|
||||||
},
|
return radarKeys.map((k) => record.value.radar[k] || 0)
|
||||||
radarValues() {
|
})
|
||||||
if (!this.record?.radar) return []
|
|
||||||
const keys = bodyTestMock.radarLabels.map((l) => l.key)
|
const metricDefs = [
|
||||||
return keys.map((k) => this.record.radar[k] || 0)
|
{ key: 'weight', label: '体重', unit: 'kg' },
|
||||||
},
|
{ key: 'bmi', label: 'BMI' },
|
||||||
metricCards() {
|
{ key: 'bodyFat', label: '体脂率', unit: '%' },
|
||||||
if (!this.record?.metrics) return []
|
{ key: 'muscleMass', label: '肌肉量', unit: 'kg' },
|
||||||
const changes = this.changes
|
{ key: 'visceralFat', label: '内脏脂肪', unit: '级' },
|
||||||
const defs = bodyTestMock.metricDefs.slice(0, 8)
|
{ key: 'bmr', label: '基础代谢', unit: 'kcal' },
|
||||||
|
{ key: 'bodyWater', label: '体水分', unit: '%' },
|
||||||
|
{ key: 'boneMass', label: '骨量', unit: 'kg' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const metricCards = computed(() => {
|
||||||
|
if (!record.value?.metrics) return []
|
||||||
|
const changesVal = changes.value
|
||||||
|
const defs = metricDefs.slice(0, 8)
|
||||||
return defs.map((def) => {
|
return defs.map((def) => {
|
||||||
const val = this.record.metrics[def.key]
|
const val = record.value.metrics[def.key]
|
||||||
const diff = changes[def.key]
|
const diff = changesVal[def.key]
|
||||||
let changeText = ''
|
let changeText = ''
|
||||||
let changeClass = ''
|
let changeClass = ''
|
||||||
if (diff !== undefined && diff !== null) {
|
if (diff !== undefined && diff !== null) {
|
||||||
@@ -227,53 +235,54 @@ export default {
|
|||||||
changeClass
|
changeClass
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
})
|
||||||
changes() {
|
|
||||||
if (this.record?.changes) return this.record.changes
|
const changes = computed(() => {
|
||||||
if (this.previous) return computeChanges(this.record, this.previous)
|
if (record.value?.changes) return record.value.changes
|
||||||
|
if (previous.value) return computeChanges(record.value, previous.value)
|
||||||
return {}
|
return {}
|
||||||
},
|
})
|
||||||
trendPreview() {
|
|
||||||
|
const trendPreview = computed(() => {
|
||||||
const store = loadMemberStore()
|
const store = loadMemberStore()
|
||||||
return getBodyTestTrendData(store, 'weight', 4)
|
return getBodyTestTrendData(store, 'weight', 4)
|
||||||
},
|
})
|
||||||
courses() {
|
|
||||||
return getRecommendedCourses(this.record)
|
const courses = computed(() => {
|
||||||
}
|
return getRecommendedCourses(record.value)
|
||||||
},
|
})
|
||||||
onLoad(options) {
|
|
||||||
this.recordId = options?.id ? Number(options.id) : null
|
function loadRecord() {
|
||||||
this.chartWidth = uni.getSystemInfoSync().windowWidth - 64
|
|
||||||
this.loadRecord()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
loadRecord() {
|
|
||||||
const store = loadMemberStore()
|
const store = loadMemberStore()
|
||||||
const records = store.bodyTest.records
|
const records = store.bodyTest.records
|
||||||
if (this.recordId) {
|
if (recordId.value) {
|
||||||
this.record = getBodyTestRecordById(store, this.recordId)
|
record.value = getBodyTestRecordById(store, recordId.value)
|
||||||
} else {
|
} else {
|
||||||
this.record = records.length ? { ...records[0] } : null
|
record.value = records.length ? { ...records[0] } : null
|
||||||
}
|
}
|
||||||
if (this.record) {
|
if (record.value) {
|
||||||
const idx = records.findIndex((r) => r.id === this.record.id)
|
const idx = records.findIndex((r) => r.id === record.value.id)
|
||||||
this.previous = idx >= 0 && records[idx + 1] ? records[idx + 1] : null
|
previous.value = idx >= 0 && records[idx + 1] ? records[idx + 1] : null
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
onBack() {
|
|
||||||
|
function onBack() {
|
||||||
goBackOrTab(PAGE.BODY_TEST_HOME)
|
goBackOrTab(PAGE.BODY_TEST_HOME)
|
||||||
},
|
}
|
||||||
goTrend() {
|
|
||||||
|
function goTrend() {
|
||||||
navigateToPage(`${PAGE.BODY_TEST_TREND}?metric=weight`)
|
navigateToPage(`${PAGE.BODY_TEST_TREND}?metric=weight`)
|
||||||
},
|
}
|
||||||
exportReport() {
|
|
||||||
|
function exportReport() {
|
||||||
uni.showLoading({ title: '生成中…' })
|
uni.showLoading({ title: '生成中…' })
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
uni.hideLoading()
|
uni.hideLoading()
|
||||||
uni.showToast({ title: '报告已保存到相册', icon: 'success' })
|
uni.showToast({ title: '报告已保存到相册', icon: 'success' })
|
||||||
}, 1200)
|
}, 1200)
|
||||||
},
|
}
|
||||||
shareReport() {
|
|
||||||
|
function shareReport() {
|
||||||
uni.showActionSheet({
|
uni.showActionSheet({
|
||||||
itemList: ['分享给微信好友', '生成分享海报', '复制报告链接'],
|
itemList: ['分享给微信好友', '生成分享海报', '复制报告链接'],
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
@@ -281,8 +290,9 @@ export default {
|
|||||||
uni.showToast({ title: msgs[res.tapIndex] || '分享成功', icon: 'none' })
|
uni.showToast({ title: msgs[res.tapIndex] || '分享成功', icon: 'none' })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
onCourseTap(course) {
|
|
||||||
|
function onCourseTap(course) {
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: course.title,
|
title: course.title,
|
||||||
content: `${course.coach}\n${course.schedule}\n\n是否前往预约?`,
|
content: `${course.coach}\n${course.schedule}\n\n是否前往预约?`,
|
||||||
@@ -292,15 +302,18 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
retest() {
|
|
||||||
|
function retest() {
|
||||||
navigateToPage(PAGE.BODY_TEST_HOME)
|
navigateToPage(PAGE.BODY_TEST_HOME)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
// Initialize
|
||||||
|
chartWidth.value = uni.getSystemInfoSync().windowWidth - 64
|
||||||
|
loadRecord()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
@import '@/common/style/base.css';
|
@import '@/common/style/base.css';
|
||||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||||
|
|||||||
@@ -3,97 +3,99 @@
|
|||||||
<view class="bt-page">
|
<view class="bt-page">
|
||||||
<MemberInfoSubNav title="体测设置" @back="onBack" />
|
<MemberInfoSubNav title="体测设置" @back="onBack" />
|
||||||
<view class="bt-page__body">
|
<view class="bt-page__body">
|
||||||
<view class="bt-card">
|
|
||||||
<text class="bt-card__title">连接与同步</text>
|
|
||||||
<view class="bt-setting">
|
|
||||||
<view>
|
|
||||||
<text class="bt-setting__label">蓝牙自动连接</text>
|
|
||||||
<text class="bt-setting__desc">进入体测页时自动搜索已配对设备</text>
|
|
||||||
</view>
|
|
||||||
<switch
|
|
||||||
:checked="settings.bluetoothEnabled"
|
|
||||||
color="#FF6B35"
|
|
||||||
@change="onSwitch('bluetoothEnabled', $event)"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
<view class="bt-setting">
|
|
||||||
<view>
|
|
||||||
<text class="bt-setting__label">测量完成自动同步</text>
|
|
||||||
<text class="bt-setting__desc">结果自动保存至云端与本地</text>
|
|
||||||
</view>
|
|
||||||
<switch
|
|
||||||
:checked="settings.autoSync"
|
|
||||||
color="#FF6B35"
|
|
||||||
@change="onSwitch('autoSync', $event)"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="bt-card">
|
|
||||||
<text class="bt-card__title">通知与隐私</text>
|
|
||||||
<view class="bt-setting">
|
|
||||||
<view>
|
|
||||||
<text class="bt-setting__label">测量完成通知</text>
|
|
||||||
<text class="bt-setting__desc">体测结束后推送报告摘要</text>
|
|
||||||
</view>
|
|
||||||
<switch
|
|
||||||
:checked="settings.notifyOnComplete"
|
|
||||||
color="#FF6B35"
|
|
||||||
@change="onSwitch('notifyOnComplete', $event)"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
<view class="bt-setting">
|
|
||||||
<view>
|
|
||||||
<text class="bt-setting__label">分享时匿名化</text>
|
|
||||||
<text class="bt-setting__desc">隐藏姓名与手机号</text>
|
|
||||||
</view>
|
|
||||||
<switch
|
|
||||||
:checked="settings.shareAnonymous"
|
|
||||||
color="#FF6B35"
|
|
||||||
@change="onSwitch('shareAnonymous', $event)"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="bt-card">
|
<view class="bt-card">
|
||||||
<text class="bt-card__title">单位制</text>
|
<text class="bt-card__title">单位制</text>
|
||||||
<view class="bt-setting">
|
<view class="bt-setting-row" @tap="toggleUnit">
|
||||||
<view>
|
<text class="bt-setting-row__label">单位制</text>
|
||||||
<text class="bt-setting__label">度量单位</text>
|
<view class="bt-setting-row__value">
|
||||||
<text class="bt-setting__desc">{{ unitLabel }}</text>
|
<text class="bt-setting-row__text">{{ unitLabel }}</text>
|
||||||
</view>
|
<image
|
||||||
<view
|
class="bt-setting-row__arrow"
|
||||||
hover-class="mi-tap--hover"
|
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/rightarrow.png"
|
||||||
:hover-stay-time="150"
|
mode="aspectFit"
|
||||||
@tap="toggleUnit"
|
/>
|
||||||
>
|
|
||||||
<text style="font-size: 14px; color: #1A4A6F; font-weight: 600;">切换</text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="bt-card">
|
<view class="bt-card">
|
||||||
<text class="bt-card__title">设备管理</text>
|
<text class="bt-card__title">设备管理</text>
|
||||||
<view class="bt-device">
|
<view class="bt-device-row">
|
||||||
<view class="bt-device__icon-wrap">
|
<view class="bt-device-row__icon">
|
||||||
<image class="bt-device__icon" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/mappin2.png" mode="aspectFit" />
|
<image
|
||||||
|
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/bluetooth.png"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
</view>
|
</view>
|
||||||
<view class="bt-device__info">
|
<view class="bt-device-row__info">
|
||||||
<text class="bt-device__name">{{ device.name }}</text>
|
<text class="bt-device-row__name">{{ device.name || '未连接设备' }}</text>
|
||||||
<text class="bt-device__status">
|
<text class="bt-device-row__status">
|
||||||
{{ device.connected ? '已连接' : '未连接' }}
|
{{ device.connected ? `已连接 · 电量 ${device.battery}%` : '点击连接体脂秤' }}
|
||||||
· 上次 {{ device.lastConnected || '--' }}
|
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
|
||||||
<view
|
<view
|
||||||
class="bt-btn bt-btn--outline"
|
v-if="device.connected"
|
||||||
style="margin-top: 12px;"
|
class="bt-device-row__disconnect"
|
||||||
hover-class="mi-tap-btn--hover"
|
hover-class="mi-tap-btn--hover"
|
||||||
:hover-stay-time="150"
|
|
||||||
@tap="disconnect"
|
@tap="disconnect"
|
||||||
>
|
>
|
||||||
<text class="bt-btn__text">解除设备配对</text>
|
<text class="bt-device-row__disconnect-text">解除</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="bt-card">
|
||||||
|
<text class="bt-card__title">通知提醒</text>
|
||||||
|
<view class="bt-setting-row">
|
||||||
|
<text class="bt-setting-row__label">体测提醒</text>
|
||||||
|
<switch
|
||||||
|
:checked="settings.remindEnabled"
|
||||||
|
@change="(e) => onSwitch('remindEnabled', e)"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view v-if="settings.remindEnabled" class="bt-setting-row">
|
||||||
|
<text class="bt-setting-row__label">提醒周期</text>
|
||||||
|
<picker
|
||||||
|
mode="selector"
|
||||||
|
:range="['每周', '每两周', '每月']"
|
||||||
|
:value="settings.remindInterval"
|
||||||
|
@change="(e) => onSwitch('remindInterval', e)"
|
||||||
|
>
|
||||||
|
<view class="bt-setting-row__value">
|
||||||
|
<text class="bt-setting-row__text">
|
||||||
|
{{ ['每周', '每两周', '每月'][settings.remindInterval] }}
|
||||||
|
</text>
|
||||||
|
<image
|
||||||
|
class="bt-setting-row__arrow"
|
||||||
|
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/rightarrow.png"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="bt-card">
|
||||||
|
<text class="bt-card__title">隐私设置</text>
|
||||||
|
<view class="bt-setting-row">
|
||||||
|
<text class="bt-setting-row__label">数据可见性</text>
|
||||||
|
<picker
|
||||||
|
mode="selector"
|
||||||
|
:range="['仅本人', '教练可见', '完全公开']"
|
||||||
|
:value="settings.privacy"
|
||||||
|
@change="(e) => onSwitch('privacy', e)"
|
||||||
|
>
|
||||||
|
<view class="bt-setting-row__value">
|
||||||
|
<text class="bt-setting-row__text">
|
||||||
|
{{ ['仅本人', '教练可见', '完全公开'][settings.privacy] }}
|
||||||
|
</text>
|
||||||
|
<image
|
||||||
|
class="bt-setting-row__arrow"
|
||||||
|
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/rightarrow.png"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -101,7 +103,8 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||||
import { PAGE, goBackOrTab } from '@/common/constants/routes.js'
|
import { PAGE, goBackOrTab } from '@/common/constants/routes.js'
|
||||||
import {
|
import {
|
||||||
@@ -113,47 +116,41 @@ import {
|
|||||||
disconnectBodyTestDevice
|
disconnectBodyTestDevice
|
||||||
} from '@/common/memberInfo/bodyTestStore.js'
|
} from '@/common/memberInfo/bodyTestStore.js'
|
||||||
|
|
||||||
export default {
|
const settings = ref({})
|
||||||
components: { MemberInfoSubNav },
|
const device = ref({})
|
||||||
data() {
|
|
||||||
return {
|
const unitLabel = computed(() => {
|
||||||
settings: {},
|
return settings.value.unitSystem === 'imperial' ? '英制 (lb / in)' : '公制 (kg / cm)'
|
||||||
device: {}
|
})
|
||||||
}
|
|
||||||
},
|
function refreshFromStore() {
|
||||||
computed: {
|
|
||||||
unitLabel() {
|
|
||||||
return this.settings.unitSystem === 'imperial' ? '英制 (lb / in)' : '公制 (kg / cm)'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onShow() {
|
|
||||||
this.refreshFromStore()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
refreshFromStore() {
|
|
||||||
const store = loadMemberStore()
|
const store = loadMemberStore()
|
||||||
this.settings = { ...store.bodyTest.settings }
|
settings.value = { ...store.bodyTest.settings }
|
||||||
this.device = { ...store.bodyTest.device }
|
device.value = { ...store.bodyTest.device }
|
||||||
},
|
}
|
||||||
onBack() {
|
|
||||||
|
function onBack() {
|
||||||
goBackOrTab(PAGE.BODY_TEST_HOME)
|
goBackOrTab(PAGE.BODY_TEST_HOME)
|
||||||
},
|
}
|
||||||
onSwitch(key, e) {
|
|
||||||
|
function onSwitch(key, e) {
|
||||||
const store = loadMemberStore()
|
const store = loadMemberStore()
|
||||||
updateBodyTestSettings(store, { [key]: e.detail.value })
|
updateBodyTestSettings(store, { [key]: e.detail.value })
|
||||||
persistMemberStore(store)
|
persistMemberStore(store)
|
||||||
this.settings = { ...store.bodyTest.settings }
|
refreshFromStore()
|
||||||
uni.showToast({ title: '已保存', icon: 'success' })
|
uni.showToast({ title: '已保存', icon: 'success' })
|
||||||
},
|
}
|
||||||
toggleUnit() {
|
|
||||||
|
function toggleUnit() {
|
||||||
const store = loadMemberStore()
|
const store = loadMemberStore()
|
||||||
const next = this.settings.unitSystem === 'metric' ? 'imperial' : 'metric'
|
const next = settings.value.unitSystem === 'metric' ? 'imperial' : 'metric'
|
||||||
updateBodyTestSettings(store, { unitSystem: next })
|
updateBodyTestSettings(store, { unitSystem: next })
|
||||||
persistMemberStore(store)
|
persistMemberStore(store)
|
||||||
this.settings = { ...store.bodyTest.settings }
|
refreshFromStore()
|
||||||
uni.showToast({ title: `已切换为${this.unitLabel}`, icon: 'none' })
|
uni.showToast({ title: `已切换为${unitLabel.value}`, icon: 'none' })
|
||||||
},
|
}
|
||||||
disconnect() {
|
|
||||||
|
function disconnect() {
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '解除配对',
|
title: '解除配对',
|
||||||
content: '解除后下次体测需重新连接设备,确定继续?',
|
content: '解除后下次体测需重新连接设备,确定继续?',
|
||||||
@@ -162,16 +159,16 @@ export default {
|
|||||||
const store = loadMemberStore()
|
const store = loadMemberStore()
|
||||||
disconnectBodyTestDevice(store)
|
disconnectBodyTestDevice(store)
|
||||||
persistMemberStore(store)
|
persistMemberStore(store)
|
||||||
this.refreshFromStore()
|
refreshFromStore()
|
||||||
uni.showToast({ title: '已解除配对', icon: 'success' })
|
uni.showToast({ title: '已解除配对', icon: 'success' })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
refreshFromStore()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
@import '@/common/style/base.css';
|
@import '@/common/style/base.css';
|
||||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||||
|
|||||||
@@ -56,91 +56,100 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||||
import BodyTestTrendChart from '@/components/memberInfo/BodyTestTrendChart.vue'
|
import BodyTestTrendChart from '@/components/memberInfo/BodyTestTrendChart.vue'
|
||||||
import { PAGE, goBackOrTab } from '@/common/constants/routes.js'
|
import { PAGE, goBackOrTab } from '@/common/constants/routes.js'
|
||||||
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||||
import { getBodyTestTrendData, bodyTestMock } from '@/common/memberInfo/bodyTestStore.js'
|
import { getBodyTestTrendData } from '@/common/memberInfo/bodyTestStore.js'
|
||||||
|
|
||||||
|
const tabs = ref([
|
||||||
|
{ key: 'weight', label: '体重' },
|
||||||
|
{ key: 'bodyFat', label: '体脂率' },
|
||||||
|
{ key: 'muscleMass', label: '肌肉量' },
|
||||||
|
{ key: 'bmi', label: 'BMI' }
|
||||||
|
])
|
||||||
|
const activeMetric = ref('weight')
|
||||||
|
const trendPoints = ref([])
|
||||||
|
const chartWidth = ref(300)
|
||||||
|
|
||||||
export default {
|
|
||||||
components: { MemberInfoSubNav, BodyTestTrendChart },
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
tabs: bodyTestMock.trendMetrics,
|
|
||||||
activeMetric: 'weight',
|
|
||||||
trendPoints: [],
|
|
||||||
chartWidth: 300
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
activeLabel() {
|
|
||||||
return this.tabs.find((t) => t.key === this.activeMetric)?.label || ''
|
|
||||||
},
|
|
||||||
activeUnit() {
|
|
||||||
const units = {
|
const units = {
|
||||||
weight: 'kg',
|
weight: 'kg',
|
||||||
bodyFat: '%',
|
bodyFat: '%',
|
||||||
muscleMass: 'kg',
|
muscleMass: 'kg',
|
||||||
bmi: ''
|
bmi: ''
|
||||||
}
|
}
|
||||||
return units[this.activeMetric] || ''
|
|
||||||
},
|
const activeLabel = computed(() => {
|
||||||
trendPointsReversed() {
|
return tabs.value.find((t) => t.key === activeMetric.value)?.label || ''
|
||||||
return [...this.trendPoints].reverse()
|
})
|
||||||
},
|
|
||||||
summaryText() {
|
const activeUnit = computed(() => {
|
||||||
if (this.trendPoints.length < 2) return ''
|
return units[activeMetric.value] || ''
|
||||||
const first = this.trendPoints[0].value
|
})
|
||||||
const last = this.trendPoints[this.trendPoints.length - 1].value
|
|
||||||
|
const trendPointsReversed = computed(() => {
|
||||||
|
return [...trendPoints.value].reverse()
|
||||||
|
})
|
||||||
|
|
||||||
|
const summaryText = computed(() => {
|
||||||
|
if (trendPoints.value.length < 2) return ''
|
||||||
|
const first = trendPoints.value[0].value
|
||||||
|
const last = trendPoints.value[trendPoints.value.length - 1].value
|
||||||
const diff = Math.round((last - first) * 10) / 10
|
const diff = Math.round((last - first) * 10) / 10
|
||||||
const sign = diff > 0 ? '上升' : diff < 0 ? '下降' : '持平'
|
const sign = diff > 0 ? '上升' : diff < 0 ? '下降' : '持平'
|
||||||
const abs = Math.abs(diff)
|
const abs = Math.abs(diff)
|
||||||
const lowerBetter = ['weight', 'bodyFat'].includes(this.activeMetric)
|
const lowerBetter = ['weight', 'bodyFat'].includes(activeMetric.value)
|
||||||
let advice = ''
|
let advice = ''
|
||||||
if (diff !== 0) {
|
if (diff !== 0) {
|
||||||
const good = lowerBetter ? diff < 0 : diff > 0
|
const good = lowerBetter ? diff < 0 : diff > 0
|
||||||
advice = good ? ',变化方向符合健康目标' : ',建议关注饮食与训练计划'
|
advice = good ? ',变化方向符合健康目标' : ',建议关注饮食与训练计划'
|
||||||
}
|
}
|
||||||
return `期间${this.activeLabel}${sign} ${abs}${this.activeUnit}${advice}`
|
return `期间${activeLabel.value}${sign} ${abs}${activeUnit.value}${advice}`
|
||||||
}
|
})
|
||||||
},
|
|
||||||
onLoad(options) {
|
function onBack() {
|
||||||
if (options?.metric) {
|
|
||||||
this.activeMetric = options.metric
|
|
||||||
}
|
|
||||||
this.chartWidth = uni.getSystemInfoSync().windowWidth - 64
|
|
||||||
this.loadTrend()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onBack() {
|
|
||||||
goBackOrTab(PAGE.BODY_TEST_HOME)
|
goBackOrTab(PAGE.BODY_TEST_HOME)
|
||||||
},
|
}
|
||||||
switchMetric(key) {
|
|
||||||
this.activeMetric = key
|
function switchMetric(key) {
|
||||||
this.loadTrend()
|
activeMetric.value = key
|
||||||
},
|
loadTrend()
|
||||||
loadTrend() {
|
}
|
||||||
|
|
||||||
|
function loadTrend() {
|
||||||
const store = loadMemberStore()
|
const store = loadMemberStore()
|
||||||
this.trendPoints = getBodyTestTrendData(store, this.activeMetric, 6)
|
trendPoints.value = getBodyTestTrendData(store, activeMetric.value, 6)
|
||||||
},
|
}
|
||||||
rowDiffText(current, older) {
|
|
||||||
|
function rowDiffText(current, older) {
|
||||||
const diff = Math.round((current.value - older.value) * 10) / 10
|
const diff = Math.round((current.value - older.value) * 10) / 10
|
||||||
const sign = diff > 0 ? '+' : ''
|
const sign = diff > 0 ? '+' : ''
|
||||||
return `${sign}${diff}${this.activeUnit}`
|
return `${sign}${diff}${activeUnit.value}`
|
||||||
},
|
}
|
||||||
rowDiffColor(current, older) {
|
|
||||||
|
function rowDiffColor(current, older) {
|
||||||
const diff = current.value - older.value
|
const diff = current.value - older.value
|
||||||
if (diff === 0) return '#8A99B4'
|
if (diff === 0) return '#8A99B4'
|
||||||
const lowerBetter = ['weight', 'bodyFat'].includes(this.activeMetric)
|
const lowerBetter = ['weight', 'bodyFat'].includes(activeMetric.value)
|
||||||
const good = lowerBetter ? diff < 0 : diff > 0
|
const good = lowerBetter ? diff < 0 : diff > 0
|
||||||
return good ? '#2ECC71' : '#F39C12'
|
return good ? '#2ECC71' : '#F39C12'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const pages = getCurrentPages()
|
||||||
|
const currentPage = pages[pages.length - 1]
|
||||||
|
const options = currentPage?.options || {}
|
||||||
|
if (options?.metric) {
|
||||||
|
activeMetric.value = options.metric
|
||||||
}
|
}
|
||||||
}
|
chartWidth.value = uni.getSystemInfoSync().windowWidth - 64
|
||||||
|
loadTrend()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
@import '@/common/style/base.css';
|
@import '@/common/style/base.css';
|
||||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||||
|
|||||||
@@ -99,6 +99,16 @@
|
|||||||
|
|
||||||
<view class="bk-card__footer">
|
<view class="bk-card__footer">
|
||||||
<text class="bk-card__footer-info">{{ item.footerText }}</text>
|
<text class="bk-card__footer-info">{{ item.footerText }}</text>
|
||||||
|
<view class="bk-card__actions">
|
||||||
|
<view
|
||||||
|
v-if="item.canSignin"
|
||||||
|
class="bk-card__signin"
|
||||||
|
hover-class="mi-tap-btn--hover"
|
||||||
|
:hover-stay-time="150"
|
||||||
|
@tap.stop="signinCourse(item)"
|
||||||
|
>
|
||||||
|
<text class="bk-card__signin-text">签到</text>
|
||||||
|
</view>
|
||||||
<view
|
<view
|
||||||
v-if="item.canCancel"
|
v-if="item.canCancel"
|
||||||
class="bk-card__cancel"
|
class="bk-card__cancel"
|
||||||
@@ -111,6 +121,7 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<view v-if="!displayedBookings.length" class="booking-page__empty">
|
<view v-if="!displayedBookings.length" class="booking-page__empty">
|
||||||
<text class="booking-page__empty-text">
|
<text class="booking-page__empty-text">
|
||||||
@@ -122,58 +133,93 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||||
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
|
import { PAGE, navigateToPage, backToMemberCenter } from '@/common/constants/routes.js'
|
||||||
import { bookingMock } from '@/common/memberInfo/mockData.js'
|
|
||||||
import {
|
import {
|
||||||
loadMemberStore,
|
loadMemberStore,
|
||||||
cancelOngoingBooking,
|
cancelOngoingBooking,
|
||||||
formatUpcomingAlert
|
formatUpcomingAlert
|
||||||
} from '@/common/memberInfo/store.js'
|
} from '@/common/memberInfo/store.js'
|
||||||
import { canCancelBooking } from '@/common/memberInfo/bookingStore.js'
|
import { canCancelBooking, canSigninCourse } from '@/common/memberInfo/bookingStore.js'
|
||||||
import { subPageMixin } from '@/common/memberInfo/mixins.js'
|
import { signinGroupCourse, cancelBooking as cancelBookingApi } from '@/api/main.js'
|
||||||
|
|
||||||
export default {
|
const tabs = ref([])
|
||||||
components: { MemberInfoSubNav },
|
const ongoingBookings = ref([])
|
||||||
mixins: [subPageMixin],
|
const historyBookings = ref([])
|
||||||
data() {
|
const activeTab = ref('ongoing')
|
||||||
return {
|
|
||||||
tabs: bookingMock.tabs,
|
const displayedBookings = computed(() => {
|
||||||
ongoingBookings: [],
|
return activeTab.value === 'ongoing'
|
||||||
historyBookings: [],
|
? ongoingBookings.value
|
||||||
activeTab: 'ongoing'
|
: historyBookings.value
|
||||||
}
|
})
|
||||||
},
|
|
||||||
computed: {
|
const upcomingAlert = computed(() => {
|
||||||
displayedBookings() {
|
return formatUpcomingAlert(ongoingBookings.value[0])
|
||||||
return this.activeTab === 'ongoing'
|
})
|
||||||
? this.ongoingBookings
|
|
||||||
: this.historyBookings
|
function refreshFromStore() {
|
||||||
},
|
|
||||||
upcomingAlert() {
|
|
||||||
return formatUpcomingAlert(this.ongoingBookings[0])
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onShow() {
|
|
||||||
this.refreshFromStore()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
refreshFromStore() {
|
|
||||||
const store = loadMemberStore()
|
const store = loadMemberStore()
|
||||||
this.ongoingBookings = store.ongoingBookings.map((item) => ({
|
ongoingBookings.value = store.ongoingBookings.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
canCancel: canCancelBooking(item)
|
canCancel: canCancelBooking(item),
|
||||||
|
canSignin: canSigninCourse(item)
|
||||||
}))
|
}))
|
||||||
this.historyBookings = store.historyBookings.map((item) => ({ ...item }))
|
historyBookings.value = store.historyBookings.map((item) => ({ ...item }))
|
||||||
},
|
}
|
||||||
goCourseList() {
|
|
||||||
|
function goBack() {
|
||||||
|
backToMemberCenter()
|
||||||
|
}
|
||||||
|
|
||||||
|
function goCourseList() {
|
||||||
navigateToPage(PAGE.COURSE_LIST)
|
navigateToPage(PAGE.COURSE_LIST)
|
||||||
},
|
}
|
||||||
setActiveTab(tab) {
|
|
||||||
this.activeTab = tab
|
function setActiveTab(tab) {
|
||||||
},
|
activeTab.value = tab
|
||||||
cancelBooking(item) {
|
}
|
||||||
|
|
||||||
|
// 签到团课
|
||||||
|
async function signinCourse(item) {
|
||||||
|
const store = loadMemberStore()
|
||||||
|
const memberId = store.memberProfile?.id || store.profile?.id
|
||||||
|
if (!memberId) {
|
||||||
|
uni.showToast({ title: '请先登录', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.showModal({
|
||||||
|
title: '确认签到',
|
||||||
|
content: `确认签到「${item.title}」吗?`,
|
||||||
|
success: async (res) => {
|
||||||
|
if (!res.confirm) return
|
||||||
|
|
||||||
|
uni.showLoading({ title: '签到中...', mask: true })
|
||||||
|
try {
|
||||||
|
const result = await signinGroupCourse(Number(memberId), Number(item.courseId || item.id))
|
||||||
|
uni.hideLoading()
|
||||||
|
|
||||||
|
if (result.code === 200 || result.success) {
|
||||||
|
uni.showToast({ title: '签到成功', icon: 'success' })
|
||||||
|
// 更新本地状态
|
||||||
|
refreshFromStore()
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: result.message || '签到失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
uni.hideLoading()
|
||||||
|
console.error('[booking] 签到失败:', e)
|
||||||
|
uni.showToast({ title: e.message || '签到失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消预约
|
||||||
|
async function cancelBooking(item) {
|
||||||
if (!canCancelBooking(item)) {
|
if (!canCancelBooking(item)) {
|
||||||
uni.showToast({ title: '距开课不足2小时,无法取消', icon: 'none' })
|
uni.showToast({ title: '距开课不足2小时,无法取消', icon: 'none' })
|
||||||
return
|
return
|
||||||
@@ -181,24 +227,39 @@ export default {
|
|||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '取消预约',
|
title: '取消预约',
|
||||||
content: `确定要取消「${item.title}」吗?`,
|
content: `确定要取消「${item.title}」吗?`,
|
||||||
success: (res) => {
|
success: async (res) => {
|
||||||
if (!res.confirm) return
|
if (!res.confirm) return
|
||||||
|
|
||||||
|
uni.showLoading({ title: '取消中...', mask: true })
|
||||||
|
try {
|
||||||
|
// 调用后端API取消预约
|
||||||
|
const result = await cancelBookingApi(Number(item.id))
|
||||||
|
uni.hideLoading()
|
||||||
|
|
||||||
|
if (result.code === 200 || result.success) {
|
||||||
|
// 更新本地状态
|
||||||
const store = loadMemberStore()
|
const store = loadMemberStore()
|
||||||
const result = cancelOngoingBooking(store, item.id)
|
const localResult = cancelOngoingBooking(store, item.id)
|
||||||
if (!result.ok) {
|
if (localResult.ok) {
|
||||||
uni.showToast({ title: result.message, icon: 'none' })
|
refreshFromStore()
|
||||||
return
|
|
||||||
}
|
}
|
||||||
this.refreshFromStore()
|
|
||||||
uni.showToast({ title: '已取消', icon: 'success' })
|
uni.showToast({ title: '已取消', icon: 'success' })
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: result.message || '取消失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
uni.hideLoading()
|
||||||
|
console.error('[booking] 取消预约失败:', e)
|
||||||
|
uni.showToast({ title: e.message || '取消失败', icon: 'none' })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
refreshFromStore()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
@import '@/common/style/base.css';
|
@import '@/common/style/base.css';
|
||||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||||
|
|||||||
@@ -46,45 +46,48 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||||
import { moduleMock, getCheckInHistory } from '@/common/memberInfo/moduleStore.js'
|
import { getCheckInHistory } from '@/common/memberInfo/moduleStore.js'
|
||||||
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||||
import { subPageMixin } from '@/common/memberInfo/mixins.js'
|
import { backToMemberCenter } from '@/common/constants/routes.js'
|
||||||
|
|
||||||
export default {
|
const tabs = ref([
|
||||||
components: { MemberInfoSubNav },
|
{ key: 'all', label: '全部' },
|
||||||
mixins: [subPageMixin],
|
{ key: 'signin', label: '签到' },
|
||||||
data() {
|
{ key: 'trial', label: '体验' },
|
||||||
return {
|
{ key: 'event', label: '活动' }
|
||||||
tabs: moduleMock.checkInTabs,
|
])
|
||||||
activeFilter: 'all',
|
const activeFilter = ref('all')
|
||||||
list: []
|
const list = ref([])
|
||||||
}
|
|
||||||
},
|
const filteredList = computed(() => {
|
||||||
computed: {
|
if (activeFilter.value === 'all') return list.value
|
||||||
filteredList() {
|
return list.value.filter((i) => i.tagTheme === activeFilter.value)
|
||||||
if (this.activeFilter === 'all') return this.list
|
})
|
||||||
return this.list.filter((i) => i.tagTheme === this.activeFilter)
|
|
||||||
}
|
function refresh() {
|
||||||
},
|
|
||||||
onShow() {
|
|
||||||
const store = loadMemberStore()
|
const store = loadMemberStore()
|
||||||
this.list = getCheckInHistory(store, 'all')
|
list.value = getCheckInHistory(store, 'all')
|
||||||
},
|
}
|
||||||
methods: {
|
|
||||||
showDetail(item) {
|
function goBack() {
|
||||||
|
backToMemberCenter()
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDetail(item) {
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: item.title,
|
title: item.title,
|
||||||
content: `${item.time}\n地点:${item.location}\n类型:${item.tag}`,
|
content: `${item.time}\n地点:${item.location}\n类型:${item.tag}`,
|
||||||
showCancel: false
|
showCancel: false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
refresh()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
@import '@/common/style/base.css';
|
@import '@/common/style/base.css';
|
||||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||||
|
|||||||
@@ -33,38 +33,37 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||||
import { PAGE, goBackOrTab } from '@/common/constants/routes.js'
|
import { PAGE, goBackOrTab } from '@/common/constants/routes.js'
|
||||||
import { loadMemberStore, persistMemberStore } from '@/common/memberInfo/store.js'
|
import { loadMemberStore, persistMemberStore } from '@/common/memberInfo/store.js'
|
||||||
import { getCouponCenterList, claimCouponFromCenter } from '@/common/memberInfo/moduleStore.js'
|
import { getCouponCenterList, claimCouponFromCenter } from '@/common/memberInfo/moduleStore.js'
|
||||||
|
|
||||||
export default {
|
const list = ref([])
|
||||||
components: { MemberInfoSubNav },
|
|
||||||
data() {
|
onMounted(() => {
|
||||||
return { list: [] }
|
|
||||||
},
|
|
||||||
onShow() {
|
|
||||||
const store = loadMemberStore()
|
const store = loadMemberStore()
|
||||||
this.list = getCouponCenterList(store)
|
list.value = getCouponCenterList(store)
|
||||||
},
|
})
|
||||||
methods: {
|
|
||||||
onBack() { goBackOrTab(PAGE.COUPONS) },
|
function onBack() {
|
||||||
claim(item) {
|
goBackOrTab(PAGE.COUPONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
function claim(item) {
|
||||||
if (item.claimed) return
|
if (item.claimed) return
|
||||||
const store = loadMemberStore()
|
const store = loadMemberStore()
|
||||||
const result = claimCouponFromCenter(store, item.id)
|
const result = claimCouponFromCenter(store, item.id)
|
||||||
uni.showToast({ title: result.message, icon: result.ok ? 'success' : 'none' })
|
uni.showToast({ title: result.message, icon: result.ok ? 'success' : 'none' })
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
persistMemberStore(store)
|
persistMemberStore(store)
|
||||||
this.list = getCouponCenterList(store)
|
list.value = getCouponCenterList(store)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
@import '@/common/style/base.css';
|
@import '@/common/style/base.css';
|
||||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||||
|
|||||||
@@ -36,31 +36,33 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||||
import { PAGE, navigateToPage, goBackOrTab } from '@/common/constants/routes.js'
|
import { PAGE, navigateToPage, goBackOrTab } from '@/common/constants/routes.js'
|
||||||
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||||
import { getCouponById } from '@/common/memberInfo/moduleStore.js'
|
import { getCouponById } from '@/common/memberInfo/moduleStore.js'
|
||||||
|
|
||||||
export default {
|
const coupon = ref(null)
|
||||||
components: { MemberInfoSubNav },
|
|
||||||
data() {
|
onMounted(() => {
|
||||||
return { coupon: null }
|
const pages = getCurrentPages()
|
||||||
},
|
const currentPage = pages[pages.length - 1]
|
||||||
onLoad(options) {
|
const options = currentPage?.options || {}
|
||||||
const store = loadMemberStore()
|
const store = loadMemberStore()
|
||||||
this.coupon = getCouponById(store, options?.id)
|
coupon.value = getCouponById(store, options?.id)
|
||||||
},
|
})
|
||||||
methods: {
|
|
||||||
onBack() { goBackOrTab(PAGE.COUPONS) },
|
function onBack() {
|
||||||
useNow() {
|
goBackOrTab(PAGE.COUPONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
function useNow() {
|
||||||
navigateToPage(PAGE.COURSE_LIST)
|
navigateToPage(PAGE.COURSE_LIST)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
@import '@/common/style/base.css';
|
@import '@/common/style/base.css';
|
||||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||||
|
|||||||
@@ -61,54 +61,55 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||||
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
|
import { PAGE, navigateToPage, backToMemberCenter } from '@/common/constants/routes.js'
|
||||||
import { moduleMock, getCouponsByStatus, useCoupon, deleteExpiredCoupon } from '@/common/memberInfo/moduleStore.js'
|
import { getCouponsByStatus, useCoupon, deleteExpiredCoupon } from '@/common/memberInfo/moduleStore.js'
|
||||||
import { loadMemberStore, persistMemberStore } from '@/common/memberInfo/store.js'
|
import { loadMemberStore, persistMemberStore } from '@/common/memberInfo/store.js'
|
||||||
import { subPageMixin } from '@/common/memberInfo/mixins.js'
|
|
||||||
|
|
||||||
export default {
|
const tabs = ref([
|
||||||
components: { MemberInfoSubNav },
|
{ key: 'available', label: '可用' },
|
||||||
mixins: [subPageMixin],
|
{ key: 'used', label: '已用' },
|
||||||
data() {
|
{ key: 'expired', label: '已失效' }
|
||||||
|
])
|
||||||
|
const activeTab = ref('available')
|
||||||
|
const coupons = ref([])
|
||||||
|
|
||||||
|
const displayedCoupons = computed(() => {
|
||||||
|
return coupons.value.filter((c) => c.status === activeTab.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const countByTab = computed(() => {
|
||||||
return {
|
return {
|
||||||
tabs: moduleMock.couponTabs,
|
available: coupons.value.filter((c) => c.status === 'available').length,
|
||||||
activeTab: 'available',
|
used: coupons.value.filter((c) => c.status === 'used').length,
|
||||||
coupons: []
|
expired: coupons.value.filter((c) => c.status === 'expired').length
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
computed: {
|
|
||||||
displayedCoupons() {
|
const activeTabLabel = computed(() => {
|
||||||
return this.coupons.filter((c) => c.status === this.activeTab)
|
return tabs.value.find((t) => t.key === activeTab.value)?.label || ''
|
||||||
},
|
})
|
||||||
countByTab() {
|
|
||||||
return {
|
function refreshList() {
|
||||||
available: this.coupons.filter((c) => c.status === 'available').length,
|
|
||||||
used: this.coupons.filter((c) => c.status === 'used').length,
|
|
||||||
expired: this.coupons.filter((c) => c.status === 'expired').length
|
|
||||||
}
|
|
||||||
},
|
|
||||||
activeTabLabel() {
|
|
||||||
return this.tabs.find((t) => t.key === this.activeTab)?.label || ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onShow() {
|
|
||||||
this.refreshList()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
refreshList() {
|
|
||||||
const store = loadMemberStore()
|
const store = loadMemberStore()
|
||||||
this.coupons = [
|
coupons.value = [
|
||||||
...getCouponsByStatus(store, 'available'),
|
...getCouponsByStatus(store, 'available'),
|
||||||
...getCouponsByStatus(store, 'used'),
|
...getCouponsByStatus(store, 'used'),
|
||||||
...getCouponsByStatus(store, 'expired')
|
...getCouponsByStatus(store, 'expired')
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
onCouponTap(item) {
|
|
||||||
|
function goBack() {
|
||||||
|
backToMemberCenter()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCouponTap(item) {
|
||||||
navigateToPage(`${PAGE.COUPON_DETAIL}?id=${item.id}`)
|
navigateToPage(`${PAGE.COUPON_DETAIL}?id=${item.id}`)
|
||||||
},
|
}
|
||||||
useNow(item) {
|
|
||||||
|
function useNow(item) {
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '使用优惠券',
|
title: '使用优惠券',
|
||||||
content: `确认使用「${item.title}」?`,
|
content: `确认使用「${item.title}」?`,
|
||||||
@@ -117,25 +118,27 @@ export default {
|
|||||||
const store = loadMemberStore()
|
const store = loadMemberStore()
|
||||||
useCoupon(store, item.id)
|
useCoupon(store, item.id)
|
||||||
persistMemberStore(store)
|
persistMemberStore(store)
|
||||||
this.refreshList()
|
refreshList()
|
||||||
navigateToPage(PAGE.COURSE_LIST)
|
navigateToPage(PAGE.COURSE_LIST)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
removeExpired(item) {
|
|
||||||
|
function removeExpired(item) {
|
||||||
const store = loadMemberStore()
|
const store = loadMemberStore()
|
||||||
deleteExpiredCoupon(store, item.id)
|
deleteExpiredCoupon(store, item.id)
|
||||||
persistMemberStore(store)
|
persistMemberStore(store)
|
||||||
this.refreshList()
|
refreshList()
|
||||||
},
|
}
|
||||||
goCenter() {
|
|
||||||
|
function goCenter() {
|
||||||
navigateToPage(PAGE.COUPON_CENTER)
|
navigateToPage(PAGE.COUPON_CENTER)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
refreshList()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
@import '@/common/style/base.css';
|
@import '@/common/style/base.css';
|
||||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||||
|
|||||||
@@ -0,0 +1,569 @@
|
|||||||
|
<template>
|
||||||
|
<view class="login-page">
|
||||||
|
<!-- 自定义Toast弹窗 -->
|
||||||
|
<view class="custom-toast" :class="{ show: toastShow }">
|
||||||
|
<text class="toast-text">{{ toastMessage }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="login-header">
|
||||||
|
<view class="logo-wrapper">
|
||||||
|
<image class="logo-image" src="/static/logo.png" mode="aspectFit" />
|
||||||
|
<text class="app-name">活氧舱欢迎您</text>
|
||||||
|
<text class="app-slogan">享受运动,拥抱健康</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="card-container">
|
||||||
|
<!-- 手机号+验证码登录卡片 -->
|
||||||
|
<view class="login-card sms-card" :class="{ active: activeCard === 'sms' }">
|
||||||
|
<view class="card-body">
|
||||||
|
<text class="phone-display">验证码登录</text>
|
||||||
|
<text class="auth-tip">认证服务由中国移动提供</text>
|
||||||
|
|
||||||
|
<view class="form-group">
|
||||||
|
<view class="input-wrapper" :class="{ focused: isSmsPhoneFocused }">
|
||||||
|
<input
|
||||||
|
v-model="smsPhone"
|
||||||
|
type="number"
|
||||||
|
placeholder="请输入手机号"
|
||||||
|
maxlength="11"
|
||||||
|
class="phone-input"
|
||||||
|
confirm-type="next"
|
||||||
|
@focus="isSmsPhoneFocused = true"
|
||||||
|
@blur="isSmsPhoneFocused = false"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-group">
|
||||||
|
<view class="input-wrapper code-input" :class="{ focused: isCodeFocused }">
|
||||||
|
<input
|
||||||
|
v-model="code"
|
||||||
|
type="number"
|
||||||
|
placeholder="请输入验证码"
|
||||||
|
maxlength="6"
|
||||||
|
class="phone-input"
|
||||||
|
confirm-type="done"
|
||||||
|
@focus="isCodeFocused = true"
|
||||||
|
@blur="isCodeFocused = false"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="send-code-btn"
|
||||||
|
:class="{ disabled: countdown > 0 || isLoading }"
|
||||||
|
@tap="handleSendCode"
|
||||||
|
:disabled="countdown > 0 || isLoading"
|
||||||
|
>
|
||||||
|
<text>{{ countdown > 0 ? `${countdown}秒后重发` : '发送验证码' }}</text>
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<button class="login-btn primary" :class="{ disabled: !canSmsSubmit, loading: isLoading }" @tap="handleSmsLogin" :disabled="!canSmsSubmit || isLoading">
|
||||||
|
<text>{{ isLoading ? '登录中...' : '验证码登录' }}</text>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="login-btn secondary" @tap="switchToPhone">
|
||||||
|
<text>其他登录方式</text>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<view class="agreement-text" :class="{ shake: shakeAgreement }">
|
||||||
|
<checkbox-group @change="onAgreementChange">
|
||||||
|
<checkbox :checked="agreed" value="agreed" color="#FF8C42" />
|
||||||
|
</checkbox-group>
|
||||||
|
<text>登录即表示同意</text>
|
||||||
|
<text class="link" @tap="showAgreement">《用户协议》</text>
|
||||||
|
<text>和</text>
|
||||||
|
<text class="link" @tap="showPrivacy">《隐私政策》</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 手机号一键登录卡片 -->
|
||||||
|
<view class="login-card phone-card" :class="{ active: activeCard === 'phone' }">
|
||||||
|
<view class="card-body">
|
||||||
|
<text class="phone-display">本机号码登录</text>
|
||||||
|
<text class="auth-tip">认证服务由中国移动提供</text>
|
||||||
|
|
||||||
|
<view class="form-group phone-login">
|
||||||
|
<view class="input-wrapper" :class="{ focused: isInputFocused }">
|
||||||
|
<input
|
||||||
|
v-model="phone"
|
||||||
|
type="number"
|
||||||
|
placeholder="请输入手机号"
|
||||||
|
maxlength="11"
|
||||||
|
class="phone-input"
|
||||||
|
confirm-type="done"
|
||||||
|
@focus="isInputFocused = true"
|
||||||
|
@blur="isInputFocused = false"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<button class="login-btn primary" :class="{ disabled: !canSubmit, loading: isLoading }" @tap="handlePhoneLogin" :disabled="!canSubmit || isLoading">
|
||||||
|
<text>{{ isLoading ? '登录中...' : '一键登录' }}</text>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="login-btn secondary" @tap="switchToSms">
|
||||||
|
<text>其他登录方式</text>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<view class="agreement-text" :class="{ shake: shakeAgreement }">
|
||||||
|
<checkbox-group @change="onAgreementChange">
|
||||||
|
<checkbox :checked="agreed" value="agreed" color="#FF8C42" />
|
||||||
|
</checkbox-group>
|
||||||
|
<text>登录即表示同意</text>
|
||||||
|
<text class="link" @tap="showAgreement">《用户协议》</text>
|
||||||
|
<text>和</text>
|
||||||
|
<text class="link" @tap="showPrivacy">《隐私政策》</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onUnmounted } from 'vue'
|
||||||
|
import { setToken } from '@/utils/request.js'
|
||||||
|
|
||||||
|
// 测试数据
|
||||||
|
const TEST_TOKEN = 'test_token_123456'
|
||||||
|
const TEST_MEMBER_INFO = {
|
||||||
|
memberId: 10001,
|
||||||
|
memberNo: 'M001',
|
||||||
|
phone: '13800138000',
|
||||||
|
nickname: '测试用户',
|
||||||
|
avatar: '/static/logo.png',
|
||||||
|
accessToken: TEST_TOKEN,
|
||||||
|
isNewUser: false
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeCard = ref('phone')
|
||||||
|
const phone = ref('')
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const isInputFocused = ref(false)
|
||||||
|
const agreed = ref(false)
|
||||||
|
const shakeAgreement = ref(false)
|
||||||
|
|
||||||
|
const smsPhone = ref('')
|
||||||
|
const isSmsPhoneFocused = ref(false)
|
||||||
|
const code = ref('')
|
||||||
|
const isCodeFocused = ref(false)
|
||||||
|
const countdown = ref(0)
|
||||||
|
let timer = null
|
||||||
|
|
||||||
|
// 自定义Toast
|
||||||
|
const toastShow = ref(false)
|
||||||
|
const toastMessage = ref('')
|
||||||
|
let toastTimer = null
|
||||||
|
|
||||||
|
function showToast(msg) {
|
||||||
|
if (toastTimer) clearTimeout(toastTimer)
|
||||||
|
toastMessage.value = msg
|
||||||
|
toastShow.value = true
|
||||||
|
toastTimer = setTimeout(() => {
|
||||||
|
toastShow.value = false
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const canSubmit = computed(() => {
|
||||||
|
return /^1[3-9]\d{9}$/.test(phone.value) && !isLoading.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const canSmsSubmit = computed(() => {
|
||||||
|
return /^1[3-9]\d{9}$/.test(smsPhone.value) && /^\d{6}$/.test(code.value) && !isLoading.value
|
||||||
|
})
|
||||||
|
|
||||||
|
function onAgreementChange(e) {
|
||||||
|
agreed.value = e.detail.value.includes('agreed')
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchToSms() {
|
||||||
|
activeCard.value = 'sms'
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchToPhone() {
|
||||||
|
activeCard.value = 'phone'
|
||||||
|
phone.value = ''
|
||||||
|
phoneError.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAgreement() {
|
||||||
|
uni.showModal({ title: '用户协议', content: '这里是用户协议内容...', showCancel: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPrivacy() {
|
||||||
|
uni.showModal({ title: '隐私政策', content: '这里是隐私政策内容...', showCancel: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
function validatePhone() {
|
||||||
|
if (!phone.value) {
|
||||||
|
showToast('请输入手机号')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!/^1[3-9]\d{9}$/.test(phone.value)) {
|
||||||
|
showToast('请输入正确的手机号')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateSmsPhone() {
|
||||||
|
if (!smsPhone.value) {
|
||||||
|
showToast('请输入手机号')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!/^1[3-9]\d{9}$/.test(smsPhone.value)) {
|
||||||
|
showToast('请输入正确的手机号')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePhoneLogin() {
|
||||||
|
if (!agreed.value) {
|
||||||
|
triggerAgreementShake()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validatePhone()) return
|
||||||
|
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setToken(TEST_TOKEN)
|
||||||
|
uni.setStorageSync('memberInfo', TEST_MEMBER_INFO)
|
||||||
|
uni.setStorageSync('isLogin', true)
|
||||||
|
showToast('登录成功')
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.switchTab({ url: '/pages/memberInfo/memberInfo' })
|
||||||
|
}, 1500)
|
||||||
|
isLoading.value = false
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerAgreementShake() {
|
||||||
|
shakeAgreement.value = true
|
||||||
|
showToast('请先同意用户协议和隐私政策')
|
||||||
|
setTimeout(() => {
|
||||||
|
shakeAgreement.value = false
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSendCode() {
|
||||||
|
if (countdown.value > 0) {
|
||||||
|
showToast('验证码在有效期内,请勿重复发送')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateSmsPhone()) return
|
||||||
|
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
countdown.value = 300
|
||||||
|
startCountdown()
|
||||||
|
showToast('验证码已发送')
|
||||||
|
isLoading.value = false
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
function startCountdown() {
|
||||||
|
if (timer) clearInterval(timer)
|
||||||
|
timer = setInterval(() => {
|
||||||
|
countdown.value--
|
||||||
|
if (countdown.value <= 0) {
|
||||||
|
clearInterval(timer)
|
||||||
|
timer = null
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSmsLogin() {
|
||||||
|
if (!agreed.value) {
|
||||||
|
triggerAgreementShake()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateSmsPhone()) return
|
||||||
|
if (!code.value) {
|
||||||
|
showToast('请输入验证码')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!/^\d{6}$/.test(code.value)) {
|
||||||
|
showToast('请输入6位验证码')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setToken(TEST_TOKEN)
|
||||||
|
uni.setStorageSync('memberInfo', TEST_MEMBER_INFO)
|
||||||
|
showToast('登录成功')
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.switchTab({ url: '/pages/memberInfo/memberInfo' })
|
||||||
|
}, 1500)
|
||||||
|
isLoading.value = false
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (timer) clearInterval(timer)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
// 自定义Toast弹窗样式
|
||||||
|
.custom-toast {
|
||||||
|
position: fixed;
|
||||||
|
top: -100rpx;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%) translateY(0);
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
border-radius: $radius-lg;
|
||||||
|
padding: 24rpx 48rpx;
|
||||||
|
z-index: 9999;
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
&.show {
|
||||||
|
top: 120rpx;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-text {
|
||||||
|
color: $text-inverse;
|
||||||
|
font-size: 28rpx;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: $bg-gradient-primary;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 48rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 80rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-image {
|
||||||
|
width: 140rpx;
|
||||||
|
height: 140rpx;
|
||||||
|
border-radius: $radius-lg;
|
||||||
|
box-shadow: $shadow-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-name {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
font-size: 42rpx;
|
||||||
|
font-weight: $font-weight-extrabold;
|
||||||
|
color: $primary-dark;
|
||||||
|
letter-spacing: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-slogan {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: $text-light;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 620rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
position: absolute;
|
||||||
|
height: 700rpx;
|
||||||
|
background: $bg-white;
|
||||||
|
border-radius: $radius-lg;
|
||||||
|
padding: 48rpx 40rpx;
|
||||||
|
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
will-change: transform, z-index, filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sms-card {
|
||||||
|
z-index: 1;
|
||||||
|
top: -60rpx;
|
||||||
|
left: -100rpx;
|
||||||
|
box-shadow: 0 16rpx 40rpx rgba(0, 0, 0, 0.12);
|
||||||
|
filter: blur(10rpx);
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
z-index: 10;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
box-shadow: 0 24rpx 60rpx rgba(0, 0, 0, 0.18), 0 8rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||||
|
filter: blur(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-card {
|
||||||
|
z-index: 10;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
box-shadow: 0 24rpx 60rpx rgba(0, 0, 0, 0.18), 0 8rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||||
|
filter: blur(0);
|
||||||
|
|
||||||
|
&:not(.active) {
|
||||||
|
z-index: 1;
|
||||||
|
top: -60rpx;
|
||||||
|
left: -100rpx;
|
||||||
|
box-shadow: 0 16rpx 40rpx rgba(0, 0, 0, 0.12);
|
||||||
|
filter: blur(10rpx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-display {
|
||||||
|
font-size: 48rpx;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
color: $text-dark;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-tip {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $text-light;
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-login{
|
||||||
|
margin-bottom: 150rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0 32rpx;
|
||||||
|
height: 100rpx;
|
||||||
|
border-bottom: 2rpx solid $border-light;
|
||||||
|
|
||||||
|
&.code-input {
|
||||||
|
padding-right: 16rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-input {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 34rpx;
|
||||||
|
color: $text-dark;
|
||||||
|
letter-spacing: 2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-code-btn {
|
||||||
|
width: 200rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
background: $accent-orange;
|
||||||
|
color: $text-inverse;
|
||||||
|
font-size: 26rpx;
|
||||||
|
border-radius: $radius-sm;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
background: $border-light;
|
||||||
|
color: $text-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 100rpx;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
background: $gradient-orange;
|
||||||
|
color: $text-inverse;
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
box-shadow: $shadow-orange-glow;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
background: $border-light;
|
||||||
|
color: $text-light;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
background: $accent-orange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.secondary {
|
||||||
|
background: #F8F9FA;
|
||||||
|
color: $text-dark;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: $font-weight-medium;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: #E9ECEF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-text {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $text-light;
|
||||||
|
margin-top: 36rpx;
|
||||||
|
|
||||||
|
.link {
|
||||||
|
color: $accent-orange;
|
||||||
|
margin: 0 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.shake {
|
||||||
|
animation: shake 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shake {
|
||||||
|
0%, 100% { transform: translateX(0); }
|
||||||
|
10%, 30%, 50%, 70%, 90% { transform: translateX(-8rpx); }
|
||||||
|
20%, 40%, 60%, 80% { transform: translateX(8rpx); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -117,52 +117,48 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||||
import { memberCardMock } from '@/common/memberInfo/mockData.js'
|
|
||||||
import {
|
import {
|
||||||
loadMemberStore,
|
loadMemberStore,
|
||||||
computeRemainingDays,
|
computeRemainingDays,
|
||||||
renewMemberCard
|
renewMemberCard
|
||||||
} from '@/common/memberInfo/store.js'
|
} from '@/common/memberInfo/store.js'
|
||||||
import { subPageMixin } from '@/common/memberInfo/mixins.js'
|
import { backToMemberCenter } from '@/common/constants/routes.js'
|
||||||
|
|
||||||
export default {
|
const card = ref({})
|
||||||
components: { MemberInfoSubNav },
|
const recordTabs = ref([])
|
||||||
mixins: [subPageMixin],
|
const records = ref([])
|
||||||
data() {
|
const rules = ref([])
|
||||||
return {
|
const activeFilter = ref('all')
|
||||||
card: {},
|
|
||||||
recordTabs: memberCardMock.recordTabs,
|
const filteredRecords = computed(() => {
|
||||||
records: [],
|
if (activeFilter.value === 'all') {
|
||||||
rules: memberCardMock.rules,
|
return records.value
|
||||||
activeFilter: 'all'
|
|
||||||
}
|
}
|
||||||
},
|
return records.value.filter((item) => item.type === activeFilter.value)
|
||||||
computed: {
|
})
|
||||||
filteredRecords() {
|
|
||||||
if (this.activeFilter === 'all') {
|
const remainingDays = computed(() => {
|
||||||
return this.records
|
return computeRemainingDays(card.value.validityEnd)
|
||||||
}
|
})
|
||||||
return this.records.filter((item) => item.type === this.activeFilter)
|
|
||||||
},
|
function refreshFromStore() {
|
||||||
remainingDays() {
|
|
||||||
return computeRemainingDays(this.card.validityEnd)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onShow() {
|
|
||||||
this.refreshFromStore()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
refreshFromStore() {
|
|
||||||
const store = loadMemberStore()
|
const store = loadMemberStore()
|
||||||
this.card = { ...store.card }
|
card.value = { ...store.card }
|
||||||
this.records = store.records.map((item) => ({ ...item }))
|
records.value = store.records.map((item) => ({ ...item }))
|
||||||
},
|
}
|
||||||
switchFilter(filter) {
|
|
||||||
this.activeFilter = filter
|
function goBack() {
|
||||||
},
|
backToMemberCenter()
|
||||||
renewCard() {
|
}
|
||||||
|
|
||||||
|
function switchFilter(filter) {
|
||||||
|
activeFilter.value = filter
|
||||||
|
}
|
||||||
|
|
||||||
|
function renewCard() {
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '续费会员卡',
|
title: '续费会员卡',
|
||||||
content: '确认续费 90 天?',
|
content: '确认续费 90 天?',
|
||||||
@@ -170,24 +166,25 @@ export default {
|
|||||||
if (!res.confirm) return
|
if (!res.confirm) return
|
||||||
const store = loadMemberStore()
|
const store = loadMemberStore()
|
||||||
renewMemberCard(store, 90)
|
renewMemberCard(store, 90)
|
||||||
this.activeFilter = 'all'
|
activeFilter.value = 'all'
|
||||||
this.refreshFromStore()
|
refreshFromStore()
|
||||||
uni.showToast({ title: '续费成功', icon: 'success' })
|
uni.showToast({ title: '续费成功', icon: 'success' })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
showRecordDetail(item) {
|
|
||||||
|
function showRecordDetail(item) {
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: item.title,
|
title: item.title,
|
||||||
content: `${item.time}\n变动:${item.value}`,
|
content: `${item.time}\n变动:${item.value}`,
|
||||||
showCancel: false
|
showCancel: false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
refreshFromStore()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
@import '@/common/style/base.css';
|
@import '@/common/style/base.css';
|
||||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||||
|
|||||||
@@ -0,0 +1,758 @@
|
|||||||
|
<template>
|
||||||
|
<scroll-view scroll-y class="scroll-container theme-light">
|
||||||
|
<view class="member-page">
|
||||||
|
<!-- Header: 用户信息区 -->
|
||||||
|
<view class="profile-header">
|
||||||
|
<view class="profile-header__toolbar" :style="toolbarStyle">
|
||||||
|
<view class="profile-header__nav">
|
||||||
|
<view class="profile-header__nav-left">
|
||||||
|
<image
|
||||||
|
class="profile-header__icon-bell"
|
||||||
|
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/bell.png"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
|
<image
|
||||||
|
class="profile-header__icon-settings"
|
||||||
|
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/settings.png"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<text class="profile-header__title">个人中心</text>
|
||||||
|
<view class="profile-header__nav-right" :style="navRightStyle"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="profile-header__toolbar-spacer" :style="toolbarSpacerStyle"></view>
|
||||||
|
<view class="profile-header__hero">
|
||||||
|
<view class="profile-header__inner">
|
||||||
|
<view class="profile-header__user" hover-class="mi-tap--hover" :hover-stay-time="150" @tap="goUserInfo">
|
||||||
|
<view class="profile-header__user-inner">
|
||||||
|
<view class="profile-header__avatar-wrap">
|
||||||
|
<view class="profile-header__avatar-ring">
|
||||||
|
<image
|
||||||
|
class="profile-header__avatar"
|
||||||
|
:key="userInfo.avatar"
|
||||||
|
:src="displayAvatar"
|
||||||
|
mode="aspectFill"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="profile-header__avatar-badge">
|
||||||
|
<image
|
||||||
|
class="profile-header__avatar-badge-icon"
|
||||||
|
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/camera.png"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="profile-header__user-meta">
|
||||||
|
<view class="profile-header__user-meta-inner">
|
||||||
|
<text class="profile-header__name">{{ userInfo.name }}</text>
|
||||||
|
<text class="profile-header__phone">{{ userInfo.phone }}</text>
|
||||||
|
<view class="profile-header__badge">
|
||||||
|
<image
|
||||||
|
class="profile-header__badge-icon"
|
||||||
|
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/crown0.png"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
|
<text class="profile-header__level">{{ userInfo.memberLevel }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="profile-header__stats">
|
||||||
|
<view class="profile-header__stats-inner">
|
||||||
|
<view class="profile-header__stat">
|
||||||
|
<view class="profile-header__stat-inner">
|
||||||
|
<text class="profile-header__stat-value">{{ stats.checkInCount }}</text>
|
||||||
|
<text class="profile-header__stat-label">累计签到</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="profile-header__stat-divider"></view>
|
||||||
|
<view class="profile-header__stat">
|
||||||
|
<view class="profile-header__stat-inner">
|
||||||
|
<text class="profile-header__stat-value">{{ stats.trainingHours }}</text>
|
||||||
|
<text class="profile-header__stat-label">训练时长</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="profile-header__stat-divider"></view>
|
||||||
|
<view class="profile-header__stat">
|
||||||
|
<view class="profile-header__stat-inner">
|
||||||
|
<text class="profile-header__stat-value">{{ stats.pointsBalance }}</text>
|
||||||
|
<text class="profile-header__stat-label">累计积分</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="member-page__body">
|
||||||
|
<view class="member-page__sections">
|
||||||
|
<!-- 会员卡区域 -->
|
||||||
|
<view class="member-card-section">
|
||||||
|
<view class="member-card-section__inner">
|
||||||
|
<view class="member-card-section__head">
|
||||||
|
<view class="member-card-section__head-inner">
|
||||||
|
<text class="member-card-section__title">我的会员卡</text>
|
||||||
|
<view class="member-card-section__link" hover-class="mi-tap--hover" :hover-stay-time="150" @tap="goMemberCard">
|
||||||
|
<text class="member-card-section__link-text">查看全部</text>
|
||||||
|
<image class="member-card-section__link-arrow" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/chevronright12.png" mode="aspectFit" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="member-card-preview" hover-class="mi-tap-card--hover" :hover-stay-time="150" @tap="goMemberCard">
|
||||||
|
<view class="member-card-preview__inner">
|
||||||
|
<view class="member-card-preview__head">
|
||||||
|
<view class="member-card-preview__head-inner">
|
||||||
|
<view class="member-card-preview__type-row">
|
||||||
|
<view class="member-card-preview__icon-wrap">
|
||||||
|
<view class="member-card-preview__icon-border">
|
||||||
|
<view class="member-card-preview__icon-bg"></view>
|
||||||
|
<view class="member-card-preview__icon-stroke"></view>
|
||||||
|
</view>
|
||||||
|
<image class="member-card-preview__icon-line" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/Line_2_468.png" mode="aspectFill" />
|
||||||
|
</view>
|
||||||
|
<text class="member-card-preview__name">{{ cardInfo.name }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="member-card-preview__tag">
|
||||||
|
<text class="member-card-preview__tag-text">{{ cardInfo.detailTag || '详情' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text class="member-card-preview__expire">{{ cardInfo.expireDate }}</text>
|
||||||
|
<view class="member-card-preview__footer">
|
||||||
|
<view class="member-card-preview__footer-inner">
|
||||||
|
<view class="member-card-preview__days">
|
||||||
|
<text class="member-card-preview__days-num">{{ cardInfo.remainingDays }}</text>
|
||||||
|
<text class="member-card-preview__days-unit">天剩余</text>
|
||||||
|
</view>
|
||||||
|
<view class="member-card-preview__renew" hover-class="mi-tap-btn--hover" :hover-stay-time="150" @tap.stop="onRenewCard">
|
||||||
|
<text class="member-card-preview__renew-text">续费</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="member-card-tip">
|
||||||
|
<view class="member-card-tip__inner">
|
||||||
|
<view class="member-card-tip__content">
|
||||||
|
<image class="member-card-tip__icon" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/clock1.png" mode="aspectFit" />
|
||||||
|
<text class="member-card-tip__text">{{ cardInfo.tip }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="member-card-tip__border"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 快捷操作区域 -->
|
||||||
|
<view class="quick-actions">
|
||||||
|
<view class="quick-actions__inner">
|
||||||
|
<view class="quick-actions__grid">
|
||||||
|
<view class="quick-actions__grid-inner">
|
||||||
|
<view v-for="item in quickActionsRow1" :key="item.key" class="quick-actions__item" hover-class="mi-tap--hover" :hover-stay-time="150" @tap="onQuickAction(item.key)">
|
||||||
|
<view class="quick-actions__item-inner">
|
||||||
|
<view class="quick-actions__icon-wrap">
|
||||||
|
<view class="quick-actions__icon-wrap-inner">
|
||||||
|
<view v-if="item.key === 'booking'" class="quick-actions__icon">
|
||||||
|
<image class="quick-actions__icon-part" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/Vector_2_490.png" mode="aspectFit" />
|
||||||
|
<image class="quick-actions__icon-part" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/Vector_2_491.png" mode="aspectFit" />
|
||||||
|
<view class="quick-actions__border-wrap">
|
||||||
|
<view class="quick-actions__rect"></view>
|
||||||
|
<view class="quick-actions__border"></view>
|
||||||
|
</view>
|
||||||
|
<image class="quick-actions__icon-part" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/Vector_2_493.png" mode="aspectFit" />
|
||||||
|
<image class="quick-actions__icon-part" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/Vector_2_494.png" mode="aspectFit" />
|
||||||
|
</view>
|
||||||
|
<image v-else class="quick-actions__icon-img" :src="item.icon" mode="aspectFit" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text :class="item.textClass">{{ item.label }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="quick-actions__divider"></view>
|
||||||
|
<view class="quick-actions__grid">
|
||||||
|
<view class="quick-actions__grid-inner">
|
||||||
|
<view v-for="item in quickActionsRow2" :key="item.key" class="quick-actions__item" hover-class="mi-tap--hover" :hover-stay-time="150" @tap="onQuickAction(item.key)">
|
||||||
|
<view class="quick-actions__item-inner">
|
||||||
|
<view class="quick-actions__icon-wrap">
|
||||||
|
<view class="quick-actions__icon-wrap-inner">
|
||||||
|
<image class="quick-actions__icon-img" :src="item.icon" mode="aspectFit" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text :class="item.textClass">{{ item.label }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 预约课程区域 -->
|
||||||
|
<view class="booking-section">
|
||||||
|
<view class="booking-section__inner">
|
||||||
|
<view class="booking-section__header">
|
||||||
|
<view class="booking-section__header-inner">
|
||||||
|
<text class="booking-section__title">我的预约</text>
|
||||||
|
<view class="booking-section__link" hover-class="mi-tap--hover" :hover-stay-time="150" @tap="goBooking">
|
||||||
|
<text class="booking-section__view-all">预约记录</text>
|
||||||
|
<image class="booking-section__link-arrow" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/chevronright4.png" mode="aspectFit" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-for="item in bookingPreview" :key="item.id" class="booking-section__item" hover-class="mi-tap-row--hover" :hover-stay-time="150" @tap="goBooking">
|
||||||
|
<view class="booking-section__item-inner">
|
||||||
|
<view class="booking-section__date">
|
||||||
|
<view class="booking-section__date-inner">
|
||||||
|
<text class="booking-section__num">{{ item.dateDay }}</text>
|
||||||
|
<text class="booking-section__date-sub">{{ item.dateMonth }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="booking-section__content">
|
||||||
|
<view class="booking-section__content-inner">
|
||||||
|
<text class="booking-section__desc">{{ item.desc }}</text>
|
||||||
|
<view class="booking-section__meta">
|
||||||
|
<view class="booking-section__meta-inner">
|
||||||
|
<image class="booking-section__icon-coach" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/user2.png" mode="aspectFit" />
|
||||||
|
<text class="booking-section__coach">教练:{{ item.coach }}</text>
|
||||||
|
<image class="booking-section__icon-location" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/mappin1.png" mode="aspectFit" />
|
||||||
|
<text class="booking-section__text">{{ item.location }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="booking-section__status-wrap">
|
||||||
|
<view class="booking-section__status-badge" :class="'booking-section__status-badge--' + item.status">
|
||||||
|
<text class="booking-section__status-text" :class="{ 'booking-section__status-text--pending': item.status === 'pending' }">{{ item.statusLabel }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="!bookingPreview.length" class="booking-section__empty">
|
||||||
|
<text class="booking-section__empty-text">暂无进行中的预约</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 签到记录区域 -->
|
||||||
|
<view class="checkin-section">
|
||||||
|
<view class="checkin-section__inner">
|
||||||
|
<view class="checkin-section__header">
|
||||||
|
<view class="checkin-section__header-inner">
|
||||||
|
<text class="checkin-section__title">签到记录</text>
|
||||||
|
<view class="checkin-section__link" hover-class="mi-tap--hover" :hover-stay-time="150" @tap="goCheckInHistory">
|
||||||
|
<text class="checkin-section__view-all">查看全部</text>
|
||||||
|
<image class="checkin-section__link-arrow" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/chevronright2.png" mode="aspectFit" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="checkin-section__list">
|
||||||
|
<view class="checkin-section__list-inner">
|
||||||
|
<view v-for="(item, index) in checkIns" :key="item.id" class="checkin-section__row">
|
||||||
|
<view v-if="index > 0" class="checkin-section__divider"></view>
|
||||||
|
<view class="checkin-section__item" hover-class="mi-tap-row--hover" :hover-stay-time="150" @tap="onCheckInTap(item)">
|
||||||
|
<view class="checkin-section__item-inner">
|
||||||
|
<view class="checkin-section__dot" :class="'checkin-section__dot--' + item.tagTheme"></view>
|
||||||
|
<view class="checkin-section__content">
|
||||||
|
<view class="checkin-section__content-inner">
|
||||||
|
<text class="checkin-section__desc">{{ item.title }}</text>
|
||||||
|
<text class="checkin-section__text">{{ item.time }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="checkin-section__tag-badge" :class="'checkin-section__tag-badge--' + item.tagTheme">
|
||||||
|
<text class="checkin-section__tag-text" :class="'checkin-section__tag-text--' + item.tagTheme">{{ item.tag }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 体测报告区域 -->
|
||||||
|
<view class="body-report-section">
|
||||||
|
<view class="body-report-section__inner">
|
||||||
|
<view class="body-report-section__header">
|
||||||
|
<view class="body-report-section__header-inner">
|
||||||
|
<text class="body-report-section__title">体测报告</text>
|
||||||
|
<view class="body-report-section__link" hover-class="mi-tap--hover" :hover-stay-time="150" @tap="goBodyTestHistory">
|
||||||
|
<text class="body-report-section__history-link">历史记录</text>
|
||||||
|
<image class="body-report-section__link-arrow" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/chevronright3.png" mode="aspectFit" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="body-report-section__card">
|
||||||
|
<view class="body-report-section__card-inner">
|
||||||
|
<view class="body-report-section__card-head">
|
||||||
|
<view class="body-report-section__card-head-inner">
|
||||||
|
<text class="body-report-section__desc">最新数据 · {{ bodyReport.date }}</text>
|
||||||
|
<view class="body-report-section__view-btn" hover-class="mi-tap-btn--hover" :hover-stay-time="150" @tap="goBodyReport">
|
||||||
|
<image class="body-report-section__view-icon" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/filetext.png" mode="aspectFit" />
|
||||||
|
<text class="body-report-section__view-report">查看报告</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="body-report-section__metrics">
|
||||||
|
<view class="body-report-section__metrics-inner">
|
||||||
|
<view class="body-report-section__metric">
|
||||||
|
<view class="body-report-section__metric-inner">
|
||||||
|
<text class="body-report-section__text">{{ bodyReport.weight }}</text>
|
||||||
|
<text class="body-report-section__metric-value">体重(kg)</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="body-report-section__metric-divider"></view>
|
||||||
|
<view class="body-report-section__metric">
|
||||||
|
<view class="body-report-section__metric-inner">
|
||||||
|
<text class="body-report-section__text-2">{{ bodyReport.bmi }}</text>
|
||||||
|
<text class="body-report-section__text-3">BMI</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="body-report-section__metric-divider"></view>
|
||||||
|
<view class="body-report-section__metric">
|
||||||
|
<view class="body-report-section__metric-inner">
|
||||||
|
<text class="body-report-section__text-4">{{ bodyReport.bodyFat }}</text>
|
||||||
|
<text class="body-report-section__metric-label">体脂率</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="body-report-section__metric-divider"></view>
|
||||||
|
<view class="body-report-section__metric">
|
||||||
|
<view class="body-report-section__metric-inner">
|
||||||
|
<text class="body-report-section__num">{{ bodyReport.bmr }}</text>
|
||||||
|
<text class="body-report-section__text-5">BMR</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="body-report-section__summary">
|
||||||
|
<view class="body-report-section__summary-inner">
|
||||||
|
<view class="body-report-section__goal">
|
||||||
|
<image class="body-report-section__goal-icon" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/target.png" mode="aspectFit" />
|
||||||
|
<text class="body-report-section__goal-text">状态:{{ bodyReport.status }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="body-report-section__change">
|
||||||
|
<image class="body-report-section__change-icon" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/trendingdown.png" mode="aspectFit" />
|
||||||
|
<text class="body-report-section__metric-value-2">较上次 {{ bodyReport.change }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 优惠券&积分区域 -->
|
||||||
|
<view class="coupon-section">
|
||||||
|
<view class="coupon-section__inner">
|
||||||
|
<view class="coupon-section__header">
|
||||||
|
<view class="coupon-section__header-inner">
|
||||||
|
<text class="coupon-section__title">优惠券 & 积分</text>
|
||||||
|
<view class="coupon-section__link" hover-class="mi-tap--hover" :hover-stay-time="150" @tap="goCoupons">
|
||||||
|
<text class="coupon-section__view-all">更多详情</text>
|
||||||
|
<image class="coupon-section__link-arrow" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/chevronright5.png" mode="aspectFit" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="coupon-section__cards">
|
||||||
|
<view class="coupon-section__cards-inner">
|
||||||
|
<view class="coupon-section__coupon" hover-class="mi-tap-card--hover" :hover-stay-time="150" @tap="goCoupons">
|
||||||
|
<view class="coupon-section__coupon-inner">
|
||||||
|
<text class="coupon-section__amount">{{ couponPoints.amount }}</text>
|
||||||
|
<text class="coupon-section__desc">{{ couponPoints.couponDesc }}</text>
|
||||||
|
<view class="coupon-section__coupon-status">
|
||||||
|
<text class="coupon-section__status">{{ couponPoints.couponAction }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="coupon-section__points" hover-class="mi-tap-card--hover" :hover-stay-time="150" @tap="goPoints">
|
||||||
|
<view class="coupon-section__points-inner">
|
||||||
|
<text class="coupon-section__num">{{ couponPoints.points }}</text>
|
||||||
|
<text class="coupon-section__points-label">{{ couponPoints.pointsLabel }}</text>
|
||||||
|
<view class="coupon-section__points-action">
|
||||||
|
<text class="coupon-section__text">{{ couponPoints.pointsAction }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 推荐奖励区域 -->
|
||||||
|
<view class="referral-section">
|
||||||
|
<view class="referral-section__inner">
|
||||||
|
<view class="referral-section__header">
|
||||||
|
<text class="referral-section__title">推荐奖励</text>
|
||||||
|
<view class="referral-section__link" hover-class="mi-tap--hover" :hover-stay-time="150" @tap="goReferral">
|
||||||
|
<text class="referral-section__records-link">规则说明</text>
|
||||||
|
<image class="referral-section__link-arrow" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/chevronright11.png" mode="aspectFit" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="referral-section__code-row">
|
||||||
|
<view class="referral-section__code-box">
|
||||||
|
<text class="referral-section__code-label">我的邀请码</text>
|
||||||
|
<text class="referral-section__code-value">{{ referral.code }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="referral-section__copy-btn" hover-class="mi-tap-btn--hover" :hover-stay-time="150" @tap="copyReferralCode">
|
||||||
|
<view class="referral-section__copy-icon">
|
||||||
|
<view class="referral-section__copy-sheet referral-section__copy-sheet--back"></view>
|
||||||
|
<view class="referral-section__copy-sheet referral-section__copy-sheet--front"></view>
|
||||||
|
</view>
|
||||||
|
<text class="referral-section__copy-text">复制</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="referral-section__stats">
|
||||||
|
<view class="referral-section__stat">
|
||||||
|
<text class="referral-section__stat-num referral-section__stat-num--orange">{{ referral.invited }}</text>
|
||||||
|
<text class="referral-section__stat-label">已推荐</text>
|
||||||
|
</view>
|
||||||
|
<view class="referral-section__stat-divider"></view>
|
||||||
|
<view class="referral-section__stat">
|
||||||
|
<text class="referral-section__stat-num referral-section__stat-num--green">{{ referral.registered }}</text>
|
||||||
|
<text class="referral-section__stat-label">已注册</text>
|
||||||
|
</view>
|
||||||
|
<view class="referral-section__stat-divider"></view>
|
||||||
|
<view class="referral-section__stat">
|
||||||
|
<text class="referral-section__stat-num referral-section__stat-num--amber">{{ referral.purchased }}</text>
|
||||||
|
<text class="referral-section__stat-label">已购课</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 设置区域 -->
|
||||||
|
<view class="settings-section">
|
||||||
|
<view class="settings-section__inner">
|
||||||
|
<text class="settings-section__title">设置与安全</text>
|
||||||
|
<view class="settings-section__list">
|
||||||
|
<view class="settings-section__list-inner">
|
||||||
|
<view v-for="(item, index) in settingsItems" :key="item.key">
|
||||||
|
<view v-if="index > 0" class="settings-section__item-divider"></view>
|
||||||
|
<view class="settings-section__item" :class="{ 'settings-section__item--tall': item.subtitle }" hover-class="mi-tap-row--hover" :hover-stay-time="150" @tap="onSetting(item.key)">
|
||||||
|
<view class="settings-section__item-inner">
|
||||||
|
<view class="settings-section__item-icon-wrap" :class="item.iconWrapClass">
|
||||||
|
<image class="settings-section__item-icon" :src="item.icon" mode="aspectFit" />
|
||||||
|
</view>
|
||||||
|
<view v-if="item.subtitle" class="settings-section__item-texts">
|
||||||
|
<text class="settings-section__item-title">{{ item.label }}</text>
|
||||||
|
<text class="settings-section__item-desc">{{ item.subtitle }}</text>
|
||||||
|
</view>
|
||||||
|
<text v-else class="settings-section__item-label" :class="item.labelClass">{{ item.label }}</text>
|
||||||
|
<image class="settings-section__item-arrow" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/chevronright10.png" mode="aspectFit" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 退出登录 -->
|
||||||
|
<view class="logout-section">
|
||||||
|
<view class="logout-section__btn" hover-class="mi-tap-btn--hover" :hover-stay-time="150" @tap="handleLogout">
|
||||||
|
<image class="logout-section__icon" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/logout.png" mode="aspectFit" />
|
||||||
|
<text class="logout-section__text">退出登录</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<!-- 固定 TabBar -->
|
||||||
|
<view class="tabbar-fixed">
|
||||||
|
<TabBar />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
|
||||||
|
import { loadMemberStore, getCenterPageData, renewMemberCard } from '@/common/memberInfo/store.js'
|
||||||
|
import TabBar from '@/components/TabBar.vue'
|
||||||
|
|
||||||
|
// 页面数据
|
||||||
|
const userInfo = ref({})
|
||||||
|
const stats = ref({})
|
||||||
|
const cardInfo = ref({})
|
||||||
|
const bookingPreview = ref([])
|
||||||
|
const checkIns = ref([])
|
||||||
|
const bodyReport = ref({})
|
||||||
|
const couponPoints = ref({})
|
||||||
|
const referral = ref({})
|
||||||
|
|
||||||
|
// 导航栏样式
|
||||||
|
const toolbarStyle = ref({})
|
||||||
|
const toolbarSpacerStyle = ref({})
|
||||||
|
const navRightStyle = ref({})
|
||||||
|
|
||||||
|
// 快捷操作配置
|
||||||
|
const quickActionsRow1 = ref([
|
||||||
|
{ key: 'booking', label: '预约课程', textClass: 'quick-actions__title', icon: '' },
|
||||||
|
{ key: 'bodyTest', label: '智能体测', textClass: 'quick-actions__title-2', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/mappin2.png' },
|
||||||
|
{ key: 'bodyReport', label: '体测报告', textClass: 'quick-actions__title-3', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/activity.png' },
|
||||||
|
{ key: 'trainReport', label: '训练报告', textClass: 'quick-actions__coach', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/usercheck.png' }
|
||||||
|
])
|
||||||
|
|
||||||
|
const quickActionsRow2 = ref([
|
||||||
|
{ key: 'coupon', label: '我的优惠券', textClass: 'quick-actions__text', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/ticket.png' },
|
||||||
|
{ key: 'points', label: '我的积分', textClass: 'quick-actions__points-desc', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/star.png' },
|
||||||
|
{ key: 'referral', label: '邀请好友', textClass: 'quick-actions__title-4', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/share2.png' },
|
||||||
|
{ key: 'course', label: '我的课程', textClass: 'quick-actions__text-2', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/play.png' }
|
||||||
|
])
|
||||||
|
|
||||||
|
// 设置项配置
|
||||||
|
const settingsItems = ref([
|
||||||
|
{
|
||||||
|
key: 'notify',
|
||||||
|
label: '通知设置',
|
||||||
|
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/bell.png',
|
||||||
|
iconWrapClass: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'password',
|
||||||
|
label: '修改密码',
|
||||||
|
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/Vector_2_727.png',
|
||||||
|
iconWrapClass: 'settings-section__item-icon-wrap--blue'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'privacy',
|
||||||
|
label: '隐私政策',
|
||||||
|
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/shield.png',
|
||||||
|
iconWrapClass: 'settings-section__item-icon-wrap--green'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'nfc',
|
||||||
|
label: 'NFC 门禁卡',
|
||||||
|
subtitle: '已绑定',
|
||||||
|
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/ticket.png',
|
||||||
|
iconWrapClass: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'delete',
|
||||||
|
label: '注销账户',
|
||||||
|
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/userx.png',
|
||||||
|
iconWrapClass: 'settings-section__item-icon-wrap--red',
|
||||||
|
labelClass: 'settings-section__item-label--danger'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const displayAvatar = computed(() => {
|
||||||
|
return userInfo.value.avatar || 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AvatarEditWrap.png'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
function syncNavSafeArea() {
|
||||||
|
try {
|
||||||
|
const sys = uni.getSystemInfoSync()
|
||||||
|
const statusBarHeight = sys.statusBarHeight || 0
|
||||||
|
const navHeight = 44
|
||||||
|
const menu = uni.getMenuButtonBoundingClientRect?.()
|
||||||
|
|
||||||
|
toolbarStyle.value = {
|
||||||
|
paddingTop: `${statusBarHeight}px`
|
||||||
|
}
|
||||||
|
|
||||||
|
toolbarSpacerStyle.value = {
|
||||||
|
height: `${statusBarHeight + navHeight}px`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menu && menu.width) {
|
||||||
|
const capsuleGap = sys.windowWidth - menu.left + 8
|
||||||
|
navRightStyle.value = {
|
||||||
|
width: `${capsuleGap}px`,
|
||||||
|
minWidth: `${capsuleGap}px`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
toolbarSpacerStyle.value = { height: '44px' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshFromStore() {
|
||||||
|
const store = loadMemberStore()
|
||||||
|
const pageData = getCenterPageData(store)
|
||||||
|
userInfo.value = pageData.userInfo
|
||||||
|
stats.value = pageData.stats
|
||||||
|
cardInfo.value = pageData.cardInfo
|
||||||
|
bookingPreview.value = pageData.bookingPreview
|
||||||
|
checkIns.value = pageData.checkIns
|
||||||
|
bodyReport.value = pageData.bodyReport
|
||||||
|
couponPoints.value = pageData.couponPoints
|
||||||
|
referral.value = pageData.referral
|
||||||
|
}
|
||||||
|
|
||||||
|
function goUserInfo() {
|
||||||
|
navigateToPage(PAGE.USER_INFO)
|
||||||
|
}
|
||||||
|
|
||||||
|
function goMemberCard() {
|
||||||
|
navigateToPage(PAGE.MEMBER_CARD)
|
||||||
|
}
|
||||||
|
|
||||||
|
function goBooking() {
|
||||||
|
navigateToPage(PAGE.BOOKING)
|
||||||
|
}
|
||||||
|
|
||||||
|
function goCheckInHistory() {
|
||||||
|
navigateToPage(PAGE.CHECK_IN_HISTORY)
|
||||||
|
}
|
||||||
|
|
||||||
|
function goBodyTestHistory() {
|
||||||
|
navigateToPage(PAGE.BODY_TEST_HISTORY)
|
||||||
|
}
|
||||||
|
|
||||||
|
function goBodyReport() {
|
||||||
|
const id = bodyReport.value?.recordId
|
||||||
|
const url = id ? `${PAGE.BODY_TEST_REPORT}?id=${id}` : PAGE.BODY_TEST_HISTORY
|
||||||
|
navigateToPage(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
function goCoupons() {
|
||||||
|
navigateToPage(PAGE.COUPONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
function goPoints() {
|
||||||
|
navigateToPage(PAGE.POINTS)
|
||||||
|
}
|
||||||
|
|
||||||
|
function goReferral() {
|
||||||
|
navigateToPage(PAGE.REFERRAL)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRenewCard() {
|
||||||
|
uni.showModal({
|
||||||
|
title: '续费会员卡',
|
||||||
|
content: '确认续费 90 天?',
|
||||||
|
success: (res) => {
|
||||||
|
if (!res.confirm) return
|
||||||
|
const store = loadMemberStore()
|
||||||
|
renewMemberCard(store, 90)
|
||||||
|
refreshFromStore()
|
||||||
|
uni.showToast({ title: '续费成功', icon: 'success' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onQuickAction(type) {
|
||||||
|
const routes = {
|
||||||
|
booking: PAGE.COURSE_LIST,
|
||||||
|
bodyTest: PAGE.BODY_TEST_HOME,
|
||||||
|
bodyReport: PAGE.BODY_TEST_HISTORY,
|
||||||
|
trainReport: PAGE.TRAIN_REPORT,
|
||||||
|
coupon: PAGE.COUPONS,
|
||||||
|
points: PAGE.POINTS,
|
||||||
|
referral: PAGE.REFERRAL,
|
||||||
|
course: PAGE.MY_COURSES
|
||||||
|
}
|
||||||
|
if (routes[type]) {
|
||||||
|
navigateToPage(routes[type])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCheckInTap(item) {
|
||||||
|
uni.showModal({
|
||||||
|
title: item.title,
|
||||||
|
content: item.time,
|
||||||
|
showCancel: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyReferralCode() {
|
||||||
|
uni.setClipboardData({
|
||||||
|
data: referral.value.code,
|
||||||
|
success: () => {
|
||||||
|
uni.showToast({ title: '已复制', icon: 'success' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSetting(key) {
|
||||||
|
const labels = {
|
||||||
|
notify: '通知设置',
|
||||||
|
password: '修改密码',
|
||||||
|
privacy: '隐私政策',
|
||||||
|
nfc: 'NFC 门禁卡',
|
||||||
|
delete: '注销账户'
|
||||||
|
}
|
||||||
|
if (key === 'delete') {
|
||||||
|
uni.showModal({
|
||||||
|
title: '注销账户',
|
||||||
|
content: '注销后数据将无法恢复,确定继续吗?',
|
||||||
|
confirmColor: '#E74C3C'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (key === 'privacy') {
|
||||||
|
uni.showModal({
|
||||||
|
title: '隐私政策',
|
||||||
|
content: '我们重视您的隐私,详细政策请前往官网查看。',
|
||||||
|
showCancel: false
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.showToast({
|
||||||
|
title: `${labels[key] || '设置'}开发中`,
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLogout() {
|
||||||
|
uni.showModal({
|
||||||
|
title: '退出登录',
|
||||||
|
content: '确定要退出登录吗?',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
uni.showToast({ title: '已退出', icon: 'success' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
syncNavSafeArea()
|
||||||
|
refreshFromStore()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '@/common/style/base.css';
|
||||||
|
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||||
|
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||||
|
@import '@/common/style/memberInfo/member-info-page.css';
|
||||||
|
@import '@/common/style/memberInfo/member-info-header.css';
|
||||||
|
@import '@/common/style/memberInfo/member-info-member-card.css';
|
||||||
|
@import '@/common/style/memberInfo/member-info-quick-actions.css';
|
||||||
|
@import '@/common/style/memberInfo/member-info-booking-list.css';
|
||||||
|
@import '@/common/style/memberInfo/member-info-check-in-list.css';
|
||||||
|
@import '@/common/style/memberInfo/member-info-body-report.css';
|
||||||
|
@import '@/common/style/memberInfo/member-info-coupon-points.css';
|
||||||
|
@import '@/common/style/memberInfo/member-info-referral.css';
|
||||||
|
@import '@/common/style/memberInfo/member-info-settings.css';
|
||||||
|
@import '@/common/style/memberInfo/member-info-logout.css';
|
||||||
|
|
||||||
|
.tabbar-fixed {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-section__empty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-section__empty-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #8A99B4;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -5,7 +5,9 @@
|
|||||||
<MemberInfoHeader
|
<MemberInfoHeader
|
||||||
:user-info="userInfo"
|
:user-info="userInfo"
|
||||||
:stats="stats"
|
:stats="stats"
|
||||||
|
:isLogin="isLogin"
|
||||||
@user-info="goUserInfo"
|
@user-info="goUserInfo"
|
||||||
|
@guest-login="handleGuestLogin"
|
||||||
/>
|
/>
|
||||||
<view class="member-page__body">
|
<view class="member-page__body">
|
||||||
<view class="member-page__sections">
|
<view class="member-page__sections">
|
||||||
@@ -13,35 +15,41 @@
|
|||||||
:card-info="cardInfo"
|
:card-info="cardInfo"
|
||||||
@view-all="goMemberCard"
|
@view-all="goMemberCard"
|
||||||
@renew="onRenewCard"
|
@renew="onRenewCard"
|
||||||
|
@purchase="goPurchaseCard"
|
||||||
/>
|
/>
|
||||||
<MemberInfoQuickActions @action="onQuickAction" />
|
<MemberInfoQuickActions @action="onQuickAction" />
|
||||||
<MemberInfoBookingList
|
<MemberInfoBookingList
|
||||||
|
v-if="isLogin"
|
||||||
:items="bookingPreview"
|
:items="bookingPreview"
|
||||||
@view-all="goBooking"
|
@view-all="goBooking"
|
||||||
@item-tap="goBooking"
|
@item-tap="goBooking"
|
||||||
/>
|
/>
|
||||||
<MemberInfoCheckInList
|
<MemberInfoCheckInList
|
||||||
|
v-if="isLogin"
|
||||||
:items="checkIns"
|
:items="checkIns"
|
||||||
@view-all="onCheckInViewAll"
|
@view-all="onCheckInViewAll"
|
||||||
@item-tap="onCheckInTap"
|
@item-tap="onCheckInTap"
|
||||||
/>
|
/>
|
||||||
<MemberInfoBodyReport
|
<MemberInfoBodyReport
|
||||||
|
v-if="isLogin"
|
||||||
:report="bodyReport"
|
:report="bodyReport"
|
||||||
@view-history="onBodyReportHistory"
|
@view-history="onBodyReportHistory"
|
||||||
@view-report="onBodyReportView"
|
@view-report="onBodyReportView"
|
||||||
/>
|
/>
|
||||||
<MemberInfoCouponPoints
|
<MemberInfoCouponPoints
|
||||||
|
v-if="isLogin"
|
||||||
:data="couponPoints"
|
:data="couponPoints"
|
||||||
@view-all="onCouponViewAll"
|
@view-all="onCouponViewAll"
|
||||||
@use-coupon="onUseCoupon"
|
@use-coupon="onUseCoupon"
|
||||||
@redeem-points="onRedeemPoints"
|
@redeem-points="onRedeemPoints"
|
||||||
/>
|
/>
|
||||||
<MemberInfoReferral
|
<MemberInfoReferral
|
||||||
|
v-if="isLogin"
|
||||||
:data="referral"
|
:data="referral"
|
||||||
@view-rules="onReferralRules"
|
@view-rules="onReferralRules"
|
||||||
/>
|
/>
|
||||||
<MemberInfoSettings @setting="onSetting" />
|
<MemberInfoSettings v-if="isLogin" @setting="onSetting" />
|
||||||
<MemberInfoLogout @logout="handleLogout" />
|
<MemberInfoLogout v-if="isLogin" @logout="handleLogout" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -53,13 +61,19 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
|
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
|
||||||
import {
|
import {
|
||||||
loadMemberStore,
|
loadMemberStore,
|
||||||
getCenterPageData,
|
getCenterPageData,
|
||||||
renewMemberCard
|
renewMemberCard,
|
||||||
|
saveMemberStore,
|
||||||
|
getDefaultStore
|
||||||
} from '@/common/memberInfo/store.js'
|
} from '@/common/memberInfo/store.js'
|
||||||
|
import { login as miniappLogin, getUserInfo, getMemberDetail, getCheckInStats, updateUserInfo } from '@/api/main.js'
|
||||||
|
import { setToken, getToken, clearToken } from '@/utils/request.js'
|
||||||
import MemberInfoHeader from '@/components/memberInfo/MemberInfoHeader.vue'
|
import MemberInfoHeader from '@/components/memberInfo/MemberInfoHeader.vue'
|
||||||
import MemberInfoMemberCard from '@/components/memberInfo/MemberInfoMemberCard.vue'
|
import MemberInfoMemberCard from '@/components/memberInfo/MemberInfoMemberCard.vue'
|
||||||
import MemberInfoQuickActions from '@/components/memberInfo/MemberInfoQuickActions.vue'
|
import MemberInfoQuickActions from '@/components/memberInfo/MemberInfoQuickActions.vue'
|
||||||
@@ -72,58 +86,163 @@ import MemberInfoSettings from '@/components/memberInfo/MemberInfoSettings.vue'
|
|||||||
import MemberInfoLogout from '@/components/memberInfo/MemberInfoLogout.vue'
|
import MemberInfoLogout from '@/components/memberInfo/MemberInfoLogout.vue'
|
||||||
import TabBar from '@/components/TabBar.vue'
|
import TabBar from '@/components/TabBar.vue'
|
||||||
|
|
||||||
export default {
|
const userInfo = ref({})
|
||||||
components: {
|
const stats = ref({})
|
||||||
MemberInfoHeader,
|
const cardInfo = ref({})
|
||||||
MemberInfoMemberCard,
|
const bookingPreview = ref([])
|
||||||
MemberInfoQuickActions,
|
const checkIns = ref([])
|
||||||
MemberInfoBookingList,
|
const bodyReport = ref({})
|
||||||
MemberInfoCheckInList,
|
const couponPoints = ref({})
|
||||||
MemberInfoBodyReport,
|
const referral = ref({})
|
||||||
MemberInfoCouponPoints,
|
const loading = ref(false)
|
||||||
MemberInfoReferral,
|
const hasActiveCard = ref(false)
|
||||||
MemberInfoSettings,
|
const isLogin = ref(false)
|
||||||
MemberInfoLogout,
|
|
||||||
TabBar
|
// 页面加载时检查token
|
||||||
},
|
onMounted(() => {
|
||||||
data() {
|
const token = getToken()
|
||||||
return {
|
const isLoginStorage = uni.getStorageSync('isLogin')
|
||||||
userInfo: {},
|
if (token || isLoginStorage) {
|
||||||
stats: {},
|
isLogin.value = true
|
||||||
cardInfo: {},
|
|
||||||
bookingPreview: [],
|
|
||||||
checkIns: [],
|
|
||||||
bodyReport: {},
|
|
||||||
couponPoints: {},
|
|
||||||
referral: {}
|
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
onShow() {
|
|
||||||
this.refreshFromStore()
|
async function fetchMemberInfo() {
|
||||||
},
|
if (loading.value) return
|
||||||
methods: {
|
loading.value = true
|
||||||
refreshFromStore() {
|
console.log('[memberInfo] fetchMemberInfo 开始执行')
|
||||||
|
try {
|
||||||
|
const res = await getUserInfo({ cache: false })
|
||||||
|
console.log('[memberInfo] API返回,res =', res)
|
||||||
|
const apiData = res.data || res
|
||||||
|
console.log('[memberInfo] apiData =', apiData)
|
||||||
|
|
||||||
|
if (apiData && (apiData.nickname !== undefined || apiData.phone !== undefined || apiData.id !== undefined)) {
|
||||||
|
console.log('[memberInfo] 收到有效数据,更新store')
|
||||||
|
const store = loadMemberStore()
|
||||||
|
// 更新memberProfile
|
||||||
|
store.memberProfile = {
|
||||||
|
...store.memberProfile,
|
||||||
|
id: apiData.id,
|
||||||
|
name: apiData.nickname || apiData.name,
|
||||||
|
phone: apiData.phone,
|
||||||
|
avatar: apiData.avatar,
|
||||||
|
memberLevel: apiData.memberLevel || '普通会员'
|
||||||
|
}
|
||||||
|
// 更新profile
|
||||||
|
store.profile = {
|
||||||
|
...store.profile,
|
||||||
|
id: apiData.id,
|
||||||
|
name: apiData.nickname || apiData.name,
|
||||||
|
phone: apiData.phone,
|
||||||
|
avatar: apiData.avatar,
|
||||||
|
gender: apiData.gender,
|
||||||
|
genderDesc: apiData.genderDesc,
|
||||||
|
birthday: apiData.birthday,
|
||||||
|
height: apiData.height,
|
||||||
|
weight: apiData.weight,
|
||||||
|
hasPhone: apiData.hasPhone,
|
||||||
|
isSubscribed: apiData.isSubscribed,
|
||||||
|
memberLevel: apiData.memberLevel || '普通会员'
|
||||||
|
}
|
||||||
|
// 获取会员详细信息(会员卡、积分)
|
||||||
|
try {
|
||||||
|
const detailRes = await getMemberDetail()
|
||||||
|
console.log('[memberInfo] getMemberDetail 返回:', JSON.stringify(detailRes, null, 2))
|
||||||
|
const detailData = detailRes.data || {}
|
||||||
|
store.card = detailData.memberCards ? detailData.memberCards[0] || {} : {}
|
||||||
|
store.cardInfo = {
|
||||||
|
name: detailData.memberCards?.[0]?.memberCardName || '暂无会员卡',
|
||||||
|
type: detailData.memberCards?.[0]?.memberCardTypeDesc || '',
|
||||||
|
remainingTimes: detailData.memberCards?.[0]?.memberCardTotalTimes || 0,
|
||||||
|
status: detailData.memberCards?.[0]?.memberCardStatusDesc || ''
|
||||||
|
}
|
||||||
|
hasActiveCard.value = (detailData.activeCardCount || 0) > 0
|
||||||
|
store.stats = {
|
||||||
|
...store.stats,
|
||||||
|
pointsBalance: detailData.pointsBalance || 0
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[memberInfo] 获取会员详情失败:', e)
|
||||||
|
}
|
||||||
|
// 获取签到统计
|
||||||
|
try {
|
||||||
|
const statsRes = await getCheckInStats()
|
||||||
|
console.log('[memberInfo] getCheckInStats 返回:', JSON.stringify(statsRes, null, 2))
|
||||||
|
const statsData = statsRes.data || {}
|
||||||
|
store.stats.checkInCount = statsData.totalCount || 0
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[memberInfo] 获取签到统计失败:', e)
|
||||||
|
store.stats.checkInCount = 0
|
||||||
|
}
|
||||||
|
saveMemberStore(store)
|
||||||
|
refreshFromStore()
|
||||||
|
console.log('[memberInfo] store已更新')
|
||||||
|
} else {
|
||||||
|
console.log('[memberInfo] 未登录或数据无效,使用默认空数据')
|
||||||
|
const store = loadMemberStore()
|
||||||
|
store.memberProfile = {}
|
||||||
|
store.profile = {}
|
||||||
|
store.stats.checkInCount = 0
|
||||||
|
hasActiveCard.value = false
|
||||||
|
saveMemberStore(store)
|
||||||
|
refreshFromStore()
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[memberInfo] 获取会员信息失败:', err)
|
||||||
|
// 失败时使用默认空数据
|
||||||
|
const store = loadMemberStore()
|
||||||
|
store.memberProfile = {}
|
||||||
|
store.profile = {}
|
||||||
|
store.stats.checkInCount = 0
|
||||||
|
hasActiveCard.value = false
|
||||||
|
saveMemberStore(store)
|
||||||
|
refreshFromStore()
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
console.log('[memberInfo] fetchMemberInfo 执行完成')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshFromStore() {
|
||||||
const store = loadMemberStore()
|
const store = loadMemberStore()
|
||||||
const pageData = getCenterPageData(store)
|
const pageData = getCenterPageData(store)
|
||||||
this.userInfo = pageData.userInfo
|
console.log('[memberInfo] refreshFromStore - pageData.userInfo:', JSON.stringify(pageData.userInfo))
|
||||||
this.stats = pageData.stats
|
userInfo.value = pageData.userInfo
|
||||||
this.cardInfo = pageData.cardInfo
|
stats.value = pageData.stats
|
||||||
this.bookingPreview = pageData.bookingPreview
|
cardInfo.value = pageData.cardInfo
|
||||||
this.checkIns = pageData.checkIns
|
bookingPreview.value = pageData.bookingPreview
|
||||||
this.bodyReport = pageData.bodyReport
|
checkIns.value = pageData.checkIns
|
||||||
this.couponPoints = pageData.couponPoints
|
bodyReport.value = pageData.bodyReport
|
||||||
this.referral = pageData.referral
|
couponPoints.value = pageData.couponPoints
|
||||||
},
|
referral.value = pageData.referral
|
||||||
goMemberCard() {
|
console.log('[memberInfo] refreshFromStore - userInfo.value:', JSON.stringify(userInfo.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
function goMemberCard() {
|
||||||
navigateToPage(PAGE.MEMBER_CARD)
|
navigateToPage(PAGE.MEMBER_CARD)
|
||||||
},
|
}
|
||||||
goBooking() {
|
|
||||||
|
function goPurchaseCard() {
|
||||||
|
navigateToPage(PAGE.PURCHASE_CARD)
|
||||||
|
}
|
||||||
|
|
||||||
|
function goBooking() {
|
||||||
navigateToPage(PAGE.BOOKING)
|
navigateToPage(PAGE.BOOKING)
|
||||||
},
|
}
|
||||||
goUserInfo() {
|
|
||||||
|
function goUserInfo() {
|
||||||
navigateToPage(PAGE.USER_INFO)
|
navigateToPage(PAGE.USER_INFO)
|
||||||
},
|
}
|
||||||
onRenewCard() {
|
|
||||||
|
function handleGuestLogin() {
|
||||||
|
console.log('[memberInfo] 用户点击登录区域')
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/memberInfo/login'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRenewCard() {
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '续费会员卡',
|
title: '续费会员卡',
|
||||||
content: '确认续费 90 天?',
|
content: '确认续费 90 天?',
|
||||||
@@ -131,12 +250,13 @@ export default {
|
|||||||
if (!res.confirm) return
|
if (!res.confirm) return
|
||||||
const store = loadMemberStore()
|
const store = loadMemberStore()
|
||||||
renewMemberCard(store, 90)
|
renewMemberCard(store, 90)
|
||||||
this.refreshFromStore()
|
refreshFromStore()
|
||||||
uni.showToast({ title: '续费成功', icon: 'success' })
|
uni.showToast({ title: '续费成功', icon: 'success' })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
onQuickAction(type) {
|
|
||||||
|
function onQuickAction(type) {
|
||||||
const routes = {
|
const routes = {
|
||||||
booking: PAGE.COURSE_LIST,
|
booking: PAGE.COURSE_LIST,
|
||||||
bodyTest: PAGE.BODY_TEST_HOME,
|
bodyTest: PAGE.BODY_TEST_HOME,
|
||||||
@@ -150,40 +270,49 @@ export default {
|
|||||||
if (routes[type]) {
|
if (routes[type]) {
|
||||||
navigateToPage(routes[type])
|
navigateToPage(routes[type])
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
onCheckInViewAll() {
|
|
||||||
|
function onCheckInViewAll() {
|
||||||
navigateToPage(PAGE.CHECK_IN_HISTORY)
|
navigateToPage(PAGE.CHECK_IN_HISTORY)
|
||||||
},
|
}
|
||||||
onCheckInTap(item) {
|
|
||||||
|
function onCheckInTap(item) {
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: item.title,
|
title: item.title,
|
||||||
content: item.time,
|
content: item.time,
|
||||||
showCancel: false
|
showCancel: false
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
onBodyReportHistory() {
|
|
||||||
|
function onBodyReportHistory() {
|
||||||
navigateToPage(PAGE.BODY_TEST_HISTORY)
|
navigateToPage(PAGE.BODY_TEST_HISTORY)
|
||||||
},
|
}
|
||||||
onBodyReportView() {
|
|
||||||
const id = this.bodyReport?.recordId
|
function onBodyReportView() {
|
||||||
|
const id = bodyReport.value?.recordId
|
||||||
const url = id
|
const url = id
|
||||||
? `${PAGE.BODY_TEST_REPORT}?id=${id}`
|
? `${PAGE.BODY_TEST_REPORT}?id=${id}`
|
||||||
: PAGE.BODY_TEST_HISTORY
|
: PAGE.BODY_TEST_HISTORY
|
||||||
navigateToPage(url)
|
navigateToPage(url)
|
||||||
},
|
}
|
||||||
onCouponViewAll() {
|
|
||||||
|
function onCouponViewAll() {
|
||||||
navigateToPage(PAGE.COUPONS)
|
navigateToPage(PAGE.COUPONS)
|
||||||
},
|
}
|
||||||
onUseCoupon() {
|
|
||||||
|
function onUseCoupon() {
|
||||||
navigateToPage(PAGE.COUPONS)
|
navigateToPage(PAGE.COUPONS)
|
||||||
},
|
}
|
||||||
onRedeemPoints() {
|
|
||||||
|
function onRedeemPoints() {
|
||||||
navigateToPage(PAGE.POINTS)
|
navigateToPage(PAGE.POINTS)
|
||||||
},
|
}
|
||||||
onReferralRules() {
|
|
||||||
|
function onReferralRules() {
|
||||||
navigateToPage(PAGE.REFERRAL)
|
navigateToPage(PAGE.REFERRAL)
|
||||||
},
|
}
|
||||||
onSetting(key) {
|
|
||||||
|
function onSetting(key) {
|
||||||
const labels = {
|
const labels = {
|
||||||
notify: '通知设置',
|
notify: '通知设置',
|
||||||
password: '修改密码',
|
password: '修改密码',
|
||||||
@@ -211,23 +340,57 @@ export default {
|
|||||||
title: `${labels[key] || '设置'}开发中`,
|
title: `${labels[key] || '设置'}开发中`,
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
handleLogout() {
|
|
||||||
|
function handleLogout() {
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '退出登录',
|
title: '退出登录',
|
||||||
content: '确定要退出登录吗?',
|
content: '确定要退出登录吗?',
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (res.confirm) {
|
if (res.confirm) {
|
||||||
|
// 清除token
|
||||||
|
clearToken()
|
||||||
|
// 重置登录状态
|
||||||
|
isLogin.value = false
|
||||||
|
// 重置store为默认状态
|
||||||
|
const defaultStore = getDefaultStore()
|
||||||
|
saveMemberStore(defaultStore)
|
||||||
|
// 重置页面数据
|
||||||
|
userInfo.value = {}
|
||||||
|
stats.value = {}
|
||||||
|
cardInfo.value = {}
|
||||||
|
bookingPreview.value = []
|
||||||
|
checkIns.value = []
|
||||||
|
bodyReport.value = {}
|
||||||
|
couponPoints.value = {}
|
||||||
|
referral.value = {}
|
||||||
|
hasActiveCard.value = false
|
||||||
|
refreshFromStore()
|
||||||
uni.showToast({ title: '已退出', icon: 'success' })
|
uni.showToast({ title: '已退出', icon: 'success' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refreshFromStore()
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
console.log('[memberInfo] onShow 被调用')
|
||||||
|
|
||||||
|
// 检查登录状态
|
||||||
|
const token = getToken()
|
||||||
|
const isLoginStorage = uni.getStorageSync('isLogin')
|
||||||
|
if (token || isLoginStorage) {
|
||||||
|
isLogin.value = true
|
||||||
|
} else {
|
||||||
|
isLogin.value = false
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
fetchMemberInfo()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
@import '@/common/style/base.css';
|
@import '@/common/style/base.css';
|
||||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||||
|
|||||||
@@ -52,35 +52,37 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||||
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
|
import { PAGE, navigateToPage, backToMemberCenter } from '@/common/constants/routes.js'
|
||||||
import { getPointsPageData } from '@/common/memberInfo/moduleStore.js'
|
import { getPointsPageData } from '@/common/memberInfo/moduleStore.js'
|
||||||
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||||
import { subPageMixin } from '@/common/memberInfo/mixins.js'
|
|
||||||
|
|
||||||
export default {
|
const balance = ref(0)
|
||||||
components: { MemberInfoSubNav },
|
const config = ref({})
|
||||||
mixins: [subPageMixin],
|
const historyPreview = ref([])
|
||||||
data() {
|
|
||||||
return { balance: 0, config: {}, historyPreview: [] }
|
function refresh() {
|
||||||
},
|
|
||||||
onShow() {
|
|
||||||
const data = getPointsPageData(loadMemberStore())
|
const data = getPointsPageData(loadMemberStore())
|
||||||
this.balance = data.balance
|
balance.value = data.balance
|
||||||
this.config = data.config
|
config.value = data.config
|
||||||
this.historyPreview = data.history.slice(0, 5)
|
historyPreview.value = data.history.slice(0, 5)
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
goMall() { navigateToPage(PAGE.POINTS_MALL) },
|
|
||||||
goHistory() { navigateToPage(PAGE.POINTS_HISTORY) },
|
|
||||||
goReferral() { navigateToPage(PAGE.REFERRAL) },
|
|
||||||
checkIn() { uni.showToast({ title: '签到成功 +10 积分', icon: 'success' }) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
backToMemberCenter()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function goMall() { navigateToPage(PAGE.POINTS_MALL) }
|
||||||
|
function goHistory() { navigateToPage(PAGE.POINTS_HISTORY) }
|
||||||
|
function goReferral() { navigateToPage(PAGE.REFERRAL) }
|
||||||
|
function checkIn() { uni.showToast({ title: '签到成功 +10 积分', icon: 'success' }) }
|
||||||
|
|
||||||
|
refresh()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
@import '@/common/style/base.css';
|
@import '@/common/style/base.css';
|
||||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||||
|
|||||||
@@ -41,40 +41,34 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||||
import { PAGE, goBackOrTab } from '@/common/constants/routes.js'
|
import { PAGE, goBackOrTab } from '@/common/constants/routes.js'
|
||||||
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||||
import { filterPointsHistory } from '@/common/memberInfo/moduleStore.js'
|
import { filterPointsHistory } from '@/common/memberInfo/moduleStore.js'
|
||||||
|
|
||||||
export default {
|
const filter = ref('all')
|
||||||
components: { MemberInfoSubNav },
|
const tabs = ref([
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
filter: 'all',
|
|
||||||
tabs: [
|
|
||||||
{ key: 'all', label: '全部' },
|
{ key: 'all', label: '全部' },
|
||||||
{ key: 'earn', label: '获取' },
|
{ key: 'earn', label: '获取' },
|
||||||
{ key: 'spend', label: '消耗' }
|
{ key: 'spend', label: '消耗' }
|
||||||
],
|
])
|
||||||
list: []
|
const list = ref([])
|
||||||
}
|
|
||||||
},
|
function onBack() { goBackOrTab(PAGE.POINTS) }
|
||||||
watch: {
|
|
||||||
filter() { this.loadList() }
|
function loadList() {
|
||||||
},
|
|
||||||
onShow() { this.loadList() },
|
|
||||||
methods: {
|
|
||||||
onBack() { goBackOrTab(PAGE.POINTS) },
|
|
||||||
loadList() {
|
|
||||||
const store = loadMemberStore()
|
const store = loadMemberStore()
|
||||||
this.list = filterPointsHistory(store, this.filter)
|
list.value = filterPointsHistory(store, filter.value)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(filter, () => { loadList() })
|
||||||
|
|
||||||
|
loadList()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
@import '@/common/style/base.css';
|
@import '@/common/style/base.css';
|
||||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||||
|
|||||||
@@ -39,26 +39,27 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||||
import { PAGE, goBackOrTab } from '@/common/constants/routes.js'
|
import { PAGE, goBackOrTab } from '@/common/constants/routes.js'
|
||||||
import { loadMemberStore, persistMemberStore } from '@/common/memberInfo/store.js'
|
import { loadMemberStore, persistMemberStore } from '@/common/memberInfo/store.js'
|
||||||
import { getPointsPageData, redeemPointsReward } from '@/common/memberInfo/moduleStore.js'
|
import { getPointsPageData, redeemPointsReward } from '@/common/memberInfo/moduleStore.js'
|
||||||
|
|
||||||
export default {
|
const balance = ref(0)
|
||||||
components: { MemberInfoSubNav },
|
const rewards = ref([])
|
||||||
data() {
|
const redeemRecords = ref([])
|
||||||
return { balance: 0, rewards: [], redeemRecords: [] }
|
|
||||||
},
|
function loadData() {
|
||||||
onShow() {
|
|
||||||
const data = getPointsPageData(loadMemberStore())
|
const data = getPointsPageData(loadMemberStore())
|
||||||
this.balance = data.balance
|
balance.value = data.balance
|
||||||
this.rewards = data.rewards
|
rewards.value = data.rewards
|
||||||
this.redeemRecords = data.redeemRecords
|
redeemRecords.value = data.redeemRecords
|
||||||
},
|
}
|
||||||
methods: {
|
|
||||||
onBack() { goBackOrTab(PAGE.POINTS) },
|
function onBack() { goBackOrTab(PAGE.POINTS) }
|
||||||
redeem(item) {
|
|
||||||
|
function redeem(item) {
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '确认兑换',
|
title: '确认兑换',
|
||||||
content: `使用 ${item.cost} 积分兑换「${item.name}」?`,
|
content: `使用 ${item.cost} 积分兑换「${item.name}」?`,
|
||||||
@@ -70,18 +71,18 @@ export default {
|
|||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
persistMemberStore(store)
|
persistMemberStore(store)
|
||||||
const data = getPointsPageData(store)
|
const data = getPointsPageData(store)
|
||||||
this.balance = data.balance
|
balance.value = data.balance
|
||||||
this.rewards = data.rewards
|
rewards.value = data.rewards
|
||||||
this.redeemRecords = data.redeemRecords
|
redeemRecords.value = data.redeemRecords
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
loadData()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
@import '@/common/style/base.css';
|
@import '@/common/style/base.css';
|
||||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||||
|
|||||||
@@ -0,0 +1,694 @@
|
|||||||
|
<template>
|
||||||
|
<view class="scroll-container theme-light">
|
||||||
|
<view class="purchase-card-page">
|
||||||
|
<MemberInfoSubNav title="购买会员卡" @back="goBack" />
|
||||||
|
<view class="purchase-card-page__body">
|
||||||
|
<!-- 头部 -->
|
||||||
|
<view class="pc-intro">
|
||||||
|
<view class="pc-intro__icon">
|
||||||
|
<image src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/crown0.png" mode="aspectFit" />
|
||||||
|
</view>
|
||||||
|
<text class="pc-intro__title">选择会员卡类型</text>
|
||||||
|
<text class="pc-intro__desc">购买后立即生效,享受专属会员权益</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 卡列表 -->
|
||||||
|
<view class="pc-list">
|
||||||
|
<view
|
||||||
|
v-for="card in cardTypes"
|
||||||
|
:key="card.id"
|
||||||
|
class="pc-card"
|
||||||
|
:class="{ 'pc-card--selected': selectedCardId === card.id }"
|
||||||
|
@tap="selectCard(card)"
|
||||||
|
>
|
||||||
|
<view class="pc-card__check">
|
||||||
|
<view
|
||||||
|
class="pc-card__check-inner"
|
||||||
|
:class="{ 'pc-card__check-inner--checked': selectedCardId === card.id }"
|
||||||
|
></view>
|
||||||
|
</view>
|
||||||
|
<view class="pc-card__content">
|
||||||
|
<view class="pc-card__header">
|
||||||
|
<text class="pc-card__name">{{ card.cardName }}</text>
|
||||||
|
<view class="pc-card__tag" :class="'pc-card__tag--' + getTagClass(card.cardType)">
|
||||||
|
<text class="pc-card__tag-text">{{ getCardTypeName(card.cardType) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="pc-card__info">
|
||||||
|
<view class="pc-card__info-row" v-if="card.validityDays">
|
||||||
|
<text class="pc-card__info-label">有效期</text>
|
||||||
|
<text class="pc-card__info-value">{{ card.validityDays }}天</text>
|
||||||
|
</view>
|
||||||
|
<view class="pc-card__info-row" v-if="card.cardType === 'COUNT' && card.totalTimes">
|
||||||
|
<text class="pc-card__info-label">总次数</text>
|
||||||
|
<text class="pc-card__info-value">{{ card.totalTimes }}次</text>
|
||||||
|
</view>
|
||||||
|
<view class="pc-card__info-row" v-if="card.cardType === 'BALANCE' && card.amount">
|
||||||
|
<text class="pc-card__info-label">储值金额</text>
|
||||||
|
<text class="pc-card__info-value">{{ card.amount }}元</text>
|
||||||
|
</view>
|
||||||
|
<view class="pc-card__info-row" v-if="card.description">
|
||||||
|
<text class="pc-card__info-label">说明</text>
|
||||||
|
<text class="pc-card__info-value">{{ card.description }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="pc-card__footer">
|
||||||
|
<text class="pc-card__price">¥{{ card.price }}</text>
|
||||||
|
<text class="pc-card__original-price" v-if="card.originalPrice && card.originalPrice !== card.price">¥{{ card.originalPrice }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 支付方式 -->
|
||||||
|
<view class="pc-payment">
|
||||||
|
<text class="pc-payment__title">支付方式</text>
|
||||||
|
<view class="pc-payment__methods">
|
||||||
|
<view class="pc-payment__method pc-payment__method--selected">
|
||||||
|
<image src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/alipay.png" mode="aspectFit" class="pc-payment__icon" />
|
||||||
|
<text class="pc-payment__name">支付宝支付</text>
|
||||||
|
<view class="pc-payment__check"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部 -->
|
||||||
|
<view class="pc-bottom">
|
||||||
|
<view class="pc-bottom__total">
|
||||||
|
<text class="pc-bottom__total-label">合计</text>
|
||||||
|
<text class="pc-bottom__total-price">¥{{ selectedCard?.price || 0 }}</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="pc-bottom__btn"
|
||||||
|
:class="{ 'pc-bottom__btn--disabled': !selectedCard || loading }"
|
||||||
|
@tap="handlePurchase"
|
||||||
|
>
|
||||||
|
<text class="pc-bottom__btn-text">{{ loading ? '处理中...' : '立即购买' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { onBackPress } from '@dcloudio/uni-app'
|
||||||
|
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||||
|
import { purchaseMemberCard, createPayment, getPaymentStatus } from '@/api/main.js'
|
||||||
|
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||||
|
|
||||||
|
const cardTypes = ref([
|
||||||
|
{ id: 1, cardName: '月卡', cardType: 'DURATION', validityDays: 30, price: 1, originalPrice: 399 },
|
||||||
|
{ id: 2, cardName: '季卡', cardType: 'DURATION', validityDays: 90, price: 1, originalPrice: 999 },
|
||||||
|
{ id: 3, cardName: '年卡', cardType: 'DURATION', validityDays: 365, price: 1, originalPrice: 2999 },
|
||||||
|
{ id: 4, cardName: '10次卡', cardType: 'COUNT', validityDays: 90, totalTimes: 10, price: 1 },
|
||||||
|
{ id: 5, cardName: '50次卡', cardType: 'COUNT', validityDays: 180, totalTimes: 50, price: 1 },
|
||||||
|
{ id: 6, cardName: '100次卡', cardType: 'COUNT', validityDays: 365, totalTimes: 100, price: 1 },
|
||||||
|
{ id: 7, cardName: '储值500', cardType: 'BALANCE', validityDays: 365, amount: 500, price: 1 },
|
||||||
|
{ id: 8, cardName: '储值1000', cardType: 'BALANCE', validityDays: 365, amount: 1000, price: 1 },
|
||||||
|
])
|
||||||
|
|
||||||
|
const selectedCardId = ref(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
const selectedCard = ref(null)
|
||||||
|
const pollingTimer = ref(null)
|
||||||
|
const isPolling = ref(false)
|
||||||
|
|
||||||
|
// 支付方式
|
||||||
|
const TRADE_TYPE = 'ALIPAY'
|
||||||
|
|
||||||
|
// ==================== 工具函数 ====================
|
||||||
|
function getCardTypeName(type) {
|
||||||
|
const map = { DURATION: '时长卡', COUNT: '次卡', BALANCE: '储值卡' }
|
||||||
|
return map[type] || type
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTagClass(type) {
|
||||||
|
const map = { DURATION: 'time', COUNT: 'count', BALANCE: 'value' }
|
||||||
|
return map[type] || 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectCard(card) {
|
||||||
|
selectedCardId.value = card.id
|
||||||
|
selectedCard.value = card
|
||||||
|
}
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
uni.redirectTo({
|
||||||
|
url: '/pages/memberInfo/memberCard'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onBackPress(() => {
|
||||||
|
uni.redirectTo({
|
||||||
|
url: '/pages/memberInfo/memberCard'
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 唤起支付宝 App ====================
|
||||||
|
function invokeAlipayApp(payUrl) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!payUrl) {
|
||||||
|
reject(new Error('支付链接为空'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Alipay] 准备唤起支付宝:', payUrl)
|
||||||
|
|
||||||
|
// 检查是否在 App 环境
|
||||||
|
if (typeof plus === 'undefined') {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '请在 App 内完成支付',
|
||||||
|
showCancel: false
|
||||||
|
})
|
||||||
|
reject(new Error('非 App 环境'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 plus.runtime.openURL 唤起支付宝
|
||||||
|
plus.runtime.openURL(payUrl, function(err) {
|
||||||
|
console.error('[Alipay] 唤起支付宝失败:', err)
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '无法唤起支付宝,请确认已安装支付宝 App',
|
||||||
|
showCancel: false
|
||||||
|
})
|
||||||
|
reject(new Error('唤起支付宝失败: ' + (err?.message || '未知错误')))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 直接 resolve,支付结果由轮询处理
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 轮询支付状态 ====================
|
||||||
|
function startPolling(orderId) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (isPolling.value) return
|
||||||
|
isPolling.value = true
|
||||||
|
|
||||||
|
let pollCount = 0
|
||||||
|
const maxRetry = 60 // 60秒
|
||||||
|
let isResolved = false
|
||||||
|
|
||||||
|
// 监听 App 回到前台
|
||||||
|
const handleResume = () => {
|
||||||
|
if (isResolved) return
|
||||||
|
console.log('[Poll] App 回到前台,立即查询')
|
||||||
|
doQuery()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof plus !== 'undefined') {
|
||||||
|
plus.globalEvent.addEventListener('resume', handleResume)
|
||||||
|
}
|
||||||
|
|
||||||
|
function doQuery() {
|
||||||
|
if (isResolved) return
|
||||||
|
pollCount++
|
||||||
|
|
||||||
|
getPaymentStatus(orderId)
|
||||||
|
.then(res => {
|
||||||
|
if (res.code === 200 && res.data) {
|
||||||
|
const status = res.data.status || res.data.payStatus
|
||||||
|
console.log(`[Poll] 第 ${pollCount} 次查询,状态:`, status)
|
||||||
|
|
||||||
|
if (status === 'SUCCESS') {
|
||||||
|
isResolved = true
|
||||||
|
cleanup()
|
||||||
|
resolve(true)
|
||||||
|
return
|
||||||
|
} else if (status === 'FAIL' || status === 'CLOSED') {
|
||||||
|
isResolved = true
|
||||||
|
cleanup()
|
||||||
|
reject(new Error('支付失败'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 继续轮询
|
||||||
|
if (pollCount < maxRetry && !isResolved) {
|
||||||
|
pollingTimer.value = setTimeout(doQuery, 1000)
|
||||||
|
} else if (!isResolved) {
|
||||||
|
isResolved = true
|
||||||
|
cleanup()
|
||||||
|
reject(new Error('支付超时,请确认是否已完成支付'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('[Poll] 查询异常:', err)
|
||||||
|
if (pollCount < maxRetry && !isResolved) {
|
||||||
|
pollingTimer.value = setTimeout(doQuery, 1000)
|
||||||
|
} else if (!isResolved) {
|
||||||
|
isResolved = true
|
||||||
|
cleanup()
|
||||||
|
reject(new Error('支付超时,请确认是否已完成支付'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
isPolling.value = false
|
||||||
|
if (pollingTimer.value) {
|
||||||
|
clearTimeout(pollingTimer.value)
|
||||||
|
pollingTimer.value = null
|
||||||
|
}
|
||||||
|
if (typeof plus !== 'undefined') {
|
||||||
|
plus.globalEvent.removeEventListener('resume', handleResume)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 延迟 1.5 秒开始轮询
|
||||||
|
pollingTimer.value = setTimeout(doQuery, 1500)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 核心购买逻辑 ====================
|
||||||
|
async function handlePurchase() {
|
||||||
|
if (!selectedCard.value) {
|
||||||
|
uni.showToast({ title: '请选择会员卡', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading.value) return
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const store = loadMemberStore()
|
||||||
|
const memberId = store.memberProfile?.id || store.profile?.id
|
||||||
|
// if (!memberId) {
|
||||||
|
// uni.showToast({ title: '请先登录', icon: 'none' })
|
||||||
|
// loading.value = false
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 金额转换:元 -> 分
|
||||||
|
const transAmt = String(Math.round(selectedCard.value.price * 100))
|
||||||
|
const cardName = selectedCard.value.cardName
|
||||||
|
|
||||||
|
// 1. 创建支付订单
|
||||||
|
uni.showLoading({ title: '创建订单中...' })
|
||||||
|
const payRes = await createPayment({
|
||||||
|
memberId: memberId,
|
||||||
|
orderType: 'MEMBER_CARD',
|
||||||
|
goodsDesc: `购买${cardName}`,
|
||||||
|
transAmt: transAmt,
|
||||||
|
tradeType: TRADE_TYPE,
|
||||||
|
remark: `会员卡类型ID:${selectedCard.value.id}`
|
||||||
|
})
|
||||||
|
|
||||||
|
if (payRes.code !== 200) {
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.showToast({ title: payRes.message || '创建支付订单失败', icon: 'none' })
|
||||||
|
loading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const payData = payRes.data
|
||||||
|
const orderId = payData.orderId
|
||||||
|
|
||||||
|
// 2. 获取支付链接
|
||||||
|
const payUrl = payData.payUrl || payData.payInfo || payData.h5PayUrl
|
||||||
|
if (!payUrl) {
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.showToast({ title: '获取支付链接失败', icon: 'none' })
|
||||||
|
loading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 唤起支付宝 App
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.showLoading({ title: '正在唤起支付宝...' })
|
||||||
|
|
||||||
|
try {
|
||||||
|
await invokeAlipayApp(payUrl)
|
||||||
|
} catch (err) {
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.showToast({ title: '唤起支付宝失败,请确认已安装支付宝', icon: 'none' })
|
||||||
|
loading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 轮询支付状态
|
||||||
|
uni.showLoading({ title: '等待支付结果...' })
|
||||||
|
await startPolling(orderId)
|
||||||
|
|
||||||
|
// 5. 支付成功,购买会员卡
|
||||||
|
uni.hideLoading()
|
||||||
|
const memberRes = await purchaseMemberCard({
|
||||||
|
memberId: memberId,
|
||||||
|
memberCardId: selectedCard.value.id,
|
||||||
|
sourceOrderId: orderId
|
||||||
|
})
|
||||||
|
|
||||||
|
if (memberRes.code === 200) {
|
||||||
|
uni.showToast({ title: '购买成功 🎉', icon: 'success' })
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.redirectTo({
|
||||||
|
url: '/pages/memberInfo/memberCard'
|
||||||
|
})
|
||||||
|
}, 1500)
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: memberRes.message || '会员卡购买失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[purchaseCard] 购买失败:', e)
|
||||||
|
uni.hideLoading()
|
||||||
|
const errorMsg = e.message || '购买失败'
|
||||||
|
uni.showToast({ title: errorMsg, icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 生命周期 ====================
|
||||||
|
onMounted(() => {
|
||||||
|
if (cardTypes.value.length > 0) {
|
||||||
|
selectCard(cardTypes.value[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
// 清理轮询
|
||||||
|
if (pollingTimer.value) {
|
||||||
|
clearTimeout(pollingTimer.value)
|
||||||
|
pollingTimer.value = null
|
||||||
|
}
|
||||||
|
isPolling.value = false
|
||||||
|
if (typeof plus !== 'undefined') {
|
||||||
|
plus.globalEvent.removeEventListener('resume', () => {})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '@/common/style/base.css';
|
||||||
|
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||||
|
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||||
|
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||||
|
@import '@/common/style/memberInfo/member-info-sub-nav.css';
|
||||||
|
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||||
|
|
||||||
|
.purchase-card-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #F5F7FA;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purchase-card-page__body {
|
||||||
|
padding-bottom: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-intro {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 32px 24px;
|
||||||
|
background: linear-gradient(135deg, #1A365D 0%, #2C5282 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-intro__icon {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
image {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-intro__title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #FFFFFF;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-intro__desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-list {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-payment {
|
||||||
|
padding: 16px;
|
||||||
|
margin: 0 16px;
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-payment__title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1A202C;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-payment__methods {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-payment__method {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px;
|
||||||
|
border: 2px solid #1677FF;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #F0F7FF;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&--selected {
|
||||||
|
border-color: #1677FF;
|
||||||
|
background: #F0F7FF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-payment__icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-payment__name {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #2D3748;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-payment__check {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background: #1677FF;
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin: 4px auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&--selected {
|
||||||
|
border-color: #2C5282;
|
||||||
|
background: #F7FAFC;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-card__check {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
margin-right: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-card__check-inner {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid #CBD5E0;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&--checked {
|
||||||
|
border-color: #2C5282;
|
||||||
|
background: #2C5282;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin: 4px auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-card__content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-card__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-card__name {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1A202C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-card__tag {
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
&--time {
|
||||||
|
background: #EBF8FF;
|
||||||
|
.pc-card__tag-text { color: #2B6CB0; }
|
||||||
|
}
|
||||||
|
&--count {
|
||||||
|
background: #FEF3E2;
|
||||||
|
.pc-card__tag-text { color: #D69E2E; }
|
||||||
|
}
|
||||||
|
&--value {
|
||||||
|
background: #F0FFF4;
|
||||||
|
.pc-card__tag-text { color: #38A169; }
|
||||||
|
}
|
||||||
|
&--default {
|
||||||
|
background: #E2E8F0;
|
||||||
|
.pc-card__tag-text { color: #718096; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-card__tag-text {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-card__info {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px 24px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-card__info-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-card__info-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #718096;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-card__info-value {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2D3748;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-card__footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-card__price {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #E53E3E;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-card__original-price {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #A0AEC0;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-bottom {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px 24px;
|
||||||
|
padding-bottom: calc(16px + env(safe-area-inset-bottom));
|
||||||
|
background: #FFFFFF;
|
||||||
|
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.05);
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-bottom__total {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-bottom__total-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #718096;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-bottom__total-price {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #E53E3E;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-bottom__btn {
|
||||||
|
background: #1677FF;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px 48px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&--disabled {
|
||||||
|
background: #A0AEC0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-bottom__btn-text {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -104,45 +104,37 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||||
import { getReferralPageData } from '@/common/memberInfo/moduleStore.js'
|
import { getReferralPageData } from '@/common/memberInfo/moduleStore.js'
|
||||||
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||||
import { subPageMixin } from '@/common/memberInfo/mixins.js'
|
import { backToMemberCenter } from '@/common/constants/routes.js'
|
||||||
|
|
||||||
export default {
|
const data = ref(getReferralPageData(loadMemberStore()))
|
||||||
components: { MemberInfoSubNav },
|
|
||||||
mixins: [subPageMixin],
|
function goBack() {
|
||||||
data() {
|
backToMemberCenter()
|
||||||
return {
|
|
||||||
data: { code: '', invited: 0, registered: 0, purchased: 0, records: [], rules: [] }
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
onShow() {
|
function copyCode() {
|
||||||
const store = loadMemberStore()
|
|
||||||
this.data = getReferralPageData(store)
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
copyCode() {
|
|
||||||
uni.setClipboardData({
|
uni.setClipboardData({
|
||||||
data: this.data.code,
|
data: data.value.code,
|
||||||
success: () => uni.showToast({ title: '邀请码已复制', icon: 'success' })
|
success: () => uni.showToast({ title: '已复制', icon: 'success' })
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
shareInvite() {
|
|
||||||
|
function shareInvite() {
|
||||||
uni.showActionSheet({
|
uni.showActionSheet({
|
||||||
itemList: ['分享给微信好友', '生成邀请海报', '复制分享链接'],
|
itemList: ['分享给微信好友', '生成分享海报'],
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
const msgs = ['已唤起分享', '海报已生成', '链接已复制']
|
uni.showToast({ title: ['已唤起微信分享', '海报已生成'][res.tapIndex] || '分享成功', icon: 'none' })
|
||||||
uni.showToast({ title: msgs[res.tapIndex] || '分享成功', icon: 'none' })
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
@import '@/common/style/base.css';
|
@import '@/common/style/base.css';
|
||||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||||
@@ -151,8 +143,4 @@ export default {
|
|||||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||||
@import '@/common/style/memberInfo/pages/body-test-common.css';
|
@import '@/common/style/memberInfo/pages/body-test-common.css';
|
||||||
@import '@/common/style/memberInfo/pages/module-pages-common.css';
|
@import '@/common/style/memberInfo/pages/module-pages-common.css';
|
||||||
|
|
||||||
.mi-mod-referral-hero .bt-hero__actions {
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -69,64 +69,57 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||||
import BodyTestTrendChart from '@/components/memberInfo/BodyTestTrendChart.vue'
|
import BodyTestTrendChart from '@/components/memberInfo/BodyTestTrendChart.vue'
|
||||||
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
|
import { PAGE, navigateToPage, backToMemberCenter } from '@/common/constants/routes.js'
|
||||||
import { getTrainingReportData, filterTrainingSessions } from '@/common/memberInfo/moduleStore.js'
|
import { getTrainingReportData, filterTrainingSessions } from '@/common/memberInfo/moduleStore.js'
|
||||||
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||||
import { subPageMixin } from '@/common/memberInfo/mixins.js'
|
|
||||||
|
|
||||||
export default {
|
const period = ref('week')
|
||||||
components: { MemberInfoSubNav, BodyTestTrendChart },
|
const periods = ref([
|
||||||
mixins: [subPageMixin],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
period: 'week',
|
|
||||||
periods: [
|
|
||||||
{ key: 'week', label: '本周' },
|
{ key: 'week', label: '本周' },
|
||||||
{ key: 'month', label: '本月' }
|
{ key: 'month', label: '本月' }
|
||||||
],
|
])
|
||||||
typeFilter: 'all',
|
const typeFilter = ref('all')
|
||||||
typeFilters: [
|
const typeFilters = ref([
|
||||||
{ key: 'all', label: '全部' },
|
{ key: 'all', label: '全部' },
|
||||||
{ key: 'group', label: '团课' },
|
{ key: 'group', label: '团课' },
|
||||||
{ key: 'private', label: '私教' },
|
{ key: 'private', label: '私教' },
|
||||||
{ key: 'free', label: '自由' }
|
{ key: 'free', label: '自由' }
|
||||||
],
|
])
|
||||||
report: { summary: {}, trendHours: [], trendCalories: [], sessions: [] },
|
const report = ref({ summary: {}, trendHours: [], trendCalories: [], sessions: [] })
|
||||||
sessions: [],
|
const chartWidth = ref(300)
|
||||||
chartWidth: 300
|
|
||||||
|
const sessions = computed(() => {
|
||||||
|
return filterTrainingSessions(report.value.sessions, typeFilter.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
function switchPeriod(p) {
|
||||||
|
period.value = p
|
||||||
|
refresh()
|
||||||
}
|
}
|
||||||
},
|
|
||||||
onLoad() {
|
function refresh() {
|
||||||
this.chartWidth = uni.getSystemInfoSync().windowWidth - 64
|
|
||||||
this.refresh()
|
|
||||||
},
|
|
||||||
onShow() { this.refresh() },
|
|
||||||
methods: {
|
|
||||||
refresh() {
|
|
||||||
const store = loadMemberStore()
|
const store = loadMemberStore()
|
||||||
this.report = getTrainingReportData(store, this.period)
|
report.value = getTrainingReportData(store, period.value)
|
||||||
this.sessions = filterTrainingSessions(store, { type: this.typeFilter })
|
|
||||||
},
|
|
||||||
switchPeriod(key) {
|
|
||||||
this.period = key
|
|
||||||
this.refresh()
|
|
||||||
},
|
|
||||||
goSession(item) {
|
|
||||||
navigateToPage(`${PAGE.TRAIN_SESSION}?id=${item.id}`)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
typeFilter() {
|
|
||||||
this.sessions = filterTrainingSessions(loadMemberStore(), { type: this.typeFilter })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
backToMemberCenter()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function goSession(item) {
|
||||||
|
navigateToPage(`${PAGE.TRAIN_SESSION_DETAIL}?id=${item.id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
chartWidth.value = uni.getSystemInfoSync().windowWidth - 64
|
||||||
|
refresh()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
@import '@/common/style/base.css';
|
@import '@/common/style/base.css';
|
||||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||||
|
|||||||
@@ -31,25 +31,28 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||||
import { PAGE, goBackOrTab } from '@/common/constants/routes.js'
|
import { PAGE, goBackOrTab } from '@/common/constants/routes.js'
|
||||||
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||||
import { getTrainingSessionById } from '@/common/memberInfo/moduleStore.js'
|
import { getTrainingSessionById } from '@/common/memberInfo/moduleStore.js'
|
||||||
|
|
||||||
export default {
|
const session = ref(null)
|
||||||
components: { MemberInfoSubNav },
|
|
||||||
data() { return { session: null } },
|
onMounted(() => {
|
||||||
onLoad(options) {
|
const pages = getCurrentPages()
|
||||||
this.session = getTrainingSessionById(loadMemberStore(), options?.id)
|
const currentPage = pages[pages.length - 1]
|
||||||
},
|
const options = currentPage?.options || {}
|
||||||
methods: {
|
session.value = getTrainingSessionById(loadMemberStore(), options?.id)
|
||||||
onBack() { goBackOrTab(PAGE.TRAIN_REPORT) }
|
})
|
||||||
}
|
|
||||||
|
function onBack() {
|
||||||
|
goBackOrTab(PAGE.TRAIN_REPORT)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
@import '@/common/style/base.css';
|
@import '@/common/style/base.css';
|
||||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||||
|
|||||||
@@ -210,13 +210,13 @@
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||||
import { fitnessGoalOptions } from '@/common/memberInfo/mockData.js'
|
import { getUserInfo, updateUserInfo } from '@/api/main.js'
|
||||||
import { loadMemberStore, saveUserProfile } from '@/common/memberInfo/store.js'
|
|
||||||
import { previewImage, persistChosenImage } from '@/common/memberInfo/media.js'
|
import { previewImage, persistChosenImage } from '@/common/memberInfo/media.js'
|
||||||
import { maskPhone, normalizePhoneForStore } from '@/common/memberInfo/format.js'
|
import { maskPhone, normalizePhoneForStore } from '@/common/memberInfo/format.js'
|
||||||
import { subPageMixin } from '@/common/memberInfo/mixins.js'
|
|
||||||
import {
|
import {
|
||||||
validateName,
|
validateName,
|
||||||
validatePhoneForRebind,
|
validatePhoneForRebind,
|
||||||
@@ -227,116 +227,194 @@ import {
|
|||||||
validateUserProfile,
|
validateUserProfile,
|
||||||
showValidationError
|
showValidationError
|
||||||
} from '@/common/memberInfo/validate.js'
|
} from '@/common/memberInfo/validate.js'
|
||||||
|
import { backToMemberCenter } from '@/common/constants/routes.js'
|
||||||
|
|
||||||
const DEFAULT_AVATAR = 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AvatarEditWrap.png'
|
const DEFAULT_AVATAR = 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AvatarEditWrap.png'
|
||||||
|
|
||||||
export default {
|
const name = ref('')
|
||||||
components: { MemberInfoSubNav },
|
const phone = ref('')
|
||||||
mixins: [subPageMixin],
|
const gender = ref('female')
|
||||||
data() {
|
const birthday = ref('')
|
||||||
return {
|
const height = ref('')
|
||||||
name: '',
|
const weight = ref('')
|
||||||
phone: '',
|
const fitnessGoals = ref([])
|
||||||
gender: 'female',
|
const avatar = ref(DEFAULT_AVATAR)
|
||||||
birthday: '',
|
const avatarKey = ref(0)
|
||||||
height: '',
|
const avatarDirty = ref(false)
|
||||||
weight: '',
|
const fitnessGoalOptions = ref(['减脂1', '增肌', '塑形', '增重', '康复', '体能'])
|
||||||
fitnessGoals: [],
|
const loading = ref(false)
|
||||||
avatar: DEFAULT_AVATAR,
|
|
||||||
avatarKey: 0,
|
const avatarSrc = computed(() => {
|
||||||
avatarDirty: false,
|
return avatar.value || DEFAULT_AVATAR
|
||||||
fitnessGoalOptions
|
})
|
||||||
}
|
|
||||||
},
|
const displayPhone = computed(() => {
|
||||||
computed: {
|
return maskPhone(phone.value)
|
||||||
avatarSrc() {
|
})
|
||||||
return this.avatar || DEFAULT_AVATAR
|
|
||||||
},
|
const birthdayValue = computed(() => {
|
||||||
displayPhone() {
|
const match = String(birthday.value).match(/(\d{4})年(\d{2})月(\d{2})日/)
|
||||||
return maskPhone(this.phone)
|
|
||||||
},
|
|
||||||
birthdayValue() {
|
|
||||||
const match = String(this.birthday).match(/(\d{4})年(\d{2})月(\d{2})日/)
|
|
||||||
if (match) {
|
if (match) {
|
||||||
return `${match[1]}-${match[2]}-${match[3]}`
|
return `${match[1]}-${match[2]}-${match[3]}`
|
||||||
}
|
}
|
||||||
return '1995-06-15'
|
return '1995-06-15'
|
||||||
}
|
})
|
||||||
},
|
|
||||||
onShow() {
|
|
||||||
// 从相册/相机返回会再次触发 onShow,不能覆盖刚选未保存的头像
|
|
||||||
this.loadProfile({ preserveLocalAvatar: true })
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
loadProfile(options = {}) {
|
|
||||||
const store = loadMemberStore()
|
|
||||||
const profile = store.profile
|
|
||||||
this.name = profile.name
|
|
||||||
this.phone = profile.phone
|
|
||||||
this.gender = profile.gender
|
|
||||||
this.birthday = profile.birthday
|
|
||||||
this.height = profile.height
|
|
||||||
this.weight = profile.weight
|
|
||||||
this.fitnessGoals = [...(profile.fitnessGoals || [])]
|
|
||||||
|
|
||||||
const storedAvatar = profile.avatar || DEFAULT_AVATAR
|
function goBack() {
|
||||||
const hasUnsavedLocalAvatar =
|
backToMemberCenter()
|
||||||
options.preserveLocalAvatar &&
|
}
|
||||||
this.avatarDirty &&
|
|
||||||
this.avatar &&
|
|
||||||
this.avatar !== storedAvatar
|
|
||||||
|
|
||||||
if (!hasUnsavedLocalAvatar) {
|
function mapApiToProfile(apiData) {
|
||||||
this.setAvatar(storedAvatar)
|
const genderMap = {
|
||||||
this.avatarDirty = false
|
'MALE': 'male',
|
||||||
|
'FEMALE': 'female',
|
||||||
|
'UNKNOWN': 'female'
|
||||||
|
}
|
||||||
|
let birthdayVal = ''
|
||||||
|
if (apiData.birthday) {
|
||||||
|
const parts = apiData.birthday.split('-')
|
||||||
|
if (parts.length === 3) {
|
||||||
|
birthdayVal = `${parts[0]}年${parts[1]}月${parts[2]}日`
|
||||||
}
|
}
|
||||||
},
|
|
||||||
setAvatar(path) {
|
|
||||||
const next = path || DEFAULT_AVATAR
|
|
||||||
if (this.avatar !== next) {
|
|
||||||
this.avatar = next
|
|
||||||
this.avatarKey += 1
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
getProfilePayload() {
|
|
||||||
return {
|
return {
|
||||||
name: this.name,
|
name: apiData.nickname || '',
|
||||||
phone: normalizePhoneForStore(this.phone),
|
phone: apiData.phone || '',
|
||||||
gender: this.gender,
|
gender: genderMap[apiData.gender] || 'female',
|
||||||
birthday: this.birthday,
|
birthday: birthdayVal,
|
||||||
height: this.height,
|
avatar: apiData.avatar || DEFAULT_AVATAR
|
||||||
weight: this.weight,
|
|
||||||
fitnessGoals: [...this.fitnessGoals],
|
|
||||||
avatar: this.avatar
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
handleSave() {
|
|
||||||
|
function mapProfileToApi(profile) {
|
||||||
|
const genderMap = {
|
||||||
|
'male': 'MALE',
|
||||||
|
'female': 'FEMALE'
|
||||||
|
}
|
||||||
|
let birthdayVal = ''
|
||||||
|
if (profile.birthday) {
|
||||||
|
const match = String(profile.birthday).match(/(\d{4})年(\d{2})月(\d{2})日/)
|
||||||
|
if (match) {
|
||||||
|
birthdayVal = `${match[1]}-${match[2]}-${match[3]}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
nickname: profile.name,
|
||||||
|
gender: genderMap[profile.gender] || 'UNKNOWN',
|
||||||
|
birthday: birthdayVal,
|
||||||
|
avatar: profile.avatar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchUserInfo() {
|
||||||
|
// 如果正在加载中,不重复请求
|
||||||
|
if (loading.value) {
|
||||||
|
console.log('[userInfo] 正在加载中,跳过重复请求')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loading.value = true
|
||||||
|
console.log('[userInfo] fetchUserInfo 开始执行')
|
||||||
|
console.log('[userInfo] 步骤1: 调用 getUserInfo API...')
|
||||||
|
try {
|
||||||
|
const res = await getUserInfo({ cache: false })
|
||||||
|
console.log('[userInfo] 步骤2: API返回,res =', res)
|
||||||
|
console.log('[userInfo] 步骤3: res类型 =', typeof res, 'res.data =', res?.data)
|
||||||
|
|
||||||
|
// 兼容两种数据格式:1.直接返回数据对象 2.返回 {code, data, message} 格式
|
||||||
|
const apiData = res.data || res
|
||||||
|
console.log('[userInfo] 步骤4: apiData =', apiData)
|
||||||
|
|
||||||
|
if (apiData && (apiData.nickname !== undefined || apiData.phone !== undefined)) {
|
||||||
|
console.log('[userInfo] 步骤5: 收到有效数据,更新页面')
|
||||||
|
const profile = mapApiToProfile(apiData)
|
||||||
|
console.log('[userInfo] 步骤6: 映射后数据 =', profile)
|
||||||
|
name.value = profile.name
|
||||||
|
phone.value = profile.phone
|
||||||
|
gender.value = profile.gender
|
||||||
|
birthday.value = profile.birthday
|
||||||
|
setAvatar(profile.avatar)
|
||||||
|
console.log('[userInfo] 步骤7: 页面数据已更新,name =', name.value)
|
||||||
|
} else {
|
||||||
|
console.log('[userInfo] 步骤5: 数据无效,跳过更新')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[userInfo] 获取用户信息失败:', err)
|
||||||
|
uni.showToast({ title: '获取信息失败: ' + (err.message || '网络错误'), icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
console.log('[userInfo] fetchUserInfo 执行完成')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadProfile(options = {}) {
|
||||||
|
// API模式下从data直接获取,不从store获取
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAvatar(path) {
|
||||||
|
const next = path || DEFAULT_AVATAR
|
||||||
|
if (avatar.value !== next) {
|
||||||
|
avatar.value = next
|
||||||
|
avatarKey.value += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProfilePayload() {
|
||||||
|
return {
|
||||||
|
name: name.value,
|
||||||
|
phone: normalizePhoneForStore(phone.value),
|
||||||
|
gender: gender.value,
|
||||||
|
birthday: birthday.value,
|
||||||
|
height: height.value,
|
||||||
|
weight: weight.value,
|
||||||
|
fitnessGoals: [...fitnessGoals.value],
|
||||||
|
avatar: avatar.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSave() {
|
||||||
const result = validateUserProfile(
|
const result = validateUserProfile(
|
||||||
this.getProfilePayload(),
|
getProfilePayload(),
|
||||||
this.fitnessGoalOptions
|
fitnessGoalOptions.value
|
||||||
)
|
)
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
showValidationError(result.message)
|
showValidationError(result.message)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = loadMemberStore()
|
applyValidatedProfile(result.value)
|
||||||
saveUserProfile(store, result.value)
|
avatarDirty.value = false
|
||||||
this.applyValidatedProfile(result.value)
|
|
||||||
this.avatarDirty = false
|
saveUserInfo(result.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveUserInfo(profile) {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const apiData = mapProfileToApi(profile)
|
||||||
|
console.log('[userInfo] PUT /api/member/info 请求:', apiData)
|
||||||
|
const res = await updateUserInfo(apiData)
|
||||||
|
console.log('[userInfo] PUT /api/member/info 返回:', res)
|
||||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||||
setTimeout(() => this.goBack(), 600)
|
setTimeout(() => goBack(), 600)
|
||||||
},
|
} catch (err) {
|
||||||
applyValidatedProfile(profile) {
|
console.error('保存用户信息失败', err)
|
||||||
this.name = profile.name
|
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||||
this.phone = profile.phone
|
} finally {
|
||||||
this.gender = profile.gender
|
loading.value = false
|
||||||
this.birthday = profile.birthday
|
}
|
||||||
this.height = profile.height
|
}
|
||||||
this.weight = profile.weight
|
|
||||||
this.fitnessGoals = [...profile.fitnessGoals]
|
function applyValidatedProfile(profile) {
|
||||||
},
|
name.value = profile.name
|
||||||
changeAvatar() {
|
phone.value = profile.phone
|
||||||
|
gender.value = profile.gender
|
||||||
|
birthday.value = profile.birthday
|
||||||
|
height.value = profile.height
|
||||||
|
weight.value = profile.weight
|
||||||
|
fitnessGoals.value = [...profile.fitnessGoals]
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeAvatar() {
|
||||||
uni.chooseImage({
|
uni.chooseImage({
|
||||||
count: 1,
|
count: 1,
|
||||||
sizeType: ['compressed'],
|
sizeType: ['compressed'],
|
||||||
@@ -345,29 +423,23 @@ export default {
|
|||||||
const tempPath =
|
const tempPath =
|
||||||
res.tempFilePaths?.[0] || res.tempFiles?.[0]?.tempFilePath
|
res.tempFilePaths?.[0] || res.tempFiles?.[0]?.tempFilePath
|
||||||
if (!tempPath) return
|
if (!tempPath) return
|
||||||
|
avatarDirty.value = true
|
||||||
// 真机先用 tempFilePath 立即展示,避免 saveFile 异步期间被 onShow 覆盖
|
setAvatar(tempPath)
|
||||||
this.setAvatar(tempPath)
|
persistChosenImage(tempPath)
|
||||||
this.avatarDirty = true
|
|
||||||
uni.showToast({ title: '头像已选择', icon: 'success' })
|
|
||||||
|
|
||||||
persistChosenImage(tempPath).then((savedPath) => {
|
|
||||||
if (savedPath && savedPath !== this.avatar) {
|
|
||||||
this.setAvatar(savedPath)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
|
||||||
},
|
function previewAvatar() {
|
||||||
previewAvatar() {
|
previewImage(avatarSrc.value)
|
||||||
previewImage(this.avatarSrc, DEFAULT_AVATAR)
|
}
|
||||||
},
|
|
||||||
editName() {
|
function editName() {
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '修改姓名',
|
title: '修改姓名',
|
||||||
editable: true,
|
editable: true,
|
||||||
placeholderText: '请输入姓名(2-8字)',
|
placeholderText: '请输入姓名',
|
||||||
content: this.name,
|
content: String(name.value),
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (!res.confirm) return
|
if (!res.confirm) return
|
||||||
const result = validateName(res.content)
|
const result = validateName(res.content)
|
||||||
@@ -375,16 +447,17 @@ export default {
|
|||||||
showValidationError(result.message)
|
showValidationError(result.message)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.name = result.value
|
name.value = result.value
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
rebindPhone() {
|
|
||||||
|
function rebindPhone() {
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '换绑手机号',
|
title: '换绑手机号',
|
||||||
editable: true,
|
editable: true,
|
||||||
placeholderText: '请输入11位手机号',
|
placeholderText: '请输入新手机号',
|
||||||
content: normalizePhoneForStore(this.phone) || this.phone,
|
content: '',
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (!res.confirm) return
|
if (!res.confirm) return
|
||||||
const result = validatePhoneForRebind(res.content)
|
const result = validatePhoneForRebind(res.content)
|
||||||
@@ -392,31 +465,30 @@ export default {
|
|||||||
showValidationError(result.message)
|
showValidationError(result.message)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.phone = result.value
|
phone.value = normalizePhoneForStore(result.value)
|
||||||
uni.showToast({ title: '手机号已更新', icon: 'success' })
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
|
||||||
selectGender(gender) {
|
|
||||||
this.gender = gender
|
|
||||||
},
|
|
||||||
onBirthdayChange(e) {
|
|
||||||
const value = e.detail.value
|
|
||||||
const [y, m, d] = value.split('-')
|
|
||||||
const formatted = `${y}年${m}月${d}日`
|
|
||||||
const result = validateBirthday(formatted)
|
|
||||||
if (!result.ok) {
|
|
||||||
showValidationError(result.message)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
this.birthday = result.value
|
|
||||||
},
|
function selectGender(g) {
|
||||||
editHeight() {
|
gender.value = g
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBirthdayChange(e) {
|
||||||
|
const val = e.detail.value
|
||||||
|
if (!val) return
|
||||||
|
const parts = val.split('-')
|
||||||
|
if (parts.length === 3) {
|
||||||
|
birthday.value = `${parts[0]}年${parts[1]}月${parts[2]}日`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function editHeight() {
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '修改身高',
|
title: '修改身高',
|
||||||
editable: true,
|
editable: true,
|
||||||
placeholderText: '50-250,单位 cm',
|
placeholderText: '100-250,单位 cm',
|
||||||
content: String(this.height),
|
content: String(height.value),
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (!res.confirm) return
|
if (!res.confirm) return
|
||||||
const result = validateHeight(res.content)
|
const result = validateHeight(res.content)
|
||||||
@@ -424,16 +496,17 @@ export default {
|
|||||||
showValidationError(result.message)
|
showValidationError(result.message)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.height = result.value
|
height.value = result.value
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
editWeight() {
|
|
||||||
|
function editWeight() {
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '修改体重',
|
title: '修改体重',
|
||||||
editable: true,
|
editable: true,
|
||||||
placeholderText: '20-300,单位 kg',
|
placeholderText: '20-300,单位 kg',
|
||||||
content: String(this.weight),
|
content: String(weight.value),
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (!res.confirm) return
|
if (!res.confirm) return
|
||||||
const result = validateWeight(res.content)
|
const result = validateWeight(res.content)
|
||||||
@@ -441,32 +514,43 @@ export default {
|
|||||||
showValidationError(result.message)
|
showValidationError(result.message)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.weight = result.value
|
weight.value = result.value
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
toggleGoal(goal) {
|
|
||||||
const index = this.fitnessGoals.indexOf(goal)
|
function toggleGoal(goal) {
|
||||||
|
const index = fitnessGoals.value.indexOf(goal)
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
this.fitnessGoals.splice(index, 1)
|
fitnessGoals.value.splice(index, 1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const preview = [...this.fitnessGoals, goal]
|
const preview = [...fitnessGoals.value, goal]
|
||||||
const result = validateFitnessGoals(preview, this.fitnessGoalOptions)
|
const result = validateFitnessGoals(preview, fitnessGoalOptions.value)
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
showValidationError(result.message)
|
showValidationError(result.message)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.fitnessGoals.push(goal)
|
fitnessGoals.value.push(goal)
|
||||||
},
|
|
||||||
isGoalSelected(goal) {
|
|
||||||
return this.fitnessGoals.includes(goal)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isGoalSelected(goal) {
|
||||||
|
return fitnessGoals.value.includes(goal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
console.log('[userInfo] onMounted 被调用,开始获取用户信息')
|
||||||
|
fetchUserInfo()
|
||||||
|
})
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
console.log('[userInfo] onShow 被调用')
|
||||||
|
// 每次进入页面都尝试获取最新数据
|
||||||
|
fetchUserInfo()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
@import '@/common/style/base.css';
|
@import '@/common/style/base.css';
|
||||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||||
|
|||||||
@@ -0,0 +1,391 @@
|
|||||||
|
<template>
|
||||||
|
<view class="message-detail-page">
|
||||||
|
<!-- 顶部导航栏 -->
|
||||||
|
<view class="nav-header" :style="navStyle">
|
||||||
|
<view class="nav-left">
|
||||||
|
<view class="back-btn" @tap="goBack">
|
||||||
|
<text class="back-icon">‹</text>
|
||||||
|
</view>
|
||||||
|
<text class="nav-title">消息详情</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 内容区域 -->
|
||||||
|
<scroll-view
|
||||||
|
scroll-y
|
||||||
|
class="detail-content"
|
||||||
|
:style="contentStyle"
|
||||||
|
>
|
||||||
|
<!-- 消息卡片 -->
|
||||||
|
<view class="message-card">
|
||||||
|
<!-- 消息图标和类型 -->
|
||||||
|
<view class="message-header">
|
||||||
|
<view :class="['message-icon', message.type]">
|
||||||
|
<image :src="getMessageIcon(message.type)" mode="aspectFit" class="icon-img" />
|
||||||
|
</view>
|
||||||
|
<view class="header-info">
|
||||||
|
<view :class="['type-tag', message.type]">
|
||||||
|
<text>{{ getTypeLabel(message.type) }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="!message.read" class="read-status">
|
||||||
|
<view class="status-dot"></view>
|
||||||
|
<text>未读</text>
|
||||||
|
</view>
|
||||||
|
<view v-else class="read-status read">
|
||||||
|
<view class="status-dot"></view>
|
||||||
|
<text>已读</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 消息标题 -->
|
||||||
|
<view class="message-title-wrap">
|
||||||
|
<text class="message-title">{{ message.title }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 消息内容 -->
|
||||||
|
<view class="message-body">
|
||||||
|
<text class="message-content">{{ message.content }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 消息时间 -->
|
||||||
|
<view class="message-footer">
|
||||||
|
<text class="message-time">{{ formatTime(message.timestamp) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部占位 -->
|
||||||
|
<view class="bottom-placeholder"></view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
|
import { markMessageAsRead } from '@/api/main.js'
|
||||||
|
|
||||||
|
const message = ref({
|
||||||
|
id: '',
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
type: 'system',
|
||||||
|
read: false,
|
||||||
|
timestamp: Date.now()
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusBarHeight = ref(20)
|
||||||
|
const navBarHeight = ref(44)
|
||||||
|
|
||||||
|
const navStyle = computed(() => ({
|
||||||
|
paddingTop: `${statusBarHeight.value}px`,
|
||||||
|
height: `${statusBarHeight.value + navBarHeight.value}px`
|
||||||
|
}))
|
||||||
|
|
||||||
|
const contentStyle = computed(() => ({
|
||||||
|
paddingTop: `${statusBarHeight.value + navBarHeight.value + 58}px`,
|
||||||
|
height: `calc(100vh - ${statusBarHeight.value + navBarHeight.value}px)`
|
||||||
|
}))
|
||||||
|
|
||||||
|
onLoad((options) => {
|
||||||
|
if (options?.message) {
|
||||||
|
try {
|
||||||
|
message.value = JSON.parse(decodeURIComponent(options.message))
|
||||||
|
|
||||||
|
// 如果是未读状态,自动标记为已读
|
||||||
|
if (!message.value.read) {
|
||||||
|
autoMarkAsRead()
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('解析消息数据失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const sysInfo = uni.getSystemInfoSync()
|
||||||
|
statusBarHeight.value = sysInfo.statusBarHeight || 20
|
||||||
|
navBarHeight.value = 10
|
||||||
|
})
|
||||||
|
|
||||||
|
const getMessageIcon = (type) => {
|
||||||
|
const icons = {
|
||||||
|
course: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/course.png',
|
||||||
|
system: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/plan.png',
|
||||||
|
activity: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/data.png',
|
||||||
|
points: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/message.png'
|
||||||
|
}
|
||||||
|
return icons[type] || icons.system
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTypeLabel = (type) => {
|
||||||
|
const labels = {
|
||||||
|
course: '课程通知',
|
||||||
|
system: '系统消息',
|
||||||
|
activity: '活动推送',
|
||||||
|
points: '积分动态'
|
||||||
|
}
|
||||||
|
return labels[type] || '通知'
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatTime = (timestamp) => {
|
||||||
|
if (!timestamp) return ''
|
||||||
|
|
||||||
|
const num = typeof timestamp === 'number' ? timestamp : Number(timestamp)
|
||||||
|
const time = num.toString().length === 10 ? num * 1000 : num
|
||||||
|
|
||||||
|
const date = new Date(time)
|
||||||
|
const now = new Date()
|
||||||
|
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
|
||||||
|
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
|
||||||
|
const msgDate = `${year}-${month}-${day}`
|
||||||
|
|
||||||
|
if (msgDate === today) {
|
||||||
|
return `今天 ${hours}:${minutes}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${year}年${month}月${day}日 ${hours}:${minutes}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
uni.navigateBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
const autoMarkAsRead = async () => {
|
||||||
|
if (message.value.read) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await markMessageAsRead(message.value.id)
|
||||||
|
message.value.read = true
|
||||||
|
uni.$emit('messageRead', { id: message.value.id })
|
||||||
|
} catch (e) {
|
||||||
|
console.error('自动标记已读失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.message-detail-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #F5F7FA;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-header {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-left: 24rpx;
|
||||||
|
padding-right: 24rpx;
|
||||||
|
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
|
.nav-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
width: 64rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(0, 0, 0, 0.04);
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.9);
|
||||||
|
background: rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-icon {
|
||||||
|
font-size: 48rpx;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
padding-top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-card {
|
||||||
|
margin: 0 24rpx;
|
||||||
|
padding: 32rpx;
|
||||||
|
background: white;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 20rpx;
|
||||||
|
margin-bottom: 28rpx;
|
||||||
|
|
||||||
|
.message-icon {
|
||||||
|
width: 96rpx;
|
||||||
|
height: 96rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
|
||||||
|
|
||||||
|
&.course {
|
||||||
|
background: linear-gradient(135deg, #4A90D9 0%, #2E6BB5 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.system {
|
||||||
|
background: linear-gradient(135deg, #52C41A 0%, #389E0D 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.activity {
|
||||||
|
background: linear-gradient(135deg, #FF6B35 0%, #E85D2C 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.points {
|
||||||
|
background: linear-gradient(135deg, #667EEA 0%, #5A4BD1 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-img {
|
||||||
|
width: 48rpx;
|
||||||
|
height: 48rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12rpx;
|
||||||
|
|
||||||
|
.type-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6rpx 16rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&.course {
|
||||||
|
background: rgba(74, 144, 217, 0.1);
|
||||||
|
color: #4A90D9;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.system {
|
||||||
|
background: rgba(82, 196, 26, 0.1);
|
||||||
|
color: #52C41A;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.activity {
|
||||||
|
background: rgba(255, 107, 53, 0.1);
|
||||||
|
color: #FF6B35;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.points {
|
||||||
|
background: rgba(102, 126, 234, 0.1);
|
||||||
|
color: #667EEA;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 12rpx;
|
||||||
|
height: 12rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #FF4757, #FF2D55);
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #FF4757;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.read {
|
||||||
|
.status-dot {
|
||||||
|
background: #52C41A;
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
text {
|
||||||
|
color: #52C41A;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-title-wrap {
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
padding-bottom: 24rpx;
|
||||||
|
border-bottom: 1rpx solid rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.message-title {
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1A1A1A;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-body {
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.8;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-footer {
|
||||||
|
padding-top: 20rpx;
|
||||||
|
border-top: 1rpx solid rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.message-time {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-placeholder {
|
||||||
|
height: 80rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -231,11 +231,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
import { onPullDownRefresh } from '@dcloudio/uni-app'
|
import { onPullDownRefresh } from '@dcloudio/uni-app'
|
||||||
|
import { getGroupCoursePage } from '@/api/main.js'
|
||||||
// 测试模式
|
|
||||||
const TEST_MODE = true
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 搜索关键词
|
// 搜索关键词
|
||||||
const keyword = ref('')
|
const keyword = ref('')
|
||||||
@@ -395,6 +391,19 @@ const getCourseTypeName = (courseType) => {
|
|||||||
return typeMap[courseType] || '其他'
|
return typeMap[courseType] || '其他'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算课程时长
|
||||||
|
*/
|
||||||
|
const calculateDuration = (startTime, endTime) => {
|
||||||
|
if (!startTime || !endTime) return '60分钟'
|
||||||
|
const start = new Date(startTime)
|
||||||
|
const end = new Date(endTime)
|
||||||
|
const diffMs = end - start
|
||||||
|
if (diffMs <= 0) return ''
|
||||||
|
const durationMinutes = Math.floor(diffMs / (1000 * 60))
|
||||||
|
return `${durationMinutes}分钟`
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取课程标签
|
* 获取课程标签
|
||||||
*/
|
*/
|
||||||
@@ -415,195 +424,6 @@ const getTagType = (course) => {
|
|||||||
return 'default'
|
return 'default'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成单条课程数据
|
|
||||||
*/
|
|
||||||
const generateSingleCourse = (seed, filterParams) => {
|
|
||||||
// 使用确定性随机函数
|
|
||||||
const random = (n) => {
|
|
||||||
const x = Math.sin(seed + n) * 10000
|
|
||||||
return x - Math.floor(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
const prefixes = [
|
|
||||||
'燃脂', '塑形', '增肌', '柔韧', '平衡', '协调', '爆发', '耐力', '核心', '康复',
|
|
||||||
'晨练', '午间', '晚训', '周末', '精品', '强化', '入门', '进阶', '突破', '蜕变',
|
|
||||||
'活力', '轻盈', '力量', '身心', '自由', '挑战', '超越', '燃烧', '唤醒', '放松',
|
|
||||||
'瑜伽', '普拉提', '舞蹈', '搏击', '骑行', '拉伸', '冥想'
|
|
||||||
]
|
|
||||||
const suffixes = [
|
|
||||||
'HIIT训练', '力量训练', '有氧操', '瑜伽课', '普拉提', '舞蹈课', '搏击操',
|
|
||||||
'动感单车', '杠铃课', '私教课', '团课', '课程', '训练营', '特训班', '体验课',
|
|
||||||
'拉伸课', '冥想课', '综合课', '专项课', '公开课'
|
|
||||||
]
|
|
||||||
const levels = ['初级', '中级', '高级']
|
|
||||||
const courseTypes = ['1', '2', '3', '4', '5']
|
|
||||||
const durations = [20, 25, 30, 35, 40, 45, 50, 55, 60, 70, 80, 90]
|
|
||||||
|
|
||||||
// 根据筛选条件调整数据生成
|
|
||||||
let levelValue = levels[Math.floor(random(1) * levels.length)]
|
|
||||||
let courseType = courseTypes[Math.floor(random(2) * courseTypes.length)]
|
|
||||||
let durationValue = durations[Math.floor(random(3) * durations.length)]
|
|
||||||
let prefix = prefixes[Math.floor(random(4) * prefixes.length)]
|
|
||||||
let suffix = suffixes[Math.floor(random(5) * suffixes.length)]
|
|
||||||
|
|
||||||
// 如果筛选了级别,强制匹配
|
|
||||||
if (filterParams.level) {
|
|
||||||
levelValue = filterParams.level
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果筛选了时长,强制匹配
|
|
||||||
if (filterParams.duration) {
|
|
||||||
switch (filterParams.duration) {
|
|
||||||
case 'short':
|
|
||||||
durationValue = Math.floor(random(6) * 11) + 20 // 20-30分钟
|
|
||||||
break
|
|
||||||
case 'medium':
|
|
||||||
durationValue = Math.floor(random(7) * 31) + 31 // 31-60分钟
|
|
||||||
break
|
|
||||||
case 'long':
|
|
||||||
durationValue = Math.floor(random(8) * 41) + 61 // 61-100分钟
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果筛选了快捷类型,调整课程名称和类型
|
|
||||||
if (filterParams.filter && filterParams.filter !== 'all') {
|
|
||||||
switch (filterParams.filter) {
|
|
||||||
case 'yoga':
|
|
||||||
prefix = '瑜伽'
|
|
||||||
suffix = '冥想课'
|
|
||||||
courseType = '1'
|
|
||||||
break
|
|
||||||
case 'strength':
|
|
||||||
prefix = '力量'
|
|
||||||
suffix = '训练课'
|
|
||||||
courseType = '3'
|
|
||||||
break
|
|
||||||
case 'cardio':
|
|
||||||
prefix = '有氧'
|
|
||||||
suffix = '燃脂课'
|
|
||||||
courseType = '2'
|
|
||||||
break
|
|
||||||
case 'dance':
|
|
||||||
prefix = '舞蹈'
|
|
||||||
suffix = '律动课'
|
|
||||||
courseType = '4'
|
|
||||||
break
|
|
||||||
case 'pilates':
|
|
||||||
prefix = '普拉提'
|
|
||||||
suffix = '核心课'
|
|
||||||
courseType = '5'
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果有关键词搜索,调整名称
|
|
||||||
let courseName = `${prefix}${suffix}`
|
|
||||||
if (filterParams.keyword && filterParams.keyword.trim()) {
|
|
||||||
if (courseName.includes(filterParams.keyword) || Math.random() > 0.5) {
|
|
||||||
courseName = `${filterParams.keyword}${courseName}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxMembers = Math.floor(random(9) * 30) + 10
|
|
||||||
const currentMembers = Math.floor(random(10) * maxMembers)
|
|
||||||
const isNew = random(11) > 0.7
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: Math.floor(seed),
|
|
||||||
courseName: courseName,
|
|
||||||
coverImage: `https://picsum.photos/id/${Math.floor(random(12) * 100) + 1}/400/300`,
|
|
||||||
duration: durationValue,
|
|
||||||
level: levelValue,
|
|
||||||
currentMembers: currentMembers,
|
|
||||||
maxMembers: maxMembers,
|
|
||||||
courseType: courseType,
|
|
||||||
status: '1',
|
|
||||||
isNew: isNew
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成随机课程数据(保证每页都有数据)
|
|
||||||
*/
|
|
||||||
const generateMockDataForPage = (page, size, filterParams) => {
|
|
||||||
console.log(`[生成数据] 第 ${page} 页,每页 ${size} 条,筛选条件:`, filterParams)
|
|
||||||
|
|
||||||
const coursesData = []
|
|
||||||
const baseSeed = page * size * 10007 + Date.now()
|
|
||||||
|
|
||||||
// 生成指定数量的课程(确保每页都有数据)
|
|
||||||
for (let i = 0; i < size; i++) {
|
|
||||||
const seed = baseSeed + i * 12345
|
|
||||||
const course = generateSingleCourse(seed, filterParams)
|
|
||||||
coursesData.push(course)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 应用关键词搜索(在已生成数据中过滤)
|
|
||||||
let filteredCourses = [...coursesData]
|
|
||||||
|
|
||||||
if (filterParams.keyword && filterParams.keyword.trim()) {
|
|
||||||
const kw = filterParams.keyword.trim().toLowerCase()
|
|
||||||
filteredCourses = filteredCourses.filter(c =>
|
|
||||||
(c.courseName || '').toLowerCase().includes(kw)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果关键词过滤后没有数据,生成一些匹配关键词的数据
|
|
||||||
if (filteredCourses.length === 0 && filterParams.keyword && filterParams.keyword.trim()) {
|
|
||||||
console.log(`关键词 "${filterParams.keyword}" 无匹配数据,生成匹配数据`)
|
|
||||||
for (let i = 0; i < size; i++) {
|
|
||||||
filteredCourses.push({
|
|
||||||
id: Math.floor(Math.random() * 100000) + page * size + i,
|
|
||||||
courseName: `${filterParams.keyword}课程${i + 1}`,
|
|
||||||
coverImage: `https://picsum.photos/id/${(i % 50) + 1}/400/300`,
|
|
||||||
duration: filterParams.duration === 'short' ? 25 : (filterParams.duration === 'medium' ? 45 : 60),
|
|
||||||
level: filterParams.level || (i % 3 === 0 ? '初级' : (i % 3 === 1 ? '中级' : '高级')),
|
|
||||||
currentMembers: Math.floor(Math.random() * 30) + 1,
|
|
||||||
maxMembers: 30,
|
|
||||||
courseType: '1',
|
|
||||||
status: '1',
|
|
||||||
isNew: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 排序
|
|
||||||
filteredCourses.sort((a, b) => {
|
|
||||||
switch (filterParams.sort) {
|
|
||||||
case 'participants': return b.currentMembers - a.currentMembers
|
|
||||||
case 'startTime': return b.id - a.id
|
|
||||||
case 'duration': return a.duration - b.duration
|
|
||||||
default: return a.id - b.id
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 转换为显示格式
|
|
||||||
const formattedCourses = filteredCourses.map(c => ({
|
|
||||||
id: c.id,
|
|
||||||
image: c.coverImage,
|
|
||||||
tag: getTag(c),
|
|
||||||
tagType: getTagType(c),
|
|
||||||
courseType: getCourseTypeName(c.courseType),
|
|
||||||
name: c.courseName,
|
|
||||||
duration: `${c.duration}分钟`,
|
|
||||||
level: c.level,
|
|
||||||
participants: c.currentMembers,
|
|
||||||
rawData: c
|
|
||||||
}))
|
|
||||||
|
|
||||||
// 永远返回 true,实现真正的无限滚动
|
|
||||||
const hasMoreData = true
|
|
||||||
|
|
||||||
console.log(`[生成数据完成] 第 ${page} 页生成了 ${formattedCourses.length} 条数据, hasMore=${hasMoreData}`)
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: formattedCourses,
|
|
||||||
hasMore: hasMoreData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取课程列表
|
// 获取课程列表
|
||||||
const fetchCourses = async (searchKeyword = '', filter = 'all', page = 0, size = 10, append = false) => {
|
const fetchCourses = async (searchKeyword = '', filter = 'all', page = 0, size = 10, append = false) => {
|
||||||
// 防止重复请求
|
// 防止重复请求
|
||||||
@@ -620,40 +440,39 @@ const fetchCourses = async (searchKeyword = '', filter = 'all', page = 0, size =
|
|||||||
try {
|
try {
|
||||||
let result
|
let result
|
||||||
|
|
||||||
// 构建筛选参数对象
|
// 构建请求参数
|
||||||
const filterParams = {
|
const params = {
|
||||||
keyword: searchKeyword,
|
page: page,
|
||||||
filter: filter,
|
size: size,
|
||||||
level: selectedLevelValue.value,
|
sort: selectedSortValue.value || 'id',
|
||||||
duration: selectedDurationValue.value,
|
order: 'desc',
|
||||||
sort: selectedSortValue.value
|
keyword: searchKeyword
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TEST_MODE) {
|
// 调用真实API
|
||||||
// 模拟网络延迟
|
result = await getGroupCoursePage(params)
|
||||||
const delay = 500 + Math.random() * 500
|
|
||||||
await new Promise(resolve => setTimeout(resolve, delay))
|
|
||||||
|
|
||||||
result = generateMockDataForPage(page, size, filterParams)
|
// 检查是否为最后一页
|
||||||
} else {
|
const isLastPage = result.hasMore === false
|
||||||
// TODO: 替换为真实API调用
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500))
|
|
||||||
result = generateMockDataForPage(page, size, filterParams)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result && result.content && result.content.length > 0) {
|
if (result && result.content && result.content.length > 0) {
|
||||||
// 课程对象直接包含图片URL
|
// 筛选掉已结束和已满员的课程
|
||||||
const processedCourses = result.content.map(course => ({
|
const filteredCourses = result.content.filter(course => {
|
||||||
|
return course.status !== '2' && course.currentMembers < course.maxMembers
|
||||||
|
})
|
||||||
|
|
||||||
|
// 将后端返回的数据转换为前端需要的格式
|
||||||
|
const processedCourses = filteredCourses.map(course => ({
|
||||||
id: course.id,
|
id: course.id,
|
||||||
image: course.image,
|
image: course.coverImage || '',
|
||||||
tag: course.tag,
|
tag: getTag(course),
|
||||||
tagType: course.tagType,
|
tagType: getTagType(course),
|
||||||
courseType: course.courseType,
|
courseType: getCourseTypeName(course.courseType),
|
||||||
name: course.name,
|
name: course.courseName || '未知课程',
|
||||||
duration: course.duration,
|
duration: calculateDuration(course.startTime, course.endTime),
|
||||||
level: course.level,
|
level: course.level || '初级',
|
||||||
participants: course.participants,
|
participants: course.currentMembers || 0,
|
||||||
rawData: course.rawData
|
rawData: course
|
||||||
}))
|
}))
|
||||||
|
|
||||||
if (append) {
|
if (append) {
|
||||||
@@ -672,13 +491,28 @@ const fetchCourses = async (searchKeyword = '', filter = 'all', page = 0, size =
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 更新是否有更多数据
|
// 更新是否有更多数据
|
||||||
hasMore.value = result.hasMore !== false
|
hasMore.value = !isLastPage && processedCourses.length > 0
|
||||||
|
|
||||||
|
// 如果是最后一页且没有更多数据,显示提示
|
||||||
|
if (isLastPage) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '已经到底了',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 没有数据时,确保不显示骨架屏
|
// 没有数据时,确保不显示骨架屏
|
||||||
skeletonCount.value = 0
|
skeletonCount.value = 0
|
||||||
if (!append && courses.value.length === 0) {
|
|
||||||
// 没有数据时,确保显示空状态,但 hasMore 设为 false 防止无限加载
|
|
||||||
hasMore.value = false
|
hasMore.value = false
|
||||||
|
|
||||||
|
// 如果是最后一页,显示提示
|
||||||
|
if (isLastPage && courses.value.length > 0) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '已经到底了',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -727,14 +561,10 @@ const loadMore = () => {
|
|||||||
const nextPage = currentPage.value + 1
|
const nextPage = currentPage.value + 1
|
||||||
currentPage.value = nextPage
|
currentPage.value = nextPage
|
||||||
|
|
||||||
// 加载更多时添加骨架屏
|
|
||||||
skeletonCount.value += pageSize
|
|
||||||
|
|
||||||
fetchCourses(keyword.value, activeFilter.value, nextPage, pageSize, true)
|
fetchCourses(keyword.value, activeFilter.value, nextPage, pageSize, true)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error('加载更多失败:', err)
|
console.error('加载更多失败:', err)
|
||||||
loadingMore.value = false
|
loadingMore.value = false
|
||||||
skeletonCount.value = Math.max(0, skeletonCount.value - pageSize)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -771,7 +601,7 @@ const toggleExpand = () => {
|
|||||||
// 课程点击
|
// 课程点击
|
||||||
const handleCourseClick = (course) => {
|
const handleCourseClick = (course) => {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/pages/course/detail?id=${course.id}`
|
url: `/pages/memberInfo/courseDetail?id=${course.id}`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<template>
|
||||||
|
<view class="webview-container">
|
||||||
|
<web-view :src="url" @message="handleMessage"></web-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const url = ref('')
|
||||||
|
|
||||||
|
onLoad((options) => {
|
||||||
|
if (options.url) {
|
||||||
|
url.value = decodeURIComponent(options.url)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleMessage(e) {
|
||||||
|
console.log('[webview] 收到消息:', e.detail)
|
||||||
|
// 处理支付回调消息
|
||||||
|
const data = e.detail.data
|
||||||
|
if (data && data.payResult) {
|
||||||
|
if (data.payResult === 'success') {
|
||||||
|
// 支付成功
|
||||||
|
uni.showToast({ title: '支付成功', icon: 'success' })
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.navigateBack()
|
||||||
|
}, 1500)
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: '支付失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.webview-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -191,16 +191,14 @@ function convertPage(pageName, config) {
|
|||||||
|
|
||||||
const extraMethods = (config.extraMethods || '').replace(/^\s*,\s*/, '');
|
const extraMethods = (config.extraMethods || '').replace(/^\s*,\s*/, '');
|
||||||
const extraDataBlock = (config.extraData || '').replace(/^\s*,\s*/, '');
|
const extraDataBlock = (config.extraData || '').replace(/^\s*,\s*/, '');
|
||||||
const dataBody = pageName === 'userInfo'
|
const dataBody = extraDataBlock;
|
||||||
? '...userInfoMock'
|
|
||||||
: extraDataBlock;
|
|
||||||
|
|
||||||
const vueOutput = `<template>
|
const vueOutput = `<template>
|
||||||
${template}
|
${template}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { statusBarTimeMixin, subPageMixin } from '@/common/memberInfo/mixins.js'${pageName === 'userInfo' ? "\nimport { userInfoMock } from '@/common/memberInfo/mockData.js'" : ''}
|
import { statusBarTimeMixin, subPageMixin } from '@/common/memberInfo/mixins.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [statusBarTimeMixin, subPageMixin],
|
mixins: [statusBarTimeMixin, subPageMixin],
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 667 KiB |
@@ -1,4 +1,5 @@
|
|||||||
const BASE_URL = '/api'
|
// const BASE_URL = '/api'
|
||||||
|
const BASE_URL = 'http://192.168.43.89:8084/api'
|
||||||
|
|
||||||
// 缓存相关常量
|
// 缓存相关常量
|
||||||
const CACHE_PREFIX = 'API_CACHE_'
|
const CACHE_PREFIX = 'API_CACHE_'
|
||||||
@@ -171,7 +172,7 @@ export const request = (options) => {
|
|||||||
|
|
||||||
console.log(`[API] 请求开始: ${method} ${BASE_URL + url}`)
|
console.log(`[API] 请求开始: ${method} ${BASE_URL + url}`)
|
||||||
console.log(`[API] 请求参数:`, data)
|
console.log(`[API] 请求参数:`, data)
|
||||||
console.log(`[API] 请求头:`, requestHeader)
|
console.log(`[API] needToken:`, needToken, 'cache:', cache)
|
||||||
|
|
||||||
uni.request({
|
uni.request({
|
||||||
url: BASE_URL + url,
|
url: BASE_URL + url,
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ import path from 'path'
|
|||||||
// 用来匹配地址,解决跨域问题
|
// 用来匹配地址,解决跨域问题
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [uni()],
|
plugins: [uni()],
|
||||||
|
// 配置源目录为根目录,不使用src
|
||||||
|
uni: {
|
||||||
|
inputDir: path.resolve(__dirname, '.')
|
||||||
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': path.resolve(__dirname, '.')
|
'@': path.resolve(__dirname, '.')
|
||||||
@@ -13,7 +17,7 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'https://www.gymmanage.xyz',
|
target: 'http://192.168.43.89:8084',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: true,
|
secure: true,
|
||||||
bypass: function(req, res, proxyOptions) {
|
bypass: function(req, res, proxyOptions) {
|
||||||
|
|||||||
Reference in New Issue
Block a user