import { useCallback, useRef, useState } from 'react'; import { Alert, alpha, Box, Button, Container, Stack, TextField, ToggleButton, ToggleButtonGroup, Typography, CircularProgress, Paper, } from '@mui/material'; import TextFieldsIcon from '@mui/icons-material/TextFields'; import UploadFileIcon from '@mui/icons-material/UploadFile'; import ImageIcon from '@mui/icons-material/Image'; import SwapHorizIcon from '@mui/icons-material/SwapHoriz'; import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; import { useTranslation } from 'react-i18next'; import PageHeader from '@/components/PageHeader'; import CopyButton from '@/components/CopyButton'; import { base64ConverterPageStyles } from '@/config/pageTheme'; import { useStorageState } from '@/utils/useStorageState'; import type { Base64ConverterPageMode } from '@/types/storage'; import { textToBase64, base64ToText, fileToBase64, isFileSizeValid, isSupportedImageType, isSupportedImageExtension, formatFileSize, MAX_FILE_SIZE, } from '@/utils/base64Converter'; import type { FileToBase64Result } from '@/utils/base64Converter'; const VALID_PAGE_MODES: readonly Base64ConverterPageMode[] = ['text', 'file', 'image']; const isValidPageMode = (val: unknown): val is Base64ConverterPageMode => typeof val === 'string' && (VALID_PAGE_MODES as readonly string[]).includes(val); type PageMode = Base64ConverterPageMode; /** 文件信息 */ interface FileInfo { name: string; size: number; type: string; } export default function Index() { const { t } = useTranslation(['base64Converter']); const [pageMode, setPageMode] = useStorageState( 'base64Converter/pageMode', 'text', isValidPageMode, ); // 文本模式状态 const [textInput, setTextInput] = useState(''); const [textOutput, setTextOutput] = useState(''); const [textError, setTextError] = useState(null); const [textDirection, setTextDirection] = useState<'encode' | 'decode'>('encode'); // 文件/图像模式状态 const [fileResult, setFileResult] = useState(null); const [fileInfo, setFileInfo] = useState(null); const [fileError, setFileError] = useState(null); const [isLoading, setIsLoading] = useState(false); const [isDragging, setIsDragging] = useState(false); const fileInputRef = useRef(null); const imageInputRef = useRef(null); /** 清空文本模式 */ const handleClearText = useCallback(() => { setTextInput(''); setTextOutput(''); setTextError(null); }, []); /** 文本编码/解码 */ const handleTextConvert = useCallback(() => { setTextError(null); try { if (textDirection === 'encode') { const result = textToBase64(textInput); setTextOutput(result.output); } else { const decoded = base64ToText(textInput); setTextOutput(decoded); } } catch (e) { setTextError(e instanceof Error ? e.message : t('base64Converter:conversionFailed')); } }, [textInput, textDirection, t]); /** 处理文件选择 */ const handleFileSelect = useCallback( async (file: File, isImageMode: boolean) => { setFileError(null); setFileResult(null); setFileInfo(null); if (!isFileSizeValid(file.size)) { setFileError( t('base64Converter:fileSizeExceeded', { max: `${MAX_FILE_SIZE / 1024 / 1024} MB` }), ); return; } if ( isImageMode && !isSupportedImageType(file.type) && !isSupportedImageExtension(file.name) ) { setFileError(t('base64Converter:unsupportedImageType')); return; } setFileInfo({ name: file.name, size: file.size, type: file.type || 'application/octet-stream', }); setIsLoading(true); try { const result = await fileToBase64(file); setFileResult(result); } catch (e) { setFileError(e instanceof Error ? e.message : t('base64Converter:conversionFailed')); } finally { setIsLoading(false); } }, [t], ); /** 清空文件/图像模式 */ const handleClearFile = useCallback(() => { setFileResult(null); setFileInfo(null); setFileError(null); if (fileInputRef.current) fileInputRef.current.value = ''; if (imageInputRef.current) imageInputRef.current.value = ''; }, []); /** 拖拽处理 */ const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(true); }, []); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); }, []); const handleDrop = useCallback( (e: React.DragEvent, isImageMode: boolean) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); const file = e.dataTransfer.files[0]; if (file) { handleFileSelect(file, isImageMode); } }, [handleFileSelect], ); /** 页面模式元数据 */ const modeTitles: Record = { text: { title: 'base64Converter:pageTitle', subtitle: 'base64Converter:pageSubtitle' }, file: { title: 'base64Converter:pageTitle', subtitle: 'base64Converter:pageSubtitle' }, image: { title: 'base64Converter:pageTitle', subtitle: 'base64Converter:pageSubtitle' }, }; const modeIcon: Record = { text: , file: , image: , }; return ( {/* 模式切换器 */} v && setPageMode(v)} sx={{ borderRadius: 3, flexWrap: 'wrap', gap: 0.5 }} > {t('base64Converter:textMode')} {t('base64Converter:fileMode')} {t('base64Converter:imageMode')} {/* ===== 文本模式 ===== */} {pageMode === 'text' && ( <> {/* 编码/解码切换 */} v && setTextDirection(v)} sx={{ borderRadius: 3 }} > {t('base64Converter:encode')} {t('base64Converter:decode')} {/* 输入区 */} { setTextInput(e.target.value); setTextError(null); }} sx={{ '& .MuiOutlinedInput-root': { bgcolor: 'background.paper', borderRadius: 3, fontSize: '0.85rem', fontFamily: 'monospace', transition: 'all 0.2s', '&:hover': { bgcolor: 'action.hover' }, '&.Mui-focused': { bgcolor: 'background.paper', boxShadow: (theme) => `0 0 0 4px ${alpha(theme.palette.info.main, 0.1)}`, }, }, }} /> {textError && {textError}} {/* 输出区 */} {textOutput && ( alpha(theme.palette.info.main, 0.04), border: '1px solid', borderColor: (theme) => alpha(theme.palette.info.main, 0.15), }} > {textDirection === 'encode' ? t('base64Converter:base64Output') : t('base64Converter:textOutput')} {}} /> {textOutput} )} )} {/* ===== 文件模式 ===== */} {pageMode === 'file' && ( <> {/* 拖拽/上传区 */} handleDrop(e, false)} onClick={() => fileInputRef.current?.click()} sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', minHeight: 180, border: '2px dashed', borderColor: isDragging ? 'info.main' : fileInfo ? 'info.main' : 'divider', borderRadius: 3, p: 4, bgcolor: (theme) => isDragging ? alpha(theme.palette.info.main, 0.08) : fileInfo ? alpha(theme.palette.info.main, 0.04) : 'action.hover', cursor: 'pointer', transition: 'all 0.2s', '&:hover': { borderColor: 'info.main', bgcolor: (theme) => alpha(theme.palette.info.main, 0.04), }, }} > { const file = e.target.files?.[0]; if (file) handleFileSelect(file, false); }} /> {isLoading ? ( ) : fileInfo ? ( {fileInfo.name} {formatFileSize(fileInfo.size)} · {fileInfo.type} {t('base64Converter:clickOrDropToReplace')} ) : ( {t('base64Converter:clickOrDropToFile')} {t('base64Converter:maxFileSize', { max: `${MAX_FILE_SIZE / 1024 / 1024} MB`, })} )} {fileError && {fileError}} {/* 文件转换结果 */} {fileResult && ( alpha(theme.palette.info.main, 0.04), border: '1px solid', borderColor: (theme) => alpha(theme.palette.info.main, 0.15), }} > {t('base64Converter:base64Output')} {}} /> {}} /> {fileResult.output.length > 2000 ? `${fileResult.output.substring(0, 2000)}...` : fileResult.output} {t('base64Converter:originalSize')}:{' '} {formatFileSize(fileResult.originalBytes)} {t('base64Converter:encodedSize')}: {formatFileSize(fileResult.outputBytes)} )} {fileInfo && ( )} )} {/* ===== 图像模式 ===== */} {pageMode === 'image' && ( <> {/* 拖拽/上传区 */} handleDrop(e, true)} onClick={() => imageInputRef.current?.click()} sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', minHeight: 180, border: '2px dashed', borderColor: isDragging ? 'info.main' : fileInfo ? 'info.main' : 'divider', borderRadius: 3, p: 4, bgcolor: (theme) => isDragging ? alpha(theme.palette.info.main, 0.08) : fileInfo ? alpha(theme.palette.info.main, 0.04) : 'action.hover', cursor: 'pointer', transition: 'all 0.2s', '&:hover': { borderColor: 'info.main', bgcolor: (theme) => alpha(theme.palette.info.main, 0.04), }, }} > { const file = e.target.files?.[0]; if (file) handleFileSelect(file, true); }} /> {isLoading ? ( ) : fileInfo ? ( {fileResult && ( )} {fileInfo.name} {formatFileSize(fileInfo.size)} · {fileInfo.type} {t('base64Converter:clickOrDropToReplace')} ) : ( {t('base64Converter:clickOrDropToImage')} {t('base64Converter:supportedFormats')} )} {fileError && {fileError}} {/* 图像转换结果 */} {fileResult && ( alpha(theme.palette.info.main, 0.04), border: '1px solid', borderColor: (theme) => alpha(theme.palette.info.main, 0.15), }} > {t('base64Converter:base64Output')} {}} /> {}} /> {fileResult.output.length > 2000 ? `${fileResult.output.substring(0, 2000)}...` : fileResult.output} {t('base64Converter:originalSize')}:{' '} {formatFileSize(fileResult.originalBytes)} {t('base64Converter:encodedSize')}: {formatFileSize(fileResult.outputBytes)} )} {fileInfo && ( )} )} ); }