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:
@@ -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';
|
||||
|
||||
// 路由配置数据
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user