更换使用wxt框架搭建testing tool
@@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"es2021": true,
|
|
||||||
"webextensions": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:react/recommended"
|
|
||||||
],
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaFeatures": {
|
|
||||||
"jsx": true
|
|
||||||
},
|
|
||||||
"ecmaVersion": "latest",
|
|
||||||
"sourceType": "module"
|
|
||||||
},
|
|
||||||
"plugins": [
|
|
||||||
"react"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"react/react-in-jsx-scope": "off",
|
|
||||||
"react/prop-types": "off",
|
|
||||||
"no-undef": "error"
|
|
||||||
},
|
|
||||||
"globals": {
|
|
||||||
"chrome": "readonly"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +1,26 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
# Logs
|
||||||
|
logs
|
||||||
# dependencies
|
*.log
|
||||||
/node_modules
|
|
||||||
/.pnp
|
|
||||||
.pnp.js
|
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
|
||||||
|
|
||||||
# misc
|
|
||||||
.DS_Store
|
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.output
|
||||||
|
stats.html
|
||||||
|
stats-*.json
|
||||||
|
.wxt
|
||||||
|
web-ext.config.ts
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
.idea
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"printWidth": 100,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"useTabs": false,
|
|
||||||
"semi": true,
|
|
||||||
"singleQuote": false,
|
|
||||||
"quoteProps": "as-needed",
|
|
||||||
"jsxSingleQuote": false,
|
|
||||||
"trailingComma": "none",
|
|
||||||
"bracketSpacing": true,
|
|
||||||
"jsxBracketSameLine": false,
|
|
||||||
"arrowParens": "always",
|
|
||||||
"requirePragma": false,
|
|
||||||
"insertPragma": false,
|
|
||||||
"proseWrap": "preserve",
|
|
||||||
"htmlWhitespaceSensitivity": "ignore",
|
|
||||||
"endOfLine": "auto"
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1,30 @@
|
|||||||
|
export default defineBackground(() => {
|
||||||
|
console.log('Hello background!', { id: browser.runtime.id });
|
||||||
|
|
||||||
|
console.log('Background script loaded');
|
||||||
|
|
||||||
|
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||||
|
console.log('收到消息:', request);
|
||||||
|
|
||||||
|
if (request.action === 'copy') {
|
||||||
|
console.log('开始复制文本:', request.text);
|
||||||
|
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(request.text)
|
||||||
|
.then(() => {
|
||||||
|
console.log('复制成功');
|
||||||
|
sendResponse({ success: true });
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('复制失败:', err);
|
||||||
|
sendResponse({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
|
||||||
|
return true; // 保持消息端口开放以支持异步响应
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对于未知的操作,也返回响应
|
||||||
|
sendResponse({ success: false, error: '未知操作' });
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export default defineContentScript({
|
||||||
|
matches: ['*://*.google.com/*'],
|
||||||
|
main() {
|
||||||
|
console.log('Hello content.');
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { HashRouter as Router, Routes, Route, NavLink } from 'react-router-dom';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import TimestampPage from '../../pages/TimestampPage';
|
||||||
|
// import {RecordeReplayPage} from '../../pages/RecordeReplayPage';
|
||||||
|
import './App.css';
|
||||||
|
|
||||||
|
const navItems = [
|
||||||
|
{ path: '/', label: '时间戳', element: <TimestampPage /> },
|
||||||
|
// {path: '/dzmy', label: '电子木鱼', element: <ElectronicWoodenFishPage/>},
|
||||||
|
// { path: '/recorde-replay', label: '录制与回放', element: <RecordeReplayPage /> },
|
||||||
|
];
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
|
const [visibleItems, setVisibleItems] = useState(navItems.length);
|
||||||
|
|
||||||
|
// 检测屏幕尺寸变化
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => {
|
||||||
|
const width = window.innerWidth;
|
||||||
|
setIsMobile(width < 768);
|
||||||
|
|
||||||
|
// 根据屏幕宽度决定显示多少个导航项
|
||||||
|
if (width >= 768) {
|
||||||
|
setVisibleItems(navItems.length); // 大屏幕显示所有
|
||||||
|
} else if (width >= 480) {
|
||||||
|
// 中等屏幕:如果导航项超过3个,显示3个,否则显示全部
|
||||||
|
setVisibleItems(Math.min(3, navItems.length));
|
||||||
|
} else {
|
||||||
|
// 小屏幕:如果导航项超过2个,显示2个,否则显示全部
|
||||||
|
setVisibleItems(Math.min(2, navItems.length));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleResize(); // 初始调用
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
return () => window.removeEventListener('resize', handleResize);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 计算哪些导航项应该显示,哪些应该折叠
|
||||||
|
const visibleNavItems = navItems.slice(0, visibleItems);
|
||||||
|
const collapsedNavItems = navItems.slice(visibleItems);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Router>
|
||||||
|
<div className="app">
|
||||||
|
<nav className="nav">
|
||||||
|
<div className="nav-content">
|
||||||
|
<ul className={`nav-list ${isMenuOpen ? 'open' : ''}`}>
|
||||||
|
{visibleNavItems.map((item) => (
|
||||||
|
<li key={item.path}>
|
||||||
|
<NavLink
|
||||||
|
to={item.path}
|
||||||
|
className={({ isActive }) => (isActive ? 'nav-link active' : 'nav-link')}
|
||||||
|
onClick={() => isMobile && setIsMenuOpen(false)}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* 折叠的导航项 */}
|
||||||
|
{collapsedNavItems.length > 0 && (
|
||||||
|
<li className="nav-collapse-item">
|
||||||
|
<div className={`nav-collapse-content ${isMenuOpen ? 'show' : ''}`}>
|
||||||
|
{isMenuOpen &&
|
||||||
|
collapsedNavItems.map((item) => (
|
||||||
|
<NavLink
|
||||||
|
key={item.path}
|
||||||
|
to={item.path}
|
||||||
|
className={({ isActive }) => (isActive ? 'nav-link active' : 'nav-link')}
|
||||||
|
onClick={() => setIsMenuOpen(false)}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</NavLink>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="nav-toggle"
|
||||||
|
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||||
|
aria-label={isMenuOpen ? '收起菜单' : '展开菜单'}
|
||||||
|
>
|
||||||
|
<span className="nav-toggle-icon">{isMenuOpen ? '×' : '☰'}</span>
|
||||||
|
{collapsedNavItems.length > 0 && !isMenuOpen && (
|
||||||
|
<span className="nav-collapse-count">+{collapsedNavItems.length}</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<Routes>
|
||||||
|
{navItems.map((item) => (
|
||||||
|
<Route key={item.path} path={item.path} element={item.element} />
|
||||||
|
))}
|
||||||
|
</Routes>
|
||||||
|
</div>
|
||||||
|
</Router>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Default Popup Title</title>
|
||||||
|
<meta name="manifest.type" content="browser_action" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="./main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App.tsx';
|
||||||
|
import './style.css';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
@@ -1,8 +1,22 @@
|
|||||||
{
|
{
|
||||||
"name": "chrome-extension-router-demo",
|
"name": "wxt-react-starter",
|
||||||
"version": "0.1.0",
|
"description": "manifest.json description",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "wxt",
|
||||||
|
"dev:firefox": "wxt -b firefox",
|
||||||
|
"build": "wxt build",
|
||||||
|
"build:firefox": "wxt build -b firefox",
|
||||||
|
"zip": "wxt zip",
|
||||||
|
"zip:firefox": "wxt zip -b firefox",
|
||||||
|
"compile": "tsc --noEmit",
|
||||||
|
"postinstall": "wxt prepare"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"react": "^19.2.3",
|
||||||
|
"react-dom": "^19.2.3",
|
||||||
"@rrweb/all": "^2.0.0-alpha.18",
|
"@rrweb/all": "^2.0.0-alpha.18",
|
||||||
"@rrweb/rrweb-plugin-console-record": "^2.0.0-alpha.18",
|
"@rrweb/rrweb-plugin-console-record": "^2.0.0-alpha.18",
|
||||||
"@rrweb/rrweb-plugin-console-replay": "^2.0.0-alpha.18",
|
"@rrweb/rrweb-plugin-console-replay": "^2.0.0-alpha.18",
|
||||||
@@ -13,43 +27,19 @@
|
|||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dexie": "^4.2.1",
|
"dexie": "^4.2.1",
|
||||||
"dexie-react-hooks": "^4.2.0",
|
"dexie-react-hooks": "^4.2.0",
|
||||||
"react": "^19.2.3",
|
|
||||||
"react-dom": "^19.2.3",
|
|
||||||
"react-markdown": "^6.0.3",
|
"react-markdown": "^6.0.3",
|
||||||
"react-router-dom": "^6.30.2",
|
"react-router-dom": "^6.30.2",
|
||||||
"react-scripts": "5.0.1",
|
|
||||||
"remark-gfm": "^1.0.0",
|
"remark-gfm": "^1.0.0",
|
||||||
"rrweb": "^2.0.0-alpha.4",
|
"rrweb": "^2.0.0-alpha.4",
|
||||||
"rrweb-player": "^1.0.0-alpha.4",
|
"rrweb-player": "^1.0.0-alpha.4",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
|
||||||
"start": "react-scripts start",
|
|
||||||
"build": "react-scripts build",
|
|
||||||
"test": "react-scripts test",
|
|
||||||
"eject": "react-scripts eject"
|
|
||||||
},
|
|
||||||
"eslintConfig": {
|
|
||||||
"extends": [
|
|
||||||
"react-app",
|
|
||||||
"react-app/jest"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"browserslist": {
|
|
||||||
"production": [
|
|
||||||
">0.2%",
|
|
||||||
"not dead",
|
|
||||||
"not op_mini all"
|
|
||||||
],
|
|
||||||
"development": [
|
|
||||||
"last 1 chrome version",
|
|
||||||
"last 1 firefox version",
|
|
||||||
"last 1 safari version"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chrome": "^0.1.36",
|
"@types/chrome": "^0.1.36",
|
||||||
"@types/react": "^19.2.9",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3"
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@wxt-dev/module-react": "^1.1.5",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"wxt": "^0.20.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
// import {sendRecordCommand} from '../services/bridge'
|
// import {sendRecordCommand} from '../services/bridge'
|
||||||
// import {storage} from "../services/storage";
|
// import {storage} from "../services/storage";
|
||||||
import {useRecorder} from "../hooks/useRecorder";
|
import {useRecorder} from "../hooks/useRecorder";
|
||||||
import {downloadHtml} from "../utils/recordUtils";
|
import {downloadHtml} from '../utils/recordUtils';
|
||||||
|
|
||||||
const RecordeReplayPage = () => {
|
const RecordeReplayPage = () => {
|
||||||
// const [isRecording, setIsRecording] = useState(false);
|
// const [isRecording, setIsRecording] = useState(false);
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
console.log('Background script loaded');
|
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|
||||||
console.log('收到消息:', request);
|
|
||||||
|
|
||||||
if (request.action === 'copy') {
|
|
||||||
console.log('开始复制文本:', request.text);
|
|
||||||
|
|
||||||
navigator.clipboard.writeText(request.text)
|
|
||||||
.then(() => {
|
|
||||||
console.log('复制成功');
|
|
||||||
sendResponse({success: true});
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error('复制失败:', err);
|
|
||||||
sendResponse({success: false, error: err.message});
|
|
||||||
});
|
|
||||||
|
|
||||||
return true; // 保持消息端口开放以支持异步响应
|
|
||||||
}
|
|
||||||
|
|
||||||
// 对于未知的操作,也返回响应
|
|
||||||
sendResponse({success: false, error: '未知操作'});
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 559 B |
|
After Width: | Height: | Size: 916 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
@@ -1,43 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<meta name="theme-color" content="#000000" />
|
|
||||||
<meta
|
|
||||||
name="description"
|
|
||||||
content="Web site created using create-react-app"
|
|
||||||
/>
|
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
|
||||||
<!--
|
|
||||||
manifest.json provides metadata used when your web app is installed on a
|
|
||||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
|
||||||
-->
|
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
|
||||||
<!--
|
|
||||||
Notice the use of %PUBLIC_URL% in the tags above.
|
|
||||||
It will be replaced with the URL of the `public` folder during the build.
|
|
||||||
Only files inside the `public` folder can be referenced from the HTML.
|
|
||||||
|
|
||||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
|
||||||
work correctly both with client-side routing and a non-root public URL.
|
|
||||||
Learn how to configure a non-root public URL by running `npm run build`.
|
|
||||||
-->
|
|
||||||
<title>React App</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
||||||
<div id="root"></div>
|
|
||||||
<!--
|
|
||||||
This HTML file is a template.
|
|
||||||
If you open it directly in the browser, you will see an empty page.
|
|
||||||
|
|
||||||
You can add webfonts, meta tags, or analytics to this file.
|
|
||||||
The build step will place the bundled scripts into the <body> tag.
|
|
||||||
|
|
||||||
To begin the development, run `npm start` or `yarn start`.
|
|
||||||
To create a production bundle, use `npm run build` or `yarn build`.
|
|
||||||
-->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"manifest_version": 3,
|
|
||||||
"name": "Testing Tools",
|
|
||||||
"version": "1.0",
|
|
||||||
"description": "测试工具",
|
|
||||||
"permissions": [
|
|
||||||
"storage",
|
|
||||||
"clipboardWrite",
|
|
||||||
"activeTab",
|
|
||||||
"scripting",
|
|
||||||
"tabs"
|
|
||||||
],
|
|
||||||
"background": {
|
|
||||||
"service_worker": "background.js"
|
|
||||||
},
|
|
||||||
"icons": {
|
|
||||||
"16": "favicon.ico",
|
|
||||||
"48": "favicon.ico",
|
|
||||||
"128": "favicon.ico"
|
|
||||||
},
|
|
||||||
"action": {
|
|
||||||
"default_popup": "index.html"
|
|
||||||
},
|
|
||||||
"options_ui": {
|
|
||||||
"page": "index.html",
|
|
||||||
"open_in_tab": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# https://www.robotstxt.org/robotstxt.html
|
|
||||||
User-agent: *
|
|
||||||
Disallow:
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<svg width="72" height="72" viewBox="0 0 72 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_305_516)">
|
||||||
|
<g clip-path="url(#clip1_305_516)">
|
||||||
|
<path d="M49.0229 69.1875C54.1272 69.1875 58.265 65.0497 58.265 59.9454V50.7033H59.9454C65.0497 50.7033 69.1875 46.5655 69.1875 41.4612C69.1875 36.357 65.0497 32.2191 59.9454 32.2191H58.265V22.9771C58.265 17.8728 54.1272 13.735 49.0229 13.735H39.7809V12.0546C39.7809 6.95032 35.643 2.8125 30.5388 2.8125C25.4345 2.8125 21.2967 6.95032 21.2967 12.0546V13.735H12.0546C6.95032 13.735 2.8125 17.8728 2.8125 22.9771V32.2191H4.49288C9.59714 32.2191 13.735 36.357 13.735 41.4612C13.735 46.5655 9.59714 50.7033 4.49288 50.7033H2.8125V69.1875H21.2967V67.5071C21.2967 62.4029 25.4345 58.265 30.5388 58.265C35.643 58.265 39.7809 62.4029 39.7809 67.5071V69.1875H49.0229Z" stroke="#67D55E" stroke-width="5.625"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_305_516">
|
||||||
|
<rect width="72" height="72" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="clip1_305_516">
|
||||||
|
<rect width="72" height="72" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -1,106 +0,0 @@
|
|||||||
import {HashRouter as Router, Routes, Route, NavLink} from 'react-router-dom';
|
|
||||||
import {useState, useEffect} from 'react';
|
|
||||||
import TimestampPage from './pages/TimestampPage';
|
|
||||||
// import ElectronicWoodenFishPage from "./pages/ElectronicWoodenFishPage";
|
|
||||||
import RecordeReplayPage from './pages/RecordeReplayPage';
|
|
||||||
import './App.css';
|
|
||||||
|
|
||||||
// 导航项配置数组,便于后续添加
|
|
||||||
// 为了测试折叠功能,我们添加更多导航项
|
|
||||||
const navItems = [
|
|
||||||
{path: '/', label: '时间戳', element: <TimestampPage/>},
|
|
||||||
// {path: '/dzmy', label: '电子木鱼', element: <ElectronicWoodenFishPage/>},
|
|
||||||
{path: '/recorde-replay', label: '录制与回放', element: <RecordeReplayPage/>},
|
|
||||||
];
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
||||||
const [isMobile, setIsMobile] = useState(false);
|
|
||||||
const [visibleItems, setVisibleItems] = useState(navItems.length);
|
|
||||||
|
|
||||||
// 检测屏幕尺寸变化
|
|
||||||
useEffect(() => {
|
|
||||||
const handleResize = () => {
|
|
||||||
const width = window.innerWidth;
|
|
||||||
setIsMobile(width < 768);
|
|
||||||
|
|
||||||
// 根据屏幕宽度决定显示多少个导航项
|
|
||||||
if (width >= 768) {
|
|
||||||
setVisibleItems(navItems.length); // 大屏幕显示所有
|
|
||||||
} else if (width >= 480) {
|
|
||||||
// 中等屏幕:如果导航项超过3个,显示3个,否则显示全部
|
|
||||||
setVisibleItems(Math.min(3, navItems.length));
|
|
||||||
} else {
|
|
||||||
// 小屏幕:如果导航项超过2个,显示2个,否则显示全部
|
|
||||||
setVisibleItems(Math.min(2, navItems.length));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleResize(); // 初始调用
|
|
||||||
window.addEventListener('resize', handleResize);
|
|
||||||
return () => window.removeEventListener('resize', handleResize);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 计算哪些导航项应该显示,哪些应该折叠
|
|
||||||
const visibleNavItems = navItems.slice(0, visibleItems);
|
|
||||||
const collapsedNavItems = navItems.slice(visibleItems);
|
|
||||||
|
|
||||||
return (<Router>
|
|
||||||
|
|
||||||
<div className="app">
|
|
||||||
<nav className="nav">
|
|
||||||
<div className="nav-content">
|
|
||||||
<ul className={`nav-list ${isMenuOpen ? 'open' : ''}`}>
|
|
||||||
{visibleNavItems.map((item) => (
|
|
||||||
<li key={item.path}>
|
|
||||||
<NavLink
|
|
||||||
to={item.path}
|
|
||||||
className={({isActive}) => isActive ? "nav-link active" : "nav-link"}
|
|
||||||
onClick={() => isMobile && setIsMenuOpen(false)}
|
|
||||||
>
|
|
||||||
{item.label}
|
|
||||||
</NavLink>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* 折叠的导航项 */}
|
|
||||||
{collapsedNavItems.length > 0 && (
|
|
||||||
<li className="nav-collapse-item">
|
|
||||||
<div className={`nav-collapse-content ${isMenuOpen ? 'show' : ''}`}>
|
|
||||||
{isMenuOpen && collapsedNavItems.map((item) => (
|
|
||||||
<NavLink
|
|
||||||
key={item.path}
|
|
||||||
to={item.path}
|
|
||||||
className={({isActive}) => isActive ? "nav-link active" : "nav-link"}
|
|
||||||
onClick={() => setIsMenuOpen(false)}
|
|
||||||
>
|
|
||||||
{item.label}
|
|
||||||
</NavLink>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className="nav-toggle"
|
|
||||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
|
||||||
aria-label={isMenuOpen ? "收起菜单" : "展开菜单"}
|
|
||||||
>
|
|
||||||
<span className="nav-toggle-icon">{isMenuOpen ? '×' : '☰'}</span>
|
|
||||||
{collapsedNavItems.length > 0 && !isMenuOpen && (
|
|
||||||
<span className="nav-collapse-count">+{collapsedNavItems.length}</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<Routes>
|
|
||||||
{navItems.map((item) => (
|
|
||||||
<Route key={item.path} path={item.path} element={item.element}/>
|
|
||||||
))}
|
|
||||||
</Routes>
|
|
||||||
</div>
|
|
||||||
</Router>);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
// import { render, screen } from '@testing-library/react';
|
|
||||||
// import App from './App';
|
|
||||||
|
|
||||||
// test('renders learn react link', () => {
|
|
||||||
// render(<App />);
|
|
||||||
// const linkElement = screen.getByText(/learn react/i);
|
|
||||||
// expect(linkElement).toBeInTheDocument();
|
|
||||||
// });
|
|
||||||
|
Before Width: | Height: | Size: 38 KiB |
@@ -1,18 +0,0 @@
|
|||||||
const TodoList = ({todoContentList = []}) => {
|
|
||||||
|
|
||||||
const todoLi = todoContentList?.map((todo, index) => {
|
|
||||||
return (
|
|
||||||
<li key={index}>{todo}</li>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="todo-list">
|
|
||||||
<ul>
|
|
||||||
{todoLi}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TodoList;
|
|
||||||
@@ -1,309 +0,0 @@
|
|||||||
/* Markdown Editor Styles */
|
|
||||||
.markdown-editor {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar {
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
border: 1px solid #e9ecef;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 15px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-section {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-label {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #495057;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.format-buttons {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.format-button {
|
|
||||||
padding: 6px 12px;
|
|
||||||
background-color: #fff;
|
|
||||||
border: 1px solid #ced4da;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #495057;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
min-width: 36px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.format-button:hover {
|
|
||||||
background-color: #e9ecef;
|
|
||||||
border-color: #adb5bd;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.format-button:active {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-button {
|
|
||||||
padding: 8px 16px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clear-button {
|
|
||||||
background-color: #dc3545;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clear-button:hover {
|
|
||||||
background-color: #c82333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reset-button {
|
|
||||||
background-color: #6c757d;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reset-button:hover {
|
|
||||||
background-color: #5a6268;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.toolbar {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-section {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.format-buttons {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-label {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-container {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.editor-container {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-section,
|
|
||||||
.preview-section {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-section label,
|
|
||||||
.preview-section label {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #444;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-input {
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 400px;
|
|
||||||
padding: 15px;
|
|
||||||
border: 2px solid #ddd;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.5;
|
|
||||||
resize: vertical;
|
|
||||||
transition: border-color 0.2s ease;
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #4a90e2;
|
|
||||||
background-color: #fff;
|
|
||||||
box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-input::placeholder {
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-preview {
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 400px;
|
|
||||||
max-height: 600px;
|
|
||||||
padding: 15px;
|
|
||||||
border: 2px solid #ddd;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow-y: auto;
|
|
||||||
background-color: #fff;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-preview h1,
|
|
||||||
.markdown-preview h2,
|
|
||||||
.markdown-preview h3,
|
|
||||||
.markdown-preview h4,
|
|
||||||
.markdown-preview h5,
|
|
||||||
.markdown-preview h6 {
|
|
||||||
margin-top: 1.5em;
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.25;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-preview h1 {
|
|
||||||
font-size: 2em;
|
|
||||||
border-bottom: 2px solid #eaeaea;
|
|
||||||
padding-bottom: 0.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-preview h2 {
|
|
||||||
font-size: 1.5em;
|
|
||||||
border-bottom: 1px solid #eaeaea;
|
|
||||||
padding-bottom: 0.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-preview p {
|
|
||||||
margin: 0 0 1em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-preview ul,
|
|
||||||
.markdown-preview ol {
|
|
||||||
padding-left: 2em;
|
|
||||||
margin: 0 0 1em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-preview li {
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-preview code {
|
|
||||||
padding: 0.2em 0.4em;
|
|
||||||
margin: 0;
|
|
||||||
font-size: 85%;
|
|
||||||
background-color: rgba(27, 31, 35, 0.05);
|
|
||||||
border-radius: 8px;
|
|
||||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-preview pre {
|
|
||||||
padding: 16px;
|
|
||||||
overflow: auto;
|
|
||||||
font-size: 85%;
|
|
||||||
line-height: 1.45;
|
|
||||||
background-color: #f6f8fa;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin: 0 0 1em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-preview pre code {
|
|
||||||
padding: 0;
|
|
||||||
background-color: transparent;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-preview blockquote {
|
|
||||||
padding: 0 1em;
|
|
||||||
color: #6a737d;
|
|
||||||
border-left: 0.25em solid #dfe2e5;
|
|
||||||
margin: 0 0 1em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-preview table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin: 0 0 1em 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-preview table th,
|
|
||||||
.markdown-preview table td {
|
|
||||||
padding: 6px 13px;
|
|
||||||
border: 1px solid #dfe2e5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-preview table th {
|
|
||||||
font-weight: 600;
|
|
||||||
background-color: #f6f8fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-preview table tr:nth-child(2n) {
|
|
||||||
background-color: #f6f8fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-preview a {
|
|
||||||
color: #0366d6;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-preview a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-preview img {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-preview hr {
|
|
||||||
height: 0.25em;
|
|
||||||
padding: 0;
|
|
||||||
margin: 24px 0;
|
|
||||||
background-color: #e1e4e8;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scrollbar styling */
|
|
||||||
.markdown-preview::-webkit-scrollbar {
|
|
||||||
width: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-preview::-webkit-scrollbar-track {
|
|
||||||
background: #f1f1f1;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-preview::-webkit-scrollbar-thumb {
|
|
||||||
background: #c1c1c1;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-preview::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: #a8a8a8;
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import './TodoMarkdownEditor.css';
|
|
||||||
import ReactMarkdown from "react-markdown";
|
|
||||||
import {useState, useRef} from "react";
|
|
||||||
|
|
||||||
const TodoMarkdownEditor = () => {
|
|
||||||
const [markdown, setMarkdown] = useState('# Hello, world!\n\nThis is a simple paragraph with some **bold** text.');
|
|
||||||
const textareaRef = useRef(null);
|
|
||||||
const handleClear = () => {
|
|
||||||
setMarkdown('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReset = () => {
|
|
||||||
setMarkdown('# Hello, world!\n\nThis is a simple paragraph with some **bold** text.');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (<div className="markdown-editor">
|
|
||||||
<div className="editor-container">
|
|
||||||
<div className="preview-section">
|
|
||||||
<label>预览区</label>
|
|
||||||
<div className="markdown-preview">
|
|
||||||
<ReactMarkdown>{markdown}</ReactMarkdown>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="input-section">
|
|
||||||
<label htmlFor="markdown-input">编辑区</label>
|
|
||||||
<textarea
|
|
||||||
id="markdown-input"
|
|
||||||
ref={textareaRef}
|
|
||||||
placeholder="输入Markdown内容..."
|
|
||||||
className="markdown-input"
|
|
||||||
value={markdown}
|
|
||||||
onChange={(e) => setMarkdown(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="toolbar">
|
|
||||||
<div className="toolbar-section">
|
|
||||||
<button className="action-button clear-button" onClick={handleClear}>
|
|
||||||
清空
|
|
||||||
</button>
|
|
||||||
<button className="action-button reset-button" onClick={handleReset}>
|
|
||||||
重置
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TodoMarkdownEditor;
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom/client';
|
|
||||||
import './index.css';
|
|
||||||
import App from './App';
|
|
||||||
import reportWebVitals from './reportWebVitals';
|
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
|
||||||
root.render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<App />
|
|
||||||
</React.StrictMode>
|
|
||||||
);
|
|
||||||
|
|
||||||
// If you want to start measuring performance in your app, pass a function
|
|
||||||
// to log results (for example: reportWebVitals(console.log))
|
|
||||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
|
||||||
reportWebVitals();
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -1,445 +0,0 @@
|
|||||||
/* CSS 变量定义 */
|
|
||||||
:root {
|
|
||||||
--dzmy-bg-color: #0a0a0a;
|
|
||||||
--dzmy-bg-gradient: linear-gradient(135deg, #1a1a1a 0%, #0a0a0a 100%);
|
|
||||||
--dzmy-text-color: #ffffff;
|
|
||||||
--dzmy-accent-color: #ffd700;
|
|
||||||
--dzmy-accent-glow: 0 0 20px rgba(255, 215, 0, 0.3);
|
|
||||||
--dzmy-container-height: 500px;
|
|
||||||
--dzmy-img-width: 160px;
|
|
||||||
--dzmy-img-height: 120px;
|
|
||||||
--dzmy-animation-duration: 0.12s;
|
|
||||||
--dzmy-float-duration: 0.5s;
|
|
||||||
--dzmy-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
|
||||||
--dzmy-border-radius: 8px;
|
|
||||||
--dzmy-controls-bg: rgba(255, 255, 255, 0.05);
|
|
||||||
--dzmy-controls-border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 主容器 */
|
|
||||||
.dzmy-bg {
|
|
||||||
background: var(--dzmy-bg-gradient);
|
|
||||||
min-height: var(--dzmy-container-height);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
position: relative;
|
|
||||||
border-radius: var(--dzmy-border-radius);
|
|
||||||
box-shadow: var(--dzmy-shadow);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
|
||||||
overflow: hidden;
|
|
||||||
padding: 20px;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 添加装饰性背景元素 */
|
|
||||||
.dzmy-bg::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 1px;
|
|
||||||
background: linear-gradient(90deg, transparent, rgba(255, 215, 0, 0.3), transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dzmy-bg::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 1px;
|
|
||||||
background: linear-gradient(90deg, transparent, rgba(255, 215, 0, 0.3), transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 计数显示 */
|
|
||||||
.dzmy-count {
|
|
||||||
position: absolute;
|
|
||||||
top: 20px;
|
|
||||||
left: 20px;
|
|
||||||
color: var(--dzmy-accent-color);
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 700;
|
|
||||||
z-index: 10;
|
|
||||||
background: rgba(0, 0, 0, 0.3);
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-radius: var(--dzmy-border-radius);
|
|
||||||
border: 1px solid rgba(255, 215, 0, 0.2);
|
|
||||||
box-shadow: var(--dzmy-accent-glow);
|
|
||||||
backdrop-filter: blur(4px);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dzmy-count:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 0 30px rgba(255, 215, 0, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 木鱼图像 */
|
|
||||||
.dzmy-img {
|
|
||||||
width: var(--dzmy-img-width);
|
|
||||||
height: var(--dzmy-img-height);
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 100%;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all var(--dzmy-animation-duration) ease;
|
|
||||||
transform: scale(1);
|
|
||||||
will-change: transform;
|
|
||||||
touch-action: manipulation;
|
|
||||||
user-select: none;
|
|
||||||
object-fit: contain;
|
|
||||||
filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.5));
|
|
||||||
position: relative;
|
|
||||||
z-index: 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dzmy-img:hover {
|
|
||||||
filter: drop-shadow(0 6px 20px rgba(255, 215, 0, 0.3));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 木鱼点击激活状态 */
|
|
||||||
.dzmy-img.active {
|
|
||||||
transform: scale(0.92);
|
|
||||||
filter: drop-shadow(0 2px 8px rgba(255, 215, 0, 0.5));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 添加点击涟漪效果 */
|
|
||||||
.dzmy-img::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-radius: var(--dzmy-border-radius);
|
|
||||||
background: radial-gradient(circle, rgba(255, 215, 0, 0.3) 0%, transparent 70%);
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
opacity: 0;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dzmy-img.active::after {
|
|
||||||
width: 200px;
|
|
||||||
height: 200px;
|
|
||||||
opacity: 1;
|
|
||||||
animation: ripple 0.3s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes ripple {
|
|
||||||
0% {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
width: 200px;
|
|
||||||
height: 200px;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 木鱼和提示容器 */
|
|
||||||
.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 var(--dzmy-float-duration) ease-out forwards;
|
|
||||||
color: var(--dzmy-accent-color);
|
|
||||||
font-weight: 800;
|
|
||||||
will-change: transform, opacity;
|
|
||||||
transform: translateZ(0);
|
|
||||||
font-size: 28px;
|
|
||||||
text-shadow:
|
|
||||||
0 0 10px rgba(255, 215, 0, 0.8),
|
|
||||||
0 0 20px rgba(255, 215, 0, 0.4),
|
|
||||||
0 0 30px rgba(255, 215, 0, 0.2);
|
|
||||||
letter-spacing: 1px;
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 0;
|
|
||||||
animation-delay: 0.1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 浮动动画 */
|
|
||||||
@keyframes floatUp {
|
|
||||||
0% {
|
|
||||||
transform: translate3d(0, 0, 0) scale(0.8);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
20% {
|
|
||||||
transform: translate3d(0, -10px, 0) scale(1.1);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translate3d(0, -80px, 0) scale(0.9);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 控制区域 */
|
|
||||||
.dzmy-controls {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 16px 24px;
|
|
||||||
background: var(--dzmy-controls-bg);
|
|
||||||
border-radius: var(--dzmy-border-radius);
|
|
||||||
margin-top: 20px;
|
|
||||||
border: var(--dzmy-controls-border);
|
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dzmy-controls:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.08);
|
|
||||||
border-color: rgba(255, 215, 0, 0.2);
|
|
||||||
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 控制区域内的输入框 */
|
|
||||||
.dzmy-controls input {
|
|
||||||
width: auto;
|
|
||||||
min-width: 140px;
|
|
||||||
margin-bottom: 0;
|
|
||||||
flex: 1;
|
|
||||||
max-width: 240px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
border-radius: var(--dzmy-border-radius);
|
|
||||||
background: rgba(0, 0, 0, 0.3);
|
|
||||||
color: var(--dzmy-text-color);
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dzmy-controls input::placeholder {
|
|
||||||
color: rgba(255, 255, 255, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dzmy-controls input:focus {
|
|
||||||
border-color: var(--dzmy-accent-color);
|
|
||||||
box-shadow: 0 0 0 3px rgba(255, 215, 0, 0.1);
|
|
||||||
background: rgba(0, 0, 0, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dzmy-controls input:hover {
|
|
||||||
border-color: rgba(255, 215, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 控制区域内的按钮 */
|
|
||||||
.dzmy-controls .action-btn {
|
|
||||||
margin-right: 0;
|
|
||||||
opacity: 1;
|
|
||||||
flex-shrink: 0;
|
|
||||||
padding: 12px 24px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
border-radius: var(--dzmy-border-radius);
|
|
||||||
background: linear-gradient(135deg, #f32152 0%, #ff0000 100%);
|
|
||||||
border: none;
|
|
||||||
color: white;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
min-width: 100px;
|
|
||||||
box-shadow: 0 4px 12px rgba(243, 33, 82, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dzmy-controls .action-btn:hover {
|
|
||||||
opacity: 0.95;
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 6px 20px rgba(243, 33, 82, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dzmy-controls .action-btn:active {
|
|
||||||
opacity: 0.9;
|
|
||||||
transform: translateY(0);
|
|
||||||
box-shadow: 0 2px 8px rgba(243, 33, 82, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dzmy-controls .action-btn:focus {
|
|
||||||
outline: none;
|
|
||||||
box-shadow: 0 0 0 3px rgba(243, 33, 82, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 响应式设计 */
|
|
||||||
|
|
||||||
/* 平板设备 */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
:root {
|
|
||||||
--dzmy-container-height: 400px;
|
|
||||||
--dzmy-img-width: 140px;
|
|
||||||
--dzmy-img-height: 105px;
|
|
||||||
--dzmy-border-radius: var(--dzmy-border-radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dzmy-count {
|
|
||||||
font-size: 16px;
|
|
||||||
top: 16px;
|
|
||||||
left: 16px;
|
|
||||||
padding: 6px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tip-item {
|
|
||||||
font-size: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dzmy-controls {
|
|
||||||
padding: 14px 20px;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dzmy-controls input {
|
|
||||||
min-width: 120px;
|
|
||||||
max-width: 180px;
|
|
||||||
padding: 10px 14px;
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dzmy-controls .action-btn {
|
|
||||||
padding: 10px 20px;
|
|
||||||
font-size: 15px;
|
|
||||||
min-width: 90px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 大手机设备 */
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
:root {
|
|
||||||
--dzmy-container-height: 350px;
|
|
||||||
--dzmy-img-width: 120px;
|
|
||||||
--dzmy-img-height: 90px;
|
|
||||||
--dzmy-border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dzmy-count {
|
|
||||||
font-size: 14px;
|
|
||||||
top: 12px;
|
|
||||||
left: 12px;
|
|
||||||
padding: 5px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tip-item {
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dzmy-controls {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
padding: 12px 16px;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dzmy-controls input {
|
|
||||||
min-width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
padding: 10px 12px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dzmy-controls .action-btn {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px 16px;
|
|
||||||
font-size: 14px;
|
|
||||||
min-width: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 小手机设备 */
|
|
||||||
@media (max-width: 360px) {
|
|
||||||
:root {
|
|
||||||
--dzmy-container-height: 300px;
|
|
||||||
--dzmy-img-width: 100px;
|
|
||||||
--dzmy-img-height: 75px;
|
|
||||||
--dzmy-border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dzmy-count {
|
|
||||||
font-size: 12px;
|
|
||||||
top: 10px;
|
|
||||||
left: 10px;
|
|
||||||
padding: 4px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tip-item {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dzmy-controls {
|
|
||||||
padding: 10px 12px;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dzmy-controls input {
|
|
||||||
padding: 8px 10px;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dzmy-controls .action-btn {
|
|
||||||
padding: 8px 12px;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 超小手机设备 */
|
|
||||||
@media (max-width: 320px) {
|
|
||||||
:root {
|
|
||||||
--dzmy-container-height: 280px;
|
|
||||||
--dzmy-img-width: 90px;
|
|
||||||
--dzmy-img-height: 68px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dzmy-count {
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tip-item {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 横屏模式优化 */
|
|
||||||
@media (max-height: 500px) and (orientation: landscape) {
|
|
||||||
:root {
|
|
||||||
--dzmy-container-height: 250px;
|
|
||||||
--dzmy-img-width: 100px;
|
|
||||||
--dzmy-img-height: 75px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dzmy-controls {
|
|
||||||
flex-direction: row;
|
|
||||||
padding: 8px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dzmy-controls input {
|
|
||||||
min-width: 120px;
|
|
||||||
max-width: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dzmy-controls .action-btn {
|
|
||||||
min-width: 80px;
|
|
||||||
padding: 6px 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,285 +0,0 @@
|
|||||||
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 () => {
|
|
||||||
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('金钱');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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 () => {
|
|
||||||
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 = 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 (
|
|
||||||
<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">
|
|
||||||
{tip.label}
|
|
||||||
</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;
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
const UserListPage = () => {
|
|
||||||
// Sample user data (replace with API calls later)
|
|
||||||
const users = [
|
|
||||||
{ id: 1, name: 'Alice', email: 'alice@example.com' },
|
|
||||||
{ id: 2, name: 'Bob', email: 'bob@example.com' },
|
|
||||||
{ id: 3, name: 'Charlie', email: 'charlie@example.com' },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="page">
|
|
||||||
<h1>User List</h1>
|
|
||||||
<p>Click "Actions" to manage users.</p>
|
|
||||||
|
|
||||||
<ul className="user-list">
|
|
||||||
{users.map((user) => (
|
|
||||||
<li key={user.id}>
|
|
||||||
<strong>{user.name}</strong> ({user.email})
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
{/* Link to ActionsPage */}
|
|
||||||
<Link to="/actions" className="btn">
|
|
||||||
Go to Actions Page
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default UserListPage;
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
const reportWebVitals = onPerfEntry => {
|
|
||||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
|
||||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
|
||||||
getCLS(onPerfEntry);
|
|
||||||
getFID(onPerfEntry);
|
|
||||||
getFCP(onPerfEntry);
|
|
||||||
getLCP(onPerfEntry);
|
|
||||||
getTTFB(onPerfEntry);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default reportWebVitals;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
|
||||||
// allows you to do things like:
|
|
||||||
// expect(element).toHaveTextContent(/react/i)
|
|
||||||
// learn more: https://github.com/testing-library/jest-dom
|
|
||||||
import '@testing-library/jest-dom';
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "./.wxt/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { defineConfig } from 'wxt';
|
||||||
|
|
||||||
|
// See https://wxt.dev/api/config.html
|
||||||
|
export default defineConfig({
|
||||||
|
modules: ['@wxt-dev/module-react'],
|
||||||
|
manifest: {
|
||||||
|
name: 'Testing Tools',
|
||||||
|
version: '1.0',
|
||||||
|
description: '测试工具',
|
||||||
|
permissions: [
|
||||||
|
'storage',
|
||||||
|
'clipboardWrite',
|
||||||
|
'activeTab',
|
||||||
|
'scripting',
|
||||||
|
'tabs',
|
||||||
|
],
|
||||||
|
action: {
|
||||||
|
default_title: 'Testing Tools',
|
||||||
|
},
|
||||||
|
// 注意:WXT 会根据 entrypoints/options/index.html 自动生成 options_ui 的 page 路径
|
||||||
|
options_ui: {
|
||||||
|
open_in_tab: true,
|
||||||
|
},
|
||||||
|
// 将 favicon.ico 放入 public/ 文件夹中
|
||||||
|
icons: {
|
||||||
|
16: 'favicon.ico',
|
||||||
|
48: 'favicon.ico',
|
||||||
|
128: 'favicon.ico',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||