Commit 515af0dc authored by fisherdaddy's avatar fisherdaddy

feature: 新增 URL 解码器 & 样式优化 & 支持多语言

parent 6b538bc4
......@@ -5,6 +5,7 @@ import JsonFormatter from './components/JsonFormatter';
import Header from './components/Header';
import Footer from './components/Footer';
import TextToImage from './components/TextToImage';
import UrlDecode from './components/UrlDecode';
function App(op) {
return (
......@@ -16,6 +17,7 @@ function App(op) {
<Route path="/" element={<Home />} />
<Route path="/text2image" element={<TextToImage />} />
<Route path="/json-formatter" element={<JsonFormatter />} />
<Route path="/url-decode" element={<UrlDecode />} />
</Routes>
</main>
</div>
......
import React, { useState, useEffect } from 'react';
import styled from 'styled-components';
import { Title, Wrapper, Container, Preview } from './SharedStyles';
import { useTranslation } from '../js/i18n';
const Container = styled.div`
padding: 20px;
max-width: 1200px;
margin: 0 auto;
font-family: 'Arial', sans-serif;
`;
const Title = styled.h2`
color: #333;
margin-bottom: 20px;
`;
const InputText = styled.textarea`
width: 100%;
height: 200px;
font-size: 14px;
padding: 10px;
border: none;
border-bottom: 1px solid #e0e0e0;
box-sizing: border-box;
outline: none;
resize: none;
const FlexContainer = styled.div`
display: flex;
gap: 20px;
@media (min-width: 768px) {
width: 35%;
height: 100%;
border-bottom: none;
border-right: 1px solid #e0e0e0;
}
`;
const TextArea = styled.textarea`
const PreviewContainer = styled.div`
width: 100%;
height: 400px;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-family: monospace;
font-size: 14px;
`;
box-sizing: border-box;
const JsonContainer = styled.div`
flex: 1;
overflow: auto;
height: 400px;
background-color: #f8f8f8;
padding: 10px;
border-radius: 4px;
@media (min-width: 768px) {
width: 65%;
height: 100%;
}
`;
const ToggleButton = styled.span`
......@@ -58,9 +57,43 @@ const JsonList = styled.ul`
margin: 0;
`;
const CopyButton = styled.button`
position: absolute;
top: 10px;
right: 10px;
background-color: transparent;
border: none;
cursor: pointer;
padding: 5px;
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; // Google green color for success feedback
}
`;
const RelativePreviewContainer = styled(PreviewContainer)`
position: relative;
`;
function JsonFormatter() {
const { t } = useTranslation();
const [input, setInput] = useState('');
const [parsedJson, setParsedJson] = useState(null);
const [isCopied, setIsCopied] = useState(false);
useEffect(() => {
try {
......@@ -71,26 +104,49 @@ function JsonFormatter() {
}
}, [input]);
const handleCopy = () => {
if (parsedJson) {
const formattedJson = JSON.stringify(parsedJson, null, 2);
navigator.clipboard.writeText(formattedJson).then(() => {
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
});
}
};
return (
<Container>
<Title>JSON格式化工具</Title>
<FlexContainer>
<div style={{ flex: 1 }}>
<TextArea
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="输入JSON数据"
/>
</div>
<JsonContainer>
<Wrapper>
<Title>{t('tools.jsonFormatter.title')}</Title>
<Container>
<InputText
placeholder={t('tools.jsonFormatter.inputPlaceholder')}
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<RelativePreviewContainer>
{parsedJson ? (
<JsonView data={parsedJson} />
<>
<Preview>
<JsonView data={parsedJson} />
</Preview>
<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>
</>
) : (
<pre>Invalid JSON</pre>
<Preview>{t('tools.jsonFormatter.invalidJson')}</Preview>
)}
</JsonContainer>
</FlexContainer>
</Container>
</RelativePreviewContainer>
</Container>
</Wrapper>
);
}
......@@ -106,11 +162,14 @@ function JsonView({ data }) {
{!isExpanded && <span>Array</span>}
{isExpanded && (
<JsonList>
[
{data.map((item, index) => (
<li key={index}>
<JsonView data={item} />
{index < data.length - 1 && ','}
</li>
))}
]
</JsonList>
)}
</div>
......@@ -124,11 +183,14 @@ function JsonView({ data }) {
{!isExpanded && <span>Object</span>}
{isExpanded && (
<JsonList>
{Object.entries(data).map(([key, value]) => (
{'{'}
{Object.entries(data).map(([key, value], index, array) => (
<li key={key}>
<Key>"{key}"</Key>: <JsonView data={value} />
{index < array.length - 1 && ','}
</li>
))}
{'}'}
</JsonList>
)}
</div>
......
......@@ -11,8 +11,10 @@ function LanguageSelector() {
return (
<div id="language-selector">
<select id="lang-select" value={lang} onChange={handleLanguageChange}>
<option value="zh">中文</option>
<option value="zh">中文</option>
<option value="en">English</option>
<option value="ja">日本語</option>
<option value="ko">한국어</option>
</select>
</div>
);
......
import styled from 'styled-components';
export const Title = styled.h1`
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', sans-serif;
font-size: 28px;
font-weight: 500;
color: #1a73e8; // Google Blue
text-align: center;
margin-bottom: 24px;
letter-spacing: -0.5px;
position: relative;
padding-bottom: 12px;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 40px;
height: 3px;
background: linear-gradient(90deg, #4285f4, #34a853, #fbbc05, #ea4335); // Google colors
border-radius: 2px;
}
`;
export const Wrapper = styled.div`
width: 100%;
max-width: 2000px;
margin: 10px auto;
`;
export const Container = styled.div`
display: flex;
flex-direction: column;
width: 100%;
background-color: white;
border-radius: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
overflow: hidden;
margin: 10px auto;
@media (min-width: 768px) {
flex-direction: row;
height: 70vh;
}
`;
export const InputText = styled.textarea`
width: 100%;
height: 200px;
font-size: 14px;
padding: 10px;
border: none;
border-bottom: 1px solid #e0e0e0;
box-sizing: border-box;
outline: none;
resize: none;
@media (min-width: 768px) {
width: 50%;
height: 100%;
border-bottom: none;
border-right: 1px solid #e0e0e0;
}
`;
export const PreviewContainer = styled.div`
width: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 10px;
box-sizing: border-box;
@media (min-width: 768px) {
width: 50%;
height: 100%;
}
`;
export const Preview = styled.div`
word-wrap: break-word;
white-space: pre-wrap;
max-width: 100%;
text-align: left;
overflow-y: auto;
flex-grow: 1;
padding-right: 10px;
font-size: 14px;
max-height: 200px;
@media (min-width: 768px) {
max-height: none;
}
h1, h2, h3, h4 {
color: #2c3e50;
margin-top: 0.5em;
margin-bottom: 0.3em;
line-height: 1.2;
}
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
}
&::-webkit-scrollbar-thumb {
background: #888;
border-radius: 3px;
}
&::-webkit-scrollbar-thumb:hover {
background: #555;
}
`;
\ No newline at end of file
import React, { useState, useRef, useEffect } from 'react';
import styled from 'styled-components';
const Wrapper = styled.div`
width: 100%;
max-width: 1000px;
margin: 10px auto;
`;
const Title = styled.h1`
font-size: 20px;
color: #333;
margin-bottom: 10px;
text-align: center;
`;
const Container = styled.div`
display: flex;
flex-direction: column;
width: 100%;
background-color: white;
border-radius: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
overflow: hidden;
margin: 10px auto;
@media (min-width: 768px) {
flex-direction: row;
height: 70vh;
}
`;
const InputText = styled.textarea`
width: 100%;
height: 200px;
font-size: 14px;
padding: 10px;
border: none;
border-bottom: 1px solid #e0e0e0;
box-sizing: border-box;
outline: none;
resize: none;
@media (min-width: 768px) {
width: 50%;
height: 100%;
border-bottom: none;
border-right: 1px solid #e0e0e0;
}
`;
const PreviewContainer = styled.div`
width: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 10px;
box-sizing: border-box;
@media (min-width: 768px) {
width: 50%;
height: 100%;
}
`;
const Preview = styled.div`
word-wrap: break-word;
white-space: pre-wrap;
max-width: 100%;
text-align: left;
overflow-y: auto;
flex-grow: 1;
padding-right: 10px;
font-size: 14px;
max-height: 200px;
@media (min-width: 768px) {
max-height: none;
}
h1, h2, h3 {
color: #2c3e50;
margin-top: 0;
}
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
}
&::-webkit-scrollbar-thumb {
background: #888;
border-radius: 3px;
}
&::-webkit-scrollbar-thumb:hover {
background: #555;
}
`;
import { Title, Wrapper, Container, InputText, PreviewContainer, Preview } from './SharedStyles';
import { useTranslation } from '../js/i18n';
const DownloadButton = styled.button`
padding: 8px 16px;
......@@ -119,15 +21,17 @@ const DownloadButton = styled.button`
`;
function TextToImage() {
const { t } = useTranslation();
const [text, setText] = useState('');
const previewRef = useRef(null);
const formatText = (text) => {
return text
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
.replace(/^### (.*$)/gim, '<h4>$1</h4>')
.replace(/^## (.*$)/gim, '<h3>$1</h3>')
.replace(/^# (.*$)/gim, '<h2>$1</h2>')
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\n{2,}/g, '<br/><br/>')
.replace(/\n/g, '<br/>');
};
......@@ -165,10 +69,10 @@ function TextToImage() {
return (
<Wrapper>
<Title>文字卡片生成器</Title>
<Title>{t('tools.text2image.title')}</Title>
<Container>
<InputText
placeholder="输入文本(可包含标题,如# 标题1)"
placeholder={t('tools.text2image.inputPlaceholder')}
value={text}
onChange={(e) => setText(e.target.value)}
/>
......@@ -177,7 +81,9 @@ function TextToImage() {
ref={previewRef}
dangerouslySetInnerHTML={{ __html: formatText(text) }}
/>
<DownloadButton onClick={handleDownload}>导出为图片</DownloadButton>
<DownloadButton onClick={handleDownload}>
{t('tools.text2image.downloadButton')}
</DownloadButton>
</PreviewContainer>
</Container>
</Wrapper>
......
import React, { useState, useCallback } from 'react';
import { Title, Wrapper, Container, InputText, Preview } from './SharedStyles';
import styled from 'styled-components';
import { useTranslation } from '../js/i18n';
const DecoderContainer = 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; // 增加右侧 padding 为按钮留出空间
border-radius: 8px;
border: 1px solid #dadce0;
font-size: 14px;
color: #202124;
min-height: 24px; // 确保即使内容为空,也有足够的高度容纳按钮
`;
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; // Google green color for success feedback
}
`;
function UrlDecoder() {
const { t } = useTranslation();
const [input, setInput] = useState('');
const [decodedText, setDecodedText] = useState('');
const [isCopied, setIsCopied] = useState(false);
const handleInputChange = (e) => {
const inputValue = e.target.value;
setInput(inputValue);
try {
const decoded = decodeURIComponent(inputValue);
setDecodedText(decoded);
} catch (error) {
setDecodedText('Invalid URL encoding');
}
};
const handleCopy = useCallback(() => {
navigator.clipboard.writeText(decodedText).then(() => {
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
});
}, [decodedText]);
return (
<Wrapper>
<Title>{t('tools.urlDecode.title')}</Title>
<DecoderContainer>
<StyledInputText
id="urlInput"
placeholder={t('tools.urlDecode.inputLabel')}
value={input}
onChange={handleInputChange}
/>
<PreviewWrapper>
<Label>{t('tools.urlDecode.resultLabel')}</Label>
<ResultContainer>
<StyledPreview>{decodedText}</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>
</PreviewWrapper>
</DecoderContainer>
</Wrapper>
);
}
export default UrlDecoder;
\ No newline at end of file
import { useState, useEffect } from 'react';
const i18n = {
en: {
title: 'AI Toolbox',
slogan: 'Your collection of intelligent assistants, solving various AI needs in one place.',
tools: {
text2image: {
title: 'Text to Image Card',
description: 'Convert text to image card',
inputPlaceholder: 'Enter text (can include titles, e.g. # Title 1)',
downloadButton: 'Export as Image'
},
jsonFormatter: {
title: 'JSON Formatter',
description: 'Beautify and validate JSON data',
inputPlaceholder: 'Enter JSON data',
invalidJson: 'Invalid JSON',
copyButton: 'Copy',
copiedMessage: 'Copied'
},
urlDecode: {
title: 'URL Decoder',
description: 'Decode URL-encoded strings',
inputLabel: 'Enter URL to decode',
resultLabel: 'Decoded result',
copyButton: 'Copy',
copiedMessage: 'Copied'
},
},
},
zh: {
title: 'AI 工具箱',
slogan: '您的智能助手集合,一站式解决各种 AI 需求。',
tools: {
text2image: {
title: '文字卡片',
description: '将文字转换为图片卡'
description: '将文字转换为图片卡',
inputPlaceholder: '输入文本(可包含标题,如# 标题1)',
downloadButton: '导出为图片'
},
jsonFormatter: {
title: 'JSON 格式化',
description: '美化和验证 JSON 数据'
description: '美化和验证 JSON 数据',
inputPlaceholder: '输入 JSON 数据',
invalidJson: '无效的 JSON',
copyButton: '复制',
copiedMessage: '已复制'
},
urlDecode: {
title: 'URL 解码器',
description: '解码 URL 编码的字符串',
inputLabel: '输入需要解码的 URL',
resultLabel: '解码结果',
copyButton: '复制',
copiedMessage: '已复制'
},
// 添加更多工具...
},
// 添加更多翻译...
},
en: {
title: 'AI Toolbox',
slogan: 'Your collection of intelligent assistants, solving various AI needs in one place.',
ja: {
title: 'AIツールボックス',
slogan: 'あなたのインテリジェントアシスタントコレクション、様々なAIニーズを一箇所で解決します。',
tools: {
text2image: {
title: 'Text to Image Card',
description: 'Convert text to image card'
title: 'テキストから画像',
description: 'テキストを画像カードに変換',
inputPlaceholder: 'テキストを入力(タイトルを含めることができます、例:# タイトル1)',
downloadButton: '画像としてエクスポート'
},
jsonFormatter: {
title: 'JSON Formatter',
description: 'Beautify and validate JSON data'
title: 'JSONフォーマッター',
description: 'JSONデータを整形し検証する',
inputPlaceholder: 'JSONデータを入力',
invalidJson: '無効なJSON',
copyButton: 'コピー',
copiedMessage: 'コピーしました'
},
urlDecode: {
title: 'URLデコーダー',
description: 'URLエンコードされた文字列をデコード',
inputLabel: 'デコードするURLを入力',
resultLabel: 'デコード結果',
copyButton: 'コピー',
copiedMessage: 'コピーしました'
},
},
},
ko: {
title: 'AI 도구 상자',
slogan: '당신의 지능형 어시스턴트 컬렉션, 다양한 AI 요구 사항을 한 곳에서 해결합니다.',
tools: {
text2image: {
title: '텍스트를 이미지로',
description: '텍스트를 이미지 카드로 변환',
inputPlaceholder: '텍스트 입력 (제목 포함 가능, 예: # 제목 1)',
downloadButton: '이미지로 내보내기'
},
jsonFormatter: {
title: 'JSON 포맷터',
description: 'JSON 데이터 정리 및 검증',
inputPlaceholder: 'JSON 데이터 입력',
invalidJson: '유효하지 않은 JSON',
copyButton: '복사',
copiedMessage: '복사됨'
},
urlDecode: {
title: 'URL 디코더',
description: 'URL 인코딩된 문자열 디코딩',
inputLabel: '디코딩할 URL 입력',
resultLabel: '디코딩 결과',
copyButton: '복사',
copiedMessage: '복사됨'
},
// 添加更多工具...
},
// 添加更多翻译...
},
// 添加更多语言...
};
let currentLanguage = localStorage.getItem('language') || 'zh'; // 从本地存储获取语言设置,默认为中文
let currentLanguage = localStorage.getItem('language') || 'en';
let listeners = [];
export function setLanguage(lang) {
if (i18n[lang]) {
currentLanguage = lang;
localStorage.setItem('language', lang); // 将语言设置保存到本地存储
localStorage.setItem('language', lang);
listeners.forEach(listener => listener(currentLanguage));
}
}
......@@ -56,7 +135,7 @@ export function t(key) {
let value = i18n[currentLanguage];
for (const k of keys) {
if (value[k] === undefined) {
return key; // 如果翻译不存在,返回原始 key
return key;
}
value = value[k];
}
......
......@@ -6,7 +6,7 @@ import { useTranslation } from '../js/i18n';
const tools = [
{ id: 'text2image', icon: 'fa-image', path: '/text2image' },
{ id: 'jsonFormatter', icon: 'fa-code', path: '/json-formatter' },
{ id: 'textTranslation', icon: 'fa-language', path: '/text-translation' },
{ id: 'urlDecode', icon: 'fa-decode', path: '/url-decode' },
];
function Home() {
......
......@@ -26,8 +26,8 @@ body {
.content-wrapper {
flex: 1;
padding-top: 2px;
padding-bottom: 30px;
padding-top: 10px;
padding-bottom: 20px;
}
header {
......@@ -56,7 +56,7 @@ main {
max-width: 1200px;
width: 100%;
margin: 0 auto;
padding: 6rem 1rem 4rem;
padding: 3rem 1rem 1rem;
}
.hero {
......@@ -88,35 +88,6 @@ h1 {
margin: 0 auto;
}
#tool-search {
width: 100%;
padding: 1.2rem 1.5rem;
font-size: 1.2rem;
border: none;
border-radius: 12px;
background-color: rgba(255, 255, 255, 0.2);
color: white;
transition: all 0.3s ease;
}
#tool-search::placeholder {
color: rgba(255, 255, 255, 0.7);
}
#tool-search:focus {
outline: none;
background-color: rgba(255, 255, 255, 0.3);
}
.search-container i {
position: absolute;
right: 1.5rem;
top: 50%;
transform: translateY(-50%);
color: rgba(255, 255, 255, 0.7);
font-size: 1.2rem;
}
.tools-section h2 {
text-align: center;
font-size: 3rem;
......@@ -167,7 +138,6 @@ footer {
background-color: var(--card-background);
color: #86868b;
text-align: center;
padding: 1.5rem;
font-size: 0.9rem;
}
......@@ -183,4 +153,5 @@ footer {
.slogan {
font-size: 1.5rem;
}
}
\ No newline at end of file
}
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