From 22985f4c1890b4df72dbc0bda08e054a9cf36669 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=A8=E9=9C=96=E9=93=83?= Date: Fri, 13 Feb 2026 17:29:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E6=B7=BB=E5=8A=A0=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E5=B9=B6=E4=BC=98=E5=8C=96=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E6=88=B3=E5=B7=A5=E5=85=B7=20refactor:=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=97=B6=E9=97=B4=E6=88=B3=E8=BD=AC=E6=8D=A2=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E5=8F=8A=E6=95=B4=E4=BD=93=E4=BB=A3=E7=A0=81=E7=BB=93=E6=9E=84?= =?UTF-8?q?=20chore:=20=E6=9B=B4=E6=96=B0=E6=9E=84=E5=BB=BA=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=92=8C=E5=8C=85=E4=BE=9D=E8=B5=96=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 5 + .gitignore | 1 - entrypoints/background.ts | 19 +-- entrypoints/content.ts | 4 + entrypoints/popup/App.tsx | 28 ++-- entrypoints/popup/components/CopyButton.tsx | 134 ++++++++++++++++++ .../popup/components/DatetimeToTimestamp.tsx | 4 - .../popup/components/TimestampExecution.tsx | 36 +++-- .../popup/components/TimestampToDatetime.tsx | 4 - entrypoints/popup/components/useCopy.ts | 39 ----- entrypoints/popup/hook/useCopy.ts | 39 ----- entrypoints/popup/pages/TestPage.tsx | 22 +++ package-lock.json | 107 ++++++++------ package.json | 2 +- utils/messages.tsx | 8 ++ web-ext.config.ts | 6 + wxt.config.ts | 3 +- 17 files changed, 281 insertions(+), 180 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 entrypoints/popup/components/CopyButton.tsx delete mode 100644 entrypoints/popup/components/useCopy.ts delete mode 100644 entrypoints/popup/hook/useCopy.ts create mode 100644 entrypoints/popup/pages/TestPage.tsx create mode 100644 web-ext.config.ts diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..fc9d25b --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,5 @@ +{ + "permissions": { + "allow": ["WebSearch"] + } +} diff --git a/.gitignore b/.gitignore index a256953..c809157 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,6 @@ node_modules stats.html stats-*.json .wxt -web-ext.config.ts # Editor directories and files .vscode/* diff --git a/entrypoints/background.ts b/entrypoints/background.ts index 8de3338..975f94a 100644 --- a/entrypoints/background.ts +++ b/entrypoints/background.ts @@ -61,10 +61,7 @@ export default defineBackground(() => { }) .catch((error: Error) => { if (error.message.includes('restricted URL')) { - console.warn( - 'Could not check status: on a restricted page.', - error.message, - ); + console.warn('Could not check status: on a restricted page.', error.message); } else if (error.message.includes('No active tab found')) { console.warn('Could not check status: no active tab found.'); } else { @@ -84,10 +81,7 @@ export default defineBackground(() => { }) .catch((error: Error) => { if (error.message.includes('restricted URL')) { - console.warn( - 'Could not start recording: on a restricted page.', - error.message, - ); + console.warn('Could not start recording: on a restricted page.', error.message); } else if (error.message.includes('No active tab found')) { console.warn('Could not start recording: no active tab found.'); } else { @@ -107,10 +101,7 @@ export default defineBackground(() => { }) .catch((error: Error) => { if (error.message.includes('restricted URL')) { - console.warn( - 'Could not stop recording: on a restricted page.', - error.message, - ); + console.warn('Could not stop recording: on a restricted page.', error.message); } else if (error.message.includes('No active tab found')) { console.warn('Could not stop recording: no active tab found.'); } else { @@ -132,6 +123,10 @@ export default defineBackground(() => { } }); + onMessage('getStringLength', (message) => { + return message.data.length; + }); + async function sendToActiveTab(message: { type: string; data?: unknown; diff --git a/entrypoints/content.ts b/entrypoints/content.ts index ff6cb6e..caab129 100644 --- a/entrypoints/content.ts +++ b/entrypoints/content.ts @@ -15,6 +15,10 @@ export default defineContentScript({ const recorder = createRecorder(); let isRecording = false; + onMessage('getStringLength', (message) => { + console.log(`[content] getStringLength received: ${message.data}`); + }); + // 监听来自 background script 的消息 chrome.runtime.onMessage.addListener( (msg: { type: string }, _sender, sendResponse: (response: IResponse) => void) => { diff --git a/entrypoints/popup/App.tsx b/entrypoints/popup/App.tsx index ea6dcc2..c7a51f1 100644 --- a/entrypoints/popup/App.tsx +++ b/entrypoints/popup/App.tsx @@ -1,38 +1,32 @@ // App.js import { HashRouter as Router, Routes, Route } from 'react-router-dom'; -import { ThemeProvider, createTheme } from '@mui/material/styles'; -import CssBaseline from '@mui/material/CssBaseline'; -import { SnackbarProvider } from 'notistack'; import TimestampPage from './pages/TimestampPage'; import RecordeReplayPage from './pages/RecordeReplayPage'; +import TestPage from './pages/TestPage'; import Navbar from './components/Navbar'; import RoutePersistence from './components/RoutePersistence'; import './App.css'; // 路由配置数据 const navItems = [ + { path: '/test', label: '测试页面', element: }, { path: '/', label: '时间戳', element: }, - // {path: '/dzmy', label: '电子木鱼', element: }, { path: '/recorde-replay', label: '录制与回放', element: }, ]; -const theme = createTheme(); - function App() { return ( - - - - - - {navItems.map((item) => ( - - ))} - - - +
+ + + + {navItems.map((item) => ( + + ))} + +
); } diff --git a/entrypoints/popup/components/CopyButton.tsx b/entrypoints/popup/components/CopyButton.tsx new file mode 100644 index 0000000..fd196bf --- /dev/null +++ b/entrypoints/popup/components/CopyButton.tsx @@ -0,0 +1,134 @@ +import { useState, useCallback, FC, ReactNode, useEffect } from 'react'; +import Button, { ButtonProps } from '@mui/material/Button'; +import Snackbar from '@mui/material/Snackbar'; +import Alert, { AlertColor } from '@mui/material/Alert'; +import ContentCopyIcon from '@mui/icons-material/ContentCopy'; +import CheckIcon from '@mui/icons-material/Check'; + +type CopyStatus = 'idle' | 'copying' | 'success' | 'error'; + +interface CopyButtonProps extends Omit { + textToCopy: string | number; + buttonText?: ReactNode; + successMessage?: string; + errorMessage?: string; + variant?: ButtonProps['variant']; +} + +const CopyButton: FC = ({ + textToCopy, + buttonText = '复制', + successMessage = '复制成功!', + errorMessage = '复制失败,请手动复制。', + variant = 'contained', + ...buttonProps +}) => { + const [status, setStatus] = useState('idle'); + const [openSnackbar, setOpenSnackbar] = useState(false); + const [snackbarContent, setSnackbarContent] = useState<{ + message: string; + severity: AlertColor; + } | null>(null); + + useEffect(() => { + if (status === 'success' || status === 'error') { + const timer = setTimeout(() => setStatus('idle'), 2000); + return () => clearTimeout(timer); + } + return undefined; + }, [status]); + + const performCopy = useCallback(async () => { + if (!textToCopy) { + console.warn('没有提供要复制的文本'); + return false; + } + const safeText = String(textToCopy); + try { + await navigator.clipboard.writeText(safeText); + return true; + } catch (err) { + console.error('使用 Clipboard API 复制失败:', err); + return false; + } + }, [textToCopy]); + + const handleClick = useCallback(async () => { + if (status !== 'idle') return; + + setStatus('copying'); + let isSuccess = false; + try { + [isSuccess] = await Promise.all([ + performCopy(), + new Promise((resolve) => setTimeout(resolve, 300)), + ]); + } catch (error) { + console.error('复制时出错:', error); + isSuccess = false; + } finally { + const newStatus = isSuccess ? 'success' : 'error'; + setStatus(newStatus); + setSnackbarContent({ + message: isSuccess ? successMessage : errorMessage, + severity: newStatus, + }); + setOpenSnackbar(true); + } + }, [performCopy, status, successMessage, errorMessage]); + + const handleCloseSnackbar = (_event?: Event | React.SyntheticEvent, reason?: string) => { + if (reason === 'clickaway') return; + setOpenSnackbar(false); + }; + + const renderButtonIcon = () => { + if (status === 'success') { + return ; + } + return ; + }; + + return ( + <> + + + {snackbarContent ? ( + + {snackbarContent.message} + + ) : undefined} + + + ); +}; + +export default CopyButton; diff --git a/entrypoints/popup/components/DatetimeToTimestamp.tsx b/entrypoints/popup/components/DatetimeToTimestamp.tsx index 8a6893e..9bdc2f6 100644 --- a/entrypoints/popup/components/DatetimeToTimestamp.tsx +++ b/entrypoints/popup/components/DatetimeToTimestamp.tsx @@ -8,7 +8,6 @@ import { FormControl, InputLabel, Paper, - Typography, Stack, Box, SelectChangeEvent, @@ -87,9 +86,6 @@ export function DatetimeToTimestamp() { return ( - - 日期时间转时间戳 - Math.floor(Date.now())); + const [currentTimestamp, setCurrentTimestamp] = useState(() => + Math.floor(Date.now()), + ); const [showMilliseconds, setShowMilliseconds] = useState(true); const [isRunningTimestamp, setIsRunningTimestamp] = useState(true); @@ -45,14 +45,19 @@ export function TimestampExecution() { setIsRunningTimestamp((prev) => !prev); }, []); - const unitButtonLabel = showMilliseconds ? '切换为秒显示' : '切换为毫秒显示'; - const toggleButtonLabel = isRunningTimestamp ? '停止时间戳自动更新' : '开始时间戳自动更新'; + const unitButtonLabel = showMilliseconds + ? '切换为秒显示' + : '切换为毫秒显示'; + const toggleButtonLabel = isRunningTimestamp + ? '停止时间戳自动更新' + : '开始时间戳自动更新'; const toggleButtonText = isRunningTimestamp ? '停止' : '开始'; - const { isCopied, copy } = useCopy(); - return ( - + - {/* */} - - + /> + + ); +}; + +export default TestPage; diff --git a/package-lock.json b/package-lock.json index d655a6d..a6e8199 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,11 +20,11 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.1", "@testing-library/user-event": "^13.5.0", + "@webext-core/messaging": "^2.3.0", "date-fns": "^4.1.0", "dayjs": "^1.11.19", "dexie": "^4.2.1", "dexie-react-hooks": "^4.2.0", - "notistack": "^3.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "react-markdown": "^6.0.3", @@ -1450,6 +1450,15 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@mui/core-downloads-tracker": { "version": "7.3.8", "resolved": "https://registry.npmmirror.com/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.8.tgz", @@ -2875,6 +2884,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@webext-core/messaging": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/@webext-core/messaging/-/messaging-2.3.0.tgz", + "integrity": "sha512-gChSVKdRs7JEq5hFH0jVROSvTq+sKq9afXTA/gBswep3RWNLhXyDsXFlvPMkYbmML1XZ8QKKC9ou2MlCKRZwSQ==", + "license": "MIT", + "dependencies": { + "serialize-error": "^11.0.0", + "uid": "^2.0.2", + "webextension-polyfill": "^0.10.0" + } + }, "node_modules/@wxt-dev/browser": { "version": "0.1.32", "resolved": "https://registry.npmmirror.com/@wxt-dev/browser/-/browser-0.1.32.tgz", @@ -5896,15 +5916,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/goober": { - "version": "2.1.18", - "resolved": "https://registry.npmmirror.com/goober/-/goober-2.1.18.tgz", - "integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==", - "license": "MIT", - "peerDependencies": { - "csstype": "^3.0.10" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", @@ -8171,37 +8182,6 @@ "node": ">=0.10.0" } }, - "node_modules/notistack": { - "version": "3.0.2", - "resolved": "https://registry.npmmirror.com/notistack/-/notistack-3.0.2.tgz", - "integrity": "sha512-0R+/arLYbK5Hh7mEfR2adt0tyXJcCC9KkA2hc56FeWik2QN6Bm/S4uW+BjzDARsJth5u06nTjelSw/VSnB1YEA==", - "license": "MIT", - "dependencies": { - "clsx": "^1.1.0", - "goober": "^2.0.33" - }, - "engines": { - "node": ">=12.0.0", - "npm": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/notistack" - }, - "peerDependencies": { - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/notistack/node_modules/clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmmirror.com/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz", @@ -9673,6 +9653,33 @@ "semver": "bin/semver.js" } }, + "node_modules/serialize-error": { + "version": "11.0.3", + "resolved": "https://registry.npmmirror.com/serialize-error/-/serialize-error-11.0.3.tgz", + "integrity": "sha512-2G2y++21dhj2R7iHAdd0FIzjGwuKZld+7Pl/bTU6YIkrC2ZMbVUjm+luj6A6V34Rv9XfKJDKpTWu9W4Gse1D9g==", + "license": "MIT", + "dependencies": { + "type-fest": "^2.12.2" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-error/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz", @@ -10573,6 +10580,18 @@ "dev": true, "license": "ISC" }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "license": "MIT", + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -11045,6 +11064,12 @@ "integrity": "sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg==", "license": "Apache-2.0" }, + "node_modules/webextension-polyfill": { + "version": "0.10.0", + "resolved": "https://registry.npmmirror.com/webextension-polyfill/-/webextension-polyfill-0.10.0.tgz", + "integrity": "sha512-c5s35LgVa5tFaHhrZDnr3FpQpjj1BB+RXhLTYUxGqBVN460HkbM8TBtEqdXWbpTKfzwCcjAZVF7zXCYSKtcp9g==", + "license": "MPL-2.0" + }, "node_modules/webpack-virtual-modules": { "version": "0.6.2", "resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", diff --git a/package.json b/package.json index ac0a335..fd6600e 100644 --- a/package.json +++ b/package.json @@ -28,11 +28,11 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.1", "@testing-library/user-event": "^13.5.0", + "@webext-core/messaging": "^2.3.0", "date-fns": "^4.1.0", "dayjs": "^1.11.19", "dexie": "^4.2.1", "dexie-react-hooks": "^4.2.0", - "notistack": "^3.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "react-markdown": "^6.0.3", diff --git a/utils/messages.tsx b/utils/messages.tsx index 94d8070..6869253 100644 --- a/utils/messages.tsx +++ b/utils/messages.tsx @@ -1,3 +1,5 @@ +import { defineExtensionMessaging } from '@webext-core/messaging'; + export const messages = { popup: { from: { @@ -28,3 +30,9 @@ export const messages = { }, }, }; + +interface ProtocolMap { + getStringLength(data: string): number; +} + +export const { sendMessage, onMessage } = defineExtensionMessaging(); diff --git a/web-ext.config.ts b/web-ext.config.ts new file mode 100644 index 0000000..2e2de5d --- /dev/null +++ b/web-ext.config.ts @@ -0,0 +1,6 @@ +import { defineWebExtConfig } from 'wxt'; + +export default defineWebExtConfig({ + startUrls: ['https://www.baidu.com', 'chrome://extensions/'], + chromiumArgs: ['chrome://extensions/', '--auto-open-devtools-for-tabs'], +}); diff --git a/wxt.config.ts b/wxt.config.ts index 4eaff28..4a50750 100644 --- a/wxt.config.ts +++ b/wxt.config.ts @@ -21,7 +21,6 @@ export default defineConfig({ action: { default_title: 'Testing Tools', }, - // 注意:WXT 会根据 entrypoints/options/index.html 自动生成 options_ui 的 page 路径 options_ui: { open_in_tab: true, }, @@ -41,7 +40,7 @@ export default defineConfig({ terserOptions: { format: { ascii_only: true, - comments: false, // 去掉注释,防止注释里有乱码 + comments: false, }, }, },