feat: 实现 React 页面层右键菜单数据联动

- 新增 useContextMenuData Hook 处理右键菜单数据传递
- 重构 Jwt/Base64Converter/TextStatistics/QrCode 页面接收右键数据
- RouterProvider 支持从 URL 参数解析右键菜单数据
- 添加 contextMenu/pendingData 存储键到 StorageSchema
- 添加 useContextMenuData 单元测试(12个测试用例)
This commit is contained in:
雨霖铃
2026-05-20 20:51:11 +08:00
parent 471e17eedf
commit 3b98c9438e
8 changed files with 422 additions and 30 deletions
+20
View File
@@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
import CopyButton from '@/components/CopyButton';
import { textToBase64, base64ToText } from '@/utils/base64Converter';
import SwitchButtonGroup from '@/components/SwitchButtonGroup';
import { useContextMenuData } from '@/utils/useContextMenuData';
const IMAGE_DATA_URI_PATTERN = /^\s*data:image\//i;
@@ -26,6 +27,25 @@ export default function TextMode({ onSwitchToImageMode }: TextModeProps = {}) {
const [error, setError] = useState<string | null>(null);
const [direction, setDirection] = useState<'encode' | 'decode'>('encode');
const handleContextMenuData = useCallback(
(payload: string) => {
setInput(payload);
setDirection('decode');
setError(null);
try {
const decoded = base64ToText(payload);
setOutput(decoded);
} catch (e) {
const message = e instanceof Error ? e.message : '';
const i18nKey = ERROR_MESSAGE_TO_I18N[message];
setError(i18nKey ? t(i18nKey) : message || t('conversionFailed'));
}
},
[t],
);
useContextMenuData({ featureKey: 'base64Converter', onData: handleContextMenuData });
const actionLabel = direction === 'encode' ? t('encode') : t('decode');
const placeholder =
direction === 'encode' ? t('textInputPlaceholder') : t('base64InputPlaceholder');
+9 -1
View File
@@ -1,4 +1,4 @@
import { useMemo, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { Box, Container, Paper, Stack, Typography } from '@mui/material';
import { useSnackbar } from '@/components/GlobalSnackbar';
import VpnKeyIcon from '@mui/icons-material/VpnKey';
@@ -7,6 +7,7 @@ import { stringifyJson, parseJwt } from '@/utils/jwt';
import CopyButton from '@/components/CopyButton';
import TextInputArea from '@/components/TextInputArea';
import { useLazyTranslation } from '@/utils/useLazyTranslation';
import { useContextMenuData } from '@/utils/useContextMenuData';
interface SectionProps {
title: string;
@@ -61,6 +62,13 @@ export default function Index() {
const { t } = useLazyTranslation('jwt');
const [jwtInput, setJwtInput] = useState('');
const handleContextMenuData = useCallback((payload: string) => {
const cleaned = payload.replace(/^Bearer\s*/i, '').trim();
setJwtInput(cleaned);
}, []);
useContextMenuData({ featureKey: 'jwt', onData: handleContextMenuData });
const result = useMemo(() => {
if (!jwtInput.trim()) {
return null;
+44 -28
View File
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useCallback, useState } from 'react';
import {
Accordion,
AccordionDetails,
@@ -18,6 +18,7 @@ import QRious from 'qrious';
import { qrCodePageStyles } from '@/config/pageTheme';
import { useSnackbar } from '@/components/GlobalSnackbar';
import { useTranslation } from 'react-i18next';
import { useContextMenuData } from '@/utils/useContextMenuData';
interface UrlToQrCodeSectionProps {
expanded: boolean;
@@ -38,6 +39,47 @@ const UrlToQrCodeSection = ({
const [qrCodeDataUrl, setQrCodeDataUrl] = useState('');
const [generating, setGenerating] = useState(false);
const generateQrCodeFromUrl = useCallback(
async (url: string) => {
try {
setGenerating(true);
setUrlError('');
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = 'https://' + url;
}
const qr = new QRious({
value: url,
size: 250,
level: 'H',
foreground: '#000000',
background: '#FFFFFF',
});
setQrCodeDataUrl(qr.toDataURL());
showMessage(t('qrCode:qrCodeSuccess'), { severity: 'success', autoHideDuration: 1000 });
} catch (error) {
console.error('生成二维码失败:', error);
showMessage(t('qrCode:parseError'), { severity: 'error', autoHideDuration: 300 });
} finally {
setGenerating(false);
}
},
[t, showMessage],
);
const handleContextMenuData = useCallback(
(payload: string) => {
setUrlInput(payload);
onExpandedChange(true);
generateQrCodeFromUrl(payload);
},
[onExpandedChange, generateQrCodeFromUrl],
);
useContextMenuData({ featureKey: 'qrCode', onData: handleContextMenuData });
const handleUrlInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setUrlInput(e.target.value);
setUrlError('');
@@ -48,33 +90,7 @@ const UrlToQrCodeSection = ({
setUrlError(t('qrCode:enterUrlError'));
return;
}
try {
setGenerating(true);
setUrlError('');
let url = urlInput;
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = 'https://' + url;
}
// 使用 QRious 替代 qrcode 库,体积更小
const qr = new QRious({
value: url,
size: 250,
level: 'H',
foreground: '#000000',
background: '#FFFFFF',
});
setQrCodeDataUrl(qr.toDataURL());
showMessage(t('qrCode:qrCodeSuccess'), { severity: 'success', autoHideDuration: 1000 });
} catch (error) {
console.error('生成二维码失败:', error);
showMessage(t('qrCode:parseError'), { severity: 'error', autoHideDuration: 300 });
} finally {
setGenerating(false);
}
await generateQrCodeFromUrl(urlInput);
};
const downloadQrCode = () => {
+8 -1
View File
@@ -1,4 +1,4 @@
import { useMemo, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { alpha, Box, Container, Grid, Paper, Typography } from '@mui/material';
import PageHeader from '@/components/PageHeader';
import TextInputArea from '@/components/TextInputArea';
@@ -6,6 +6,7 @@ import DescriptionIcon from '@mui/icons-material/Description';
import { formatByteSize, getTextStats } from '@/utils/textStatistics';
import { textStatisticsPageStyles } from '@/config/pageTheme';
import { useLazyTranslation } from '@/utils/useLazyTranslation';
import { useContextMenuData } from '@/utils/useContextMenuData';
/**
* 文本统计页面组件
@@ -16,6 +17,12 @@ export default function Index() {
const { t } = useLazyTranslation('textStatistics');
const [text, setText] = useState('');
const handleContextMenuData = useCallback((payload: string) => {
setText(payload);
}, []);
useContextMenuData({ featureKey: 'textStatistics', onData: handleContextMenuData });
// 实时计算统计信息,使用 useMemo 优化性能
// 对于 10,000 字符以上的文本,Intl.Segmenter 也能保持良好的性能
const stats = useMemo(() => getTextStats(text), [text]);