topfans/frontend/pages/support-activity/components/ActivityRankingModal.vue
zheng020 0a09048a85 feat: 添加 ActivityRankingModal 活动榜单弹窗组件
- 新建 ActivityRankingModal.vue 组件,实现单活动排名展示
- TOP3 展示使用 TOP3Card 组件
- 排名列表使用 RankingListItem 组件
- 支持下拉刷新和滚动加载更多
- 当前用户栏固定底部显示
- ThemeBanner 添加 @tap 事件触发弹窗
- index.vue 集成 ActivityRankingModal

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 13:06:19 +08:00

1038 lines
22 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 v-if="visible" class="modal-wrapper">
<transition name="fade">
<view v-if="visible" class="modal-mask" @tap="handleMaskClick">
</view>
</transition>
<transition name="slide-up">
<view v-if="visible" class="modal-container" :class="{ 'dragging': isDragging }"
:style="{ transform: `translateY(${dragOffset}px)` }" @tap.stop>
<!-- 背景图 -->
<view class="modal-background"></view>
<!-- 内容区域 -->
<view class="modal-content">
<!-- 头部区域 - 活动名称 + 关闭按钮 -->
<view class="header-section" @touchstart.stop="handleTouchStart" @touchmove.stop="handleTouchMove"
@touchend.stop="handleTouchEnd">
<text class="activity-title">{{ activityTitle }}</text>
<view class="close-button" @tap="handleClose">
<text class="close-icon">✕</text>
</view>
</view>
<!-- 滚动内容区域 -->
<scroll-view class="scrollable-content" scroll-y="true" :show-scrollbar="false"
@scrolltolower="handleScrollToLower" :lower-threshold="100" :refresher-enabled="true"
:refresher-triggered="isRefreshing" @refresherrefresh="handleRefresh"
refresher-background="transparent">
<!-- 加载中提示 -->
<view v-if="isLoadingData && !isRefreshing" class="loading-container">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
<!-- 加载错误提示 -->
<view v-else-if="dataLoadError && !isRefreshing" class="error-container">
<text class="error-text">{{ dataLoadError }}</text>
<button class="retry-button" @tap="loadRankingData">重试</button>
</view>
<!-- TOP3 展示区域 -->
<view v-else-if="top3Users.length > 0" class="top3-section">
<TOP3Card v-for="user in top3Users" :key="user.userId" :rank="user.rank" :avatar="user.avatar"
:nickname="user.nickname" :popularityScore="user.popularityScore"
:artworkImage="user.artworkImage" :userId="user.userId"
:isCurrentUser="isCurrentUser(user.userId)" @visit="handleVisit(user.userId, user.nickname)"
@view-profile="handleViewProfile" @avatar-click="(payload) => emit('view-profile', { userId: payload.userId })" />
</view>
<!-- 空数据提示 -->
<view v-if="!isLoadingData && !dataLoadError && rankingData.length === 0 && !isRefreshing"
class="empty-data">
<text class="empty-text">暂无排名数据</text>
</view>
<!-- 排名列表区域 -->
<view v-if="!isLoadingData && !dataLoadError && listUsers.length > 0"
class="ranking-list-section">
<RankingListItem v-for="item in listUsers" :key="item.userId" :rank="item.rank"
:userId="item.userId" :avatar="item.avatar" :nickname="item.nickname"
:popularityScore="item.popularityScore" :artworkImage="item.artworkImage"
:artworkId="item.artworkId" :showVisitButton="!isCurrentUser(item.userId)"
:isCurrentUser="isCurrentUser(item.userId)" @visit="handleVisit(item.userId, item.nickname)"
@view-profile="handleViewProfile" @artwork-click="handleArtworkClick" />
</view>
<!-- 加载更多提示 -->
<view v-if="isLoadingMore" class="loading-more-container">
<view class="loading-spinner-small"></view>
<text class="loading-more-text">加载中...</text>
</view>
<!-- 没有更多数据提示 -->
<view v-if="!isLoadingMore && hasNoMoreData && rankingData.length > 0" class="no-more-data">
<text class="no-more-text">没有更多了</text>
</view>
<!-- 底部占位,防止内容被当前用户栏遮挡 -->
<view class="bottom-spacer"></view>
</scroll-view>
<!-- 当前用户栏 - 固定在底部 -->
<view v-if="!isLoadingData" class="current-user-bar">
<view class="current-user-content">
<!-- 用户头像 -->
<image class="current-user-avatar" :src="currentUserInfo.avatar || '/static/avatar/1.jpeg'"
mode="aspectFill" @error="handleCurrentUserAvatarError"></image>
<!-- 用户信息 -->
<view class="current-user-info">
<view class="current-user-score">
<image class="flame-icon" src="/static/rank/spark.png" mode="aspectFit"></image>
<text class="score-text">{{ formatPopularityScore(currentUserInfo.popularityScore)
}}</text>
</view>
</view>
<!-- 排名状态 -->
<view class="current-user-rank">
<text class="rank-text">{{ formatCurrentUserRank(currentUserInfo.rank) }}</text>
</view>
</view>
</view>
</view>
</view>
</transition>
</view>
</template>
<script setup>
import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
import { getActivityRankingApi } from '@/utils/api.js';
import TOP3Card from '@/pages/components/TOP3Card.vue';
import RankingListItem from '@/pages/components/RankingListItem.vue';
const props = defineProps({
visible: {
type: Boolean,
default: false
},
activityId: {
type: [String, Number],
required: true
},
starId: {
type: [String, Number],
default: null
},
activityTitle: {
type: String,
default: '活动排名'
},
currentUser: {
type: Object,
default: null
}
});
const emit = defineEmits(['update:visible', 'visit', 'view-profile', 'view-artwork']);
// 数据状态管理
const rankingData = ref([]);
const isLoadingData = ref(false);
const dataLoadError = ref(null);
// 分页状态管理
const currentPage = ref(1);
const isLoadingMore = ref(false);
const hasNoMoreData = ref(false);
const isRefreshing = ref(false);
const PAGE_SIZE = 10;
// 当前用户数据
const currentUserInfo = ref({
userId: 'currentUser',
avatar: '/static/avatar/1.jpeg',
nickname: '我',
popularityScore: 0,
rank: null
});
// 下拉关闭状态管理
const dragOffset = ref(0);
const startY = ref(0);
const isDragging = ref(false);
const DRAG_THRESHOLD = 150;
// 加载锁,防止重复触发
let _scrollLoadLocked = false;
// TOP3 用户数据
const top3Users = computed(() => {
return rankingData.value.filter(user => user.rank >= 1 && user.rank <= 3);
});
// 列表用户数据第4名及以后
const listUsers = computed(() => {
return rankingData.value.filter(user => user.rank >= 4);
});
// 判断是否为当前用户
const isCurrentUser = (userId) => {
return userId === currentUserInfo.value.userId;
};
// 获取OSS图片URL - 直接使用后端返回的URL
const getOssImageUrl = async (fileName, type = 'avatar') => {
if (!fileName || fileName === '') return;
return fileName;
};
// 转换活动排名数据
const transformActivityRankingData = async (apiResponse) => {
if (!apiResponse || !apiResponse.data) {
return [];
}
const { items } = apiResponse.data;
if (!Array.isArray(items)) {
return [];
}
const transformedItems = await Promise.all(items.map(async (item) => {
const avatarUrl = await getOssImageUrl(item.avatar_url || '', 'avatar');
return {
rank: item.rank,
userId: String(item.user_id),
avatar: avatarUrl || '/static/avatar/1.jpeg',
nickname: item.nickname || '未知用户',
popularityScore: item.total_contribution || 0,
artworkImage: '',
artworkId: ''
};
}));
return transformedItems;
};
// 转换当前用户数据
const transformMyActivityContribution = async (myContribution) => {
const avatarUrl = await getOssImageUrl(myContribution?.avatar_url, 'avatar');
return {
userId: 'currentUser',
avatar: avatarUrl || '/static/avatar/1.jpeg',
popularityScore: myContribution?.total_contribution || 0,
rank: (myContribution?.rank > 0) ? myContribution.rank : null
};
};
// 加载排名数据
const loadRankingData = async (page = 1, isRefreshAction = false) => {
try {
if (page === 1 && !isRefreshAction) {
isLoadingData.value = true;
} else if (page > 1) {
isLoadingMore.value = true;
}
dataLoadError.value = null;
const starId = props.starId || uni.getStorageSync('star_id');
const apiResponse = await getActivityRankingApi(
props.activityId,
starId,
page,
PAGE_SIZE
);
if (apiResponse && apiResponse.code === 200 && apiResponse.data) {
// 转换数据
const transformedData = await transformActivityRankingData(apiResponse);
if (page === 1) {
rankingData.value = transformedData;
} else {
rankingData.value.push(...transformedData);
}
// 更新分页状态
hasNoMoreData.value = transformedData.length < PAGE_SIZE;
// 保存当前用户数据(仅第一页)
if (page === 1 && apiResponse.data.my_contribution) {
const myContributionData = await transformMyActivityContribution(apiResponse.data.my_contribution);
if (!props.currentUser) {
currentUserInfo.value = myContributionData;
}
}
return true;
} else {
throw new Error(apiResponse?.message || '获取排行榜数据失败');
}
} catch (error) {
console.error('Failed to fetch ranking data:', error);
dataLoadError.value = error.message;
uni.showToast({
title: '加载失败',
icon: 'none',
duration: 2000
});
return false;
} finally {
if (page === 1 && !isRefreshAction) {
isLoadingData.value = false;
} else if (page > 1) {
isLoadingMore.value = false;
}
}
};
// 处理滚动到底部
const handleScrollToLower = async () => {
if (_scrollLoadLocked || isLoadingMore.value || hasNoMoreData.value || isLoadingData.value) {
return;
}
_scrollLoadLocked = true;
try {
await loadRankingData(currentPage.value + 1);
currentPage.value += 1;
} finally {
_scrollLoadLocked = false;
}
};
// 处理下拉刷新
const handleRefresh = async () => {
if (isRefreshing.value) {
return;
}
isRefreshing.value = true;
try {
// 重置分页
currentPage.value = 1;
hasNoMoreData.value = false;
// 重新加载
const success = await loadRankingData(1, true);
if (success) {
uni.showToast({
title: '刷新成功',
icon: 'success',
duration: 1500
});
} else {
uni.showToast({
title: '刷新失败',
icon: 'none',
duration: 2000
});
}
} catch (error) {
console.error('Refresh failed:', error);
uni.showToast({
title: '刷新失败',
icon: 'none',
duration: 2000
});
} finally {
isRefreshing.value = false;
}
};
// 格式化人气值显示
const formatPopularityScore = (score) => {
if (typeof score !== 'number' || isNaN(score) || score < 0) {
return '0';
}
if (score >= 1000000) {
return (score / 1000000).toFixed(1) + 'M';
} else if (score >= 1000) {
return (score / 1000).toFixed(1) + 'K';
}
return score.toString();
};
// 格式化当前用户排名显示
const formatCurrentUserRank = (rank) => {
if (rank === null || rank === undefined || typeof rank !== 'number' || isNaN(rank) || rank <= 0) {
return '未上榜';
}
return `${rank}`;
};
// 处理拜访按钮点击
const handleVisit = (userId, nickname) => {
emit('visit', { userId, nickname });
};
// 处理查看个人信息
const handleViewProfile = (userId) => {
emit('view-profile', { userId });
};
// 处理作品点击
const handleArtworkClick = (data) => {
emit('view-artwork', {
artworkId: data.artworkId,
userId: data.userId
});
};
// 处理关闭
const handleClose = () => {
emit('update:visible', false);
};
// 处理遮罩层点击
const handleMaskClick = () => {
handleClose();
};
// 处理触摸开始
const handleTouchStart = (e) => {
const touch = e.touches[0];
startY.value = touch.clientY;
isDragging.value = true;
};
// 处理触摸移动
const handleTouchMove = (e) => {
if (!isDragging.value) {
return;
}
const touch = e.touches[0];
const deltaY = touch.clientY - startY.value;
if (deltaY > 0) {
dragOffset.value = deltaY;
}
};
// 处理触摸结束
const handleTouchEnd = () => {
if (!isDragging.value) {
return;
}
isDragging.value = false;
if (dragOffset.value > DRAG_THRESHOLD) {
handleClose();
}
dragOffset.value = 0;
};
// 处理当前用户头像加载失败
const handleCurrentUserAvatarError = (e) => {
e.target.src = '/static/avatar/1.jpeg';
};
// 监听可见性变化
watch(() => props.visible, async (newVisible, oldVisible) => {
if (newVisible && !oldVisible) {
// 重置状态
rankingData.value = [];
currentPage.value = 1;
hasNoMoreData.value = false;
isLoadingData.value = true;
dataLoadError.value = null;
// 使用传入的 currentUser 或重置
if (props.currentUser) {
currentUserInfo.value = { ...props.currentUser };
} else {
currentUserInfo.value = {
userId: 'currentUser',
avatar: '/static/avatar/1.jpeg',
nickname: '我',
popularityScore: 0,
rank: null
};
}
// 加载数据
await loadRankingData(1);
}
}, { immediate: false });
// 监听传入的 currentUser 变化
watch(() => props.currentUser, (newUser) => {
if (newUser) {
currentUserInfo.value = { ...newUser };
}
}, { immediate: true });
onUnmounted(() => {
_scrollLoadLocked = false;
});
</script>
<style scoped>
/* 动画效果 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.slide-up-enter-active,
.slide-up-leave-active {
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.slide-up-enter-from,
.slide-up-leave-to {
opacity: 0;
transform: translateY(100%);
}
.modal-wrapper {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
z-index: 9999;
display: flex;
align-items: flex-end;
justify-content: center;
box-sizing: border-box;
pointer-events: auto;
}
.modal-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
z-index: 5;
pointer-events: auto;
cursor: pointer;
}
.modal-container {
position: absolute;
width: 100%;
height: 80vh;
bottom: 0;
border-radius: 48rpx 48rpx 0 0;
overflow: hidden;
z-index: 10;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.modal-container.dragging {
transition: none;
}
.modal-background {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image: url('/static/rank/paihangbang.png');
background-size: 100% 100%;
background-repeat: no-repeat;
background-position: center;
z-index: 0;
}
.modal-background::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(ellipse at center, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
}
.modal-content {
position: relative;
z-index: 2;
padding: 50rpx 10rpx;
display: flex;
flex-direction: column;
height: 100%;
align-items: center;
}
/* 头部区域 */
.header-section {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 40rpx;
padding: 0 32rpx;
position: relative;
width: 100%;
}
.activity-title {
font-size: 36rpx;
font-weight: bold;
color: #FFFFFF;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.4);
font-family: 'yt', sans-serif;
}
.close-button {
position: absolute;
right: 32rpx;
width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
}
.close-icon {
font-size: 32rpx;
color: #FFFFFF;
}
/* 滚动内容区域 */
.scrollable-content {
flex: 1;
overflow-y: auto;
padding-bottom: 180rpx;
padding-top: 16rpx;
}
/* 加载中容器 */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 400rpx;
padding: 40rpx;
}
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid rgba(255, 255, 255, 0.3);
border-top-color: #FFFFFF;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.loading-text {
margin-top: 20rpx;
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
font-family: 'yt', sans-serif;
}
/* 错误容器 */
.error-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 400rpx;
padding: 40rpx;
}
.error-text {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
font-family: 'yt', sans-serif;
text-align: center;
margin-bottom: 30rpx;
}
.retry-button {
padding: 16rpx 40rpx;
background: linear-gradient(135deg, rgba(255, 107, 157, 0.9) 0%, rgba(255, 177, 153, 0.9) 100%);
border-radius: 40rpx;
border: none;
color: #FFFFFF;
font-size: 28rpx;
font-family: 'yt', sans-serif;
box-shadow: 0 4rpx 12rpx rgba(255, 107, 157, 0.3);
}
/* TOP3 展示区域 */
.top3-section {
display: flex;
justify-content: space-between;
gap: 60rpx;
margin-bottom: 84rpx;
padding: 0 48rpx;
}
/* 空数据提示 */
.empty-data {
display: flex;
align-items: center;
justify-content: center;
min-height: 320rpx;
background: rgba(255, 255, 255, 0.08);
border-radius: 24rpx;
border: 1rpx solid rgba(255, 255, 255, 0.15);
margin: 0 15rpx;
}
.empty-text {
font-size: 30rpx;
color: rgba(255, 255, 255, 0.7);
font-family: 'yt', sans-serif;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2);
}
/* 排名列表区域 */
.ranking-list-section {
margin-top: 20rpx;
padding: 0 48rpx;
}
/* 加载更多容器 */
.loading-more-container {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 30rpx 20rpx;
gap: 12rpx;
}
.loading-spinner-small {
width: 40rpx;
height: 40rpx;
border: 3rpx solid rgba(255, 255, 255, 0.3);
border-top-color: #FFFFFF;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.loading-more-text {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.7);
font-family: 'yt', sans-serif;
}
/* 没有更多数据提示 */
.no-more-data {
display: flex;
align-items: center;
justify-content: center;
padding: 30rpx 20rpx;
}
.no-more-text {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.5);
font-family: 'yt', sans-serif;
}
/* 底部占位 */
.bottom-spacer {
height: 20rpx;
}
/* 当前用户栏 - 固定在底部 */
.current-user-bar {
position: absolute;
bottom: 120rpx;
left: 84rpx;
right: 84rpx;
padding: 24rpx;
background: linear-gradient(135deg, rgba(255, 107, 157, 0.9) 0%, rgba(255, 177, 153, 0.9) 100%);
border-radius: 32rpx;
box-shadow: 0 -4rpx 16rpx rgba(255, 107, 157, 0.35);
border: 2rpx solid rgba(255, 255, 255, 0.3);
z-index: 10;
}
.current-user-bar::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.2) 0%, transparent 50%, rgba(255, 255, 255, 0.1) 100%);
border-radius: 30rpx;
pointer-events: none;
}
.current-user-content {
display: flex;
align-items: center;
position: relative;
z-index: 1;
height: 64rpx;
}
.current-user-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
border: 4rpx solid rgba(255, 255, 255, 0.9);
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.3), 0 2rpx 8rpx rgba(0, 0, 0, 0.2);
}
.current-user-info {
flex: 1;
display: flex;
margin-left: 34rpx;
margin-right: 34rpx;
flex-direction: column;
}
.current-user-score {
display: flex;
align-items: center;
}
.flame-icon {
width: 44rpx;
height: 80rpx;
filter: drop-shadow(0 2rpx 4rpx rgba(0, 0, 0, 0.3));
}
.score-text {
font-size: 26rpx;
margin-left: 8rpx;
color: rgba(255, 255, 255, 0.95);
font-family: 'yt', sans-serif;
font-weight: 500;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.3);
}
.current-user-rank {
display: flex;
align-items: center;
padding: 16rpx 28rpx;
}
.rank-text {
font-size: 30rpx;
color: #FFFFFF;
font-family: 'yt', sans-serif;
text-shadow: 0 3rpx 6rpx rgba(0, 0, 0, 0.4), 0 1rpx 3rpx rgba(0, 0, 0, 0.3);
}
/* 响应式布局 */
@media screen and (max-width: 750rpx) {
.modal-container {
border-radius: 40rpx 40rpx 0 0;
}
.modal-content {
padding: 40rpx 30rpx;
}
.header-section {
margin-bottom: 32rpx;
}
.activity-title {
font-size: 32rpx;
}
.scrollable-content {
padding-bottom: 160rpx;
}
.top3-section {
gap: 12rpx;
padding: 0 8rpx;
}
.ranking-list-section {
padding: 0 8rpx;
}
.current-user-bar {
bottom: 100rpx;
left: 15rpx;
right: 15rpx;
padding: 20rpx;
border-radius: 28rpx;
}
}
@media screen and (max-width: 600rpx) {
.modal-container {
border-radius: 36rpx 36rpx 0 0;
max-height: 85vh;
}
.modal-content {
padding: 36rpx 24rpx;
}
.header-section {
margin-bottom: 28rpx;
padding: 0 24rpx;
}
.activity-title {
font-size: 28rpx;
}
.scrollable-content {
padding-bottom: 140rpx;
}
.top3-section {
gap: 8rpx;
margin-bottom: 32rpx;
padding: 0 4rpx;
}
.ranking-list-section {
padding: 0 4rpx;
}
.current-user-bar {
bottom: 80rpx;
left: 12rpx;
right: 12rpx;
padding: 18rpx 24rpx;
border-radius: 24rpx;
}
.current-user-content {
gap: 16rpx;
}
.current-user-avatar {
width: 72rpx;
height: 72rpx;
}
.current-user-info {
margin-left: 20rpx;
margin-right: 20rpx;
}
.score-text {
font-size: 22rpx;
}
.rank-text {
font-size: 26rpx;
}
}
@media screen and (max-width: 480rpx) {
.modal-wrapper {
align-items: flex-end;
}
.modal-container {
border-radius: 32rpx 32rpx 0 0;
max-height: 80vh;
}
.modal-content {
padding: 32rpx 20rpx;
}
.header-section {
margin-bottom: 24rpx;
}
.close-button {
right: 16rpx;
width: 48rpx;
height: 48rpx;
}
.close-icon {
font-size: 28rpx;
}
.scrollable-content {
padding-bottom: 120rpx;
}
.top3-section {
gap: 6rpx;
margin-bottom: 28rpx;
padding: 0 2rpx;
}
.ranking-list-section {
padding: 0 2rpx;
}
.current-user-bar {
bottom: 60rpx;
left: 10rpx;
right: 10rpx;
padding: 16rpx 20rpx;
border-radius: 20rpx;
}
.current-user-content {
gap: 12rpx;
}
.current-user-avatar {
width: 64rpx;
height: 64rpx;
border-width: 3rpx;
}
.flame-icon {
width: 22rpx;
height: 22rpx;
}
.score-text {
font-size: 20rpx;
}
.current-user-rank {
padding: 10rpx 16rpx;
}
.rank-text {
font-size: 24rpx;
}
}
/* 触摸优化 */
@media (hover: none) and (pointer: coarse) {
.close-button {
min-width: 88rpx;
min-height: 88rpx;
}
}
</style>