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,6 +32,7 @@ function App() { ...@@ -33,6 +32,7 @@ function App() {
return ( return (
<div className="app-container"> <div className="app-container">
<Header /> <Header />
<div className="pt-4">
<div className="content-wrapper"> <div className="content-wrapper">
<main> <main>
<Suspense fallback={<div>Loading...</div>}> <Suspense fallback={<div>Loading...</div>}>
...@@ -68,7 +68,7 @@ function App() { ...@@ -68,7 +68,7 @@ function App() {
</Suspense> </Suspense>
</main>- </main>-
</div> </div>
<Footer /> </div>
</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">
......
...@@ -3,7 +3,6 @@ import React, { useState, useEffect, useRef } from 'react'; ...@@ -3,7 +3,6 @@ import React, { useState, useEffect, useRef } from 'react';
import { NavLink, useNavigate } from 'react-router-dom'; import { NavLink, useNavigate } from 'react-router-dom';
import LanguageSelector from './LanguageSelector'; import LanguageSelector from './LanguageSelector';
import { useTranslation } from '../js/i18n'; import { useTranslation } from '../js/i18n';
import '../styles/Header.css';
import logo from '/assets/logo.png'; import logo from '/assets/logo.png';
function Header() { function Header() {
...@@ -45,81 +44,311 @@ function Header() { ...@@ -45,81 +44,311 @@ function Header() {
setMobileMenuOpen(false); setMobileMenuOpen(false);
}; };
// 添加键盘导航支持
const handleKeyDown = (e) => {
if (e.key === 'Escape') {
setMenuOpen(false);
setMobileMenuOpen(false);
}
};
// 添加焦点管理
useEffect(() => {
const handleFocusTrap = (e) => {
if (!mobileMenuOpen) return;
const mobileMenu = document.querySelector('[role="dialog"]');
const focusableElements = mobileMenu.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
} else if (!e.shiftKey && document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
};
document.addEventListener('keydown', handleKeyDown);
document.addEventListener('keydown', handleFocusTrap);
return () => {
document.removeEventListener('keydown', handleKeyDown);
document.removeEventListener('keydown', handleFocusTrap);
};
}, [mobileMenuOpen]);
// 处理滚动锁定
useEffect(() => {
if (mobileMenuOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
return () => {
document.body.style.overflow = '';
};
}, [mobileMenuOpen]);
return ( return (
<header> <header className="fixed top-0 left-0 right-0 z-50">
<nav> <div className="absolute inset-0 bg-white/70 backdrop-blur-lg border-b border-gray-100"></div>
<div className="logo-title-container">
<NavLink to="/" className="title no-underline" onClick={handleNavClick}> <nav
<img src={logo} alt="Logo" className="logo" /> className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"
role="navigation"
aria-label={t('navigation.main')}
>
<div className="flex justify-between items-center h-20">
<div className="flex items-center">
<div className="flex-shrink-0 mr-8">
<NavLink
to="/"
className="flex items-center space-x-3 group"
aria-label={t('navigation.home')}
>
<img
src={logo}
alt={t('logo.alt')}
className="w-10 h-10 object-contain transition transform group-hover:scale-105"
width="40"
height="40"
loading="eager"
/>
<span className="text-xl font-semibold bg-gradient-to-r from-indigo-500/90 to-indigo-600/80 bg-clip-text text-transparent">
{t('title')} {t('title')}
</span>
</NavLink> </NavLink>
</div> </div>
<button className="mobile-menu-button" onClick={toggleMobileMenu}> <div className="hidden md:flex items-center space-x-6">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <NavLink
{mobileMenuOpen ? ( to="/dev-tools"
<path d="M6 18L18 6M6 6l12 12" /> className={({isActive}) =>
) : ( `px-3 py-2 text-base font-medium transition-all duration-200 border-b-2 ${
<path d="M4 6h16M4 12h16M4 18h16" /> isActive
)} ? 'text-indigo-500 border-indigo-500'
</svg> : 'text-gray-600 border-transparent hover:text-indigo-500 hover:border-indigo-500'
</button> }`
}
<div className={`menu-container ${mobileMenuOpen ? 'mobile-menu-open' : ''}`}> >
<div className="menu-items">
<NavLink to="/dev-tools" onClick={handleNavClick}>
{t('dev-tools.title')} {t('dev-tools.title')}
</NavLink> </NavLink>
<NavLink to="/image-tools" onClick={handleNavClick}> <NavLink
to="/image-tools"
className={({isActive}) =>
`px-3 py-2 text-base font-medium transition-all duration-200 border-b-2 ${
isActive
? 'text-indigo-500 border-indigo-500'
: 'text-gray-600 border-transparent hover:text-indigo-500 hover:border-indigo-500'
}`
}
onClick={handleNavClick}
>
{t('image-tools.title')} {t('image-tools.title')}
</NavLink> </NavLink>
<NavLink to="/ai-products" onClick={handleNavClick}> <NavLink
to="/ai-products"
className={({isActive}) =>
`px-3 py-2 text-base font-medium transition-all duration-200 border-b-2 ${
isActive
? 'text-indigo-500 border-indigo-500'
: 'text-gray-600 border-transparent hover:text-indigo-500 hover:border-indigo-500'
}`
}
onClick={handleNavClick}
>
{t('ai-products.title')} {t('ai-products.title')}
</NavLink> </NavLink>
<NavLink to="/blog" onClick={handleNavClick}> <NavLink
to="/blog"
className={({isActive}) =>
`px-3 py-2 text-base font-medium transition-all duration-200 border-b-2 ${
isActive
? 'text-indigo-500 border-indigo-500'
: 'text-gray-600 border-transparent hover:text-indigo-500 hover:border-indigo-500'
}`
}
onClick={handleNavClick}
>
{t('blog.title')} {t('blog.title')}
</NavLink> </NavLink>
</div> </div>
<div className="right-container"> </div>
<div className="flex items-center space-x-4">
<LanguageSelector /> <LanguageSelector />
<div className="auth-container">
{user ? ( {user ? (
<div className="user-info"> <div className="relative" ref={menuRef}>
<div className="avatar-container" ref={menuRef}> <button
onClick={toggleMenu}
className="flex items-center space-x-3 focus:outline-none"
>
<img <img
src={user.picture} src={user.picture}
alt="User Avatar" alt="User Avatar"
className="avatar" className="w-8 h-8 sm:w-10 sm:h-10 rounded-full ring-2 ring-offset-2 ring-indigo-500 transition transform hover:scale-105"
onClick={toggleMenu}
/> />
<div className={`dropdown-menu ${menuOpen ? 'active' : ''}`}> </button>
<button onClick={handleLogout}>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> {menuOpen && (
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" /> <div className="absolute right-0 mt-2 w-48 rounded-lg shadow-lg bg-white ring-1 ring-black ring-opacity-5 py-1 focus:outline-none">
<polyline points="16 17 21 12 16 7" /> <button
<line x1="21" y1="12" x2="9" y2="12" /> onClick={handleLogout}
</svg> className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-indigo-50 hover:text-indigo-600 transition-colors duration-150"
>
{t('logout')} {t('logout')}
</button> </button>
</div> </div>
)}
</div> </div>
) : (
<NavLink
to="/login"
className="hidden sm:inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-lg text-white bg-indigo-600 hover:bg-indigo-700 transition-colors duration-200 shadow-sm hover:shadow-md"
>
{t('login')}
</NavLink>
)}
</div> </div>
<button
className="md:hidden p-2 rounded-md text-gray-600 hover:text-gray-900 hover:bg-gray-100 transition-colors duration-200"
onClick={toggleMobileMenu}
aria-expanded={mobileMenuOpen}
aria-controls="mobile-menu"
aria-label={mobileMenuOpen ? t('navigation.close') : t('navigation.menu')}
>
<span className="sr-only">
{mobileMenuOpen ? t('navigation.close') : t('navigation.menu')}
</span>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
{mobileMenuOpen ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
) : ( ) : (
<NavLink to="/login" className="login-button"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> )}
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4" />
<polyline points="10 17 15 12 10 7" />
<line x1="15" y1="12" x2="3" y2="12" />
</svg> </svg>
</button>
<div
id="mobile-menu"
role="dialog"
aria-label={t('navigation.mobile_menu')}
aria-modal="true"
className={`md:hidden fixed inset-0 z-40 transform ${
mobileMenuOpen ? 'translate-x-0' : 'translate-x-full'
} transition-transform duration-300 ease-in-out`}
>
<div
className="fixed inset-0 bg-gray-600 bg-opacity-75 transition-opacity"
aria-hidden="true"
onClick={() => setMobileMenuOpen(false)}
/>
<nav
className="relative flex flex-col h-full w-full max-w-sm ml-auto bg-white shadow-xl"
role="navigation"
aria-label={t('navigation.mobile')}
>
<div className="flex items-center justify-between p-4 border-b border-gray-200">
<span className="text-lg font-semibold text-gray-800">{t('navigation.menu')}</span>
<button
onClick={() => setMobileMenuOpen(false)}
className="p-2 rounded-md text-gray-600 hover:text-gray-900 hover:bg-gray-100"
aria-label={t('navigation.close')}
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="flex-1 overflow-y-auto">
<div className="px-2 pt-2 pb-3 space-y-1">
<NavLink
to="/dev-tools"
className={({isActive}) =>
`block px-4 py-3 rounded-lg text-base font-medium transition-colors duration-200 ${
isActive
? 'bg-indigo-50 text-indigo-600'
: 'text-gray-700 hover:bg-gray-50 hover:text-indigo-600'
}`
}
onClick={() => setMobileMenuOpen(false)}
>
{t('dev-tools.title')}
</NavLink>
<NavLink
to="/image-tools"
className={({isActive}) =>
`block px-4 py-3 rounded-lg text-base font-medium transition-colors duration-200 ${
isActive
? 'bg-indigo-50 text-indigo-600'
: 'text-gray-700 hover:bg-gray-50 hover:text-indigo-600'
}`
}
onClick={() => setMobileMenuOpen(false)}
>
{t('image-tools.title')}
</NavLink>
<NavLink
to="/ai-products"
className={({isActive}) =>
`block px-4 py-3 rounded-lg text-base font-medium transition-colors duration-200 ${
isActive
? 'bg-indigo-50 text-indigo-600'
: 'text-gray-700 hover:bg-gray-50 hover:text-indigo-600'
}`
}
onClick={() => setMobileMenuOpen(false)}
>
{t('ai-products.title')}
</NavLink>
<NavLink
to="/blog"
className={({isActive}) =>
`block px-4 py-3 rounded-lg text-base font-medium transition-colors duration-200 ${
isActive
? 'bg-indigo-50 text-indigo-600'
: 'text-gray-700 hover:bg-gray-50 hover:text-indigo-600'
}`
}
onClick={() => setMobileMenuOpen(false)}
>
{t('blog.title')}
</NavLink>
</div>
</div>
<div className="border-t border-gray-200 p-4">
{!user && (
<NavLink
to="/login"
className="flex items-center justify-center px-4 py-2 border border-transparent text-base font-medium rounded-lg text-white bg-indigo-600 hover:bg-indigo-700 transition-colors duration-200 shadow-sm hover:shadow-md w-full"
onClick={() => setMobileMenuOpen(false)}
>
{t('login')} {t('login')}
</NavLink> </NavLink>
)} )}
</div> </div>
</nav>
</div> </div>
</div> </div>
</nav> </nav>
</header> </header>
); );
} }
export default Header; export default Header;
...@@ -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 {
......
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import styled from 'styled-components';
import { Title, Wrapper, Container, Preview } from '../js/SharedStyles';
import { useTranslation } from '../js/i18n'; import { useTranslation } from '../js/i18n';
import SEO from '../components/SEO'; import SEO from '../components/SEO';
import styled from 'styled-components';
const InputText = styled.textarea` const Container = styled.div`
width: 100%; min-height: 100vh;
height: 200px; background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%);
font-size: 15px; padding: 4rem 2rem 2rem;
padding: 16px; position: relative;
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);
}
@media (min-width: 768px) {
width: 40%;
height: 100%;
}
`;
const PreviewContainer = styled.div`
width: 100%;
display: flex;
flex-direction: column;
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;
box-sizing: border-box;
@media (min-width: 768px) {
width: 58%;
height: 100%;
}
`;
const ButtonGroup = styled.div` &::before {
display: flex; content: '';
gap: 8px;
position: absolute; position: absolute;
top: 12px; top: 0;
right: 12px; left: 0;
`; right: 0;
bottom: 0;
const ActionButton = styled.button` background:
background: rgba(99, 102, 241, 0.1); linear-gradient(90deg, rgba(99, 102, 241, 0.05) 1px, transparent 1px),
border: none; linear-gradient(rgba(99, 102, 241, 0.05) 1px, transparent 1px);
border-radius: 6px; background-size: 20px 20px;
padding: 6px 12px; pointer-events: none;
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 RelativePreviewContainer = styled(PreviewContainer)` const ContentWrapper = styled.div`
max-width: 1400px;
margin: 0 auto;
position: relative; position: relative;
z-index: 1;
`; `;
const ToggleButton = styled.button` const Title = styled.h2`
background: none; font-size: 1.8rem;
border: none; margin-bottom: 1.5rem;
cursor: pointer; background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
color: #6366F1; -webkit-background-clip: text;
font-size: 13px; -webkit-text-fill-color: transparent;
padding: 2px 6px; font-weight: 700;
margin-right: 6px; letter-spacing: -0.02em;
border-radius: 4px; text-align: center;
display: inline-flex;
align-items: center;
transition: all 0.2s ease;
&:hover {
background: rgba(99, 102, 241, 0.1);
}
svg {
width: 16px;
height: 16px;
}
`;
const JsonList = styled.ul`
list-style-type: none;
padding-left: 24px;
margin: 0;
font-size: 15px;
line-height: 1.6;
`;
const Key = styled.span`
color: #6366F1;
font-weight: 500;
font-size: 15px;
`;
const Value = styled.span`
color: #374151;
font-size: 15px;
&:not(:last-child) {
margin-right: 4px;
}
`; `;
function JsonFormatter() { function JsonFormatter() {
...@@ -142,8 +51,12 @@ function JsonFormatter() { ...@@ -142,8 +51,12 @@ function JsonFormatter() {
useEffect(() => { useEffect(() => {
try { try {
if (input.trim()) {
const parsed = JSON.parse(input); const parsed = JSON.parse(input);
setParsedJson(parsed); setParsedJson(parsed);
} else {
setParsedJson(null);
}
} catch (error) { } catch (error) {
setParsedJson(null); setParsedJson(null);
} }
...@@ -171,135 +84,208 @@ function JsonFormatter() { ...@@ -171,135 +84,208 @@ function JsonFormatter() {
title={t('tools.jsonFormatter.title')} title={t('tools.jsonFormatter.title')}
description={t('tools.jsonFormatter.description')} description={t('tools.jsonFormatter.description')}
/> />
<Wrapper>
<Title>{t('tools.jsonFormatter.title')}</Title>
<Container> <Container>
<InputText <ContentWrapper>
<Title>{t('tools.jsonFormatter.title')}</Title>
<div className="flex flex-col lg:flex-row gap-4 lg:gap-6 h-[calc(100vh-220px)]">
<textarea
className="w-full lg:w-5/12 p-4 text-sm font-mono border border-indigo-100 rounded-xl bg-white/80 backdrop-blur-sm focus:border-indigo-300 focus:ring-4 focus:ring-indigo-100 outline-none resize-none transition duration-300"
placeholder={t('tools.jsonFormatter.inputPlaceholder')} placeholder={t('tools.jsonFormatter.inputPlaceholder')}
value={input} value={input}
onChange={(e) => setInput(e.target.value)} onChange={(e) => setInput(e.target.value)}
/> />
<RelativePreviewContainer>
<div className="w-full lg:w-7/12 relative border border-indigo-100 rounded-xl bg-white/80 backdrop-blur-sm p-4">
{parsedJson ? ( {parsedJson ? (
<> <>
<Preview> <div className="font-mono text-sm leading-relaxed overflow-auto">
{isCompressed ? ( {isCompressed ? (
<pre style={{ margin: 0, whiteSpace: 'nowrap', overflowX: 'auto' }}> <pre className="m-0 whitespace-nowrap">
{JSON.stringify(parsedJson)} {JSON.stringify(parsedJson)}
</pre> </pre>
) : ( ) : (
<JsonView data={parsedJson} /> <JsonView data={parsedJson} />
)} )}
</Preview> </div>
<ButtonGroup> <div className="absolute top-4 right-4 flex gap-2">
<ActionButton <button
onClick={toggleCompression} onClick={toggleCompression}
className={isCompressed ? 'active' : ''} className={`
flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-lg transition-all duration-200
${isCompressed
? 'bg-indigo-100 text-indigo-700'
: 'bg-white/50 hover:bg-indigo-50 text-gray-600 hover:text-indigo-600'
}
`}
> >
{isCompressed ? ( {isCompressed ? (
<svg viewBox="0 0 24 24" fill="currentColor"> <>
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M4 9h16v2H4V9zm0 4h16v2H4v-2z"/> <path d="M4 9h16v2H4V9zm0 4h16v2H4v-2z"/>
</svg> </svg>
{t('tools.jsonFormatter.expand')}
</>
) : ( ) : (
<svg viewBox="0 0 24 24" fill="currentColor"> <>
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 13H5v-2h14v2z"/> <path d="M19 13H5v-2h14v2z"/>
</svg> </svg>
{t('tools.jsonFormatter.compress')}
</>
)} )}
{isCompressed ? '展开' : '压缩'} </button>
</ActionButton> <button
<ActionButton onClick={handleCopy} className={isCopied ? 'active' : ''}> onClick={handleCopy}
className={`
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 ? ( {isCopied ? (
<svg viewBox="0 0 24 24" fill="currentColor"> <>
<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"/> <path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
</svg> </svg>
{t('tools.jsonFormatter.copied')}
</>
) : ( ) : (
<svg viewBox="0 0 24 24" fill="currentColor"> <>
<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"/> <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> </svg>
{t('tools.jsonFormatter.copy')}
</>
)} )}
{isCopied ? '已复制' : '复制'} </button>
</ActionButton> </div>
</ButtonGroup>
</> </>
) : ( ) : (
<Preview>{t('tools.jsonFormatter.invalidJson')}</Preview> <div className="p-4 text-gray-500">
{input.trim() ? t('tools.jsonFormatter.invalidJson') : t('tools.jsonFormatter.emptyInput')}
</div>
)} )}
</RelativePreviewContainer> </div>
</div>
</ContentWrapper>
</Container> </Container>
</Wrapper>
</> </>
); );
} }
function JsonView({ data }) { function JsonView({ data }) {
const [isExpanded, setIsExpanded] = useState(true); if (data === null || data === undefined) return null;
const [collapsedKeys, setCollapsedKeys] = useState(new Set());
if (Array.isArray(data)) { const toggleCollapse = (key) => {
const newCollapsedKeys = new Set(collapsedKeys);
if (newCollapsedKeys.has(key)) {
newCollapsedKeys.delete(key);
} else {
newCollapsedKeys.add(key);
}
setCollapsedKeys(newCollapsedKeys);
};
const renderCollapsibleValue = (value, path = '') => {
if (value === null) return <span className="text-indigo-400">null</span>;
if (typeof value === 'boolean') return <span className="text-indigo-400">{value.toString()}</span>;
if (typeof value === 'number') return <span className="text-emerald-500">{value}</span>;
if (typeof value === 'string') return <span className="text-amber-500">"{value}"</span>;
const isCollapsed = collapsedKeys.has(path);
const hasChildren = Array.isArray(value) || (typeof value === 'object' && value !== null);
if (Array.isArray(value)) {
if (isCollapsed) {
return ( return (
<div> <div className="inline-flex items-center gap-1">
<ToggleButton onClick={() => setIsExpanded(!isExpanded)}> <button
{isExpanded ? ( onClick={() => toggleCollapse(path)}
<svg viewBox="0 0 24 24" fill="currentColor"> className="w-4 h-4 inline-flex items-center justify-center text-gray-500 hover:text-gray-700"
<path d="M19 13H5v-2h14v2z"/> >
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg> </svg>
) : ( </button>
<svg viewBox="0 0 24 24" fill="currentColor"> <span className="text-gray-500">[...]</span>
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/> <span className="text-gray-400 text-sm ml-1">({value.length} items)</span>
</div>
);
}
return (
<div className="ml-5">
<div className="inline-flex items-center gap-1">
<button
onClick={() => toggleCollapse(path)}
className="w-4 h-4 inline-flex items-center justify-center text-gray-500 hover:text-gray-700"
>
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg> </svg>
)} </button>
</ToggleButton>
{!isExpanded ? (
<span style={{ color: '#6B7280', fontSize: '15px' }}>Array [{data.length}]</span>
) : (
<JsonList>
[ [
{data.map((item, index) => ( </div>
<li key={index}> {value.map((item, index) => (
<JsonView data={item} /> <div key={index} className="ml-5">
{index < data.length - 1 && ','} {renderCollapsibleValue(item, `${path}[${index}]`)}
</li> {index < value.length - 1 ? ',' : ''}
</div>
))} ))}
] <div>]</div>
</JsonList>
)}
</div> </div>
); );
} else if (typeof data === 'object' && data !== null) { }
const entries = Object.entries(data);
if (typeof value === 'object') {
const entries = Object.entries(value);
if (isCollapsed) {
return ( return (
<div> <div className="inline-flex items-center gap-1">
<ToggleButton onClick={() => setIsExpanded(!isExpanded)}> <button
{isExpanded ? ( onClick={() => toggleCollapse(path)}
<svg viewBox="0 0 24 24" fill="currentColor"> className="w-4 h-4 inline-flex items-center justify-center text-gray-500 hover:text-gray-700"
<path d="M19 13H5v-2h14v2z"/> >
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg> </svg>
) : ( </button>
<svg viewBox="0 0 24 24" fill="currentColor"> <span className="text-gray-500">{'{...}'}</span>
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/> <span className="text-gray-400 text-sm ml-1">({entries.length} properties)</span>
</div>
);
}
return (
<div className="ml-5">
<div className="inline-flex items-center gap-1">
<button
onClick={() => toggleCollapse(path)}
className="w-4 h-4 inline-flex items-center justify-center text-gray-500 hover:text-gray-700"
>
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg> </svg>
)} </button>
</ToggleButton>
{!isExpanded ? (
<span style={{ color: '#6B7280', fontSize: '15px' }}>Object {`{${entries.length}}`}</span>
) : (
<JsonList>
{'{'} {'{'}
{entries.map(([key, value], index) => ( </div>
<li key={key}> {entries.map(([key, val], index) => (
<Key>"{key}"</Key>: <JsonView data={value} /> <div key={key} className="ml-5">
{index < entries.length - 1 && ','} <span className="text-pink-500">"{key}"</span>: {renderCollapsibleValue(val, `${path}.${key}`)}
</li> {index < entries.length - 1 ? ',' : ''}
</div>
))} ))}
{'}'} <div>{'}'}</div>
</JsonList>
)}
</div> </div>
); );
} else if (typeof data === 'string') {
return <Value>"{data}"</Value>;
} else {
return <Value>{JSON.stringify(data)}</Value>;
} }
return value;
};
return renderCollapsibleValue(data, 'root');
} }
export default JsonFormatter; export default JsonFormatter;
...@@ -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">
<div className="py-1">
{Object.entries(languages).map(([code, name]) => ( {Object.entries(languages).map(([code, name]) => (
<li key={code} onClick={() => handleLanguageChange(code)}> <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} {name}
</li> </button>
))} ))}
</ul> </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 {
......
...@@ -3,7 +3,7 @@ import styled from 'styled-components'; ...@@ -3,7 +3,7 @@ import styled from 'styled-components';
import { marked } from 'marked'; import { marked } from 'marked';
import { useTranslation } from '../js/i18n'; import { useTranslation } from '../js/i18n';
import SEO from './SEO'; import SEO from './SEO';
import Marked from 'marked-react'; import DOMPurify from 'dompurify';
// 更新预设模板 // 更新预设模板
const templates = [ const templates = [
...@@ -54,7 +54,7 @@ const templates = [ ...@@ -54,7 +54,7 @@ const templates = [
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 {
...@@ -114,6 +114,16 @@ const Section = styled.div` ...@@ -114,6 +114,16 @@ const Section = styled.div`
gap: 0.5rem; gap: 0.5rem;
`; `;
const TemplateSection = styled(Section)`
margin-bottom: 1rem;
`;
const EditorSection = styled(Section)`
flex: 1;
display: flex;
flex-direction: column;
`;
const Label = styled.label` const Label = styled.label`
font-size: 1rem; font-size: 1rem;
color: #333333; color: #333333;
...@@ -158,77 +168,141 @@ const TemplateItem = styled.button` ...@@ -158,77 +168,141 @@ const TemplateItem = styled.button`
`} `}
`; `;
const MarkdownEditor = styled.textarea` const Editor = styled.textarea`
width: 100%; width: 100%;
height: 100px; height: 500px;
padding: 0.5rem; padding: 1rem;
border: 1px solid #dadce0; border: none;
border-radius: 4px; background: transparent;
font-size: 16px; font-family: 'SF Mono', monospace;
color: #333333; font-size: 14px;
line-height: 1.5;
resize: vertical; resize: vertical;
color: #1a1a1a;
overflow-y: auto;
&:focus {
outline: none;
}
&::placeholder {
color: #64748b;
}
`; `;
const DownloadButton = styled.button` const DownloadButton = styled.button`
background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%); background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
color: white; color: white;
padding: 0.5rem 1rem;
border: none; border: none;
padding: 1rem; border-radius: 6px;
border-radius: 8px;
font-weight: 600;
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; font-weight: 600;
align-self: flex-end; transition: opacity 0.2s;
margin-top: 1rem; font-size: 0.9rem;
position: absolute;
top: 1.5rem;
right: 1.5rem;
opacity: ${props => props.visible ? 1 : 0};
pointer-events: ${props => props.visible ? 'auto' : 'none'};
&:hover { &:hover {
transform: translateY(-2px); opacity: 0.9;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2);
} }
`; `;
const PreviewContainer = styled.div` const PreviewContainer = styled(InputContainer)`
flex: 1; overflow: auto;
background: ${(props) => props.bgColor || '#ffffff'}; position: relative;
padding: 2rem; min-height: 400px;
border-radius: 16px; display: block;
box-shadow: 0 8px 32px rgba(99, 102, 241, 0.1);
border: 1px solid rgba(99, 102, 241, 0.1); img {
display: flex; max-width: 100%;
flex-direction: column; height: auto;
justify-content: center; display: block;
align-items: center; margin: 1em auto;
min-height: 200px; }
height: fit-content;
align-self: flex-start;
transition: all 0.3s ease;
margin-top: 1rem;
width: 100%;
&:hover { h1, h2, h3, h4, h5, h6 {
transform: translateY(-5px); margin-top: 1.5em;
box-shadow: 0 12px 24px rgba(99, 102, 241, 0.15); margin-bottom: 0.5em;
font-weight: 600;
line-height: 1.3;
} }
@media (max-width: 768px) { p {
margin-top: 2rem; margin: 1em 0;
line-height: 1.6;
}
ul, ol {
margin: 1em 0;
padding-left: 2em;
}
ul {
list-style-type: disc;
}
ol {
list-style-type: decimal;
}
li {
margin: 0.5em 0;
line-height: 1.6;
}
pre, code {
background: rgba(0, 0, 0, 0.05);
border-radius: 4px;
padding: 0.2em 0.4em;
font-family: 'SF Mono', monospace;
}
pre code {
background: none;
padding: 0;
}
blockquote {
border-left: 4px solid #e2e8f0;
margin: 1em 0;
padding-left: 1em;
color: #64748b;
}
table {
border-collapse: collapse;
width: 100%; width: 100%;
margin: 1em 0;
}
th, td {
border: 1px solid #e2e8f0;
padding: 0.5em;
text-align: left;
}
th {
background: rgba(0, 0, 0, 0.05);
} }
`; `;
const Preview = styled.div` const Preview = styled.div`
font-size: clamp(16px, 2.5vw, 24px); font-family: ${(props) => props.$font || '-apple-system, system-ui, sans-serif'};
margin-bottom: 16px; font-size: 16px;
color: ${(props) => props.$color || '#333333'}; line-height: 1.6;
text-align: center; color: ${(props) => props.$color || '#1a1a1a'};
line-height: 1.5; background: ${(props) => props.$background || '#ffffff'};
font-family: ${(props) => props.$fontFamily || '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'}; padding: ${(props) => props.$padding || '40px'};
white-space: pre-wrap; border-radius: 8px;
overflow-wrap: break-word;
word-wrap: break-word; word-wrap: break-word;
width: 100%; hyphens: auto;
`; `;
function TextToImage() { function TextToImage() {
const { t } = useTranslation(); const { t } = useTranslation();
const [text, setText] = useState(''); const [text, setText] = useState('');
...@@ -238,42 +312,111 @@ function TextToImage() { ...@@ -238,42 +312,111 @@ function TextToImage() {
const formatText = (text) => { const formatText = (text) => {
return marked.parse(text, { return marked.parse(text, {
breaks: true, breaks: true,
gfm: true gfm: true,
headerIds: false,
mangle: false,
// 自定义图片渲染
renderer: {
image(href, title, text) {
// 处理相对路径
if (href.startsWith('/')) {
href = window.location.origin + href;
}
return `<img src="${href}" alt="${text}" title="${title || ''}" style="max-width: 100%; height: auto;" crossorigin="anonymous" />`;
}
}
}); });
}; };
const handleDownload = async () => { const handleDownload = async () => {
const previewClone = previewRef.current.cloneNode(true); const previewElement = previewRef.current;
document.body.appendChild(previewClone); if (!previewElement) return;
previewClone.style.position = 'absolute';
previewClone.style.left = '-9999px';
previewClone.style.width = 'auto';
previewClone.style.maxWidth = '800px';
previewClone.style.height = 'auto';
previewClone.style.whiteSpace = 'pre-wrap';
previewClone.style.backgroundColor = 'white';
previewClone.style.padding = '40px';
try { try {
// 等待图片加载
const waitForImages = () => {
const images = previewElement.getElementsByTagName('img');
const promises = Array.from(images).map(img => {
if (img.complete) return Promise.resolve();
return new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
// 确保图片使用完整的 URL
if (img.src.startsWith('/')) {
img.src = window.location.origin + img.src;
}
// 添加跨域属性
img.crossOrigin = 'anonymous';
});
});
return Promise.all(promises);
};
await waitForImages();
// 等待渲染完成
await new Promise(resolve => setTimeout(resolve, 500));
const html2canvas = (await import('html2canvas')).default; const html2canvas = (await import('html2canvas')).default;
const canvas = await html2canvas(previewClone, { const canvas = await html2canvas(previewElement, {
backgroundColor: 'white', backgroundColor: selectedTemplate.bgColor,
scale: 2, scale: 2,
width: previewClone.offsetWidth, useCORS: true,
height: previewClone.offsetHeight allowTaint: false,
logging: false,
onclone: (clonedDoc) => {
const clonedElement = clonedDoc.querySelector('.markdown-content');
if (clonedElement) {
clonedElement.style.width = '100%';
clonedElement.style.position = 'relative';
clonedElement.style.transform = 'none';
clonedElement.style.transformOrigin = '0 0';
clonedElement.style.overflow = 'visible';
}
}
}); });
const link = document.createElement('a'); const link = document.createElement('a');
link.download = 'text_image.png'; link.download = 'markdown-preview.png';
link.href = canvas.toDataURL('image/png'); link.href = canvas.toDataURL('image/png');
link.click(); link.click();
} catch (error) { } catch (error) {
console.error('Failed to load html2canvas:', error); console.error('导出图片失败:', error);
} finally {
document.body.removeChild(previewClone);
} }
}; };
const renderPreview = () => {
// 配置 marked 选项
marked.setOptions({
gfm: true, // 启用 GitHub 风格的 Markdown
breaks: true, // 启用换行符转换为 <br>
headerIds: true,
mangle: false,
pedantic: false,
smartLists: true, // 优化列表输出
smartypants: true, // 优化标点符号
});
// 使用 DOMPurify 清理 HTML
const cleanHtml = DOMPurify.sanitize(marked(text), {
ADD_TAGS: ['img'],
ADD_ATTR: ['src', 'alt'],
});
return (
<div
ref={previewRef}
dangerouslySetInnerHTML={{ __html: cleanHtml }}
style={{
fontFamily: selectedTemplate.font,
color: selectedTemplate.textColor,
background: selectedTemplate.bgColor,
padding: selectedTemplate.padding,
minHeight: '100%',
}}
/>
);
};
return ( return (
<> <>
<SEO <SEO
...@@ -287,7 +430,7 @@ function TextToImage() { ...@@ -287,7 +430,7 @@ function TextToImage() {
<TitleLabel>{t('tools.markdown2image.title')}</TitleLabel> <TitleLabel>{t('tools.markdown2image.title')}</TitleLabel>
{/* 模板选择 */} {/* 模板选择 */}
<Section> <TemplateSection>
<Label>{t('tools.markdown2image.selectTemplate')}</Label> <Label>{t('tools.markdown2image.selectTemplate')}</Label>
<TemplateGrid> <TemplateGrid>
{templates.map(template => ( {templates.map(template => (
...@@ -302,39 +445,27 @@ function TextToImage() { ...@@ -302,39 +445,27 @@ function TextToImage() {
</TemplateItem> </TemplateItem>
))} ))}
</TemplateGrid> </TemplateGrid>
</Section> </TemplateSection>
{/* Markdown 编辑器 */} {/* Markdown 编辑器 */}
<Section> <EditorSection>
<Label>{t('tools.markdown2image.inputLabel')}</Label> <Label>{t('tools.markdown2image.inputLabel')}</Label>
<MarkdownEditor <Editor
value={text} value={text}
onChange={(e) => setText(e.target.value)} onChange={(e) => setText(e.target.value)}
placeholder={t('tools.markdown2image.placeholder')} placeholder={t('tools.markdown2image.placeholder')}
/> />
</Section> </EditorSection>
<DownloadButton onClick={handleDownload}>
{t('tools.markdown2image.downloadButton')}
</DownloadButton>
</InputContainer> </InputContainer>
<PreviewContainer <PreviewContainer>
ref={previewRef} <DownloadButton
bgColor={selectedTemplate.bgColor} onClick={handleDownload}
visible={text.length > 0}
> >
<div {t('tools.markdown2image.downloadButton')}
style={{ </DownloadButton>
padding: selectedTemplate.padding, {renderPreview()}
color: selectedTemplate.textColor,
fontFamily: selectedTemplate.font,
width: '100%'
}}
>
<Marked>
{text || t('tools.markdown2image.previewDefault')}
</Marked>
</div>
</PreviewContainer> </PreviewContainer>
</ContentWrapper> </ContentWrapper>
</Container> </Container>
......
...@@ -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` position: relative;
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 { &::before {
border-color: rgba(99, 102, 241, 0.3); content: '';
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.1); position: absolute;
outline: none; 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 ResultContainer = styled.div` const ContentWrapper = styled.div`
max-width: 1400px;
margin: 0 auto;
position: relative; position: relative;
width: 100%; z-index: 1;
`; `;
const StyledPreview = styled.div` const Title = styled.h2`
background: rgba(255, 255, 255, 0.8); font-size: 1.8rem;
backdrop-filter: blur(10px); margin-bottom: 1.5rem;
border-radius: 12px; background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
border: 1px solid rgba(99, 102, 241, 0.1); -webkit-background-clip: text;
padding: 16px; -webkit-text-fill-color: transparent;
font-size: 15px; font-weight: 700;
color: #374151; letter-spacing: -0.02em;
min-height: 24px; text-align: center;
line-height: 1.5;
position: relative;
`;
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;
}
`; `;
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>
<ContentWrapper>
<Title>{t('tools.urlEncodeDecode.title')}</Title> <Title>{t('tools.urlEncodeDecode.title')}</Title>
<EncoderDecoderContainer>
<ModeSwitcher> <div className="flex flex-col gap-6">
<Label>{t('tools.urlEncodeDecode.modeLabel')}</Label> <div className="space-y-2">
<select value={mode} onChange={handleModeChange}> <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"
>
<option value="encode">{t('tools.urlEncodeDecode.encode')}</option> <option value="encode">{t('tools.urlEncodeDecode.encode')}</option>
<option value="decode">{t('tools.urlEncodeDecode.decode')}</option> <option value="decode">{t('tools.urlEncodeDecode.decode')}</option>
</select> </select>
</ModeSwitcher> </div>
<div> <div className="flex flex-col lg:flex-row gap-4 lg:gap-6">
<Label> <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')} {mode === 'decode' ? t('tools.urlDecode.inputLabel') : t('tools.urlEncode.inputLabel')}
</Label> </label>
<StyledInputText <textarea
value={input} value={input}
onChange={handleInputChange} onChange={handleInputChange}
placeholder={mode === 'decode' ? t('tools.urlDecode.inputLabel') : t('tools.urlEncode.inputLabel')} 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>
<div> <div className="w-full lg:w-1/2 space-y-2">
<Label> <label className="block text-sm font-medium text-gray-700">
{mode === 'decode' ? t('tools.urlDecode.resultLabel') : t('tools.urlEncode.resultLabel')} {mode === 'decode' ? t('tools.urlDecode.resultLabel') : t('tools.urlEncode.resultLabel')}
</Label> </label>
<ResultContainer> <div className="relative h-[calc(100vh-400px)]">
<StyledPreview>{resultText}</StyledPreview> <div className="h-full w-full px-4 py-3 bg-white/80 backdrop-blur-sm border border-indigo-100
<ActionButton rounded-xl text-sm font-mono text-gray-700 whitespace-pre-wrap break-all overflow-auto">
{resultText}
</div>
<button
onClick={handleCopy} onClick={handleCopy}
className={isCopied ? 'active' : ''} 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 ? ( {isCopied ? (
<svg viewBox="0 0 24 24" fill="currentColor"> <>
<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"/> <path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
</svg> </svg>
{t('tools.jsonFormatter.copied')}
</>
) : ( ) : (
<svg viewBox="0 0 24 24" fill="currentColor"> <>
<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"/> <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> </svg>
{t('tools.jsonFormatter.copy')}
</>
)} )}
{isCopied ? t('tools.jsonFormatter.copiedMessage') : t('tools.jsonFormatter.copyButton')} </button>
</ActionButton> </div>
</ResultContainer> </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) => { return (
const content = (
<> <>
<SEO
title={t('title')}
description={t('slogan')}
/>
<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="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>
</>
);
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> </Link>
);
};
return (
<>
<SEO
title={t('title')}
description={t('slogan')}
/>
<main>
<section className="tools-section">
<div className="tools-grid">
{tools.map(tool => (
<React.Fragment key={tool.id}>
{renderToolLink(tool)}
</React.Fragment>
))} ))}
</div> </div>
</section> </section>
......
.no-underline {
text-decoration: none;
color: inherit;
}
.no-underline:hover {
text-decoration: none;
}
.logo {
width: 40px;
height: 40px;
margin-right: 15px;
object-fit: contain;
}
.title {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', sans-serif;
font-size: 1.8rem;
font-weight: 600;
letter-spacing: 0.02em;
color: #6366F1; /* 使用与图片中紫色渐变相近的颜色 */
transition: color 0.2s ease;
}
.logo-title-container {
display: flex;
align-items: center;
animation: fadeIn 0.5s ease-in-out;
}
.logo-title-container .title {
display: flex;
align-items: center;
}
.logo-title-container:hover .title {
color: #4F46E5; /* 悬停时稍微深一点的紫色 */
}
/* 添加一个简单的动画效果 */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* 为移动设备优化 */
@media (max-width: 768px) {
.title {
font-size: 1.3rem;
}
}
header nav {
display: flex;
align-items: center;
padding: 10px 20px;
}
.menu-items {
display: flex;
align-items: center;
margin-left: 40px; /* 调整导航菜单与标题之间的距离 */
}
.menu-items a {
color: #4B5563;
text-decoration: none;
font-size: 1.2rem;
font-weight: 500;
padding: 0.5rem 0;
position: relative;
transition: color 0.3s ease;
}
.menu-items a:hover {
color: #4F46E5; /* 悬停时的颜色,稍深的紫色 */
}
.menu-items a.active {
color: #6366F1; /* 高亮颜色,与网站主题一致 */
font-weight: bold;
border-bottom: 2px solid #6366F1; /* 添加下划线高亮 */
}
.right-container {
display: flex;
align-items: center;
margin-left: auto; /* 将右侧容器推到最右边 */
}
.right-container > * {
margin-left: 15px;
}
.auth-container {
display: flex;
align-items: center;
}
.user-info {
display: flex;
align-items: center;
}
.user-info span {
margin-right: 10px;
}
.user-info button {
padding: 5px 10px;
cursor: pointer;
}
@media (max-width: 768px) {
.right-container {
flex-direction: column;
align-items: flex-end;
}
.right-container > * {
margin-left: 0;
margin-top: 10px;
}
}
/* 头像容器 */
.avatar-container {
position: relative;
display: inline-block;
}
/* 用户头像 */
.avatar {
width: 32px;
height: 32px;
border-radius: 50%;
cursor: pointer;
}
/* 下拉菜单 */
.dropdown-menu {
position: absolute;
top: 100%; /* 紧贴在头像下方 */
left: 50%;
transform: translateX(-50%);
margin-top: 5px; /* 可选:头像和菜单之间的间距 */
background-color: white;
border: 1px solid #ccc;
padding: 10px;
min-width: 100px; /* 可选:设置最小宽度 */
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: 1000;
}
/* 下拉菜单中的按钮 */
.dropdown-menu button {
background: none;
border: none;
cursor: pointer;
font-size: 16px;
color: #333;
}
.dropdown-menu button:hover {
color: #007bff;
}
/* 调整导航栏布局 */
header nav {
display: flex;
align-items: center;
padding: 10px 20px;
}
.right-container {
margin-left: auto; /* 将右侧容器推到最右边 */
display: flex;
align-items: center;
}
/* 导航栏基础样式 */
header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
header nav {
max-width: 1200px;
margin: 0 auto;
display: flex;
align-items: center;
padding: 0.8rem 1.5rem;
}
/* Logo和标题容器 */
.logo-title-container {
display: flex;
align-items: center;
gap: 0.8rem;
}
.logo {
width: 35px;
height: 35px;
transition: transform 0.3s ease;
}
.logo:hover {
transform: scale(1.05);
}
.title {
font-size: 1.8rem;
font-weight: 600;
background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
letter-spacing: 0.02em;
}
/* 导航菜单 */
.menu-items {
display: flex;
align-items: center;
margin-left: 3rem;
gap: 2rem;
}
.menu-items a {
color: #4B5563;
text-decoration: none;
font-size: 1.2rem;
font-weight: 500;
padding: 0.5rem 0;
position: relative;
transition: color 0.3s ease;
}
.menu-items a:hover {
color: #6366F1;
}
.menu-items a::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 0;
height: 2px;
background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
transition: width 0.3s ease;
}
.menu-items a:hover::after {
width: 100%;
}
.menu-items a.active {
color: #6366F1;
}
.menu-items a.active::after {
width: 100%;
}
/* 右侧容器 */
.right-container {
margin-left: auto;
display: flex;
align-items: center;
gap: 1.5rem;
}
/* 用户头像和下拉菜单 */
.avatar-container {
position: relative;
}
.avatar {
width: 35px;
height: 35px;
border-radius: 50%;
cursor: pointer;
border: 2px solid transparent;
transition: all 0.3s ease;
}
.avatar:hover {
border-color: #6366F1;
transform: scale(1.05);
}
.dropdown-menu {
position: absolute;
top: 120%;
right: 0;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 8px;
padding: 0.5rem;
min-width: 120px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
opacity: 0;
transform: translateY(-10px);
transition: all 0.3s ease;
pointer-events: none;
}
.dropdown-menu.active {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
.dropdown-menu button {
width: 100%;
padding: 0.6rem 1rem;
background: none;
border: none;
color: #4B5563;
font-size: 0.9rem;
text-align: left;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s ease;
}
.dropdown-menu button:hover {
background: rgba(99, 102, 241, 0.1);
color: #6366F1;
}
/* 响应式设计 */
@media (max-width: 768px) {
.menu-items {
display: none;
}
.right-container {
gap: 1rem;
}
}
/* 登录按钮样式 */
.auth-container .login-button {
background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
color: white;
padding: 8px 20px;
border-radius: 8px;
font-size: 0.95rem;
font-weight: 500;
text-decoration: none;
transition: all 0.3s ease;
border: none;
display: inline-flex;
align-items: center;
gap: 8px;
}
.auth-container .login-button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2);
background: linear-gradient(135deg, #4F46E5 0%, #4338CA 100%);
}
/* 头像和下拉菜单样式优化 */
.avatar-container {
position: relative;
cursor: pointer;
}
.avatar {
width: 35px;
height: 35px;
border-radius: 50%;
border: 2px solid transparent;
transition: all 0.3s ease;
}
.avatar:hover {
border-color: #6366F1;
transform: scale(1.05);
}
.dropdown-menu {
position: absolute;
top: calc(100% + 8px);
right: 0;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 8px;
padding: 8px 0;
min-width: 150px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: all 0.3s ease;
}
.dropdown-menu.active {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.dropdown-menu button {
width: 100%;
padding: 8px 16px;
background: none;
border: none;
color: #4B5563;
font-size: 0.95rem;
text-align: left;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 8px;
}
.dropdown-menu button:hover {
background: rgba(99, 102, 241, 0.1);
color: #6366F1;
}
@media screen and (max-width: 768px) {
header {
height: auto;
padding: 0;
}
header nav {
padding: 0.6rem 1rem;
}
.logo {
width: 30px;
height: 30px;
}
.title {
font-size: 1.4rem;
}
.menu-items {
position: fixed;
top: 60px;
left: 0;
right: 0;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
padding: 1rem;
flex-direction: column;
gap: 1rem;
border-bottom: 1px solid rgba(99, 102, 241, 0.1);
display: none;
}
.menu-items.active {
display: flex;
}
.menu-items a {
width: 100%;
text-align: center;
padding: 0.8rem;
}
.right-container {
gap: 0.8rem;
}
.auth-container .login-button {
padding: 6px 12px;
font-size: 0.9rem;
}
}
/* 移动端菜单按钮 */
.mobile-menu-button {
display: none;
background: none;
border: none;
padding: 8px;
cursor: pointer;
color: #4B5563;
transition: color 0.3s ease;
}
.mobile-menu-button:hover {
color: #6366F1;
}
/* 移动端适配 */
@media screen and (max-width: 768px) {
header {
position: fixed;
top: 0;
left: 0;
right: 0;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
z-index: 1000;
}
header nav {
padding: 0.8rem 1rem;
justify-content: space-between;
align-items: center;
}
.mobile-menu-button {
display: block;
z-index: 1001;
}
.menu-container {
position: fixed;
top: 60px; /* 导航栏高度 */
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.98);
padding: 1rem;
transform: translateX(100%);
transition: transform 0.3s ease;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 1rem;
z-index: 1000;
}
.menu-container.mobile-menu-open {
transform: translateX(0);
}
.menu-items {
display: flex;
flex-direction: column;
margin: 0;
width: 100%;
gap: 0.5rem;
}
.menu-items a {
width: 100%;
padding: 1rem;
text-align: center;
}
.right-container {
margin-top: 1rem;
width: 100%;
}
}
/* 横屏模式优化 */
@media screen and (max-width: 768px) and (orientation: landscape) {
.menu-container {
top: 50px;
}
.menu-items {
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
}
.menu-items a {
width: auto;
}
.right-container {
flex-direction: row;
justify-content: center;
}
}
/* 移动端样式统一处理 */
@media screen and (max-width: 768px) {
header {
position: fixed;
top: 0;
left: 0;
right: 0;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
z-index: 1000;
}
header nav {
padding: 0.8rem 1rem;
justify-content: space-between;
align-items: center;
}
.mobile-menu-button {
display: block;
z-index: 1001;
background: none;
border: none;
padding: 8px;
cursor: pointer;
color: #4B5563;
transition: color 0.3s ease;
}
.mobile-menu-button:hover {
color: #6366F1;
}
.menu-container {
position: fixed;
top: 60px;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(10px);
padding: 1rem;
transform: translateX(100%);
transition: transform 0.3s ease;
z-index: 999;
display: flex;
flex-direction: column;
overflow-y: auto;
}
.menu-container.mobile-menu-open {
transform: translateX(0);
}
.menu-items {
display: flex;
flex-direction: column;
margin: 0;
width: 100%;
gap: 1rem;
}
.menu-items a {
width: 100%;
text-align: center;
padding: 1rem;
font-size: 1.1rem;
}
.right-container {
margin-top: 1rem;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.auth-container {
width: 100%;
display: flex;
justify-content: center;
}
.login-button {
width: 100%;
justify-content: center;
}
}
/* 横屏模式优化 */
@media screen and (max-width: 768px) and (orientation: landscape) {
.menu-container {
top: 50px;
}
.menu-items {
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
}
.menu-items a {
width: auto;
}
.right-container {
flex-direction: row;
justify-content: center;
}
}
/* 移动端菜单按钮 */
.mobile-menu-button {
display: none;
background: none;
border: none;
padding: 8px;
cursor: pointer;
color: #4B5563;
transition: color 0.3s ease;
}
/* 移动端菜单容器 */
.menu-container {
display: flex;
align-items: center;
flex: 1;
}
@media screen and (max-width: 768px) {
.mobile-menu-button {
display: block;
}
.menu-container {
position: fixed;
top: 60px;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.98);
padding: 1rem;
transform: translateX(100%);
transition: transform 0.3s ease;
z-index: 999;
display: flex;
flex-direction: column;
}
.menu-container.mobile-menu-open {
transform: translateX(0);
}
.menu-items {
display: flex !important; /* 覆盖之前的 display: none */
flex-direction: column;
margin: 0;
width: 100%;
gap: 1rem;
}
.menu-items a {
width: 100%;
text-align: center;
padding: 1rem;
}
.right-container {
margin-top: 1rem;
width: 100%;
}
}
/* 横屏模式优化 */
@media screen and (max-width: 768px) and (orientation: landscape) {
.menu-container {
top: 50px;
}
.menu-items {
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
}
.menu-items a {
width: auto;
}
}
...@@ -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