Files
jisuxiang/src/app/tools/base64_to_image/page.tsx
T
2025-04-16 22:47:19 +08:00

577 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import React, { useState, useEffect, useRef, ChangeEvent } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faDownload, faTrash, faImage, faCopy, faCheck, faExchangeAlt, faUpload } from '@fortawesome/free-solid-svg-icons';
import ToolHeader from '@/components/ToolHeader';
import { useLanguage } from '@/context/LanguageContext';
import tools from '@/config/tools';
import BackToTop from '@/components/BackToTop';
// 添加CSS变量样式
const styles = {
container: "min-h-screen flex flex-col max-w-[1440px] mx-auto p-4 md:p-6",
card: "card p-6 mb-6",
inputLabel: "block text-secondary text-sm font-bold mb-2",
heading: "text-lg font-semibold mb-2 text-primary",
textArea: "search-input w-full h-56 font-mono text-sm",
switchBtn: "btn-secondary flex items-center justify-center my-4 mx-auto",
imageContainer: "bg-block rounded-lg p-4 min-h-64 flex items-center justify-center border border-purple-glow/15 mt-4",
image: "max-w-full max-h-96 object-contain",
placeholder: "text-tertiary",
actionBtn: "w-full",
actionBtnDisabled: "btn-secondary opacity-50 cursor-not-allowed w-full",
errorMsg: "mt-2 text-red-500 text-sm",
successMsg: "mt-2 text-green-500 text-sm",
copyBtn: "btn-secondary text-sm py-1 px-3 flex items-center",
fileUploadBtn: "btn-primary flex items-center justify-center cursor-pointer",
fileName: "ml-3 text-secondary text-sm",
tabs: "flex mb-6 border-b border-purple-glow/20",
tab: "py-2 px-4 cursor-pointer text-secondary border-b-2 border-transparent",
activeTab: "py-2 px-4 text-primary border-b-2 border-primary font-medium",
};
// 转换模式类型
type ConversionMode = 'base64_to_image' | 'image_to_base64';
export default function Base64ImageConverter() {
const { t } = useLanguage();
// 从工具配置中获取当前工具图标
const toolIcon = tools.find(tool => tool.code === 'base64_to_image')?.icon || faImage;
// 状态管理
const [conversionMode, setConversionMode] = useState<ConversionMode>('base64_to_image');
// Base64转图片状态
const [base64Input, setBase64Input] = useState('');
const [imageOutput, setImageOutput] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [mimeType, setMimeType] = useState<string>('');
const [fileName, setFileName] = useState<string>('image');
const [copied, setCopied] = useState(false);
// 图片转Base64状态
const [uploadedFile, setUploadedFile] = useState<File | null>(null);
const [uploadedImage, setUploadedImage] = useState<string | null>(null);
const [base64Output, setBase64Output] = useState<string>('');
const [rawBase64Output, setRawBase64Output] = useState<string>('');
const [outputWithPrefix, setOutputWithPrefix] = useState<boolean>(true);
const [uploadError, setUploadError] = useState<string | null>(null);
const [uploadSuccess, setUploadSuccess] = useState<string | null>(null);
const [base64Copied, setBase64Copied] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
// 当Base64输入变化时处理转换(Base64转图片)
useEffect(() => {
if (conversionMode !== 'base64_to_image' || !base64Input.trim()) {
setImageOutput(null);
setError(null);
setMimeType('');
return;
}
try {
// 清除可能的数据URL前缀
const processedInput = base64Input.trim();
// 检查是否已经是完整的数据URL
if (processedInput.startsWith('data:')) {
// 从数据URL中提取MIME类型
const mimeMatch = processedInput.match(/^data:([^;]+);base64,/);
if (mimeMatch) {
setMimeType(mimeMatch[1]);
// 使用完整的数据URL
setImageOutput(processedInput);
setError(null);
// 根据MIME类型设置合适的文件扩展名
updateFileNameFromMimeType(mimeMatch[1]);
} else {
throw new Error(t('tools.base64_to_image.invalid_data_url'));
}
} else {
// 假设这是原始Base64,需要添加数据URL前缀
// 尝试推断MIME类型或使用默认值
const inferredMimeType = inferMimeTypeFromBase64(processedInput);
setMimeType(inferredMimeType);
// 创建完整的数据URL
const dataUrl = `data:${inferredMimeType};base64,${processedInput}`;
setImageOutput(dataUrl);
setError(null);
// 根据MIME类型设置合适的文件扩展名
updateFileNameFromMimeType(inferredMimeType);
}
} catch (err) {
console.error('Error processing base64:', err);
setError((err as Error).message || t('tools.base64_to_image.processing_error'));
setImageOutput(null);
}
}, [base64Input, conversionMode, t]);
// 从Base64数据推断MIME类型的简单逻辑
const inferMimeTypeFromBase64 = (base64String: string): string => {
// 默认为PNG
let mimeType = 'image/png';
// 检查前几个字符以尝试推断类型
// 注意:这是一个非常简化的版本,实际生产中可能需要更复杂的逻辑
const firstChars = base64String.substring(0, 4);
if (firstChars.startsWith('/9j/')) {
mimeType = 'image/jpeg';
} else if (firstChars.startsWith('iVBO') || firstChars.startsWith('IVBO')) {
mimeType = 'image/png';
} else if (firstChars.startsWith('R0lG')) {
mimeType = 'image/gif';
} else if (firstChars.startsWith('UE5H')) {
mimeType = 'image/png';
} else if (firstChars.startsWith('Qk0=')) {
mimeType = 'image/bmp';
} else if (firstChars.startsWith('PHN2')) {
mimeType = 'image/svg+xml';
} else if (firstChars.startsWith('AAAA')) {
mimeType = 'image/webp';
}
return mimeType;
};
// 根据MIME类型更新文件名的扩展名
const updateFileNameFromMimeType = (mime: string) => {
let extension = 'png'; // 默认扩展名
// 根据MIME类型设置扩展名
if (mime === 'image/jpeg' || mime === 'image/jpg') {
extension = 'jpg';
} else if (mime === 'image/png') {
extension = 'png';
} else if (mime === 'image/gif') {
extension = 'gif';
} else if (mime === 'image/bmp') {
extension = 'bmp';
} else if (mime === 'image/svg+xml') {
extension = 'svg';
} else if (mime === 'image/webp') {
extension = 'webp';
}
setFileName(`image.${extension}`);
};
// 清空输入和输出(Base64转图片)
const clearBase64ToImage = () => {
setBase64Input('');
setImageOutput(null);
setError(null);
setMimeType('');
setFileName('image');
};
// 清空输入和输出(图片转Base64)
const clearImageToBase64 = () => {
setUploadedFile(null);
setUploadedImage(null);
setBase64Output('');
setRawBase64Output('');
setUploadError(null);
setUploadSuccess(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
// 下载图片
const downloadImage = () => {
if (!imageOutput) return;
const link = document.createElement('a');
link.href = imageOutput;
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
// 加载示例Base64
const loadExample = () => {
// 一个简单的彩色渐变PNG图像的Base64
const exampleBase64 = 'iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9TtSIVByuIOGSoThZERRylikWwUNoKrTqYXPohNGlIUlwcBdeCgx+LVQcXZ10dXAVB8APE1cVJ0UVK/F9SaBHjwXE/3t173L0DhGaVqWbPOKBqlpFOxMVcflUMvCKIEMKIISgxU5+TZRm4jq97+Ph6F+NZ3uf+HANKwWSATySeY7phEW8QT29aOud94ggrSQrxOfGYQRckfuS67PIb55LDAs8MGZnUPHGEWCx1sdzFrGKoxFPEUUXVKN+fc1nhvMVZrdZZ+578hcGCtpLhOs1RJLCEJFIQIaOOCqqwEKNVI8VEmvbjHv4Rx58il0yuChg5FlCDCsnxg//B727N0uSEmxROAH0vtv0xAoR2gVbDtr+Pbbt1AgSegSut499oAjOfpDc6WuwIGNwGLq47mrIHXO4AQ0+GbMqO5KcpFIvA+xl9Uw4YuAUCa25vzX2cPgBpamX5Nxg4AiJlyl73eHdPd2//nmn39wNZjHKYJHtTZAAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+cMEwwTIg9nvK0AAACbSURBVHja7dAxAQAgDMAwwL/n8SSBJ2uGLdite3YEhhCGEIYQhhCGEIYQhhCGEIYQhhCGEIYQhhCGEIYQhhCGEIYQhhCGEIYQhhCGEIYQhhCGEIYQhhCGEIYQhhCGEIYQhhCGEIYQhhCGEIYQhhCGEIYQhhCGEIYQhhCGEIYQhhCGEH8tC+O6BRrHuUAeAAAAAElFTkSuQmCC';
setBase64Input(exampleBase64);
};
// 复制现有Base64到剪贴板(Base64转图片)
const copyBase64 = () => {
if (!base64Input) return;
navigator.clipboard.writeText(base64Input)
.then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
})
.catch(err => {
console.error('Failed to copy:', err);
setError(t('tools.base64_to_image.copy_failed'));
});
};
// 复制生成的Base64到剪贴板(图片转Base64
const copyGeneratedBase64 = () => {
if (!base64Output) return;
navigator.clipboard.writeText(base64Output)
.then(() => {
setBase64Copied(true);
setTimeout(() => setBase64Copied(false), 2000);
})
.catch(err => {
console.error('Failed to copy:', err);
setUploadError(t('tools.base64_to_image.copy_failed'));
});
};
// 切换转换模式
const toggleConversionMode = () => {
setConversionMode(prevMode =>
prevMode === 'base64_to_image' ? 'image_to_base64' : 'base64_to_image'
);
// 清空各自的状态
clearBase64ToImage();
clearImageToBase64();
};
// 处理文件上传(图片转Base64)
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
// 检查文件类型
if (!file.type.startsWith('image/')) {
setUploadError(t('tools.base64_to_image.invalid_image_file'));
return;
}
// 检查文件大小(限制为10MB
if (file.size > 10 * 1024 * 1024) {
setUploadError(t('tools.base64_to_image.file_too_large'));
return;
}
setUploadedFile(file);
setUploadError(null);
// 读取并显示图片预览
const reader = new FileReader();
reader.onload = (e) => {
const result = e.target?.result as string;
setUploadedImage(result);
// 提取Base64部分
const base64WithPrefix = result;
const base64Raw = result.split(',')[1];
setRawBase64Output(base64Raw);
setBase64Output(outputWithPrefix ? base64WithPrefix : base64Raw);
setUploadSuccess(t('tools.base64_to_image.conversion_success'));
};
reader.onerror = () => {
setUploadError(t('tools.base64_to_image.file_reading_error'));
};
reader.readAsDataURL(file);
};
// 切换输出格式(是否包含前缀)
const toggleOutputFormat = () => {
setOutputWithPrefix(!outputWithPrefix);
setBase64Output(outputWithPrefix ? rawBase64Output : (uploadedImage || ''));
};
// 保存Base64内容为文本文件
const saveBase64AsTextFile = () => {
if (!base64Output) return;
const blob = new Blob([base64Output], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = uploadedFile ? `${uploadedFile.name}.txt` : 'base64_data.txt';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
return (
<div className={styles.container}>
{/* 工具标题 */}
<ToolHeader
icon={toolIcon}
toolCode="base64_to_image"
title=""
description=""
/>
{/* 切换按钮 */}
<button
className={styles.switchBtn}
onClick={toggleConversionMode}
>
<FontAwesomeIcon icon={faExchangeAlt} className="mr-2 icon" />
{conversionMode === 'base64_to_image'
? t('tools.base64_to_image.switch_to_image_to_base64')
: t('tools.base64_to_image.switch_to_base64_to_image')
}
</button>
{/* 选项卡 */}
<div className={styles.tabs}>
<div
className={conversionMode === 'base64_to_image' ? styles.activeTab : styles.tab}
onClick={() => setConversionMode('base64_to_image')}
>
{t('tools.base64_to_image.base64_to_image_tab')}
</div>
<div
className={conversionMode === 'image_to_base64' ? styles.activeTab : styles.tab}
onClick={() => setConversionMode('image_to_base64')}
>
{t('tools.base64_to_image.image_to_base64_tab')}
</div>
</div>
{/* Base64转图片 */}
{conversionMode === 'base64_to_image' && (
<div className="flex flex-col lg:flex-row gap-6">
{/* 输入区域 */}
<div className="w-full lg:w-1/2">
<div className={styles.card}>
<h2 className={styles.heading}>{t('tools.base64_to_image.input_title')}</h2>
<label className={styles.inputLabel}>{t('tools.base64_to_image.base64_input')}</label>
<textarea
className={styles.textArea}
value={base64Input}
onChange={(e) => setBase64Input(e.target.value)}
placeholder={t('tools.base64_to_image.input_placeholder')}
spellCheck="false"
/>
{error && <div className={styles.errorMsg}>{error}</div>}
<div className="flex items-center justify-between mt-4">
<button
onClick={loadExample}
className="btn-secondary"
>
{t('tools.base64_to_image.load_example')}
</button>
<button
onClick={copyBase64}
disabled={!base64Input}
className={!base64Input ? styles.actionBtnDisabled : "btn-secondary"}
>
<FontAwesomeIcon icon={copied ? faCheck : faCopy} className="mr-2 icon" />
{copied ? t('common.copySuccess') : t('tools.base64_to_image.copy')}
</button>
<button
onClick={clearBase64ToImage}
className="btn-secondary"
>
<FontAwesomeIcon icon={faTrash} className="mr-2 icon" />
{t('tools.base64_to_image.clear_all')}
</button>
</div>
</div>
</div>
{/* 输出区域 */}
<div className="w-full lg:w-1/2">
<div className={styles.card}>
<h2 className={styles.heading}>{t('tools.base64_to_image.output_title')}</h2>
<div className={styles.imageContainer}>
{imageOutput ? (
<img
src={imageOutput}
alt={t('tools.base64_to_image.converted_image')}
className={styles.image}
/>
) : (
<div className={styles.placeholder}>
{t('tools.base64_to_image.no_image')}
</div>
)}
</div>
{imageOutput && (
<div className="mt-4">
<div className="flex justify-between items-center mb-2">
<span className="text-secondary text-sm">{t('tools.base64_to_image.image_type')}: {mimeType}</span>
<input
type="text"
className="search-input w-48"
value={fileName}
onChange={(e) => setFileName(e.target.value)}
placeholder={t('tools.base64_to_image.file_name')}
/>
</div>
<button
onClick={downloadImage}
className="btn-primary w-full"
disabled={!imageOutput}
>
<FontAwesomeIcon icon={faDownload} className="mr-2 icon" />
{t('tools.base64_to_image.download_image')}
</button>
</div>
)}
</div>
</div>
</div>
)}
{/* 图片转Base64 */}
{conversionMode === 'image_to_base64' && (
<div className="flex flex-col lg:flex-row gap-6">
{/* 输入区域 */}
<div className="w-full lg:w-1/2">
<div className={styles.card}>
<h2 className={styles.heading}>{t('tools.base64_to_image.image_input_title')}</h2>
<div className="mb-6">
<label className={styles.inputLabel}>
{t('tools.base64_to_image.select_image')}
</label>
<div className="flex items-center">
<label className={styles.fileUploadBtn}>
<FontAwesomeIcon icon={faUpload} className="mr-2 icon" />
{t('tools.base64_to_image.choose_file')}
<input
type="file"
className="hidden"
accept="image/*"
onChange={handleFileChange}
ref={fileInputRef}
/>
</label>
{uploadedFile && (
<span className={styles.fileName}>{uploadedFile.name}</span>
)}
</div>
</div>
{uploadError && <div className={styles.errorMsg}>{uploadError}</div>}
{uploadSuccess && <div className={styles.successMsg}>{uploadSuccess}</div>}
<div className={styles.imageContainer}>
{uploadedImage ? (
<img
src={uploadedImage}
alt={t('tools.base64_to_image.uploaded_image')}
className={styles.image}
/>
) : (
<div className={styles.placeholder}>
{t('tools.base64_to_image.no_uploaded_image')}
</div>
)}
</div>
<div className="flex items-center justify-end mt-4">
<button
onClick={clearImageToBase64}
className="btn-secondary"
disabled={!uploadedFile}
>
<FontAwesomeIcon icon={faTrash} className="mr-2 icon" />
{t('tools.base64_to_image.clear_all')}
</button>
</div>
</div>
</div>
{/* 输出区域 */}
<div className="w-full lg:w-1/2">
<div className={styles.card}>
<h2 className={styles.heading}>{t('tools.base64_to_image.base64_output_title')}</h2>
<div className="flex items-center mb-4">
<input
type="checkbox"
id="includePrefix"
checked={outputWithPrefix}
onChange={toggleOutputFormat}
className="mr-2 accent-[rgb(var(--color-primary))] w-4 h-4"
/>
<label htmlFor="includePrefix" className="text-secondary text-sm">
{t('tools.base64_to_image.include_prefix')}
</label>
</div>
<textarea
className={styles.textArea}
value={base64Output}
readOnly
placeholder={t('tools.base64_to_image.output_placeholder')}
spellCheck="false"
/>
{uploadedFile && base64Output && (
<div className="flex items-center justify-between mt-4">
<button
onClick={copyGeneratedBase64}
className="btn-secondary"
>
<FontAwesomeIcon icon={base64Copied ? faCheck : faCopy} className="mr-2 icon" />
{base64Copied ? t('common.copySuccess') : t('tools.base64_to_image.copy_base64')}
</button>
<button
onClick={saveBase64AsTextFile}
className="btn-primary"
>
<FontAwesomeIcon icon={faDownload} className="mr-2 icon" />
{t('tools.base64_to_image.save_as_text')}
</button>
</div>
)}
</div>
</div>
</div>
)}
{/* 使用说明 */}
<div className="card p-4 mb-6 mt-6">
<h3 className="text-md font-medium text-primary mb-2">{t('tools.base64_to_image.usage_guide')}</h3>
<ul className="list-disc list-inside text-sm space-y-1 text-tertiary">
{conversionMode === 'base64_to_image' ? (
<>
<li>{t('tools.base64_to_image.guide_1')}</li>
<li>{t('tools.base64_to_image.guide_2')}</li>
<li>{t('tools.base64_to_image.guide_3')}</li>
<li>{t('tools.base64_to_image.guide_4')}</li>
</>
) : (
<>
<li>{t('tools.base64_to_image.guide_5')}</li>
<li>{t('tools.base64_to_image.guide_6')}</li>
<li>{t('tools.base64_to_image.guide_7')}</li>
<li>{t('tools.base64_to_image.guide_8')}</li>
</>
)}
</ul>
</div>
{/* 回到顶部按钮 */}
<BackToTop />
</div>
);
}