refactor: 统一扩展消息机制并优化组件结构

1、统一消息系统:将 background、content 和 popup 的通信方式从原生的 chrome.runtime 迁移至 @webext-core/messaging,修复了消息回调返回 undefined 的问题。
2、优化组件结构:将通用组件从 entrypoints/popup/components 迁移至根目录 components,并使用 MUI 重构了 Navbar。
3、同步更新:调整了录制工具 (useRecorder) 和各页面组件(RecordeReplayPage, TestPage等),以适配新的消息协议和组件路径。
This commit is contained in:
雨霖铃
2026-02-13 20:38:15 +08:00
parent 22985f4c18
commit 50034331bc
16 changed files with 252 additions and 271 deletions
+54 -81
View File
@@ -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!;
}
});
+29 -54
View File
@@ -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 };
}
});
},
});
+2 -2
View File
@@ -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';
// 路由配置数据
-134
View File
@@ -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<ButtonProps, 'onClick' | 'variant'> {
textToCopy: string | number;
buttonText?: ReactNode;
successMessage?: string;
errorMessage?: string;
variant?: ButtonProps['variant'];
}
const CopyButton: FC<CopyButtonProps> = ({
textToCopy,
buttonText = '复制',
successMessage = '复制成功!',
errorMessage = '复制失败,请手动复制。',
variant = 'contained',
...buttonProps
}) => {
const [status, setStatus] = useState<CopyStatus>('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 <CheckIcon sx={{ mr: 1 }} fontSize="small" />;
}
return <ContentCopyIcon sx={{ mr: 1 }} fontSize="small" />;
};
return (
<>
<Button
onClick={handleClick}
disabled={status !== 'idle'}
variant={variant}
{...buttonProps}
sx={{
...buttonProps.sx,
transition: 'background-color 0.3s',
...(status === 'success' && {
bgcolor: 'success.main',
'&:hover': {
bgcolor: 'success.dark',
},
}),
}}
>
{renderButtonIcon()}
{buttonText}
</Button>
<Snackbar
open={openSnackbar}
autoHideDuration={2000}
onClose={handleCloseSnackbar}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
>
{snackbarContent ? (
<Alert
onClose={handleCloseSnackbar}
severity={snackbarContent.severity}
variant="filled"
sx={{ width: '100%' }}
>
{snackbarContent.message}
</Alert>
) : undefined}
</Snackbar>
</>
);
};
export default CopyButton;
@@ -1,155 +0,0 @@
import { useState, useCallback } from 'react';
import dayjs from '@/utils/dayjs';
import {
Button,
TextField,
Select,
MenuItem,
FormControl,
InputLabel,
Paper,
Stack,
Box,
SelectChangeEvent,
} from '@mui/material';
const TIME_ZONE_LIST = [
'America/New_York',
'America/Chicago',
'America/Denver',
'America/Los_Angeles',
'America/Anchorage',
'America/Honolulu',
'Europe/London',
'Europe/Paris',
'Europe/Berlin',
'Europe/Moscow',
'Asia/Tokyo',
'Asia/Shanghai',
'Asia/Hong_Kong',
'Asia/Singapore',
'Asia/Dubai',
'Asia/Kolkata',
'Australia/Sydney',
'Pacific/Auckland',
];
const TIMESTAMP_UNITS = [
{ 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) => {
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();
},
[],
);
const handleConvert = useCallback(() => {
const newResult = performConversion(dateValue, selectedZone, unit);
setResult(newResult);
}, [dateValue, selectedZone, unit, performConversion]);
const handleUnitChange = useCallback(
(e: SelectChangeEvent<string>) => {
const newUnit = e.target.value;
setUnit(newUnit);
if (result) {
setResult(performConversion(dateValue, selectedZone, newUnit) || '');
}
},
[dateValue, selectedZone, result, performConversion],
);
const handleZoneChange = useCallback((e: SelectChangeEvent<string>) => {
setSelectedZone(e.target.value);
}, []);
return (
<Paper elevation={3} sx={{ p: 2, my: 2, borderRadius: 2 }}>
<Stack spacing={2} sx={{ mt: 2 }}>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
<TextField
label="输入日期时间"
value={dateValue}
onChange={(e) => {
setDateValue(e.target.value);
if (error) setError('');
}}
error={!!error}
helperText={error || '格式: YYYY/MM/DD HH:mm:ss'}
fullWidth
variant="outlined"
/>
<FormControl fullWidth>
<InputLabel></InputLabel>
<Select
value={selectedZone}
label="时区"
onChange={handleZoneChange}
MenuProps={{ disableScrollLock: true }}
>
{TIME_ZONE_LIST.map((zone) => (
<MenuItem key={zone} value={zone}>
{zone}
</MenuItem>
))}
</Select>
</FormControl>
</Stack>
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<Button variant="contained" size="medium" color="primary" onClick={handleConvert}>
</Button>
</Box>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
<TextField
label="转换结果"
value={result}
fullWidth
variant="outlined"
InputProps={{
readOnly: true,
}}
/>
<FormControl fullWidth>
<InputLabel></InputLabel>
<Select
value={unit}
label="单位"
onChange={handleUnitChange}
MenuProps={{ disableScrollLock: true }}
>
{TIMESTAMP_UNITS.map(({ value, label }) => (
<MenuItem key={value} value={value}>
{label}
</MenuItem>
))}
</Select>
</FormControl>
</Stack>
</Stack>
</Paper>
);
}
-97
View File
@@ -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 (
<nav className="nav">
<div className="nav-content">
<ul className={`nav-list ${isMenuOpen ? 'open' : ''}`}>
{/* 显示的导航项 */}
{visibleNavItems.map((item) => (
<li key={item?.path}>
<NavLink
to={item?.path}
className={({ isActive }) => (isActive ? 'nav-link active' : 'nav-link')}
onClick={() => isMobile && setIsMenuOpen(false)}
>
{item?.label}
</NavLink>
</li>
))}
{/* 折叠区域逻辑 */}
{collapsedNavItems.length > 0 && (
<li className="nav-collapse-item">
<div className={`nav-collapse-content ${isMenuOpen ? 'show' : ''}`}>
{isMenuOpen &&
collapsedNavItems.map((item) => (
<NavLink
key={item?.path}
to={item?.path}
className={({ isActive }) => (isActive ? 'nav-link active' : 'nav-link')}
onClick={() => setIsMenuOpen(false)}
>
{item?.label}
</NavLink>
))}
</div>
<IconButton
color="inherit"
onClick={() => setIsMenuOpen(!isMenuOpen)}
aria-label={isMenuOpen ? '收起菜单' : '展开菜单'}
>
{isMenuOpen ? <CloseIcon /> : <MenuIcon />}
</IconButton>
</li>
)}
</ul>
</div>
</nav>
);
}
export default Navbar;
@@ -1,45 +0,0 @@
import { useEffect, useRef } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { storage } from '@/utils/storage';
const RoutePersistence = () => {
const location = useLocation();
const navigate = useNavigate();
const isRestored = useRef(false);
useEffect(() => {
const restoreRoute = async () => {
if (isRestored.current) return;
try {
const lastRoute = await storage.get('app/lastRoute');
if (lastRoute && lastRoute !== '/' && location.pathname === '/') {
navigate(lastRoute, { replace: true });
console.log('跳转路由', lastRoute);
}
} catch (err) {
console.error('恢复路由失败', err);
} finally {
isRestored.current = true;
}
};
restoreRoute();
}, [location, navigate]);
useEffect(() => {
const saveRoute = async () => {
if (!isRestored.current) return;
await storage.set('app/lastRoute', location.pathname);
console.log('保存路由', location.pathname);
};
saveRoute();
}, [location]);
return null;
};
export default RoutePersistence;
@@ -1,123 +0,0 @@
import { useEffect, useState, useCallback } from 'react';
import CopyButton from './CopyButton';
import { Button, Paper, Typography, Stack, Box } from '@mui/material';
/**
* 时间戳显示和执行组件
*
* @component
* @example
* ```jsx
* <TimestampExecution />
* ```
*
* @returns {JSX.Element} 时间戳组件
*/
export function TimestampExecution() {
const [currentTimestamp, setCurrentTimestamp] = useState(() =>
Math.floor(Date.now()),
);
const [showMilliseconds, setShowMilliseconds] = useState(true);
const [isRunningTimestamp, setIsRunningTimestamp] = useState(true);
const displayTimestamp = showMilliseconds
? currentTimestamp
: Math.floor(currentTimestamp / 1000);
const unitText = 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);
}, []);
const unitButtonLabel = showMilliseconds
? '切换为秒显示'
: '切换为毫秒显示';
const toggleButtonLabel = isRunningTimestamp
? '停止时间戳自动更新'
: '开始时间戳自动更新';
const toggleButtonText = isRunningTimestamp ? '停止' : '开始';
return (
<Paper
elevation={3}
sx={{ p: 2, my: 2, borderRadius: 2, minWidth: 320, textAlign: 'center' }}
>
<Box
sx={{
display: 'flex',
alignItems: 'baseline',
justifyContent: 'center',
gap: 1,
mb: 2,
overflowX: 'auto',
pb: 1,
}}
>
<Typography
variant="h4"
component="span"
sx={{
fontFamily: 'monospace',
fontWeight: 'bold',
color: 'primary.main',
whiteSpace: 'nowrap',
}}
>
{displayTimestamp}
</Typography>
<Typography
variant="h6"
component="span"
sx={{ color: 'text.secondary', whiteSpace: 'nowrap' }}
>
{unitText}
</Typography>
</Box>
<Stack direction="row" spacing={2} justifyContent="center" flexWrap="wrap">
<Button
variant="contained"
color="secondary"
onClick={toggleUnit}
aria-label={unitButtonLabel}
title={unitButtonLabel}
>
</Button>
<CopyButton
textToCopy={String(currentTimestamp)}
buttonText="复制"
aria-label="复制当前时间戳到剪贴板"
color="primary"
/>
<Button
variant="contained"
color={isRunningTimestamp ? 'error' : 'primary'}
onClick={toggleTimestamp}
aria-label={toggleButtonLabel}
title={toggleButtonLabel}
>
{toggleButtonText}
</Button>
</Stack>
</Paper>
);
}
@@ -1,176 +0,0 @@
import { useState, useCallback } from 'react';
import dayjs from 'dayjs';
import {
Button,
TextField,
Select,
MenuItem,
FormControl,
InputLabel,
Paper,
Stack,
Box,
SelectChangeEvent,
} from '@mui/material';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
dayjs.extend(utc);
dayjs.extend(timezone);
const TIME_ZONE_LIST = [
'America/New_York',
'America/Chicago',
'America/Denver',
'America/Los_Angeles',
'America/Anchorage',
'America/Honolulu',
'Europe/London',
'Europe/Paris',
'Europe/Berlin',
'Europe/Moscow',
'Asia/Tokyo',
'Asia/Shanghai',
'Asia/Hong_Kong',
'Asia/Singapore',
'Asia/Dubai',
'Asia/Kolkata',
'Australia/Sydney',
'Pacific/Auckland',
];
const TIMESTAMP_UNITS = [
{ value: 'milliseconds', label: '毫秒 (ms)' },
{ value: 'seconds', label: '秒 (s)' },
];
export function TimestampToDatetime() {
const [timestampValue, setTimestampValue] = useState(() => dayjs().valueOf().toString());
const [timestampResult, setTimestampResult] = useState('');
const [unit, setUnit] = useState('milliseconds');
const [selectedZone, setSelectedZone] = useState('Asia/Shanghai');
const [error, setError] = useState('');
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: React.ChangeEvent<HTMLInputElement>) => {
setTimestampValue(e.target.value);
if (error) setError('');
},
[error],
);
const handleZoneChange = useCallback(
(e: SelectChangeEvent<string>) => {
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: SelectChangeEvent<string>) => {
const newUnit = e.target.value;
setUnit(newUnit);
if (timestampResult) {
const newResult = performConversion(timestampValue, selectedZone, newUnit);
setTimestampResult(newResult || '');
}
},
[performConversion, timestampResult, timestampValue, selectedZone],
);
return (
<Paper elevation={3} sx={{ p: 2, my: 2, borderRadius: 2 }}>
<Stack spacing={2} sx={{ mt: 2 }}>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
<TextField
label="输入时间戳"
placeholder="如: 1704067200000"
value={timestampValue}
onChange={handleInputChange}
error={!!error}
helperText={error}
fullWidth
variant="outlined"
/>
<FormControl fullWidth>
<InputLabel></InputLabel>
<Select
value={unit}
label="单位"
onChange={handleUnitChange}
MenuProps={{ disableScrollLock: true }}
>
{TIMESTAMP_UNITS.map(({ value, label }) => (
<MenuItem key={value} value={value}>
{label}
</MenuItem>
))}
</Select>
</FormControl>
</Stack>
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<Button variant="contained" size="medium" color="primary" onClick={handleConvert}>
</Button>
</Box>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
<TextField
label="转换结果"
value={timestampResult}
fullWidth
variant="outlined"
InputProps={{
readOnly: true,
}}
/>
<FormControl fullWidth>
<InputLabel></InputLabel>
<Select
value={selectedZone}
label="时区"
onChange={handleZoneChange}
MenuProps={{ disableScrollLock: true }}
>
{TIME_ZONE_LIST.map((zone) => (
<MenuItem key={zone} value={zone}>
{zone}
</MenuItem>
))}
</Select>
</FormControl>
</Stack>
</Stack>
</Paper>
);
}
+23 -21
View File
@@ -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);
+2 -6
View File
@@ -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 (
<div>
+3 -3
View File
@@ -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 (