diff --git a/src/main.js b/src/main.js index 073463b..1316faf 100644 --- a/src/main.js +++ b/src/main.js @@ -18,6 +18,7 @@ import decimalUtil from '@/utils/decimalUtil.js' import getLodop from '@/utils/LodopFuncs.js' import pdf from 'vue-pdf' import { debounce,throttle} from '@/utils/common.js' +import { installClickBloom } from '@/utils/clickBloom' Vue.component('downloadExcel', JsonExcel) Vue.component('pdf', pdf) @@ -40,6 +41,7 @@ Vue.prototype.throttle = throttle // 限流 // 保存整站vuex本地储存初始状态 window.SITE_CONFIG['storeState'] = cloneDeep(store.state) +installClickBloom(store) /* eslint-disable no-new */ new Vue({ diff --git a/src/store/modules/common.js b/src/store/modules/common.js index 1903bb4..dd98b5d 100644 --- a/src/store/modules/common.js +++ b/src/store/modules/common.js @@ -16,7 +16,8 @@ export default { contentIsNeedRefresh: false, // 主入口标签页 mainTabs: [], - mainTabsActiveName: '' + mainTabsActiveName: '', + clickEffectEnabled: false }, mutations: { updateDocumentClientHeight (state, height) { @@ -45,6 +46,9 @@ export default { }, updateMainTabsActiveName (state, name) { state.mainTabsActiveName = name + }, + updateClickEffectEnabled (state, enabled) { + state.clickEffectEnabled = !!enabled } } } diff --git a/src/utils/clickBloom.js b/src/utils/clickBloom.js new file mode 100644 index 0000000..0a15cad --- /dev/null +++ b/src/utils/clickBloom.js @@ -0,0 +1,334 @@ +const EFFECT_MS = 2500 +const MAX_BURSTS = 4 + +let canvas = null +let ctx = null +let bursts = [] +let frameId = 0 + +function ensureCanvas () { + if (canvas) { + return + } + canvas = document.createElement('canvas') + canvas.style.position = 'fixed' + canvas.style.left = '0' + canvas.style.top = '0' + canvas.style.width = '100%' + canvas.style.height = '100%' + canvas.style.pointerEvents = 'none' + canvas.style.zIndex = '10000' + document.body.appendChild(canvas) + ctx = canvas.getContext('2d') + resizeCanvas() + window.addEventListener('resize', resizeCanvas) +} + +function resizeCanvas () { + if (!canvas) { + return + } + const ratio = window.devicePixelRatio || 1 + canvas.width = Math.floor(window.innerWidth * ratio) + canvas.height = Math.floor(window.innerHeight * ratio) + if (ctx) { + ctx.setTransform(ratio, 0, 0, ratio, 0, 0) + } +} + +function removeCanvas () { + if (frameId) { + cancelAnimationFrame(frameId) + frameId = 0 + } + window.removeEventListener('resize', resizeCanvas) + if (canvas && canvas.parentNode) { + canvas.parentNode.removeChild(canvas) + } + canvas = null + ctx = null + bursts = [] +} + +function farthestScreenDistance (x, y) { + const width = window.innerWidth + const height = window.innerHeight + const corners = [[0, 0], [width, 0], [0, height], [width, height]] + let maxDistance = 0 + corners.forEach(function (corner) { + const dx = corner[0] - x + const dy = corner[1] - y + const distance = Math.sqrt(dx * dx + dy * dy) + if (distance > maxDistance) { + maxDistance = distance + } + }) + return maxDistance + 48 +} + +function createBurst (x, y) { + const reach = farthestScreenDistance(x, y) + const particles = [] + const particleCount = 46 + for (let i = 0; i < particleCount; i++) { + const angle = Math.random() * Math.PI * 2 + particles.push({ + angle: angle, + speed: reach * (0.72 + Math.random() * 0.38), + size: 2.2 + Math.random() * 3.6 + }) + } + const bolts = [] + const boltCount = 18 + for (let i = 0; i < boltCount; i++) { + const angle = (Math.PI * 2 * i) / boltCount + (Math.random() - 0.5) * 0.28 + const length = reach * (0.92 + Math.random() * 0.16) + const segments = 16 + const points = [{ x: x, y: y }] + for (let s = 1; s <= segments; s++) { + const ratio = s / segments + const dist = length * ratio + const sway = (Math.random() - 0.5) * 54 * (1 - ratio * 0.25) + points.push({ + x: x + Math.cos(angle) * dist + Math.cos(angle + Math.PI / 2) * sway, + y: y + Math.sin(angle) * dist + Math.sin(angle + Math.PI / 2) * sway + }) + } + bolts.push({ points: points }) + } + const stars = [] + const starCount = 200 + for (let i = 0; i < starCount; i++) { + stars.push({ + x: Math.random() * window.innerWidth, + y: Math.random() * window.innerHeight, + size: 1.4 + Math.random() * 6.2, + phase: Math.random() * Math.PI * 2, + blinks: 3 + Math.floor(Math.random() * 4), + gold: i % 2 === 0 + }) + } + return { + x: x, + y: y, + reach: reach, + start: performance.now(), + particles: particles, + bolts: bolts, + stars: stars + } +} + +function pointOnBolt (points, t) { + const clamped = Math.max(0, Math.min(1, t)) + const scaled = clamped * (points.length - 1) + const index = Math.floor(scaled) + const next = Math.min(index + 1, points.length - 1) + const mix = scaled - index + return { + x: points[index].x + (points[next].x - points[index].x) * mix, + y: points[index].y + (points[next].y - points[index].y) * mix + } +} + +function drawBolt (bolt, progress) { + const travel = Math.min(1, progress / 0.42) + const head = travel + const tail = Math.max(0, head - 0.16) + if (head <= tail) { + return + } + const steps = 10 + ctx.beginPath() + const start = pointOnBolt(bolt.points, tail) + ctx.moveTo(start.x, start.y) + for (let step = 1; step <= steps; step++) { + const point = pointOnBolt(bolt.points, tail + (head - tail) * (step / steps)) + ctx.lineTo(point.x, point.y) + } + const tipFade = travel < 1 ? 1 : Math.max(0, 1 - (progress - 0.42) / 0.58) + ctx.save() + ctx.globalAlpha = tipFade + ctx.lineCap = 'round' + ctx.lineJoin = 'round' + ctx.strokeStyle = 'rgba(70, 225, 255, 0.95)' + ctx.lineWidth = 5.5 + ctx.shadowColor = 'rgba(0, 200, 255, 1)' + ctx.shadowBlur = 18 + ctx.stroke() + ctx.shadowBlur = 0 + ctx.strokeStyle = 'rgba(255, 255, 255, 0.98)' + ctx.lineWidth = 1.7 + ctx.stroke() + ctx.restore() +} + +function drawSeismicWave (burst, progress) { + const delays = [0, 0.1, 0.2] + delays.forEach(function (delay, index) { + const span = 1 - delay + const local = (progress - delay) / span + if (local <= 0 || local > 1) { + return + } + const eased = 1 - Math.pow(1 - local, 2) + const radius = 28 + eased * (burst.reach + index * 36) + const alpha = (1 - local) * 0.42 + const steps = 80 + ctx.save() + ctx.beginPath() + for (let i = 0; i <= steps; i++) { + const angle = (Math.PI * 2 * i) / steps + const wobble = 1 + Math.sin(angle * 7 + index * 1.4) * 0.04 + const pointX = burst.x + Math.cos(angle) * radius * wobble + const pointY = burst.y + Math.sin(angle) * radius * wobble + if (i === 0) { + ctx.moveTo(pointX, pointY) + } else { + ctx.lineTo(pointX, pointY) + } + } + ctx.closePath() + ctx.lineWidth = 10 * (1 - local * 0.55) + ctx.strokeStyle = 'rgba(190, 245, 255, ' + alpha + ')' + ctx.shadowColor = 'rgba(140, 230, 255, ' + alpha + ')' + ctx.shadowBlur = 14 + ctx.stroke() + ctx.restore() + }) +} + +function drawDiamond (x, y, size, alpha) { + if (alpha <= 0.02) { + return + } + ctx.save() + ctx.translate(x, y) + ctx.rotate(Math.PI / 4) + ctx.fillStyle = 'rgba(176, 232, 255, ' + alpha + ')' + ctx.fillRect(-size, -size, size * 2, size * 2) + ctx.fillStyle = 'rgba(255, 255, 255, ' + alpha + ')' + ctx.fillRect(-size * 0.38, -size * 0.38, size * 0.76, size * 0.76) + ctx.restore() +} + +function drawScreenStars (burst, progress) { + const fade = 0.45 + (1 - progress) * 0.55 + burst.stars.forEach(function (star) { + const twinkle = Math.abs(Math.sin(progress * Math.PI * star.blinks + star.phase)) + if (twinkle < 0.12) { + return + } + const alpha = fade * twinkle + ctx.save() + ctx.translate(star.x, star.y) + ctx.rotate(Math.PI / 4) + ctx.fillStyle = star.gold ? 'rgba(255, 230, 0, ' + alpha + ')' : 'rgba(0, 245, 255, ' + alpha + ')' + ctx.fillRect(-star.size, -star.size, star.size * 2, star.size * 2) + ctx.restore() + }) +} + +function drawWaveDiamonds (burst, progress) { + const delays = [0, 0.1, 0.2] + delays.forEach(function (delay, index) { + const span = 1 - delay + const local = (progress - delay) / span + if (local <= 0 || local > 1) { + return + } + const eased = 1 - Math.pow(1 - local, 2) + const radius = 28 + eased * (burst.reach + index * 36) + const alpha = (1 - local) * 0.9 + const count = 26 + for (let i = 0; i < count; i++) { + const angle = (Math.PI * 2 * i) / count + progress * 0.6 + index + const wobble = 1 + Math.sin(angle * 7 + index * 1.4) * 0.04 + drawDiamond( + burst.x + Math.cos(angle) * radius * wobble, + burst.y + Math.sin(angle) * radius * wobble, + 2.2 + (i % 3) * 0.7, + alpha + ) + } + }) +} + +function drawBurst (burst, now) { + const progress = Math.min(1, (now - burst.start) / EFFECT_MS) + const fade = 1 - progress + drawSeismicWave(burst, progress) + drawWaveDiamonds(burst, progress) + drawScreenStars(burst, progress) + burst.particles.forEach(function (particle) { + const dist = particle.speed * progress + const px = burst.x + Math.cos(particle.angle) * dist + const py = burst.y + Math.sin(particle.angle) * dist + const radius = particle.size * (1 - progress * 0.25) + ctx.save() + ctx.beginPath() + ctx.fillStyle = 'rgba(214, 224, 236, ' + fade + ')' + ctx.shadowColor = 'rgba(236, 244, 255, ' + fade + ')' + ctx.shadowBlur = 16 + ctx.arc(px, py, radius * 2.1, 0, Math.PI * 2) + ctx.fill() + ctx.shadowBlur = 0 + ctx.beginPath() + ctx.fillStyle = 'rgba(120, 230, 255, ' + fade + ')' + ctx.arc(px, py, radius * 0.72, 0, Math.PI * 2) + ctx.fill() + ctx.restore() + }) + burst.bolts.forEach(function (bolt) { + drawBolt(bolt, progress) + }) + return progress < 1 +} + +function render (now) { + if (!ctx || !canvas) { + frameId = 0 + return + } + ctx.clearRect(0, 0, window.innerWidth, window.innerHeight) + const alive = [] + bursts.forEach(function (burst) { + if (drawBurst(burst, now)) { + alive.push(burst) + } + }) + bursts = alive + if (bursts.length === 0) { + removeCanvas() + return + } + frameId = requestAnimationFrame(render) +} + +function spawn (x, y) { + ensureCanvas() + bursts.push(createBurst(x, y)) + if (bursts.length > MAX_BURSTS) { + bursts.splice(0, bursts.length - MAX_BURSTS) + } + if (!frameId) { + frameId = requestAnimationFrame(render) + } +} + +export function installClickBloom (store) { + if (!store || window.__wmsClickBloomInstalled) { + return + } + window.__wmsClickBloomInstalled = true + document.addEventListener('mousedown', function (event) { + const enabled = store.state && store.state.common && store.state.common.clickEffectEnabled + if (!enabled || event.button !== 0) { + if (!enabled) { + removeCanvas() + } + return + } + spawn(event.clientX, event.clientY) + }, true) +} diff --git a/src/views/common/login.vue b/src/views/common/login.vue index 9db5ff1..038c3af 100644 --- a/src/views/common/login.vue +++ b/src/views/common/login.vue @@ -241,6 +241,7 @@ localStorage.setItem('site', this.selectedSite) // 保存选择的工厂 // 登录后立即回写store.user.site,保证菜单拉取前可读取当前工厂 - rqrq this.$store.commit('user/updateSite', this.selectedSite) + this.$store.commit('common/updateClickEffectEnabled', false) this.getConfigParams() this.cacheUserAuthorizedSubSiteCodes(this.dataForm.userName, this.selectedSite).finally(() => { diff --git a/src/views/common/theme.vue b/src/views/common/theme.vue index ce35f22..e1bdc78 100644 --- a/src/views/common/theme.vue +++ b/src/views/common/theme.vue @@ -13,6 +13,9 @@ dark + + + @@ -26,6 +29,10 @@ sidebarLayoutSkin: { get () { return this.$store.state.common.sidebarLayoutSkin }, set (val) { this.$store.commit('common/updateSidebarLayoutSkin', val) } + }, + clickEffectEnabled: { + get () { return this.$store.state.common.clickEffectEnabled }, + set (val) { this.$store.commit('common/updateClickEffectEnabled', val) } } } }