Commit c717bfff authored by fisherdaddy's avatar fisherdaddy

chore: 使用 tailwind css 首页样式

parent 637c7e34
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
"antd": "^5.21.6", "antd": "^5.21.6",
"browser-image-compression": "^2.0.2", "browser-image-compression": "^2.0.2",
"diff": "^7.0.0", "diff": "^7.0.0",
"dompurify": "^3.1.7", "dompurify": "^3.2.1",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"i18next": "^23.16.5", "i18next": "^23.16.5",
"katex": "^0.16.11", "katex": "^0.16.11",
...@@ -27,7 +27,11 @@ ...@@ -27,7 +27,11 @@
"styled-components": "^6.1.13" "styled-components": "^6.1.13"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/line-clamp": "^0.4.4",
"@vitejs/plugin-react": "^4.0.0", "@vitejs/plugin-react": "^4.0.0",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.15",
"vite": "^4.3.9", "vite": "^4.3.9",
"vite-plugin-sitemap": "^0.7.1" "vite-plugin-sitemap": "^0.7.1"
} }
......
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
...@@ -2,7 +2,6 @@ import React, { Suspense, lazy } from 'react'; ...@@ -2,7 +2,6 @@ import React, { Suspense, lazy } from 'react';
import { Routes, Route } from 'react-router-dom'; import { Routes, Route } from 'react-router-dom';
import Home from './pages/Home'; import Home from './pages/Home';
import Header from './components/Header'; import Header from './components/Header';
import Footer from './components/Footer';
import NotFound from './pages/NotFound'; import NotFound from './pages/NotFound';
import Login from './pages/Login'; import Login from './pages/Login';
...@@ -33,42 +32,43 @@ function App() { ...@@ -33,42 +32,43 @@ function App() {
return ( return (
<div className="app-container"> <div className="app-container">
<Header /> <Header />
<div className="content-wrapper"> <div className="pt-4">
<main> <div className="content-wrapper">
<Suspense fallback={<div>Loading...</div>}> <main>
<Routes> <Suspense fallback={<div>Loading...</div>}>
<Route path="/" element={<Home />} /> <Routes>
<Route path="/login" element={<Login />} /> <Route path="/" element={<Home />} />
<Route path="/about" element={<About />} /> <Route path="/login" element={<Login />} />
<Route path="/about" element={<About />} />
<Route path="/dev-tools" element={<DevTools />} /> <Route path="/dev-tools" element={<DevTools />} />
<Route path="/image-tools" element={<ImageTools />} /> <Route path="/image-tools" element={<ImageTools />} />
<Route path="/ai-products" element={<AIProduct />} /> <Route path="/ai-products" element={<AIProduct />} />
<Route path="/blog" element={<Blog />} /> <Route path="/blog" element={<Blog />} />
<Route path="/markdown-to-image" element={<MarkdownToImage />} /> <Route path="/markdown-to-image" element={<MarkdownToImage />} />
<Route path="/json-formatter" element={<JsonFormatter />} /> <Route path="/json-formatter" element={<JsonFormatter />} />
<Route path="/url-encode-and-decode" element={<UrlEnDecode />} /> <Route path="/url-encode-and-decode" element={<UrlEnDecode />} />
<Route path="/openai-timeline" element={<OpenAITimeline />} /> <Route path="/openai-timeline" element={<OpenAITimeline />} />
<Route path="/llm-model-price" element={<PricingCharts />} /> <Route path="/llm-model-price" element={<PricingCharts />} />
<Route path="/handwriting" element={<HandwriteGen />} /> <Route path="/handwriting" element={<HandwriteGen />} />
<Route path="/image-base64" element={<ImageBase64Converter />} /> <Route path="/image-base64" element={<ImageBase64Converter />} />
<Route path="/quote-card" element={<QuoteCard />} /> <Route path="/quote-card" element={<QuoteCard />} />
<Route path="/latex-to-image" element={<LatexToImage />} /> <Route path="/latex-to-image" element={<LatexToImage />} />
<Route path="/text-diff" element={<TextDiff />} /> <Route path="/text-diff" element={<TextDiff />} />
<Route path="/subtitle-to-image" element={<SubtitleGenerator />} /> <Route path="/subtitle-to-image" element={<SubtitleGenerator />} />
<Route path="/image-compressor" element={<ImageCompressor />} /> <Route path="/image-compressor" element={<ImageCompressor />} />
<Route path="/image-watermark" element={<ImageWatermark />} /> <Route path="/image-watermark" element={<ImageWatermark />} />
<Route path="/text-behind-image" element={<TextBehindImage />} /> <Route path="/text-behind-image" element={<TextBehindImage />} />
<Route path="/background-remover" element={<BackgroundRemover />} /> <Route path="/background-remover" element={<BackgroundRemover />} />
<Route path="/anthropic-timeline" element={<AnthropicTimeline />} /> <Route path="/anthropic-timeline" element={<AnthropicTimeline />} />
<Route path="*" element={<NotFound />} /> <Route path="*" element={<NotFound />} />
</Routes> </Routes>
</Suspense> </Suspense>
</main>- </main>-
</div>
</div> </div>
<Footer />
</div> </div>
); );
} }
......
...@@ -8,7 +8,7 @@ import SEO from './SEO'; ...@@ -8,7 +8,7 @@ import SEO from './SEO';
const Container = styled.div` const Container = styled.div`
min-height: 100vh; min-height: 100vh;
background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%); background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%);
padding: 2rem; padding: 4rem 2rem 2rem;
position: relative; position: relative;
&::before { &::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() { ...@@ -145,7 +145,7 @@ function HandwritingGenerator() {
const backgroundOffset = -(lineSpacing * fontSize - fontSize); const backgroundOffset = -(lineSpacing * fontSize - fontSize);
return ( return (
<div className="handwrite-container"> <div className="handwrite-container" style={{ paddingTop: '4rem' }}>
<Layout> <Layout>
<Sider width={300} className="site-layout-background"> <Sider width={300} className="site-layout-background">
<div className="settings-section"> <div className="settings-section">
......
This diff is collapsed.
...@@ -14,6 +14,7 @@ const ConverterContainer = styled(Container)` ...@@ -14,6 +14,7 @@ const ConverterContainer = styled(Container)`
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
border: 1px solid rgba(99, 102, 241, 0.1); border: 1px solid rgba(99, 102, 241, 0.1);
border-radius: 12px; border-radius: 12px;
padding-top: 4rem; // 添加顶部内边距
`; `;
const Section = styled.div` const Section = styled.div`
......
...@@ -8,7 +8,7 @@ import imageCompression from 'browser-image-compression'; ...@@ -8,7 +8,7 @@ import imageCompression from 'browser-image-compression';
const Container = styled.div` const Container = styled.div`
min-height: 100vh; min-height: 100vh;
background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%); background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%);
padding: 2rem; padding: 4rem 2rem 2rem;
position: relative; position: relative;
&::before { &::before {
......
...@@ -7,7 +7,7 @@ import SEO from './SEO'; ...@@ -7,7 +7,7 @@ import SEO from './SEO';
const Container = styled.div` const Container = styled.div`
min-height: 100vh; min-height: 100vh;
background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%); background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%);
padding: 2rem; padding: 4rem 2rem 2rem;
position: relative; position: relative;
&::before { &::before {
......
This diff is collapsed.
...@@ -32,18 +32,50 @@ function LanguageSelector() { ...@@ -32,18 +32,50 @@ function LanguageSelector() {
}, []); }, []);
return ( return (
<div className="language-selector"> <div className="relative" ref={dropdownRef}>
<button onClick={() => setIsOpen(!isOpen)} className="language-button"> <button
{languages[lang]} 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> </button>
{isOpen && ( {isOpen && (
<ul className="language-dropdown" ref={dropdownRef}> <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">
{Object.entries(languages).map(([code, name]) => ( <div className="py-1">
<li key={code} onClick={() => handleLanguageChange(code)}> {Object.entries(languages).map(([code, name]) => (
{name} <button
</li> key={code}
))} onClick={() => handleLanguageChange(code)}
</ul> 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> </div>
); );
......
...@@ -11,7 +11,7 @@ import html2canvas from 'html2canvas'; ...@@ -11,7 +11,7 @@ import html2canvas from 'html2canvas';
const Container = styled.div` const Container = styled.div`
min-height: 100vh; min-height: 100vh;
background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%); background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%);
padding: 2rem; padding: 4rem 2rem 2rem;
position: relative; position: relative;
&::before { &::before {
......
This diff is collapsed.
...@@ -30,7 +30,7 @@ const backgroundOptions = [ ...@@ -30,7 +30,7 @@ const backgroundOptions = [
const Container = styled.div` const Container = styled.div`
min-height: 100vh; min-height: 100vh;
background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%); background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%);
padding: 2rem; padding: 4rem 2rem 2rem;
position: relative; position: relative;
&::before { &::before {
......
...@@ -247,9 +247,21 @@ useEffect(() => { ...@@ -247,9 +247,21 @@ useEffect(() => {
const Container = styled.div` const Container = styled.div`
display: flex; display: flex;
gap: 2rem; gap: 2rem;
padding: 2rem; padding: 4rem 2rem 2rem;
background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%); background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%);
min-height: 100vh; 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) { @media (max-width: 768px) {
flex-direction: column; flex-direction: column;
......
...@@ -8,7 +8,7 @@ import '../styles/fonts.css'; ...@@ -8,7 +8,7 @@ import '../styles/fonts.css';
const Container = styled.div` const Container = styled.div`
min-height: 100vh; min-height: 100vh;
background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%); background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%);
padding: 2rem; padding: 4rem 2rem 2rem;
position: relative; position: relative;
&::before { &::before {
......
...@@ -6,7 +6,7 @@ import SEO from './SEO'; ...@@ -6,7 +6,7 @@ import SEO from './SEO';
const Container = styled.div` const Container = styled.div`
min-height: 100vh; min-height: 100vh;
background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%); background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%);
padding: 2rem; padding: 4rem 2rem 2rem;
position: relative; position: relative;
&::before { &::before {
......
import React, { useState, useCallback } from 'react'; 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 { useTranslation } from '../js/i18n';
import SEO from './SEO'; import SEO from './SEO';
import styled from 'styled-components';
const EncoderDecoderContainer = styled(Container)` // 复用相同的样式组件
flex-direction: column; const Container = styled.div`
gap: 16px; min-height: 100vh;
`; background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%);
padding: 4rem 2rem 2rem;
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`
position: relative; 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` const ContentWrapper = styled.div`
background: rgba(255, 255, 255, 0.8); max-width: 1400px;
backdrop-filter: blur(10px); margin: 0 auto;
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;
position: relative; position: relative;
z-index: 1;
`; `;
const ActionButton = styled.button` const Title = styled.h2`
position: absolute; font-size: 1.8rem;
top: 12px; margin-bottom: 1.5rem;
right: 12px; background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
background: rgba(99, 102, 241, 0.1); -webkit-background-clip: text;
border: none; -webkit-text-fill-color: transparent;
border-radius: 6px; font-weight: 700;
padding: 6px 12px; letter-spacing: -0.02em;
cursor: pointer; text-align: center;
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;
}
`; `;
function UrlEncoderDecoder() { function UrlEncoderDecoder() {
...@@ -115,11 +48,10 @@ function UrlEncoderDecoder() { ...@@ -115,11 +48,10 @@ function UrlEncoderDecoder() {
const [input, setInput] = useState(''); const [input, setInput] = useState('');
const [resultText, setResultText] = useState(''); const [resultText, setResultText] = useState('');
const [isCopied, setIsCopied] = useState(false); const [isCopied, setIsCopied] = useState(false);
const [mode, setMode] = useState('decode'); // 'encode' 或 'decode' const [mode, setMode] = useState('decode');
const handleModeChange = (e) => { const handleModeChange = (e) => {
setMode(e.target.value); setMode(e.target.value);
// 当模式切换时,清空输入和输出
setInput(''); setInput('');
setResultText(''); setResultText('');
}; };
...@@ -153,53 +85,81 @@ function UrlEncoderDecoder() { ...@@ -153,53 +85,81 @@ function UrlEncoderDecoder() {
title={t('tools.urlEncodeDecode.title')} title={t('tools.urlEncodeDecode.title')}
description={t('tools.urlEncodeDecode.description')} description={t('tools.urlEncodeDecode.description')}
/> />
<Wrapper> <Container>
<Title>{t('tools.urlEncodeDecode.title')}</Title> <ContentWrapper>
<EncoderDecoderContainer> <Title>{t('tools.urlEncodeDecode.title')}</Title>
<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>
<div> <div className="flex flex-col gap-6">
<Label> <div className="space-y-2">
{mode === 'decode' ? t('tools.urlDecode.inputLabel') : t('tools.urlEncode.inputLabel')} <label className="block text-sm font-medium text-gray-700">
</Label> {t('tools.urlEncodeDecode.modeLabel')}
<StyledInputText </label>
value={input} <select
onChange={handleInputChange} value={mode}
placeholder={mode === 'decode' ? t('tools.urlDecode.inputLabel') : t('tools.urlEncode.inputLabel')} onChange={handleModeChange}
/> className="w-full sm:w-48 px-3 py-2 bg-white/80 backdrop-blur-sm border border-indigo-100 rounded-xl
</div> focus:ring-4 focus:ring-indigo-100 focus:border-indigo-300 focus:outline-none
text-sm text-gray-700 transition duration-300"
<div>
<Label>
{mode === 'decode' ? t('tools.urlDecode.resultLabel') : t('tools.urlEncode.resultLabel')}
</Label>
<ResultContainer>
<StyledPreview>{resultText}</StyledPreview>
<ActionButton
onClick={handleCopy}
className={isCopied ? 'active' : ''}
> >
{isCopied ? ( <option value="encode">{t('tools.urlEncodeDecode.encode')}</option>
<svg viewBox="0 0 24 24" fill="currentColor"> <option value="decode">{t('tools.urlEncodeDecode.decode')}</option>
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/> </select>
</svg> </div>
) : (
<svg viewBox="0 0 24 24" fill="currentColor"> <div className="flex flex-col lg:flex-row gap-4 lg:gap-6">
<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"/> <div className="w-full lg:w-1/2 space-y-2">
</svg> <label className="block text-sm font-medium text-gray-700">
)} {mode === 'decode' ? t('tools.urlDecode.inputLabel') : t('tools.urlEncode.inputLabel')}
{isCopied ? t('tools.jsonFormatter.copiedMessage') : t('tools.jsonFormatter.copyButton')} </label>
</ActionButton> <textarea
</ResultContainer> 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> </div>
</EncoderDecoderContainer> </ContentWrapper>
</Wrapper> </Container>
</> </>
); );
} }
......
...@@ -34,15 +34,29 @@ export const Container = styled.div` ...@@ -34,15 +34,29 @@ export const Container = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
background-color: white; min-height: 100vh;
border-radius: 10px; background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); padding: 4rem 2rem 2rem;
overflow: hidden; position: relative;
margin: 10px auto;
&::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) { @media (min-width: 768px) {
flex-direction: row; flex-direction: row;
height: 70vh; height: 100vh;
} }
`; `;
......
...@@ -5,8 +5,8 @@ ...@@ -5,8 +5,8 @@
"selectTemplate": "选择模板", "selectTemplate": "选择模板",
"inputLabel": "输入文本 (支持 Markdown)", "inputLabel": "输入文本 (支持 Markdown)",
"placeholder": "# 标题\n## 子标题\n- 列表项\n**粗体** *斜体*", "placeholder": "# 标题\n## 子标题\n- 列表项\n**粗体** *斜体*",
"downloadButton": "生成图片", "downloadButton": "导出图片",
"previewDefault": "# 预览区域\n输入文本后在这里预览效果", "previewDefault": "输入文本后在这里预览效果",
"templates": { "templates": {
"simple": "简约", "simple": "简约",
"ai-style": "AI风格", "ai-style": "AI风格",
...@@ -56,8 +56,11 @@ ...@@ -56,8 +56,11 @@
"description": "美化和验证 JSON 数据", "description": "美化和验证 JSON 数据",
"inputPlaceholder": "输入 JSON 数据", "inputPlaceholder": "输入 JSON 数据",
"invalidJson": "无效的 JSON", "invalidJson": "无效的 JSON",
"copyButton": "复制", "emptyInput": "",
"copiedMessage": "已复制" "copy": "复制",
"copied": "已复制",
"compress": "压缩",
"expand": "展开"
}, },
"urlEncodeDecode": { "urlEncodeDecode": {
"title": "URL 编码/解码", "title": "URL 编码/解码",
......
...@@ -251,47 +251,47 @@ const AIProduct = () => { ...@@ -251,47 +251,47 @@ const AIProduct = () => {
title={t('ai-products.title')} title={t('ai-products.title')}
description={t('ai-products.description')} description={t('ai-products.description')}
/> />
<main> <main className="container mx-auto px-4 pt-16 pb-8">
<section className="tools-section"> <section className="mt-8">
{Object.keys(groupedTools).map(category => ( {Object.keys(groupedTools).map(category => (
<div key={category} className="category-group"> <div key={category} className="mb-8">
<h2 className="category-title">{t(`categories.${category}`)}</h2> <h2 className="text-2xl font-semibold mb-4 px-4 text-gray-800">{t(`categories.${category}`)}</h2>
<div className="tools-grid"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 px-4">
{groupedTools[category].map(tool => ( {groupedTools[category].map(tool => (
tool.external ? ( tool.external ? (
<a <a
href={tool.path} href={tool.path}
key={tool.id} 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" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<img <img
src={tool.icon} src={tool.icon}
alt={`${t(`aiproducts.${tool.id}.title`)} icon`} alt={`${t(`aiproducts.${tool.id}.title`)} icon`}
className="tool-icon" className="w-12 h-12 object-contain mr-4"
loading="lazy" loading="lazy"
/> />
<div className="tool-content"> <div className="flex-1 min-w-0">
<h3 className="tool-title">{t(`aiproducts.${tool.id}.title`)}</h3> <h3 className="text-lg font-semibold mb-1 text-gray-800">{t(`aiproducts.${tool.id}.title`)}</h3>
<p className="tool-description">{t(`aiproducts.${tool.id}.description`)}</p> <p className="text-sm text-gray-600 line-clamp-2">{t(`aiproducts.${tool.id}.description`)}</p>
</div> </div>
</a> </a>
) : ( ) : (
<Link <Link
to={tool.path} to={tool.path}
key={tool.id} 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 <img
src={tool.icon} src={tool.icon}
alt={`${t(`tools.${tool.id}.title`)} icon`} alt={`${t(`tools.${tool.id}.title`)} icon`}
className="tool-icon" className="w-12 h-12 object-contain mr-4"
loading="lazy" loading="lazy"
/> />
<div className="tool-content"> <div className="flex-1 min-w-0">
<h3 className="tool-title">{t(`tools.${tool.id}.title`)}</h3> <h3 className="text-lg font-semibold mb-1 text-gray-800">{t(`tools.${tool.id}.title`)}</h3>
<p className="tool-description">{t(`tools.${tool.id}.description`)}</p> <p className="text-sm text-gray-600 line-clamp-2">{t(`tools.${tool.id}.description`)}</p>
</div> </div>
</Link> </Link>
) )
......
...@@ -12,7 +12,7 @@ const About = () => { ...@@ -12,7 +12,7 @@ const About = () => {
title={t('about.title')} title={t('about.title')}
description={t('about.description')} description={t('about.description')}
/> />
<main> <main className="container mx-auto px-4 pt-16 pb-8">
<section className="about-section"> <section className="about-section">
<div className="about-header"> <div className="about-header">
<h1>{t('about.title')}</h1> <h1>{t('about.title')}</h1>
......
...@@ -18,20 +18,24 @@ const Home = () => { ...@@ -18,20 +18,24 @@ const Home = () => {
title={t('blog.title')} title={t('blog.title')}
description={t('blog.description')} description={t('blog.description')}
/> />
<main> <main className="container mx-auto px-4 pt-16 pb-8">
<section className="tools-section"> <section className="mt-8">
<div className="tools-grid"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 px-4">
{tools.map(tool => ( {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 <img
src={tool.icon} src={tool.icon}
alt={`${t(`tools.${tool.id}.title`)} icon`} alt={`${t(`tools.${tool.id}.title`)} icon`}
className="tool-icon" className="w-12 h-12 object-contain mr-4"
loading="lazy" loading="lazy"
/> />
<div className="tool-content"> <div className="flex-1 min-w-0">
<h3 className="tool-title">{t(`tools.${tool.id}.title`)}</h3> <h3 className="text-lg font-semibold mb-1 text-gray-800">{t(`tools.${tool.id}.title`)}</h3>
<p className="tool-description">{t(`tools.${tool.id}.description`)}</p> <p className="text-sm text-gray-600 line-clamp-2">{t(`tools.${tool.id}.description`)}</p>
</div> </div>
</Link> </Link>
))} ))}
......
...@@ -20,20 +20,24 @@ const DevTools = () => { ...@@ -20,20 +20,24 @@ const DevTools = () => {
title={t('dev-tools.title')} title={t('dev-tools.title')}
description={t('dev-tools.description')} description={t('dev-tools.description')}
/> />
<main> <main className="container mx-auto px-4 pt-16 pb-8">
<section className="tools-section"> <section className="mt-8">
<div className="tools-grid"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 px-4">
{tools.map(tool => ( {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 <img
src={tool.icon} src={tool.icon}
alt={`${t(`tools.${tool.id}.title`)} icon`} alt={`${t(`tools.${tool.id}.title`)} icon`}
className="tool-icon" className="w-12 h-12 object-contain mr-4"
loading="lazy" loading="lazy"
/> />
<div className="tool-content"> <div className="flex-1 min-w-0">
<h3 className="tool-title">{t(`tools.${tool.id}.title`)}</h3> <h3 className="text-lg font-semibold mb-1 text-gray-800">{t(`tools.${tool.id}.title`)}</h3>
<p className="tool-description">{t(`tools.${tool.id}.description`)}</p> <p className="text-sm text-gray-600 line-clamp-2">{t(`tools.${tool.id}.description`)}</p>
</div> </div>
</Link> </Link>
))} ))}
......
...@@ -12,17 +12,15 @@ const tools = [ ...@@ -12,17 +12,15 @@ const tools = [
{ id: 'imageWatermark', icon: '/assets/icon/image-watermark.png', path: '/image-watermark' }, { id: 'imageWatermark', icon: '/assets/icon/image-watermark.png', path: '/image-watermark' },
{ id: 'imageBackgroundRemover', icon: '/assets/icon/image-background-remover.png', path: '/background-remover' }, { 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: 'textBehindImage', icon: '/assets/icon/text-behind-image.png', path: '/text-behind-image' },
{ id: 'latex2image', icon: '/assets/icon/latex2image.png', path: '/latex-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: 'jsonFormatter', icon: '/assets/icon/json-format.png', path: '/json-formatter' },
{ id: 'urlEncodeDecode', icon: '/assets/icon/url-endecode.png', path: '/url-encode-and-decode' }, { 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: 'imageBase64Converter', icon: '/assets/icon/image-base64.png', path: '/image-base64' },
{ id: 'textDiff', icon: '/assets/icon/diff.png', path: '/text-diff' }, { id: 'textDiff', icon: '/assets/icon/diff.png', path: '/text-diff' },
{ id: 'openAITimeline', icon: '/assets/icon/openai_small.svg', path: '/openai-timeline' }, { id: 'openAITimeline', icon: '/assets/icon/openai_small.svg', path: '/openai-timeline' },
{ id: 'anthropicTimeline', icon: '/assets/icon/anthropic_small.svg', path: '/anthropic-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: '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 = () => { const Home = () => {
...@@ -30,31 +28,35 @@ const Home = () => { ...@@ -30,31 +28,35 @@ const Home = () => {
const renderToolLink = (tool) => { const renderToolLink = (tool) => {
const content = ( 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 <img
src={tool.icon} src={tool.icon}
alt={`${t(`tools.${tool.id}.title`)} 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" loading="lazy"
/> />
<div className="tool-content"> <div className="flex-1">
<h3 className="tool-title">{t(`tools.${tool.id}.title`)}</h3> <h3 className="text-lg font-semibold text-gray-800 mb-1 group-hover:text-indigo-600 transition-colors duration-300">
<p className="tool-description">{t(`tools.${tool.id}.description`)}</p> {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>
</> </div>
); );
return tool.external ? ( return tool.external ? (
<a <a
href={tool.path} href={tool.path}
className="tool-card" className="block"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
{content} {content}
</a> </a>
) : ( ) : (
<Link to={tool.path} className="tool-card"> <Link to={tool.path} className="block">
{content} {content}
</Link> </Link>
); );
...@@ -66,17 +68,83 @@ const Home = () => { ...@@ -66,17 +68,83 @@ const Home = () => {
title={t('title')} title={t('title')}
description={t('slogan')} description={t('slogan')}
/> />
<main> <main className="min-h-screen bg-gradient-to-br from-indigo-50/50 via-white to-indigo-50/50 pt-16">
<section className="tools-section"> {/* Hero Section */}
<div className="tools-grid"> <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 => ( {tools.map(tool => (
<React.Fragment key={tool.id}> <React.Fragment key={tool.id}>
{renderToolLink(tool)} {renderToolLink(tool)}
</React.Fragment> </React.Fragment>
))} ))}
</div> </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> </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 = [ ...@@ -18,51 +18,32 @@ const tools = [
const ImageTools = () => { const ImageTools = () => {
const { t } = useTranslation(); 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 ( return (
<> <>
<SEO <SEO
title={t('title')} title={t('title')}
description={t('slogan')} description={t('slogan')}
/> />
<main> <main className="container mx-auto px-4 pt-16 pb-8">
<section className="tools-section"> <section className="mt-8">
<div className="tools-grid"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 px-4">
{tools.map(tool => ( {tools.map(tool => (
<React.Fragment key={tool.id}> <Link
{renderToolLink(tool)} to={tool.path}
</React.Fragment> 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> </div>
</section> </section>
......
This diff is collapsed.
...@@ -187,7 +187,9 @@ ...@@ -187,7 +187,9 @@
} }
.pricing-charts-container { .pricing-charts-container {
padding: 2rem; padding: 4rem 2rem 2rem;
max-width: 1200px;
margin: 0 auto;
background: var(--bg-primary); background: var(--bg-primary);
min-height: 100vh; min-height: 100vh;
} }
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
line-height: 1.6; line-height: 1.6;
max-width: 1000px; max-width: 1000px;
margin: 0 auto; margin: 0 auto;
padding: 2rem; padding: 4rem 2rem 2rem;
position: relative; position: relative;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.9), rgba(255, 255, 255, 0.95)); background: linear-gradient(135deg, rgba(255, 255, 255, 0.9), rgba(255, 255, 255, 0.95));
min-height: 100vh; min-height: 100vh;
......
:root { @tailwind base;
--primary-color: #000; @tailwind components;
--secondary-color: #06c; @tailwind utilities;
--background-color: #fff; \ No newline at end of file
--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
/** @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