272 lines
7.1 KiB
Vue
272 lines
7.1 KiB
Vue
<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"
|
||
:class="{ [`cover-image-${i}`]: true }"
|
||
: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×72,top-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: 18%;
|
||
left: 18%;
|
||
/* 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: 136rpx;
|
||
height: 168rpx; /* 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;
|
||
padding: 8rpx 0 16rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.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;
|
||
padding: 27.2rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.cover-image-0{
|
||
padding: 32rpx;
|
||
}
|
||
|
||
.base-image {
|
||
position: absolute;
|
||
bottom: -8rpx;
|
||
left: 16rpx;
|
||
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);
|
||
z-index: 9;
|
||
}
|
||
11.11% {
|
||
transform: translate(84px, -13px) scale(1.05);
|
||
z-index: 8;
|
||
}
|
||
22.22% {
|
||
transform: translate(157px, -43px) scale(0.95);
|
||
z-index: 7;
|
||
}
|
||
33.33% {
|
||
transform: translate(113px, -83px) scale(0.85);
|
||
z-index: 5;
|
||
}
|
||
44.44% {
|
||
transform: translate(45px, -107px) scale(0.75);
|
||
z-index: 3;
|
||
}
|
||
55.55% {
|
||
transform: translate(-45px, -107px) scale(0.75);
|
||
z-index: 3;
|
||
}
|
||
66.66% {
|
||
transform: translate(-113px, -83px) scale(0.85);
|
||
z-index: 5;
|
||
}
|
||
77.77% {
|
||
transform: translate(-156px, -43px) scale(0.95);
|
||
z-index: 7;
|
||
}
|
||
88.88% {
|
||
transform: translate(-84px, -13px) scale(1.05);
|
||
z-index: 8;
|
||
}
|
||
100% {
|
||
transform: translate(0, 0) scale(1.15);
|
||
z-index: 9;
|
||
}
|
||
}
|
||
|
||
@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>
|