feat(electronic-wooden-fish): 实现电子木鱼页面及本地存储适配器

- 新增电子木鱼点击动画与计数功能
- 添加浮动提示动画效果
- 实现计数名称自定义输入框
- 增加重置所有数据按钮
- 创建跨平台本地存储适配器(支持浏览器和Chrome扩展)
- 样式优化:按钮增加过渡动画和按下效果
- 页面结构重构,分离CSS样式文件
This commit is contained in:
雨霖铃
2025-12-20 11:43:35 +08:00
parent d7740a3d4f
commit 6dc71bf056
4 changed files with 197 additions and 22 deletions
+15 -1
View File
@@ -75,15 +75,29 @@
padding: 8px 16px; padding: 8px 16px;
background: #4caf50; background: #4caf50;
color: white; color: white;
border: none;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
border: 1px solid #d9d9d9;
box-shadow: 0 2px #00000004;
transition: .8s;
opacity: 4;
} }
.action-btn:hover { .action-btn:hover {
background: #3d8b40; background: #3d8b40;
} }
.action-btn:active {
box-shadow: 0 0 0 6px rgba(0, 0, 0, 0.2);
opacity: 0;
}
.action-btn:active::after {
box-shadow: none;
opacity: 0.4;
transition: 0s;
}
.stop-btn { .stop-btn {
margin-right: 10px; margin-right: 10px;
padding: 8px 16px; padding: 8px 16px;
+64
View File
@@ -0,0 +1,64 @@
.dzmy-bg {
background-color: black;
width: 100%;
height: 500px;
display: flex;
flex-direction: column;
justify-content: space-evenly;
position: relative;
}
.dzmy-count {
position: absolute;
top: 10px;
left: 10px;
color: #ffffff;
}
.dzmy-img {
width: 160px;
height: 120px;
cursor: pointer;
transition: transform 0.12s ease;
transform: scale(1);
}
.dzmy-img.active {
transform: scale(0.92);
}
.dzmy-box {
text-align: center;
position: relative;
}
.tips {
position: absolute;
top: -50px;
width: 100%;
text-align: center;
pointer-events: none;
}
.tip-item {
position: absolute;
width: 100%;
animation: floatUp 0.5s ease-out forwards;
color: #fadb14;
font-weight: bold;
}
@keyframes floatUp {
0% {
transform: translateY(0);
opacity: 1;
}
100% {
transform: translateY(-50px);
opacity: 0;
}
}
.dzmy-controls {
margin-bottom: 10px;
}
+89 -22
View File
@@ -1,32 +1,99 @@
import React, {useState, useCallback, useEffect} from 'react';
import dzmyImg from '../assets/images/dzmy.png'; import dzmyImg from '../assets/images/dzmy.png';
import {useState} from "react"; import './ElectronicWoodenFish.css';
import storageAdapter from "../utils/storageAdapter";
const ElectronicWoodenFishPage = () => { const ElectronicWoodenFishPage = () => {
const [active, setActive] = useState(false); const [active, setActive] = useState(false);
const [count, setCount] = useState(0);
const [tipList, setTipList] = useState([]);
const [countName, setCountName] = useState('金钱');
const clickAnimation = () => { useEffect(() => {
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);
}
if (savedCountName) {
setCountName(savedCountName);
} else {
await storageAdapter.set('countName', '金钱');
}
}
init();
}, []);
const clickAnimation = useCallback(async () => {
// 1. 木鱼缩放动画触发
setActive(true); setActive(true);
setTimeout(() => { setTimeout(() => setActive(false), 100);
setActive(false);
}, 120);
};
return (<div> // 2. 更新计数
电子木鱼 setCount((prev) => prev + 1);
<img
alt={'电子木鱼'} // 3. 生成唯一的 Tip 对象
src={dzmyImg} const newTip = {
style={{ id: Date.now() + Math.random(), // 唯一 ID
backgroundColor: 'black', value: count + 1
width: '80%', };
height: '80%',
cursor: 'pointer', // 4. 添加到列表
transition: 'transform 0.12s ease', setTipList((prev) => [...prev, newTip]);
transform: active ? 'scale(0.92)' : 'scale(1)',
}} // 5. 自动移除:500ms 后删除该特定 Tip
onClick={clickAnimation} setTimeout(() => {
/> setTipList((prev) => prev.filter((t) => t.id !== newTip.id));
</div>); }, 500);
await storageAdapter.set('count', count + 1);
await storageAdapter.set('countName', countName);
}, [count]);
const resetAll = () => {
setCount(0);
setTipList([]);
setCountName('金钱');
}
return (
<div>
<div className="dzmy-bg">
<div className="dzmy-count">{countName}: {count}</div>
<div className="dzmy-box">
<div className="tips">
{tipList.map((tip) => (
<div key={tip.id} className="tip-item">
{countName} +1
</div>
))}
</div>
<img
alt="电子木鱼"
src={dzmyImg}
className={`dzmy-img ${active ? 'active' : ''}`}
onClick={clickAnimation}
/>
</div>
</div>
<div className={'dzmy-controls'}>
<input
type="text"
value={countName}
onChange={(e) => setCountName(e.target.value)}
/>
<button
className={'action-btn'}
onClick={resetAll}>重置
</button>
</div>
</div>
);
}; };
export default ElectronicWoodenFishPage; export default ElectronicWoodenFishPage;
+30
View File
@@ -0,0 +1,30 @@
// 判断当前是否处于 Chrome 插件环境
const isExtension = typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local;
const storageAdapter = {
// 获取数据
get: async (key) => {
if (isExtension) {
const result = await chrome.storage.local.get([key]);
return result[key];
} else {
const item = localStorage.getItem(key);
try {
return JSON.parse(item); // 保持与插件版一致的对象处理
} catch {
return item;
}
}
},
// 设置数据
set: async (key, value) => {
if (isExtension) {
await chrome.storage.local.set({[key]: value});
} else {
localStorage.setItem(key, JSON.stringify(value));
}
}
};
export default storageAdapter;