diff --git a/pages/QrCode/QrCodeToUrlSection.tsx b/pages/QrCode/QrCodeToUrlSection.tsx deleted file mode 100644 index e17a2fc..0000000 --- a/pages/QrCode/QrCodeToUrlSection.tsx +++ /dev/null @@ -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 ( - onExpandedChange(isExpanded)} - sx={forceExpanded ? qrCodePageStyles.ACCORDION_DESKTOP : qrCodePageStyles.ACCORDION} - > - } sx={qrCodePageStyles.ACCORDION_SUMMARY}> - - - - {t('qrCode:qrToUrl')} - - - - - - - - - - - - - - ), - }, - }} - sx={qrCodePageStyles.INPUT_STYLE} - /> - - - {parseError && ( - - {parseError} - - )} - - - - ); -}; - -export default QrCodeToUrlSection; diff --git a/pages/QrCode/UrlToQrCodeSection.tsx b/pages/QrCode/UrlToQrCodeSection.tsx deleted file mode 100644 index f73293a..0000000 --- a/pages/QrCode/UrlToQrCodeSection.tsx +++ /dev/null @@ -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) => { - 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 ( - onExpandedChange(isExpanded)} - sx={forceExpanded ? qrCodePageStyles.ACCORDION_DESKTOP : qrCodePageStyles.ACCORDION} - > - } sx={qrCodePageStyles.ACCORDION_SUMMARY}> - - - - {t('qrCode:urlToQr')} - - - - - - - - - - - - - - - - ); -}; - -export default UrlToQrCodeSection; diff --git a/pages/QrCode/components/GeneratePanel.tsx b/pages/QrCode/components/GeneratePanel.tsx new file mode 100644 index 0000000..d48835d --- /dev/null +++ b/pages/QrCode/components/GeneratePanel.tsx @@ -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 ( + + + + + + + + + ); +} diff --git a/pages/QrCode/components/ParsePanel.tsx b/pages/QrCode/components/ParsePanel.tsx new file mode 100644 index 0000000..72a0a3a --- /dev/null +++ b/pages/QrCode/components/ParsePanel.tsx @@ -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 ( + + + setParserState((prev) => ({ ...prev, previewUrl: url }))} + dragging={parserState.dragging} + onDraggingChange={(dragging) => setParserState((prev) => ({ ...prev, dragging }))} + /> + + + { + if (parserState.selectedFile) { + parseQrCode(parserState.selectedFile); + } + }, + }, + ]} + showMessage={showMessage} + /> + + + ); +} diff --git a/pages/QrCode/contexts/QrCodeContext.ts b/pages/QrCode/contexts/QrCodeContext.ts new file mode 100644 index 0000000..e89d1be --- /dev/null +++ b/pages/QrCode/contexts/QrCodeContext.ts @@ -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; + downloadQrCode: () => void; + copyQrCode: () => Promise; + + // 解析器状态 + parserState: QrCodeParserState; + setParserState: React.Dispatch>; + parseQrCode: (file: File) => Promise; + handleFileChange: (file: File) => void; + handleClearFile: () => void; +} + +export const QrCodeContext = createContext(null); + +export function useQrCodeContext() { + const context = useContext(QrCodeContext); + if (!context) { + throw new Error('useQrCodeContext must be used within QrCodeProvider'); + } + return context; +} diff --git a/pages/QrCode/hooks/useQrCode.ts b/pages/QrCode/hooks/useQrCode.ts new file mode 100644 index 0000000..a18f146 --- /dev/null +++ b/pages/QrCode/hooks/useQrCode.ts @@ -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('generate'); + + // 二维码生成器状态 + const [generatorState, setGeneratorState] = useState({ + textToEncode: '', + qrCodeDataUrl: '', + generating: false, + inputError: '', + }); + + // 二维码解析器状态 + const [parserState, setParserState] = useState({ + 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, + }; +} diff --git a/pages/QrCode/index.tsx b/pages/QrCode/index.tsx index 4f055e8..be9262e 100644 --- a/pages/QrCode/index.tsx +++ b/pages/QrCode/index.tsx @@ -1,242 +1,20 @@ -import { useState, useCallback, useEffect, useRef } from 'react'; -import { Box, Container, Grid, useMediaQuery, useTheme } from '@mui/material'; +import { Box, Container, useMediaQuery, useTheme } from '@mui/material'; import QrCodeIcon from '@mui/icons-material/QrCode'; -import QRious from 'qrious'; import PageHeader from '@/components/PageHeader'; 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 { 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 { QrCodeMode, QrCodeGeneratorState, QrCodeParserState } from './types'; +import { QrCodeContext } from './contexts/QrCodeContext'; +import { useQrCode } from './hooks/useQrCode'; +import GeneratePanel from './components/GeneratePanel'; +import ParsePanel from './components/ParsePanel'; +import type { QrCodeMode } from './types'; export default function Index() { const { t } = useLazyTranslation('qrCode'); const theme = useTheme(); - const { showMessage } = useSnackbar(); const isDesktop = useMediaQuery(theme.breakpoints.up('md')); - - // 当前模式 - const [mode, setMode] = useState('generate'); - - // 二维码生成器状态 - const [generatorState, setGeneratorState] = useState({ - textToEncode: '', - qrCodeDataUrl: '', - generating: false, - inputError: '', - }); - - // 二维码解析器状态 - const [parserState, setParserState] = useState({ - 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 qrCode = useQrCode(); // 模式选项 const modeOptions = [ @@ -244,97 +22,31 @@ export default function Index() { { value: 'parse' as QrCodeMode, label: t('qrCode:qrToUrl') }, ]; - // 渲染左侧面板 - const renderLeftPanel = () => { - if (mode === 'generate') { - return ( - setGeneratorState((prev) => ({ ...prev, textToEncode: text }))} - placeholder={t('qrCode:urlInputPlaceholder')} - showCount - showClear - allowCopy - externalError={generatorState.inputError} - showMessage={showMessage} - /> - ); - } - - return ( - setParserState((prev) => ({ ...prev, previewUrl: url }))} - dragging={parserState.dragging} - onDraggingChange={(dragging) => setParserState((prev) => ({ ...prev, dragging }))} - /> - ); - }; - - // 渲染右侧面板 - const renderRightPanel = () => { - if (mode === 'generate') { - return ( - - ); - } - - return ( - { - if (parserState.selectedFile) { - parseQrCode(parserState.selectedFile); - } - }, - }, - ]} - showMessage={showMessage} - /> - ); - }; - return ( - - - } - iconColor={qrCodePageStyles.primaryColor} - sx={{ mb: 2.5 }} - /> + + + + } + iconColor={qrCodePageStyles.primaryColor} + sx={{ mb: 2.5 }} + /> - + - - {renderLeftPanel()} - {renderRightPanel()} - - - + {qrCode.mode === 'generate' ? : } + + + ); } diff --git a/pages/QrCode/types.ts b/pages/QrCode/types.ts index fdcf842..24572db 100644 --- a/pages/QrCode/types.ts +++ b/pages/QrCode/types.ts @@ -32,93 +32,3 @@ export interface QrCodeParserState { /** 是否正在拖拽 */ 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; -}