2133 lines
51 KiB
Vue
2133 lines
51 KiB
Vue
<template>
|
||
<view v-if="visible" class="modal-wrapper">
|
||
<transition name="fade">
|
||
<view v-if="visible" class="modal-mask" @tap="handleMaskClick">
|
||
</view>
|
||
</transition>
|
||
|
||
<transition name="scale">
|
||
<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">
|
||
<image class="nav-arrow left-arrow" :class="{ 'switching': isSwitching }"
|
||
src="/static/rank/left-arrow.png" mode="aspectFit" @tap="handleLeftArrow"></image>
|
||
<image class="ranking-title-image"
|
||
:class="{ 'switching': isSwitching, 'loaded': isImageLoaded }"
|
||
:src="currentRankingTypeImage" mode="aspectFit"
|
||
@load="handleTitleImageLoad" @error="handleTitleImageError"></image>
|
||
<image class="nav-arrow right-arrow" :class="{ 'switching': isSwitching }"
|
||
src="/static/rank/right-arrow.png" mode="aspectFit" @tap="handleRightArrow"></image>
|
||
</view>
|
||
|
||
<!-- 标签区域 -->
|
||
<view class="tab-section" :class="{ 'has-icon': currentRankingType === RANKING_TYPES.ACTIVITY }">
|
||
<view v-for="(tab, index) in displayTabs" :key="index" :ref="'tab-' + index" class="tab-item"
|
||
:class="{ 'active': activeTabIndex === index, 'has-icon': tab.icon }"
|
||
@tap="handleTabClick(index)">
|
||
<image v-if="tab.icon" class="tab-icon" :class="{ 'active': activeTabIndex === index }"
|
||
:src="tab.icon" mode="aspectFit"></image>
|
||
<text class="tab-text">{{ tab.type }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 滚动内容区域:包含TOP3和排名列表 -->
|
||
<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="loadAllTabsData(currentRankingType)">重试</button>
|
||
</view>
|
||
|
||
<!-- TOP3 展示区域 -->
|
||
<view v-else-if="top3Users.length > 0" class="top3-section">
|
||
<TOP3Card v-for="user in top3Users" :key="user.userId" :ref="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)"
|
||
@view-profile="handleViewProfile" @avatar-click="() => { }" />
|
||
</view>
|
||
|
||
<!-- 空数据提示 -->
|
||
<view v-if="!isLoadingData && !dataLoadError && currentRankingData.length === 0"
|
||
class="empty-data">
|
||
<text class="empty-text">暂无排名数据</text>
|
||
</view>
|
||
|
||
<!-- 排名列表区域 - 使用普通列表 + 原生 lazy-load -->
|
||
<view v-if="!isLoadingData && !dataLoadError && listUsers.length > 0"
|
||
class="ranking-list-section">
|
||
<RankingListItem v-for="item in listUsers" :key="item.userId" :ref="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)"
|
||
@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 class="bottom-spacer"></view>
|
||
</scroll-view>
|
||
|
||
<!-- 当前用户栏 - 固定在底部 -->
|
||
<view v-if="!isLoadingData" class="current-user-bar">
|
||
<view class="current-user-content">
|
||
<!-- 用户头像或作品图片(根据榜单类型决定) -->
|
||
<image class="current-user-avatar" :class="{ 'no-artwork': hasActualArtwork }"
|
||
:src="currentUserDisplayImage" 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,
|
||
nextTick
|
||
} from 'vue';
|
||
import {
|
||
RANKING_TYPES,
|
||
RANKING_TYPE_IMAGES
|
||
} from './rankingMockData.js';
|
||
import {
|
||
getHotRankingApi,
|
||
getOriginalRankingApi,
|
||
getActivityRankingApi,
|
||
getActivityListApi,
|
||
getOssPresignedUrlApi
|
||
} from '@/utils/api.js';
|
||
import {
|
||
ACTIVITY_TYPES
|
||
} from '@/utils/activity-config.js';
|
||
import TOP3Card from './TOP3Card.vue';
|
||
import RankingListItem from './RankingListItem.vue';
|
||
|
||
const props = defineProps({
|
||
visible: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
rankingType: {
|
||
type: String,
|
||
default: '热度',
|
||
validator: (value) => ['热度', '自制', '活动'].includes(value)
|
||
},
|
||
title: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
tabs: {
|
||
type: Array,
|
||
default: () => ['在线', '本月', '历史']
|
||
},
|
||
rankingData: {
|
||
type: Object,
|
||
default: null
|
||
},
|
||
currentUser: {
|
||
type: Object,
|
||
default: null
|
||
},
|
||
// 新增:用于检测父组件是否处于活跃状态
|
||
parentActive: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
// 新增:活动ID(用于活动排名,可选)
|
||
activityId: {
|
||
type: [String, Number],
|
||
default: null
|
||
},
|
||
// 新增:明星ID(用于获取活动列表)
|
||
starId: {
|
||
type: [String, Number],
|
||
default: null
|
||
}
|
||
});
|
||
|
||
const emit = defineEmits(['update:visible', 'ranking-type-change', 'visit', 'tab-change', 'view-profile',
|
||
'view-artwork'
|
||
]);
|
||
|
||
// 当前选中的标签索引
|
||
const activeTabIndex = ref(0);
|
||
|
||
// 当前榜单类型状态管理
|
||
const currentRankingType = ref(props.rankingType || RANKING_TYPES.POPULARITY);
|
||
|
||
// 切换防抖和加载状态管理
|
||
const isSwitching = ref(false);
|
||
const switchDebounceTimer = ref(null);
|
||
|
||
// 图片预加载状态
|
||
const preloadedImages = ref(new Set());
|
||
const isImageLoaded = ref(true);
|
||
|
||
// API数据状态管理
|
||
const apiRankingData = ref({});
|
||
const isLoadingData = ref(false);
|
||
const dataLoadError = ref(null);
|
||
|
||
// 分页状态管理
|
||
const pageInfo = ref({});
|
||
const isLoadingMore = ref(false);
|
||
const hasNoMoreData = ref(false);
|
||
const isRefreshing = ref(false);
|
||
const PAGE_SIZE = 10; // 每页加载10条数据
|
||
|
||
// 活动列表状态管理
|
||
const activityList = ref([]);
|
||
const selectedActivityId = ref(null);
|
||
const isLoadingActivities = ref(false);
|
||
const activityLoadError = ref(null);
|
||
|
||
// 按榜单类型分开存储当前用户数据,避免互相污染
|
||
const currentUserByType = ref({});
|
||
|
||
// 下拉关闭状态管理
|
||
const dragOffset = ref(0);
|
||
const startY = ref(0);
|
||
const isDragging = ref(false);
|
||
const DRAG_THRESHOLD = 150; // 下拉超过150px时关闭
|
||
|
||
// 榜单类型循环切换算法
|
||
const getRankingTypeOrder = () => [RANKING_TYPES.POPULARITY, RANKING_TYPES.CUSTOM, RANKING_TYPES.ACTIVITY];
|
||
|
||
const getNextRankingType = (currentType) => {
|
||
const order = getRankingTypeOrder();
|
||
const currentIndex = order.indexOf(currentType);
|
||
return order[(currentIndex + 1) % order.length];
|
||
};
|
||
|
||
const getPreviousRankingType = (currentType) => {
|
||
const order = getRankingTypeOrder();
|
||
const currentIndex = order.indexOf(currentType);
|
||
return order[(currentIndex - 1 + order.length) % order.length];
|
||
};
|
||
|
||
// 显示的标题(使用传入的标题或默认标题)
|
||
const displayTitle = computed(() => {
|
||
return props.title || 'TOPFANS 热度排行榜';
|
||
});
|
||
|
||
// 当前榜单类型对应的标识图片
|
||
const currentRankingTypeImage = computed(() => {
|
||
return RANKING_TYPE_IMAGES[currentRankingType.value] || RANKING_TYPE_IMAGES[RANKING_TYPES.POPULARITY];
|
||
});
|
||
|
||
// 显示的标签(使用传入的标签或默认标签)
|
||
const displayTabs = computed(() => {
|
||
// 如果是活动榜单,显示活动列表作为标签
|
||
if (currentRankingType.value === RANKING_TYPES.ACTIVITY) {
|
||
// 如果还没有加载活动列表,显示加载提示
|
||
// if (isLoadingActivities.value) {
|
||
// return [{
|
||
// name: '加载中...',
|
||
// type: 'loading',
|
||
// icon: null,
|
||
// id: null
|
||
// }];
|
||
// }
|
||
|
||
// 如果加载失败,显示错误提示
|
||
// if (activityLoadError.value) {
|
||
// return [{
|
||
// name: '加载失败',
|
||
// type: 'error',
|
||
// icon: null,
|
||
// id: null
|
||
// }];
|
||
// }
|
||
|
||
// 如果没有活动,显示空提示
|
||
// if (activityList.value.length === 0) {
|
||
// return [{
|
||
// name: '暂无活动',
|
||
// type: 'empty',
|
||
// icon: null,
|
||
// id: null
|
||
// }];
|
||
// }
|
||
|
||
// 显示活动列表
|
||
return activityList.value.map(activity => ({
|
||
name: activity.title,
|
||
type: activity.theme,
|
||
icon: getActivityIcon(activity.activity_type),
|
||
id: activity.id
|
||
}));
|
||
}
|
||
// 其他榜单使用传入的标签
|
||
return props.tabs.map(tab => ({
|
||
name: tab,
|
||
type: tab,
|
||
icon: null,
|
||
id: null
|
||
}));
|
||
});
|
||
|
||
// 获取活动类型对应的图标
|
||
const getActivityIcon = (activityType) => {
|
||
const iconMap = {
|
||
[ACTIVITY_TYPES.BUS]: '/static/rank/bus-icon.png',
|
||
[ACTIVITY_TYPES.BIRTHDAY]: '/static/rank/three-layer-cake-icon.png',
|
||
[ACTIVITY_TYPES.CONCERT]: '/static/rank/stage-icon.png'
|
||
};
|
||
return iconMap[activityType] || null;
|
||
};
|
||
|
||
|
||
// 标签名称到API dimension参数的映射
|
||
const tabToDimensionMap = {
|
||
'在线': 'displaying',
|
||
'本月': 'month',
|
||
'历史': 'total'
|
||
};
|
||
|
||
// 获取活动列表
|
||
const fetchActivityList = async () => {
|
||
const starId = props.starId || uni.getStorageSync('star_id');
|
||
if (!starId) {
|
||
console.warn('starId is required for fetching activity list');
|
||
activityLoadError.value = '缺少明星ID';
|
||
return;
|
||
}
|
||
|
||
try {
|
||
isLoadingActivities.value = true;
|
||
activityLoadError.value = null;
|
||
const response = await getActivityListApi(starId, 1, 10);
|
||
|
||
if (response && response.code === 200 && response.data) {
|
||
activityList.value = response.data.activities || [];
|
||
|
||
// 如果有传入的activityId,使用它;否则选择第一个活动
|
||
if (props.activityId) {
|
||
selectedActivityId.value = props.activityId;
|
||
} else if (activityList.value.length > 0) {
|
||
selectedActivityId.value = activityList.value[0].id;
|
||
}
|
||
} else {
|
||
throw new Error(response?.message || '获取活动列表失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to fetch activity list:', error);
|
||
activityLoadError.value = error.message;
|
||
uni.showToast({
|
||
title: '获取活动列表失败',
|
||
icon: 'none',
|
||
duration: 2000
|
||
});
|
||
} finally {
|
||
isLoadingActivities.value = false;
|
||
}
|
||
};
|
||
|
||
// 获取OSS图片的预签名URL
|
||
const getOssImageUrl = async (fileName, type = 'avatar') => {
|
||
if (!fileName || fileName === '') return;
|
||
try {
|
||
const response = await getOssPresignedUrlApi(fileName, 3600, type);
|
||
if (response && response.code === 200 && response.data && response.data.url) {
|
||
return response.data.url;
|
||
}
|
||
return fileName; // 如果获取失败,返回原始文件名
|
||
} catch (error) {
|
||
console.error('Failed to get OSS presigned URL:', error);
|
||
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: ''
|
||
};
|
||
}));
|
||
|
||
return transformedItems;
|
||
};
|
||
|
||
// 转换活动排名中的当前用户数据(只取展示所需字段)
|
||
const transformMyActivityContribution = async (myContribution) => {
|
||
const avatarUrl = await getOssImageUrl(myContribution.avatar_url, 'avatar')
|
||
|
||
return {
|
||
userId: 'currentUser',
|
||
avatar: avatarUrl,
|
||
popularityScore: myContribution?.total_contribution || 0,
|
||
rank: (myContribution?.rank > 0) ? myContribution.rank : null
|
||
};
|
||
};
|
||
|
||
// 将API响应数据转换为组件所需格式
|
||
const transformApiDataToComponentFormat = async (apiResponse) => {
|
||
if (!apiResponse || !apiResponse.data) {
|
||
return [];
|
||
}
|
||
|
||
const {
|
||
items
|
||
} = apiResponse.data;
|
||
if (!Array.isArray(items)) {
|
||
return [];
|
||
}
|
||
|
||
// 批量获取所有图片的预签名URL
|
||
const transformedItems = await Promise.all(items.map(async (item) => {
|
||
const [avatarUrl, coverUrl] = await Promise.all([
|
||
getOssImageUrl(item.owner_avatar || '', 'avatar'),
|
||
getOssImageUrl(item.cover_url || '', 'asset')
|
||
]);
|
||
|
||
return {
|
||
rank: item.rank,
|
||
userId: String(item.owner_uid), // 转换为字符串
|
||
avatar: avatarUrl || '/static/avatar/1.jpeg', // 使用用户头像
|
||
nickname: item.owner_nickname || '未知用户',
|
||
popularityScore: item.like_count || 0,
|
||
artworkImage: coverUrl || '', // 作品封面图
|
||
artworkId: String(item.asset_id) // 转换为字符串
|
||
};
|
||
}));
|
||
|
||
return transformedItems;
|
||
};
|
||
|
||
// 转换当前用户排名数据
|
||
const transformMyRankingData = async (myRanking) => {
|
||
if (!myRanking) {
|
||
return {
|
||
userId: 'currentUser',
|
||
avatar: '/static/avatar/1.jpeg',
|
||
nickname: '我',
|
||
popularityScore: 0,
|
||
rank: null,
|
||
artworkImage: null,
|
||
artworkId: null
|
||
};
|
||
}
|
||
|
||
// 获取封面图的预签名URL
|
||
const coverUrl = await getOssImageUrl(myRanking.cover_url || '', 'asset');
|
||
|
||
// 没有作品封面时,使用头像
|
||
const avatarUrl = await getOssImageUrl(myRanking.owner_avatar|| '', 'avatar');
|
||
|
||
return {
|
||
userId: 'currentUser',
|
||
avatar: coverUrl || avatarUrl,
|
||
nickname: '我',
|
||
popularityScore: myRanking.like_count || 0,
|
||
rank: myRanking.rank > 0 ? myRanking.rank : null,
|
||
artworkImage: coverUrl || '',
|
||
artworkId: myRanking.asset_id ? String(myRanking.asset_id) : null
|
||
};
|
||
};
|
||
|
||
// 获取排行榜数据
|
||
const fetchRankingData = async (rankingType, dimension, page = 1, pageSize = PAGE_SIZE, isRefreshAction = false) => {
|
||
try {
|
||
// 刷新时不显示加载提示,保持显示已有列表
|
||
if (page === 1 && !isRefreshAction) {
|
||
isLoadingData.value = true;
|
||
} else if (page > 1) {
|
||
isLoadingMore.value = true;
|
||
}
|
||
dataLoadError.value = null;
|
||
|
||
let apiResponse;
|
||
|
||
// 根据榜单类型调用不同的API
|
||
if (rankingType === RANKING_TYPES.POPULARITY) {
|
||
apiResponse = await getHotRankingApi(dimension, null, page, pageSize);
|
||
} else if (rankingType === RANKING_TYPES.CUSTOM) {
|
||
apiResponse = await getOriginalRankingApi(dimension, null, page, pageSize);
|
||
} else if (rankingType === RANKING_TYPES.ACTIVITY) {
|
||
// 活动榜单需要activityId
|
||
// 使用 selectedActivityId(从活动列表中选择的活动)
|
||
const activityIdToUse = selectedActivityId.value || props.activityId;
|
||
|
||
if (!activityIdToUse) {
|
||
throw new Error('活动ID不能为空,请先选择一个活动');
|
||
}
|
||
|
||
apiResponse = await getActivityRankingApi(activityIdToUse, null, page, pageSize);
|
||
} else {
|
||
throw new Error('未知的榜单类型');
|
||
}
|
||
|
||
if (apiResponse && apiResponse.code === 200) {
|
||
return apiResponse;
|
||
} 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 null;
|
||
} finally {
|
||
if (page === 1 && !isRefreshAction) {
|
||
isLoadingData.value = false;
|
||
} else if (page > 1) {
|
||
isLoadingMore.value = false;
|
||
}
|
||
}
|
||
};
|
||
|
||
// 加载单个标签的数据(按需加载)
|
||
const loadSingleTabData = async (rankingType, tabName, page = 1, isRefreshAction = false) => {
|
||
// 初始化页面信息键
|
||
const pageKey = `${rankingType}_${tabName}`;
|
||
|
||
// 如果是第一页且不是刷新操作,检查是否已经加载过该标签的数据
|
||
if (page === 1 && !isRefreshAction && apiRankingData.value[rankingType] && apiRankingData.value[
|
||
rankingType][tabName]) {
|
||
// 活动榜单:从按活动缓存的当前用户数据中恢复
|
||
if (rankingType === RANKING_TYPES.ACTIVITY && !props.currentUser) {
|
||
const cachedUserKey = `${rankingType}_${tabName}`;
|
||
if (currentUserByType.value[cachedUserKey]) {
|
||
currentUserByType.value[RANKING_TYPES.ACTIVITY] = currentUserByType.value[cachedUserKey];
|
||
}
|
||
}
|
||
isLoadingData.value = false;
|
||
return true;
|
||
}
|
||
|
||
// 活动榜单不使用dimension参数
|
||
const dimension = rankingType === RANKING_TYPES.ACTIVITY ? null : (tabToDimensionMap[tabName] || 'total');
|
||
const apiResponse = await fetchRankingData(rankingType, dimension, page, PAGE_SIZE, isRefreshAction);
|
||
|
||
if (apiResponse && apiResponse.data) {
|
||
// 初始化榜单类型对象(如果不存在)
|
||
if (!apiRankingData.value[rankingType]) {
|
||
apiRankingData.value[rankingType] = {};
|
||
}
|
||
|
||
// 根据榜单类型转换数据
|
||
let transformedData;
|
||
if (rankingType === RANKING_TYPES.ACTIVITY) {
|
||
transformedData = await transformActivityRankingData(apiResponse);
|
||
} else {
|
||
transformedData = await transformApiDataToComponentFormat(apiResponse);
|
||
}
|
||
|
||
// 如果是第一页,直接赋值;否则追加数据
|
||
if (page === 1) {
|
||
apiRankingData.value[rankingType][tabName] = transformedData;
|
||
// 初始化分页信息
|
||
if (!pageInfo.value[pageKey]) {
|
||
pageInfo.value[pageKey] = {
|
||
currentPage: 1,
|
||
hasMore: true
|
||
};
|
||
}
|
||
pageInfo.value[pageKey].currentPage = 1;
|
||
pageInfo.value[pageKey].hasMore = transformedData.length >= PAGE_SIZE;
|
||
} else {
|
||
// 追加数据
|
||
if (!apiRankingData.value[rankingType][tabName]) {
|
||
apiRankingData.value[rankingType][tabName] = [];
|
||
}
|
||
apiRankingData.value[rankingType][tabName].push(...transformedData);
|
||
// 更新分页信息
|
||
pageInfo.value[pageKey].currentPage = page;
|
||
pageInfo.value[pageKey].hasMore = transformedData.length >= PAGE_SIZE;
|
||
}
|
||
|
||
// 更新"没有更多数据"状态
|
||
hasNoMoreData.value = !pageInfo.value[pageKey].hasMore;
|
||
|
||
// 保存当前用户排名信息(仅第一页)
|
||
if (page === 1) {
|
||
if (rankingType === RANKING_TYPES.ACTIVITY && apiResponse.data.my_contribution) {
|
||
const myContributionData = await transformMyActivityContribution(apiResponse.data.my_contribution);
|
||
if (!props.currentUser) {
|
||
// 按活动标签缓存,同时更新当前显示
|
||
const cachedUserKey = `${rankingType}_${tabName}`;
|
||
currentUserByType.value[cachedUserKey] = myContributionData;
|
||
currentUserByType.value[rankingType] = myContributionData;
|
||
}
|
||
} else if (apiResponse.data.my_ranking) {
|
||
const myRankingData = await transformMyRankingData(apiResponse.data.my_ranking);
|
||
if (!props.currentUser) {
|
||
currentUserByType.value[rankingType] = myRankingData;
|
||
}
|
||
}
|
||
}
|
||
|
||
return true; // 加载成功
|
||
}
|
||
|
||
return false; // 加载失败
|
||
};
|
||
|
||
// 加载所有标签的数据(用于初始化)
|
||
const loadAllTabsData = async (rankingType) => {
|
||
// 活动榜单使用模拟数据
|
||
if (rankingType === RANKING_TYPES.ACTIVITY) {
|
||
return;
|
||
}
|
||
|
||
// 并行加载所有标签的数据
|
||
const loadPromises = displayTabs.value.map(tabName => loadSingleTabData(rankingType, tabName));
|
||
await Promise.all(loadPromises);
|
||
};
|
||
|
||
// 当前标签对应的排名数据 - 优化:使用缓存避免重复排序
|
||
const currentRankingData = computed(() => {
|
||
const tab = displayTabs.value[activeTabIndex.value];
|
||
const tabName = tab ? tab.name : '';
|
||
|
||
// 如果正在加载数据,返回空数组(显示加载状态)
|
||
if (isLoadingData.value) {
|
||
return [];
|
||
}
|
||
|
||
// 优先使用API数据
|
||
if (apiRankingData.value[currentRankingType.value] &&
|
||
apiRankingData.value[currentRankingType.value][tabName]) {
|
||
const data = apiRankingData.value[currentRankingType.value][tabName];
|
||
// 如果data已经是数组(来自API),直接使用
|
||
if (Array.isArray(data)) {
|
||
return [...data].sort((a, b) => a.rank - b.rank);
|
||
}
|
||
}
|
||
|
||
// 如果有props传入的数据,使用props数据
|
||
if (props.rankingData) {
|
||
const data = props.rankingData;
|
||
let tabData = [];
|
||
|
||
if (data && typeof data === 'object') {
|
||
// 如果data已经是数组,直接使用
|
||
if (Array.isArray(data)) {
|
||
tabData = data;
|
||
}
|
||
// 检查是否为三层嵌套结构(榜单类型 → 时间段 → 用户数据)
|
||
else if (data[currentRankingType.value] && typeof data[currentRankingType.value] === 'object') {
|
||
tabData = data[currentRankingType.value][tabName] || [];
|
||
}
|
||
// 兼容旧的二层结构(时间段 → 用户数据)
|
||
else if (data[tabName]) {
|
||
tabData = data[tabName] || [];
|
||
}
|
||
}
|
||
|
||
if (Array.isArray(tabData) && tabData.length > 0) {
|
||
return [...tabData].sort((a, b) => a.rank - b.rank);
|
||
}
|
||
}
|
||
|
||
// 默认返回空数组 - 不再使用模拟数据
|
||
return [];
|
||
});
|
||
|
||
// TOP3 用户数据(前3名)- 优化:直接从已排序数据中提取
|
||
const top3Users = computed(() => {
|
||
const data = currentRankingData.value;
|
||
// 已排序数据,直接取前3个 rank <= 3 的用户
|
||
const result = [];
|
||
for (let i = 0; i < data.length && result.length < 3; i++) {
|
||
if (data[i].rank >= 1 && data[i].rank <= 3) {
|
||
result.push(data[i]);
|
||
}
|
||
}
|
||
return result;
|
||
});
|
||
|
||
// 列表用户数据(第4名及以后)- 优化:直接从已排序数据中提取
|
||
const listUsers = computed(() => {
|
||
const data = currentRankingData.value;
|
||
// 已排序数据,直接过滤 rank >= 4 的用户,无需再次排序
|
||
return data.filter(user => user.rank >= 4);
|
||
});
|
||
|
||
// 当前用户信息
|
||
const currentUserInfo = computed(() => {
|
||
if (props.currentUser) return props.currentUser;
|
||
return currentUserByType.value[currentRankingType.value] || {
|
||
userId: 'currentUser',
|
||
avatar: '/static/avatar/1.jpeg',
|
||
nickname: '我',
|
||
popularityScore: 0,
|
||
rank: null,
|
||
artworkImage: null,
|
||
artworkId: null
|
||
};
|
||
});
|
||
|
||
// 当前用户显示的图片(根据榜单类型决定显示作品图片还是头像)
|
||
const currentUserDisplayImage = computed(() => {
|
||
const userInfo = currentUserInfo.value;
|
||
|
||
// 活动榜单始终显示头像
|
||
if (currentRankingType.value === RANKING_TYPES.ACTIVITY) {
|
||
return userInfo.avatar;
|
||
}
|
||
|
||
// 热度和自制榜单:优先显示作品图片,没有则显示头像
|
||
return userInfo.artworkImage || userInfo.avatar ;
|
||
});
|
||
|
||
const hasActualArtwork = computed(() => {
|
||
const userInfo = currentUserInfo.value;
|
||
|
||
if (currentRankingType.value === RANKING_TYPES.ACTIVITY) {
|
||
return false;
|
||
}
|
||
|
||
return !!userInfo.artworkImage;
|
||
});
|
||
|
||
|
||
// 用于跳过 visible 打开时 currentRankingType watch 的重复触发
|
||
let _skipRankingTypeWatch = false;
|
||
|
||
// 切换榜单类型并加载对应数据(统一入口,避免重复触发)
|
||
const switchRankingType = async (newType) => {
|
||
emit('ranking-type-change', newType);
|
||
activeTabIndex.value = 0;
|
||
hasNoMoreData.value = false;
|
||
|
||
if (newType === RANKING_TYPES.ACTIVITY) {
|
||
await fetchActivityList();
|
||
if (activityList.value.length > 0) {
|
||
const firstActivity = activityList.value[0];
|
||
selectedActivityId.value = firstActivity.id;
|
||
await loadSingleTabData(newType, firstActivity.title);
|
||
}
|
||
} else {
|
||
const tab = displayTabs.value[0];
|
||
const tabName = tab ? tab.name : '';
|
||
await loadSingleTabData(newType, tabName);
|
||
}
|
||
};
|
||
|
||
// 监听标签配置变化,重置选中状态
|
||
watch(() => props.tabs, (newTabs) => {
|
||
if (newTabs && newTabs.length > 0 && activeTabIndex.value >= newTabs.length) {
|
||
activeTabIndex.value = 0;
|
||
}
|
||
}, { immediate: true });
|
||
|
||
// 监听父组件传入的 rankingType,同步内部状态并触发数据加载
|
||
watch(() => props.rankingType, (newType, oldType) => {
|
||
if (newType !== oldType && newType !== currentRankingType.value) {
|
||
currentRankingType.value = newType;
|
||
// currentRankingType watch 会接管后续加载,无需在此重复
|
||
}
|
||
}, { immediate: false });
|
||
|
||
// 监听内部榜单类型变化,统一负责数据加载
|
||
watch(currentRankingType, async (newType, oldType) => {
|
||
if (newType === oldType || _skipRankingTypeWatch) return;
|
||
await switchRankingType(newType);
|
||
}, { immediate: false });
|
||
|
||
// 监听弹窗可见性变化
|
||
watch(() => props.visible, async (newVisible, oldVisible) => {
|
||
if (newVisible && !oldVisible) {
|
||
// 立即设置加载状态,避免显示旧数据
|
||
isLoadingData.value = true;
|
||
|
||
// 清空缓存,确保切换账号后数据刷新
|
||
apiRankingData.value = {};
|
||
currentUserByType.value = {};
|
||
pageInfo.value = {};
|
||
hasNoMoreData.value = false;
|
||
|
||
// 用 flag 阻止 currentRankingType watch 重复触发
|
||
_skipRankingTypeWatch = true;
|
||
currentRankingType.value = RANKING_TYPES.POPULARITY;
|
||
activeTabIndex.value = 0;
|
||
_skipRankingTypeWatch = false;
|
||
|
||
// 直接加载数据,不经过 watch
|
||
const tab = displayTabs.value[0];
|
||
const tabName = tab ? tab.name : '';
|
||
await loadSingleTabData(RANKING_TYPES.POPULARITY, tabName);
|
||
}
|
||
}, { immediate: false });
|
||
|
||
// 监听父组件活跃状态变化,当父组件不活跃时自动关闭弹窗
|
||
watch(() => props.parentActive, (newActive, oldActive) => {
|
||
if (oldActive && !newActive && props.visible) {
|
||
handleClose();
|
||
}
|
||
}, { immediate: false });
|
||
|
||
// 处理标签点击
|
||
const handleTabClick = async (index) => {
|
||
// 如果点击的是当前已选中的标签,不做任何操作
|
||
if (activeTabIndex.value === index) {
|
||
return;
|
||
}
|
||
|
||
// 更新选中的标签索引
|
||
activeTabIndex.value = index;
|
||
const tab = displayTabs.value[index];
|
||
const tabName = tab ? tab.name : '';
|
||
|
||
// 如果是活动榜单,更新选中的活动ID
|
||
if (currentRankingType.value === RANKING_TYPES.ACTIVITY) {
|
||
if (tab && tab.id) {
|
||
selectedActivityId.value = tab.id;
|
||
} else {
|
||
// 如果是加载中、错误或空状态,不加载数据
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 重置"没有更多数据"状态
|
||
const pageKey = `${currentRankingType.value}_${tabName}`;
|
||
hasNoMoreData.value = pageInfo.value[pageKey] ? !pageInfo.value[pageKey].hasMore : false;
|
||
|
||
// 懒加载:加载新标签的数据(如果还没加载)
|
||
await loadSingleTabData(currentRankingType.value, tabName);
|
||
|
||
// 触发标签切换事件,传递标签名称和索引
|
||
emit('tab-change', {
|
||
tabName,
|
||
tabType: tab ? tab.type : '',
|
||
tabIndex: index,
|
||
activityId: selectedActivityId.value,
|
||
dataCount: currentRankingData.value.length
|
||
});
|
||
};
|
||
|
||
// 同步节流 flag,防止 isLoadingMore 异步赋值前的重复触发
|
||
let _scrollLoadLocked = false;
|
||
|
||
// 处理滚动到底部 - 加载更多数据
|
||
const handleScrollToLower = async () => {
|
||
if (_scrollLoadLocked || isLoadingMore.value || hasNoMoreData.value || isLoadingData.value) {
|
||
return;
|
||
}
|
||
|
||
_scrollLoadLocked = true;
|
||
try {
|
||
const tab = displayTabs.value[activeTabIndex.value];
|
||
const tabName = tab ? tab.name : '';
|
||
const pageKey = `${currentRankingType.value}_${tabName}`;
|
||
const currentPage = pageInfo.value[pageKey]?.currentPage || 1;
|
||
|
||
await loadSingleTabData(currentRankingType.value, tabName, currentPage + 1);
|
||
} finally {
|
||
_scrollLoadLocked = false;
|
||
}
|
||
};
|
||
|
||
// 处理下拉刷新
|
||
const handleRefresh = async () => {
|
||
// 如果正在刷新,直接返回
|
||
if (isRefreshing.value) {
|
||
return;
|
||
}
|
||
|
||
isRefreshing.value = true;
|
||
|
||
try {
|
||
const tab = displayTabs.value[activeTabIndex.value];
|
||
const tabName = tab ? tab.name : '';
|
||
const pageKey = `${currentRankingType.value}_${tabName}`;
|
||
|
||
// 清除当前标签的缓存数据
|
||
if (apiRankingData.value[currentRankingType.value]) {
|
||
apiRankingData.value[currentRankingType.value][tabName] = null;
|
||
}
|
||
|
||
// 重置分页信息
|
||
if (pageInfo.value[pageKey]) {
|
||
pageInfo.value[pageKey] = {
|
||
currentPage: 1,
|
||
hasMore: true
|
||
};
|
||
}
|
||
hasNoMoreData.value = false;
|
||
|
||
// 重新加载第一页数据(传递刷新标识)
|
||
const success = await loadSingleTabData(currentRankingType.value, tabName, 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 {
|
||
// 延迟关闭刷新状态,确保动画流畅
|
||
refreshTimer = setTimeout(() => {
|
||
isRefreshing.value = false;
|
||
refreshTimer = null;
|
||
}, 500);
|
||
}
|
||
};
|
||
|
||
// 程序化切换标签的方法
|
||
const switchToTab = (tabNameOrIndex) => {
|
||
let targetIndex = -1;
|
||
|
||
if (typeof tabNameOrIndex === 'number') {
|
||
// 如果传入的是索引
|
||
targetIndex = tabNameOrIndex;
|
||
} else if (typeof tabNameOrIndex === 'string') {
|
||
// 如果传入的是标签名称
|
||
targetIndex = displayTabs.value.findIndex(tab => tab.name === tabNameOrIndex);
|
||
}
|
||
|
||
// 验证索引有效性
|
||
if (targetIndex >= 0 && targetIndex < displayTabs.value.length) {
|
||
handleTabClick(targetIndex);
|
||
} else {
|
||
}
|
||
};
|
||
|
||
// 获取当前选中标签的信息
|
||
const getCurrentTabInfo = () => {
|
||
const tab = displayTabs.value[activeTabIndex.value];
|
||
return {
|
||
tabName: tab ? tab.name : '',
|
||
tabType: tab ? tab.type : '',
|
||
tabIndex: activeTabIndex.value,
|
||
dataCount: currentRankingData.value.length
|
||
};
|
||
};
|
||
|
||
// 处理拜访按钮点击
|
||
const handleVisit = (userId) => {
|
||
emit('visit', userId);
|
||
};
|
||
|
||
// 处理查看个人信息
|
||
const handleViewProfile = (userId) => {
|
||
emit('view-profile', userId);
|
||
};
|
||
|
||
// 处理作品点击
|
||
const handleArtworkClick = (data) => {
|
||
emit('view-artwork', {
|
||
artworkId: data.artworkId,
|
||
userId: data.userId
|
||
});
|
||
};
|
||
|
||
// 防抖处理函数 - 优化版本,减少延迟
|
||
const debounceSwitch = (callback, delay = 800) => {
|
||
// 如果正在切换中,直接返回
|
||
if (isSwitching.value) {
|
||
return;
|
||
}
|
||
|
||
// 清除之前的定时器
|
||
if (switchDebounceTimer.value) {
|
||
clearTimeout(switchDebounceTimer.value);
|
||
}
|
||
|
||
// 设置加载状态
|
||
isSwitching.value = true;
|
||
isImageLoaded.value = false;
|
||
|
||
// 立即执行回调,不等待延迟
|
||
try {
|
||
callback();
|
||
|
||
// 使用nextTick确保DOM更新后再重置状态
|
||
nextTick(() => {
|
||
// 短暂延迟以确保动画流畅
|
||
switchDebounceTimer.value = setTimeout(() => {
|
||
isSwitching.value = false;
|
||
isImageLoaded.value = true;
|
||
switchDebounceTimer.value = null;
|
||
}, delay);
|
||
});
|
||
} catch (error) {
|
||
console.error('Switch operation failed:', error);
|
||
isSwitching.value = false;
|
||
isImageLoaded.value = true;
|
||
switchDebounceTimer.value = null;
|
||
}
|
||
};
|
||
|
||
// 处理左箭头点击(上一个榜单类型)
|
||
const handleLeftArrow = () => {
|
||
debounceSwitch(() => {
|
||
const currentType = currentRankingType.value;
|
||
const previousType = getPreviousRankingType(currentType);
|
||
currentRankingType.value = previousType;
|
||
});
|
||
};
|
||
|
||
// 处理右箭头点击(下一个榜单类型)
|
||
const handleRightArrow = () => {
|
||
debounceSwitch(() => {
|
||
const currentType = currentRankingType.value;
|
||
const nextType = getNextRankingType(currentType);
|
||
currentRankingType.value = nextType;
|
||
});
|
||
};
|
||
|
||
// 处理弹窗关闭
|
||
const handleClose = () => {
|
||
emit('update:visible', false);
|
||
};
|
||
|
||
// 路由变化时自动关闭弹窗(切换菜单/标签页)
|
||
const handleRouteChange = () => {
|
||
if (props.visible) {
|
||
handleClose();
|
||
}
|
||
};
|
||
|
||
// 预加载所有榜单标识图片
|
||
const preloadRankingImages = () => {
|
||
const imagesToPreload = Object.values(RANKING_TYPE_IMAGES);
|
||
|
||
imagesToPreload.forEach(imageSrc => {
|
||
if (!preloadedImages.value.has(imageSrc)) {
|
||
// 使用uni.getImageInfo预加载图片
|
||
if (typeof uni !== 'undefined') {
|
||
uni.getImageInfo({
|
||
src: imageSrc,
|
||
success: () => {
|
||
preloadedImages.value.add(imageSrc);
|
||
},
|
||
fail: (err) => {
|
||
}
|
||
});
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
// 模块级定时器引用,确保 onUnmounted 能正确清理
|
||
let routeCheckInterval = null;
|
||
let refreshTimer = null;
|
||
|
||
onMounted(async () => {
|
||
// 预加载所有榜单标识图片
|
||
preloadRankingImages();
|
||
|
||
// 监听路由变化(uni-app)- 仅在切换菜单/标签页时关闭
|
||
if (typeof getCurrentPages === 'function') {
|
||
// 保存当前页面路径
|
||
const currentPages = getCurrentPages();
|
||
const currentPath = currentPages.length > 0 ? currentPages[currentPages.length - 1].route : '';
|
||
|
||
// 定期检查页面路径变化
|
||
routeCheckInterval = setInterval(() => {
|
||
if (!props.visible) {
|
||
clearInterval(routeCheckInterval);
|
||
routeCheckInterval = null;
|
||
return;
|
||
}
|
||
|
||
const newPages = getCurrentPages();
|
||
const newPath = newPages.length > 0 ? newPages[newPages.length - 1].route : '';
|
||
|
||
if (newPath !== currentPath && props.visible) {
|
||
handleRouteChange();
|
||
clearInterval(routeCheckInterval);
|
||
routeCheckInterval = null;
|
||
}
|
||
}, 500); // 每500ms检查一次路由变化
|
||
}
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
// 清理路由检查定时器
|
||
if (routeCheckInterval) {
|
||
clearInterval(routeCheckInterval);
|
||
routeCheckInterval = null;
|
||
}
|
||
|
||
// 清理刷新延迟定时器
|
||
if (refreshTimer) {
|
||
clearTimeout(refreshTimer);
|
||
refreshTimer = null;
|
||
}
|
||
|
||
// 清理防抖定时器
|
||
if (switchDebounceTimer.value) {
|
||
clearTimeout(switchDebounceTimer.value);
|
||
switchDebounceTimer.value = null;
|
||
}
|
||
|
||
// 重置滚动锁,防止组件重用时状态残留
|
||
_scrollLoadLocked = false;
|
||
_skipRankingTypeWatch = false;
|
||
});
|
||
|
||
// 判断是否为当前用户
|
||
const isCurrentUser = (userId) => {
|
||
return userId === currentUserInfo.value.userId;
|
||
};
|
||
|
||
// 格式化人气值显示
|
||
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}名`;
|
||
};
|
||
|
||
// 暴露方法给父组件使用
|
||
defineExpose({
|
||
switchToTab,
|
||
getCurrentTabInfo,
|
||
activeTabIndex: computed(() => activeTabIndex.value),
|
||
currentTabName: computed(() => {
|
||
const tab = displayTabs.value[activeTabIndex.value];
|
||
return tab ? tab.name : '';
|
||
}),
|
||
currentRankingType: computed(() => currentRankingType.value),
|
||
isSwitching: computed(() => isSwitching.value),
|
||
switchToNextRankingType: () => handleRightArrow(),
|
||
switchToPreviousRankingType: () => handleLeftArrow()
|
||
});
|
||
|
||
// 处理当前用户头像加载失败
|
||
const handleCurrentUserAvatarError = (e) => {
|
||
// 设置默认头像 - 使用现有的头像作为备用
|
||
e.target.src = '/static/avatar/1.jpeg';
|
||
};
|
||
|
||
// 处理标题图片加载成功
|
||
const handleTitleImageLoad = () => {
|
||
isImageLoaded.value = true;
|
||
};
|
||
|
||
// 处理标题图片加载失败
|
||
const handleTitleImageError = (e) => {
|
||
// 设置默认标题图片
|
||
e.target.src = RANKING_TYPE_IMAGES[RANKING_TYPES.POPULARITY];
|
||
isImageLoaded.value = true;
|
||
};
|
||
|
||
// 处理遮罩层点击关闭
|
||
const handleMaskClick = (e) => {
|
||
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 = (e) => {
|
||
if (!isDragging.value) {
|
||
return;
|
||
}
|
||
|
||
isDragging.value = false;
|
||
|
||
// 如果下拉距离超过阈值,关闭弹窗
|
||
if (dragOffset.value > DRAG_THRESHOLD) {
|
||
handleClose();
|
||
}
|
||
|
||
// 重置偏移量
|
||
dragOffset.value = 0;
|
||
};
|
||
</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;
|
||
}
|
||
|
||
.scale-enter-active,
|
||
.scale-leave-active {
|
||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||
}
|
||
|
||
.scale-enter-from,
|
||
.scale-leave-to {
|
||
opacity: 0;
|
||
transform: scale(0.85) translateY(20rpx);
|
||
}
|
||
|
||
.modal-wrapper {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
z-index: 9999;
|
||
display: flex;
|
||
align-items: center;
|
||
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: calc(100% - 224rpx);
|
||
bottom: 32rpx;
|
||
border-radius: 48rpx;
|
||
overflow: hidden;
|
||
z-index: 10;
|
||
/* 优化:3层阴影简化为1层,提升渲染性能 */
|
||
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.35);
|
||
border: 2rpx solid rgba(255, 255, 255, 0.2);
|
||
/* 添加过渡动画,使下拉关闭更流畅 */
|
||
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;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-image: url('/static/rank/rank-bg.png');
|
||
background-repeat: no-repeat;
|
||
background-position: center;
|
||
z-index: 0;
|
||
}
|
||
|
||
.modal-background::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
/* 优化:3层渐变简化为1层,提升性能 */
|
||
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: space-between;
|
||
margin-bottom: 40rpx;
|
||
padding: 0 15rpx;
|
||
position: relative;
|
||
}
|
||
|
||
.nav-arrow {
|
||
width: 48rpx;
|
||
height: 48rpx;
|
||
opacity: 0.9;
|
||
filter: drop-shadow(0 4rpx 8rpx rgba(0, 0, 0, 0.3));
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
cursor: pointer;
|
||
padding: 8rpx;
|
||
}
|
||
|
||
|
||
|
||
@keyframes switching-pulse {
|
||
0% {
|
||
transform: scale(1);
|
||
opacity: 0.5;
|
||
}
|
||
|
||
100% {
|
||
transform: scale(1.05);
|
||
opacity: 0.7;
|
||
}
|
||
}
|
||
|
||
|
||
|
||
.ranking-title-image {
|
||
flex: 1;
|
||
max-width: 400rpx;
|
||
height: 60rpx;
|
||
object-fit: contain;
|
||
filter: drop-shadow(0 4rpx 8rpx rgba(0, 0, 0, 0.4));
|
||
pointer-events: none;
|
||
transform: scale(1);
|
||
opacity: 1;
|
||
transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||
transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||
will-change: opacity, transform;
|
||
}
|
||
|
||
.ranking-title-image.switching {
|
||
opacity: 0.3;
|
||
transform: scale(0.95);
|
||
}
|
||
|
||
.ranking-title-image.loaded {
|
||
opacity: 1;
|
||
transform: scale(2.5);
|
||
}
|
||
|
||
|
||
/* 标签区域 */
|
||
.tab-section {
|
||
width: 92%;
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 80rpx;
|
||
margin-bottom: 10rpx;
|
||
padding: 12rpx 0;
|
||
position: relative;
|
||
/* 关键:为伪元素定位做准备 */
|
||
}
|
||
|
||
.tab-section.has-icon {
|
||
gap: 8rpx
|
||
}
|
||
|
||
/* 使用 ::after 伪元素绘制完美的圆角横线 */
|
||
.tab-section::after {
|
||
content: '';
|
||
position: absolute;
|
||
bottom: -6rpx;
|
||
left: 0;
|
||
right: 0;
|
||
height: 8rpx;
|
||
background-color: rgba(252, 252, 252, 0.6);
|
||
border-radius: 8rpx;
|
||
}
|
||
|
||
.tab-item {
|
||
/* 移除 flex: 1,让内容自动撑开宽度 */
|
||
padding: 18rpx 28rpx;
|
||
border-radius: 40rpx;
|
||
background: transparent;
|
||
/* 优化:只动画必要属性,避免 transition: all */
|
||
transition: background 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||
transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||
box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
text-align: center;
|
||
position: relative;
|
||
/* 移除固定的 min-width 和 min-height,让文字撑开 */
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
white-space: nowrap; /* 防止文字换行 */
|
||
}
|
||
|
||
.tab-item:active {
|
||
transform: scale(0.95);
|
||
}
|
||
|
||
.tab-item.active {
|
||
/* 渐变:左浅橙粉 → 右柔粉红 */
|
||
background: linear-gradient(to bottom right,
|
||
#F0E4B1 0%, /* 左:浅橙粉 */
|
||
#F08399 50%,
|
||
#B94E73 100% /* 右:柔粉红 */
|
||
);
|
||
|
||
border-radius: 999rpx; /* 胶囊形 */
|
||
|
||
/* 立体感核心:多层阴影 + 内阴影模拟凸起 */
|
||
box-shadow:
|
||
/* 外层投影 - 让按钮浮起 */
|
||
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
|
||
0 2rpx 6rpx rgba(255, 143, 158, 0.15),
|
||
|
||
/* 内阴影 - 模拟顶部受光 + 底部凹陷 */
|
||
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4), /* 顶部高光 */
|
||
inset 0 -2rpx 4rpx rgba(0, 0, 0, 0.05); /* 底部暗部 */
|
||
|
||
position: relative;
|
||
}
|
||
|
||
|
||
.tab-text {
|
||
font-size: 24rpx;
|
||
color: rgba(255, 255, 255, 0.8);
|
||
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.3);
|
||
transition: all 0.3s ease;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.tab-item.active .tab-text {
|
||
color: #FFFFFF;
|
||
text-shadow:
|
||
0 2rpx 4rpx rgba(0, 0, 0, 0.4),
|
||
0 1rpx 2rpx rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
/* 标签图标样式 */
|
||
.tab-icon {
|
||
width: 32rpx;
|
||
height: 32rpx;
|
||
margin-bottom: 8rpx;
|
||
filter: drop-shadow(0 2rpx 4rpx rgba(0, 0, 0, 0.3));
|
||
transition: all 0.3s ease;
|
||
transform: scale(3.5)
|
||
}
|
||
|
||
.tab-icon.active {
|
||
position: absolute;
|
||
transform: scale(4.2);
|
||
bottom: 24rpx;
|
||
left: 32rpx;
|
||
}
|
||
|
||
.tab-item.has-icon {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-around;
|
||
gap: 8rpx;
|
||
}
|
||
|
||
.tab-item.has-icon.active {
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
/* 内容占位区域 */
|
||
.content-placeholder {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-height: 400rpx;
|
||
background: rgba(255, 255, 255, 0.08);
|
||
backdrop-filter: blur(10rpx);
|
||
border-radius: 24rpx;
|
||
border: 1rpx solid rgba(255, 255, 255, 0.15);
|
||
box-shadow: inset 0 1rpx 0 rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.placeholder-text {
|
||
font-size: 32rpx;
|
||
color: rgba(255, 255, 255, 0.7);
|
||
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
/* 滚动内容区域 */
|
||
.scrollable-content {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding-bottom: 224rpx; /* 为当前用户栏留出空间 */
|
||
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: 'ZaoZiGongFangJianHei-1', 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: 'ZaoZiGongFangJianHei-1', 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: 'ZaoZiGongFangJianHei-1', 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 15rpx;
|
||
}
|
||
|
||
/* 空数据提示 */
|
||
.empty-data {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-height: 320rpx;
|
||
background: rgba(255, 255, 255, 0.08);
|
||
backdrop-filter: blur(10rpx);
|
||
border-radius: 24rpx;
|
||
border: 1rpx solid rgba(255, 255, 255, 0.15);
|
||
box-shadow: inset 0 1rpx 0 rgba(255, 255, 255, 0.1);
|
||
margin: 0 15rpx;
|
||
}
|
||
|
||
.empty-text {
|
||
font-size: 30rpx;
|
||
color: rgba(255, 255, 255, 0.7);
|
||
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
/* 排名列表区域 */
|
||
.ranking-list-section {
|
||
margin-top: 20rpx;
|
||
padding: 0 15rpx;
|
||
}
|
||
|
||
/* 加载更多容器 */
|
||
.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: 'ZaoZiGongFangJianHei-1', 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: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||
}
|
||
|
||
/* 底部占位,防止内容被当前用户栏遮挡 */
|
||
.bottom-spacer {
|
||
height: 20rpx;
|
||
}
|
||
|
||
/* 当前用户栏 - 固定在底部 */
|
||
.current-user-bar {
|
||
position: absolute;
|
||
bottom: 148rpx;
|
||
left: 20rpx;
|
||
right: 20rpx;
|
||
padding: 24rpx;
|
||
/* 优化:5色渐变简化为2色,提升性能 */
|
||
background: linear-gradient(135deg, rgba(255, 107, 157, 0.9) 0%, rgba(255, 177, 153, 0.9) 100%);
|
||
border-radius: 32rpx;
|
||
/* 优化:3层阴影简化为1层 */
|
||
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;
|
||
/* gap: 34rpx; */
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.current-user-avatar {
|
||
width: 88rpx;
|
||
height: 88rpx;
|
||
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-avatar.no-artwork {
|
||
width: 64rpx;
|
||
border-radius: 0;
|
||
}
|
||
|
||
.current-user-info {
|
||
flex: 1;
|
||
display: flex;
|
||
margin-left: 34rpx;
|
||
margin-right: 34rpx;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.current-user-nickname {
|
||
font-size: 34rpx;
|
||
font-weight: bold;
|
||
color: #FFFFFF;
|
||
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||
text-shadow:
|
||
0 3rpx 6rpx rgba(0, 0, 0, 0.4),
|
||
0 1rpx 3rpx rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.current-user-score {
|
||
display: flex;
|
||
align-items: center;
|
||
/* gap: 8rpx; */
|
||
}
|
||
|
||
.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: 'ZaoZiGongFangJianHei-1', 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: 'ZaoZiGongFangJianHei-1', 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 {
|
||
max-width: 100%;
|
||
border-radius: 40rpx;
|
||
}
|
||
|
||
.modal-content {
|
||
padding: 40rpx 30rpx;
|
||
}
|
||
|
||
.nav-arrow {
|
||
width: 44rpx;
|
||
height: 44rpx;
|
||
}
|
||
|
||
.ranking-title-image {
|
||
max-width: 350rpx;
|
||
height: 55rpx;
|
||
}
|
||
|
||
.tab-section {
|
||
gap: 6rpx;
|
||
flex-wrap: nowrap;
|
||
padding: 10rpx;
|
||
}
|
||
|
||
.tab-item {
|
||
padding: 16rpx 24rpx;
|
||
flex: 1;
|
||
min-width: auto;
|
||
min-height: 56rpx;
|
||
}
|
||
|
||
.tab-text {
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.scrollable-content {
|
||
padding-bottom: 140rpx;
|
||
}
|
||
|
||
.top3-section {
|
||
gap: 12rpx;
|
||
padding: 0 8rpx;
|
||
}
|
||
|
||
.ranking-list-section {
|
||
padding: 0 8rpx;
|
||
}
|
||
|
||
.current-user-bar {
|
||
bottom: 15rpx;
|
||
left: 15rpx;
|
||
right: 15rpx;
|
||
padding: 20rpx;
|
||
border-radius: 28rpx;
|
||
}
|
||
|
||
.current-user-content {
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.current-user-avatar {
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
}
|
||
|
||
.current-user-nickname {
|
||
font-size: 30rpx;
|
||
}
|
||
|
||
.score-text {
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.rank-text {
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.current-user-rank {
|
||
padding: 14rpx 24rpx;
|
||
}
|
||
}
|
||
|
||
@media screen and (max-width: 600rpx) {
|
||
.modal-container {
|
||
border-radius: 36rpx;
|
||
max-height: 90vh;
|
||
}
|
||
|
||
.modal-content {
|
||
padding: 36rpx 24rpx;
|
||
}
|
||
|
||
.header-section {
|
||
margin-bottom: 32rpx;
|
||
padding: 0 8rpx;
|
||
}
|
||
|
||
.nav-arrow {
|
||
width: 40rpx;
|
||
height: 40rpx;
|
||
}
|
||
|
||
.ranking-title-image {
|
||
max-width: 300rpx;
|
||
height: 50rpx;
|
||
}
|
||
|
||
.tab-section {
|
||
gap: 6rpx;
|
||
margin-bottom: 32rpx;
|
||
padding: 10rpx 12rpx;
|
||
}
|
||
|
||
.tab-item {
|
||
padding: 14rpx 20rpx;
|
||
flex: 1;
|
||
min-width: auto;
|
||
max-width: none;
|
||
min-height: 52rpx;
|
||
}
|
||
|
||
.tab-text {
|
||
font-size: 26rpx;
|
||
}
|
||
|
||
.scrollable-content {
|
||
padding-bottom: 130rpx;
|
||
}
|
||
|
||
.top3-section {
|
||
gap: 8rpx;
|
||
margin-bottom: 32rpx;
|
||
padding: 0 4rpx;
|
||
}
|
||
|
||
.ranking-list-section {
|
||
padding: 0 4rpx;
|
||
}
|
||
|
||
.current-user-bar {
|
||
bottom: 12rpx;
|
||
left: 12rpx;
|
||
right: 12rpx;
|
||
padding: 18rpx 24rpx;
|
||
border-radius: 24rpx;
|
||
}
|
||
|
||
.current-user-content {
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.current-user-avatar {
|
||
width: 72rpx;
|
||
height: 72rpx;
|
||
border-width: 3rpx;
|
||
}
|
||
|
||
.current-user-info {
|
||
gap: 8rpx;
|
||
}
|
||
|
||
.current-user-nickname {
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.flame-icon {
|
||
width: 24rpx;
|
||
height: 24rpx;
|
||
}
|
||
|
||
.score-text {
|
||
font-size: 22rpx;
|
||
}
|
||
|
||
.current-user-rank {
|
||
padding: 12rpx 20rpx;
|
||
border-radius: 20rpx;
|
||
}
|
||
|
||
.rank-text {
|
||
font-size: 26rpx;
|
||
}
|
||
}
|
||
|
||
@media screen and (max-width: 480rpx) {
|
||
.modal-wrapper {
|
||
align-items: flex-start;
|
||
padding-top: 60rpx;
|
||
}
|
||
|
||
.modal-container {
|
||
border-radius: 32rpx;
|
||
max-height: 85vh;
|
||
}
|
||
|
||
.modal-content {
|
||
padding: 32rpx 20rpx;
|
||
}
|
||
|
||
.header-section {
|
||
margin-bottom: 28rpx;
|
||
}
|
||
|
||
.nav-arrow {
|
||
width: 36rpx;
|
||
height: 36rpx;
|
||
}
|
||
|
||
.ranking-title-image {
|
||
max-width: 250rpx;
|
||
height: 45rpx;
|
||
}
|
||
|
||
.tab-section {
|
||
gap: 4rpx;
|
||
margin-bottom: 28rpx;
|
||
padding: 8rpx;
|
||
}
|
||
|
||
.tab-item {
|
||
padding: 12rpx 16rpx;
|
||
flex: 1;
|
||
min-width: auto;
|
||
border-radius: 36rpx;
|
||
min-height: 48rpx;
|
||
}
|
||
|
||
.tab-text {
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.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: 10rpx;
|
||
left: 10rpx;
|
||
right: 10rpx;
|
||
padding: 16rpx 20rpx;
|
||
border-radius: 20rpx;
|
||
}
|
||
|
||
.current-user-content {
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.current-user-avatar {
|
||
width: 64rpx;
|
||
height: 64rpx;
|
||
}
|
||
|
||
.current-user-nickname {
|
||
font-size: 26rpx;
|
||
}
|
||
|
||
.flame-icon {
|
||
width: 22rpx;
|
||
height: 22rpx;
|
||
}
|
||
|
||
.score-text {
|
||
font-size: 20rpx;
|
||
}
|
||
|
||
.current-user-rank {
|
||
padding: 10rpx 16rpx;
|
||
border-radius: 18rpx;
|
||
}
|
||
|
||
.rank-text {
|
||
font-size: 24rpx;
|
||
}
|
||
}
|
||
|
||
/* 触摸优化 */
|
||
@media (hover: none) and (pointer: coarse) {
|
||
.nav-arrow,
|
||
.tab-item,
|
||
.visit-button {
|
||
min-height: 88rpx;
|
||
}
|
||
|
||
.nav-arrow {
|
||
min-width: 88rpx;
|
||
transform: scale(2)
|
||
}
|
||
|
||
.tab-item {
|
||
padding: 16rpx 24rpx;
|
||
flex: 1;
|
||
min-height: 20rpx;
|
||
}
|
||
|
||
.tab-item.has-icon{
|
||
padding: 8rpx 24rpx;
|
||
}
|
||
|
||
.nav-arrow {
|
||
padding: 12rpx;
|
||
}
|
||
}
|
||
</style>
|