refactor(qrcode): 组件化重构,采用 Context + Hook 模式
- 创建 QrCodeContext 和 QrCodeProvider 管理共享状态 - 提取 useQrCode Hook 封装所有状态和业务逻辑 - 创建 GeneratePanel 组件处理二维码生成模式 - 创建 ParsePanel 组件处理二维码解析模式 - 简化 index.tsx 为容器组件 (340行 → 47行) - 删除未使用的旧组件文件 (QrCodeToUrlSection, UrlToQrCodeSection) - 清理 types.ts 中未使用的类型定义 - 603 个测试全部通过
This commit is contained in:
@@ -1,157 +0,0 @@
|
|||||||
import { useCallback } from 'react';
|
|
||||||
import {
|
|
||||||
Accordion,
|
|
||||||
AccordionDetails,
|
|
||||||
AccordionSummary,
|
|
||||||
Alert,
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
CircularProgress,
|
|
||||||
InputAdornment,
|
|
||||||
Stack,
|
|
||||||
TextField,
|
|
||||||
Typography,
|
|
||||||
} from '@mui/material';
|
|
||||||
import LinkIcon from '@mui/icons-material/Link';
|
|
||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
|
||||||
import CopyButton from '@/components/CopyButton';
|
|
||||||
import ImageUploader from '@/components/ImageUploader';
|
|
||||||
import { qrCodePageStyles } from '@/config/pageTheme';
|
|
||||||
import { useSnackbar } from '@/components/GlobalSnackbar';
|
|
||||||
import { parseQrCodeFromFile } from '@/utils/qrCodeParser';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import type { QrCodeParserProps } from './types';
|
|
||||||
|
|
||||||
const QrCodeToUrlSection = ({
|
|
||||||
decodedResult,
|
|
||||||
onDecodedResultChange,
|
|
||||||
parsing,
|
|
||||||
onParsingChange,
|
|
||||||
parseError,
|
|
||||||
onParseErrorChange,
|
|
||||||
selectedFile,
|
|
||||||
onFileChange,
|
|
||||||
onClearFile,
|
|
||||||
previewUrl,
|
|
||||||
onPreviewUrlChange,
|
|
||||||
dragging,
|
|
||||||
onDraggingChange,
|
|
||||||
expanded,
|
|
||||||
onExpandedChange,
|
|
||||||
forceExpanded = false,
|
|
||||||
}: QrCodeParserProps) => {
|
|
||||||
const { t } = useTranslation(['qrCode']);
|
|
||||||
const { showMessage } = useSnackbar();
|
|
||||||
|
|
||||||
const handleClearFile = useCallback(() => {
|
|
||||||
onClearFile();
|
|
||||||
onDecodedResultChange('');
|
|
||||||
onParseErrorChange('');
|
|
||||||
}, [onClearFile, onDecodedResultChange, onParseErrorChange]);
|
|
||||||
|
|
||||||
const parseQrCode = async () => {
|
|
||||||
if (!selectedFile) {
|
|
||||||
showMessage(t('qrCode:selectImage'), { severity: 'error', autoHideDuration: 300 });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
onParsingChange(true);
|
|
||||||
onParseErrorChange('');
|
|
||||||
onDecodedResultChange('');
|
|
||||||
|
|
||||||
const result = await parseQrCodeFromFile(selectedFile);
|
|
||||||
|
|
||||||
if (result.success && result.data) {
|
|
||||||
onDecodedResultChange(result.data);
|
|
||||||
showMessage(t('qrCode:parseSuccess'), { severity: 'success', autoHideDuration: 1000 });
|
|
||||||
} else {
|
|
||||||
onParseErrorChange(result.error || t('qrCode:noQrDetected'));
|
|
||||||
showMessage(result.error || t('qrCode:noQrDetected'), {
|
|
||||||
severity: 'error',
|
|
||||||
autoHideDuration: 1000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('解析二维码失败:', error);
|
|
||||||
onParseErrorChange(t('qrCode:parseError'));
|
|
||||||
showMessage(t('qrCode:parseError'), { severity: 'error', autoHideDuration: 300 });
|
|
||||||
} finally {
|
|
||||||
onParsingChange(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Accordion
|
|
||||||
expanded={forceExpanded ? true : expanded}
|
|
||||||
onChange={forceExpanded ? undefined : (_, isExpanded) => onExpandedChange(isExpanded)}
|
|
||||||
sx={forceExpanded ? qrCodePageStyles.ACCORDION_DESKTOP : qrCodePageStyles.ACCORDION}
|
|
||||||
>
|
|
||||||
<AccordionSummary expandIcon={<ExpandMoreIcon />} sx={qrCodePageStyles.ACCORDION_SUMMARY}>
|
|
||||||
<Box sx={qrCodePageStyles.ACCORDION_TITLE_ICON}>
|
|
||||||
<LinkIcon color="success" />
|
|
||||||
<Typography variant="subtitle1" sx={qrCodePageStyles.ACCORDION_TITLE_TEXT}>
|
|
||||||
{t('qrCode:qrToUrl')}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</AccordionSummary>
|
|
||||||
<AccordionDetails>
|
|
||||||
<Stack spacing={3}>
|
|
||||||
<ImageUploader
|
|
||||||
selectedFile={selectedFile}
|
|
||||||
onFileChange={onFileChange}
|
|
||||||
onClearFile={handleClearFile}
|
|
||||||
previewUrl={previewUrl}
|
|
||||||
onPreviewUrlChange={onPreviewUrlChange}
|
|
||||||
dragging={dragging}
|
|
||||||
onDraggingChange={onDraggingChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
startIcon={parsing ? <CircularProgress size={16} color="inherit" /> : <LinkIcon />}
|
|
||||||
onClick={parseQrCode}
|
|
||||||
disabled={parsing}
|
|
||||||
sx={qrCodePageStyles.PRIMARY_BUTTON}
|
|
||||||
>
|
|
||||||
{parsing ? t('qrCode:parsing') : t('qrCode:parseButton')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Box sx={qrCodePageStyles.RESULT_INPUT}>
|
|
||||||
<TextField
|
|
||||||
label={t('qrCode:resultLabel')}
|
|
||||||
value={decodedResult}
|
|
||||||
fullWidth
|
|
||||||
variant="outlined"
|
|
||||||
slotProps={{
|
|
||||||
input: {
|
|
||||||
readOnly: true,
|
|
||||||
endAdornment: (
|
|
||||||
<InputAdornment position="end">
|
|
||||||
<CopyButton
|
|
||||||
text={decodedResult}
|
|
||||||
tooltip={t('qrCode:copyTooltip')}
|
|
||||||
size="small"
|
|
||||||
color={qrCodePageStyles.primaryColor}
|
|
||||||
showMessage={showMessage}
|
|
||||||
/>
|
|
||||||
</InputAdornment>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
sx={qrCodePageStyles.INPUT_STYLE}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{parseError && (
|
|
||||||
<Alert severity="error" sx={{ borderRadius: 3 }}>
|
|
||||||
{parseError}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</AccordionDetails>
|
|
||||||
</Accordion>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QrCodeToUrlSection;
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
import React, { useCallback } from 'react';
|
|
||||||
import {
|
|
||||||
Accordion,
|
|
||||||
AccordionDetails,
|
|
||||||
AccordionSummary,
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
CircularProgress,
|
|
||||||
Stack,
|
|
||||||
TextField,
|
|
||||||
Typography,
|
|
||||||
} from '@mui/material';
|
|
||||||
import QrCodeIcon from '@mui/icons-material/QrCode';
|
|
||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
|
||||||
import QRious from 'qrious';
|
|
||||||
import { qrCodePageStyles } from '@/config/pageTheme';
|
|
||||||
import { useSnackbar } from '@/components/GlobalSnackbar';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useContextMenuData } from '@/utils/useContextMenuData';
|
|
||||||
import QrCodePreview from '@/components/QrCodePreview';
|
|
||||||
import type { QrCodeGeneratorProps } from './types';
|
|
||||||
|
|
||||||
const UrlToQrCodeSection = ({
|
|
||||||
textToEncode,
|
|
||||||
onTextChange,
|
|
||||||
qrCodeDataUrl,
|
|
||||||
onQrCodeDataUrlChange,
|
|
||||||
generating,
|
|
||||||
onGeneratingChange,
|
|
||||||
inputError,
|
|
||||||
onInputErrorChange,
|
|
||||||
expanded,
|
|
||||||
onExpandedChange,
|
|
||||||
forceExpanded = false,
|
|
||||||
}: QrCodeGeneratorProps) => {
|
|
||||||
const { t } = useTranslation(['qrCode']);
|
|
||||||
const { showMessage } = useSnackbar();
|
|
||||||
|
|
||||||
const generateQrCodeFromUrl = useCallback(
|
|
||||||
async (url: string) => {
|
|
||||||
try {
|
|
||||||
onGeneratingChange(true);
|
|
||||||
onInputErrorChange('');
|
|
||||||
|
|
||||||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
|
||||||
url = 'https://' + url;
|
|
||||||
}
|
|
||||||
|
|
||||||
const qr = new QRious({
|
|
||||||
value: url,
|
|
||||||
size: 250,
|
|
||||||
level: 'H',
|
|
||||||
foreground: '#000000',
|
|
||||||
background: '#FFFFFF',
|
|
||||||
});
|
|
||||||
|
|
||||||
onQrCodeDataUrlChange(qr.toDataURL());
|
|
||||||
showMessage(t('qrCode:qrCodeSuccess'), { severity: 'success', autoHideDuration: 1000 });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('生成二维码失败:', error);
|
|
||||||
showMessage(t('qrCode:parseError'), { severity: 'error', autoHideDuration: 300 });
|
|
||||||
} finally {
|
|
||||||
onGeneratingChange(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[t, showMessage, onGeneratingChange, onInputErrorChange, onQrCodeDataUrlChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleContextMenuData = useCallback(
|
|
||||||
(payload: string) => {
|
|
||||||
onTextChange(payload);
|
|
||||||
onExpandedChange(true);
|
|
||||||
generateQrCodeFromUrl(payload);
|
|
||||||
},
|
|
||||||
[onExpandedChange, generateQrCodeFromUrl, onTextChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
useContextMenuData({ featureKey: 'qrCode', onData: handleContextMenuData });
|
|
||||||
|
|
||||||
const handleUrlInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
onTextChange(e.target.value);
|
|
||||||
onInputErrorChange('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const generateQrCode = async () => {
|
|
||||||
if (!textToEncode) {
|
|
||||||
onInputErrorChange(t('qrCode:enterUrlError'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await generateQrCodeFromUrl(textToEncode);
|
|
||||||
};
|
|
||||||
|
|
||||||
const downloadQrCode = () => {
|
|
||||||
if (!qrCodeDataUrl) return;
|
|
||||||
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = qrCodeDataUrl;
|
|
||||||
link.download = 'qrcode.png';
|
|
||||||
link.click();
|
|
||||||
showMessage(t('qrCode:qrCodeDownloadSuccess'), { severity: 'success', autoHideDuration: 300 });
|
|
||||||
};
|
|
||||||
|
|
||||||
const copyQrCode = async () => {
|
|
||||||
if (!qrCodeDataUrl) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(qrCodeDataUrl);
|
|
||||||
const blob = await response.blob();
|
|
||||||
|
|
||||||
await navigator.clipboard.write([
|
|
||||||
new ClipboardItem({
|
|
||||||
'image/png': blob,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
showMessage(t('qrCode:qrCodeCopySuccess'), { severity: 'success', autoHideDuration: 1000 });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('复制二维码失败:', error);
|
|
||||||
showMessage(t('qrCode:parseError'), { severity: 'error', autoHideDuration: 300 });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Accordion
|
|
||||||
expanded={forceExpanded ? true : expanded}
|
|
||||||
onChange={forceExpanded ? undefined : (_, isExpanded) => onExpandedChange(isExpanded)}
|
|
||||||
sx={forceExpanded ? qrCodePageStyles.ACCORDION_DESKTOP : qrCodePageStyles.ACCORDION}
|
|
||||||
>
|
|
||||||
<AccordionSummary expandIcon={<ExpandMoreIcon />} sx={qrCodePageStyles.ACCORDION_SUMMARY}>
|
|
||||||
<Box sx={qrCodePageStyles.ACCORDION_TITLE_ICON}>
|
|
||||||
<QrCodeIcon color="primary" />
|
|
||||||
<Typography variant="subtitle1" sx={qrCodePageStyles.ACCORDION_TITLE_TEXT}>
|
|
||||||
{t('qrCode:urlToQr')}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</AccordionSummary>
|
|
||||||
<AccordionDetails>
|
|
||||||
<Stack spacing={3}>
|
|
||||||
<TextField
|
|
||||||
label={t('qrCode:urlInputLabel')}
|
|
||||||
placeholder={t('qrCode:urlInputPlaceholder')}
|
|
||||||
value={textToEncode}
|
|
||||||
onChange={handleUrlInputChange}
|
|
||||||
fullWidth
|
|
||||||
variant="outlined"
|
|
||||||
error={!!inputError}
|
|
||||||
helperText={inputError}
|
|
||||||
sx={qrCodePageStyles.INPUT_STYLE}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
startIcon={generating ? <CircularProgress size={16} color="inherit" /> : <QrCodeIcon />}
|
|
||||||
onClick={generateQrCode}
|
|
||||||
disabled={generating}
|
|
||||||
sx={qrCodePageStyles.PRIMARY_BUTTON}
|
|
||||||
>
|
|
||||||
{generating ? t('qrCode:generating') : t('qrCode:generateButton')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Box className="qr-flex-grow">
|
|
||||||
<QrCodePreview
|
|
||||||
qrCodeDataUrl={qrCodeDataUrl}
|
|
||||||
onDownload={downloadQrCode}
|
|
||||||
onCopy={copyQrCode}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
</AccordionDetails>
|
|
||||||
</Accordion>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default UrlToQrCodeSection;
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { Grid } from '@mui/material';
|
||||||
|
import TextInputArea from '@/components/TextInputArea';
|
||||||
|
import QrCodePreview from '@/components/QrCodePreview';
|
||||||
|
import { useSnackbar } from '@/components/GlobalSnackbar';
|
||||||
|
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
||||||
|
import { useQrCodeContext } from '../contexts/QrCodeContext';
|
||||||
|
|
||||||
|
export default function GeneratePanel() {
|
||||||
|
const { t } = useLazyTranslation('qrCode');
|
||||||
|
const { showMessage } = useSnackbar();
|
||||||
|
const { generatorState, setTextToEncode, downloadQrCode, copyQrCode } = useQrCodeContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
<Grid size={{ xs: 12, md: 6 }}>
|
||||||
|
<TextInputArea
|
||||||
|
title={t('qrCode:urlInputLabel')}
|
||||||
|
value={generatorState.textToEncode}
|
||||||
|
onChange={setTextToEncode}
|
||||||
|
placeholder={t('qrCode:urlInputPlaceholder')}
|
||||||
|
showCount
|
||||||
|
showClear
|
||||||
|
allowCopy
|
||||||
|
externalError={generatorState.inputError}
|
||||||
|
showMessage={showMessage}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, md: 6 }}>
|
||||||
|
<QrCodePreview
|
||||||
|
qrCodeDataUrl={generatorState.qrCodeDataUrl}
|
||||||
|
onDownload={downloadQrCode}
|
||||||
|
onCopy={copyQrCode}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { useEffect, useCallback } from 'react';
|
||||||
|
import { Grid } from '@mui/material';
|
||||||
|
import TextInputArea from '@/components/TextInputArea';
|
||||||
|
import ImageUploader from '@/components/ImageUploader';
|
||||||
|
import { useSnackbar } from '@/components/GlobalSnackbar';
|
||||||
|
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
||||||
|
import { useQrCodeContext } from '../contexts/QrCodeContext';
|
||||||
|
|
||||||
|
export default function ParsePanel() {
|
||||||
|
const { t } = useLazyTranslation('qrCode');
|
||||||
|
const { showMessage } = useSnackbar();
|
||||||
|
const { parserState, setParserState, parseQrCode, handleFileChange, handleClearFile } =
|
||||||
|
useQrCodeContext();
|
||||||
|
|
||||||
|
// 全局粘贴事件监听
|
||||||
|
const handlePaste = useCallback(
|
||||||
|
async (e: ClipboardEvent) => {
|
||||||
|
const items = e.clipboardData?.items;
|
||||||
|
if (!items) return;
|
||||||
|
|
||||||
|
// 检查是否有图片
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
if (items[i].type.startsWith('image/')) {
|
||||||
|
e.preventDefault();
|
||||||
|
const file = items[i].getAsFile();
|
||||||
|
if (file) {
|
||||||
|
handleFileChange(file);
|
||||||
|
showMessage(t('qrCode:imagePasted'), { severity: 'success', autoHideDuration: 1000 });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有 Base64 字符串
|
||||||
|
const text = e.clipboardData?.getData('text/plain');
|
||||||
|
if (text && text.startsWith('data:image/')) {
|
||||||
|
e.preventDefault();
|
||||||
|
try {
|
||||||
|
const response = await fetch(text);
|
||||||
|
const blob = await response.blob();
|
||||||
|
const file = new File([blob], 'pasted-image.png', { type: blob.type });
|
||||||
|
handleFileChange(file);
|
||||||
|
showMessage(t('qrCode:imagePasted'), { severity: 'success', autoHideDuration: 1000 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('处理 Base64 图片失败:', error);
|
||||||
|
showMessage(t('qrCode:imagePasteError'), { severity: 'error', autoHideDuration: 3000 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleFileChange, showMessage, t],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('paste', handlePaste);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('paste', handlePaste);
|
||||||
|
};
|
||||||
|
}, [handlePaste]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
<Grid size={{ xs: 12, md: 6 }}>
|
||||||
|
<ImageUploader
|
||||||
|
selectedFile={parserState.selectedFile}
|
||||||
|
onFileChange={handleFileChange}
|
||||||
|
onClearFile={handleClearFile}
|
||||||
|
previewUrl={parserState.previewUrl}
|
||||||
|
onPreviewUrlChange={(url) => setParserState((prev) => ({ ...prev, previewUrl: url }))}
|
||||||
|
dragging={parserState.dragging}
|
||||||
|
onDraggingChange={(dragging) => setParserState((prev) => ({ ...prev, dragging }))}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, md: 6 }}>
|
||||||
|
<TextInputArea
|
||||||
|
title={t('qrCode:resultLabel')}
|
||||||
|
value={parserState.decodedResult}
|
||||||
|
readOnly
|
||||||
|
showClear={false}
|
||||||
|
allowCopy
|
||||||
|
externalError={parserState.parseError}
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
key: 'parse',
|
||||||
|
label: parserState.parsing ? t('qrCode:parsing') : t('qrCode:parseButton'),
|
||||||
|
type: 'primary',
|
||||||
|
position: 'bottom',
|
||||||
|
disabled: !parserState.selectedFile || parserState.parsing,
|
||||||
|
onClick: () => {
|
||||||
|
if (parserState.selectedFile) {
|
||||||
|
parseQrCode(parserState.selectedFile);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
showMessage={showMessage}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { createContext, useContext } from 'react';
|
||||||
|
import type { QrCodeMode, QrCodeGeneratorState, QrCodeParserState } from '../types';
|
||||||
|
|
||||||
|
export interface QrCodeContextValue {
|
||||||
|
// 模式
|
||||||
|
mode: QrCodeMode;
|
||||||
|
setMode: (mode: QrCodeMode) => void;
|
||||||
|
|
||||||
|
// 生成器状态
|
||||||
|
generatorState: QrCodeGeneratorState;
|
||||||
|
setTextToEncode: (text: string) => void;
|
||||||
|
generateQrCode: (text: string) => Promise<void>;
|
||||||
|
downloadQrCode: () => void;
|
||||||
|
copyQrCode: () => Promise<void>;
|
||||||
|
|
||||||
|
// 解析器状态
|
||||||
|
parserState: QrCodeParserState;
|
||||||
|
setParserState: React.Dispatch<React.SetStateAction<QrCodeParserState>>;
|
||||||
|
parseQrCode: (file: File) => Promise<void>;
|
||||||
|
handleFileChange: (file: File) => void;
|
||||||
|
handleClearFile: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QrCodeContext = createContext<QrCodeContextValue | null>(null);
|
||||||
|
|
||||||
|
export function useQrCodeContext() {
|
||||||
|
const context = useContext(QrCodeContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useQrCodeContext must be used within QrCodeProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
import { useState, useCallback, useEffect, useRef } from 'react';
|
||||||
|
import QRious from 'qrious';
|
||||||
|
import { useSnackbar } from '@/components/GlobalSnackbar';
|
||||||
|
import { parseQrCodeFromFile } from '@/utils/qrCodeParser';
|
||||||
|
import { useContextMenuData } from '@/utils/useContextMenuData';
|
||||||
|
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
||||||
|
import { useDebounce } from '@/utils/useDebounce';
|
||||||
|
import type { QrCodeContextValue } from '../contexts/QrCodeContext';
|
||||||
|
import type { QrCodeMode, QrCodeGeneratorState, QrCodeParserState } from '../types';
|
||||||
|
|
||||||
|
export function useQrCode(): QrCodeContextValue {
|
||||||
|
const { t } = useLazyTranslation('qrCode');
|
||||||
|
const { showMessage } = useSnackbar();
|
||||||
|
|
||||||
|
// 当前模式
|
||||||
|
const [mode, setMode] = useState<QrCodeMode>('generate');
|
||||||
|
|
||||||
|
// 二维码生成器状态
|
||||||
|
const [generatorState, setGeneratorState] = useState<QrCodeGeneratorState>({
|
||||||
|
textToEncode: '',
|
||||||
|
qrCodeDataUrl: '',
|
||||||
|
generating: false,
|
||||||
|
inputError: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 二维码解析器状态
|
||||||
|
const [parserState, setParserState] = useState<QrCodeParserState>({
|
||||||
|
decodedResult: '',
|
||||||
|
parsing: false,
|
||||||
|
parseError: '',
|
||||||
|
selectedFile: null,
|
||||||
|
previewUrl: '',
|
||||||
|
dragging: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 生成二维码
|
||||||
|
const generateQrCode = useCallback(
|
||||||
|
async (text: string) => {
|
||||||
|
if (!text) {
|
||||||
|
setGeneratorState((prev) => ({ ...prev, qrCodeDataUrl: '' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setGeneratorState((prev) => ({ ...prev, generating: true, inputError: '' }));
|
||||||
|
|
||||||
|
let url = text;
|
||||||
|
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||||
|
url = 'https://' + url;
|
||||||
|
}
|
||||||
|
|
||||||
|
const qr = new QRious({
|
||||||
|
value: url,
|
||||||
|
size: 250,
|
||||||
|
level: 'H',
|
||||||
|
foreground: '#000000',
|
||||||
|
background: '#FFFFFF',
|
||||||
|
});
|
||||||
|
|
||||||
|
setGeneratorState((prev) => ({ ...prev, qrCodeDataUrl: qr.toDataURL() }));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('生成二维码失败:', error);
|
||||||
|
setGeneratorState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
inputError: t('qrCode:generateError'),
|
||||||
|
}));
|
||||||
|
showMessage(t('qrCode:generateError'), { severity: 'error', autoHideDuration: 3000 });
|
||||||
|
} finally {
|
||||||
|
setGeneratorState((prev) => ({ ...prev, generating: false }));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[t, showMessage],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 使用 useRef 存储 generateQrCode 的最新引用,避免无限循环
|
||||||
|
const generateQrCodeRef = useRef(generateQrCode);
|
||||||
|
generateQrCodeRef.current = generateQrCode;
|
||||||
|
|
||||||
|
// 防抖处理输入文本(200ms)
|
||||||
|
const debouncedTextToEncode = useDebounce(generatorState.textToEncode, 200);
|
||||||
|
|
||||||
|
// 当防抖后的文本变化时,自动生成二维码
|
||||||
|
useEffect(() => {
|
||||||
|
if (debouncedTextToEncode && mode === 'generate') {
|
||||||
|
generateQrCodeRef.current(debouncedTextToEncode);
|
||||||
|
}
|
||||||
|
}, [debouncedTextToEncode, mode]);
|
||||||
|
|
||||||
|
// 设置输入文本
|
||||||
|
const setTextToEncode = useCallback((text: string) => {
|
||||||
|
setGeneratorState((prev) => ({ ...prev, textToEncode: text }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 处理右键菜单数据
|
||||||
|
const handleContextMenuData = useCallback((payload: string) => {
|
||||||
|
setMode('generate');
|
||||||
|
setGeneratorState((prev) => ({ ...prev, textToEncode: payload }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useContextMenuData({ featureKey: 'qrCode', onData: handleContextMenuData });
|
||||||
|
|
||||||
|
// 解析二维码
|
||||||
|
const parseQrCode = useCallback(
|
||||||
|
async (file: File) => {
|
||||||
|
try {
|
||||||
|
setParserState((prev) => ({ ...prev, parsing: true, parseError: '', decodedResult: '' }));
|
||||||
|
|
||||||
|
const result = await parseQrCodeFromFile(file);
|
||||||
|
|
||||||
|
if (result.success && result.data) {
|
||||||
|
setParserState((prev) => ({ ...prev, decodedResult: result.data! }));
|
||||||
|
showMessage(t('qrCode:parseSuccess'), { severity: 'success', autoHideDuration: 1000 });
|
||||||
|
} else {
|
||||||
|
const errorMsg = result.error || t('qrCode:noQrDetected');
|
||||||
|
setParserState((prev) => ({ ...prev, parseError: errorMsg }));
|
||||||
|
showMessage(errorMsg, { severity: 'error', autoHideDuration: 3000 });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析二维码失败:', error);
|
||||||
|
const errorMsg = error instanceof Error ? error.message : t('qrCode:parseError');
|
||||||
|
setParserState((prev) => ({ ...prev, parseError: errorMsg }));
|
||||||
|
showMessage(errorMsg, { severity: 'error', autoHideDuration: 3000 });
|
||||||
|
} finally {
|
||||||
|
setParserState((prev) => ({ ...prev, parsing: false }));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[t, showMessage],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 下载二维码
|
||||||
|
const downloadQrCode = useCallback(() => {
|
||||||
|
if (!generatorState.qrCodeDataUrl) return;
|
||||||
|
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = generatorState.qrCodeDataUrl;
|
||||||
|
link.download = 'qrcode.png';
|
||||||
|
link.click();
|
||||||
|
showMessage(t('qrCode:qrCodeDownloadSuccess'), { severity: 'success', autoHideDuration: 1000 });
|
||||||
|
}, [generatorState.qrCodeDataUrl, showMessage, t]);
|
||||||
|
|
||||||
|
// 复制二维码
|
||||||
|
const copyQrCode = useCallback(async () => {
|
||||||
|
if (!generatorState.qrCodeDataUrl) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(generatorState.qrCodeDataUrl);
|
||||||
|
const blob = await response.blob();
|
||||||
|
|
||||||
|
await navigator.clipboard.write([
|
||||||
|
new ClipboardItem({
|
||||||
|
'image/png': blob,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
showMessage(t('qrCode:qrCodeCopySuccess'), { severity: 'success', autoHideDuration: 1000 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('复制二维码失败:', error);
|
||||||
|
showMessage(t('qrCode:copyError'), { severity: 'error', autoHideDuration: 3000 });
|
||||||
|
}
|
||||||
|
}, [generatorState.qrCodeDataUrl, showMessage, t]);
|
||||||
|
|
||||||
|
// 处理文件选择
|
||||||
|
const handleFileChange = useCallback(
|
||||||
|
(file: File) => {
|
||||||
|
setParserState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
selectedFile: file,
|
||||||
|
previewUrl: URL.createObjectURL(file),
|
||||||
|
decodedResult: '',
|
||||||
|
parseError: '',
|
||||||
|
}));
|
||||||
|
// 自动解析
|
||||||
|
parseQrCode(file);
|
||||||
|
},
|
||||||
|
[parseQrCode],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 清除文件
|
||||||
|
const handleClearFile = useCallback(() => {
|
||||||
|
if (parserState.previewUrl) {
|
||||||
|
URL.revokeObjectURL(parserState.previewUrl);
|
||||||
|
}
|
||||||
|
setParserState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
selectedFile: null,
|
||||||
|
previewUrl: '',
|
||||||
|
decodedResult: '',
|
||||||
|
parseError: '',
|
||||||
|
}));
|
||||||
|
}, [parserState.previewUrl]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
mode,
|
||||||
|
setMode,
|
||||||
|
generatorState,
|
||||||
|
setTextToEncode,
|
||||||
|
generateQrCode,
|
||||||
|
downloadQrCode,
|
||||||
|
copyQrCode,
|
||||||
|
parserState,
|
||||||
|
setParserState,
|
||||||
|
parseQrCode,
|
||||||
|
handleFileChange,
|
||||||
|
handleClearFile,
|
||||||
|
};
|
||||||
|
}
|
||||||
+30
-318
@@ -1,242 +1,20 @@
|
|||||||
import { useState, useCallback, useEffect, useRef } from 'react';
|
import { Box, Container, useMediaQuery, useTheme } from '@mui/material';
|
||||||
import { Box, Container, Grid, useMediaQuery, useTheme } from '@mui/material';
|
|
||||||
import QrCodeIcon from '@mui/icons-material/QrCode';
|
import QrCodeIcon from '@mui/icons-material/QrCode';
|
||||||
import QRious from 'qrious';
|
|
||||||
import PageHeader from '@/components/PageHeader';
|
import PageHeader from '@/components/PageHeader';
|
||||||
import SwitchButtonGroup from '@/components/SwitchButtonGroup';
|
import SwitchButtonGroup from '@/components/SwitchButtonGroup';
|
||||||
import TextInputArea from '@/components/TextInputArea';
|
|
||||||
import QrCodePreview from '@/components/QrCodePreview';
|
|
||||||
import ImageUploader from '@/components/ImageUploader';
|
|
||||||
import { qrCodePageStyles } from '@/config/pageTheme';
|
import { qrCodePageStyles } from '@/config/pageTheme';
|
||||||
import { useSnackbar } from '@/components/GlobalSnackbar';
|
|
||||||
import { parseQrCodeFromFile } from '@/utils/qrCodeParser';
|
|
||||||
import { useContextMenuData } from '@/utils/useContextMenuData';
|
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
||||||
import { useDebounce } from '@/utils/useDebounce';
|
import { QrCodeContext } from './contexts/QrCodeContext';
|
||||||
import type { QrCodeMode, QrCodeGeneratorState, QrCodeParserState } from './types';
|
import { useQrCode } from './hooks/useQrCode';
|
||||||
|
import GeneratePanel from './components/GeneratePanel';
|
||||||
|
import ParsePanel from './components/ParsePanel';
|
||||||
|
import type { QrCodeMode } from './types';
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const { t } = useLazyTranslation('qrCode');
|
const { t } = useLazyTranslation('qrCode');
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { showMessage } = useSnackbar();
|
|
||||||
const isDesktop = useMediaQuery(theme.breakpoints.up('md'));
|
const isDesktop = useMediaQuery(theme.breakpoints.up('md'));
|
||||||
|
const qrCode = useQrCode();
|
||||||
// 当前模式
|
|
||||||
const [mode, setMode] = useState<QrCodeMode>('generate');
|
|
||||||
|
|
||||||
// 二维码生成器状态
|
|
||||||
const [generatorState, setGeneratorState] = useState<QrCodeGeneratorState>({
|
|
||||||
textToEncode: '',
|
|
||||||
qrCodeDataUrl: '',
|
|
||||||
generating: false,
|
|
||||||
inputError: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
// 二维码解析器状态
|
|
||||||
const [parserState, setParserState] = useState<QrCodeParserState>({
|
|
||||||
decodedResult: '',
|
|
||||||
parsing: false,
|
|
||||||
parseError: '',
|
|
||||||
selectedFile: null,
|
|
||||||
previewUrl: '',
|
|
||||||
dragging: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 生成二维码
|
|
||||||
const generateQrCode = useCallback(
|
|
||||||
async (text: string) => {
|
|
||||||
if (!text) {
|
|
||||||
setGeneratorState((prev) => ({ ...prev, qrCodeDataUrl: '' }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setGeneratorState((prev) => ({ ...prev, generating: true, inputError: '' }));
|
|
||||||
|
|
||||||
let url = text;
|
|
||||||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
|
||||||
url = 'https://' + url;
|
|
||||||
}
|
|
||||||
|
|
||||||
const qr = new QRious({
|
|
||||||
value: url,
|
|
||||||
size: 250,
|
|
||||||
level: 'H',
|
|
||||||
foreground: '#000000',
|
|
||||||
background: '#FFFFFF',
|
|
||||||
});
|
|
||||||
|
|
||||||
setGeneratorState((prev) => ({ ...prev, qrCodeDataUrl: qr.toDataURL() }));
|
|
||||||
} catch (error) {
|
|
||||||
console.error('生成二维码失败:', error);
|
|
||||||
setGeneratorState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
inputError: t('qrCode:generateError'),
|
|
||||||
}));
|
|
||||||
showMessage(t('qrCode:generateError'), { severity: 'error', autoHideDuration: 3000 });
|
|
||||||
} finally {
|
|
||||||
setGeneratorState((prev) => ({ ...prev, generating: false }));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[t, showMessage],
|
|
||||||
);
|
|
||||||
|
|
||||||
// 使用 useRef 存储 generateQrCode 的最新引用,避免无限循环
|
|
||||||
const generateQrCodeRef = useRef(generateQrCode);
|
|
||||||
generateQrCodeRef.current = generateQrCode;
|
|
||||||
|
|
||||||
// 防抖处理输入文本(200ms)
|
|
||||||
const debouncedTextToEncode = useDebounce(generatorState.textToEncode, 200);
|
|
||||||
|
|
||||||
// 当防抖后的文本变化时,自动生成二维码
|
|
||||||
useEffect(() => {
|
|
||||||
if (debouncedTextToEncode && mode === 'generate') {
|
|
||||||
generateQrCodeRef.current(debouncedTextToEncode);
|
|
||||||
}
|
|
||||||
}, [debouncedTextToEncode, mode]);
|
|
||||||
|
|
||||||
// 处理右键菜单数据
|
|
||||||
const handleContextMenuData = useCallback((payload: string) => {
|
|
||||||
setMode('generate');
|
|
||||||
setGeneratorState((prev) => ({ ...prev, textToEncode: payload }));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useContextMenuData({ featureKey: 'qrCode', onData: handleContextMenuData });
|
|
||||||
|
|
||||||
// 解析二维码
|
|
||||||
const parseQrCode = useCallback(
|
|
||||||
async (file: File) => {
|
|
||||||
try {
|
|
||||||
setParserState((prev) => ({ ...prev, parsing: true, parseError: '', decodedResult: '' }));
|
|
||||||
|
|
||||||
const result = await parseQrCodeFromFile(file);
|
|
||||||
|
|
||||||
if (result.success && result.data) {
|
|
||||||
setParserState((prev) => ({ ...prev, decodedResult: result.data! }));
|
|
||||||
showMessage(t('qrCode:parseSuccess'), { severity: 'success', autoHideDuration: 1000 });
|
|
||||||
} else {
|
|
||||||
const errorMsg = result.error || t('qrCode:noQrDetected');
|
|
||||||
setParserState((prev) => ({ ...prev, parseError: errorMsg }));
|
|
||||||
showMessage(errorMsg, { severity: 'error', autoHideDuration: 3000 });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('解析二维码失败:', error);
|
|
||||||
const errorMsg = error instanceof Error ? error.message : t('qrCode:parseError');
|
|
||||||
setParserState((prev) => ({ ...prev, parseError: errorMsg }));
|
|
||||||
showMessage(errorMsg, { severity: 'error', autoHideDuration: 3000 });
|
|
||||||
} finally {
|
|
||||||
setParserState((prev) => ({ ...prev, parsing: false }));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[t, showMessage],
|
|
||||||
);
|
|
||||||
|
|
||||||
// 下载二维码
|
|
||||||
const downloadQrCode = useCallback(() => {
|
|
||||||
if (!generatorState.qrCodeDataUrl) return;
|
|
||||||
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = generatorState.qrCodeDataUrl;
|
|
||||||
link.download = 'qrcode.png';
|
|
||||||
link.click();
|
|
||||||
showMessage(t('qrCode:qrCodeDownloadSuccess'), { severity: 'success', autoHideDuration: 1000 });
|
|
||||||
}, [generatorState.qrCodeDataUrl, showMessage, t]);
|
|
||||||
|
|
||||||
// 复制二维码
|
|
||||||
const copyQrCode = useCallback(async () => {
|
|
||||||
if (!generatorState.qrCodeDataUrl) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(generatorState.qrCodeDataUrl);
|
|
||||||
const blob = await response.blob();
|
|
||||||
|
|
||||||
await navigator.clipboard.write([
|
|
||||||
new ClipboardItem({
|
|
||||||
'image/png': blob,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
showMessage(t('qrCode:qrCodeCopySuccess'), { severity: 'success', autoHideDuration: 1000 });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('复制二维码失败:', error);
|
|
||||||
showMessage(t('qrCode:copyError'), { severity: 'error', autoHideDuration: 3000 });
|
|
||||||
}
|
|
||||||
}, [generatorState.qrCodeDataUrl, showMessage, t]);
|
|
||||||
|
|
||||||
// 处理文件选择
|
|
||||||
const handleFileChange = useCallback(
|
|
||||||
(file: File) => {
|
|
||||||
setParserState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
selectedFile: file,
|
|
||||||
previewUrl: URL.createObjectURL(file),
|
|
||||||
decodedResult: '',
|
|
||||||
parseError: '',
|
|
||||||
}));
|
|
||||||
// 自动解析
|
|
||||||
parseQrCode(file);
|
|
||||||
},
|
|
||||||
[parseQrCode],
|
|
||||||
);
|
|
||||||
|
|
||||||
// 清除文件
|
|
||||||
const handleClearFile = useCallback(() => {
|
|
||||||
if (parserState.previewUrl) {
|
|
||||||
URL.revokeObjectURL(parserState.previewUrl);
|
|
||||||
}
|
|
||||||
setParserState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
selectedFile: null,
|
|
||||||
previewUrl: '',
|
|
||||||
decodedResult: '',
|
|
||||||
parseError: '',
|
|
||||||
}));
|
|
||||||
}, [parserState.previewUrl]);
|
|
||||||
|
|
||||||
// 全局粘贴事件监听(解析模式下)
|
|
||||||
useEffect(() => {
|
|
||||||
if (mode !== 'parse') return;
|
|
||||||
|
|
||||||
const handlePaste = async (e: ClipboardEvent) => {
|
|
||||||
const items = e.clipboardData?.items;
|
|
||||||
if (!items) return;
|
|
||||||
|
|
||||||
// 检查是否有图片
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
|
||||||
if (items[i].type.startsWith('image/')) {
|
|
||||||
e.preventDefault();
|
|
||||||
const file = items[i].getAsFile();
|
|
||||||
if (file) {
|
|
||||||
handleFileChange(file);
|
|
||||||
showMessage(t('qrCode:imagePasted'), { severity: 'success', autoHideDuration: 1000 });
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否有 Base64 字符串
|
|
||||||
const text = e.clipboardData?.getData('text/plain');
|
|
||||||
if (text && text.startsWith('data:image/')) {
|
|
||||||
e.preventDefault();
|
|
||||||
try {
|
|
||||||
// 将 Base64 转换为 File
|
|
||||||
const response = await fetch(text);
|
|
||||||
const blob = await response.blob();
|
|
||||||
const file = new File([blob], 'pasted-image.png', { type: blob.type });
|
|
||||||
handleFileChange(file);
|
|
||||||
showMessage(t('qrCode:imagePasted'), { severity: 'success', autoHideDuration: 1000 });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('处理 Base64 图片失败:', error);
|
|
||||||
showMessage(t('qrCode:imagePasteError'), { severity: 'error', autoHideDuration: 3000 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('paste', handlePaste);
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('paste', handlePaste);
|
|
||||||
};
|
|
||||||
}, [mode, handleFileChange, showMessage, t]);
|
|
||||||
|
|
||||||
// 模式选项
|
// 模式选项
|
||||||
const modeOptions = [
|
const modeOptions = [
|
||||||
@@ -244,97 +22,31 @@ export default function Index() {
|
|||||||
{ value: 'parse' as QrCodeMode, label: t('qrCode:qrToUrl') },
|
{ value: 'parse' as QrCodeMode, label: t('qrCode:qrToUrl') },
|
||||||
];
|
];
|
||||||
|
|
||||||
// 渲染左侧面板
|
|
||||||
const renderLeftPanel = () => {
|
|
||||||
if (mode === 'generate') {
|
|
||||||
return (
|
|
||||||
<TextInputArea
|
|
||||||
title={t('qrCode:urlInputLabel')}
|
|
||||||
value={generatorState.textToEncode}
|
|
||||||
onChange={(text) => setGeneratorState((prev) => ({ ...prev, textToEncode: text }))}
|
|
||||||
placeholder={t('qrCode:urlInputPlaceholder')}
|
|
||||||
showCount
|
|
||||||
showClear
|
|
||||||
allowCopy
|
|
||||||
externalError={generatorState.inputError}
|
|
||||||
showMessage={showMessage}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ImageUploader
|
|
||||||
selectedFile={parserState.selectedFile}
|
|
||||||
onFileChange={handleFileChange}
|
|
||||||
onClearFile={handleClearFile}
|
|
||||||
previewUrl={parserState.previewUrl}
|
|
||||||
onPreviewUrlChange={(url) => setParserState((prev) => ({ ...prev, previewUrl: url }))}
|
|
||||||
dragging={parserState.dragging}
|
|
||||||
onDraggingChange={(dragging) => setParserState((prev) => ({ ...prev, dragging }))}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 渲染右侧面板
|
|
||||||
const renderRightPanel = () => {
|
|
||||||
if (mode === 'generate') {
|
|
||||||
return (
|
|
||||||
<QrCodePreview
|
|
||||||
qrCodeDataUrl={generatorState.qrCodeDataUrl}
|
|
||||||
onDownload={downloadQrCode}
|
|
||||||
onCopy={copyQrCode}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TextInputArea
|
|
||||||
title={t('qrCode:resultLabel')}
|
|
||||||
value={parserState.decodedResult}
|
|
||||||
readOnly
|
|
||||||
showClear={false}
|
|
||||||
allowCopy
|
|
||||||
externalError={parserState.parseError}
|
|
||||||
actions={[
|
|
||||||
{
|
|
||||||
key: 'parse',
|
|
||||||
label: parserState.parsing ? t('qrCode:parsing') : t('qrCode:parseButton'),
|
|
||||||
type: 'primary',
|
|
||||||
position: 'bottom',
|
|
||||||
disabled: !parserState.selectedFile || parserState.parsing,
|
|
||||||
onClick: () => {
|
|
||||||
if (parserState.selectedFile) {
|
|
||||||
parseQrCode(parserState.selectedFile);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
showMessage={showMessage}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<QrCodeContext.Provider value={qrCode}>
|
||||||
<Container
|
<Box>
|
||||||
maxWidth={isDesktop ? 'lg' : false}
|
<Container
|
||||||
sx={{ py: 2, maxWidth: isDesktop ? undefined : 400 }}
|
maxWidth={isDesktop ? 'lg' : false}
|
||||||
>
|
sx={{ py: 2, maxWidth: isDesktop ? undefined : 400 }}
|
||||||
<PageHeader
|
>
|
||||||
title={t('qrCode:pageTitle')}
|
<PageHeader
|
||||||
subtitle={t('qrCode:pageSubtitle')}
|
title={t('qrCode:pageTitle')}
|
||||||
icon={<QrCodeIcon />}
|
subtitle={t('qrCode:pageSubtitle')}
|
||||||
iconColor={qrCodePageStyles.primaryColor}
|
icon={<QrCodeIcon />}
|
||||||
sx={{ mb: 2.5 }}
|
iconColor={qrCodePageStyles.primaryColor}
|
||||||
/>
|
sx={{ mb: 2.5 }}
|
||||||
|
/>
|
||||||
|
|
||||||
<SwitchButtonGroup value={mode} options={modeOptions} onChange={setMode} sx={{ mb: 3 }} />
|
<SwitchButtonGroup
|
||||||
|
value={qrCode.mode}
|
||||||
|
options={modeOptions}
|
||||||
|
onChange={qrCode.setMode}
|
||||||
|
sx={{ mb: 3 }}
|
||||||
|
/>
|
||||||
|
|
||||||
<Grid container spacing={3}>
|
{qrCode.mode === 'generate' ? <GeneratePanel /> : <ParsePanel />}
|
||||||
<Grid size={{ xs: 12, md: 6 }}>{renderLeftPanel()}</Grid>
|
</Container>
|
||||||
<Grid size={{ xs: 12, md: 6 }}>{renderRightPanel()}</Grid>
|
</Box>
|
||||||
</Grid>
|
</QrCodeContext.Provider>
|
||||||
</Container>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,93 +32,3 @@ export interface QrCodeParserState {
|
|||||||
/** 是否正在拖拽 */
|
/** 是否正在拖拽 */
|
||||||
dragging: boolean;
|
dragging: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 二维码生成器组件的 Props */
|
|
||||||
export interface QrCodeGeneratorProps {
|
|
||||||
/** 输入文本 */
|
|
||||||
textToEncode: string;
|
|
||||||
/** 输入文本变更回调 */
|
|
||||||
onTextChange: (text: string) => void;
|
|
||||||
/** 生成的二维码 Data URL */
|
|
||||||
qrCodeDataUrl: string;
|
|
||||||
/** 二维码 Data URL 变更回调 */
|
|
||||||
onQrCodeDataUrlChange: (dataUrl: string) => void;
|
|
||||||
/** 是否正在生成 */
|
|
||||||
generating: boolean;
|
|
||||||
/** 生成状态变更回调 */
|
|
||||||
onGeneratingChange: (generating: boolean) => void;
|
|
||||||
/** 输入错误信息 */
|
|
||||||
inputError: string;
|
|
||||||
/** 输入错误变更回调 */
|
|
||||||
onInputErrorChange: (error: string) => void;
|
|
||||||
/** 展开状态 */
|
|
||||||
expanded: boolean;
|
|
||||||
/** 展开状态变更回调 */
|
|
||||||
onExpandedChange: (expanded: boolean) => void;
|
|
||||||
/** 是否强制展开(桌面端) */
|
|
||||||
forceExpanded?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 二维码预览组件的 Props */
|
|
||||||
export interface QrCodePreviewProps {
|
|
||||||
/** 二维码 Data URL */
|
|
||||||
qrCodeDataUrl: string;
|
|
||||||
/** 下载回调 */
|
|
||||||
onDownload: () => void;
|
|
||||||
/** 复制回调 */
|
|
||||||
onCopy: () => void;
|
|
||||||
/** 是否正在生成 */
|
|
||||||
generating?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 图片上传器组件的 Props */
|
|
||||||
export interface ImageUploaderProps {
|
|
||||||
/** 选中的文件 */
|
|
||||||
selectedFile: File | null;
|
|
||||||
/** 文件变更回调 */
|
|
||||||
onFileChange: (file: File) => void;
|
|
||||||
/** 清除文件回调 */
|
|
||||||
onClearFile: () => void;
|
|
||||||
/** 文件预览 URL */
|
|
||||||
previewUrl: string;
|
|
||||||
/** 是否正在拖拽 */
|
|
||||||
dragging: boolean;
|
|
||||||
/** 拖拽状态变更回调 */
|
|
||||||
onDraggingChange: (dragging: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 二维码解析器组件的 Props */
|
|
||||||
export interface QrCodeParserProps {
|
|
||||||
/** 解析结果文本 */
|
|
||||||
decodedResult: string;
|
|
||||||
/** 解析结果变更回调 */
|
|
||||||
onDecodedResultChange: (result: string) => void;
|
|
||||||
/** 是否正在解析 */
|
|
||||||
parsing: boolean;
|
|
||||||
/** 解析状态变更回调 */
|
|
||||||
onParsingChange: (parsing: boolean) => void;
|
|
||||||
/** 解析错误信息 */
|
|
||||||
parseError: string;
|
|
||||||
/** 解析错误变更回调 */
|
|
||||||
onParseErrorChange: (error: string) => void;
|
|
||||||
/** 选中的文件 */
|
|
||||||
selectedFile: File | null;
|
|
||||||
/** 文件变更回调 */
|
|
||||||
onFileChange: (file: File) => void;
|
|
||||||
/** 清除文件回调 */
|
|
||||||
onClearFile: () => void;
|
|
||||||
/** 文件预览 URL */
|
|
||||||
previewUrl: string;
|
|
||||||
/** 预览 URL 变更回调 */
|
|
||||||
onPreviewUrlChange: (url: string) => void;
|
|
||||||
/** 是否正在拖拽 */
|
|
||||||
dragging: boolean;
|
|
||||||
/** 拖拽状态变更回调 */
|
|
||||||
onDraggingChange: (dragging: boolean) => void;
|
|
||||||
/** 展开状态 */
|
|
||||||
expanded: boolean;
|
|
||||||
/** 展开状态变更回调 */
|
|
||||||
onExpandedChange: (expanded: boolean) => void;
|
|
||||||
/** 是否强制展开(桌面端) */
|
|
||||||
forceExpanded?: boolean;
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user