Commit b035711f authored by fisherdaddy's avatar fisherdaddy

feature: 新增图片与base64转换器 & FisherAI插件

parent a1599de3
......@@ -13,6 +13,7 @@ const About = lazy(() => import('./pages/About'));
const OpenAITimeline = lazy(() => import('./components/OpenAITimeline'));
const PricingCharts = lazy(() => import('./components/PricingCharts'));
const HandwriteGen = lazy(() => import('./components/HandwriteGen'));
const ImageBase64Converter = lazy(() => import('./components/ImageBase64Converter'));
function App() {
return (
......@@ -31,6 +32,7 @@ function App() {
<Route path="/openai-timeline" element={<OpenAITimeline />} />
<Route path="/llm-model-price" element={<PricingCharts />} />
<Route path="/handwriting" element={<HandwriteGen />} />
<Route path="/image-base64" element={<ImageBase64Converter />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Suspense>
......
// ImageBase64Converter.jsx
import React, { useState, useCallback } from 'react';
import { Title, Wrapper, Container, InputText, Preview } from '../js/SharedStyles';
import styled from 'styled-components';
import { useTranslation } from '../js/i18n';
import SEO from '../components/SEO';
const ConverterContainer = styled(Container)`
flex-direction: column;
`;
const StyledInputText = styled(InputText)`
height: 100px;
margin-bottom: 20px;
@media (min-width: 768px) {
height: 100px;
width: 100%;
}
`;
const PreviewWrapper = styled.div`
width: 100%;
`;
const Label = styled.label`
font-weight: 500;
font-size: 14px;
color: #5f6368;
margin-bottom: 8px;
display: block;
letter-spacing: 0.1px;
`;
const StyledPreview = styled(Preview)`
background-color: #f8f9fa;
padding: 12px 40px 12px 12px;
border-radius: 8px;
border: 1px solid #dadce0;
font-size: 14px;
color: #202124;
min-height: 24px;
word-break: break-all;
`;
const ResultContainer = styled.div`
position: relative;
width: 100%;
`;
const CopyButton = styled.button`
position: absolute;
top: 8px;
right: 8px;
background-color: transparent;
border: none;
cursor: pointer;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.6;
transition: opacity 0.3s, color 0.3s;
&:hover {
opacity: 1;
}
svg {
width: 16px;
height: 16px;
}
&.copied {
color: #34a853;
}
`;
const StyledInputFile = styled.input`
margin-bottom: 20px;
`;
const ImagePreviewContainer = styled.div`
position: relative;
display: inline-block;
margin-top: 10px;
`;
const ImagePreview = styled.img`
max-width: 100%;
height: auto;
border: 1px solid #dadce0;
border-radius: 8px;
display: block;
`;
const DownloadButton = styled.button`
position: absolute;
top: 8px;
right: 8px;
background-color: #fff;
border: 1px solid #dadce0;
border-radius: 4px;
cursor: pointer;
padding: 6px 8px;
font-size: 12px;
color: #202124;
display: flex;
align-items: center;
opacity: 0.8;
transition: opacity 0.3s;
&:hover {
opacity: 1;
}
svg {
width: 16px;
height: 16px;
margin-right: 4px;
}
`;
const ErrorText = styled.div`
color: red;
margin-top: 8px;
`;
function ImageBase64Converter() {
const { t } = useTranslation();
// 图片转 Base64 的状态
const [imageFile, setImageFile] = useState(null);
const [base64String, setBase64String] = useState('');
const [isCopied, setIsCopied] = useState(false);
// Base64 转图片的状态
const [inputBase64, setInputBase64] = useState('');
const [imageSrc, setImageSrc] = useState('');
const [error, setError] = useState('');
// 处理图片上传
const handleImageUpload = (e) => {
const file = e.target.files[0];
if (file) {
setImageFile(file);
const reader = new FileReader();
reader.onloadend = () => {
setBase64String(reader.result);
};
reader.readAsDataURL(file);
}
};
// 复制 Base64 字符串
const handleCopy = useCallback(() => {
navigator.clipboard.writeText(base64String).then(() => {
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
});
}, [base64String]);
// 处理 Base64 输入变化
const handleBase64InputChange = (e) => {
const input = e.target.value.trim();
setInputBase64(input);
if (input) {
let src = input;
if (!input.startsWith('data:image/')) {
// 尝试自动推断图片类型
const match = input.match(/^data:(image\/[a-zA-Z]+);base64,/);
let mimeType = 'image/png'; // 默认类型
if (match && match[1]) {
mimeType = match[1];
}
src = `data:${mimeType};base64,${input}`;
}
setImageSrc(src);
setError('');
} else {
setImageSrc('');
setError('');
}
};
// 图片加载错误处理
const handleImageError = () => {
setError(t('tools.imageBase64Converter.invalidBase64'));
setImageSrc('');
};
// 下载图片
const handleDownload = () => {
const link = document.createElement('a');
link.href = imageSrc;
// 尝试从 Base64 字符串中提取文件类型和扩展名
let fileName = 'downloaded_image';
const match = imageSrc.match(/^data:(image\/[a-zA-Z]+);base64,/);
if (match && match[1]) {
const mime = match[1];
const extension = mime.split('/')[1];
fileName += `.${extension}`;
} else {
fileName += `.png`;
}
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
return (
<>
<SEO
title={t('tools.imageBase64Converter.title')}
description={t('tools.imageBase64Converter.description')}
/>
<Wrapper>
<Title>{t('tools.imageBase64Converter.title')}</Title>
<ConverterContainer>
{/* 图片转 Base64 部分 */}
<Label>{t('tools.imageBase64Converter.imageToBase64')}</Label>
<StyledInputFile
type="file"
accept="image/*"
onChange={handleImageUpload}
/>
{base64String && (
<>
<Label>{t('tools.imageBase64Converter.base64Result')}</Label>
<ResultContainer>
<StyledPreview>{base64String}</StyledPreview>
<CopyButton onClick={handleCopy} className={isCopied ? 'copied' : ''}>
{isCopied ? (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1 .9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1 -.9-2 -2-2zm0 16H8V7h11v14z" />
</svg>
)}
</CopyButton>
</ResultContainer>
</>
)}
{/* Base64 转图片部分 */}
<Label>{t('tools.imageBase64Converter.base64ToImage')}</Label>
<StyledInputText
value={inputBase64}
onChange={handleBase64InputChange}
placeholder={t('tools.imageBase64Converter.base64InputPlaceholder')}
/>
{error && <ErrorText>{error}</ErrorText>}
{imageSrc && (
<div>
<Label>{t('tools.imageBase64Converter.imageResult')}</Label>
<ImagePreviewContainer>
<ImagePreview src={imageSrc} alt="Base64 to Image" onError={handleImageError} />
<DownloadButton onClick={handleDownload}>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2M12 12v9m0 0l-3-3m3 3l3-3" />
</svg>
{t('tools.imageBase64Converter.download')}
</DownloadButton>
</ImagePreviewContainer>
</div>
)}
</ConverterContainer>
</Wrapper>
</>
);
}
export default ImageBase64Converter;
......@@ -45,7 +45,22 @@
},
"handwrite": {
"title": "Handwriting Font Generator",
"description": "Generates effects similar to writing on paper"
"description": "Generate an effect comparable to writing on paper"
},
"imageBase64Converter": {
"title": "Image and Base64 Converter",
"description": "Mutual conversion between images and Base64",
"imageToBase64": "Image to Base64",
"base64Result": "Base64 Result",
"base64ToImage": "Base64 to Image",
"base64InputPlaceholder": "Paste Base64 string here",
"imageResult": "Image Result",
"invalidBase64": "Invalid Base64 string",
"download": "Download Image"
},
"fisherai": {
"title": "One-Click Summary Plugin",
"description": "The Best Summary Extension for Chrome Browser"
}
},
"notFound": {
......@@ -115,7 +130,22 @@
},
"handwrite": {
"title": "手写字体生成器",
"description": "生成和纸上书写一样的效果"
"description": "生成和纸上书写相媲美的效果"
},
"imageBase64Converter": {
"title": "图片与 Base64 转换器",
"description": "图片与 Base64 的互相转换",
"imageToBase64": "图片转 Base64",
"base64Result": "Base64 结果",
"base64ToImage": "Base64 转图片",
"base64InputPlaceholder": "在此粘贴 Base64 字符串",
"imageResult": "图片结果",
"invalidBase64": "无效的 Base64 字符串",
"download": "下载图片"
},
"fisherai": {
"title": "一键摘要插件",
"description": "最好用的 Chrome 浏览器摘要插件"
}
},
"notFound": {
......@@ -185,7 +215,22 @@
},
"handwrite": {
"title": "手書きフォントジェネレーター",
"description": "紙に書くような効果を生成します"
"description": "紙に書くのと同等の効果を生成します"
},
"imageBase64Converter": {
"title": "画像とBase64コンバーター",
"description": "画像とBase64の相互変換",
"imageToBase64": "画像をBase64に変換",
"base64Result": "Base64の結果",
"base64ToImage": "Base64を画像に変換",
"base64InputPlaceholder": "ここにBase64文字列を貼り付け",
"imageResult": "画像の結果",
"invalidBase64": "無効なBase64文字列",
"download": "画像をダウンロード"
},
"fisherai": {
"title": "ワンクリック要約プラグイン",
"description": "最高のChromeブラウザ用要約プラグイン"
}
},
"notFound": {
......@@ -255,7 +300,22 @@
},
"handwrite": {
"title": "손글씨 폰트 생성기",
"description": "종이에 쓴 것 같은 효과를 생성합니다"
"description": "종이에 쓰는 것과 견줄 수 있는 효과를 생성합니다"
},
"imageBase64Converter": {
"title": "이미지 및 Base64 변환기",
"description": "이미지와 Base64의 상호 변환",
"imageToBase64": "이미지를 Base64로 변환",
"base64Result": "Base64 결과",
"base64ToImage": "Base64를 이미지로 변환",
"base64InputPlaceholder": "여기에 Base64 문자열을 붙여넣기",
"imageResult": "이미지 결과",
"invalidBase64": "잘못된 Base64 문자열",
"download": "이미지 다운로드"
},
"fisherai": {
"title": "원클릭 요약 플러그인",
"description": "가장 유용한 Chrome 브라우저 요약 확장 프로그램"
}
},
"notFound": {
......
......@@ -8,9 +8,11 @@ const tools = [
{ id: 'jsonFormatter', icon: 'fa-jsonformat', path: '/json-formatter' },
{ id: 'urlDecode', icon: 'fa-decode', path: '/url-decode' },
{ id: 'urlEncode', icon: 'fa-encode', path: '/url-encode' },
{ id: 'imageBase64Converter', icon: 'fa-image-base64', path: '/image-base64' },
{ id: 'handwrite', icon: 'fa-handwrite', path: '/handwriting' },
{ id: 'openAITimeline', icon: 'fa-openai-timeline', path: '/openai-timeline' },
{ id: 'modelPrice', icon: 'fa-model-price', path: '/llm-model-price' },
{ id: 'fisherai', icon: 'fa-fisherai', path: 'https://chromewebstore.google.com/detail/fisherai-your-best-summar/ipfiijaobcenaibdpaacbbpbjefgekbj', external: true } // 新增外部链接
];
const Home = () => {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment