refactor:
1、添加全局toast 2、将CopyButton拆分为useCopy hook
This commit is contained in:
@@ -1,5 +1,8 @@
|
|||||||
// App.js
|
// App.js
|
||||||
import { HashRouter as Router, Routes, Route } from 'react-router-dom';
|
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 TimestampPage from './pages/TimestampPage';
|
||||||
import RecordeReplayPage from './pages/RecordeReplayPage';
|
import RecordeReplayPage from './pages/RecordeReplayPage';
|
||||||
import Navbar from './components/Navbar';
|
import Navbar from './components/Navbar';
|
||||||
@@ -13,19 +16,23 @@ const navItems = [
|
|||||||
{ path: '/recorde-replay', label: '录制与回放', element: <RecordeReplayPage /> },
|
{ path: '/recorde-replay', label: '录制与回放', element: <RecordeReplayPage /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const theme = createTheme();
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<Router future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
|
<Router future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
|
||||||
<RoutePersistence />
|
<RoutePersistence />
|
||||||
<div className="app">
|
<ThemeProvider theme={theme}>
|
||||||
|
<CssBaseline />
|
||||||
|
<SnackbarProvider maxSnack={3} anchorOrigin={{ vertical: 'top', horizontal: 'center' }}>
|
||||||
<Navbar items={navItems} />
|
<Navbar items={navItems} />
|
||||||
|
|
||||||
<Routes>
|
<Routes>
|
||||||
{navItems.map((item) => (
|
{navItems.map((item) => (
|
||||||
<Route key={item.path} path={item.path} element={item.element} />
|
<Route key={item.path} path={item.path} element={item.element} />
|
||||||
))}
|
))}
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</SnackbarProvider>
|
||||||
|
</ThemeProvider>
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,6 +1,8 @@
|
|||||||
import { useEffect, useState, useCallback } from 'react';
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
import CopyButton from './CopyButton';
|
|
||||||
import { Button, Paper, Typography, Stack, Box } from '@mui/material';
|
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} 时间戳组件
|
* @returns {JSX.Element} 时间戳组件
|
||||||
*/
|
*/
|
||||||
export function TimestampExecution() {
|
export function TimestampExecution() {
|
||||||
const [currentTimestamp, setCurrentTimestamp] = useState(() =>
|
const [currentTimestamp, setCurrentTimestamp] = useState(() => Math.floor(Date.now()));
|
||||||
Math.floor(Date.now()),
|
|
||||||
);
|
|
||||||
const [showMilliseconds, setShowMilliseconds] = useState(true);
|
const [showMilliseconds, setShowMilliseconds] = useState(true);
|
||||||
const [isRunningTimestamp, setIsRunningTimestamp] = useState(true);
|
const [isRunningTimestamp, setIsRunningTimestamp] = useState(true);
|
||||||
|
|
||||||
@@ -45,19 +45,14 @@ export function TimestampExecution() {
|
|||||||
setIsRunningTimestamp((prev) => !prev);
|
setIsRunningTimestamp((prev) => !prev);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const unitButtonLabel = showMilliseconds
|
const unitButtonLabel = showMilliseconds ? '切换为秒显示' : '切换为毫秒显示';
|
||||||
? '切换为秒显示'
|
const toggleButtonLabel = isRunningTimestamp ? '停止时间戳自动更新' : '开始时间戳自动更新';
|
||||||
: '切换为毫秒显示';
|
|
||||||
const toggleButtonLabel = isRunningTimestamp
|
|
||||||
? '停止时间戳自动更新'
|
|
||||||
: '开始时间戳自动更新';
|
|
||||||
const toggleButtonText = isRunningTimestamp ? '停止' : '开始';
|
const toggleButtonText = isRunningTimestamp ? '停止' : '开始';
|
||||||
|
|
||||||
|
const { isCopied, copy } = useCopy();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Paper elevation={3} sx={{ p: 2, my: 2, borderRadius: 2, minWidth: 320, textAlign: 'center' }}>
|
||||||
elevation={3}
|
|
||||||
sx={{ p: 2, my: 2, borderRadius: 2, minWidth: 320, textAlign: 'center' }}
|
|
||||||
>
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -101,12 +96,21 @@ export function TimestampExecution() {
|
|||||||
切换单位
|
切换单位
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<CopyButton
|
{/* <CopyButton
|
||||||
textToCopy={String(currentTimestamp)}
|
textToCopy={String(currentTimestamp)}
|
||||||
buttonText="复制"
|
buttonText="复制"
|
||||||
aria-label="复制当前时间戳到剪贴板"
|
aria-label="复制当前时间戳到剪贴板"
|
||||||
color="primary"
|
color="primary"
|
||||||
/>
|
/> */}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => copy(String(currentTimestamp))}
|
||||||
|
startIcon={isCopied ? <CheckIcon /> : <ContentCopyIcon />}
|
||||||
|
color={isCopied ? 'success' : 'primary'}
|
||||||
|
>
|
||||||
|
{isCopied ? '已复制' : '复制'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import { useSnackbar } from 'notistack'; // 1. 引入 hook
|
||||||
|
|
||||||
|
export const useCopy = (resetInterval = 2000) => {
|
||||||
|
const [isCopied, setIsCopied] = useState(false);
|
||||||
|
const { enqueueSnackbar } = useSnackbar(); // 2. 获取触发函数
|
||||||
|
|
||||||
|
const copy = useCallback(
|
||||||
|
async (text: string | number) => {
|
||||||
|
if (!text) {
|
||||||
|
enqueueSnackbar('没有可复制的内容', { variant: 'warning' });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(String(text));
|
||||||
|
setIsCopied(true);
|
||||||
|
|
||||||
|
// 3. 触发成功通知
|
||||||
|
enqueueSnackbar('复制成功!', { variant: 'success' });
|
||||||
|
|
||||||
|
// 自动重置状态
|
||||||
|
setTimeout(() => setIsCopied(false), resetInterval);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Copy failed:', error);
|
||||||
|
|
||||||
|
// 4. 触发错误通知
|
||||||
|
enqueueSnackbar('复制失败,请手动复制', { variant: 'error' });
|
||||||
|
|
||||||
|
setIsCopied(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[enqueueSnackbar, resetInterval],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { isCopied, copy };
|
||||||
|
};
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import { useSnackbar } from 'notistack'; // 1. 引入 hook
|
||||||
|
|
||||||
|
export const useCopy = (resetInterval = 2000) => {
|
||||||
|
const [isCopied, setIsCopied] = useState(false);
|
||||||
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
|
|
||||||
|
const copy = useCallback(
|
||||||
|
async (text: string | number) => {
|
||||||
|
if (!text) {
|
||||||
|
enqueueSnackbar('没有可复制的内容', { variant: 'warning' });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(String(text));
|
||||||
|
setIsCopied(true);
|
||||||
|
|
||||||
|
// 3. 触发成功通知
|
||||||
|
enqueueSnackbar('复制成功!', { variant: 'success' });
|
||||||
|
|
||||||
|
// 自动重置状态
|
||||||
|
setTimeout(() => setIsCopied(false), resetInterval);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Copy failed:', error);
|
||||||
|
|
||||||
|
// 4. 触发错误通知
|
||||||
|
enqueueSnackbar('复制失败,请手动复制', { variant: 'error' });
|
||||||
|
|
||||||
|
setIsCopied(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[enqueueSnackbar, resetInterval],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { isCopied, copy };
|
||||||
|
};
|
||||||
Generated
+41
@@ -24,6 +24,7 @@
|
|||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"dexie": "^4.2.1",
|
"dexie": "^4.2.1",
|
||||||
"dexie-react-hooks": "^4.2.0",
|
"dexie-react-hooks": "^4.2.0",
|
||||||
|
"notistack": "^3.0.2",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.3",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.3",
|
||||||
"react-markdown": "^6.0.3",
|
"react-markdown": "^6.0.3",
|
||||||
@@ -5895,6 +5896,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/gopd": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
|
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
|
||||||
@@ -8161,6 +8171,37 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/nth-check": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz",
|
"resolved": "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"dexie": "^4.2.1",
|
"dexie": "^4.2.1",
|
||||||
"dexie-react-hooks": "^4.2.0",
|
"dexie-react-hooks": "^4.2.0",
|
||||||
|
"notistack": "^3.0.2",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.3",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.3",
|
||||||
"react-markdown": "^6.0.3",
|
"react-markdown": "^6.0.3",
|
||||||
|
|||||||
Reference in New Issue
Block a user