topfans/frontend/pages/components/RankingModal.vue
2026-04-13 17:34:03 +08:00

2133 lines
51 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="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>