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

271 lines
6.3 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="banner-top3">
<image class="banner-bg" src="/static/rank/rank-bg.png" mode="aspectFill" />
<image class="banner-bg banner-mask" src="/static/rank/bg-text.png" mode="aspectFill" />
<view
v-for="(item, index) in top3"
:key="item.asset_id || index"
class="top3-card"
>
<view class="artwork-container">
<image class="artwork-image" :src="item.cover_url || '/static/avatar/1.jpeg'" mode="aspectFill" />
<!-- 人气值 -->
<!--<view class="popularity-overlay">
<image class="fire-icon" src="/static/rank/spark.png" mode="aspectFit" />
<text class="popularity-score">{{ formatScore(item.like_count) }}</text>
</view>-->
<!-- 排名徽章 -->
<view class="rank-badge-bottom">
<image class="rank-icon" :src="`/static/rank/charm-rank-icon${index + 1}.png`" mode="aspectFit" />
</view>
<view class="rank-badge-bottom rank-badge-bottom2">
<image class="rank-icon" :src="`/static/rank/rank-icon${index + 1}.png`" mode="aspectFit" />
</view>
</view>
<!-- 用户信息 -->
<view class="user-info-with-artwork">
<image
class="user-avatar-small"
:src="item.avatar_url || '/static/avatar/1.jpeg'"
mode="aspectFit"
/>
<text class="user-nickname">用户 :
<text class="user-nickname-name">{{ item.owner_nickname || '未知用户' }}</text>
</text>
</view>
</view>
<!-- 骨架屏 -->
<view v-if="loading" class="skeleton-wrap">
<view v-for="i in 3" :key="i" class="skeleton-card" />
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { getHotRankingApi, getOssPresignedUrlApi } from '@/utils/api.js';
const top3 = ref([]);
const loading = ref(true);
const formatScore = (score) => {
if (typeof score !== 'number' || isNaN(score) || score < 0) return '0';
if (score >= 1000000) return (score / 1000000).toFixed(1) + 'M';
if (score >= 1000) return (score / 1000).toFixed(1) + 'K';
return score.toString();
};
// 获取 OSS 预签名 URL失败时降级返回原值
const resolveOssUrl = async (fileName, type) => {
if (!fileName) return '';
try {
const res = await getOssPresignedUrlApi(fileName, 3600, type);
if (res?.code === 200 && res.data?.url) return res.data.url;
} catch (e) {
console.warn('[BannerTop3] OSS URL 获取失败', fileName, e?.message);
}
return fileName;
};
const loadTop3 = async () => {
loading.value = true;
try {
const res = await getHotRankingApi('total', null, 1, 3);
if (res.code === 200 && res.data?.items) {
const items = res.data.items.slice(0, 3);
// 并发解析所有封面图和头像的 OSS 预签名 URL
top3.value = await Promise.all(items.map(async (item) => {
const [coverUrl, avatarUrl] = await Promise.all([
resolveOssUrl(item.cover_url || '', 'asset'),
resolveOssUrl(item.avatar_url || '', 'avatar'),
]);
return { ...item, cover_url: coverUrl, avatar_url: avatarUrl };
}));
}
} catch (e) {
console.error('[BannerTop3] 加载失败', e?.message ?? e);
} finally {
loading.value = false;
}
};
onMounted(loadTop3);
defineExpose({ reload: loadTop3 });
</script>
<style scoped>
.banner-top3 {
width: 100%;
display: flex;
flex-direction: row;
align-items: flex-end;
justify-content: space-around;
padding: 0 16rpx;
box-sizing: border-box;
min-height: 200rpx;
position: relative;
overflow: hidden;
border-radius: 24rpx;
}
.banner-bg {
position: absolute;
top: 0;
width: 105%;
height: 100%;
z-index: 0;
}
/* ===== 复用 TOP3Card 样式 ===== */
.top3-card {
width: 25%;
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
/* gap: 32rpx; */
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
margin-bottom: 16rpx;
}
.rank-icon {
width: 100%;
height: 100%;
filter: drop-shadow(0 4rpx 8rpx rgba(0, 0, 0, 0.3));
}
/* 有作品布局 */
.artwork-container {
width: 90%;
height: 208rpx;
margin-top: 16rpx;
margin-bottom: 32rpx;
background-image: url('/static/rank/frames/frame1.png');
background-size: 130% 115%;
background-repeat: no-repeat;
background-position: center;
position: relative;
}
.artwork-image {
width: calc(100% - 16rpx);
height: calc(100% - 24rpx);
top:16rpx;
left: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;
}
.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;
}
.rank-badge-bottom {
position: absolute;
bottom: -40rpx;
left: 50%;
transform: translateX(-50%) scale(1.5);
z-index: 10;
width: 80rpx;
height: 80rpx;
pointer-events: none;
}
.rank-badge-bottom2 {
bottom: -50rpx;
transform: translateX(-50%) scale(2);
}
.user-info-with-artwork {
display: flex;
flex-direction: row;
align-items: center;
/* gap: 8rpx; */
width: 100%;
justify-content: flex-start;
padding: 0 12rpx;
}
.user-avatar-small {
width: 32rpx;
height: 32rpx;
border-radius: 50%;
border: 3rpx solid rgba(255, 255, 255, 0.7);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.3);
flex-shrink: 0;
}
.user-nickname {
font-size: 14rpx;
margin-left: 8rpx;
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;
color: #FFA500;
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;
}
/* 骨架屏 */
.skeleton-wrap {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: space-around;
padding: 0 16rpx;
box-sizing: border-box;
}
.skeleton-card {
width: 25%;
height: 180rpx;
border-radius: 16rpx;
background: linear-gradient(90deg,
rgba(255,255,255,0.06) 25%,
rgba(255,255,255,0.14) 50%,
rgba(255,255,255,0.06) 75%);
background-size: 200% 100%;
animation: shimmer 1.4s infinite;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
</style>