Commit 564294bc authored by fisherdaddy's avatar fisherdaddy

feature: 新增diff工具

parent e390e4a6
......@@ -10,6 +10,7 @@
"dependencies": {
"@react-oauth/google": "^0.12.1",
"antd": "^5.21.6",
"diff": "^7.0.0",
"dompurify": "^3.1.7",
"html2canvas": "^1.4.1",
"i18next": "^23.16.5",
......
......@@ -21,6 +21,7 @@ const HandwriteGen = lazy(() => import('./components/HandwriteGen'));
const ImageBase64Converter = lazy(() => import('./components/ImageBase64Converter'));
const QuoteCard = lazy(() => import('./components/QuoteCard'));
const LatexToImage = lazy(() => import('./components/LatexToImage'));
const TextDiff = lazy(() => import('./components/TextDiff'));
function App() {
return (
......@@ -48,6 +49,7 @@ function App() {
<Route path="/image-base64" element={<ImageBase64Converter />} />
<Route path="/quote-card" element={<QuoteCard />} />
<Route path="/latex-to-image" element={<LatexToImage />} />
<Route path="/text-diff" element={<TextDiff />} />
<Route path="*" element={<NotFound />} />
</Routes>
......
import React, { useState } from 'react';
import styled from 'styled-components';
import { useTranslation } from '../js/i18n';
import SEO from './SEO';
const Container = styled.div`
min-height: 100vh;
background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%);
padding: 2rem;
position: relative;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
linear-gradient(90deg, rgba(99, 102, 241, 0.05) 1px, transparent 1px),
linear-gradient(rgba(99, 102, 241, 0.05) 1px, transparent 1px);
background-size: 20px 20px;
pointer-events: none;
}
`;
const ContentWrapper = styled.div`
display: flex;
gap: 2rem;
max-width: 1400px;
margin: 0 auto;
position: relative;
z-index: 1;
@media (max-width: 768px) {
flex-direction: column;
}
`;
const InputContainer = styled.div`
flex: 1;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 1.5rem;
box-shadow: 0 8px 32px rgba(99, 102, 241, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
display: flex;
flex-direction: column;
gap: 1rem;
`;
const TitleLabel = styled.h2`
font-size: 1.8rem;
margin-bottom: 1.5rem;
background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: 700;
letter-spacing: -0.02em;
`;
const TextArea = styled.textarea`
width: 100%;
height: 400px;
padding: 1rem;
border: 1px solid rgba(99, 102, 241, 0.2);
border-radius: 8px;
font-size: 14px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
resize: none;
background: rgba(255, 255, 255, 0.9);
&:focus {
outline: none;
border-color: rgba(99, 102, 241, 0.5);
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.1);
}
`;
const DiffContainer = styled.div`
flex: 1;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 1.5rem;
box-shadow: 0 8px 32px rgba(99, 102, 241, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
`;
const DiffViewContainer = styled.div`
display: flex;
gap: 2rem;
width: 100%;
max-width: 1400px;
margin: 0 auto;
`;
const DiffPanel = styled.div`
flex: 1;
min-width: 0;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 1.5rem;
box-shadow: 0 8px 32px rgba(99, 102, 241, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
display: flex;
flex-direction: column;
`;
const LineNumbersContainer = styled.div`
width: 40px;
padding-right: 10px;
text-align: right;
color: #6e7681;
user-select: none;
border-right: 1px solid #d0d7de;
`;
const DiffContent = styled.div`
display: flex;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
font-size: 14px;
line-height: 1.5;
overflow-x: auto;
white-space: pre;
margin-top: 1rem;
border: 1px solid rgba(99, 102, 241, 0.2);
border-radius: 8px;
background: rgba(255, 255, 255, 0.9);
`;
const DiffLine = styled.div`
display: flex;
width: 100%;
background-color: ${props => {
if (props.$added) return 'rgba(46, 160, 67, 0.15)';
if (props.$removed) return 'rgba(248, 81, 73, 0.15)';
return 'transparent';
}};
`;
const LineContent = styled.div`
padding: 0 8px;
flex: 1;
color: ${props => {
if (props.$added) return '#1a7f37';
if (props.$removed) return '#cf222e';
return 'inherit';
}};
`;
const LineNumber = styled.div`
color: #6e7681;
padding: 0 8px;
text-align: right;
user-select: none;
min-width: 40px;
border-right: 1px solid #d0d7de;
`;
const DiffHeader = styled(TitleLabel)`
font-size: 1.4rem;
margin-bottom: 1rem;
`;
function TextDiff() {
const { t } = useTranslation();
const [oldText, setOldText] = useState('');
const [newText, setNewText] = useState('');
const processText = (text) => {
return text.split('\n');
};
const oldLines = processText(oldText);
const newLines = processText(newText);
const maxLines = Math.max(oldLines.length, newLines.length);
return (
<>
<SEO
title={t('tools.textDiff.title')}
description={t('tools.textDiff.description')}
/>
<Container>
<DiffViewContainer>
<DiffPanel>
<DiffHeader>{t('tools.textDiff.originalText')}</DiffHeader>
<textarea
value={oldText}
onChange={(e) => setOldText(e.target.value)}
placeholder={t('tools.textDiff.originalPlaceholder')}
style={{
width: '100%',
height: '200px',
padding: '8px',
border: '1px solid rgba(99, 102, 241, 0.2)',
borderRadius: '8px',
fontFamily: 'Monaco, Menlo, Consolas, monospace',
resize: 'none'
}}
/>
<DiffContent>
<LineNumber>
{oldLines.map((_, i) => (
<div key={i}>{i + 1}</div>
))}
</LineNumber>
<div style={{ flex: 1 }}>
{oldLines.map((line, i) => (
<DiffLine
key={i}
$removed={!newLines.includes(line)}
>
<LineContent $removed={!newLines.includes(line)}>
{line}
</LineContent>
</DiffLine>
))}
</div>
</DiffContent>
</DiffPanel>
<DiffPanel>
<DiffHeader>{t('tools.textDiff.newText')}</DiffHeader>
<textarea
value={newText}
onChange={(e) => setNewText(e.target.value)}
placeholder={t('tools.textDiff.newPlaceholder')}
style={{
width: '100%',
height: '200px',
padding: '8px',
border: '1px solid rgba(99, 102, 241, 0.2)',
borderRadius: '8px',
fontFamily: 'Monaco, Menlo, Consolas, monospace',
resize: 'none'
}}
/>
<DiffContent>
<LineNumber>
{newLines.map((_, i) => (
<div key={i}>{i + 1}</div>
))}
</LineNumber>
<div style={{ flex: 1 }}>
{newLines.map((line, i) => (
<DiffLine
key={i}
$added={!oldLines.includes(line)}
>
<LineContent $added={!oldLines.includes(line)}>
{line}
</LineContent>
</DiffLine>
))}
</div>
</DiffContent>
</DiffPanel>
</DiffViewContainer>
</Container>
</>
);
}
export default TextDiff;
\ No newline at end of file
......@@ -104,5 +104,13 @@
"fisherai": {
"title": "FisherAI",
"description": "The Best Summary Extension for Chrome Browser"
},
"textDiff": {
"title": "Text Diff",
"description": "Compare differences between two texts",
"originalText": "Original Text",
"newText": "New Text",
"originalPlaceholder": "Enter original text here...",
"newPlaceholder": "Enter new text here..."
}
}
\ No newline at end of file
......@@ -104,5 +104,13 @@
"fisherai": {
"title": "FisherAI",
"description": "最高のChromeブラウザ用要約プラグイン"
},
"textDiff": {
"title": "テキスト差分",
"description": "2つのテキストの違いを比較",
"originalText": "元のテキスト",
"newText": "新しいテキスト",
"originalPlaceholder": "元のテキストを入力...",
"newPlaceholder": "新しいテキストを入力..."
}
}
\ No newline at end of file
......@@ -105,5 +105,13 @@
"fisherai": {
"title": "FisherAI",
"description": "가장 유용한 Chrome 브라우저 요약 확장 프로그램"
},
"textDiff": {
"title": "텍스트 비교",
"description": "두 텍스트의 차이점 비교",
"originalText": "원본 텍스트",
"newText": "새 텍스트",
"originalPlaceholder": "원본 텍스트를 입력하세요...",
"newPlaceholder": "새 텍스트를 입력하세요..."
}
}
\ No newline at end of file
......@@ -103,5 +103,13 @@
"fisherai": {
"title": "FisherAI",
"description": "最好用的 Chrome 浏览器摘要插件"
},
"textDiff": {
"title": "文本差异对比",
"description": "比较两段文本的差异",
"originalText": "原始文本",
"newText": "新文本",
"originalPlaceholder": "在此输入原始文本...",
"newPlaceholder": "在此输入新文本..."
}
}
\ No newline at end of file
......@@ -7,6 +7,8 @@ const tools = [
{ id: 'jsonFormatter', icon: '/assets/icon/json-format.png', path: '/json-formatter' },
{ id: 'urlEncodeDecode', icon: '/assets/icon/url-endecode.png', path: '/url-encode-and-decode' },
{ id: 'imageBase64Converter', icon: '/assets/icon/image-base64.png', path: '/image-base64' },
{ id: 'textDiff', icon: '/assets/icon/diff.png', path: '/text-diff' },
];
const DevTools = () => {
......
......@@ -7,10 +7,13 @@ const tools = [
{ id: 'handwrite', icon: '/assets/icon/handwrite.png', path: '/handwriting' },
{ id: 'quoteCard', icon: '/assets/icon/quotecard.png', path: '/quote-card' },
{ id: 'markdown2image', icon: '/assets/icon/markdown2image.png', path: '/markdown-to-image' },
{ id: 'latex2image', icon: '/assets/icon/latex2image.png', path: '/latex-to-image' },
{ id: 'jsonFormatter', icon: '/assets/icon/json-format.png', path: '/json-formatter' },
{ id: 'urlEncodeDecode', icon: '/assets/icon/url-endecode.png', path: '/url-encode-and-decode' },
{ id: 'imageBase64Converter', icon: '/assets/icon/image-base64.png', path: '/image-base64' },
{ id: 'textDiff', icon: '/assets/icon/diff.png', path: '/text-diff' },
{ id: 'openAITimeline', icon: '/assets/icon/openai_small.svg', path: '/openai-timeline' },
{ id: 'modelPrice', icon: '/assets/icon/openai_small.svg', path: '/llm-model-price' },
{ id: 'fisherai', icon: '/assets/icon/fisherai.png', path: 'https://chromewebstore.google.com/detail/fisherai-your-best-summar/ipfiijaobcenaibdpaacbbpbjefgekbj', external: true } // 新增外部链接
......
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