feat(electronic-wooden-fish): 实现电子木鱼页面及本地存储适配器
- 新增电子木鱼点击动画与计数功能 - 添加浮动提示动画效果 - 实现计数名称自定义输入框 - 增加重置所有数据按钮 - 创建跨平台本地存储适配器(支持浏览器和Chrome扩展) - 样式优化:按钮增加过渡动画和按下效果 - 页面结构重构,分离CSS样式文件
This commit is contained in:
+15
-1
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
// 2. 更新计数
|
||||||
|
setCount((prev) => prev + 1);
|
||||||
|
|
||||||
|
// 3. 生成唯一的 Tip 对象
|
||||||
|
const newTip = {
|
||||||
|
id: Date.now() + Math.random(), // 唯一 ID
|
||||||
|
value: count + 1
|
||||||
};
|
};
|
||||||
|
|
||||||
return (<div>
|
// 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]);
|
||||||
|
|
||||||
|
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
|
<img
|
||||||
alt={'电子木鱼'}
|
alt="电子木鱼"
|
||||||
src={dzmyImg}
|
src={dzmyImg}
|
||||||
style={{
|
className={`dzmy-img ${active ? 'active' : ''}`}
|
||||||
backgroundColor: 'black',
|
|
||||||
width: '80%',
|
|
||||||
height: '80%',
|
|
||||||
cursor: 'pointer',
|
|
||||||
transition: 'transform 0.12s ease',
|
|
||||||
transform: active ? 'scale(0.92)' : 'scale(1)',
|
|
||||||
}}
|
|
||||||
onClick={clickAnimation}
|
onClick={clickAnimation}
|
||||||
/>
|
/>
|
||||||
</div>);
|
</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;
|
||||||
@@ -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;
|
||||||
Reference in New Issue
Block a user