会员个人中心页面初步完成
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
/** 与 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/memberInfo/courseList',
|
||||
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)
|
||||
}
|
||||
|
||||
export function navigateToPage(url) {
|
||||
const path = normalizePath(url)
|
||||
if (TAB_PAGES.has(path)) {
|
||||
switchToTab(path)
|
||||
return
|
||||
}
|
||||
uni.navigateTo({
|
||||
url,
|
||||
fail: (err) => {
|
||||
console.error('[navigateTo]', url, err)
|
||||
uni.showToast({ title: '页面跳转失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function switchToTab(url) {
|
||||
const path = normalizePath(url)
|
||||
if (getCurrentRoutePath() === path || tabNavigating) return
|
||||
|
||||
tabNavigating = true
|
||||
const done = () => {
|
||||
setTimeout(() => {
|
||||
tabNavigating = false
|
||||
}, 320)
|
||||
}
|
||||
|
||||
uni.switchTab({
|
||||
url: path,
|
||||
complete: done,
|
||||
fail: (err) => {
|
||||
console.warn('[switchTab]', path, err)
|
||||
uni.showToast({ title: '页面跳转失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function goToMemberCenter() {
|
||||
switchToTab(PAGE.MEMBER)
|
||||
}
|
||||
|
||||
export function goBackOrTab(fallbackUrl = PAGE.MEMBER) {
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
fail: () => switchToTab(fallbackUrl)
|
||||
})
|
||||
}
|
||||
|
||||
/** 子页面返回:统一回到 tab 页「个人中心」 */
|
||||
export function backToMemberCenter() {
|
||||
switchToTab(PAGE.MEMBER)
|
||||
}
|
||||
@@ -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: '/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: '/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: '/static/images/dumbbell.png',
|
||||
iconTheme: 'orange'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'checkin',
|
||||
title: '自由进馆',
|
||||
time: '2024-07-11 18:30',
|
||||
value: '-1天',
|
||||
valueType: 'negative',
|
||||
icon: '/static/images/mappin.png',
|
||||
iconTheme: 'green'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 'consume',
|
||||
title: '会员卡充值',
|
||||
time: '2024-07-01 10:00',
|
||||
value: '+90天',
|
||||
valueType: 'positive',
|
||||
icon: '/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: '/static/images/target.png' },
|
||||
{ key: 'bmi', label: 'BMI', unit: '', icon: '/static/images/activity.png' },
|
||||
{ key: 'bodyFat', label: '体脂率', unit: '%', icon: '/static/images/trendingdown.png' },
|
||||
{ key: 'muscleMass', label: '肌肉量', unit: 'kg', icon: '/static/images/dumbbell.png' },
|
||||
{ key: 'visceralFat', label: '内脏脂肪', unit: '级', icon: '/static/images/alertcircle.png' },
|
||||
{ key: 'bmr', label: '基础代谢', unit: 'kcal', icon: '/static/images/clock.png' },
|
||||
{ key: 'bodyWater', label: '体水分', unit: '%', icon: '/static/images/shield.png' },
|
||||
{ key: 'boneMass', label: '骨量', unit: 'kg', icon: '/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: '/static/images/AC1Banner.png',
|
||||
tag: '减脂推荐'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '核心力量塑形',
|
||||
coach: '王强教练',
|
||||
schedule: '每周一、三 18:30',
|
||||
banner: '/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: '/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: '/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: '/static/images/AC1Banner.png',
|
||||
status: 'completed',
|
||||
statusLabel: '已完成',
|
||||
schedule: '07月10日 19:00-20:00',
|
||||
coach: '赵敏教练',
|
||||
footerText: '已签到',
|
||||
canCancel: false
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '普拉提进阶',
|
||||
banner: '/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: '/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: '/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: '/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: '/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: '/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: '/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: '/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: '/static/images/AC2Banner.png',
|
||||
intro: '核心稳定与体态矫正进阶课程。',
|
||||
suitable: '有普拉提基础者',
|
||||
coachBio: '国家一级瑜伽指导员',
|
||||
coachRating: 4.9,
|
||||
reviews: [],
|
||||
cancelRule: '至少提前 2 小时取消'
|
||||
},
|
||||
{
|
||||
id: 105,
|
||||
title: '动感单车',
|
||||
type: 'group',
|
||||
coach: '赵敏教练',
|
||||
coachAvatar: '/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: '/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: '/static/images/ticket.png' },
|
||||
{ id: 2, name: '运动毛巾', cost: 800, stock: 5, icon: '/static/images/dumbbell.png' },
|
||||
{ id: 3, name: '私教体验30分钟', cost: 2000, stock: 3, icon: '/static/images/usercheck.png' },
|
||||
{ id: 4, name: '蛋白粉小样', cost: 350, stock: 20, icon: '/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: '/static/images/user0.png', status: 'purchased', statusLabel: '已购课', time: '2024-07-05', reward: '+300积分', rewardStatus: '已发放' },
|
||||
{ id: 2, name: '王**', avatar: '/static/images/user1.png', status: 'registered', statusLabel: '已注册', time: '2024-06-20', reward: '+100积分', rewardStatus: '已发放' },
|
||||
{ id: 3, name: '陈**', avatar: '/static/images/user2.png', status: 'invited', statusLabel: '已邀请', time: '2024-06-15', reward: '待注册', rewardStatus: '待发放' },
|
||||
{ id: 4, name: '赵**', avatar: '/static/images/user3.png', status: 'purchased', statusLabel: '已购课', time: '2024-06-01', reward: '+300积分', rewardStatus: '已发放' },
|
||||
{ id: 5, name: '刘**', avatar: '/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: '/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: '/static/images/AC1Banner.png',
|
||||
progress: 8,
|
||||
total: 8,
|
||||
schedule: '已结课',
|
||||
location: '单车房',
|
||||
completedAt: '2024-06-30',
|
||||
canEvaluate: true
|
||||
}
|
||||
]
|
||||
},
|
||||
private: {
|
||||
remaining: 7,
|
||||
coach: '王强教练',
|
||||
coachAvatar: '/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: '/static/images/AC2Banner.png',
|
||||
duration: '45分钟',
|
||||
progress: 60,
|
||||
chapters: 6,
|
||||
watched: 4,
|
||||
type: 'video'
|
||||
},
|
||||
{
|
||||
id: 202,
|
||||
title: '直播 · 晨间拉伸',
|
||||
cover: '/static/images/AC1Banner.png',
|
||||
duration: '30分钟',
|
||||
progress: 0,
|
||||
liveTime: '07月20日 07:00',
|
||||
type: 'live'
|
||||
}
|
||||
],
|
||||
package: [
|
||||
{
|
||||
id: 301,
|
||||
title: '28天减脂训练营',
|
||||
banner: '/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: '/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,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,87 @@
|
||||
.scroll-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
--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-color: var(--bg-light);
|
||||
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
@@ -1,59 +1,91 @@
|
||||
<template>
|
||||
<!-- 底部导航栏容器 -->
|
||||
<view class="tab-bar">
|
||||
<!-- 导航栏项 -->
|
||||
<view
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="index"
|
||||
:class="['tab-item', { active: activeTab === index }]"
|
||||
@click="activeTab = index"
|
||||
:key="tab.path"
|
||||
:class="['tab-item', { active: currentIndex === index }]"
|
||||
hover-class="tab-item--hover"
|
||||
@tap="onTabTap(index)"
|
||||
>
|
||||
<!-- 导航栏图标 -->
|
||||
<image :src="activeTab === index ? tab.iconActive : tab.icon" mode="aspectFit" class="tab-icon" />
|
||||
<!-- 导航栏标签文字 -->
|
||||
<image
|
||||
:src="currentIndex === index ? tab.iconActive : tab.icon"
|
||||
mode="aspectFit"
|
||||
class="tab-icon"
|
||||
/>
|
||||
<text class="tab-label">{{ tab.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import {
|
||||
PAGE,
|
||||
TAB_ROUTES,
|
||||
getCurrentRoutePath,
|
||||
getTabIndexByRoute,
|
||||
switchToTab
|
||||
} from '@/common/constants/routes.js'
|
||||
|
||||
// 当前激活的导航栏索引
|
||||
const activeTab = ref(0)
|
||||
const props = defineProps({
|
||||
/** 当前 Tab 索引,由 Tab 页传入以保证高亮准确 */
|
||||
active: {
|
||||
type: Number,
|
||||
default: -1
|
||||
}
|
||||
})
|
||||
|
||||
const tapping = ref(false)
|
||||
|
||||
// 导航栏数据列表
|
||||
const tabs = [
|
||||
{
|
||||
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/home.png',
|
||||
iconActive: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/active/home.png',
|
||||
label: '首页',
|
||||
path: PAGE.INDEX,
|
||||
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/home.png',
|
||||
iconActive: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/active/home.png',
|
||||
label: '首页'
|
||||
},
|
||||
{
|
||||
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/course.png',
|
||||
iconActive: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/active/course.png',
|
||||
label: '课程'
|
||||
path: PAGE.COURSE,
|
||||
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/course.png',
|
||||
iconActive: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/active/course.png',
|
||||
label: '课程'
|
||||
},
|
||||
{
|
||||
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/train.png',
|
||||
iconActive: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/active/train.png',
|
||||
label: '训练' ,
|
||||
path: PAGE.TRAIN,
|
||||
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/train.png',
|
||||
iconActive: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/active/train.png',
|
||||
label: '训练'
|
||||
},
|
||||
{
|
||||
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/discover.png',
|
||||
iconActive: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/active/discover.png',
|
||||
label: '发现',
|
||||
path: PAGE.DISCOVER,
|
||||
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/discover.png',
|
||||
iconActive: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/active/discover.png',
|
||||
label: '发现'
|
||||
},
|
||||
{
|
||||
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/profile.png',
|
||||
iconActive: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/active/profile.png',
|
||||
label: '我的',
|
||||
path: PAGE.MEMBER,
|
||||
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/profile.png',
|
||||
iconActive: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/active/profile.png',
|
||||
label: '我的'
|
||||
}
|
||||
]
|
||||
|
||||
const currentIndex = computed(() => {
|
||||
if (props.active >= 0) return props.active
|
||||
return getTabIndexByRoute(getCurrentRoutePath())
|
||||
})
|
||||
|
||||
function onTabTap(index) {
|
||||
if (tapping.value || index === currentIndex.value) return
|
||||
tapping.value = true
|
||||
switchToTab(TAB_ROUTES[index])
|
||||
setTimeout(() => {
|
||||
tapping.value = false
|
||||
}, 350)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 底部导航栏容器样式 */
|
||||
.tab-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
@@ -68,9 +100,9 @@ const tabs = [
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06);
|
||||
border-radius: 32rpx 32rpx 0 0;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
/* 导航栏项样式 */
|
||||
.tab-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -79,19 +111,16 @@ const tabs = [
|
||||
padding: 12rpx 24rpx;
|
||||
}
|
||||
|
||||
/* 导航栏图标样式 */
|
||||
.tab-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
}
|
||||
|
||||
/* 导航栏标签文字样式 */
|
||||
.tab-label {
|
||||
font-size: 22rpx;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
/* 导航栏激活状态文字样式 */
|
||||
.tab-item.active .tab-label {
|
||||
color: #f97316;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -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="/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="/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="/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="/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="/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="/static/images/user2.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="booking-section__coach">教练:{{ item.coach }}</text>
|
||||
<image
|
||||
class="booking-section__icon-location"
|
||||
src="/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="/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="/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="/static/images/bell.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<image
|
||||
class="profile-header__icon-settings"
|
||||
src="/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="/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="/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 || '/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="/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="/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="/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="/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="/static/images/Vector_2_490.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<image
|
||||
class="quick-actions__icon-part"
|
||||
src="/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="/static/images/Vector_2_493.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<image
|
||||
class="quick-actions__icon-part"
|
||||
src="/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: '/static/images/mappin2.png' },
|
||||
{ key: 'bodyReport', label: '体测报告', textClass: 'quick-actions__title-3', icon: '/static/images/activity.png' },
|
||||
{ key: 'trainReport', label: '训练报告', textClass: 'quick-actions__coach', icon: '/static/images/usercheck.png' }
|
||||
],
|
||||
row2: [
|
||||
{ key: 'coupon', label: '我的优惠券', textClass: 'quick-actions__text', icon: '/static/images/ticket.png' },
|
||||
{ key: 'points', label: '我的积分', textClass: 'quick-actions__points-desc', icon: '/static/images/star.png' },
|
||||
{ key: 'referral', label: '邀请好友', textClass: 'quick-actions__title-4', icon: '/static/images/share2.png' },
|
||||
{ key: 'course', label: '我的课程', textClass: 'quick-actions__text-2', icon: '/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="/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="/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: '/static/images/bell.png',
|
||||
iconWrapClass: ''
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
label: '修改密码',
|
||||
icon: '/static/images/Vector_2_727.png',
|
||||
iconWrapClass: 'settings-section__item-icon-wrap--blue'
|
||||
},
|
||||
{
|
||||
key: 'privacy',
|
||||
label: '隐私政策',
|
||||
icon: '/static/images/shield.png',
|
||||
iconWrapClass: 'settings-section__item-icon-wrap--green'
|
||||
},
|
||||
{
|
||||
key: 'nfc',
|
||||
label: 'NFC 门禁卡',
|
||||
subtitle: '已绑定',
|
||||
icon: '/static/images/ticket.png',
|
||||
iconWrapClass: ''
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: '注销账户',
|
||||
icon: '/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="/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>
|
||||
@@ -1,17 +1,267 @@
|
||||
{
|
||||
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "健身房"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/course/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "课程"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/train/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "训练"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/discover/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "发现"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/memberInfo/memberInfo",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "我的"
|
||||
}
|
||||
},
|
||||
{
|
||||
"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": "课程评价"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarTitleText": "uni-app",
|
||||
"navigationBarTitleText": "健身房",
|
||||
"navigationBarBackgroundColor": "#F8F8F8",
|
||||
"backgroundColor": "#F8F8F8"
|
||||
},
|
||||
"tabBar": {
|
||||
"custom": true,
|
||||
"color": "#94a3b8",
|
||||
"selectedColor": "#f97316",
|
||||
"backgroundColor": "#1A4A6F",
|
||||
"borderStyle": "white",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/index/index",
|
||||
"text": "首页",
|
||||
"iconPath": "static/images/home.png",
|
||||
"selectedIconPath": "static/images/home.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/course/index",
|
||||
"text": "课程",
|
||||
"iconPath": "static/images/bookmark.png",
|
||||
"selectedIconPath": "static/images/bookmark.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/train/index",
|
||||
"text": "训练",
|
||||
"iconPath": "static/images/dumbbell.png",
|
||||
"selectedIconPath": "static/images/dumbbell.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/discover/index",
|
||||
"text": "发现",
|
||||
"iconPath": "static/images/activity.png",
|
||||
"selectedIconPath": "static/images/activity.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/memberInfo/memberInfo",
|
||||
"text": "我的",
|
||||
"iconPath": "static/images/user.png",
|
||||
"selectedIconPath": "static/images/user.png"
|
||||
}
|
||||
]
|
||||
},
|
||||
"uniIdRouter": {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<view class="tab-page">
|
||||
<view class="tab-page__header">
|
||||
<text class="tab-page__title">课程</text>
|
||||
<text class="tab-page__subtitle">精品团课 · 私教 · 线上课</text>
|
||||
</view>
|
||||
|
||||
<RecommendCourses />
|
||||
|
||||
<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>
|
||||
|
||||
<view class="bottom-placeholder"></view>
|
||||
<TabBar :active="1" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import RecommendCourses from '@/components/index/RecommendCourses.vue'
|
||||
import TabBar from '@/components/TabBar.vue'
|
||||
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
|
||||
|
||||
function goCourseList() {
|
||||
navigateToPage(PAGE.COURSE_LIST)
|
||||
}
|
||||
|
||||
function goMyCourses() {
|
||||
navigateToPage(PAGE.MY_COURSES)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tab-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f0f4f8;
|
||||
padding-bottom: 160rpx;
|
||||
}
|
||||
|
||||
.tab-page__header {
|
||||
padding: 48rpx 32rpx 16rpx;
|
||||
}
|
||||
|
||||
.tab-page__title {
|
||||
display: block;
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #1a202c;
|
||||
}
|
||||
|
||||
.tab-page__subtitle {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.tab-page__actions {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
}
|
||||
|
||||
.tab-page__btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
border-radius: 999rpx;
|
||||
background: linear-gradient(135deg, #ff6b35 0%, #ff8c5a 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tab-page__btn--ghost {
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.tab-page__btn-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.tab-page__btn-text--ghost {
|
||||
color: #1a4a6f;
|
||||
}
|
||||
|
||||
.bottom-placeholder {
|
||||
height: 40rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<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>
|
||||
<TabBar :active="3" />
|
||||
</view>
|
||||
</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>
|
||||
.tab-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f0f4f8;
|
||||
padding-bottom: 160rpx;
|
||||
}
|
||||
|
||||
.tab-page__header {
|
||||
padding: 48rpx 32rpx 16rpx;
|
||||
}
|
||||
|
||||
.tab-page__title {
|
||||
display: block;
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #1a202c;
|
||||
}
|
||||
|
||||
.tab-page__subtitle {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.discover-links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
}
|
||||
|
||||
.discover-link {
|
||||
padding: 24rpx 28rpx;
|
||||
border-radius: 20rpx;
|
||||
background: #fff;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.discover-link__title {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #1e2a3a;
|
||||
}
|
||||
|
||||
.discover-link__desc {
|
||||
display: block;
|
||||
margin-top: 6rpx;
|
||||
font-size: 22rpx;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.bottom-placeholder {
|
||||
height: 40rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -16,7 +16,7 @@
|
||||
<view class="bottom-placeholder"></view>
|
||||
|
||||
<!-- 底部导航 -->
|
||||
<TabBar />
|
||||
<TabBar :active="0" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -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="/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="/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="/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: '/static/images/clock.png' },
|
||||
{ key: 'compare', label: '历史对比', icon: '/static/images/trendingdown.png' },
|
||||
{ key: 'trend', label: '趋势分析', icon: '/static/images/activity.png' },
|
||||
{ key: 'report', label: '体测报告', icon: '/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="/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="/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="/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>
|
||||
@@ -0,0 +1,182 @@
|
||||
<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-setting">
|
||||
<view>
|
||||
<text class="bt-setting__label">蓝牙自动连接</text>
|
||||
<text class="bt-setting__desc">进入体测页时自动搜索已配对设备</text>
|
||||
</view>
|
||||
<switch
|
||||
:checked="settings.bluetoothEnabled"
|
||||
color="#FF6B35"
|
||||
@change="onSwitch('bluetoothEnabled', $event)"
|
||||
/>
|
||||
</view>
|
||||
<view class="bt-setting">
|
||||
<view>
|
||||
<text class="bt-setting__label">测量完成自动同步</text>
|
||||
<text class="bt-setting__desc">结果自动保存至云端与本地</text>
|
||||
</view>
|
||||
<switch
|
||||
:checked="settings.autoSync"
|
||||
color="#FF6B35"
|
||||
@change="onSwitch('autoSync', $event)"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">通知与隐私</text>
|
||||
<view class="bt-setting">
|
||||
<view>
|
||||
<text class="bt-setting__label">测量完成通知</text>
|
||||
<text class="bt-setting__desc">体测结束后推送报告摘要</text>
|
||||
</view>
|
||||
<switch
|
||||
:checked="settings.notifyOnComplete"
|
||||
color="#FF6B35"
|
||||
@change="onSwitch('notifyOnComplete', $event)"
|
||||
/>
|
||||
</view>
|
||||
<view class="bt-setting">
|
||||
<view>
|
||||
<text class="bt-setting__label">分享时匿名化</text>
|
||||
<text class="bt-setting__desc">隐藏姓名与手机号</text>
|
||||
</view>
|
||||
<switch
|
||||
:checked="settings.shareAnonymous"
|
||||
color="#FF6B35"
|
||||
@change="onSwitch('shareAnonymous', $event)"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">单位制</text>
|
||||
<view class="bt-setting">
|
||||
<view>
|
||||
<text class="bt-setting__label">度量单位</text>
|
||||
<text class="bt-setting__desc">{{ unitLabel }}</text>
|
||||
</view>
|
||||
<view
|
||||
hover-class="mi-tap--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="toggleUnit"
|
||||
>
|
||||
<text style="font-size: 14px; color: #1A4A6F; font-weight: 600;">切换</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="/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">
|
||||
{{ device.connected ? '已连接' : '未连接' }}
|
||||
· 上次 {{ device.lastConnected || '--' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="bt-btn bt-btn--outline"
|
||||
style="margin-top: 12px;"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="disconnect"
|
||||
>
|
||||
<text class="bt-btn__text">解除设备配对</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { PAGE, goBackOrTab } from '@/common/constants/routes.js'
|
||||
import {
|
||||
loadMemberStore,
|
||||
persistMemberStore
|
||||
} from '@/common/memberInfo/store.js'
|
||||
import {
|
||||
updateBodyTestSettings,
|
||||
disconnectBodyTestDevice
|
||||
} from '@/common/memberInfo/bodyTestStore.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
data() {
|
||||
return {
|
||||
settings: {},
|
||||
device: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
unitLabel() {
|
||||
return this.settings.unitSystem === 'imperial' ? '英制 (lb / in)' : '公制 (kg / cm)'
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.refreshFromStore()
|
||||
},
|
||||
methods: {
|
||||
refreshFromStore() {
|
||||
const store = loadMemberStore()
|
||||
this.settings = { ...store.bodyTest.settings }
|
||||
this.device = { ...store.bodyTest.device }
|
||||
},
|
||||
onBack() {
|
||||
goBackOrTab(PAGE.BODY_TEST_HOME)
|
||||
},
|
||||
onSwitch(key, e) {
|
||||
const store = loadMemberStore()
|
||||
updateBodyTestSettings(store, { [key]: e.detail.value })
|
||||
persistMemberStore(store)
|
||||
this.settings = { ...store.bodyTest.settings }
|
||||
uni.showToast({ title: '已保存', icon: 'success' })
|
||||
},
|
||||
toggleUnit() {
|
||||
const store = loadMemberStore()
|
||||
const next = this.settings.unitSystem === 'metric' ? 'imperial' : 'metric'
|
||||
updateBodyTestSettings(store, { unitSystem: next })
|
||||
persistMemberStore(store)
|
||||
this.settings = { ...store.bodyTest.settings }
|
||||
uni.showToast({ title: `已切换为${this.unitLabel}`, icon: 'none' })
|
||||
},
|
||||
disconnect() {
|
||||
uni.showModal({
|
||||
title: '解除配对',
|
||||
content: '解除后下次体测需重新连接设备,确定继续?',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
const store = loadMemberStore()
|
||||
disconnectBodyTestDevice(store)
|
||||
persistMemberStore(store)
|
||||
this.refreshFromStore()
|
||||
uni.showToast({ title: '已解除配对', icon: 'success' })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</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,151 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page">
|
||||
<MemberInfoSubNav title="趋势分析" @back="onBack" />
|
||||
<view class="bt-tabs">
|
||||
<view
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="bt-tab"
|
||||
:class="{ 'bt-tab--active': activeMetric === tab.key }"
|
||||
hover-class="mi-tap-tab--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="switchMetric(tab.key)"
|
||||
>
|
||||
<text class="bt-tab__text">{{ tab.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bt-page__body">
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">{{ activeLabel }}变化趋势</text>
|
||||
<text class="bt-card__desc">基于最近 {{ trendPoints.length }} 次体测数据</text>
|
||||
<BodyTestTrendChart
|
||||
:points="trendPoints"
|
||||
:unit="activeUnit"
|
||||
:width="chartWidth"
|
||||
:height="200"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">数据明细</text>
|
||||
<view
|
||||
v-for="(pt, idx) in trendPointsReversed"
|
||||
:key="pt.id"
|
||||
class="bt-compare-row"
|
||||
>
|
||||
<text class="bt-compare-row__label">{{ pt.date }}</text>
|
||||
<text class="bt-compare-row__val">{{ pt.value }}{{ activeUnit }}</text>
|
||||
<text
|
||||
v-if="idx < trendPointsReversed.length - 1"
|
||||
class="bt-compare-row__diff"
|
||||
:style="{ color: rowDiffColor(pt, trendPointsReversed[idx + 1]) }"
|
||||
>
|
||||
{{ rowDiffText(pt, trendPointsReversed[idx + 1]) }}
|
||||
</text>
|
||||
<text v-else class="bt-compare-row__diff">--</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="summaryText" class="bt-card">
|
||||
<text class="bt-card__title">趋势解读</text>
|
||||
<text class="bt-card__desc">{{ summaryText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import BodyTestTrendChart from '@/components/memberInfo/BodyTestTrendChart.vue'
|
||||
import { PAGE, goBackOrTab } from '@/common/constants/routes.js'
|
||||
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||
import { getBodyTestTrendData, bodyTestMock } from '@/common/memberInfo/bodyTestStore.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav, BodyTestTrendChart },
|
||||
data() {
|
||||
return {
|
||||
tabs: bodyTestMock.trendMetrics,
|
||||
activeMetric: 'weight',
|
||||
trendPoints: [],
|
||||
chartWidth: 300
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
activeLabel() {
|
||||
return this.tabs.find((t) => t.key === this.activeMetric)?.label || ''
|
||||
},
|
||||
activeUnit() {
|
||||
const units = {
|
||||
weight: 'kg',
|
||||
bodyFat: '%',
|
||||
muscleMass: 'kg',
|
||||
bmi: ''
|
||||
}
|
||||
return units[this.activeMetric] || ''
|
||||
},
|
||||
trendPointsReversed() {
|
||||
return [...this.trendPoints].reverse()
|
||||
},
|
||||
summaryText() {
|
||||
if (this.trendPoints.length < 2) return ''
|
||||
const first = this.trendPoints[0].value
|
||||
const last = this.trendPoints[this.trendPoints.length - 1].value
|
||||
const diff = Math.round((last - first) * 10) / 10
|
||||
const sign = diff > 0 ? '上升' : diff < 0 ? '下降' : '持平'
|
||||
const abs = Math.abs(diff)
|
||||
const lowerBetter = ['weight', 'bodyFat'].includes(this.activeMetric)
|
||||
let advice = ''
|
||||
if (diff !== 0) {
|
||||
const good = lowerBetter ? diff < 0 : diff > 0
|
||||
advice = good ? ',变化方向符合健康目标' : ',建议关注饮食与训练计划'
|
||||
}
|
||||
return `期间${this.activeLabel}${sign} ${abs}${this.activeUnit}${advice}`
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
if (options?.metric) {
|
||||
this.activeMetric = options.metric
|
||||
}
|
||||
this.chartWidth = uni.getSystemInfoSync().windowWidth - 64
|
||||
this.loadTrend()
|
||||
},
|
||||
methods: {
|
||||
onBack() {
|
||||
goBackOrTab(PAGE.BODY_TEST_HOME)
|
||||
},
|
||||
switchMetric(key) {
|
||||
this.activeMetric = key
|
||||
this.loadTrend()
|
||||
},
|
||||
loadTrend() {
|
||||
const store = loadMemberStore()
|
||||
this.trendPoints = getBodyTestTrendData(store, this.activeMetric, 6)
|
||||
},
|
||||
rowDiffText(current, older) {
|
||||
const diff = Math.round((current.value - older.value) * 10) / 10
|
||||
const sign = diff > 0 ? '+' : ''
|
||||
return `${sign}${diff}${this.activeUnit}`
|
||||
},
|
||||
rowDiffColor(current, older) {
|
||||
const diff = current.value - older.value
|
||||
if (diff === 0) return '#8A99B4'
|
||||
const lowerBetter = ['weight', 'bodyFat'].includes(this.activeMetric)
|
||||
const good = lowerBetter ? diff < 0 : diff > 0
|
||||
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,209 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="booking-page">
|
||||
<MemberInfoSubNav title="我的预约" @back="goBack" />
|
||||
|
||||
<view class="booking-page__tabs">
|
||||
<view
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="booking-page__tab"
|
||||
:class="{ 'booking-page__tab--active': activeTab === tab.key }"
|
||||
hover-class="mi-tap-tab--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="setActiveTab(tab.key)"
|
||||
>
|
||||
<text
|
||||
class="booking-page__tab-text"
|
||||
:class="{ 'booking-page__tab-text--active': activeTab === tab.key }"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</text>
|
||||
<view
|
||||
v-if="activeTab === tab.key"
|
||||
class="booking-page__tab-indicator"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bt-page__action-bar bt-page__action-bar--end">
|
||||
<text
|
||||
class="bt-page__action-link bt-page__action-link--primary"
|
||||
hover-class="mi-tap--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="goCourseList"
|
||||
>
|
||||
预约课程
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="booking-page__body">
|
||||
<view
|
||||
v-if="activeTab === 'ongoing' && upcomingAlert"
|
||||
class="booking-page__alert"
|
||||
>
|
||||
<image
|
||||
class="booking-page__alert-icon"
|
||||
src="/static/images/clock1.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="booking-page__alert-text">{{ upcomingAlert }}</text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
v-for="item in displayedBookings"
|
||||
:key="item.id"
|
||||
class="bk-card"
|
||||
hover-class="mi-tap-card--hover"
|
||||
:hover-stay-time="150"
|
||||
>
|
||||
<image
|
||||
class="bk-card__banner"
|
||||
:src="item.banner"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="bk-card__content">
|
||||
<view class="bk-card__header">
|
||||
<text class="bk-card__title">{{ item.title }}</text>
|
||||
<view
|
||||
class="bk-card__status"
|
||||
:class="'bk-card__status--' + item.status"
|
||||
>
|
||||
<text
|
||||
class="bk-card__status-text"
|
||||
:class="'bk-card__status-text--' + item.status"
|
||||
>
|
||||
{{ item.statusLabel }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bk-card__meta">
|
||||
<view class="bk-card__meta-row">
|
||||
<image
|
||||
class="bk-card__meta-icon"
|
||||
src="/static/images/clock0.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="bk-card__meta-text">{{ item.schedule }}</text>
|
||||
</view>
|
||||
<view class="bk-card__meta-row">
|
||||
<image
|
||||
class="bk-card__meta-icon"
|
||||
src="/static/images/user0.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="bk-card__meta-text">{{ item.coach }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bk-card__footer">
|
||||
<text class="bk-card__footer-info">{{ item.footerText }}</text>
|
||||
<view
|
||||
v-if="item.canCancel"
|
||||
class="bk-card__cancel"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap.stop="cancelBooking(item)"
|
||||
>
|
||||
<text class="bk-card__cancel-text">取消预约</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="!displayedBookings.length" class="booking-page__empty">
|
||||
<text class="booking-page__empty-text">
|
||||
{{ activeTab === 'ongoing' ? '暂无进行中的预约' : '暂无历史预约' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
|
||||
import { bookingMock } from '@/common/memberInfo/mockData.js'
|
||||
import {
|
||||
loadMemberStore,
|
||||
cancelOngoingBooking,
|
||||
formatUpcomingAlert
|
||||
} from '@/common/memberInfo/store.js'
|
||||
import { canCancelBooking } from '@/common/memberInfo/bookingStore.js'
|
||||
import { subPageMixin } from '@/common/memberInfo/mixins.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
mixins: [subPageMixin],
|
||||
data() {
|
||||
return {
|
||||
tabs: bookingMock.tabs,
|
||||
ongoingBookings: [],
|
||||
historyBookings: [],
|
||||
activeTab: 'ongoing'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
displayedBookings() {
|
||||
return this.activeTab === 'ongoing'
|
||||
? this.ongoingBookings
|
||||
: this.historyBookings
|
||||
},
|
||||
upcomingAlert() {
|
||||
return formatUpcomingAlert(this.ongoingBookings[0])
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.refreshFromStore()
|
||||
},
|
||||
methods: {
|
||||
refreshFromStore() {
|
||||
const store = loadMemberStore()
|
||||
this.ongoingBookings = store.ongoingBookings.map((item) => ({
|
||||
...item,
|
||||
canCancel: canCancelBooking(item)
|
||||
}))
|
||||
this.historyBookings = store.historyBookings.map((item) => ({ ...item }))
|
||||
},
|
||||
goCourseList() {
|
||||
navigateToPage(PAGE.COURSE_LIST)
|
||||
},
|
||||
setActiveTab(tab) {
|
||||
this.activeTab = tab
|
||||
},
|
||||
cancelBooking(item) {
|
||||
if (!canCancelBooking(item)) {
|
||||
uni.showToast({ title: '距开课不足2小时,无法取消', icon: 'none' })
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '取消预约',
|
||||
content: `确定要取消「${item.title}」吗?`,
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
const store = loadMemberStore()
|
||||
const result = cancelOngoingBooking(store, item.id)
|
||||
if (!result.ok) {
|
||||
uni.showToast({ title: result.message, icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.refreshFromStore()
|
||||
uni.showToast({ title: '已取消', icon: 'success' })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</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/booking-page.css';
|
||||
</style>
|
||||
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page">
|
||||
<MemberInfoSubNav title="签到记录" @back="goBack" />
|
||||
<view class="mi-mod-tabs">
|
||||
<view
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="bt-tab"
|
||||
:class="{ 'bt-tab--active': activeFilter === tab.key }"
|
||||
hover-class="mi-tap-tab--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="activeFilter = tab.key"
|
||||
>
|
||||
<text class="bt-tab__text">{{ tab.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bt-page__body">
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">共 {{ filteredList.length }} 条记录</text>
|
||||
<view
|
||||
v-for="item in filteredList"
|
||||
:key="item.id"
|
||||
class="mi-mod-checkin-row"
|
||||
hover-class="mi-tap-row--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="showDetail(item)"
|
||||
>
|
||||
<view class="mi-mod-checkin-row__icon" :class="'mi-mod-checkin-row__icon--' + item.tagTheme">
|
||||
<image class="mi-mod-checkin-row__icon-img" src="/static/images/usercheck.png" mode="aspectFit" />
|
||||
</view>
|
||||
<view class="mi-mod-checkin-row__info">
|
||||
<text class="mi-mod-checkin-row__title">{{ item.title }}</text>
|
||||
<text class="mi-mod-checkin-row__time">{{ item.time }} · {{ item.location }}</text>
|
||||
</view>
|
||||
<view class="mi-mod-checkin-row__tag" :class="'mi-mod-checkin-row__tag--' + item.tagTheme">
|
||||
<text class="mi-mod-checkin-row__tag-text">{{ item.tag }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="!filteredList.length" class="bt-empty">
|
||||
<text class="bt-empty__text">暂无签到记录</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { moduleMock, getCheckInHistory } from '@/common/memberInfo/moduleStore.js'
|
||||
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||
import { subPageMixin } from '@/common/memberInfo/mixins.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
mixins: [subPageMixin],
|
||||
data() {
|
||||
return {
|
||||
tabs: moduleMock.checkInTabs,
|
||||
activeFilter: 'all',
|
||||
list: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredList() {
|
||||
if (this.activeFilter === 'all') return this.list
|
||||
return this.list.filter((i) => i.tagTheme === this.activeFilter)
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
const store = loadMemberStore()
|
||||
this.list = getCheckInHistory(store, 'all')
|
||||
},
|
||||
methods: {
|
||||
showDetail(item) {
|
||||
uni.showModal({
|
||||
title: item.title,
|
||||
content: `${item.time}\n地点:${item.location}\n类型:${item.tag}`,
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</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';
|
||||
@import '@/common/style/memberInfo/pages/module-pages-common.css';
|
||||
</style>
|
||||
@@ -0,0 +1,134 @@
|
||||
<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>
|
||||
<text class="bt-card__desc">领取后自动放入「可用」列表</text>
|
||||
</view>
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mi-center-coupon"
|
||||
>
|
||||
<view class="mi-center-coupon__info">
|
||||
<text class="mi-center-coupon__amount">¥{{ item.amount }}</text>
|
||||
<view>
|
||||
<text class="mi-center-coupon__title">{{ item.title }}</text>
|
||||
<text class="mi-center-coupon__desc">{{ item.desc }} · {{ item.expireDays }}天有效</text>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="mi-center-coupon__btn"
|
||||
:class="{ 'mi-center-coupon__btn--done': item.claimed }"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
@tap="claim(item)"
|
||||
>
|
||||
<text>{{ item.claimed ? '已领取' : '立即领取' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { PAGE, goBackOrTab } from '@/common/constants/routes.js'
|
||||
import { loadMemberStore, persistMemberStore } from '@/common/memberInfo/store.js'
|
||||
import { getCouponCenterList, claimCouponFromCenter } from '@/common/memberInfo/moduleStore.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
data() {
|
||||
return { list: [] }
|
||||
},
|
||||
onShow() {
|
||||
const store = loadMemberStore()
|
||||
this.list = getCouponCenterList(store)
|
||||
},
|
||||
methods: {
|
||||
onBack() { goBackOrTab(PAGE.COUPONS) },
|
||||
claim(item) {
|
||||
if (item.claimed) return
|
||||
const store = loadMemberStore()
|
||||
const result = claimCouponFromCenter(store, item.id)
|
||||
uni.showToast({ title: result.message, icon: result.ok ? 'success' : 'none' })
|
||||
if (result.ok) {
|
||||
persistMemberStore(store)
|
||||
this.list = getCouponCenterList(store)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</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-center-coupon {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 16px;
|
||||
background: var(--bg-white, #fff);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.mi-center-coupon__info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mi-center-coupon__amount {
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
color: var(--accent-orange, #FF6B35);
|
||||
}
|
||||
|
||||
.mi-center-coupon__title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-dark, #1E2A3A);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mi-center-coupon__desc {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted, #5E6F8D);
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.mi-center-coupon__btn {
|
||||
padding: 8px 14px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.mi-center-coupon__btn text {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.mi-center-coupon__btn--done {
|
||||
background: var(--bg-gray, #F2F5F9);
|
||||
}
|
||||
|
||||
.mi-center-coupon__btn--done text {
|
||||
color: var(--text-light, #8A99B4);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page" v-if="coupon">
|
||||
<MemberInfoSubNav title="优惠券详情" @back="onBack" />
|
||||
<view class="bt-page__body">
|
||||
<view class="mi-mod-coupon mi-mod-coupon--detail">
|
||||
<view class="mi-mod-coupon__left">
|
||||
<text class="mi-mod-coupon__amount">¥{{ coupon.amount }}</text>
|
||||
<text class="mi-mod-coupon__min">满{{ coupon.minSpend }}可用</text>
|
||||
</view>
|
||||
<view class="mi-mod-coupon__right">
|
||||
<text class="mi-mod-coupon__title">{{ coupon.title }}</text>
|
||||
<text class="mi-mod-coupon__desc">{{ coupon.desc }}</text>
|
||||
<text class="mi-mod-coupon__expire">有效期至 {{ coupon.expire }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">使用规则</text>
|
||||
<text class="bt-card__desc" style="white-space: pre-line;">{{ coupon.rules }}</text>
|
||||
</view>
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">适用范围</text>
|
||||
<text class="bt-card__desc">{{ coupon.scope }}</text>
|
||||
</view>
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">使用流程</text>
|
||||
<text class="bt-card__desc">{{ coupon.flow }}</text>
|
||||
</view>
|
||||
<view v-if="coupon.status === 'available'" class="bt-footer-actions">
|
||||
<view class="bt-btn bt-btn--primary" hover-class="mi-tap-btn--hover" @tap="useNow">
|
||||
<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 } from '@/common/memberInfo/store.js'
|
||||
import { getCouponById } from '@/common/memberInfo/moduleStore.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
data() {
|
||||
return { coupon: null }
|
||||
},
|
||||
onLoad(options) {
|
||||
const store = loadMemberStore()
|
||||
this.coupon = getCouponById(store, options?.id)
|
||||
},
|
||||
methods: {
|
||||
onBack() { goBackOrTab(PAGE.COUPONS) },
|
||||
useNow() {
|
||||
navigateToPage(PAGE.COURSE_LIST)
|
||||
}
|
||||
}
|
||||
}
|
||||
</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';
|
||||
@import '@/common/style/memberInfo/pages/module-pages-common.css';
|
||||
.mi-mod-coupon--detail { margin-bottom: 12px; }
|
||||
.bt-footer-actions .bt-btn { flex: 1; }
|
||||
</style>
|
||||
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page">
|
||||
<MemberInfoSubNav title="我的优惠券" @back="goBack" />
|
||||
<view class="mi-mod-tabs">
|
||||
<view
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="bt-tab"
|
||||
:class="{ 'bt-tab--active': activeTab === tab.key }"
|
||||
hover-class="mi-tap-tab--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="activeTab = tab.key"
|
||||
>
|
||||
<text class="bt-tab__text">{{ tab.label }}({{ countByTab[tab.key] }})</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bt-page__action-bar bt-page__action-bar--end">
|
||||
<text
|
||||
class="bt-page__action-link bt-page__action-link--primary"
|
||||
hover-class="mi-tap--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="goCenter"
|
||||
>
|
||||
领券中心
|
||||
</text>
|
||||
</view>
|
||||
<view class="bt-page__body">
|
||||
<view
|
||||
v-for="item in displayedCoupons"
|
||||
:key="item.id"
|
||||
class="mi-mod-coupon"
|
||||
:class="'mi-mod-coupon--' + item.status"
|
||||
hover-class="mi-tap-card--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="onCouponTap(item)"
|
||||
>
|
||||
<view class="mi-mod-coupon__left">
|
||||
<text class="mi-mod-coupon__amount">¥{{ item.amount }}</text>
|
||||
<text class="mi-mod-coupon__min">满{{ item.minSpend }}可用</text>
|
||||
</view>
|
||||
<view class="mi-mod-coupon__right">
|
||||
<text class="mi-mod-coupon__title">{{ item.title }}</text>
|
||||
<text class="mi-mod-coupon__desc">{{ item.desc }}</text>
|
||||
<text class="mi-mod-coupon__expire">
|
||||
{{ item.status === 'used' ? `已于 ${item.usedAt} 使用` : `有效期至 ${item.expire}` }}
|
||||
</text>
|
||||
<view v-if="item.status === 'available'" class="mi-mod-coupon__use" @tap.stop="useNow(item)">
|
||||
<text>立即使用</text>
|
||||
</view>
|
||||
<view v-if="item.status === 'expired'" class="mi-mod-coupon__del" @tap.stop="removeExpired(item)">
|
||||
<text>删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="!displayedCoupons.length" class="bt-empty">
|
||||
<text class="bt-empty__text">暂无{{ activeTabLabel }}优惠券</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
|
||||
import { moduleMock, getCouponsByStatus, useCoupon, deleteExpiredCoupon } from '@/common/memberInfo/moduleStore.js'
|
||||
import { loadMemberStore, persistMemberStore } from '@/common/memberInfo/store.js'
|
||||
import { subPageMixin } from '@/common/memberInfo/mixins.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
mixins: [subPageMixin],
|
||||
data() {
|
||||
return {
|
||||
tabs: moduleMock.couponTabs,
|
||||
activeTab: 'available',
|
||||
coupons: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
displayedCoupons() {
|
||||
return this.coupons.filter((c) => c.status === this.activeTab)
|
||||
},
|
||||
countByTab() {
|
||||
return {
|
||||
available: this.coupons.filter((c) => c.status === 'available').length,
|
||||
used: this.coupons.filter((c) => c.status === 'used').length,
|
||||
expired: this.coupons.filter((c) => c.status === 'expired').length
|
||||
}
|
||||
},
|
||||
activeTabLabel() {
|
||||
return this.tabs.find((t) => t.key === this.activeTab)?.label || ''
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.refreshList()
|
||||
},
|
||||
methods: {
|
||||
refreshList() {
|
||||
const store = loadMemberStore()
|
||||
this.coupons = [
|
||||
...getCouponsByStatus(store, 'available'),
|
||||
...getCouponsByStatus(store, 'used'),
|
||||
...getCouponsByStatus(store, 'expired')
|
||||
]
|
||||
},
|
||||
onCouponTap(item) {
|
||||
navigateToPage(`${PAGE.COUPON_DETAIL}?id=${item.id}`)
|
||||
},
|
||||
useNow(item) {
|
||||
uni.showModal({
|
||||
title: '使用优惠券',
|
||||
content: `确认使用「${item.title}」?`,
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
const store = loadMemberStore()
|
||||
useCoupon(store, item.id)
|
||||
persistMemberStore(store)
|
||||
this.refreshList()
|
||||
navigateToPage(PAGE.COURSE_LIST)
|
||||
}
|
||||
})
|
||||
},
|
||||
removeExpired(item) {
|
||||
const store = loadMemberStore()
|
||||
deleteExpiredCoupon(store, item.id)
|
||||
persistMemberStore(store)
|
||||
this.refreshList()
|
||||
},
|
||||
goCenter() {
|
||||
navigateToPage(PAGE.COUPON_CENTER)
|
||||
}
|
||||
}
|
||||
}
|
||||
</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';
|
||||
@import '@/common/style/memberInfo/pages/module-pages-common.css';
|
||||
|
||||
.mi-mod-coupon__use {
|
||||
margin-top: 8px;
|
||||
align-self: flex-start;
|
||||
padding: 4px 12px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.mi-mod-coupon__use text {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.mi-mod-coupon__del {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.mi-mod-coupon__del text {
|
||||
font-size: 11px;
|
||||
color: var(--text-light, #8A99B4);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page" v-if="course">
|
||||
<MemberInfoSubNav title="课程详情" @back="onBack" />
|
||||
<image class="mi-detail-hero" :src="course.banner" mode="aspectFill" />
|
||||
<view class="bt-page__body">
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">{{ course.title }}</text>
|
||||
<text class="bt-card__desc">{{ course.startTime }}-{{ course.endTime }} · {{ course.location }}</text>
|
||||
<view class="mi-detail-coach">
|
||||
<image class="mi-detail-coach__avatar" :src="course.coachAvatar" mode="aspectFill" />
|
||||
<view>
|
||||
<text class="mi-detail-coach__name">{{ course.coach }}</text>
|
||||
<text class="mi-detail-coach__rating">评分 {{ course.coachRating }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">课程介绍</text>
|
||||
<text class="bt-card__desc">{{ course.intro }}</text>
|
||||
<text class="bt-card__title" style="margin-top:12px;">适合人群</text>
|
||||
<text class="bt-card__desc">{{ course.suitable }}</text>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">教练介绍</text>
|
||||
<text class="bt-card__desc">{{ course.coachBio }}</text>
|
||||
</view>
|
||||
|
||||
<view v-if="course.reviews?.length" class="bt-card">
|
||||
<text class="bt-card__title">会员评价</text>
|
||||
<view v-for="(r, i) in course.reviews" :key="i" class="mi-detail-review">
|
||||
<text class="mi-detail-review__user">{{ r.user }} · {{ r.score }}分</text>
|
||||
<text class="mi-detail-review__text">{{ r.text }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">取消规则</text>
|
||||
<text class="bt-card__desc">{{ course.cancelRule }}</text>
|
||||
<text class="bt-card__desc" style="margin-top:8px;">费用:{{ course.price }}</text>
|
||||
</view>
|
||||
|
||||
<view class="bt-footer-actions">
|
||||
<view
|
||||
class="bt-btn bt-btn--primary"
|
||||
:class="{ 'bt-btn--outline': course.full }"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
@tap="confirmBook"
|
||||
>
|
||||
<text class="bt-btn__text">{{ course.full ? '已约满' : '确认预约' }}</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 { getCourseById, enrichCourseForDisplay, bookCourse } from '@/common/memberInfo/bookingStore.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
data() {
|
||||
return { course: null, courseId: null }
|
||||
},
|
||||
onLoad(options) {
|
||||
this.courseId = options?.id
|
||||
this.loadCourse()
|
||||
},
|
||||
methods: {
|
||||
loadCourse() {
|
||||
const store = loadMemberStore()
|
||||
const raw = getCourseById(store, this.courseId)
|
||||
this.course = raw ? enrichCourseForDisplay(raw) : null
|
||||
},
|
||||
onBack() {
|
||||
goBackOrTab(PAGE.COURSE_LIST)
|
||||
},
|
||||
confirmBook() {
|
||||
if (!this.course || this.course.full) return
|
||||
uni.showModal({
|
||||
title: '确认预约',
|
||||
content: `预约「${this.course.title}」\n${this.course.date} ${this.course.startTime}-${this.course.endTime}`,
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
const store = loadMemberStore()
|
||||
const result = bookCourse(store, this.courseId)
|
||||
if (!result.ok) {
|
||||
uni.showToast({ title: result.message, icon: 'none' })
|
||||
return
|
||||
}
|
||||
persistMemberStore(store)
|
||||
uni.showToast({ title: '预约成功', icon: 'success' })
|
||||
setTimeout(() => navigateToPage(PAGE.BOOKING), 800)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</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';
|
||||
@import '@/common/style/memberInfo/pages/module-pages-common.css';
|
||||
|
||||
.mi-detail-hero {
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mi-detail-coach {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.mi-detail-coach__avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.mi-detail-coach__name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-dark, #1E2A3A);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mi-detail-coach__rating {
|
||||
font-size: 12px;
|
||||
color: var(--accent-orange, #FF6B35);
|
||||
}
|
||||
|
||||
.mi-detail-review {
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid var(--border-light, #E9EDF2);
|
||||
}
|
||||
|
||||
.mi-detail-review:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.mi-detail-review__user {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-dark, #1E2A3A);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mi-detail-review__text {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted, #5E6F8D);
|
||||
margin-top: 4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.bt-footer-actions .bt-btn {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,88 @@
|
||||
<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">{{ title }}</text>
|
||||
<text class="bt-card__desc">请为本次课程打分并填写评价</text>
|
||||
</view>
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">评分</text>
|
||||
<view class="mi-eval-stars">
|
||||
<text
|
||||
v-for="n in 5"
|
||||
:key="n"
|
||||
class="mi-eval-star"
|
||||
:class="{ 'mi-eval-star--on': n <= score }"
|
||||
@tap="score = n"
|
||||
>★</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">评价内容</text>
|
||||
<textarea
|
||||
v-model="comment"
|
||||
class="mi-eval-textarea"
|
||||
placeholder="分享您的上课体验…"
|
||||
maxlength="200"
|
||||
/>
|
||||
</view>
|
||||
<view class="bt-footer-actions">
|
||||
<view class="bt-btn bt-btn--primary" hover-class="mi-tap-btn--hover" @tap="submit">
|
||||
<text class="bt-btn__text">提交评价</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { PAGE, goBackOrTab } from '@/common/constants/routes.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
data() {
|
||||
return { title: '', score: 5, comment: '' }
|
||||
},
|
||||
onLoad(options) {
|
||||
this.title = decodeURIComponent(options?.title || '课程评价')
|
||||
},
|
||||
methods: {
|
||||
onBack() { goBackOrTab(PAGE.MY_COURSES) },
|
||||
submit() {
|
||||
if (!this.comment.trim()) {
|
||||
uni.showToast({ title: '请填写评价', icon: 'none' })
|
||||
return
|
||||
}
|
||||
uni.showToast({ title: '评价已提交', icon: 'success' })
|
||||
setTimeout(() => goBackOrTab(PAGE.MY_COURSES), 800)
|
||||
}
|
||||
}
|
||||
}
|
||||
</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-sub-nav.css';
|
||||
@import '@/common/style/memberInfo/pages/body-test-common.css';
|
||||
|
||||
.mi-eval-stars { display: flex; flex-direction: row; gap: 8px; margin-top: 8px; }
|
||||
.mi-eval-star { font-size: 32px; color: #E9EDF2; }
|
||||
.mi-eval-star--on { color: #FF6B35; }
|
||||
.mi-eval-textarea {
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
margin-top: 8px;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
background: var(--bg-gray, #F2F5F9);
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.bt-footer-actions .bt-btn { flex: 1; }
|
||||
</style>
|
||||
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page mi-course-list">
|
||||
<MemberInfoSubNav title="预约课程" @back="goBack" />
|
||||
<view class="mi-course-list__filters">
|
||||
<view class="mi-course-list__date-bar">
|
||||
<view
|
||||
class="mi-course-list__mode"
|
||||
hover-class="mi-tap--hover"
|
||||
@tap="toggleDateMode"
|
||||
>
|
||||
<text class="mi-course-list__mode-text">{{ dateMode === 'day' ? '按天' : '按周' }}</text>
|
||||
</view>
|
||||
<scroll-view scroll-x class="mi-course-list__dates">
|
||||
<view
|
||||
v-for="d in dateOptions"
|
||||
:key="d.value"
|
||||
class="mi-course-list__date"
|
||||
:class="{ 'mi-course-list__date--active': selectedDate === d.value }"
|
||||
@tap="selectedDate = d.value"
|
||||
>
|
||||
<text class="mi-course-list__date-week">{{ d.week }}</text>
|
||||
<text class="mi-course-list__date-day">{{ d.day }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<scroll-view scroll-x class="mi-course-list__chips">
|
||||
<view
|
||||
v-for="t in typeOptions"
|
||||
:key="t.key"
|
||||
class="bt-tab"
|
||||
:class="{ 'bt-tab--active': courseType === t.key }"
|
||||
@tap="courseType = t.key"
|
||||
>
|
||||
<text class="bt-tab__text">{{ t.label }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="mi-course-list__row">
|
||||
<picker :range="coaches" @change="onCoachChange">
|
||||
<view class="mi-course-list__picker">
|
||||
<text>{{ coach }}</text>
|
||||
<image class="mi-course-list__arrow" src="/static/images/chevronright3.png" mode="aspectFit" />
|
||||
</view>
|
||||
</picker>
|
||||
<picker :range="periodLabels" @change="onPeriodChange">
|
||||
<view class="mi-course-list__picker">
|
||||
<text>{{ periodLabel }}</text>
|
||||
<image class="mi-course-list__arrow" src="/static/images/chevronright3.png" mode="aspectFit" />
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bt-page__body">
|
||||
<view
|
||||
v-for="course in displayCourses"
|
||||
:key="course.id"
|
||||
class="mi-course-card"
|
||||
hover-class="mi-tap-card--hover"
|
||||
@tap="openDetail(course)"
|
||||
>
|
||||
<image class="mi-course-card__banner" :src="course.banner" mode="aspectFill" />
|
||||
<view class="mi-course-card__body">
|
||||
<view class="mi-course-card__head">
|
||||
<text class="mi-course-card__title">{{ course.title }}</text>
|
||||
<view class="mi-course-card__type" :class="'mi-course-card__type--' + course.type">
|
||||
<text>{{ course.type === 'group' ? '团课' : '私教' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mi-course-card__coach">
|
||||
<image class="mi-course-card__avatar" :src="course.coachAvatar" mode="aspectFill" />
|
||||
<text>{{ course.coach }}</text>
|
||||
</view>
|
||||
<text class="mi-course-card__meta">
|
||||
{{ course.startTime }}-{{ course.endTime }} · {{ course.location }}
|
||||
</text>
|
||||
<view class="mi-course-card__capacity">
|
||||
<view class="mi-course-card__bar">
|
||||
<view class="mi-course-card__bar-fill" :style="{ width: course.percent + '%' }"></view>
|
||||
</view>
|
||||
<text class="mi-course-card__cap-text">{{ course.enrolled }}/{{ course.capacity }}</text>
|
||||
<text v-if="course.scarcityLabel" class="mi-course-card__scarcity">{{ course.scarcityLabel }}</text>
|
||||
</view>
|
||||
<view class="mi-course-card__footer">
|
||||
<text class="mi-course-card__price">{{ course.price }}</text>
|
||||
<view
|
||||
class="mi-course-card__btn"
|
||||
:class="{ 'mi-course-card__btn--disabled': course.full }"
|
||||
@tap.stop="quickBook(course)"
|
||||
>
|
||||
<text>{{ course.full ? '已约满' : '预约' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="!displayCourses.length" class="bt-empty">
|
||||
<text class="bt-empty__text">暂无符合条件的课程</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="mi-course-list__fab"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
@tap="goMyBooking"
|
||||
>
|
||||
<image class="mi-course-list__fab-icon" src="/static/images/clock.png" mode="aspectFit" />
|
||||
<text class="mi-course-list__fab-text">我的预约</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
|
||||
import { loadMemberStore, persistMemberStore } from '@/common/memberInfo/store.js'
|
||||
import {
|
||||
courseCatalogMock,
|
||||
filterCourses,
|
||||
enrichCourseForDisplay,
|
||||
getWeekDates,
|
||||
bookCourse
|
||||
} from '@/common/memberInfo/bookingStore.js'
|
||||
import { subPageMixin } from '@/common/memberInfo/mixins.js'
|
||||
|
||||
const WEEK = ['日', '一', '二', '三', '四', '五', '六']
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
mixins: [subPageMixin],
|
||||
data() {
|
||||
return {
|
||||
dateMode: 'day',
|
||||
selectedDate: '2024-07-15',
|
||||
courseType: 'all',
|
||||
coach: '全部',
|
||||
period: 'all',
|
||||
coaches: courseCatalogMock.coaches,
|
||||
typeOptions: courseCatalogMock.typeOptions,
|
||||
periodOptions: courseCatalogMock.periodOptions,
|
||||
courses: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
periodLabels() {
|
||||
return this.periodOptions.map((p) => p.label)
|
||||
},
|
||||
periodLabel() {
|
||||
return this.periodOptions.find((p) => p.key === this.period)?.label || '全部时段'
|
||||
},
|
||||
weekDates() {
|
||||
return getWeekDates(this.selectedDate)
|
||||
},
|
||||
dateOptions() {
|
||||
const dates = this.dateMode === 'week' ? this.weekDates : this.weekDates
|
||||
return dates.map((value) => {
|
||||
const d = new Date(value.replace(/-/g, '/'))
|
||||
return {
|
||||
value,
|
||||
week: WEEK[d.getDay()],
|
||||
day: `${d.getMonth() + 1}/${d.getDate()}`
|
||||
}
|
||||
})
|
||||
},
|
||||
displayCourses() {
|
||||
const filters = {
|
||||
date: this.dateMode === 'day' ? this.selectedDate : '',
|
||||
weekDates: this.dateMode === 'week' ? this.weekDates : [],
|
||||
type: this.courseType,
|
||||
coach: this.coach,
|
||||
period: this.period
|
||||
}
|
||||
return filterCourses(this.courses, filters).map(enrichCourseForDisplay)
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
const store = loadMemberStore()
|
||||
this.courses = store.courseCatalog.map((c) => ({ ...c }))
|
||||
if (!this.selectedDate && this.courses.length) {
|
||||
this.selectedDate = this.courses[0].date
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleDateMode() {
|
||||
this.dateMode = this.dateMode === 'day' ? 'week' : 'day'
|
||||
},
|
||||
onCoachChange(e) {
|
||||
this.coach = this.coaches[e.detail.value]
|
||||
},
|
||||
onPeriodChange(e) {
|
||||
this.period = this.periodOptions[e.detail.value].key
|
||||
},
|
||||
openDetail(course) {
|
||||
navigateToPage(`${PAGE.COURSE_DETAIL}?id=${course.id}`)
|
||||
},
|
||||
quickBook(course) {
|
||||
if (course.full) return
|
||||
navigateToPage(`${PAGE.COURSE_DETAIL}?id=${course.id}`)
|
||||
},
|
||||
goMyBooking() {
|
||||
navigateToPage(PAGE.BOOKING)
|
||||
}
|
||||
}
|
||||
}
|
||||
</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';
|
||||
@import '@/common/style/memberInfo/pages/module-pages-common.css';
|
||||
@import '@/common/style/memberInfo/pages/course-list-page.css';
|
||||
</style>
|
||||
@@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="member-card-page">
|
||||
<MemberInfoSubNav title="我的会员卡" @back="goBack" />
|
||||
<view class="member-card-page__body">
|
||||
<view class="mc-hero">
|
||||
<view class="mc-hero__top">
|
||||
<view class="mc-hero__title-row">
|
||||
<image
|
||||
class="mc-hero__crown"
|
||||
src="/static/images/crown.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="mc-hero__name">{{ card.name }}</text>
|
||||
</view>
|
||||
<view class="mc-hero__badge">
|
||||
<text class="mc-hero__badge-text">{{ card.status }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="mc-hero__validity">有效期:{{ card.validity }}</text>
|
||||
<view class="mc-hero__bottom">
|
||||
<view class="mc-hero__days">
|
||||
<text class="mc-hero__days-num">{{ remainingDays }}</text>
|
||||
<text class="mc-hero__days-unit">天</text>
|
||||
</view>
|
||||
<view
|
||||
class="mc-hero__renew"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="renewCard"
|
||||
>
|
||||
<image
|
||||
class="mc-hero__renew-icon"
|
||||
src="/static/images/refreshcw.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="mc-hero__renew-text">立即续费</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mc-records">
|
||||
<view class="mc-records__header">
|
||||
<text class="mc-records__title">使用记录</text>
|
||||
<view class="mc-records__tabs">
|
||||
<view
|
||||
v-for="tab in recordTabs"
|
||||
:key="tab.key"
|
||||
class="mc-records__tab"
|
||||
:class="{ 'mc-records__tab--active': activeFilter === tab.key }"
|
||||
hover-class="mi-tap-tab--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="switchFilter(tab.key)"
|
||||
>
|
||||
<text
|
||||
class="mc-records__tab-text"
|
||||
:class="{ 'mc-records__tab-text--active': activeFilter === tab.key }"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mc-records__divider"></view>
|
||||
<view
|
||||
v-for="(item, index) in filteredRecords"
|
||||
:key="item.id"
|
||||
class="mc-records__item"
|
||||
>
|
||||
<view v-if="index > 0" class="mc-records__divider"></view>
|
||||
<view
|
||||
class="mc-records__item-inner"
|
||||
hover-class="mi-tap-row--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="showRecordDetail(item)"
|
||||
>
|
||||
<view
|
||||
class="mc-records__icon-wrap"
|
||||
:class="'mc-records__icon-wrap--' + item.iconTheme"
|
||||
>
|
||||
<image
|
||||
class="mc-records__icon"
|
||||
:src="item.icon"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
<view class="mc-records__info">
|
||||
<text class="mc-records__item-title">{{ item.title }}</text>
|
||||
<text class="mc-records__item-time">{{ item.time }}</text>
|
||||
</view>
|
||||
<text
|
||||
class="mc-records__value"
|
||||
:class="'mc-records__value--' + item.valueType"
|
||||
>
|
||||
{{ item.value }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="!filteredRecords.length" class="mc-records__empty">
|
||||
<text class="mc-records__empty-text">暂无记录</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mc-rules">
|
||||
<text class="mc-rules__title">使用规则</text>
|
||||
<view
|
||||
v-for="(rule, index) in rules"
|
||||
:key="index"
|
||||
class="mc-rules__item"
|
||||
>
|
||||
<view class="mc-rules__bullet"></view>
|
||||
<text class="mc-rules__text">{{ rule }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { memberCardMock } from '@/common/memberInfo/mockData.js'
|
||||
import {
|
||||
loadMemberStore,
|
||||
computeRemainingDays,
|
||||
renewMemberCard
|
||||
} from '@/common/memberInfo/store.js'
|
||||
import { subPageMixin } from '@/common/memberInfo/mixins.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
mixins: [subPageMixin],
|
||||
data() {
|
||||
return {
|
||||
card: {},
|
||||
recordTabs: memberCardMock.recordTabs,
|
||||
records: [],
|
||||
rules: memberCardMock.rules,
|
||||
activeFilter: 'all'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredRecords() {
|
||||
if (this.activeFilter === 'all') {
|
||||
return this.records
|
||||
}
|
||||
return this.records.filter((item) => item.type === this.activeFilter)
|
||||
},
|
||||
remainingDays() {
|
||||
return computeRemainingDays(this.card.validityEnd)
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.refreshFromStore()
|
||||
},
|
||||
methods: {
|
||||
refreshFromStore() {
|
||||
const store = loadMemberStore()
|
||||
this.card = { ...store.card }
|
||||
this.records = store.records.map((item) => ({ ...item }))
|
||||
},
|
||||
switchFilter(filter) {
|
||||
this.activeFilter = filter
|
||||
},
|
||||
renewCard() {
|
||||
uni.showModal({
|
||||
title: '续费会员卡',
|
||||
content: '确认续费 90 天?',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
const store = loadMemberStore()
|
||||
renewMemberCard(store, 90)
|
||||
this.activeFilter = 'all'
|
||||
this.refreshFromStore()
|
||||
uni.showToast({ title: '续费成功', icon: 'success' })
|
||||
}
|
||||
})
|
||||
},
|
||||
showRecordDetail(item) {
|
||||
uni.showModal({
|
||||
title: item.title,
|
||||
content: `${item.time}\n变动:${item.value}`,
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</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/member-card-page.css';
|
||||
|
||||
.mc-records__empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32px 16px;
|
||||
}
|
||||
|
||||
.mc-records__empty-text {
|
||||
font-size: 14px;
|
||||
color: #8A99B4;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,240 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="member-page">
|
||||
<MemberInfoHeader
|
||||
:user-info="userInfo"
|
||||
:stats="stats"
|
||||
@user-info="goUserInfo"
|
||||
/>
|
||||
<view class="member-page__body">
|
||||
<view class="member-page__sections">
|
||||
<MemberInfoMemberCard
|
||||
:card-info="cardInfo"
|
||||
@view-all="goMemberCard"
|
||||
@renew="onRenewCard"
|
||||
/>
|
||||
<MemberInfoQuickActions @action="onQuickAction" />
|
||||
<MemberInfoBookingList
|
||||
:items="bookingPreview"
|
||||
@view-all="goBooking"
|
||||
@item-tap="goBooking"
|
||||
/>
|
||||
<MemberInfoCheckInList
|
||||
:items="checkIns"
|
||||
@view-all="onCheckInViewAll"
|
||||
@item-tap="onCheckInTap"
|
||||
/>
|
||||
<MemberInfoBodyReport
|
||||
:report="bodyReport"
|
||||
@view-history="onBodyReportHistory"
|
||||
@view-report="onBodyReportView"
|
||||
/>
|
||||
<MemberInfoCouponPoints
|
||||
:data="couponPoints"
|
||||
@view-all="onCouponViewAll"
|
||||
@use-coupon="onUseCoupon"
|
||||
@redeem-points="onRedeemPoints"
|
||||
/>
|
||||
<MemberInfoReferral
|
||||
:data="referral"
|
||||
@view-rules="onReferralRules"
|
||||
/>
|
||||
<MemberInfoSettings @setting="onSetting" />
|
||||
<MemberInfoLogout @logout="handleLogout" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<TabBar :active="4" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
|
||||
import {
|
||||
loadMemberStore,
|
||||
getCenterPageData,
|
||||
renewMemberCard
|
||||
} from '@/common/memberInfo/store.js'
|
||||
import MemberInfoHeader from '@/components/memberInfo/MemberInfoHeader.vue'
|
||||
import MemberInfoMemberCard from '@/components/memberInfo/MemberInfoMemberCard.vue'
|
||||
import MemberInfoQuickActions from '@/components/memberInfo/MemberInfoQuickActions.vue'
|
||||
import MemberInfoBookingList from '@/components/memberInfo/MemberInfoBookingList.vue'
|
||||
import MemberInfoCheckInList from '@/components/memberInfo/MemberInfoCheckInList.vue'
|
||||
import MemberInfoBodyReport from '@/components/memberInfo/MemberInfoBodyReport.vue'
|
||||
import MemberInfoCouponPoints from '@/components/memberInfo/MemberInfoCouponPoints.vue'
|
||||
import MemberInfoReferral from '@/components/memberInfo/MemberInfoReferral.vue'
|
||||
import MemberInfoSettings from '@/components/memberInfo/MemberInfoSettings.vue'
|
||||
import MemberInfoLogout from '@/components/memberInfo/MemberInfoLogout.vue'
|
||||
import TabBar from '@/components/TabBar.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MemberInfoHeader,
|
||||
MemberInfoMemberCard,
|
||||
MemberInfoQuickActions,
|
||||
MemberInfoBookingList,
|
||||
MemberInfoCheckInList,
|
||||
MemberInfoBodyReport,
|
||||
MemberInfoCouponPoints,
|
||||
MemberInfoReferral,
|
||||
MemberInfoSettings,
|
||||
MemberInfoLogout,
|
||||
TabBar
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
userInfo: {},
|
||||
stats: {},
|
||||
cardInfo: {},
|
||||
bookingPreview: [],
|
||||
checkIns: [],
|
||||
bodyReport: {},
|
||||
couponPoints: {},
|
||||
referral: {}
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.refreshFromStore()
|
||||
},
|
||||
methods: {
|
||||
refreshFromStore() {
|
||||
const store = loadMemberStore()
|
||||
const pageData = getCenterPageData(store)
|
||||
this.userInfo = pageData.userInfo
|
||||
this.stats = pageData.stats
|
||||
this.cardInfo = pageData.cardInfo
|
||||
this.bookingPreview = pageData.bookingPreview
|
||||
this.checkIns = pageData.checkIns
|
||||
this.bodyReport = pageData.bodyReport
|
||||
this.couponPoints = pageData.couponPoints
|
||||
this.referral = pageData.referral
|
||||
},
|
||||
goMemberCard() {
|
||||
navigateToPage(PAGE.MEMBER_CARD)
|
||||
},
|
||||
goBooking() {
|
||||
navigateToPage(PAGE.BOOKING)
|
||||
},
|
||||
goUserInfo() {
|
||||
navigateToPage(PAGE.USER_INFO)
|
||||
},
|
||||
onRenewCard() {
|
||||
uni.showModal({
|
||||
title: '续费会员卡',
|
||||
content: '确认续费 90 天?',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
const store = loadMemberStore()
|
||||
renewMemberCard(store, 90)
|
||||
this.refreshFromStore()
|
||||
uni.showToast({ title: '续费成功', icon: 'success' })
|
||||
}
|
||||
})
|
||||
},
|
||||
onQuickAction(type) {
|
||||
const routes = {
|
||||
booking: PAGE.COURSE_LIST,
|
||||
bodyTest: PAGE.BODY_TEST_HOME,
|
||||
bodyReport: PAGE.BODY_TEST_HISTORY,
|
||||
trainReport: PAGE.TRAIN_REPORT,
|
||||
coupon: PAGE.COUPONS,
|
||||
points: PAGE.POINTS,
|
||||
referral: PAGE.REFERRAL,
|
||||
course: PAGE.MY_COURSES
|
||||
}
|
||||
if (routes[type]) {
|
||||
navigateToPage(routes[type])
|
||||
}
|
||||
},
|
||||
onCheckInViewAll() {
|
||||
navigateToPage(PAGE.CHECK_IN_HISTORY)
|
||||
},
|
||||
onCheckInTap(item) {
|
||||
uni.showModal({
|
||||
title: item.title,
|
||||
content: item.time,
|
||||
showCancel: false
|
||||
})
|
||||
},
|
||||
onBodyReportHistory() {
|
||||
navigateToPage(PAGE.BODY_TEST_HISTORY)
|
||||
},
|
||||
onBodyReportView() {
|
||||
const id = this.bodyReport?.recordId
|
||||
const url = id
|
||||
? `${PAGE.BODY_TEST_REPORT}?id=${id}`
|
||||
: PAGE.BODY_TEST_HISTORY
|
||||
navigateToPage(url)
|
||||
},
|
||||
onCouponViewAll() {
|
||||
navigateToPage(PAGE.COUPONS)
|
||||
},
|
||||
onUseCoupon() {
|
||||
navigateToPage(PAGE.COUPONS)
|
||||
},
|
||||
onRedeemPoints() {
|
||||
navigateToPage(PAGE.POINTS)
|
||||
},
|
||||
onReferralRules() {
|
||||
navigateToPage(PAGE.REFERRAL)
|
||||
},
|
||||
onSetting(key) {
|
||||
const labels = {
|
||||
notify: '通知设置',
|
||||
password: '修改密码',
|
||||
privacy: '隐私政策',
|
||||
nfc: 'NFC 门禁卡',
|
||||
delete: '注销账户'
|
||||
}
|
||||
if (key === 'delete') {
|
||||
uni.showModal({
|
||||
title: '注销账户',
|
||||
content: '注销后数据将无法恢复,确定继续吗?',
|
||||
confirmColor: '#E74C3C'
|
||||
})
|
||||
return
|
||||
}
|
||||
if (key === 'privacy') {
|
||||
uni.showModal({
|
||||
title: '隐私政策',
|
||||
content: '我们重视您的隐私,详细政策请前往官网查看。',
|
||||
showCancel: false
|
||||
})
|
||||
return
|
||||
}
|
||||
uni.showToast({
|
||||
title: `${labels[key] || '设置'}开发中`,
|
||||
icon: 'none'
|
||||
})
|
||||
},
|
||||
handleLogout() {
|
||||
uni.showModal({
|
||||
title: '退出登录',
|
||||
content: '确定要退出登录吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showToast({ title: '已退出', icon: 'success' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import '@/common/style/base.css';
|
||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||
@import '@/common/style/memberInfo/member-info-page.css';
|
||||
@import '@/common/style/memberInfo/member-info-header.css';
|
||||
@import '@/common/style/memberInfo/member-info-member-card.css';
|
||||
@import '@/common/style/memberInfo/member-info-quick-actions.css';
|
||||
@import '@/common/style/memberInfo/member-info-booking-list.css';
|
||||
@import '@/common/style/memberInfo/member-info-check-in-list.css';
|
||||
@import '@/common/style/memberInfo/member-info-body-report.css';
|
||||
@import '@/common/style/memberInfo/member-info-coupon-points.css';
|
||||
@import '@/common/style/memberInfo/member-info-referral.css';
|
||||
@import '@/common/style/memberInfo/member-info-settings.css';
|
||||
@import '@/common/style/memberInfo/member-info-logout.css';
|
||||
</style>
|
||||
@@ -0,0 +1,225 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page">
|
||||
<MemberInfoSubNav title="我的课程" @back="goBack" />
|
||||
<view class="mi-mod-tabs">
|
||||
<view
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="bt-tab"
|
||||
:class="{ 'bt-tab--active': activeTab === tab.key }"
|
||||
hover-class="mi-tap-tab--hover"
|
||||
@tap="activeTab = tab.key"
|
||||
>
|
||||
<text class="bt-tab__text">{{ tab.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bt-page__body">
|
||||
<!-- 团课 -->
|
||||
<template v-if="activeTab === 'group'">
|
||||
<view class="mi-mod-subtabs">
|
||||
<text
|
||||
class="mi-mod-subtab"
|
||||
:class="{ 'mi-mod-subtab--on': groupSub === 'ongoing' }"
|
||||
@tap="groupSub = 'ongoing'"
|
||||
>进行中</text>
|
||||
<text
|
||||
class="mi-mod-subtab"
|
||||
:class="{ 'mi-mod-subtab--on': groupSub === 'completed' }"
|
||||
@tap="groupSub = 'completed'"
|
||||
>已完成</text>
|
||||
</view>
|
||||
<view
|
||||
v-for="item in groupList"
|
||||
:key="item.id"
|
||||
class="mi-mod-course-card"
|
||||
hover-class="mi-tap-card--hover"
|
||||
@tap="onGroupCourse(item)"
|
||||
>
|
||||
<image class="mi-mod-course-card__banner" :src="item.banner" mode="aspectFill" />
|
||||
<view class="mi-mod-course-card__content">
|
||||
<text class="mi-mod-course-card__title">{{ item.title }}</text>
|
||||
<text class="mi-mod-course-card__coach">{{ item.coach }}</text>
|
||||
<view class="mi-mod-course-card__progress">
|
||||
<view class="mi-mod-course-card__progress-bar">
|
||||
<view class="mi-mod-course-card__progress-fill" :style="{ width: pct(item) + '%' }"></view>
|
||||
</view>
|
||||
<text class="mi-mod-course-card__progress-text">{{ item.progress }}/{{ item.total }}</text>
|
||||
</view>
|
||||
<text class="mi-mod-course-card__meta">{{ item.schedule }} · {{ item.location }}</text>
|
||||
<view v-if="groupSub === 'ongoing' && item.canCancel" class="mi-mod-course-card__action" @tap.stop="goBooking">
|
||||
<text>取消预约</text>
|
||||
</view>
|
||||
<view v-if="groupSub === 'completed' && item.canEvaluate" class="mi-mod-course-card__action" @tap.stop="evaluate(item)">
|
||||
<text>去评价</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 私教 -->
|
||||
<template v-else-if="activeTab === 'private'">
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">剩余课时 {{ privateData.remaining }} 节</text>
|
||||
<view class="mi-detail-coach">
|
||||
<image class="mi-detail-coach__avatar" :src="privateData.coachAvatar" mode="aspectFill" />
|
||||
<view>
|
||||
<text class="mi-detail-coach__name">{{ privateData.coach }}</text>
|
||||
<text class="mi-detail-coach__rating">下次 {{ privateData.nextClass }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">已约课程</text>
|
||||
<view v-for="b in privateData.bookings" :key="b.id" class="mi-mod-session">
|
||||
<text class="mi-mod-session__title">{{ b.title }}</text>
|
||||
<text class="mi-mod-session__meta">{{ b.time }} · {{ b.location }} · {{ b.status }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 线上课 -->
|
||||
<template v-else-if="activeTab === 'online'">
|
||||
<view
|
||||
v-for="item in onlineList"
|
||||
:key="item.id"
|
||||
class="mi-mod-course-card"
|
||||
hover-class="mi-tap-card--hover"
|
||||
@tap="goOnline(item)"
|
||||
>
|
||||
<image class="mi-mod-course-card__banner" :src="item.cover" mode="aspectFill" />
|
||||
<view class="mi-mod-course-card__content">
|
||||
<text class="mi-mod-course-card__title">{{ item.title }}</text>
|
||||
<text class="mi-mod-course-card__meta">{{ item.duration }} · 进度 {{ item.progress }}%</text>
|
||||
<text v-if="item.type === 'live'" class="mi-mod-course-card__next">直播 {{ item.liveTime }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 训练营 -->
|
||||
<template v-else>
|
||||
<view
|
||||
v-for="item in packageList"
|
||||
:key="item.id"
|
||||
class="mi-mod-course-card"
|
||||
>
|
||||
<image class="mi-mod-course-card__banner" :src="item.banner" mode="aspectFill" />
|
||||
<view class="mi-mod-course-card__content">
|
||||
<text class="mi-mod-course-card__title">{{ item.title }}</text>
|
||||
<text class="mi-mod-course-card__coach">{{ item.coach }}</text>
|
||||
<view class="mi-mod-course-card__progress">
|
||||
<view class="mi-mod-course-card__progress-bar">
|
||||
<view class="mi-mod-course-card__progress-fill" :style="{ width: pct(item) + '%' }"></view>
|
||||
</view>
|
||||
<text class="mi-mod-course-card__progress-text">{{ item.progress }}/{{ item.total }} 节</text>
|
||||
</view>
|
||||
<text class="mi-mod-course-card__meta">{{ item.schedule }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view v-if="isEmpty" 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 } from '@/common/constants/routes.js'
|
||||
import { moduleMock, getMyCoursesData } from '@/common/memberInfo/moduleStore.js'
|
||||
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||
import { subPageMixin } from '@/common/memberInfo/mixins.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
mixins: [subPageMixin],
|
||||
data() {
|
||||
return {
|
||||
tabs: moduleMock.myCourseTabs,
|
||||
activeTab: 'group',
|
||||
groupSub: 'ongoing',
|
||||
privateData: {},
|
||||
onlineList: [],
|
||||
packageList: [],
|
||||
groupData: { ongoing: [], completed: [] }
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
groupList() {
|
||||
return this.groupData[this.groupSub] || []
|
||||
},
|
||||
isEmpty() {
|
||||
if (this.activeTab === 'group') return !this.groupList.length
|
||||
if (this.activeTab === 'private') return !this.privateData.bookings?.length
|
||||
if (this.activeTab === 'online') return !this.onlineList.length
|
||||
return !this.packageList.length
|
||||
}
|
||||
},
|
||||
onShow() { this.refresh() },
|
||||
methods: {
|
||||
refresh() {
|
||||
const store = loadMemberStore()
|
||||
this.groupData = getMyCoursesData(store, 'group')
|
||||
this.privateData = getMyCoursesData(store, 'private')
|
||||
this.onlineList = getMyCoursesData(store, 'online').list || []
|
||||
this.packageList = getMyCoursesData(store, 'package').list || []
|
||||
},
|
||||
pct(item) {
|
||||
return item.total ? Math.min(100, Math.round((item.progress / item.total) * 100)) : 0
|
||||
},
|
||||
goBooking() { navigateToPage(PAGE.BOOKING) },
|
||||
goOnline(item) { navigateToPage(`${PAGE.ONLINE_COURSE}?id=${item.id}`) },
|
||||
evaluate(item) {
|
||||
navigateToPage(`${PAGE.COURSE_EVALUATE}?title=${encodeURIComponent(item.title)}`)
|
||||
},
|
||||
onGroupCourse(item) {
|
||||
uni.showModal({
|
||||
title: item.title,
|
||||
content: `${item.coach}\n${item.schedule}\n${item.location}`,
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</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';
|
||||
@import '@/common/style/memberInfo/pages/module-pages-common.css';
|
||||
|
||||
.mi-mod-subtabs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.mi-mod-subtab {
|
||||
font-size: 14px;
|
||||
color: var(--text-muted, #5E6F8D);
|
||||
}
|
||||
|
||||
.mi-mod-subtab--on {
|
||||
color: var(--primary-dark, #0B2B4B);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.mi-mod-course-card__action {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.mi-mod-course-card__action text {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--accent-orange, #FF6B35);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page" v-if="course">
|
||||
<MemberInfoSubNav title="线上课程" @back="onBack" />
|
||||
<image class="mi-detail-hero" :src="course.cover" mode="aspectFill" />
|
||||
<view class="bt-page__body">
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">{{ course.title }}</text>
|
||||
<text class="bt-card__desc">时长 {{ course.duration }} · 进度 {{ course.progress }}%</text>
|
||||
<view class="mi-mod-course-card__progress" style="margin-top:12px;">
|
||||
<view class="mi-mod-course-card__progress-bar">
|
||||
<view class="mi-mod-course-card__progress-fill" :style="{ width: course.progress + '%' }"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">视频播放</text>
|
||||
<view class="mi-online-player">
|
||||
<image class="mi-online-player__play" src="/static/images/play.png" mode="aspectFit" />
|
||||
<text class="mi-online-player__hint">点击播放(支持倍速与拖拽)</text>
|
||||
</view>
|
||||
<view class="mi-online-controls">
|
||||
<text class="mi-online-controls__btn" @tap="seek(-10)">-10s</text>
|
||||
<text class="mi-online-controls__btn" @tap="togglePlay">{{ playing ? '暂停' : '播放' }}</text>
|
||||
<text class="mi-online-controls__btn" @tap="seek(10)">+10s</text>
|
||||
<text class="mi-online-controls__btn" @tap="changeSpeed">倍速 {{ speed }}x</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">课程目录</text>
|
||||
<view v-for="ch in course.chapters" :key="ch.id" class="mi-online-chapter">
|
||||
<text class="mi-online-chapter__title">{{ ch.title }}</text>
|
||||
<text class="mi-online-chapter__meta">{{ ch.duration }} · {{ ch.done ? '已学完' : '未学习' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { PAGE, goBackOrTab } from '@/common/constants/routes.js'
|
||||
import { loadMemberStore, persistMemberStore } from '@/common/memberInfo/store.js'
|
||||
import { getOnlineCourseById, updateOnlineProgress } from '@/common/memberInfo/moduleStore.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
data() {
|
||||
return { course: null, playing: false, speed: 1 }
|
||||
},
|
||||
onLoad(options) {
|
||||
this.course = getOnlineCourseById(loadMemberStore(), options?.id)
|
||||
},
|
||||
methods: {
|
||||
onBack() { goBackOrTab(PAGE.MY_COURSES) },
|
||||
togglePlay() {
|
||||
this.playing = !this.playing
|
||||
if (this.playing && this.course.progress < 100) {
|
||||
const store = loadMemberStore()
|
||||
updateOnlineProgress(store, this.course.id, Math.min(100, this.course.progress + 5))
|
||||
persistMemberStore(store)
|
||||
this.course.progress = Math.min(100, this.course.progress + 5)
|
||||
}
|
||||
},
|
||||
seek(sec) { uni.showToast({ title: `跳转 ${sec > 0 ? '+' : ''}${sec}s`, icon: 'none' }) },
|
||||
changeSpeed() {
|
||||
const speeds = [1, 1.25, 1.5, 2]
|
||||
const idx = (speeds.indexOf(this.speed) + 1) % speeds.length
|
||||
this.speed = speeds[idx]
|
||||
}
|
||||
}
|
||||
}
|
||||
</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/pages/body-test-common.css';
|
||||
@import '@/common/style/memberInfo/pages/module-pages-common.css';
|
||||
|
||||
.mi-online-player {
|
||||
height: 180px;
|
||||
border-radius: 12px;
|
||||
background: #000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mi-online-player__play { width: 48px; height: 48px; }
|
||||
.mi-online-player__hint { font-size: 12px; color: rgba(255,255,255,0.7); }
|
||||
|
||||
.mi-online-controls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.mi-online-controls__btn {
|
||||
font-size: 12px;
|
||||
color: var(--primary-deep, #1A4A6F);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mi-online-chapter {
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid var(--border-light, #E9EDF2);
|
||||
}
|
||||
|
||||
.mi-online-chapter__title { font-size: 14px; color: var(--text-dark, #1E2A3A); display: block; }
|
||||
.mi-online-chapter__meta { font-size: 11px; color: var(--text-muted, #5E6F8D); }
|
||||
</style>
|
||||
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page">
|
||||
<MemberInfoSubNav title="我的积分" @back="goBack" />
|
||||
<view class="bt-page__body">
|
||||
<view class="mi-mod-points-hero">
|
||||
<text class="mi-mod-points-hero__label">当前积分</text>
|
||||
<text class="mi-mod-points-hero__value">{{ balance }}</text>
|
||||
<text class="mi-mod-points-hero__tip">{{ config.rate }} · {{ config.rule }}</text>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">快捷入口</text>
|
||||
<view class="bt-grid">
|
||||
<view class="bt-grid__item" hover-class="mi-tap--hover" @tap="goMall">
|
||||
<image class="bt-grid__icon" src="/static/images/star.png" mode="aspectFit" />
|
||||
<text class="bt-grid__label">积分商城</text>
|
||||
</view>
|
||||
<view class="bt-grid__item" hover-class="mi-tap--hover" @tap="goHistory">
|
||||
<image class="bt-grid__icon" src="/static/images/clock.png" mode="aspectFit" />
|
||||
<text class="bt-grid__label">积分明细</text>
|
||||
</view>
|
||||
<view class="bt-grid__item" hover-class="mi-tap--hover" @tap="checkIn">
|
||||
<image class="bt-grid__icon" src="/static/images/usercheck.png" mode="aspectFit" />
|
||||
<text class="bt-grid__label">签到赚积分</text>
|
||||
</view>
|
||||
<view class="bt-grid__item" hover-class="mi-tap--hover" @tap="goReferral">
|
||||
<image class="bt-grid__icon" src="/static/images/share2.png" mode="aspectFit" />
|
||||
<text class="bt-grid__label">邀请赚积分</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<view class="bt-trend-link" style="border:none;margin:0;padding:0;" @tap="goHistory">
|
||||
<text class="bt-card__title">最近明细</text>
|
||||
<text class="bt-trend-link__text">查看全部</text>
|
||||
</view>
|
||||
<view v-for="item in historyPreview" :key="item.id" class="mi-mod-points-row">
|
||||
<view class="mi-mod-points-row__info">
|
||||
<text class="mi-mod-points-row__title">{{ item.title }}</text>
|
||||
<text class="mi-mod-points-row__time">{{ item.time }}</text>
|
||||
</view>
|
||||
<text
|
||||
class="mi-mod-points-row__amount"
|
||||
:class="item.amount > 0 ? 'mi-mod-points-row__amount--earn' : 'mi-mod-points-row__amount--spend'"
|
||||
>{{ item.amount > 0 ? '+' : '' }}{{ item.amount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
|
||||
import { getPointsPageData } from '@/common/memberInfo/moduleStore.js'
|
||||
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||
import { subPageMixin } from '@/common/memberInfo/mixins.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
mixins: [subPageMixin],
|
||||
data() {
|
||||
return { balance: 0, config: {}, historyPreview: [] }
|
||||
},
|
||||
onShow() {
|
||||
const data = getPointsPageData(loadMemberStore())
|
||||
this.balance = data.balance
|
||||
this.config = data.config
|
||||
this.historyPreview = data.history.slice(0, 5)
|
||||
},
|
||||
methods: {
|
||||
goMall() { navigateToPage(PAGE.POINTS_MALL) },
|
||||
goHistory() { navigateToPage(PAGE.POINTS_HISTORY) },
|
||||
goReferral() { navigateToPage(PAGE.REFERRAL) },
|
||||
checkIn() { uni.showToast({ title: '签到成功 +10 积分', icon: 'success' }) }
|
||||
}
|
||||
}
|
||||
</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';
|
||||
@import '@/common/style/memberInfo/pages/module-pages-common.css';
|
||||
</style>
|
||||
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page">
|
||||
<MemberInfoSubNav title="积分明细" @back="onBack" />
|
||||
<view class="mi-mod-tabs">
|
||||
<view
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="bt-tab"
|
||||
:class="{ 'bt-tab--active': filter === tab.key }"
|
||||
@tap="filter = tab.key"
|
||||
>
|
||||
<text class="bt-tab__text">{{ tab.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bt-page__body">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mi-mod-points-row"
|
||||
>
|
||||
<view class="mi-mod-points-row__info">
|
||||
<text class="mi-mod-points-row__title">{{ item.title }}</text>
|
||||
<text class="mi-mod-points-row__time">{{ item.time }}</text>
|
||||
</view>
|
||||
<view class="mi-mod-points-row__right">
|
||||
<text
|
||||
class="mi-mod-points-row__amount"
|
||||
:class="item.amount > 0 ? 'mi-mod-points-row__amount--earn' : 'mi-mod-points-row__amount--spend'"
|
||||
>
|
||||
{{ item.amount > 0 ? '+' : '' }}{{ item.amount }}
|
||||
</text>
|
||||
<text class="mi-mod-points-row__balance">余额 {{ item.balance }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="!list.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, goBackOrTab } from '@/common/constants/routes.js'
|
||||
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||
import { filterPointsHistory } from '@/common/memberInfo/moduleStore.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
data() {
|
||||
return {
|
||||
filter: 'all',
|
||||
tabs: [
|
||||
{ key: 'all', label: '全部' },
|
||||
{ key: 'earn', label: '获取' },
|
||||
{ key: 'spend', label: '消耗' }
|
||||
],
|
||||
list: []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
filter() { this.loadList() }
|
||||
},
|
||||
onShow() { this.loadList() },
|
||||
methods: {
|
||||
onBack() { goBackOrTab(PAGE.POINTS) },
|
||||
loadList() {
|
||||
const store = loadMemberStore()
|
||||
this.list = filterPointsHistory(store, this.filter)
|
||||
}
|
||||
}
|
||||
}
|
||||
</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';
|
||||
@import '@/common/style/memberInfo/pages/module-pages-common.css';
|
||||
</style>
|
||||
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page">
|
||||
<MemberInfoSubNav title="积分商城" @back="onBack" />
|
||||
<view class="bt-page__body">
|
||||
<view class="mi-mod-points-hero">
|
||||
<text class="mi-mod-points-hero__label">可用积分</text>
|
||||
<text class="mi-mod-points-hero__value">{{ balance }}</text>
|
||||
</view>
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">兑换商品</text>
|
||||
<view class="mi-mod-rewards">
|
||||
<view
|
||||
v-for="item in rewards"
|
||||
:key="item.id"
|
||||
class="mi-mod-reward"
|
||||
hover-class="mi-tap-card--hover"
|
||||
@tap="redeem(item)"
|
||||
>
|
||||
<image class="mi-mod-reward__icon" :src="item.icon" mode="aspectFit" />
|
||||
<text class="mi-mod-reward__name">{{ item.name }}</text>
|
||||
<text class="mi-mod-reward__cost">{{ item.cost }} 积分</text>
|
||||
<text class="mi-mod-reward__stock">剩余 {{ item.stock }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="redeemRecords.length" class="bt-card">
|
||||
<text class="bt-card__title">兑换记录</text>
|
||||
<view v-for="r in redeemRecords" :key="r.id" class="mi-mod-points-row">
|
||||
<view class="mi-mod-points-row__info">
|
||||
<text class="mi-mod-points-row__title">{{ r.name }}</text>
|
||||
<text class="mi-mod-points-row__time">{{ r.time }}</text>
|
||||
</view>
|
||||
<text class="mi-mod-points-row__amount mi-mod-points-row__amount--spend">-{{ r.cost }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { PAGE, goBackOrTab } from '@/common/constants/routes.js'
|
||||
import { loadMemberStore, persistMemberStore } from '@/common/memberInfo/store.js'
|
||||
import { getPointsPageData, redeemPointsReward } from '@/common/memberInfo/moduleStore.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
data() {
|
||||
return { balance: 0, rewards: [], redeemRecords: [] }
|
||||
},
|
||||
onShow() {
|
||||
const data = getPointsPageData(loadMemberStore())
|
||||
this.balance = data.balance
|
||||
this.rewards = data.rewards
|
||||
this.redeemRecords = data.redeemRecords
|
||||
},
|
||||
methods: {
|
||||
onBack() { goBackOrTab(PAGE.POINTS) },
|
||||
redeem(item) {
|
||||
uni.showModal({
|
||||
title: '确认兑换',
|
||||
content: `使用 ${item.cost} 积分兑换「${item.name}」?`,
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
const store = loadMemberStore()
|
||||
const result = redeemPointsReward(store, item.id)
|
||||
uni.showToast({ title: result.message, icon: result.ok ? 'success' : 'none' })
|
||||
if (result.ok) {
|
||||
persistMemberStore(store)
|
||||
const data = getPointsPageData(store)
|
||||
this.balance = data.balance
|
||||
this.rewards = data.rewards
|
||||
this.redeemRecords = data.redeemRecords
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</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';
|
||||
@import '@/common/style/memberInfo/pages/module-pages-common.css';
|
||||
</style>
|
||||
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page">
|
||||
<MemberInfoSubNav title="邀请好友" @back="goBack" />
|
||||
<view class="bt-page__body">
|
||||
<view class="mi-mod-referral-hero">
|
||||
<text class="mi-mod-referral-hero__title">邀请好友一起健身</text>
|
||||
<text class="mi-mod-referral-hero__desc">好友注册/购课,双方均可获得积分奖励</text>
|
||||
<view class="mi-mod-referral-code">
|
||||
<text class="mi-mod-referral-code__label">我的邀请码</text>
|
||||
<text class="mi-mod-referral-code__value">{{ data.code }}</text>
|
||||
</view>
|
||||
<view class="bt-hero__actions">
|
||||
<view
|
||||
class="bt-btn bt-btn--primary"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="copyCode"
|
||||
>
|
||||
<text class="bt-btn__text">复制邀请码</text>
|
||||
</view>
|
||||
<view
|
||||
class="bt-btn bt-btn--ghost"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="shareInvite"
|
||||
>
|
||||
<image class="bt-btn__icon" src="/static/images/share2.png" mode="aspectFit" />
|
||||
<text class="bt-btn__text">分享给好友</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">邀请成果</text>
|
||||
<view class="mi-mod-referral-stats">
|
||||
<view class="mi-mod-referral-stat">
|
||||
<text class="mi-mod-referral-stat__num mi-mod-referral-stat__num--orange">{{ data.invited }}</text>
|
||||
<text class="mi-mod-referral-stat__label">已推荐</text>
|
||||
</view>
|
||||
<view class="mi-mod-referral-stat">
|
||||
<text class="mi-mod-referral-stat__num mi-mod-referral-stat__num--green">{{ data.registered }}</text>
|
||||
<text class="mi-mod-referral-stat__label">已注册</text>
|
||||
</view>
|
||||
<view class="mi-mod-referral-stat">
|
||||
<text class="mi-mod-referral-stat__num mi-mod-referral-stat__num--amber">{{ data.purchased }}</text>
|
||||
<text class="mi-mod-referral-stat__label">已购课</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">已获得奖励</text>
|
||||
<view class="mi-mod-referral-stats">
|
||||
<view class="mi-mod-referral-stat">
|
||||
<text class="mi-mod-referral-stat__num mi-mod-referral-stat__num--orange">{{ data.rewardSummary?.totalPoints || 0 }}</text>
|
||||
<text class="mi-mod-referral-stat__label">积分</text>
|
||||
</view>
|
||||
<view class="mi-mod-referral-stat">
|
||||
<text class="mi-mod-referral-stat__num mi-mod-referral-stat__num--green">{{ data.rewardSummary?.totalCoupons || 0 }}</text>
|
||||
<text class="mi-mod-referral-stat__label">优惠券</text>
|
||||
</view>
|
||||
<view class="mi-mod-referral-stat">
|
||||
<text class="mi-mod-referral-stat__num mi-mod-referral-stat__num--amber">{{ data.rewardSummary?.pendingCount || 0 }}</text>
|
||||
<text class="mi-mod-referral-stat__label">待发放</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">邀请记录</text>
|
||||
<view
|
||||
v-for="item in data.records"
|
||||
:key="item.id"
|
||||
class="mi-mod-referral-row"
|
||||
>
|
||||
<view class="mi-mod-referral-row__info">
|
||||
<text class="mi-mod-referral-row__name">{{ item.name }}</text>
|
||||
<text class="mi-mod-referral-row__time">{{ item.time }}</text>
|
||||
</view>
|
||||
<view class="mi-mod-referral-row__right">
|
||||
<text class="mi-mod-referral-row__status">{{ item.statusLabel }}</text>
|
||||
<text class="mi-mod-referral-row__reward">{{ item.reward }}</text>
|
||||
<text class="mi-mod-referral-row__reward-status">{{ item.rewardStatus }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">活动规则</text>
|
||||
<view class="bt-advice-list">
|
||||
<view
|
||||
v-for="(rule, idx) in data.rules"
|
||||
:key="idx"
|
||||
class="bt-advice-item"
|
||||
>
|
||||
<view class="bt-advice-item__dot"></view>
|
||||
<text class="bt-advice-item__text">{{ rule }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { getReferralPageData } from '@/common/memberInfo/moduleStore.js'
|
||||
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||
import { subPageMixin } from '@/common/memberInfo/mixins.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
mixins: [subPageMixin],
|
||||
data() {
|
||||
return {
|
||||
data: { code: '', invited: 0, registered: 0, purchased: 0, records: [], rules: [] }
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
const store = loadMemberStore()
|
||||
this.data = getReferralPageData(store)
|
||||
},
|
||||
methods: {
|
||||
copyCode() {
|
||||
uni.setClipboardData({
|
||||
data: this.data.code,
|
||||
success: () => uni.showToast({ title: '邀请码已复制', icon: 'success' })
|
||||
})
|
||||
},
|
||||
shareInvite() {
|
||||
uni.showActionSheet({
|
||||
itemList: ['分享给微信好友', '生成邀请海报', '复制分享链接'],
|
||||
success: (res) => {
|
||||
const msgs = ['已唤起分享', '海报已生成', '链接已复制']
|
||||
uni.showToast({ title: msgs[res.tapIndex] || '分享成功', icon: 'none' })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</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';
|
||||
@import '@/common/style/memberInfo/pages/module-pages-common.css';
|
||||
|
||||
.mi-mod-referral-hero .bt-hero__actions {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page">
|
||||
<MemberInfoSubNav title="训练报告" @back="goBack" />
|
||||
<view class="mi-mod-tabs">
|
||||
<view
|
||||
v-for="p in periods"
|
||||
:key="p.key"
|
||||
class="bt-tab"
|
||||
:class="{ 'bt-tab--active': period === p.key }"
|
||||
@tap="switchPeriod(p.key)"
|
||||
>
|
||||
<text class="bt-tab__text">{{ p.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bt-page__body">
|
||||
<view class="bt-metrics">
|
||||
<view class="bt-metric"><text class="bt-metric__value">{{ report.summary.sessions }}</text><text class="bt-metric__label">完成课程</text></view>
|
||||
<view class="bt-metric"><text class="bt-metric__value">{{ report.summary.hours }}</text><text class="bt-metric__label">运动时长(h)</text></view>
|
||||
<view class="bt-metric"><text class="bt-metric__value">{{ report.summary.calories }}</text><text class="bt-metric__label">消耗(kcal)</text></view>
|
||||
<view class="bt-metric"><text class="bt-metric__value">{{ report.summary.visits }}</text><text class="bt-metric__label">到店次数</text></view>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">运动时长趋势</text>
|
||||
<BodyTestTrendChart :points="report.trendHours" unit="h" :width="chartWidth" :height="150" />
|
||||
</view>
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">消耗卡路里趋势</text>
|
||||
<BodyTestTrendChart :points="report.trendCalories" unit="" :width="chartWidth" :height="150" />
|
||||
</view>
|
||||
|
||||
<view class="mi-mod-tabs">
|
||||
<view
|
||||
v-for="t in typeFilters"
|
||||
:key="t.key"
|
||||
class="bt-tab"
|
||||
:class="{ 'bt-tab--active': typeFilter === t.key }"
|
||||
@tap="typeFilter = t.key"
|
||||
>
|
||||
<text class="bt-tab__text">{{ t.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">课程完成列表</text>
|
||||
<view
|
||||
v-for="item in sessions"
|
||||
:key="item.id"
|
||||
class="mi-mod-session"
|
||||
hover-class="mi-tap-row--hover"
|
||||
@tap="goSession(item)"
|
||||
>
|
||||
<view class="mi-mod-session__head">
|
||||
<text class="mi-mod-session__title">{{ item.title }}</text>
|
||||
<view class="mi-mod-session__tag" :class="'mi-mod-session__tag--' + item.type">
|
||||
<text class="mi-mod-session__tag-text">{{ item.typeLabel }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="mi-mod-session__meta">{{ item.date }} · {{ item.time }} · {{ item.coach }}</text>
|
||||
<view class="mi-mod-session__footer">
|
||||
<text class="mi-mod-session__stat">{{ item.duration }}</text>
|
||||
<text class="mi-mod-session__stat">{{ item.calories }} kcal</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import BodyTestTrendChart from '@/components/memberInfo/BodyTestTrendChart.vue'
|
||||
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
|
||||
import { getTrainingReportData, filterTrainingSessions } from '@/common/memberInfo/moduleStore.js'
|
||||
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||
import { subPageMixin } from '@/common/memberInfo/mixins.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav, BodyTestTrendChart },
|
||||
mixins: [subPageMixin],
|
||||
data() {
|
||||
return {
|
||||
period: 'week',
|
||||
periods: [
|
||||
{ key: 'week', label: '本周' },
|
||||
{ key: 'month', label: '本月' }
|
||||
],
|
||||
typeFilter: 'all',
|
||||
typeFilters: [
|
||||
{ key: 'all', label: '全部' },
|
||||
{ key: 'group', label: '团课' },
|
||||
{ key: 'private', label: '私教' },
|
||||
{ key: 'free', label: '自由' }
|
||||
],
|
||||
report: { summary: {}, trendHours: [], trendCalories: [], sessions: [] },
|
||||
sessions: [],
|
||||
chartWidth: 300
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.chartWidth = uni.getSystemInfoSync().windowWidth - 64
|
||||
this.refresh()
|
||||
},
|
||||
onShow() { this.refresh() },
|
||||
methods: {
|
||||
refresh() {
|
||||
const store = loadMemberStore()
|
||||
this.report = getTrainingReportData(store, this.period)
|
||||
this.sessions = filterTrainingSessions(store, { type: this.typeFilter })
|
||||
},
|
||||
switchPeriod(key) {
|
||||
this.period = key
|
||||
this.refresh()
|
||||
},
|
||||
goSession(item) {
|
||||
navigateToPage(`${PAGE.TRAIN_SESSION}?id=${item.id}`)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
typeFilter() {
|
||||
this.sessions = filterTrainingSessions(loadMemberStore(), { type: this.typeFilter })
|
||||
}
|
||||
}
|
||||
}
|
||||
</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';
|
||||
@import '@/common/style/memberInfo/pages/module-pages-common.css';
|
||||
</style>
|
||||
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page" v-if="session">
|
||||
<MemberInfoSubNav title="训练详情" @back="onBack" />
|
||||
<view class="bt-page__body">
|
||||
<view class="bt-score-card">
|
||||
<view class="bt-score-card__info">
|
||||
<text class="bt-score-card__title">{{ session.title }}</text>
|
||||
<text class="bt-score-card__date">{{ session.date }} {{ session.time }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">训练数据</text>
|
||||
<view class="bt-metrics">
|
||||
<view class="bt-metric"><text class="bt-metric__value">{{ session.duration }}</text><text class="bt-metric__label">时长</text></view>
|
||||
<view class="bt-metric"><text class="bt-metric__value">{{ session.calories }}</text><text class="bt-metric__label">消耗(kcal)</text></view>
|
||||
<view class="bt-metric"><text class="bt-metric__value">{{ session.heartRate }}</text><text class="bt-metric__label">平均心率</text></view>
|
||||
<view class="bt-metric"><text class="bt-metric__value">{{ session.typeLabel }}</text><text class="bt-metric__label">类型</text></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">签到时间</text>
|
||||
<text class="bt-card__desc">{{ session.checkInTime }}</text>
|
||||
</view>
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">教练评语</text>
|
||||
<text class="bt-card__desc">{{ session.comment }}</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 { getTrainingSessionById } from '@/common/memberInfo/moduleStore.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
data() { return { session: null } },
|
||||
onLoad(options) {
|
||||
this.session = getTrainingSessionById(loadMemberStore(), options?.id)
|
||||
},
|
||||
methods: {
|
||||
onBack() { goBackOrTab(PAGE.TRAIN_REPORT) }
|
||||
}
|
||||
}
|
||||
</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-sub-nav.css';
|
||||
@import '@/common/style/memberInfo/pages/body-test-common.css';
|
||||
</style>
|
||||
@@ -0,0 +1,483 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="Pixso-frame-2_791">
|
||||
<MemberInfoSubNav title="个人信息" @back="goBack" />
|
||||
<view class="Pixso-frame-2_802">
|
||||
<view class="frame-content-2_802">
|
||||
<view class="avatar-block">
|
||||
<view class="avatar-block__inner">
|
||||
<image
|
||||
class="avatar-block__photo"
|
||||
:key="avatarKey"
|
||||
:src="avatarSrc"
|
||||
mode="aspectFill"
|
||||
@tap="previewAvatar"
|
||||
/>
|
||||
<view
|
||||
class="avatar-block__change"
|
||||
hover-class="mi-tap--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap.stop="changeAvatar"
|
||||
>
|
||||
<image
|
||||
class="avatar-block__icon"
|
||||
src="/static/images/camera.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="avatar-block__text">更换</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="Pixso-frame-2_810">
|
||||
<view class="frame-content-2_810">
|
||||
<view
|
||||
class="Pixso-frame-2_811 user-info-row"
|
||||
hover-class="mi-tap-row--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="editName"
|
||||
>
|
||||
<view class="frame-content-2_811">
|
||||
<text class="Pixso-paragraph-2_812">姓名</text>
|
||||
<text class="Pixso-paragraph-2_813">{{ name }}</text>
|
||||
<image
|
||||
class="Pixso-vector-2_814"
|
||||
src="/static/images/chevronright1.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="Pixso-frame-2_816"></view>
|
||||
<view
|
||||
class="Pixso-frame-2_817 user-info-row"
|
||||
hover-class="mi-tap-row--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="rebindPhone"
|
||||
>
|
||||
<view class="frame-content-2_817">
|
||||
<text class="Pixso-paragraph-2_818">手机号</text>
|
||||
<view class="Pixso-frame-2_819">
|
||||
<view class="frame-content-2_819">
|
||||
<text class="Pixso-paragraph-2_820">{{ displayPhone }}</text>
|
||||
<view
|
||||
class="Pixso-frame-2_821"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap.stop="rebindPhone"
|
||||
>
|
||||
<text class="Pixso-paragraph-2_822">换绑</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<image
|
||||
class="Pixso-vector-2_823"
|
||||
src="/static/images/chevronright.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="Pixso-frame-2_825"></view>
|
||||
<view class="Pixso-frame-2_826">
|
||||
<view class="frame-content-2_826">
|
||||
<text class="Pixso-paragraph-2_827">性别</text>
|
||||
<view class="Pixso-frame-2_828">
|
||||
<view class="frame-content-2_828">
|
||||
<view
|
||||
class="gender-btn"
|
||||
:class="{ 'gender-btn--active': gender === 'female' }"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="selectGender('female')"
|
||||
>
|
||||
<image
|
||||
class="gender-btn__icon"
|
||||
src="/static/images/venus.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="gender-btn__text">女</text>
|
||||
</view>
|
||||
<view
|
||||
class="gender-btn"
|
||||
:class="{ 'gender-btn--active': gender === 'male' }"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="selectGender('male')"
|
||||
>
|
||||
<image
|
||||
class="gender-btn__icon"
|
||||
src="/static/images/mars.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="gender-btn__text">男</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="Pixso-frame-2_841"></view>
|
||||
<picker mode="date" :value="birthdayValue" @change="onBirthdayChange">
|
||||
<view
|
||||
class="Pixso-frame-2_842 user-info-row"
|
||||
hover-class="mi-tap-row--hover"
|
||||
:hover-stay-time="150"
|
||||
>
|
||||
<view class="frame-content-2_842">
|
||||
<text class="Pixso-paragraph-2_843">生日</text>
|
||||
<text class="Pixso-paragraph-2_844">{{ birthday }}</text>
|
||||
<image
|
||||
class="Pixso-vector-2_845"
|
||||
src="/static/images/chevronright0.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</picker>
|
||||
<view class="Pixso-frame-2_847"></view>
|
||||
<view class="Pixso-frame-2_848">
|
||||
<view class="frame-content-2_848">
|
||||
<text class="Pixso-paragraph-2_849">身高</text>
|
||||
<view
|
||||
class="Pixso-frame-2_850 user-info-measure"
|
||||
hover-class="mi-tap-row--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="editHeight"
|
||||
>
|
||||
<view class="frame-content-2_850">
|
||||
<text class="Pixso-paragraph-2_851">{{ height }}</text>
|
||||
<text class="Pixso-paragraph-2_852">cm</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="Pixso-frame-2_853"></view>
|
||||
<text class="Pixso-paragraph-2_854">体重</text>
|
||||
<view
|
||||
class="Pixso-frame-2_855 user-info-measure"
|
||||
hover-class="mi-tap-row--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="editWeight"
|
||||
>
|
||||
<view class="frame-content-2_855">
|
||||
<text class="Pixso-paragraph-2_856">{{ weight }}</text>
|
||||
<text class="Pixso-paragraph-2_857">kg</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="Pixso-frame-2_858"></view>
|
||||
<view class="Pixso-frame-2_859">
|
||||
<view class="frame-content-2_859">
|
||||
<text class="Pixso-paragraph-2_860">微信</text>
|
||||
<text class="Pixso-paragraph-2_861">已授权绑定</text>
|
||||
<view class="stroke-wrapper-2_862">
|
||||
<view class="Pixso-frame-2_862">
|
||||
<text class="Pixso-paragraph-2_863">已绑定</text>
|
||||
</view>
|
||||
<view class="stroke-2_862"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="Pixso-frame-2_864">
|
||||
<view class="frame-content-2_864">
|
||||
<text class="Pixso-paragraph-2_865">健身目标</text>
|
||||
<view class="goal-tags">
|
||||
<view
|
||||
v-for="goal in fitnessGoalOptions"
|
||||
:key="goal"
|
||||
class="goal-tag"
|
||||
:class="{ 'goal-tag--selected': isGoalSelected(goal) }"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="toggleGoal(goal)"
|
||||
>
|
||||
<text class="goal-tag__text">{{ goal }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="user-info-save-bar">
|
||||
<view
|
||||
class="user-info-save-bar__btn"
|
||||
hover-class="mi-tap-save--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="handleSave"
|
||||
>
|
||||
<text class="user-info-save-bar__text">保存</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { fitnessGoalOptions } from '@/common/memberInfo/mockData.js'
|
||||
import { loadMemberStore, saveUserProfile } from '@/common/memberInfo/store.js'
|
||||
import { previewImage, persistChosenImage } from '@/common/memberInfo/media.js'
|
||||
import { maskPhone, normalizePhoneForStore } from '@/common/memberInfo/format.js'
|
||||
import { subPageMixin } from '@/common/memberInfo/mixins.js'
|
||||
import {
|
||||
validateName,
|
||||
validatePhoneForRebind,
|
||||
validateHeight,
|
||||
validateWeight,
|
||||
validateBirthday,
|
||||
validateFitnessGoals,
|
||||
validateUserProfile,
|
||||
showValidationError
|
||||
} from '@/common/memberInfo/validate.js'
|
||||
|
||||
const DEFAULT_AVATAR = '/static/images/AvatarEditWrap.png'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
mixins: [subPageMixin],
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
phone: '',
|
||||
gender: 'female',
|
||||
birthday: '',
|
||||
height: '',
|
||||
weight: '',
|
||||
fitnessGoals: [],
|
||||
avatar: DEFAULT_AVATAR,
|
||||
avatarKey: 0,
|
||||
avatarDirty: false,
|
||||
fitnessGoalOptions
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
avatarSrc() {
|
||||
return this.avatar || DEFAULT_AVATAR
|
||||
},
|
||||
displayPhone() {
|
||||
return maskPhone(this.phone)
|
||||
},
|
||||
birthdayValue() {
|
||||
const match = String(this.birthday).match(/(\d{4})年(\d{2})月(\d{2})日/)
|
||||
if (match) {
|
||||
return `${match[1]}-${match[2]}-${match[3]}`
|
||||
}
|
||||
return '1995-06-15'
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
// 从相册/相机返回会再次触发 onShow,不能覆盖刚选未保存的头像
|
||||
this.loadProfile({ preserveLocalAvatar: true })
|
||||
},
|
||||
methods: {
|
||||
loadProfile(options = {}) {
|
||||
const store = loadMemberStore()
|
||||
const profile = store.profile
|
||||
this.name = profile.name
|
||||
this.phone = profile.phone
|
||||
this.gender = profile.gender
|
||||
this.birthday = profile.birthday
|
||||
this.height = profile.height
|
||||
this.weight = profile.weight
|
||||
this.fitnessGoals = [...(profile.fitnessGoals || [])]
|
||||
|
||||
const storedAvatar = profile.avatar || DEFAULT_AVATAR
|
||||
const hasUnsavedLocalAvatar =
|
||||
options.preserveLocalAvatar &&
|
||||
this.avatarDirty &&
|
||||
this.avatar &&
|
||||
this.avatar !== storedAvatar
|
||||
|
||||
if (!hasUnsavedLocalAvatar) {
|
||||
this.setAvatar(storedAvatar)
|
||||
this.avatarDirty = false
|
||||
}
|
||||
},
|
||||
setAvatar(path) {
|
||||
const next = path || DEFAULT_AVATAR
|
||||
if (this.avatar !== next) {
|
||||
this.avatar = next
|
||||
this.avatarKey += 1
|
||||
}
|
||||
},
|
||||
getProfilePayload() {
|
||||
return {
|
||||
name: this.name,
|
||||
phone: normalizePhoneForStore(this.phone),
|
||||
gender: this.gender,
|
||||
birthday: this.birthday,
|
||||
height: this.height,
|
||||
weight: this.weight,
|
||||
fitnessGoals: [...this.fitnessGoals],
|
||||
avatar: this.avatar
|
||||
}
|
||||
},
|
||||
handleSave() {
|
||||
const result = validateUserProfile(
|
||||
this.getProfilePayload(),
|
||||
this.fitnessGoalOptions
|
||||
)
|
||||
if (!result.ok) {
|
||||
showValidationError(result.message)
|
||||
return
|
||||
}
|
||||
|
||||
const store = loadMemberStore()
|
||||
saveUserProfile(store, result.value)
|
||||
this.applyValidatedProfile(result.value)
|
||||
this.avatarDirty = false
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
setTimeout(() => this.goBack(), 600)
|
||||
},
|
||||
applyValidatedProfile(profile) {
|
||||
this.name = profile.name
|
||||
this.phone = profile.phone
|
||||
this.gender = profile.gender
|
||||
this.birthday = profile.birthday
|
||||
this.height = profile.height
|
||||
this.weight = profile.weight
|
||||
this.fitnessGoals = [...profile.fitnessGoals]
|
||||
},
|
||||
changeAvatar() {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: (res) => {
|
||||
const tempPath =
|
||||
res.tempFilePaths?.[0] || res.tempFiles?.[0]?.tempFilePath
|
||||
if (!tempPath) return
|
||||
|
||||
// 真机先用 tempFilePath 立即展示,避免 saveFile 异步期间被 onShow 覆盖
|
||||
this.setAvatar(tempPath)
|
||||
this.avatarDirty = true
|
||||
uni.showToast({ title: '头像已选择', icon: 'success' })
|
||||
|
||||
persistChosenImage(tempPath).then((savedPath) => {
|
||||
if (savedPath && savedPath !== this.avatar) {
|
||||
this.setAvatar(savedPath)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
previewAvatar() {
|
||||
previewImage(this.avatarSrc, DEFAULT_AVATAR)
|
||||
},
|
||||
editName() {
|
||||
uni.showModal({
|
||||
title: '修改姓名',
|
||||
editable: true,
|
||||
placeholderText: '请输入姓名(2-8字)',
|
||||
content: this.name,
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
const result = validateName(res.content)
|
||||
if (!result.ok) {
|
||||
showValidationError(result.message)
|
||||
return
|
||||
}
|
||||
this.name = result.value
|
||||
}
|
||||
})
|
||||
},
|
||||
rebindPhone() {
|
||||
uni.showModal({
|
||||
title: '换绑手机号',
|
||||
editable: true,
|
||||
placeholderText: '请输入11位手机号',
|
||||
content: normalizePhoneForStore(this.phone) || this.phone,
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
const result = validatePhoneForRebind(res.content)
|
||||
if (!result.ok) {
|
||||
showValidationError(result.message)
|
||||
return
|
||||
}
|
||||
this.phone = result.value
|
||||
uni.showToast({ title: '手机号已更新', icon: 'success' })
|
||||
}
|
||||
})
|
||||
},
|
||||
selectGender(gender) {
|
||||
this.gender = gender
|
||||
},
|
||||
onBirthdayChange(e) {
|
||||
const value = e.detail.value
|
||||
const [y, m, d] = value.split('-')
|
||||
const formatted = `${y}年${m}月${d}日`
|
||||
const result = validateBirthday(formatted)
|
||||
if (!result.ok) {
|
||||
showValidationError(result.message)
|
||||
return
|
||||
}
|
||||
this.birthday = result.value
|
||||
},
|
||||
editHeight() {
|
||||
uni.showModal({
|
||||
title: '修改身高',
|
||||
editable: true,
|
||||
placeholderText: '50-250,单位 cm',
|
||||
content: String(this.height),
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
const result = validateHeight(res.content)
|
||||
if (!result.ok) {
|
||||
showValidationError(result.message)
|
||||
return
|
||||
}
|
||||
this.height = result.value
|
||||
}
|
||||
})
|
||||
},
|
||||
editWeight() {
|
||||
uni.showModal({
|
||||
title: '修改体重',
|
||||
editable: true,
|
||||
placeholderText: '20-300,单位 kg',
|
||||
content: String(this.weight),
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
const result = validateWeight(res.content)
|
||||
if (!result.ok) {
|
||||
showValidationError(result.message)
|
||||
return
|
||||
}
|
||||
this.weight = result.value
|
||||
}
|
||||
})
|
||||
},
|
||||
toggleGoal(goal) {
|
||||
const index = this.fitnessGoals.indexOf(goal)
|
||||
if (index >= 0) {
|
||||
this.fitnessGoals.splice(index, 1)
|
||||
return
|
||||
}
|
||||
const preview = [...this.fitnessGoals, goal]
|
||||
const result = validateFitnessGoals(preview, this.fitnessGoalOptions)
|
||||
if (!result.ok) {
|
||||
showValidationError(result.message)
|
||||
return
|
||||
}
|
||||
this.fitnessGoals.push(goal)
|
||||
},
|
||||
isGoalSelected(goal) {
|
||||
return this.fitnessGoals.includes(goal)
|
||||
}
|
||||
}
|
||||
}
|
||||
</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/user-info-page.css';
|
||||
@import '@/common/style/memberInfo/pages/user-info-pixso.css';
|
||||
|
||||
.user-info-row,
|
||||
.user-info-measure {
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<view class="tab-page">
|
||||
<view class="tab-page__header">
|
||||
<text class="tab-page__title">训练</text>
|
||||
<text class="tab-page__subtitle">记录每一次进步</text>
|
||||
</view>
|
||||
|
||||
<view class="train-cards">
|
||||
<view class="train-card" hover-class="train-card--hover" @tap="goTrainReport">
|
||||
<text class="train-card__title">训练报告</text>
|
||||
<text class="train-card__desc">查看本周/本月运动数据与趋势</text>
|
||||
</view>
|
||||
<view class="train-card" hover-class="train-card--hover" @tap="goBodyTest">
|
||||
<text class="train-card__title">智能体测</text>
|
||||
<text class="train-card__desc">连接设备,获取专业体测报告</text>
|
||||
</view>
|
||||
<view class="train-card" hover-class="train-card--hover" @tap="goBodyHistory">
|
||||
<text class="train-card__title">体测报告</text>
|
||||
<text class="train-card__desc">历史记录与对比分析</text>
|
||||
</view>
|
||||
<view class="train-card" hover-class="train-card--hover" @tap="goCheckIn">
|
||||
<text class="train-card__title">签到记录</text>
|
||||
<text class="train-card__desc">到店打卡与训练频次</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bottom-placeholder"></view>
|
||||
<TabBar :active="2" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import TabBar from '@/components/TabBar.vue'
|
||||
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
|
||||
|
||||
function goTrainReport() {
|
||||
navigateToPage(PAGE.TRAIN_REPORT)
|
||||
}
|
||||
|
||||
function goBodyTest() {
|
||||
navigateToPage(PAGE.BODY_TEST_HOME)
|
||||
}
|
||||
|
||||
function goBodyHistory() {
|
||||
navigateToPage(PAGE.BODY_TEST_HISTORY)
|
||||
}
|
||||
|
||||
function goCheckIn() {
|
||||
navigateToPage(PAGE.CHECK_IN_HISTORY)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tab-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f0f4f8;
|
||||
padding-bottom: 160rpx;
|
||||
}
|
||||
|
||||
.tab-page__header {
|
||||
padding: 48rpx 32rpx 16rpx;
|
||||
}
|
||||
|
||||
.tab-page__title {
|
||||
display: block;
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #1a202c;
|
||||
}
|
||||
|
||||
.tab-page__subtitle {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.train-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
}
|
||||
|
||||
.train-card {
|
||||
padding: 28rpx 32rpx;
|
||||
border-radius: 24rpx;
|
||||
background: #fff;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.train-card__title {
|
||||
display: block;
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #1e2a3a;
|
||||
}
|
||||
|
||||
.train-card__desc {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.bottom-placeholder {
|
||||
height: 40rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,239 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const CSS_DIR = path.join(__dirname, '../common/style/memberInfo/pages');
|
||||
const STYLE_PREFIX = '@/common/style/memberInfo/pages';
|
||||
|
||||
const PAGES = {
|
||||
booking: {
|
||||
cssFile: 'booking-pixso.css',
|
||||
pageCssFile: 'booking-page.css',
|
||||
rootClass: 'Pixso-frame-2_964',
|
||||
backFrameClass: 'Pixso-frame-2_969',
|
||||
extraData: `,
|
||||
activeTab: 'ongoing'`,
|
||||
extraMethods: `,
|
||||
switchTab(tab) {
|
||||
this.activeTab = tab
|
||||
},
|
||||
cancelBooking() {
|
||||
uni.showModal({
|
||||
title: '取消预约',
|
||||
content: '确定要取消该预约吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showToast({ title: '已取消', icon: 'success' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}`
|
||||
},
|
||||
memberCard: {
|
||||
cssFile: 'member-card-pixso.css',
|
||||
pageCssFile: 'member-card-page.css',
|
||||
rootClass: 'Pixso-frame-2_877',
|
||||
backFrameClass: 'Pixso-frame-2_882',
|
||||
extraData: `,
|
||||
activeFilter: 'all'`,
|
||||
extraMethods: `,
|
||||
switchFilter(filter) {
|
||||
this.activeFilter = filter
|
||||
},
|
||||
renewCard() {
|
||||
uni.showToast({ title: '续费功能开发中', icon: 'none' })
|
||||
}`
|
||||
},
|
||||
userInfo: {
|
||||
cssFile: 'user-info-pixso.css',
|
||||
pageCssFile: 'user-info-page.css',
|
||||
rootClass: 'Pixso-frame-2_791',
|
||||
backFrameClass: 'Pixso-frame-2_796',
|
||||
navWrapperClass: 'stroke-wrapper-2_795',
|
||||
navContentClass: 'frame-content-2_795',
|
||||
navTitleClass: 'Pixso-paragraph-2_799',
|
||||
saveFrameClass: 'Pixso-frame-2_800',
|
||||
extraData: `,
|
||||
name: '张小芳',
|
||||
phone: '138****6789',
|
||||
gender: 'female',
|
||||
birthday: '1995年06月15日',
|
||||
height: '165',
|
||||
weight: '63.5',
|
||||
fitnessGoals: ['增肌']`,
|
||||
extraMethods: `,
|
||||
handleSave() {
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
},
|
||||
changeAvatar() {
|
||||
uni.showToast({ title: '更换头像功能开发中', icon: 'none' })
|
||||
},
|
||||
rebindPhone() {
|
||||
uni.showToast({ title: '换绑功能开发中', icon: 'none' })
|
||||
},
|
||||
selectGender(gender) {
|
||||
this.gender = gender
|
||||
},
|
||||
toggleGoal(goal) {
|
||||
const index = this.fitnessGoals.indexOf(goal)
|
||||
if (index >= 0) {
|
||||
this.fitnessGoals.splice(index, 1)
|
||||
} else {
|
||||
this.fitnessGoals.push(goal)
|
||||
}
|
||||
},
|
||||
isGoalSelected(goal) {
|
||||
return this.fitnessGoals.includes(goal)
|
||||
}`
|
||||
}
|
||||
};
|
||||
|
||||
function alignCss(css) {
|
||||
let result = css.replace(/@import\s+['"]\.\/base\.css['"];\s*/g, '');
|
||||
const replacements = [
|
||||
['url(@/assets/images/', 'url(@/static/images/'],
|
||||
['box-shadow: 0px 8px 16px 0px rgba(255, 107, 53, 0.25098039215686274)', 'box-shadow: var(--shadow-orange-glow)'],
|
||||
['background-color: rgba(255, 255, 255, 1)', 'background-color: var(--bg-white)'],
|
||||
['color: rgba(255, 255, 255, 1)', 'color: var(--text-inverse)'],
|
||||
['border-color: rgba(255, 255, 255, 1)', 'border-color: var(--bg-white)'],
|
||||
['rgba(11, 43, 75, 1)', 'var(--primary-dark)'],
|
||||
['rgba(26, 74, 111, 1)', 'var(--primary-deep)'],
|
||||
['rgba(255, 107, 53, 1)', 'var(--accent-orange)'],
|
||||
['rgba(255, 140, 90, 1)', 'var(--accent-orange-light)'],
|
||||
['rgba(249, 250, 254, 1)', 'var(--bg-light)'],
|
||||
['rgba(30, 42, 58, 1)', 'var(--text-dark)'],
|
||||
['rgba(94, 111, 141, 1)', 'var(--text-muted)'],
|
||||
['rgba(138, 153, 180, 1)', 'var(--text-light)'],
|
||||
['rgba(233, 237, 242, 1)', 'var(--border-light)'],
|
||||
['rgba(46, 204, 113, 1)', 'var(--success-green)'],
|
||||
['rgba(231, 76, 60, 1)', 'var(--error-red)'],
|
||||
['height: 900px', 'height: auto;\n min-height: 100%'],
|
||||
['height: 2300px', 'height: auto;\n min-height: 100%'],
|
||||
['height: 1901px', 'height: auto']
|
||||
];
|
||||
for (const [from, to] of replacements) {
|
||||
result = result.split(from).join(to);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function convertTemplate(template, config) {
|
||||
let result = template.replace(/\r\n/g, '\n');
|
||||
|
||||
result = result.replace(/<div\b/g, '<view');
|
||||
result = result.replace(/<\/div>/g, '</view>');
|
||||
result = result.replace(/<p\b/g, '<text');
|
||||
result = result.replace(/<\/p>/g, '</text>');
|
||||
result = result.replace(/\s+id="[^"]*"/g, '');
|
||||
result = result.replace(/\{\{\s*"([^"]*?)"\s*\}\}/gs, '$1');
|
||||
result = result.replace(/\{\{\s*'([^']*?)'\s*\}\}/gs, '$1');
|
||||
result = result.replace(
|
||||
'<view class="scroll-container">',
|
||||
'<view class="scroll-container theme-light">'
|
||||
);
|
||||
result = result.replace(/>\s*9:41\s*</g, '>{{ statusBarTime }}<');
|
||||
|
||||
if (config.backFrameClass) {
|
||||
result = result.replace(
|
||||
new RegExp(`class="${config.backFrameClass}"`, 'g'),
|
||||
`class="${config.backFrameClass} nav-back" @tap.stop="goBack"`
|
||||
);
|
||||
}
|
||||
|
||||
if (config.navTitleClass) {
|
||||
result = result.replace(
|
||||
new RegExp(`class="${config.navTitleClass}"`, 'g'),
|
||||
`class="${config.navTitleClass} nav-title"`
|
||||
);
|
||||
}
|
||||
|
||||
if (config.saveFrameClass) {
|
||||
result = result.replace(
|
||||
new RegExp(`class="${config.saveFrameClass}"`, 'g'),
|
||||
`class="${config.saveFrameClass}" @tap="handleSave"`
|
||||
);
|
||||
}
|
||||
|
||||
if (config.bindings) {
|
||||
for (const { from, to } of config.bindings) {
|
||||
result = result.replace(from, to);
|
||||
}
|
||||
}
|
||||
|
||||
return result.trim();
|
||||
}
|
||||
|
||||
function buildPageCss(config) {
|
||||
return `@import './sub-page-base.css';
|
||||
|
||||
.${config.rootClass} {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function convertPage(pageName, config) {
|
||||
const vuePath = path.join(__dirname, `../pages/memberInfo/${pageName}.vue`);
|
||||
const cssPath = path.join(CSS_DIR, config.cssFile);
|
||||
const pageCssPath = path.join(CSS_DIR, config.pageCssFile);
|
||||
|
||||
const content = fs.readFileSync(vuePath, 'utf8').replace(/\r\n/g, '\n');
|
||||
const templateMatch = content.match(/<template>([\s\S]*?)<\/template>/);
|
||||
const styleMatch = content.match(/<style>([\s\S]*?)<\/style>/);
|
||||
|
||||
if (!templateMatch || !styleMatch) {
|
||||
throw new Error(`Could not parse ${pageName}.vue`);
|
||||
}
|
||||
|
||||
const template = convertTemplate(templateMatch[1], config);
|
||||
const css = alignCss(styleMatch[1].trim());
|
||||
const pageCss = buildPageCss(config);
|
||||
|
||||
const extraMethods = (config.extraMethods || '').replace(/^\s*,\s*/, '');
|
||||
const extraDataBlock = (config.extraData || '').replace(/^\s*,\s*/, '');
|
||||
const dataBody = pageName === 'userInfo'
|
||||
? '...userInfoMock'
|
||||
: extraDataBlock;
|
||||
|
||||
const vueOutput = `<template>
|
||||
${template}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { statusBarTimeMixin, subPageMixin } from '@/common/memberInfo/mixins.js'${pageName === 'userInfo' ? "\nimport { userInfoMock } from '@/common/memberInfo/mockData.js'" : ''}
|
||||
|
||||
export default {
|
||||
mixins: [statusBarTimeMixin, subPageMixin],
|
||||
data() {
|
||||
return {
|
||||
${dataBody}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
${extraMethods}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import '${STYLE_PREFIX}/${config.pageCssFile}';
|
||||
@import '${STYLE_PREFIX}/${config.cssFile}';
|
||||
</style>
|
||||
`;
|
||||
|
||||
fs.writeFileSync(cssPath, css + '\n', 'utf8');
|
||||
fs.writeFileSync(pageCssPath, pageCss, 'utf8');
|
||||
fs.writeFileSync(vuePath, vueOutput, 'utf8');
|
||||
console.log(`converted ${pageName}.vue -> ${config.cssFile}`);
|
||||
}
|
||||
|
||||
const target = process.argv[2];
|
||||
const entries = target
|
||||
? [[target, PAGES[target]]].filter(([, config]) => config)
|
||||
: Object.entries(PAGES);
|
||||
|
||||
for (const [pageName, config] of entries) {
|
||||
convertPage(pageName, config);
|
||||
}
|
||||
|
||||
console.log('done');
|
||||
@@ -0,0 +1,293 @@
|
||||
/**
|
||||
|
||||
* 微信小程序组件/页面 WXSS 不支持本地 background-image,
|
||||
|
||||
* 将 /static/images/ 背景图改为 <image> 标签,并清理 CSS。
|
||||
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
const path = require('path');
|
||||
|
||||
|
||||
|
||||
const root = path.join(__dirname, '..');
|
||||
|
||||
|
||||
|
||||
const COMPONENT_MAP = {
|
||||
|
||||
MemberInfoStatusBar: 'member-info-status-bar.css',
|
||||
|
||||
MemberInfoHeader: 'member-info-header.css',
|
||||
|
||||
MemberInfoMemberCard: 'member-info-member-card.css',
|
||||
|
||||
MemberInfoQuickActions: 'member-info-quick-actions.css',
|
||||
|
||||
MemberInfoBookingList: 'member-info-booking-list.css',
|
||||
|
||||
MemberInfoCheckInList: 'member-info-check-in-list.css',
|
||||
|
||||
MemberInfoBodyReport: 'member-info-body-report.css',
|
||||
|
||||
MemberInfoCouponPoints: 'member-info-coupon-points.css',
|
||||
|
||||
MemberInfoReferral: 'member-info-referral.css',
|
||||
|
||||
MemberInfoSettings: 'member-info-settings.css',
|
||||
|
||||
MemberInfoLogout: 'member-info-logout.css'
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
const PAGE_MAP = {
|
||||
|
||||
booking: ['booking-pixso.css'],
|
||||
|
||||
memberCard: ['member-card-pixso.css'],
|
||||
|
||||
userInfo: ['user-info-pixso.css']
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
function extractBgImages(css) {
|
||||
|
||||
const map = new Map();
|
||||
|
||||
const ruleRe = /^\.([a-zA-Z0-9_-]+)\s*\{([^}]*)\}/gm;
|
||||
|
||||
let match;
|
||||
|
||||
while ((match = ruleRe.exec(css)) !== null) {
|
||||
|
||||
const className = match[1];
|
||||
|
||||
const body = match[2];
|
||||
|
||||
const urlMatch = body.match(/background-image:\s*url\(\/static\/images\/([^)]+)\)/);
|
||||
|
||||
if (urlMatch) {
|
||||
|
||||
map.set(className, `/static/images/${urlMatch[1]}`);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return map;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
function ensureImageDisplay(css, className) {
|
||||
|
||||
const ruleRe = new RegExp(`(\\.${className.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\{)([^}]*)(\\})`);
|
||||
|
||||
return css.replace(ruleRe, (full, head, body, tail) => {
|
||||
|
||||
if (/display:\s*block/.test(body)) return full;
|
||||
|
||||
return `${head}${body.trim()}\n display: block;\n${tail}`;
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
function getMode(className) {
|
||||
|
||||
if (/avatar|banner|photo|card-preview|AC\d/i.test(className)) return 'aspectFill';
|
||||
|
||||
return 'aspectFit';
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
function replaceViewWithImage(template, className, src) {
|
||||
|
||||
const mode = getMode(className);
|
||||
|
||||
const imageTag = `<image class="${className}" src="${src}" mode="${mode}" />`;
|
||||
|
||||
const escaped = className.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
|
||||
|
||||
const patterns = [
|
||||
|
||||
new RegExp(`<view\\s+class="${escaped}"\\s*></view>`, 'g'),
|
||||
|
||||
new RegExp(`<view\\s+class="${escaped}"\\s*/>`, 'g'),
|
||||
|
||||
new RegExp(`<view\\s+class="${escaped}"\\s*>\\s*</view>`, 'g'),
|
||||
|
||||
new RegExp(`<view\\s+\\n\\s*class="${escaped}"\\s*\\n\\s*></view>`, 'g'),
|
||||
|
||||
new RegExp(`<view\\s+\\n\\s*class="${escaped}"\\s*\\n\\s*/>`, 'g'),
|
||||
|
||||
new RegExp(`<view\\s+[^>]*class="${escaped}"[^>]*>\\s*</view>`, 'g')
|
||||
|
||||
];
|
||||
|
||||
|
||||
|
||||
let result = template;
|
||||
|
||||
for (const re of patterns) {
|
||||
|
||||
result = result.replace(re, imageTag);
|
||||
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
function stripAllLocalBgFromCss(css) {
|
||||
|
||||
let next = css;
|
||||
|
||||
next = next.replace(/background-image:\s*url\(\/static\/images\/[^)]+\);/g, '');
|
||||
|
||||
next = next.replace(/\n\s*background-size:\s*100%\s*100%;/g, '');
|
||||
|
||||
next = next.replace(/\n\s*background-repeat:\s*no-repeat;/g, '');
|
||||
|
||||
return next;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
function convertPair(vuePath, cssPath) {
|
||||
|
||||
if (!fs.existsSync(vuePath) || !fs.existsSync(cssPath)) return;
|
||||
|
||||
|
||||
|
||||
let css = fs.readFileSync(cssPath, 'utf8');
|
||||
|
||||
const hadLocalBg = /background-image:\s*url\(\/static\/images\//.test(css);
|
||||
|
||||
if (!hadLocalBg) return;
|
||||
|
||||
|
||||
|
||||
const bgMap = extractBgImages(css);
|
||||
|
||||
let template = fs.readFileSync(vuePath, 'utf8');
|
||||
|
||||
const templateMatch = template.match(/<template>([\s\S]*?)<\/template>/);
|
||||
|
||||
if (!templateMatch) {
|
||||
|
||||
css = stripAllLocalBgFromCss(css);
|
||||
|
||||
fs.writeFileSync(cssPath, css, 'utf8');
|
||||
|
||||
console.log('css-only (no template)', path.relative(root, cssPath));
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
let inner = templateMatch[1];
|
||||
|
||||
let changed = false;
|
||||
|
||||
|
||||
|
||||
for (const [className, src] of bgMap.entries()) {
|
||||
|
||||
const before = inner;
|
||||
|
||||
inner = replaceViewWithImage(inner, className, src);
|
||||
|
||||
if (inner !== before) {
|
||||
|
||||
changed = true;
|
||||
|
||||
css = ensureImageDisplay(css, className);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
css = stripAllLocalBgFromCss(css);
|
||||
|
||||
|
||||
|
||||
if (changed) {
|
||||
|
||||
template = template.replace(templateMatch[1], inner);
|
||||
|
||||
fs.writeFileSync(vuePath, template, 'utf8');
|
||||
|
||||
}
|
||||
|
||||
fs.writeFileSync(cssPath, css, 'utf8');
|
||||
|
||||
console.log(
|
||||
|
||||
changed ? 'converted' : 'css-only',
|
||||
|
||||
path.relative(root, vuePath),
|
||||
|
||||
`(${bgMap.size} rules stripped)`
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
for (const [name, cssFile] of Object.entries(COMPONENT_MAP)) {
|
||||
|
||||
convertPair(
|
||||
|
||||
path.join(root, 'components/memberInfo', `${name}.vue`),
|
||||
|
||||
path.join(root, 'common/style/memberInfo', cssFile)
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
for (const [page, cssFiles] of Object.entries(PAGE_MAP)) {
|
||||
|
||||
for (const cssFile of cssFiles) {
|
||||
|
||||
convertPair(
|
||||
|
||||
path.join(root, 'pages/memberInfo', `${page}.vue`),
|
||||
|
||||
path.join(root, 'common/style/memberInfo/pages', cssFile)
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
console.log('done');
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const dir = path.join(__dirname, '../components/memberInfo');
|
||||
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.vue'));
|
||||
|
||||
for (const file of files) {
|
||||
let content = fs.readFileSync(path.join(dir, file), 'utf8');
|
||||
content = content.replace(
|
||||
/export default \{\n\s+options:/,
|
||||
'export default {\n options:'
|
||||
);
|
||||
fs.writeFileSync(path.join(dir, file), content, 'utf8');
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const dir = path.join(__dirname, '../components/memberInfo');
|
||||
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.vue'));
|
||||
|
||||
const optionsBlock = ` options: {
|
||||
virtualHost: true,
|
||||
styleIsolation: 'apply-shared'
|
||||
},`;
|
||||
|
||||
for (const file of files) {
|
||||
let content = fs.readFileSync(path.join(dir, file), 'utf8');
|
||||
content = content.replace(/\r\n/g, '\n');
|
||||
content = content.replace(/\n<style>[\s\S]*?<\/style>\n?/g, '\n');
|
||||
|
||||
if (content.includes('options:')) {
|
||||
content = content.replace(/options:\s*\{[\s\S]*?\},?/m, optionsBlock);
|
||||
} else {
|
||||
content = content.replace(/export default \{\n/, `export default {\n${optionsBlock}\n`);
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.join(dir, file), content.endsWith('\n') ? content : content + '\n', 'utf8');
|
||||
console.log('updated', file);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const root = path.join(__dirname, '..');
|
||||
const styleDir = path.join(root, 'common/style/memberInfo');
|
||||
const pagesDir = path.join(styleDir, 'pages');
|
||||
const componentsDir = path.join(root, 'components/memberInfo');
|
||||
|
||||
const cssMap = {
|
||||
MemberInfoStatusBar: 'member-info-status-bar.css',
|
||||
MemberInfoHeader: 'member-info-header.css',
|
||||
MemberInfoMemberCard: 'member-info-member-card.css',
|
||||
MemberInfoQuickActions: 'member-info-quick-actions.css',
|
||||
MemberInfoBookingList: 'member-info-booking-list.css',
|
||||
MemberInfoCheckInList: 'member-info-check-in-list.css',
|
||||
MemberInfoBodyReport: 'member-info-body-report.css',
|
||||
MemberInfoCouponPoints: 'member-info-coupon-points.css',
|
||||
MemberInfoReferral: 'member-info-referral.css',
|
||||
MemberInfoSettings: 'member-info-settings.css',
|
||||
MemberInfoLogout: 'member-info-logout.css'
|
||||
};
|
||||
|
||||
function stripCssImports(filePath) {
|
||||
let css = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
||||
const next = css.replace(/^@import\s+[^;]+;\s*\n/gm, '');
|
||||
if (next !== css) {
|
||||
fs.writeFileSync(filePath, next.replace(/^\n+/, ''), 'utf8');
|
||||
console.log('stripped imports:', path.relative(root, filePath));
|
||||
}
|
||||
}
|
||||
|
||||
// 纯样式文件:去掉嵌套 @import(小程序只认 vue 里第一层 @import)
|
||||
for (const name of fs.readdirSync(styleDir)) {
|
||||
if (!name.endsWith('.css') || name === 'member-info-all.css') continue;
|
||||
stripCssImports(path.join(styleDir, name));
|
||||
}
|
||||
for (const name of fs.readdirSync(pagesDir)) {
|
||||
if (!name.endsWith('.css')) continue;
|
||||
stripCssImports(path.join(pagesDir, name));
|
||||
}
|
||||
|
||||
const componentStyleBlock = (cssFile) => `<style>
|
||||
@import '@/common/style/base.css';
|
||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||
@import '@/common/style/memberInfo/${cssFile}';
|
||||
</style>
|
||||
`;
|
||||
|
||||
for (const [name, cssFile] of Object.entries(cssMap)) {
|
||||
const filePath = path.join(componentsDir, `${name}.vue`);
|
||||
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
||||
content = content.replace(/\n<style>[\s\S]*?<\/style>\n?/g, '\n');
|
||||
content = content.trimEnd() + '\n' + componentStyleBlock(cssFile);
|
||||
fs.writeFileSync(filePath, content, 'utf8');
|
||||
console.log('updated component:', name);
|
||||
}
|
||||
|
||||
const memberInfoVue = path.join(root, 'pages/memberInfo/memberInfo.vue');
|
||||
let memberInfoContent = fs.readFileSync(memberInfoVue, 'utf8').replace(/\r\n/g, '\n');
|
||||
memberInfoContent = memberInfoContent.replace(
|
||||
/<style>[\s\S]*?<\/style>/,
|
||||
`<style>
|
||||
@import '@/common/style/base.css';
|
||||
@import '@/common/style/memberInfo/member-info-page.css';
|
||||
</style>`
|
||||
);
|
||||
fs.writeFileSync(memberInfoVue, memberInfoContent, 'utf8');
|
||||
console.log('updated memberInfo.vue');
|
||||
|
||||
const subPages = {
|
||||
booking: ['booking-page.css', 'booking-pixso.css'],
|
||||
memberCard: ['member-card-page.css', 'member-card-pixso.css'],
|
||||
userInfo: ['user-info-page.css', 'user-info-pixso.css']
|
||||
};
|
||||
|
||||
for (const [page, files] of Object.entries(subPages)) {
|
||||
const vuePath = path.join(root, `pages/memberInfo/${page}.vue`);
|
||||
let content = fs.readFileSync(vuePath, 'utf8').replace(/\r\n/g, '\n');
|
||||
const imports = [
|
||||
"@import '@/common/style/base.css';",
|
||||
"@import '@/common/style/memberInfo/pages/page-reset.css';",
|
||||
"@import '@/common/style/memberInfo/pages/sub-page-base.css';",
|
||||
...files.map((f) => `@import '@/common/style/memberInfo/pages/${f}';`)
|
||||
].join('\n');
|
||||
content = content.replace(/<style>[\s\S]*?<\/style>/, `<style>\n${imports}\n</style>`);
|
||||
fs.writeFileSync(vuePath, content, 'utf8');
|
||||
console.log('updated', `${page}.vue`);
|
||||
}
|
||||
|
||||
console.log('done');
|
||||
@@ -0,0 +1,27 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const styleDir = path.join(__dirname, '../common/style');
|
||||
|
||||
function walk(dir) {
|
||||
for (const name of fs.readdirSync(dir)) {
|
||||
const full = path.join(dir, name);
|
||||
if (fs.statSync(full).isDirectory()) {
|
||||
walk(full);
|
||||
continue;
|
||||
}
|
||||
if (!name.endsWith('.css')) continue;
|
||||
let css = fs.readFileSync(full, 'utf8');
|
||||
const next = css
|
||||
.replace(/url\(@\/static\/images\//g, 'url(/static/images/')
|
||||
.replace(/url\("@\/static\/images\//g, 'url("/static/images/')
|
||||
.replace(/url\('@\/static\/images\//g, "url('/static/images/");
|
||||
if (next !== css) {
|
||||
fs.writeFileSync(full, next, 'utf8');
|
||||
console.log('fixed', path.relative(styleDir, full));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walk(styleDir);
|
||||
console.log('done');
|
||||
@@ -0,0 +1,77 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const styleDir = path.join(__dirname, '../common/style/memberInfo');
|
||||
|
||||
/** 硬编码 px → 统一字号变量(从大到小替换,避免误匹配) */
|
||||
const fontSizePxMap = [
|
||||
['32px', 'var(--font-size-5xl)'],
|
||||
['28px', 'var(--font-size-4xl)'],
|
||||
['24px', 'var(--font-size-3xl)'],
|
||||
['22px', 'var(--font-size-2xl)'],
|
||||
['20px', 'var(--font-size-xl)'],
|
||||
['18px', 'var(--font-size-lg)'],
|
||||
['17px', 'var(--font-size-md)'],
|
||||
['16px', 'var(--font-size-md)'],
|
||||
['15px', 'var(--font-size-base)'],
|
||||
['14px', 'var(--font-size-base)'],
|
||||
['13px', 'var(--font-size-base)'],
|
||||
['12px', 'var(--font-size-sm)'],
|
||||
['11px', 'var(--font-size-xs)'],
|
||||
['10px', 'var(--font-size-xs)']
|
||||
];
|
||||
|
||||
function walk(dir) {
|
||||
for (const name of fs.readdirSync(dir)) {
|
||||
const full = path.join(dir, name);
|
||||
if (fs.statSync(full).isDirectory()) {
|
||||
walk(full);
|
||||
continue;
|
||||
}
|
||||
if (!name.endsWith('.css')) continue;
|
||||
normalize(full);
|
||||
}
|
||||
}
|
||||
|
||||
function normalize(filePath) {
|
||||
let css = fs.readFileSync(filePath, 'utf8');
|
||||
let next = css;
|
||||
|
||||
const fontFamilyMap = [
|
||||
['"Noto Sans SC-Bold"', 'var(--font-family)'],
|
||||
['"Noto Sans SC-Medium"', 'var(--font-family)'],
|
||||
['"Noto Sans SC-Regular"', 'var(--font-family)'],
|
||||
['"Noto Sans SC-SemiBold"', 'var(--font-family)'],
|
||||
['"Inter-Semi Bold"', 'var(--font-family)'],
|
||||
['"Inter-Bold"', 'var(--font-family)'],
|
||||
['"Inter-Regular"', 'var(--font-family)']
|
||||
];
|
||||
for (const [from, to] of fontFamilyMap) {
|
||||
next = next.split(`font-family: ${from}`).join(`font-family: ${to}`);
|
||||
}
|
||||
|
||||
const weightMap = [
|
||||
['font-weight: Bold', 'font-weight: 700'],
|
||||
['font-weight: Medium', 'font-weight: 500'],
|
||||
['font-weight: Regular', 'font-weight: 400'],
|
||||
['font-weight: SemiBold', 'font-weight: 600'],
|
||||
['font-weight: Semi Bold', 'font-weight: 600']
|
||||
];
|
||||
for (const [from, to] of weightMap) {
|
||||
next = next.split(from).join(to);
|
||||
}
|
||||
|
||||
next = next.replace(/white-space:\s*pre;/g, 'white-space: nowrap;');
|
||||
|
||||
for (const [px, token] of fontSizePxMap) {
|
||||
next = next.replace(new RegExp(`font-size:\\s*${px.replace('.', '\\.')};`, 'g'), `font-size: ${token};`);
|
||||
}
|
||||
|
||||
if (next !== css) {
|
||||
fs.writeFileSync(filePath, next, 'utf8');
|
||||
console.log('normalized', path.relative(styleDir, filePath));
|
||||
}
|
||||
}
|
||||
|
||||
walk(styleDir);
|
||||
console.log('done');
|
||||
@@ -0,0 +1,61 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const componentsDir = path.join(__dirname, '../components/memberInfo');
|
||||
|
||||
const cssMap = {
|
||||
MemberInfoStatusBar: 'member-info-status-bar.css',
|
||||
MemberInfoHeader: 'member-info-header.css',
|
||||
MemberInfoMemberCard: 'member-info-member-card.css',
|
||||
MemberInfoQuickActions: 'member-info-quick-actions.css',
|
||||
MemberInfoBookingList: 'member-info-booking-list.css',
|
||||
MemberInfoCheckInList: 'member-info-check-in-list.css',
|
||||
MemberInfoBodyReport: 'member-info-body-report.css',
|
||||
MemberInfoCouponPoints: 'member-info-coupon-points.css',
|
||||
MemberInfoReferral: 'member-info-referral.css',
|
||||
MemberInfoSettings: 'member-info-settings.css',
|
||||
MemberInfoLogout: 'member-info-logout.css'
|
||||
};
|
||||
|
||||
for (const [name, cssFile] of Object.entries(cssMap)) {
|
||||
const filePath = path.join(componentsDir, `${name}.vue`);
|
||||
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
||||
|
||||
content = content.replace(/\n<style>[\s\S]*?<\/style>\n?/g, '\n');
|
||||
|
||||
const styleBlock = `\n<style>
|
||||
@import '@/common/style/base.css';
|
||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||
@import '@/common/style/memberInfo/${cssFile}';
|
||||
</style>\n`;
|
||||
|
||||
if (!content.trimEnd().endsWith('</script>')) {
|
||||
console.warn('skip unexpected format:', name);
|
||||
continue;
|
||||
}
|
||||
|
||||
content = content.trimEnd() + styleBlock;
|
||||
fs.writeFileSync(filePath, content, 'utf8');
|
||||
console.log('restored styles:', name);
|
||||
}
|
||||
|
||||
const pageFiles = [
|
||||
path.join(__dirname, '../pages/memberInfo/memberInfo.vue'),
|
||||
path.join(__dirname, '../pages/memberInfo/userInfo.vue')
|
||||
];
|
||||
|
||||
for (const pagePath of pageFiles) {
|
||||
let content = fs.readFileSync(pagePath, 'utf8');
|
||||
content = content.replace(
|
||||
"@import '@/common/style/member-info-all.css';",
|
||||
"@import '@/common/style/memberInfo/member-info-page.css';"
|
||||
);
|
||||
content = content.replace(
|
||||
"@import '@/common/style/member-info-page.css';",
|
||||
"@import '@/common/style/memberInfo/member-info-page.css';"
|
||||
);
|
||||
fs.writeFileSync(pagePath, content, 'utf8');
|
||||
console.log('updated page:', path.basename(pagePath));
|
||||
}
|
||||
|
||||
console.log('done');
|
||||
@@ -0,0 +1,19 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const root = path.join(__dirname, '..');
|
||||
const componentsDir = path.join(root, 'components/memberInfo');
|
||||
|
||||
const componentFiles = fs.readdirSync(componentsDir).filter((name) => name.endsWith('.vue'));
|
||||
|
||||
for (const file of componentFiles) {
|
||||
const filePath = path.join(componentsDir, file);
|
||||
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
||||
const next = content.replace(/\n<style>[\s\S]*?<\/style>\n?/g, '\n');
|
||||
if (next !== content) {
|
||||
fs.writeFileSync(filePath, next.trimEnd() + '\n', 'utf8');
|
||||
console.log('removed style block:', file);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('done');
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 184 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 764 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 561 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user