diff --git a/src/pages/ElectronicWoodenFish.css b/src/pages/ElectronicWoodenFish.css index 4c15948..6ab4ec6 100644 --- a/src/pages/ElectronicWoodenFish.css +++ b/src/pages/ElectronicWoodenFish.css @@ -1,64 +1,154 @@ +/* CSS 变量定义 */ +:root { + --dzmy-bg-color: black; + --dzmy-text-color: #ffffff; + --dzmy-accent-color: #fadb14; + --dzmy-container-height: 500px; + --dzmy-img-width: 160px; + --dzmy-img-height: 120px; + --dzmy-animation-duration: 0.12s; + --dzmy-float-duration: 0.5s; +} + +/* 主容器 */ .dzmy-bg { - background-color: black; + background-color: var(--dzmy-bg-color); width: 100%; - height: 500px; + min-height: var(--dzmy-container-height); display: flex; flex-direction: column; justify-content: space-evenly; position: relative; } +/* 计数显示 */ .dzmy-count { position: absolute; top: 10px; left: 10px; - color: #ffffff; + color: var(--dzmy-text-color); + font-size: 18px; + font-weight: bold; + z-index: 10; } +/* 木鱼图像 */ .dzmy-img { - width: 160px; - height: 120px; + width: var(--dzmy-img-width); + height: var(--dzmy-img-height); + max-width: 100%; + max-height: 100%; cursor: pointer; - transition: transform 0.12s ease; + transition: transform var(--dzmy-animation-duration) ease; transform: scale(1); + will-change: transform; + touch-action: manipulation; + user-select: none; + object-fit: contain; } +/* 木鱼点击激活状态 */ .dzmy-img.active { transform: scale(0.92); } +/* 木鱼和提示容器 */ .dzmy-box { text-align: center; position: relative; + z-index: 5; } +/* 浮动提示容器 */ .tips { position: absolute; top: -50px; width: 100%; text-align: center; pointer-events: none; + z-index: 20; + overflow: visible; } +/* 单个浮动提示项 */ .tip-item { position: absolute; width: 100%; - animation: floatUp 0.5s ease-out forwards; - color: #fadb14; + animation: floatUp var(--dzmy-float-duration) ease-out forwards; + color: var(--dzmy-accent-color); font-weight: bold; + will-change: transform, opacity; + transform: translateZ(0); + font-size: 24px; + text-shadow: 0 0 4px rgba(0, 0, 0, 0.5); } +/* 浮动动画 */ @keyframes floatUp { 0% { - transform: translateY(0); + transform: translate3d(0, 0, 0); opacity: 1; } 100% { - transform: translateY(-50px); + transform: translate3d(0, -50px, 0); opacity: 0; } } +/* 控制区域 */ .dzmy-controls { margin-bottom: 10px; + display: flex; + gap: 10px; + justify-content: center; + align-items: center; + padding: 10px; + background-color: rgba(0, 0, 0, 0.1); + border-radius: 8px; + margin-top: 10px; +} + +/* 控制区域内的输入框 */ +.dzmy-controls input { + width: auto; + min-width: 120px; + margin-bottom: 0; + flex: 1; + max-width: 200px; +} + +/* 控制区域内的按钮 */ +.dzmy-controls .action-btn { + margin-right: 0; + opacity: 1; + flex-shrink: 0; +} +.dzmy-controls .action-btn:hover { + opacity: 0.9; +} +.dzmy-controls .action-btn:active { + opacity: 0.8; + box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.3); +} + +/* 响应式设计 */ +@media (max-width: 768px) { + :root { + --dzmy-container-height: 300px; + --dzmy-img-width: 120px; + --dzmy-img-height: 90px; + } + .dzmy-count { + font-size: 14px; + } + .tip-item { + font-size: 18px; + } + .dzmy-controls { + flex-wrap: wrap; + } + .dzmy-controls input { + min-width: 100px; + max-width: 150px; + } } \ No newline at end of file diff --git a/src/pages/ElectronicWoodenFishPage.js b/src/pages/ElectronicWoodenFishPage.js index dc8493c..b3e29d1 100644 --- a/src/pages/ElectronicWoodenFishPage.js +++ b/src/pages/ElectronicWoodenFishPage.js @@ -1,62 +1,248 @@ -import React, {useState, useCallback, useEffect} from 'react'; +import React, {useState, useCallback, useEffect, useRef} from 'react'; import dzmyImg from '../assets/images/dzmy.png'; import './ElectronicWoodenFish.css'; import storageAdapter from "../utils/storageAdapter"; +const MAX_TIP_LIST_SIZE = 50; // 最大提示数量限制 + const ElectronicWoodenFishPage = () => { const [active, setActive] = useState(false); const [count, setCount] = useState(0); const [tipList, setTipList] = useState([]); const [countName, setCountName] = useState('金钱'); + // Refs for cleanup and avoiding stale closures + const timerRef = useRef(null); + const countNameRef = useRef(countName); + const tipTimersRef = useRef(new Map()); + const firstRenderRef = useRef(true); + const storageTimerRef = useRef(null); + useEffect(() => { + let isMounted = true; + const init = async () => { - const savedCount = await storageAdapter.get('count'); - const savedCountName = await storageAdapter.get('countName'); - if (savedCount) { - setCount(savedCount); - } else { - await storageAdapter.set('count', 0); + try { + const [savedCount, savedCountName] = await Promise.all([ + storageAdapter.get('count'), + storageAdapter.get('countName') + ]); + + if (!isMounted) return; + + // 处理 count:确保是有效数字 + let validCount = 0; + console.log('初始化存储数据:', savedCount, savedCountName); + if (savedCount !== undefined && savedCount !== null) { + const num = Number(savedCount); + if (!isNaN(num) && isFinite(num) && num >= 0) { + validCount = Math.floor(num); // 取整,确保是整数 + } + } + setCount(validCount); + // 如果存储的值无效或不存在,更新存储 + if (savedCount !== validCount) { + await storageAdapter.set('count', validCount); + } + + // 处理 countName:确保是有效非空字符串 + let validCountName = '金钱'; + if (typeof savedCountName === 'string') { + const trimmed = savedCountName.trim(); + // 拒绝空字符串、'undefined'、'null'等无效值 + if (trimmed.length > 0 && + trimmed.toLowerCase() !== 'undefined' && + trimmed.toLowerCase() !== 'null' && + trimmed !== 'NaN') { + validCountName = trimmed; + } + } + setCountName(validCountName); + // 如果存储的值无效或不存在,更新存储 + if (savedCountName !== validCountName) { + await storageAdapter.set('countName', validCountName); + } + } catch (error) { + console.error('初始化存储数据失败:', error); + // 静默失败,使用默认值 + if (isMounted) { + setCount(0); + setCountName('金钱'); + } } - if (savedCountName) { - setCountName(savedCountName); - } else { - await storageAdapter.set('countName', '金钱'); - } - } + }; + init(); + + return () => { + isMounted = false; + }; + }, []); + + // 同步 countName 到 ref,避免闭包问题 + useEffect(() => { + countNameRef.current = countName; + }, [countName]); + + // 存储 countName 变化(防抖处理) + useEffect(() => { + if (firstRenderRef.current) { + firstRenderRef.current = false; + return; + } + + // 清理之前的定时器 + if (storageTimerRef.current) { + clearTimeout(storageTimerRef.current); + } + + // 防抖存储:500ms 后存储 + storageTimerRef.current = setTimeout(async () => { + try { + await storageAdapter.set('countName', countName); + } catch (error) { + console.error('存储 countName 失败:', error); + } + }, 500); + + return () => { + if (storageTimerRef.current) { + clearTimeout(storageTimerRef.current); + } + }; + }, [countName]); + + // 组件卸载时清理所有定时器 + useEffect(() => { + return () => { + // 清理动画定时器 + if (timerRef.current) { + clearTimeout(timerRef.current); + timerRef.current = null; + } + + // 清理所有 tip 定时器 + tipTimersRef.current.forEach((timer) => { + clearTimeout(timer); + }); + tipTimersRef.current.clear(); + + // 清理存储定时器 + if (storageTimerRef.current) { + clearTimeout(storageTimerRef.current); + storageTimerRef.current = null; + } + }; }, []); const clickAnimation = useCallback(async () => { - // 1. 木鱼缩放动画触发 - setActive(true); - setTimeout(() => setActive(false), 100); - - // 2. 更新计数 - setCount((prev) => prev + 1); - - // 3. 生成唯一的 Tip 对象 - const newTip = { - id: Date.now() + Math.random(), // 唯一 ID - value: count + 1 - }; - - // 4. 添加到列表 - setTipList((prev) => [...prev, newTip]); - - // 5. 自动移除:500ms 后删除该特定 Tip - setTimeout(() => { - setTipList((prev) => prev.filter((t) => t.id !== newTip.id)); - }, 500); - - await storageAdapter.set('count', count + 1); - await storageAdapter.set('countName', countName); - }, [count]); + try { + // 1. 木鱼缩放动画 - 使用 requestAnimationFrame 优化时序 + setActive(true); + + // 清理之前的动画定时器 + if (timerRef.current) { + clearTimeout(timerRef.current); + } + + // 设置动画结束定时器 + timerRef.current = setTimeout(() => { + setActive(false); + timerRef.current = null; + }, 100); + + // 2. 更新计数并获取新值,同时执行存储 + setCount((prev) => { + const newCount = prev + 1; + + // 异步存储更新(不阻塞 UI) + const storagePromises = [ + storageAdapter.set('count', newCount), + storageAdapter.set('countName', countNameRef.current) + ]; + + // 使用 Promise.all 并行存储,但忽略错误(静默失败) + Promise.all(storagePromises).catch(error => { + console.error('存储更新失败:', error); + // 可以选择重试或记录错误 + }); + + return newCount; + }); + + // 3. 生成唯一的 Tip 对象 + const tipId = `tip_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + const newTip = { + id: tipId, + // 使用当前 countName(通过 ref 获取最新值) + label: `${countNameRef.current} +1` + }; + + // 4. 添加到列表(限制最大长度) + setTipList((prev) => { + const newList = [...prev, newTip]; + // 如果超过最大长度,移除最旧的 tip + if (newList.length > MAX_TIP_LIST_SIZE) { + const removedTip = newList.shift(); // 移除第一个元素 + // 清理被移除 tip 的定时器 + const removedTimer = tipTimersRef.current.get(removedTip.id); + if (removedTimer) { + clearTimeout(removedTimer); + tipTimersRef.current.delete(removedTip.id); + } + } + return newList; + }); + + // 5. 自动移除:500ms 后删除该特定 Tip + const removalTimer = setTimeout(() => { + setTipList((prev) => prev.filter((t) => t.id !== tipId)); + tipTimersRef.current.delete(tipId); + }, 500); + + tipTimersRef.current.set(tipId, removalTimer); + + } catch (error) { + console.error('点击动画执行失败:', error); + // 确保动画状态重置 + setActive(false); + } + }, []); // 无依赖,使用 refs 获取最新状态 - const resetAll = () => { - setCount(0); - setTipList([]); - setCountName('金钱'); + const resetAll = async () => { + try { + // 清理所有 tip 定时器 + tipTimersRef.current.forEach((timer) => { + clearTimeout(timer); + }); + tipTimersRef.current.clear(); + + // 清理动画定时器 + if (timerRef.current) { + clearTimeout(timerRef.current); + timerRef.current = null; + } + + // 重置状态 + setCount(0); + setTipList([]); + setCountName('金钱'); + + // 同时更新 ref + countNameRef.current = '金钱'; + + // 异步存储重置(不阻塞 UI) + const storagePromises = [ + storageAdapter.set('count', 0), + storageAdapter.set('countName', '金钱') + ]; + + Promise.all(storagePromises).catch(error => { + console.error('重置存储失败:', error); + }); + } catch (error) { + console.error('重置操作失败:', error); + } } return ( @@ -68,7 +254,7 @@ const ElectronicWoodenFishPage = () => {