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')}
-
-
-
-
-
-
-
- : }
- onClick={parseQrCode}
- disabled={parsing}
- sx={qrCodePageStyles.PRIMARY_BUTTON}
- >
- {parsing ? t('qrCode:parsing') : t('qrCode:parseButton')}
-
-
-
-
-
-
- ),
- },
- }}
- 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')}
-
-
-
-
-
-
-
- : }
- onClick={generateQrCode}
- disabled={generating}
- sx={qrCodePageStyles.PRIMARY_BUTTON}
- >
- {generating ? t('qrCode:generating') : t('qrCode:generateButton')}
-
-
-
-
-
-
-
-
- );
-};
-
-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;
-}