Commit c717bfff authored by fisherdaddy's avatar fisherdaddy

chore: 使用 tailwind css 首页样式

parent 637c7e34
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
......@@ -2,7 +2,6 @@ import React, { Suspense, lazy } from 'react';
import { Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import Header from './components/Header';
import Footer from './components/Footer';
import NotFound from './pages/NotFound';
import Login from './pages/Login';
......@@ -33,42 +32,43 @@ function App() {
return (
<div className="app-container">
<Header />
<div className="content-wrapper">
<main>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/about" element={<About />} />
<div className="pt-4">
<div className="content-wrapper">
<main>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/about" element={<About />} />
<Route path="/dev-tools" element={<DevTools />} />
<Route path="/image-tools" element={<ImageTools />} />
<Route path="/ai-products" element={<AIProduct />} />
<Route path="/blog" element={<Blog />} />
<Route path="/dev-tools" element={<DevTools />} />
<Route path="/image-tools" element={<ImageTools />} />
<Route path="/ai-products" element={<AIProduct />} />
<Route path="/blog" element={<Blog />} />
<Route path="/markdown-to-image" element={<MarkdownToImage />} />
<Route path="/json-formatter" element={<JsonFormatter />} />
<Route path="/url-encode-and-decode" element={<UrlEnDecode />} />
<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="/quote-card" element={<QuoteCard />} />
<Route path="/latex-to-image" element={<LatexToImage />} />
<Route path="/text-diff" element={<TextDiff />} />
<Route path="/subtitle-to-image" element={<SubtitleGenerator />} />
<Route path="/image-compressor" element={<ImageCompressor />} />
<Route path="/image-watermark" element={<ImageWatermark />} />
<Route path="/text-behind-image" element={<TextBehindImage />} />
<Route path="/background-remover" element={<BackgroundRemover />} />
<Route path="/anthropic-timeline" element={<AnthropicTimeline />} />
<Route path="*" element={<NotFound />} />
<Route path="/markdown-to-image" element={<MarkdownToImage />} />
<Route path="/json-formatter" element={<JsonFormatter />} />
<Route path="/url-encode-and-decode" element={<UrlEnDecode />} />
<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="/quote-card" element={<QuoteCard />} />
<Route path="/latex-to-image" element={<LatexToImage />} />
<Route path="/text-diff" element={<TextDiff />} />
<Route path="/subtitle-to-image" element={<SubtitleGenerator />} />
<Route path="/image-compressor" element={<ImageCompressor />} />
<Route path="/image-watermark" element={<ImageWatermark />} />
<Route path="/text-behind-image" element={<TextBehindImage />} />
<Route path="/background-remover" element={<BackgroundRemover />} />
<Route path="/anthropic-timeline" element={<AnthropicTimeline />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Suspense>
</main>-
</Routes>
</Suspense>
</main>-
</div>
</div>
<Footer />
</div>
);
}
......
......@@ -8,7 +8,7 @@ import SEO from './SEO';
const Container = styled.div`
min-height: 100vh;
background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%);
padding: 2rem;
padding: 4rem 2rem 2rem;
position: relative;
&::before {
......
import React from 'react';
import { Link } from 'react-router-dom';
import { useTranslation } from '../js/i18n';
const Footer = React.memo(() => {
const { t } = useTranslation();
return (
<footer className="footer">
<p>
&copy; {new Date().getFullYear()} {t('footer.copyRight')}
<span className="footer-separator" />
<Link to="/about" className="footer-link">
{t('navigation.about')}
</Link>
</p>
</footer>
);
});
export default Footer;
\ No newline at end of file
......@@ -145,7 +145,7 @@ function HandwritingGenerator() {
const backgroundOffset = -(lineSpacing * fontSize - fontSize);
return (
<div className="handwrite-container">
<div className="handwrite-container" style={{ paddingTop: '4rem' }}>
<Layout>
<Sider width={300} className="site-layout-background">
<div className="settings-section">
......
This diff is collapsed.
......@@ -14,6 +14,7 @@ const ConverterContainer = styled(Container)`
backdrop-filter: blur(10px);
border: 1px solid rgba(99, 102, 241, 0.1);
border-radius: 12px;
padding-top: 4rem; // 添加顶部内边距
`;
const Section = styled.div`
......
......@@ -8,7 +8,7 @@ import imageCompression from 'browser-image-compression';
const Container = styled.div`
min-height: 100vh;
background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%);
padding: 2rem;
padding: 4rem 2rem 2rem;
position: relative;
&::before {
......
......@@ -7,7 +7,7 @@ import SEO from './SEO';
const Container = styled.div`
min-height: 100vh;
background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%);
padding: 2rem;
padding: 4rem 2rem 2rem;
position: relative;
&::before {
......
This diff is collapsed.
......@@ -32,18 +32,50 @@ function LanguageSelector() {
}, []);
return (
<div className="language-selector">
<button onClick={() => setIsOpen(!isOpen)} className="language-button">
{languages[lang]}
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setIsOpen(!isOpen)}
className="inline-flex items-center px-3 py-2 text-sm font-medium text-gray-600 hover:text-indigo-600 transition-colors duration-200 focus:outline-none"
aria-expanded={isOpen}
aria-haspopup="true"
>
<span>{languages[lang]}</span>
<svg
className={`ml-2 h-4 w-4 transform transition-transform duration-200 ${
isOpen ? 'rotate-180' : ''
}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
{isOpen && (
<ul className="language-dropdown" ref={dropdownRef}>
{Object.entries(languages).map(([code, name]) => (
<li key={code} onClick={() => handleLanguageChange(code)}>
{name}
</li>
))}
</ul>
<div className="absolute right-0 mt-2 w-40 rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none z-50">
<div className="py-1">
{Object.entries(languages).map(([code, name]) => (
<button
key={code}
onClick={() => handleLanguageChange(code)}
className={`w-full text-left px-4 py-2 text-sm hover:bg-indigo-50 transition-colors duration-150 ${
code === lang
? 'text-indigo-600 bg-indigo-50 font-medium'
: 'text-gray-700 hover:text-indigo-600'
}`}
>
{name}
</button>
))}
</div>
</div>
)}
</div>
);
......
......@@ -11,7 +11,7 @@ import html2canvas from 'html2canvas';
const Container = styled.div`
min-height: 100vh;
background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%);
padding: 2rem;
padding: 4rem 2rem 2rem;
position: relative;
&::before {
......
This diff is collapsed.
......@@ -30,7 +30,7 @@ const backgroundOptions = [
const Container = styled.div`
min-height: 100vh;
background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%);
padding: 2rem;
padding: 4rem 2rem 2rem;
position: relative;
&::before {
......
......@@ -247,9 +247,21 @@ useEffect(() => {
const Container = styled.div`
display: flex;
gap: 2rem;
padding: 2rem;
padding: 4rem 2rem 2rem;
background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%);
min-height: 100vh;
position: relative;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 4rem;
background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%);
z-index: -1;
}
@media (max-width: 768px) {
flex-direction: column;
......
......@@ -8,7 +8,7 @@ import '../styles/fonts.css';
const Container = styled.div`
min-height: 100vh;
background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%);
padding: 2rem;
padding: 4rem 2rem 2rem;
position: relative;
&::before {
......
......@@ -6,7 +6,7 @@ import SEO from './SEO';
const Container = styled.div`
min-height: 100vh;
background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%);
padding: 2rem;
padding: 4rem 2rem 2rem;
position: relative;
&::before {
......
import React, { useState, useCallback } from 'react';
import { Title, Wrapper, Container, Preview } from '../js/SharedStyles';
import styled from 'styled-components';
import { useTranslation } from '../js/i18n';
import SEO from './SEO';
import styled from 'styled-components';
const EncoderDecoderContainer = styled(Container)`
flex-direction: column;
gap: 16px;
`;
const StyledInputText = styled.textarea`
width: 100%;
height: 120px;
font-size: 15px;
padding: 16px;
border: 1px solid rgba(99, 102, 241, 0.1);
border-radius: 12px;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
box-sizing: border-box;
outline: none;
resize: none;
transition: all 0.3s ease;
line-height: 1.5;
&:focus {
border-color: rgba(99, 102, 241, 0.3);
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.1);
}
`;
const Label = styled.label`
font-weight: 500;
font-size: 14px;
color: #374151;
margin-bottom: 8px;
display: block;
letter-spacing: 0.1px;
`;
const ModeSwitcher = styled.div`
margin-bottom: 8px;
select {
padding: 8px 12px;
border-radius: 8px;
border: 1px solid rgba(99, 102, 241, 0.1);
font-size: 14px;
color: #374151;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
cursor: pointer;
transition: all 0.3s ease;
&:focus {
border-color: rgba(99, 102, 241, 0.3);
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.1);
outline: none;
}
}
`;
const ResultContainer = styled.div`
// 复用相同的样式组件
const Container = styled.div`
min-height: 100vh;
background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%);
padding: 4rem 2rem 2rem;
position: relative;
width: 100%;
&::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 StyledPreview = styled.div`
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
border-radius: 12px;
border: 1px solid rgba(99, 102, 241, 0.1);
padding: 16px;
font-size: 15px;
color: #374151;
min-height: 24px;
line-height: 1.5;
const ContentWrapper = styled.div`
max-width: 1400px;
margin: 0 auto;
position: relative;
z-index: 1;
`;
const ActionButton = styled.button`
position: absolute;
top: 12px;
right: 12px;
background: rgba(99, 102, 241, 0.1);
border: none;
border-radius: 6px;
padding: 6px 12px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #6366F1;
transition: all 0.3s ease;
&:hover {
background: rgba(99, 102, 241, 0.2);
}
&.active {
background: #6366F1;
color: white;
}
svg {
width: 14px;
height: 14px;
}
const Title = 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;
text-align: center;
`;
function UrlEncoderDecoder() {
......@@ -115,11 +48,10 @@ function UrlEncoderDecoder() {
const [input, setInput] = useState('');
const [resultText, setResultText] = useState('');
const [isCopied, setIsCopied] = useState(false);
const [mode, setMode] = useState('decode'); // 'encode' 或 'decode'
const [mode, setMode] = useState('decode');
const handleModeChange = (e) => {
setMode(e.target.value);
// 当模式切换时,清空输入和输出
setInput('');
setResultText('');
};
......@@ -153,53 +85,81 @@ function UrlEncoderDecoder() {
title={t('tools.urlEncodeDecode.title')}
description={t('tools.urlEncodeDecode.description')}
/>
<Wrapper>
<Title>{t('tools.urlEncodeDecode.title')}</Title>
<EncoderDecoderContainer>
<ModeSwitcher>
<Label>{t('tools.urlEncodeDecode.modeLabel')}</Label>
<select value={mode} onChange={handleModeChange}>
<option value="encode">{t('tools.urlEncodeDecode.encode')}</option>
<option value="decode">{t('tools.urlEncodeDecode.decode')}</option>
</select>
</ModeSwitcher>
<Container>
<ContentWrapper>
<Title>{t('tools.urlEncodeDecode.title')}</Title>
<div>
<Label>
{mode === 'decode' ? t('tools.urlDecode.inputLabel') : t('tools.urlEncode.inputLabel')}
</Label>
<StyledInputText
value={input}
onChange={handleInputChange}
placeholder={mode === 'decode' ? t('tools.urlDecode.inputLabel') : t('tools.urlEncode.inputLabel')}
/>
</div>
<div>
<Label>
{mode === 'decode' ? t('tools.urlDecode.resultLabel') : t('tools.urlEncode.resultLabel')}
</Label>
<ResultContainer>
<StyledPreview>{resultText}</StyledPreview>
<ActionButton
onClick={handleCopy}
className={isCopied ? 'active' : ''}
<div className="flex flex-col gap-6">
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700">
{t('tools.urlEncodeDecode.modeLabel')}
</label>
<select
value={mode}
onChange={handleModeChange}
className="w-full sm:w-48 px-3 py-2 bg-white/80 backdrop-blur-sm border border-indigo-100 rounded-xl
focus:ring-4 focus:ring-indigo-100 focus:border-indigo-300 focus:outline-none
text-sm text-gray-700 transition duration-300"
>
{isCopied ? (
<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 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>
)}
{isCopied ? t('tools.jsonFormatter.copiedMessage') : t('tools.jsonFormatter.copyButton')}
</ActionButton>
</ResultContainer>
<option value="encode">{t('tools.urlEncodeDecode.encode')}</option>
<option value="decode">{t('tools.urlEncodeDecode.decode')}</option>
</select>
</div>
<div className="flex flex-col lg:flex-row gap-4 lg:gap-6">
<div className="w-full lg:w-1/2 space-y-2">
<label className="block text-sm font-medium text-gray-700">
{mode === 'decode' ? t('tools.urlDecode.inputLabel') : t('tools.urlEncode.inputLabel')}
</label>
<textarea
value={input}
onChange={handleInputChange}
placeholder={mode === 'decode' ? t('tools.urlDecode.inputLabel') : t('tools.urlEncode.inputLabel')}
className="w-full h-[calc(100vh-400px)] px-4 py-3 bg-white/80 backdrop-blur-sm border border-indigo-100 rounded-xl
focus:ring-4 focus:ring-indigo-100 focus:border-indigo-300 focus:outline-none
text-sm font-mono text-gray-700 transition duration-300 resize-none"
/>
</div>
<div className="w-full lg:w-1/2 space-y-2">
<label className="block text-sm font-medium text-gray-700">
{mode === 'decode' ? t('tools.urlDecode.resultLabel') : t('tools.urlEncode.resultLabel')}
</label>
<div className="relative h-[calc(100vh-400px)]">
<div className="h-full w-full px-4 py-3 bg-white/80 backdrop-blur-sm border border-indigo-100
rounded-xl text-sm font-mono text-gray-700 whitespace-pre-wrap break-all overflow-auto">
{resultText}
</div>
<button
onClick={handleCopy}
className={`absolute top-2 right-2 flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-lg transition-all duration-200
${isCopied
? 'bg-green-100 text-green-700'
: 'bg-white/50 hover:bg-indigo-50 text-gray-600 hover:text-indigo-600'
}`}
>
{isCopied ? (
<>
<svg className="w-4 h-4" 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>
{t('tools.jsonFormatter.copied')}
</>
) : (
<>
<svg className="w-4 h-4" 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>
{t('tools.jsonFormatter.copy')}
</>
)}
</button>
</div>
</div>
</div>
</div>
</EncoderDecoderContainer>
</Wrapper>
</ContentWrapper>
</Container>
</>
);
}
......
......@@ -34,15 +34,29 @@ 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;
min-height: 100vh;
background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%);
padding: 4rem 2rem 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;
z-index: -1;
}
@media (min-width: 768px) {
flex-direction: row;
height: 70vh;
height: 100vh;
}
`;
......
......@@ -5,8 +5,8 @@
"selectTemplate": "选择模板",
"inputLabel": "输入文本 (支持 Markdown)",
"placeholder": "# 标题\n## 子标题\n- 列表项\n**粗体** *斜体*",
"downloadButton": "生成图片",
"previewDefault": "# 预览区域\n输入文本后在这里预览效果",
"downloadButton": "导出图片",
"previewDefault": "输入文本后在这里预览效果",
"templates": {
"simple": "简约",
"ai-style": "AI风格",
......@@ -56,8 +56,11 @@
"description": "美化和验证 JSON 数据",
"inputPlaceholder": "输入 JSON 数据",
"invalidJson": "无效的 JSON",
"copyButton": "复制",
"copiedMessage": "已复制"
"emptyInput": "",
"copy": "复制",
"copied": "已复制",
"compress": "压缩",
"expand": "展开"
},
"urlEncodeDecode": {
"title": "URL 编码/解码",
......
......@@ -251,47 +251,47 @@ const AIProduct = () => {
title={t('ai-products.title')}
description={t('ai-products.description')}
/>
<main>
<section className="tools-section">
<main className="container mx-auto px-4 pt-16 pb-8">
<section className="mt-8">
{Object.keys(groupedTools).map(category => (
<div key={category} className="category-group">
<h2 className="category-title">{t(`categories.${category}`)}</h2>
<div className="tools-grid">
<div key={category} className="mb-8">
<h2 className="text-2xl font-semibold mb-4 px-4 text-gray-800">{t(`categories.${category}`)}</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 px-4">
{groupedTools[category].map(tool => (
tool.external ? (
<a
href={tool.path}
key={tool.id}
className="tool-card"
className="flex items-center p-4 bg-white/10 backdrop-blur-md rounded-xl border border-white/10 transition-all hover:translate-y-[-2px] hover:shadow-lg hover:bg-white/15"
target="_blank"
rel="noopener noreferrer"
>
<img
src={tool.icon}
alt={`${t(`aiproducts.${tool.id}.title`)} icon`}
className="tool-icon"
className="w-12 h-12 object-contain mr-4"
loading="lazy"
/>
<div className="tool-content">
<h3 className="tool-title">{t(`aiproducts.${tool.id}.title`)}</h3>
<p className="tool-description">{t(`aiproducts.${tool.id}.description`)}</p>
<div className="flex-1 min-w-0">
<h3 className="text-lg font-semibold mb-1 text-gray-800">{t(`aiproducts.${tool.id}.title`)}</h3>
<p className="text-sm text-gray-600 line-clamp-2">{t(`aiproducts.${tool.id}.description`)}</p>
</div>
</a>
) : (
<Link
to={tool.path}
key={tool.id}
className="tool-card"
className="flex items-center p-4 bg-white/10 backdrop-blur-md rounded-xl border border-white/10 transition-all hover:translate-y-[-2px] hover:shadow-lg hover:bg-white/15"
>
<img
src={tool.icon}
alt={`${t(`tools.${tool.id}.title`)} icon`}
className="tool-icon"
className="w-12 h-12 object-contain mr-4"
loading="lazy"
/>
<div className="tool-content">
<h3 className="tool-title">{t(`tools.${tool.id}.title`)}</h3>
<p className="tool-description">{t(`tools.${tool.id}.description`)}</p>
<div className="flex-1 min-w-0">
<h3 className="text-lg font-semibold mb-1 text-gray-800">{t(`tools.${tool.id}.title`)}</h3>
<p className="text-sm text-gray-600 line-clamp-2">{t(`tools.${tool.id}.description`)}</p>
</div>
</Link>
)
......
......@@ -12,7 +12,7 @@ const About = () => {
title={t('about.title')}
description={t('about.description')}
/>
<main>
<main className="container mx-auto px-4 pt-16 pb-8">
<section className="about-section">
<div className="about-header">
<h1>{t('about.title')}</h1>
......
......@@ -18,20 +18,24 @@ const Home = () => {
title={t('blog.title')}
description={t('blog.description')}
/>
<main>
<section className="tools-section">
<div className="tools-grid">
<main className="container mx-auto px-4 pt-16 pb-8">
<section className="mt-8">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 px-4">
{tools.map(tool => (
<Link to={tool.path} key={tool.id} className="tool-card">
<Link
to={tool.path}
key={tool.id}
className="flex items-center p-4 bg-white/10 backdrop-blur-md rounded-xl border border-white/10 transition-all hover:translate-y-[-2px] hover:shadow-lg hover:bg-white/15"
>
<img
src={tool.icon}
alt={`${t(`tools.${tool.id}.title`)} icon`}
className="tool-icon"
className="w-12 h-12 object-contain mr-4"
loading="lazy"
/>
<div className="tool-content">
<h3 className="tool-title">{t(`tools.${tool.id}.title`)}</h3>
<p className="tool-description">{t(`tools.${tool.id}.description`)}</p>
<div className="flex-1 min-w-0">
<h3 className="text-lg font-semibold mb-1 text-gray-800">{t(`tools.${tool.id}.title`)}</h3>
<p className="text-sm text-gray-600 line-clamp-2">{t(`tools.${tool.id}.description`)}</p>
</div>
</Link>
))}
......
......@@ -20,20 +20,24 @@ const DevTools = () => {
title={t('dev-tools.title')}
description={t('dev-tools.description')}
/>
<main>
<section className="tools-section">
<div className="tools-grid">
<main className="container mx-auto px-4 pt-16 pb-8">
<section className="mt-8">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 px-4">
{tools.map(tool => (
<Link to={tool.path} key={tool.id} className="tool-card">
<Link
to={tool.path}
key={tool.id}
className="flex items-center p-4 bg-white/10 backdrop-blur-md rounded-xl border border-white/10 transition-all hover:translate-y-[-2px] hover:shadow-lg hover:bg-white/15"
>
<img
src={tool.icon}
alt={`${t(`tools.${tool.id}.title`)} icon`}
className="tool-icon"
className="w-12 h-12 object-contain mr-4"
loading="lazy"
/>
<div className="tool-content">
<h3 className="tool-title">{t(`tools.${tool.id}.title`)}</h3>
<p className="tool-description">{t(`tools.${tool.id}.description`)}</p>
<div className="flex-1 min-w-0">
<h3 className="text-lg font-semibold mb-1 text-gray-800">{t(`tools.${tool.id}.title`)}</h3>
<p className="text-sm text-gray-600 line-clamp-2">{t(`tools.${tool.id}.description`)}</p>
</div>
</Link>
))}
......
......@@ -12,17 +12,15 @@ const tools = [
{ id: 'imageWatermark', icon: '/assets/icon/image-watermark.png', path: '/image-watermark' },
{ id: 'imageBackgroundRemover', icon: '/assets/icon/image-background-remover.png', path: '/background-remover' },
{ id: 'textBehindImage', icon: '/assets/icon/text-behind-image.png', path: '/text-behind-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: 'anthropicTimeline', icon: '/assets/icon/anthropic_small.svg', path: '/anthropic-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 } // 新增外部链接
{ id: 'fisherai', icon: '/assets/icon/fisherai.png', path: 'https://chromewebstore.google.com/detail/fisherai-your-best-summar/ipfiijaobcenaibdpaacbbpbjefgekbj', external: true }
];
const Home = () => {
......@@ -30,31 +28,35 @@ const Home = () => {
const renderToolLink = (tool) => {
const content = (
<>
<div className="group flex items-center gap-4 p-6 bg-white rounded-xl shadow-lg hover:shadow-xl transform hover:-translate-y-1 transition-all duration-300">
<img
src={tool.icon}
alt={`${t(`tools.${tool.id}.title`)} icon`}
className="tool-icon"
className="w-12 h-12 object-contain group-hover:scale-110 transition-transform duration-300"
loading="lazy"
/>
<div className="tool-content">
<h3 className="tool-title">{t(`tools.${tool.id}.title`)}</h3>
<p className="tool-description">{t(`tools.${tool.id}.description`)}</p>
<div className="flex-1">
<h3 className="text-lg font-semibold text-gray-800 mb-1 group-hover:text-indigo-600 transition-colors duration-300">
{t(`tools.${tool.id}.title`)}
</h3>
<p className="text-sm text-gray-600 overflow-hidden text-ellipsis [-webkit-line-clamp:2] [display:-webkit-box] [-webkit-box-orient:vertical]">
{t(`tools.${tool.id}.description`)}
</p>
</div>
</>
</div>
);
return tool.external ? (
<a
href={tool.path}
className="tool-card"
className="block"
target="_blank"
rel="noopener noreferrer"
>
{content}
</a>
) : (
<Link to={tool.path} className="tool-card">
<Link to={tool.path} className="block">
{content}
</Link>
);
......@@ -66,17 +68,83 @@ const Home = () => {
title={t('title')}
description={t('slogan')}
/>
<main>
<section className="tools-section">
<div className="tools-grid">
<main className="min-h-screen bg-gradient-to-br from-indigo-50/50 via-white to-indigo-50/50 pt-16">
{/* Hero Section */}
<div className="relative overflow-hidden">
<div className="absolute inset-0 bg-grid-pattern opacity-5"></div>
<div className="max-w-7xl mx-auto px-4 pt-20 sm:pt-32 pb-12 sm:pb-20">
<div className="text-center relative z-10">
<h1 className="text-4xl sm:text-5xl font-bold text-indigo-900/90 mb-4 sm:mb-6 animate-fade-in">
AI Toolbox
</h1>
<p className="text-lg sm:text-xl text-indigo-800/80 max-w-2xl mx-auto mb-8 sm:mb-12 animate-fade-in-delay px-4">
{t('slogan')}
</p>
<div className="w-full h-0.5 max-w-xs mx-auto bg-gradient-to-r from-transparent via-indigo-400/50 to-transparent opacity-75"></div>
</div>
</div>
</div>
{/* Tools Grid */}
<div className="max-w-7xl mx-auto px-4 py-8 sm:py-16">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-8">
{tools.map(tool => (
<React.Fragment key={tool.id}>
{renderToolLink(tool)}
</React.Fragment>
))}
</div>
</section>
</div>
{/* Footer Navigation */}
<div className="max-w-7xl mx-auto px-4 pb-12 sm:pb-20">
<div className="flex flex-wrap justify-center gap-4 sm:gap-8">
<a
href="https://github.com/fisherdaddy/ai-toolbox"
target="_blank"
rel="noopener noreferrer"
className="group flex items-center px-6 py-3 rounded-full bg-white/80 hover:bg-white shadow-sm hover:shadow-md transition-all duration-300"
>
<svg className="w-5 h-5 mr-3 text-gray-700 group-hover:text-indigo-500 transition-colors" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/>
</svg>
<span className="text-gray-700 group-hover:text-indigo-500 font-medium transition-colors">GitHub</span>
</a>
<Link
to="/about"
className="group flex items-center px-6 py-3 rounded-full bg-white/80 hover:bg-white shadow-sm hover:shadow-md transition-all duration-300"
>
<svg className="w-5 h-5 mr-3 text-gray-700 group-hover:text-indigo-500 transition-colors" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path strokeLinecap="round" strokeLinejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="text-gray-700 group-hover:text-indigo-500 font-medium transition-colors">{t('navigation.about')}</span>
</Link>
</div>
</div>
</main>
<style jsx global>{`
.bg-grid-pattern {
background-image: radial-gradient(circle at 1px 1px, rgb(226 232 240 / 30%) 1px, transparent 0);
background-size: 24px 24px;
}
.animate-fade-in {
animation: fadeIn 0.8s ease-out;
}
.animate-fade-in-delay {
animation: fadeIn 0.8s ease-out 0.2s both;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
`}</style>
</>
);
};
......
......@@ -18,51 +18,32 @@ const tools = [
const ImageTools = () => {
const { t } = useTranslation();
const renderToolLink = (tool) => {
const content = (
<>
<img
src={tool.icon}
alt={`${t(`tools.${tool.id}.title`)} icon`}
className="tool-icon"
loading="lazy"
/>
<div className="tool-content">
<h3 className="tool-title">{t(`tools.${tool.id}.title`)}</h3>
<p className="tool-description">{t(`tools.${tool.id}.description`)}</p>
</div>
</>
);
return tool.external ? (
<a
href={tool.path}
className="tool-card"
target="_blank"
rel="noopener noreferrer"
>
{content}
</a>
) : (
<Link to={tool.path} className="tool-card">
{content}
</Link>
);
};
return (
<>
<SEO
title={t('title')}
description={t('slogan')}
/>
<main>
<section className="tools-section">
<div className="tools-grid">
<main className="container mx-auto px-4 pt-16 pb-8">
<section className="mt-8">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 px-4">
{tools.map(tool => (
<React.Fragment key={tool.id}>
{renderToolLink(tool)}
</React.Fragment>
<Link
to={tool.path}
key={tool.id}
className="flex items-center p-4 bg-white/10 backdrop-blur-md rounded-xl border border-white/10 transition-all hover:translate-y-[-2px] hover:shadow-lg hover:bg-white/15"
>
<img
src={tool.icon}
alt={`${t(`tools.${tool.id}.title`)} icon`}
className="w-12 h-12 object-contain mr-4"
loading="lazy"
/>
<div className="flex-1 min-w-0">
<h3 className="text-lg font-semibold mb-1 text-gray-800">{t(`tools.${tool.id}.title`)}</h3>
<p className="text-sm text-gray-600 line-clamp-2">{t(`tools.${tool.id}.description`)}</p>
</div>
</Link>
))}
</div>
</section>
......
This diff is collapsed.
......@@ -187,7 +187,9 @@
}
.pricing-charts-container {
padding: 2rem;
padding: 4rem 2rem 2rem;
max-width: 1200px;
margin: 0 auto;
background: var(--bg-primary);
min-height: 100vh;
}
......
......@@ -3,7 +3,7 @@
line-height: 1.6;
max-width: 1000px;
margin: 0 auto;
padding: 2rem;
padding: 4rem 2rem 2rem;
position: relative;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.9), rgba(255, 255, 255, 0.95));
min-height: 100vh;
......
:root {
--primary-color: #000;
--secondary-color: #06c;
--background-color: #fff;
--text-color: #1d1d1f;
--card-background: #fbfbfd;
--card-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background-color: var(--background-color);
color: var(--text-color);
margin: 0;
padding: 0;
line-height: 1.47059;
font-weight: 400;
letter-spacing: -0.022em;
}
.app-container {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.content-wrapper {
flex: 1;
padding-top: 44px;
padding-bottom: 20px;
}
header {
background-color: rgba(255, 255, 255, 0.8);
backdrop-filter: saturate(180%) blur(20px);
padding: 0 5%;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
height: 44px;
display: flex;
align-items: center;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
nav {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
main {
max-width: 1200px;
width: 100%;
margin: 0 auto;
padding: 1rem 1rem 1rem;
}
.tools-section h2 {
text-align: center;
font-size: 2.5rem;
margin-bottom: 2rem;
background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: 700;
letter-spacing: -0.02em;
}
.tools-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 2rem;
padding: 0 2rem;
max-width: 1400px;
margin: 0 auto;
}
.tool-card {
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);
transition: all 0.3s ease;
display: flex;
align-items: center;
text-decoration: none;
height: 100%;
}
.tool-card:hover {
transform: translateY(-5px);
box-shadow: 0 12px 24px rgba(99, 102, 241, 0.15);
border-color: rgba(99, 102, 241, 0.3);
}
.tool-icon {
width: 40px; /* 调整图标宽度 */
height: 40px; /* 调整图标高度 */
object-fit: contain;
margin-right: 1rem; /* 缩小图标与文本之间的间距 */
}
.tool-content {
display: flex;
flex-direction: column;
}
.tool-title {
font-size: 1.4rem;
font-weight: 600;
margin: 0;
margin-bottom: 0.5rem;
background: linear-gradient(135deg, #1a1a1a 0%, #333333 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
letter-spacing: -0.01em;
}
.tool-description {
font-size: 1rem;
color: #4B5563;
line-height: 1.5;
margin: 0;
font-weight: 400;
}
footer {
background-color: var(--card-background);
color: #86868b;
text-align: center;
font-size: 0.9rem;
}
@media (max-width: 768px) {
.tools-grid {
grid-template-columns: 1fr;
}
.tool-card {
flex-direction: column; /* 在小屏幕上堆叠图标和文本 */
align-items: center; /* 居中对齐 */
text-align: center;
padding: 1rem;
}
.tool-icon {
margin-right: 0;
margin-bottom: 1rem;
}
.tool-title {
margin-bottom: 0.25rem; /* 减少标题与描述之间的间距 */
}
.tool-description {
line-height: 1.4; /* 保持一致的行高 */
}
}
.language-selector {
position: relative;
display: inline-block;
}
.language-button {
background: none;
border: none;
padding: 8px 12px;
font-size: 14px;
cursor: pointer;
color: #333;
}
.language-dropdown {
position: absolute;
top: 100%;
right: 0;
background-color: white;
border-radius: 4px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
list-style-type: none;
padding: 0;
margin: 4px 0 0;
z-index: 1000;
max-height: 200px;
overflow-y: auto;
min-width: 120px;
}
.language-dropdown li {
padding: 8px 16px;
cursor: pointer;
}
.language-dropdown li:hover {
background-color: #f5f5f5;
}
@media (max-width: 768px) {
.language-dropdown {
right: auto;
left: 0;
}
}
.footer-separator {
display: inline-block;
width: 1px;
height: 1em;
background-color: #ccc;
margin: 0 8px;
vertical-align: middle;
}
.footer-link {
text-decoration: none;
color: inherit;
}
.footer-link:hover {
text-decoration: none;
}
.footer a {
text-decoration: none;
color: inherit;
}
.footer a:hover {
text-decoration: none;
}
.category-group {
margin-bottom: 1rem;
margin-top: 6rem;
}
.category-group:first-child {
margin-top: 0;
}
/* 添加网格背景效果 */
.tools-section {
position: relative;
padding: 1rem 0;
background:
linear-gradient(rgba(255, 255, 255, 0.9), rgba(255, 255, 255, 0.9)),
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: 100% 100%, 20px 20px, 20px 20px;
}
/* 移动端适配基础设置 */
@media screen and (max-width: 768px) {
:root {
font-size: 14px; /* 调整基础字体大小 */
}
main {
padding: 1rem;
margin-top: 3.5rem; /* 为固定导航栏留出空间 */
}
.tools-section {
padding: 1rem 0;
}
.tools-section h2 {
font-size: 2rem;
margin-bottom: 1.5rem;
padding: 0 1rem;
}
.tools-grid {
grid-template-columns: 1fr;
gap: 1rem;
padding: 0 1rem;
}
.tool-card {
padding: 1.2rem;
}
.tool-icon {
width: 35px;
height: 35px;
}
.tool-title {
font-size: 1.2rem;
}
.tool-description {
font-size: 0.95rem;
}
}
/* 针对更小屏幕的优化 */
@media screen and (max-width: 480px) {
.tools-section h2 {
font-size: 1.8rem;
}
.tool-card {
padding: 1rem;
}
.tool-icon {
width: 30px;
height: 30px;
}
}
/* 针对横屏模式的优化 */
@media screen and (max-width: 768px) and (orientation: landscape) {
.tools-grid {
grid-template-columns: repeat(2, 1fr);
}
}
\ No newline at end of file
@tailwind base;
@tailwind components;
@tailwind utilities;
\ No newline at end of file
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {
colors: {
indigo: {
50: '#EEF2FF',
500: '#6366F1',
600: '#4F46E5',
},
},
},
},
plugins: [
// 注意:line-clamp 现在已经内置在 Tailwind CSS v3.3+ 中
// 不需要额外的插件了
],
}
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