1.7
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user