310 lines
6.7 KiB
Vue
310 lines
6.7 KiB
Vue
<template>
|
||
<view class="stargalaxy-container">
|
||
<!-- 装饰层:粉红渐变 overlay + 樱花粉光晕 + 暖黄光晕 -->
|
||
<!-- <view class="decoration-layer">
|
||
<view class="halo-pink"></view>
|
||
<view class="halo-yellow"></view>
|
||
</view> -->
|
||
|
||
<!-- 标题 -->
|
||
<!-- <view class="title">★ 星河 ★</view> -->
|
||
|
||
<!-- Loading 骨架 -->
|
||
<view v-if="loading" class="skeleton-grid">
|
||
<view v-for="i in 3" :key="'p' + i" class="skeleton-podium"></view>
|
||
<view v-for="i in 9" :key="'s' + i" class="skeleton-ring"></view>
|
||
</view>
|
||
|
||
<!-- Error -->
|
||
<view v-else-if="error" class="error-state">
|
||
<text class="error-text">加载失败,点击重试</text>
|
||
<view class="retry-btn" @click="loadData">重试</view>
|
||
</view>
|
||
|
||
<!-- Empty:数据为空 -->
|
||
<view v-else-if="items.length === 0" class="empty-state">
|
||
<text class="empty-text">暂无星河数据</text>
|
||
</view>
|
||
|
||
<!-- Success -->
|
||
<template v-else>
|
||
<!-- 颁奖台:TOP 1-3 -->
|
||
<!-- <view class="podium-row"> -->
|
||
<PodiumCard
|
||
v-for="(item, i) in podiumItems"
|
||
:key="item.id || i"
|
||
:item="item"
|
||
:rank="i + 1"
|
||
@cardClick="handleCardClick"
|
||
/>
|
||
<!-- </view> -->
|
||
|
||
<!-- 散落 9 item:TOP 4-12 -->
|
||
<ScatteredRanks
|
||
:items="scatteredItems"
|
||
:positions="ringPositions"
|
||
@cardClick="handleCardClick"
|
||
/>
|
||
|
||
<!-- 底部提示 -->
|
||
<!-- <view class="footer-hint">每日 0:00 更新榜单</view> -->
|
||
</template>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted, onUnmounted } from "vue";
|
||
import PodiumCard from "./PodiumCard.vue";
|
||
import ScatteredRanks from "./ScatteredRanks.vue";
|
||
import { generateRingPositions } from "./config.js";
|
||
import { getHotRankingApi } from "@/utils/api.js";
|
||
import { getAssetCoverRealUrl } from "@/utils/assetImageHelper.js";
|
||
|
||
const emit = defineEmits(["cardClick"]);
|
||
|
||
const items = ref([]);
|
||
const loading = ref(true);
|
||
const error = ref(false);
|
||
const ringPositions = generateRingPositions();
|
||
|
||
const podiumItems = computed(() => items.value.slice(0, 3));
|
||
const scatteredItems = computed(() => items.value.slice(3, 12));
|
||
|
||
async function resolveUrl(item) {
|
||
const cover = item.cover_url || item.cover_image || "";
|
||
if (cover) {
|
||
item.cover_url = await getAssetCoverRealUrl(cover);
|
||
}
|
||
return item;
|
||
}
|
||
|
||
async function loadData() {
|
||
loading.value = true;
|
||
error.value = false;
|
||
try {
|
||
const res = await getHotRankingApi("displaying", null, 1, 12);
|
||
if (res && res.code === 200 && res.data?.items) {
|
||
items.value = await Promise.all(
|
||
res.data.items.map(async (item) => {
|
||
return await resolveUrl({ ...item, id: item.id || item.asset_id });
|
||
}),
|
||
);
|
||
} else {
|
||
items.value = [];
|
||
}
|
||
} catch (e) {
|
||
console.error("[StarGalaxy] 加载失败", e?.message ?? e);
|
||
error.value = true;
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
function handleCardClick(item) {
|
||
emit("cardClick", item);
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadData();
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
// 清理(如有 timer)
|
||
});
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.stargalaxy-container {
|
||
position: relative;
|
||
width: 750rpx;
|
||
min-height: 1440rpx;
|
||
padding-bottom: 200rpx;
|
||
top: -128rpx;
|
||
// [方案3] 伪元素承载 bj.png,对图片单独设 opacity
|
||
&::before {
|
||
content: "";
|
||
position: absolute;
|
||
inset: 0;
|
||
background: url("/static/square/galaxy/bj.png") center no-repeat;
|
||
background-size: 115% 100%;
|
||
pointer-events: none;
|
||
z-index: 0;
|
||
bottom: 512rpx;
|
||
// 顶部渐隐蒙版:让 StarGalaxy 顶部 20% 渐变到透明,
|
||
// 上方 header/页面 bj.png 从顶部自然透出,形成柔和衔接
|
||
// 700rpx 中 0-20%(0-140rpx) 完全透明,20-40%(140-280rpx) 渐变到不透明
|
||
mask-image: linear-gradient(
|
||
to bottom,
|
||
transparent 0%,
|
||
transparent 0%,
|
||
#000 14%,
|
||
#000 100%
|
||
);
|
||
-webkit-mask-image: linear-gradient(
|
||
to bottom,
|
||
transparent 0%,
|
||
transparent 0%,
|
||
#000 14%,
|
||
#000 100%
|
||
);
|
||
}
|
||
}
|
||
|
||
.decoration-layer {
|
||
position: absolute;
|
||
inset: 0;
|
||
pointer-events: none;
|
||
z-index: 0;
|
||
background: linear-gradient(
|
||
179deg,
|
||
#ffe5e5 0%,
|
||
#f3a0a1 0%,
|
||
#ff9c9c 86%,
|
||
#ff2024 100%
|
||
);
|
||
opacity: 0.85;
|
||
}
|
||
|
||
.halo-pink {
|
||
position: absolute;
|
||
top: 580rpx;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
width: 680rpx;
|
||
height: 340rpx;
|
||
background: #f3d3e3;
|
||
border-radius: 50%;
|
||
filter: blur(60rpx);
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.halo-yellow {
|
||
position: absolute;
|
||
top: 100rpx;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
width: 400rpx;
|
||
height: 400rpx;
|
||
background: #fffabd;
|
||
border-radius: 50%;
|
||
filter: blur(50rpx);
|
||
opacity: 0.3;
|
||
}
|
||
|
||
.title {
|
||
position: absolute;
|
||
top: 28rpx;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
font-size: 40rpx;
|
||
font-weight: 900;
|
||
color: #fffabd;
|
||
text-shadow: -1rpx 1rpx 4rpx rgba(206, 9, 9, 0.84);
|
||
letter-spacing: 8rpx;
|
||
z-index: 5;
|
||
}
|
||
|
||
.podium-row {
|
||
position: relative;
|
||
z-index: 2;
|
||
}
|
||
|
||
.skeleton-grid {
|
||
position: relative;
|
||
z-index: 2;
|
||
padding: 200rpx 60rpx;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 40rpx;
|
||
justify-content: center;
|
||
}
|
||
|
||
.skeleton-podium {
|
||
width: 200rpx;
|
||
height: 200rpx;
|
||
background: rgba(255, 255, 255, 0.15);
|
||
border-radius: 12rpx;
|
||
animation: shimmer 1.5s linear infinite;
|
||
background-image: linear-gradient(
|
||
90deg,
|
||
rgba(255, 255, 255, 0.05) 0%,
|
||
rgba(255, 255, 255, 0.25) 50%,
|
||
rgba(255, 255, 255, 0.05) 100%
|
||
);
|
||
background-size: 200% 100%;
|
||
}
|
||
|
||
.skeleton-ring {
|
||
width: 60rpx;
|
||
height: 80rpx;
|
||
background: rgba(255, 255, 255, 0.15);
|
||
border-radius: 8rpx;
|
||
background-image: linear-gradient(
|
||
90deg,
|
||
rgba(255, 255, 255, 0.05) 0%,
|
||
rgba(255, 255, 255, 0.25) 50%,
|
||
rgba(255, 255, 255, 0.05) 100%
|
||
);
|
||
background-size: 200% 100%;
|
||
animation: shimmer 1.5s linear infinite;
|
||
}
|
||
|
||
@keyframes shimmer {
|
||
0% {
|
||
background-position: 200% 0;
|
||
}
|
||
100% {
|
||
background-position: -200% 0;
|
||
}
|
||
}
|
||
|
||
.error-state {
|
||
position: relative;
|
||
z-index: 2;
|
||
padding: 600rpx 0 0;
|
||
text-align: center;
|
||
}
|
||
|
||
.error-text {
|
||
display: block;
|
||
font-size: 28rpx;
|
||
color: #fff;
|
||
margin-bottom: 30rpx;
|
||
}
|
||
|
||
.empty-state {
|
||
position: relative;
|
||
z-index: 2;
|
||
padding: 600rpx 0 0;
|
||
text-align: center;
|
||
}
|
||
|
||
.empty-text {
|
||
display: block;
|
||
font-size: 28rpx;
|
||
color: rgba(255, 255, 255, 0.85);
|
||
text-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.retry-btn {
|
||
display: inline-block;
|
||
padding: 16rpx 60rpx;
|
||
background: linear-gradient(135deg, #ffd700, #ff6b6b);
|
||
color: #fff;
|
||
border-radius: 30rpx;
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.footer-hint {
|
||
position: absolute;
|
||
bottom: 16rpx;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
font-size: 20rpx;
|
||
color: rgba(255, 255, 255, 0.7);
|
||
text-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.3);
|
||
z-index: 3;
|
||
}
|
||
</style>
|