From a93a8b3e9a28c624fc409ec1936ab185e64daee3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=A8=E9=9C=96=E9=93=83?= Date: Thu, 12 Feb 2026 21:26:41 +0800 Subject: [PATCH 01/10] =?UTF-8?q?refactor:=E4=BC=98=E5=8C=96=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E6=88=B3=E5=B7=A5=E5=85=B7=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?CopyButton?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- entrypoints/background.ts | 59 +- entrypoints/popup/App.css | 2 +- entrypoints/popup/components/CopyButton.tsx | 159 ++-- .../popup/components/DatetimeToTimestamp.tsx | 202 +++-- entrypoints/popup/components/Navbar.tsx | 14 +- .../popup/components/TimestampExecution.tsx | 116 +-- .../popup/components/TimestampToDatetime.tsx | 255 ++++--- entrypoints/popup/pages/RecordeReplayPage.tsx | 11 +- package-lock.json | 701 +++++++++++++++++- package.json | 4 + 10 files changed, 1124 insertions(+), 399 deletions(-) diff --git a/entrypoints/background.ts b/entrypoints/background.ts index 189c7d4..8de3338 100644 --- a/entrypoints/background.ts +++ b/entrypoints/background.ts @@ -59,8 +59,17 @@ export default defineBackground(() => { .then((res) => { sendResponse(res); }) - .catch(() => { - console.error('Failed to check status in content script'); + .catch((error: Error) => { + if (error.message.includes('restricted URL')) { + 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 { + console.error('Failed to check status in content script', error); + } sendResponse({ ok: false }); }); return true; @@ -73,8 +82,17 @@ export default defineBackground(() => { await chrome.runtime.sendMessage({ type: messages.popup.to.started }); sendResponse({ ok: true }); }) - .catch(() => { - console.error('Failed to start recording in content script'); + .catch((error: Error) => { + if (error.message.includes('restricted URL')) { + 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 { + console.error('Failed to start recording in content script', error); + } sendResponse({ ok: false }); }); } @@ -87,7 +105,17 @@ export default defineBackground(() => { downloadHtmlInBackground(events); sendResponse({ ok: true }); }) - .catch(() => { + .catch((error: Error) => { + if (error.message.includes('restricted URL')) { + 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 { + console.error('Failed to stop recording in content script', error); + } sendResponse({ ok: false }); }); } @@ -115,6 +143,27 @@ export default defineBackground(() => { currentWindow: true, }); const activeTab = activeTabs[0]; + + if (!activeTab) { + throw new Error('No active tab found.'); + } + + // 检查URL是否受限 + if (activeTab?.url) { + const restrictedProtocols = [ + 'chrome:', + 'chrome-extension:', + 'about:', + 'edge:', + 'view-source:', + 'data:', + 'file:', + ]; + if (restrictedProtocols.some((protocol) => activeTab.url!.startsWith(protocol))) { + throw new Error(`Cannot send message to a restricted URL: ${activeTab.url}`); + } + } + const sendTo = message.activeTabId || activeTab.id; try { return await chrome.tabs.sendMessage(sendTo!, message); diff --git a/entrypoints/popup/App.css b/entrypoints/popup/App.css index ba8e741..4ac0fca 100644 --- a/entrypoints/popup/App.css +++ b/entrypoints/popup/App.css @@ -32,7 +32,7 @@ body { margin: 0 auto; padding: 20px; font-family: Arial, sans-serif; - width: 100%; + width: 380px; min-width: 320px; min-height: 100%; box-sizing: border-box; diff --git a/entrypoints/popup/components/CopyButton.tsx b/entrypoints/popup/components/CopyButton.tsx index 20d467e..fd196bf 100644 --- a/entrypoints/popup/components/CopyButton.tsx +++ b/entrypoints/popup/components/CopyButton.tsx @@ -1,90 +1,133 @@ -import { useEffect, useState, useCallback } from 'react'; +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'; -const CopyButton = ({ - text = '要复制的文本', - buttonText = '复制文本', - className = 'action-btn', +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 = '复制失败,请手动复制。', - copyingMessage = '复制中...', + variant = 'contained', + ...buttonProps }) => { - const [status, setStatus] = useState('idle'); // 'idle' | 'copying' | 'success' | 'error' + const [status, setStatus] = useState('idle'); + const [openSnackbar, setOpenSnackbar] = useState(false); + const [snackbarContent, setSnackbarContent] = useState<{ + message: string; + severity: AlertColor; + } | null>(null); useEffect(() => { - let timer: number; if (status === 'success' || status === 'error') { - timer = window.setTimeout(() => { - setStatus('idle'); - }, 2000); + const timer = setTimeout(() => setStatus('idle'), 2000); + return () => clearTimeout(timer); } - - return () => clearTimeout(timer); + return undefined; }, [status]); const performCopy = useCallback(async () => { - if (!text) { + if (!textToCopy) { console.warn('没有提供要复制的文本'); return false; } - - const safeText = String(text); - - if (navigator.clipboard && navigator.clipboard.writeText) { - try { - await navigator.clipboard.writeText(safeText); - return true; - } catch (err) { - console.error('使用 Clipboard API 复制失败:', err); - return false; - } + const safeText = String(textToCopy); + try { + await navigator.clipboard.writeText(safeText); + return true; + } catch (err) { + console.error('使用 Clipboard API 复制失败:', err); + return false; } - - return false; - }, [text]); + }, [textToCopy]); const handleClick = useCallback(async () => { - setStatus('copying'); + if (status !== 'idle') return; + setStatus('copying'); + let isSuccess = false; try { - const isSuccess = await performCopy(); - setStatus(isSuccess ? 'success' : 'error'); + [isSuccess] = await Promise.all([ + performCopy(), + new Promise((resolve) => setTimeout(resolve, 300)), + ]); } catch (error) { console.error('复制时出错:', error); - setStatus('error'); + isSuccess = false; + } finally { + const newStatus = isSuccess ? 'success' : 'error'; + setStatus(newStatus); + setSnackbarContent({ + message: isSuccess ? successMessage : errorMessage, + severity: newStatus, + }); + setOpenSnackbar(true); } - }, [performCopy]); + }, [performCopy, status, successMessage, errorMessage]); - // 根据状态计算当前显示的文本 - const currentText = - status === 'success' - ? successMessage - : status === 'error' - ? errorMessage - : status === 'copying' - ? copyingMessage - : buttonText; + const handleCloseSnackbar = (_event?: Event | React.SyntheticEvent, reason?: string) => { + if (reason === 'clickaway') return; + setOpenSnackbar(false); + }; - // 动态样式:只在非默认状态下覆盖颜色,平时让 className 控制 - const getStyle = () => { + const renderButtonIcon = () => { if (status === 'success') { - return { backgroundColor: '#4CAF50', color: 'white' }; + return ; } - if (status === 'error') { - return { backgroundColor: '#f44336', color: 'white' }; - } - - return {}; + return ; }; return ( - + <> + + + {snackbarContent ? ( + + {snackbarContent.message} + + ) : undefined} + + ); }; diff --git a/entrypoints/popup/components/DatetimeToTimestamp.tsx b/entrypoints/popup/components/DatetimeToTimestamp.tsx index 52885c0..8a6893e 100644 --- a/entrypoints/popup/components/DatetimeToTimestamp.tsx +++ b/entrypoints/popup/components/DatetimeToTimestamp.tsx @@ -1,26 +1,19 @@ import { useState, useCallback } from 'react'; import dayjs from '@/utils/dayjs'; +import { + Button, + TextField, + Select, + MenuItem, + FormControl, + InputLabel, + Paper, + Typography, + Stack, + Box, + SelectChangeEvent, +} from '@mui/material'; -/** - * 日期时间转时间戳组件 - * - * 功能特性: - * 1. 将日期时间字符串转换为时间戳 - * 2. 支持多种时区选择 - * 3. 支持毫秒和秒单位切换 - * 4. 提供输入验证和错误提示 - * 5. 实时单位转换 - * - * @component - * @example - * ```jsx - * - * ``` - * - * @returns {JSX.Element} 日期时间转时间戳组件 - */ - -// 常用时区列表 const TIME_ZONE_LIST = [ 'America/New_York', 'America/Chicago', @@ -42,48 +35,32 @@ const TIME_ZONE_LIST = [ 'Pacific/Auckland', ]; -// 时间戳单位选项 const TIMESTAMP_UNITS = [ - { value: 'milliseconds', label: '毫秒(ms)' }, - { value: 'seconds', label: '秒(s)' }, + { value: 'milliseconds', label: '毫秒 (ms)' }, + { value: 'seconds', label: '秒 (s)' }, ]; export function DatetimeToTimestamp() { const [dateValue, setDateValue] = useState(() => dayjs().format('YYYY/MM/DD HH:mm:ss')); - const [selectedZone, setSelectedZone] = useState('Asia/Shanghai'); - const [result, setResult] = useState(''); - const [unit, setUnit] = useState('milliseconds'); - const [error, setError] = useState(''); const performConversion = useCallback( (currentDate: string, zone: string, currentUnit: string) => { - try { - if (!currentDate) { - setError('请输入有效的日期时间'); - return ''; - } - - const timestamp = dayjs.tz(currentDate, zone); - - if (!timestamp.isValid()) { - setError('无效的日期时间格式'); - return ''; - } - - setError(''); - - const ms = timestamp.valueOf(); - return currentUnit === TIMESTAMP_UNITS[0].value - ? ms.toString() - : Math.floor(ms / 1000).toString(); - } catch (err) { - console.error('转换错误:', err); + if (!currentDate) { + setError('请输入有效的日期时间'); return ''; } + const timestamp = dayjs.tz(currentDate, zone); + if (!timestamp.isValid()) { + setError('无效的日期时间格式'); + return ''; + } + setError(''); + const ms = timestamp.valueOf(); + return currentUnit === 'milliseconds' ? ms.toString() : Math.floor(ms / 1000).toString(); }, [], ); @@ -93,15 +70,10 @@ export function DatetimeToTimestamp() { setResult(newResult); }, [dateValue, selectedZone, unit, performConversion]); - /** - * 处理时间戳单位变化 - */ const handleUnitChange = useCallback( - (e: React.ChangeEvent) => { + (e: SelectChangeEvent) => { const newUnit = e.target.value; setUnit(newUnit); - - // 如果已有结果,重新计算 if (result) { setResult(performConversion(dateValue, selectedZone, newUnit) || ''); } @@ -109,77 +81,79 @@ export function DatetimeToTimestamp() { [dateValue, selectedZone, result, performConversion], ); - return ( -
-

日期时间转时间戳

+ const handleZoneChange = useCallback((e: SelectChangeEvent) => { + setSelectedZone(e.target.value); + }, []); -
-
- + + 日期时间转时间戳 + + + + { setDateValue(e.target.value); if (error) setError(''); }} - aria-label="输入要转换的日期时间" - title="支持格式: YYYY-MM-DD HH:mm:ss" + error={!!error} + helperText={error || '格式: YYYY/MM/DD HH:mm:ss'} + fullWidth + variant="outlined" /> - -
+ + 时区 + + + - {error && ( -
- ⚠️ {error} -
- )} - -
- -
+ + -
- + - -
-
-
+ + 单位 + + + + + ); } diff --git a/entrypoints/popup/components/Navbar.tsx b/entrypoints/popup/components/Navbar.tsx index 8e190eb..c965751 100644 --- a/entrypoints/popup/components/Navbar.tsx +++ b/entrypoints/popup/components/Navbar.tsx @@ -1,5 +1,8 @@ import { NavLink } from 'react-router-dom'; import { useState, useEffect, ReactNode } from 'react'; +import { IconButton } from '@mui/material'; +import MenuIcon from '@mui/icons-material/Menu'; +import CloseIcon from '@mui/icons-material/Close'; interface RouteItem { path: string; @@ -76,16 +79,13 @@ function Navbar({ items = [] }: NavbarProps) { ))} - + {isMenuOpen ? : } + )} diff --git a/entrypoints/popup/components/TimestampExecution.tsx b/entrypoints/popup/components/TimestampExecution.tsx index efb9217..279562a 100644 --- a/entrypoints/popup/components/TimestampExecution.tsx +++ b/entrypoints/popup/components/TimestampExecution.tsx @@ -1,5 +1,6 @@ import { useEffect, useState, useCallback } from 'react'; import CopyButton from './CopyButton'; +import { Button, Paper, Typography, Stack, Box } from '@mui/material'; /** * 时间戳显示和执行组件 @@ -13,109 +14,110 @@ import CopyButton from './CopyButton'; * @returns {JSX.Element} 时间戳组件 */ export function TimestampExecution() { - /** @type {[number, function]} 当前时间戳(毫秒)和更新函数 */ - const [currentTimestamp, setCurrentTimestamp] = useState(() => Math.floor(Date.now())); - - /** @type {[boolean, function]} 是否显示毫秒(true=毫秒,false=秒) */ + const [currentTimestamp, setCurrentTimestamp] = useState(() => + Math.floor(Date.now()), + ); const [showMilliseconds, setShowMilliseconds] = useState(true); - - /** @type {[boolean, function]} 时间戳是否正在自动更新 */ const [isRunningTimestamp, setIsRunningTimestamp] = useState(true); - /** - * 计算显示的时间戳值 - * @type {number} - */ const displayTimestamp = showMilliseconds ? currentTimestamp : Math.floor(currentTimestamp / 1000); const unitText = showMilliseconds ? '毫秒' : '秒'; - /** - * 定时更新时间戳的副作用 - * 根据 isRunningTimestamp 和 showMilliseconds 控制定时器的启停和间隔 - */ useEffect(() => { let timer: number; - if (isRunningTimestamp) { const interval = showMilliseconds ? 100 : 1000; timer = window.setInterval(() => { setCurrentTimestamp(Math.floor(Date.now())); }, interval); } - - // 清理函数 return () => clearInterval(timer); }, [isRunningTimestamp, showMilliseconds]); - /** - * 切换时间戳显示单位(毫秒/秒) - */ const toggleUnit = useCallback(() => { setShowMilliseconds((prev) => !prev); }, []); - /** - * 切换时间戳自动更新状态(启动/停止) - */ const toggleTimestamp = useCallback(() => { setIsRunningTimestamp((prev) => !prev); }, []); - /** - * 切换单位按钮的辅助文本 - * @type {string} - */ - const unitButtonLabel = showMilliseconds ? '切换为秒显示' : '切换为毫秒显示'; - - /** - * 启动/停止按钮的辅助文本 - * @type {string} - */ - const toggleButtonLabel = isRunningTimestamp ? '停止时间戳自动更新' : '开始时间戳自动更新'; - - /** - * 启动/停止按钮的显示文本 - * @type {string} - */ + const unitButtonLabel = showMilliseconds + ? '切换为秒显示' + : '切换为毫秒显示'; + const toggleButtonLabel = isRunningTimestamp + ? '停止时间戳自动更新' + : '开始时间戳自动更新'; const toggleButtonText = isRunningTimestamp ? '停止' : '开始'; return ( -
-
- {displayTimestamp} - {unitText} -
+ + + + {displayTimestamp} + + + {unitText} + + -
- + - -
-
+ + + ); } diff --git a/entrypoints/popup/components/TimestampToDatetime.tsx b/entrypoints/popup/components/TimestampToDatetime.tsx index 264c9ef..ed464a2 100644 --- a/entrypoints/popup/components/TimestampToDatetime.tsx +++ b/entrypoints/popup/components/TimestampToDatetime.tsx @@ -1,19 +1,24 @@ import { useState, useCallback } from 'react'; +import dayjs from 'dayjs'; +import { + Button, + TextField, + Select, + MenuItem, + FormControl, + InputLabel, + Paper, + Typography, + Stack, + Box, + SelectChangeEvent, +} from '@mui/material'; +import utc from 'dayjs/plugin/utc'; +import timezone from 'dayjs/plugin/timezone'; -/** - * 时间戳转日期时间组件 - * - * 功能特性: - * 1. 将时间戳转换为日期时间字符串 - * 2. 支持多种时区选择 - * 3. 支持毫秒和秒单位切换 - * 4. 提供输入验证和错误提示 - * ``` - * - * @returns {JSX.Element} 时间戳转日期时间组件 - */ +dayjs.extend(utc); +dayjs.extend(timezone); -// 常用时区列表 const TIME_ZONE_LIST = [ 'America/New_York', 'America/Chicago', @@ -35,10 +40,9 @@ const TIME_ZONE_LIST = [ 'Pacific/Auckland', ]; -// 时间戳单位选项 const TIMESTAMP_UNITS = [ - { value: 'milliseconds', label: '毫秒(ms)' }, - { value: 'seconds', label: '秒(s)' }, + { value: 'milliseconds', label: '毫秒 (ms)' }, + { value: 'seconds', label: '秒 (s)' }, ]; export function TimestampToDatetime() { @@ -48,136 +52,129 @@ export function TimestampToDatetime() { const [selectedZone, setSelectedZone] = useState('Asia/Shanghai'); const [error, setError] = useState(''); - const performConversion = useCallback( - (timestampValue: string, selectedZone: string, unit: string) => { - if (!timestampValue || timestampValue.trim() === '') { - setError('请输入有效的日期时间'); - return ''; - } - - try { - const numberValue = Number(timestampValue); - if (isNaN(numberValue)) { - setError('时间戳必须是数字'); - return ''; - } - - const d = unit === TIMESTAMP_UNITS[0].value ? dayjs(numberValue) : dayjs.unix(numberValue); - - if (!d.isValid()) { - setError('无效的时间戳格式'); - return ''; - } - - const dateTime = d.tz(selectedZone).format('YYYY/MM/DD HH:mm:ss'); - - setError(''); - return dateTime; - } catch (err) { - console.error('转换错误:', err); - return ''; - } - }, - [], - ); + const performConversion = useCallback((val: string, zone: string, u: string) => { + if (!val || val.trim() === '') { + setError('请输入有效的时间戳'); + return ''; + } + const numberValue = Number(val); + if (isNaN(numberValue)) { + setError('时间戳必须是数字'); + return ''; + } + const d = u === 'milliseconds' ? dayjs(numberValue) : dayjs.unix(numberValue); + if (!d.isValid()) { + setError('无效的时间戳格式'); + return ''; + } + setError(''); + return d.tz(zone).format('YYYY/MM/DD HH:mm:ss'); + }, []); const handleConvert = useCallback(() => { const newResult = performConversion(timestampValue, selectedZone, unit); setTimestampResult(newResult); }, [timestampValue, selectedZone, unit, performConversion]); - /** - * 处理时间戳输入变化 - */ - const handleInputChange = useCallback((e) => { - setTimestampValue(e.target.value); - setError(''); // 清除错误信息 - }, []); + const handleInputChange = useCallback( + (e: React.ChangeEvent) => { + setTimestampValue(e.target.value); + if (error) setError(''); + }, + [error], + ); - /** - * 处理时区选择变化 - */ - const handleZoneChange = useCallback((e: React.ChangeEvent) => { - const newZone = e.target.value; - setSelectedZone(newZone); - }, []); + const handleZoneChange = useCallback( + (e: SelectChangeEvent) => { + const newZone = e.target.value; + setSelectedZone(newZone); + if (timestampResult) { + const newResult = performConversion(timestampValue, newZone, unit); + setTimestampResult(newResult || ''); + } + }, + [performConversion, timestampResult, timestampValue, unit], + ); - /** - * 处理时间戳单位变化 - */ - const handleUnitChange = useCallback((e: React.ChangeEvent) => { - const newUnit = e.target.value; - setUnit(newUnit); - }, []); + const handleUnitChange = useCallback( + (e: SelectChangeEvent) => { + const newUnit = e.target.value; + setUnit(newUnit); + if (timestampResult) { + const newResult = performConversion(timestampValue, selectedZone, newUnit); + setTimestampResult(newResult || ''); + } + }, + [performConversion, timestampResult, timestampValue, selectedZone], + ); return ( -
-

时间戳转日期时间

- -
-
- + + 时间戳转日期时间 + + + + - -
+ + 单位 + + + - {error && ( -
- ⚠️ {error} -
- )} - -
- -
+ + -
- + - -
-
-
+ + 时区 + + + + + ); } diff --git a/entrypoints/popup/pages/RecordeReplayPage.tsx b/entrypoints/popup/pages/RecordeReplayPage.tsx index 6055090..6d42dc0 100644 --- a/entrypoints/popup/pages/RecordeReplayPage.tsx +++ b/entrypoints/popup/pages/RecordeReplayPage.tsx @@ -1,6 +1,7 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState, useMemo } from 'react'; import { AppState } from '../types'; import { messages } from '@/utils/messages'; +import { Button } from '@mui/material'; const RecordeReplayPage = () => { const [status, setStatus] = useState(AppState.READ); @@ -53,12 +54,14 @@ const RecordeReplayPage = () => { return (
- +
); diff --git a/package-lock.json b/package-lock.json index 0f7b715..6235753 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,10 @@ "version": "0.0.0", "hasInstallScript": true, "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/icons-material": "^7.3.8", + "@mui/material": "^7.3.8", "@rrweb/all": "^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", @@ -223,7 +227,6 @@ "version": "7.28.6", "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.28.6.tgz", "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.28.6", @@ -257,7 +260,6 @@ "version": "7.28.0", "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz", "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -267,7 +269,6 @@ "version": "7.28.6", "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.28.6", @@ -309,7 +310,6 @@ "version": "7.27.1", "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -352,7 +352,6 @@ "version": "7.28.6", "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.6.tgz", "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.28.6" @@ -409,7 +408,6 @@ "version": "7.28.6", "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.28.6.tgz", "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.28.6", @@ -424,7 +422,6 @@ "version": "7.28.6", "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.28.6.tgz", "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.28.6", @@ -443,7 +440,6 @@ "version": "7.28.6", "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.6.tgz", "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -513,6 +509,179 @@ } } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmmirror.com/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmmirror.com/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmmirror.com/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmmirror.com/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmmirror.com/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.14.1", + "resolved": "https://registry.npmmirror.com/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmmirror.com/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmmirror.com/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.2", "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", @@ -1227,7 +1396,6 @@ "version": "0.3.13", "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -1249,7 +1417,6 @@ "version": "3.1.2", "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1270,20 +1437,326 @@ "version": "1.5.5", "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "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", + "integrity": "sha512-s9UHZo7QJVly7gNArEZkbbsimHqJZhElgBpXIJdehZ4OWXt+CCr0SBDgUCDJnQrqpd1dWK2dLq5rmO4mCBmI3w==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "7.3.8", + "resolved": "https://registry.npmmirror.com/@mui/icons-material/-/icons-material-7.3.8.tgz", + "integrity": "sha512-88sWg/UJc1X82OMO+ISR4E3P58I3BjFVg0qkmDu7OWlN8VijneZD3ylFA+ImxuPjMHW3SHosfSJYy1fztoz0fw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^7.3.8", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/icons-material/node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@mui/material": { + "version": "7.3.8", + "resolved": "https://registry.npmmirror.com/@mui/material/-/material-7.3.8.tgz", + "integrity": "sha512-QKd1RhDXE1hf2sQDNayA9ic9jGkEgvZOf0tTkJxlBPG8ns8aS4rS8WwYURw2x5y3739p0HauUXX9WbH7UufFLw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "@mui/core-downloads-tracker": "^7.3.8", + "@mui/system": "^7.3.8", + "@mui/types": "^7.4.11", + "@mui/utils": "^7.3.8", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "csstype": "^3.2.3", + "prop-types": "^15.8.1", + "react-is": "^19.2.3", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material-pigment-css": "^7.3.8", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material/node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@mui/material/node_modules/react-is": { + "version": "19.2.4", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-19.2.4.tgz", + "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", + "license": "MIT" + }, + "node_modules/@mui/private-theming": { + "version": "7.3.8", + "resolved": "https://registry.npmmirror.com/@mui/private-theming/-/private-theming-7.3.8.tgz", + "integrity": "sha512-du5dlPZ9XL3xW2apHoGDXBI+QLtyVJGrXNCfcNYfP/ojkz1RQ0rRV6VG9Rkm1DqEFRG8mjjTL7zmE1Bvn1eR4A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "@mui/utils": "^7.3.8", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming/node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@mui/styled-engine": { + "version": "7.3.8", + "resolved": "https://registry.npmmirror.com/@mui/styled-engine/-/styled-engine-7.3.8.tgz", + "integrity": "sha512-JHAeXQzS0tJ+Fq3C6J4TVDsW+yKhO4uuxuiLaopNStJeQYBIUCXpKYyUCcgXym4AmhbznQnv9RlHywSH6b0FOg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "csstype": "^3.2.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine/node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@mui/system": { + "version": "7.3.8", + "resolved": "https://registry.npmmirror.com/@mui/system/-/system-7.3.8.tgz", + "integrity": "sha512-hoFRj4Zw2Km8DPWZp/nKG+ao5Jw5LSk2m/e4EGc6M3RRwXKEkMSG4TgtfVJg7dS2homRwtdXSMW+iRO0ZJ4+IA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "@mui/private-theming": "^7.3.8", + "@mui/styled-engine": "^7.3.8", + "@mui/types": "^7.4.11", + "@mui/utils": "^7.3.8", + "clsx": "^2.1.1", + "csstype": "^3.2.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/system/node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@mui/types": { + "version": "7.4.11", + "resolved": "https://registry.npmmirror.com/@mui/types/-/types-7.4.11.tgz", + "integrity": "sha512-fZ2xO9D08IKOxO2oUBi1nnVKH6oJUD+64cnv4YAaFoC0E5+i1+S5AHbNqqvZlYYsbPEQ6qEVwuBqY3jl5W4G+Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types/node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@mui/utils": { + "version": "7.3.8", + "resolved": "https://registry.npmmirror.com/@mui/utils/-/utils-7.3.8.tgz", + "integrity": "sha512-kZRcE2620CBGr+XI8YMmwPj6WIPwSF7uMJjvSfqd8zXVvlz0MCJbzRRUGNf8NgflCLthdji2DdS643TeyJ3+nA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "@mui/types": "^7.4.11", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.2.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils/node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@mui/utils/node_modules/react-is": { + "version": "19.2.4", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-19.2.4.tgz", + "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1367,6 +1840,16 @@ "node": ">=12" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmmirror.com/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@remix-run/router": { "version": "1.23.2", "resolved": "https://registry.npmmirror.com/@remix-run/router/-/router-1.23.2.tgz", @@ -2018,6 +2501,18 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.9", "resolved": "https://registry.npmmirror.com/@types/react/-/react-19.2.9.tgz", @@ -2037,6 +2532,15 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmmirror.com/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/@types/unist": { "version": "2.0.11", "resolved": "https://registry.npmmirror.com/@types/unist/-/unist-2.0.11.tgz", @@ -2819,6 +3323,41 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/babel-plugin-macros/node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/bail": { "version": "1.0.5", "resolved": "https://registry.npmmirror.com/bail/-/bail-1.0.5.tgz", @@ -3109,7 +3648,6 @@ "version": "3.1.0", "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3468,6 +4006,15 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", @@ -3606,6 +4153,61 @@ "dev": true, "license": "MIT" }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmmirror.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/cosmiconfig/node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/cosmiconfig/node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmmirror.com/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3942,6 +4544,16 @@ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "license": "MIT" }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -4131,7 +4743,6 @@ "version": "1.3.4", "resolved": "https://registry.npmmirror.com/error-ex/-/error-ex-1.3.4.tgz", "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -4882,6 +5493,12 @@ "node": ">=8" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz", @@ -5010,7 +5627,6 @@ "version": "1.1.2", "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5398,7 +6014,6 @@ "version": "2.0.2", "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -5424,6 +6039,21 @@ "hermes-estree": "0.25.1" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmmirror.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/hookable": { "version": "5.5.3", "resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz", @@ -5508,7 +6138,6 @@ "version": "3.3.1", "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -5648,7 +6277,6 @@ "version": "0.2.1", "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, "license": "MIT" }, "node_modules/is-async-function": { @@ -5744,7 +6372,6 @@ "version": "2.16.1", "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -6340,7 +6967,6 @@ "version": "3.1.0", "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -7899,7 +8525,6 @@ "version": "1.0.1", "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -7970,9 +8595,17 @@ "version": "1.0.7", "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, "license": "MIT" }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", @@ -8491,6 +9124,22 @@ "react-dom": ">=16.8" } }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmmirror.com/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", @@ -8698,7 +9347,6 @@ "version": "4.0.0", "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -9567,6 +10215,12 @@ "inline-style-parser": "0.1.1" } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", @@ -9584,7 +10238,6 @@ "version": "1.0.0", "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" diff --git a/package.json b/package.json index e1f2cd5..612f4a6 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,10 @@ "lint": "eslint . --max-warnings=0" }, "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/icons-material": "^7.3.8", + "@mui/material": "^7.3.8", "@rrweb/all": "^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", From ee4d328b9c4dc6e635187fbb0dfd72c1c278262f 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 00:24:56 +0800 Subject: [PATCH 02/10] =?UTF-8?q?refactor=EF=BC=9A=201=E3=80=81=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=85=A8=E5=B1=80toast=202=E3=80=81=E5=B0=86CopyButto?= =?UTF-8?q?n=E6=8B=86=E5=88=86=E4=B8=BAuseCopy=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- entrypoints/popup/App.tsx | 25 ++-- entrypoints/popup/components/CopyButton.tsx | 134 ------------------ .../popup/components/TimestampExecution.tsx | 36 ++--- entrypoints/popup/components/useCopy.ts | 39 +++++ entrypoints/popup/hook/useCopy.ts | 39 +++++ package-lock.json | 41 ++++++ package.json | 1 + 7 files changed, 156 insertions(+), 159 deletions(-) delete mode 100644 entrypoints/popup/components/CopyButton.tsx create mode 100644 entrypoints/popup/components/useCopy.ts create mode 100644 entrypoints/popup/hook/useCopy.ts diff --git a/entrypoints/popup/App.tsx b/entrypoints/popup/App.tsx index 3a6f405..ea6dcc2 100644 --- a/entrypoints/popup/App.tsx +++ b/entrypoints/popup/App.tsx @@ -1,5 +1,8 @@ // 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 Navbar from './components/Navbar'; @@ -13,19 +16,23 @@ const navItems = [ { 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 deleted file mode 100644 index fd196bf..0000000 --- a/entrypoints/popup/components/CopyButton.tsx +++ /dev/null @@ -1,134 +0,0 @@ -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/TimestampExecution.tsx b/entrypoints/popup/components/TimestampExecution.tsx index 279562a..72cf8cd 100644 --- a/entrypoints/popup/components/TimestampExecution.tsx +++ b/entrypoints/popup/components/TimestampExecution.tsx @@ -1,6 +1,8 @@ import { useEffect, useState, useCallback } from 'react'; -import CopyButton from './CopyButton'; import { Button, Paper, Typography, Stack, Box } from '@mui/material'; +import ContentCopyIcon from '@mui/icons-material/ContentCopy'; +import CheckIcon from '@mui/icons-material/Check'; +import { useCopy } from './useCopy'; /** * 时间戳显示和执行组件 @@ -14,9 +16,7 @@ import { Button, Paper, Typography, Stack, Box } from '@mui/material'; * @returns {JSX.Element} 时间戳组件 */ export function TimestampExecution() { - const [currentTimestamp, setCurrentTimestamp] = useState(() => - Math.floor(Date.now()), - ); + const [currentTimestamp, setCurrentTimestamp] = useState(() => Math.floor(Date.now())); const [showMilliseconds, setShowMilliseconds] = useState(true); const [isRunningTimestamp, setIsRunningTimestamp] = useState(true); @@ -45,19 +45,14 @@ 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 ( - + - + /> */} + + + + {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, }, }, }, From 50034331bc48590679fa952a9a315ce6a83116b7 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 20:38:15 +0800 Subject: [PATCH 04/10] =?UTF-8?q?refactor:=20=E7=BB=9F=E4=B8=80=E6=89=A9?= =?UTF-8?q?=E5=B1=95=E6=B6=88=E6=81=AF=E6=9C=BA=E5=88=B6=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E7=BB=84=E4=BB=B6=E7=BB=93=E6=9E=84=201=E3=80=81?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E6=B6=88=E6=81=AF=E7=B3=BB=E7=BB=9F=EF=BC=9A?= =?UTF-8?q?=E5=B0=86=20background=E3=80=81content=20=E5=92=8C=20popup=20?= =?UTF-8?q?=E7=9A=84=E9=80=9A=E4=BF=A1=E6=96=B9=E5=BC=8F=E4=BB=8E=E5=8E=9F?= =?UTF-8?q?=E7=94=9F=E7=9A=84=20chrome.runtime=20=E8=BF=81=E7=A7=BB?= =?UTF-8?q?=E8=87=B3=20@webext-core/messaging=EF=BC=8C=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E4=BA=86=E6=B6=88=E6=81=AF=E5=9B=9E=E8=B0=83=E8=BF=94=E5=9B=9E?= =?UTF-8?q?=20undefined=20=E7=9A=84=E9=97=AE=E9=A2=98=E3=80=82=202?= =?UTF-8?q?=E3=80=81=E4=BC=98=E5=8C=96=E7=BB=84=E4=BB=B6=E7=BB=93=E6=9E=84?= =?UTF-8?q?=EF=BC=9A=E5=B0=86=E9=80=9A=E7=94=A8=E7=BB=84=E4=BB=B6=E4=BB=8E?= =?UTF-8?q?=20entrypoints/popup/components=20=E8=BF=81=E7=A7=BB=E8=87=B3?= =?UTF-8?q?=E6=A0=B9=E7=9B=AE=E5=BD=95=20components=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=20MUI=20=E9=87=8D=E6=9E=84=E4=BA=86=20Navbar?= =?UTF-8?q?=E3=80=82=203=E3=80=81=E5=90=8C=E6=AD=A5=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=EF=BC=9A=E8=B0=83=E6=95=B4=E4=BA=86=E5=BD=95=E5=88=B6=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=20(useRecorder)=20=E5=92=8C=E5=90=84=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E7=BB=84=E4=BB=B6=EF=BC=88RecordeReplayPage,=20TestPa?= =?UTF-8?q?ge=E7=AD=89=EF=BC=89=EF=BC=8C=E4=BB=A5=E9=80=82=E9=85=8D?= =?UTF-8?q?=E6=96=B0=E7=9A=84=E6=B6=88=E6=81=AF=E5=8D=8F=E8=AE=AE=E5=92=8C?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E8=B7=AF=E5=BE=84=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components => components}/CopyButton.tsx | 0 .../DatetimeToTimestamp.tsx | 0 components/Navbar.tsx | 117 +++++++++++++++ .../RoutePersistence.tsx | 0 .../TimestampExecution.tsx | 0 .../TimestampToDatetime.tsx | 0 entrypoints/background.ts | 135 +++++++----------- entrypoints/content.ts | 83 ++++------- entrypoints/popup/App.tsx | 4 +- entrypoints/popup/components/Navbar.tsx | 97 ------------- entrypoints/popup/pages/RecordeReplayPage.tsx | 44 +++--- entrypoints/popup/pages/TestPage.tsx | 8 +- entrypoints/popup/pages/TimestampPage.tsx | 6 +- tsconfig.json | 3 +- utils/messages.tsx | 18 ++- utils/useRecorder.tsx | 8 +- 16 files changed, 252 insertions(+), 271 deletions(-) rename {entrypoints/popup/components => components}/CopyButton.tsx (100%) rename {entrypoints/popup/components => components}/DatetimeToTimestamp.tsx (100%) create mode 100644 components/Navbar.tsx rename {entrypoints/popup/components => components}/RoutePersistence.tsx (100%) rename {entrypoints/popup/components => components}/TimestampExecution.tsx (100%) rename {entrypoints/popup/components => components}/TimestampToDatetime.tsx (100%) delete mode 100644 entrypoints/popup/components/Navbar.tsx diff --git a/entrypoints/popup/components/CopyButton.tsx b/components/CopyButton.tsx similarity index 100% rename from entrypoints/popup/components/CopyButton.tsx rename to components/CopyButton.tsx diff --git a/entrypoints/popup/components/DatetimeToTimestamp.tsx b/components/DatetimeToTimestamp.tsx similarity index 100% rename from entrypoints/popup/components/DatetimeToTimestamp.tsx rename to components/DatetimeToTimestamp.tsx diff --git a/components/Navbar.tsx b/components/Navbar.tsx new file mode 100644 index 0000000..f306c3f --- /dev/null +++ b/components/Navbar.tsx @@ -0,0 +1,117 @@ +import { NavLink, useLocation } from 'react-router-dom'; +import { useState, ReactNode, MouseEvent } from 'react'; +import { + AppBar, + Box, + IconButton, + Menu, + MenuItem, + Tab, + Tabs, + Toolbar, + useMediaQuery, +} from '@mui/material'; +import MenuIcon from '@mui/icons-material/Menu'; + +interface RouteItem { + path: string; + label: string; + element: ReactNode; +} + +interface NavbarProps { + items?: RouteItem[]; +} + +function Navbar({ items = [] }: NavbarProps) { + const [anchorEl, setAnchorEl] = useState(null); + const isMenuOpen = Boolean(anchorEl); + const location = useLocation(); + + // Replicating the screen size logic from original component + const isLargeScreen = useMediaQuery('(min-width:768px)'); + const isMediumScreen = useMediaQuery('(min-width:480px)'); + + let visibleItemsCount: number; + if (isLargeScreen) { + visibleItemsCount = items.length; + } else if (isMediumScreen) { + visibleItemsCount = Math.min(3, items.length); + } else { + // Small screen + visibleItemsCount = Math.min(2, items.length); + } + + const visibleNavItems = items.slice(0, visibleItemsCount); + const collapsedNavItems = items.slice(visibleItemsCount); + + const handleMenuOpen = (event: MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleMenuClose = () => { + setAnchorEl(null); + }; + + // Find the current active tab index for the Tabs value + // Using startsWith to handle nested routes correctly. + const activeTabIndex = visibleNavItems.findIndex((item) => + location.pathname.startsWith(item.path), + ); + + return ( + + + + {visibleNavItems.map((item) => ( + + ))} + + + {collapsedNavItems.length > 0 && ( + + + + + + {collapsedNavItems.map((item) => ( + + {item.label} + + ))} + + + )} + + + ); +} + +export default Navbar; diff --git a/entrypoints/popup/components/RoutePersistence.tsx b/components/RoutePersistence.tsx similarity index 100% rename from entrypoints/popup/components/RoutePersistence.tsx rename to components/RoutePersistence.tsx diff --git a/entrypoints/popup/components/TimestampExecution.tsx b/components/TimestampExecution.tsx similarity index 100% rename from entrypoints/popup/components/TimestampExecution.tsx rename to components/TimestampExecution.tsx diff --git a/entrypoints/popup/components/TimestampToDatetime.tsx b/components/TimestampToDatetime.tsx similarity index 100% rename from entrypoints/popup/components/TimestampToDatetime.tsx rename to components/TimestampToDatetime.tsx diff --git a/entrypoints/background.ts b/entrypoints/background.ts index 975f94a..2745de5 100644 --- a/entrypoints/background.ts +++ b/entrypoints/background.ts @@ -1,6 +1,7 @@ import '../.wxt/types/imports.d.ts'; import { browser } from 'wxt/browser'; import { downloadHtmlInBackground } from '@/utils/recordUtils.tsx'; +import { sendMessage, onMessage } from '@/utils/messages'; const events: unknown[] = []; @@ -51,88 +52,65 @@ export default defineBackground(() => { ); }); - // 监听来自 popup script 的消息 - chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { - if (msg.type === messages.popup.checkStatus) { - console.log('[bg] checkStatus received'); - sendToActiveTab({ type: messages.content.checkStatus }) - .then((res) => { - sendResponse(res); - }) - .catch((error: Error) => { - if (error.message.includes('restricted URL')) { - 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 { - console.error('Failed to check status in content script', error); - } - sendResponse({ ok: false }); - }); - return true; - } + onMessage('popup:check-status', async (message) => { + console.log(`[background] popup:check-status: ${message}`); + const obj = { + active: false, + startTime: -1, + }; - if (msg.type === messages.popup.from.start) { - console.log('[bg] startRecording received'); - sendToActiveTab({ type: messages.content.to.startRecording }) - .then(async () => { - await chrome.runtime.sendMessage({ type: messages.popup.to.started }); - sendResponse({ ok: true }); - }) - .catch((error: Error) => { - if (error.message.includes('restricted URL')) { - 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 { - console.error('Failed to start recording in content script', error); - } - sendResponse({ ok: false }); - }); + try { + const tabId = await getActiveTabId(); + const result = await sendMessage('content:check-status', undefined, tabId); + console.log(`[background]111${result}`); + obj.active = result; + obj.startTime = new Date().getTime(); + } catch (err) { + console.error(`[background-err]${err}`); } + return obj; + }); - if (msg.type === messages.popup.from.stop) { - console.log('[bg] stopRecording received'); - sendToActiveTab({ type: messages.content.to.stopRecording }) - .then(async () => { - await chrome.runtime.sendMessage({ type: messages.popup.to.stopped }); - downloadHtmlInBackground(events); - sendResponse({ ok: true }); - }) - .catch((error: Error) => { - if (error.message.includes('restricted URL')) { - 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 { - console.error('Failed to stop recording in content script', error); - } - sendResponse({ ok: false }); - }); + onMessage('popup:start', async () => { + console.log('[bg] startRecording received'); + try { + const tabId = await getActiveTabId(); + const response = await sendMessage('content:start-recording', undefined, tabId); + if (response.ok) { + await sendMessage('popup:started', undefined); + return { ok: true }; + } + return { ok: false }; + } catch (error) { + console.error('Failed to start recording in content script', error); + return { ok: false }; } + }); + onMessage('popup:stop', async () => { + console.log('[bg] stopRecording received'); + try { + const tabId = await getActiveTabId(); + const response = await sendMessage('content:stop-recording', undefined, tabId); + if (response.ok) { + await sendMessage('popup:stopped', undefined); + downloadHtmlInBackground(events); + return { ok: true }; + } + return { ok: false }; + } catch (error: unknown) { + console.error('Failed to stop recording in content script', error); + return { ok: false }; + } + }); + + onMessage('content:save-tracke-events', (eventsList) => { + console.log('[bg] saveTrackeEvents received:', eventsList); + events.push(eventsList); return true; }); - // 监听来自 content script 的消息 - chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { - if (msg.type === messages.content.from.saveTrackeEvents) { - console.log('[bg] saveTrackeEvents received:', msg.payload); - events.push(msg.payload); - sendResponse({ ok: true }); - } - }); - - onMessage('getStringLength', (message) => { - return message.data.length; - }); - - async function sendToActiveTab(message: { - type: string; - data?: unknown; - activeTabId?: number; - [key: string]: unknown; - }) { + async function getActiveTabId() { const activeTabs = await chrome.tabs.query({ active: true, currentWindow: true, @@ -159,12 +137,7 @@ export default defineBackground(() => { } } - const sendTo = message.activeTabId || activeTab.id; - try { - return await chrome.tabs.sendMessage(sendTo!, message); - } catch (error) { - console.error('Error sending message to active tab:', error); - throw error; - } + const sendTo = activeTab.id; + return sendTo!; } }); diff --git a/entrypoints/content.ts b/entrypoints/content.ts index caab129..df8818d 100644 --- a/entrypoints/content.ts +++ b/entrypoints/content.ts @@ -1,11 +1,6 @@ import '../.wxt/types/imports.d.ts'; import { createRecorder } from '@/utils/useRecorder'; - -interface IResponse { - ok: boolean; - error?: string; - data?: unknown; -} +import { onMessage } from '@/utils/messages.tsx'; export default defineContentScript({ // matches: ['*://*.google.com/*'], @@ -15,56 +10,36 @@ export default defineContentScript({ const recorder = createRecorder(); let isRecording = false; - onMessage('getStringLength', (message) => { - console.log(`[content] getStringLength received: ${message.data}`); + onMessage('content:check-status', () => { + console.log(`[content]${isRecording}`); + return isRecording; }); - // 监听来自 background script 的消息 - chrome.runtime.onMessage.addListener( - (msg: { type: string }, _sender, sendResponse: (response: IResponse) => void) => { - const handleAsyncMessage = async () => { - try { - switch (msg.type) { - // 在这里处理异步消息类型 - case messages.content.checkStatus: - return { ok: true, data: { isRecording } }; + onMessage('content:start-recording', async () => { + try { + const started = await recorder.startRecord(); + if (started) { + isRecording = true; + return { ok: true }; + } else { + return { ok: false, error: 'Failed to start recorder' }; + } + } catch (error) { + console.error('Error handling start recording:', error); + return { ok: false }; + } + }); - case messages.content.to.startRecording: { - const started = await recorder.startRecord(messages.content.from.saveTrackeEvents); - - if (started) { - isRecording = true; - return { ok: true }; - } else { - // 如果启动失败,返回错误信息 - return { ok: false, error: 'Failed to start recorder' }; - } - } - - case messages.content.to.stopRecording: - // 停止录制的处理逻辑 - console.log('[content] stopRecording received'); - recorder.stopRecord(); - isRecording = false; - return { ok: true }; - - default: - return { ok: false, error: 'Unknown message type' }; - } - } catch (error) { - console.error('Error handling message:', error); - return { ok: false }; - } - }; - - handleAsyncMessage().then((response) => { - if (sendResponse) { - sendResponse(response); - } - }); - - return true; - }, - ); + onMessage('content:stop-recording', () => { + try { + console.log('[content] stopRecording received'); + recorder.stopRecord(); + isRecording = false; + return { ok: true }; + } catch (error) { + console.error('Error handling stop recording:', error); + return { ok: false }; + } + }); }, }); diff --git a/entrypoints/popup/App.tsx b/entrypoints/popup/App.tsx index c7a51f1..000290a 100644 --- a/entrypoints/popup/App.tsx +++ b/entrypoints/popup/App.tsx @@ -3,8 +3,8 @@ import { HashRouter as Router, Routes, Route } from 'react-router-dom'; 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 Navbar from '../../components/Navbar'; +import RoutePersistence from '../../components/RoutePersistence'; import './App.css'; // 路由配置数据 diff --git a/entrypoints/popup/components/Navbar.tsx b/entrypoints/popup/components/Navbar.tsx deleted file mode 100644 index c965751..0000000 --- a/entrypoints/popup/components/Navbar.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { NavLink } from 'react-router-dom'; -import { useState, useEffect, ReactNode } from 'react'; -import { IconButton } from '@mui/material'; -import MenuIcon from '@mui/icons-material/Menu'; -import CloseIcon from '@mui/icons-material/Close'; - -interface RouteItem { - path: string; - label: string; - element: ReactNode; -} - -interface NavbarProps { - items?: RouteItem[]; -} - -function Navbar({ items = [] }: NavbarProps) { - const [isMenuOpen, setIsMenuOpen] = useState(false); - const [isMobile, setIsMobile] = useState(false); - const [visibleItems, setVisibleItems] = useState(items.length); - - // 检测屏幕尺寸变化 - useEffect(() => { - const handleResize = () => { - const width = window.innerWidth; - setIsMobile(width < 768); - - // 根据屏幕宽度决定显示多少个导航项 - if (width >= 768) { - setVisibleItems(items.length); // 大屏幕显示所有 - } else if (width >= 480) { - // 中等屏幕:如果导航项超过3个,显示3个,否则显示全部 - setVisibleItems(Math.min(3, items.length)); - } else { - // 小屏幕:如果导航项超过2个,显示2个,否则显示全部 - setVisibleItems(Math.min(2, items.length)); - } - }; - - handleResize(); // 初始调用 - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); - }, [items.length]); // 添加依赖,如果 items 长度变化也需要重新计算 - - // 计算哪些导航项应该显示,哪些应该折叠 - const visibleNavItems = items.slice(0, visibleItems); - const collapsedNavItems = items.slice(visibleItems); - - return ( - - ); -} - -export default Navbar; diff --git a/entrypoints/popup/pages/RecordeReplayPage.tsx b/entrypoints/popup/pages/RecordeReplayPage.tsx index 6d42dc0..7de7e06 100644 --- a/entrypoints/popup/pages/RecordeReplayPage.tsx +++ b/entrypoints/popup/pages/RecordeReplayPage.tsx @@ -1,6 +1,6 @@ import { useEffect, useState, useMemo } from 'react'; import { AppState } from '../types'; -import { messages } from '@/utils/messages'; +import { sendMessage, onMessage } from '@/utils/messages'; import { Button } from '@mui/material'; const RecordeReplayPage = () => { @@ -8,27 +8,22 @@ const RecordeReplayPage = () => { const isRecording = useMemo(() => status === AppState.RECORDING, [status]); useEffect(() => { - const handleMessage = (msg: { type: string }) => { - if (msg.type === messages.popup.ready) { - setStatus(AppState.READ); - } + const unlistenStarted = onMessage('popup:started', () => { + console.log('[popup] Received started message'); + setStatus(AppState.RECORDING); + }); - if (msg.type === messages.popup.to.started) { - console.log('[popup] Received started message'); - setStatus(AppState.RECORDING); - } + const unlistenStopped = onMessage('popup:stopped', () => { + setStatus(AppState.READ); + }); - if (msg.type === messages.popup.to.stopped) { - setStatus(AppState.READ); - } - }; + const unlistenReady = onMessage('popup:ready', () => { + setStatus(AppState.READ); + }); - browser.runtime.onMessage.addListener(handleMessage); - - browser.runtime - .sendMessage({ type: messages.popup.checkStatus }) + sendMessage('popup:check-status', undefined) .then((res) => { - if (res?.data?.isRecording) { + if (res?.active) { setStatus(AppState.RECORDING); } else { setStatus(AppState.READ); @@ -38,13 +33,20 @@ const RecordeReplayPage = () => { console.error(err); }); - return () => browser.runtime.onMessage.removeListener(handleMessage); + return () => { + unlistenStarted(); + unlistenStopped(); + unlistenReady(); + }; }, []); const toggleRecording = async () => { try { - const actionType = isRecording ? messages.popup.from.stop : messages.popup.from.start; - await browser.runtime.sendMessage({ type: actionType }); + if (isRecording) { + await sendMessage('popup:stop', undefined); + } else { + await sendMessage('popup:start', undefined); + } } catch (error) { console.error('Error toggling recording:', error); setStatus(AppState.READ); diff --git a/entrypoints/popup/pages/TestPage.tsx b/entrypoints/popup/pages/TestPage.tsx index a90eb49..6e915fe 100644 --- a/entrypoints/popup/pages/TestPage.tsx +++ b/entrypoints/popup/pages/TestPage.tsx @@ -3,12 +3,8 @@ import { Button } from '@mui/material'; const TestPage = () => { const handleSeedMessage = async () => { - const tabs = await browser.tabs.query({ active: true, currentWindow: true }); - tabs.forEach(async (tab) => { - console.log(tab.id); - const length = await sendMessage('getStringLength', 'hello world', tab.id); - console.log('字符串长度:', length); - }); + const status = await sendMessage('popup:check-status'); + console.log(`[popup]status: ${status}`); }; return (
diff --git a/entrypoints/popup/pages/TimestampPage.tsx b/entrypoints/popup/pages/TimestampPage.tsx index f53057b..d02c555 100644 --- a/entrypoints/popup/pages/TimestampPage.tsx +++ b/entrypoints/popup/pages/TimestampPage.tsx @@ -1,6 +1,6 @@ -import { TimestampToDatetime } from '../components/TimestampToDatetime'; -import { DatetimeToTimestamp } from '../components/DatetimeToTimestamp'; -import { TimestampExecution } from '../components/TimestampExecution'; +import { TimestampToDatetime } from '@/components/TimestampToDatetime'; +import { DatetimeToTimestamp } from '@/components/DatetimeToTimestamp'; +import { TimestampExecution } from '@/components/TimestampExecution'; const TimestampPage = () => { return ( diff --git a/tsconfig.json b/tsconfig.json index 2ac5e7c..263e7b8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -48,7 +48,8 @@ "assets/**/*", "hooks/**/*", ".wxt/types/**/*.ts", - ".wxt/types/*.d.ts" + ".wxt/types/*.d.ts", + "components" ], "exclude": ["node_modules", ".wxt", "eslint.config.ts"] } diff --git a/utils/messages.tsx b/utils/messages.tsx index 6869253..c1d8e06 100644 --- a/utils/messages.tsx +++ b/utils/messages.tsx @@ -32,7 +32,23 @@ export const messages = { }; interface ProtocolMap { - getStringLength(data: string): number; + // --- Popup 相关 --- + 'popup:start': () => { ok: boolean }; + 'popup:stop': () => { ok: boolean }; + 'popup:started': () => void; + 'popup:stopped': () => void; + 'popup:check-status': () => { active: boolean; startTime?: number }; + 'popup:ready': () => void; + + // --- Content 相关 --- + 'content:save-tracke-events': (event) => boolean; + 'content:start-recording': () => { ok: boolean; error?: string }; + 'content:stop-recording': () => { ok: boolean }; + 'content:check-status': () => boolean; + + // --- Offscreen 相关 --- + 'offscreen:start-recording': (streamId: string) => void; + 'offscreen:stop-recording': () => void; } export const { sendMessage, onMessage } = defineExtensionMessaging(); diff --git a/utils/useRecorder.tsx b/utils/useRecorder.tsx index 949d4bb..87b4e7f 100644 --- a/utils/useRecorder.tsx +++ b/utils/useRecorder.tsx @@ -1,18 +1,16 @@ import * as rrweb from 'rrweb'; import { listenerHandler } from '@rrweb/types'; import { getRecordConsolePlugin } from '@rrweb/rrweb-plugin-console-record'; +import { sendMessage } from '@/utils/messages'; export const createRecorder = () => { let stopFn: listenerHandler | null = null; - const startRecord = async (msg: string) => { + const startRecord = async () => { try { const handler = rrweb.record({ emit(event) { - chrome.runtime.sendMessage({ - type: msg, - payload: event, - }); + sendMessage('content:save-tracke-events', event); }, plugins: [getRecordConsolePlugin()], }); From 3ece5e2e96a83aabfb465378df2ac86d7681f0e6 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 23:46:16 +0800 Subject: [PATCH 05/10] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20ESLint=20?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E5=8F=8A=E7=BB=84=E4=BB=B6=E5=BC=83=E7=94=A8?= =?UTF-8?q?=E5=B1=9E=E6=80=A7=E8=AD=A6=E5=91=8A=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 工程配置: - 更新 tsconfig.json include 规则,解决 eslint.config.ts 未被包含的问题。 2. 组件重构 (Material UI 适配): - Navbar: 替换已弃用的 PaperProps 为 slotProps.paper。 - TimestampToDatetime / DatetimeToTimestamp: 替换已弃用的 InputProps 为 slotProps.input。 3. 类型与逻辑修复: - utils/recordUtils.tsx: 引入缺失依赖,忽略 UNSAFE_replayCanvas 等类型报错,修复构建问题。 - utils/messages.tsx: 完善事件参数类型定义。 4. 其他优化: - 统一 App.css 代码格式。 - 补充部分组件 (CopyButton, TimestampExecution) 的 React 导入和类型定义。 --- components/CopyButton.tsx | 2 +- components/DatetimeToTimestamp.tsx | 10 ++++++---- components/Navbar.tsx | 10 ++++++---- components/TimestampExecution.tsx | 29 ++++++----------------------- components/TimestampToDatetime.tsx | 12 +++++++----- entrypoints/popup/App.css | 1 - tsconfig.json | 5 +++-- utils/messages.tsx | 2 +- utils/recordUtils.tsx | 11 +++++++---- 9 files changed, 37 insertions(+), 45 deletions(-) diff --git a/components/CopyButton.tsx b/components/CopyButton.tsx index fd196bf..1571093 100644 --- a/components/CopyButton.tsx +++ b/components/CopyButton.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback, FC, ReactNode, useEffect } from 'react'; +import React, { 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'; diff --git a/components/DatetimeToTimestamp.tsx b/components/DatetimeToTimestamp.tsx index 9bdc2f6..3ccd2f9 100644 --- a/components/DatetimeToTimestamp.tsx +++ b/components/DatetimeToTimestamp.tsx @@ -70,7 +70,7 @@ export function DatetimeToTimestamp() { }, [dateValue, selectedZone, unit, performConversion]); const handleUnitChange = useCallback( - (e: SelectChangeEvent) => { + (e: SelectChangeEvent) => { const newUnit = e.target.value; setUnit(newUnit); if (result) { @@ -80,7 +80,7 @@ export function DatetimeToTimestamp() { [dateValue, selectedZone, result, performConversion], ); - const handleZoneChange = useCallback((e: SelectChangeEvent) => { + const handleZoneChange = useCallback((e: SelectChangeEvent) => { setSelectedZone(e.target.value); }, []); @@ -129,8 +129,10 @@ export function DatetimeToTimestamp() { value={result} fullWidth variant="outlined" - InputProps={{ - readOnly: true, + slotProps={{ + input: { + readOnly: true, + }, }} /> diff --git a/components/Navbar.tsx b/components/Navbar.tsx index f306c3f..0b50059 100644 --- a/components/Navbar.tsx +++ b/components/Navbar.tsx @@ -88,10 +88,12 @@ function Navbar({ items = [] }: NavbarProps) { anchorEl={anchorEl} open={isMenuOpen} onClose={handleMenuClose} - PaperProps={{ - style: { - maxHeight: 48 * 4.5, - width: '20ch', + slotProps={{ + paper: { + style: { + maxHeight: 48 * 4.5, + width: '20ch', + }, }, }} > diff --git a/components/TimestampExecution.tsx b/components/TimestampExecution.tsx index 279562a..a912131 100644 --- a/components/TimestampExecution.tsx +++ b/components/TimestampExecution.tsx @@ -1,22 +1,12 @@ -import { useEffect, useState, useCallback } from 'react'; +import { useEffect, useState, useCallback, JSX } from 'react'; import CopyButton from './CopyButton'; import { Button, Paper, Typography, Stack, Box } from '@mui/material'; /** * 时间戳显示和执行组件 - * - * @component - * @example - * ```jsx - * - * ``` - * - * @returns {JSX.Element} 时间戳组件 */ -export function TimestampExecution() { - const [currentTimestamp, setCurrentTimestamp] = useState(() => - Math.floor(Date.now()), - ); +export function TimestampExecution(): JSX.Element { + const [currentTimestamp, setCurrentTimestamp] = useState(() => Math.floor(Date.now())); const [showMilliseconds, setShowMilliseconds] = useState(true); const [isRunningTimestamp, setIsRunningTimestamp] = useState(true); @@ -45,19 +35,12 @@ export function TimestampExecution() { setIsRunningTimestamp((prev) => !prev); }, []); - const unitButtonLabel = showMilliseconds - ? '切换为秒显示' - : '切换为毫秒显示'; - const toggleButtonLabel = isRunningTimestamp - ? '停止时间戳自动更新' - : '开始时间戳自动更新'; + const unitButtonLabel = showMilliseconds ? '切换为秒显示' : '切换为毫秒显示'; + const toggleButtonLabel = isRunningTimestamp ? '停止时间戳自动更新' : '开始时间戳自动更新'; const toggleButtonText = isRunningTimestamp ? '停止' : '开始'; return ( - + ) => { + (e: SelectChangeEvent) => { const newZone = e.target.value; setSelectedZone(newZone); if (timestampResult) { @@ -96,7 +96,7 @@ export function TimestampToDatetime() { ); const handleUnitChange = useCallback( - (e: SelectChangeEvent) => { + (e: SelectChangeEvent) => { const newUnit = e.target.value; setUnit(newUnit); if (timestampResult) { @@ -150,8 +150,10 @@ export function TimestampToDatetime() { value={timestampResult} fullWidth variant="outlined" - InputProps={{ - readOnly: true, + slotProps={{ + input: { + readOnly: true, + }, }} /> diff --git a/entrypoints/popup/App.css b/entrypoints/popup/App.css index 4ac0fca..96d0316 100644 --- a/entrypoints/popup/App.css +++ b/entrypoints/popup/App.css @@ -147,7 +147,6 @@ body { .nav-collapse-content .nav-link { padding: 12px 20px; - border-bottom: 1px solid #f8f9fa; width: 100%; text-align: left; box-sizing: border-box; diff --git a/tsconfig.json b/tsconfig.json index 263e7b8..5231664 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -49,7 +49,8 @@ "hooks/**/*", ".wxt/types/**/*.ts", ".wxt/types/*.d.ts", - "components" + "components", + "eslint.config.ts" ], - "exclude": ["node_modules", ".wxt", "eslint.config.ts"] + "exclude": ["node_modules", ".wxt"] } diff --git a/utils/messages.tsx b/utils/messages.tsx index c1d8e06..c9f3b97 100644 --- a/utils/messages.tsx +++ b/utils/messages.tsx @@ -41,7 +41,7 @@ interface ProtocolMap { 'popup:ready': () => void; // --- Content 相关 --- - 'content:save-tracke-events': (event) => boolean; + 'content:save-tracke-events': (event: unknown) => boolean; 'content:start-recording': () => { ok: boolean; error?: string }; 'content:stop-recording': () => { ok: boolean }; 'content:check-status': () => boolean; diff --git a/utils/recordUtils.tsx b/utils/recordUtils.tsx index 18c7ba0..7f13962 100644 --- a/utils/recordUtils.tsx +++ b/utils/recordUtils.tsx @@ -21,7 +21,8 @@ export const downloadHtmlInBackground = (events: unknown[]) => { @@ -59,7 +62,7 @@ export const downloadHtmlInBackground = (events: unknown[]) => { url: reader.result as string, filename: `replay-${Date.now()}.html`, saveAs: true, - }); + }).then(r => console.log(r)); }; reader.readAsDataURL(blob); }; From c01659c9f9022f9b1ce9682c4f3763f5972e7d38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=A8=E9=9C=96=E9=93=83?= Date: Sat, 14 Feb 2026 01:53:27 +0800 Subject: [PATCH 06/10] =?UTF-8?q?docs(readme):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E6=96=87=E6=A1=A3=E4=BB=A5=E5=8F=8D=E6=98=A0?= =?UTF-8?q?=E6=B5=8F=E8=A7=88=E5=99=A8=E6=89=A9=E5=B1=95=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将原有 Create React App 模板文档替换为 Testing Tools 浏览器扩展介绍 - 添加项目概述、功能特性和技术栈说明 - 提供详细的安装运行指南和构建命令 - 补充项目结构、权限说明和主要依赖信息 - 增加贡献指南和许可证说明 --- README.md | 133 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 95 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 58beeac..4c678c1 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,127 @@ -# Getting Started with Create React App +# Testing Tools Browser Extension -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). +这是一个基于 WXT 框架的浏览器扩展项目,提供了多种实用的测试工具,包括时间戳转换、用户操作录制与回放等功能。 -## Available Scripts +## 项目概述 -In the project directory, you can run: +Testing Tools 是一个功能丰富的浏览器扩展,旨在帮助开发者和测试人员更高效地进行网页测试工作。项目采用现代化的技术栈,包括 React 19、TypeScript 和 Material UI,并利用 WXT 框架简化浏览器扩展的开发流程。 -### `npm start` +## 功能特性 -Runs the app in the development mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in your browser. +### 1. 时间戳转换工具 -The page will reload when you make changes.\ -You may also see any lint errors in the console. +- 日期与时间戳之间的双向转换 +- 支持多种日期格式 +- 快速复制转换结果 -### `npm test` +### 2. 录制与回放功能 -Launches the test runner in the interactive watch mode.\ -See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. +- 基于 rrweb 库的用户操作录制 +- 完整的会话回放功能 +- 支持复杂交互场景的重现 -### `npm run build` +### 3. 测试工具页面 -Builds the app for production to the `build` folder.\ -It correctly bundles React in production mode and optimizes the build for the best performance. +- 提供多种实用的测试功能 +- 集成测试库支持 -The build is minified and the filenames include the hashes.\ -Your app is ready to be deployed! +## 技术栈 -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. +- **框架**: WXT (Web Extension Toolkit) +- **前端**: React 19 + TypeScript +- **UI 库**: Material UI +- **状态管理**: React Hooks +- **数据库**: Dexie.js (IndexedDB 包装器) +- **录制回放**: rrweb +- **路由**: React Router DOM -### `npm run eject` +## 项目结构 -**Note: this is a one-way operation. Once you `eject`, you can't go back!** +``` +├── components/ # 可复用的 UI 组件 +├── entrypoints/ # 浏览器扩展入口点 +│ ├── popup/ # 扩展弹窗界面 +│ ├── options/ # 选项页面 +│ ├── offscreen/ # 离屏文档 +│ ├── background.ts # 后台脚本 +│ └── content.ts # 内容脚本 +├── assets/ # 静态资源 +├── wxt.config.ts # WXT 配置文件 +├── package.json # 项目依赖和脚本 +└── README.md # 项目说明文档 +``` -If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. +## 开发环境要求 -Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. +- Node.js >= 18 +- npm 或 yarn -You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. +## 安装与运行 -## Learn More +### 1. 安装依赖 -You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). +```bash +npm install +``` -To learn React, check out the [React documentation](https://reactjs.org/). +### 2. 开发模式 -### Code Splitting +```bash +# Chrome 浏览器 +npm run dev -This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) +# Firefox 浏览器 +npm run dev:firefox +``` -### Analyzing the Bundle Size +### 3. 构建生产版本 -This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) +```bash +# Chrome 浏览器 +npm run build -### Making a Progressive Web App +# Firefox 浏览器 +npm run build:firefox +``` -This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) +### 4. 打包分发 -### Advanced Configuration +```bash +# Chrome 浏览器 +npm run zip -This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) +# Firefox 浏览器 +npm run zip:firefox +``` -### Deployment +## 权限说明 -This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) +扩展请求以下权限: -### `npm run build` fails to minify +- `storage` 和 `unlimitedStorage` - 本地数据存储 +- `clipboardWrite` - 剪贴板写入 +- `activeTab`, `scripting`, `tabs` - 当前标签页控制 +- `offscreen` - 离屏文档处理 +- `downloads` - 下载管理 +- `` - 访问所有网站内容 -This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) +## 主要依赖 + +- `react`, `react-dom` - 前端框架 +- `@mui/material` - UI 组件库 +- `rrweb`, `rrweb-player` - 录制回放功能 +- `dexie`, `dexie-react-hooks` - 数据库操作 +- `react-router-dom` - 路由管理 +- `@testing-library/*` - 测试工具 + +## 贡献指南 + +1. Fork 项目 +2. 创建功能分支 (`git checkout -b feature/AmazingFeature`) +3. 提交更改 (`git commit -m 'Add some AmazingFeature'`) +4. 推送到分支 (`git push origin feature/AmazingFeature`) +5. 创建 Pull Request + +## 许可证 + +此项目为私有项目 (private: true),仅供内部使用。 From 19b7ab141a8418f35ea500d87f46ca4e84795b63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=A8=E9=9C=96=E9=93=83?= Date: Sun, 15 Feb 2026 01:35:23 +0800 Subject: [PATCH 07/10] =?UTF-8?q?feat(extension):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=B0=83=E8=AF=95=E5=99=A8=E5=8A=9F=E8=83=BD=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E7=95=8C=E9=9D=A2=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 集成 Chrome 调试器 API 支持 attach/detach 功能 - 新增 tabUtils 工具模块用于获取活动标签页 ID - 在背景脚本中添加标签页激活和更新事件监听 - 使用 Material-UI 容器和堆叠组件重构页面布局 - 移除 Chromium 启动参数中的自动打开开发者工具选项 - 更新权限配置添加调试器相关权限 - 在测试页面添加多个调试功能按钮和状态管理 --- components/TimestampExecution.tsx | 4 +- entrypoints/background.ts | 40 +++-------- entrypoints/popup/pages/RecordeReplayPage.tsx | 10 +-- entrypoints/popup/pages/TestPage.tsx | 68 +++++++++++++++++++ entrypoints/popup/pages/TimestampPage.tsx | 13 ++-- utils/tabUtils.ts | 35 ++++++++++ web-ext.config.ts | 2 +- wxt.config.ts | 1 + 8 files changed, 129 insertions(+), 44 deletions(-) create mode 100644 utils/tabUtils.ts diff --git a/components/TimestampExecution.tsx b/components/TimestampExecution.tsx index a912131..7ca6b07 100644 --- a/components/TimestampExecution.tsx +++ b/components/TimestampExecution.tsx @@ -40,7 +40,7 @@ export function TimestampExecution(): JSX.Element { const toggleButtonText = isRunningTimestamp ? '停止' : '开始'; return ( - + { ); }); + chrome.tabs.onActivated.addListener((activeInfo) => { + console.log('当前活跃的 Tab ID:', activeInfo.tabId); + }); + + chrome.tabs.onUpdated.addListener((tabId) => { + console.log('加载完成的 Tab ID:', tabId); + }); + onMessage('popup:check-status', async (message) => { console.log(`[background] popup:check-status: ${message}`); const obj = { @@ -109,35 +118,4 @@ export default defineBackground(() => { events.push(eventsList); return true; }); - - async function getActiveTabId() { - const activeTabs = await chrome.tabs.query({ - active: true, - currentWindow: true, - }); - const activeTab = activeTabs[0]; - - if (!activeTab) { - throw new Error('No active tab found.'); - } - - // 检查URL是否受限 - if (activeTab?.url) { - const restrictedProtocols = [ - 'chrome:', - 'chrome-extension:', - 'about:', - 'edge:', - 'view-source:', - 'data:', - 'file:', - ]; - if (restrictedProtocols.some((protocol) => activeTab.url!.startsWith(protocol))) { - throw new Error(`Cannot send message to a restricted URL: ${activeTab.url}`); - } - } - - const sendTo = activeTab.id; - return sendTo!; - } }); diff --git a/entrypoints/popup/pages/RecordeReplayPage.tsx b/entrypoints/popup/pages/RecordeReplayPage.tsx index 7de7e06..1f9dd19 100644 --- a/entrypoints/popup/pages/RecordeReplayPage.tsx +++ b/entrypoints/popup/pages/RecordeReplayPage.tsx @@ -1,7 +1,7 @@ import { useEffect, useState, useMemo } from 'react'; import { AppState } from '../types'; import { sendMessage, onMessage } from '@/utils/messages'; -import { Button } from '@mui/material'; +import { Button, Container, Stack } from '@mui/material'; const RecordeReplayPage = () => { const [status, setStatus] = useState(AppState.READ); @@ -54,8 +54,8 @@ const RecordeReplayPage = () => { }; return ( -
-
+ + -
-
+ + ); }; diff --git a/entrypoints/popup/pages/TestPage.tsx b/entrypoints/popup/pages/TestPage.tsx index 6e915fe..318d9e8 100644 --- a/entrypoints/popup/pages/TestPage.tsx +++ b/entrypoints/popup/pages/TestPage.tsx @@ -1,16 +1,84 @@ import { sendMessage } from '@/utils/messages'; import { Button } from '@mui/material'; +import { useState, useEffect } from 'react'; const TestPage = () => { + const [tabId, setTabId] = useState(-1); + + useEffect(() => { + (async () => { + const activeTabs = await chrome.tabs.query({ + active: true, + currentWindow: true, + }); + console.log(`activeTabs:`, activeTabs[0].id); + if (activeTabs[0].id && activeTabs[0].id > 0) setTabId(activeTabs[0].id); + else throw new Error('no active tab'); + })(); + }); + const handleSeedMessage = async () => { const status = await sendMessage('popup:check-status'); console.log(`[popup]status: ${status}`); }; + + const handleAttach = () => { + try { + chrome.debugger.attach({ tabId: tabId }, '1.2', () => { + console.log('[debugger] attached'); + }); + } catch (err) { + console.error(err); + } + }; + + const handleDetach = async () => { + try { + chrome.debugger.detach({ tabId: tabId }, () => { + console.log('[debugger] detached'); + }); + } catch (err) { + console.error(err); + } + }; + + const handleGetTarget = () => { + chrome.debugger.getTargets((targets) => { + console.log('[debugger] targets:', targets); + }); + }; + + const enableRuntime = () => { + chrome.debugger.sendCommand({ tabId: tabId }, 'Runtime.enable', {}, (res) => { + console.log('[debugger] Runtime.enable:', res); + }); + }; + + const disableRuntime = () => { + chrome.debugger.sendCommand({ tabId: tabId }, 'Runtime.disable', {}, (res) => { + console.log('[debugger] Runtime.disable:', res); + }); + }; return (
+ + + + +
); }; diff --git a/entrypoints/popup/pages/TimestampPage.tsx b/entrypoints/popup/pages/TimestampPage.tsx index d02c555..172ab41 100644 --- a/entrypoints/popup/pages/TimestampPage.tsx +++ b/entrypoints/popup/pages/TimestampPage.tsx @@ -1,14 +1,17 @@ import { TimestampToDatetime } from '@/components/TimestampToDatetime'; import { DatetimeToTimestamp } from '@/components/DatetimeToTimestamp'; import { TimestampExecution } from '@/components/TimestampExecution'; +import { Container, Box } from '@mui/material'; const TimestampPage = () => { return ( -
- - - -
+ + + + + + + ); }; diff --git a/utils/tabUtils.ts b/utils/tabUtils.ts new file mode 100644 index 0000000..95d1dc8 --- /dev/null +++ b/utils/tabUtils.ts @@ -0,0 +1,35 @@ +import { browser } from 'wxt/browser'; + +/** + * 获取当前活动标签页的 ID + * @returns Promise 返回活动标签页的 ID,如果未找到则返回 undefined + */ +export async function getActiveTabId(): Promise { + const activeTabs = await browser.tabs.query({ + active: true, + currentWindow: true, + }); + const activeTab = activeTabs[0]; + + if (!activeTab) { + throw new Error('No active tab found.'); + } + + // 检查URL是否受限 + if (activeTab?.url) { + const restrictedProtocols = [ + 'chrome:', + 'chrome-extension:', + 'about:', + 'edge:', + 'view-source:', + 'data:', + 'file:', + ]; + if (restrictedProtocols.some((protocol) => activeTab.url!.startsWith(protocol))) { + throw new Error(`Cannot send message to a restricted URL: ${activeTab.url}`); + } + } + + return activeTab.id; +} \ No newline at end of file diff --git a/web-ext.config.ts b/web-ext.config.ts index 2e2de5d..63bf5c3 100644 --- a/web-ext.config.ts +++ b/web-ext.config.ts @@ -2,5 +2,5 @@ import { defineWebExtConfig } from 'wxt'; export default defineWebExtConfig({ startUrls: ['https://www.baidu.com', 'chrome://extensions/'], - chromiumArgs: ['chrome://extensions/', '--auto-open-devtools-for-tabs'], + chromiumArgs: ['chrome://extensions/'], }); diff --git a/wxt.config.ts b/wxt.config.ts index 4a50750..d79a761 100644 --- a/wxt.config.ts +++ b/wxt.config.ts @@ -16,6 +16,7 @@ export default defineConfig({ 'tabs', 'offscreen', 'downloads', + 'debugger', ], host_permissions: [''], action: { From 80e510225c58f62e93ca7ccf6cd40b47ee55f3f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=A8=E9=9C=96=E9=93=83?= Date: Sat, 21 Feb 2026 15:09:07 +0800 Subject: [PATCH 08/10] =?UTF-8?q?feat(background):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=BD=95=E5=88=B6=E7=8A=B6=E6=80=81=E6=8C=81=E4=B9=85=E5=8C=96?= =?UTF-8?q?=E5=92=8CTab=E5=88=87=E6=8D=A2=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现录制状态存储到本地storage,包括isRecording、recordingTabId、events和startTime - 添加loadRecorderState和saveRecorderState函数用于状态管理 - 监听Tab切换事件,当切换离开录制Tab时发出警告并通知popup - 监听Tab关闭事件,自动停止录制并清理状态 - 添加录制状态同步检查,确保content script状态一致 - 在开始录制前检查是否已在录制状态,避免重复录制 - 添加错误处理返回详细错误信息 - 优化回放HTML样式和脚本加载方式 - 添加Tab切换消息通信协议支持 --- components/TimestampExecution.tsx | 2 +- entrypoints/background.ts | 172 ++++++++++++++---- entrypoints/popup/pages/RecordeReplayPage.tsx | 11 +- utils/messages.tsx | 6 +- utils/recordUtils.tsx | 29 ++- 5 files changed, 166 insertions(+), 54 deletions(-) diff --git a/components/TimestampExecution.tsx b/components/TimestampExecution.tsx index 7ca6b07..a404562 100644 --- a/components/TimestampExecution.tsx +++ b/components/TimestampExecution.tsx @@ -76,7 +76,7 @@ export function TimestampExecution(): JSX.Element {