diff --git a/src/assets/scss/global.scss b/src/assets/scss/global.scss index 2c585f5..ca4e76a 100644 --- a/src/assets/scss/global.scss +++ b/src/assets/scss/global.scss @@ -465,7 +465,7 @@ a:hover{ /* src/styles/element-custom.css */ .el-input.is-disabled .el-input__inner { - color: #212121 !important; + color: #8691A8 !important; } @@ -498,3 +498,56 @@ a:hover{ animation: gradient-text-flow 3s ease-in-out infinite; } +/* 点击烟花特效:粒子基础样式 */ +.firework-particle { + position: fixed; + width: 6px; + height: 6px; + border-radius: 50%; + pointer-events: none; + transform: translate(-50%, -50%); + animation: firework-move 700ms ease-out forwards, firework-fade 700ms ease-out forwards; + z-index: 99999; + box-shadow: 0 0 8px rgba(255, 215, 0, 0.9), 0 0 16px rgba(255, 215, 0, 0.6); +} + +@keyframes firework-move { + 0% { + transform: translate(-50%, -50%) translate(0, 0) scale(1); + } + 100% { + transform: translate(-50%, -50%) translate(var(--dx), var(--dy)) scale(0.8); + } +} + +@keyframes firework-fade { + 0% { opacity: 1; } + 100% { opacity: 0; } +} + +///* 路由切换:全屏渐变覆盖效果(与 App.vue 中 transition name="fade" 对应) */ +//.fade-enter-active, .fade-leave-active { +// transition: opacity 300ms ease; +//} +//.fade-enter, .fade-leave-to { +// opacity: 0; +//} +///* 渐变滑动遮罩(进入与离开时均出现一次,时间短、透明) */ +//.fade-enter-active::after, .fade-leave-active::after { +// content: ""; +// position: fixed; +// left: 0; top: 0; width: 100vw; height: 100vh; +// pointer-events: none; +// z-index: 99990; /* 低于烟花(99999),不干扰点击 */ +// background: linear-gradient(120deg, #d94cf3, #ff0000, #fff200, #1360ff); +// background-size: 300% 100%; +// opacity: 0.22; +// animation: route-gradient-swipe 400ms ease forwards; +//} +// +//@keyframes route-gradient-swipe { +// 0% { transform: translateX(-100%); opacity: 0.22; } +// 50% { transform: translateX(0%); opacity: 0.22; } +// 100% { transform: translateX(100%); opacity: 0; } +//} +// diff --git a/src/utils/directives.js b/src/utils/directives.js index 9d62237..1421a8d 100644 --- a/src/utils/directives.js +++ b/src/utils/directives.js @@ -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__; + } + } +}) diff --git a/src/views/main.vue b/src/views/main.vue index 8830d50..7820a3e 100644 --- a/src/views/main.vue +++ b/src/views/main.vue @@ -1,5 +1,5 @@