Files
1818uniapp/src/pages/points/subscribe.vue
2026-02-13 17:36:42 +08:00

1036 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="subscribe-page">
<!-- 背景图 -->
<image
class="page-bg"
:src="bgImages[activeIndex]"
mode="aspectFill"
/>
<!-- 顶部导航 -->
<view class="nav-bar" :style="{ paddingTop: safeAreaInsets.top + 'px' }">
<view class="nav-left" @click="goBack">
<image class="back-icon" src="/static/icons/Left (左).png" mode="aspectFit" />
</view>
<view class="nav-center">
<view class="points-badge" @click="showPointsDetail">
<text class="points-label">我的积分</text>
<image class="points-icon" src="/static/icons/points.png" mode="aspectFit" />
<text class="points-value">{{ userPoints }}</text>
<text class="points-arrow"></text>
</view>
</view>
<view class="nav-right" />
</view>
<!-- 套餐Tab切换 - 胶囊样式 -->
<view class="package-tabs" v-if="packages.length > 0">
<view class="tabs-wrapper">
<view
v-for="(pkg, index) in packages"
:key="pkg.id"
class="tab-item"
:class="{ active: activeIndex === index }"
@click="selectPackage(index)"
>
<text class="tab-text">{{ getTabName(pkg.name) }}</text>
</view>
</view>
</view>
<!-- 套餐详情区域 -->
<view class="package-detail" v-if="currentPackage">
<view class="detail-content">
<view class="points-display">
<text class="points-num" :class="'gradient-' + activeIndex">{{ currentPackage.points }}</text>
<image class="points-diamond" src="/static/icons/points.png" mode="aspectFit" />
<text class="points-unit" :class="'gradient-' + activeIndex">积分</text>
</view>
<view class="feature-list">
<view class="feature-item" v-for="(desc, idx) in getDescriptions" :key="idx">
<image class="feature-icon" src="/static/icons/bard-fill.png" mode="aspectFit" />
<text class="feature-text">{{ desc }}</text>
</view>
<view class="feature-item" v-if="currentPackage.bonusPoints > 0">
<image class="feature-icon" src="/static/icons/bard-fill.png" mode="aspectFit" />
<text class="feature-text">额外赠送 {{ currentPackage.bonusPoints }} 积分</text>
</view>
</view>
</view>
</view>
<!-- 底部购买区 -->
<view class="purchase-section" v-if="currentPackage">
<view class="price-card">
<image class="card-bg-image" :src="cardBgImages[activeIndex]" mode="aspectFill" />
<view class="price-info">
<text class="package-name" :class="'text-gradient-' + activeIndex">{{ getTabName(currentPackage.name) }}套餐</text>
<text class="package-total"> {{ (currentPackage.points || 0) + (currentPackage.bonusPoints || 0) }} 积分</text>
</view>
<view class="price-value">
<text class="currency" :class="'text-gradient-' + activeIndex">¥</text>
<text class="amount" :class="'text-gradient-' + activeIndex">{{ currentPackage.price }}</text>
</view>
</view>
<view class="buy-btn" :style="btnStyles[activeIndex]" @click="handleBuy">
<text class="buy-text">立即购买</text>
</view>
<view class="agreement-row">
<view class="checkbox" :class="{ checked: agreed }" @click="agreed = !agreed">
<text v-if="agreed" class="check-mark"></text>
</view>
<text class="agreement-text">已阅读并同意</text>
<text class="agreement-link" @click="openAgreement">1818AI付费服务协议</text>
</view>
</view>
<!-- 加载中 -->
<view class="loading-state" v-if="loading">
<text class="loading-text">加载中...</text>
</view>
<!-- 积分详情弹窗 -->
<view v-if="showPointsModal" class="modal-overlay" @click="closePointsModal">
<view class="modal-content points-modal" @click.stop>
<!-- 固定头部 -->
<view class="modal-header-fixed">
<view class="modal-header">
<text class="modal-title">积分明细</text>
<view class="modal-close" @click="closePointsModal">
<text class="close-icon"></text>
</view>
</view>
<!-- 积分统计 -->
<view class="pts-stats">
<view class="pts-label">我的积分</view>
<view class="pts-value-row">
<text class="pts-value">{{ userPoints }}</text>
<image class="pts-icon" src="/static/icons/points.png" mode="aspectFit" />
</view>
<view class="pts-detail">
<text class="pts-detail-item">累计充值 {{ pointsStatsData.subscribePoints || 0 }}</text>
<text class="pts-detail-divider">|</text>
<text class="pts-detail-item">累计获得赠送 {{ pointsStatsData.giftPoints || 0 }}</text>
</view>
</view>
<!-- Tab切换 -->
<view class="pts-tab-bar">
<view
v-for="tab in pointsTabs"
:key="tab.value"
class="pts-tab-item"
:class="{ active: currentPointsTab === tab.value }"
@click="switchPointsTab(tab.value)"
>
<text class="pts-tab-text">{{ tab.label }}</text>
</view>
</view>
</view>
<!-- 可滚动列表区域 -->
<scroll-view
class="modal-scroll-list"
scroll-y
@scrolltolower="handleScrollToLower"
lower-threshold="100"
>
<view class="record-list">
<view
v-for="item in pointsRecordList"
:key="item.id"
class="record-item-simple"
>
<view class="record-info-simple">
<text class="record-title">{{ item.remark || item.typeName }}</text>
<text class="record-time">{{ formatTime(item.createdAt) }}</text>
</view>
<text
class="record-points"
:class="{ positive: item.points > 0, negative: item.points < 0 }"
>
{{ item.points > 0 ? '+' : '' }}{{ item.points }}
</text>
</view>
<view v-if="!pointsHasMore && pointsRecordList.length > 0" class="no-more">没有更多了</view>
<view v-if="pointsListLoading" class="loading-tip">加载中...</view>
<view v-if="pointsRecordList.length === 0 && !pointsListLoading" class="empty-tip">暂无记录</view>
</view>
</scroll-view>
<!-- 固定底部按钮 -->
<view class="modal-footer-fixed">
<view class="footer-btn" @click="closePointsModal">
<text class="footer-btn-text">购买套餐得积分</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { onShow, onLoad } from '@dcloudio/uni-app'
import { useUserStore } from '@/store/modules/user'
import { getPointsPackages, createPointsOrder } from '@/api/points'
import { getPointsRecords, getPointsStats } from '@/api/user'
import { useSafeArea } from '@/hooks/useSafeArea'
import { handleShareCode } from '@/utils/navigation'
const { safeAreaInsets } = useSafeArea()
const userStore = useUserStore()
const userPoints = computed(() => userStore.userInfo?.points || 0)
const loading = ref(false)
const activeIndex = ref(0)
const agreed = ref(false)
const packages = ref([])
const currentPackage = computed(() => packages.value[activeIndex.value] || null)
// 积分详情弹窗相关
const showPointsModal = ref(false)
const currentPointsTab = ref('increase')
const pointsRecordList = ref([])
const pointsPageNum = ref(1)
const pointsPageSize = ref(20)
const pointsHasMore = ref(true)
const pointsListLoading = ref(false)
const pointsStatsData = ref({ subscribePoints: 0, giftPoints: 0 })
const pointsTabs = [
{ label: '全部', value: 'all' },
{ label: '增加', value: 'increase' },
{ label: '消耗', value: 'decrease' }
]
// 写死的背景图
const bgImages = [
'https://weixin-1818ai-1302947942.cos.ap-shanghai.myqcloud.com/1818_bg/Points-TrialVersion.png',
'https://weixin-1818ai-1302947942.cos.ap-shanghai.myqcloud.com/1818_bg/Points-DeluxeVersion.png',
'https://weixin-1818ai-1302947942.cos.ap-shanghai.myqcloud.com/1818_bg/Points-PremiumVersion.png'
]
// 卡片背景图
const cardBgImages = [
'https://weixin-1818ai-1302947942.cos.ap-shanghai.myqcloud.com/1818_bg/Points1.png',
'https://weixin-1818ai-1302947942.cos.ap-shanghai.myqcloud.com/1818_bg/Points2.png',
'https://weixin-1818ai-1302947942.cos.ap-shanghai.myqcloud.com/1818_bg/Points3.png'
]
// 从套餐名称解析描述用逗号分隔换行跳过第一个第一个是Tab名称
const getDescriptions = computed(() => {
if (!currentPackage.value || !currentPackage.value.name) return []
// 支持中文逗号和英文逗号分隔
const parts = currentPackage.value.name.split(/[,]+/).filter(item => item.trim())
// 跳过第一个Tab名称返回后面的描述
return parts.slice(1)
})
// 获取Tab显示名称取第一个逗号前的内容
const getTabName = (name) => {
if (!name) return ''
const parts = name.split(/[,]+/)
return parts[0] || name
}
// 写死的按钮样式
const btnStyles = [
{ borderRadius: '10px', background: 'linear-gradient(90deg, rgba(202, 227, 255, 1), rgba(127, 168, 255, 1) 100%)' },
{ borderRadius: '10px', background: 'linear-gradient(90deg, rgba(244, 234, 191, 1), rgba(194, 168, 113, 1) 100%)' },
{ borderRadius: '10px', background: 'linear-gradient(90deg, rgba(222, 218, 254, 1), rgba(182, 151, 242, 1) 100%)' }
]
const loadPackages = async () => {
loading.value = true
try {
const res = await getPointsPackages()
packages.value = res || []
} catch (e) {
console.error('获取套餐列表失败', e)
} finally {
loading.value = false
}
}
const goBack = () => {
uni.navigateBack()
}
const selectPackage = (index) => {
activeIndex.value = index
}
const handleBuy = async () => {
if (!agreed.value) {
uni.showToast({ title: '请先同意服务协议', icon: 'none' })
return
}
if (!userStore.isLogin) {
uni.navigateTo({ url: '/pages/login/index' })
return
}
if (!currentPackage.value) return
try {
uni.showLoading({ title: '创建订单中...' })
const res = await createPointsOrder({ packageId: currentPackage.value.id })
uni.hideLoading()
uni.requestPayment({
provider: 'wxpay',
timeStamp: res.timeStamp,
nonceStr: res.nonceStr,
package: res.packageVal,
signType: res.signType,
paySign: res.paySign,
success: () => {
uni.showToast({ title: '支付成功', icon: 'success' })
userStore.fetchUserInfo()
},
fail: (err) => {
if (err.errMsg !== 'requestPayment:fail cancel') {
uni.showToast({ title: '支付失败', icon: 'none' })
}
}
})
} catch (e) {
uni.hideLoading()
const errorMsg = e?.data?.message || (typeof e === 'string' ? e : '创建订单失败')
uni.showToast({ title: errorMsg, icon: 'none' })
}
}
const openAgreement = () => {
uni.navigateTo({ url: '/pages/agreement/payment' })
}
// 显示积分详情弹窗
const showPointsDetail = async () => {
if (!userStore.isLogin) {
uni.navigateTo({ url: '/pages/login/index' })
return
}
showPointsModal.value = true
currentPointsTab.value = 'increase'
pointsRecordList.value = []
pointsPageNum.value = 1
pointsHasMore.value = true
try {
const stats = await getPointsStats()
pointsStatsData.value = stats
} catch (e) {
console.error('获取积分统计失败', e)
}
loadPointsRecords()
}
// 关闭积分弹窗
const closePointsModal = () => {
showPointsModal.value = false
pointsRecordList.value = []
}
// 切换积分Tab
const switchPointsTab = (tab) => {
if (currentPointsTab.value === tab) return
currentPointsTab.value = tab
pointsRecordList.value = []
pointsPageNum.value = 1
pointsHasMore.value = true
loadPointsRecords()
}
// 加载积分记录
const loadPointsRecords = async () => {
if (!pointsHasMore.value || pointsListLoading.value) return
pointsListLoading.value = true
try {
let type = null
if (currentPointsTab.value === 'decrease') {
type = 2
}
const res = await getPointsRecords(pointsPageNum.value, pointsPageSize.value, type)
let newList = res.list || []
if (currentPointsTab.value === 'increase') {
newList = newList.filter(item => item.points > 0)
}
pointsRecordList.value = [...pointsRecordList.value, ...newList]
pointsHasMore.value = res.hasNext || false
} catch (e) {
console.error('加载积分记录失败', e)
} finally {
pointsListLoading.value = false
}
}
// 滚动加载更多
const handleScrollToLower = () => {
if (pointsHasMore.value && !pointsListLoading.value) {
pointsPageNum.value++
loadPointsRecords()
}
}
// 格式化时间
const formatTime = (time) => {
if (!time) return ''
const date = new Date(time)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hour = String(date.getHours()).padStart(2, '0')
const minute = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hour}:${minute}`
}
// 处理页面参数(分享码)
onLoad((options) => {
console.log('=== 积分订阅页面加载 ===')
console.log('页面参数:', options)
// 处理分享码逻辑
const shareResult = handleShareCode(options)
console.log('积分订阅页面分享码处理结果:', shareResult)
})
onMounted(() => {
loadPackages()
// 刷新用户积分
if (userStore.isLogin) {
userStore.fetchUserInfo()
}
})
onShow(() => {
// 页面显示时刷新积分
if (userStore.isLogin) {
userStore.fetchUserInfo()
}
})
</script>
<style scoped>
.subscribe-page {
height: 100vh;
background: #09090b;
position: relative;
overflow: hidden;
}
.page-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
.nav-bar {
position: relative;
z-index: 10;
display: flex;
align-items: center;
justify-content: space-between;
padding-left: 16px;
padding-right: 16px;
height: 44px;
box-sizing: content-box;
}
.nav-left, .nav-right {
width: 40px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
}
.back-icon {
width: 24px;
height: 24px;
}
.nav-center {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
height: 44px;
}
.points-badge {
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.3);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
padding: 6px 10px;
border-radius: 16px;
border: 0.5px solid rgba(255, 255, 255, 0.2);
height: 32px;
box-sizing: border-box;
gap: 4px;
}
.points-label {
font-size: 13px;
color: #ffffff;
white-space: nowrap;
}
.points-icon {
width: 12px;
height: 12px;
}
.points-value {
font-size: 15px;
color: #ffffff;
font-weight: 600;
}
.package-tabs {
position: relative;
z-index: 10;
display: flex;
justify-content: center;
padding: 20px 16px;
}
.tabs-wrapper {
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.08);
border-radius: 25px;
padding: 4px;
}
.tab-item {
padding: 10px 24px;
text-align: center;
border-radius: 21px;
transition: all 0.3s;
}
.tab-item.active {
background: rgba(255, 255, 255, 0.15);
}
.tab-text {
font-size: 14px;
color: #d4d4d8;
}
.tab-item.active .tab-text {
color: #ffffff;
font-weight: 500;
}
.package-detail {
position: relative;
z-index: 10;
margin: 0 16px;
padding: 20px 24px;
flex: 1;
overflow: hidden;
}
.points-display {
display: flex;
align-items: baseline;
margin-bottom: 20px;
}
.points-num {
font-size: 72px;
font-weight: 700;
line-height: 1;
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
/* 体验版渐变 - 蓝色系 */
.points-num.gradient-0,
.points-unit.gradient-0 {
background: linear-gradient(135deg, rgba(7, 17, 37, 1) 0%, rgba(76, 163, 255, 1) 25%, rgba(76, 222, 255, 1) 50%, rgba(177, 243, 255, 1) 75%, rgba(255, 255, 255, 1) 100%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
/* 豪华版渐变 - 金色系 */
.points-num.gradient-1,
.points-unit.gradient-1 {
background: linear-gradient(135deg, rgba(17, 16, 1, 1) 0%, rgba(194, 168, 113, 1) 25%, rgba(244, 234, 191, 1) 50%, rgba(255, 243, 200, 1) 75%, rgba(255, 255, 255, 1) 100%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
/* 至尊版渐变 - 紫色系 */
.points-num.gradient-2,
.points-unit.gradient-2 {
background: linear-gradient(135deg, rgba(3, 3, 23, 1) 0%, rgba(182, 151, 242, 1) 25%, rgba(222, 218, 254, 1) 50%, rgba(240, 238, 255, 1) 75%, rgba(255, 255, 255, 1) 100%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.points-diamond {
width: 28px;
height: 28px;
margin-left: 8px;
margin-bottom: 8px;
}
.points-unit {
font-size: 16px;
margin-left: 8px;
white-space: nowrap;
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.feature-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.feature-item {
display: flex;
align-items: center;
}
.feature-icon {
width: 16px;
height: 16px;
margin-right: 12px;
flex-shrink: 0;
}
.feature-text {
font-size: 16px;
color: #d4d4d8;
}
.purchase-section {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 20;
background: #09090b;
padding: 16px;
padding-bottom: calc(16px + env(safe-area-inset-bottom));
}
.price-card {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
margin-bottom: 12px;
border-radius: 10px;
overflow: hidden;
}
.card-bg-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
/* 卡片背景已改用图片删除CSS渐变 */
/* 文字渐变 - 体验版蓝色系 */
.text-gradient-0 {
background: linear-gradient(135deg, rgba(76, 163, 255, 1) 0%, rgba(76, 222, 255, 1) 50%, rgba(177, 243, 255, 1) 100%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
/* 文字渐变 - 豪华版金色系 */
.text-gradient-1 {
background: linear-gradient(135deg, rgba(194, 168, 113, 1) 0%, rgba(244, 234, 191, 1) 50%, rgba(255, 243, 200, 1) 100%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
/* 文字渐变 - 至尊版紫色系 */
.text-gradient-2 {
background: linear-gradient(135deg, rgba(182, 151, 242, 1) 0%, rgba(222, 218, 254, 1) 50%, rgba(240, 238, 255, 1) 100%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.price-info {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
}
.package-name {
font-size: 16px;
font-weight: 500;
}
.package-total {
font-size: 13px;
color: #71717a;
margin-top: 4px;
}
.price-value {
position: relative;
z-index: 1;
display: flex;
align-items: baseline;
}
.currency {
font-size: 18px;
font-weight: 500;
}
.amount {
font-size: 36px;
font-weight: 700;
}
.buy-btn {
display: flex;
align-items: center;
justify-content: center;
height: 50px;
margin-bottom: 12px;
}
.buy-text {
font-size: 16px;
color: #18181b;
font-weight: 600;
}
.agreement-row {
display: flex;
align-items: center;
justify-content: center;
}
.checkbox {
width: 16px;
height: 16px;
border: 1px solid #52525b;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 8px;
}
.checkbox.checked {
background: #60a5fa;
border-color: #60a5fa;
}
.check-mark {
font-size: 10px;
color: #ffffff;
}
.agreement-text {
font-size: 12px;
color: #71717a;
}
.agreement-link {
font-size: 12px;
color: #60a5fa;
}
.loading-state {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 10;
}
.loading-text {
font-size: 14px;
color: #71717a;
}
/* 积分箭头 */
.points-arrow {
font-size: 16px;
color: rgba(255, 255, 255, 0.6);
margin-left: 2px;
}
/* 积分详情弹窗 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 1000;
display: flex;
align-items: flex-end;
}
.modal-content {
width: 100%;
height: 75vh;
max-height: 80vh;
background: #1c1c1e;
border-radius: 24px 24px 0 0;
display: flex;
flex-direction: column;
}
.points-modal {
max-height: 80vh;
background: #2c2c2e;
}
.modal-header-fixed {
flex-shrink: 0;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px;
border-bottom: 1px solid #3a3a3c;
}
.modal-title {
font-size: 18px;
font-weight: 600;
color: #ffffff;
}
.modal-close {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: #3a3a3c;
}
.close-icon {
font-size: 18px;
color: #8e8e93;
}
/* 积分统计 */
.pts-stats {
padding: 24px 20px;
background: #2c2c2e;
}
.pts-label {
font-size: 14px;
color: #8e8e93;
margin-bottom: 8px;
}
.pts-value-row {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.pts-value {
font-size: 48px;
font-weight: 700;
color: #ffffff;
margin-right: 8px;
}
.pts-icon {
width: 32px;
height: 32px;
}
.pts-detail {
display: flex;
align-items: center;
font-size: 14px;
color: #8e8e93;
}
.pts-detail-item {
color: #8e8e93;
}
.pts-detail-divider {
margin: 0 12px;
color: #3a3a3c;
}
/* Tab栏 */
.pts-tab-bar {
display: flex;
padding: 0 20px;
background: #2c2c2e;
border-bottom: 1px solid #3a3a3c;
}
.pts-tab-item {
flex: 1;
padding: 16px 0;
text-align: center;
position: relative;
}
.pts-tab-text {
font-size: 15px;
color: #8e8e93;
}
.pts-tab-item.active .pts-tab-text {
color: #ffffff;
font-weight: 600;
}
.pts-tab-item.active::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 40px;
height: 3px;
background: #ffffff;
border-radius: 2px;
}
/* 可滚动列表 */
.modal-scroll-list {
flex: 1;
height: 0;
background: #1c1c1e;
}
.record-list {
padding: 20px;
}
.record-item-simple {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 0;
border-bottom: 1px solid #2c2c2e;
}
.record-item-simple:last-child {
border-bottom: none;
}
.record-info-simple {
display: flex;
flex-direction: column;
flex: 1;
}
.record-title {
font-size: 15px;
color: #ffffff;
margin-bottom: 4px;
}
.record-time {
font-size: 13px;
color: #8e8e93;
}
.record-points {
font-size: 18px;
font-weight: 600;
margin-left: 12px;
}
.record-points.positive {
color: #3ED0F5;
}
.record-points.negative {
color: #ffffff;
}
/* 底部按钮 */
.modal-footer-fixed {
flex-shrink: 0;
padding: 16px 20px 32px;
background: #2c2c2e;
border-top: 1px solid #3a3a3c;
}
.footer-btn {
width: 100%;
height: 50px;
background: #ffffff;
border-radius: 25px;
display: flex;
align-items: center;
justify-content: center;
}
.footer-btn-text {
font-size: 16px;
font-weight: 600;
color: #000000;
}
.no-more {
text-align: center;
padding: 20px 0;
font-size: 13px;
color: #8e8e93;
}
.loading-tip {
text-align: center;
padding: 20px 0;
font-size: 13px;
color: #8e8e93;
}
.empty-tip {
text-align: center;
padding: 60px 0;
font-size: 14px;
color: #8e8e93;
}
</style>