feat: migrate ice venue module

This commit is contained in:
gqt
2026-06-08 21:00:32 +08:00
parent 8e1443f7e5
commit 0f4e04f4ad
28 changed files with 5684 additions and 0 deletions
@@ -0,0 +1,511 @@
<template>
<view class="detail-container">
<StatePanel
v-if="pageState === 'loading'"
title="正在加载预约信息"
description="请稍候"
type="loading"
/>
<StatePanel
v-else-if="sessionStatus === SESSION_STATUS.GUEST"
title="登录后查看预约详情"
description="请先登录并完善会员资料。"
primary-text="微信一键登录"
type="login"
:loading="isLoginSubmitting"
@primary="onWechatLogin"
/>
<StatePanel
v-else-if="sessionStatus === SESSION_STATUS.EXPIRED"
title="登录状态已过期"
description="为了保护会员资料,请重新登录后继续查看。"
primary-text="重新登录"
type="expired"
:loading="isLoginSubmitting"
@primary="onWechatLogin"
/>
<StatePanel
v-else-if="sessionStatus === SESSION_STATUS.PROFILE_REQUIRED"
title="完善会员资料后查看预约"
description="请先在会员中心完成姓名和手机号绑定,用于匹配你的预约记录。"
primary-text="去完善资料"
type="profile"
@primary="goProfile"
/>
<StatePanel
v-else-if="sessionStatus === SESSION_STATUS.NETWORK_ERROR"
title="暂时无法连接服务"
:description="pageMessage || '请检查网络后重试。'"
primary-text="重试"
type="error"
@primary="loadPage"
/>
<StatePanel
v-else-if="pageState === 'not_found'"
title="未找到预约信息"
:description="pageMessage || '该预约不存在,或你没有查看权限。'"
primary-text="返回预约列表"
type="empty"
@primary="goBookings"
/>
<StatePanel
v-else-if="pageState === 'error'"
title="预约详情加载失败"
:description="pageMessage"
primary-text="重试"
type="error"
@primary="loadPage"
/>
<block v-else>
<!-- 1. 顶部状态横幅 -->
<view class="status-banner" :class="booking.status">
<text class="status-text">{{ formatStatus(booking.status) }}</text>
</view>
<!-- 2. 课程信息卡片支持点击跳转到课程详情 -->
<view class="detail-card clickable-card" @tap="onTapCourse(booking.course?.id)">
<view class="card-section">
<text class="section-tag">关联课程</text>
<view class="title-with-arrow">
<text class="course-title">{{ booking.course?.title }}</text>
<text class="nav-arrow"></text>
</view>
</view>
<view class="divider"></view>
<view class="info-grid">
<view class="info-row">
<text class="info-label">上课时段</text>
<text class="info-val highlight-blue">时间{{ formatDateTimeRange(booking.course?.start_time, booking.course?.end_time) }}</text>
</view>
<view class="info-row">
<text class="info-label">预约人数</text>
<text class="info-val font-semibold">已约 {{ booking.course?.booked_count }}/{{ booking.course?.capacity }} </text>
</view>
</view>
</view>
<!-- 3. 会员卡信息卡片支持点击跳转到卡片详情 -->
<view class="detail-card clickable-card" v-if="booking.card" @tap="onTapCard(booking.card?.id)">
<view class="card-section">
<text class="section-tag">使用卡项</text>
<view class="title-with-arrow">
<text class="card-title">{{ booking.card.card_name }}</text>
<text class="nav-arrow"></text>
</view>
</view>
</view>
<!-- 4. 流程时间节点展示 -->
<view class="detail-card timeline-card">
<text class="section-tag">时间节点</text>
<view class="log-timeline">
<view class="timeline-item" v-if="booking.created_at">
<view class="timeline-dot active"></view>
<view class="timeline-content">
<text class="log-action">预约时间</text>
<text class="log-time">{{ formatFullDateTime(booking.created_at) }}</text>
</view>
</view>
<view class="timeline-item" v-if="booking.status === 'checked_in' && booking.checked_in_at">
<view class="timeline-dot active success"></view>
<view class="timeline-content">
<text class="log-action success">核销时间</text>
<text class="log-time">{{ formatFullDateTime(booking.checked_in_at) }}</text>
</view>
</view>
<view class="timeline-item" v-if="booking.status === 'cancelled' && booking.cancelled_at">
<view class="timeline-dot active danger"></view>
<view class="timeline-content">
<text class="log-action danger">取消时间</text>
<text class="log-time">{{ formatFullDateTime(booking.cancelled_at) }}</text>
</view>
</view>
</view>
</view>
<!-- 5. 温馨提示 -->
<view class="bottom-disclaimer">
<text class="disclaimer-title">行程变动须知</text>
<text class="disclaimer-content">所有的上课行程均由场馆统一排定展示如需取消或改约请提前联系场馆前台处理</text>
</view>
</block>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { bookingApi } from '@/modules/ice/api/booking'
import { errorMessage, getMemberSession, loginAndStoreToken, sessionStatusFromError, SESSION_STATUS } from '@/modules/ice/api/session'
import StatePanel from '@/components/ice/StatePanel.vue'
const booking = ref({
id: null,
status: '',
created_at: '',
checked_in_at: null,
cancelled_at: null,
course: {
id: null,
title: '',
start_time: '',
end_time: '',
capacity: 0,
booked_count: 0
},
card: null
})
const pageState = ref('loading')
const pageMessage = ref('')
const sessionStatus = ref(SESSION_STATUS.CHECKING)
const isLoginSubmitting = ref(false)
const currentBookingId = ref('')
onShow(() => {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const options = currentPage.options || {}
currentBookingId.value = options.id || ''
loadPage()
})
const loadPage = async () => {
if (!currentBookingId.value) {
pageState.value = 'not_found'
pageMessage.value = '未找到要查看的预约记录。'
return
}
pageState.value = 'loading'
pageMessage.value = ''
const session = await getMemberSession()
sessionStatus.value = session.status
if (session.status !== SESSION_STATUS.MEMBER) {
pageState.value = 'blocked'
pageMessage.value = session.message || ''
return
}
try {
booking.value = await bookingApi.getBookingDetail(currentBookingId.value)
pageState.value = 'ready'
} catch (err) {
const status = sessionStatusFromError(err)
if (status) {
sessionStatus.value = status
pageState.value = 'blocked'
pageMessage.value = err?.message || ''
return
}
if (err?.type === 'not_found') {
pageState.value = 'not_found'
pageMessage.value = '该预约不存在,或你没有查看权限。'
return
}
pageState.value = 'error'
pageMessage.value = errorMessage(err, '预约详情加载失败,请稍后重试。')
}
}
const onWechatLogin = async () => {
if (isLoginSubmitting.value) return
isLoginSubmitting.value = true
uni.showLoading({ title: '登录中...' })
try {
await loginAndStoreToken()
await loadPage()
if (sessionStatus.value === SESSION_STATUS.MEMBER && pageState.value === 'ready') {
uni.showToast({ title: '登录成功', icon: 'success' })
}
} catch (err) {
uni.showModal({
title: '登录失败',
content: errorMessage(err, '微信登录失败,请稍后重试'),
showCancel: false,
confirmText: '知道了'
})
} finally {
isLoginSubmitting.value = false
uni.hideLoading()
}
}
const formatStatus = (status) => {
const map = {
booked: '待核销上课',
checked_in: '已签到核销',
cancelled: '行程已取消'
}
return map[status] || status
}
const onTapCourse = (courseId) => {
if (!courseId) return
uni.navigateTo({
url: `/pages/ice/courses/detail?id=${courseId}`
})
}
const onTapCard = (cardId) => {
if (!cardId) return
uni.navigateTo({
url: `/pages/ice/profile/card-history?id=${cardId}`
})
}
const goProfile = () => {
uni.reLaunch({ url: '/pages/ice/profile/index' })
}
const goBookings = () => {
uni.reLaunch({ url: '/pages/ice/bookings/index' })
}
const formatDateTimeRange = (startStr, endStr) => {
if (!startStr || !endStr) return '-'
const start = new Date(startStr)
const end = new Date(endStr)
const pad = (n) => String(n).padStart(2, '0')
const mm = pad(start.getMonth() + 1)
const dd = pad(start.getDate())
const startHhMm = `${pad(start.getHours())}:${pad(start.getMinutes())}`
const endHhMm = `${pad(end.getHours())}:${pad(end.getMinutes())}`
return `${mm}-${dd} ${startHhMm} - ${endHhMm}`
}
const formatFullDateTime = (dateStr) => {
if (!dateStr) return ''
const date = new Date(dateStr)
const pad = (n) => String(n).padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
}
</script>
<style lang="scss" scoped>
.detail-container {
min-height: 100vh;
background-color: $bg-color-soft;
padding: $spacing-md;
display: flex;
flex-direction: column;
gap: $spacing-md;
}
/* 顶部状态横幅 */
.status-banner {
border-radius: $border-radius-lg;
padding: $spacing-lg;
color: #FFFFFF;
display: flex;
flex-direction: column;
gap: $spacing-xs;
&.booked {
background-color: $text-color-main;
}
&.checked_in {
background-color: $color-success;
}
&.cancelled {
background-color: $text-color-muted;
}
.status-text {
font-size: 36rpx;
font-weight: 700;
}
.status-desc {
font-size: 22rpx;
opacity: 0.9;
}
}
/* 详情卡片通用 */
.detail-card {
background-color: $bg-color-main;
border: 1rpx solid rgba(226, 232, 240, 0.8);
border-radius: $border-radius-lg;
padding: $spacing-lg;
&.clickable-card {
transition: all 0.2s ease;
&:active {
background-color: $bg-color-hover;
}
}
.card-section {
display: flex;
flex-direction: column;
gap: 6rpx;
}
.section-tag {
font-size: 18rpx;
font-weight: 700;
color: $color-primary;
letter-spacing: 1.5rpx;
text-transform: uppercase;
margin-bottom: 4rpx;
display: block;
}
.title-with-arrow {
display: flex;
justify-content: space-between;
align-items: center;
gap: $spacing-md;
.nav-arrow {
font-size: 30rpx;
color: $text-color-light;
font-weight: bold;
}
}
.course-title, .card-title {
font-size: 30rpx;
font-weight: 700;
color: $text-color-main;
line-height: 1.35;
flex: 1;
}
.info-grid {
display: flex;
flex-direction: column;
gap: $spacing-md;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 24rpx;
}
.info-label {
color: $text-color-muted;
}
.info-val {
color: $text-color-main;
font-weight: 500;
&.font-semibold {
font-weight: 700;
}
&.highlight-blue {
color: $color-primary;
font-weight: 700;
}
}
}
/* 极简时间轴 */
.log-timeline {
display: flex;
flex-direction: column;
gap: $spacing-lg;
padding: $spacing-md 0 0 $spacing-xs;
}
.timeline-item {
display: flex;
gap: $spacing-md;
position: relative;
&::before {
content: '';
position: absolute;
left: 8rpx;
top: 20rpx;
bottom: -32rpx;
width: 2rpx;
background-color: $bg-color-hover;
}
&:last-child::before {
display: none;
}
.timeline-dot {
width: 18rpx;
height: 18rpx;
border-radius: 50%;
background-color: $border-color;
margin-top: 8rpx;
flex-shrink: 0;
&.active {
background-color: $text-color-main;
&.success {
background-color: $color-success;
}
&.danger {
background-color: $color-danger;
}
}
}
.timeline-content {
display: flex;
flex-direction: column;
gap: 4rpx;
}
.log-action {
font-size: 24rpx;
font-weight: 600;
color: $text-color-regular;
&.success {
color: $color-success;
}
&.danger {
color: $color-danger;
}
}
.log-time {
font-size: 20rpx;
color: $text-color-light;
}
}
/* 温馨提示 */
.bottom-disclaimer {
padding: 0 $spacing-xs;
display: flex;
flex-direction: column;
gap: 8rpx;
.disclaimer-title {
font-size: 22rpx;
font-weight: 700;
color: $text-color-muted;
text-transform: uppercase;
}
.disclaimer-content {
font-size: 20rpx;
color: $text-color-light;
line-height: 1.5;
}
}
</style>
@@ -0,0 +1,544 @@
<template>
<view class="bookings-container">
<StatePanel
v-if="sessionStatus === SESSION_STATUS.CHECKING"
title="正在检查登录状态"
description="请稍候"
type="loading"
/>
<StatePanel
v-else-if="sessionStatus === SESSION_STATUS.GUEST"
primary-text="微信一键登录"
type="login"
:show-mark="false"
:loading="isLoginSubmitting"
@primary="onWechatLogin"
/>
<StatePanel
v-else-if="sessionStatus === SESSION_STATUS.EXPIRED"
title="登录状态已过期"
description="为了保护会员资料,请重新登录后继续查看预约行程。"
primary-text="重新登录"
type="expired"
:loading="isLoginSubmitting"
@primary="onWechatLogin"
/>
<StatePanel
v-else-if="sessionStatus === SESSION_STATUS.PROFILE_REQUIRED"
title="完善会员资料后查看预约"
description="请先在会员中心完成姓名和手机号绑定,用于匹配你的预约记录。"
primary-text="去完善资料"
type="profile"
@primary="goProfile"
/>
<StatePanel
v-else-if="sessionStatus === SESSION_STATUS.NETWORK_ERROR"
title="暂时无法连接服务"
:description="sessionMessage || '请检查网络后重试。'"
primary-text="重试"
type="error"
@primary="checkSessionAndLoad"
/>
<!-- 已登录正常预约列表 -->
<block v-else>
<!-- 顶部极简选项卡 -->
<view class="tab-header">
<view
v-for="tab in tabs"
:key="tab.status"
class="tab-item"
:class="{ active: currentStatus === tab.status }"
@tap="onSelectTab(tab.status)"
>
<text class="tab-label">{{ tab.label }}</text>
<view class="active-indicator"></view>
</view>
</view>
<!-- 预约列表 -->
<view class="booking-list">
<StatePanel
v-if="listState === 'error'"
title="预约行程加载失败"
:description="listErrorMessage"
primary-text="重试"
type="error"
compact
@primary="loadBookings"
/>
<!-- 列表卡片 -->
<block v-else>
<view
v-for="booking in bookings"
:key="booking.id"
class="booking-card"
:class="[booking.status]"
@tap="onViewDetail(booking.id)"
>
<view class="card-top">
<text class="course-name">{{ booking.course.title }}</text>
<text class="status-badge" :class="booking.status">{{ formatStatus(booking.status) }}</text>
</view>
<view class="card-details">
<text class="detail-timehighlight highlight-blue">时间{{ formatDateTimeRange(booking.course.start_time, booking.course.end_time) }}</text>
<text class="detail-capacity"> 人数已约 {{ booking.course.booked_count }}/{{ booking.course.capacity }}</text>
</view>
</view>
</block>
<!-- 加载更多提示 -->
<view v-if="isLoadingMore" class="load-more-tip">
<text>加载中...</text>
</view>
<view v-else-if="bookings.length > 0 && !hasMore" class="load-more-tip">
<text> 已加载全部 </text>
</view>
<!-- 极致优雅空状态 -->
<StatePanel
v-if="listState !== 'error' && !isLoading && bookings.length === 0"
title="暂无预约行程"
description="场馆暂未为你登记预约行程。"
type="empty"
compact
/>
</view>
</block>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onPullDownRefresh, onReachBottom, onShow } from '@dcloudio/uni-app'
import { bookingApi } from '@/modules/ice/api/booking'
import { authStorage } from '@/modules/ice/api/member'
import { errorMessage, getMemberSession, loginAndStoreToken, sessionStatusFromError, SESSION_STATUS } from '@/modules/ice/api/session'
import StatePanel from '@/components/ice/StatePanel.vue'
const isLoggedIn = ref(false)
const isLoginSubmitting = ref(false)
const sessionStatus = ref(SESSION_STATUS.CHECKING)
const sessionMessage = ref('')
const currentStatus = ref('all')
const isLoading = ref(false)
const isLoadingMore = ref(false)
const listState = ref('idle')
const listErrorMessage = ref('')
const page = ref(1)
const pageSize = 20
const total = ref(0)
const hasMore = computed(() => bookings.value.length < total.value)
const tabs = [
{ label: '全部', status: 'all' },
{ label: '已预约', status: 'booked' },
{ label: '已核销', status: 'checked_in' },
{ label: '已取消', status: 'cancelled' }
]
const bookings = ref([])
onShow(() => {
checkSessionAndLoad()
})
const checkSessionAndLoad = async () => {
sessionStatus.value = SESSION_STATUS.CHECKING
const session = await getMemberSession()
sessionStatus.value = session.status
sessionMessage.value = session.message || ''
isLoggedIn.value = session.status === SESSION_STATUS.MEMBER
if (session.status === SESSION_STATUS.MEMBER) {
await loadBookings()
return
}
bookings.value = []
total.value = 0
listState.value = 'idle'
}
const onWechatLogin = async () => {
if (isLoginSubmitting.value) return
isLoginSubmitting.value = true
uni.showLoading({ title: '登录中...' })
try {
await loginAndStoreToken()
await checkSessionAndLoad()
if (sessionStatus.value === SESSION_STATUS.MEMBER) {
uni.showToast({ title: '登录成功', icon: 'success' })
}
} catch (err) {
uni.showModal({
title: '登录失败',
content: errorMessage(err, '微信登录失败,请稍后重试'),
showCancel: false,
confirmText: '知道了'
})
} finally {
isLoginSubmitting.value = false
uni.hideLoading()
}
}
onPullDownRefresh(() => {
page.value = 1
const task = sessionStatus.value === SESSION_STATUS.MEMBER ? loadBookings() : checkSessionAndLoad()
task.finally(() => {
uni.stopPullDownRefresh()
})
})
onReachBottom(() => {
if (sessionStatus.value === SESSION_STATUS.MEMBER && hasMore.value && !isLoadingMore.value) {
page.value++
loadMoreBookings()
}
})
const loadBookings = async () => {
if (isLoading.value) return
isLoading.value = true
try {
const data = await bookingApi.getBookings({
status: currentStatus.value,
page: 1,
pageSize
})
bookings.value = data.items
total.value = data.total
listState.value = 'ready'
listErrorMessage.value = ''
} catch (err) {
if (applySessionError(err)) return
listState.value = 'error'
listErrorMessage.value = errorMessage(err, '预约行程加载失败,请稍后重试。')
} finally {
isLoading.value = false
}
}
const loadMoreBookings = async () => {
if (isLoadingMore.value) return
isLoadingMore.value = true
try {
const data = await bookingApi.getBookings({
status: currentStatus.value,
page: page.value,
pageSize
})
bookings.value = [...bookings.value, ...data.items]
total.value = data.total
} catch (err) {
if (applySessionError(err)) return
page.value--
uni.showToast({ title: errorMessage(err, '加载更多失败,请重试'), icon: 'none' })
} finally {
isLoadingMore.value = false
}
}
const onSelectTab = (status) => {
currentStatus.value = status
page.value = 1
if (sessionStatus.value === SESSION_STATUS.MEMBER) {
loadBookings()
}
}
const onViewDetail = (id) => {
uni.navigateTo({
url: `/pages/ice/bookings/detail?id=${id}`
})
}
const formatStatus = (status) => {
const map = {
booked: '已预约',
checked_in: '已核销',
cancelled: '已取消'
}
return map[status] || status
}
const formatDateTimeRange = (startStr, endStr) => {
const start = new Date(startStr)
const end = new Date(endStr)
const pad = (n) => String(n).padStart(2, '0')
const mm = pad(start.getMonth() + 1)
const dd = pad(start.getDate())
const startHhMm = `${pad(start.getHours())}:${pad(start.getMinutes())}`
const endHhMm = `${pad(end.getHours())}:${pad(end.getMinutes())}`
return `${mm}-${dd} ${startHhMm} - ${endHhMm}`
}
const applySessionError = (err) => {
const status = sessionStatusFromError(err)
if (!status) return false
sessionStatus.value = status
sessionMessage.value = err?.message || ''
isLoggedIn.value = false
bookings.value = []
total.value = 0
listState.value = 'idle'
if (status === SESSION_STATUS.EXPIRED) {
authStorage.clear()
}
return true
}
const goProfile = () => {
uni.reLaunch({ url: '/pages/ice/profile/index' })
}
</script>
<style lang="scss" scoped>
.bookings-container {
min-height: 100vh;
background-color: $bg-color-soft;
padding: $spacing-lg $spacing-md;
display: flex;
flex-direction: column;
}
/* 未登录全屏占位 */
.unlogged-full-wrapper {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 160rpx 0 $spacing-xl 0;
width: 100%;
.brand-logo-area {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 120rpx;
text-align: center;
.brand-title-en {
font-size: 48rpx;
font-weight: 800;
color: $text-color-main;
letter-spacing: 6rpx;
line-height: 1.2;
}
.brand-subtitle-zh {
font-size: 24rpx;
font-weight: 500;
color: $text-color-muted;
letter-spacing: 4rpx;
margin-top: 16rpx;
}
.brand-line {
width: 40rpx;
height: 2rpx;
background-color: $text-color-main;
margin-top: 24rpx;
}
}
.btn-auth-login-only {
width: 80%;
height: 90rpx;
background-color: $text-color-main;
color: #FFFFFF;
font-size: 28rpx;
font-weight: 700;
border-radius: $border-radius-base;
display: flex;
align-items: center;
justify-content: center;
border: none;
box-shadow: 0 4rpx 15rpx rgba(15, 23, 42, 0.08);
transition: all 0.2s ease;
&::after {
border: none;
}
&:active {
opacity: 0.9;
background-color: #000000;
}
}
}
/* 顶部极简选项卡 */
.tab-header {
display: flex;
background-color: $bg-color-main;
border-bottom: 1rpx solid $bg-color-hover;
margin: (-$spacing-lg) (-$spacing-md) 0;
padding: 0 $spacing-md;
}
.tab-item {
flex: 1;
text-align: center;
padding: $spacing-md 0;
position: relative;
transition: all 0.2s ease;
.tab-label {
font-size: 26rpx;
color: $text-color-muted;
font-weight: 500;
}
.active-indicator {
position: absolute;
bottom: 0;
left: 40%;
right: 40%;
height: 4rpx;
background-color: transparent;
border-radius: 2rpx;
transition: all 0.2s ease;
}
&.active {
.tab-label {
color: $text-color-main;
font-weight: 700;
}
.active-indicator {
background-color: $text-color-main;
}
}
}
/* 预约列表 */
.booking-list {
padding: $spacing-lg 0 $spacing-md;
display: flex;
flex-direction: column;
}
.booking-card {
background-color: $bg-color-main;
border: 1rpx solid rgba(226, 232, 240, 0.8);
border-radius: $border-radius-lg;
padding: $spacing-lg;
margin-bottom: $spacing-md;
overflow: hidden;
transition: all 0.2s ease;
&:active {
background-color: $bg-color-hover;
}
/* 状态左侧极简粗边指示 */
&.booked {
border-left: 6rpx solid $color-primary;
}
&.checked_in {
border-left: 6rpx solid $color-success;
}
&.cancelled {
border-left: 6rpx solid $text-color-light;
opacity: 0.6;
}
.card-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: $spacing-sm;
min-height: 38rpx;
}
.course-name {
font-size: 28rpx;
font-weight: 700;
color: $text-color-main;
line-height: 1.35;
flex: 1;
}
.status-badge {
font-size: 22rpx;
font-weight: 700;
flex-shrink: 0;
margin-left: $spacing-xs;
&.booked {
color: $color-primary;
}
&.checked_in {
color: $color-success;
}
&.cancelled {
color: $text-color-muted;
}
}
.card-details {
display: flex;
flex-direction: column;
gap: 4rpx;
}
.detail-timehighlight {
font-size: 22rpx;
color: $text-color-muted;
&.highlight-blue {
color: $color-primary;
font-weight: 600;
}
}
.detail-capacity {
font-size: 22rpx;
color: $text-color-muted;
}
}
/* 极致空状态 */
.empty-state {
text-align: center;
padding: 160rpx $spacing-lg;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.empty-text {
font-size: 30rpx;
font-weight: 700;
color: $text-color-main;
margin-bottom: $spacing-md;
}
.empty-sub {
font-size: 24rpx;
color: $text-color-muted;
line-height: 1.6;
max-width: 520rpx;
}
}
.load-more-tip {
text-align: center;
padding: 0 0 $spacing-md;
font-size: 22rpx;
color: $text-color-light;
}
</style>