|
|
|
@ -78,3 +78,227 @@ Vue.directive('drag', { |
|
|
|
} |
|
|
|
} |
|
|
|
}) |
|
|
|
//---------------以下都可删---------------------
|
|
|
|
// v-fireworks: 容器内按钮点击烟花特效(全局可复用)
|
|
|
|
Vue.directive('fireworks', { |
|
|
|
bind(el, binding) { |
|
|
|
const colors = ['#FFD700', '#DAA520', '#B8860B', '#C9A227', '#E6B800']; |
|
|
|
const getSkyConfig = () => { |
|
|
|
let type = 'rain'; |
|
|
|
let enabled = true; |
|
|
|
const val = binding && binding.value; |
|
|
|
if (typeof val === 'string') { |
|
|
|
if (val === 'rain' || val === 'meteor') type = val; |
|
|
|
if (val === 'none') enabled = false; |
|
|
|
} else if (typeof val === 'object' && val) { |
|
|
|
if (val.sky === 'rain' || val.sky === 'meteor') type = val.sky; |
|
|
|
if (val.sky === 'none') enabled = false; |
|
|
|
if (typeof val.skyEnabled === 'boolean') enabled = val.skyEnabled; |
|
|
|
if (typeof val.enabled === 'boolean') enabled = val.enabled; |
|
|
|
} else if (val === false) { |
|
|
|
enabled = false; |
|
|
|
} |
|
|
|
const ds = el.getAttribute && el.getAttribute('data-sky'); |
|
|
|
if (ds === 'rain' || ds === 'meteor') type = ds; |
|
|
|
if (ds === 'none') enabled = false; |
|
|
|
const dse = el.getAttribute && el.getAttribute('data-sky-enabled'); |
|
|
|
if (dse === 'false' || dse === '0') enabled = false; |
|
|
|
if (dse === 'true' || dse === '1') enabled = true; |
|
|
|
return { type, enabled }; |
|
|
|
} |
|
|
|
|
|
|
|
// ===== 全屏天空特效(雨/流星) =====
|
|
|
|
let skyCanvas = null; |
|
|
|
let skyCtx = null; |
|
|
|
let skyRaf = null; |
|
|
|
let skyTimer = null; |
|
|
|
let skyDelayTimer = null; |
|
|
|
let skyParticles = []; |
|
|
|
|
|
|
|
const ensureSkyCanvas = () => { |
|
|
|
if (skyCanvas) return; |
|
|
|
skyCanvas = document.createElement('canvas'); |
|
|
|
skyCanvas.className = 'sky-effect-canvas'; |
|
|
|
skyCanvas.style.position = 'fixed'; |
|
|
|
skyCanvas.style.left = '0'; |
|
|
|
skyCanvas.style.top = '0'; |
|
|
|
skyCanvas.style.width = '100vw'; |
|
|
|
skyCanvas.style.height = '100vh'; |
|
|
|
skyCanvas.style.pointerEvents = 'none'; |
|
|
|
skyCanvas.style.zIndex = '99998'; |
|
|
|
document.body.appendChild(skyCanvas); |
|
|
|
skyCtx = skyCanvas.getContext('2d'); |
|
|
|
const resize = () => { |
|
|
|
skyCanvas.width = window.innerWidth; |
|
|
|
skyCanvas.height = window.innerHeight; |
|
|
|
}; |
|
|
|
resize(); |
|
|
|
window.addEventListener('resize', resize); |
|
|
|
skyCanvas.__resize__ = resize; |
|
|
|
}; |
|
|
|
|
|
|
|
const clearSky = () => { |
|
|
|
if (skyRaf) cancelAnimationFrame(skyRaf); |
|
|
|
skyRaf = null; |
|
|
|
if (skyTimer) clearTimeout(skyTimer); |
|
|
|
skyTimer = null; |
|
|
|
if (skyDelayTimer) clearTimeout(skyDelayTimer); |
|
|
|
skyDelayTimer = null; |
|
|
|
if (skyCanvas) { |
|
|
|
window.removeEventListener('resize', skyCanvas.__resize__); |
|
|
|
if (skyCanvas.parentNode) skyCanvas.parentNode.removeChild(skyCanvas); |
|
|
|
} |
|
|
|
skyCanvas = null; |
|
|
|
skyCtx = null; |
|
|
|
skyParticles = []; |
|
|
|
}; |
|
|
|
|
|
|
|
const startRain = (duration = 3000) => { |
|
|
|
ensureSkyCanvas(); |
|
|
|
const count = Math.min(140, Math.floor((window.innerWidth * window.innerHeight) / 12000)); |
|
|
|
skyParticles = new Array(count).fill(0).map(() => ({ |
|
|
|
x: Math.random() * skyCanvas.width, |
|
|
|
y: Math.random() * skyCanvas.height, |
|
|
|
len: 10 + Math.random() * 14, |
|
|
|
speed: 3 + Math.random() * 6, |
|
|
|
wind: 1 + Math.random() * 2 |
|
|
|
})); |
|
|
|
const stroke = 'rgba(255, 215, 0, 0.8)'; |
|
|
|
const draw = () => { |
|
|
|
if (!skyCtx) return; |
|
|
|
skyCtx.clearRect(0, 0, skyCanvas.width, skyCanvas.height); |
|
|
|
skyCtx.strokeStyle = stroke; |
|
|
|
skyCtx.lineWidth = 1.2; |
|
|
|
skyCtx.lineCap = 'round'; |
|
|
|
for (const p of skyParticles) { |
|
|
|
skyCtx.beginPath(); |
|
|
|
skyCtx.moveTo(p.x, p.y); |
|
|
|
skyCtx.lineTo(p.x + p.wind, p.y + p.len); |
|
|
|
skyCtx.stroke(); |
|
|
|
p.x += p.wind; |
|
|
|
p.y += p.speed; |
|
|
|
if (p.y > skyCanvas.height || p.x > skyCanvas.width) { |
|
|
|
p.x = Math.random() * skyCanvas.width; |
|
|
|
p.y = -20; |
|
|
|
} |
|
|
|
} |
|
|
|
skyRaf = requestAnimationFrame(draw); |
|
|
|
}; |
|
|
|
draw(); |
|
|
|
skyTimer = setTimeout(clearSky, duration); |
|
|
|
}; |
|
|
|
|
|
|
|
const startMeteor = (duration = 3000) => { |
|
|
|
ensureSkyCanvas(); |
|
|
|
const count = 8; |
|
|
|
skyParticles = new Array(count).fill(0).map(() => newMeteor()); |
|
|
|
function newMeteor() { |
|
|
|
const startX = Math.random() * skyCanvas.width; |
|
|
|
const startY = Math.random() * (skyCanvas.height * 0.3); |
|
|
|
const speed = 6 + Math.random() * 6; |
|
|
|
const angle = Math.PI / 3; // 60° 斜向
|
|
|
|
const vx = Math.cos(angle) * speed; |
|
|
|
const vy = Math.sin(angle) * speed; |
|
|
|
const len = 120 + Math.random() * 120; |
|
|
|
const life = 40 + Math.random() * 40; |
|
|
|
return { x: startX, y: startY, vx, vy, len, life, maxLife: life }; |
|
|
|
} |
|
|
|
const draw = () => { |
|
|
|
if (!skyCtx) return; |
|
|
|
skyCtx.clearRect(0, 0, skyCanvas.width, skyCanvas.height); |
|
|
|
for (let i = 0; i < skyParticles.length; i++) { |
|
|
|
const m = skyParticles[i]; |
|
|
|
const grad = skyCtx.createLinearGradient(m.x, m.y, m.x - m.vx * 10, m.y - m.vy * 10); |
|
|
|
grad.addColorStop(0, 'rgba(255,215,0,0.9)'); |
|
|
|
grad.addColorStop(1, 'rgba(255,215,0,0.0)'); |
|
|
|
skyCtx.strokeStyle = grad; |
|
|
|
skyCtx.lineWidth = 2; |
|
|
|
skyCtx.beginPath(); |
|
|
|
skyCtx.moveTo(m.x, m.y); |
|
|
|
skyCtx.lineTo(m.x - (m.vx * (m.len / 10)), m.y - (m.vy * (m.len / 10))); |
|
|
|
skyCtx.stroke(); |
|
|
|
m.x += m.vx; |
|
|
|
m.y += m.vy; |
|
|
|
m.life -= 1; |
|
|
|
if (m.life <= 0 || m.x > skyCanvas.width + 100 || m.y > skyCanvas.height + 100) { |
|
|
|
skyParticles[i] = newMeteor(); |
|
|
|
} |
|
|
|
} |
|
|
|
skyRaf = requestAnimationFrame(draw); |
|
|
|
}; |
|
|
|
draw(); |
|
|
|
skyTimer = setTimeout(clearSky, duration); |
|
|
|
}; |
|
|
|
|
|
|
|
const startSkyEffect = () => { |
|
|
|
const cfg = getSkyConfig(); |
|
|
|
if (!cfg.enabled) return; |
|
|
|
// 若已有画布或定时器,刷新计时即可,避免叠加
|
|
|
|
if (skyTimer) { |
|
|
|
clearTimeout(skyTimer); |
|
|
|
skyTimer = null; |
|
|
|
} |
|
|
|
if (skyDelayTimer) { |
|
|
|
clearTimeout(skyDelayTimer); |
|
|
|
skyDelayTimer = null; |
|
|
|
} |
|
|
|
if (cfg.type === 'meteor') { |
|
|
|
startMeteor(3500); |
|
|
|
} else { |
|
|
|
// 雨滴延迟1秒出现,持续时间为1秒
|
|
|
|
skyDelayTimer = setTimeout(() => { |
|
|
|
startRain(1000); |
|
|
|
}, 1000); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
const isButtonLike = (node) => { |
|
|
|
if (!node) return false; |
|
|
|
if (node.closest) { |
|
|
|
const match = node.closest('button, .el-button, [role="button"], input[type="button"], input[type="submit"], a.el-button, a'); |
|
|
|
return !!match; |
|
|
|
} |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
const spawnFireworks = (x, y) => { |
|
|
|
const count = 14; |
|
|
|
for (let i = 0; i < count; i++) { |
|
|
|
const particle = document.createElement('span'); |
|
|
|
particle.className = 'firework-particle'; |
|
|
|
const angle = (Math.PI * 2 * i) / count + Math.random() * 0.3; |
|
|
|
const distance = 60 + Math.random() * 60; |
|
|
|
const dx = Math.cos(angle) * distance; |
|
|
|
const dy = Math.sin(angle) * distance; |
|
|
|
particle.style.left = x + 'px'; |
|
|
|
particle.style.top = y + 'px'; |
|
|
|
particle.style.background = colors[i % colors.length]; |
|
|
|
particle.style.setProperty('--dx', dx + 'px'); |
|
|
|
particle.style.setProperty('--dy', dy + 'px'); |
|
|
|
document.body.appendChild(particle); |
|
|
|
setTimeout(() => { |
|
|
|
if (particle && particle.parentNode) { |
|
|
|
particle.parentNode.removeChild(particle); |
|
|
|
} |
|
|
|
}, 700); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
const handler = (e) => { |
|
|
|
if (!isButtonLike(e.target)) return; |
|
|
|
const x = e.clientX; |
|
|
|
const y = e.clientY; |
|
|
|
spawnFireworks(x, y); |
|
|
|
startSkyEffect(); |
|
|
|
}; |
|
|
|
|
|
|
|
el.__fireworksHandler__ = handler; |
|
|
|
el.addEventListener('click', handler, false); |
|
|
|
}, |
|
|
|
unbind(el) { |
|
|
|
if (el.__fireworksHandler__) { |
|
|
|
el.removeEventListener('click', el.__fireworksHandler__, false); |
|
|
|
delete el.__fireworksHandler__; |
|
|
|
} |
|
|
|
} |
|
|
|
}) |