topfans/frontend/pages/components/TOP3Card.vue
2026-04-07 23:08:49 +08:00

793 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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

<template>
<view class="top3-card" :class="{ 'top3-card2': !hasArtwork }">
<!-- 有作品的布局 -->
<template v-if="hasArtwork">
<!-- 作品图片主要视觉元素 - 使用懒加载 -->
<view class="artwork-container">
<LazyImage
class="artwork-image"
:src="artworkImage"
mode="aspectFill"
:width="'100%'"
:height="'100%'"
@error="handleArtworkError"
/>
<!-- 人气值覆盖在作品图片左上角 -->
<view class="popularity-overlay">
<image class="fire-icon" src="/static/rank/spark.png" mode="aspectFit" lazy-load></image>
<text class="popularity-score">{{ formattedScore }}</text>
</view>
<!-- 排名徽章在作品图片底部 -->
<view class="rank-badge-bottom">
<image class="rank-icon" :src="`/static/rank/charm-rank-icon${rank}.png`" mode="aspectFit"
@error="handleRankIconError" lazy-load></image>
</view>
<view class="rank-badge-bottom rank-badge-bottom2">
<image class="rank-icon" :src="`/static/rank/rank-icon${rank}.png`" mode="aspectFit"
@error="handleRankIconError" lazy-load></image>
</view>
</view>
<!-- 用户信息头像和昵称在作品图片下方 - 使用懒加载 -->
<view class="user-info-with-artwork">
<LazyImage
class="user-avatar-small"
:src="avatar || '/static/avatar/1.jpeg'"
mode="aspectFit"
:width="'32rpx'"
:height="'32rpx'"
@error="handleAvatarError"
@tap="handleAvatarClick"
/>
<text class="user-nickname">{{'用户 :'}}
<text class="user-nickname-name">{{ nickname || '未知用户' }}</text>
</text>
</view>
</template>
<!-- 无作品的布局 -->
<template v-else>
<!-- 人气值在头像上方 -->
<view class="popularity-container">
<image class="fire-icon" src="/static/rank/spark.png" mode="aspectFit" lazy-load></image>
<text class="popularity-score">{{ formattedScore }}</text>
</view>
<!-- 用户头像居中显示大尺寸 - 使用懒加载 -->
<view class="avatar-container">
<LazyImage
class="user-avatar-large"
:src="avatar || '/static/avatar/1.jpeg'"
mode="aspectFill"
:width="'180rpx'"
:height="'180rpx'"
@error="handleAvatarError"
@tap="handleAvatarClick"
/>
<view class="rank-badge-bottom">
<image class="rank-icon" :src="`/static/rank/charm-rank-icon${rank}.png`" mode="aspectFit"
@error="handleRankIconError" lazy-load></image>
</view>
<view class="rank-badge-bottom rank-badge-bottom2">
<image class="rank-icon" :src="`/static/rank/rank-icon${rank}.png`" mode="aspectFit"
@error="handleRankIconError" lazy-load></image>
</view>
</view>
<!-- 用户昵称在头像下方 -->
<view class="nickname-container">
<text class="user-nickname">{{'用户 :'}}
<text class="user-nickname-name">{{ nickname || '未知用户' }}
</text>
</text>
</view>
</template>
<!-- 操作菜单 -->
<view v-if="showActionMenu" class="action-menu-overlay" @tap="closeActionMenu">
<view class="action-menu" @tap.stop>
<view
v-if="!isCurrentUser"
class="menu-item"
@tap="handleMenuVisit"
>
<image class="menu-icon" src="/static/icon/visit-house.png" mode="aspectFit" lazy-load></image>
<text class="menu-text">拜访小屋</text>
</view>
<view class="menu-item" @tap="handleMenuViewProfile">
<image class="menu-icon" src="/static/icon/character.png" mode="aspectFit" lazy-load></image>
<text class="menu-text">查看个人信息</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import {
ref,
computed
} from 'vue';
import LazyImage from '@/components/LazyImage.vue';
const props = defineProps({
rank: {
type: Number,
required: true,
validator: (value) => [1, 2, 3].includes(value)
},
avatar: {
type: String,
default: '/static/avatar/1.jpeg'
},
nickname: {
type: String,
required: true
},
popularityScore: {
type: Number,
required: true
},
artworkImage: {
type: String,
default: '' // 改为可选,默认为空字符串
},
userId: {
type: String,
default: ''
},
isCurrentUser: {
type: Boolean,
default: false
}
});
const emit = defineEmits(['avatar-click', 'visit', 'view-profile']);
// 操作菜单显示状态
const showActionMenu = ref(false);
// 判断是否有作品图片
const hasArtwork = computed(() => {
return !!props.artworkImage && props.artworkImage.trim() !== '';
});
// 格式化人气值(超过百万显示为 M
const formattedScore = computed(() => {
const score = props.popularityScore;
// 处理无效数据
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 handleAvatarClick = () => {
showActionMenu.value = true;
emit('avatar-click', {
userId: props.userId,
isCurrentUser: props.isCurrentUser
});
};
// 关闭操作菜单
const closeActionMenu = () => {
showActionMenu.value = false;
};
// 处理菜单中的拜访选项
const handleMenuVisit = () => {
closeActionMenu();
emit('visit');
};
// 处理菜单中的查看个人信息选项
const handleMenuViewProfile = () => {
closeActionMenu();
emit('view-profile', props.userId);
};
// 处理排名图标加载失败
const handleRankIconError = (e) => {
// 可以设置一个默认的排名图标或者隐藏
};
// 处理头像加载失败
const handleAvatarError = (e) => {
e.target.src = '/static/avatar/1.jpeg';
};
// 处理作品图片加载失败
const handleArtworkError = (e) => {
e.target.src = '/static/avatar/1.jpeg';
};
</script>
<style scoped>
.top3-card {
width: 25%;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
/* gap: 64rpx; */
/* 优化:只动画必要属性,避免 transition: all */
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.top3-card2{
margin-top:40rpx
}
/* 排名徽章 */
.rank-badge {
position: absolute;
top: -12rpx;
left: 50%;
transform: translateX(-50%);
z-index: 10;
width: 80rpx;
height: 80rpx;
}
.rank-icon {
width: 100%;
height: 100%;
filter: drop-shadow(0 4rpx 8rpx rgba(0, 0, 0, 0.3));
}
/* ========== 有作品的布局样式 ========== */
/* 作品图片容器 */
.artwork-container {
width: 100%;
height: 240rpx;
margin-top: 0;
margin-bottom: 84rpx;
background-image: url('/static/rank/frames/frame1.png');
background-size: 130% 115%;
background-repeat: no-repeat;
background-position: center;
position: relative;
padding: 8rpx;
padding-top: 16rpx;
}
.artwork-image {
margin: 4rpx;
border-radius: 12rpx;
}
/* LazyImage wrapper styling for artwork */
.artwork-image :deep(.lazy-image-wrapper) {
width: calc(100% - 8rpx);
height: calc(100% - 8rpx);
border-radius: 12rpx;
}
/* 人气值覆盖层(在作品图片左上角) */
.popularity-overlay {
position: absolute;
top: 24rpx;
left: 0;
display: flex;
gap: 4rpx;
align-items: center;
padding: 6rpx 12rpx;
border-radius: 12rpx;
z-index: 2;
}
/* 排名徽章在作品图片底部 */
.rank-badge-bottom {
position: absolute;
bottom: -40rpx;
left: 50%;
transform: translateX(-50%) scale(2.5);
z-index: 10;
width: 80rpx;
height: 80rpx;
pointer-events: none;
}
.rank-badge-bottom2 {
bottom: -50rpx;
transform: translateX(-50%) scale(3)
}
.rank-badge-bottom .rank-icon {
width: 100%;
height: 100%;
filter: drop-shadow(0 4rpx 8rpx rgba(0, 0, 0, 0.3));
}
/* 有作品时的用户信息 */
.user-info-with-artwork {
display: flex;
flex-direction: row;
align-items: center;
/* gap: 12rpx; */
width: 100%;
justify-content: flex-start;
padding: 0 12rpx;
}
.user-avatar-small {
border-radius: 50%;
border: 3rpx solid rgba(255, 255, 255, 0.7);
/* 优化2层阴影简化为1层 */
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.3);
cursor: pointer;
/* 优化:只动画 transform避免 transition: all */
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.user-avatar-small:active {
transform: scale(0.95);
}
/* LazyImage wrapper styling for small avatar */
.user-avatar-small :deep(.lazy-image-wrapper) {
border-radius: 50%;
}
/* ========== 无作品的布局样式 ========== */
/* 人气值容器(在头像上方) */
.popularity-container {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: -32rpx;
}
.popularity-container .fire-icon {
width: 32rpx;
height: 40rpx;
}
.popularity-container .popularity-score {
font-size: 32rpx;
font-weight: bold;
color: #FFFFFF;
text-shadow:
0 2rpx 4rpx rgba(0, 0, 0, 0.5),
0 1rpx 2rpx rgba(0, 0, 0, 0.3);
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
}
.popularity-overlay .fire-icon {
width: 32rpx;
height: 40rpx;
}
.popularity-overlay .popularity-score {
font-size: 24rpx;
font-weight: bold;
color: #FFFFFF;
text-shadow:
0 2rpx 4rpx rgba(0, 0, 0, 0.5),
0 1rpx 2rpx rgba(0, 0, 0, 0.3);
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
}
/* 头像容器(居中显示,大尺寸) */
.avatar-container {
width: 180rpx;
height: 180rpx;
margin-bottom: 80rpx;
margin-top: 64rpx;
position: relative;
}
.user-avatar-large {
border-radius: 50%;
border: 5rpx solid rgba(255, 255, 255, 0.8);
/* 优化2层阴影简化为1层 */
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.3);
cursor: pointer;
/* 优化:只动画 transform避免 transition: all */
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.user-avatar-large:active {
transform: scale(0.95);
}
/* LazyImage wrapper styling for large avatar */
.user-avatar-large :deep(.lazy-image-wrapper) {
width: 100%;
height: 100%;
border-radius: 50%;
}
/* 昵称容器(在头像下方) */
.nickname-container {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
padding: 0 12rpx;
}
.user-nickname {
font-size: 14rpx;
margin-left: 12rpx;
color: #FFFFFF;
text-align: center;
max-width: 100%;
text-overflow: ellipsis;
white-space: nowrap;
text-shadow:
0 3rpx 6rpx rgba(0, 0, 0, 0.4),
0 1rpx 3rpx rgba(0, 0, 0, 0.3);
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
}
.user-nickname-name{
font-size: 14rpx;
margin-left: 0.5em; /* 缩进2格 */
color: #FFA500; /* 橙色,接近图中 LISA 的颜色 */
text-shadow:
0 3rpx 6rpx rgba(0, 0, 0, 0.4),
0 1rpx 3rpx rgba(0, 0, 0, 0.3);
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
}
/* 响应式布局 */
@media screen and (max-width: 750rpx) {
.top3-card {
padding: 20rpx;
border-radius: 24rpx;
}
/* 有作品布局 */
.artwork-container {
width: 220rpx;
height: 300rpx;
border-radius: 18rpx;
border-width: 3rpx;
}
.rank-badge-bottom {
width: 72rpx;
height: 72rpx;
bottom: -18rpx;
}
.user-avatar-small {
width: 48rpx;
height: 48rpx;
}
/* 无作品布局 */
.avatar-container {
width: 160rpx;
height: 160rpx;
}
.user-avatar-large {
border-width: 4rpx;
}
.user-nickname {
font-size: 26rpx;
}
.rank-badge {
width: 72rpx;
height: 72rpx;
top: -10rpx;
}
.rank-icon {
width: 100%;
height: 100%;
}
.charm-icon {
width: 26rpx;
height: 26rpx;
}
.popularity-score {
font-size: 28rpx;
}
.popularity-overlay .charm-icon {
width: 18rpx;
height: 18rpx;
}
.popularity-overlay .fire-icon {
width: 18rpx;
height: 18rpx;
}
.popularity-overlay .popularity-score {
font-size: 18rpx;
}
}
@media screen and (max-width: 600rpx) {
.top3-card {
padding: 18rpx;
border-radius: 20rpx;
}
.rank-badge {
top: -10rpx;
padding: 8rpx 20rpx;
border-radius: 20rpx;
}
.badge-text {
font-size: 20rpx;
}
/* 有作品布局 */
.artwork-container {
width: 200rpx;
height: 280rpx;
margin-top: 0;
margin-bottom: 16rpx;
border-radius: 16rpx;
}
.rank-badge-bottom {
width: 68rpx;
height: 68rpx;
bottom: -16rpx;
}
.user-info-with-artwork {
gap: 10rpx;
}
.user-avatar-small {
width: 40rpx;
height: 40rpx;
}
.popularity-overlay {
top: 6rpx;
left: 6rpx;
padding: 5rpx 10rpx;
border-radius: 10rpx;
gap: 5rpx;
}
/* 无作品布局 */
.popularity-container {
margin-top: 20rpx;
margin-bottom: 14rpx;
gap: 6rpx;
}
.avatar-container {
width: 140rpx;
height: 140rpx;
margin-bottom: 14rpx;
}
.user-avatar-large {
border-width: 4rpx;
}
.user-nickname {
font-size: 24rpx;
}
.fire-icon {
width: 26rpx;
height: 26rpx;
}
.popularity-score {
font-size: 26rpx;
}
.popularity-overlay .fire-icon {
width: 18rpx;
height: 18rpx;
}
.popularity-overlay .popularity-score {
font-size: 16rpx;
}
}
@media screen and (max-width: 480rpx) {
.top3-card {
padding: 16rpx;
border-radius: 18rpx;
}
.rank-badge {
top: -8rpx;
padding: 6rpx 16rpx;
border-radius: 18rpx;
}
.badge-text {
font-size: 18rpx;
}
/* 有作品布局 */
.artwork-container {
width: 180rpx;
height: 260rpx;
margin-top: 0;
margin-bottom: 12rpx;
border-radius: 14rpx;
border-width: 2rpx;
}
.rank-badge-bottom {
width: 64rpx;
height: 64rpx;
bottom: -14rpx;
}
.user-info-with-artwork {
gap: 8rpx;
}
.user-avatar-small {
width: 33rpx;
height: 33rpx;
border-width: 2rpx;
}
.popularity-overlay {
top: 5rpx;
left: 5rpx;
padding: 4rpx 8rpx;
border-radius: 8rpx;
gap: 4rpx;
}
/* 无作品布局 */
.popularity-container {
margin-top: 16rpx;
margin-bottom: 12rpx;
gap: 5rpx;
}
.avatar-container {
width: 120rpx;
height: 120rpx;
margin-bottom: 12rpx;
}
.user-avatar-large {
border-width: 3rpx;
}
.user-nickname {
font-size: 22rpx;
}
.fire-icon {
width: 24rpx;
height: 24rpx;
}
.popularity-score {
font-size: 24rpx;
}
.popularity-overlay .fire-icon {
width: 16rpx;
height: 16rpx;
}
.popularity-overlay .popularity-score {
font-size: 14rpx;
}
}
/* 操作菜单样式 */
.action-menu-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.5);
/* 优化:移除 backdrop-filterGPU 密集型操作 */
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.action-menu {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0.9) 100%);
/* 优化:移除 backdrop-filterGPU 密集型操作 */
border-radius: 24rpx;
padding: 20rpx;
min-width: 400rpx;
/* 优化2层阴影简化为1层 */
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.3);
border: 2rpx solid rgba(255, 255, 255, 0.8);
animation: scaleIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.9) translateY(20rpx);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.menu-item {
display: flex;
align-items: center;
gap: 20rpx;
padding: 28rpx 32rpx;
border-radius: 16rpx;
background: rgba(255, 255, 255, 0.5);
margin-bottom: 12rpx;
/* 优化:只动画必要属性,避免 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);
cursor: pointer;
border: 1rpx solid rgba(123, 104, 238, 0.2);
}
.menu-item:last-child {
margin-bottom: 0;
}
.menu-item:active {
background: linear-gradient(135deg, rgba(123, 104, 238, 0.3) 0%, rgba(123, 104, 238, 0.2) 100%);
transform: scale(0.98);
box-shadow: 0 2rpx 8rpx rgba(123, 104, 238, 0.3);
}
.menu-icon {
width: 40rpx;
height: 40rpx;
filter: drop-shadow(0 2rpx 4rpx rgba(0, 0, 0, 0.2));
}
.menu-text {
font-size: 32rpx;
font-weight: 600;
color: #333333;
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
}
</style>