Compare commits
25 Commits
dev
...
feature/uni-app
| Author | SHA1 | Date | |
|---|---|---|---|
| 95c2ded69e | |||
| 1e093a0688 | |||
| 33d1140fbf | |||
| 51bdf15613 | |||
| be7eabdbb1 | |||
| 823d626440 | |||
| 207a248b01 | |||
| c4de871977 | |||
| f0dda998a8 | |||
| 8c96e2099c | |||
| 1922d0fb1e | |||
| 7350293d0e | |||
| 8cf3c9ccee | |||
| 1fc020ab00 | |||
| 4981a240fa | |||
| 349b7ae03b | |||
| 2357dcfc67 | |||
| 14a0fe8d4f | |||
| e304c1b724 | |||
| f0d97e58d1 | |||
| 2b58b672d5 | |||
| 8bedac5ab5 | |||
| 8e7c8f52f6 | |||
| b4710b6397 | |||
| daff741c65 |
+1
@@ -56,6 +56,7 @@ public class SecurityConfig {
|
|||||||
.pathMatchers("/api/admin/member/**").permitAll()
|
.pathMatchers("/api/admin/member/**").permitAll()
|
||||||
.pathMatchers("/api/member-cards/**").permitAll()
|
.pathMatchers("/api/member-cards/**").permitAll()
|
||||||
.pathMatchers("/api/member-card-records/**").permitAll()
|
.pathMatchers("/api/member-card-records/**").permitAll()
|
||||||
|
// .pathMatchers("/**").permitAll()
|
||||||
.pathMatchers("/api/member-card-transactions/**").permitAll();
|
.pathMatchers("/api/member-card-transactions/**").permitAll();
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
unpackage/
|
||||||
|
.hbuilderx/
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<template>
|
||||||
|
<view>
|
||||||
|
<GlobalLoading />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import GlobalLoading from '@/components/global/GlobalLoading.vue'
|
||||||
|
export default {
|
||||||
|
onLaunch: function() {
|
||||||
|
console.log('App Launch')
|
||||||
|
this.preloadTabData()
|
||||||
|
},
|
||||||
|
onShow: function() {
|
||||||
|
console.log('App Show')
|
||||||
|
},
|
||||||
|
onHide: function() {
|
||||||
|
console.log('App Hide')
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 预加载所有 Tab 页面的核心数据
|
||||||
|
preloadTabData() {
|
||||||
|
// 延迟执行,不阻塞首屏
|
||||||
|
setTimeout(() => {
|
||||||
|
// 预加载课程数据
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
// 小程序端预请求数据
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// 预加载训练数据
|
||||||
|
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import 'common/style/base.css';
|
||||||
|
|
||||||
|
/* 全局骨架屏样式 */
|
||||||
|
.skeleton {
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: skeleton-loading 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes skeleton-loading {
|
||||||
|
0% { background-position: 200% 0; }
|
||||||
|
100% { background-position: -200% 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面切换动画 */
|
||||||
|
.page-enter-active,
|
||||||
|
.page-leave-active {
|
||||||
|
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(30rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-30rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-container {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
max-width: 430px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: var(--bg-light);
|
||||||
|
position: relative;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import request from "@/utils/request.js"
|
||||||
|
|
||||||
|
export function login(params) {
|
||||||
|
return request.post('/member/auth/miniapp/login', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logout() {
|
||||||
|
return request.post('/member/auth/logout')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getQRCode(options = { cache: true, cacheTime: 5 * 60 * 1000 }) {
|
||||||
|
return request.get('/checkIn/qrcode', {}, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkIn(params) {
|
||||||
|
return request.post('/checkIn/scan', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUserInfo(options = { cache: true, cacheTime: 30 * 60 * 1000 }) {
|
||||||
|
return request.get('/member/info', {}, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateUserInfo(params) {
|
||||||
|
return request.put('/member/info', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRecommendCourses(options = { cache: true, cacheTime: 10 * 60 * 1000 }) {
|
||||||
|
return request.get('/course/recommend', {}, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCourseDetail(id, options = { cache: true, cacheTime: 15 * 60 * 1000 }) {
|
||||||
|
return request.get(`/course/${id}`, {}, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getGroupCoursePage(params = {}, options = { cache: true, cacheTime: 5 * 60 * 1000 }) {
|
||||||
|
const { page = 0, size = 10, sort = 'id', order = 'asc', keyword } = params
|
||||||
|
return request.post('/groupCourse/page', { page, size, sort, order, keyword }, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
getQRCode,
|
||||||
|
checkIn,
|
||||||
|
getUserInfo,
|
||||||
|
updateUserInfo,
|
||||||
|
getRecommendCourses,
|
||||||
|
getCourseDetail,
|
||||||
|
getGroupCoursePage
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
// common/constants/routes.js
|
||||||
|
|
||||||
|
/** 与 pages.json 保持一致 */
|
||||||
|
export const PAGE = {
|
||||||
|
INDEX: '/pages/index/index',
|
||||||
|
COURSE: '/pages/course/index',
|
||||||
|
TRAIN: '/pages/train/index',
|
||||||
|
DISCOVER: '/pages/discover/index',
|
||||||
|
MEMBER: '/pages/memberInfo/memberInfo',
|
||||||
|
BOOKING: '/pages/memberInfo/booking',
|
||||||
|
MEMBER_CARD: '/pages/memberInfo/memberCard',
|
||||||
|
USER_INFO: '/pages/memberInfo/userInfo',
|
||||||
|
BODY_TEST_HOME: '/pages/memberInfo/bodyTestHome',
|
||||||
|
BODY_TEST_CONNECT: '/pages/memberInfo/bodyTestConnect',
|
||||||
|
BODY_TEST_MEASURING: '/pages/memberInfo/bodyTestMeasuring',
|
||||||
|
BODY_TEST_REPORT: '/pages/memberInfo/bodyTestReport',
|
||||||
|
BODY_TEST_HISTORY: '/pages/memberInfo/bodyTestHistory',
|
||||||
|
BODY_TEST_COMPARE: '/pages/memberInfo/bodyTestCompare',
|
||||||
|
BODY_TEST_SETTINGS: '/pages/memberInfo/bodyTestSettings',
|
||||||
|
BODY_TEST_TREND: '/pages/memberInfo/bodyTestTrend',
|
||||||
|
COURSE_LIST: '/pages/groupCourse/list',
|
||||||
|
COURSE_DETAIL: '/pages/memberInfo/courseDetail',
|
||||||
|
COUPON_DETAIL: '/pages/memberInfo/couponDetail',
|
||||||
|
COUPON_CENTER: '/pages/memberInfo/couponCenter',
|
||||||
|
POINTS_MALL: '/pages/memberInfo/pointsMall',
|
||||||
|
POINTS_HISTORY: '/pages/memberInfo/pointsHistory',
|
||||||
|
ONLINE_COURSE: '/pages/memberInfo/onlineCourseDetail',
|
||||||
|
COURSE_EVALUATE: '/pages/memberInfo/courseEvaluate',
|
||||||
|
TRAIN_SESSION: '/pages/memberInfo/trainSessionDetail',
|
||||||
|
TRAIN_REPORT: '/pages/memberInfo/trainReport',
|
||||||
|
COUPONS: '/pages/memberInfo/coupons',
|
||||||
|
POINTS: '/pages/memberInfo/points',
|
||||||
|
REFERRAL: '/pages/memberInfo/referral',
|
||||||
|
MY_COURSES: '/pages/memberInfo/myCourses',
|
||||||
|
CHECK_IN_HISTORY: '/pages/memberInfo/checkInHistory'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 底部 TabBar 页面路径,顺序与 TabBar.vue 一致 */
|
||||||
|
export const TAB_ROUTES = [
|
||||||
|
PAGE.INDEX,
|
||||||
|
PAGE.COURSE,
|
||||||
|
PAGE.TRAIN,
|
||||||
|
PAGE.DISCOVER,
|
||||||
|
PAGE.MEMBER
|
||||||
|
]
|
||||||
|
|
||||||
|
const TAB_PAGES = new Set(TAB_ROUTES)
|
||||||
|
|
||||||
|
/** 防止 Tab 连点触发并发路由 */
|
||||||
|
let tabNavigating = false
|
||||||
|
|
||||||
|
function normalizePath(url) {
|
||||||
|
if (!url) return ''
|
||||||
|
const path = url.split('?')[0]
|
||||||
|
return path.startsWith('/') ? path : `/${path}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTabIndexByRoute(route) {
|
||||||
|
const path = normalizePath(route)
|
||||||
|
const idx = TAB_ROUTES.indexOf(path)
|
||||||
|
return idx >= 0 ? idx : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCurrentRoutePath() {
|
||||||
|
const pages = getCurrentPages()
|
||||||
|
if (!pages.length) return PAGE.INDEX
|
||||||
|
const page = pages[pages.length - 1]
|
||||||
|
const route = page.route || page.$page?.fullPath || ''
|
||||||
|
return normalizePath(route ? `/${route}` : PAGE.INDEX)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转到普通页面(非 TabBar 页面)
|
||||||
|
* 使用 navigateTo,保留页面栈,可以正常返回
|
||||||
|
*/
|
||||||
|
export function navigateToPage(url) {
|
||||||
|
uni.showLoading({ title: '加载中...', mask: true })
|
||||||
|
const path = normalizePath(url)
|
||||||
|
|
||||||
|
// ✅ 如果目标是 TabBar 页面,不应该使用 navigateTo
|
||||||
|
// 这种情况应该使用 switchToTabPage(会清空页面栈)
|
||||||
|
if (TAB_PAGES.has(path)) {
|
||||||
|
console.warn('[navigateToPage] 不应该用 navigateTo 跳转 TabBar 页面,请使用 switchToTabPage')
|
||||||
|
switchToTabPage(path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[navigateToPage] 跳转到:', url)
|
||||||
|
|
||||||
|
uni.navigateTo({
|
||||||
|
url,
|
||||||
|
fail: (err) => {
|
||||||
|
console.error('[navigateTo]', url, err)
|
||||||
|
// 页面栈满时降级使用 redirectTo
|
||||||
|
if (err.errMsg && err.errMsg.includes('limit')) {
|
||||||
|
uni.redirectTo({ url })
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: '页面跳转失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
success: () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.hideLoading()
|
||||||
|
},3000)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转到 TabBar 页面(清空页面栈)
|
||||||
|
* 用于从任何页面跳转到首页/课程/训练等 TabBar 页面
|
||||||
|
*/
|
||||||
|
export function switchToTabPage(url) {
|
||||||
|
const path = normalizePath(url)
|
||||||
|
if (!TAB_PAGES.has(path)) {
|
||||||
|
console.warn('[switchToTabPage] 目标不是 TabBar 页面:', path)
|
||||||
|
navigateToPage(url)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getCurrentRoutePath() === path || tabNavigating) return
|
||||||
|
|
||||||
|
console.log('[switchToTabPage] 跳转到 TabBar:', path)
|
||||||
|
|
||||||
|
tabNavigating = true
|
||||||
|
uni.switchTab({ // ✅ 改用 switchTab,而不是 reLaunch
|
||||||
|
url: path,
|
||||||
|
complete: () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
tabNavigating = false
|
||||||
|
}, 320)
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
console.error('[switchTab]', path, err)
|
||||||
|
// 降级使用 reLaunch
|
||||||
|
uni.reLaunch({
|
||||||
|
url: path,
|
||||||
|
complete: () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
tabNavigating = false
|
||||||
|
}, 320)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置到 TabBar 页面(清空所有历史)
|
||||||
|
* 用于退出登录、强制跳转等场景
|
||||||
|
*/
|
||||||
|
export function reLaunchToTabPage(url) {
|
||||||
|
const path = normalizePath(url)
|
||||||
|
console.log('[reLaunchToTabPage] 重置到:', path)
|
||||||
|
|
||||||
|
uni.reLaunch({
|
||||||
|
url: path,
|
||||||
|
fail: (err) => {
|
||||||
|
console.error('[reLaunch]', path, err)
|
||||||
|
uni.switchTab({ url: path })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回上一页,如果没有上一页则跳转到指定 TabBar 页面
|
||||||
|
*/
|
||||||
|
export function goBackOrTab(fallbackUrl = PAGE.MEMBER) {
|
||||||
|
const pages = getCurrentPages()
|
||||||
|
if (pages.length > 1) {
|
||||||
|
uni.navigateBack({ delta: 1 })
|
||||||
|
} else {
|
||||||
|
switchToTabPage(fallbackUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 子页面返回个人中心
|
||||||
|
*/
|
||||||
|
export function backToMemberCenter() {
|
||||||
|
goBackOrTab(PAGE.MEMBER)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 子页面返回指定 TabBar 页面
|
||||||
|
*/
|
||||||
|
export function backToTab(tabUrl) {
|
||||||
|
goBackOrTab(tabUrl)
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
const COLORS = {
|
||||||
|
primary: '#0B2B4B',
|
||||||
|
accent: '#FF6B35',
|
||||||
|
accentLight: 'rgba(255, 107, 53, 0.25)',
|
||||||
|
grid: '#E9EDF2',
|
||||||
|
text: '#5E6F8D',
|
||||||
|
fill: 'rgba(26, 74, 111, 0.35)',
|
||||||
|
line: '#1A4A6F'
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupCanvas(canvas, width, height, dpr) {
|
||||||
|
canvas.width = width * dpr
|
||||||
|
canvas.height = height * dpr
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
ctx.scale(dpr, dpr)
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 绘制雷达图 */
|
||||||
|
export function drawRadarChart(canvas, opts = {}) {
|
||||||
|
if (!canvas) return
|
||||||
|
const {
|
||||||
|
width = 280,
|
||||||
|
height = 240,
|
||||||
|
labels = [],
|
||||||
|
values = [],
|
||||||
|
dpr = 1
|
||||||
|
} = opts
|
||||||
|
const ctx = setupCanvas(canvas, width, height, dpr)
|
||||||
|
ctx.clearRect(0, 0, width, height)
|
||||||
|
|
||||||
|
const cx = width / 2
|
||||||
|
const cy = height / 2 + 8
|
||||||
|
const radius = Math.min(width, height) * 0.32
|
||||||
|
const count = labels.length || 6
|
||||||
|
const angleStep = (Math.PI * 2) / count
|
||||||
|
|
||||||
|
for (let level = 1; level <= 4; level += 1) {
|
||||||
|
ctx.beginPath()
|
||||||
|
const r = (radius * level) / 4
|
||||||
|
for (let i = 0; i <= count; i += 1) {
|
||||||
|
const angle = -Math.PI / 2 + i * angleStep
|
||||||
|
const x = cx + r * Math.cos(angle)
|
||||||
|
const y = cy + r * Math.sin(angle)
|
||||||
|
if (i === 0) ctx.moveTo(x, y)
|
||||||
|
else ctx.lineTo(x, y)
|
||||||
|
}
|
||||||
|
ctx.strokeStyle = COLORS.grid
|
||||||
|
ctx.lineWidth = 1
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i += 1) {
|
||||||
|
const angle = -Math.PI / 2 + i * angleStep
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(cx, cy)
|
||||||
|
ctx.lineTo(cx + radius * Math.cos(angle), cy + radius * Math.sin(angle))
|
||||||
|
ctx.strokeStyle = COLORS.grid
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.beginPath()
|
||||||
|
values.forEach((val, i) => {
|
||||||
|
const ratio = Math.min(1, Math.max(0, val / 100))
|
||||||
|
const angle = -Math.PI / 2 + i * angleStep
|
||||||
|
const x = cx + radius * ratio * Math.cos(angle)
|
||||||
|
const y = cy + radius * ratio * Math.sin(angle)
|
||||||
|
if (i === 0) ctx.moveTo(x, y)
|
||||||
|
else ctx.lineTo(x, y)
|
||||||
|
})
|
||||||
|
ctx.closePath()
|
||||||
|
ctx.fillStyle = COLORS.fill
|
||||||
|
ctx.fill()
|
||||||
|
ctx.strokeStyle = COLORS.accent
|
||||||
|
ctx.lineWidth = 2
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
ctx.font = '11px sans-serif'
|
||||||
|
ctx.fillStyle = COLORS.text
|
||||||
|
ctx.textAlign = 'center'
|
||||||
|
labels.forEach((label, i) => {
|
||||||
|
const angle = -Math.PI / 2 + i * angleStep
|
||||||
|
const x = cx + (radius + 18) * Math.cos(angle)
|
||||||
|
const y = cy + (radius + 18) * Math.sin(angle) + 4
|
||||||
|
ctx.fillText(label, x, y)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 绘制折线趋势图 */
|
||||||
|
export function drawTrendChart(canvas, opts = {}) {
|
||||||
|
if (!canvas) return
|
||||||
|
const {
|
||||||
|
width = 300,
|
||||||
|
height = 160,
|
||||||
|
points = [],
|
||||||
|
dpr = 1,
|
||||||
|
unit = ''
|
||||||
|
} = opts
|
||||||
|
const ctx = setupCanvas(canvas, width, height, dpr)
|
||||||
|
ctx.clearRect(0, 0, width, height)
|
||||||
|
|
||||||
|
if (!points.length) {
|
||||||
|
ctx.fillStyle = COLORS.text
|
||||||
|
ctx.font = '13px sans-serif'
|
||||||
|
ctx.textAlign = 'center'
|
||||||
|
ctx.fillText('暂无趋势数据', width / 2, height / 2)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const pad = { top: 16, right: 12, bottom: 28, left: 12 }
|
||||||
|
const chartW = width - pad.left - pad.right
|
||||||
|
const chartH = height - pad.top - pad.bottom
|
||||||
|
const values = points.map((p) => p.value)
|
||||||
|
const min = Math.min(...values) * 0.95
|
||||||
|
const max = Math.max(...values) * 1.05
|
||||||
|
const range = max - min || 1
|
||||||
|
|
||||||
|
ctx.strokeStyle = COLORS.grid
|
||||||
|
ctx.lineWidth = 1
|
||||||
|
for (let i = 0; i <= 3; i += 1) {
|
||||||
|
const y = pad.top + (chartH * i) / 3
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(pad.left, y)
|
||||||
|
ctx.lineTo(width - pad.right, y)
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
const coords = points.map((p, i) => ({
|
||||||
|
x: pad.left + (chartW * i) / Math.max(1, points.length - 1),
|
||||||
|
y: pad.top + chartH - ((p.value - min) / range) * chartH
|
||||||
|
}))
|
||||||
|
|
||||||
|
ctx.beginPath()
|
||||||
|
coords.forEach((pt, i) => {
|
||||||
|
if (i === 0) ctx.moveTo(pt.x, pt.y)
|
||||||
|
else ctx.lineTo(pt.x, pt.y)
|
||||||
|
})
|
||||||
|
ctx.strokeStyle = COLORS.line
|
||||||
|
ctx.lineWidth = 2
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
coords.forEach((pt, i) => {
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(pt.x, pt.y, 4, 0, Math.PI * 2)
|
||||||
|
ctx.fillStyle = COLORS.accent
|
||||||
|
ctx.fill()
|
||||||
|
ctx.strokeStyle = '#fff'
|
||||||
|
ctx.lineWidth = 1.5
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
ctx.fillStyle = COLORS.text
|
||||||
|
ctx.font = '10px sans-serif'
|
||||||
|
ctx.textAlign = 'center'
|
||||||
|
ctx.fillText(points[i].label, pt.x, height - 8)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (unit) {
|
||||||
|
ctx.fillStyle = COLORS.text
|
||||||
|
ctx.font = '10px sans-serif'
|
||||||
|
ctx.textAlign = 'left'
|
||||||
|
ctx.fillText(unit, pad.left, pad.top - 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
import { bodyTestMock } from './mockData.js'
|
||||||
|
|
||||||
|
function clone(value) {
|
||||||
|
return JSON.parse(JSON.stringify(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRecordTime(date) {
|
||||||
|
const y = date.getFullYear()
|
||||||
|
const m = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const d = String(date.getDate()).padStart(2, '0')
|
||||||
|
const h = String(date.getHours()).padStart(2, '0')
|
||||||
|
const min = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
return `${y}-${m}-${d} ${h}:${min}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatIsoDate(date) {
|
||||||
|
const y = date.getFullYear()
|
||||||
|
const m = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const d = String(date.getDate()).padStart(2, '0')
|
||||||
|
return `${y}-${m}-${d}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(date) {
|
||||||
|
const h = String(date.getHours()).padStart(2, '0')
|
||||||
|
const min = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
return `${h}:${min}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDefaultBodyTestState() {
|
||||||
|
return {
|
||||||
|
settings: { ...bodyTestMock.settings },
|
||||||
|
device: { ...bodyTestMock.device },
|
||||||
|
records: clone(bodyTestMock.records)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mergeBodyTestState(saved) {
|
||||||
|
const defaults = getDefaultBodyTestState()
|
||||||
|
if (!saved) return defaults
|
||||||
|
return {
|
||||||
|
settings: { ...defaults.settings, ...(saved.settings || {}) },
|
||||||
|
device: { ...defaults.device, ...(saved.device || {}) },
|
||||||
|
records: saved.records?.length ? saved.records : defaults.records
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLatestBodyTestRecord(store) {
|
||||||
|
const records = store.bodyTest?.records || []
|
||||||
|
return records.length ? { ...records[0] } : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBodyTestRecordById(store, id) {
|
||||||
|
const numId = Number(id)
|
||||||
|
const record = (store.bodyTest?.records || []).find((item) => item.id === numId)
|
||||||
|
return record ? { ...record } : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBodyTestHistory(store, year) {
|
||||||
|
let list = (store.bodyTest?.records || []).map((item) => ({ ...item }))
|
||||||
|
if (year && year !== 'all') {
|
||||||
|
list = list.filter((r) => r.date.startsWith(String(year)))
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBodyTestChangeBadge(record, previous) {
|
||||||
|
if (!previous?.metrics || !record?.metrics) return null
|
||||||
|
const diff = Math.round((record.metrics.bodyFat - previous.metrics.bodyFat) * 10) / 10
|
||||||
|
if (diff === 0) return null
|
||||||
|
const sign = diff > 0 ? '+' : ''
|
||||||
|
return { key: 'bodyFat', text: `体脂率${sign}${diff}%`, good: diff < 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBodyTestYears(store) {
|
||||||
|
const years = new Set((store.bodyTest?.records || []).map((r) => r.date.slice(0, 4)))
|
||||||
|
return ['all', ...Array.from(years).sort().reverse()]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeGrade(score) {
|
||||||
|
if (score >= 90) return { grade: 'A', gradeLabel: '优秀' }
|
||||||
|
if (score >= 80) return { grade: 'B+', gradeLabel: '良好' }
|
||||||
|
if (score >= 70) return { grade: 'B', gradeLabel: '中等' }
|
||||||
|
if (score >= 60) return { grade: 'C', gradeLabel: '一般' }
|
||||||
|
return { grade: 'D', gradeLabel: '需改善' }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeScore(metrics) {
|
||||||
|
const bmi = metrics.bmi || 22
|
||||||
|
const bodyFat = metrics.bodyFat || 25
|
||||||
|
const muscle = metrics.muscleMass || 22
|
||||||
|
const bmiScore = bmi >= 18.5 && bmi <= 24 ? 90 : bmi >= 17 && bmi <= 27 ? 75 : 60
|
||||||
|
const fatScore = bodyFat <= 22 ? 92 : bodyFat <= 26 ? 80 : bodyFat <= 30 ? 68 : 55
|
||||||
|
const muscleScore = muscle >= 22 ? 88 : muscle >= 20 ? 76 : 62
|
||||||
|
return Math.round((bmiScore + fatScore + muscleScore) / 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeChanges(current, previous) {
|
||||||
|
if (!previous?.metrics) return {}
|
||||||
|
const keys = ['weight', 'bmi', 'bodyFat', 'muscleMass', 'visceralFat', 'bmr', 'bodyWater', 'boneMass']
|
||||||
|
const changes = {}
|
||||||
|
keys.forEach((key) => {
|
||||||
|
const cur = Number(current.metrics[key])
|
||||||
|
const prev = Number(previous.metrics[key])
|
||||||
|
if (Number.isFinite(cur) && Number.isFinite(prev)) {
|
||||||
|
const diff = Math.round((cur - prev) * 10) / 10
|
||||||
|
changes[key] = diff
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return changes
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatChangeValue(key, diff, unitSystem = 'metric') {
|
||||||
|
if (diff === undefined || diff === null) return '--'
|
||||||
|
const sign = diff > 0 ? '+' : ''
|
||||||
|
const units = {
|
||||||
|
weight: unitSystem === 'metric' ? 'kg' : 'lb',
|
||||||
|
bodyFat: '%',
|
||||||
|
muscleMass: 'kg',
|
||||||
|
bmi: '',
|
||||||
|
visceralFat: '级',
|
||||||
|
bmr: 'kcal',
|
||||||
|
bodyWater: '%',
|
||||||
|
boneMass: 'kg'
|
||||||
|
}
|
||||||
|
const unit = units[key] || ''
|
||||||
|
return `${sign}${diff}${unit}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildBodyReportSummary(record, previous) {
|
||||||
|
if (!record) {
|
||||||
|
return {
|
||||||
|
date: '--',
|
||||||
|
weight: '--',
|
||||||
|
bmi: '--',
|
||||||
|
bodyFat: '--',
|
||||||
|
bmr: '--',
|
||||||
|
status: '暂无数据',
|
||||||
|
change: '--'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const changes = computeChanges(record, previous)
|
||||||
|
const weightChange = changes.weight
|
||||||
|
let changeText = '--'
|
||||||
|
if (weightChange !== undefined) {
|
||||||
|
const sign = weightChange > 0 ? '+' : ''
|
||||||
|
changeText = `${sign}${weightChange}kg`
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
date: record.date,
|
||||||
|
weight: String(record.metrics.weight),
|
||||||
|
bmi: String(record.metrics.bmi),
|
||||||
|
bodyFat: `${record.metrics.bodyFat}%`,
|
||||||
|
bmr: String(record.metrics.bmr),
|
||||||
|
status: record.status,
|
||||||
|
change: changeText,
|
||||||
|
recordId: record.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBodyTestTrendData(store, metricKey, limit = 6) {
|
||||||
|
const records = [...(store.bodyTest?.records || [])].reverse().slice(-limit)
|
||||||
|
return records.map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
date: item.date,
|
||||||
|
label: item.date.slice(5),
|
||||||
|
value: Number(item.metrics[metricKey]) || 0
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCompareData(store, idA, idB) {
|
||||||
|
const a = getBodyTestRecordById(store, idA)
|
||||||
|
const b = getBodyTestRecordById(store, idB)
|
||||||
|
if (!a || !b) return null
|
||||||
|
const keys = ['weight', 'bmi', 'bodyFat', 'muscleMass', 'visceralFat', 'bmr']
|
||||||
|
const metrics = keys.map((key) => ({
|
||||||
|
key,
|
||||||
|
label: bodyTestMock.metricDefs.find((m) => m.key === key)?.label || key,
|
||||||
|
valueA: a.metrics[key],
|
||||||
|
valueB: b.metrics[key],
|
||||||
|
diff: Math.round((a.metrics[key] - b.metrics[key]) * 10) / 10
|
||||||
|
}))
|
||||||
|
return { recordA: a, recordB: b, metrics }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRecommendedCourses(record) {
|
||||||
|
const ids = record?.recommendedCourseIds || []
|
||||||
|
return bodyTestMock.recommendedCourses.filter((c) => ids.includes(c.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateBodyTestSettings(store, patch) {
|
||||||
|
store.bodyTest.settings = { ...store.bodyTest.settings, ...patch }
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
|
||||||
|
export function connectBodyTestDevice(store) {
|
||||||
|
store.bodyTest.device = {
|
||||||
|
...store.bodyTest.device,
|
||||||
|
connected: true,
|
||||||
|
battery: Math.min(100, (store.bodyTest.device.battery || 80) + Math.floor(Math.random() * 5)),
|
||||||
|
lastConnected: formatRecordTime(new Date())
|
||||||
|
}
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
|
||||||
|
export function disconnectBodyTestDevice(store) {
|
||||||
|
store.bodyTest.device = { ...store.bodyTest.device, connected: false }
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextRecordId(records) {
|
||||||
|
return (records || []).reduce((max, item) => Math.max(max, item.id || 0), 0) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 模拟一次完整体测并写入记录 */
|
||||||
|
export function saveSimulatedBodyTestRecord(store, finalMetrics) {
|
||||||
|
const now = new Date()
|
||||||
|
const previous = getLatestBodyTestRecord(store)
|
||||||
|
const metrics = { ...finalMetrics }
|
||||||
|
const heightCm = Number(store.profile?.height) || 165
|
||||||
|
const heightM = heightCm / 100
|
||||||
|
metrics.bmi = Math.round((metrics.weight / (heightM * heightM)) * 10) / 10
|
||||||
|
|
||||||
|
const score = computeScore(metrics)
|
||||||
|
const { grade, gradeLabel } = computeGrade(score)
|
||||||
|
const status = score >= 80 ? '比较健康' : score >= 70 ? '需关注' : '建议改善'
|
||||||
|
|
||||||
|
const radar = {
|
||||||
|
weight: Math.min(95, Math.round(score * 0.9 + Math.random() * 5)),
|
||||||
|
bodyFat: Math.min(95, Math.round(100 - metrics.bodyFat * 2.5)),
|
||||||
|
muscle: Math.min(95, Math.round(metrics.muscleMass * 3.2)),
|
||||||
|
bone: Math.min(95, Math.round(metrics.boneMass * 32)),
|
||||||
|
water: Math.min(95, Math.round(metrics.bodyWater * 1.4)),
|
||||||
|
bmr: Math.min(95, Math.round(metrics.bmr / 16))
|
||||||
|
}
|
||||||
|
|
||||||
|
const record = {
|
||||||
|
id: nextRecordId(store.bodyTest.records),
|
||||||
|
date: formatIsoDate(now),
|
||||||
|
time: formatTime(now),
|
||||||
|
score,
|
||||||
|
grade,
|
||||||
|
gradeLabel,
|
||||||
|
status,
|
||||||
|
metrics,
|
||||||
|
radar,
|
||||||
|
bodySegments: clone(bodyTestMock.records[0].bodySegments),
|
||||||
|
advice: clone(bodyTestMock.records[0].advice),
|
||||||
|
recommendedCourseIds: [1, 2]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previous) {
|
||||||
|
record.changes = computeChanges(record, previous)
|
||||||
|
}
|
||||||
|
|
||||||
|
store.bodyTest.records.unshift(record)
|
||||||
|
store.bodyReport = buildBodyReportSummary(record, previous)
|
||||||
|
return record
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 测量过程实时数据插值 */
|
||||||
|
export function interpolateMeasuringMetrics(progress, profile) {
|
||||||
|
const baseWeight = Number(profile?.weight) || 64
|
||||||
|
const target = {
|
||||||
|
weight: baseWeight - 0.3 + Math.random() * 0.2,
|
||||||
|
bodyFat: 24.5 + Math.random() * 0.8,
|
||||||
|
muscleMass: 22.4 + Math.random() * 0.3,
|
||||||
|
visceralFat: 6,
|
||||||
|
bmr: 1380 + Math.floor(Math.random() * 20),
|
||||||
|
bodyWater: 52.5 + Math.random(),
|
||||||
|
boneMass: 2.4,
|
||||||
|
protein: 16.2
|
||||||
|
}
|
||||||
|
const ratio = Math.min(1, progress / 100)
|
||||||
|
return {
|
||||||
|
weight: Math.round((baseWeight + (target.weight - baseWeight) * ratio) * 10) / 10,
|
||||||
|
bodyFat: Math.round((26 + (target.bodyFat - 26) * ratio) * 10) / 10,
|
||||||
|
muscleMass: Math.round((21.5 + (target.muscleMass - 21.5) * ratio) * 10) / 10,
|
||||||
|
bmr: Math.round(1340 + (target.bmr - 1340) * ratio),
|
||||||
|
bodyWater: Math.round((51 + (target.bodyWater - 51) * ratio) * 10) / 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { bodyTestMock }
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import { courseCatalogMock } from './mockData.js'
|
||||||
|
|
||||||
|
function clone(value) {
|
||||||
|
return JSON.parse(JSON.stringify(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDefaultCourseCatalog() {
|
||||||
|
return clone(courseCatalogMock.courses)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mergeCourseCatalog(saved) {
|
||||||
|
const defaults = getDefaultCourseCatalog()
|
||||||
|
if (!saved?.length) return defaults
|
||||||
|
return saved.map((item, i) => ({ ...defaults[i], ...item }))
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCourseStart(course) {
|
||||||
|
const str = `${course.date} ${course.startTime}`.replace(/-/g, '/')
|
||||||
|
return new Date(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPeriod(hour) {
|
||||||
|
if (hour < 12) return 'morning'
|
||||||
|
if (hour < 18) return 'afternoon'
|
||||||
|
return 'evening'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterCourses(courses, filters = {}) {
|
||||||
|
const {
|
||||||
|
date = '',
|
||||||
|
weekDates = [],
|
||||||
|
type = 'all',
|
||||||
|
coach = '全部',
|
||||||
|
period = 'all'
|
||||||
|
} = filters
|
||||||
|
|
||||||
|
return courses.filter((c) => {
|
||||||
|
if (type !== 'all' && c.type !== type) return false
|
||||||
|
if (coach !== '全部' && c.coach !== coach) return false
|
||||||
|
if (period !== 'all' && c.period !== period) return false
|
||||||
|
if (date && c.date !== date) {
|
||||||
|
if (!weekDates.length || !weekDates.includes(c.date)) return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCourseById(store, id) {
|
||||||
|
const course = (store.courseCatalog || []).find((c) => c.id === Number(id))
|
||||||
|
return course ? { ...course } : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canCancelBooking(item) {
|
||||||
|
if (!item?.courseDate || !item?.startTime) return !!item?.canCancel
|
||||||
|
const start = new Date(`${item.courseDate} ${item.startTime}`.replace(/-/g, '/'))
|
||||||
|
const diff = start - Date.now()
|
||||||
|
return diff >= 2 * 3600000
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bookCourse(store, courseId) {
|
||||||
|
const course = store.courseCatalog.find((c) => c.id === Number(courseId))
|
||||||
|
if (!course) return { ok: false, message: '课程不存在' }
|
||||||
|
if (course.enrolled >= course.capacity) return { ok: false, message: '课程已约满' }
|
||||||
|
const exists = store.ongoingBookings.some((b) => b.courseId === course.id)
|
||||||
|
if (exists) return { ok: false, message: '您已预约该课程' }
|
||||||
|
|
||||||
|
course.enrolled += 1
|
||||||
|
const nextId = store.ongoingBookings.reduce((m, b) => Math.max(m, b.id || 0), 0) + 1
|
||||||
|
const parts = course.date.split('-')
|
||||||
|
const booking = {
|
||||||
|
id: nextId,
|
||||||
|
courseId: course.id,
|
||||||
|
title: course.title,
|
||||||
|
banner: course.banner,
|
||||||
|
status: 'booked',
|
||||||
|
statusLabel: '已预约',
|
||||||
|
schedule: `${parts[1]}月${parts[2]}日 ${course.startTime}-${course.endTime}`,
|
||||||
|
dateDay: parts[2],
|
||||||
|
dateMonth: `月${parts[2]}日`,
|
||||||
|
timeRange: `${course.startTime}-${course.endTime}`,
|
||||||
|
courseDate: course.date,
|
||||||
|
startTime: course.startTime,
|
||||||
|
coach: course.coach,
|
||||||
|
coachShort: course.coach.replace('教练', ''),
|
||||||
|
location: course.location,
|
||||||
|
footerText: `可取消(需提前2小时,截止 ${parts[1]}/${parts[2]} ${course.startTime} 前2小时)`,
|
||||||
|
canCancel: true,
|
||||||
|
type: course.type
|
||||||
|
}
|
||||||
|
store.ongoingBookings.unshift(booking)
|
||||||
|
return { ok: true, message: '预约成功', booking }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWeekDates(baseDateStr) {
|
||||||
|
const base = baseDateStr ? new Date(baseDateStr.replace(/-/g, '/')) : new Date()
|
||||||
|
const day = base.getDay() || 7
|
||||||
|
const monday = new Date(base)
|
||||||
|
monday.setDate(base.getDate() - day + 1)
|
||||||
|
const dates = []
|
||||||
|
for (let i = 0; i < 7; i += 1) {
|
||||||
|
const d = new Date(monday)
|
||||||
|
d.setDate(monday.getDate() + i)
|
||||||
|
dates.push(formatIso(d))
|
||||||
|
}
|
||||||
|
return dates
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatIso(d) {
|
||||||
|
const y = d.getFullYear()
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(d.getDate()).padStart(2, '0')
|
||||||
|
return `${y}-${m}-${day}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function enrichCourseForDisplay(course) {
|
||||||
|
const remaining = course.capacity - course.enrolled
|
||||||
|
const percent = Math.round((course.enrolled / course.capacity) * 100)
|
||||||
|
return {
|
||||||
|
...course,
|
||||||
|
remaining,
|
||||||
|
percent,
|
||||||
|
full: remaining <= 0,
|
||||||
|
scarcityLabel: remaining > 0 && remaining <= 5 ? `仅剩${remaining}席` : ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { courseCatalogMock }
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/** 手机号展示脱敏(中间四位 ****) */
|
||||||
|
|
||||||
|
export function maskPhone(phone) {
|
||||||
|
if (phone == null || phone === '') return ''
|
||||||
|
|
||||||
|
const str = String(phone).trim()
|
||||||
|
if (str.includes('****')) return str
|
||||||
|
|
||||||
|
const digits = str.replace(/\D/g, '')
|
||||||
|
if (digits.length === 11) {
|
||||||
|
return `${digits.slice(0, 3)}****${digits.slice(7)}`
|
||||||
|
}
|
||||||
|
if (digits.length > 4) {
|
||||||
|
const hideLen = Math.min(4, digits.length - 3)
|
||||||
|
const start = Math.floor((digits.length - hideLen) / 2)
|
||||||
|
return `${digits.slice(0, start)}${'*'.repeat(hideLen)}${digits.slice(start + hideLen)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 个人中心头部:138****6789 已绑定微信 */
|
||||||
|
export function formatMemberCenterPhone(phone) {
|
||||||
|
const masked = maskPhone(phone)
|
||||||
|
return masked ? `${masked} 已绑定微信` : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 保存前规范化:尽量存 11 位数字;已是脱敏串则原样保留 */
|
||||||
|
export function normalizePhoneForStore(phone) {
|
||||||
|
const str = String(phone || '').trim()
|
||||||
|
if (!str) return ''
|
||||||
|
if (str.includes('****')) return str
|
||||||
|
|
||||||
|
const digits = str.replace(/\D/g, '')
|
||||||
|
if (digits.length >= 11) return digits.slice(0, 11)
|
||||||
|
return digits || str
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
export { memberCenterMock, userInfoMock, fitnessGoalOptions, bookingMock, memberCardMock, bodyTestMock, moduleMock, courseCatalogMock } from './mockData.js'
|
||||||
|
export { statusBarTimeMixin, subPageMixin } from './mixins.js'
|
||||||
|
export {
|
||||||
|
loadMemberStore,
|
||||||
|
saveMemberStore,
|
||||||
|
persistMemberStore,
|
||||||
|
syncStats,
|
||||||
|
computeRemainingDays,
|
||||||
|
buildCardTip,
|
||||||
|
formatUpcomingAlert,
|
||||||
|
getBookingPreview,
|
||||||
|
getCenterPageData,
|
||||||
|
cancelOngoingBooking,
|
||||||
|
renewMemberCard,
|
||||||
|
parseLocalDate,
|
||||||
|
saveUserProfile
|
||||||
|
} from './store.js'
|
||||||
|
export {
|
||||||
|
getLatestBodyTestRecord,
|
||||||
|
getBodyTestRecordById,
|
||||||
|
getBodyTestHistory,
|
||||||
|
computeChanges,
|
||||||
|
formatChangeValue,
|
||||||
|
buildBodyReportSummary,
|
||||||
|
getBodyTestTrendData,
|
||||||
|
getCompareData,
|
||||||
|
getRecommendedCourses,
|
||||||
|
getBodyTestChangeBadge,
|
||||||
|
getBodyTestYears,
|
||||||
|
updateBodyTestSettings,
|
||||||
|
connectBodyTestDevice,
|
||||||
|
disconnectBodyTestDevice,
|
||||||
|
saveSimulatedBodyTestRecord,
|
||||||
|
interpolateMeasuringMetrics,
|
||||||
|
bodyTestMock
|
||||||
|
} from './bodyTestStore.js'
|
||||||
|
export {
|
||||||
|
getTrainingReportData,
|
||||||
|
getTrainingSessionById,
|
||||||
|
filterTrainingSessions,
|
||||||
|
getCouponsByStatus,
|
||||||
|
getCouponById,
|
||||||
|
useCoupon,
|
||||||
|
deleteExpiredCoupon,
|
||||||
|
getCouponCenterList,
|
||||||
|
claimCouponFromCenter,
|
||||||
|
getPointsPageData,
|
||||||
|
redeemPointsReward,
|
||||||
|
filterPointsHistory,
|
||||||
|
getReferralPageData,
|
||||||
|
getMyCoursesData,
|
||||||
|
getMyCoursesByTab,
|
||||||
|
getOnlineCourseById,
|
||||||
|
updateOnlineProgress,
|
||||||
|
getCheckInHistory,
|
||||||
|
moduleMock
|
||||||
|
} from './moduleStore.js'
|
||||||
|
export {
|
||||||
|
filterCourses,
|
||||||
|
getCourseById,
|
||||||
|
bookCourse,
|
||||||
|
canCancelBooking,
|
||||||
|
enrichCourseForDisplay,
|
||||||
|
getWeekDates,
|
||||||
|
courseCatalogMock
|
||||||
|
} from './bookingStore.js'
|
||||||
|
export { previewImage, persistChosenImage, isLocalFilePath } from './media.js'
|
||||||
|
export { maskPhone, formatMemberCenterPhone, normalizePhoneForStore } from './format.js'
|
||||||
|
export {
|
||||||
|
validateName,
|
||||||
|
validatePhone,
|
||||||
|
validatePhoneForRebind,
|
||||||
|
validateHeight,
|
||||||
|
validateWeight,
|
||||||
|
validateBirthday,
|
||||||
|
validateFitnessGoals,
|
||||||
|
validateUserProfile,
|
||||||
|
showValidationError
|
||||||
|
} from './validate.js'
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
/** 头像等媒体:真机选图后须 saveFile,/static/ 须 getImageInfo */
|
||||||
|
|
||||||
|
function buildStaticPathCandidates(url) {
|
||||||
|
const list = [url]
|
||||||
|
if (url.startsWith('/')) {
|
||||||
|
list.push(url.slice(1))
|
||||||
|
} else {
|
||||||
|
list.push(`/${url}`)
|
||||||
|
}
|
||||||
|
return [...new Set(list.filter(Boolean))]
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPackageStaticPath(url) {
|
||||||
|
return /^(\/)?static\//i.test(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** chooseImage / saveFile 产生的本地路径(含真机 temp、usr、store) */
|
||||||
|
export function isLocalFilePath(url) {
|
||||||
|
if (!url) return false
|
||||||
|
if (/^(wxfile:|file:|blob:|data:)/i.test(url)) return true
|
||||||
|
if (/^https?:\/\/(tmp|usr|store)\//i.test(url)) return true
|
||||||
|
if (/^https?:\/\//i.test(url) && !isPackageStaticPath(url)) return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPreviewFail() {
|
||||||
|
uni.showToast({ title: '无法预览头像', icon: 'none' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPreview(path, onFail) {
|
||||||
|
if (!path) {
|
||||||
|
;(onFail || showPreviewFail)()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.previewImage({
|
||||||
|
urls: [path],
|
||||||
|
current: path,
|
||||||
|
fail: () => (onFail ? onFail() : showPreviewFail())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function previewLocalFile(url) {
|
||||||
|
openPreview(url, () => {
|
||||||
|
uni.getImageInfo({
|
||||||
|
src: url,
|
||||||
|
success: (res) => {
|
||||||
|
openPreview(res.path || url, showPreviewFail)
|
||||||
|
},
|
||||||
|
fail: showPreviewFail
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryGetImageInfo(candidates, index, onSuccess, onFail) {
|
||||||
|
if (index >= candidates.length) {
|
||||||
|
onFail()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.getImageInfo({
|
||||||
|
src: candidates[index],
|
||||||
|
success: (res) => onSuccess(res.path || candidates[index]),
|
||||||
|
fail: () => tryGetImageInfo(candidates, index + 1, onSuccess, onFail)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMpUserDataPath() {
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
return wx.env.USER_DATA_PATH
|
||||||
|
// #endif
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryCopyFile(candidates, index, onSuccess, onFail) {
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
const userPath = getMpUserDataPath()
|
||||||
|
if (!userPath) {
|
||||||
|
onFail()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const fs = uni.getFileSystemManager()
|
||||||
|
const extMatch = candidates[0]?.match(/\.(\w+)(?:\?|$)/)
|
||||||
|
const ext = extMatch ? extMatch[1] : 'png'
|
||||||
|
const dest = `${userPath}/avatar_preview_${Date.now()}.${ext}`
|
||||||
|
|
||||||
|
if (index >= candidates.length) {
|
||||||
|
onFail()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.copyFile({
|
||||||
|
srcPath: candidates[index],
|
||||||
|
destPath: dest,
|
||||||
|
success: () => onSuccess(dest),
|
||||||
|
fail: () => tryCopyFile(candidates, index + 1, onSuccess, onFail)
|
||||||
|
})
|
||||||
|
// #endif
|
||||||
|
// #ifndef MP-WEIXIN
|
||||||
|
onFail()
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
|
||||||
|
function previewPackageStatic(url) {
|
||||||
|
const candidates = buildStaticPathCandidates(url)
|
||||||
|
tryGetImageInfo(
|
||||||
|
candidates,
|
||||||
|
0,
|
||||||
|
(path) => openPreview(path, showPreviewFail),
|
||||||
|
() => {
|
||||||
|
tryCopyFile(
|
||||||
|
candidates,
|
||||||
|
0,
|
||||||
|
(path) => openPreview(path, showPreviewFail),
|
||||||
|
showPreviewFail
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 选图后将临时文件转为真机可预览、可持久化的本地路径 */
|
||||||
|
export function persistChosenImage(tempPath) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const path = String(tempPath || '').trim()
|
||||||
|
if (!path) {
|
||||||
|
resolve('')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
uni.saveFile({
|
||||||
|
tempFilePath: path,
|
||||||
|
success: (res) => resolve(res.savedFilePath || path),
|
||||||
|
fail: () => resolve(path)
|
||||||
|
})
|
||||||
|
// #endif
|
||||||
|
// #ifndef MP-WEIXIN
|
||||||
|
resolve(path)
|
||||||
|
// #endif
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function previewImage(src, fallback = '') {
|
||||||
|
const url = String(src || fallback || '').trim()
|
||||||
|
if (!url) {
|
||||||
|
uni.showToast({ title: '暂无头像', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLocalFilePath(url)) {
|
||||||
|
previewLocalFile(url)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPackageStaticPath(url)) {
|
||||||
|
previewPackageStatic(url)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
previewLocalFile(url)
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { backToMemberCenter } from '../constants/routes.js'
|
||||||
|
|
||||||
|
/** 状态栏时间(Pixso 顶栏占位) */
|
||||||
|
export const statusBarTimeMixin = {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
statusBarTime: '9:41'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad() {
|
||||||
|
this.updateStatusBarTime()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateStatusBarTime() {
|
||||||
|
const now = new Date()
|
||||||
|
this.statusBarTime = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 子页面返回个人中心 tab */
|
||||||
|
export const subPageMixin = {
|
||||||
|
methods: {
|
||||||
|
goBack() {
|
||||||
|
backToMemberCenter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,884 @@
|
|||||||
|
/** 个人中心模块 mock 数据(后续可替换为 API) */
|
||||||
|
|
||||||
|
export const memberCenterMock = {
|
||||||
|
userInfo: {
|
||||||
|
name: '张小芳',
|
||||||
|
phone: '13812345678 已绑定微信',
|
||||||
|
memberLevel: '黄金会员',
|
||||||
|
avatar: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AvatarEditWrap.png'
|
||||||
|
},
|
||||||
|
stats: {
|
||||||
|
checkInCount: 128,
|
||||||
|
trainingHours: 23,
|
||||||
|
pointsBalance: 1250
|
||||||
|
},
|
||||||
|
cardInfo: {
|
||||||
|
name: '健身时长卡',
|
||||||
|
detailTag: '详情',
|
||||||
|
expireDate: '有效期至 2025年12月31日',
|
||||||
|
remainingDays: 187,
|
||||||
|
tip: '距离下次到期还有187天,请及时续费'
|
||||||
|
},
|
||||||
|
checkIns: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: '今日签到 · 瑜伽初级班',
|
||||||
|
time: '2024-07-12 09:05',
|
||||||
|
tag: '团课',
|
||||||
|
tagTheme: 'group'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: '自由训练 · 进馆记录',
|
||||||
|
time: '2024-07-11 18:30',
|
||||||
|
tag: '自由',
|
||||||
|
tagTheme: 'free'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: '私教课 · 力量训练',
|
||||||
|
time: '2024-07-10 14:00',
|
||||||
|
tag: '私教',
|
||||||
|
tagTheme: 'private'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
bodyReport: {
|
||||||
|
date: '2024-07-01',
|
||||||
|
weight: '63.5',
|
||||||
|
bmi: '22.1',
|
||||||
|
bodyFat: '24.8%',
|
||||||
|
bmr: '165',
|
||||||
|
status: '比较健康',
|
||||||
|
change: '-1.2kg'
|
||||||
|
},
|
||||||
|
couponPoints: {
|
||||||
|
amount: '¥50',
|
||||||
|
couponDesc: '满500可用 · 1张',
|
||||||
|
couponAction: '去使用',
|
||||||
|
points: 1250,
|
||||||
|
pointsLabel: '我的积分',
|
||||||
|
pointsAction: '去兑换'
|
||||||
|
},
|
||||||
|
referral: {
|
||||||
|
code: 'FIT-ZXF-2024',
|
||||||
|
invited: 5,
|
||||||
|
registered: 3,
|
||||||
|
purchased: 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const userInfoMock = {
|
||||||
|
name: '张小芳',
|
||||||
|
phone: '13812345678',
|
||||||
|
gender: 'female',
|
||||||
|
birthday: '1995年06月15日',
|
||||||
|
height: '165',
|
||||||
|
weight: '63.5',
|
||||||
|
fitnessGoals: ['减脂', '塑形'],
|
||||||
|
avatar: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AvatarEditWrap.png'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fitnessGoalOptions = ['减脂', '塑形', '增肌', '提升耐力', '改善体态']
|
||||||
|
|
||||||
|
export const memberCardMock = {
|
||||||
|
card: {
|
||||||
|
name: '黄金健身时长卡',
|
||||||
|
status: '生效中',
|
||||||
|
validityStart: '2024年01月01日',
|
||||||
|
validity: '2024年01月01日 - 2025年12月31日',
|
||||||
|
validityEnd: '2025-12-31',
|
||||||
|
remainingDays: 187
|
||||||
|
},
|
||||||
|
recordTabs: [
|
||||||
|
{ key: 'all', label: '全部' },
|
||||||
|
{ key: 'consume', label: '消费' },
|
||||||
|
{ key: 'checkin', label: '签到' }
|
||||||
|
],
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
type: 'checkin',
|
||||||
|
title: '瑜伽初级班 · 团课签到',
|
||||||
|
time: '2024-07-12 09:05',
|
||||||
|
value: '-1次',
|
||||||
|
valueType: 'negative',
|
||||||
|
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/dumbbell.png',
|
||||||
|
iconTheme: 'orange'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
type: 'checkin',
|
||||||
|
title: '自由进馆',
|
||||||
|
time: '2024-07-11 18:30',
|
||||||
|
value: '-1天',
|
||||||
|
valueType: 'negative',
|
||||||
|
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/mappin.png',
|
||||||
|
iconTheme: 'green'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
type: 'consume',
|
||||||
|
title: '会员卡充值',
|
||||||
|
time: '2024-07-01 10:00',
|
||||||
|
value: '+90天',
|
||||||
|
valueType: 'positive',
|
||||||
|
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/pluscircle.png',
|
||||||
|
iconTheme: 'orange'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
rules: [
|
||||||
|
'时长卡有效期内不限入场次数,但需提前预约团课',
|
||||||
|
'卡到期后不退余额,请合理安排使用',
|
||||||
|
'一卡仅限本人使用,不可转让'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 智能体测模块 mock 数据 */
|
||||||
|
export const bodyTestMock = {
|
||||||
|
settings: {
|
||||||
|
autoSync: true,
|
||||||
|
bluetoothEnabled: true,
|
||||||
|
notifyOnComplete: true,
|
||||||
|
shareAnonymous: false,
|
||||||
|
unitSystem: 'metric'
|
||||||
|
},
|
||||||
|
device: {
|
||||||
|
connected: false,
|
||||||
|
name: 'InBody 270',
|
||||||
|
model: 'IB-270',
|
||||||
|
battery: 86,
|
||||||
|
signal: 'strong',
|
||||||
|
lastConnected: '2024-07-10 18:20'
|
||||||
|
},
|
||||||
|
connectSteps: [
|
||||||
|
{ step: 1, title: '开启体测仪', desc: '长按电源键 3 秒,等待蓝牙指示灯闪烁' },
|
||||||
|
{ step: 2, title: '靠近设备', desc: '将手机靠近体测仪 1 米范围内' },
|
||||||
|
{ step: 3, title: '确认连接', desc: '点击下方按钮搜索并配对设备' }
|
||||||
|
],
|
||||||
|
metricDefs: [
|
||||||
|
{ key: 'weight', label: '体重', unit: 'kg', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/target.png' },
|
||||||
|
{ key: 'bmi', label: 'BMI', unit: '', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/activity.png' },
|
||||||
|
{ key: 'bodyFat', label: '体脂率', unit: '%', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/trendingdown.png' },
|
||||||
|
{ key: 'muscleMass', label: '肌肉量', unit: 'kg', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/dumbbell.png' },
|
||||||
|
{ key: 'visceralFat', label: '内脏脂肪', unit: '级', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/alertcircle.png' },
|
||||||
|
{ key: 'bmr', label: '基础代谢', unit: 'kcal', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/clock.png' },
|
||||||
|
{ key: 'bodyWater', label: '体水分', unit: '%', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/shield.png' },
|
||||||
|
{ key: 'boneMass', label: '骨量', unit: 'kg', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/user.png' }
|
||||||
|
],
|
||||||
|
radarLabels: [
|
||||||
|
{ key: 'weight', label: '体重控制' },
|
||||||
|
{ key: 'bodyFat', label: '体脂肪' },
|
||||||
|
{ key: 'muscle', label: '肌肉量' },
|
||||||
|
{ key: 'bone', label: '骨量' },
|
||||||
|
{ key: 'water', label: '体水分' },
|
||||||
|
{ key: 'bmr', label: '基础代谢' }
|
||||||
|
],
|
||||||
|
trendMetrics: [
|
||||||
|
{ key: 'weight', label: '体重' },
|
||||||
|
{ key: 'bodyFat', label: '体脂率' },
|
||||||
|
{ key: 'muscleMass', label: '肌肉量' },
|
||||||
|
{ key: 'bmi', label: 'BMI' }
|
||||||
|
],
|
||||||
|
recommendedCourses: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: '燃脂 HIIT 团课',
|
||||||
|
coach: '李明教练',
|
||||||
|
schedule: '每周二、四 19:00',
|
||||||
|
banner: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AC1Banner.png',
|
||||||
|
tag: '减脂推荐'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: '核心力量塑形',
|
||||||
|
coach: '王强教练',
|
||||||
|
schedule: '每周一、三 18:30',
|
||||||
|
banner: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AC2Banner.png',
|
||||||
|
tag: '塑形推荐'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
date: '2024-07-12',
|
||||||
|
time: '09:05',
|
||||||
|
score: 85,
|
||||||
|
grade: 'B+',
|
||||||
|
gradeLabel: '良好',
|
||||||
|
status: '比较健康',
|
||||||
|
bodyAge: 27,
|
||||||
|
realAge: 29,
|
||||||
|
metrics: {
|
||||||
|
weight: 63.5,
|
||||||
|
bmi: 22.1,
|
||||||
|
bodyFat: 24.8,
|
||||||
|
muscleMass: 22.6,
|
||||||
|
visceralFat: 6,
|
||||||
|
bmr: 1385,
|
||||||
|
bodyWater: 52.8,
|
||||||
|
boneMass: 2.42,
|
||||||
|
protein: 16.4
|
||||||
|
},
|
||||||
|
radar: { weight: 78, bodyFat: 72, muscle: 74, bone: 81, water: 79, bmr: 73 },
|
||||||
|
bodySegments: [
|
||||||
|
{ part: '左臂', level: 'normal', value: '2.1kg' },
|
||||||
|
{ part: '右臂', level: 'normal', value: '2.2kg' },
|
||||||
|
{ part: '躯干', level: 'high', value: '28.5kg' },
|
||||||
|
{ part: '左腿', level: 'normal', value: '8.6kg' },
|
||||||
|
{ part: '右腿', level: 'normal', value: '8.7kg' }
|
||||||
|
],
|
||||||
|
advice: [
|
||||||
|
'体脂率略高,建议增加有氧训练频率至每周 3-4 次',
|
||||||
|
'核心肌群表现良好,可尝试进阶力量课程',
|
||||||
|
'保持当前蛋白质摄入,有助于维持肌肉量'
|
||||||
|
],
|
||||||
|
recommendedCourseIds: [1, 2]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
date: '2024-06-28',
|
||||||
|
time: '18:40',
|
||||||
|
score: 82,
|
||||||
|
grade: 'B+',
|
||||||
|
gradeLabel: '良好',
|
||||||
|
status: '比较健康',
|
||||||
|
bodyAge: 28,
|
||||||
|
realAge: 29,
|
||||||
|
metrics: {
|
||||||
|
weight: 64.7,
|
||||||
|
bmi: 22.5,
|
||||||
|
bodyFat: 25.3,
|
||||||
|
muscleMass: 22.2,
|
||||||
|
visceralFat: 7,
|
||||||
|
bmr: 1370,
|
||||||
|
bodyWater: 52.1,
|
||||||
|
boneMass: 2.4,
|
||||||
|
protein: 16.1
|
||||||
|
},
|
||||||
|
radar: { weight: 74, bodyFat: 68, muscle: 70, bone: 80, water: 76, bmr: 70 },
|
||||||
|
bodySegments: [
|
||||||
|
{ part: '左臂', level: 'normal', value: '2.0kg' },
|
||||||
|
{ part: '右臂', level: 'normal', value: '2.1kg' },
|
||||||
|
{ part: '躯干', level: 'high', value: '28.2kg' },
|
||||||
|
{ part: '左腿', level: 'normal', value: '8.5kg' },
|
||||||
|
{ part: '右腿', level: 'normal', value: '8.6kg' }
|
||||||
|
],
|
||||||
|
advice: [
|
||||||
|
'体重较上次下降 0.8kg,减脂方向正确',
|
||||||
|
'建议配合拉伸课程改善体态'
|
||||||
|
],
|
||||||
|
recommendedCourseIds: [1]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
date: '2024-06-10',
|
||||||
|
time: '10:15',
|
||||||
|
score: 79,
|
||||||
|
grade: 'B',
|
||||||
|
gradeLabel: '中等',
|
||||||
|
status: '需关注',
|
||||||
|
bodyAge: 30,
|
||||||
|
realAge: 29,
|
||||||
|
metrics: {
|
||||||
|
weight: 65.5,
|
||||||
|
bmi: 22.8,
|
||||||
|
bodyFat: 26.1,
|
||||||
|
muscleMass: 21.8,
|
||||||
|
visceralFat: 8,
|
||||||
|
bmr: 1355,
|
||||||
|
bodyWater: 51.5,
|
||||||
|
boneMass: 2.38,
|
||||||
|
protein: 15.8
|
||||||
|
},
|
||||||
|
radar: { weight: 70, bodyFat: 62, muscle: 66, bone: 78, water: 72, bmr: 66 },
|
||||||
|
bodySegments: [
|
||||||
|
{ part: '左臂', level: 'low', value: '1.9kg' },
|
||||||
|
{ part: '右臂', level: 'normal', value: '2.0kg' },
|
||||||
|
{ part: '躯干', level: 'high', value: '28.0kg' },
|
||||||
|
{ part: '左腿', level: 'normal', value: '8.4kg' },
|
||||||
|
{ part: '右腿', level: 'normal', value: '8.5kg' }
|
||||||
|
],
|
||||||
|
advice: [
|
||||||
|
'内脏脂肪偏高,建议减少高糖饮食',
|
||||||
|
'增加抗阻训练提升肌肉量'
|
||||||
|
],
|
||||||
|
recommendedCourseIds: [2]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
date: '2024-05-20',
|
||||||
|
time: '14:30',
|
||||||
|
score: 76,
|
||||||
|
grade: 'B',
|
||||||
|
gradeLabel: '中等',
|
||||||
|
status: '需关注',
|
||||||
|
bodyAge: 31,
|
||||||
|
realAge: 29,
|
||||||
|
metrics: {
|
||||||
|
weight: 66.2,
|
||||||
|
bmi: 23.1,
|
||||||
|
bodyFat: 26.8,
|
||||||
|
muscleMass: 21.5,
|
||||||
|
visceralFat: 9,
|
||||||
|
bmr: 1340,
|
||||||
|
bodyWater: 51.0,
|
||||||
|
boneMass: 2.35,
|
||||||
|
protein: 15.5
|
||||||
|
},
|
||||||
|
radar: { weight: 66, bodyFat: 58, muscle: 62, bone: 76, water: 68, bmr: 62 },
|
||||||
|
bodySegments: [
|
||||||
|
{ part: '左臂', level: 'low', value: '1.8kg' },
|
||||||
|
{ part: '右臂', level: 'low', value: '1.9kg' },
|
||||||
|
{ part: '躯干', level: 'high', value: '27.8kg' },
|
||||||
|
{ part: '左腿', level: 'normal', value: '8.3kg' },
|
||||||
|
{ part: '右腿', level: 'normal', value: '8.4kg' }
|
||||||
|
],
|
||||||
|
advice: [
|
||||||
|
'建议制定 8 周减脂计划并定期复测',
|
||||||
|
'每日饮水量建议达到 2000ml'
|
||||||
|
],
|
||||||
|
recommendedCourseIds: [1, 2]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const bookingMock = {
|
||||||
|
upcomingAlert: '明天 09:00 有一堂瑜伽课,请提前 30 分钟到场',
|
||||||
|
tabs: [
|
||||||
|
{ key: 'ongoing', label: '进行中' },
|
||||||
|
{ key: 'history', label: '历史预约' }
|
||||||
|
],
|
||||||
|
ongoing: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: '瑜伽基础班',
|
||||||
|
banner: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AC1Banner.png',
|
||||||
|
status: 'booked',
|
||||||
|
statusLabel: '已预约',
|
||||||
|
schedule: '07月15日 09:00-10:00',
|
||||||
|
dateDay: '07',
|
||||||
|
dateMonth: '月15日',
|
||||||
|
timeRange: '09:00-10:00',
|
||||||
|
coach: '李明教练',
|
||||||
|
coachShort: '李明',
|
||||||
|
location: '一楼 大厅',
|
||||||
|
footerText: '可取消(截止 07/15 07:00)',
|
||||||
|
canCancel: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: '私教健身课',
|
||||||
|
banner: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AC2Banner.png',
|
||||||
|
status: 'pending',
|
||||||
|
statusLabel: '待上课',
|
||||||
|
schedule: '07月18日 14:00-15:00',
|
||||||
|
dateDay: '07',
|
||||||
|
dateMonth: '月18日',
|
||||||
|
timeRange: '14:00-15:00',
|
||||||
|
coach: '王强教练',
|
||||||
|
coachShort: '王强',
|
||||||
|
location: 'B区私教室',
|
||||||
|
footerText: '地点:B区私教室',
|
||||||
|
canCancel: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
history: [
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: '动感单车',
|
||||||
|
banner: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AC1Banner.png',
|
||||||
|
status: 'completed',
|
||||||
|
statusLabel: '已完成',
|
||||||
|
schedule: '07月10日 19:00-20:00',
|
||||||
|
coach: '赵敏教练',
|
||||||
|
footerText: '已签到',
|
||||||
|
canCancel: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: '普拉提进阶',
|
||||||
|
banner: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AC2Banner.png',
|
||||||
|
status: 'cancelled',
|
||||||
|
statusLabel: '已取消',
|
||||||
|
schedule: '07月05日 10:00-11:00',
|
||||||
|
coach: '李明教练',
|
||||||
|
footerText: '用户主动取消',
|
||||||
|
canCancel: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 可预约课程 catalog */
|
||||||
|
export const courseCatalogMock = {
|
||||||
|
coaches: ['全部', '李明教练', '王强教练', '赵敏教练'],
|
||||||
|
periodOptions: [
|
||||||
|
{ key: 'all', label: '全部时段' },
|
||||||
|
{ key: 'morning', label: '上午' },
|
||||||
|
{ key: 'afternoon', label: '下午' },
|
||||||
|
{ key: 'evening', label: '晚上' }
|
||||||
|
],
|
||||||
|
typeOptions: [
|
||||||
|
{ key: 'all', label: '全部' },
|
||||||
|
{ key: 'group', label: '团课' },
|
||||||
|
{ key: 'private', label: '私教' }
|
||||||
|
],
|
||||||
|
courses: [
|
||||||
|
{
|
||||||
|
id: 101,
|
||||||
|
title: '瑜伽基础班',
|
||||||
|
type: 'group',
|
||||||
|
coach: '李明教练',
|
||||||
|
coachAvatar: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/user0.png',
|
||||||
|
date: '2024-07-15',
|
||||||
|
startTime: '09:00',
|
||||||
|
endTime: '10:00',
|
||||||
|
location: '一楼大厅',
|
||||||
|
enrolled: 12,
|
||||||
|
capacity: 20,
|
||||||
|
price: '次卡扣 1 次',
|
||||||
|
payType: 'session',
|
||||||
|
period: 'morning',
|
||||||
|
banner: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AC1Banner.png',
|
||||||
|
intro: '适合零基础学员,重点提升柔韧性与呼吸控制。',
|
||||||
|
suitable: '久坐办公族、初学者、想改善体态者',
|
||||||
|
coachBio: '国家一级瑜伽指导员,5年教学经验',
|
||||||
|
coachRating: 4.9,
|
||||||
|
reviews: [
|
||||||
|
{ user: '会员 A', score: 5, text: '教练讲解很细致,氛围很好' },
|
||||||
|
{ user: '会员 B', score: 5, text: '适合新手,推荐' }
|
||||||
|
],
|
||||||
|
cancelRule: '至少提前 2 小时取消,否则视为爽约'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 102,
|
||||||
|
title: 'HIIT 燃脂团课',
|
||||||
|
type: 'group',
|
||||||
|
coach: '赵敏教练',
|
||||||
|
coachAvatar: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/user1.png',
|
||||||
|
date: '2024-07-15',
|
||||||
|
startTime: '19:00',
|
||||||
|
endTime: '20:00',
|
||||||
|
location: '有氧区',
|
||||||
|
enrolled: 18,
|
||||||
|
capacity: 20,
|
||||||
|
price: '时长卡',
|
||||||
|
payType: 'duration',
|
||||||
|
period: 'evening',
|
||||||
|
banner: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AC1Banner.png',
|
||||||
|
intro: '高强度间歇训练,快速燃脂提升心肺。',
|
||||||
|
suitable: '有一定运动基础、目标减脂者',
|
||||||
|
coachBio: 'ACE 认证教练,擅长 HIIT 与动感单车',
|
||||||
|
coachRating: 4.8,
|
||||||
|
reviews: [{ user: '会员 C', score: 5, text: '强度够,出汗很多' }],
|
||||||
|
cancelRule: '至少提前 2 小时取消'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 103,
|
||||||
|
title: '私教 · 力量训练',
|
||||||
|
type: 'private',
|
||||||
|
coach: '王强教练',
|
||||||
|
coachAvatar: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/user2.png',
|
||||||
|
date: '2024-07-16',
|
||||||
|
startTime: '14:00',
|
||||||
|
endTime: '15:00',
|
||||||
|
location: 'B区私教室',
|
||||||
|
enrolled: 1,
|
||||||
|
capacity: 1,
|
||||||
|
price: '私教课时卡',
|
||||||
|
payType: 'private',
|
||||||
|
period: 'afternoon',
|
||||||
|
banner: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AC2Banner.png',
|
||||||
|
intro: '一对一力量训练,定制训练计划。',
|
||||||
|
suitable: '增肌塑形、康复训练',
|
||||||
|
coachBio: 'NSCA 认证私教,8年从业经验',
|
||||||
|
coachRating: 5.0,
|
||||||
|
reviews: [{ user: '会员 D', score: 5, text: '非常专业' }],
|
||||||
|
cancelRule: '至少提前 2 小时取消'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 104,
|
||||||
|
title: '普拉提进阶',
|
||||||
|
type: 'group',
|
||||||
|
coach: '李明教练',
|
||||||
|
coachAvatar: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/user0.png',
|
||||||
|
date: '2024-07-17',
|
||||||
|
startTime: '10:30',
|
||||||
|
endTime: '11:30',
|
||||||
|
location: '二楼瑜伽室',
|
||||||
|
enrolled: 8,
|
||||||
|
capacity: 15,
|
||||||
|
price: '次卡扣 1 次',
|
||||||
|
payType: 'session',
|
||||||
|
period: 'morning',
|
||||||
|
banner: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AC2Banner.png',
|
||||||
|
intro: '核心稳定与体态矫正进阶课程。',
|
||||||
|
suitable: '有普拉提基础者',
|
||||||
|
coachBio: '国家一级瑜伽指导员',
|
||||||
|
coachRating: 4.9,
|
||||||
|
reviews: [],
|
||||||
|
cancelRule: '至少提前 2 小时取消'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 105,
|
||||||
|
title: '动感单车',
|
||||||
|
type: 'group',
|
||||||
|
coach: '赵敏教练',
|
||||||
|
coachAvatar: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/user1.png',
|
||||||
|
date: '2024-07-18',
|
||||||
|
startTime: '18:30',
|
||||||
|
endTime: '19:30',
|
||||||
|
location: '单车房',
|
||||||
|
enrolled: 20,
|
||||||
|
capacity: 20,
|
||||||
|
price: '储值卡 ¥39',
|
||||||
|
payType: 'stored',
|
||||||
|
period: 'evening',
|
||||||
|
banner: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AC1Banner.png',
|
||||||
|
intro: '音乐骑行,团队氛围燃脂。',
|
||||||
|
suitable: '所有级别,可调节阻力',
|
||||||
|
coachBio: 'ACE 认证教练',
|
||||||
|
coachRating: 4.7,
|
||||||
|
reviews: [{ user: '会员 E', score: 4, text: '音乐很带感' }],
|
||||||
|
cancelRule: '至少提前 2 小时取消'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 个人中心其它模块 mock 数据 */
|
||||||
|
export const moduleMock = {
|
||||||
|
trainingReport: {
|
||||||
|
periodLabel: '本周训练',
|
||||||
|
summary: {
|
||||||
|
sessions: 4,
|
||||||
|
hours: 6.5,
|
||||||
|
calories: 2180,
|
||||||
|
streak: 3,
|
||||||
|
visits: 5
|
||||||
|
},
|
||||||
|
monthlyHours: [
|
||||||
|
{ label: '第1周', value: 4.2 },
|
||||||
|
{ label: '第2周', value: 5.8 },
|
||||||
|
{ label: '第3周', value: 6.5 },
|
||||||
|
{ label: '第4周', value: 5.0 }
|
||||||
|
],
|
||||||
|
monthlyCalories: [
|
||||||
|
{ label: '第1周', value: 1200 },
|
||||||
|
{ label: '第2周', value: 1680 },
|
||||||
|
{ label: '第3周', value: 2180 },
|
||||||
|
{ label: '第4周', value: 1850 }
|
||||||
|
],
|
||||||
|
weeklyHours: [
|
||||||
|
{ label: '一', value: 1.2 },
|
||||||
|
{ label: '二', value: 0 },
|
||||||
|
{ label: '三', value: 1.5 },
|
||||||
|
{ label: '四', value: 0.8 },
|
||||||
|
{ label: '五', value: 1.0 },
|
||||||
|
{ label: '六', value: 2.0 },
|
||||||
|
{ label: '日', value: 0 }
|
||||||
|
],
|
||||||
|
sessions: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: '瑜伽基础班',
|
||||||
|
coach: '李明教练',
|
||||||
|
date: '2024-07-12',
|
||||||
|
time: '09:00-10:00',
|
||||||
|
duration: '60分钟',
|
||||||
|
calories: 320,
|
||||||
|
type: 'group',
|
||||||
|
typeLabel: '团课'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: '自由训练 · 力量',
|
||||||
|
coach: '自主训练',
|
||||||
|
date: '2024-07-11',
|
||||||
|
time: '18:30-19:45',
|
||||||
|
duration: '75分钟',
|
||||||
|
calories: 480,
|
||||||
|
type: 'free',
|
||||||
|
typeLabel: '自由'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: '私教 · 核心塑形',
|
||||||
|
coach: '王强教练',
|
||||||
|
date: '2024-07-10',
|
||||||
|
time: '14:00-15:00',
|
||||||
|
duration: '60分钟',
|
||||||
|
calories: 410,
|
||||||
|
type: 'private',
|
||||||
|
typeLabel: '私教'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: '动感单车',
|
||||||
|
coach: '赵敏教练',
|
||||||
|
date: '2024-07-08',
|
||||||
|
time: '19:00-20:00',
|
||||||
|
duration: '60分钟',
|
||||||
|
calories: 520,
|
||||||
|
type: 'group',
|
||||||
|
typeLabel: '团课'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
couponTabs: [
|
||||||
|
{ key: 'available', label: '可用' },
|
||||||
|
{ key: 'used', label: '已使用' },
|
||||||
|
{ key: 'expired', label: '已过期' }
|
||||||
|
],
|
||||||
|
coupons: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
status: 'available',
|
||||||
|
amount: 50,
|
||||||
|
title: '满500减50',
|
||||||
|
desc: '全场团课/私教可用',
|
||||||
|
expire: '2024-12-31',
|
||||||
|
minSpend: 500,
|
||||||
|
tag: '通用券',
|
||||||
|
rules: '1. 满500元可用\n2. 适用于团课/私教\n3. 不可与其他优惠叠加\n4. 有效期至2024-12-31',
|
||||||
|
scope: '全门店 · 团课/私教',
|
||||||
|
flow: '选择课程 → 确认订单 → 选择优惠券 → 完成支付'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
status: 'available',
|
||||||
|
amount: 30,
|
||||||
|
title: '新人专享',
|
||||||
|
desc: '首次购课立减',
|
||||||
|
expire: '2024-08-31',
|
||||||
|
minSpend: 200,
|
||||||
|
tag: '新人券',
|
||||||
|
rules: '1. 限新注册用户首次购课\n2. 满200可用',
|
||||||
|
scope: '全门店 · 首次购课',
|
||||||
|
flow: '首次预约课程时自动提示使用'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
status: 'used',
|
||||||
|
amount: 20,
|
||||||
|
title: '签到奖励券',
|
||||||
|
desc: '连续签到7天获得',
|
||||||
|
expire: '2024-07-01',
|
||||||
|
minSpend: 100,
|
||||||
|
tag: '奖励券',
|
||||||
|
usedAt: '2024-06-28',
|
||||||
|
rules: '满100可用',
|
||||||
|
scope: '团课',
|
||||||
|
flow: '预约时使用'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
status: 'expired',
|
||||||
|
amount: 100,
|
||||||
|
title: '周年庆特惠',
|
||||||
|
desc: '满1000可用',
|
||||||
|
expire: '2024-06-01',
|
||||||
|
minSpend: 1000,
|
||||||
|
tag: '活动券',
|
||||||
|
rules: '满1000可用,已过期',
|
||||||
|
scope: '全门店',
|
||||||
|
flow: '—'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
couponCenter: [
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
amount: 20,
|
||||||
|
title: '周末团课券',
|
||||||
|
desc: '周末团课满200减20',
|
||||||
|
expireDays: 30,
|
||||||
|
minSpend: 200,
|
||||||
|
tag: '可领取',
|
||||||
|
claimed: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 12,
|
||||||
|
amount: 50,
|
||||||
|
title: '私教体验券',
|
||||||
|
desc: '私教课满500减50',
|
||||||
|
expireDays: 15,
|
||||||
|
minSpend: 500,
|
||||||
|
tag: '限时',
|
||||||
|
claimed: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 13,
|
||||||
|
amount: 10,
|
||||||
|
title: '签到加油券',
|
||||||
|
desc: '无门槛10元券',
|
||||||
|
expireDays: 7,
|
||||||
|
minSpend: 0,
|
||||||
|
tag: '每日',
|
||||||
|
claimed: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
pointsConfig: {
|
||||||
|
rate: '100积分 = 1元',
|
||||||
|
rule: '签到、训练、邀请好友、购课均可获得积分;积分可用于商城兑换。'
|
||||||
|
},
|
||||||
|
pointsRewards: [
|
||||||
|
{ id: 1, name: '团课体验券', cost: 500, stock: 12, icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/ticket.png' },
|
||||||
|
{ id: 2, name: '运动毛巾', cost: 800, stock: 5, icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/dumbbell.png' },
|
||||||
|
{ id: 3, name: '私教体验30分钟', cost: 2000, stock: 3, icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/usercheck.png' },
|
||||||
|
{ id: 4, name: '蛋白粉小样', cost: 350, stock: 20, icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/star.png' }
|
||||||
|
],
|
||||||
|
pointsHistory: [
|
||||||
|
{ id: 1, type: 'earn', title: '团课签到', amount: 50, time: '2024-07-12 09:10', balance: 1250 },
|
||||||
|
{ id: 2, type: 'earn', title: '邀请好友注册', amount: 200, time: '2024-07-08 15:30', balance: 1200 },
|
||||||
|
{ id: 3, type: 'spend', title: '兑换团课体验券', amount: -500, time: '2024-07-01 11:00', balance: 1000 },
|
||||||
|
{ id: 4, type: 'earn', title: '会员卡续费奖励', amount: 100, time: '2024-07-01 10:05', balance: 1500 },
|
||||||
|
{ id: 5, type: 'earn', title: '体测完成奖励', amount: 30, time: '2024-06-28 18:45', balance: 1400 }
|
||||||
|
],
|
||||||
|
referralRules: [
|
||||||
|
'好友通过您的邀请码注册,双方各得 100 积分',
|
||||||
|
'好友首次购课成功后,您额外获得 300 积分',
|
||||||
|
'每月邀请奖励上限 10 人,超出不再计奖',
|
||||||
|
'积分可用于兑换课程体验券及周边礼品'
|
||||||
|
],
|
||||||
|
referralRecords: [
|
||||||
|
{ id: 1, name: '李**', avatar: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/user0.png', status: 'purchased', statusLabel: '已购课', time: '2024-07-05', reward: '+300积分', rewardStatus: '已发放' },
|
||||||
|
{ id: 2, name: '王**', avatar: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/user1.png', status: 'registered', statusLabel: '已注册', time: '2024-06-20', reward: '+100积分', rewardStatus: '已发放' },
|
||||||
|
{ id: 3, name: '陈**', avatar: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/user2.png', status: 'invited', statusLabel: '已邀请', time: '2024-06-15', reward: '待注册', rewardStatus: '待发放' },
|
||||||
|
{ id: 4, name: '赵**', avatar: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/user3.png', status: 'purchased', statusLabel: '已购课', time: '2024-06-01', reward: '+300积分', rewardStatus: '已发放' },
|
||||||
|
{ id: 5, name: '刘**', avatar: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/user0.png', status: 'registered', statusLabel: '已注册', time: '2024-05-28', reward: '+100积分', rewardStatus: '已发放' }
|
||||||
|
],
|
||||||
|
referralRewardSummary: {
|
||||||
|
totalPoints: 800,
|
||||||
|
totalCoupons: 2,
|
||||||
|
pendingCount: 1
|
||||||
|
},
|
||||||
|
myCourseTabs: [
|
||||||
|
{ key: 'group', label: '团课' },
|
||||||
|
{ key: 'private', label: '私教' },
|
||||||
|
{ key: 'online', label: '线上课' },
|
||||||
|
{ key: 'package', label: '训练营' }
|
||||||
|
],
|
||||||
|
myCourses: {
|
||||||
|
group: {
|
||||||
|
ongoing: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: '瑜伽基础班',
|
||||||
|
coach: '李明教练',
|
||||||
|
banner: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AC1Banner.png',
|
||||||
|
progress: 6,
|
||||||
|
total: 12,
|
||||||
|
schedule: '每周二、四 09:00',
|
||||||
|
location: '一楼大厅',
|
||||||
|
nextClass: '07月16日 09:00',
|
||||||
|
canCancel: true,
|
||||||
|
bookingId: 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
completed: [
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: '动感单车入门',
|
||||||
|
coach: '赵敏教练',
|
||||||
|
banner: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AC1Banner.png',
|
||||||
|
progress: 8,
|
||||||
|
total: 8,
|
||||||
|
schedule: '已结课',
|
||||||
|
location: '单车房',
|
||||||
|
completedAt: '2024-06-30',
|
||||||
|
canEvaluate: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
private: {
|
||||||
|
remaining: 7,
|
||||||
|
coach: '王强教练',
|
||||||
|
coachAvatar: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/user2.png',
|
||||||
|
nextClass: '07月15日 14:00',
|
||||||
|
bookings: [
|
||||||
|
{ id: 2, title: '私教 · 力量训练', time: '07月18日 14:00', status: '已预约', location: 'B区私教室' }
|
||||||
|
],
|
||||||
|
completed: [
|
||||||
|
{ id: 5, title: '私教 · 核心塑形', time: '2024-07-10 14:00', coach: '王强教练' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
online: [
|
||||||
|
{
|
||||||
|
id: 201,
|
||||||
|
title: '居家核心训练',
|
||||||
|
cover: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AC2Banner.png',
|
||||||
|
duration: '45分钟',
|
||||||
|
progress: 60,
|
||||||
|
chapters: 6,
|
||||||
|
watched: 4,
|
||||||
|
type: 'video'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 202,
|
||||||
|
title: '直播 · 晨间拉伸',
|
||||||
|
cover: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AC1Banner.png',
|
||||||
|
duration: '30分钟',
|
||||||
|
progress: 0,
|
||||||
|
liveTime: '07月20日 07:00',
|
||||||
|
type: 'live'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
package: [
|
||||||
|
{
|
||||||
|
id: 301,
|
||||||
|
title: '28天减脂训练营',
|
||||||
|
banner: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AC1Banner.png',
|
||||||
|
progress: 3,
|
||||||
|
total: 10,
|
||||||
|
coach: '李明教练',
|
||||||
|
schedule: '每周5练'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
checkInTabs: [
|
||||||
|
{ key: 'all', label: '全部' },
|
||||||
|
{ key: 'group', label: '团课' },
|
||||||
|
{ key: 'private', label: '私教' },
|
||||||
|
{ key: 'free', label: '自由' }
|
||||||
|
],
|
||||||
|
checkInHistory: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: '今日签到 · 瑜伽初级班',
|
||||||
|
time: '2024-07-12 09:05',
|
||||||
|
tag: '团课',
|
||||||
|
tagTheme: 'group',
|
||||||
|
location: '一楼大厅'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: '自由训练 · 进馆记录',
|
||||||
|
time: '2024-07-11 18:30',
|
||||||
|
tag: '自由',
|
||||||
|
tagTheme: 'free',
|
||||||
|
location: '器械区'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: '私教课 · 力量训练',
|
||||||
|
time: '2024-07-10 14:00',
|
||||||
|
tag: '私教',
|
||||||
|
tagTheme: 'private',
|
||||||
|
location: 'B区私教室'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: '团课签到 · 动感单车',
|
||||||
|
time: '2024-07-08 19:02',
|
||||||
|
tag: '团课',
|
||||||
|
tagTheme: 'group',
|
||||||
|
location: '单车房'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
title: '自由训练 · 进馆记录',
|
||||||
|
time: '2024-07-06 17:45',
|
||||||
|
tag: '自由',
|
||||||
|
tagTheme: 'free',
|
||||||
|
location: '有氧区'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
import { moduleMock } from './mockData.js'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function clone(value) {
|
||||||
|
|
||||||
|
return JSON.parse(JSON.stringify(value))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function getDefaultModuleState() {
|
||||||
|
|
||||||
|
return {
|
||||||
|
|
||||||
|
trainingReport: clone(moduleMock.trainingReport),
|
||||||
|
|
||||||
|
coupons: clone(moduleMock.coupons),
|
||||||
|
|
||||||
|
couponCenter: clone(moduleMock.couponCenter),
|
||||||
|
|
||||||
|
pointsHistory: clone(moduleMock.pointsHistory),
|
||||||
|
|
||||||
|
pointsRewards: clone(moduleMock.pointsRewards),
|
||||||
|
|
||||||
|
redeemRecords: [],
|
||||||
|
|
||||||
|
referralRecords: clone(moduleMock.referralRecords),
|
||||||
|
|
||||||
|
myCourses: clone(moduleMock.myCourses),
|
||||||
|
|
||||||
|
checkInHistory: clone(moduleMock.checkInHistory)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function mergeModuleState(saved) {
|
||||||
|
|
||||||
|
const defaults = getDefaultModuleState()
|
||||||
|
|
||||||
|
if (!saved) return defaults
|
||||||
|
|
||||||
|
return {
|
||||||
|
|
||||||
|
trainingReport: { ...defaults.trainingReport, ...(saved.trainingReport || {}) },
|
||||||
|
|
||||||
|
coupons: saved.coupons?.length ? saved.coupons : defaults.coupons,
|
||||||
|
|
||||||
|
couponCenter: saved.couponCenter?.length ? saved.couponCenter : defaults.couponCenter,
|
||||||
|
|
||||||
|
pointsHistory: saved.pointsHistory?.length ? saved.pointsHistory : defaults.pointsHistory,
|
||||||
|
|
||||||
|
pointsRewards: saved.pointsRewards?.length ? saved.pointsRewards : defaults.pointsRewards,
|
||||||
|
|
||||||
|
redeemRecords: saved.redeemRecords || defaults.redeemRecords,
|
||||||
|
|
||||||
|
referralRecords: saved.referralRecords?.length ? saved.referralRecords : defaults.referralRecords,
|
||||||
|
|
||||||
|
myCourses: saved.myCourses ? mergeMyCourses(defaults.myCourses, saved.myCourses) : defaults.myCourses,
|
||||||
|
|
||||||
|
checkInHistory: saved.checkInHistory?.length ? saved.checkInHistory : defaults.checkInHistory
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function mergeMyCourses(defaults, saved) {
|
||||||
|
|
||||||
|
return {
|
||||||
|
|
||||||
|
group: saved.group || defaults.group,
|
||||||
|
|
||||||
|
private: saved.private || defaults.private,
|
||||||
|
|
||||||
|
online: saved.online?.length ? saved.online : defaults.online,
|
||||||
|
|
||||||
|
package: saved.package?.length ? saved.package : defaults.package
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function syncCouponSummary(store) {
|
||||||
|
|
||||||
|
const available = store.modules.coupons.filter((c) => c.status === 'available')
|
||||||
|
|
||||||
|
const top = available[0]
|
||||||
|
|
||||||
|
store.couponPoints = {
|
||||||
|
|
||||||
|
...store.couponPoints,
|
||||||
|
|
||||||
|
amount: top ? `¥${top.amount}` : '暂无',
|
||||||
|
|
||||||
|
couponDesc: top
|
||||||
|
|
||||||
|
? `满${top.minSpend}可用 · ${available.length}张`
|
||||||
|
|
||||||
|
: '暂无可用优惠券',
|
||||||
|
|
||||||
|
couponAction: available.length ? '去使用' : '去领取',
|
||||||
|
|
||||||
|
points: store.stats.pointsBalance,
|
||||||
|
|
||||||
|
pointsLabel: '我的积分',
|
||||||
|
|
||||||
|
pointsAction: '去兑换'
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return store
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function finalizeModules(store) {
|
||||||
|
|
||||||
|
syncCouponSummary(store)
|
||||||
|
|
||||||
|
store.checkIns = store.modules.checkInHistory.slice(0, 3).map((item) => ({ ...item }))
|
||||||
|
|
||||||
|
return store
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function getTrainingReportData(store, period = 'week') {
|
||||||
|
|
||||||
|
const report = store.modules.trainingReport
|
||||||
|
|
||||||
|
const trend = period === 'month' ? report.monthlyHours : report.weeklyHours
|
||||||
|
|
||||||
|
const calTrend = period === 'month' ? report.monthlyCalories : report.weeklyHours.map((w, i) => ({
|
||||||
|
|
||||||
|
label: w.label,
|
||||||
|
|
||||||
|
value: Math.round((report.summary.calories / 7) * (w.value || 0.5))
|
||||||
|
|
||||||
|
}))
|
||||||
|
|
||||||
|
return {
|
||||||
|
|
||||||
|
...report,
|
||||||
|
|
||||||
|
period,
|
||||||
|
|
||||||
|
summary: {
|
||||||
|
|
||||||
|
...report.summary,
|
||||||
|
|
||||||
|
hours: store.stats.trainingHours ?? report.summary.hours,
|
||||||
|
|
||||||
|
visits: report.summary.visits ?? store.stats.checkInCount ?? 5
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
trendHours: trend.map((t) => ({ ...t, id: t.label })),
|
||||||
|
|
||||||
|
trendCalories: calTrend.map((t) => ({ ...t, id: t.label })),
|
||||||
|
|
||||||
|
sessions: report.sessions.map((s) => ({ ...s }))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function getTrainingSessionById(store, id) {
|
||||||
|
|
||||||
|
const session = store.modules.trainingReport.sessions.find((s) => s.id === Number(id))
|
||||||
|
|
||||||
|
if (!session) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
|
||||||
|
...session,
|
||||||
|
|
||||||
|
heartRate: '128 bpm',
|
||||||
|
|
||||||
|
comment: '动作标准,核心发力良好,下次可增加负重。',
|
||||||
|
|
||||||
|
checkInTime: `${session.date} ${session.time.split('-')[0]}`
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function filterTrainingSessions(store, filters = {}) {
|
||||||
|
|
||||||
|
let list = store.modules.trainingReport.sessions.map((s) => ({ ...s }))
|
||||||
|
|
||||||
|
if (filters.type && filters.type !== 'all') {
|
||||||
|
|
||||||
|
list = list.filter((s) => s.type === filters.type)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return list
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function getCouponsByStatus(store, status) {
|
||||||
|
|
||||||
|
return store.modules.coupons.filter((c) => c.status === status).map((c) => ({ ...c }))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function getCouponById(store, id) {
|
||||||
|
|
||||||
|
const c = store.modules.coupons.find((item) => item.id === Number(id))
|
||||||
|
|
||||||
|
return c ? { ...c } : null
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function useCoupon(store, id) {
|
||||||
|
|
||||||
|
const coupon = store.modules.coupons.find((c) => c.id === id)
|
||||||
|
|
||||||
|
if (!coupon || coupon.status !== 'available') return null
|
||||||
|
|
||||||
|
coupon.status = 'used'
|
||||||
|
|
||||||
|
coupon.usedAt = new Date().toISOString().slice(0, 10)
|
||||||
|
|
||||||
|
syncCouponSummary(store)
|
||||||
|
|
||||||
|
return coupon
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function deleteExpiredCoupon(store, id) {
|
||||||
|
|
||||||
|
const idx = store.modules.coupons.findIndex((c) => c.id === id && c.status === 'expired')
|
||||||
|
|
||||||
|
if (idx >= 0) store.modules.coupons.splice(idx, 1)
|
||||||
|
|
||||||
|
syncCouponSummary(store)
|
||||||
|
|
||||||
|
return store
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function getCouponCenterList(store) {
|
||||||
|
|
||||||
|
return store.modules.couponCenter.map((c) => ({ ...c }))
|
||||||
|
|
||||||
@@ -0,0 +1,308 @@
|
|||||||
|
import {
|
||||||
|
memberCenterMock,
|
||||||
|
userInfoMock,
|
||||||
|
memberCardMock,
|
||||||
|
bookingMock
|
||||||
|
} from './mockData.js'
|
||||||
|
import { formatMemberCenterPhone, normalizePhoneForStore } from './format.js'
|
||||||
|
import {
|
||||||
|
getDefaultBodyTestState,
|
||||||
|
mergeBodyTestState,
|
||||||
|
getLatestBodyTestRecord,
|
||||||
|
buildBodyReportSummary
|
||||||
|
} from './bodyTestStore.js'
|
||||||
|
import {
|
||||||
|
getDefaultModuleState,
|
||||||
|
mergeModuleState,
|
||||||
|
finalizeModules
|
||||||
|
} from './moduleStore.js'
|
||||||
|
import {
|
||||||
|
getDefaultCourseCatalog,
|
||||||
|
mergeCourseCatalog,
|
||||||
|
canCancelBooking
|
||||||
|
} from './bookingStore.js'
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'gym_member_info_v1'
|
||||||
|
|
||||||
|
function clone(value) {
|
||||||
|
return JSON.parse(JSON.stringify(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCardTip(remainingDays) {
|
||||||
|
return `距离下次到期还有${remainingDays}天,请及时续费`
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyCardInfo(store) {
|
||||||
|
const days = computeRemainingDays(store.card.validityEnd)
|
||||||
|
store.card.remainingDays = days
|
||||||
|
store.cardInfo.remainingDays = days
|
||||||
|
store.cardInfo.tip = buildCardTip(days)
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
|
||||||
|
export function syncStats(store) {
|
||||||
|
store.stats = {
|
||||||
|
...store.stats,
|
||||||
|
pointsBalance: store.stats.pointsBalance ?? 1250
|
||||||
|
}
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
|
||||||
|
function finalizeStore(store) {
|
||||||
|
syncStats(store)
|
||||||
|
applyCardInfo(store)
|
||||||
|
if (store.profile?.avatar) {
|
||||||
|
store.memberProfile.avatar = store.profile.avatar
|
||||||
|
}
|
||||||
|
if (store.profile?.phone) {
|
||||||
|
store.memberProfile.phone = formatMemberCenterPhone(store.profile.phone)
|
||||||
|
}
|
||||||
|
const latestBodyTest = getLatestBodyTestRecord(store)
|
||||||
|
if (latestBodyTest) {
|
||||||
|
const previous = store.bodyTest.records[1]
|
||||||
|
store.bodyReport = buildBodyReportSummary(latestBodyTest, previous)
|
||||||
|
}
|
||||||
|
if (store.modules) {
|
||||||
|
finalizeModules(store)
|
||||||
|
}
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultStore() {
|
||||||
|
return finalizeStore({
|
||||||
|
profile: { ...userInfoMock, avatar: memberCenterMock.userInfo.avatar },
|
||||||
|
memberProfile: { ...memberCenterMock.userInfo },
|
||||||
|
stats: { ...memberCenterMock.stats },
|
||||||
|
cardInfo: { ...memberCenterMock.cardInfo },
|
||||||
|
card: { ...memberCardMock.card },
|
||||||
|
records: clone(memberCardMock.records),
|
||||||
|
ongoingBookings: clone(bookingMock.ongoing),
|
||||||
|
historyBookings: clone(bookingMock.history),
|
||||||
|
checkIns: clone(memberCenterMock.checkIns),
|
||||||
|
bodyReport: { ...memberCenterMock.bodyReport },
|
||||||
|
bodyTest: getDefaultBodyTestState(),
|
||||||
|
modules: getDefaultModuleState(),
|
||||||
|
courseCatalog: getDefaultCourseCatalog(),
|
||||||
|
couponPoints: { ...memberCenterMock.couponPoints },
|
||||||
|
referral: { ...memberCenterMock.referral }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeDefaults(saved) {
|
||||||
|
const defaults = getDefaultStore()
|
||||||
|
return finalizeStore({
|
||||||
|
profile: { ...defaults.profile, ...(saved.profile || {}) },
|
||||||
|
memberProfile: { ...defaults.memberProfile, ...(saved.memberProfile || {}) },
|
||||||
|
stats: { ...defaults.stats, ...(saved.stats || {}) },
|
||||||
|
cardInfo: { ...defaults.cardInfo, ...(saved.cardInfo || {}) },
|
||||||
|
card: { ...defaults.card, ...(saved.card || {}) },
|
||||||
|
records: saved.records?.length ? saved.records : defaults.records,
|
||||||
|
ongoingBookings: saved.ongoingBookings ?? defaults.ongoingBookings,
|
||||||
|
historyBookings: saved.historyBookings ?? defaults.historyBookings,
|
||||||
|
checkIns: saved.checkIns?.length ? saved.checkIns : defaults.checkIns,
|
||||||
|
bodyReport: { ...defaults.bodyReport, ...(saved.bodyReport || {}) },
|
||||||
|
bodyTest: mergeBodyTestState(saved.bodyTest),
|
||||||
|
modules: mergeModuleState(saved.modules),
|
||||||
|
courseCatalog: mergeCourseCatalog(saved.courseCatalog),
|
||||||
|
couponPoints: { ...defaults.couponPoints, ...(saved.couponPoints || {}) },
|
||||||
|
referral: { ...defaults.referral, ...(saved.referral || {}) }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadMemberStore() {
|
||||||
|
try {
|
||||||
|
const saved = uni.getStorageSync(STORAGE_KEY)
|
||||||
|
if (saved && typeof saved === 'object') {
|
||||||
|
return mergeDefaults(saved)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[memberStore] load failed', e)
|
||||||
|
}
|
||||||
|
return getDefaultStore()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveMemberStore(store) {
|
||||||
|
uni.setStorageSync(STORAGE_KEY, store)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解析为本地 0 点,避免 ISO 字符串时区偏差 */
|
||||||
|
export function parseLocalDate(dateStr) {
|
||||||
|
if (!dateStr) return null
|
||||||
|
const str = String(dateStr).trim()
|
||||||
|
const iso = str.match(/^(\d{4})-(\d{2})-(\d{2})$/)
|
||||||
|
if (iso) {
|
||||||
|
return new Date(Number(iso[1]), Number(iso[2]) - 1, Number(iso[3]))
|
||||||
|
}
|
||||||
|
const cn = str.match(/(\d{4})年(\d{2})月(\d{2})日/)
|
||||||
|
if (cn) {
|
||||||
|
return new Date(Number(cn[1]), Number(cn[2]) - 1, Number(cn[3]))
|
||||||
|
}
|
||||||
|
const parsed = new Date(str.replace(/-/g, '/'))
|
||||||
|
return Number.isNaN(parsed.getTime()) ? null : parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatIsoDate(date) {
|
||||||
|
const y = date.getFullYear()
|
||||||
|
const m = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const d = String(date.getDate()).padStart(2, '0')
|
||||||
|
return `${y}-${m}-${d}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatChineseDate(date) {
|
||||||
|
const y = date.getFullYear()
|
||||||
|
const m = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
return `${y}年${m}月${day}日`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRecordTime(date) {
|
||||||
|
const y = date.getFullYear()
|
||||||
|
const m = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const d = String(date.getDate()).padStart(2, '0')
|
||||||
|
const h = String(date.getHours()).padStart(2, '0')
|
||||||
|
const min = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
return `${y}-${m}-${d} ${h}:${min}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextRecordId(records) {
|
||||||
|
return (records || []).reduce((max, item) => Math.max(max, item.id || 0), 0) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeRemainingDays(endDateStr) {
|
||||||
|
const end = parseLocalDate(endDateStr)
|
||||||
|
if (!end) return 0
|
||||||
|
const now = new Date()
|
||||||
|
now.setHours(0, 0, 0, 0)
|
||||||
|
end.setHours(0, 0, 0, 0)
|
||||||
|
const diff = Math.ceil((end - now) / 86400000)
|
||||||
|
return Math.max(0, diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatUpcomingAlert(booking) {
|
||||||
|
if (!booking) return ''
|
||||||
|
const timePart = booking.timeRange || booking.schedule?.split(' ')[1] || ''
|
||||||
|
return `明天 ${timePart} 有一堂${booking.title},请提前 30 分钟到场`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toBookingPreviewItem(item) {
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
dateDay: item.dateDay,
|
||||||
|
dateMonth: item.dateMonth,
|
||||||
|
desc: `${item.title} · ${item.timeRange}`,
|
||||||
|
coach: item.coachShort || item.coach.replace('教练', ''),
|
||||||
|
location: item.location ? `地点:${item.location}` : '',
|
||||||
|
status: item.status,
|
||||||
|
statusLabel: item.statusLabel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBookingPreview(store, limit = 2) {
|
||||||
|
return store.ongoingBookings.slice(0, limit).map(toBookingPreviewItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCenterPageData(store) {
|
||||||
|
return {
|
||||||
|
userInfo: { ...store.memberProfile },
|
||||||
|
stats: { ...store.stats },
|
||||||
|
cardInfo: { ...store.cardInfo },
|
||||||
|
bookingPreview: getBookingPreview(store),
|
||||||
|
checkIns: store.checkIns.map((item) => ({ ...item })),
|
||||||
|
bodyReport: {
|
||||||
|
...store.bodyReport,
|
||||||
|
weight: store.profile.weight || store.bodyReport.weight
|
||||||
|
},
|
||||||
|
couponPoints: {
|
||||||
|
...store.couponPoints,
|
||||||
|
points: store.stats.pointsBalance
|
||||||
|
},
|
||||||
|
referral: { ...store.referral }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cancelOngoingBooking(store, id) {
|
||||||
|
const index = store.ongoingBookings.findIndex((b) => b.id === id)
|
||||||
|
if (index < 0) return { ok: false, message: '预约不存在' }
|
||||||
|
|
||||||
|
const item = store.ongoingBookings[index]
|
||||||
|
if (!canCancelBooking(item)) {
|
||||||
|
return { ok: false, message: '距开课不足2小时,无法取消' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const [removed] = store.ongoingBookings.splice(index, 1)
|
||||||
|
if (removed.courseId) {
|
||||||
|
const course = store.courseCatalog.find((c) => c.id === removed.courseId)
|
||||||
|
if (course && course.enrolled > 0) course.enrolled -= 1
|
||||||
|
}
|
||||||
|
store.historyBookings.unshift({
|
||||||
|
...removed,
|
||||||
|
status: 'cancelled',
|
||||||
|
statusLabel: '已取消',
|
||||||
|
footerText: '用户主动取消',
|
||||||
|
canCancel: false
|
||||||
|
})
|
||||||
|
finalizeStore(store)
|
||||||
|
saveMemberStore(store)
|
||||||
|
return { ok: true, message: '已取消' }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renewMemberCard(store, addDays = 90) {
|
||||||
|
const now = new Date()
|
||||||
|
now.setHours(0, 0, 0, 0)
|
||||||
|
|
||||||
|
let base = parseLocalDate(store.card.validityEnd) || new Date(now)
|
||||||
|
base.setHours(0, 0, 0, 0)
|
||||||
|
// 已过期:从今天起续费;未过期:从当前到期日起顺延
|
||||||
|
if (base < now) {
|
||||||
|
base = new Date(now)
|
||||||
|
}
|
||||||
|
|
||||||
|
const end = new Date(base)
|
||||||
|
end.setDate(end.getDate() + addDays)
|
||||||
|
|
||||||
|
const validityEnd = formatIsoDate(end)
|
||||||
|
const validityEndCn = formatChineseDate(end)
|
||||||
|
store.card.validityEnd = validityEnd
|
||||||
|
store.card.validity = store.card.validityStart
|
||||||
|
? `${store.card.validityStart} - ${validityEndCn}`
|
||||||
|
: `2024年01月01日 - ${validityEndCn}`
|
||||||
|
store.cardInfo.expireDate = `有效期至 ${validityEndCn}`
|
||||||
|
|
||||||
|
store.records.unshift({
|
||||||
|
id: nextRecordId(store.records),
|
||||||
|
type: 'consume',
|
||||||
|
title: '会员卡续费',
|
||||||
|
time: formatRecordTime(new Date()),
|
||||||
|
value: `+${addDays}天`,
|
||||||
|
valueType: 'positive',
|
||||||
|
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/pluscircle.png',
|
||||||
|
iconTheme: 'orange'
|
||||||
|
})
|
||||||
|
|
||||||
|
finalizeStore(store)
|
||||||
|
saveMemberStore(store)
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveUserProfile(store, profile) {
|
||||||
|
const phone = normalizePhoneForStore(profile.phone ?? store.profile.phone)
|
||||||
|
store.profile = { ...store.profile, ...profile, phone }
|
||||||
|
store.memberProfile = {
|
||||||
|
...store.memberProfile,
|
||||||
|
name: store.profile.name,
|
||||||
|
phone: formatMemberCenterPhone(store.profile.phone),
|
||||||
|
avatar: store.profile.avatar || store.memberProfile.avatar
|
||||||
|
}
|
||||||
|
if (store.profile.weight) {
|
||||||
|
store.bodyReport.weight = store.profile.weight
|
||||||
|
}
|
||||||
|
finalizeStore(store)
|
||||||
|
saveMemberStore(store)
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
|
||||||
|
export function persistMemberStore(store) {
|
||||||
|
finalizeStore(store)
|
||||||
|
saveMemberStore(store)
|
||||||
|
return store
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
/** 个人信息页前端校验(与后端手机号规则对齐:^1[3-9]\\d{9}$) */
|
||||||
|
|
||||||
|
const PHONE_REG = /^1[3-9]\d{9}$/
|
||||||
|
const MIN_NAME_LEN = 2
|
||||||
|
const MAX_NAME_LEN = 8
|
||||||
|
const NAME_REG = new RegExp(
|
||||||
|
`^[\\u4e00-\\u9fa5a-zA-Z·\\s]{${MIN_NAME_LEN},${MAX_NAME_LEN}}$`
|
||||||
|
)
|
||||||
|
const MEASURE_REG = /^\d+(\.\d)?$/
|
||||||
|
|
||||||
|
const MIN_HEIGHT = 50
|
||||||
|
const MAX_HEIGHT = 250
|
||||||
|
const MIN_WEIGHT = 20
|
||||||
|
const MAX_WEIGHT = 300
|
||||||
|
const MIN_BIRTH_YEAR = 1900
|
||||||
|
const MIN_AGE = 14
|
||||||
|
const MAX_FITNESS_GOALS = 5
|
||||||
|
|
||||||
|
export function isMaskedPhone(phone) {
|
||||||
|
return String(phone || '').includes('****')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateName(name) {
|
||||||
|
const value = String(name ?? '').trim()
|
||||||
|
if (!value) {
|
||||||
|
return { ok: false, message: '请输入姓名' }
|
||||||
|
}
|
||||||
|
if (!NAME_REG.test(value)) {
|
||||||
|
return { ok: false, message: `姓名为${MIN_NAME_LEN}-${MAX_NAME_LEN}个汉字或字母` }
|
||||||
|
}
|
||||||
|
return { ok: true, value }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 保存时使用:允许保留已脱敏的旧手机号 */
|
||||||
|
export function validatePhone(phone, options = {}) {
|
||||||
|
const { allowMasked = true } = options
|
||||||
|
const raw = String(phone ?? '').trim()
|
||||||
|
if (!raw) {
|
||||||
|
return { ok: false, message: '请绑定手机号' }
|
||||||
|
}
|
||||||
|
if (allowMasked && isMaskedPhone(raw)) {
|
||||||
|
const digits = raw.replace(/\D/g, '')
|
||||||
|
if (digits.length >= 7) {
|
||||||
|
return { ok: true, value: raw }
|
||||||
|
}
|
||||||
|
return { ok: false, message: '手机号格式不正确' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const digits = raw.replace(/\D/g, '')
|
||||||
|
if (!PHONE_REG.test(digits)) {
|
||||||
|
return { ok: false, message: '请输入11位有效手机号' }
|
||||||
|
}
|
||||||
|
return { ok: true, value: digits }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 换绑时必须输入完整新号 */
|
||||||
|
export function validatePhoneForRebind(phone) {
|
||||||
|
return validatePhone(phone, { allowMasked: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMeasure(value) {
|
||||||
|
const str = String(value ?? '').trim()
|
||||||
|
if (!str || !MEASURE_REG.test(str)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const num = Number(str)
|
||||||
|
return Number.isFinite(num) ? num : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMeasure(num) {
|
||||||
|
return Number.isInteger(num) ? String(num) : String(Number(num.toFixed(1)))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateHeight(height) {
|
||||||
|
const num = parseMeasure(height)
|
||||||
|
if (num == null) {
|
||||||
|
return { ok: false, message: '请输入有效身高(单位 cm)' }
|
||||||
|
}
|
||||||
|
if (num < MIN_HEIGHT || num > MAX_HEIGHT) {
|
||||||
|
return { ok: false, message: `身高请在 ${MIN_HEIGHT}-${MAX_HEIGHT} cm 之间` }
|
||||||
|
}
|
||||||
|
return { ok: true, value: formatMeasure(num) }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateWeight(weight) {
|
||||||
|
const num = parseMeasure(weight)
|
||||||
|
if (num == null) {
|
||||||
|
return { ok: false, message: '请输入有效体重(单位 kg)' }
|
||||||
|
}
|
||||||
|
if (num < MIN_WEIGHT || num > MAX_WEIGHT) {
|
||||||
|
return { ok: false, message: `体重请在 ${MIN_WEIGHT}-${MAX_WEIGHT} kg 之间` }
|
||||||
|
}
|
||||||
|
return { ok: true, value: formatMeasure(num) }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseBirthdayChinese(birthday) {
|
||||||
|
const match = String(birthday ?? '').match(/(\d{4})年(\d{2})月(\d{2})日/)
|
||||||
|
if (!match) return null
|
||||||
|
return {
|
||||||
|
year: Number(match[1]),
|
||||||
|
month: Number(match[2]),
|
||||||
|
day: Number(match[3])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateBirthday(birthday) {
|
||||||
|
const parts = parseBirthdayChinese(birthday)
|
||||||
|
if (!parts) {
|
||||||
|
return { ok: false, message: '请选择生日' }
|
||||||
|
}
|
||||||
|
const { year, month, day } = parts
|
||||||
|
if (year < MIN_BIRTH_YEAR) {
|
||||||
|
return { ok: false, message: '生日年份不合理' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date(year, month - 1, day)
|
||||||
|
if (
|
||||||
|
date.getFullYear() !== year ||
|
||||||
|
date.getMonth() !== month - 1 ||
|
||||||
|
date.getDate() !== day
|
||||||
|
) {
|
||||||
|
return { ok: false, message: '生日日期无效' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const today = new Date()
|
||||||
|
today.setHours(0, 0, 0, 0)
|
||||||
|
if (date > today) {
|
||||||
|
return { ok: false, message: '生日不能晚于今天' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const minBirth = new Date(
|
||||||
|
today.getFullYear() - MIN_AGE,
|
||||||
|
today.getMonth(),
|
||||||
|
today.getDate()
|
||||||
|
)
|
||||||
|
if (date > minBirth) {
|
||||||
|
return { ok: false, message: `需年满 ${MIN_AGE} 周岁` }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true, value: `${year}年${String(month).padStart(2, '0')}月${String(day).padStart(2, '0')}日` }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateGender(gender) {
|
||||||
|
if (gender === 'male' || gender === 'female') {
|
||||||
|
return { ok: true, value: gender }
|
||||||
|
}
|
||||||
|
return { ok: false, message: '请选择性别' }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateFitnessGoals(goals, options = []) {
|
||||||
|
const list = Array.isArray(goals) ? goals : []
|
||||||
|
const allowed = new Set(options)
|
||||||
|
const invalid = list.filter((g) => !allowed.has(g))
|
||||||
|
if (invalid.length) {
|
||||||
|
return { ok: false, message: '健身目标选项无效' }
|
||||||
|
}
|
||||||
|
if (list.length > MAX_FITNESS_GOALS) {
|
||||||
|
return { ok: false, message: `最多选择 ${MAX_FITNESS_GOALS} 个健身目标` }
|
||||||
|
}
|
||||||
|
return { ok: true, value: [...list] }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateUserProfile(profile, goalOptions = []) {
|
||||||
|
const nameResult = validateName(profile.name)
|
||||||
|
if (!nameResult.ok) return nameResult
|
||||||
|
|
||||||
|
const phoneResult = validatePhone(profile.phone)
|
||||||
|
if (!phoneResult.ok) return phoneResult
|
||||||
|
|
||||||
|
const genderResult = validateGender(profile.gender)
|
||||||
|
if (!genderResult.ok) return genderResult
|
||||||
|
|
||||||
|
const birthdayResult = validateBirthday(profile.birthday)
|
||||||
|
if (!birthdayResult.ok) return birthdayResult
|
||||||
|
|
||||||
|
const heightResult = validateHeight(profile.height)
|
||||||
|
if (!heightResult.ok) return heightResult
|
||||||
|
|
||||||
|
const weightResult = validateWeight(profile.weight)
|
||||||
|
if (!weightResult.ok) return weightResult
|
||||||
|
|
||||||
|
const goalsResult = validateFitnessGoals(profile.fitnessGoals, goalOptions)
|
||||||
|
if (!goalsResult.ok) return goalsResult
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
value: {
|
||||||
|
...profile,
|
||||||
|
name: nameResult.value,
|
||||||
|
phone: phoneResult.value,
|
||||||
|
gender: genderResult.value,
|
||||||
|
birthday: birthdayResult.value,
|
||||||
|
height: heightResult.value,
|
||||||
|
weight: weightResult.value,
|
||||||
|
fitnessGoals: goalsResult.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showValidationError(message) {
|
||||||
|
uni.showToast({ title: message, icon: 'none' })
|
||||||
|
}
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
/**
|
||||||
|
* ============================================
|
||||||
|
* 健身房管理系统小程序 - 全局配色变量
|
||||||
|
* 主题:清新健康运动风格
|
||||||
|
* 主色调:浅蓝渐变 + 活力橙点缀
|
||||||
|
* 兼容暗色/浅色模式基础,保证可访问性
|
||||||
|
* ============================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* ========== 主品牌色(清新浅蓝色系)========== */
|
||||||
|
--primary-dark: #0B2B4B; /* 深蓝主色 - 用于重要文字、品牌标识,体现专业信赖感 */
|
||||||
|
--primary-deep: #1A4A6F; /* 中深蓝色 - 用于hover状态、次级按钮、图标点缀,增加层次感 */
|
||||||
|
|
||||||
|
/* 主页主题浅蓝渐变色系 */
|
||||||
|
--primary-sky-100: #D6EEF8; /* 最浅蓝 - 渐变起始色,清新自然 */
|
||||||
|
--primary-sky-200: #E4F2FA; /* 浅蓝 - 渐变第二层 */
|
||||||
|
--primary-sky-300: #EEF6FB; /* 淡蓝 - 渐变第三层 */
|
||||||
|
--primary-sky-400: #F5FAFD; /* 微蓝 - 渐变第四层 */
|
||||||
|
--primary-sky-500: #FAFCFE; /* 极浅蓝 - 渐变第五层,接近白色 */
|
||||||
|
|
||||||
|
/* 光晕效果色 */
|
||||||
|
--glow-blue-1: rgba(160, 210, 235, 0.35); /* 蓝绿色光晕 */
|
||||||
|
--glow-blue-2: rgba(180, 220, 240, 0.3); /* 浅蓝色光晕 */
|
||||||
|
--glow-blue-3: rgba(170, 215, 238, 0.25); /* 浅蓝绿色光晕 */
|
||||||
|
|
||||||
|
/* ========== 强调/行动色(活力橙)========== */
|
||||||
|
--accent-orange: #FF6B35; /* 活力橙 - 主要CTA按钮、会员标识、高亮徽章、关键数据,刺激行动力 */
|
||||||
|
--accent-orange-light: #FF8C5A; /* 浅橙色 - hover轻量背景、渐变辅助,带来温暖运动感 */
|
||||||
|
--accent-orange-dark: #E55A2B; /* 深橙色 - 按压状态或重要警告,保持色彩体系完整 */
|
||||||
|
|
||||||
|
/* ========== 背景色系(主页主题)========== */
|
||||||
|
--bg-gradient-primary: linear-gradient(180deg, #D6EEF8 0%, #E4F2FA 15%, #EEF6FB 30%, #F5FAFD 50%, #FAFCFE 70%, #FFFFFF 100%); /* 主页主渐变背景 */
|
||||||
|
--bg-light: #F5FAFD; /* 全局浅蓝背景 - 柔和且提升蓝色/橙色的视觉舒适度 */
|
||||||
|
--bg-white: #FFFFFF; /* 纯白卡片背景 - 用于内容卡片、表单区域,提高可读性与层次感 */
|
||||||
|
--bg-gray: #F2F5F9; /* 浅灰辅助背景 - 分割区域或禁用态背景 */
|
||||||
|
|
||||||
|
/* ========== 文本色系 ========== */
|
||||||
|
--text-dark: #1E2A3A; /* 主要文字 - 标题、正文,保证高对比度 */
|
||||||
|
--text-muted: #5E6F8D; /* 辅助文字 - 次要信息、占位符,保持易读性 */
|
||||||
|
--text-light: #8A99B4; /* 更浅文字 - 提示语、时间戳,但需注意与背景对比 */
|
||||||
|
--text-inverse: #FFFFFF; /* 反白文字 - 深色/橙色背景上的文字 */
|
||||||
|
|
||||||
|
/* ========== 边框/分割线 ========== */
|
||||||
|
--border-light: #E9EDF2; /* 浅边框 - 卡片分割、列表边界,细腻柔和 */
|
||||||
|
--border-focus: #FF6B35; /* 聚焦边框 - 输入框选中或强调区域,使用橙色点缀 */
|
||||||
|
|
||||||
|
/* ========== 状态颜色(功能性) ========== */
|
||||||
|
--success-green: #2ECC71; /* 成功绿 - 已完成课程、健康打卡 */
|
||||||
|
--warning-amber: #F39C12; /* 警示橙黄 - 提醒、到期提示 */
|
||||||
|
--error-red: #E74C3C; /* 错误红 - 异常情况或取消预约 */
|
||||||
|
--info-blue: #3498DB; /* 信息蓝 - 提示气泡、帮助文字 */
|
||||||
|
|
||||||
|
/* ========== 渐变色 (提升活力感) ========== */
|
||||||
|
--gradient-orange: linear-gradient(135deg, #FF6B35 0%, #FF8C5A 100%); /* 橙色渐变 - 会员按钮、重要徽章 */
|
||||||
|
--gradient-blue: linear-gradient(135deg, #0B2B4B 0%, #1A4A6F 100%); /* 深蓝渐变 - 头部banner或特别卡片 */
|
||||||
|
--gradient-sky: linear-gradient(180deg, #D6EEF8 0%, #E4F2FA 15%, #EEF6FB 30%, #F5FAFD 50%, #FAFCFE 70%, #FFFFFF 100%); /* 主页天空渐变 - 全局背景 */
|
||||||
|
--gradient-subtle: linear-gradient(120deg, #F9FAFE 0%, #FFFFFF 100%); /* 微弱渐变 - 增加细节精致度 */
|
||||||
|
|
||||||
|
/* ========== TabBar 配色(清新蓝调风格)========== */
|
||||||
|
/* 引用位置:components/TabBar.vue */
|
||||||
|
--tabbar-bg: rgba(200, 225, 238, 0.8); /* TabBar背景色 - 半透明浅蓝色毛玻璃效果 */
|
||||||
|
--tabbar-shadow: rgba(120, 185, 215, 0.2); /* TabBar阴影色 - 蓝色系柔和阴影 */
|
||||||
|
--tabbar-icon-inactive: gray; /* 未选中图标颜色 - 灰色 */
|
||||||
|
--tabbar-icon-active: #5A98B0; /* 选中图标颜色 - 蓝绿色 */
|
||||||
|
--tabbar-text-inactive: #8AABBB; /* 未选中文字颜色 - 浅灰蓝 */
|
||||||
|
--tabbar-text-active: #5A98B0; /* 选中文字颜色 - 蓝绿色(与图标一致) */
|
||||||
|
|
||||||
|
/* ========== 通用蓝色系阴影(用于卡片、按钮等)========== */
|
||||||
|
/* 引用位置:components/index/RecommendCourses.vue, QuickEntry.vue, TodayRecommend.vue */
|
||||||
|
--shadow-blue-light: rgba(120, 185, 215, 0.18); /* 浅蓝色阴影 - 卡片悬浮效果 */
|
||||||
|
|
||||||
|
/* ========== 阴影层级 ========== */
|
||||||
|
--shadow-sm: 0 8px 20px rgba(0, 0, 0, 0.03), 0 2px 6px rgba(0, 0, 0, 0.05); /* 卡片小阴影 轻量浮起 */
|
||||||
|
--shadow-md: 0 12px 28px rgba(0, 0, 0, 0.08); /* 中等阴影 - 弹窗或下拉菜单 */
|
||||||
|
--shadow-lg: 0 20px 35px rgba(0, 0, 0, 0.12); /* 大阴影 - 模态框、悬浮元素 */
|
||||||
|
--shadow-orange-glow: 0 4px 12px rgba(255, 107, 53, 0.25); /* 橙色光晕 - 增强CTA吸引力 */
|
||||||
|
--shadow-sky-glow: 0 4px 12px rgba(160, 210, 235, 0.2); /* 蓝色光晕 - 主页效果增强 */
|
||||||
|
|
||||||
|
/* ========== 圆角规范 (柔和运动风) ========== */
|
||||||
|
--radius-sm: 12px; /* 小组件、标签圆角 */
|
||||||
|
--radius-md: 20px; /* 标准卡片圆角 */
|
||||||
|
--radius-lg: 28px; /* 大容器、头部卡片圆角 */
|
||||||
|
--radius-full: 999px; /* 胶囊按钮、头像完全圆角 */
|
||||||
|
|
||||||
|
/* ========== 布局与间距 ========== */
|
||||||
|
--spacing-xs: 4px;
|
||||||
|
--spacing-sm: 8px;
|
||||||
|
--spacing-md: 16px;
|
||||||
|
--spacing-lg: 24px;
|
||||||
|
--spacing-xl: 32px;
|
||||||
|
|
||||||
|
/* ========== 字体 (移动端优先) ========== */
|
||||||
|
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
--font-size-xs: 0.7rem; /* 辅助标注 */
|
||||||
|
--font-size-sm: 0.8rem; /* 次要文字 */
|
||||||
|
--font-size-base: 0.9rem; /* 正文基准 */
|
||||||
|
--font-size-md: 1rem; /* 小标题 */
|
||||||
|
--font-size-lg: 1.2rem; /* 卡片标题 */
|
||||||
|
--font-size-xl: 1.4rem; /* 大数字/欢迎语 */
|
||||||
|
--font-weight-regular: 400;
|
||||||
|
--font-weight-medium: 500;
|
||||||
|
--font-weight-bold: 700;
|
||||||
|
--font-weight-extrabold: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 暗色模式适配(可选,保持品牌一致性) ========== */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
/* 暗色模式下微调背景与文字,保留品牌色核心 */
|
||||||
|
--bg-light: #121826;
|
||||||
|
--bg-white: #1E2636;
|
||||||
|
--bg-gray: #0F141F;
|
||||||
|
--text-dark: #EDF2F7;
|
||||||
|
--text-muted: #9AA9C1;
|
||||||
|
--border-light: #2A3346;
|
||||||
|
--shadow-sm: 0 8px 20px rgba(0, 0, 0, 0.4);
|
||||||
|
/* 保留主色深蓝与橙色不变,但可适当提高对比 */
|
||||||
|
--primary-dark: #123A5E; /* 亮一点保证深色背景可见度 */
|
||||||
|
--accent-orange: #FF7846; /* 稍微提亮橙色 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 辅助类 (方便开发直接复用) ========== */
|
||||||
|
|
||||||
|
/* 背景色类 */
|
||||||
|
.bg-primary {
|
||||||
|
background-color: var(--primary-dark);
|
||||||
|
}
|
||||||
|
.bg-accent {
|
||||||
|
background-color: var(--accent-orange);
|
||||||
|
}
|
||||||
|
.bg-gradient-sky {
|
||||||
|
background: var(--gradient-sky);
|
||||||
|
}
|
||||||
|
.bg-gradient-primary {
|
||||||
|
background: var(--bg-gradient-primary);
|
||||||
|
}
|
||||||
|
.bg-sky-100 {
|
||||||
|
background-color: var(--primary-sky-100);
|
||||||
|
}
|
||||||
|
.bg-sky-200 {
|
||||||
|
background-color: var(--primary-sky-200);
|
||||||
|
}
|
||||||
|
.bg-sky-300 {
|
||||||
|
background-color: var(--primary-sky-300);
|
||||||
|
}
|
||||||
|
.bg-sky-400 {
|
||||||
|
background-color: var(--primary-sky-400);
|
||||||
|
}
|
||||||
|
.bg-sky-500 {
|
||||||
|
background-color: var(--primary-sky-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文字色类 */
|
||||||
|
.text-primary {
|
||||||
|
color: var(--primary-dark);
|
||||||
|
}
|
||||||
|
.text-accent {
|
||||||
|
color: var(--accent-orange);
|
||||||
|
}
|
||||||
|
.text-sky-100 {
|
||||||
|
color: var(--primary-sky-100);
|
||||||
|
}
|
||||||
|
.text-sky-200 {
|
||||||
|
color: var(--primary-sky-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 光晕效果类 */
|
||||||
|
.glow-blue-1 {
|
||||||
|
background: radial-gradient(circle, var(--glow-blue-1) 0%, transparent 70%);
|
||||||
|
}
|
||||||
|
.glow-blue-2 {
|
||||||
|
background: radial-gradient(circle, var(--glow-blue-2) 0%, transparent 70%);
|
||||||
|
}
|
||||||
|
.glow-blue-3 {
|
||||||
|
background: radial-gradient(circle, var(--glow-blue-3) 0%, transparent 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮类 */
|
||||||
|
.btn-orange {
|
||||||
|
background: var(--gradient-orange);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
box-shadow: var(--shadow-orange-glow);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
.btn-orange:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
background: var(--accent-orange-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片类 */
|
||||||
|
.card-default {
|
||||||
|
background: var(--bg-white);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
}
|
||||||
|
.card-sky {
|
||||||
|
background: var(--gradient-sky);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 阴影类 */
|
||||||
|
.shadow-sky {
|
||||||
|
box-shadow: var(--shadow-sky-glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 通用页面容器 */
|
||||||
|
.page-container-sky {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: var(--gradient-sky);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动容器 */
|
||||||
|
.scroll-container-sky {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
background: var(--gradient-sky);
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,25 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: "iconfont_courseCard"; /* Project id */
|
||||||
|
src: url('./font/iconfont_courseCard.ttf?t=1780537357472') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconfont_courseCard {
|
||||||
|
font-family: "iconfont_courseCard" !important;
|
||||||
|
font-size: 16px;
|
||||||
|
font-style: normal;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-didian:before {
|
||||||
|
content: "\e61a";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-renwu-ren:before {
|
||||||
|
content: "\e749";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-shijian:before {
|
||||||
|
content: "\e61d";
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: "iconfont_time_select"; /* Project id */
|
||||||
|
src: url('./font/iconfont_time_select.ttf?t=1780535096813') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconfont_time_select {
|
||||||
|
font-family: "iconfont_time_select" !important;
|
||||||
|
font-size: 25px;
|
||||||
|
font-style: normal;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-zaochen:before {
|
||||||
|
content: "\e784";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-gengduo:before {
|
||||||
|
content: "\e6df";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-xiawucha:before {
|
||||||
|
content: "\100ff";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-yewan:before {
|
||||||
|
content: "\e67e";
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
@import './member-info-page.css';
|
||||||
|
@import './member-info-status-bar.css';
|
||||||
|
@import './member-info-header.css';
|
||||||
|
@import './member-info-member-card.css';
|
||||||
|
@import './member-info-quick-actions.css';
|
||||||
|
@import './member-info-booking-list.css';
|
||||||
|
@import './member-info-check-in-list.css';
|
||||||
|
@import './member-info-body-report.css';
|
||||||
|
@import './member-info-coupon-points.css';
|
||||||
|
@import './member-info-referral.css';
|
||||||
|
@import './member-info-settings.css';
|
||||||
|
@import './member-info-logout.css';
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
.body-report-section {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-report-section__inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-report-section__header {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-report-section__header-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-report-section__title {
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-report-section__card {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow: 0px 2px 10px 0px rgba(26, 25, 24, 0.03137254901960784);
|
||||||
|
background-color: var(--bg-white, #ffffff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-report-section__card-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: var(--spacing-md, 16px);
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-report-section__card-head {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-report-section__card-head-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-report-section__desc {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-report-section__view-btn {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-report-section__view-icon {width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-report-section__view-report {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--accent-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-report-section__metrics {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-report-section__metrics-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-report-section__metric {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-report-section__metric-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-report-section__text {
|
||||||
|
font-size: var(--font-size-xl);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-report-section__text-2 {
|
||||||
|
font-size: var(--font-size-xl);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--success-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-report-section__text-4 {
|
||||||
|
font-size: var(--font-size-xl);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 700;
|
||||||
|
color: rgba(243, 156, 18, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-report-section__num {
|
||||||
|
font-size: var(--font-size-xl);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--primary-deep);
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-report-section__metric-value,
|
||||||
|
.body-report-section__text-3,
|
||||||
|
.body-report-section__metric-label,
|
||||||
|
.body-report-section__text-5 {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-report-section__metric-divider {
|
||||||
|
width: 1px;
|
||||||
|
height: 30px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background-color: var(--border-light, #e9edf2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-report-section__summary {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: var(--bg-light, #f9fafe);
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-report-section__summary-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-report-section__goal {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 58%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
align-items: center;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 100px;
|
||||||
|
background-color: rgba(240, 250, 245, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-report-section__goal-icon {width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-report-section__goal-text {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--success-green);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-report-section__change {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-report-section__change-icon {width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-report-section__metric-value-2 {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--success-green);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
.booking-section {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-section__inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-section__header {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-section__header-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-section__title {
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-dark);
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-section__link {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-section__item {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow: 0px 2px 10px 0px rgba(26, 25, 24, 0.03137254901960784);
|
||||||
|
background-color: var(--bg-white, #ffffff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-section__item-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 14px;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-section__date {
|
||||||
|
width: 48px;
|
||||||
|
height: 56px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: rgba(255, 243, 238, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-section__date-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-section__num {
|
||||||
|
font-size: var(--font-size-2xl);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent-orange);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-section__date-sub {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: var(--font-weight-regular);
|
||||||
|
color: var(--accent-orange-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-section__content {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
flex-shrink: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-section__content-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-section__desc {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
color: var(--text-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-section__meta {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-section__meta-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-section__icon-coach {width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-section__coach {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: var(--font-weight-regular);
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-section__icon-location {width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-section__text {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: var(--font-weight-regular);
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-section__status-wrap {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-section__status-badge {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-section__status-badge--booked {
|
||||||
|
background-color: var(--success-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-section__status-badge--pending {
|
||||||
|
background-color: rgba(255, 243, 238, 1);
|
||||||
|
border: 1px solid rgba(212, 166, 74, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-section__status-text {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
color: var(--text-inverse);
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-section__status-text--pending {
|
||||||
|
color: var(--accent-orange);
|
||||||
|
}
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
.checkin-section {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkin-section__inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkin-section__header {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkin-section__header-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkin-section__title {
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
color: var(--text-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkin-section__list {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
background-color: var(--bg-white);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkin-section__list-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkin-section__row {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkin-section__item {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkin-section__item-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 14px var(--spacing-md);
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkin-section__dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
background-color: var(--success-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkin-section__dot--group {
|
||||||
|
background-color: var(--success-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkin-section__dot--free {
|
||||||
|
background-color: var(--accent-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkin-section__dot--private {
|
||||||
|
background-color: var(--primary-deep);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkin-section__content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkin-section__content-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkin-section__desc {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
color: var(--text-dark);
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkin-section__text {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: var(--font-weight-regular);
|
||||||
|
color: var(--text-light);
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkin-section__tag-badge {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: var(--spacing-sm);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 3px var(--spacing-sm);
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: rgba(240, 250, 245, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkin-section__tag-badge--group {
|
||||||
|
background-color: rgba(240, 250, 245, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkin-section__tag-badge--free {
|
||||||
|
background-color: rgba(255, 243, 238, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkin-section__tag-badge--private {
|
||||||
|
background-color: rgba(235, 243, 250, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkin-section__tag-text {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
color: var(--success-green);
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkin-section__tag-text--group {
|
||||||
|
color: var(--success-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkin-section__tag-text--free {
|
||||||
|
color: var(--accent-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkin-section__tag-text--private {
|
||||||
|
color: var(--primary-deep);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkin-section__divider {
|
||||||
|
height: 1px;
|
||||||
|
background-color: var(--border-light);
|
||||||
|
margin-left: calc(var(--spacing-md) + 8px + 12px);
|
||||||
|
margin-right: var(--spacing-md);
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
/* 组件根节点:锁定浅色变量 + box-sizing(小程序组件内继承 page 的 theme-light 不稳定) */
|
||||||
|
.status-bar,
|
||||||
|
.profile-header,
|
||||||
|
.member-card-section,
|
||||||
|
.quick-actions,
|
||||||
|
.booking-section,
|
||||||
|
.checkin-section,
|
||||||
|
.body-report-section,
|
||||||
|
.coupon-section,
|
||||||
|
.referral-section,
|
||||||
|
.settings-section,
|
||||||
|
.logout-btn__border-wrap,
|
||||||
|
.logout-section {
|
||||||
|
box-sizing: border-box;
|
||||||
|
--primary-dark: #0B2B4B;
|
||||||
|
--primary-deep: #1A4A6F;
|
||||||
|
--primary-light: #2C6288;
|
||||||
|
--accent-orange: #FF6B35;
|
||||||
|
--accent-orange-light: #FF8C5A;
|
||||||
|
--accent-orange-dark: #E55A2B;
|
||||||
|
--bg-light: #F9FAFE;
|
||||||
|
--bg-white: #FFFFFF;
|
||||||
|
--bg-gray: #F2F5F9;
|
||||||
|
--text-dark: #1E2A3A;
|
||||||
|
--text-muted: #5E6F8D;
|
||||||
|
--text-light: #8A99B4;
|
||||||
|
--text-inverse: #FFFFFF;
|
||||||
|
--border-light: #E9EDF2;
|
||||||
|
--border-focus: #FF6B35;
|
||||||
|
--success-green: #2ECC71;
|
||||||
|
--warning-amber: #F39C12;
|
||||||
|
--error-red: #E74C3C;
|
||||||
|
--info-blue: #3498DB;
|
||||||
|
--spacing-xs: 4px;
|
||||||
|
--spacing-sm: 8px;
|
||||||
|
--spacing-md: 16px;
|
||||||
|
--spacing-lg: 24px;
|
||||||
|
--spacing-xl: 32px;
|
||||||
|
--radius-sm: 12px;
|
||||||
|
--radius-md: 20px;
|
||||||
|
--radius-lg: 28px;
|
||||||
|
--radius-full: 999px;
|
||||||
|
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
--font-size-xs: 11px;
|
||||||
|
--font-size-sm: 12px;
|
||||||
|
--font-size-base: 14px;
|
||||||
|
--font-size-md: 16px;
|
||||||
|
--font-size-lg: 18px;
|
||||||
|
--font-size-xl: 20px;
|
||||||
|
--font-size-2xl: 22px;
|
||||||
|
--font-size-3xl: 24px;
|
||||||
|
--font-size-4xl: 28px;
|
||||||
|
--font-size-5xl: 32px;
|
||||||
|
--font-weight-regular: 400;
|
||||||
|
--font-weight-medium: 500;
|
||||||
|
--font-weight-bold: 700;
|
||||||
|
--shadow-sm: 0 8px 20px rgba(0, 0, 0, 0.03), 0 2px 6px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 区块标题右侧操作链接:查看全部 / 全部记录 / 历史数据 / 推荐记录 */
|
||||||
|
.member-card-section__link-text,
|
||||||
|
.booking-section__view-all,
|
||||||
|
.checkin-section__view-all,
|
||||||
|
.coupon-section__view-all,
|
||||||
|
.body-report-section__history-link,
|
||||||
|
.referral-section__records-link {
|
||||||
|
font-size: var(--font-size-base, 14px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--accent-orange);
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-section__link,
|
||||||
|
.booking-section__link,
|
||||||
|
.checkin-section__link,
|
||||||
|
.coupon-section__link,
|
||||||
|
.body-report-section__link,
|
||||||
|
.referral-section__link {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-section__link-arrow,
|
||||||
|
.booking-section__link-arrow,
|
||||||
|
.checkin-section__link-arrow,
|
||||||
|
.coupon-section__link-arrow,
|
||||||
|
.body-report-section__link-arrow,
|
||||||
|
.referral-section__link-arrow {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
@import '@/common/style/memberInfo/member-info-gradient-cards.css';
|
||||||
|
|
||||||
|
.coupon-section {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-section__inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-section__header {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-section__header-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-section__title {
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-dark);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-section__link {
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-section__view-all {
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-section__link-arrow {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-section__cards {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-section__cards-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-section__coupon {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 0;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-section__coupon-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 14px 14px 14px 14px;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-section__amount {
|
||||||
|
font-size: var(--font-size-3xl);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-inverse);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-section__desc {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: rgba(255, 212, 184, 1);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-section__coupon-status {
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 2px 6px 2px 6px;
|
||||||
|
border-radius: 6px 6px 6px 6px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.1882352977991104);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-section__status {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-inverse);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-section__points {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 0;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-section__points-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 14px 14px 14px 14px;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-section__num {
|
||||||
|
font-size: var(--font-size-3xl);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-inverse);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-section__points-label {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: rgba(255, 212, 184, 1);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-section__points-action {
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 2px 6px 2px 6px;
|
||||||
|
border-radius: 6px 6px 6px 6px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.1882352977991104);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-section__text {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-inverse);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* 与个人中心首页 coupon-section 一致的渐变卡片背景
|
||||||
|
* 使用 SVG 背景,兼容微信小程序(CSS 变量渐变在部分端不生效)
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mi-gradient-blue,
|
||||||
|
.coupon-section__points,
|
||||||
|
.mi-mod-points-hero,
|
||||||
|
.bt-hero,
|
||||||
|
.bt-score-card,
|
||||||
|
.mi-mod-referral-hero {
|
||||||
|
background-color: #0B2B4B;
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
|
background-image: url("data:image/svg+xml;utf8,%3Csvg%20viewBox%3D'0%200%20174%20106'%20preserveAspectRatio%3D'none'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%0A%20%20%20%20%20%20%3Cdefs%3E%0A%20%20%20%20%20%20%20%20%3ClinearGradient%20id%3D'grad'%20gradientUnits%3D'objectBoundingBox'%20x1%3D'0'%20y1%3D'0.5'%20x2%3D'1'%20y2%3D'0.5'%20gradientTransform%3D'matrix(-0.7071%2C%200.7071%2C%20-0.7071%2C%20-0.7071%2C%201.2071%2C%200.5000)'%3E%0A%20%20%20%20%20%20%20%20%20%20%3Cstop%20stop-color%3D'rgba(11%2C43%2C75%2C1)'%20offset%3D'0'%2F%3E%3Cstop%20stop-color%3D'rgba(26%2C74%2C111%2C1)'%20offset%3D'1'%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FlinearGradient%3E%0A%20%20%20%20%20%20%3C%2Fdefs%3E%0A%20%20%20%20%20%20%3Crect%20width%3D'100%25'%20height%3D'100%25'%20fill%3D'url(%23grad)'%2F%3E%0A%20%20%20%20%3C%2Fsvg%3E");
|
||||||
|
box-shadow: 0px 4px 8px 0px rgba(11, 43, 75, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-gradient-orange,
|
||||||
|
.coupon-section__coupon,
|
||||||
|
.mi-mod-coupon__left,
|
||||||
|
.mi-mod-coupon__use,
|
||||||
|
.mi-center-coupon__btn:not(.mi-center-coupon__btn--done),
|
||||||
|
.bt-page__action-link--primary,
|
||||||
|
.bt-btn--primary {
|
||||||
|
background-color: #FF6B35;
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
|
background-image: url("data:image/svg+xml;utf8,%3Csvg%20viewBox%3D'0%200%20174%20106'%20preserveAspectRatio%3D'none'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%0A%20%20%20%20%20%20%3Cdefs%3E%0A%20%20%20%20%20%20%20%20%3ClinearGradient%20id%3D'grad'%20gradientUnits%3D'objectBoundingBox'%20x1%3D'0'%20y1%3D'0.5'%20x2%3D'1'%20y2%3D'0.5'%20gradientTransform%3D'matrix(-0.7071%2C%200.7071%2C%20-0.7071%2C%20-0.7071%2C%201.2071%2C%200.5000)'%3E%0A%20%20%20%20%20%20%20%20%20%20%3Cstop%20stop-color%3D'rgba(255%2C107%2C53%2C1)'%20offset%3D'0'%2F%3E%3Cstop%20stop-color%3D'rgba(255%2C140%2C90%2C1)'%20offset%3D'1'%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FlinearGradient%3E%0A%20%20%20%20%20%20%3C%2Fdefs%3E%0A%20%20%20%20%20%20%3Crect%20width%3D'100%25'%20height%3D'100%25'%20fill%3D'url(%23grad)'%2F%3E%0A%20%20%20%20%3C%2Fsvg%3E");
|
||||||
|
box-shadow: 0px 4px 8px 0px rgba(255, 107, 53, 0.25);
|
||||||
|
}
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
.profile-header {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶栏固定:仅白底导航栏吸顶,下方用户信息可滚动 */
|
||||||
|
.profile-header__toolbar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--bg-white, #ffffff);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header__toolbar-spacer {
|
||||||
|
width: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header__nav {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
height: 44px;
|
||||||
|
padding-left: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header__nav-left {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
min-width: 56px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header__nav-right {
|
||||||
|
min-width: 72px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header__title {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
max-width: 42%;
|
||||||
|
text-align: center;
|
||||||
|
font-size: var(--font-size-md, 16px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-dark);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header__icon-bell,
|
||||||
|
.profile-header__icon-settings {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* settings.png 为白色线稿,白底顶栏需着色后才可见 */
|
||||||
|
.profile-header__icon-settings {
|
||||||
|
filter: brightness(0) saturate(100%) invert(52%) sepia(98%) saturate(1800%) hue-rotate(346deg) brightness(102%) contrast(101%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 用户信息渐变区 */
|
||||||
|
.profile-header__hero {
|
||||||
|
width: 100%;
|
||||||
|
background-position: center;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-image: url("data:image/svg+xml;utf8,%3Csvg%20viewBox%3D'0%200%20390%20239'%20preserveAspectRatio%3D'none'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%0A%20%20%20%20%20%20%3Cdefs%3E%0A%20%20%20%20%20%20%20%20%3ClinearGradient%20id%3D'grad'%20gradientUnits%3D'objectBoundingBox'%20x1%3D'0'%20y1%3D'0.5'%20x2%3D'1'%20y2%3D'0.5'%20gradientTransform%3D'matrix(-0.0000%2C%201.0000%2C%20-1.0000%2C%20-0.0000%2C%201.0000%2C%200.0000)'%3E%0A%20%20%20%20%20%20%20%20%20%20%3Cstop%20stop-color%3D'rgba(11%2C43%2C75%2C1)'%20offset%3D'0'%2F%3E%3Cstop%20stop-color%3D'rgba(26%2C74%2C111%2C1)'%20offset%3D'1'%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FlinearGradient%3E%0A%20%20%20%20%20%20%3C%2Fdefs%3E%0A%20%20%20%20%20%20%3Crect%20width%3D'100%25'%20height%3D'100%25'%20fill%3D'url(%23grad)'%2F%3E%0A%20%20%20%20%3C%2Fsvg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header__inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 16px 20px 28px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header__user {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header__user-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 14px;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header__avatar-wrap {
|
||||||
|
position: relative;
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header__avatar-ring {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid #ffffff;
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: rgba(255, 255, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header__avatar {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header__avatar-badge {
|
||||||
|
position: absolute;
|
||||||
|
right: -1px;
|
||||||
|
bottom: -1px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid #ffffff;
|
||||||
|
background-color: #2ecc71;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 2;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header__avatar-badge-icon {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
display: block;
|
||||||
|
flex-shrink: 0;
|
||||||
|
filter: brightness(0) invert(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header__user-meta {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header__user-meta-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-xs, 4px);
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header__name {
|
||||||
|
font-size: var(--font-size-xl, 20px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-inverse);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header__phone {
|
||||||
|
font-size: var(--font-size-base, 14px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: rgba(255, 212, 184, 1);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header__badge {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--spacing-xs, 4px);
|
||||||
|
align-items: center;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: var(--accent-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header__badge-icon {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header__level {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-inverse);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header__stats {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header__stats-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding-top: 16px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header__stat {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header__stat-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header__stat-value {
|
||||||
|
font-size: var(--font-size-2xl, 22px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-inverse);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header__stat-label {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: rgba(255, 212, 184, 1);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header__stat-divider {
|
||||||
|
width: 1px;
|
||||||
|
height: 32px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background-color: rgba(255, 255, 255, 0.31);
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
.logout-section {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-section__btn {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 50px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow: 0 2px 10px rgba(26, 25, 24, 0.03);
|
||||||
|
background-color: #ffffff;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-section__icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-section__text {
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #e74c3c;
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-tap-btn--hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
@@ -0,0 +1,362 @@
|
|||||||
|
.member-card-section {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-section__inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-section__head {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-section__head-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-section__title {
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-dark);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-section__link {
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-section__link-text {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-section__link-arrow {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-preview {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 140px;
|
||||||
|
height: auto;
|
||||||
|
overflow: visible;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0px 8px 16px 0px rgba(255, 107, 53, 0.25);
|
||||||
|
background-position: center;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-image: url("data:image/svg+xml;utf8,%3Csvg%20viewBox%3D'0%200%20358%20140'%20preserveAspectRatio%3D'none'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%0A%20%20%20%20%20%20%3Cdefs%3E%0A%20%20%20%20%20%20%20%20%3ClinearGradient%20id%3D'grad'%20gradientUnits%3D'objectBoundingBox'%20x1%3D'0'%20y1%3D'0.5'%20x2%3D'1'%20y2%3D'0.5'%20gradientTransform%3D'matrix(-0.7071%2C%200.7071%2C%20-0.7071%2C%20-0.7071%2C%201.2071%2C%200.5000)'%3E%0A%20%20%20%20%20%20%20%20%20%20%3Cstop%20stop-color%3D'rgba(255%2C107%2C53%2C1)'%20offset%3D'0'%2F%3E%3Cstop%20stop-color%3D'rgba(255%2C140%2C90%2C1)'%20offset%3D'0.6000000238418579'%2F%3E%3Cstop%20stop-color%3D'rgba(229%2C90%2C43%2C1)'%20offset%3D'1'%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FlinearGradient%3E%0A%20%20%20%20%20%20%3C%2Fdefs%3E%0A%20%20%20%20%20%20%3Crect%20width%3D'100%25'%20height%3D'100%25'%20fill%3D'url(%23grad)'%2F%3E%0A%20%20%20%20%3C%2Fsvg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-preview__inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: stretch;
|
||||||
|
padding: 20px 16px 16px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-preview__head {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-preview__head-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-preview__type-row {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-preview__icon-wrap {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-preview__icon-bg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
border-radius: 1.5px 1.5px 1.5px 1.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-preview__icon-border {
|
||||||
|
position: absolute;
|
||||||
|
left: 8.33%;
|
||||||
|
top: 20.83%;
|
||||||
|
right: 8.33%;
|
||||||
|
bottom: 20.83%;
|
||||||
|
width: 83.34%;
|
||||||
|
height: 58.34%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-preview__icon-stroke {
|
||||||
|
position: absolute;
|
||||||
|
inset: -0.75px -0.75px -0.75px -0.75px;
|
||||||
|
border-radius: 1.5px 1.5px 1.5px 1.5px;
|
||||||
|
pointer-events: none;
|
||||||
|
border-width: 1.5px 1.5px 1.5px 1.5px;
|
||||||
|
border-style: solid;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-color: var(--text-inverse);
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-preview__icon-line {width: 83.34%;
|
||||||
|
height: 8.33%;
|
||||||
|
position: absolute;
|
||||||
|
left: 8.33%;
|
||||||
|
right: 8.33%;
|
||||||
|
top: 41.67%;
|
||||||
|
bottom: 50%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-preview__name {
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-inverse);
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-preview__tag {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.19);
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-preview__tag-text {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-inverse);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-preview__expire {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: rgba(255, 212, 184, 1);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-preview__footer {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-preview__footer-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-preview__days {
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-preview__days-num {
|
||||||
|
font-size: var(--font-size-4xl);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-inverse);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-preview__days-unit {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: rgba(255, 212, 184, 1);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-preview__renew {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background-color: var(--bg-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-preview__renew-text {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--accent-orange);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-tip__inner {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 10px 10px 10px 10px;
|
||||||
|
background-color: rgba(255, 243, 238, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-tip__content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px 8px 12px;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-tip {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-tip__border {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0px;
|
||||||
|
border-radius: 10px 10px 10px 10px;
|
||||||
|
pointer-events: none;
|
||||||
|
border-width: 1px 1px 1px 1px;
|
||||||
|
border-style: solid;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-color: rgba(255, 204, 170, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-tip__icon {width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-tip__text {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--accent-orange);
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
.scroll-container {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: var(--gradient-sky);
|
||||||
|
--spacing-xs: 4px;
|
||||||
|
--spacing-sm: 8px;
|
||||||
|
--spacing-md: 16px;
|
||||||
|
--spacing-lg: 24px;
|
||||||
|
--spacing-xl: 32px;
|
||||||
|
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
--font-size-xs: 11px;
|
||||||
|
--font-size-sm: 12px;
|
||||||
|
--font-size-base: 14px;
|
||||||
|
--font-size-md: 16px;
|
||||||
|
--font-size-lg: 18px;
|
||||||
|
--font-size-xl: 20px;
|
||||||
|
--font-size-2xl: 22px;
|
||||||
|
--font-size-3xl: 24px;
|
||||||
|
--font-size-4xl: 28px;
|
||||||
|
--font-size-5xl: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-container > view {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-page {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
height: auto;
|
||||||
|
overflow: visible;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
background: var(--gradient-sky);
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding-bottom: calc(120rpx + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-page__body {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-page__sections {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: stretch;
|
||||||
|
padding: var(--spacing-md, 16px);
|
||||||
|
padding-bottom: calc(var(--spacing-md, 16px) + env(safe-area-inset-bottom));
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-page__sections text {
|
||||||
|
font-family: var(--font-family);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ????????? */
|
||||||
|
.status-bar,
|
||||||
|
.profile-header,
|
||||||
|
.member-card-section,
|
||||||
|
.quick-actions,
|
||||||
|
.booking-section,
|
||||||
|
.checkin-section,
|
||||||
|
.body-report-section,
|
||||||
|
.coupon-section,
|
||||||
|
.referral-section,
|
||||||
|
.settings-section,
|
||||||
|
.logout-btn__border-wrap,
|
||||||
|
.logout-section {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
.quick-actions {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0px 2px 12px 0px rgba(26, 25, 24, 0.03137254901960784);
|
||||||
|
background-color: var(--bg-white, #ffffff);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions__inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions__grid {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions__grid-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions__item {
|
||||||
|
flex: 1;
|
||||||
|
height: 80px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions__item-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions__icon-wrap {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions__icon-wrap-inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions__icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions__icon-part {
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions__icon-part:nth-child(1) {
|
||||||
|
width: 7.69%;
|
||||||
|
height: 16.67%;
|
||||||
|
left: 30.77%;
|
||||||
|
top: 8.33%;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions__icon-part:nth-child(2) {
|
||||||
|
width: 7.69%;
|
||||||
|
height: 16.67%;
|
||||||
|
left: 61.54%;
|
||||||
|
top: 8.33%;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions__icon-part:nth-child(4) {
|
||||||
|
width: 7.69%;
|
||||||
|
height: 16.67%;
|
||||||
|
left: 30.77%;
|
||||||
|
top: 58.33%;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions__icon-part:nth-child(5) {
|
||||||
|
width: 7.69%;
|
||||||
|
height: 16.67%;
|
||||||
|
left: 61.54%;
|
||||||
|
top: 58.33%;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions__border-wrap {
|
||||||
|
position: absolute;
|
||||||
|
left: 12.5%;
|
||||||
|
top: 25%;
|
||||||
|
width: 75%;
|
||||||
|
height: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions__rect {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions__border {
|
||||||
|
position: absolute;
|
||||||
|
inset: -1px;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid var(--accent-orange);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions__icon-img {width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions__title,
|
||||||
|
.quick-actions__title-2,
|
||||||
|
.quick-actions__title-3,
|
||||||
|
.quick-actions__title-4,
|
||||||
|
.quick-actions__coach,
|
||||||
|
.quick-actions__text,
|
||||||
|
.quick-actions__text-2,
|
||||||
|
.quick-actions__points-desc {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-dark);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions__divider {
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
background-color: var(--border-light, #e9edf2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 第�?�?�??�?*/
|
||||||
|
.quick-actions__grid:nth-of-type(1) .quick-actions__item:nth-child(1),
|
||||||
|
.quick-actions__grid:nth-of-type(1) .quick-actions__item:nth-child(1) .quick-actions__icon-wrap {
|
||||||
|
background-color: rgba(255, 243, 238, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions__grid:nth-of-type(1) .quick-actions__item:nth-child(2),
|
||||||
|
.quick-actions__grid:nth-of-type(1) .quick-actions__item:nth-child(2) .quick-actions__icon-wrap {
|
||||||
|
background-color: rgba(240, 250, 245, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions__grid:nth-of-type(1) .quick-actions__item:nth-child(2) .quick-actions__icon-img {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions__grid:nth-of-type(1) .quick-actions__item:nth-child(3),
|
||||||
|
.quick-actions__grid:nth-of-type(1) .quick-actions__item:nth-child(3) .quick-actions__icon-wrap {
|
||||||
|
background-color: rgba(235, 243, 250, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions__grid:nth-of-type(1) .quick-actions__item:nth-child(3) .quick-actions__icon-img {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions__grid:nth-of-type(1) .quick-actions__item:nth-child(4),
|
||||||
|
.quick-actions__grid:nth-of-type(1) .quick-actions__item:nth-child(4) .quick-actions__icon-wrap {
|
||||||
|
background-color: rgba(255, 243, 238, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions__grid:nth-of-type(1) .quick-actions__item:nth-child(4) .quick-actions__icon-img {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 第�?�?�??�?*/
|
||||||
|
.quick-actions__grid:nth-of-type(3) .quick-actions__item:nth-child(1),
|
||||||
|
.quick-actions__grid:nth-of-type(3) .quick-actions__item:nth-child(1) .quick-actions__icon-wrap {
|
||||||
|
background-color: rgba(255, 236, 236, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions__grid:nth-of-type(3) .quick-actions__item:nth-child(1) .quick-actions__icon-img {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions__grid:nth-of-type(3) .quick-actions__item:nth-child(2),
|
||||||
|
.quick-actions__grid:nth-of-type(3) .quick-actions__item:nth-child(2) .quick-actions__icon-wrap {
|
||||||
|
background-color: rgba(255, 243, 238, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions__grid:nth-of-type(3) .quick-actions__item:nth-child(2) .quick-actions__icon-img {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions__grid:nth-of-type(3) .quick-actions__item:nth-child(3),
|
||||||
|
.quick-actions__grid:nth-of-type(3) .quick-actions__item:nth-child(3) .quick-actions__icon-wrap {
|
||||||
|
background-color: rgba(240, 250, 245, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions__grid:nth-of-type(3) .quick-actions__item:nth-child(3) .quick-actions__icon-img {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions__grid:nth-of-type(3) .quick-actions__item:nth-child(4),
|
||||||
|
.quick-actions__grid:nth-of-type(3) .quick-actions__item:nth-child(4) .quick-actions__icon-wrap {
|
||||||
|
background-color: rgba(235, 243, 250, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions__grid:nth-of-type(3) .quick-actions__item:nth-child(4) .quick-actions__icon-img {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
.referral-section {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow: 0 2px 10px rgba(26, 25, 24, 0.03);
|
||||||
|
background-color: var(--bg-white, #ffffff);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-section__inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-section__header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-section__title {
|
||||||
|
font-size: var(--font-size-md, 16px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-dark);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-section__link {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-section__records-link {
|
||||||
|
font-size: var(--font-size-base, 14px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--accent-orange);
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-section__link-arrow {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
display: block;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 推荐码行:grid 避免小程序 flex 宽度计算异常 */
|
||||||
|
.referral-section__code-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
column-gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-section__code-box {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #f2f5f9;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-section__code-label {
|
||||||
|
display: block;
|
||||||
|
font-size: var(--font-size-xs, 11px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-muted);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-section__code-value {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
font-size: var(--font-size-base, 14px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--primary-dark);
|
||||||
|
line-height: 1.3;
|
||||||
|
letter-spacing: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-section__copy-btn {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 52px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: var(--accent-orange);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 例图:双矩形复制图标 */
|
||||||
|
.referral-section__copy-icon {
|
||||||
|
position: relative;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-section__copy-sheet {
|
||||||
|
position: absolute;
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
border: 1.5px solid #ffffff;
|
||||||
|
border-radius: 2px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-section__copy-sheet--back {
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-section__copy-sheet--front {
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-section__copy-text {
|
||||||
|
display: block;
|
||||||
|
font-size: var(--font-size-base, 14px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-inverse);
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-section__stats {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-section__stat {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-section__stat-num {
|
||||||
|
display: block;
|
||||||
|
font-size: var(--font-size-lg, 18px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-section__stat-num--orange {
|
||||||
|
color: var(--accent-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-section__stat-num--green {
|
||||||
|
color: var(--success-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-section__stat-num--amber {
|
||||||
|
color: #f39c12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-section__stat-label {
|
||||||
|
display: block;
|
||||||
|
font-size: var(--font-size-xs, 11px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-light);
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-section__stat-divider {
|
||||||
|
width: 1px;
|
||||||
|
height: 28px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background-color: var(--border-light);
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
.settings-section {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
--bg-white: #ffffff;
|
||||||
|
--text-dark: #1e2a3a;
|
||||||
|
--text-light: #8a99b4;
|
||||||
|
--error-red: #e74c3c;
|
||||||
|
--success-green: #2ecc71;
|
||||||
|
--border-light: #e9edf2;
|
||||||
|
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
--font-size-xs: 11px;
|
||||||
|
--font-size-base: 14px;
|
||||||
|
--font-size-md: 16px;
|
||||||
|
--font-weight-medium: 500;
|
||||||
|
--font-weight-bold: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section__inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section__title {
|
||||||
|
display: block;
|
||||||
|
font-size: 16px;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e2a3a;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section__list {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow: 0 2px 10px rgba(26, 25, 24, 0.03);
|
||||||
|
background-color: #ffffff;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section__list-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section__item {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 52px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section__item--tall {
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section__item-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section__item-icon-wrap {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: rgba(255, 243, 238, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section__item-icon-wrap--blue {
|
||||||
|
background-color: rgba(235, 243, 250, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section__item-icon-wrap--green {
|
||||||
|
background-color: rgba(240, 250, 245, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section__item-icon-wrap--red {
|
||||||
|
background-color: rgba(255, 236, 236, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section__item-icon-wrap-inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section__item-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section__item-label {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1e2a3a;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section__item-label--danger {
|
||||||
|
color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section__item-texts {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section__item-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1e2a3a;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section__item-desc {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: #2ecc71;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section__item-arrow {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section__item-divider {
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
background-color: #e9edf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-tap-row--hover {
|
||||||
|
opacity: 0.72;
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
.status-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 62px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar__inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0px 20px 0px 20px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar__time {
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-inverse);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar__icons {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-inverse);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
.sub-nav {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-nav__toolbar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
width: 100%;
|
||||||
|
background-color: #ffffff;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-bottom: 1px solid #e9edf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-nav__spacer {
|
||||||
|
width: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-nav__nav {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
height: 44px;
|
||||||
|
padding-left: 12px;
|
||||||
|
padding-right: 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-nav__back {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
min-width: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #f9fafe;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-nav__back-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
max-width: 20px;
|
||||||
|
max-height: 20px;
|
||||||
|
display: block;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-nav__title {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
max-width: 50%;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 16px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e2a3a;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-nav__right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: auto;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-nav__action {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-nav__action-text {
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #ff6b35;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-nav__action--button {
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #ff6b35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-nav__action--button .sub-nav__action-text {
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-nav__capsule {
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-nav__capsule--h5 {
|
||||||
|
width: 0 !important;
|
||||||
|
min-width: 0 !important;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/* 小程序点击反馈(配合 hover-class 使用) */
|
||||||
|
.mi-tap--hover,
|
||||||
|
.mi-tap--scale,
|
||||||
|
.mi-tap-card--hover,
|
||||||
|
.mi-tap-btn--hover,
|
||||||
|
.mi-tap-tab--hover,
|
||||||
|
.mi-tap-save--hover {
|
||||||
|
transition: opacity 0.15s ease, transform 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-tap--scale {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-tap-card--hover {
|
||||||
|
opacity: 0.92;
|
||||||
|
transform: scale(0.995);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-tap-btn--hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-tap-tab--hover {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-tap-row--hover {
|
||||||
|
background-color: rgba(249, 250, 254, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-tap-save--hover {
|
||||||
|
opacity: 0.88;
|
||||||
|
transform: scale(0.99);
|
||||||
|
}
|
||||||
@@ -0,0 +1,860 @@
|
|||||||
|
/* 智能体测模块 - 公共样式(基于 base.css 变量) */
|
||||||
|
@import '@/common/style/memberInfo/member-info-gradient-cards.css';
|
||||||
|
|
||||||
|
.bt-page {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow-x: hidden;
|
||||||
|
background-color: var(--bg-light, #F9FAFE);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-page__body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-sm, 12px);
|
||||||
|
padding: var(--spacing-md, 16px) var(--spacing-md, 16px) 40px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-card {
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: var(--radius-md, 20px);
|
||||||
|
background-color: var(--bg-white, #fff);
|
||||||
|
box-shadow: var(--shadow-sm, 0 8px 20px rgba(0, 0, 0, 0.03));
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-card__title {
|
||||||
|
font-size: var(--font-size-md, 16px);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-dark, #1E2A3A);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-card__desc {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
color: var(--text-muted, #5E6F8D);
|
||||||
|
line-height: 1.5;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-hero {
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: var(--radius-sm, 12px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-hero__top {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-hero__label {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
color: rgba(255, 255, 255, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-hero__badge {
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-hero__badge-text {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
color: var(--text-inverse, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-hero__score-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-hero__score {
|
||||||
|
font-size: 48px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--text-inverse, #fff);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-hero__grade {
|
||||||
|
font-size: var(--font-size-xl, 20px);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent-orange-light, #FF8C5A);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-hero__meta {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
color: rgba(255, 212, 184, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-hero__actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-btn {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: var(--radius-full, 999px);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-btn--primary {
|
||||||
|
box-shadow: var(--shadow-orange-glow, 0 4px 12px rgba(255, 107, 53, 0.25));
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-btn--ghost {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-btn--outline {
|
||||||
|
background: var(--bg-white, #fff);
|
||||||
|
border: 1px solid var(--border-light, #E9EDF2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-btn__text {
|
||||||
|
font-size: var(--font-size-base, 14px);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-btn--primary .bt-btn__text,
|
||||||
|
.bt-btn--ghost .bt-btn__text {
|
||||||
|
color: var(--text-inverse, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-btn--outline .bt-btn__text {
|
||||||
|
color: var(--primary-dark, #0B2B4B);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-btn__icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-grid__item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 12px 4px;
|
||||||
|
border-radius: var(--radius-sm, 12px);
|
||||||
|
background: var(--bg-gray, #F2F5F9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-grid__icon {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-grid__label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted, #5E6F8D);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-device {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-device__icon-wrap {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--bg-gray, #F2F5F9);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-device__icon {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-device__info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-device__name {
|
||||||
|
font-size: var(--font-size-base, 14px);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-dark, #1E2A3A);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-device__status {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
color: var(--text-muted, #5E6F8D);
|
||||||
|
display: block;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-device__status--on {
|
||||||
|
color: var(--success-green, #2ECC71);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-device__dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--text-light, #8A99B4);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-device__dot--on {
|
||||||
|
background: var(--success-green, #2ECC71);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-metrics {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-metric {
|
||||||
|
padding: 14px;
|
||||||
|
border-radius: var(--radius-sm, 12px);
|
||||||
|
background: var(--bg-gray, #F2F5F9);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-metric__value {
|
||||||
|
font-size: var(--font-size-xl, 20px);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-dark, #1E2A3A);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-metric__label {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
color: var(--text-muted, #5E6F8D);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-metric__change {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-metric__change--down {
|
||||||
|
color: var(--success-green, #2ECC71);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-metric__change--up {
|
||||||
|
color: var(--warning-amber, #F39C12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-steps {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-step {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-step__num {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--primary-dark, #0B2B4B);
|
||||||
|
color: var(--text-inverse, #fff);
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
font-weight: 700;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-step__content {
|
||||||
|
flex: 1;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid var(--border-light, #E9EDF2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-step:last-child .bt-step__content {
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-step__title {
|
||||||
|
font-size: var(--font-size-base, 14px);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-dark, #1E2A3A);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-step__desc {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
color: var(--text-muted, #5E6F8D);
|
||||||
|
margin-top: 4px;
|
||||||
|
display: block;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-measure {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 24px 16px;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-measure__ring-wrap {
|
||||||
|
position: relative;
|
||||||
|
width: 180px;
|
||||||
|
height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-measure__ring-bg {
|
||||||
|
width: 180px;
|
||||||
|
height: 180px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 10px solid var(--border-light, #E9EDF2);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-measure__ring-fill {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 180px;
|
||||||
|
height: 180px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 10px solid transparent;
|
||||||
|
border-top-color: var(--accent-orange, #FF6B35);
|
||||||
|
border-right-color: var(--accent-orange, #FF6B35);
|
||||||
|
box-sizing: border-box;
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-measure__center {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-measure__percent {
|
||||||
|
font-size: 36px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--primary-dark, #0B2B4B);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-measure__hint {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
color: var(--text-muted, #5E6F8D);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-measure__live {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-measure__live-item {
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: var(--radius-sm, 12px);
|
||||||
|
background: var(--bg-gray, #F2F5F9);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-measure__live-value {
|
||||||
|
font-size: var(--font-size-lg, 18px);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-dark, #1E2A3A);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-measure__live-label {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
color: var(--text-muted, #5E6F8D);
|
||||||
|
margin-top: 2px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-score-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: var(--radius-sm, 12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-score-card__circle {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-score-card__num {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--text-inverse, #fff);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-score-card__grade {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--accent-orange-light, #FF8C5A);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-score-card__info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-score-card__title {
|
||||||
|
font-size: var(--font-size-md, 16px);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-inverse, #fff);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-score-card__date {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
margin-top: 4px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-body-map {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-body-map__figure {
|
||||||
|
width: 120px;
|
||||||
|
height: 200px;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-body-map__head {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--primary-light, #2C6288);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-body-map__torso {
|
||||||
|
width: 56px;
|
||||||
|
height: 70px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--primary-deep, #1A4A6F);
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-body-map__limbs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 48px;
|
||||||
|
margin-top: -60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-body-map__arm {
|
||||||
|
width: 16px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--primary-light, #2C6288);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-body-map__legs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-body-map__leg {
|
||||||
|
width: 22px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--primary-light, #2C6288);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-body-map__segments {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-body-map__seg {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--bg-gray, #F2F5F9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-body-map__seg--high {
|
||||||
|
border-left: 3px solid var(--accent-orange, #FF6B35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-body-map__seg--low {
|
||||||
|
border-left: 3px solid var(--info-blue, #3498DB);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-body-map__seg-name {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
color: var(--text-dark, #1E2A3A);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-body-map__seg-val {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted, #5E6F8D);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-advice-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-advice-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-advice-item__dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent-orange, #FF6B35);
|
||||||
|
margin-top: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-advice-item__text {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
color: var(--text-muted, #5E6F8D);
|
||||||
|
line-height: 1.5;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-course {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid var(--border-light, #E9EDF2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-course:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-course__banner {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
border-radius: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-course__info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-course__tag {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--accent-orange, #FF6B35);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-course__title {
|
||||||
|
font-size: var(--font-size-base, 14px);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-dark, #1E2A3A);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-course__meta {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
color: var(--text-muted, #5E6F8D);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-history-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px 0;
|
||||||
|
border-bottom: 1px solid var(--border-light, #E9EDF2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-history-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-history-item__date {
|
||||||
|
width: 52px;
|
||||||
|
text-align: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-history-item__day {
|
||||||
|
font-size: var(--font-size-xl, 20px);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--primary-dark, #0B2B4B);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-history-item__month {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-muted, #5E6F8D);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-history-item__info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-history-item__score-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-history-item__grade {
|
||||||
|
font-size: var(--font-size-base, 14px);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-dark, #1E2A3A);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-history-item__status {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
color: var(--text-muted, #5E6F8D);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-history-item__metrics {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-light, #8A99B4);
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-history-item__arrow {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-compare-header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-compare-picker {
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: var(--radius-sm, 12px);
|
||||||
|
background: var(--bg-gray, #F2F5F9);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-compare-picker__label {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-light, #8A99B4);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-compare-picker__date {
|
||||||
|
font-size: var(--font-size-base, 14px);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-dark, #1E2A3A);
|
||||||
|
margin-top: 4px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-compare-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid var(--border-light, #E9EDF2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-compare-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-compare-row__label {
|
||||||
|
flex: 1;
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
color: var(--text-muted, #5E6F8D);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-compare-row__val {
|
||||||
|
width: 60px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-dark, #1E2A3A);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-compare-row__diff {
|
||||||
|
width: 56px;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-setting {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 14px 0;
|
||||||
|
border-bottom: 1px solid var(--border-light, #E9EDF2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-setting:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-setting__label {
|
||||||
|
font-size: var(--font-size-base, 14px);
|
||||||
|
color: var(--text-dark, #1E2A3A);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-setting__desc {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
color: var(--text-light, #8A99B4);
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-footer-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-empty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-empty__text {
|
||||||
|
font-size: var(--font-size-base, 14px);
|
||||||
|
color: var(--text-light, #8A99B4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-tabs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--spacing-sm, 8px);
|
||||||
|
padding: 0 var(--spacing-md, 16px) var(--spacing-sm, 8px);
|
||||||
|
overflow-x: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-tab {
|
||||||
|
padding: 8px 14px;
|
||||||
|
border-radius: var(--radius-full, 999px);
|
||||||
|
background: var(--bg-white, #fff);
|
||||||
|
border: 1px solid var(--border-light, #E9EDF2);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-tab--active {
|
||||||
|
background: var(--primary-dark, #0B2B4B);
|
||||||
|
border-color: var(--primary-dark, #0B2B4B);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-tab__text {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
color: var(--text-muted, #5E6F8D);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-tab--active .bt-tab__text {
|
||||||
|
color: var(--text-inverse, #fff);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-trend-link {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 0 0;
|
||||||
|
margin-top: 8px;
|
||||||
|
border-top: 1px solid var(--border-light, #E9EDF2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-trend-link__text {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
color: var(--primary-deep, #1A4A6F);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-trend-link__arrow {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,272 @@
|
|||||||
|
.booking-page {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow-x: hidden;
|
||||||
|
background-color: var(--bg-light, #F9FAFE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tab 栏 */
|
||||||
|
.booking-page__tabs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: stretch;
|
||||||
|
width: 100%;
|
||||||
|
height: 44px;
|
||||||
|
background-color: var(--bg-white, #ffffff);
|
||||||
|
border-bottom: 1px solid var(--border-light, #E9EDF2);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-page__tab {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 44px;
|
||||||
|
padding: 0 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-page__tab-text {
|
||||||
|
font-size: var(--font-size-base, 14px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-light, #8A99B4);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-page__tab-text--active {
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent-orange, #FF6B35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-page__tab-indicator {
|
||||||
|
position: absolute;
|
||||||
|
left: 16px;
|
||||||
|
right: 16px;
|
||||||
|
bottom: 0;
|
||||||
|
height: 2px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background-color: var(--accent-orange, #FF6B35);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 内容区 */
|
||||||
|
.booking-page__body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px 16px 40px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 提醒横幅 */
|
||||||
|
.booking-page__alert {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid rgba(255, 204, 170, 1);
|
||||||
|
background-color: rgba(255, 243, 238, 1);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-page__alert-icon {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-page__alert-text {
|
||||||
|
flex: 1;
|
||||||
|
font-size: var(--font-size-base, 14px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--accent-orange, #FF6B35);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 预约卡片 */
|
||||||
|
.bk-card {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow: 0 2px 10px rgba(26, 25, 24, 0.03);
|
||||||
|
background-color: var(--bg-white, #ffffff);
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: opacity 0.15s ease, transform 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-card__banner {
|
||||||
|
width: 100%;
|
||||||
|
height: 80px;
|
||||||
|
display: block;
|
||||||
|
border-radius: 14px 14px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-card__content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-card__header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-card__title {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: var(--font-size-md, 16px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-dark, #1E2A3A);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-card__status {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-card__status--booked {
|
||||||
|
background-color: var(--success-green, #2ECC71);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-card__status--pending {
|
||||||
|
background-color: rgba(255, 243, 238, 1);
|
||||||
|
border: 1px solid rgba(212, 166, 74, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-card__status--completed {
|
||||||
|
background-color: var(--bg-light, #F9FAFE);
|
||||||
|
border: 1px solid var(--border-light, #E9EDF2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-card__status--cancelled {
|
||||||
|
background-color: var(--bg-light, #F9FAFE);
|
||||||
|
border: 1px solid var(--border-light, #E9EDF2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-card__status-text {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-card__status-text--booked {
|
||||||
|
color: var(--text-inverse, #ffffff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-card__status-text--pending {
|
||||||
|
color: var(--accent-orange, #FF6B35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-card__status-text--completed {
|
||||||
|
color: var(--text-muted, #5E6F8D);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-card__status-text--cancelled {
|
||||||
|
color: var(--text-light, #8A99B4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 时间与教练信息(分两行) */
|
||||||
|
.bk-card__meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-card__meta-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-card__meta-icon {
|
||||||
|
width: 13px;
|
||||||
|
height: 13px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-card__meta-text {
|
||||||
|
font-size: var(--font-size-base, 14px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-muted, #5E6F8D);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 底部操作行 */
|
||||||
|
.bk-card__footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-card__footer-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-light, #8A99B4);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-card__cancel {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border-light, #E9EDF2);
|
||||||
|
background-color: var(--bg-white, #ffffff);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-card__cancel-text {
|
||||||
|
font-size: var(--font-size-base, 14px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--error-red, #E74C3C);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 空状态 */
|
||||||
|
.booking-page__empty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 48px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-page__empty-text {
|
||||||
|
font-size: var(--font-size-base, 14px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-light, #8A99B4);
|
||||||
|
}
|
||||||
@@ -0,0 +1,774 @@
|
|||||||
|
.Pixso-frame-2_965 {
|
||||||
|
width: 100%;
|
||||||
|
height: 62px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--bg-white);
|
||||||
|
}
|
||||||
|
.frame-content-2_965 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0px 20px 0px 20px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_966 {
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-dark);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_967 {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-dark);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_968 {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--bg-white);
|
||||||
|
}
|
||||||
|
.frame-content-2_968 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0px 16px 0px 16px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.stroke-wrapper-2_968 {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 52px;
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.stroke-2_968 {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0px;
|
||||||
|
border-radius: 0px 0px 0px 0px;
|
||||||
|
pointer-events: none;
|
||||||
|
border-width: 0px 0px 1px 0px;
|
||||||
|
border-style: solid;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-color: var(--border-light);
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_969 {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 8px 8px 8px 8px;
|
||||||
|
background-color: var(--bg-light);
|
||||||
|
}
|
||||||
|
.frame-content-2_969 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-vector-2_970 {width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_972 {
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--primary-dark);
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 0;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_973 {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background-color: rgba(249, 250, 254, 0);
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_974 {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--bg-white);
|
||||||
|
}
|
||||||
|
.frame-content-2_974 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0px 16px 0px 16px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.stroke-wrapper-2_974 {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 44px;
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.stroke-2_974 {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0px;
|
||||||
|
border-radius: 0px 0px 0px 0px;
|
||||||
|
pointer-events: none;
|
||||||
|
border-width: 0px 0px 1px 0px;
|
||||||
|
border-style: solid;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-color: var(--border-light);
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_975 {
|
||||||
|
width: 74px;
|
||||||
|
height: 44px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
align-self: stretch;
|
||||||
|
}
|
||||||
|
.frame-content-2_975 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0px 16px 0px 16px;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_976 {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent-orange);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_977 {
|
||||||
|
width: 74px;
|
||||||
|
height: 2px;
|
||||||
|
position: absolute;
|
||||||
|
left: 0px;
|
||||||
|
top: 42px;
|
||||||
|
border-radius: 2px 2px 2px 2px;
|
||||||
|
background-color: var(--accent-orange);
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_978 {
|
||||||
|
width: auto;
|
||||||
|
height: 44px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
align-self: stretch;
|
||||||
|
}
|
||||||
|
.frame-content-2_978 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0px 16px 0px 16px;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_979 {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-light);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_980 {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.frame-content-2_980 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 16px 16px 40px 16px;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_981 {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 10px 10px 10px 10px;
|
||||||
|
background-color: rgba(255, 243, 238, 1);
|
||||||
|
}
|
||||||
|
.frame-content-2_981 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 12px 10px 12px;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.stroke-wrapper-2_981 {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.stroke-2_981 {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0px;
|
||||||
|
border-radius: 10px 10px 10px 10px;
|
||||||
|
pointer-events: none;
|
||||||
|
border-width: 1px 1px 1px 1px;
|
||||||
|
border-style: solid;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-color: rgba(255, 204, 170, 1);
|
||||||
|
}
|
||||||
|
.Pixso-vector-2_982 {width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_985 {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--accent-orange);
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 0;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_986 {
|
||||||
|
width: 100%;
|
||||||
|
height: 195px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
border-radius: 14px 14px 14px 14px;
|
||||||
|
box-shadow: 0px 2px 10px 0px rgba(26, 25, 24, 0.03137254901960784);
|
||||||
|
background-color: var(--bg-white);
|
||||||
|
}
|
||||||
|
.frame-content-2_986 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-vector-2_987 {width: 100%;
|
||||||
|
height: 80px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_988 {
|
||||||
|
width: 358px;
|
||||||
|
height: 80px;
|
||||||
|
position: absolute;
|
||||||
|
left: 0px;
|
||||||
|
top: 0px;
|
||||||
|
border-radius: 14px 14px 0px 0px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.1882352977991104);
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_989 {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.frame-content-2_989 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 12px 14px 12px 14px;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_990 {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.frame-content-2_990 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_991 {
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-dark);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_992 {
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 4px 10px 4px 10px;
|
||||||
|
border-radius: 6px 6px 6px 6px;
|
||||||
|
background-color: var(--success-green);
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_993 {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-inverse);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_994 {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.frame-content-2_994 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 14px;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-vector-2_995 {width: 13px;
|
||||||
|
height: 13px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_998 {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-muted);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-vector-2_999 {width: 13px;
|
||||||
|
height: 13px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_1002 {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-muted);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_1003 {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.frame-content-2_1003 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_1004 {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-light);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_1005 {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 6px 14px 6px 14px;
|
||||||
|
border-radius: 8px 8px 8px 8px;
|
||||||
|
background-color: var(--bg-white);
|
||||||
|
}
|
||||||
|
.stroke-wrapper-2_1005 {
|
||||||
|
position: relative;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.stroke-2_1005 {
|
||||||
|
position: absolute;
|
||||||
|
inset: -1px -1px -1px -1px;
|
||||||
|
border-radius: 9px 9px 9px 9px;
|
||||||
|
pointer-events: none;
|
||||||
|
border-width: 1px 1px 1px 1px;
|
||||||
|
border-style: solid;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-color: var(--border-light);
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_1006 {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--error-red);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_1007 {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
border-radius: 14px 14px 14px 14px;
|
||||||
|
box-shadow: 0px 2px 10px 0px rgba(26, 25, 24, 0.03137254901960784);
|
||||||
|
background-color: var(--bg-white);
|
||||||
|
}
|
||||||
|
.frame-content-2_1007 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-vector-2_1008 {width: 100%;
|
||||||
|
height: 80px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_1009 {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.frame-content-2_1009 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 12px 14px 12px 14px;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_1010 {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.frame-content-2_1010 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_1011 {
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-dark);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_1012 {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 4px 10px 4px 10px;
|
||||||
|
border-radius: 6px 6px 6px 6px;
|
||||||
|
background-color: rgba(255, 243, 238, 1);
|
||||||
|
}
|
||||||
|
.stroke-wrapper-2_1012 {
|
||||||
|
position: relative;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.stroke-2_1012 {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0px;
|
||||||
|
border-radius: 6px 6px 6px 6px;
|
||||||
|
pointer-events: none;
|
||||||
|
border-width: 1px 1px 1px 1px;
|
||||||
|
border-style: solid;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-color: rgba(212, 166, 74, 1);
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_1013 {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--accent-orange);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_1014 {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.frame-content-2_1014 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 14px;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-vector-2_1015 {width: 13px;
|
||||||
|
height: 13px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_1018 {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-muted);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-vector-2_1019 {width: 13px;
|
||||||
|
height: 13px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_1022 {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-muted);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_1023 {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.frame-content-2_1023 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_1024 {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-light);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_1025 {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 6px 14px 6px 14px;
|
||||||
|
border-radius: 8px 8px 8px 8px;
|
||||||
|
background-color: var(--bg-white);
|
||||||
|
}
|
||||||
|
.stroke-wrapper-2_1025 {
|
||||||
|
position: relative;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.stroke-2_1025 {
|
||||||
|
position: absolute;
|
||||||
|
inset: -1px -1px -1px -1px;
|
||||||
|
border-radius: 9px 9px 9px 9px;
|
||||||
|
pointer-events: none;
|
||||||
|
border-width: 1px 1px 1px 1px;
|
||||||
|
border-style: solid;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-color: var(--border-light);
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_1026 {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--error-red);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
.mi-course-list__filters {
|
||||||
|
padding: 0 var(--spacing-md, 16px) var(--spacing-sm, 8px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-sm, 10px);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-list__date-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-list__mode {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--primary-dark, #0B2B4B);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-list__mode-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-list__dates {
|
||||||
|
flex: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-list__date {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-right: 6px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--bg-white, #fff);
|
||||||
|
border: 1px solid var(--border-light, #E9EDF2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-list__date--active {
|
||||||
|
background: rgba(255, 107, 53, 0.12);
|
||||||
|
border-color: var(--accent-orange, #FF6B35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-list__date-week {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-muted, #5E6F8D);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-list__date-day {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-dark, #1E2A3A);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-list__chips {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-list__row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-list__picker {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--bg-white, #fff);
|
||||||
|
border: 1px solid var(--border-light, #E9EDF2);
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-dark, #1E2A3A);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-list__arrow {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: var(--bg-white, #fff);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-card__banner {
|
||||||
|
width: 96px;
|
||||||
|
height: 120px;
|
||||||
|
border-radius: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-card__body {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-card__head {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-card__title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-dark, #1E2A3A);
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-card__type {
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-card__type--group {
|
||||||
|
background: rgba(255, 107, 53, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-card__type--private {
|
||||||
|
background: rgba(11, 43, 75, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-card__type text {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted, #5E6F8D);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-card__coach {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted, #5E6F8D);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-card__avatar {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-card__meta {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-light, #8A99B4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-card__capacity {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-card__bar {
|
||||||
|
flex: 1;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: var(--bg-gray, #F2F5F9);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-card__bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: var(--gradient-orange);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-card__cap-text {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-muted, #5E6F8D);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-card__scarcity {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--accent-orange, #FF6B35);
|
||||||
|
font-weight: 600;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-card__footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-card__price {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--primary-deep, #1A4A6F);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-card__btn {
|
||||||
|
padding: 6px 16px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--gradient-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-card__btn text {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-card__btn--disabled {
|
||||||
|
background: var(--bg-gray, #F2F5F9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-card__btn--disabled text {
|
||||||
|
color: var(--text-light, #8A99B4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-list__fab {
|
||||||
|
position: fixed;
|
||||||
|
right: 16px;
|
||||||
|
bottom: 32px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--primary-dark, #0B2B4B);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-list__fab-icon {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-course-list__fab-text {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
.member-card-page {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow-x: hidden;
|
||||||
|
background-color: var(--bg-light, #F9FAFE);
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card-page__body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px 16px 40px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 会员卡 */
|
||||||
|
.mc-hero {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 160px;
|
||||||
|
padding: 20px 20px 16px;
|
||||||
|
border-radius: 18px;
|
||||||
|
box-shadow: 0 10px 20px rgba(11, 43, 75, 0.31);
|
||||||
|
background: linear-gradient(135deg, #0B2B4B 0%, #1A4A6F 100%);
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-hero__top {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-hero__title-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-hero__crown {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-hero__name {
|
||||||
|
font-size: var(--font-size-md, 16px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-inverse, #ffffff);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-hero__badge {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.19);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-hero__badge-text {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-inverse, #ffffff);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-hero__validity {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: rgba(255, 212, 184, 1);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-hero__bottom {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-hero__days {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-hero__days-num {
|
||||||
|
font-size: var(--font-size-5xl, 32px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-inverse, #ffffff);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-hero__days-unit {
|
||||||
|
font-size: var(--font-size-base, 14px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: rgba(255, 212, 184, 1);
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-hero__renew {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 8px 18px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background-color: var(--bg-white, #ffffff);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-hero__renew-icon {
|
||||||
|
width: 13px;
|
||||||
|
height: 13px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-hero__renew-text {
|
||||||
|
font-size: var(--font-size-base, 14px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent-orange, #FF6B35);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 使用记录 */
|
||||||
|
.mc-records {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow: 0 2px 10px rgba(26, 25, 24, 0.03);
|
||||||
|
background-color: var(--bg-white, #ffffff);
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-records__header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 16px;
|
||||||
|
height: 48px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-records__title {
|
||||||
|
font-size: var(--font-size-base, 14px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-dark, #1E2A3A);
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-records__tabs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 2px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--bg-light, #F9FAFE);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-records__tab {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-records__tab--active {
|
||||||
|
background-color: var(--bg-white, #ffffff);
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-records__tab-text {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-light, #8A99B4);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-records__tab-text--active {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-dark, #1E2A3A);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-records__divider {
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
background-color: var(--bg-light, #F9FAFE);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-records__item-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-records__icon-wrap {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-records__icon-wrap--orange {
|
||||||
|
background-color: rgba(255, 243, 238, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-records__icon-wrap--green {
|
||||||
|
background-color: rgba(240, 250, 245, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-records__icon {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
display: block;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-records__info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-records__item-title {
|
||||||
|
font-size: var(--font-size-base, 14px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-dark, #1E2A3A);
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-records__item-time {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-light, #8A99B4);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-records__value {
|
||||||
|
font-size: var(--font-size-base, 14px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 700;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-records__value--negative {
|
||||||
|
color: var(--error-red, #E74C3C);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-records__value--positive {
|
||||||
|
color: var(--success-green, #2ECC71);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 使用规则 */
|
||||||
|
.mc-rules {
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow: 0 2px 10px rgba(26, 25, 24, 0.03);
|
||||||
|
background-color: var(--bg-white, #ffffff);
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-rules__title {
|
||||||
|
font-size: var(--font-size-base, 14px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-dark, #1E2A3A);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-rules__item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-rules__bullet {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
margin-top: 6px;
|
||||||
|
border-radius: 1px;
|
||||||
|
background-color: var(--accent-orange, #FF6B35);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-rules__text {
|
||||||
|
flex: 1;
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-muted, #5E6F8D);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
@@ -0,0 +1,982 @@
|
|||||||
|
.Pixso-frame-2_878 {
|
||||||
|
width: 100%;
|
||||||
|
height: 62px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--bg-white);
|
||||||
|
}
|
||||||
|
.frame-content-2_878 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0px 20px 0px 20px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_879 {
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-dark);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_880 {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-dark);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_881 {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--bg-white);
|
||||||
|
}
|
||||||
|
.frame-content-2_881 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0px 16px 0px 16px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.stroke-wrapper-2_881 {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 52px;
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.stroke-2_881 {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0px;
|
||||||
|
border-radius: 0px 0px 0px 0px;
|
||||||
|
pointer-events: none;
|
||||||
|
border-width: 0px 0px 1px 0px;
|
||||||
|
border-style: solid;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-color: var(--border-light);
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_882 {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 8px 8px 8px 8px;
|
||||||
|
background-color: var(--bg-light);
|
||||||
|
}
|
||||||
|
.frame-content-2_882 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-vector-2_883 {width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_885 {
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--primary-dark);
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 0;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_886 {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background-color: rgba(249, 250, 254, 0);
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_887 {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.frame-content-2_887 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 16px 16px 40px 16px;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_888 {
|
||||||
|
width: 100%;
|
||||||
|
height: 160px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
border-radius: 18px 18px 18px 18px;
|
||||||
|
box-shadow: 0px 10px 20px 0px rgba(11, 43, 75, 0.3137254901960784);
|
||||||
|
background-position: center;
|
||||||
|
background-image: url("data:image/svg+xml;utf8,%3Csvg%20viewBox%3D'0%200%20358%20160'%20preserveAspectRatio%3D'none'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%0A%20%20%20%20%20%20%3Cdefs%3E%0A%20%20%20%20%20%20%20%20%3ClinearGradient%20id%3D'grad'%20gradientUnits%3D'objectBoundingBox'%20x1%3D'0'%20y1%3D'0.5'%20x2%3D'1'%20y2%3D'0.5'%20gradientTransform%3D'matrix(-0.7071%2C%200.7071%2C%20-0.7071%2C%20-0.7071%2C%201.2071%2C%200.5000)'%3E%0A%20%20%20%20%20%20%20%20%20%20%3Cstop%20stop-color%3D'rgba(11%2C43%2C75%2C1)'%20offset%3D'0'%2F%3E%3Cstop%20stop-color%3D'rgba(26%2C74%2C111%2C1)'%20offset%3D'1'%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FlinearGradient%3E%0A%20%20%20%20%20%20%3C%2Fdefs%3E%0A%20%20%20%20%20%20%3Crect%20width%3D'100%25'%20height%3D'100%25'%20fill%3D'url(%23grad)'%2F%3E%0A%20%20%20%20%3C%2Fsvg%3E");
|
||||||
|
}
|
||||||
|
.frame-content-2_888 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 20px 20px 16px 20px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_889 {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.frame-content-2_889 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_890 {
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.Pixso-vector-2_891 {width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_894 {
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-inverse);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_895 {
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 4px 10px 4px 10px;
|
||||||
|
border-radius: 10px 10px 10px 10px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.1882352977991104);
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_896 {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-inverse);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_897 {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: rgba(255, 212, 184, 1);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_898 {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.frame-content-2_898 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_899 {
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_900 {
|
||||||
|
font-size: var(--font-size-5xl);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-inverse);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_901 {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: rgba(255, 212, 184, 1);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_902 {
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 18px 8px 18px;
|
||||||
|
border-radius: 16px 16px 16px 16px;
|
||||||
|
background-color: var(--bg-white);
|
||||||
|
}
|
||||||
|
.Pixso-vector-2_903 {width: 13px;
|
||||||
|
height: 13px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_908 {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent-orange);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_909 {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
border-radius: 14px 14px 14px 14px;
|
||||||
|
box-shadow: 0px 2px 10px 0px rgba(26, 25, 24, 0.03137254901960784);
|
||||||
|
background-color: var(--bg-white);
|
||||||
|
}
|
||||||
|
.frame-content-2_909 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_910 {
|
||||||
|
width: 100%;
|
||||||
|
height: 48px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.frame-content-2_910 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0px 16px 0px 16px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_911 {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-dark);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_912 {
|
||||||
|
width: auto;
|
||||||
|
height: 28px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 8px 8px 8px 8px;
|
||||||
|
background-color: var(--bg-light);
|
||||||
|
}
|
||||||
|
.frame-content-2_912 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 2px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 2px 2px 2px;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_913 {
|
||||||
|
width: auto;
|
||||||
|
height: 24px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
align-self: stretch;
|
||||||
|
border-radius: 6px 6px 6px 6px;
|
||||||
|
box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.06274509803921569);
|
||||||
|
background-color: var(--bg-white);
|
||||||
|
}
|
||||||
|
.frame-content-2_913 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0px 10px 0px 10px;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_914 {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-dark);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_915 {
|
||||||
|
width: auto;
|
||||||
|
height: 24px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
align-self: stretch;
|
||||||
|
border-radius: 6px 6px 6px 6px;
|
||||||
|
background-color: rgba(249, 250, 254, 0);
|
||||||
|
}
|
||||||
|
.frame-content-2_915 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0px 10px 0px 10px;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_916 {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-light);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_917 {
|
||||||
|
width: auto;
|
||||||
|
height: 24px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
align-self: stretch;
|
||||||
|
border-radius: 6px 6px 6px 6px;
|
||||||
|
background-color: rgba(249, 250, 254, 0);
|
||||||
|
}
|
||||||
|
.frame-content-2_917 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0px 10px 0px 10px;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_918 {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-light);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_919 {
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background-color: var(--bg-light);
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_920 {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.frame-content-2_920 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 14px 16px 14px 16px;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_921 {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 10px 10px 10px 10px;
|
||||||
|
background-color: rgba(255, 243, 238, 1);
|
||||||
|
}
|
||||||
|
.frame-content-2_921 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-vector-2_922 {width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_928 {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 0;
|
||||||
|
}
|
||||||
|
.frame-content-2_928 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_929 {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-dark);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_930 {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-light);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_931 {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--error-red);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_932 {
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background-color: var(--bg-light);
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_933 {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.frame-content-2_933 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 14px 16px 14px 16px;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_934 {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 10px 10px 10px 10px;
|
||||||
|
background-color: rgba(240, 250, 245, 1);
|
||||||
|
}
|
||||||
|
.frame-content-2_934 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-vector-2_935 {width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_938 {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 0;
|
||||||
|
}
|
||||||
|
.frame-content-2_938 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_939 {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-dark);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_940 {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-light);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_941 {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--error-red);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_942 {
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background-color: var(--bg-light);
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_943 {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.frame-content-2_943 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 14px 16px 14px 16px;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_944 {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 10px 10px 10px 10px;
|
||||||
|
background-color: rgba(255, 243, 238, 1);
|
||||||
|
}
|
||||||
|
.frame-content-2_944 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-vector-2_945 {width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_949 {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 0;
|
||||||
|
}
|
||||||
|
.frame-content-2_949 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_950 {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-dark);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_951 {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-light);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_952 {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--success-green);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_953 {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
border-radius: 14px 14px 14px 14px;
|
||||||
|
box-shadow: 0px 2px 10px 0px rgba(26, 25, 24, 0.03137254901960784);
|
||||||
|
background-color: var(--bg-white);
|
||||||
|
}
|
||||||
|
.frame-content-2_953 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 16px 16px 16px 16px;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_954 {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-dark);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_955 {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.frame-content-2_955 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_956 {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--accent-orange);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_957 {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-muted);
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 0;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_958 {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.frame-content-2_958 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_959 {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--accent-orange);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_960 {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-muted);
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 0;
|
||||||
|
}
|
||||||
|
.Pixso-frame-2_961 {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.frame-content-2_961 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_962 {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--accent-orange);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.Pixso-paragraph-2_963 {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-muted);
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,584 @@
|
|||||||
|
/* 个人中心其它模块页面样式 */
|
||||||
|
@import '@/common/style/memberInfo/member-info-gradient-cards.css';
|
||||||
|
|
||||||
|
.mi-mod-tabs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--spacing-sm, 8px);
|
||||||
|
padding: 0 var(--spacing-md, 16px) var(--spacing-sm, 8px);
|
||||||
|
overflow-x: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 训练明细 */
|
||||||
|
.mi-mod-session {
|
||||||
|
padding: 14px 0;
|
||||||
|
border-bottom: 1px solid var(--border-light, #E9EDF2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-session:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-session__head {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-session__title {
|
||||||
|
font-size: var(--font-size-base, 14px);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-dark, #1E2A3A);
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-session__tag {
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-session__tag--group {
|
||||||
|
background: rgba(255, 107, 53, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-session__tag--private {
|
||||||
|
background: rgba(11, 43, 75, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-session__tag--free {
|
||||||
|
background: rgba(46, 204, 113, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-session__tag-text {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted, #5E6F8D);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-session__meta {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
color: var(--text-muted, #5E6F8D);
|
||||||
|
margin-top: 4px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-session__footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-session__stat {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--primary-deep, #1A4A6F);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 优惠券 */
|
||||||
|
.mi-mod-coupon {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
border-radius: var(--radius-md, 20px);
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-coupon--expired,
|
||||||
|
.mi-mod-coupon--used {
|
||||||
|
opacity: 0.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-coupon__left {
|
||||||
|
width: 100px;
|
||||||
|
padding: 16px 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-coupon--used .mi-mod-coupon__left,
|
||||||
|
.mi-mod-coupon--expired .mi-mod-coupon__left {
|
||||||
|
background: var(--bg-gray, #F2F5F9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-coupon__amount {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--text-inverse, #fff);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-coupon--used .mi-mod-coupon__amount,
|
||||||
|
.mi-mod-coupon--expired .mi-mod-coupon__amount {
|
||||||
|
color: var(--text-muted, #5E6F8D);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-coupon__min {
|
||||||
|
font-size: 10px;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-coupon--used .mi-mod-coupon__min,
|
||||||
|
.mi-mod-coupon--expired .mi-mod-coupon__min {
|
||||||
|
color: var(--text-light, #8A99B4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-coupon__right {
|
||||||
|
flex: 1;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: var(--bg-white, #fff);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-coupon__title {
|
||||||
|
font-size: var(--font-size-base, 14px);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-dark, #1E2A3A);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-coupon__desc {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
color: var(--text-muted, #5E6F8D);
|
||||||
|
margin-top: 4px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-coupon__expire {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-light, #8A99B4);
|
||||||
|
margin-top: 8px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-coupon__tag {
|
||||||
|
position: absolute;
|
||||||
|
top: 14px;
|
||||||
|
right: 14px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--bg-gray, #F2F5F9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-coupon__tag-text {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--accent-orange, #FF6B35);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 积分 */
|
||||||
|
.mi-mod-points-hero {
|
||||||
|
padding: 24px 20px;
|
||||||
|
border-radius: var(--radius-sm, 12px);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-points-hero__label {
|
||||||
|
font-size: var(--font-size-xs, 12px);
|
||||||
|
color: rgba(255, 212, 184, 1);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-points-hero__value {
|
||||||
|
font-size: var(--font-size-3xl, 2rem);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-inverse, #fff);
|
||||||
|
line-height: 1.2;
|
||||||
|
display: block;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-points-hero__tip {
|
||||||
|
font-size: var(--font-size-xs, 12px);
|
||||||
|
color: rgba(255, 212, 184, 1);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-rewards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-reward {
|
||||||
|
padding: 14px;
|
||||||
|
border-radius: var(--radius-sm, 12px);
|
||||||
|
background: var(--bg-gray, #F2F5F9);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-reward__icon {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-reward__name {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-dark, #1E2A3A);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-reward__cost {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--accent-orange, #FF6B35);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-reward__stock {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-light, #8A99B4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-points-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid var(--border-light, #E9EDF2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-points-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-points-row__info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-points-row__title {
|
||||||
|
font-size: var(--font-size-base, 14px);
|
||||||
|
color: var(--text-dark, #1E2A3A);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-points-row__time {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
color: var(--text-light, #8A99B4);
|
||||||
|
margin-top: 2px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-points-row__right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-points-row__amount {
|
||||||
|
font-size: var(--font-size-md, 16px);
|
||||||
|
font-weight: 700;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-points-row__amount--earn {
|
||||||
|
color: var(--success-green, #2ECC71);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-points-row__amount--spend {
|
||||||
|
color: var(--warning-amber, #F39C12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-points-row__balance {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-light, #8A99B4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 邀请 */
|
||||||
|
.mi-mod-referral-hero {
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: var(--radius-sm, 12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-referral-hero__title {
|
||||||
|
font-size: var(--font-size-lg, 18px);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-inverse, #fff);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-referral-hero__desc {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
color: rgba(255, 212, 184, 1);
|
||||||
|
margin-top: 6px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-referral-code {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-referral-code__label {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
color: rgba(255, 255, 255, 0.75);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-referral-code__value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--text-inverse, #fff);
|
||||||
|
letter-spacing: 2px;
|
||||||
|
margin-top: 6px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-referral-stats {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-referral-stat {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-referral-stat__num {
|
||||||
|
font-size: var(--font-size-xl, 20px);
|
||||||
|
font-weight: 800;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-referral-stat__num--orange {
|
||||||
|
color: var(--accent-orange, #FF6B35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-referral-stat__num--green {
|
||||||
|
color: var(--success-green, #2ECC71);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-referral-stat__num--amber {
|
||||||
|
color: var(--warning-amber, #F39C12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-referral-stat__label {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
color: var(--text-muted, #5E6F8D);
|
||||||
|
margin-top: 4px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-referral-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid var(--border-light, #E9EDF2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-referral-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-referral-row__info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-referral-row__name {
|
||||||
|
font-size: var(--font-size-base, 14px);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-dark, #1E2A3A);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-referral-row__time {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
color: var(--text-light, #8A99B4);
|
||||||
|
margin-top: 2px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-referral-row__right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-referral-row__status {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
color: var(--text-muted, #5E6F8D);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-referral-row__reward {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--accent-orange, #FF6B35);
|
||||||
|
margin-top: 2px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 我的课程 */
|
||||||
|
.mi-mod-course-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px;
|
||||||
|
border-radius: var(--radius-md, 20px);
|
||||||
|
background: var(--bg-white, #fff);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-course-card__banner {
|
||||||
|
width: 88px;
|
||||||
|
height: 88px;
|
||||||
|
border-radius: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-course-card__content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-course-card__title {
|
||||||
|
font-size: var(--font-size-base, 14px);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-dark, #1E2A3A);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-course-card__coach {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
color: var(--text-muted, #5E6F8D);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-course-card__progress {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-course-card__progress-bar {
|
||||||
|
flex: 1;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: var(--bg-gray, #F2F5F9);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-course-card__progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: var(--gradient-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-course-card__progress-text {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-muted, #5E6F8D);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-course-card__meta {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-light, #8A99B4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-course-card__next {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--primary-deep, #1A4A6F);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 签到记录 */
|
||||||
|
.mi-mod-checkin-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid var(--border-light, #E9EDF2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-checkin-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-checkin-row__icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-checkin-row__icon--group {
|
||||||
|
background: rgba(255, 107, 53, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-checkin-row__icon--private {
|
||||||
|
background: rgba(11, 43, 75, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-checkin-row__icon--free {
|
||||||
|
background: rgba(46, 204, 113, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-checkin-row__icon-img {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-checkin-row__info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-checkin-row__title {
|
||||||
|
font-size: var(--font-size-base, 14px);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-dark, #1E2A3A);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-checkin-row__time {
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
color: var(--text-muted, #5E6F8D);
|
||||||
|
margin-top: 2px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-checkin-row__tag {
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-checkin-row__tag--group {
|
||||||
|
background: rgba(255, 107, 53, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-checkin-row__tag--private {
|
||||||
|
background: rgba(11, 43, 75, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-checkin-row__tag--free {
|
||||||
|
background: rgba(46, 204, 113, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-mod-checkin-row__tag-text {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted, #5E6F8D);
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/* 子页面根容器:锁定变量,与 H5 theme-light 一致 */
|
||||||
|
.scroll-container {
|
||||||
|
box-sizing: border-box;
|
||||||
|
--primary-dark: #0B2B4B;
|
||||||
|
--primary-deep: #1A4A6F;
|
||||||
|
--accent-orange: #FF6B35;
|
||||||
|
--accent-orange-light: #FF8C5A;
|
||||||
|
--bg-light: #F9FAFE;
|
||||||
|
--bg-white: #FFFFFF;
|
||||||
|
--text-dark: #1E2A3A;
|
||||||
|
--text-muted: #5E6F8D;
|
||||||
|
--text-light: #8A99B4;
|
||||||
|
--text-inverse: #FFFFFF;
|
||||||
|
--border-light: #E9EDF2;
|
||||||
|
--success-green: #2ECC71;
|
||||||
|
--spacing-xs: 4px;
|
||||||
|
--spacing-sm: 8px;
|
||||||
|
--spacing-md: 16px;
|
||||||
|
--spacing-lg: 24px;
|
||||||
|
--spacing-xl: 32px;
|
||||||
|
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
--font-size-xs: 11px;
|
||||||
|
--font-size-sm: 12px;
|
||||||
|
--font-size-base: 14px;
|
||||||
|
--font-size-md: 16px;
|
||||||
|
--font-size-lg: 18px;
|
||||||
|
--font-size-xl: 20px;
|
||||||
|
--font-size-2xl: 22px;
|
||||||
|
--font-size-3xl: 24px;
|
||||||
|
--font-size-4xl: 28px;
|
||||||
|
--font-size-5xl: 32px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
.scroll-container {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-container > view {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 子页面统一布局(参考 base.css 间距变量) ========== */
|
||||||
|
|
||||||
|
.bt-page,
|
||||||
|
.booking-page {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow-x: hidden;
|
||||||
|
background-color: var(--bg-light, #F9FAFE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 导航栏下方首个区块:与固定顶栏留出间距 */
|
||||||
|
.bt-page > .sub-nav + .mi-mod-tabs,
|
||||||
|
.bt-page > .sub-nav + .mi-course-list__filters,
|
||||||
|
.bt-page > .sub-nav + .bt-page__action-bar,
|
||||||
|
.bt-page > .sub-nav + .bt-page__body,
|
||||||
|
.booking-page > .sub-nav + .booking-page__tabs {
|
||||||
|
margin-top: var(--spacing-md, 16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tabs / 筛选栏下方内容区:避免重复过大顶边距 */
|
||||||
|
.bt-page > .sub-nav + .mi-mod-tabs + .bt-page__action-bar + .bt-page__body,
|
||||||
|
.bt-page > .sub-nav + .mi-mod-tabs + .bt-page__body,
|
||||||
|
.bt-page > .sub-nav + .mi-course-list__filters + .bt-page__body,
|
||||||
|
.booking-page > .sub-nav + .booking-page__tabs + .bt-page__action-bar + .booking-page__body,
|
||||||
|
.booking-page > .sub-nav + .booking-page__tabs + .booking-page__body {
|
||||||
|
padding-top: var(--spacing-sm, 8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 导航栏下直接跟操作栏(无 tabs) */
|
||||||
|
.bt-page > .sub-nav + .bt-page__action-bar + .bt-page__body {
|
||||||
|
padding-top: var(--spacing-sm, 8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面内次级操作栏(原导航栏右侧按钮下移至此) */
|
||||||
|
.bt-page__action-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--spacing-sm, 8px);
|
||||||
|
padding: 0 var(--spacing-md, 16px) var(--spacing-sm, 8px);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-page__action-bar--end {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-page__action-bar-text {
|
||||||
|
flex: 1;
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
color: var(--text-muted, #5E6F8D);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-page__action-link {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: var(--font-size-sm, 12px);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--primary-deep, #1A4A6F);
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: var(--radius-full, 999px);
|
||||||
|
background-color: var(--bg-white, #FFFFFF);
|
||||||
|
border: 1px solid var(--border-light, #E9EDF2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-page__action-link--primary {
|
||||||
|
color: var(--text-inverse, #FFFFFF);
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
.Pixso-frame-2_791 {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: visible;
|
||||||
|
background-color: var(--bg-light, #F9FAFE);
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame-content-2_802 {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding-bottom: calc(88px + env(safe-area-inset-bottom)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info-save-bar {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 100;
|
||||||
|
padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
|
||||||
|
background-color: var(--bg-white, #ffffff);
|
||||||
|
box-shadow: 0 -2px 12px rgba(26, 25, 24, 0.06);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info-save-bar__btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background-color: var(--accent-orange, #FF6B35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info-save-bar__text {
|
||||||
|
font-size: var(--font-size-md, 16px);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-inverse, #ffffff);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-block {
|
||||||
|
width: 100%;
|
||||||
|
height: 120px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow: 0 2px 10px rgba(26, 25, 24, 0.03);
|
||||||
|
background-color: var(--bg-white);
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-block__inner {
|
||||||
|
position: relative;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background-color: var(--bg-light, #f9fafe);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-block__photo {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-block__change {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 28px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.55);
|
||||||
|
z-index: 2;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-block__icon {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
filter: brightness(0) invert(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-block__text {
|
||||||
|
font-size: var(--font-size-xs, 11px);
|
||||||
|
line-height: 1;
|
||||||
|
color: #ffffff;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame-content-2_811,
|
||||||
|
.frame-content-2_817,
|
||||||
|
.frame-content-2_826,
|
||||||
|
.frame-content-2_842,
|
||||||
|
.frame-content-2_848,
|
||||||
|
.frame-content-2_859 {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Pixso-paragraph-2_813,
|
||||||
|
.Pixso-paragraph-2_844,
|
||||||
|
.Pixso-paragraph-2_861 {
|
||||||
|
display: block;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gender-btn {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--bg-light, #F9FAFE);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gender-btn__icon {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
display: block;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gender-btn__text {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-light, #8A99B4);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gender-btn--active {
|
||||||
|
background-color: var(--accent-orange, #FF6B35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gender-btn--active .gender-btn__text {
|
||||||
|
color: var(--text-inverse, #ffffff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tag {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 7px 16px;
|
||||||
|
border-radius: 100px;
|
||||||
|
border: 1px solid rgba(209, 208, 205, 1);
|
||||||
|
background-color: var(--bg-white, #ffffff);
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tag__text {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-muted, #5E6F8D);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tag--selected {
|
||||||
|
border-color: var(--accent-orange, #FF6B35);
|
||||||
|
background-color: var(--accent-orange, #FF6B35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tag--selected .goal-tag__text {
|
||||||
|
color: var(--text-inverse, #ffffff);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: "iconfont"; /* Project id */
|
||||||
|
src: url('tabbar.ttf?t=1780818759010') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconfont {
|
||||||
|
font-family: "iconfont" !important;
|
||||||
|
font-size: 16px;
|
||||||
|
font-style: normal;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-home:before {
|
||||||
|
content: "\e666";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-course:before {
|
||||||
|
content: "\e692";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-train:before {
|
||||||
|
content: "\e8be";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-discover:before {
|
||||||
|
content: "\e726";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-profile:before {
|
||||||
|
content: "\e501";
|
||||||
|
}
|
||||||
|
|
||||||
Binary file not shown.
@@ -0,0 +1,97 @@
|
|||||||
|
<template>
|
||||||
|
<view class="qr-status">
|
||||||
|
<!-- 加载中状态 -->
|
||||||
|
<view v-if="status === 'loading'" class="status-loading">
|
||||||
|
<view class="status-icon">
|
||||||
|
<view class="loading-spinner"></view>
|
||||||
|
</view>
|
||||||
|
<text>生成中...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 签到成功状态 -->
|
||||||
|
<view v-else-if="status === 'scanned'" class="status-success">
|
||||||
|
<view class="status-icon">
|
||||||
|
<uni-icons type="checkmarkcircle" size="40rpx" color="#2ECC71"></uni-icons>
|
||||||
|
</view>
|
||||||
|
<text>签到成功!</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 错误状态(支持自定义文案) -->
|
||||||
|
<view v-else-if="status === 'error'" class="status-error">
|
||||||
|
<view class="status-icon">
|
||||||
|
<uni-icons type="closecircle" size="40rpx" color="#E74C3C"></uni-icons>
|
||||||
|
</view>
|
||||||
|
<text>{{ errorText || '签到失败,请重试' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps } from 'vue';
|
||||||
|
|
||||||
|
// 扩展Props,支持自定义错误文案
|
||||||
|
const props = defineProps({
|
||||||
|
status: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// 自定义错误文本(可选)
|
||||||
|
errorText: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 保留原样式,新增加载中样式 */
|
||||||
|
.qr-status {
|
||||||
|
margin-bottom: 48rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-loading,
|
||||||
|
.status-waiting,
|
||||||
|
.status-success,
|
||||||
|
.status-error {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
font-size: 29rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载中样式 */
|
||||||
|
.status-loading {
|
||||||
|
color: #FF6B35;
|
||||||
|
}
|
||||||
|
.loading-spinner {
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
border: 4rpx solid #E9EDF2;
|
||||||
|
border-top-color: #FF6B35;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-success {
|
||||||
|
color: #2ECC71;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-error {
|
||||||
|
color: #E74C3C;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 旋转动画 */
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
<template>
|
||||||
|
<SkeletonBase>
|
||||||
|
<view class="skeleton-banner"></view>
|
||||||
|
|
||||||
|
<view class="skeleton-entry">
|
||||||
|
<view v-for="i in 4" :key="i" class="skeleton-entry-item">
|
||||||
|
<view class="skeleton-icon"></view>
|
||||||
|
<view class="skeleton-text"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="skeleton-section">
|
||||||
|
<view class="skeleton-section-title"></view>
|
||||||
|
<view v-for="i in 3" :key="i" class="skeleton-course-item">
|
||||||
|
<view class="skeleton-course-img"></view>
|
||||||
|
<view class="skeleton-course-info">
|
||||||
|
<view class="skeleton-course-title"></view>
|
||||||
|
<view class="skeleton-course-desc"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</SkeletonBase>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import SkeletonBase from './SkeletonBase.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.skeleton-banner {
|
||||||
|
height: 300rpx;
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 1.5s infinite;
|
||||||
|
margin: 20rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-entry {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
padding: 30rpx 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-entry-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-icon {
|
||||||
|
width: 80rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 1.5s infinite;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text {
|
||||||
|
width: 60rpx;
|
||||||
|
height: 24rpx;
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 1.5s infinite;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-section {
|
||||||
|
padding: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-section-title {
|
||||||
|
height: 40rpx;
|
||||||
|
width: 200rpx;
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 1.5s infinite;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-course-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 20rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-course-img {
|
||||||
|
width: 160rpx;
|
||||||
|
height: 160rpx;
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 1.5s infinite;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-course-info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-course-title {
|
||||||
|
height: 36rpx;
|
||||||
|
width: 80%;
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 1.5s infinite;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-course-desc {
|
||||||
|
height: 28rpx;
|
||||||
|
width: 60%;
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 1.5s infinite;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% { background-position: 200% 0; }
|
||||||
|
100% { background-position: -200% 0; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<view class="skeleton" :style="{ padding: padding }">
|
||||||
|
<slot />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
padding: {
|
||||||
|
type: String,
|
||||||
|
default: '20rpx'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.skeleton {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.skeleton-shimmer) {
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.skeleton-line) {
|
||||||
|
height: 32rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.skeleton-block) {
|
||||||
|
border-radius: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.skeleton-circle) {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% { background-position: 200% 0; }
|
||||||
|
100% { background-position: -200% 0; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
<!-- components/TabBar.vue -->
|
||||||
|
<template>
|
||||||
|
<view v-if="shouldShowTabBar" class="tab-bar">
|
||||||
|
<view
|
||||||
|
v-for="(tab, index) in tabs"
|
||||||
|
:key="tab.path"
|
||||||
|
:class="['tab-item', { active: currentActiveIndex === index }]"
|
||||||
|
hover-class="tab-item--hover"
|
||||||
|
@tap.stop="onTabTap(index)"
|
||||||
|
>
|
||||||
|
<!-- 判断是否使用字体图标(我的页面用字体,其他用图片) -->
|
||||||
|
<text
|
||||||
|
v-if="tab.useFontIcon"
|
||||||
|
:class="['iconfont', tab.icon]"
|
||||||
|
class="tab-icon-font"
|
||||||
|
:style="{ fontSize: tab.fontSize}"
|
||||||
|
></text>
|
||||||
|
<text class="tab-label">{{ tab.label }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
import {
|
||||||
|
PAGE,
|
||||||
|
TAB_ROUTES,
|
||||||
|
getCurrentRoutePath,
|
||||||
|
getTabIndexByRoute
|
||||||
|
} from '@/common/constants/routes.js'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
active: { type: Number, default: -1 },
|
||||||
|
activeTab: { type: Number, default: -1 }
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:active', 'tab-change'])
|
||||||
|
|
||||||
|
const currentActiveIndex = ref(-1)
|
||||||
|
const shouldShowTabBar = ref(true)
|
||||||
|
|
||||||
|
const HIDE_TABBAR_PAGES = [
|
||||||
|
'pages/memberInfo/courseList',
|
||||||
|
'pages/memberInfo/courseDetail',
|
||||||
|
'pages/memberInfo/booking',
|
||||||
|
'pages/memberInfo/bodyTestReport',
|
||||||
|
'pages/groupCourse/list',
|
||||||
|
'pages/groupCourse/detail',
|
||||||
|
'pages/searchCourse/searchCourse',
|
||||||
|
'pages/checkIn/checkIn',
|
||||||
|
'pages/memberInfo/myCourses',
|
||||||
|
'pages/memberInfo/coupons',
|
||||||
|
'pages/memberInfo/points',
|
||||||
|
'pages/memberInfo/pointsMall',
|
||||||
|
'pages/memberInfo/referral',
|
||||||
|
'pages/memberInfo/userInfo',
|
||||||
|
'pages/memberInfo/memberCard',
|
||||||
|
]
|
||||||
|
|
||||||
|
function getActiveIndexFromRoute() {
|
||||||
|
const routePath = getCurrentRoutePath()
|
||||||
|
const index = getTabIndexByRoute(routePath)
|
||||||
|
console.log('从路由获取索引:', routePath, '->', index)
|
||||||
|
return index >= 0 ? index : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncActiveState() {
|
||||||
|
const routeIndex = getActiveIndexFromRoute()
|
||||||
|
if (routeIndex >= 0) {
|
||||||
|
currentActiveIndex.value = routeIndex
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (props.active >= 0) {
|
||||||
|
currentActiveIndex.value = props.active
|
||||||
|
} else if (props.activeTab >= 0) {
|
||||||
|
currentActiveIndex.value = props.activeTab
|
||||||
|
} else {
|
||||||
|
currentActiveIndex.value = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkShouldShow() {
|
||||||
|
let routePath = getCurrentRoutePath()
|
||||||
|
if (routePath.startsWith('/')) {
|
||||||
|
routePath = routePath.slice(1)
|
||||||
|
}
|
||||||
|
if (routePath.includes('?')) {
|
||||||
|
routePath = routePath.split('?')[0]
|
||||||
|
}
|
||||||
|
const shouldHide = HIDE_TABBAR_PAGES.includes(routePath)
|
||||||
|
shouldShowTabBar.value = !shouldHide
|
||||||
|
console.log('=== TabBar 显示控制 ===')
|
||||||
|
console.log('原始路径:', getCurrentRoutePath())
|
||||||
|
console.log('标准化路径:', routePath)
|
||||||
|
console.log('是否隐藏:', shouldHide)
|
||||||
|
console.log('是否显示 TabBar:', shouldShowTabBar.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
let routeWatcher = null
|
||||||
|
let appRouteCallback = null
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
syncActiveState()
|
||||||
|
checkShouldShow()
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
routeWatcher = setInterval(() => {
|
||||||
|
syncActiveState()
|
||||||
|
checkShouldShow()
|
||||||
|
}, 300)
|
||||||
|
// #endif
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
if (typeof uni.onAppRoute === 'function') {
|
||||||
|
appRouteCallback = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
syncActiveState()
|
||||||
|
checkShouldShow()
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
uni.onAppRoute(appRouteCallback)
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
if (routeWatcher) { clearInterval(routeWatcher) }
|
||||||
|
// #endif
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
if (appRouteCallback && typeof uni.offAppRoute === 'function') {
|
||||||
|
uni.offAppRoute(appRouteCallback)
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => props.active, () => {
|
||||||
|
const routeIndex = getActiveIndexFromRoute()
|
||||||
|
if (routeIndex !== currentActiveIndex.value) { syncActiveState() }
|
||||||
|
})
|
||||||
|
|
||||||
|
// tabs 配置:只有"我的"用字体图标
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
path: PAGE.INDEX,
|
||||||
|
icon: 'icon-home',
|
||||||
|
label: '首页',
|
||||||
|
useFontIcon: true,
|
||||||
|
fontSize:"36rpx"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: PAGE.COURSE,
|
||||||
|
icon: 'icon-course',
|
||||||
|
label: '课程',
|
||||||
|
useFontIcon: true,
|
||||||
|
fontSize:"36rpx"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: PAGE.TRAIN,
|
||||||
|
icon: 'icon-train',
|
||||||
|
label: '训练',
|
||||||
|
useFontIcon: true,
|
||||||
|
fontSize:"48rpx"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: PAGE.DISCOVER,
|
||||||
|
icon: 'icon-discover',
|
||||||
|
label: '发现',
|
||||||
|
useFontIcon: true,
|
||||||
|
fontSize:"48rpx"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: PAGE.MEMBER,
|
||||||
|
icon: 'icon-profile',
|
||||||
|
label: '我的',
|
||||||
|
useFontIcon: true,
|
||||||
|
fontSize:"36rpx"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
let isSwitching = false
|
||||||
|
|
||||||
|
function onTabTap(index) {
|
||||||
|
if (isSwitching) return
|
||||||
|
const targetPath = TAB_ROUTES[index]
|
||||||
|
const currentPath = TAB_ROUTES[currentActiveIndex.value]
|
||||||
|
if (targetPath === currentPath) return
|
||||||
|
console.log('Tab 点击:', index, targetPath)
|
||||||
|
currentActiveIndex.value = index
|
||||||
|
emit('update:active', index)
|
||||||
|
emit('tab-change', index)
|
||||||
|
let timer = setTimeout(() => {
|
||||||
|
uni.showLoading({ title: '加载中...', mask: true })
|
||||||
|
}, 50)
|
||||||
|
isSwitching = true
|
||||||
|
uni.switchTab({
|
||||||
|
url: targetPath,
|
||||||
|
success: () => { console.log('switchTab 成功:', targetPath) },
|
||||||
|
fail: (err) => {
|
||||||
|
console.error('switchTab 失败:', err)
|
||||||
|
uni.reLaunch({ url: targetPath })
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
clearTimeout(timer)
|
||||||
|
uni.hideLoading()
|
||||||
|
setTimeout(() => {
|
||||||
|
isSwitching = false
|
||||||
|
syncActiveState()
|
||||||
|
checkShouldShow()
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
// 引入字体图标 CSS(定义 @font-face)
|
||||||
|
@import '/common/style/tabbar_icon/tabbar.css';
|
||||||
|
|
||||||
|
.tab-bar {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 120rpx;
|
||||||
|
background: white;
|
||||||
|
backdrop-filter: blur(24px);
|
||||||
|
-webkit-backdrop-filter: blur(24px);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
padding-bottom: constant(safe-area-inset-bottom);
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
box-shadow: 0 -4rpx 24rpx var(--tabbar-shadow);
|
||||||
|
border-radius: 32rpx 32rpx 0 0;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
padding: 12rpx 24rpx;
|
||||||
|
transition: all 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片图标样式
|
||||||
|
.tab-icon {
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字体图标样式
|
||||||
|
.tab-icon-font {
|
||||||
|
font-size: 44rpx;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字体图标颜色控制(根据选中状态)
|
||||||
|
.tab-item .iconfont {
|
||||||
|
color: rgba(150, 150, 165, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.active .iconfont {
|
||||||
|
color: rgba(130, 220, 130, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-label {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: rgba(150, 150, 165, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.active .tab-label {
|
||||||
|
color: rgba(130, 220, 130, 0.9);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<!-- components/GlobalLoading.vue -->
|
||||||
|
<template>
|
||||||
|
<view v-if="visible" class="global-loading">
|
||||||
|
<view class="loading-mask"></view>
|
||||||
|
<view class="loading-content">
|
||||||
|
<view class="loading-spinner"></view>
|
||||||
|
<text class="loading-text">{{ text }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const visible = ref(false)
|
||||||
|
const text = ref('加载中...')
|
||||||
|
|
||||||
|
// 显示
|
||||||
|
function show(loadingText = '加载中...') {
|
||||||
|
visible.value = true
|
||||||
|
text.value = loadingText
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏
|
||||||
|
function hide() {
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 挂载到全局
|
||||||
|
if (typeof uni !== 'undefined') {
|
||||||
|
uni.$globalLoading = { show, hide }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.global-loading {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-mask {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-content {
|
||||||
|
position: relative;
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 32rpx 48rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 48rpx;
|
||||||
|
height: 48rpx;
|
||||||
|
border: 4rpx solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-top-color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,487 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 团课卡片容器 -->
|
||||||
|
<view class="course-card" @click="goDetail">
|
||||||
|
<!-- 卡片顶部图片区域 -->
|
||||||
|
<view class="card-top">
|
||||||
|
<!-- 图片骨架屏 -->
|
||||||
|
<view v-if="!imageLoaded" class="skeleton skeleton-image"></view>
|
||||||
|
<!-- 课程封面图片 -->
|
||||||
|
<image
|
||||||
|
:src="course.coverImage"
|
||||||
|
mode="aspectFill"
|
||||||
|
class="cover-image"
|
||||||
|
:class="{ hidden: !imageLoaded }"
|
||||||
|
@load="imageLoaded = true"
|
||||||
|
@error="imageLoaded = true"
|
||||||
|
/>
|
||||||
|
<!-- 课程状态标签 -->
|
||||||
|
<view :class="['status-tag', statusClass]">
|
||||||
|
{{ statusText }}
|
||||||
|
</view>
|
||||||
|
<!-- 剩余名额标签 -->
|
||||||
|
<view class="spots-tag" v-if="course.currentMembers < course.maxMembers">
|
||||||
|
<span class="iconfont_courseCard icon-renwu-ren"></span>
|
||||||
|
<text>{{ course.maxMembers - course.currentMembers }}个名额</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 卡片内容区域 -->
|
||||||
|
<view class="card-content">
|
||||||
|
<!-- 课程名称 -->
|
||||||
|
<view class="course-name-wrapper">
|
||||||
|
<text class="course-name">{{ course.courseName }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 课程信息 -->
|
||||||
|
<view class="course-info">
|
||||||
|
<!-- 上课时间 -->
|
||||||
|
<view class="info-item">
|
||||||
|
<span class="iconfont_courseCard icon-shijian "></span>
|
||||||
|
<text class="info-text">{{ formatTime(course.startTime) }}</text>
|
||||||
|
</view>
|
||||||
|
<!-- 上课地点 -->
|
||||||
|
<view class="info-item">
|
||||||
|
<span class="iconfont_courseCard icon-didian"></span>
|
||||||
|
<text class="info-text">{{ course.location }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 课程时长 -->
|
||||||
|
<view class="course-duration">
|
||||||
|
<uni-icons type="time" size="14" color="#8A99B4" />
|
||||||
|
<text>{{ formatDuration(course.startTime, course.endTime) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 卡片底部操作区域 -->
|
||||||
|
<view class="card-footer">
|
||||||
|
<!-- 价格信息 -->
|
||||||
|
<view class="price-info">
|
||||||
|
<!-- 免费课程 -->
|
||||||
|
<view v-if="course.storedValueAmount === 0 && course.pointCardAmount === 0" class="price-free">
|
||||||
|
<text class="free-text">免费</text>
|
||||||
|
</view>
|
||||||
|
<!-- 支持多种支付方式 -->
|
||||||
|
<view v-else-if="course.storedValueAmount > 0 && course.pointCardAmount > 0" class="price-multi">
|
||||||
|
<view class="price-item stored-value">
|
||||||
|
<text class="currency">¥</text>
|
||||||
|
<text class="amount">{{ course.storedValueAmount }}</text>
|
||||||
|
<text class="label">储值卡</text>
|
||||||
|
</view>
|
||||||
|
<view class="price-divider"></view>
|
||||||
|
<view class="price-item point-card">
|
||||||
|
<uni-icons type="shop" size="14" color="#FF6B35" />
|
||||||
|
<text class="amount">{{ course.pointCardAmount }}次</text>
|
||||||
|
<text class="label">次卡</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- 仅储值卡支付 -->
|
||||||
|
<view v-else-if="course.storedValueAmount > 0" class="price-single">
|
||||||
|
<text class="price">
|
||||||
|
<text class="currency">¥</text>{{ course.storedValueAmount }}
|
||||||
|
</text>
|
||||||
|
<text class="pay-label">储值卡</text>
|
||||||
|
</view>
|
||||||
|
<!-- 仅次卡支付 -->
|
||||||
|
<view v-else-if="course.pointCardAmount > 0" class="price-single">
|
||||||
|
<text class="price points">
|
||||||
|
<uni-icons type="shop" size="14" color="#FF6B35" />
|
||||||
|
<text>{{ course.pointCardAmount }}次</text>
|
||||||
|
</text>
|
||||||
|
<text class="pay-label">次卡</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- 预约按钮 -->
|
||||||
|
<view :class="['booking-btn', { disabled: !canBook }]" @click.stop="handleBooking">
|
||||||
|
<text>{{ canBook ? '立即预约' : (course.currentMembers >= course.maxMembers ? '已满员' : '已结束') }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
// 图片加载状态
|
||||||
|
const imageLoaded = ref(false)
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
course: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['booking', 'detail'])
|
||||||
|
|
||||||
|
const statusText = computed(() => {
|
||||||
|
const status = props.course.status
|
||||||
|
if (status === '0') return '进行中'
|
||||||
|
if (status === '1') return '已取消'
|
||||||
|
if (status === '2') return '已结束'
|
||||||
|
return '未知'
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusClass = computed(() => {
|
||||||
|
const status = props.course.status
|
||||||
|
if (status === '0') return 'active'
|
||||||
|
if (status === '1') return 'canceled'
|
||||||
|
if (status === '2') return 'ended'
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const canBook = computed(() => {
|
||||||
|
const status = props.course.status
|
||||||
|
const isFull = props.course.currentMembers >= props.course.maxMembers
|
||||||
|
return status === '0' && !isFull
|
||||||
|
})
|
||||||
|
|
||||||
|
const formatTime = (dateStr) => {
|
||||||
|
if (!dateStr) return ''
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
const month = date.getMonth() + 1
|
||||||
|
const day = date.getDate()
|
||||||
|
const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
||||||
|
const weekDay = weekDays[date.getDay()]
|
||||||
|
const hours = date.getHours().toString().padStart(2, '0')
|
||||||
|
const minutes = date.getMinutes().toString().padStart(2, '0')
|
||||||
|
return `${month}月${day}日 ${weekDay} ${hours}:${minutes}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDuration = (startStr, endStr) => {
|
||||||
|
if (!startStr || !endStr) return ''
|
||||||
|
const start = new Date(startStr)
|
||||||
|
const end = new Date(endStr)
|
||||||
|
const minutes = Math.floor((end.getTime() - start.getTime()) / 60000)
|
||||||
|
return `${minutes}分钟`
|
||||||
|
}
|
||||||
|
|
||||||
|
const goDetail = () => {
|
||||||
|
emit('detail', props.course.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBooking = () => {
|
||||||
|
if (canBook.value) {
|
||||||
|
emit('booking', props.course)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
@import "@/common/style/iconfont_courseCard.css";
|
||||||
|
|
||||||
|
/* 团课卡片容器 */
|
||||||
|
.course-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.04);
|
||||||
|
margin-bottom: 28rpx;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片顶部图片区域 */
|
||||||
|
.card-top {
|
||||||
|
position: relative;
|
||||||
|
height: 320rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 课程封面图片 */
|
||||||
|
.cover-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 骨架屏基础样式 */
|
||||||
|
.skeleton {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background: linear-gradient(90deg, #f6f7f8 0%, #e0e0e0 20%, #f6f7f8 40%, #f6f7f8 100%);
|
||||||
|
background-size: 100% 100%;
|
||||||
|
animation: skeleton-loading 1.5s infinite;
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 骨架屏动画 */
|
||||||
|
@keyframes skeleton-loading {
|
||||||
|
0% {
|
||||||
|
background-position: 100% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: -100% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图片骨架屏 */
|
||||||
|
.skeleton-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 课程状态标签 */
|
||||||
|
.status-tag {
|
||||||
|
position: absolute;
|
||||||
|
top: 24rpx;
|
||||||
|
left: 24rpx;
|
||||||
|
padding: 10rpx 24rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
backdrop-filter: blur(8rpx);
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: linear-gradient(135deg, #10B981 0%, #059669 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.canceled {
|
||||||
|
background: linear-gradient(135deg, #9CA3AF 0%, #6B7280 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ended {
|
||||||
|
background: linear-gradient(135deg, #D1D5DB 0%, #9CA3AF 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 剩余名额标签 */
|
||||||
|
.spots-tag {
|
||||||
|
position: absolute;
|
||||||
|
top: 24rpx;
|
||||||
|
right: 24rpx;
|
||||||
|
padding: 10rpx 20rpx;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
backdrop-filter: blur(8rpx);
|
||||||
|
border-radius: 24rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #ffffff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片内容区域 */
|
||||||
|
.card-content {
|
||||||
|
padding: 28rpx 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 课程名称容器 */
|
||||||
|
.course-name-wrapper {
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 课程名称 */
|
||||||
|
.course-name {
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1F2937;
|
||||||
|
display: block;
|
||||||
|
line-height: 1.4;
|
||||||
|
letter-spacing: 0.5rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 课程信息容器 */
|
||||||
|
.course-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 信息项 */
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
padding: 12rpx 16rpx;
|
||||||
|
background: linear-gradient(135deg, #F9FAFB 0%, #F3F4F6 100%);
|
||||||
|
border-radius: 16rpx;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: linear-gradient(135deg, #F3F4F6 0%, #E5E7EB 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 信息图标 */
|
||||||
|
.info-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图标字体垂直居中 */
|
||||||
|
.iconfont_courseCard {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 信息文字 */
|
||||||
|
.info-text {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #4B5563;
|
||||||
|
line-height: 1.5;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 课程时长 */
|
||||||
|
.course-duration {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
padding: 10rpx 20rpx;
|
||||||
|
background: linear-gradient(135deg, #EFF6FF 0%, #DBEAFE 100%);
|
||||||
|
border-radius: 20rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #3B82F6;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片底部区域 */
|
||||||
|
.card-footer {
|
||||||
|
padding: 24rpx 32rpx 28rpx;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border-top: 1rpx solid #F3F4F6;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 价格信息 */
|
||||||
|
.price-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.price {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #FF6B35;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 4rpx;
|
||||||
|
|
||||||
|
.currency {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.points {
|
||||||
|
color: #FF6B35;
|
||||||
|
gap: 6rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 免费课程样式 */
|
||||||
|
.price-free {
|
||||||
|
.free-text {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #10B981;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 多种支付方式样式 */
|
||||||
|
.price-multi {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
|
||||||
|
.price-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6rpx;
|
||||||
|
|
||||||
|
.currency {
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #FF6B35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #FF6B35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
margin-left: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.stored-value {
|
||||||
|
.currency, .amount {
|
||||||
|
color: #FF6B35;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.point-card {
|
||||||
|
.amount {
|
||||||
|
color: #FF6B35;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-divider {
|
||||||
|
width: 2rpx;
|
||||||
|
height: 32rpx;
|
||||||
|
background: #E5E7EB;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 单一支付方式样式 */
|
||||||
|
.price-single {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8rpx;
|
||||||
|
|
||||||
|
.pay-label {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
padding: 4rpx 12rpx;
|
||||||
|
background: #F3F4F6;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 预约按钮 */
|
||||||
|
.booking-btn {
|
||||||
|
padding: 18rpx 48rpx;
|
||||||
|
background: linear-gradient(135deg, #FF6B35 0%, #FF8C5A 100%);
|
||||||
|
border-radius: 44rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
box-shadow: 0 8rpx 20rpx rgba(255, 107, 53, 0.25);
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
letter-spacing: 1rpx;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(255, 107, 53, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
background: linear-gradient(135deg, #F3F4F6 0%, #E5E7EB 100%);
|
||||||
|
color: #9CA3AF;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
<template>
|
||||||
|
<view class="filter-section">
|
||||||
|
<!-- 时间区间筛选 -->
|
||||||
|
<view class="filter-item" @click="handleTimePick">
|
||||||
|
<uni-icons type="calendar" size="18" color="#5E6F8D" class="filter-icon" />
|
||||||
|
<text class="filter-text">{{ timeRangeText || '选择时间' }}</text>
|
||||||
|
<uni-icons type="right" size="20" color="#A0AEC0" class="filter-arrow" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 排序方式 -->
|
||||||
|
<picker
|
||||||
|
mode="selector"
|
||||||
|
:range="sortOptions"
|
||||||
|
range-key="label"
|
||||||
|
:value="sortIndex"
|
||||||
|
@change="onSortChange"
|
||||||
|
>
|
||||||
|
<view class="filter-item">
|
||||||
|
<uni-icons type="list" size="18" color="#5E6F8D" class="filter-icon" />
|
||||||
|
<text class="filter-text">{{ sortOptions[sortIndex].label }}</text>
|
||||||
|
<uni-icons type="right" size="20" color="#A0AEC0" class="filter-arrow" />
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
timeRangeText: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
sortOptions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [
|
||||||
|
{ label: '默认排序', value: 'default' },
|
||||||
|
{ label: '价格从低到高', value: 'priceAsc' },
|
||||||
|
{ label: '价格从高到低', value: 'priceDesc' },
|
||||||
|
{ label: '剩余名额最多', value: 'spotsDesc' },
|
||||||
|
{ label: '仅次数卡', value: 'pointCardOnly' },
|
||||||
|
{ label: '仅储值卡', value: 'storedValueOnly' },
|
||||||
|
{ label: '两种支付', value: 'bothPayment' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
sortIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:sortIndex', 'timePick'])
|
||||||
|
|
||||||
|
const localSortIndex = ref(props.sortIndex)
|
||||||
|
|
||||||
|
watch(() => props.sortIndex, (val) => {
|
||||||
|
localSortIndex.value = val
|
||||||
|
})
|
||||||
|
|
||||||
|
const onSortChange = (e) => {
|
||||||
|
localSortIndex.value = e.detail.value
|
||||||
|
console.log('[FilterSection] 排序方式变更:', {
|
||||||
|
index: localSortIndex.value,
|
||||||
|
value: props.sortOptions[localSortIndex.value]
|
||||||
|
})
|
||||||
|
emit('update:sortIndex', localSortIndex.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTimePick = () => {
|
||||||
|
console.log('[FilterSection] 触发时间选择器')
|
||||||
|
emit('timePick')
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFilterParams = () => {
|
||||||
|
const params = {
|
||||||
|
sortType: props.sortOptions[localSortIndex.value].value,
|
||||||
|
timeRangeText: props.timeRangeText
|
||||||
|
}
|
||||||
|
console.log('[FilterSection] 获取筛选参数:', params)
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSortType = () => {
|
||||||
|
return props.sortOptions[localSortIndex.value].value
|
||||||
|
}
|
||||||
|
|
||||||
|
const comparePrice = (a, b, ascending = true) => {
|
||||||
|
const getEffectivePrice = (item) => {
|
||||||
|
if (item.storedValueAmount > 0 && item.pointCardAmount > 0) {
|
||||||
|
return Math.min(item.storedValueAmount, item.pointCardAmount * 50)
|
||||||
|
} else if (item.storedValueAmount > 0) {
|
||||||
|
return item.storedValueAmount
|
||||||
|
} else if (item.pointCardAmount > 0) {
|
||||||
|
return item.pointCardAmount * 50
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const priceA = getEffectivePrice(a)
|
||||||
|
const priceB = getEffectivePrice(b)
|
||||||
|
|
||||||
|
return ascending ? priceA - priceB : priceB - priceA
|
||||||
|
}
|
||||||
|
|
||||||
|
const compareByPaymentType = (a, b, sortType) => {
|
||||||
|
const getPaymentType = (item) => {
|
||||||
|
const hasPointCard = item.pointCardAmount > 0
|
||||||
|
const hasStoredValue = item.storedValueAmount > 0
|
||||||
|
|
||||||
|
if (hasPointCard && !hasStoredValue) return 1
|
||||||
|
if (!hasPointCard && hasStoredValue) return 2
|
||||||
|
if (hasPointCard && hasStoredValue) return 3
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeA = getPaymentType(a)
|
||||||
|
const typeB = getPaymentType(b)
|
||||||
|
|
||||||
|
switch (sortType) {
|
||||||
|
case 'pointCardOnly':
|
||||||
|
if (typeA === 1 && typeB !== 1) return -1
|
||||||
|
if (typeA !== 1 && typeB === 1) return 1
|
||||||
|
return 0
|
||||||
|
case 'storedValueOnly':
|
||||||
|
if (typeA === 2 && typeB !== 2) return -1
|
||||||
|
if (typeA !== 2 && typeB === 2) return 1
|
||||||
|
return 0
|
||||||
|
case 'bothPayment':
|
||||||
|
if (typeA === 3 && typeB !== 3) return -1
|
||||||
|
if (typeA !== 3 && typeB === 3) return 1
|
||||||
|
return 0
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortCourses = (courses) => {
|
||||||
|
const sortType = getSortType()
|
||||||
|
if (!courses || !Array.isArray(courses)) return []
|
||||||
|
|
||||||
|
const sorted = [...courses]
|
||||||
|
|
||||||
|
switch (sortType) {
|
||||||
|
case 'priceAsc':
|
||||||
|
sorted.sort((a, b) => comparePrice(a, b, true))
|
||||||
|
break
|
||||||
|
case 'priceDesc':
|
||||||
|
sorted.sort((a, b) => comparePrice(a, b, false))
|
||||||
|
break
|
||||||
|
case 'spotsDesc':
|
||||||
|
sorted.sort((a, b) => (b.maxMembers - b.currentMembers) - (a.maxMembers - a.currentMembers))
|
||||||
|
break
|
||||||
|
case 'pointCardOnly':
|
||||||
|
case 'storedValueOnly':
|
||||||
|
case 'bothPayment':
|
||||||
|
sorted.sort((a, b) => compareByPaymentType(a, b, sortType))
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
sorted.sort((a, b) => new Date(a.startTime) - new Date(b.startTime))
|
||||||
|
}
|
||||||
|
|
||||||
|
return sorted
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
getFilterParams,
|
||||||
|
getSortType,
|
||||||
|
sortCourses
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.filter-section {
|
||||||
|
display: flex;
|
||||||
|
gap: 16rpx;
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
padding: 20rpx 24rpx;
|
||||||
|
background: #F5F7FA;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #5E6F8D;
|
||||||
|
|
||||||
|
.filter-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-arrow {
|
||||||
|
margin-left: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
<template>
|
||||||
|
<view class="search-bar-wrapper">
|
||||||
|
<!-- 搜索框 -->
|
||||||
|
<view class="search-bar">
|
||||||
|
<view class="search-input-wrapper">
|
||||||
|
<uni-icons type="search" size="20" color="#A0AEC0" class="search-icon" />
|
||||||
|
<input
|
||||||
|
class="search-input"
|
||||||
|
v-model="keyword"
|
||||||
|
placeholder="搜索课程名称"
|
||||||
|
placeholder-class="input-placeholder"
|
||||||
|
@confirm="handleSearch"
|
||||||
|
/>
|
||||||
|
<uni-icons
|
||||||
|
v-if="keyword"
|
||||||
|
type="closeempty"
|
||||||
|
size="16"
|
||||||
|
color="#A0AEC0"
|
||||||
|
class="clear-icon"
|
||||||
|
@click="clearSearch"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="search-btn" @click="handleSearch">搜索</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 热门关键词 -->
|
||||||
|
<view class="hot-keywords">
|
||||||
|
<text class="hot-label">热门搜索:</text>
|
||||||
|
<view
|
||||||
|
v-for="(kw, index) in hotKeywords"
|
||||||
|
:key="index"
|
||||||
|
:class="['hot-tag', { active: keyword === kw }]"
|
||||||
|
@click="selectKeyword(kw)"
|
||||||
|
>
|
||||||
|
{{ kw }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
hotKeywords: {
|
||||||
|
type: Array,
|
||||||
|
default: () => ['燃脂', '瑜伽', '单车', '普拉提', '高强度']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'search'])
|
||||||
|
|
||||||
|
const keyword = ref(props.modelValue)
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (val) => {
|
||||||
|
keyword.value = val
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
console.log('[SearchBar] 搜索参数:', { keyword: keyword.value })
|
||||||
|
emit('update:modelValue', keyword.value)
|
||||||
|
emit('search', { keyword: keyword.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearSearch = () => {
|
||||||
|
keyword.value = ''
|
||||||
|
emit('update:modelValue', '')
|
||||||
|
console.log('[SearchBar] 已清除搜索关键词')
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectKeyword = (kw) => {
|
||||||
|
keyword.value = kw
|
||||||
|
handleSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSearchParams = () => {
|
||||||
|
console.log('[SearchBar] 获取搜索参数:', { keyword: keyword.value })
|
||||||
|
return { keyword: keyword.value }
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
getSearchParams,
|
||||||
|
clearSearch
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
/* 搜索框 */
|
||||||
|
.search-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
|
||||||
|
.search-input-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #F5F7FA;
|
||||||
|
border-radius: 44rpx;
|
||||||
|
padding: 0 24rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
margin-right: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #1a202c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-placeholder {
|
||||||
|
color: #A0AEC0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-icon {
|
||||||
|
padding: 8rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-btn {
|
||||||
|
padding: 0 32rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
line-height: 72rpx;
|
||||||
|
background: linear-gradient(135deg, #FF6B35 0%, #FF8C5A 100%);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 44rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 热门关键词 */
|
||||||
|
.hot-keywords {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
|
||||||
|
.hot-label {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #8A99B4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hot-tag {
|
||||||
|
padding: 8rpx 20rpx;
|
||||||
|
background: #F5F7FA;
|
||||||
|
color: #5E6F8D;
|
||||||
|
font-size: 24rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: linear-gradient(135deg, #FF6B35 0%, #FF8C5A 100%);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
<template>
|
||||||
|
<view class="time-period-selector">
|
||||||
|
<view class="sort-header">
|
||||||
|
<uni-icons type="calendar" size="16" color="#FF6B35" class="sort-icon" />
|
||||||
|
<text class="sort-label">上课时段</text>
|
||||||
|
</view>
|
||||||
|
<view class="slider-wrapper">
|
||||||
|
<view
|
||||||
|
v-for="(option, index) in timePeriodOptions"
|
||||||
|
:key="index"
|
||||||
|
:class="['slider-item', { active: currentIndex === index }]"
|
||||||
|
@click="handlePeriodChange(index)"
|
||||||
|
>
|
||||||
|
<span class="iconfont_time_select" v-bind:class="getPeriodIcon(option.value)"></span>
|
||||||
|
|
||||||
|
<text :class="['slider-text', { active: currentIndex === index }]">
|
||||||
|
{{ option.label.split(' ')[0] }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<!-- 滑动指示器 -->
|
||||||
|
<view
|
||||||
|
class="slider-indicator"
|
||||||
|
:style="{ left: `calc(8rpx + ${currentIndex} * (100% - 16rpx) / 4)` }"
|
||||||
|
></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
timePeriodOptions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [
|
||||||
|
{ label: '全部', value: 'all' },
|
||||||
|
{ label: '早上 (6-12 点)', value: 'morning', startHour: 6, endHour: 12 },
|
||||||
|
{ label: '下午 (12-18 点)', value: 'afternoon', startHour: 12, endHour: 18 },
|
||||||
|
{ label: '晚上 (18-24 点)', value: 'evening', startHour: 18, endHour: 24 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'change'])
|
||||||
|
|
||||||
|
const currentIndex = ref(props.modelValue)
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (val) => {
|
||||||
|
currentIndex.value = val
|
||||||
|
})
|
||||||
|
|
||||||
|
const getPeriodIcon = (value) => {
|
||||||
|
const iconMap = {
|
||||||
|
'all': 'icon-gengduo ',
|
||||||
|
'morning': 'icon-zaochen',
|
||||||
|
'afternoon': 'icon-xiawucha ',
|
||||||
|
'evening': 'icon-yewan '
|
||||||
|
}
|
||||||
|
return iconMap[value] || iconMap[all]
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePeriodChange = (index) => {
|
||||||
|
currentIndex.value = index
|
||||||
|
const option = props.timePeriodOptions[index]
|
||||||
|
console.log('[TimePeriodSelector] 时间段变更:', {
|
||||||
|
index,
|
||||||
|
value: option.value,
|
||||||
|
label: option.label
|
||||||
|
})
|
||||||
|
emit('update:modelValue', index)
|
||||||
|
emit('change', option)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTimePeriodParams = () => {
|
||||||
|
const option = props.timePeriodOptions[currentIndex.value]
|
||||||
|
const params = {
|
||||||
|
index: currentIndex.value,
|
||||||
|
value: option.value,
|
||||||
|
label: option.label,
|
||||||
|
startHour: option.startHour,
|
||||||
|
endHour: option.endHour
|
||||||
|
}
|
||||||
|
console.log('[TimePeriodSelector] 获取时间段参数:', params)
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
getTimePeriodParams,
|
||||||
|
getPeriodIcon
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
@import "@/common/style/iconfont_time_select.css";
|
||||||
|
|
||||||
|
.time-period-selector {
|
||||||
|
padding: 24rpx;
|
||||||
|
background: linear-gradient(135deg, #F5F7FA 0%, #E9EDF2 100%);
|
||||||
|
border-radius: 20rpx;
|
||||||
|
box-shadow: inset 0 2rpx 8rpx rgba(0, 0, 0, 0.03);
|
||||||
|
|
||||||
|
.sort-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
|
||||||
|
.sort-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-label {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #1a202c;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 8rpx;
|
||||||
|
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.slider-item {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
padding: 20rpx 0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
.slider-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #8A99B4;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
.slider-icon {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-text {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滑动指示器 */
|
||||||
|
.slider-indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 8rpx;
|
||||||
|
left: 8rpx;
|
||||||
|
width: calc((100% - 16rpx) / 4);
|
||||||
|
height: calc(100% - 16rpx);
|
||||||
|
background: linear-gradient(135deg, #FF6B35 0%, #FF8C5A 100%);
|
||||||
|
border-radius: 12rpx;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(255, 107, 53, 0.4);
|
||||||
|
transition: left 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,728 @@
|
|||||||
|
<template>
|
||||||
|
<view>
|
||||||
|
<!-- 时间范围选择弹窗 -->
|
||||||
|
<Transition name="modal">
|
||||||
|
<view v-if="visible" class="time-range-modal">
|
||||||
|
<view class="modal-mask" @click="handleClose"></view>
|
||||||
|
<view class="modal-content">
|
||||||
|
<view class="modal-header">
|
||||||
|
<text class="modal-title">选择时间范围</text>
|
||||||
|
<view class="header-actions">
|
||||||
|
<text class="modal-clear" @click="handleClear">清空</text>
|
||||||
|
<text class="modal-close" @click="handleClose">×</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="modal-body">
|
||||||
|
<!-- 开始日期 -->
|
||||||
|
<view class="date-item">
|
||||||
|
<text class="date-label">开始日期</text>
|
||||||
|
<view class="date-value" @click="toggleStartPicker">
|
||||||
|
<text>{{ localStartDate || '请选择' }}</text>
|
||||||
|
<text class="date-arrow">›</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- 结束日期 -->
|
||||||
|
<view class="date-item">
|
||||||
|
<text class="date-label">结束日期</text>
|
||||||
|
<view class="date-value" @click="toggleEndPicker">
|
||||||
|
<text>{{ localEndDate || '请选择' }}</text>
|
||||||
|
<text class="date-arrow">›</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- 快捷选择 -->
|
||||||
|
<view class="quick-select">
|
||||||
|
<text class="quick-label">快捷选择</text>
|
||||||
|
<view class="quick-btns">
|
||||||
|
<view
|
||||||
|
v-for="item in quickOptions"
|
||||||
|
:key="item.value"
|
||||||
|
class="quick-btn"
|
||||||
|
:class="{ active: quickSelected === item.value }"
|
||||||
|
@click="applyQuickOption(item.value)"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="modal-footer">
|
||||||
|
<view class="btn btn-cancel" @click="handleClose">取消</view>
|
||||||
|
<view class="btn btn-confirm" @click="handleConfirm">确定</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- 日期选择器弹窗 -->
|
||||||
|
<Transition name="date-picker">
|
||||||
|
<view v-if="showDatePicker" class="date-picker-modal">
|
||||||
|
<view class="date-mask" @click="showDatePicker = false"></view>
|
||||||
|
<view class="date-picker-content">
|
||||||
|
<view class="date-header">
|
||||||
|
<text class="date-prev" @click="prevMonth">‹</text>
|
||||||
|
<text class="date-title">{{ currentYear }}年{{ currentMonth }}月</text>
|
||||||
|
<text class="date-next" @click="nextMonth">›</text>
|
||||||
|
</view>
|
||||||
|
<view class="date-weekdays">
|
||||||
|
<text v-for="day in weekdays" :key="day">{{ day }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="date-days">
|
||||||
|
<view
|
||||||
|
v-for="(day, index) in calendarDays"
|
||||||
|
:key="index"
|
||||||
|
class="date-day"
|
||||||
|
:class="{
|
||||||
|
'other-month': !day.currentMonth,
|
||||||
|
'today': day.isToday,
|
||||||
|
'selected': day.date === selectedDate,
|
||||||
|
'disabled': day.disabled
|
||||||
|
}"
|
||||||
|
@click="selectDate(day)"
|
||||||
|
>
|
||||||
|
{{ day.day }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</Transition>
|
||||||
|
</view>
|
||||||
|
</Transition>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch, computed, onMounted } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
startDate: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
endDate: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:visible', 'update:startDate', 'update:endDate', 'confirm', 'cancel'])
|
||||||
|
|
||||||
|
const localStartDate = ref(props.startDate)
|
||||||
|
const localEndDate = ref(props.endDate)
|
||||||
|
const showDatePicker = ref(false)
|
||||||
|
const pickerType = ref('start') // 'start' | 'end'
|
||||||
|
const currentYear = ref(2024)
|
||||||
|
const currentMonth = ref(1)
|
||||||
|
const selectedDate = ref('')
|
||||||
|
const quickSelected = ref('')
|
||||||
|
|
||||||
|
const weekdays = ['日', '一', '二', '三', '四', '五', '六']
|
||||||
|
|
||||||
|
const quickOptions = [
|
||||||
|
{ label: '近7天', value: '7d' },
|
||||||
|
{ label: '近30天', value: '30d' },
|
||||||
|
{ label: '本月', value: 'month' },
|
||||||
|
{ label: '上月', value: 'lastMonth' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 监听 props 变化
|
||||||
|
watch(() => props.startDate, (val) => {
|
||||||
|
localStartDate.value = val
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => props.endDate, (val) => {
|
||||||
|
localEndDate.value = val
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => props.visible, (val) => {
|
||||||
|
if (val) {
|
||||||
|
// 弹窗打开时设置当前日期
|
||||||
|
const now = new Date()
|
||||||
|
currentYear.value = now.getFullYear()
|
||||||
|
currentMonth.value = now.getMonth() + 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算时间范围文本
|
||||||
|
const timeRangeText = computed(() => {
|
||||||
|
if (localStartDate.value && localEndDate.value) {
|
||||||
|
return `${localStartDate.value} 至 ${localEndDate.value}`
|
||||||
|
} else if (localStartDate.value) {
|
||||||
|
return `${localStartDate.value} 起`
|
||||||
|
} else if (localEndDate.value) {
|
||||||
|
return `至 ${localEndDate.value}`
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 生成日历日期
|
||||||
|
const calendarDays = computed(() => {
|
||||||
|
const days = []
|
||||||
|
const firstDay = new Date(currentYear.value, currentMonth.value - 1, 1)
|
||||||
|
const lastDay = new Date(currentYear.value, currentMonth.value, 0)
|
||||||
|
const startDay = firstDay.getDay()
|
||||||
|
const totalDays = lastDay.getDate()
|
||||||
|
|
||||||
|
// 上个月的天数
|
||||||
|
const prevMonthLastDay = new Date(currentYear.value, currentMonth.value - 1, 0).getDate()
|
||||||
|
for (let i = startDay - 1; i >= 0; i--) {
|
||||||
|
days.push({
|
||||||
|
day: prevMonthLastDay - i,
|
||||||
|
date: formatDate(currentYear.value, currentMonth.value - 1, prevMonthLastDay - i),
|
||||||
|
currentMonth: false,
|
||||||
|
isToday: false,
|
||||||
|
disabled: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 本月的天数
|
||||||
|
const today = new Date()
|
||||||
|
for (let i = 1; i <= totalDays; i++) {
|
||||||
|
const dateStr = formatDate(currentYear.value, currentMonth.value, i)
|
||||||
|
days.push({
|
||||||
|
day: i,
|
||||||
|
date: dateStr,
|
||||||
|
currentMonth: true,
|
||||||
|
isToday: isToday(currentYear.value, currentMonth.value, i),
|
||||||
|
disabled: isFuture(currentYear.value, currentMonth.value, i)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下个月的天数
|
||||||
|
const remaining = 42 - days.length
|
||||||
|
for (let i = 1; i <= remaining; i++) {
|
||||||
|
days.push({
|
||||||
|
day: i,
|
||||||
|
date: formatDate(currentYear.value, currentMonth.value + 1, i),
|
||||||
|
currentMonth: false,
|
||||||
|
isToday: false,
|
||||||
|
disabled: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return days
|
||||||
|
})
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
function formatDate(year, month, day) {
|
||||||
|
const m = month.toString().padStart(2, '0')
|
||||||
|
const d = day.toString().padStart(2, '0')
|
||||||
|
return `${year}-${m}-${d}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否是今天
|
||||||
|
function isToday(year, month, day) {
|
||||||
|
const today = new Date()
|
||||||
|
return year === today.getFullYear() &&
|
||||||
|
month === today.getMonth() + 1 &&
|
||||||
|
day === today.getDate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否是未来日期
|
||||||
|
function isFuture(year, month, day) {
|
||||||
|
const today = new Date()
|
||||||
|
today.setHours(0, 0, 0, 0)
|
||||||
|
const date = new Date(year, month - 1, day)
|
||||||
|
return date > today
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换日期选择器
|
||||||
|
const toggleStartPicker = () => {
|
||||||
|
pickerType.value = 'start'
|
||||||
|
selectedDate.value = localStartDate.value
|
||||||
|
showDatePicker.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleEndPicker = () => {
|
||||||
|
pickerType.value = 'end'
|
||||||
|
selectedDate.value = localEndDate.value
|
||||||
|
showDatePicker.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 月份切换
|
||||||
|
const prevMonth = () => {
|
||||||
|
if (currentMonth.value === 1) {
|
||||||
|
currentYear.value--
|
||||||
|
currentMonth.value = 12
|
||||||
|
} else {
|
||||||
|
currentMonth.value--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextMonth = () => {
|
||||||
|
if (currentMonth.value === 12) {
|
||||||
|
currentYear.value++
|
||||||
|
currentMonth.value = 1
|
||||||
|
} else {
|
||||||
|
currentMonth.value++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择日期
|
||||||
|
const selectDate = (day) => {
|
||||||
|
if (day.disabled) return
|
||||||
|
selectedDate.value = day.date
|
||||||
|
if (pickerType.value === 'start') {
|
||||||
|
localStartDate.value = day.date
|
||||||
|
emit('update:startDate', day.date)
|
||||||
|
} else {
|
||||||
|
localEndDate.value = day.date
|
||||||
|
emit('update:endDate', day.date)
|
||||||
|
}
|
||||||
|
showDatePicker.value = false
|
||||||
|
console.log(`[TimeRangePicker] ${pickerType.value === 'start' ? '开始' : '结束'}日期变更:`, day.date)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用快捷选项
|
||||||
|
const applyQuickOption = (value) => {
|
||||||
|
quickSelected.value = value
|
||||||
|
const today = new Date()
|
||||||
|
let startDate, endDate
|
||||||
|
|
||||||
|
switch (value) {
|
||||||
|
case '7d':
|
||||||
|
startDate = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000)
|
||||||
|
endDate = today
|
||||||
|
break
|
||||||
|
case '30d':
|
||||||
|
startDate = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000)
|
||||||
|
endDate = today
|
||||||
|
break
|
||||||
|
case 'month':
|
||||||
|
startDate = new Date(today.getFullYear(), today.getMonth(), 1)
|
||||||
|
endDate = today
|
||||||
|
break
|
||||||
|
case 'lastMonth':
|
||||||
|
startDate = new Date(today.getFullYear(), today.getMonth() - 1, 1)
|
||||||
|
endDate = new Date(today.getFullYear(), today.getMonth(), 0)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
localStartDate.value = formatDate(startDate.getFullYear(), startDate.getMonth() + 1, startDate.getDate())
|
||||||
|
localEndDate.value = formatDate(endDate.getFullYear(), endDate.getMonth() + 1, endDate.getDate())
|
||||||
|
emit('update:startDate', localStartDate.value)
|
||||||
|
emit('update:endDate', localEndDate.value)
|
||||||
|
console.log('[TimeRangePicker] 快捷选择:', value, { start: localStartDate.value, end: localEndDate.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空选择
|
||||||
|
const handleClear = () => {
|
||||||
|
localStartDate.value = ''
|
||||||
|
localEndDate.value = ''
|
||||||
|
quickSelected.value = ''
|
||||||
|
emit('update:startDate', '')
|
||||||
|
emit('update:endDate', '')
|
||||||
|
console.log('[TimeRangePicker] 已清空时间选择')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭弹窗
|
||||||
|
const handleClose = () => {
|
||||||
|
console.log('[TimeRangePicker] 关闭时间选择器')
|
||||||
|
emit('update:visible', false)
|
||||||
|
emit('cancel')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认选择
|
||||||
|
const handleConfirm = () => {
|
||||||
|
console.log('[TimeRangePicker] 确认时间范围:', {
|
||||||
|
startDate: localStartDate.value,
|
||||||
|
endDate: localEndDate.value,
|
||||||
|
timeRangeText: timeRangeText.value
|
||||||
|
})
|
||||||
|
emit('update:visible', false)
|
||||||
|
emit('confirm', {
|
||||||
|
startDate: localStartDate.value,
|
||||||
|
endDate: localEndDate.value,
|
||||||
|
timeRangeText: timeRangeText.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取参数
|
||||||
|
const getTimeRangeParams = () => {
|
||||||
|
const params = {
|
||||||
|
startDate: localStartDate.value,
|
||||||
|
endDate: localEndDate.value,
|
||||||
|
timeRangeText: timeRangeText.value
|
||||||
|
}
|
||||||
|
console.log('[TimeRangePicker] 获取时间范围参数:', params)
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置日期
|
||||||
|
const resetDate = () => {
|
||||||
|
localStartDate.value = ''
|
||||||
|
localEndDate.value = ''
|
||||||
|
quickSelected.value = ''
|
||||||
|
emit('update:startDate', '')
|
||||||
|
emit('update:endDate', '')
|
||||||
|
console.log('[TimeRangePicker] 已重置日期')
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
getTimeRangeParams,
|
||||||
|
resetDate,
|
||||||
|
timeRangeText
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.time-range-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.modal-mask {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
position: relative;
|
||||||
|
width: 640rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 32rpx;
|
||||||
|
border-bottom: 1rpx solid #E9EDF2;
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a202c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-clear {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #8A99B4;
|
||||||
|
padding: 8rpx 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
font-size: 48rpx;
|
||||||
|
color: #8A99B4;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 32rpx;
|
||||||
|
|
||||||
|
.date-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 24rpx 0;
|
||||||
|
border-bottom: 1rpx solid #F0F2F5;
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-label {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-value {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #1a202c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-arrow {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #C4C9D4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-select {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
|
||||||
|
.quick-label {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-btns {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16rpx;
|
||||||
|
|
||||||
|
.quick-btn {
|
||||||
|
padding: 16rpx 28rpx;
|
||||||
|
background: #F5F7FA;
|
||||||
|
border-radius: 32rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: #FF6B35;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
display: flex;
|
||||||
|
border-top: 1rpx solid #E9EDF2;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 32rpx;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 30rpx;
|
||||||
|
|
||||||
|
&.btn-cancel {
|
||||||
|
color: #6B7280;
|
||||||
|
border-right: 1rpx solid #E9EDF2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-confirm {
|
||||||
|
color: #FF6B35;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 1001;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
|
||||||
|
.date-mask {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker-content {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 32rpx 32rpx 0 0;
|
||||||
|
transform: translateY(0);
|
||||||
|
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
.date-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 32rpx;
|
||||||
|
|
||||||
|
.date-prev, .date-next {
|
||||||
|
font-size: 40rpx;
|
||||||
|
color: #1a202c;
|
||||||
|
width: 64rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a202c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-weekdays {
|
||||||
|
display: flex;
|
||||||
|
padding: 0 24rpx;
|
||||||
|
|
||||||
|
text {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
padding: 16rpx 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-days {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 16rpx 24rpx 32rpx;
|
||||||
|
|
||||||
|
.date-day {
|
||||||
|
width: calc(100% / 7);
|
||||||
|
height: 80rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #1a202c;
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
&.other-month {
|
||||||
|
color: #D1D5DB;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.today {
|
||||||
|
background: #F5F7FA;
|
||||||
|
color: #FF6B35;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background: #FF6B35;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
color: #E5E7EB;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 居中弹窗进入动画 */
|
||||||
|
.modal-enter-active {
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-enter-active .modal-mask {
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-enter-active .modal-content {
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-enter-from .modal-mask {
|
||||||
|
background: rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-enter-from .modal-content {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 居中弹窗退出动画 */
|
||||||
|
.modal-leave-active {
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-leave-active .modal-mask {
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-leave-active .modal-content {
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-leave-to .modal-mask {
|
||||||
|
background: rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-leave-to .modal-content {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 日期选择器进入动画 */
|
||||||
|
.date-picker-enter-active {
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker-enter-active .date-mask {
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker-enter-active .date-picker-content {
|
||||||
|
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker-enter-from .date-mask {
|
||||||
|
background: rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker-enter-from .date-picker-content {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 日期选择器退出动画 */
|
||||||
|
.date-picker-leave-active {
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker-leave-active .date-mask {
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker-leave-active .date-picker-content {
|
||||||
|
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker-leave-to .date-mask {
|
||||||
|
background: rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker-leave-to .date-picker-content {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
<template>
|
||||||
|
<view class="banner-container">
|
||||||
|
<swiper
|
||||||
|
class="banner-swiper"
|
||||||
|
:circular="true"
|
||||||
|
:autoplay="true"
|
||||||
|
:interval="4000"
|
||||||
|
:duration="500"
|
||||||
|
:indicator-dots="false"
|
||||||
|
@change="onSwiperChange"
|
||||||
|
>
|
||||||
|
<swiper-item v-for="(banner, index) in banners" :key="index">
|
||||||
|
<view class="banner-content" @click="previewImage(index)">
|
||||||
|
<!-- 添加 lazy-load 属性实现懒加载 -->
|
||||||
|
<image
|
||||||
|
:src="banner.image"
|
||||||
|
mode="aspectFill"
|
||||||
|
class="banner-image"
|
||||||
|
lazy-load
|
||||||
|
:show-menu-by-longpress="false"
|
||||||
|
@load="onImageLoad(index)"
|
||||||
|
@error="onImageError(index)"
|
||||||
|
/>
|
||||||
|
<view class="banner-overlay"></view>
|
||||||
|
<view class="banner-text">
|
||||||
|
<text class="banner-title">{{ banner.title }}</text>
|
||||||
|
<text class="banner-subtitle">{{ banner.subtitle }}</text>
|
||||||
|
<text class="banner-desc">{{ banner.desc }}</text>
|
||||||
|
</view>
|
||||||
|
<!-- 可选:添加加载占位符 -->
|
||||||
|
<view v-if="!imageLoaded[index]" class="image-placeholder">
|
||||||
|
<view class="loading-spinner"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</swiper-item>
|
||||||
|
</swiper>
|
||||||
|
<view class="banner-dots">
|
||||||
|
<view
|
||||||
|
v-for="(_, index) in banners"
|
||||||
|
:key="index"
|
||||||
|
:class="['dot', { active: currentIndex === index }]"
|
||||||
|
></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const banners = [
|
||||||
|
{
|
||||||
|
image: 'https://images.unsplash.com/photo-1534438327276-14e5300c3a48?w=800&q=80',
|
||||||
|
title: '突破自我',
|
||||||
|
subtitle: '超越极限',
|
||||||
|
desc: '科学训练 · 遇见更好的自己'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: 'https://images.unsplash.com/photo-1517836357463-d25dfeac3438?w=800&q=80',
|
||||||
|
title: '专业指导',
|
||||||
|
subtitle: '高效训练',
|
||||||
|
desc: '私人定制 · 专属健身方案'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: 'https://images.unsplash.com/photo-1571019614242-c5c5dee9f50b?w=800&q=80',
|
||||||
|
title: '健康生活',
|
||||||
|
subtitle: '从这里开始',
|
||||||
|
desc: '全方位 · 打造完美体态'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const currentIndex = ref(0)
|
||||||
|
// 记录每张图片的加载状态
|
||||||
|
const imageLoaded = ref(banners.map(() => false))
|
||||||
|
|
||||||
|
const onSwiperChange = (e) => {
|
||||||
|
currentIndex.value = e.detail.current
|
||||||
|
}
|
||||||
|
|
||||||
|
const previewImage = (index) => {
|
||||||
|
const urls = banners.map(banner => banner.image)
|
||||||
|
uni.previewImage({
|
||||||
|
urls: urls,
|
||||||
|
current: urls[index],
|
||||||
|
indicator: 'default',
|
||||||
|
loop: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片加载成功回调
|
||||||
|
const onImageLoad = (index) => {
|
||||||
|
imageLoaded.value[index] = true
|
||||||
|
console.log(`图片 ${index} 加载完成`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片加载失败回调
|
||||||
|
const onImageError = (index) => {
|
||||||
|
console.error(`图片 ${index} 加载失败`)
|
||||||
|
// 可选:设置默认占位图
|
||||||
|
// banners[index].image = '默认图片URL'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.banner-container {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-swiper {
|
||||||
|
width: 100%;
|
||||||
|
height: 480rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 添加图片占位符样式 */
|
||||||
|
.image-placeholder {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(135deg, #f5f7fa 0%, #e8ecf1 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 60rpx;
|
||||||
|
height: 60rpx;
|
||||||
|
border: 4rpx solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-top: 4rpx solid #7AB5CC;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-overlay {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 2; /* 确保遮罩层在占位符上面 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 其余样式保持不变 */
|
||||||
|
.banner-text {
|
||||||
|
position: absolute;
|
||||||
|
left: 36rpx;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 48rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #ffffff;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
text-shadow: 0 4rpx 16rpx rgba(80, 150, 190, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-subtitle {
|
||||||
|
display: block;
|
||||||
|
font-size: 56rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #E0F0FA;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
text-shadow: 0 4rpx 16rpx rgba(80, 150, 190, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-desc {
|
||||||
|
display: block;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-dots {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: -100rpx;
|
||||||
|
position: relative;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 48rpx;
|
||||||
|
height: 8rpx;
|
||||||
|
border-radius: 9999rpx;
|
||||||
|
background: #D0E4EE;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot.active {
|
||||||
|
width: 64rpx;
|
||||||
|
background: linear-gradient(90deg, #7AB5CC, #9CCFDF);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
<template>
|
||||||
|
<view class="quick-entry">
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in entries"
|
||||||
|
:key="index"
|
||||||
|
class="entry-item"
|
||||||
|
@tap="QEClick(item.path)"
|
||||||
|
>
|
||||||
|
<view :class="['entry-icon', { accent: item.accent }]">
|
||||||
|
<image :src="item.icon" mode="aspectFit" class="icon-img" />
|
||||||
|
</view>
|
||||||
|
<text class="entry-title">{{ item.title }}</text>
|
||||||
|
<text class="entry-desc">{{ item.desc }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
const QEClick = path => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url:path
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const entries = [
|
||||||
|
{
|
||||||
|
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/course.png',
|
||||||
|
title: '找课程',
|
||||||
|
desc: '精品课程',
|
||||||
|
accent: false,
|
||||||
|
path: "/pages/searchCourse/searchCourse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/plan.png',
|
||||||
|
title: '训练计划',
|
||||||
|
desc: '个性定制',
|
||||||
|
accent: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/data.png',
|
||||||
|
title: '健身数据',
|
||||||
|
desc: '记录分析',
|
||||||
|
accent: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/message.png',
|
||||||
|
title: '消息',
|
||||||
|
desc: '通知消息',
|
||||||
|
accent: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/checkIn.png',
|
||||||
|
title: '签到',
|
||||||
|
desc: '打卡签到',
|
||||||
|
accent: false,
|
||||||
|
path: "/pages/checkIn/checkIn"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.quick-entry {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 32rpx 24rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.55);
|
||||||
|
backdrop-filter: blur(24px);
|
||||||
|
-webkit-backdrop-filter: blur(24px);
|
||||||
|
margin: 24rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
box-shadow: 0 8rpx 32rpx var(--shadow-blue-light);
|
||||||
|
border: 1rpx solid rgba(255, 255, 255, 0.7);
|
||||||
|
position: relative;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-icon {
|
||||||
|
width: 104rpx;
|
||||||
|
height: 104rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: rgba(130, 220, 130, 0.9);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
box-shadow: 0 6rpx 20rpx rgba(130, 220, 130, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-img {
|
||||||
|
width: 52rpx;
|
||||||
|
height: 52rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-icon.accent {
|
||||||
|
background: rgba(130, 220, 130, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-title {
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2D4A5A;
|
||||||
|
margin-bottom: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-desc {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: var(--tabbar-text-inactive);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,370 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 推荐课程容器 -->
|
||||||
|
<view class="recommend-courses">
|
||||||
|
<!-- 区域标题栏 -->
|
||||||
|
<view class="section-header">
|
||||||
|
<!-- 区域标题 -->
|
||||||
|
<text class="section-title">推荐课程</text>
|
||||||
|
<!-- 查看更多按钮 -->
|
||||||
|
<view class="view-more">
|
||||||
|
<text>查看更多</text>
|
||||||
|
<text class="arrow">
|
||||||
|
<uni-icons type="right" size="20" color="#8CA0B0"/>
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 课程横向滚动容器 -->
|
||||||
|
<scroll-view class="courses-scroll" scroll-x="true" :show-scrollbar="false">
|
||||||
|
<!-- 课程列表 -->
|
||||||
|
<view class="courses-list">
|
||||||
|
<!-- 课程卡片 -->
|
||||||
|
<view
|
||||||
|
v-for="(course, index) in courses"
|
||||||
|
:key="course.id || index"
|
||||||
|
class="course-card"
|
||||||
|
>
|
||||||
|
<!-- 课程图片区域 -->
|
||||||
|
<view class="course-image">
|
||||||
|
<!-- 课程封面图片 -->
|
||||||
|
<image :src="course.image" mode="aspectFill" class="img" />
|
||||||
|
<!-- 图片渐变遮罩 -->
|
||||||
|
<view class="course-overlay"></view>
|
||||||
|
<!-- 课程标签 -->
|
||||||
|
<text :class="['course-tag', course.tagType]">{{ course.tag }}</text>
|
||||||
|
<!-- 课程信息区域 -->
|
||||||
|
<view class="course-info">
|
||||||
|
<!-- 课程名称 -->
|
||||||
|
<text class="course-name">{{ course.name }}</text>
|
||||||
|
<!-- 课程元信息(时长、难度) -->
|
||||||
|
<view class="course-meta">
|
||||||
|
<!-- 时长信息 -->
|
||||||
|
<view class="meta-item">
|
||||||
|
<text class="meta-icon">
|
||||||
|
<image src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/time.png"/>
|
||||||
|
</text>
|
||||||
|
<text>{{ course.duration }}</text>
|
||||||
|
</view>
|
||||||
|
<!-- 难度信息 -->
|
||||||
|
<view class="meta-item">
|
||||||
|
<text class="meta-icon">
|
||||||
|
<image src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/intensity.png"/>
|
||||||
|
</text>
|
||||||
|
<text>{{ course.level }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- 课程底部区域 -->
|
||||||
|
<view class="course-footer">
|
||||||
|
<!-- 参与人数信息 -->
|
||||||
|
<view class="participants">
|
||||||
|
<text class="fire-icon">
|
||||||
|
<image src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/hot.png"/>
|
||||||
|
</text>
|
||||||
|
<text>{{ course.participants }}人参与</text>
|
||||||
|
</view>
|
||||||
|
<!-- 去参与按钮 -->
|
||||||
|
<view class="join-btn" @click="handleJoinCourse(course)">
|
||||||
|
<text>去参与</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { getGroupCoursePage } from '@/api/main.js'
|
||||||
|
|
||||||
|
// 测试开关:设置为 true 时使用假数据,false 时使用真实API数据
|
||||||
|
const USE_MOCK_DATA = true
|
||||||
|
|
||||||
|
// 推荐课程数据列表
|
||||||
|
const courses = ref([])
|
||||||
|
|
||||||
|
// 课程类型映射(用于显示标签)
|
||||||
|
const getCourseTypeName = (type) => {
|
||||||
|
const typeMap = {
|
||||||
|
'1': '瑜伽',
|
||||||
|
'2': '搏击',
|
||||||
|
'3': '塑形'
|
||||||
|
}
|
||||||
|
return typeMap[type] || '课程'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据课程信息获取标签文本
|
||||||
|
const getTag = (course) => {
|
||||||
|
if (course.currentMembers >= course.maxMembers) return '已满员'
|
||||||
|
if (course.status === '2') return '已结束'
|
||||||
|
if (course.currentMembers / course.maxMembers >= 0.8) return '热门'
|
||||||
|
return getCourseTypeName(course.courseType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据课程信息获取标签样式类型
|
||||||
|
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'
|
||||||
|
return 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算课程时长
|
||||||
|
const calculateDuration = (startTime, endTime) => {
|
||||||
|
if (!startTime || !endTime) return '60分钟'
|
||||||
|
const start = new Date(startTime)
|
||||||
|
const end = new Date(endTime)
|
||||||
|
const durationMinutes = Math.floor((end - start) / (1000 * 60))
|
||||||
|
return `${durationMinutes}分钟`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取课程难度
|
||||||
|
const getCourseLevel = (course) => {
|
||||||
|
if (course.courseType === '2') return '中级'
|
||||||
|
if (course.courseType === '3') return '高级'
|
||||||
|
if (course.courseType === '1') return '初级'
|
||||||
|
return '初级'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理图片URL
|
||||||
|
const getImageUrl = (coverImage) => {
|
||||||
|
if (!coverImage) return 'https://images.unsplash.com/photo-1534438327276-14e5300c3a48?w=400&q=80'
|
||||||
|
if (coverImage.startsWith('http')) return coverImage
|
||||||
|
return `https://your-domain.com${coverImage}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取推荐课程
|
||||||
|
const fetchRecommendCourses = async () => {
|
||||||
|
// 如果测试开关打开,直接使用假数据
|
||||||
|
if (USE_MOCK_DATA) {
|
||||||
|
useFallbackData()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await getGroupCoursePage({
|
||||||
|
page: 0, size: 5, sort: 'current_members', order: 'desc'
|
||||||
|
}, { cache: true, cacheTime: 5 * 60 * 1000 })
|
||||||
|
if (res && res.content && Array.isArray(res.content)) {
|
||||||
|
courses.value = res.content.map(course => ({
|
||||||
|
id: course.id, image: getImageUrl(course.coverImage), tag: getTag(course),
|
||||||
|
tagType: getTagType(course), name: course.courseName || '未知课程',
|
||||||
|
duration: calculateDuration(course.startTime, course.endTime),
|
||||||
|
level: getCourseLevel(course), participants: course.currentMembers || 0, rawData: course
|
||||||
|
}))
|
||||||
|
} else { useFallbackData() }
|
||||||
|
} catch (err) { useFallbackData() }
|
||||||
|
}
|
||||||
|
|
||||||
|
const useFallbackData = () => {
|
||||||
|
const fallbackContent = [
|
||||||
|
{ id: "3", courseName: "燃脂搏击", courseType: "2", startTime: "2026-06-10T18:30:00", endTime: "2026-06-10T19:30:00", maxMembers: 20, currentMembers: 20, status: "0", coverImage: "https://picsum.photos/id/100/800/600", description: "高强度间歇训练" },
|
||||||
|
{ id: "2", courseName: "清晨流瑜伽", courseType: "1", startTime: "2026-06-12T09:00:00", endTime: "2026-06-12T10:30:00", maxMembers: 15, currentMembers: 5, status: "0", coverImage: "https://picsum.photos/id/101/800/600", description: "流畅体式" },
|
||||||
|
{ id: "4", courseName: "哈他瑜伽", courseType: "1", startTime: "2026-06-01T15:20:00", endTime: "2026-06-01T16:50:00", maxMembers: 12, currentMembers: 3, status: "0", coverImage: "https://picsum.photos/id/102/800/600", description: "基础瑜伽" },
|
||||||
|
{ id: "6", courseName: "蜜桃臀塑造", courseType: "3", startTime: "2026-05-30T19:00:00", endTime: "2026-05-30T20:00:00", maxMembers: 10, currentMembers: 8, status: "2", coverImage: "https://picsum.photos/id/103/800/600", description: "臀部训练" },
|
||||||
|
{ id: "7", courseName: "午间冥想放松", courseType: "1", startTime: "2026-05-31T12:00:00", endTime: "2026-05-31T13:00:00", maxMembers: 15, currentMembers: 6, status: "2", coverImage: "https://picsum.photos/id/104/800/600", description: "冥想" }
|
||||||
|
]
|
||||||
|
courses.value = fallbackContent.map(course => ({
|
||||||
|
id: course.id, image: getImageUrl(course.coverImage), tag: getTag(course),
|
||||||
|
tagType: getTagType(course), name: course.courseName || '未知课程',
|
||||||
|
duration: calculateDuration(course.startTime, course.endTime),
|
||||||
|
level: getCourseLevel(course), participants: course.currentMembers || 0, rawData: course
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleJoinCourse = (course) => {
|
||||||
|
if (course.rawData.status === '2') { 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}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => { fetchRecommendCourses() })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.recommend-courses {
|
||||||
|
padding: 0 24rpx;
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2D4A5A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-more {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: var(--tabbar-text-inactive);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
font-size: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.courses-scroll {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.courses-list {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 48rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-card {
|
||||||
|
width: 320rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.6);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
-webkit-backdrop-filter: blur(16px);
|
||||||
|
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: 280rpx;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
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%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-tag {
|
||||||
|
position: absolute;
|
||||||
|
top: 16rpx;
|
||||||
|
right: 16rpx;
|
||||||
|
padding: 8rpx 16rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
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: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-name {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 16rpx;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: end;
|
||||||
|
gap: 6rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-icon {
|
||||||
|
font-size: 20rpx;
|
||||||
|
image{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 25rpx;
|
||||||
|
height: 25rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-footer {
|
||||||
|
padding: 16rpx 10rpx;
|
||||||
|
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 {
|
||||||
|
font-size: 24rpx;
|
||||||
|
image{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 30rpx;
|
||||||
|
height: 30rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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>
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 今日推荐容器 -->
|
||||||
|
<view class="today-recommend">
|
||||||
|
<!-- 区域标题栏 -->
|
||||||
|
<view class="section-header">
|
||||||
|
<!-- 区域标题 -->
|
||||||
|
<text class="section-title">今日推荐</text>
|
||||||
|
<!-- 查看更多按钮 -->
|
||||||
|
<view class="view-more">
|
||||||
|
<text>查看更多</text>
|
||||||
|
<text class="arrow">
|
||||||
|
<uni-icons type="right" size="20" color="#8CA0B0"></uni-icons>
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 推荐列表 -->
|
||||||
|
<view class="recommend-list">
|
||||||
|
<!-- 推荐项 -->
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in recommends"
|
||||||
|
:key="index"
|
||||||
|
class="recommend-item"
|
||||||
|
>
|
||||||
|
<!-- 推荐项图片 -->
|
||||||
|
<image :src="item.image" mode="aspectFill" class="item-image" />
|
||||||
|
<!-- 推荐项内容区域 -->
|
||||||
|
<view class="item-content">
|
||||||
|
<!-- 推荐项标题 -->
|
||||||
|
<text class="item-title">{{ item.title }}</text>
|
||||||
|
<!-- 推荐项标签列表 -->
|
||||||
|
<view class="item-tags">
|
||||||
|
<text v-for="(tag, tagIndex) in item.tags" :key="tagIndex" class="tag">{{ tag }}</text>
|
||||||
|
</view>
|
||||||
|
<!-- 推荐项描述 -->
|
||||||
|
<text class="item-desc">{{ item.desc }}</text>
|
||||||
|
</view>
|
||||||
|
<!-- 推荐项操作区域 -->
|
||||||
|
<view class="item-action">
|
||||||
|
<!-- 开始训练按钮 -->
|
||||||
|
<view class="start-btn">
|
||||||
|
<text class="start-btn-text">开始训练</text>
|
||||||
|
</view>
|
||||||
|
<!-- 参与人数 -->
|
||||||
|
<text class="participants">{{ item.participants }}人参与</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
// 今日推荐数据列表
|
||||||
|
const recommends = [
|
||||||
|
{
|
||||||
|
image: 'https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?w=300&q=80',
|
||||||
|
title: '晨间活力唤醒跑',
|
||||||
|
tags: ['20分钟', '初级'],
|
||||||
|
desc: '唤醒身体,开启活力一天',
|
||||||
|
participants: '2784'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: 'https://images.unsplash.com/photo-1581009146145-b5ef050c149a?w=300&q=80',
|
||||||
|
title: '全身力量塑形',
|
||||||
|
tags: ['50分钟', '中级'],
|
||||||
|
desc: '全身综合训练,塑造完美线条',
|
||||||
|
participants: '4126'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: 'https://images.unsplash.com/photo-1490645935967-10de6ba17061?w=300&q=80',
|
||||||
|
title: '蛋白增肌饮食指南',
|
||||||
|
tags: ['营养饮食', '12分钟'],
|
||||||
|
desc: '科学饮食搭配,助力肌肉增长',
|
||||||
|
participants: '1865'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.today-recommend {
|
||||||
|
padding: 0 24rpx;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2D4A5A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-more {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: var(--tabbar-text-inactive);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
font-size: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommend-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommend-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 24rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.6);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
-webkit-backdrop-filter: blur(16px);
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
box-shadow: 0 8rpx 28rpx var(--shadow-blue-light);
|
||||||
|
border: 1rpx solid rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-image {
|
||||||
|
width: 200rpx;
|
||||||
|
height: 160rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2D4A5A;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-tags {
|
||||||
|
display: flex;
|
||||||
|
gap: 12rpx;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
padding: 6rpx 16rpx;
|
||||||
|
background: rgba(122, 181, 204, 0.12);
|
||||||
|
border-radius: 8rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #6BA8C0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-desc {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: var(--tabbar-text-inactive);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-action {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-btn {
|
||||||
|
padding: 16rpx 28rpx;
|
||||||
|
background: rgba(130, 220, 130, 0.9);
|
||||||
|
border-radius: 9999rpx;
|
||||||
|
box-shadow: 0 6rpx 20rpx rgba(130, 220, 130, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-btn-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.participants {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: var(--tabbar-text-inactive);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<view class="bt-radar">
|
||||||
|
<canvas
|
||||||
|
:id="canvasId"
|
||||||
|
:canvas-id="canvasId"
|
||||||
|
type="2d"
|
||||||
|
class="bt-radar__canvas"
|
||||||
|
:style="{ width: width + 'px', height: height + 'px' }"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { drawRadarChart } from '@/common/memberInfo/bodyTestChart.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
options: {
|
||||||
|
virtualHost: false,
|
||||||
|
styleIsolation: 'apply-shared'
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
labels: { type: Array, default: () => [] },
|
||||||
|
values: { type: Array, default: () => [] },
|
||||||
|
width: { type: Number, default: 280 },
|
||||||
|
height: { type: Number, default: 240 }
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
canvasId: `bt-radar-${Math.random().toString(36).slice(2, 9)}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
values: {
|
||||||
|
deep: true,
|
||||||
|
handler() {
|
||||||
|
this.renderChart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.renderChart()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
renderChart() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const query = uni.createSelectorQuery().in(this)
|
||||||
|
query
|
||||||
|
.select(`#${this.canvasId}`)
|
||||||
|
.fields({ node: true, size: true })
|
||||||
|
.exec((res) => {
|
||||||
|
const node = res?.[0]?.node
|
||||||
|
if (!node) return
|
||||||
|
const dpr = uni.getSystemInfoSync().pixelRatio || 1
|
||||||
|
drawRadarChart(node, {
|
||||||
|
width: this.width,
|
||||||
|
height: this.height,
|
||||||
|
labels: this.labels,
|
||||||
|
values: this.values,
|
||||||
|
dpr
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.bt-radar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-radar__canvas {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<view class="bt-trend">
|
||||||
|
<canvas
|
||||||
|
:id="canvasId"
|
||||||
|
:canvas-id="canvasId"
|
||||||
|
type="2d"
|
||||||
|
class="bt-trend__canvas"
|
||||||
|
:style="{ width: width + 'px', height: height + 'px' }"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { drawTrendChart } from '@/common/memberInfo/bodyTestChart.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
options: {
|
||||||
|
virtualHost: false,
|
||||||
|
styleIsolation: 'apply-shared'
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
points: { type: Array, default: () => [] },
|
||||||
|
unit: { type: String, default: '' },
|
||||||
|
width: { type: Number, default: 300 },
|
||||||
|
height: { type: Number, default: 160 }
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
canvasId: `bt-trend-${Math.random().toString(36).slice(2, 9)}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
points: {
|
||||||
|
deep: true,
|
||||||
|
handler() {
|
||||||
|
this.renderChart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.renderChart()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
renderChart() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const query = uni.createSelectorQuery().in(this)
|
||||||
|
query
|
||||||
|
.select(`#${this.canvasId}`)
|
||||||
|
.fields({ node: true, size: true })
|
||||||
|
.exec((res) => {
|
||||||
|
const node = res?.[0]?.node
|
||||||
|
if (!node) return
|
||||||
|
const dpr = uni.getSystemInfoSync().pixelRatio || 1
|
||||||
|
drawTrendChart(node, {
|
||||||
|
width: this.width,
|
||||||
|
height: this.height,
|
||||||
|
points: this.points,
|
||||||
|
unit: this.unit,
|
||||||
|
dpr
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.bt-trend {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-trend__canvas {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
<template>
|
||||||
|
<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="$emit('view-history')"
|
||||||
|
>
|
||||||
|
<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">
|
||||||
|
最新数据 · {{ report.date }}
|
||||||
|
</text>
|
||||||
|
<view
|
||||||
|
class="body-report-section__view-btn"
|
||||||
|
hover-class="mi-tap-btn--hover"
|
||||||
|
:hover-stay-time="150"
|
||||||
|
@tap="$emit('view-report')"
|
||||||
|
>
|
||||||
|
<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">{{ report.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">{{ report.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">{{ report.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">{{ report.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">
|
||||||
|
状态:{{ report.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">
|
||||||
|
较上次 {{ report.change }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
options: {
|
||||||
|
virtualHost: false,
|
||||||
|
styleIsolation: 'apply-shared'
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
report: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
date: '2024-07-01',
|
||||||
|
weight: '63.5',
|
||||||
|
bmi: '22.1',
|
||||||
|
bodyFat: '24.8%',
|
||||||
|
bmr: '165',
|
||||||
|
status: '比较健康',
|
||||||
|
change: '-1.2kg'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['view-history', 'view-report']
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||||
|
@import '@/common/style/memberInfo/member-info-body-report.css';
|
||||||
|
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
<template>
|
||||||
|
<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="$emit('view-all')"
|
||||||
|
>
|
||||||
|
<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 previewItems"
|
||||||
|
:key="item.id"
|
||||||
|
class="booking-section__item"
|
||||||
|
hover-class="mi-tap-row--hover"
|
||||||
|
:hover-stay-time="150"
|
||||||
|
@tap="$emit('item-tap', item)"
|
||||||
|
>
|
||||||
|
<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="!previewItems.length" class="booking-section__empty">
|
||||||
|
<text class="booking-section__empty-text">暂无进行中的预约</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
options: {
|
||||||
|
virtualHost: false,
|
||||||
|
styleIsolation: 'apply-shared'
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['view-all', 'item-tap'],
|
||||||
|
computed: {
|
||||||
|
previewItems() {
|
||||||
|
return this.items
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||||
|
@import '@/common/style/memberInfo/member-info-booking-list.css';
|
||||||
|
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||||
|
|
||||||
|
.booking-section__empty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-section__empty-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #8A99B4;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<template>
|
||||||
|
<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="$emit('view-all')"
|
||||||
|
>
|
||||||
|
<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 items"
|
||||||
|
: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="$emit('item-tap', 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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
options: {
|
||||||
|
virtualHost: false,
|
||||||
|
styleIsolation: 'apply-shared'
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['view-all', 'item-tap']
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@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-tap.css';
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<template>
|
||||||
|
<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="$emit('view-all')"
|
||||||
|
>
|
||||||
|
<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="$emit('use-coupon')"
|
||||||
|
>
|
||||||
|
<view class="coupon-section__coupon-inner">
|
||||||
|
<text class="coupon-section__amount">{{ data.amount }}</text>
|
||||||
|
<text class="coupon-section__desc">{{ data.couponDesc }}</text>
|
||||||
|
<view class="coupon-section__coupon-status">
|
||||||
|
<text class="coupon-section__status">{{ data.couponAction }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="coupon-section__points"
|
||||||
|
hover-class="mi-tap-card--hover"
|
||||||
|
:hover-stay-time="150"
|
||||||
|
@tap="$emit('redeem-points')"
|
||||||
|
>
|
||||||
|
<view class="coupon-section__points-inner">
|
||||||
|
<text class="coupon-section__num">{{ data.points }}</text>
|
||||||
|
<text class="coupon-section__points-label">{{ data.pointsLabel }}</text>
|
||||||
|
<view class="coupon-section__points-action">
|
||||||
|
<text class="coupon-section__text">{{ data.pointsAction }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
options: {
|
||||||
|
virtualHost: false,
|
||||||
|
styleIsolation: 'apply-shared'
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
amount: '¥50',
|
||||||
|
couponDesc: '满500可用 · 1张',
|
||||||
|
couponAction: '去使用',
|
||||||
|
points: 1250,
|
||||||
|
pointsLabel: '我的积分',
|
||||||
|
pointsAction: '去兑换'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['view-all', 'use-coupon', 'redeem-points']
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||||
|
@import '@/common/style/memberInfo/member-info-coupon-points.css';
|
||||||
|
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
<template>
|
||||||
|
<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="$emit('user-info')">
|
||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
options: {
|
||||||
|
virtualHost: false,
|
||||||
|
styleIsolation: 'apply-shared'
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
userInfo: { type: Object, required: true },
|
||||||
|
stats: { type: Object, required: true }
|
||||||
|
},
|
||||||
|
emits: ['user-info'],
|
||||||
|
computed: {
|
||||||
|
displayAvatar() {
|
||||||
|
return this.userInfo.avatar || 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AvatarEditWrap.png'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
toolbarStyle: {},
|
||||||
|
toolbarSpacerStyle: {},
|
||||||
|
navRightStyle: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.syncNavSafeArea()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
syncNavSafeArea() {
|
||||||
|
try {
|
||||||
|
const sys = uni.getSystemInfoSync()
|
||||||
|
const statusBarHeight = sys.statusBarHeight || 0
|
||||||
|
const navHeight = 44
|
||||||
|
const menu = uni.getMenuButtonBoundingClientRect?.()
|
||||||
|
|
||||||
|
this.toolbarStyle = {
|
||||||
|
paddingTop: `${statusBarHeight}px`
|
||||||
|
}
|
||||||
|
|
||||||
|
this.toolbarSpacerStyle = {
|
||||||
|
height: `${statusBarHeight + navHeight}px`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menu && menu.width) {
|
||||||
|
const capsuleGap = sys.windowWidth - menu.left + 8
|
||||||
|
this.navRightStyle = {
|
||||||
|
width: `${capsuleGap}px`,
|
||||||
|
minWidth: `${capsuleGap}px`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.toolbarSpacerStyle = { height: '44px' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||||
|
@import '@/common/style/memberInfo/member-info-header.css';
|
||||||
|
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<view class="logout-section">
|
||||||
|
<view
|
||||||
|
class="logout-section__btn"
|
||||||
|
hover-class="mi-tap-btn--hover"
|
||||||
|
:hover-stay-time="150"
|
||||||
|
@tap="$emit('logout')"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
options: {
|
||||||
|
virtualHost: false,
|
||||||
|
styleIsolation: 'apply-shared'
|
||||||
|
},
|
||||||
|
emits: ['logout']
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@import '@/common/style/memberInfo/member-info-logout.css';
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
<template>
|
||||||
|
<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="$emit('view-all')"
|
||||||
|
>
|
||||||
|
<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="$emit('view-all')"
|
||||||
|
>
|
||||||
|
<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="$emit('renew')"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
options: {
|
||||||
|
virtualHost: false,
|
||||||
|
styleIsolation: 'apply-shared'
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
cardInfo: { type: Object, required: true }
|
||||||
|
},
|
||||||
|
emits: ['view-all', 'renew'],
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||||
|
@import '@/common/style/memberInfo/member-info-member-card.css';
|
||||||
|
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
<template>
|
||||||
|
<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 row1"
|
||||||
|
:key="item.key"
|
||||||
|
class="quick-actions__item"
|
||||||
|
hover-class="mi-tap--hover"
|
||||||
|
:hover-stay-time="150"
|
||||||
|
@tap="$emit('action', 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 row2"
|
||||||
|
:key="item.key"
|
||||||
|
class="quick-actions__item"
|
||||||
|
hover-class="mi-tap--hover"
|
||||||
|
:hover-stay-time="150"
|
||||||
|
@tap="$emit('action', 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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
options: {
|
||||||
|
virtualHost: false,
|
||||||
|
styleIsolation: 'apply-shared'
|
||||||
|
},
|
||||||
|
emits: ['action'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
row1: [
|
||||||
|
{ 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' }
|
||||||
|
],
|
||||||
|
row2: [
|
||||||
|
{ 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' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||||
|
@import '@/common/style/memberInfo/member-info-quick-actions.css';
|
||||||
|
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
<template>
|
||||||
|
<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="$emit('view-rules')"
|
||||||
|
>
|
||||||
|
<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">{{ data.code }}</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="referral-section__copy-btn"
|
||||||
|
hover-class="mi-tap-btn--hover"
|
||||||
|
:hover-stay-time="150"
|
||||||
|
@tap="copyCode"
|
||||||
|
>
|
||||||
|
<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">
|
||||||
|
{{ data.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">
|
||||||
|
{{ data.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">
|
||||||
|
{{ data.purchased }}
|
||||||
|
</text>
|
||||||
|
<text class="referral-section__stat-label">已购课</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
options: {
|
||||||
|
virtualHost: false,
|
||||||
|
styleIsolation: 'apply-shared'
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
code: 'FIT-ZXF-2024',
|
||||||
|
invited: 5,
|
||||||
|
registered: 3,
|
||||||
|
purchased: 2
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['view-rules'],
|
||||||
|
methods: {
|
||||||
|
copyCode() {
|
||||||
|
uni.setClipboardData({
|
||||||
|
data: this.data.code,
|
||||||
|
success: () => {
|
||||||
|
uni.showToast({ title: '已复制', icon: 'success' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||||
|
@import '@/common/style/memberInfo/member-info-referral.css';
|
||||||
|
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
<template>
|
||||||
|
<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 items"
|
||||||
|
: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="$emit('setting', 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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
options: {
|
||||||
|
virtualHost: false,
|
||||||
|
styleIsolation: 'apply-shared'
|
||||||
|
},
|
||||||
|
emits: ['setting'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||||
|
@import '@/common/style/memberInfo/member-info-settings.css';
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<view class="status-bar">
|
||||||
|
<view class="status-bar__inner">
|
||||||
|
<text class="status-bar__time">{{ statusBarTime }}</text>
|
||||||
|
<text class="status-bar__icons">...</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
options: {
|
||||||
|
virtualHost: true,
|
||||||
|
styleIsolation: 'apply-shared'
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
statusBarTime: { type: String, default: '9:41' }
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
<template>
|
||||||
|
<view class="sub-nav">
|
||||||
|
<view class="sub-nav__toolbar" :style="toolbarStyle">
|
||||||
|
<view class="sub-nav__nav">
|
||||||
|
<view class="sub-nav__back" @tap.stop="$emit('back')">
|
||||||
|
<image
|
||||||
|
class="sub-nav__back-icon"
|
||||||
|
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/chevronleft.png"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<text class="sub-nav__title">{{ title }}</text>
|
||||||
|
<view class="sub-nav__right">
|
||||||
|
<view
|
||||||
|
v-if="rightText"
|
||||||
|
class="sub-nav__action"
|
||||||
|
:class="{ 'sub-nav__action--button': actionButton }"
|
||||||
|
@tap.stop="$emit('right-action')"
|
||||||
|
>
|
||||||
|
<text class="sub-nav__action-text">{{ rightText }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="sub-nav__capsule" :class="{ 'sub-nav__capsule--h5': isH5 }" :style="capsuleStyle"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="sub-nav__spacer" :style="toolbarSpacerStyle"></view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
options: {
|
||||||
|
virtualHost: false,
|
||||||
|
styleIsolation: 'apply-shared'
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
title: { type: String, required: true },
|
||||||
|
rightText: { type: String, default: '' },
|
||||||
|
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: {
|
||||||
|
syncNavSafeArea() {
|
||||||
|
try {
|
||||||
|
const sys = uni.getSystemInfoSync()
|
||||||
|
const statusBarHeight = sys.statusBarHeight || 0
|
||||||
|
const navHeight = 44
|
||||||
|
const extraGap = 4
|
||||||
|
const menu = uni.getMenuButtonBoundingClientRect?.()
|
||||||
|
// #ifdef H5
|
||||||
|
this.isH5 = true
|
||||||
|
// #endif
|
||||||
|
// #ifndef H5
|
||||||
|
this.isH5 = false
|
||||||
|
// #endif
|
||||||
|
if (!this.isH5 && typeof window !== 'undefined' && !menu?.width) {
|
||||||
|
this.isH5 = sys.uniPlatform === 'web' || sys.platform === 'web'
|
||||||
|
}
|
||||||
|
|
||||||
|
this.toolbarStyle = {
|
||||||
|
paddingTop: `${statusBarHeight}px`
|
||||||
|
}
|
||||||
|
|
||||||
|
this.toolbarSpacerStyle = {
|
||||||
|
height: `${statusBarHeight + navHeight + extraGap}px`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isH5 && menu && menu.width) {
|
||||||
|
const capsuleGap = sys.windowWidth - menu.left + 8
|
||||||
|
this.capsuleStyle = {
|
||||||
|
width: `${capsuleGap}px`,
|
||||||
|
minWidth: `${capsuleGap}px`
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.capsuleStyle = {
|
||||||
|
width: '0px',
|
||||||
|
minWidth: '0px'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.toolbarSpacerStyle = { height: '44px' }
|
||||||
|
this.isH5 = true
|
||||||
|
this.capsuleStyle = { width: '0px', minWidth: '0px' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@import '@/common/style/memberInfo/member-info-sub-nav.css';
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { groupCourseService } from '@/request_api/groupCourse.mock.js'
|
||||||
|
|
||||||
|
export function useGroupCourseList() {
|
||||||
|
// 分页相关
|
||||||
|
const pageNum = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const total = ref(0)
|
||||||
|
const totalPages = ref(0)
|
||||||
|
const loading = ref(false)
|
||||||
|
const hasMore = ref(true)
|
||||||
|
|
||||||
|
// 团课列表数据
|
||||||
|
const courseList = ref([])
|
||||||
|
|
||||||
|
// 搜索相关
|
||||||
|
const searchKeyword = ref('')
|
||||||
|
const hotKeywords = ref(['燃脂', '瑜伽', '单车', '普拉提', '高强度'])
|
||||||
|
|
||||||
|
// 排序相关
|
||||||
|
const sortOptions = ref([
|
||||||
|
{ label: '默认排序', value: 'default' },
|
||||||
|
{ label: '价格从低到高', value: 'priceAsc' },
|
||||||
|
{ label: '价格从高到低', value: 'priceDesc' },
|
||||||
|
{ label: '剩余名额最多', value: 'spotsDesc' }
|
||||||
|
])
|
||||||
|
const sortIndex = ref(0)
|
||||||
|
|
||||||
|
// 时间段选择相关
|
||||||
|
const timePeriodOptions = ref([
|
||||||
|
{ label: '全部', value: 'all' },
|
||||||
|
{ label: '早上 (6-12 点)', value: 'morning', startHour: 6, endHour: 12 },
|
||||||
|
{ label: '下午 (12-18 点)', value: 'afternoon', startHour: 12, endHour: 18 },
|
||||||
|
{ label: '晚上 (18-24 点)', value: 'evening', startHour: 18, endHour: 24 }
|
||||||
|
])
|
||||||
|
const timePeriodIndex = ref(0)
|
||||||
|
|
||||||
|
// 时间筛选相关
|
||||||
|
const showTimePicker = ref(false)
|
||||||
|
const startDate = ref('')
|
||||||
|
const endDate = ref('')
|
||||||
|
const timeRangeText = ref('')
|
||||||
|
|
||||||
|
// 筛选后的课程列表
|
||||||
|
const filteredCourseList = computed(() => {
|
||||||
|
let result = [...courseList.value]
|
||||||
|
|
||||||
|
// 关键词搜索
|
||||||
|
if (searchKeyword.value) {
|
||||||
|
result = result.filter(course =>
|
||||||
|
course.courseName.includes(searchKeyword.value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间范围筛选
|
||||||
|
if (startDate.value || endDate.value) {
|
||||||
|
result = result.filter(course => {
|
||||||
|
const courseDate = course.startTime.split('T')[0]
|
||||||
|
if (startDate.value && courseDate < startDate.value) return false
|
||||||
|
if (endDate.value && courseDate > endDate.value) return false
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间段筛选
|
||||||
|
const timePeriod = timePeriodOptions.value[timePeriodIndex.value]
|
||||||
|
if (timePeriod.value !== 'all') {
|
||||||
|
result = result.filter(course => {
|
||||||
|
const courseHour = new Date(course.startTime).getHours()
|
||||||
|
return courseHour >= timePeriod.startHour && courseHour < timePeriod.endHour
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排序
|
||||||
|
const sortType = sortOptions.value[sortIndex.value].value
|
||||||
|
if (sortType === 'priceAsc') {
|
||||||
|
result.sort((a, b) => (a.storedValueAmount || a.pointCardAmount) - (b.storedValueAmount || b.pointCardAmount))
|
||||||
|
} else if (sortType === 'priceDesc') {
|
||||||
|
result.sort((a, b) => (b.storedValueAmount || b.pointCardAmount) - (a.storedValueAmount || a.pointCardAmount))
|
||||||
|
} else if (sortType === 'spotsDesc') {
|
||||||
|
result.sort((a, b) => (b.maxMembers - b.currentMembers) - (a.maxMembers - a.currentMembers))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取所有搜索参数
|
||||||
|
const getAllSearchParams = (searchBarRef, filterSectionRef, timePeriodRef, timeRangePickerRef) => {
|
||||||
|
const searchParams = searchBarRef?.getSearchParams?.() || { keyword: searchKeyword.value }
|
||||||
|
const filterParams = filterSectionRef?.getFilterParams?.() || { sortType: sortOptions.value[sortIndex.value].value }
|
||||||
|
const timePeriodParams = timePeriodRef?.getTimePeriodParams?.() || { index: timePeriodIndex.value, value: timePeriodOptions.value[timePeriodIndex.value].value }
|
||||||
|
const timeRangeParams = timeRangePickerRef?.getTimeRangeParams?.() || { startDate: startDate.value, endDate: endDate.value, timeRangeText: timeRangeText.value }
|
||||||
|
|
||||||
|
const allParams = {
|
||||||
|
search: searchParams,
|
||||||
|
filter: filterParams,
|
||||||
|
timePeriod: timePeriodParams,
|
||||||
|
timeRange: timeRangeParams
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[useGroupCourseList] 获取所有搜索参数:', allParams)
|
||||||
|
return allParams
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索处理
|
||||||
|
const handleSearch = (params) => {
|
||||||
|
console.log('[useGroupCourseList] 搜索触发:', params)
|
||||||
|
uni.showToast({
|
||||||
|
title: params.keyword ? `搜索:${params.keyword}` : '请输入关键词',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间段变化处理
|
||||||
|
const onTimePeriodChange = (option) => {
|
||||||
|
console.log('[useGroupCourseList] 时间段选择:', option)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间范围确认处理
|
||||||
|
const onTimeRangeConfirm = (params) => {
|
||||||
|
console.log('[useGroupCourseList] 时间范围确认:', params)
|
||||||
|
timeRangeText.value = params.timeRangeText
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预约处理
|
||||||
|
const handleBooking = (course) => {
|
||||||
|
console.log('[useGroupCourseList] 预约课程:', course)
|
||||||
|
uni.showToast({
|
||||||
|
title: `预约课程:${course.courseName}`,
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转详情
|
||||||
|
const goDetail = (courseId) => {
|
||||||
|
console.log('[useGroupCourseList] 跳转到课程详情:', courseId)
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/groupCourse/detail?id=${courseId}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取团课列表
|
||||||
|
const fetchCourseList = async (isLoadMore = false) => {
|
||||||
|
if (loading.value) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await groupCourseService.getList({
|
||||||
|
pageNum: pageNum.value,
|
||||||
|
pageSize: pageSize.value
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.code === 0 && result.data) {
|
||||||
|
const { list, total: totalCount, pageNum: currentPage, totalPages: pages } = result.data
|
||||||
|
|
||||||
|
if (isLoadMore) {
|
||||||
|
courseList.value = [...courseList.value, ...list]
|
||||||
|
} else {
|
||||||
|
courseList.value = list
|
||||||
|
}
|
||||||
|
|
||||||
|
total.value = totalCount
|
||||||
|
pageNum.value = currentPage
|
||||||
|
totalPages.value = pages
|
||||||
|
hasMore.value = pageNum.value < totalPages.value
|
||||||
|
|
||||||
|
console.log('[useGroupCourseList] 团课列表获取成功:', {
|
||||||
|
total: total.value,
|
||||||
|
currentPage: pageNum.value,
|
||||||
|
totalPages: totalPages.value,
|
||||||
|
hasMore: hasMore.value
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
console.error('[useGroupCourseList] 获取团课列表失败:', result.message)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[useGroupCourseList] 获取团课列表异常:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载更多
|
||||||
|
const loadMore = () => {
|
||||||
|
if (!hasMore.value || loading.value) return
|
||||||
|
pageNum.value++
|
||||||
|
fetchCourseList(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 滚动到底部触发
|
||||||
|
const onScrollToLower = () => {
|
||||||
|
loadMore()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 状态
|
||||||
|
pageNum,
|
||||||
|
pageSize,
|
||||||
|
total,
|
||||||
|
totalPages,
|
||||||
|
loading,
|
||||||
|
hasMore,
|
||||||
|
courseList,
|
||||||
|
searchKeyword,
|
||||||
|
hotKeywords,
|
||||||
|
sortOptions,
|
||||||
|
sortIndex,
|
||||||
|
timePeriodOptions,
|
||||||
|
timePeriodIndex,
|
||||||
|
showTimePicker,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
timeRangeText,
|
||||||
|
filteredCourseList,
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
getAllSearchParams,
|
||||||
|
handleSearch,
|
||||||
|
onTimePeriodChange,
|
||||||
|
onTimeRangeConfirm,
|
||||||
|
handleBooking,
|
||||||
|
goDetail,
|
||||||
|
fetchCourseList,
|
||||||
|
loadMore,
|
||||||
|
onScrollToLower
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
# 团课管理系统 API 文档
|
||||||
|
|
||||||
|
## 1. 项目概述
|
||||||
|
|
||||||
|
本项目是一个基于 UniApp 的健身房团课管理系统,包含完整的 API 层设计,支持开发/生产环境的快速切换。
|
||||||
|
|
||||||
|
### 1.1 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
gym-manage-uniapp/
|
||||||
|
├── api/ # API 层目录
|
||||||
|
│ ├── requestBase.js # 基础请求封装
|
||||||
|
│ ├── groupCourse.js # 团课真实API
|
||||||
|
│ ├── groupCourse.mock.js # 团课模拟数据API
|
||||||
|
│ └── envConfig.js # 环境配置与服务导出
|
||||||
|
├── pages/
|
||||||
|
│ └── groupCourse/
|
||||||
|
│ └── list.vue # 团课列表页面
|
||||||
|
└── components/ # 组件目录
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. API 层设计
|
||||||
|
|
||||||
|
### 2.1 架构设计
|
||||||
|
|
||||||
|
| 层级 | 文件 | 职责 |
|
||||||
|
|------|------|------|
|
||||||
|
| 基础层 | `requestBase.js` | 封装 uni.request,统一处理加载状态、错误提示 |
|
||||||
|
| 接口层 | `groupCourse.js` | 定义真实后端API接口 |
|
||||||
|
| 模拟层 | `groupCourse.mock.js` | 提供模拟数据,支持开发测试 |
|
||||||
|
| 配置层 | `envConfig.js` | 环境配置,统一服务导出入口 |
|
||||||
|
|
||||||
|
### 2.2 环境切换机制
|
||||||
|
|
||||||
|
通过修改 `envConfig.js` 中的 `ENV_MODE` 变量实现环境切换:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// envConfig.js
|
||||||
|
export const ENV_MODE = 'development' // 'production' | 'development'
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`production`**:生产环境,使用真实网络请求
|
||||||
|
- **`development`**:开发环境,使用模拟数据
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 文件详细说明
|
||||||
|
|
||||||
|
### 3.1 requestBase.js - 基础请求封装
|
||||||
|
|
||||||
|
**功能定位**:封装 `uni.request`,提供统一的请求处理逻辑。
|
||||||
|
|
||||||
|
**核心特性**:
|
||||||
|
- 自动显示/隐藏加载提示
|
||||||
|
- 统一的响应状态码处理
|
||||||
|
- 统一的错误提示机制
|
||||||
|
|
||||||
|
**接口定义**:
|
||||||
|
|
||||||
|
| 方法 | 说明 | 参数 | 返回值 |
|
||||||
|
|------|------|------|--------|
|
||||||
|
| `request(options)` | 发起网络请求 | `options` 对象 | `Promise` |
|
||||||
|
|
||||||
|
**options 参数**:
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `url` | string | 是 | 请求路径 |
|
||||||
|
| `method` | string | 否 | 请求方法,默认 `GET` |
|
||||||
|
| `data` | object | 否 | 请求数据 |
|
||||||
|
| `header` | object | 否 | 请求头 |
|
||||||
|
|
||||||
|
**响应数据结构**:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 成功响应
|
||||||
|
{
|
||||||
|
code: 0, // 状态码,0表示成功
|
||||||
|
message: 'success', // 提示信息
|
||||||
|
data: {} // 业务数据
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 groupCourse.js - 团课真实 API
|
||||||
|
|
||||||
|
**功能定位**:定义团课相关的真实后端接口。
|
||||||
|
|
||||||
|
**接口列表**:
|
||||||
|
|
||||||
|
| 方法 | 说明 | 参数 | HTTP方法 | 路径 |
|
||||||
|
|------|------|------|----------|------|
|
||||||
|
| `getList()` | 获取团课列表 | 无 | GET | `/api/groupCourse/list` |
|
||||||
|
| `getDetail(id)` | 获取团课详情 | `id`: 课程ID | GET | `/api/groupCourse/detail/{id}` |
|
||||||
|
| `book(data)` | 预约团课 | `data`: 预约数据 | POST | `/api/groupCourse/book` |
|
||||||
|
| `cancelBooking(id)` | 取消预约 | `id`: 预约记录ID | POST | `/api/groupCourse/cancel/{id}` |
|
||||||
|
|
||||||
|
**book 方法参数结构**:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
courseId: string, // 课程ID
|
||||||
|
memberId: string // 会员ID
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 groupCourse.mock.js - 团课模拟数据 API
|
||||||
|
|
||||||
|
**功能定位**:提供模拟数据,支持开发测试,无需后端服务。
|
||||||
|
|
||||||
|
**特性**:
|
||||||
|
- 模拟网络延迟(300-500ms)
|
||||||
|
- 接口签名与真实API完全一致
|
||||||
|
- 包含6条完整的模拟团课数据
|
||||||
|
|
||||||
|
**模拟数据结构**:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `id` | string | 课程唯一标识 |
|
||||||
|
| `courseName` | string | 课程名称 |
|
||||||
|
| `coachName` | string | 教练姓名 |
|
||||||
|
| `coachAvatar` | string | 教练头像 |
|
||||||
|
| `startTime` | string | 开始时间(ISO格式) |
|
||||||
|
| `endTime` | string | 结束时间(ISO格式) |
|
||||||
|
| `duration` | number | 课程时长(分钟) |
|
||||||
|
| `location` | string | 上课地点 |
|
||||||
|
| `maxMembers` | number | 最大人数 |
|
||||||
|
| `currentMembers` | number | 当前人数 |
|
||||||
|
| `storedValueAmount` | number | 储值卡价格(元) |
|
||||||
|
| `pointCardAmount` | number | 次卡次数 |
|
||||||
|
| `courseType` | string | 课程类型 |
|
||||||
|
| `level` | string | 难度级别 |
|
||||||
|
| `description` | string | 课程描述 |
|
||||||
|
| `tags` | array | 标签列表 |
|
||||||
|
| `status` | string | 状态(available/closed) |
|
||||||
|
|
||||||
|
### 3.4 envConfig.js - 环境配置
|
||||||
|
|
||||||
|
**功能定位**:统一服务导出入口,根据环境模式自动选择 API 实现。
|
||||||
|
|
||||||
|
**导出内容**:
|
||||||
|
|
||||||
|
| 导出项 | 类型 | 说明 |
|
||||||
|
|--------|------|------|
|
||||||
|
| `ENV_MODE` | string | 当前环境模式 |
|
||||||
|
| `groupCourseService` | object | 团课服务实例 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 使用示例
|
||||||
|
|
||||||
|
### 4.1 在页面中使用
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// pages/groupCourse/list.vue
|
||||||
|
import { groupCourseService } from '@/api/envConfig.js'
|
||||||
|
|
||||||
|
// 获取团课列表
|
||||||
|
const fetchCourseList = async () => {
|
||||||
|
try {
|
||||||
|
const data = await groupCourseService.getList()
|
||||||
|
courseList.value = data
|
||||||
|
console.log('团课列表获取成功:', data)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取团课列表异常:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 切换环境
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// api/envConfig.js
|
||||||
|
// 开发环境 - 使用模拟数据
|
||||||
|
export const ENV_MODE = 'development'
|
||||||
|
|
||||||
|
// 生产环境 - 使用真实网络请求
|
||||||
|
export const ENV_MODE = 'production'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 数据流转图
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ 页面层 (View) │
|
||||||
|
│ pages/groupCourse/list.vue │
|
||||||
|
└───────────────────────────┬─────────────────────────────────┘
|
||||||
|
│ import & call
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ 环境配置层 (Config) │
|
||||||
|
│ api/envConfig.js │
|
||||||
|
│ 根据 ENV_MODE 选择对应的服务实现 │
|
||||||
|
└───────────────────────────┬─────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌─────────────────┴─────────────────┐
|
||||||
|
▼ ▼
|
||||||
|
┌─────────────────────────┐ ┌─────────────────────────────┐
|
||||||
|
│ groupCourse.js │ │ groupCourse.mock.js │
|
||||||
|
│ (production 环境) │ │ (development 环境) │
|
||||||
|
└───────────┬─────────────┘ └─────────────┬───────────────┘
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
┌─────────────────────────┐ ┌─────────────────────┐
|
||||||
|
│ requestBase.js │ │ 本地模拟数据 │
|
||||||
|
│ (真实网络请求) │ │ (无需后端) │
|
||||||
|
└───────────┬─────────────┘ └─────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ 后端服务器 API │
|
||||||
|
└─────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 注意事项
|
||||||
|
|
||||||
|
1. **环境变量配置**:部署前务必确认 `ENV_MODE` 设置正确
|
||||||
|
2. **数据一致性**:模拟数据结构应与真实API保持一致
|
||||||
|
3. **错误处理**:所有API调用都应包含 try-catch 错误处理
|
||||||
|
4. **日志记录**:建议在关键节点添加日志,便于调试
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 版本历史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 更新内容 |
|
||||||
|
|------|------|----------|
|
||||||
|
| v1.0 | 2026-06-04 | 初始版本,完成基础API层设计 |
|
||||||
@@ -0,0 +1,570 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||||
|
<title>健身房管理系统 | 动感配色方案</title>
|
||||||
|
<!-- 引入Font Awesome 6 (免费图标库,增强视觉) -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: #F5F7FC; /* 整体背景柔和灰白,衬托主色 */
|
||||||
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
padding: 24px 20px 48px;
|
||||||
|
color: #1E2A3A;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主色调变量定义 – 活力运动风格
|
||||||
|
主色: 动感深蓝 (#0B2B4B) – 权威、专业、沉稳
|
||||||
|
辅色: 活力橙 (#FF6B35) – 热情、能量、行动召唤
|
||||||
|
背景浅色: #F9FAFE, 卡片白, 强调蓝绿渐变点缀
|
||||||
|
*/
|
||||||
|
:root {
|
||||||
|
--primary-dark: #0B2B4B; /* 深蓝主色,用于头部、重要按钮、边框强调 */
|
||||||
|
--primary-deep: #1A4A6F; /* 中深蓝,用于hover、次级强调 */
|
||||||
|
--accent-orange: #FF6B35; /* 活力橙,CTA、会员卡片、高亮标签 */
|
||||||
|
--accent-orange-light: #FF8C5A;
|
||||||
|
--bg-light: #F9FAFE;
|
||||||
|
--card-white: #FFFFFF;
|
||||||
|
--text-dark: #1E2A3A;
|
||||||
|
--text-muted: #5E6F8D;
|
||||||
|
--border-light: #E9EDF2;
|
||||||
|
--success-green: #2ECC71;
|
||||||
|
--warning-amber: #F39C12;
|
||||||
|
--shadow-sm: 0 8px 20px rgba(0, 0, 0, 0.03), 0 2px 6px rgba(0, 0, 0, 0.05);
|
||||||
|
--shadow-md: 0 12px 28px rgba(0, 0, 0, 0.08);
|
||||||
|
--gradient-orange: linear-gradient(135deg, #FF6B35 0%, #FF8C5A 100%);
|
||||||
|
--gradient-blue: linear-gradient(135deg, #0B2B4B 0%, #1A4A6F 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 整体容器 */
|
||||||
|
.demo-container {
|
||||||
|
max-width: 450px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: var(--bg-light);
|
||||||
|
border-radius: 36px;
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态栏模拟 (小程序顶部风格) */
|
||||||
|
.status-bar {
|
||||||
|
background: var(--primary-dark);
|
||||||
|
padding: 12px 20px 6px;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主头部导航 */
|
||||||
|
.main-header {
|
||||||
|
background: var(--primary-dark);
|
||||||
|
padding: 12px 20px 20px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-top {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-area h2 {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.3px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-area h2 i {
|
||||||
|
color: var(--accent-orange);
|
||||||
|
font-size: 1.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-area p {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions i {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
background: rgba(255,255,255,0.15);
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 30px;
|
||||||
|
margin-left: 8px;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 欢迎卡片 + 活力橙强调 */
|
||||||
|
.welcome-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 28px;
|
||||||
|
padding: 18px 20px;
|
||||||
|
margin-top: 12px;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-text h4 {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-text h3 {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
margin-top: 4px;
|
||||||
|
color: var(--primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-member {
|
||||||
|
background: var(--gradient-orange);
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 40px;
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
box-shadow: 0 4px 8px rgba(255,107,53,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主要指标卡片区 */
|
||||||
|
.stats-grid {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
background: var(--bg-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: var(--card-white);
|
||||||
|
flex: 1;
|
||||||
|
border-radius: 24px;
|
||||||
|
padding: 14px 0;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
transition: all 0.2s;
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card i {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
color: var(--accent-orange);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 功能菜单 grid */
|
||||||
|
.menu-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
padding: 8px 20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
background: var(--card-white);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 12px 6px;
|
||||||
|
text-align: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item i {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
color: var(--primary-deep);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item span {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:active {
|
||||||
|
transform: scale(0.96);
|
||||||
|
background: #FEF5F0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 课程/热门活动区域 */
|
||||||
|
.section-title {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 20px 4px;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title h3 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title a {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--accent-orange);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.class-list {
|
||||||
|
padding: 8px 20px 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.class-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
border-left: 5px solid var(--accent-orange);
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.class-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
background: rgba(255,107,53,0.1);
|
||||||
|
border-radius: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.class-icon i {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
color: var(--accent-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
.class-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.class-info h4 {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.class-info p {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.class-time {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
background: #F0F3F9;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 40px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--primary-deep);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 底部导航栏 (Tab Bar 模拟) */
|
||||||
|
.bottom-tab {
|
||||||
|
background: white;
|
||||||
|
backdrop-filter: blur(0px);
|
||||||
|
border-top: 1px solid var(--border-light);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
padding: 10px 20px 22px;
|
||||||
|
border-radius: 0 0 36px 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-muted);
|
||||||
|
transition: 0.1s;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item i {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item span {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
display: block;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.active {
|
||||||
|
color: var(--accent-orange);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 教练推荐卡片额外装饰 */
|
||||||
|
.trainer-spot {
|
||||||
|
background: var(--gradient-blue);
|
||||||
|
margin: 0 20px 20px;
|
||||||
|
border-radius: 28px;
|
||||||
|
padding: 16px 18px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trainer-info h4 {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trainer-info h3 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trainer-btn {
|
||||||
|
background: var(--accent-orange);
|
||||||
|
border: none;
|
||||||
|
padding: 8px 18px;
|
||||||
|
border-radius: 50px;
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 配色展示条 / 设计说明 */
|
||||||
|
.color-palette-show {
|
||||||
|
max-width: 450px;
|
||||||
|
margin: 28px auto 0;
|
||||||
|
background: white;
|
||||||
|
border-radius: 32px;
|
||||||
|
padding: 18px 20px;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-title {
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: var(--primary-dark);
|
||||||
|
font-size: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-swatches {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swatch {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 70px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swatch-circle {
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 16px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
box-shadow: 0 2px 6px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.swatch span {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin: 20px 0 8px;
|
||||||
|
border: none;
|
||||||
|
height: 1px;
|
||||||
|
background: var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
i, .tab-item, .menu-item, .stat-card {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="demo-container">
|
||||||
|
<!-- 模拟小程序状态栏/胶囊区 -->
|
||||||
|
<div class="status-bar">
|
||||||
|
<span>9:41</span>
|
||||||
|
<span><i class="fas fa-signal"></i> <i class="fas fa-battery-full"></i></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主头部深蓝区 -->
|
||||||
|
<div class="main-header">
|
||||||
|
<div class="header-top">
|
||||||
|
<div class="logo-area">
|
||||||
|
<h2><i class="fas fa-dumbbell"></i> IRONPULSE</h2>
|
||||||
|
<p>智能健身 · 即刻燃动</p>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<i class="fas fa-bell"></i>
|
||||||
|
<i class="fas fa-user-circle"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 用户欢迎卡片,橙色高亮会员 -->
|
||||||
|
<div class="welcome-card">
|
||||||
|
<div class="welcome-text">
|
||||||
|
<h4>欢迎回来,</h4>
|
||||||
|
<h3>张峻铭 <span style="font-size: 0.8rem;">💪</span></h3>
|
||||||
|
</div>
|
||||||
|
<div class="badge-member">MVP 黑金会员</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 关键指标 活力橙点缀 -->
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<i class="fas fa-fire"></i>
|
||||||
|
<div class="stat-number">1,280</div>
|
||||||
|
<div class="stat-label">今日卡路里</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<i class="fas fa-clock"></i>
|
||||||
|
<div class="stat-number">126</div>
|
||||||
|
<div class="stat-label">运动分钟</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<i class="fas fa-calendar-check"></i>
|
||||||
|
<div class="stat-number">12</div>
|
||||||
|
<div class="stat-label">本月课程</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 功能区 网格菜单(四大模块) -->
|
||||||
|
<div class="menu-grid">
|
||||||
|
<div class="menu-item"><i class="fas fa-qrcode"></i><span>签到</span></div>
|
||||||
|
<div class="menu-item"><i class="fas fa-chalkboard-user"></i><span>团课预约</span></div>
|
||||||
|
<div class="menu-item"><i class="fas fa-chart-line"></i><span>体测报告</span></div>
|
||||||
|
<div class="menu-item"><i class="fas fa-cog"></i><span>我的会籍</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 热门课程板块 (动态橙边风格) -->
|
||||||
|
<div class="section-title">
|
||||||
|
<h3><i class="fas fa-heartbeat" style="color: var(--accent-orange); margin-right: 6px;"></i> 热门团课</h3>
|
||||||
|
<a href="#">查看全部 →</a>
|
||||||
|
</div>
|
||||||
|
<div class="class-list">
|
||||||
|
<div class="class-card">
|
||||||
|
<div class="class-icon"><i class="fas fa-bicycle"></i></div>
|
||||||
|
<div class="class-info">
|
||||||
|
<h4>极速燃脂 · 动感单车</h4>
|
||||||
|
<p>张教练 | 综合有氧</p>
|
||||||
|
</div>
|
||||||
|
<div class="class-time">19:30 满员</div>
|
||||||
|
</div>
|
||||||
|
<div class="class-card">
|
||||||
|
<div class="class-icon"><i class="fas fa-hand-fist"></i></div>
|
||||||
|
<div class="class-info">
|
||||||
|
<h4>搏击风暴 · 泰拳基础</h4>
|
||||||
|
<p>李娜 | 格斗区</p>
|
||||||
|
</div>
|
||||||
|
<div class="class-time">18:00 火热</div>
|
||||||
|
</div>
|
||||||
|
<div class="class-card">
|
||||||
|
<div class="class-icon"><i class="fas fa-person-walking"></i></div>
|
||||||
|
<div class="class-info">
|
||||||
|
<h4>普拉提核心唤醒</h4>
|
||||||
|
<p>Elena | 瑜伽室</p>
|
||||||
|
</div>
|
||||||
|
<div class="class-time">明早 09:30</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 教练推荐 使用深蓝渐变+橙色按钮 -->
|
||||||
|
<div class="trainer-spot">
|
||||||
|
<div class="trainer-info">
|
||||||
|
<h4>🌟 明星私教推荐</h4>
|
||||||
|
<h3>Alex 王 · 增肌塑形专家</h3>
|
||||||
|
<p style="font-size: 0.7rem; opacity:0.85;">剩余3个时段可约</p>
|
||||||
|
</div>
|
||||||
|
<div class="trainer-btn">立即预约</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部tab栏 当前选中“首页”凸显橙色-->
|
||||||
|
<div class="bottom-tab">
|
||||||
|
<div class="tab-item active"><i class="fas fa-home"></i><span>首页</span></div>
|
||||||
|
<div class="tab-item"><i class="fas fa-calendar-alt"></i><span>日程</span></div>
|
||||||
|
<div class="tab-item"><i class="fas fa-chart-simple"></i><span>数据</span></div>
|
||||||
|
<div class="tab-item"><i class="fas fa-user"></i><span>我的</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 配色方案展示区,直观看到颜色搭配,帮助设计决策 -->
|
||||||
|
<div class="color-palette-show">
|
||||||
|
<div class="color-title">
|
||||||
|
<i class="fas fa-palette" style="color: #FF6B35;"></i> 健身房管理系统 · 能量配色方案
|
||||||
|
</div>
|
||||||
|
<div class="color-swatches">
|
||||||
|
<div class="swatch">
|
||||||
|
<div class="swatch-circle" style="background: #0B2B4B;"></div>
|
||||||
|
<span>深蓝主色 #0B2B4B<br>专业信赖</span>
|
||||||
|
</div>
|
||||||
|
<div class="swatch">
|
||||||
|
<div class="swatch-circle" style="background: #FF6B35;"></div>
|
||||||
|
<span>活力橙 #FF6B35<br>行动/热情</span>
|
||||||
|
</div>
|
||||||
|
<div class="swatch">
|
||||||
|
<div class="swatch-circle" style="background: #1A4A6F;"></div>
|
||||||
|
<span>深邃辅助 #1A4A6F</span>
|
||||||
|
</div>
|
||||||
|
<div class="swatch">
|
||||||
|
<div class="swatch-circle" style="background: #F9FAFE; border:1px solid #E2E8F0;"></div>
|
||||||
|
<span>浅色背景 #F9FAFE</span>
|
||||||
|
</div>
|
||||||
|
<div class="swatch">
|
||||||
|
<div class="swatch-circle" style="background: #FFFFFF; border:1px solid #E2E8F0;"></div>
|
||||||
|
<span>卡片白 #FFFFFF</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="note">
|
||||||
|
<i class="fas fa-lightbulb" style="color: #FF6B35;"></i> 设计理念:深蓝色传递专业与稳定感,活力橙色提升运动热情与CTA转化。<br>
|
||||||
|
圆润卡片 + 强烈对比,适合健身管理小程序的年轻、力量与现代氛围。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 额外注释:颜色可访问性强,橙蓝互补但明度舒适 -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<script>
|
||||||
|
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
|
||||||
|
CSS.supports('top: constant(a)'))
|
||||||
|
document.write(
|
||||||
|
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
|
||||||
|
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
|
||||||
|
</script>
|
||||||
|
<title></title>
|
||||||
|
<!--preload-links-->
|
||||||
|
<!--app-context-->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"><!--app-html--></div>
|
||||||
|
<script type="module" src="/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import App from './App'
|
||||||
|
|
||||||
|
// #ifndef VUE3
|
||||||
|
import Vue from 'vue'
|
||||||
|
import './uni.promisify.adaptor'
|
||||||
|
Vue.config.productionTip = false
|
||||||
|
App.mpType = 'app'
|
||||||
|
const app = new Vue({
|
||||||
|
...App
|
||||||
|
})
|
||||||
|
app.$mount()
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef VUE3
|
||||||
|
import { createSSRApp } from 'vue'
|
||||||
|
export function createApp() {
|
||||||
|
const app = createSSRApp(App)
|
||||||
|
// 全局混入:所有页面加载时自动隐藏 loading
|
||||||
|
app.mixin({
|
||||||
|
onLoad() {
|
||||||
|
// 页面加载完成,隐藏 loading
|
||||||
|
uni.hideLoading()
|
||||||
|
},
|
||||||
|
|
||||||
|
async onReady() {
|
||||||
|
uni.hideLoading()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
app
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
@@ -1,64 +1,72 @@
|
|||||||
{
|
{
|
||||||
"name": "gym-manage-uniapp",
|
"name" : "gym-manage-uniapp",
|
||||||
"appid": "",
|
"appid" : "__UNI__1F1874C",
|
||||||
"description": "Gym Management System Mobile App",
|
"description" : "",
|
||||||
"versionName": "1.0.0",
|
"versionName" : "1.0.0",
|
||||||
"versionCode": "100",
|
"versionCode" : "100",
|
||||||
"transformPx": false,
|
"transformPx" : false,
|
||||||
"app-plus": {
|
/* 5+App特有相关 */
|
||||||
"usingComponents": true,
|
"app-plus" : {
|
||||||
"nvueStyleCompiler": "uni-app",
|
"usingComponents" : true,
|
||||||
"compilerVersion": 3,
|
"nvueStyleCompiler" : "uni-app",
|
||||||
"splashscreen": {
|
"compilerVersion" : 3,
|
||||||
"alwaysShowBeforeRender": true,
|
"splashscreen" : {
|
||||||
"waiting": true,
|
"alwaysShowBeforeRender" : true,
|
||||||
"autoclose": true,
|
"waiting" : true,
|
||||||
"delay": 0
|
"autoclose" : true,
|
||||||
|
"delay" : 0
|
||||||
|
},
|
||||||
|
/* 模块配置 */
|
||||||
|
"modules" : {},
|
||||||
|
/* 应用发布信息 */
|
||||||
|
"distribute" : {
|
||||||
|
/* android打包配置 */
|
||||||
|
"android" : {
|
||||||
|
"permissions" : [
|
||||||
|
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||||
|
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||||
|
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
/* ios打包配置 */
|
||||||
|
"ios" : {},
|
||||||
|
/* SDK配置 */
|
||||||
|
"sdkConfigs" : {}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"modules": {},
|
/* 快应用特有相关 */
|
||||||
"distribute": {
|
"quickapp" : {},
|
||||||
"android": {
|
/* 小程序特有相关 */
|
||||||
"permissions": [
|
"mp-weixin" : {
|
||||||
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
"appid" : "wx8f0d644d1d8985f6",
|
||||||
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
"setting" : {
|
||||||
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
"urlCheck" : false
|
||||||
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
},
|
||||||
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
"usingComponents" : true
|
||||||
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
|
||||||
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"ios": {},
|
|
||||||
"sdkConfigs": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"quickapp": {},
|
|
||||||
"mp-weixin": {
|
|
||||||
"appid": "",
|
|
||||||
"setting": {
|
|
||||||
"urlCheck": false
|
|
||||||
},
|
},
|
||||||
"usingComponents": true
|
"mp-alipay" : {
|
||||||
},
|
"usingComponents" : true
|
||||||
"mp-alipay": {
|
},
|
||||||
"usingComponents": true
|
"mp-baidu" : {
|
||||||
},
|
"usingComponents" : true
|
||||||
"mp-baidu": {
|
},
|
||||||
"usingComponents": true
|
"mp-toutiao" : {
|
||||||
},
|
"usingComponents" : true
|
||||||
"mp-toutiao": {
|
},
|
||||||
"usingComponents": true
|
"uniStatistics" : {
|
||||||
},
|
"enable" : false
|
||||||
"uniStatistics": {
|
},
|
||||||
"enable": false
|
"vueVersion" : "3"
|
||||||
},
|
|
||||||
"vueVersion": "3"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "gym-manage-uniapp",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Gym Management System Mobile App",
|
|
||||||
"main": "main.js",
|
|
||||||
"scripts": {
|
|
||||||
"dev:mp-weixin": "uni -p mp-weixin",
|
|
||||||
"build:mp-weixin": "uni build -p mp-weixin",
|
|
||||||
"dev:h5": "uni",
|
|
||||||
"build:h5": "uni build"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"uniapp",
|
|
||||||
"gym",
|
|
||||||
"management"
|
|
||||||
],
|
|
||||||
"author": "Novalon",
|
|
||||||
"license": "MIT"
|
|
||||||
}
|
|
||||||
+295
-27
@@ -1,28 +1,296 @@
|
|||||||
{
|
{
|
||||||
"pages": [
|
"pages": [
|
||||||
{
|
{
|
||||||
"path": "pages/index/index",
|
"path": "pages/index/index",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "首页"
|
"navigationStyle":"custom",
|
||||||
}
|
"navigationBarTitleText": "健身房",
|
||||||
}
|
"app-plus": {
|
||||||
],
|
"animationType": "fade-in",
|
||||||
"globalStyle": {
|
"animationDuration": 200
|
||||||
"navigationBarTextStyle": "black",
|
}
|
||||||
"navigationBarTitleText": "健身房管理系统",
|
}
|
||||||
"navigationBarBackgroundColor": "#F8F8F8",
|
},
|
||||||
"backgroundColor": "#F8F8F8"
|
{
|
||||||
},
|
"path": "pages/course/index",
|
||||||
"tabBar": {
|
"style": {
|
||||||
"color": "#7A7E83",
|
"navigationBarTitleText": "课程",
|
||||||
"selectedColor": "#007AFF",
|
"app-plus": {
|
||||||
"borderStyle": "black",
|
"animationType": "fade-in",
|
||||||
"backgroundColor": "#F8F8F8",
|
"animationDuration": 200
|
||||||
"list": [
|
}
|
||||||
{
|
}
|
||||||
"pagePath": "pages/index/index",
|
},
|
||||||
"text": "首页"
|
{
|
||||||
}
|
"path": "pages/train/index",
|
||||||
]
|
"style": {
|
||||||
}
|
"navigationBarTitleText": "训练",
|
||||||
}
|
"app-plus": {
|
||||||
|
"animationType": "fade-in",
|
||||||
|
"animationDuration": 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/discover/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "发现",
|
||||||
|
"app-plus": {
|
||||||
|
"animationType": "fade-in",
|
||||||
|
"animationDuration": 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/memberInfo/memberInfo",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "我的",
|
||||||
|
"app-plus": {
|
||||||
|
"animationType": "fade-in",
|
||||||
|
"animationDuration": 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/memberInfo/memberCard",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "我的会员卡"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/memberInfo/userInfo",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "个人信息"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/memberInfo/booking",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "我的预约"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/memberInfo/bodyTestHome",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "智能体测"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/memberInfo/bodyTestConnect",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "连接设备"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/memberInfo/bodyTestMeasuring",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "测量中"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/memberInfo/bodyTestReport",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "体测报告"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/memberInfo/bodyTestHistory",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "体测记录"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/memberInfo/bodyTestCompare",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "历史对比"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/memberInfo/bodyTestSettings",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "体测设置"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/memberInfo/bodyTestTrend",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "趋势分析"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/memberInfo/trainReport",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "训练报告"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/memberInfo/trainSessionDetail",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "训练详情"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/memberInfo/coupons",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "我的优惠券"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/memberInfo/couponDetail",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "优惠券详情"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/memberInfo/couponCenter",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "领券中心"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/memberInfo/points",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "我的积分"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/memberInfo/pointsMall",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "积分商城"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/memberInfo/pointsHistory",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "积分明细"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/memberInfo/referral",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "邀请好友"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/memberInfo/myCourses",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "我的课程"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/memberInfo/checkInHistory",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "签到记录"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/memberInfo/courseList",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "预约课程"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/memberInfo/courseDetail",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "课程详情"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/memberInfo/onlineCourseDetail",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "线上课程"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/memberInfo/courseEvaluate",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "课程评价"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/checkIn/checkIn",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "会员签到"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/groupCourse/list",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "团课列表"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/groupCourse/detail",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "课程详情",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/LoadingOverlay/LoadingOverlay",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/searchCourse/searchCourse",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "搜索课程"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "components/global/GlobalLoading",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"globalStyle": {
|
||||||
|
"navigationBarTextStyle": "black",
|
||||||
|
"navigationBarTitleText": "健身房",
|
||||||
|
"navigationBarBackgroundColor": "#F8F8F8",
|
||||||
|
"backgroundColor": "#F8F8F8",
|
||||||
|
"app-plus": {
|
||||||
|
"animationType": "pop-in",
|
||||||
|
"animationDuration": 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniIdRouter": {},
|
||||||
|
"tabBar": {
|
||||||
|
"custom": true, // 启用自定义 tabBar
|
||||||
|
"list": [
|
||||||
|
{ "pagePath": "pages/index/index", "text": "首页" },
|
||||||
|
{ "pagePath": "pages/course/index", "text": "课程" },
|
||||||
|
{ "pagePath": "pages/train/index", "text": "训练" },
|
||||||
|
{ "pagePath": "pages/discover/index", "text": "发现" },
|
||||||
|
{ "pagePath": "pages/memberInfo/memberInfo", "text": "我的" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<!-- components/LoadingOverlay.vue -->
|
||||||
|
<template>
|
||||||
|
<view v-if="visible" class="loading-overlay">
|
||||||
|
<view class="loading-content">
|
||||||
|
<view class="loading-spinner"></view>
|
||||||
|
<text class="loading-text">{{ text }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
text: { type: String, default: '加载中...' },
|
||||||
|
delay: { type: Number, default: 200 }
|
||||||
|
})
|
||||||
|
|
||||||
|
const visible = ref(false)
|
||||||
|
let timer = null
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
visible.value = true
|
||||||
|
}, props.delay)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (timer) clearTimeout(timer)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.loading-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-content {
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 32rpx 48rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 48rpx;
|
||||||
|
height: 48rpx;
|
||||||
|
border: 4rpx solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-top-color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,262 @@
|
|||||||
|
<!-- pages/course/index.vue -->
|
||||||
|
<template>
|
||||||
|
<!-- <view class="bg-wrapper">
|
||||||
|
<image src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/wave_top.png" mode="widthFix" class="wave-bg wave-top" />
|
||||||
|
<image src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/wave_bottom.png" mode="widthFix" class="wave-bg wave-bottom" />
|
||||||
|
</view> -->
|
||||||
|
<view class="tab-page">
|
||||||
|
<view class="tab-page__header">
|
||||||
|
<text class="tab-page__title">课程</text>
|
||||||
|
<text class="tab-page__subtitle">精品团课 · 私教 · 线上课</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 骨架屏 -->
|
||||||
|
<view v-if="loading" class="skeleton-container">
|
||||||
|
<view class="skeleton-item" v-for="i in 3" :key="i">
|
||||||
|
<view class="skeleton-img"></view>
|
||||||
|
<view class="skeleton-text"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 真实内容 -->
|
||||||
|
<template v-else>
|
||||||
|
<RecommendCourses :data="courseData" />
|
||||||
|
|
||||||
|
<view class="tab-page__actions">
|
||||||
|
<view class="tab-page__btn" hover-class="tab-page__btn--hover" @tap="goCourseList">
|
||||||
|
<text class="tab-page__btn-text">预约课程</text>
|
||||||
|
</view>
|
||||||
|
<view class="tab-page__btn tab-page__btn--ghost" hover-class="tab-page__btn--hover" @tap="goMyCourses">
|
||||||
|
<text class="tab-page__btn-text tab-page__btn-text--ghost">我的课程</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<view class="bottom-placeholder"></view>
|
||||||
|
</view>
|
||||||
|
<TabBar @update:active="handleTabActive" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||||
|
import RecommendCourses from '@/components/index/RecommendCourses.vue'
|
||||||
|
import TabBar from '@/components/TabBar.vue'
|
||||||
|
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
|
||||||
|
|
||||||
|
const loading = ref(true)
|
||||||
|
const courseData = ref(null)
|
||||||
|
|
||||||
|
// 从缓存加载数据
|
||||||
|
function loadFromCache() {
|
||||||
|
try {
|
||||||
|
const cached = uni.getStorageSync('course_cache')
|
||||||
|
if (cached && Date.now() - cached.time < 5 * 60 * 1000) {
|
||||||
|
courseData.value = cached.data
|
||||||
|
loading.value = false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('读取缓存失败', e)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从网络加载数据
|
||||||
|
async function loadFromNetwork() {
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 模拟 API 请求
|
||||||
|
const res = await new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve({ code: 0, data: { list: [] } })
|
||||||
|
}, 500)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.code === 0) {
|
||||||
|
courseData.value = res.data
|
||||||
|
// 更新缓存
|
||||||
|
uni.setStorageSync('course_cache', {
|
||||||
|
data: res.data,
|
||||||
|
time: Date.now()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('加载失败', err)
|
||||||
|
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面激活时刷新数据(可选)
|
||||||
|
function handleTabActive(index) {
|
||||||
|
// Tab 切换时后台刷新数据
|
||||||
|
if (index === 1 && !loading.value) {
|
||||||
|
loadFromNetwork()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad(() => {
|
||||||
|
// 优先显示缓存
|
||||||
|
const hasCache = loadFromCache()
|
||||||
|
if (!hasCache) {
|
||||||
|
loadFromNetwork()
|
||||||
|
} else {
|
||||||
|
// 后台静默更新
|
||||||
|
setTimeout(() => {
|
||||||
|
loadFromNetwork()
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
// 每次显示时确保加载完成
|
||||||
|
if (loading.value && !courseData.value) {
|
||||||
|
loadFromNetwork()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function goCourseList() {
|
||||||
|
navigateToPage(PAGE.COURSE_LIST)
|
||||||
|
}
|
||||||
|
|
||||||
|
function goMyCourses() {
|
||||||
|
navigateToPage(PAGE.MY_COURSES)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
.bg-wrapper {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-bg {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-top {
|
||||||
|
top: 0;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-bottom {
|
||||||
|
bottom: 100rpx;
|
||||||
|
opacity: 0.35;
|
||||||
|
}
|
||||||
|
.tab-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding-bottom: 160rpx;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-page__header {
|
||||||
|
padding: 48rpx 32rpx 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-page__title {
|
||||||
|
display: block;
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
color: $text-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-page__subtitle {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-page__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 20rpx;
|
||||||
|
padding: 24rpx 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-page__btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 80rpx;
|
||||||
|
border-radius: $radius-full;
|
||||||
|
background: $gradient-orange;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: $shadow-orange-glow;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-page__btn:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
background: $accent-orange-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-page__btn--ghost {
|
||||||
|
background: $bg-white;
|
||||||
|
border: 1px solid $border-light;
|
||||||
|
box-shadow: $shadow-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-page__btn-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
color: $text-inverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-page__btn-text--ghost {
|
||||||
|
color: $primary-deep;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-placeholder {
|
||||||
|
height: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 骨架屏样式 */
|
||||||
|
.skeleton-container {
|
||||||
|
padding: 24rpx 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-item {
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
background: $bg-white;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
box-shadow: $shadow-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 200rpx;
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: skeleton-loading 1.5s infinite;
|
||||||
|
border-radius: $radius-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text {
|
||||||
|
height: 32rpx;
|
||||||
|
margin-top: 16rpx;
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: skeleton-loading 1.5s infinite;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes skeleton-loading {
|
||||||
|
0% { background-position: 200% 0; }
|
||||||
|
100% { background-position: -200% 0; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
<template>
|
||||||
|
<!-- <view class="bg-wrapper">
|
||||||
|
<image src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/wave_top.png" mode="widthFix" class="wave-bg wave-top" />
|
||||||
|
<image src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/wave_bottom.png" mode="widthFix" class="wave-bg wave-bottom" />
|
||||||
|
</view> -->
|
||||||
|
<view class="tab-page">
|
||||||
|
<view class="tab-page__header">
|
||||||
|
<text class="tab-page__title">发现</text>
|
||||||
|
<text class="tab-page__subtitle">活动 · 资讯 · 今日推荐</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<TodayRecommend />
|
||||||
|
|
||||||
|
<view class="discover-links">
|
||||||
|
<view class="discover-link" hover-class="discover-link--hover" @tap="goReferral">
|
||||||
|
<text class="discover-link__title">邀请好友</text>
|
||||||
|
<text class="discover-link__desc">邀请注册/购课,双方得积分</text>
|
||||||
|
</view>
|
||||||
|
<view class="discover-link" hover-class="discover-link--hover" @tap="goCouponCenter">
|
||||||
|
<text class="discover-link__title">领券中心</text>
|
||||||
|
<text class="discover-link__desc">限时优惠券,先到先得</text>
|
||||||
|
</view>
|
||||||
|
<view class="discover-link" hover-class="discover-link--hover" @tap="goPointsMall">
|
||||||
|
<text class="discover-link__title">积分商城</text>
|
||||||
|
<text class="discover-link__desc">积分兑换好礼</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="bottom-placeholder"></view>
|
||||||
|
</view>
|
||||||
|
<TabBar :active="3" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import TodayRecommend from '@/components/index/TodayRecommend.vue'
|
||||||
|
import TabBar from '@/components/TabBar.vue'
|
||||||
|
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
|
||||||
|
|
||||||
|
function goReferral() {
|
||||||
|
navigateToPage(PAGE.REFERRAL)
|
||||||
|
}
|
||||||
|
|
||||||
|
function goCouponCenter() {
|
||||||
|
navigateToPage(PAGE.COUPON_CENTER)
|
||||||
|
}
|
||||||
|
|
||||||
|
function goPointsMall() {
|
||||||
|
navigateToPage(PAGE.POINTS_MALL)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.bg-wrapper {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-bg {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-top {
|
||||||
|
top: 0;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-bottom {
|
||||||
|
bottom: 100rpx;
|
||||||
|
opacity: 0.35;
|
||||||
|
}
|
||||||
|
.tab-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding-bottom: 160rpx;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-page__header {
|
||||||
|
padding: 48rpx 32rpx 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-page__title {
|
||||||
|
display: block;
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
color: $text-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-page__subtitle {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discover-links {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16rpx;
|
||||||
|
padding: 24rpx 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discover-link {
|
||||||
|
padding: 24rpx 28rpx;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
background: $bg-white;
|
||||||
|
box-shadow: $shadow-sm;
|
||||||
|
border: 1px solid $border-light;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discover-link:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.discover-link__title {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
color: $text-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discover-link__desc {
|
||||||
|
display: block;
|
||||||
|
margin-top: 6rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: $text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-placeholder {
|
||||||
|
height: 40rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,908 @@
|
|||||||
|
<template>
|
||||||
|
<view class="course-detail-page">
|
||||||
|
<!-- 顶部导航 -->
|
||||||
|
<MemberInfoSubNav :title="course.courseName || '课程详情'" @back="goBack" />
|
||||||
|
|
||||||
|
<!-- 骨架屏 -->
|
||||||
|
<view v-if="loading" class="skeleton">
|
||||||
|
<view class="skeleton-header">
|
||||||
|
<view class="skeleton-cover skeleton-block"></view>
|
||||||
|
</view>
|
||||||
|
<view class="skeleton-content">
|
||||||
|
<view class="skeleton-name skeleton-block"></view>
|
||||||
|
<view class="skeleton-meta">
|
||||||
|
<view class="skeleton-meta-item skeleton-block"></view>
|
||||||
|
<view class="skeleton-meta-item skeleton-block"></view>
|
||||||
|
<view class="skeleton-meta-item skeleton-block"></view>
|
||||||
|
</view>
|
||||||
|
<view class="skeleton-card">
|
||||||
|
<view class="skeleton-card-title skeleton-block"></view>
|
||||||
|
<view class="skeleton-card-content">
|
||||||
|
<view class="skeleton-text-line skeleton-block"></view>
|
||||||
|
<view class="skeleton-text-line skeleton-block"></view>
|
||||||
|
<view class="skeleton-text-line skeleton-block short"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="skeleton-card">
|
||||||
|
<view class="skeleton-info-row">
|
||||||
|
<view class="skeleton-info-label skeleton-block"></view>
|
||||||
|
<view class="skeleton-info-value skeleton-block"></view>
|
||||||
|
</view>
|
||||||
|
<view class="skeleton-info-row">
|
||||||
|
<view class="skeleton-info-label skeleton-block"></view>
|
||||||
|
<view class="skeleton-info-value skeleton-block"></view>
|
||||||
|
</view>
|
||||||
|
<view class="skeleton-info-row">
|
||||||
|
<view class="skeleton-info-label skeleton-block"></view>
|
||||||
|
<view class="skeleton-info-value skeleton-block"></view>
|
||||||
|
</view>
|
||||||
|
<view class="skeleton-info-row">
|
||||||
|
<view class="skeleton-info-label skeleton-block"></view>
|
||||||
|
<view class="skeleton-payment">
|
||||||
|
<view class="skeleton-payment-item skeleton-block"></view>
|
||||||
|
<view class="skeleton-payment-item skeleton-block"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="skeleton-card">
|
||||||
|
<view class="skeleton-card-title skeleton-block"></view>
|
||||||
|
<view class="skeleton-notice">
|
||||||
|
<view class="skeleton-notice-item skeleton-block"></view>
|
||||||
|
<view class="skeleton-notice-item skeleton-block"></view>
|
||||||
|
<view class="skeleton-notice-item skeleton-block"></view>
|
||||||
|
<view class="skeleton-notice-item skeleton-block"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 内容区域 -->
|
||||||
|
<view v-else>
|
||||||
|
<!-- 顶部图片区域 -->
|
||||||
|
<view class="header-section">
|
||||||
|
<!-- 课程封面图 -->
|
||||||
|
<view class="cover-image-wrapper">
|
||||||
|
<image
|
||||||
|
v-if="course.coverImage"
|
||||||
|
:src="course.coverImage"
|
||||||
|
mode="aspectFill"
|
||||||
|
class="cover-image"
|
||||||
|
/>
|
||||||
|
<view v-else class="cover-image-placeholder">
|
||||||
|
<uni-icons type="image" size="80" color="#CCCCCC" />
|
||||||
|
<text class="placeholder-text">暂无封面</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 课程基本信息 -->
|
||||||
|
<view class="basic-info-section">
|
||||||
|
<view class="course-name-wrapper">
|
||||||
|
<text class="course-name">{{ course.courseName }}</text>
|
||||||
|
<!-- 课程状态标签 -->
|
||||||
|
<view :class="['status-tag', statusClass]">
|
||||||
|
{{ statusText }}
|
||||||
|
</view>
|
||||||
|
<view :class="['course-type-badge', courseTypeClass]">
|
||||||
|
{{ courseTypeText }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="course-meta">
|
||||||
|
<view class="meta-item">
|
||||||
|
<uni-icons type="calendar" size="24" color="#8A99B4" />
|
||||||
|
<text class="meta-text">{{ formatDate(course.startTime) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="meta-item">
|
||||||
|
<uni-icons type="clock" size="24" color="#8A99B4" />
|
||||||
|
<text class="meta-text">{{ formatTime(course.startTime) }} - {{ formatTime(course.endTime) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="meta-item">
|
||||||
|
<uni-icons type="map-pin" size="24" color="#8A99B4" />
|
||||||
|
<text class="meta-text">{{ course.location }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 课程详情卡片 -->
|
||||||
|
<view class="detail-card">
|
||||||
|
<view class="card-header">
|
||||||
|
<uni-icons type="info" size="24" color="#FF6B35" />
|
||||||
|
<text class="card-title">课程详情</text>
|
||||||
|
</view>
|
||||||
|
<view class="card-content">
|
||||||
|
<text class="description-text">{{ course.description }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 课程信息卡片 -->
|
||||||
|
<view class="info-card">
|
||||||
|
<view class="info-row">
|
||||||
|
<view class="info-label">
|
||||||
|
<uni-icons type="users" size="24" color="#5E6F8D" />
|
||||||
|
<text>课程容量</text>
|
||||||
|
</view>
|
||||||
|
<text class="info-value">{{ course.currentMembers }} / {{ course.maxMembers }}人</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row">
|
||||||
|
<view class="info-label">
|
||||||
|
<uni-icons type="time" size="24" color="#5E6F8D" />
|
||||||
|
<text>课程时长</text>
|
||||||
|
</view>
|
||||||
|
<text class="info-value">{{ formatDuration(course.startTime, course.endTime) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row">
|
||||||
|
<view class="info-label">
|
||||||
|
<uni-icons type="star" size="24" color="#5E6F8D" />
|
||||||
|
<text>教练</text>
|
||||||
|
</view>
|
||||||
|
<text class="info-value">ID: {{ course.coachId }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row">
|
||||||
|
<view class="info-label">
|
||||||
|
<uni-icons type="credit-card" size="24" color="#5E6F8D" />
|
||||||
|
<text>支付方式</text>
|
||||||
|
</view>
|
||||||
|
<view class="payment-options">
|
||||||
|
<view
|
||||||
|
v-if="course.storedValueAmount > 0"
|
||||||
|
:class="['payment-item', { active: selectedPayment === 'storedValue' || (!selectedPayment && course.pointCardAmount === 0) }]"
|
||||||
|
@click="selectPayment('storedValue')"
|
||||||
|
>
|
||||||
|
<text class="payment-label">储值卡</text>
|
||||||
|
<text class="payment-value">¥{{ course.storedValueAmount }}</text>
|
||||||
|
<view v-if="selectedPayment === 'storedValue' || (!selectedPayment && course.pointCardAmount === 0)" class="payment-check">
|
||||||
|
<uni-icons type="checkmark" size="20" color="#ffffff" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-if="course.pointCardAmount > 0"
|
||||||
|
:class="['payment-item', { active: selectedPayment === 'pointCard' || (!selectedPayment && course.storedValueAmount === 0) }]"
|
||||||
|
@click="selectPayment('pointCard')"
|
||||||
|
>
|
||||||
|
<text class="payment-label">次卡</text>
|
||||||
|
<text class="payment-value">{{ course.pointCardAmount }}次</text>
|
||||||
|
<view v-if="selectedPayment === 'pointCard' || (!selectedPayment && course.storedValueAmount === 0)" class="payment-check">
|
||||||
|
<uni-icons type="checkmark" size="20" color="#ffffff" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="course.storedValueAmount === 0 && course.pointCardAmount === 0" class="payment-item free">
|
||||||
|
<text class="payment-value">免费</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 预约须知 -->
|
||||||
|
<view class="notice-card">
|
||||||
|
<view class="card-header">
|
||||||
|
<uni-icons type="alert-circle" size="24" color="#F39C12" />
|
||||||
|
<text class="card-title">预约须知</text>
|
||||||
|
</view>
|
||||||
|
<view class="notice-list">
|
||||||
|
<view class="notice-item">
|
||||||
|
<text class="notice-dot">●</text>
|
||||||
|
<text>请在课程开始前30分钟到达教室</text>
|
||||||
|
</view>
|
||||||
|
<view class="notice-item">
|
||||||
|
<text class="notice-dot">●</text>
|
||||||
|
<text>如需取消预约,请提前2小时操作</text>
|
||||||
|
</view>
|
||||||
|
<view class="notice-item">
|
||||||
|
<text class="notice-dot">●</text>
|
||||||
|
<text>课程开始后将无法取消预约</text>
|
||||||
|
</view>
|
||||||
|
<view class="notice-item">
|
||||||
|
<text class="notice-dot">●</text>
|
||||||
|
<text>迟到超过15分钟将无法入场</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部操作栏 -->
|
||||||
|
<view class="bottom-bar">
|
||||||
|
<view class="price-display">
|
||||||
|
<text v-if="selectedPrice.type === 'storedValue'" class="price">
|
||||||
|
<text class="currency">¥</text>{{ selectedPrice.amount }}
|
||||||
|
</text>
|
||||||
|
<text v-else-if="selectedPrice.type === 'pointCard'" class="price points">
|
||||||
|
<uni-icons type="shop" size="20" color="#FF6B35" />
|
||||||
|
<text>{{ selectedPrice.amount }}次</text>
|
||||||
|
</text>
|
||||||
|
<text v-else class="price free">免费</text>
|
||||||
|
</view>
|
||||||
|
<view :class="['booking-btn', { disabled: !canBook }]" @click="handleBooking">
|
||||||
|
<text>{{ canBook ? '立即预约' : (course.currentMembers >= course.maxMembers ? '已满员' : statusText) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { groupCourseService } from '@/request_api/groupCourse.mock.js'
|
||||||
|
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
const loading = ref(true)
|
||||||
|
|
||||||
|
// 课程详情数据
|
||||||
|
const course = ref({
|
||||||
|
id: '',
|
||||||
|
courseName: '',
|
||||||
|
courseType: '',
|
||||||
|
startTime: '',
|
||||||
|
endTime: '',
|
||||||
|
maxMembers: 0,
|
||||||
|
currentMembers: 0,
|
||||||
|
status: '',
|
||||||
|
location: '',
|
||||||
|
coverImage: '',
|
||||||
|
description: '',
|
||||||
|
coachId: '',
|
||||||
|
pointCardAmount: 0,
|
||||||
|
storedValueAmount: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 选中的支付方式 ('storedValue' | 'pointCard' | 'free')
|
||||||
|
const selectedPayment = ref('')
|
||||||
|
|
||||||
|
// 课程状态文本
|
||||||
|
const statusText = computed(() => {
|
||||||
|
const status = course.value.status
|
||||||
|
if (status === '0') return '进行中'
|
||||||
|
if (status === '1') return '已取消'
|
||||||
|
if (status === '2') return '已结束'
|
||||||
|
return '未知'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 课程状态样式
|
||||||
|
const statusClass = computed(() => {
|
||||||
|
const status = course.value.status
|
||||||
|
if (status === '0') return 'active'
|
||||||
|
if (status === '1') return 'canceled'
|
||||||
|
if (status === '2') return 'ended'
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 课程类型文本
|
||||||
|
const courseTypeText = computed(() => {
|
||||||
|
const type = course.value.courseType
|
||||||
|
if (type === '1') return '瑜伽/普拉提'
|
||||||
|
if (type === '2') return '有氧训练'
|
||||||
|
if (type === '3') return '力量训练'
|
||||||
|
return '其他'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 课程类型样式
|
||||||
|
const courseTypeClass = computed(() => {
|
||||||
|
const type = course.value.courseType
|
||||||
|
if (type === '1') return 'yoga'
|
||||||
|
if (type === '2') return 'cardio'
|
||||||
|
if (type === '3') return 'strength'
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 是否可以预约
|
||||||
|
const canBook = computed(() => {
|
||||||
|
const status = course.value.status
|
||||||
|
const isFull = course.value.currentMembers >= course.value.maxMembers
|
||||||
|
return status === '0' && !isFull
|
||||||
|
})
|
||||||
|
|
||||||
|
// 是否有多种支付方式
|
||||||
|
const hasMultiplePayment = computed(() => {
|
||||||
|
return course.value.storedValueAmount > 0 && course.value.pointCardAmount > 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 当前选中的支付方式价格显示
|
||||||
|
const selectedPrice = computed(() => {
|
||||||
|
if (selectedPayment.value === 'storedValue') {
|
||||||
|
return { type: 'storedValue', amount: course.value.storedValueAmount }
|
||||||
|
} else if (selectedPayment.value === 'pointCard') {
|
||||||
|
return { type: 'pointCard', amount: course.value.pointCardAmount }
|
||||||
|
} else if (course.value.storedValueAmount > 0 && course.value.pointCardAmount > 0) {
|
||||||
|
// 默认优先显示储值卡
|
||||||
|
return { type: 'storedValue', amount: course.value.storedValueAmount }
|
||||||
|
} else if (course.value.storedValueAmount > 0) {
|
||||||
|
return { type: 'storedValue', amount: course.value.storedValueAmount }
|
||||||
|
} else if (course.value.pointCardAmount > 0) {
|
||||||
|
return { type: 'pointCard', amount: course.value.pointCardAmount }
|
||||||
|
}
|
||||||
|
return { type: 'free', amount: 0 }
|
||||||
|
})
|
||||||
|
|
||||||
|
// 选择支付方式
|
||||||
|
const selectPayment = (type) => {
|
||||||
|
selectedPayment.value = type
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (dateStr) => {
|
||||||
|
if (!dateStr) return ''
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||||
|
const day = date.getDate().toString().padStart(2, '0')
|
||||||
|
const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
||||||
|
const weekDay = weekDays[date.getDay()]
|
||||||
|
return `${year}年${month}月${day}日 ${weekDay}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化时间
|
||||||
|
const formatTime = (dateStr) => {
|
||||||
|
if (!dateStr) return ''
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
const hours = date.getHours().toString().padStart(2, '0')
|
||||||
|
const minutes = date.getMinutes().toString().padStart(2, '0')
|
||||||
|
return `${hours}:${minutes}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化时长
|
||||||
|
const formatDuration = (startStr, endStr) => {
|
||||||
|
if (!startStr || !endStr) return ''
|
||||||
|
const start = new Date(startStr)
|
||||||
|
const end = new Date(endStr)
|
||||||
|
const minutes = Math.floor((end.getTime() - start.getTime()) / 60000)
|
||||||
|
return `${minutes}分钟`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回上一页
|
||||||
|
const goBack = () => {
|
||||||
|
uni.navigateBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预约处理
|
||||||
|
const handleBooking = () => {
|
||||||
|
if (!canBook.value) return
|
||||||
|
|
||||||
|
uni.showModal({
|
||||||
|
title: '确认预约',
|
||||||
|
content: `确定要预约「${course.value.courseName}」吗?`,
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
uni.showLoading({ title: '预约中...' })
|
||||||
|
groupCourseService.book({
|
||||||
|
courseId: course.value.id,
|
||||||
|
memberId: '1' // 模拟会员ID
|
||||||
|
}).then(() => {
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.showToast({
|
||||||
|
title: '预约成功',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.navigateBack()
|
||||||
|
}, 1500)
|
||||||
|
}).catch(() => {
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.showToast({
|
||||||
|
title: '预约失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取课程详情
|
||||||
|
const fetchCourseDetail = async (id) => {
|
||||||
|
try {
|
||||||
|
const result = await groupCourseService.getDetail(id)
|
||||||
|
course.value = result
|
||||||
|
console.log('[detail.vue] 课程详情获取成功:', course.value)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[detail.vue] 获取课程详情失败:', error)
|
||||||
|
uni.showToast({
|
||||||
|
title: '获取课程详情失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面挂载时获取课程详情
|
||||||
|
onMounted(() => {
|
||||||
|
const pages = getCurrentPages()
|
||||||
|
const currentPage = pages[pages.length - 1]
|
||||||
|
const options = currentPage.options || {}
|
||||||
|
const courseId = options.id || '1'
|
||||||
|
|
||||||
|
console.log('[detail.vue] 页面挂载,课程ID:', courseId)
|
||||||
|
fetchCourseDetail(courseId)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.course-detail-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #F5F7FA;
|
||||||
|
padding-bottom: calc(140rpx + constant(safe-area-inset-bottom));
|
||||||
|
padding-bottom: calc(140rpx + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部图片区域 */
|
||||||
|
.header-section {
|
||||||
|
width: 100%;
|
||||||
|
background: #FFFFFF;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-image-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 480rpx;
|
||||||
|
background: #F5F7FA;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-image-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(135deg, #F3F4F6 0%, #E5E7EB 100%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 课程基本信息 */
|
||||||
|
.basic-info-section {
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 32rpx;
|
||||||
|
margin-top: -40rpx;
|
||||||
|
border-radius: 32rpx 32rpx 0 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-name-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-name {
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1E2A3A;
|
||||||
|
flex: 1;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 课程基本信息区域的状态标签 */
|
||||||
|
.status-tag {
|
||||||
|
padding: 8rpx 20rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: linear-gradient(135deg, #10B981 0%, #059669 100%);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.canceled {
|
||||||
|
background: linear-gradient(135deg, #9CA3AF 0%, #6B7280 100%);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ended {
|
||||||
|
background: linear-gradient(135deg, #D1D5DB 0%, #9CA3AF 100%);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-type-badge {
|
||||||
|
padding: 8rpx 20rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&.yoga {
|
||||||
|
background: linear-gradient(135deg, #D1FAE5 0%, #A7F3D0 100%);
|
||||||
|
color: #065F46;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cardio {
|
||||||
|
background: linear-gradient(135deg, #FEF3C7 0%, #FDE68A 100%);
|
||||||
|
color: #92400E;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.strength {
|
||||||
|
background: linear-gradient(135deg, #DBEAFE 0%, #93C5FD 100%);
|
||||||
|
color: #1E40AF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
padding: 16rpx 20rpx;
|
||||||
|
background: linear-gradient(135deg, #F9FAFB 0%, #F3F4F6 100%);
|
||||||
|
border-radius: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #4B5563;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 详情卡片 */
|
||||||
|
.detail-card {
|
||||||
|
background: #ffffff;
|
||||||
|
margin: 24rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 28rpx;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1E2A3A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
padding-top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #5E6F8D;
|
||||||
|
line-height: 1.8;
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 信息卡片 */
|
||||||
|
.info-card {
|
||||||
|
background: #ffffff;
|
||||||
|
margin: 0 24rpx 24rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 28rpx;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20rpx 0;
|
||||||
|
border-bottom: 1rpx solid #F3F4F6;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
width: 200rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #5E6F8D;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #1E2A3A;
|
||||||
|
font-weight: 500;
|
||||||
|
flex: 1;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-options {
|
||||||
|
display: flex;
|
||||||
|
gap: 20rpx;
|
||||||
|
flex: 1;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16rpx 28rpx;
|
||||||
|
background: linear-gradient(135deg, #F9FAFB 0%, #F3F4F6 100%);
|
||||||
|
border-radius: 16rpx;
|
||||||
|
border: 2rpx solid transparent;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: linear-gradient(135deg, #FF6B35 0%, #FF8C5A 100%);
|
||||||
|
border-color: #FF6B35;
|
||||||
|
|
||||||
|
.payment-label,
|
||||||
|
.payment-value {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.free {
|
||||||
|
background: linear-gradient(135deg, #ECFDF5 0%, #D1FAE5 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-label {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-value {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #FF6B35;
|
||||||
|
|
||||||
|
.free & {
|
||||||
|
color: #059669;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-check {
|
||||||
|
position: absolute;
|
||||||
|
top: -8rpx;
|
||||||
|
right: -8rpx;
|
||||||
|
width: 36rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
background: #059669;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 预约须知 */
|
||||||
|
.notice-card {
|
||||||
|
background: #ffffff;
|
||||||
|
margin: 0 24rpx 24rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 28rpx;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-list {
|
||||||
|
padding-top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 12rpx;
|
||||||
|
padding: 12rpx 0;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #5E6F8D;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-dot {
|
||||||
|
color: #F39C12;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 底部操作栏 */
|
||||||
|
.bottom-bar {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 20rpx 24rpx;
|
||||||
|
padding-bottom: calc(20rpx + constant(safe-area-inset-bottom));
|
||||||
|
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24rpx;
|
||||||
|
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.08);
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-display {
|
||||||
|
flex: 1;
|
||||||
|
padding-left: 20rpx;
|
||||||
|
|
||||||
|
.price {
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #FF6B35;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 4rpx;
|
||||||
|
|
||||||
|
.currency {
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.points {
|
||||||
|
gap: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.free {
|
||||||
|
color: #10B981;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-btn {
|
||||||
|
padding: 24rpx 64rpx;
|
||||||
|
background: linear-gradient(135deg, #FF6B35 0%, #FF8C5A 100%);
|
||||||
|
border-radius: 52rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
box-shadow: 0 8rpx 24rpx rgba(255, 107, 53, 0.3);
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(255, 107, 53, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
background: linear-gradient(135deg, #F3F4F6 0%, #E5E7EB 100%);
|
||||||
|
color: #9CA3AF;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 骨架屏样式 */
|
||||||
|
.skeleton {
|
||||||
|
padding-bottom: calc(140rpx + constant(safe-area-inset-bottom));
|
||||||
|
padding-bottom: calc(140rpx + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-header {
|
||||||
|
background: #ffffff;
|
||||||
|
padding-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-cover {
|
||||||
|
width: 100%;
|
||||||
|
height: 480rpx;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-content {
|
||||||
|
margin-top: -40rpx;
|
||||||
|
padding: 0 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-block {
|
||||||
|
background: linear-gradient(90deg, #F0F0F0 25%, #E0E0E0 50%, #F0F0F0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: skeleton-loading 1.5s ease-in-out infinite;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes skeleton-loading {
|
||||||
|
0% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-name {
|
||||||
|
width: 50%;
|
||||||
|
height: 48rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-meta-item {
|
||||||
|
width: 70%;
|
||||||
|
height: 48rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 28rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-card-title {
|
||||||
|
width: 30%;
|
||||||
|
height: 36rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-card-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text-line {
|
||||||
|
height: 32rpx;
|
||||||
|
|
||||||
|
&.short {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-info-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20rpx 0;
|
||||||
|
border-bottom: 1rpx solid #F3F4F6;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-info-label {
|
||||||
|
width: 160rpx;
|
||||||
|
height: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-info-value {
|
||||||
|
width: 120rpx;
|
||||||
|
height: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-payment {
|
||||||
|
display: flex;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-payment-item {
|
||||||
|
width: 100rpx;
|
||||||
|
height: 60rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-notice {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16rpx;
|
||||||
|
padding-top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-notice-item {
|
||||||
|
height: 32rpx;
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
<template>
|
||||||
|
<view class="group-course-page">
|
||||||
|
<!-- 搜索区域 -->
|
||||||
|
<view class="search-section">
|
||||||
|
<!-- 搜索框组件 -->
|
||||||
|
<SearchBar
|
||||||
|
v-model="searchKeyword"
|
||||||
|
:hot-keywords="hotKeywords"
|
||||||
|
@search="handleSearch"
|
||||||
|
ref="searchBarRef"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 筛选条件组件 -->
|
||||||
|
<FilterSection
|
||||||
|
:time-range-text="timeRangeText"
|
||||||
|
:sort-options="sortOptions"
|
||||||
|
v-model:sort-index="sortIndex"
|
||||||
|
@time-pick="showTimePicker = true"
|
||||||
|
ref="filterSectionRef"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 时间段选择组件 -->
|
||||||
|
<TimePeriodSelector
|
||||||
|
:time-period-options="timePeriodOptions"
|
||||||
|
v-model="timePeriodIndex"
|
||||||
|
@change="onTimePeriodChange"
|
||||||
|
ref="timePeriodRef"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 团课列表 -->
|
||||||
|
<scroll-view
|
||||||
|
scroll-y
|
||||||
|
class="course-list"
|
||||||
|
@scrolltolower="onScrollToLower"
|
||||||
|
scroll-with-animation
|
||||||
|
>
|
||||||
|
<GroupCourseCard
|
||||||
|
v-for="course in filteredCourseList"
|
||||||
|
:key="course.id"
|
||||||
|
:course="course"
|
||||||
|
@booking="handleBooking"
|
||||||
|
@detail="goDetail"
|
||||||
|
></GroupCourseCard>
|
||||||
|
|
||||||
|
<!-- 加载更多提示 -->
|
||||||
|
<view class="load-more">
|
||||||
|
<text v-if="loading">加载中...</text>
|
||||||
|
<text v-else-if="!hasMore">没有更多数据了</text>
|
||||||
|
<text v-else class="load-more-text" @tap="loadMore">点击加载更多</text>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<!-- 底部导航 -->
|
||||||
|
<TabBar :active-tab="1" />
|
||||||
|
|
||||||
|
<!-- 时间选择器组件 -->
|
||||||
|
<TimeRangePicker
|
||||||
|
v-model:visible="showTimePicker"
|
||||||
|
v-model:start-date="startDate"
|
||||||
|
v-model:end-date="endDate"
|
||||||
|
@confirm="onTimeRangeConfirm"
|
||||||
|
ref="timeRangePickerRef"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import TabBar from '@/components/TabBar.vue'
|
||||||
|
import GroupCourseCard from '@/components/groupCourse/CourseCard.vue'
|
||||||
|
import SearchBar from '@/components/groupCourse/SearchBar.vue'
|
||||||
|
import FilterSection from '@/components/groupCourse/FilterSection.vue'
|
||||||
|
import TimePeriodSelector from '@/components/groupCourse/TimePeriodSelector.vue'
|
||||||
|
import TimeRangePicker from '@/components/groupCourse/TimeRangePicker.vue'
|
||||||
|
import { useGroupCourseList } from '@/composables/useGroupCourseList.js'
|
||||||
|
|
||||||
|
// 组件引用
|
||||||
|
const searchBarRef = ref(null)
|
||||||
|
const filterSectionRef = ref(null)
|
||||||
|
const timePeriodRef = ref(null)
|
||||||
|
const timeRangePickerRef = ref(null)
|
||||||
|
|
||||||
|
// 使用组合式函数
|
||||||
|
const {
|
||||||
|
// 状态
|
||||||
|
loading,
|
||||||
|
hasMore,
|
||||||
|
searchKeyword,
|
||||||
|
hotKeywords,
|
||||||
|
sortOptions,
|
||||||
|
sortIndex,
|
||||||
|
timePeriodOptions,
|
||||||
|
timePeriodIndex,
|
||||||
|
showTimePicker,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
timeRangeText,
|
||||||
|
filteredCourseList,
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
getAllSearchParams,
|
||||||
|
handleSearch,
|
||||||
|
onTimePeriodChange,
|
||||||
|
onTimeRangeConfirm,
|
||||||
|
handleBooking,
|
||||||
|
goDetail,
|
||||||
|
fetchCourseList,
|
||||||
|
loadMore,
|
||||||
|
onScrollToLower
|
||||||
|
} = useGroupCourseList()
|
||||||
|
|
||||||
|
// 组件挂载时调用接口获取团课列表
|
||||||
|
onMounted(() => {
|
||||||
|
console.log('[list.vue] 页面组件已挂载,开始获取团课列表')
|
||||||
|
fetchCourseList()
|
||||||
|
console.log('[list.vue] 可用的搜索参数获取方法:')
|
||||||
|
console.log(' - searchBarRef.getSearchParams()')
|
||||||
|
console.log(' - filterSectionRef.getFilterParams()')
|
||||||
|
console.log(' - timePeriodRef.getTimePeriodParams()')
|
||||||
|
console.log(' - timeRangePickerRef.getTimeRangeParams()')
|
||||||
|
console.log(' - getAllSearchParams() 获取所有参数')
|
||||||
|
})
|
||||||
|
|
||||||
|
// 暴露方法供外部调用
|
||||||
|
defineExpose({
|
||||||
|
getAllSearchParams: () => getAllSearchParams(searchBarRef.value, filterSectionRef.value, timePeriodRef.value, timeRangePickerRef.value),
|
||||||
|
searchBarRef,
|
||||||
|
filterSectionRef,
|
||||||
|
timePeriodRef,
|
||||||
|
timeRangePickerRef
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
.group-course-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #F5F7FA;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 搜索区域 */
|
||||||
|
.search-section {
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 24rpx;
|
||||||
|
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 课程列表 */
|
||||||
|
.course-list {
|
||||||
|
height: calc(100vh - 380rpx);
|
||||||
|
padding: 24rpx 24rpx;
|
||||||
|
padding-bottom: calc(160rpx + constant(safe-area-inset-bottom));
|
||||||
|
padding-bottom: calc(160rpx + env(safe-area-inset-bottom));
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载更多提示 */
|
||||||
|
.load-more {
|
||||||
|
text-align: center;
|
||||||
|
padding: 30rpx 0;
|
||||||
|
color: #999999;
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more-text {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 水波纹背景 - 顶层显示 -->
|
||||||
|
<!-- <view class="bg-wrapper">
|
||||||
|
<image src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/wave_top.png" mode="widthFix" class="wave-bg wave-top" />
|
||||||
|
<image src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/wave_bottom.png" mode="widthFix" class="wave-bg wave-bottom" />
|
||||||
|
</view> -->
|
||||||
|
<!-- 固定白色块(滚动时显示) -->
|
||||||
|
<view class="hand" :style="{height : handHeight + 'rpx'}" v-show="isShow"></view>
|
||||||
|
|
||||||
|
<!-- 滚动内容区域 -->
|
||||||
|
<scroll-view
|
||||||
|
scroll-y
|
||||||
|
refresher-enabled
|
||||||
|
:refresher-triggered="isRefreshing"
|
||||||
|
refresher-default-style="none"
|
||||||
|
@refresherrefresh="onRefresh"
|
||||||
|
@scroll="handleScroll"
|
||||||
|
class="scroll-container"
|
||||||
|
>
|
||||||
|
<!-- 主内容 -->
|
||||||
|
<view class="home-page">
|
||||||
|
<HomeSkeleton v-if="loading" />
|
||||||
|
<template v-else>
|
||||||
|
<BannerSwiper />
|
||||||
|
<QuickEntry />
|
||||||
|
<RecommendCourses />
|
||||||
|
<TodayRecommend />
|
||||||
|
<view class="bottom-placeholder"></view>
|
||||||
|
</template>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<!-- TabBar 固定在底部 -->
|
||||||
|
<view class="tabbar-fixed">
|
||||||
|
<TabBar />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import BannerSwiper from '@/components/index/BannerSwiper.vue'
|
||||||
|
import QuickEntry from '@/components/index/QuickEntry.vue'
|
||||||
|
import RecommendCourses from '@/components/index/RecommendCourses.vue'
|
||||||
|
import TodayRecommend from '@/components/index/TodayRecommend.vue'
|
||||||
|
import TabBar from '@/components/TabBar.vue'
|
||||||
|
import HomeSkeleton from '@/components/Skeleton/HomeSkeleton.vue'
|
||||||
|
|
||||||
|
const loading = ref(true)
|
||||||
|
const isShow = ref(false)
|
||||||
|
const handHeight = ref(0)
|
||||||
|
const scrollDistance = ref(0)
|
||||||
|
const isRefreshing = ref(false)
|
||||||
|
|
||||||
|
// 获取可视窗口高度
|
||||||
|
const windowHeight = ref(0)
|
||||||
|
// 获取整个滚动内容的高度
|
||||||
|
const scrollContentHeight = ref(0)
|
||||||
|
|
||||||
|
// 滚动监听
|
||||||
|
const handleScroll = (e) => {
|
||||||
|
const distance = e.detail.scrollTop
|
||||||
|
scrollDistance.value = distance
|
||||||
|
|
||||||
|
// 获取滚动容器的高度(可视区域高度)
|
||||||
|
const scrollViewHeight = e.detail.scrollHeight
|
||||||
|
scrollContentHeight.value = scrollViewHeight
|
||||||
|
|
||||||
|
// 计算滚动百分比
|
||||||
|
// 滚动百分比 = 当前滚动距离 / (内容总高度 - 可视区域高度) * 100
|
||||||
|
const maxScrollDistance = scrollContentHeight.value - windowHeight.value
|
||||||
|
let scrollPercent = 0
|
||||||
|
|
||||||
|
if (maxScrollDistance > 0) {
|
||||||
|
scrollPercent = (distance / maxScrollDistance) * 100
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`滚动距离: ${distance}, 滚动百分比: ${scrollPercent.toFixed(2)}%`)
|
||||||
|
|
||||||
|
// 当滚动超过 10% 时显示白色块(你可以调整这个百分比)
|
||||||
|
const SHOW_THRESHOLD = 40 // 10% 的阈值,可以根据需要调整
|
||||||
|
isShow.value = scrollPercent > SHOW_THRESHOLD
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下拉刷新处理
|
||||||
|
const onRefresh = async () => {
|
||||||
|
console.log('开始下拉刷新')
|
||||||
|
isRefreshing.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await refreshData()
|
||||||
|
isRefreshing.value = false
|
||||||
|
uni.showToast({
|
||||||
|
title: '刷新成功',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('刷新失败', error)
|
||||||
|
isRefreshing.value = false
|
||||||
|
uni.showToast({
|
||||||
|
title: '刷新失败',
|
||||||
|
icon: 'error'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新数据
|
||||||
|
const refreshData = () => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('数据已刷新')
|
||||||
|
resolve()
|
||||||
|
}, 1500)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
loading.value = false
|
||||||
|
}, 1500)
|
||||||
|
|
||||||
|
// 获取胶囊按钮高度
|
||||||
|
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
|
||||||
|
const navTotalHeight = menuButtonInfo.top + menuButtonInfo.height
|
||||||
|
handHeight.value = navTotalHeight * 2.5
|
||||||
|
|
||||||
|
// 获取可视窗口高度
|
||||||
|
uni.getSystemInfo({
|
||||||
|
success: (res) => {
|
||||||
|
windowHeight.value = res.windowHeight
|
||||||
|
console.log('可视窗口高度:', windowHeight.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 延迟获取滚动内容高度(确保DOM已渲染)
|
||||||
|
setTimeout(() => {
|
||||||
|
const query = uni.createSelectorQuery().in(this)
|
||||||
|
query.select('.home-page').boundingClientRect(data => {
|
||||||
|
if (data) {
|
||||||
|
scrollContentHeight.value = data.height
|
||||||
|
console.log('内容总高度:', scrollContentHeight.value)
|
||||||
|
}
|
||||||
|
}).exec()
|
||||||
|
}, 500)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
/* 背景包装器 - 固定在最底层 */
|
||||||
|
.bg-wrapper {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-bg {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-top {
|
||||||
|
top: 0;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-bottom {
|
||||||
|
bottom: 100rpx;
|
||||||
|
opacity: 0.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动容器 */
|
||||||
|
.scroll-container {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主内容区域 */
|
||||||
|
.home-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding-bottom: 160rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 固定白色块 */
|
||||||
|
.hand {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 100;
|
||||||
|
background-color: white;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 固定 TabBar */
|
||||||
|
.tabbar-fixed {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-placeholder {
|
||||||
|
height: 120rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
<template>
|
||||||
|
<view class="scroll-container theme-light">
|
||||||
|
<view class="bt-page">
|
||||||
|
<MemberInfoSubNav title="历史对比" @back="onBack" />
|
||||||
|
<view class="bt-page__body">
|
||||||
|
<view class="bt-card">
|
||||||
|
<text class="bt-card__title">选择对比记录</text>
|
||||||
|
<view class="bt-compare-header">
|
||||||
|
<view
|
||||||
|
class="bt-compare-picker"
|
||||||
|
hover-class="mi-tap--hover"
|
||||||
|
:hover-stay-time="150"
|
||||||
|
@tap="pickRecord('a')"
|
||||||
|
>
|
||||||
|
<text class="bt-compare-picker__label">记录 A(较新)</text>
|
||||||
|
<text class="bt-compare-picker__date">{{ recordA?.date || '点击选择' }}</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="bt-compare-picker"
|
||||||
|
hover-class="mi-tap--hover"
|
||||||
|
:hover-stay-time="150"
|
||||||
|
@tap="pickRecord('b')"
|
||||||
|
>
|
||||||
|
<text class="bt-compare-picker__label">记录 B(较旧)</text>
|
||||||
|
<text class="bt-compare-picker__date">{{ recordB?.date || '点击选择' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="compareData" class="bt-card">
|
||||||
|
<text class="bt-card__title">指标对比</text>
|
||||||
|
<view class="bt-compare-row">
|
||||||
|
<text class="bt-compare-row__label">指标</text>
|
||||||
|
<text class="bt-compare-row__val">A</text>
|
||||||
|
<text class="bt-compare-row__val">B</text>
|
||||||
|
<text class="bt-compare-row__diff">差值</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-for="row in compareData.metrics"
|
||||||
|
:key="row.key"
|
||||||
|
class="bt-compare-row"
|
||||||
|
>
|
||||||
|
<text class="bt-compare-row__label">{{ row.label }}</text>
|
||||||
|
<text class="bt-compare-row__val">{{ row.valueA }}</text>
|
||||||
|
<text class="bt-compare-row__val">{{ row.valueB }}</text>
|
||||||
|
<text
|
||||||
|
class="bt-compare-row__diff"
|
||||||
|
:style="{ color: diffColor(row) }"
|
||||||
|
>
|
||||||
|
{{ formatDiff(row.diff, row.key) }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="compareData" class="bt-card">
|
||||||
|
<text class="bt-card__title">评分变化</text>
|
||||||
|
<view class="bt-metrics">
|
||||||
|
<view class="bt-metric">
|
||||||
|
<text class="bt-metric__value">{{ compareData.recordA.score }}</text>
|
||||||
|
<text class="bt-metric__label">记录 A 评分</text>
|
||||||
|
</view>
|
||||||
|
<view class="bt-metric">
|
||||||
|
<text class="bt-metric__value">{{ compareData.recordB.score }}</text>
|
||||||
|
<text class="bt-metric__label">记录 B 评分</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text class="bt-card__desc" style="margin-top: 12px;">
|
||||||
|
综合评分变化 {{ scoreDiff > 0 ? '+' : '' }}{{ scoreDiff }} 分
|
||||||
|
{{ scoreDiff > 0 ? ',整体趋势向好' : scoreDiff < 0 ? ',建议加强训练' : '' }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||||
|
import { PAGE, goBackOrTab } from '@/common/constants/routes.js'
|
||||||
|
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||||
|
import { getBodyTestHistory, getCompareData } from '@/common/memberInfo/bodyTestStore.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { MemberInfoSubNav },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
records: [],
|
||||||
|
recordA: null,
|
||||||
|
recordB: null,
|
||||||
|
compareData: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
scoreDiff() {
|
||||||
|
if (!this.compareData) return 0
|
||||||
|
return this.compareData.recordA.score - this.compareData.recordB.score
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onShow() {
|
||||||
|
const store = loadMemberStore()
|
||||||
|
this.records = getBodyTestHistory(store)
|
||||||
|
if (this.records.length >= 2 && !this.recordA) {
|
||||||
|
this.recordA = this.records[0]
|
||||||
|
this.recordB = this.records[1]
|
||||||
|
this.refreshCompare()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onBack() {
|
||||||
|
goBackOrTab(PAGE.BODY_TEST_HISTORY)
|
||||||
|
},
|
||||||
|
pickRecord(which) {
|
||||||
|
const labels = this.records.map(
|
||||||
|
(r) => `${r.date} · ${r.score}分 · ${r.gradeLabel}`
|
||||||
|
)
|
||||||
|
uni.showActionSheet({
|
||||||
|
itemList: labels,
|
||||||
|
success: (res) => {
|
||||||
|
const picked = this.records[res.tapIndex]
|
||||||
|
if (which === 'a') this.recordA = picked
|
||||||
|
else this.recordB = picked
|
||||||
|
this.refreshCompare()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
refreshCompare() {
|
||||||
|
if (!this.recordA || !this.recordB) {
|
||||||
|
this.compareData = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.recordA.id === this.recordB.id) {
|
||||||
|
uni.showToast({ title: '请选择两条不同记录', icon: 'none' })
|
||||||
|
this.compareData = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const store = loadMemberStore()
|
||||||
|
this.compareData = getCompareData(store, this.recordA.id, this.recordB.id)
|
||||||
|
},
|
||||||
|
formatDiff(diff, key) {
|
||||||
|
const sign = diff > 0 ? '+' : ''
|
||||||
|
const units = { bodyFat: '%', weight: '', bmi: '', muscleMass: '', visceralFat: '', bmr: '' }
|
||||||
|
return `${sign}${diff}${units[key] || ''}`
|
||||||
|
},
|
||||||
|
diffColor(row) {
|
||||||
|
const lowerBetter = ['weight', 'bodyFat', 'visceralFat'].includes(row.key)
|
||||||
|
const good = lowerBetter ? row.diff < 0 : row.diff > 0
|
||||||
|
if (row.diff === 0) return '#8A99B4'
|
||||||
|
return good ? '#2ECC71' : '#F39C12'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@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';
|
||||||
|
@import '@/common/style/memberInfo/pages/body-test-common.css';
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
<template>
|
||||||
|
<view class="scroll-container theme-light">
|
||||||
|
<view class="bt-page">
|
||||||
|
<MemberInfoSubNav title="连接设备" @back="onBack" />
|
||||||
|
<view class="bt-page__body">
|
||||||
|
<view class="bt-card">
|
||||||
|
<text class="bt-card__title">{{ device.name }}</text>
|
||||||
|
<text class="bt-card__desc">型号 {{ device.model }} · 请按以下步骤完成蓝牙配对</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="bt-card">
|
||||||
|
<text class="bt-card__title">连接引导</text>
|
||||||
|
<view class="bt-steps">
|
||||||
|
<view
|
||||||
|
v-for="step in steps"
|
||||||
|
:key="step.step"
|
||||||
|
class="bt-step"
|
||||||
|
>
|
||||||
|
<view class="bt-step__num">{{ step.step }}</view>
|
||||||
|
<view class="bt-step__content">
|
||||||
|
<text class="bt-step__title">{{ step.title }}</text>
|
||||||
|
<text class="bt-step__desc">{{ step.desc }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="searching" class="bt-card">
|
||||||
|
<view class="bt-measure">
|
||||||
|
<text class="bt-measure__hint">正在搜索附近设备…</text>
|
||||||
|
<text class="bt-measure__hint">{{ searchHint }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="connected" class="bt-card">
|
||||||
|
<view class="bt-device">
|
||||||
|
<view class="bt-device__icon-wrap">
|
||||||
|
<image class="bt-device__icon" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/shield.png" mode="aspectFit" />
|
||||||
|
</view>
|
||||||
|
<view class="bt-device__info">
|
||||||
|
<text class="bt-device__name">连接成功</text>
|
||||||
|
<text class="bt-device__status bt-device__status--on">
|
||||||
|
{{ device.name }} · 电量 {{ device.battery }}%
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view class="bt-device__dot bt-device__dot--on"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="bt-footer-actions">
|
||||||
|
<view
|
||||||
|
v-if="!connected"
|
||||||
|
class="bt-btn bt-btn--primary"
|
||||||
|
:class="{ 'bt-btn--outline': searching }"
|
||||||
|
hover-class="mi-tap-btn--hover"
|
||||||
|
:hover-stay-time="150"
|
||||||
|
@tap="searchDevice"
|
||||||
|
>
|
||||||
|
<text class="bt-btn__text">{{ searching ? '搜索中…' : '搜索并连接' }}</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-else
|
||||||
|
class="bt-btn bt-btn--primary"
|
||||||
|
hover-class="mi-tap-btn--hover"
|
||||||
|
:hover-stay-time="150"
|
||||||
|
@tap="goMeasuring"
|
||||||
|
>
|
||||||
|
<text class="bt-btn__text">开始测量</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||||
|
import { PAGE, navigateToPage, goBackOrTab } from '@/common/constants/routes.js'
|
||||||
|
import {
|
||||||
|
loadMemberStore,
|
||||||
|
persistMemberStore
|
||||||
|
} from '@/common/memberInfo/store.js'
|
||||||
|
import {
|
||||||
|
connectBodyTestDevice,
|
||||||
|
bodyTestMock
|
||||||
|
} from '@/common/memberInfo/bodyTestStore.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { MemberInfoSubNav },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
device: {},
|
||||||
|
steps: bodyTestMock.connectSteps,
|
||||||
|
searching: false,
|
||||||
|
connected: false,
|
||||||
|
searchHint: '请保持手机蓝牙已开启'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onShow() {
|
||||||
|
const store = loadMemberStore()
|
||||||
|
this.device = { ...store.bodyTest.device }
|
||||||
|
this.connected = store.bodyTest.device.connected
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onBack() {
|
||||||
|
goBackOrTab(PAGE.BODY_TEST_HOME)
|
||||||
|
},
|
||||||
|
searchDevice() {
|
||||||
|
if (this.searching) return
|
||||||
|
this.searching = true
|
||||||
|
this.searchHint = '发现 InBody 270…'
|
||||||
|
setTimeout(() => {
|
||||||
|
const store = loadMemberStore()
|
||||||
|
connectBodyTestDevice(store)
|
||||||
|
persistMemberStore(store)
|
||||||
|
this.device = { ...store.bodyTest.device }
|
||||||
|
this.connected = true
|
||||||
|
this.searching = false
|
||||||
|
uni.showToast({ title: '设备已连接', icon: 'success' })
|
||||||
|
}, 1800)
|
||||||
|
},
|
||||||
|
goMeasuring() {
|
||||||
|
navigateToPage(PAGE.BODY_TEST_MEASURING)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@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';
|
||||||
|
@import '@/common/style/memberInfo/pages/body-test-common.css';
|
||||||
|
|
||||||
|
.bt-footer-actions .bt-btn--primary.bt-btn--outline {
|
||||||
|
background: var(--bg-gray, #F2F5F9);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-footer-actions .bt-btn--primary.bt-btn--outline .bt-btn__text {
|
||||||
|
color: var(--text-muted, #5E6F8D);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
<template>
|
||||||
|
<view class="scroll-container theme-light">
|
||||||
|
<view class="bt-page">
|
||||||
|
<MemberInfoSubNav title="体测报告" @back="onBack" />
|
||||||
|
<view class="mi-mod-tabs">
|
||||||
|
<view
|
||||||
|
v-for="y in years"
|
||||||
|
:key="y"
|
||||||
|
class="bt-tab"
|
||||||
|
:class="{ 'bt-tab--active': activeYear === y }"
|
||||||
|
hover-class="mi-tap-tab--hover"
|
||||||
|
:hover-stay-time="150"
|
||||||
|
@tap="activeYear = y"
|
||||||
|
>
|
||||||
|
<text class="bt-tab__text">{{ y === 'all' ? '全部' : y + '年' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<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="goCompare"
|
||||||
|
>
|
||||||
|
历史对比
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view class="bt-page__body">
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in records"
|
||||||
|
:key="item.id"
|
||||||
|
class="mi-timeline-card"
|
||||||
|
hover-class="mi-tap-row--hover"
|
||||||
|
@tap="viewReport(item)"
|
||||||
|
>
|
||||||
|
<view class="mi-timeline-card__line">
|
||||||
|
<view class="mi-timeline-card__dot"></view>
|
||||||
|
<view v-if="index < records.length - 1" class="mi-timeline-card__bar"></view>
|
||||||
|
</view>
|
||||||
|
<view class="mi-timeline-card__content">
|
||||||
|
<view class="mi-timeline-card__head">
|
||||||
|
<text class="mi-timeline-card__date">{{ item.date }} {{ item.time }}</text>
|
||||||
|
<text class="mi-timeline-card__score">{{ item.score }}分</text>
|
||||||
|
</view>
|
||||||
|
<text class="mi-timeline-card__grade">{{ item.grade }} {{ item.gradeLabel }} · {{ item.status }}</text>
|
||||||
|
<text class="mi-timeline-card__metrics">
|
||||||
|
体脂 {{ item.metrics.bodyFat }}% · 肌肉 {{ item.metrics.muscleMass }}kg · BMI {{ item.metrics.bmi }}
|
||||||
|
</text>
|
||||||
|
<view v-if="item.changeBadge" class="mi-timeline-card__badge" :class="item.changeBadge.good ? 'mi-timeline-card__badge--good' : 'mi-timeline-card__badge--warn'">
|
||||||
|
<text>{{ item.changeBadge.text }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="!records.length" class="bt-empty">
|
||||||
|
<text class="bt-empty__text">暂无体测报告</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||||
|
import { PAGE, navigateToPage, goBackOrTab } from '@/common/constants/routes.js'
|
||||||
|
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||||
|
import { getBodyTestHistory, getBodyTestYears, getBodyTestChangeBadge } from '@/common/memberInfo/bodyTestStore.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { MemberInfoSubNav },
|
||||||
|
data() {
|
||||||
|
return { activeYear: 'all', years: [], allRecords: [] }
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
records() {
|
||||||
|
const list = this.activeYear === 'all'
|
||||||
|
? this.allRecords
|
||||||
|
: this.allRecords.filter((r) => r.date.startsWith(this.activeYear))
|
||||||
|
return list.map((item, index) => {
|
||||||
|
const previous = list[index + 1]
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
changeBadge: getBodyTestChangeBadge(item, previous)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
activeYear() { this.loadList() }
|
||||||
|
},
|
||||||
|
onShow() { this.loadList() },
|
||||||
|
methods: {
|
||||||
|
loadList() {
|
||||||
|
const store = loadMemberStore()
|
||||||
|
this.years = getBodyTestYears(store)
|
||||||
|
this.allRecords = getBodyTestHistory(store)
|
||||||
|
},
|
||||||
|
onBack() { goBackOrTab(PAGE.MEMBER) },
|
||||||
|
viewReport(item) {
|
||||||
|
navigateToPage(`${PAGE.BODY_TEST_REPORT}?id=${item.id}`)
|
||||||
|
},
|
||||||
|
goCompare() {
|
||||||
|
navigateToPage(PAGE.BODY_TEST_COMPARE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@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';
|
||||||
|
@import '@/common/style/memberInfo/pages/body-test-common.css';
|
||||||
|
|
||||||
|
.mi-timeline-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-timeline-card__line {
|
||||||
|
width: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-timeline-card__dot {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent-orange, #FF6B35);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-timeline-card__bar {
|
||||||
|
flex: 1;
|
||||||
|
width: 2px;
|
||||||
|
background: var(--border-light, #E9EDF2);
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-timeline-card__content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px 14px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: var(--bg-white, #fff);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-timeline-card__head {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-timeline-card__date {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-dark, #1E2A3A);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-timeline-card__score {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--primary-dark, #0B2B4B);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-timeline-card__grade {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted, #5E6F8D);
|
||||||
|
margin-top: 4px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-timeline-card__metrics {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-light, #8A99B4);
|
||||||
|
margin-top: 4px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-timeline-card__badge {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-timeline-card__badge--good {
|
||||||
|
background: rgba(46, 204, 113, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi-timeline-card__badge--good text {
|
||||||
|
font-size: 10px;
|
||||||
|
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>
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
<template>
|
||||||
|
<view class="scroll-container theme-light">
|
||||||
|
<view class="bt-page">
|
||||||
|
<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-hero">
|
||||||
|
<view class="bt-hero__top">
|
||||||
|
<text class="bt-hero__label">最新体测评分</text>
|
||||||
|
<view class="bt-hero__badge">
|
||||||
|
<text class="bt-hero__badge-text">{{ latest?.status || '暂无数据' }}</text>
|
||||||
|
</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-btn bt-btn--primary"
|
||||||
|
hover-class="mi-tap-btn--hover"
|
||||||
|
: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"
|
||||||
|
>
|
||||||
|
<text class="bt-btn__text">查看报告</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="bt-card">
|
||||||
|
<text class="bt-card__title">设备状态</text>
|
||||||
|
<view class="bt-device">
|
||||||
|
<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
|
||||||
|
class="bt-device__dot"
|
||||||
|
:class="{ 'bt-device__dot--on': device.connected }"
|
||||||
|
></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="bt-card">
|
||||||
|
<text class="bt-card__title">快捷入口</text>
|
||||||
|
<view class="bt-grid">
|
||||||
|
<view
|
||||||
|
v-for="item in quickLinks"
|
||||||
|
:key="item.key"
|
||||||
|
class="bt-grid__item"
|
||||||
|
hover-class="mi-tap--hover"
|
||||||
|
:hover-stay-time="150"
|
||||||
|
@tap="onQuickLink(item.key)"
|
||||||
|
>
|
||||||
|
<image class="bt-grid__icon" :src="item.icon" mode="aspectFit" />
|
||||||
|
<text class="bt-grid__label">{{ item.label }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="latest" class="bt-card">
|
||||||
|
<text class="bt-card__title">核心指标概览</text>
|
||||||
|
<view class="bt-metrics">
|
||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||||
|
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
|
||||||
|
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||||
|
import { getLatestBodyTestRecord } from '@/common/memberInfo/bodyTestStore.js'
|
||||||
|
import { subPageMixin } from '@/common/memberInfo/mixins.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { MemberInfoSubNav },
|
||||||
|
mixins: [subPageMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
latest: null,
|
||||||
|
device: {},
|
||||||
|
quickLinks: [
|
||||||
|
{ 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: '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' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
deviceStatusText() {
|
||||||
|
if (this.device.connected) {
|
||||||
|
return `已连接 · 电量 ${this.device.battery}%`
|
||||||
|
}
|
||||||
|
return '未连接 · 点击开始体测进行配对'
|
||||||
|
},
|
||||||
|
previewMetrics() {
|
||||||
|
if (!this.latest?.metrics) return []
|
||||||
|
const m = this.latest.metrics
|
||||||
|
return [
|
||||||
|
{ key: 'weight', label: '体重(kg)', value: m.weight },
|
||||||
|
{ key: 'bmi', label: 'BMI', value: m.bmi },
|
||||||
|
{ key: 'bodyFat', label: '体脂率(%)', value: m.bodyFat },
|
||||||
|
{ key: 'muscleMass', label: '肌肉量(kg)', value: m.muscleMass }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onShow() {
|
||||||
|
this.refreshFromStore()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
refreshFromStore() {
|
||||||
|
const store = loadMemberStore()
|
||||||
|
this.latest = getLatestBodyTestRecord(store)
|
||||||
|
this.device = { ...store.bodyTest.device }
|
||||||
|
},
|
||||||
|
startMeasure() {
|
||||||
|
const store = loadMemberStore()
|
||||||
|
if (store.bodyTest.device.connected) {
|
||||||
|
navigateToPage(PAGE.BODY_TEST_MEASURING)
|
||||||
|
} else {
|
||||||
|
navigateToPage(PAGE.BODY_TEST_CONNECT)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
viewLatestReport() {
|
||||||
|
if (!this.latest) return
|
||||||
|
navigateToPage(`${PAGE.BODY_TEST_REPORT}?id=${this.latest.id}`)
|
||||||
|
},
|
||||||
|
goSettings() {
|
||||||
|
navigateToPage(PAGE.BODY_TEST_SETTINGS)
|
||||||
|
},
|
||||||
|
onQuickLink(key) {
|
||||||
|
const routes = {
|
||||||
|
history: PAGE.BODY_TEST_HISTORY,
|
||||||
|
compare: PAGE.BODY_TEST_COMPARE,
|
||||||
|
trend: PAGE.BODY_TEST_TREND,
|
||||||
|
report: this.latest
|
||||||
|
? `${PAGE.BODY_TEST_REPORT}?id=${this.latest.id}`
|
||||||
|
: PAGE.BODY_TEST_HISTORY
|
||||||
|
}
|
||||||
|
navigateToPage(routes[key] || PAGE.BODY_TEST_HOME)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@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';
|
||||||
|
@import '@/common/style/memberInfo/pages/body-test-common.css';
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
<template>
|
||||||
|
<view class="scroll-container theme-light">
|
||||||
|
<view class="bt-page">
|
||||||
|
<MemberInfoSubNav title="测量中" @back="onCancel" />
|
||||||
|
<view class="bt-page__body">
|
||||||
|
<view class="bt-card">
|
||||||
|
<view class="bt-measure">
|
||||||
|
<view class="bt-measure__ring-wrap">
|
||||||
|
<view class="bt-measure__ring-bg"></view>
|
||||||
|
<view
|
||||||
|
class="bt-measure__ring-fill"
|
||||||
|
:style="{ transform: `rotate(${ringRotation}deg)` }"
|
||||||
|
></view>
|
||||||
|
<view class="bt-measure__center">
|
||||||
|
<text class="bt-measure__percent">{{ progress }}%</text>
|
||||||
|
<text class="bt-measure__hint">{{ phaseHint }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text class="bt-card__desc">{{ statusText }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="bt-card">
|
||||||
|
<text class="bt-card__title">实时数据</text>
|
||||||
|
<view class="bt-measure__live">
|
||||||
|
<view
|
||||||
|
v-for="item in liveDisplay"
|
||||||
|
:key="item.key"
|
||||||
|
class="bt-measure__live-item"
|
||||||
|
>
|
||||||
|
<text class="bt-measure__live-value">{{ item.value }}</text>
|
||||||
|
<text class="bt-measure__live-label">{{ item.label }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||||
|
import { PAGE, navigateToPage, goBackOrTab } from '@/common/constants/routes.js'
|
||||||
|
import {
|
||||||
|
loadMemberStore,
|
||||||
|
persistMemberStore
|
||||||
|
} from '@/common/memberInfo/store.js'
|
||||||
|
import {
|
||||||
|
interpolateMeasuringMetrics,
|
||||||
|
saveSimulatedBodyTestRecord
|
||||||
|
} from '@/common/memberInfo/bodyTestStore.js'
|
||||||
|
|
||||||
|
const PHASES = [
|
||||||
|
{ until: 20, hint: '校准中', text: '请保持站立姿势,双手自然下垂' },
|
||||||
|
{ until: 50, hint: '阻抗测量', text: '请勿移动,正在进行生物电阻抗分析' },
|
||||||
|
{ until: 80, hint: '数据分析', text: '正在计算体脂与肌肉分布' },
|
||||||
|
{ until: 100, hint: '即将完成', text: '生成健康报告中…' }
|
||||||
|
]
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { MemberInfoSubNav },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
progress: 0,
|
||||||
|
liveMetrics: {},
|
||||||
|
timer: null,
|
||||||
|
finished: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
ringRotation() {
|
||||||
|
return -90 + (this.progress / 100) * 360
|
||||||
|
},
|
||||||
|
phaseHint() {
|
||||||
|
const phase = PHASES.find((p) => this.progress <= p.until)
|
||||||
|
return phase?.hint || '完成'
|
||||||
|
},
|
||||||
|
statusText() {
|
||||||
|
const phase = PHASES.find((p) => this.progress <= p.until)
|
||||||
|
return phase?.text || '测量完成'
|
||||||
|
},
|
||||||
|
liveDisplay() {
|
||||||
|
const m = this.liveMetrics
|
||||||
|
return [
|
||||||
|
{ key: 'weight', label: '体重(kg)', value: m.weight ?? '--' },
|
||||||
|
{ key: 'bodyFat', label: '体脂率(%)', value: m.bodyFat ?? '--' },
|
||||||
|
{ key: 'muscleMass', label: '肌肉量(kg)', value: m.muscleMass ?? '--' },
|
||||||
|
{ key: 'bmr', label: '基础代谢', value: m.bmr ?? '--' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad() {
|
||||||
|
const store = loadMemberStore()
|
||||||
|
if (!store.bodyTest.device.connected) {
|
||||||
|
uni.showToast({ title: '请先连接设备', icon: 'none' })
|
||||||
|
setTimeout(() => {
|
||||||
|
navigateToPage(PAGE.BODY_TEST_CONNECT)
|
||||||
|
}, 800)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.startMeasurement()
|
||||||
|
},
|
||||||
|
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()
|
||||||
|
this.liveMetrics = interpolateMeasuringMetrics(this.progress, s.profile)
|
||||||
|
}, 120)
|
||||||
|
},
|
||||||
|
completeMeasurement() {
|
||||||
|
if (this.finished) return
|
||||||
|
this.finished = true
|
||||||
|
this.clearTimer()
|
||||||
|
const store = loadMemberStore()
|
||||||
|
const record = saveSimulatedBodyTestRecord(store, {
|
||||||
|
...this.liveMetrics,
|
||||||
|
visceralFat: 6,
|
||||||
|
boneMass: 2.42,
|
||||||
|
bodyWater: this.liveMetrics.bodyWater || 52.8,
|
||||||
|
protein: 16.4
|
||||||
|
})
|
||||||
|
persistMemberStore(store)
|
||||||
|
uni.showToast({ title: '测量完成', icon: 'success' })
|
||||||
|
setTimeout(() => {
|
||||||
|
navigateToPage(`${PAGE.BODY_TEST_REPORT}?id=${record.id}&new=1`)
|
||||||
|
}, 600)
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
if (this.finished) return
|
||||||
|
uni.showModal({
|
||||||
|
title: '取消测量',
|
||||||
|
content: '确定要中断当前体测吗?',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
this.clearTimer()
|
||||||
|
goBackOrTab(PAGE.BODY_TEST_HOME)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@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';
|
||||||
|
@import '@/common/style/memberInfo/pages/body-test-common.css';
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,315 @@
|
|||||||
|
<template>
|
||||||
|
<view class="scroll-container theme-light">
|
||||||
|
<view class="bt-page">
|
||||||
|
<MemberInfoSubNav title="体测报告" @back="onBack" />
|
||||||
|
<view v-if="record" class="bt-page__body">
|
||||||
|
<view class="bt-score-card">
|
||||||
|
<view class="bt-score-card__circle">
|
||||||
|
<text class="bt-score-card__num">{{ record.score }}</text>
|
||||||
|
<text class="bt-score-card__grade">{{ record.grade }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="bt-score-card__info">
|
||||||
|
<text class="bt-score-card__title">{{ record.gradeLabel }} · {{ record.status }}</text>
|
||||||
|
<text class="bt-score-card__date">{{ record.date }} {{ record.time }}</text>
|
||||||
|
<text v-if="record.bodyAge" class="bt-score-card__date">
|
||||||
|
身体年龄 {{ record.bodyAge }} 岁 · 实际 {{ record.realAge }} 岁
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="bt-card">
|
||||||
|
<text class="bt-card__title">核心指标</text>
|
||||||
|
<view class="bt-metrics">
|
||||||
|
<view
|
||||||
|
v-for="m in metricCards"
|
||||||
|
:key="m.key"
|
||||||
|
class="bt-metric"
|
||||||
|
>
|
||||||
|
<text class="bt-metric__value">{{ m.display }}</text>
|
||||||
|
<text class="bt-metric__label">{{ m.label }}</text>
|
||||||
|
<text
|
||||||
|
v-if="m.changeText"
|
||||||
|
class="bt-metric__change"
|
||||||
|
:class="m.changeClass"
|
||||||
|
>
|
||||||
|
较上次 {{ m.changeText }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="bt-card">
|
||||||
|
<text class="bt-card__title">体成分雷达图</text>
|
||||||
|
<BodyTestRadarChart
|
||||||
|
:labels="radarLabels"
|
||||||
|
:values="radarValues"
|
||||||
|
:width="chartWidth"
|
||||||
|
:height="220"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="bt-card">
|
||||||
|
<text class="bt-card__title">人体成分分布</text>
|
||||||
|
<view class="bt-body-map">
|
||||||
|
<view class="bt-body-map__figure">
|
||||||
|
<view class="bt-body-map__head"></view>
|
||||||
|
<view class="bt-body-map__limbs">
|
||||||
|
<view class="bt-body-map__arm"></view>
|
||||||
|
<view class="bt-body-map__arm"></view>
|
||||||
|
</view>
|
||||||
|
<view class="bt-body-map__torso"></view>
|
||||||
|
<view class="bt-body-map__legs">
|
||||||
|
<view class="bt-body-map__leg"></view>
|
||||||
|
<view class="bt-body-map__leg"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="bt-body-map__segments">
|
||||||
|
<view
|
||||||
|
v-for="seg in record.bodySegments"
|
||||||
|
:key="seg.part"
|
||||||
|
class="bt-body-map__seg"
|
||||||
|
:class="'bt-body-map__seg--' + seg.level"
|
||||||
|
>
|
||||||
|
<text class="bt-body-map__seg-name">{{ seg.part }}</text>
|
||||||
|
<text class="bt-body-map__seg-val">{{ seg.value }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="bt-card">
|
||||||
|
<text class="bt-card__title">指标变化趋势</text>
|
||||||
|
<BodyTestTrendChart
|
||||||
|
:points="trendPreview"
|
||||||
|
unit="kg"
|
||||||
|
:width="chartWidth"
|
||||||
|
:height="140"
|
||||||
|
/>
|
||||||
|
<view
|
||||||
|
class="bt-trend-link"
|
||||||
|
hover-class="mi-tap--hover"
|
||||||
|
:hover-stay-time="150"
|
||||||
|
@tap="goTrend"
|
||||||
|
>
|
||||||
|
<text class="bt-trend-link__text">查看完整趋势分析</text>
|
||||||
|
<image
|
||||||
|
class="bt-trend-link__arrow"
|
||||||
|
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/chevronright3.png"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="bt-card">
|
||||||
|
<text class="bt-card__title">健康建议</text>
|
||||||
|
<view class="bt-advice-list">
|
||||||
|
<view
|
||||||
|
v-for="(tip, idx) in record.advice"
|
||||||
|
:key="idx"
|
||||||
|
class="bt-advice-item"
|
||||||
|
>
|
||||||
|
<view class="bt-advice-item__dot"></view>
|
||||||
|
<text class="bt-advice-item__text">{{ tip }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="courses.length" class="bt-card">
|
||||||
|
<text class="bt-card__title">推荐课程</text>
|
||||||
|
<view
|
||||||
|
v-for="course in courses"
|
||||||
|
:key="course.id"
|
||||||
|
class="bt-course"
|
||||||
|
hover-class="mi-tap-row--hover"
|
||||||
|
:hover-stay-time="150"
|
||||||
|
@tap="onCourseTap(course)"
|
||||||
|
>
|
||||||
|
<image class="bt-course__banner" :src="course.banner" mode="aspectFill" />
|
||||||
|
<view class="bt-course__info">
|
||||||
|
<text class="bt-course__tag">{{ course.tag }}</text>
|
||||||
|
<text class="bt-course__title">{{ course.title }}</text>
|
||||||
|
<text class="bt-course__meta">{{ course.coach }} · {{ course.schedule }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="bt-footer-actions">
|
||||||
|
<view
|
||||||
|
class="bt-btn bt-btn--outline"
|
||||||
|
hover-class="mi-tap-btn--hover"
|
||||||
|
:hover-stay-time="150"
|
||||||
|
@tap="exportReport"
|
||||||
|
>
|
||||||
|
<image class="bt-btn__icon" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/filetext.png" mode="aspectFit" />
|
||||||
|
<text class="bt-btn__text">导出 PDF</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="bt-btn bt-btn--outline"
|
||||||
|
hover-class="mi-tap-btn--hover"
|
||||||
|
:hover-stay-time="150"
|
||||||
|
@tap="shareReport"
|
||||||
|
>
|
||||||
|
<image class="bt-btn__icon" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/share2.png" mode="aspectFit" />
|
||||||
|
<text class="bt-btn__text">分享</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="bt-btn bt-btn--primary"
|
||||||
|
hover-class="mi-tap-btn--hover"
|
||||||
|
:hover-stay-time="150"
|
||||||
|
@tap="retest"
|
||||||
|
>
|
||||||
|
<text class="bt-btn__text">再次体测</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="bt-empty">
|
||||||
|
<text class="bt-empty__text">未找到体测报告</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||||
|
import BodyTestRadarChart from '@/components/memberInfo/BodyTestRadarChart.vue'
|
||||||
|
import BodyTestTrendChart from '@/components/memberInfo/BodyTestTrendChart.vue'
|
||||||
|
import { PAGE, navigateToPage, goBackOrTab } from '@/common/constants/routes.js'
|
||||||
|
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||||
|
import {
|
||||||
|
getBodyTestRecordById,
|
||||||
|
getBodyTestTrendData,
|
||||||
|
getRecommendedCourses,
|
||||||
|
computeChanges,
|
||||||
|
formatChangeValue,
|
||||||
|
bodyTestMock
|
||||||
|
} from '@/common/memberInfo/bodyTestStore.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { MemberInfoSubNav, BodyTestRadarChart, BodyTestTrendChart },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
recordId: null,
|
||||||
|
record: null,
|
||||||
|
previous: null,
|
||||||
|
chartWidth: 300
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
radarLabels() {
|
||||||
|
return bodyTestMock.radarLabels.map((l) => l.label)
|
||||||
|
},
|
||||||
|
radarValues() {
|
||||||
|
if (!this.record?.radar) return []
|
||||||
|
const keys = bodyTestMock.radarLabels.map((l) => l.key)
|
||||||
|
return keys.map((k) => this.record.radar[k] || 0)
|
||||||
|
},
|
||||||
|
metricCards() {
|
||||||
|
if (!this.record?.metrics) return []
|
||||||
|
const changes = this.changes
|
||||||
|
const defs = bodyTestMock.metricDefs.slice(0, 8)
|
||||||
|
return defs.map((def) => {
|
||||||
|
const val = this.record.metrics[def.key]
|
||||||
|
const diff = changes[def.key]
|
||||||
|
let changeText = ''
|
||||||
|
let changeClass = ''
|
||||||
|
if (diff !== undefined && diff !== null) {
|
||||||
|
changeText = formatChangeValue(def.key, diff)
|
||||||
|
const lowerBetter = ['weight', 'bodyFat', 'visceralFat'].includes(def.key)
|
||||||
|
const isGood = lowerBetter ? diff < 0 : diff > 0
|
||||||
|
changeClass = isGood ? 'bt-metric__change--down' : 'bt-metric__change--up'
|
||||||
|
}
|
||||||
|
const display = def.unit ? `${val}${def.unit === '%' ? '%' : def.unit === 'kg' ? '' : ` ${def.unit}`}` : val
|
||||||
|
return {
|
||||||
|
key: def.key,
|
||||||
|
label: def.label + (def.unit && def.unit !== '%' && def.unit !== 'kg' ? `(${def.unit})` : def.unit === 'kg' ? '(kg)' : def.unit === '%' ? '(%)' : ''),
|
||||||
|
display: def.key === 'bodyFat' ? `${val}%` : def.key === 'bodyWater' ? `${val}%` : def.unit === 'kg' ? val : def.unit ? `${val}` : val,
|
||||||
|
changeText,
|
||||||
|
changeClass
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
changes() {
|
||||||
|
if (this.record?.changes) return this.record.changes
|
||||||
|
if (this.previous) return computeChanges(this.record, this.previous)
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
trendPreview() {
|
||||||
|
const store = loadMemberStore()
|
||||||
|
return getBodyTestTrendData(store, 'weight', 4)
|
||||||
|
},
|
||||||
|
courses() {
|
||||||
|
return getRecommendedCourses(this.record)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad(options) {
|
||||||
|
this.recordId = options?.id ? Number(options.id) : null
|
||||||
|
this.chartWidth = uni.getSystemInfoSync().windowWidth - 64
|
||||||
|
this.loadRecord()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
loadRecord() {
|
||||||
|
const store = loadMemberStore()
|
||||||
|
const records = store.bodyTest.records
|
||||||
|
if (this.recordId) {
|
||||||
|
this.record = getBodyTestRecordById(store, this.recordId)
|
||||||
|
} else {
|
||||||
|
this.record = records.length ? { ...records[0] } : null
|
||||||
|
}
|
||||||
|
if (this.record) {
|
||||||
|
const idx = records.findIndex((r) => r.id === this.record.id)
|
||||||
|
this.previous = idx >= 0 && records[idx + 1] ? records[idx + 1] : null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onBack() {
|
||||||
|
goBackOrTab(PAGE.BODY_TEST_HOME)
|
||||||
|
},
|
||||||
|
goTrend() {
|
||||||
|
navigateToPage(`${PAGE.BODY_TEST_TREND}?metric=weight`)
|
||||||
|
},
|
||||||
|
exportReport() {
|
||||||
|
uni.showLoading({ title: '生成中…' })
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.showToast({ title: '报告已保存到相册', icon: 'success' })
|
||||||
|
}, 1200)
|
||||||
|
},
|
||||||
|
shareReport() {
|
||||||
|
uni.showActionSheet({
|
||||||
|
itemList: ['分享给微信好友', '生成分享海报', '复制报告链接'],
|
||||||
|
success: (res) => {
|
||||||
|
const msgs = ['已唤起微信分享', '海报已生成', '链接已复制']
|
||||||
|
uni.showToast({ title: msgs[res.tapIndex] || '分享成功', icon: 'none' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onCourseTap(course) {
|
||||||
|
uni.showModal({
|
||||||
|
title: course.title,
|
||||||
|
content: `${course.coach}\n${course.schedule}\n\n是否前往预约?`,
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
navigateToPage(PAGE.COURSE_LIST)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
retest() {
|
||||||
|
navigateToPage(PAGE.BODY_TEST_HOME)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@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';
|
||||||
|
@import '@/common/style/memberInfo/pages/body-test-common.css';
|
||||||
|
|
||||||
|
.bt-footer-actions .bt-btn {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user