diff --git a/pages/QrCode/index.tsx b/pages/QrCode/index.tsx index a03c75e..98e93fe 100644 --- a/pages/QrCode/index.tsx +++ b/pages/QrCode/index.tsx @@ -1,22 +1,27 @@ import { useState, useCallback } from 'react'; -import { Box, CircularProgress, Container, Stack, useMediaQuery, useTheme } from '@mui/material'; +import { Box, Container, Grid, useMediaQuery, useTheme } from '@mui/material'; import QrCodeIcon from '@mui/icons-material/QrCode'; -import UrlToQrCodeSection from '@/pages/QrCode/UrlToQrCodeSection'; -import QrCodeToUrlSection from '@/pages/QrCode/QrCodeToUrlSection'; -import { useStorageState } from '@/utils/useStorageState'; -import { qrCodePageStyles } from '@/config/pageTheme'; +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 type { QrCodeGeneratorState, QrCodeParserState } from './types'; +import type { QrCodeMode, QrCodeGeneratorState, QrCodeParserState } from './types'; export default function Index() { const { t } = useLazyTranslation('qrCode'); const theme = useTheme(); + const { showMessage } = useSnackbar(); const isDesktop = useMediaQuery(theme.breakpoints.up('md')); - // 使用自定义钩子管理展开状态(移动端使用) - const [urlExpanded, setUrlExpanded, urlInitialized] = useStorageState('qrCode/urlExpanded', true); - const [qrExpanded, setQrExpanded, qrInitialized] = useStorageState('qrCode/qrExpanded', false); + // 当前模式 + const [mode, setMode] = useState('generate'); // 二维码生成器状态 const [generatorState, setGeneratorState] = useState({ @@ -36,49 +41,115 @@ export default function Index() { dragging: false, }); - // 生成器状态更新函数 - const setTextToEncode = useCallback((text: string) => { - setGeneratorState((prev) => ({ ...prev, textToEncode: text })); - }, []); + // 生成二维码 + const generateQrCode = useCallback( + async (text: string) => { + if (!text) return; - const setQrCodeDataUrl = useCallback((dataUrl: string) => { - setGeneratorState((prev) => ({ ...prev, qrCodeDataUrl: dataUrl })); - }, []); + try { + setGeneratorState((prev) => ({ ...prev, generating: true, inputError: '' })); - const setGenerating = useCallback((generating: boolean) => { - setGeneratorState((prev) => ({ ...prev, generating })); - }, []); + let url = text; + if (!url.startsWith('http://') && !url.startsWith('https://')) { + url = 'https://' + url; + } - const setInputError = useCallback((error: string) => { - setGeneratorState((prev) => ({ ...prev, inputError: error })); - }, []); + const qr = new QRious({ + value: url, + size: 250, + level: 'H', + foreground: '#000000', + background: '#FFFFFF', + }); - // 解析器状态更新函数 - const setDecodedResult = useCallback((result: string) => { - setParserState((prev) => ({ ...prev, decodedResult: result })); - }, []); + setGeneratorState((prev) => ({ ...prev, qrCodeDataUrl: qr.toDataURL() })); + showMessage(t('qrCode:qrCodeSuccess'), { severity: 'success', autoHideDuration: 1000 }); + } catch (error) { + console.error('生成二维码失败:', error); + showMessage(t('qrCode:parseError'), { severity: 'error', autoHideDuration: 300 }); + } finally { + setGeneratorState((prev) => ({ ...prev, generating: false })); + } + }, + [t, showMessage], + ); - const setParsing = useCallback((parsing: boolean) => { - setParserState((prev) => ({ ...prev, parsing })); - }, []); + // 处理右键菜单数据 + const handleContextMenuData = useCallback( + (payload: string) => { + setMode('generate'); + setGeneratorState((prev) => ({ ...prev, textToEncode: payload })); + generateQrCode(payload); + }, + [generateQrCode], + ); - const setParseError = useCallback((error: string) => { - setParserState((prev) => ({ ...prev, parseError: error })); - }, []); + useContextMenuData({ featureKey: 'qrCode', onData: handleContextMenuData }); - const setSelectedFile = useCallback((file: File | null) => { - setParserState((prev) => ({ ...prev, selectedFile: file })); - }, []); + // 解析二维码 + const parseQrCode = useCallback( + async (file: File) => { + try { + setParserState((prev) => ({ ...prev, parsing: true, parseError: '', decodedResult: '' })); - const setPreviewUrl = useCallback((url: string) => { - setParserState((prev) => ({ ...prev, previewUrl: url })); - }, []); + const result = await parseQrCodeFromFile(file); - const setDragging = useCallback((dragging: boolean) => { - setParserState((prev) => ({ ...prev, dragging })); - }, []); + 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: 1000 }); + } + } catch (error) { + console.error('解析二维码失败:', error); + setParserState((prev) => ({ ...prev, parseError: t('qrCode:parseError') })); + showMessage(t('qrCode:parseError'), { severity: 'error', autoHideDuration: 300 }); + } 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: 300 }); + }, [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:parseError'), { severity: 'error', autoHideDuration: 300 }); + } + }, [generatorState.qrCodeDataUrl, showMessage, t]); + + // 清除文件 const handleClearFile = useCallback(() => { + if (parserState.previewUrl) { + URL.revokeObjectURL(parserState.previewUrl); + } setParserState((prev) => ({ ...prev, selectedFile: null, @@ -86,88 +157,103 @@ export default function Index() { decodedResult: '', parseError: '', })); - }, []); + }, [parserState.previewUrl]); + + // 模式选项 + const modeOptions = [ + { value: 'generate' as QrCodeMode, label: t('qrCode:urlToQr') }, + { 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} + actions={[ + { + key: 'generate', + label: generatorState.generating + ? t('qrCode:generating') + : t('qrCode:generateButton'), + type: 'primary', + position: 'bottom', + disabled: (value) => !value || generatorState.generating, + onClick: (value) => generateQrCode(value), + }, + ]} + showMessage={showMessage} + /> + ); + } - // 初始化未完成时显示加载状态 - if (!urlInitialized || !qrInitialized) { return ( - - - - ); - } - - const sections = isDesktop ? ( - <> - - - - - - - - ) : ( - <> - - { + setParserState((prev) => ({ + ...prev, + selectedFile: file, + previewUrl: URL.createObjectURL(file), + decodedResult: '', + parseError: '', + })); + }} onClearFile={handleClearFile} previewUrl={parserState.previewUrl} - onPreviewUrlChange={setPreviewUrl} + onPreviewUrlChange={(url) => setParserState((prev) => ({ ...prev, previewUrl: url }))} dragging={parserState.dragging} - onDraggingChange={setDragging} - expanded={qrExpanded} - onExpandedChange={setQrExpanded} + onDraggingChange={(dragging) => setParserState((prev) => ({ ...prev, dragging }))} /> - - ); + ); + }; + + // 渲染右侧面板 + const renderRightPanel = () => { + if (mode === 'generate') { + return ( + + ); + } + + return ( + { + if (parserState.selectedFile) { + parseQrCode(parserState.selectedFile); + } + }, + }, + ]} + showMessage={showMessage} + /> + ); + }; return ( @@ -183,11 +269,12 @@ export default function Index() { sx={{ mb: 2.5 }} /> - {isDesktop ? ( - {sections} - ) : ( - {sections} - )} + + + + {renderLeftPanel()} + {renderRightPanel()} + );