topfans/frontend/pages/square/components/StarGalaxy/ScatteredRanks.vue

253 lines
6.8 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="scattered-ranks">
<!-- 椭圆轨道装饰虚线 -->
<!-- <svg class="orbit-svg" viewBox="0 0 375 170" preserveAspectRatio="none">
<defs>
<linearGradient id="sg-orbit-grad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="rgba(255,255,255,0.15)" />
<stop offset="50%" stop-color="rgba(255,255,255,0.4)" />
<stop offset="100%" stop-color="rgba(255,250,189,0.85)" />
</linearGradient>
<radialGradient id="sg-center-glow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="rgba(255,250,189,0.5)" />
<stop offset="100%" stop-color="transparent" />
</radialGradient>
</defs>
<ellipse cx="187" cy="55" rx="80" ry="35" fill="url(#sg-center-glow)" />
<ellipse cx="187" cy="55" rx="130" ry="55" fill="none" stroke="url(#sg-orbit-grad)" stroke-width="1.5" stroke-dasharray="3,3" />
<path d="M 57,55 A 130,55 0 0,0 317,55" stroke="rgba(255,250,189,0.85)" stroke-width="2.5" fill="none" />
</svg> -->
<!-- 9 个散落 item -->
<view
v-for="(p, i) in positions"
:key="p.rank"
class="ring-item"
:class="'r' + (p.rank - 4)"
:style="ringItemStyle(p)"
@click="handleClick(items[i])"
>
<!-- 相框(中间层) -->
<view class="top-label">{{ formatLabel(p.rank) }}</view>
<image class="ring-frame" :src="ringFrameSrc(p.rank)" mode="aspectFit" />
<image
class="cover-image"
:src="items[i]?.cover_url || items[i]?.cover_image || ''"
mode="aspectFill"
/>
<!-- 底座最底层 -->
<image
v-if="baseSrc(p.rank)"
class="base-image"
:src="baseSrc(p.rank)"
mode="aspectFit"
/>
</view>
</view>
</template>
<script setup>
// Note: @keyframes orbit is inlined below (not imported from config.js ORBIT_KEYFRAMES)
// because Vue <style> blocks cannot interpolate JS string constants.
// config.js ORBIT_KEYFRAMES is kept as documentation/source-of-truth.
import { RING_DELAYS } from "./config.js";
const props = defineProps({
items: { type: Array, required: true }, // length 9
positions: { type: Array, required: true }, // from generateRingPositions()
});
const emit = defineEmits(["cardClick"]);
// 静态 base 位置slot 0 中心 (187, 565)item 46×72top-left = (164, 530)
const BASE_X = 164;
const BASE_Y = 530;
// Each item's static fallback position = position at its slot in the orbit keyframe cycle.
// When animation runs, the keyframe overrides these. When animation doesn't run (e.g. prefers-reduced-motion),
// items stay at their slot positions instead of stacking at (BASE_X, BASE_Y).
const SLOT_TRANSFORMS = [
"translate(0, 0) scale(1.15)", // slot 0: TOP 4 (front center)
"translate(84px, -13px) scale(1.05)", // slot 1: TOP 5
"translate(157px, -43px) scale(0.95)", // slot 2: TOP 6
"translate(113px, -83px) scale(0.85)", // slot 3: TOP 7
"translate(45px, -107px) scale(0.75)", // slot 4: TOP 8
"translate(-45px, -107px) scale(0.75)", // slot 5: TOP 9
"translate(-113px, -83px) scale(0.85)", // slot 6: TOP 10
"translate(-156px, -43px) scale(0.95)", // slot 7: TOP 11
"translate(-84px, -13px) scale(1.05)", // slot 8: TOP 12
];
function ringItemStyle(p) {
return {
left: BASE_X + "rpx",
top: BASE_Y + "rpx",
zIndex: p.zIndex,
transform: SLOT_TRANSFORMS[p.rank - 4],
animationDelay: RING_DELAYS[p.rank - 4] + "s",
};
}
function formatLabel(rank) {
return "TOP " + rank;
}
function ringFrameSrc(rank) {
// rank 4 → LV4 (TOP 4), rank 5 → LV5 (TOP 5), ..., rank 12 → LV12 (TOP 12)
// ScatteredRanks rank == display rank (no offset, unlike PodiumCard which uses rank - 3)
return `/static/square/galaxy/LV${rank}.png`;
}
function baseSrc(rank) {
// 不同名次区间使用不同的底座4-6 → dizuo1, 7-9 → dizuo2, 10-12 → dizuo3
if (rank >= 4 && rank <= 6) return "/static/square/galaxy/dizuo1.png";
if (rank >= 7 && rank <= 9) return "/static/square/galaxy/dizuo2.png";
if (rank >= 10 && rank <= 12) return "/static/square/galaxy/dizuo3.png";
return "";
}
function handleClick(item) {
if (item) emit("cardClick", item);
}
</script>
<style scoped>
.scattered-ranks {
position: absolute;
top: 22%;
left: 22%;
width: 750rpx;
height: 720rpx;
pointer-events: none;
}
.orbit-svg {
position: absolute;
top: 390rpx;
left: 0;
width: 750rpx;
height: 340rpx;
pointer-events: none;
z-index: 0;
}
.ring-item {
position: absolute;
width: 84rpx;
height: 104rpx; /* 14 label + 2 gap + 56 cover */
transform-origin: center;
pointer-events: auto;
cursor: pointer;
/* display: flex; */
/* flex-direction: column; */
will-change: transform;
animation: orbit 36s linear infinite;
}
.ring-frame {
position: absolute;
inset: 0; /* extend slightly outside the item bounds */
width: 100%;
height: 100%;
z-index: 2;
pointer-events: none;
}
.top-label {
width: 80rpx;
position: relative;
bottom: 32rpx;
left: 50%;
transform: translateX(-50%);
color: #fffabd;
font-size: 20rpx;
font-weight: 800;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
text-shadow: -1px 1px 4px #ce0909d6;
background: linear-gradient(
93.1deg,
rgba(224, 180, 247, 0.71) -12.06%,
rgba(178, 246, 204, 0.71) 52.09%,
rgba(98, 178, 244, 0.71) 163.5%
);
backdrop-filter: blur(11.699999809265137px);
z-index: 7;
}
.cover-image {
width: 100%;
height: 100%;
border-radius: 5rpx;
z-index: 1; /* between frame and label */
position: absolute;
top: 0;
}
.base-image {
position: absolute;
bottom: -24rpx;
left: -8rpx;
width: 96rpx; /* 比 cover (84rpx) 略宽,呈现"承托"感 */
height: 32rpx;
z-index: 0; /* 位于 cover 之下 */
pointer-events: none;
}
</style>
<style>
/* 关键帧:放在非 scoped 块中,让所有 ring-item 共享 */
@keyframes orbit {
0% {
transform: translate(0, 0) scale(1.15);
}
11.11% {
transform: translate(84px, -13px) scale(1.05);
}
22.22% {
transform: translate(157px, -43px) scale(0.95);
}
33.33% {
transform: translate(113px, -83px) scale(0.85);
}
44.44% {
transform: translate(45px, -107px) scale(0.75);
}
55.55% {
transform: translate(-45px, -107px) scale(0.75);
}
66.66% {
transform: translate(-113px, -83px) scale(0.85);
}
77.77% {
transform: translate(-156px, -43px) scale(0.95);
}
88.88% {
transform: translate(-84px, -13px) scale(1.05);
}
100% {
transform: translate(0, 0) scale(1.15);
}
}
@keyframes crownPulse {
0%,
100% {
transform: translateX(-50%) scale(1);
}
50% {
transform: translateX(-50%) scale(1.15);
}
}
/* 可访问性:减少动画 */
/* @media (prefers-reduced-motion: reduce) {
.ring-item,
.crown {
animation: none !important;
}
} */
</style>