This commit is contained in:
2026-06-14 09:25:59 +08:00
parent a0f441d8ae
commit 6fbf610277
39 changed files with 2492 additions and 2 deletions
+242
View File
@@ -0,0 +1,242 @@
<template>
<div class="flash-layer">
<!-- 萤火虫粒子背景 -->
<transition name="flash-fade">
<div v-if="active" class="flash-overlay">
<span v-for="i in 18" :key="i" class="firefly" :style="fireflyStyle(i)"></span>
</div>
</transition>
<!-- 中央集气面板 -->
<transition name="flash-pop">
<div v-if="active" class="flash-panel">
<span class="flash-close" @click.stop="dismiss"></span>
<div class="flash-tap-area" @click="onTap">
<div class="flash-title"> 萤火虫降临</div>
<div class="flash-progress-ring">
<svg viewBox="0 0 100 100" class="ring-svg">
<circle cx="50" cy="50" r="44" class="ring-bg" />
<circle cx="50" cy="50" r="44" class="ring-fill"
:style="{ strokeDashoffset: ringOffset }" />
</svg>
<div class="ring-center">
<div class="ring-count">{{ progress.total }}</div>
<div class="ring-target">/ {{ active.target_clicks }}</div>
</div>
</div>
<div class="flash-hint">点击集气 · 集满全服掉限定叶 🌟</div>
<div v-if="myClicks > 0" class="my-clicks">你贡献了 {{ myClicks }} </div>
</div>
</div>
</transition>
<!-- 达标庆祝 -->
<transition name="flash-pop">
<div v-if="reward" class="reward-modal" @click.self="reward = null">
<div class="reward-card">
<div class="reward-glow"></div>
<div class="reward-leaf">
<svg viewBox="0 0 100 120" class="reward-svg">
<defs>
<radialGradient id="flash-grad">
<stop offset="0%" :stop-color="leafColor(style, 25)" />
<stop offset="100%" :stop-color="leafColor(style)" />
</radialGradient>
</defs>
<path :d="leafPath(style.shapeVariant)" fill="url(#flash-grad)" />
<g stroke="rgba(255,255,255,0.6)" stroke-width="0.8" fill="none">
<path v-for="(vp, i) in veinPaths(style)" :key="i" :d="vp" />
</g>
<!-- 萤火虫光点 -->
<circle v-for="i in 4" :key="'g'+i" :cx="30 + i*12" :cy="40 + (i%2)*30" r="2" fill="rgba(255,235,100,0.9)" />
</svg>
</div>
<h3>限定纪念叶到手</h3>
<p class="reward-variant">{{ reward.leaf_variant }}</p>
<p class="reward-desc">永不复刻 · 已收入图鉴</p>
<n-button type="primary" round @click="claimReward">收入图鉴</n-button>
</div>
</div>
</transition>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { flashApi } from '@/api/flash'
import { generateLeafStyle, leafColor, leafPath, veinPaths } from '@/utils/leafGenerator'
const active = ref<any>(null)
const reward = ref<any>(null)
const progress = ref<any>({ total: 0 })
const myClicks = ref(0)
const dismissedIds = ref<Set<string>>(new Set()) // 用户已忽略的事件,不再弹回
let pollTimer: ReturnType<typeof setInterval>
let claimTimer: ReturnType<typeof setTimeout>
const style = computed(() => reward.value
? generateLeafStyle(reward.value.leaf_seed)
: generateLeafStyle('0000000000000000'))
const ringOffset = computed(() => {
const p = progress.value.progress || 0
return 276 - 276 * p // 276 = 2π*44
})
function fireflyStyle(i: number): Record<string, string> {
const left = (i * 53) % 100
const top = (i * 37) % 100
const delay = (i % 6) * 0.3
const dur = 2.5 + (i % 4)
return {
left: left + '%',
top: top + '%',
animationDelay: delay + 's',
animationDuration: dur + 's',
}
}
function onFlash(e: Event) {
const data = (e as CustomEvent).detail
if (data._phase === 'flash.spawn') {
if (dismissedIds.value.has(data.id)) return // 已忽略的不再弹
active.value = data
progress.value = { total: data.total_clicks || 0, progress: data.progress || 0, target: data.target_clicks }
} else if (data._phase === 'flash.progress') {
progress.value = data
} else if (data._phase === 'flash.result' && data.reached) {
// 达标:显示奖励并关闭集气面板
reward.value = { ...data, event_id: data.event_id }
active.value = null
}
}
async function onTap() {
if (!active.value) return
try {
const { data } = await flashApi.click(active.value.id)
progress.value = data
myClicks.value = data.my_clicks
// 本地达标也立即关闭面板,触发奖励
if (data.reached && !reward.value) {
reward.value = { event_id: active.value.id, leaf_seed: active.value.leaf_seed, leaf_variant: active.value.leaf_variant }
active.value = null
}
} catch {}
}
function dismiss() {
if (active.value) dismissedIds.value.add(active.value.id)
active.value = null
}
async function claimReward() {
if (!reward.value) return
try {
await flashApi.claim(reward.value.event_id)
reward.value = null
} catch {}
}
// 轮询检查是否有进行中的事件(弥补未在线时错过 spawn 的情况)
async function pollActive() {
// 若当前已有面板或奖励弹窗,先检查是否到期
if (active.value) {
if (new Date(active.value.end_at).getTime() <= Date.now()) {
active.value = null // 事件到期,自动关闭
return
}
}
try {
const { data } = await flashApi.getActive()
if (data.event && !reward.value && !dismissedIds.value.has(data.event.id)) {
active.value = data.event
progress.value = {
total: data.event.total_clicks,
progress: data.event.progress,
target: data.event.target_clicks,
}
}
} catch {}
}
onMounted(() => {
window.addEventListener('qingye:flash', onFlash)
pollActive()
pollTimer = setInterval(pollActive, 20000) // 每 20 秒检查一次(更轻量)
})
onUnmounted(() => {
window.removeEventListener('qingye:flash', onFlash)
clearInterval(pollTimer)
})
</script>
<style scoped>
.flash-layer { position: fixed; inset: 0; pointer-events: none; z-index: 1400; }
/* 萤火虫粒子 */
.flash-overlay { position: fixed; inset: 0; pointer-events: none; }
.firefly {
position: absolute; width: 6px; height: 6px; border-radius: 50%;
background: radial-gradient(circle, #FFEB3B 0%, rgba(255,200,0,0.4) 60%, transparent 100%);
box-shadow: 0 0 8px #FFEB3B;
animation: firefly-float ease-in-out infinite;
}
@keyframes firefly-float {
0%, 100% { transform: translate(0, 0); opacity: 0.3; }
50% { transform: translate(20px, -30px); opacity: 1; }
}
/* 中央面板 */
.flash-panel {
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
background: rgba(20,30,28,0.92); color: white; padding: 24px 32px; border-radius: 24px;
text-align: center; pointer-events: auto;
box-shadow: 0 0 60px rgba(255,235,59,0.4); backdrop-filter: blur(8px);
}
.flash-close {
position: absolute; top: 10px; right: 14px; cursor: pointer;
font-size: 18px; color: rgba(255,255,255,0.6); line-height: 1; z-index: 2;
}
.flash-close:hover { color: white; }
.flash-tap-area { cursor: pointer; }
.flash-title { font-size: 16px; font-weight: 700; margin-bottom: 12px; color: #FFEB3B; }
.flash-progress-ring { position: relative; width: 140px; height: 140px; margin: 0 auto 12px; }
.ring-svg { width: 100%; height: 100%; transform: rotate(-90deg); }
.ring-bg { fill: none; stroke: rgba(255,255,255,0.15); stroke-width: 6; }
.ring-fill { fill: none; stroke: #FFEB3B; stroke-width: 6; stroke-linecap: round; stroke-dasharray: 276; transition: stroke-dashoffset 0.3s; filter: drop-shadow(0 0 4px #FFEB3B); }
.ring-center { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; }
.ring-count { font-size: 32px; font-weight: 800; color: white; }
.ring-target { font-size: 12px; color: rgba(255,255,255,0.6); }
.flash-hint { font-size: 12px; color: rgba(255,255,255,0.7); }
.my-clicks { font-size: 11px; color: #FFEB3B; margin-top: 6px; }
/* 达标奖励 */
.reward-modal {
position: fixed; inset: 0; background: rgba(0,0,0,0.7); pointer-events: auto;
display: flex; align-items: center; justify-content: center; z-index: 1600;
}
.reward-card {
background: var(--color-surface); padding: 32px; border-radius: 24px; text-align: center;
position: relative; overflow: hidden; max-width: 320px;
}
.reward-glow {
position: absolute; inset: 0; background: radial-gradient(circle at 50% 30%, rgba(255,235,59,0.3), transparent 60%);
animation: glow 2s ease-in-out infinite;
}
@keyframes glow { 0%,100% { opacity: 0.6; } 50% { opacity: 1; } }
.reward-leaf { position: relative; margin: 0 auto 16px; width: 120px; height: 144px; filter: drop-shadow(0 0 20px rgba(255,200,0,0.6)); }
.reward-svg { width: 100%; height: 100%; animation: reward-bloom 0.8s ease; }
@keyframes reward-bloom { 0% { transform: scale(0.3) rotate(-180deg); opacity: 0; } 100% { transform: scale(1) rotate(0); opacity: 1; } }
.reward-card h3 { margin: 0 0 4px; font-size: 18px; color: var(--color-primary-darker); position: relative; }
.reward-variant { font-size: 12px; color: var(--color-text-hint); margin: 4px 0; font-family: monospace; position: relative; }
.reward-desc { font-size: 13px; color: var(--color-text-secondary); margin: 4px 0 16px; position: relative; }
/* transitions */
.flash-fade-enter-active, .flash-fade-leave-active { transition: opacity 0.5s; }
.flash-fade-enter-from, .flash-fade-leave-to { opacity: 0; }
.flash-pop-enter-active { animation: pop-in 0.4s ease; }
.flash-pop-leave-active { transition: opacity 0.3s; }
.flash-pop-leave-to { opacity: 0; }
@keyframes pop-in { 0% { transform: translate(-50%, -50%) scale(0.7); opacity: 0; } 100% { transform: translate(-50%, -50%) scale(1); opacity: 1; } }
</style>