修正多处问题

This commit is contained in:
2026-06-29 13:35:29 +08:00
parent 09e587cb65
commit e85f39ab83
37 changed files with 3545 additions and 2538 deletions
+1 -4
View File
@@ -183,9 +183,7 @@ function onTabTap(index) {
emit('tab-change', index)
// 设置导航标志,阻止轮询覆盖状态
isNavigating = true
let timer = setTimeout(() => {
uni.showLoading({ title: '加载中...', mask: true })
}, 50)
uni.showLoading({ title: '加载中...', mask: true })
isSwitching = true
uni.switchTab({
url: targetPath,
@@ -195,7 +193,6 @@ function onTabTap(index) {
uni.reLaunch({ url: targetPath })
},
complete: () => {
clearTimeout(timer)
uni.hideLoading()
setTimeout(() => {
isSwitching = false
@@ -103,7 +103,7 @@
</view>
<!-- 预约按钮 -->
<view :class="['booking-btn', { disabled: !canBook }]" @click.stop="handleBooking">
<text>{{ canBook ? '立即预约' : (course.currentMembers >= course.maxMembers ? '已满员' : '已结束') }}</text>
<text>{{ buttonText }}</text>
</view>
</view>
</view>
@@ -123,6 +123,10 @@ const props = defineProps({
courseTypeLabels: {
type: Array,
default: () => []
},
booked: {
type: Boolean,
default: false
}
})
@@ -147,7 +151,14 @@ const statusClass = computed(() => {
const canBook = computed(() => {
const status = props.course.status
const isFull = props.course.currentMembers >= props.course.maxMembers
return status === '0' && !isFull
return status === '0' && !isFull && !props.booked
})
const buttonText = computed(() => {
if (props.booked) return '已预约'
if (!canBook.value && props.course.currentMembers >= props.course.maxMembers) return '已满员'
if (!canBook.value && Number(props.course.status) !== 0) return '已结束'
return '立即预约'
})
const formatTime = (dateStr) => {
@@ -191,7 +202,7 @@ const formatDuration = (startStr, endStr) => {
}
const goDetail = () => {
emit('detail', props.course.id)
emit('detail', Number(props.course.id))
}
const handleBooking = () => {
@@ -45,9 +45,11 @@
</template>
<script setup>
import { ref } from 'vue'
import { ref, onMounted } from 'vue'
import { getActiveBanners } from '@/api/main.js'
const banners = [
// 默认静态数据(后端无数据时作为兜底)
const fallbackBanners = [
{
image: 'https://images.unsplash.com/photo-1534438327276-14e5300c3a48?w=800&q=80',
title: '突破自我',
@@ -68,9 +70,49 @@ const banners = [
}
]
// 先用回退数据初始化,确保 swiper 立即可见
const banners = ref([...fallbackBanners])
const currentIndex = ref(0)
// 记录每张图片的加载状态
const imageLoaded = ref(banners.map(() => false))
const imageLoaded = ref(fallbackBanners.map(() => false))
const isFetching = ref(false)
async function fetchBanners() {
isFetching.value = true
try {
// 禁用缓存,确保每次获取最新数据
const res = await getActiveBanners({ cache: false })
// 处理后端返回数据:可能是数组或 { data: [...] }
const data = Array.isArray(res) ? res : (res?.data || [])
if (data.length > 0) {
// 映射后端字段到组件字段,替换回退数据
banners.value = data.map(item => ({
image: item.imageUrl || '',
title: item.title || '',
subtitle: item.subtitle || '',
desc: item.description || ''
}))
// 更新 imageLoaded 状态
imageLoaded.value = banners.value.map(() => false)
console.log('[BannerSwiper] 轮播图数据加载成功,共', data.length, '条')
} else {
console.log('[BannerSwiper] 后端无轮播图数据,使用回退数据')
// 保持已有的 fallbackBanners,无需重新赋值
imageLoaded.value = banners.value.map(() => false)
}
} catch (err) {
console.error('[BannerSwiper] 轮播图数据加载失败,使用回退数据:', err)
// 保持已有的 fallbackBanners,无需重新赋值
imageLoaded.value = banners.value.map(() => false)
} finally {
isFetching.value = false
}
}
onMounted(() => {
fetchBanners()
})
const onSwiperChange = (e) => {
currentIndex.value = e.detail.current
@@ -86,30 +128,28 @@ const previewImage = (index) => {
})
}
// 图片加载成功回调
const onImageLoad = (index) => {
imageLoaded.value[index] = true
console.log(`图片 ${index} 加载完成`)
}
// 图片加载失败回调
const onImageError = (index) => {
console.error(`图片 ${index} 加载失败`)
// 可选:设置默认占位图
// banners[index].image = '默认图片URL'
}
</script>
<style lang="scss" scoped>
.banner-container {
position: relative;
z-index: 2;
width: 100%;
padding: 0 24rpx;
box-sizing: border-box;
}
.banner-swiper {
width: 100%;
height: 480rpx;
height: 360rpx;
border-radius: 20rpx;
overflow: hidden;
}
.banner-content {
@@ -124,7 +164,6 @@ const onImageError = (index) => {
height: 100%;
}
/* 添加图片占位符样式 */
.image-placeholder {
position: absolute;
top: 0;
@@ -158,10 +197,10 @@ const onImageError = (index) => {
right: 0;
top: 0;
bottom: 0;
z-index: 2; /* 确保遮罩层在占位符上面 */
background: linear-gradient(135deg, rgba(0,0,0,0.15) 0%, rgba(0,0,0,0.05) 50%, rgba(0,0,0,0.25) 100%);
z-index: 2;
}
/* 其余样式保持不变 */
.banner-text {
position: absolute;
left: 36rpx;
@@ -198,9 +237,7 @@ const onImageError = (index) => {
display: flex;
justify-content: center;
gap: 16rpx;
margin-top: -100rpx;
position: relative;
z-index: 3;
padding: 20rpx 0 8rpx;
}
.dot {
@@ -1,17 +1,23 @@
<template>
<view class="tab-page__header" :style="{ padding: `${statusBarHeight}rpx`, height: `${statusBarHeight + 80}rpx` }">
<!-- 状态栏占位区 App 端生效H5/小程序无状态栏概念 -->
<view class="tab-page__status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
<!-- 导航栏主体 -->
<view class="tab-page__header">
<view v-if="showBack" :class="['tab-page__back-btn', { 'tab-page__back-btn--animate': isAnimating }]" @tap="goBack">
<text class="tab-page__back-icon"></text>
</view>
<view class="tab-page__title-wrap">
<text class="tab-page__title">{{ title }}</text>
<text class="tab-page__subtitle">{{ subtitle }}</text>
<text v-if="subtitle" class="tab-page__subtitle">{{ subtitle }}</text>
</view>
<view v-if="$slots.right" class="tab-page__right">
<slot name="right"></slot>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, computed } from 'vue'
const props = defineProps({
title: {
@@ -29,11 +35,19 @@ const props = defineProps({
})
const isAnimating = ref(false)
const statusBarHeight = ref(20)
const statusBarHeight = ref(0)
onMounted(() => {
// #ifdef APP-PLUS
// App 端获取真实状态栏高度(px)
const sysInfo = uni.getSystemInfoSync()
statusBarHeight.value = sysInfo.statusBarHeight || 20
statusBarHeight.value = sysInfo.statusBarHeight || 0
// #endif
// #ifndef APP-PLUS
// H5/小程序:状态栏由浏览器或框架处理,不额外占位
statusBarHeight.value = 0
// #endif
if (props.showBack) {
isAnimating.value = true
}
@@ -51,10 +65,16 @@ function goBack() {
</script>
<style lang="scss" scoped>
/* 状态栏占位:仅在 App 端有高度,H5/小程序为 0,背景继承页面 */
.tab-page__status-bar {
width: 100%;
}
.tab-page__header {
padding: 0 32rpx;
padding: 0 32rpx 24rpx 32rpx;
display: flex;
align-items: center;
height: 88rpx;
position: relative;
}
@@ -114,4 +134,11 @@ function goBack() {
font-size: 24rpx;
color: $text-muted;
}
.tab-page__right {
margin-left: 16rpx;
flex-shrink: 0;
display: flex;
align-items: center;
}
</style>
+51 -131
View File
@@ -12,26 +12,12 @@
<text class="entry-title">{{ item.title }}</text>
<text class="entry-desc">{{ item.desc }}</text>
</view>
<view v-if="showCheckInMenu" class="checkin-menu-overlay" @tap="showCheckInMenu = false">
<view class="checkin-menu" @tap.stop>
<view class="menu-item" @tap="handleStoreCheckIn">
<image src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/checkIn.png" mode="aspectFit" class="menu-icon" />
<text class="menu-text">到店签到</text>
</view>
<view class="menu-item" @tap="handleGroupCourseCheckIn">
<image src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/course.png" mode="aspectFit" class="menu-icon" />
<text class="menu-text">团课签到</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
const showCheckInMenu = ref(false)
import { qrSignInGroupCourse } from '@/api/groupCourse.js'
import { getMemberId } from '@/utils/request.js'
const handleEntryClick = (item) => {
if (item.title === '签到') {
@@ -53,7 +39,8 @@ const handleEntryClick = (item) => {
})
return
}
showCheckInMenu.value = !showCheckInMenu.value
// 直接调用扫码签到
qrScanSignIn()
} else if (item.path) {
if (item.isTabBar) {
uni.switchTab({
@@ -67,30 +54,57 @@ const handleEntryClick = (item) => {
}
}
const handleStoreCheckIn = () => {
showCheckInMenu.value = false
uni.navigateTo({
url: '/pages/checkIn/checkIn'
})
}
const handleGroupCourseCheckIn = () => {
showCheckInMenu.value = false
const qrScanSignIn = () => {
uni.scanCode({
onlyFromCamera: true,
success: (res) => {
success: async (res) => {
console.log('扫码结果:', res)
uni.showToast({
title: '扫码成功',
icon: 'success'
})
const qrContent = res.result
// 解析二维码内容获取 courseId
let courseId = null
try {
const parsed = JSON.parse(qrContent)
courseId = Number(parsed.courseId)
} catch {
const num = Number(qrContent)
if (!isNaN(num)) {
courseId = num
}
}
if (!courseId) {
uni.showToast({ title: '无效的签到二维码', icon: 'none' })
return
}
const memberId = getMemberId()
if (!memberId) {
uni.showToast({ title: '请先登录', icon: 'none' })
return
}
uni.showLoading({ title: '签到中...' })
try {
const result = await qrSignInGroupCourse(courseId, memberId)
uni.hideLoading()
if (result.success) {
uni.showToast({ title: '签到成功', icon: 'success' })
setTimeout(() => {
uni.navigateTo({ url: '/pages/memberInfo/myCourses' })
}, 1200)
} else {
uni.showToast({ title: result.message || '签到失败', icon: 'none', duration: 3000 })
}
} catch (e) {
uni.hideLoading()
console.error('团课签到失败:', e)
const errMsg = e?.message || e?.data?.message || '签到失败'
uni.showToast({ title: errMsg, icon: 'none', duration: 3000 })
}
},
fail: (err) => {
console.error('扫码失败:', err)
uni.showToast({
title: '扫码失败',
icon: 'none'
})
}
})
}
@@ -118,15 +132,10 @@ const entries = [
display: flex;
justify-content: space-between;
padding: 32rpx 24rpx;
background: rgba(255, 255, 255, 0.55);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
background: #FFFFFF;
margin: 24rpx;
border-radius: 28rpx;
box-shadow: 0 8rpx 32rpx var(--shadow-blue-light);
border: 1rpx solid rgba(255, 255, 255, 0.7);
position: relative;
z-index: 3;
box-shadow: 0 8rpx 32rpx rgba(45,74,90,0.08);
}
.entry-item {
@@ -182,93 +191,4 @@ const entries = [
font-size: 22rpx;
color: var(--tabbar-text-inactive);
}
.checkin-menu-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
display: flex;
align-items: flex-start;
justify-content: flex-end;
padding-top: 280rpx;
padding-right: 60rpx;
animation: fadeIn 0.2s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.checkin-menu {
position: absolute;
right: 0;
top: 140rpx;
background: white;
border-radius: 24rpx;
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.15);
padding: 16rpx;
min-width: 320rpx;
animation: slideDown 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
&::before {
content: '';
position: absolute;
top: -24rpx;
right: 60rpx;
width: 0;
height: 0;
border-left: 24rpx solid transparent;
border-right: 24rpx solid transparent;
border-bottom: 24rpx solid white;
}
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-40rpx) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.menu-item {
display: flex;
align-items: center;
padding: 24rpx 32rpx;
border-radius: 16rpx;
transition: all 0.2s ease;
&:active {
background: rgba(0, 0, 0, 0.04);
transform: scale(0.98);
}
& + .menu-item {
margin-top: 8rpx;
}
}
.menu-icon {
width: 48rpx;
height: 48rpx;
margin-right: 20rpx;
}
.menu-text {
font-size: 30rpx;
font-weight: 600;
color: #2D4A5A;
}
</style>
@@ -72,7 +72,10 @@
<script setup>
import { ref, onMounted } from 'vue'
import { getGroupCoursePage } from '@/api/main.js'
import { onShow } from '@dcloudio/uni-app'
import { getActiveRecommendCourses } from '@/api/groupCourse.js'
console.log('[RecommendCourses] 组件脚本已加载')
// 推荐课程数据列表
const courses = ref([])
@@ -105,18 +108,23 @@ const getImageUrl = (coverImage) => {
// 获取推荐课程
const fetchRecommendCourses = async () => {
console.log('[RecommendCourses] fetchRecommendCourses 开始请求')
try {
const res = await getGroupCoursePage({
page: 0, size: 10, sort: 'current_members', order: 'desc'
}, { cache: true, cacheTime: 5 * 60 * 1000 })
if (res && res.content && Array.isArray(res.content)) {
// 筛选掉已结束和已满员的课程
const filteredCourses = res.content.filter(course => {
return course.status !== '2' && course.currentMembers < course.maxMembers
const res = await getActiveRecommendCourses()
console.log('[RecommendCourses] API 返回:', JSON.stringify(res).substring(0, 200))
if (res && Array.isArray(res)) {
// 提取推荐中的 groupCourse,只排除已满员的课程(已结束的仍展示,点击时拦截)
const filteredCourses = res.filter(recommend => {
const course = recommend.groupCourse
if (!course) return false
const current = Number(course.currentMembers) || 0
const max = Number(course.maxMembers) || 999
return current < max
})
// 只取前3个符合条件的课程
courses.value = filteredCourses.slice(0, 3).map(course => {
// 只取前3个符合条件的推荐课程
courses.value = filteredCourses.slice(0, 3).map(recommend => {
const course = recommend.groupCourse
return {
id: course.id,
image: getImageUrl(course.coverImage),
@@ -127,6 +135,9 @@ const fetchRecommendCourses = async () => {
rawData: course
}
})
console.log('[RecommendCourses] 最终展示 courses.value 数量:', courses.value.length)
} else {
console.warn('[RecommendCourses] API 返回格式异常:', res)
}
} catch (err) {
console.error('获取推荐课程失败:', err)
@@ -143,7 +154,14 @@ const handleViewMore = () => {
uni.navigateTo({ url: '/pages/recommendCourses/index' })
}
onMounted(() => { fetchRecommendCourses() })
onMounted(() => { console.log('[RecommendCourses] onMounted 触发'); fetchRecommendCourses() })
onShow(() => {
console.log('[RecommendCourses] onShow 触发,刷新推荐课程')
fetchRecommendCourses()
})
defineExpose({ fetchRecommendCourses })
</script>
<style lang="scss">
@@ -45,7 +45,10 @@
<script setup>
import { ref, onMounted } from 'vue'
import { getGroupCoursePage } from '@/api/main.js'
import { onShow } from '@dcloudio/uni-app'
import { getActiveRecommendCourses } from '@/api/groupCourse.js'
console.log('[TodayRecommend] 组件脚本已加载')
// 今日推荐数据列表
const recommends = ref([])
@@ -78,16 +81,22 @@ const getImageUrl = (coverImage) => {
// 获取今日推荐
const fetchTodayRecommend = async () => {
console.log('[TodayRecommend] fetchTodayRecommend 开始请求')
try {
const res = await getGroupCoursePage({
page: 0, size: 10, sort: 'current_members', order: 'desc'
}, { cache: true, cacheTime: 5 * 60 * 1000 })
if (res && res.content && Array.isArray(res.content)) {
const filteredCourses = res.content.filter(course => {
return course.status !== '2' && course.currentMembers < course.maxMembers
const res = await getActiveRecommendCourses()
console.log('[TodayRecommend] API 返回:', JSON.stringify(res).substring(0, 200))
if (res && Array.isArray(res)) {
// 提取推荐中的 groupCourse,只排除已满员的课程
const filteredCourses = res.filter(recommend => {
const course = recommend.groupCourse
if (!course) return false
const current = Number(course.currentMembers) || 0
const max = Number(course.maxMembers) || 999
return current < max
})
recommends.value = filteredCourses.slice(0, 3).map(course => {
recommends.value = filteredCourses.slice(0, 3).map(recommend => {
const course = recommend.groupCourse
return {
id: course.id,
image: getImageUrl(course.coverImage),
@@ -97,6 +106,9 @@ const fetchTodayRecommend = async () => {
participants: String(course.currentMembers || 0)
}
})
console.log('[TodayRecommend] 最终展示 recommends.value 数量:', recommends.value.length)
} else {
console.warn('[TodayRecommend] API 返回格式异常:', res)
}
} catch (err) {
console.error('获取今日推荐失败:', err)
@@ -108,7 +120,14 @@ const handleItemClick = (item) => {
uni.navigateTo({ url: `/pages/groupCourse/detail?id=${item.id}` })
}
onMounted(() => { fetchTodayRecommend() })
onMounted(() => { console.log('[TodayRecommend] onMounted 触发'); fetchTodayRecommend() })
onShow(() => {
console.log('[TodayRecommend] onShow 触发,刷新今日推荐')
fetchTodayRecommend()
})
defineExpose({ fetchTodayRecommend })
</script>
<style lang="scss" scoped>
@@ -10,69 +10,60 @@
:hover-stay-time="150"
@tap="$emit('view-all')"
>
<text class="booking-section__view-all">预约记录</text>
<text class="booking-section__view-all">查看全部</text>
<image
class="booking-section__link-arrow"
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/chevronright4.png"
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/chevronright2.png"
mode="aspectFit"
/>
</view>
</view>
</view>
<view
v-for="item in previewItems"
:key="item.id"
class="booking-section__item"
hover-class="mi-tap-row--hover"
:hover-stay-time="150"
@tap="$emit('item-tap', item)"
>
<view class="booking-section__item-inner">
<view class="booking-section__date">
<view class="booking-section__date-inner">
<text class="booking-section__num">{{ item.dateDay }}</text>
<text class="booking-section__date-sub">{{ item.dateMonth }}</text>
</view>
</view>
<view class="booking-section__content">
<view class="booking-section__content-inner">
<text class="booking-section__desc">{{ item.desc }}</text>
<view class="booking-section__meta">
<view class="booking-section__meta-inner">
<image
class="booking-section__icon-coach"
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/user2.png"
mode="aspectFit"
/>
<text class="booking-section__coach">教练{{ item.coach }}</text>
<image
class="booking-section__icon-location"
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/mappin1.png"
mode="aspectFit"
/>
<text class="booking-section__text">{{ item.location }}</text>
<view class="booking-section__list">
<!-- 无数据 -->
<view v-if="!displayItems || displayItems.length === 0" class="booking-section__empty">
<text class="booking-section__empty-text">暂无预约记录</text>
</view>
<view v-else class="booking-section__list-inner">
<view
v-for="(item, index) in displayItems"
:key="item.id"
class="booking-section__row"
>
<view v-if="index > 0" class="booking-section__divider"></view>
<view
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__dot"
:class="'booking-section__dot--' + item.status"
></view>
<view class="booking-section__content">
<view class="booking-section__content-inner">
<text class="booking-section__desc">{{ item.title }}</text>
<text class="booking-section__text">{{ item.time }}</text>
</view>
</view>
<view
class="booking-section__tag-badge"
:class="'booking-section__tag-badge--' + item.status"
>
<text
class="booking-section__tag-text"
:class="'booking-section__tag-text--' + item.status"
>
{{ item.statusLabel }}
</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>
@@ -89,25 +80,186 @@ const props = defineProps({
defineEmits(['view-all', 'item-tap'])
const previewItems = computed(() => {
return props.items
const displayItems = computed(() => {
return props.items || []
})
</script>
<style lang="scss">
@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 {
.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%;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
justify-content: center;
padding: 24px 16px;
}
.booking-section__header-inner {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
}
.booking-section__title {
font-size: var(--font-size-md);
font-family: var(--font-family);
font-weight: var(--font-weight-bold);
color: var(--text-dark);
}
.booking-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;
}
.booking-section__list-inner {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
}
.booking-section__empty {
padding: 48rpx 32rpx;
display: flex;
align-items: center;
justify-content: center;
}
.booking-section__empty-text {
font-size: 14px;
color: #8A99B4;
font-size: 26rpx;
color: #999;
}
.booking-section__row {
width: 100%;
}
.booking-section__divider {
height: 1px;
background: var(--border-light);
margin: 0 16rpx;
}
.booking-section__item {
padding: 24rpx 16rpx;
}
.booking-section__item-inner {
display: flex;
flex-direction: row;
align-items: center;
gap: 16rpx;
}
.booking-section__dot {
width: 14rpx;
height: 14rpx;
border-radius: 50%;
flex-shrink: 0;
}
.booking-section__dot--ongoing {
background: #4CAF50;
}
.booking-section__dot--completed {
background: #2196F3;
}
.booking-section__dot--cancelled {
background: var(--text-muted);
}
.booking-section__content {
flex: 1;
min-width: 0;
}
.booking-section__content-inner {
display: flex;
flex-direction: column;
gap: 6rpx;
}
.booking-section__desc {
font-size: var(--font-size-md);
font-family: var(--font-family);
color: var(--text-dark);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.booking-section__text {
font-size: var(--font-size-sm);
font-family: var(--font-family);
color: var(--text-muted);
}
.booking-section__tag-badge {
padding: 4rpx 14rpx;
border-radius: 8rpx;
flex-shrink: 0;
}
.booking-section__tag-badge--ongoing {
background: rgba(76, 175, 80, 0.1);
}
.booking-section__tag-badge--completed {
background: rgba(33, 150, 243, 0.1);
}
.booking-section__tag-badge--cancelled {
background: rgba(153, 153, 153, 0.1);
}
.booking-section__tag-text {
font-size: var(--font-size-xs);
font-family: var(--font-family);
}
.booking-section__tag-text--ongoing {
color: #4CAF50;
}
.booking-section__tag-text--completed {
color: #2196F3;
}
.booking-section__tag-text--cancelled {
color: var(--text-muted);
}
</style>
@@ -20,7 +20,11 @@
</view>
</view>
<view class="checkin-section__list">
<view class="checkin-section__list-inner">
<!-- 无数据 -->
<view v-if="!items || items.length === 0" class="checkin-section__empty">
<text class="checkin-section__empty-text">暂无签到记录</text>
</view>
<view v-else class="checkin-section__list-inner">
<view
v-for="(item, index) in items"
:key="item.id"
@@ -79,4 +83,16 @@ defineEmits(['view-all', 'item-tap'])
@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';
.checkin-section__empty {
padding: 48rpx 32rpx;
display: flex;
align-items: center;
justify-content: center;
}
.checkin-section__empty-text {
font-size: 26rpx;
color: #999;
}
</style>
@@ -1,89 +1,50 @@
<template>
<view class="profile-header">
<!-- 用户信息区深蓝渐变 -->
<view class="profile-header__hero">
<view class="profile-header__inner" v-if="isLogin">
<view class="profile-header__user" hover-class="mi-tap--hover" :hover-stay-time="150" @tap="handleUserTap">
<view class="profile-header__user-inner">
<view class="profile-header__avatar-wrap">
<view class="profile-header__avatar-ring">
<image
class="profile-header__avatar"
:key="userInfo.avatar"
:src="displayAvatar"
mode="aspectFill"
/>
</view>
<view class="profile-header__avatar-badge">
<image
class="profile-header__avatar-badge-icon"
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/camera.png"
mode="aspectFit"
/>
</view>
</view>
<view class="profile-header__user-meta">
<view class="profile-header__user-meta-inner">
<text class="profile-header__name">{{ userInfo.name || "活氧舱用户" }}</text>
<text class="profile-header__phone">{{ userInfo.phone }}</text>
<view class="profile-header__badge">
<image
class="profile-header__badge-icon"
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/crown0.png"
mode="aspectFit"
/>
<text class="profile-header__level">{{ userInfo.memberLevel }}</text>
</view>
</view>
<!-- 登录状态 -->
<view class="profile-card" v-if="isLogin" hover-class="mi-tap--hover" :hover-stay-time="150" @tap="handleUserTap">
<view class="profile-card__main">
<view class="profile-avatar">
<image
class="profile-avatar__img"
:key="userInfo.avatar"
:src="displayAvatar"
mode="aspectFill"
/>
</view>
<view class="profile-info">
<view class="profile-info__row">
<text class="profile-info__name">{{ userInfo.name || "活氧舱用户" }}</text>
<view class="profile-info__badge">
<image class="profile-info__badge-icon" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/crown0.png" mode="aspectFit" />
<text class="profile-info__level">{{ userInfo.memberLevel }}</text>
</view>
</view>
<text class="profile-info__phone">{{ userInfo.phone }}</text>
</view>
</view>
<view class="profile-card__stats">
<view class="profile-stat">
<text class="profile-stat__value">{{ stats.checkInCount ?? 0 }}</text>
<text class="profile-stat__label">累计签到</text>
</view>
<view class="profile-stat">
<text class="profile-stat__value">{{ stats.courseCount ?? 0 }}</text>
<text class="profile-stat__label">报课次数</text>
</view>
</view>
</view>
<!-- 未登录状态 -->
<view class="profile-card profile-card--guest" v-else @tap="handleGuestLogin">
<view class="profile-card__main">
<view class="profile-avatar">
<image class="profile-avatar__img" src="/static/OIP-C.png" mode="aspectFill" />
</view>
<view class="profile-info">
<text class="profile-info__name">小伙伴点我登录</text>
<text class="profile-info__phone">陪你遇见更好的自己 ~</text>
</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 class="profile-header__inner" v-else>
<view class="profile-header__user" hover-class="mi-tap--hover" :hover-stay-time="150" @tap="handleUserTap">
<view
class="profile-header__user-inner--guest"
@tap="handleGuestLogin"
>
<view class="profile-header__avatar-wrap--guest">
<image
class="profile-header__avatar--guest"
src="/static/OIP-C.png"
mode="aspectFill"
/>
</view>
<view class="profile-header__user-meta--guest">
<text class="profile-header__name--guest">小伙伴点我登录</text>
<text class="profile-header__phone--guest">陪你遇见更好的自己 ~</text>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
@@ -94,10 +55,9 @@ import { computed } from 'vue'
const props = defineProps({
userInfo: { type: Object, required: true },
stats: { type: Object, required: true },
isLogin: { type: Boolean }
isLogin: { type: Boolean }
})
const emit = defineEmits(['user-info', 'guest-login'])
const displayAvatar = computed(() => {
@@ -115,8 +75,112 @@ function handleGuestLogin() {
}
</script>
<style lang="scss">
@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 lang="scss" scoped>
.profile-header {
padding: 24rpx 24rpx 0;
}
.profile-card {
background: #FFFFFF;
border-radius: 20rpx;
padding: 28rpx 28rpx 24rpx;
box-shadow: 0 4rpx 20rpx rgba(45, 74, 90, 0.06);
display: flex;
flex-direction: column;
gap: 24rpx;
&--guest {
cursor: pointer;
}
}
.profile-card__main {
display: flex;
align-items: center;
gap: 20rpx;
}
.profile-avatar {
width: 88rpx;
height: 88rpx;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
border: 3rpx solid #E8F4F8;
}
.profile-avatar__img {
width: 100%;
height: 100%;
border-radius: 50%;
}
.profile-info {
flex: 1;
min-width: 0;
}
.profile-info__row {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 6rpx;
}
.profile-info__name {
font-size: 32rpx;
font-weight: 700;
color: #1A202C;
line-height: 1.2;
}
.profile-info__badge {
display: flex;
align-items: center;
gap: 4rpx;
background: linear-gradient(135deg, #FFF7ED, #FFF1E6);
border-radius: 20rpx;
padding: 4rpx 14rpx;
}
.profile-info__badge-icon {
width: 24rpx;
height: 24rpx;
}
.profile-info__level {
font-size: 20rpx;
font-weight: 600;
color: #ED8936;
}
.profile-info__phone {
font-size: 24rpx;
color: #A0AEC0;
}
.profile-card__stats {
display: flex;
justify-content: space-around;
padding-top: 20rpx;
border-top: 1rpx solid #F0F4F8;
}
.profile-stat {
display: flex;
flex-direction: column;
align-items: center;
gap: 4rpx;
}
.profile-stat__value {
font-size: 36rpx;
font-weight: 700;
color: #2D4A5A;
}
.profile-stat__label {
font-size: 22rpx;
color: #A0AEC0;
}
</style>
@@ -1,98 +1,82 @@
<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__count"
>
<text class="member-card-section__count-num">{{ cardInfo.activeCount || 0 }}</text>
<text class="member-card-section__title">我的会员卡</text>
<view class="member-card-section__count" v-if="activeCount > 0">
<text class="member-card-section__count-num">{{ activeCount }}</text>
<text class="member-card-section__count-text">张有效</text>
</view>
</view>
</view>
<view
class="member-card-preview"
hover-class="mi-tap-card--hover"
:hover-stay-time="150"
@tap="$emit('view-all')"
>
<view class="member-card-preview__inner">
<view class="member-card-preview__head">
<view class="member-card-preview__head-inner">
<view
class="member-card-preview__type-row"
>
<view
class="member-card-preview__icon-wrap"
>
<view
class="member-card-preview__icon-border"
>
<view
class="member-card-preview__icon-bg"
></view>
<view
class="member-card-preview__icon-stroke"
></view>
</view>
<image class="member-card-preview__icon-line" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/Line_2_468.png" mode="aspectFill" />
</view>
<text
class="member-card-preview__name"
>
{{ cardInfo.hasActiveCard ? cardInfo.name : '暂无会员卡' }}
</text>
</view>
<view
class="member-card-preview__tag"
:class="getTagClass()"
v-if="cardInfo.hasActiveCard"
>
<text class="member-card-preview__tag-text">
{{ getTagText() }}
</text>
</view>
</view>
<!-- 三张卡展示 -->
<view class="card-three-row">
<!-- 储值卡 -->
<view
class="card-three-item"
:class="{ 'card-three-item--inactive': !storedCard.hasCard }"
hover-class="mi-tap-card--hover"
:hover-stay-time="150"
@tap="$emit('stored-card-tap')"
>
<view class="card-three-item__icon-wrap card-three-item__icon-wrap--stored">
<uni-icons type="wallet" size="28rpx" :color="storedCard.hasCard ? '#667eea' : '#B0BEC5'" />
</view>
<text class="member-card-preview__expire" v-if="cardInfo.hasActiveCard && cardInfo.expireDate">
有效期至 {{ cardInfo.expireDate }}
</text>
<view class="member-card-preview__footer">
<view class="member-card-preview__footer-inner">
<view
class="member-card-preview__days"
v-if="cardInfo.hasActiveCard"
>
<text
class="member-card-preview__days-num"
:class="getDaysClass()"
>
{{ getDisplayDays() }}
</text>
<text
class="member-card-preview__days-unit"
>
{{ getDaysUnit() }}
</text>
</view>
<view
class="member-card-preview__purchase"
hover-class="mi-tap-btn--hover"
:hover-stay-time="150"
@tap.stop="$emit('purchase')"
>
<text
class="member-card-preview__purchase-text"
>
购买新卡
</text>
</view>
</view>
<text class="card-three-item__label">储值卡</text>
<text class="card-three-item__value" v-if="storedCard.hasCard">¥{{ storedCard.balance }}</text>
<text class="card-three-item__value card-three-item__value--empty" v-else>未购买</text>
<text class="card-three-item__sub" v-if="storedCard.hasCard">余额</text>
</view>
<!-- 次数卡 -->
<view
class="card-three-item"
:class="{ 'card-three-item--inactive': !countCard.hasCard }"
>
<view class="card-three-item__icon-wrap card-three-item__icon-wrap--count">
<uni-icons type="loop" size="28rpx" :color="countCard.hasCard ? '#4CAF50' : '#B0BEC5'" />
</view>
<text class="card-three-item__label">次数卡</text>
<text class="card-three-item__value" v-if="countCard.hasCard">{{ countCard.remainingTimes }}</text>
<text class="card-three-item__value card-three-item__value--empty" v-else>未购买</text>
<text class="card-three-item__sub" v-if="countCard.hasCard">剩余</text>
</view>
<!-- 时长卡 -->
<view
class="card-three-item"
:class="{ 'card-three-item--inactive': !timeCard.hasCard }"
>
<view class="card-three-item__icon-wrap card-three-item__icon-wrap--time">
<uni-icons type="calendar" size="28rpx" :color="timeCard.hasCard ? '#FF6B35' : '#B0BEC5'" />
</view>
<text class="card-three-item__label">时长卡</text>
<text class="card-three-item__value" v-if="timeCard.hasCard">{{ timeCard.remainingDays }}</text>
<text class="card-three-item__value card-three-item__value--empty" v-else>未购买</text>
<text class="card-three-item__sub" v-if="timeCard.hasCard">剩余</text>
</view>
</view>
<!-- 操作按钮 -->
<view class="card-actions">
<view
class="card-actions__btn card-actions__btn--primary"
hover-class="mi-tap-btn--hover"
:hover-stay-time="150"
@tap="$emit('view-all')"
>
<text class="card-actions__btn-text">查看卡片</text>
</view>
<view
class="card-actions__btn card-actions__btn--ghost"
hover-class="mi-tap-btn--hover"
:hover-stay-time="150"
@tap.stop="$emit('purchase')"
>
<text class="card-actions__btn-text card-actions__btn-text--ghost">购买新卡</text>
</view>
</view>
</view>
@@ -100,176 +84,260 @@
</template>
<script setup>
import { reactive, onMounted } from 'vue'
import { getPrimaryMemberCard, getMyMemberCardsWithStatus } from '@/api/main.js'
import { reactive, ref, onMounted } from 'vue'
import { getStoredCardInfo, getMyMemberCardsWithStatus } from '@/api/main.js'
import { getMemberId } from '@/utils/request.js'
defineEmits(['view-all', 'purchase'])
const cardInfo = reactive({
name: '',
type: '',
remainingTimes: 0,
remainingDays: 0,
expireDate: '',
activeCount: 0,
isExpiring: false,
isExpired: false,
isUsedUp: false,
hasActiveCard: false
})
defineEmits(['view-all', 'purchase', 'stored-card-tap'])
const storedCard = reactive({ hasCard: false, balance: 0 })
const countCard = reactive({ hasCard: false, remainingTimes: 0 })
const timeCard = reactive({ hasCard: false, remainingDays: 0 })
const activeCount = ref(0)
let isLoading = false
// 格式化日期
function formatDate(dateStr) {
if (!dateStr) return ''
try {
const date = new Date(dateStr)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
} catch (e) {
return dateStr
}
}
// 加载会员卡数据
async function loadMemberCard() {
if (isLoading) {
console.log('[MemberInfoMemberCard] 正在加载中,跳过重复请求')
return
}
if (isLoading) return
isLoading = true
try {
// 使用统一的 getMemberId 方法获取会员ID
const memberId = getMemberId()
console.log('[MemberInfoMemberCard] memberId:', memberId)
if (!memberId) {
console.log('[MemberInfoMemberCard] 未登录,无会员ID')
return
}
console.log('[MemberInfoMemberCard] 加载会员卡数据, memberId:', memberId)
// 并行获取主要会员卡和有效卡列表
const [primaryCardRes, activeCardsRes] = await Promise.all([
getPrimaryMemberCard(memberId),
getMyMemberCardsWithStatus(memberId, 'active')
if (!memberId) return
// 并行获取储值卡和所有有效卡
const [storedRes, activeCardsRes] = await Promise.all([
getStoredCardInfo(memberId).catch(() => null),
getMyMemberCardsWithStatus(memberId, 'active').catch(() => null)
])
console.log('[MemberInfoMemberCard] getPrimaryMemberCard 返回:', JSON.stringify(primaryCardRes, null, 2))
console.log('[MemberInfoMemberCard] getMyMemberCardsWithStatus(active) 返回:', JSON.stringify(activeCardsRes, null, 2))
// 解析主要会员卡数据
let displayCard = null
if (primaryCardRes?.data) {
displayCard = primaryCardRes.data
} else if (primaryCardRes?.value) {
displayCard = primaryCardRes.value
} else if (primaryCardRes && typeof primaryCardRes === 'object') {
displayCard = primaryCardRes
}
// 计算有效会员卡数量(过滤储值卡)
let activeCount = 0
let allActiveCards = []
if (activeCardsRes?.value) {
allActiveCards = activeCardsRes.value
} else if (Array.isArray(activeCardsRes)) {
allActiveCards = activeCardsRes
} else if (activeCardsRes?.data) {
allActiveCards = Array.isArray(activeCardsRes.data) ? activeCardsRes.data : (activeCardsRes.data.value || [])
}
// 过滤储值卡
allActiveCards = allActiveCards.filter(card => card.memberCardType !== 'STORED_VALUE_CARD')
activeCount = allActiveCards.length
// 计算卡片状态
let diffDays = 0
let isExpiring = false
let isExpired = false
let isUsedUp = false
let displayDays = 0
if (displayCard) {
if (displayCard.status === 'USED_UP') {
isUsedUp = true
displayDays = displayCard.remainingTimes || 0
} else if (displayCard.expireTime) {
const expireTime = new Date(displayCard.expireTime)
const now = new Date()
diffDays = Math.ceil((expireTime - now) / (1000 * 60 * 60 * 24))
isExpiring = diffDays > 0 && diffDays <= 3
isExpired = diffDays <= 0
displayDays = Math.abs(diffDays)
// 解析储值卡
if (storedRes) {
const data = storedRes.data || storedRes.value || storedRes
if (data && (data.balance > 0 || data.totalAmount > 0 || data.id)) {
storedCard.hasCard = true
storedCard.balance = data.balance || data.totalAmount || 0
}
}
console.log(displayCard)
// 更新数据
cardInfo.name = displayCard?.memberCardName || '暂无会员卡'
cardInfo.type = displayCard?.memberCardType || ''
cardInfo.remainingTimes = displayCard?.remainingTimes || displayCard?.remainingAmount || 0
cardInfo.remainingDays = displayDays
cardInfo.expireDate = displayCard?.expireTime ? formatDate(displayCard.expireTime) : ''
cardInfo.activeCount = activeCount
cardInfo.isExpiring = isExpiring
cardInfo.isExpired = isExpired
cardInfo.isUsedUp = isUsedUp
cardInfo.hasActiveCard = activeCount > 0
console.log('[MemberInfoMemberCard] 更新后的 cardInfo:', JSON.stringify(cardInfo, null, 2))
// 解析有效卡列表(排除储值卡)
let allActive = []
if (activeCardsRes?.value) {
allActive = activeCardsRes.value
} else if (Array.isArray(activeCardsRes)) {
allActive = activeCardsRes
} else if (activeCardsRes?.data) {
allActive = Array.isArray(activeCardsRes.data) ? activeCardsRes.data : (activeCardsRes.data.value || [])
}
allActive = allActive.filter(card => card.memberCardType !== 'STORED_VALUE_CARD')
activeCount.value = allActive.length
// 查找次数卡
const countCardData = allActive.find(c => c.memberCardType === 'COUNT_CARD')
if (countCardData) {
countCard.hasCard = true
countCard.remainingTimes = countCardData.remainingTimes || countCardData.remainingAmount || 0
}
// 查找时长卡(优先找有效期最长的)
const timeCards = allActive.filter(c => c.memberCardType === 'TIME_CARD')
if (timeCards.length > 0) {
timeCard.hasCard = true
// 计算剩余天数
let maxDays = 0
timeCards.forEach(c => {
if (c.expireTime) {
const now = new Date()
const expire = new Date(c.expireTime)
const days = Math.max(0, Math.ceil((expire - now) / (1000 * 60 * 60 * 24)))
if (days > maxDays) maxDays = days
}
})
timeCard.remainingDays = maxDays
}
} catch (e) {
console.error('[MemberInfoMemberCard] 加载会员卡数据失败:', e)
cardInfo.hasActiveCard = false
console.error('[MemberInfoMemberCard] 加载失败:', e)
} finally {
isLoading = false
}
}
// 组件挂载时加载数据
onMounted(() => {
console.log('[MemberInfoMemberCard] 组件已挂载,开始加载数据')
loadMemberCard()
})
function getTagClass() {
if (cardInfo.isExpiring) return 'member-card-preview__tag--expiring'
if (cardInfo.isExpired) return 'member-card-preview__tag--expired'
if (cardInfo.isUsedUp) return 'member-card-preview__tag--expired'
return 'member-card-preview__tag--active'
}
function getTagText() {
if (cardInfo.isUsedUp) return '已用完'
if (cardInfo.isExpired) return '已失效'
if (cardInfo.isExpiring) return '临期'
return '有效'
}
function getDaysClass() {
if (cardInfo.isExpiring) return 'member-card-preview__days-num--expiring'
if (cardInfo.isExpired) return 'member-card-preview__days-num--expired'
if (cardInfo.isUsedUp) return 'member-card-preview__days-num--expired'
return ''
}
function getDisplayDays() {
if (cardInfo.isUsedUp) return cardInfo.remainingTimes || 0
return cardInfo.remainingDays || 0
}
function getDaysUnit() {
if (cardInfo.isUsedUp) return '次剩余'
return '天剩余'
}
defineExpose({ loadMemberCard })
</script>
<style lang="scss">
<style lang="scss" scoped>
@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';
// ========== 容器 ==========
.member-card-section {
width: 100%;
flex-shrink: 0;
}
.member-card-section__inner {
display: flex;
flex-direction: column;
gap: 20rpx;
width: 100%;
}
// ========== 标题栏 ==========
.member-card-section__head {
width: 100%;
}
.member-card-section__head-inner {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
}
.member-card-section__title {
font-size: var(--font-size-md);
font-weight: 700;
color: var(--text-dark);
}
.member-card-section__count {
display: inline-flex;
flex-direction: row;
align-items: center;
gap: 4rpx;
padding: 4rpx 14rpx;
border-radius: 20rpx;
background: rgba(255, 107, 53, 0.1);
flex-shrink: 0;
}
.member-card-section__count-num {
font-size: 26rpx;
font-weight: 700;
color: #FF6B35;
line-height: 1;
}
.member-card-section__count-text {
font-size: 22rpx;
font-weight: 400;
color: #FF6B35;
line-height: 1;
}
// ========== 三张卡片行 ==========
.card-three-row {
display: flex;
gap: 16rpx;
width: 100%;
}
.card-three-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
padding: 24rpx 12rpx;
border-radius: 16rpx;
background: #FFFFFF;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
transition: all 0.2s;
}
.card-three-item--inactive {
opacity: 0.55;
background: #F5F7FA;
box-shadow: none;
}
.card-three-item__icon-wrap {
width: 56rpx;
height: 56rpx;
border-radius: 28rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.card-three-item__icon-wrap--stored {
background: rgba(102, 126, 234, 0.1);
}
.card-three-item__icon-wrap--count {
background: rgba(76, 175, 80, 0.1);
}
.card-three-item__icon-wrap--time {
background: rgba(255, 107, 53, 0.1);
}
.card-three-item--inactive .card-three-item__icon-wrap {
background: rgba(0, 0, 0, 0.04);
}
.card-three-item__label {
font-size: 24rpx;
font-weight: 500;
color: #5E6F8D;
}
.card-three-item__value {
font-size: 26rpx;
font-weight: 700;
color: #1E2A3A;
line-height: 1;
}
.card-three-item__value--empty {
font-size: 24rpx;
font-weight: 400;
color: #B0BEC5;
}
.card-three-item__sub {
font-size: 20rpx;
color: #A0AEC0;
}
// ========== 操作按钮 ==========
.card-actions {
display: flex;
gap: 16rpx;
width: 100%;
}
.card-actions__btn {
flex: 1;
height: 72rpx;
border-radius: 36rpx;
display: flex;
align-items: center;
justify-content: center;
}
.card-actions__btn--primary {
background: linear-gradient(135deg, #FF6B35, #FF8C5A);
box-shadow: 0 4rpx 12rpx rgba(255, 107, 53, 0.3);
}
.card-actions__btn--ghost {
background: #FFFFFF;
border: 2rpx solid #FF6B35;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
}
.card-actions__btn-text {
font-size: 28rpx;
font-weight: 600;
color: #FFFFFF;
}
.card-actions__btn-text--ghost {
color: #FF6B35;
}
</style>