271 lines
6.3 KiB
Vue
271 lines
6.3 KiB
Vue
<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>
|