Commit 90edd27f authored by fisherdaddy's avatar fisherdaddy

chore: 更新样式

parent f6603ad5
...@@ -2,4 +2,3 @@ node_modules/ ...@@ -2,4 +2,3 @@ node_modules/
package-lock.json package-lock.json
dist/ dist/
src/.DS_Store src/.DS_Store
!robots.txt
\ No newline at end of file
...@@ -73,12 +73,12 @@ function Header() { ...@@ -73,12 +73,12 @@ function Header() {
<NavLink to="/image-tools" onClick={handleNavClick}> <NavLink to="/image-tools" onClick={handleNavClick}>
{t('image-tools')} {t('image-tools')}
</NavLink> </NavLink>
<NavLink to="/blog" onClick={handleNavClick}>
{t('blog')}
</NavLink>
<NavLink to="/ai-products" onClick={handleNavClick}> <NavLink to="/ai-products" onClick={handleNavClick}>
{t('ai-products')} {t('ai-products')}
</NavLink> </NavLink>
<NavLink to="/blog" onClick={handleNavClick}>
{t('blog')}
</NavLink>
</div> </div>
<div className="right-container"> <div className="right-container">
<LanguageSelector /> <LanguageSelector />
......
// ImageBase64Converter.jsx // ImageBase64Converter.jsx
import React, { useState, useCallback } from 'react'; import React, { useState, useCallback } from 'react';
import { Title, Wrapper, Container, InputText, Preview } from '../js/SharedStyles'; import { Title, Wrapper, Container } from '../js/SharedStyles';
import styled from 'styled-components'; import styled from 'styled-components';
import { useTranslation } from '../js/i18n'; import { useTranslation } from '../js/i18n';
import SEO from '../components/SEO'; import SEO from '../components/SEO';
const ConverterContainer = styled(Container)` const ConverterContainer = styled(Container)`
flex-direction: column; flex-direction: column;
gap: 24px;
padding: 24px;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
border: 1px solid rgba(99, 102, 241, 0.1);
border-radius: 12px;
`; `;
const StyledInputText = styled(InputText)` const Section = styled.div`
height: 100px;
margin-bottom: 20px;
@media (min-width: 768px) {
height: 100px;
width: 100%;
}
`;
const PreviewWrapper = styled.div`
width: 100%; width: 100%;
`; `;
const Label = styled.label` const Label = styled.label`
font-weight: 500; font-weight: 500;
font-size: 14px; font-size: 14px;
color: #5f6368; color: #374151;
margin-bottom: 8px; margin-bottom: 12px;
display: block; display: block;
letter-spacing: 0.1px; letter-spacing: 0.1px;
`; `;
const StyledPreview = styled(Preview)` const StyledInputText = styled.textarea`
background-color: #f8f9fa; width: 100%;
padding: 12px 40px 12px 12px; height: 120px;
border-radius: 8px; font-size: 15px;
border: 1px solid #dadce0; 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 FileInputWrapper = styled.div`
position: relative;
width: 100%;
height: 120px;
border: 2px dashed rgba(99, 102, 241, 0.2);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
border-color: rgba(99, 102, 241, 0.4);
background: rgba(99, 102, 241, 0.05);
}
input {
position: absolute;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
}
span {
color: #6366F1;
font-size: 14px; font-size: 14px;
color: #202124; display: flex;
min-height: 24px; align-items: center;
word-break: break-all; gap: 8px;
}
`; `;
const ResultContainer = styled.div` const ResultContainer = styled.div`
position: relative; position: relative;
width: 100%; width: 100%;
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;
min-height: 120px;
`; `;
const CopyButton = styled.button` const ActionButton = styled.button`
position: absolute; background: rgba(99, 102, 241, 0.1);
top: 8px;
right: 8px;
background-color: transparent;
border: none; border: none;
border-radius: 6px;
padding: 6px 12px;
cursor: pointer; cursor: pointer;
padding: 4px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; gap: 6px;
opacity: 0.6; font-size: 13px;
transition: opacity 0.3s, color 0.3s; color: #6366F1;
transition: all 0.3s ease;
&:hover { &:hover {
opacity: 1; background: rgba(99, 102, 241, 0.2);
} }
svg { &.active {
width: 16px; background: #6366F1;
height: 16px; color: white;
} }
&.copied { svg {
color: #34a853; width: 14px;
height: 14px;
} }
`; `;
const StyledInputFile = styled.input`
margin-bottom: 20px;
`;
const ImagePreviewContainer = styled.div` const ImagePreviewContainer = styled.div`
position: relative; position: relative;
display: inline-block; width: 100%;
margin-top: 10px; margin-top: 8px;
display: flex;
flex-direction: column;
gap: 12px;
`; `;
const ImagePreview = styled.img` const ThumbnailContainer = styled.div`
max-width: 100%; position: relative;
height: auto; width: 120px;
border: 1px solid #dadce0; height: 120px;
border-radius: 8px; border-radius: 8px;
display: block; overflow: hidden;
border: 1px solid rgba(99, 102, 241, 0.1);
`; `;
const DownloadButton = styled.button` const PreviewActions = styled.div`
position: absolute;
top: 8px;
right: 8px;
background-color: #fff;
border: 1px solid #dadce0;
border-radius: 4px;
cursor: pointer;
padding: 6px 8px;
font-size: 12px;
color: #202124;
display: flex; display: flex;
align-items: center; gap: 8px;
opacity: 0.8; margin-top: 8px;
transition: opacity 0.3s; `;
&:hover { const Thumbnail = styled.img`
opacity: 1; width: 100%;
} height: 100%;
object-fit: cover;
`;
svg { const ImageDetails = styled.div`
width: 16px; font-size: 13px;
height: 16px; color: #6B7280;
margin-right: 4px; margin-top: 4px;
}
`; `;
const ErrorText = styled.div` const ErrorText = styled.div`
color: red; color: #EF4444;
font-size: 14px;
margin-top: 8px; margin-top: 8px;
`; `;
...@@ -218,34 +257,50 @@ function ImageBase64Converter() { ...@@ -218,34 +257,50 @@ function ImageBase64Converter() {
<Wrapper> <Wrapper>
<Title>{t('tools.imageBase64Converter.title')}</Title> <Title>{t('tools.imageBase64Converter.title')}</Title>
<ConverterContainer> <ConverterContainer>
{/* 图片转 Base64 部分 */} <Section>
<Label>{t('tools.imageBase64Converter.imageToBase64')}</Label> <Label>{t('tools.imageBase64Converter.imageToBase64')}</Label>
<StyledInputFile <FileInputWrapper>
<input
type="file" type="file"
accept="image/*" accept="image/*"
onChange={handleImageUpload} onChange={handleImageUpload}
/> />
<span>
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
<path d="M19 7v2.99s-1.99.01-2 0V7h-3s.01-1.99 0-2h3V2h2v3h3v2h-3zm-3 4V8h-3V5H5c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2v-8h-3zM5 19l3-4 2 3 3-4 4 5H5z"/>
</svg>
{t('tools.imageBase64Converter.dragOrClick')}
</span>
</FileInputWrapper>
{base64String && ( {base64String && (
<>
<Label>{t('tools.imageBase64Converter.base64Result')}</Label>
<ResultContainer> <ResultContainer>
<StyledPreview>{base64String}</StyledPreview> <pre>{base64String}</pre>
<CopyButton onClick={handleCopy} className={isCopied ? 'copied' : ''}> <ActionButton onClick={handleCopy} className={isCopied ? 'active' : ''}>
{isCopied ? ( {isCopied ? (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"> <svg viewBox="0 0 24 24" fill="currentColor">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" /> <path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
</svg> </svg>
) : ( ) : (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"> <svg viewBox="0 0 24 24" fill="currentColor">
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1 .9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1 -.9-2 -2-2zm0 16H8V7h11v14z" /> <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>
)} )}
</CopyButton> {isCopied ? t('tools.jsonFormatter.copiedMessage') : t('tools.jsonFormatter.copyButton')}
</ActionButton>
<ThumbnailContainer>
<Thumbnail src={base64String} alt="Preview" />
</ThumbnailContainer>
{imageFile && (
<ImageDetails>
{t('tools.imageBase64Converter.fileName')}: {imageFile.name}<br />
{t('tools.imageBase64Converter.fileSize')}: {(imageFile.size / 1024).toFixed(2)} KB
</ImageDetails>
)}
</ResultContainer> </ResultContainer>
</>
)} )}
</Section>
{/* Base64 转图片部分 */} <Section>
<Label>{t('tools.imageBase64Converter.base64ToImage')}</Label> <Label>{t('tools.imageBase64Converter.base64ToImage')}</Label>
<StyledInputText <StyledInputText
value={inputBase64} value={inputBase64}
...@@ -254,19 +309,21 @@ function ImageBase64Converter() { ...@@ -254,19 +309,21 @@ function ImageBase64Converter() {
/> />
{error && <ErrorText>{error}</ErrorText>} {error && <ErrorText>{error}</ErrorText>}
{imageSrc && ( {imageSrc && (
<div>
<Label>{t('tools.imageBase64Converter.imageResult')}</Label>
<ImagePreviewContainer> <ImagePreviewContainer>
<ImagePreview src={imageSrc} alt="Base64 to Image" onError={handleImageError} /> <ThumbnailContainer>
<DownloadButton onClick={handleDownload}> <Thumbnail src={imageSrc} alt="Thumbnail" onError={handleImageError} />
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> </ThumbnailContainer>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2M12 12v9m0 0l-3-3m3 3l3-3" /> <PreviewActions>
<ActionButton onClick={handleDownload}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2M12 12v9m0 0l-3-3m3 3l3-3"/>
</svg> </svg>
{t('tools.imageBase64Converter.download')} {t('tools.imageBase64Converter.download')}
</DownloadButton> </ActionButton>
</PreviewActions>
</ImagePreviewContainer> </ImagePreviewContainer>
</div>
)} )}
</Section>
</ConverterContainer> </ConverterContainer>
</Wrapper> </Wrapper>
</> </>
......
...@@ -7,19 +7,26 @@ import SEO from '../components/SEO'; ...@@ -7,19 +7,26 @@ import SEO from '../components/SEO';
const InputText = styled.textarea` const InputText = styled.textarea`
width: 100%; width: 100%;
height: 200px; height: 200px;
font-size: 14px; font-size: 15px;
padding: 10px; padding: 16px;
border: none; border: 1px solid rgba(99, 102, 241, 0.1);
border-bottom: 1px solid #e0e0e0; border-radius: 12px;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
box-sizing: border-box; box-sizing: border-box;
outline: none; outline: none;
resize: 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) { @media (min-width: 768px) {
width: 35%; width: 40%;
height: 100%; height: 100%;
border-bottom: none;
border-right: 1px solid #e0e0e0;
} }
`; `;
...@@ -27,67 +34,103 @@ const PreviewContainer = styled.div` ...@@ -27,67 +34,103 @@ const PreviewContainer = styled.div`
width: 100%; width: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; background: rgba(255, 255, 255, 0.8);
padding: 10px; backdrop-filter: blur(10px);
border-radius: 12px;
border: 1px solid rgba(99, 102, 241, 0.1);
padding: 16px;
box-sizing: border-box; box-sizing: border-box;
@media (min-width: 768px) { @media (min-width: 768px) {
width: 65%; width: 58%;
height: 100%; height: 100%;
} }
`; `;
const ToggleButton = styled.span` const ButtonGroup = styled.div`
cursor: pointer; display: flex;
color: #666; gap: 8px;
font-weight: bold; position: absolute;
margin-right: 5px; top: 12px;
right: 12px;
`; `;
const Key = styled.span` const ActionButton = styled.button`
color: #881391; 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;
const Value = styled.span` &:hover {
color: #1a1aa6; background: rgba(99, 102, 241, 0.2);
}
&.active {
background: #6366F1;
color: white;
}
svg {
width: 14px;
height: 14px;
}
`; `;
const JsonList = styled.ul` const RelativePreviewContainer = styled(PreviewContainer)`
list-style-type: none; position: relative;
padding-left: 20px;
margin: 0;
`; `;
const CopyButton = styled.button` const ToggleButton = styled.button`
position: absolute; background: none;
top: 10px;
right: 10px;
background-color: transparent;
border: none; border: none;
cursor: pointer; cursor: pointer;
padding: 5px; color: #6366F1;
display: flex; font-size: 13px;
padding: 2px 6px;
margin-right: 6px;
border-radius: 4px;
display: inline-flex;
align-items: center; align-items: center;
justify-content: center; transition: all 0.2s ease;
opacity: 0.6;
transition: opacity 0.3s, color 0.3s;
&:hover { &:hover {
opacity: 1; background: rgba(99, 102, 241, 0.1);
} }
svg { svg {
width: 16px; width: 16px;
height: 16px; height: 16px;
} }
`;
&.copied { const JsonList = styled.ul`
color: #34a853; // Google green color for success feedback list-style-type: none;
} padding-left: 24px;
margin: 0;
font-size: 15px;
line-height: 1.6;
`; `;
const RelativePreviewContainer = styled(PreviewContainer)` const Key = styled.span`
position: relative; 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() {
...@@ -95,6 +138,7 @@ function JsonFormatter() { ...@@ -95,6 +138,7 @@ function JsonFormatter() {
const [input, setInput] = useState(''); const [input, setInput] = useState('');
const [parsedJson, setParsedJson] = useState(null); const [parsedJson, setParsedJson] = useState(null);
const [isCopied, setIsCopied] = useState(false); const [isCopied, setIsCopied] = useState(false);
const [isCompressed, setIsCompressed] = useState(false);
useEffect(() => { useEffect(() => {
try { try {
...@@ -107,7 +151,9 @@ function JsonFormatter() { ...@@ -107,7 +151,9 @@ function JsonFormatter() {
const handleCopy = () => { const handleCopy = () => {
if (parsedJson) { if (parsedJson) {
const formattedJson = JSON.stringify(parsedJson, null, 2); const formattedJson = isCompressed
? JSON.stringify(parsedJson)
: JSON.stringify(parsedJson, null, 2);
navigator.clipboard.writeText(formattedJson).then(() => { navigator.clipboard.writeText(formattedJson).then(() => {
setIsCopied(true); setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000); setTimeout(() => setIsCopied(false), 2000);
...@@ -115,6 +161,10 @@ function JsonFormatter() { ...@@ -115,6 +161,10 @@ function JsonFormatter() {
} }
}; };
const toggleCompression = () => {
setIsCompressed(!isCompressed);
};
return ( return (
<> <>
<SEO <SEO
...@@ -133,19 +183,43 @@ function JsonFormatter() { ...@@ -133,19 +183,43 @@ function JsonFormatter() {
{parsedJson ? ( {parsedJson ? (
<> <>
<Preview> <Preview>
{isCompressed ? (
<pre style={{ margin: 0, whiteSpace: 'nowrap', overflowX: 'auto' }}>
{JSON.stringify(parsedJson)}
</pre>
) : (
<JsonView data={parsedJson} /> <JsonView data={parsedJson} />
)}
</Preview> </Preview>
<CopyButton onClick={handleCopy} className={isCopied ? 'copied' : ''}> <ButtonGroup>
<ActionButton
onClick={toggleCompression}
className={isCompressed ? 'active' : ''}
>
{isCompressed ? (
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M4 9h16v2H4V9zm0 4h16v2H4v-2z"/>
</svg>
) : (
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M19 13H5v-2h14v2z"/>
</svg>
)}
{isCompressed ? '展开' : '压缩'}
</ActionButton>
<ActionButton onClick={handleCopy} className={isCopied ? 'active' : ''}>
{isCopied ? ( {isCopied ? (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"> <svg viewBox="0 0 24 24" fill="currentColor">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/> <path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
</svg> </svg>
) : ( ) : (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"> <svg viewBox="0 0 24 24" fill="currentColor">
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/> <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>
)} )}
</CopyButton> {isCopied ? '已复制' : '复制'}
</ActionButton>
</ButtonGroup>
</> </>
) : ( ) : (
<Preview>{t('tools.jsonFormatter.invalidJson')}</Preview> <Preview>{t('tools.jsonFormatter.invalidJson')}</Preview>
...@@ -164,10 +238,19 @@ function JsonView({ data }) { ...@@ -164,10 +238,19 @@ function JsonView({ data }) {
return ( return (
<div> <div>
<ToggleButton onClick={() => setIsExpanded(!isExpanded)}> <ToggleButton onClick={() => setIsExpanded(!isExpanded)}>
{isExpanded ? '[-]' : '[+]'} {isExpanded ? (
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M19 13H5v-2h14v2z"/>
</svg>
) : (
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</svg>
)}
</ToggleButton> </ToggleButton>
{!isExpanded && <span>Array</span>} {!isExpanded ? (
{isExpanded && ( <span style={{ color: '#6B7280', fontSize: '15px' }}>Array [{data.length}]</span>
) : (
<JsonList> <JsonList>
[ [
{data.map((item, index) => ( {data.map((item, index) => (
...@@ -182,19 +265,29 @@ function JsonView({ data }) { ...@@ -182,19 +265,29 @@ function JsonView({ data }) {
</div> </div>
); );
} else if (typeof data === 'object' && data !== null) { } else if (typeof data === 'object' && data !== null) {
const entries = Object.entries(data);
return ( return (
<div> <div>
<ToggleButton onClick={() => setIsExpanded(!isExpanded)}> <ToggleButton onClick={() => setIsExpanded(!isExpanded)}>
{isExpanded ? '{-}' : '{+}'} {isExpanded ? (
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M19 13H5v-2h14v2z"/>
</svg>
) : (
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</svg>
)}
</ToggleButton> </ToggleButton>
{!isExpanded && <span>Object</span>} {!isExpanded ? (
{isExpanded && ( <span style={{ color: '#6B7280', fontSize: '15px' }}>Object {`{${entries.length}}`}</span>
) : (
<JsonList> <JsonList>
{'{'} {'{'}
{Object.entries(data).map(([key, value], index, array) => ( {entries.map(([key, value], index) => (
<li key={key}> <li key={key}>
<Key>"{key}"</Key>: <JsonView data={value} /> <Key>"{key}"</Key>: <JsonView data={value} />
{index < array.length - 1 && ','} {index < entries.length - 1 && ','}
</li> </li>
))} ))}
{'}'} {'}'}
......
...@@ -4,8 +4,7 @@ import events from '../data/openai-releases.json'; ...@@ -4,8 +4,7 @@ import events from '../data/openai-releases.json';
import SEO from '../components/SEO'; import SEO from '../components/SEO';
import { useTranslation } from '../js/i18n'; import { useTranslation } from '../js/i18n';
const Timeline = () => {
const timeline = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
...@@ -15,15 +14,15 @@ const timeline = () => { ...@@ -15,15 +14,15 @@ const timeline = () => {
description={t('tools.openAITimeline.description')} description={t('tools.openAITimeline.description')}
/> />
<div className="container"> <div className="container">
<div className="timeline-title">OpenAI 产品发布时间线</div> <h1 className="timeline-title">{t('tools.openAITimeline.title')}</h1>
<ul className="timeline"> <ul className="timeline">
{events.map((item, index) => ( {events.map((item, index) => (
<li className="event" key={index}> <li className="event" key={index}>
<div className="event-content"> <div className="event-content">
<div className="event-date">{item.date}</div> <div className="event-date">{item.date}</div>
<div className="event-title">{item.title}</div> <div className="event-title">{item.title}</div>
<div class="event-feature">{item.feature}</div> <div className="event-feature">{item.feature}</div>
<div class="event-description">{item.description}</div> <div className="event-description">{item.description}</div>
</div> </div>
</li> </li>
))} ))}
...@@ -33,4 +32,4 @@ const timeline = () => { ...@@ -33,4 +32,4 @@ const timeline = () => {
); );
}; };
export default timeline; export default Timeline;
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import '../styles/PricingChart.css'; import '../styles/PricingChart.css';
const ChartLegend = ({ onLegendClick, highlightedBarTypes }) => ( const ChartLegend = ({ onLegendClick, highlightedBarTypes }) => {
return (
<div className="legend"> <div className="legend">
<div <div
className="legend-item" className="legend-item"
...@@ -10,7 +11,7 @@ const ChartLegend = ({ onLegendClick, highlightedBarTypes }) => ( ...@@ -10,7 +11,7 @@ const ChartLegend = ({ onLegendClick, highlightedBarTypes }) => (
style={{ cursor: 'pointer', opacity: highlightedBarTypes.input ? 1 : 0.5 }} style={{ cursor: 'pointer', opacity: highlightedBarTypes.input ? 1 : 0.5 }}
> >
<div className="legend-color input-color"></div> <div className="legend-color input-color"></div>
<span>Input price</span> <span>Input Price</span>
</div> </div>
<div <div
className="legend-item" className="legend-item"
...@@ -18,10 +19,11 @@ const ChartLegend = ({ onLegendClick, highlightedBarTypes }) => ( ...@@ -18,10 +19,11 @@ const ChartLegend = ({ onLegendClick, highlightedBarTypes }) => (
style={{ cursor: 'pointer', opacity: highlightedBarTypes.output ? 1 : 0.5 }} style={{ cursor: 'pointer', opacity: highlightedBarTypes.output ? 1 : 0.5 }}
> >
<div className="legend-color output-color"></div> <div className="legend-color output-color"></div>
<span>Output price</span> <span>Output Price</span>
</div> </div>
</div> </div>
); );
};
const ChartBar = ({ price, type, maxPrice, highlighted }) => { const ChartBar = ({ price, type, maxPrice, highlighted }) => {
const getBarHeight = () => { const getBarHeight = () => {
......
...@@ -3,21 +3,21 @@ import PricingChart from '../components/PricingChart'; ...@@ -3,21 +3,21 @@ import PricingChart from '../components/PricingChart';
import OpenaiPricing from '../data/openai-pricing.json'; import OpenaiPricing from '../data/openai-pricing.json';
import LLMPricing from '../data/llm-pricing.json'; import LLMPricing from '../data/llm-pricing.json';
import VisionPricing from '../data/vision-model-pricing.json'; import VisionPricing from '../data/vision-model-pricing.json';
import { useTranslation } from '../js/i18n';
import SEO from '../components/SEO'; import SEO from '../components/SEO';
const PricingCharts = () => { const PricingCharts = () => {
const { t } = useTranslation(); const lastUpdateTime = '2024-11-06 21:30';
const lastUpdateTime = '2024-11-06 21:30'; // 硬编码的更新时间
return ( return (
<> <>
<SEO <SEO
title={t('tools.modelPrice.title')} title="AI Model Pricing Comparison"
description={t('tools.modelPrice.description')} description="Compare prices of different AI models"
/> />
<div className="pricing-charts-container"> <div className="pricing-charts-container">
<div className="update-time">数据最后更新时间: {lastUpdateTime}</div> <div className="update-time">
Last Updated: {lastUpdateTime}
</div>
<PricingChart data={OpenaiPricing} /> <PricingChart data={OpenaiPricing} />
<PricingChart data={LLMPricing} /> <PricingChart data={LLMPricing} />
<PricingChart data={VisionPricing} /> <PricingChart data={VisionPricing} />
......
import React, { useState, useCallback } from 'react'; import React, { useState, useCallback } from 'react';
import { Title, Wrapper, Container, InputText, Preview } from '../js/SharedStyles'; import { Title, Wrapper, Container, Preview } from '../js/SharedStyles';
import styled from 'styled-components'; import styled from 'styled-components';
import { useTranslation } from '../js/i18n'; import { useTranslation } from '../js/i18n';
import SEO from './SEO'; import SEO from './SEO';
const EncoderDecoderContainer = styled(Container)` const EncoderDecoderContainer = styled(Container)`
flex-direction: column; flex-direction: column;
gap: 16px;
`; `;
const StyledInputText = styled(InputText)` const StyledInputText = styled.textarea`
height: 100px;
margin-bottom: 20px;
@media (min-width: 768px) {
height: 100px;
width: 100%; 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 PreviewWrapper = styled.div`
width: 100%;
`;
const Label = styled.label` const Label = styled.label`
font-weight: 500; font-weight: 500;
font-size: 14px; font-size: 14px;
color: #5f6368; color: #374151;
margin-bottom: 8px; margin-bottom: 8px;
display: block; display: block;
letter-spacing: 0.1px; letter-spacing: 0.1px;
`; `;
const StyledPreview = styled(Preview)` const ModeSwitcher = styled.div`
background-color: #f8f9fa; margin-bottom: 8px;
padding: 12px 40px 12px 12px; // 增加右侧 padding 为按钮留出空间
select {
padding: 8px 12px;
border-radius: 8px; border-radius: 8px;
border: 1px solid #dadce0; border: 1px solid rgba(99, 102, 241, 0.1);
font-size: 14px; font-size: 14px;
color: #202124; color: #374151;
min-height: 24px; // 确保即使内容为空,也有足够的高度容纳按钮 background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
cursor: pointer;
transition: all 0.3s ease;
&:focus {
border-color: rgba(99, 102, 241, 0.3);
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.1);
outline: none;
}
}
`; `;
const ResultContainer = styled.div` const ResultContainer = styled.div`
...@@ -45,43 +66,47 @@ const ResultContainer = styled.div` ...@@ -45,43 +66,47 @@ const ResultContainer = styled.div`
width: 100%; width: 100%;
`; `;
const CopyButton = styled.button` const StyledPreview = styled.div`
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
border-radius: 12px;
border: 1px solid rgba(99, 102, 241, 0.1);
padding: 16px;
font-size: 15px;
color: #374151;
min-height: 24px;
line-height: 1.5;
position: relative;
`;
const ActionButton = styled.button`
position: absolute; position: absolute;
top: 8px; top: 12px;
right: 8px; right: 12px;
background-color: transparent; background: rgba(99, 102, 241, 0.1);
border: none; border: none;
border-radius: 6px;
padding: 6px 12px;
cursor: pointer; cursor: pointer;
padding: 4px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; gap: 6px;
opacity: 0.6; font-size: 13px;
transition: opacity 0.3s, color 0.3s; color: #6366F1;
transition: all 0.3s ease;
&:hover { &:hover {
opacity: 1; background: rgba(99, 102, 241, 0.2);
} }
svg { &.active {
width: 16px; background: #6366F1;
height: 16px; color: white;
}
&.copied {
color: #34a853; // 成功复制后的反馈颜色
} }
`;
const ModeSwitcher = styled.div` svg {
margin-bottom: 20px; width: 14px;
height: 14px;
select {
padding: 8px;
border-radius: 4px;
border: 1px solid #dadce0;
font-size: 14px;
color: #202124;
} }
`; `;
...@@ -138,29 +163,41 @@ function UrlEncoderDecoder() { ...@@ -138,29 +163,41 @@ function UrlEncoderDecoder() {
<option value="decode">{t('tools.urlEncodeDecode.decode')}</option> <option value="decode">{t('tools.urlEncodeDecode.decode')}</option>
</select> </select>
</ModeSwitcher> </ModeSwitcher>
<div>
<Label>
{mode === 'decode' ? t('tools.urlDecode.inputLabel') : t('tools.urlEncode.inputLabel')}
</Label>
<StyledInputText <StyledInputText
id="urlInput"
placeholder={mode === 'decode' ? t('tools.urlDecode.inputLabel') : t('tools.urlEncode.inputLabel')}
value={input} value={input}
onChange={handleInputChange} onChange={handleInputChange}
placeholder={mode === 'decode' ? t('tools.urlDecode.inputLabel') : t('tools.urlEncode.inputLabel')}
/> />
<PreviewWrapper> </div>
<Label>{mode === 'decode' ? t('tools.urlDecode.resultLabel') : t('tools.urlEncode.resultLabel')}</Label>
<div>
<Label>
{mode === 'decode' ? t('tools.urlDecode.resultLabel') : t('tools.urlEncode.resultLabel')}
</Label>
<ResultContainer> <ResultContainer>
<StyledPreview>{resultText}</StyledPreview> <StyledPreview>{resultText}</StyledPreview>
<CopyButton onClick={handleCopy} className={isCopied ? 'copied' : ''}> <ActionButton
onClick={handleCopy}
className={isCopied ? 'active' : ''}
>
{isCopied ? ( {isCopied ? (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"> <svg viewBox="0 0 24 24" fill="currentColor">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/> <path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
</svg> </svg>
) : ( ) : (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"> <svg viewBox="0 0 24 24" fill="currentColor">
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/> <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>
)} )}
</CopyButton> {isCopied ? t('tools.jsonFormatter.copiedMessage') : t('tools.jsonFormatter.copyButton')}
</ActionButton>
</ResultContainer> </ResultContainer>
</PreviewWrapper> </div>
</EncoderDecoderContainer> </EncoderDecoderContainer>
</Wrapper> </Wrapper>
</> </>
......
export default {
tools: {
modelPrice: {
title: "AI Model Pricing Comparison",
description: "Compare prices of different AI models",
lastUpdate: "Last Updated",
inputPrice: "Input Price",
outputPrice: "Output Price",
openai: {
title: "OpenAI Models Pricing",
subtitle: "Price per 1K tokens"
}
}
}
};
\ No newline at end of file
...@@ -89,7 +89,10 @@ ...@@ -89,7 +89,10 @@
"base64InputPlaceholder": "Paste Base64 string here", "base64InputPlaceholder": "Paste Base64 string here",
"imageResult": "Image Result", "imageResult": "Image Result",
"invalidBase64": "Invalid Base64 string", "invalidBase64": "Invalid Base64 string",
"download": "Download Image" "download": "Download Image",
"dragOrClick": "Drag and drop or click to upload",
"fileName": "File name",
"fileSize": "File size"
}, },
"fisherai": { "fisherai": {
"title": "FisherAI", "title": "FisherAI",
......
export default {
tools: {
modelPrice: {
title: "AIモデル価格比較",
description: "異なるAIモデルの価格を比較",
lastUpdate: "最終更新",
inputPrice: "入力価格",
outputPrice: "出力価格",
openai: {
title: "OpenAIモデル価格",
subtitle: "1Kトークンあたりの価格"
}
}
}
};
\ No newline at end of file
...@@ -81,15 +81,18 @@ ...@@ -81,15 +81,18 @@
"description": "紙に書くのと同等の効果を生成します" "description": "紙に書くのと同等の効果を生成します"
}, },
"imageBase64Converter": { "imageBase64Converter": {
"title": "画像とBase64コンバーター", "title": "Base64 と画像の変換",
"description": "画像とBase64の相互変換", "description": "画像と Base64 の相互変換",
"imageToBase64": "画像をBase64に変換", "imageToBase64": "画像を Base64 に変換",
"base64Result": "Base64の結果", "base64Result": "Base64 結果",
"base64ToImage": "Base64を画像に変換", "base64ToImage": "Base64 を画像に変換",
"base64InputPlaceholder": "ここにBase64文字列を貼り付け", "base64InputPlaceholder": "ここに Base64 文字列を貼り付けてください",
"imageResult": "画像の結果", "imageResult": "画像結果",
"invalidBase64": "無効なBase64文字列", "invalidBase64": "無効な Base64 文字列",
"download": "画像をダウンロード" "download": "画像をダウンロード",
"dragOrClick": "画像をドラッグまたはクリックしてアップロード",
"fileName": "ファイル名",
"fileSize": "ファイルサイズ"
}, },
"fisherai": { "fisherai": {
"title": "FisherAI", "title": "FisherAI",
......
export default {
tools: {
modelPrice: {
title: "AI 모델 가격 비교",
description: "다양한 AI 모델의 가격 비교",
lastUpdate: "마지막 업데이트",
inputPrice: "입력 가격",
outputPrice: "출력 가격",
openai: {
title: "OpenAI 모델 가격",
subtitle: "1K 토큰당 가격"
}
}
}
};
\ No newline at end of file
...@@ -90,7 +90,10 @@ ...@@ -90,7 +90,10 @@
"base64InputPlaceholder": "여기에 Base64 문자열을 붙여넣기", "base64InputPlaceholder": "여기에 Base64 문자열을 붙여넣기",
"imageResult": "이미지 결과", "imageResult": "이미지 결과",
"invalidBase64": "잘못된 Base64 문자열", "invalidBase64": "잘못된 Base64 문자열",
"download": "이미지 다운로드" "download": "이미지 다운로드",
"dragOrClick": "드래그 앤 드롭 또는 클릭하여 업로드",
"fileName": "파일 이름",
"fileSize": "파일 크기"
}, },
"fisherai": { "fisherai": {
"title": "FisherAI", "title": "FisherAI",
......
export default {
tools: {
modelPrice: {
title: "AI模型价格对比",
description: "比较不同AI模型的价格",
lastUpdate: "数据最后更新时间",
inputPrice: "输入价格",
outputPrice: "输出价格",
openai: {
title: "OpenAI模型价格",
subtitle: "每千字符价格"
}
}
}
};
\ No newline at end of file
...@@ -88,7 +88,10 @@ ...@@ -88,7 +88,10 @@
"base64InputPlaceholder": "在此粘贴 Base64 字符串", "base64InputPlaceholder": "在此粘贴 Base64 字符串",
"imageResult": "图片结果", "imageResult": "图片结果",
"invalidBase64": "无效的 Base64 字符串", "invalidBase64": "无效的 Base64 字符串",
"download": "下载图片" "download": "下载图片",
"dragOrClick": "拖拽或点击上传图片",
"fileName": "文件名",
"fileSize": "文件大小"
}, },
"fisherai": { "fisherai": {
"title": "FisherAI", "title": "FisherAI",
......
.pricing-chart { .pricing-chart {
padding: 2rem; padding: 2.5rem;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; background: linear-gradient(145deg, rgba(255,255,255,0.05) 0%, rgba(240,242,245,0.1) 100%);
border-radius: 16px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.1);
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
margin-bottom: 2rem;
} }
.chart-title { .chart-title {
font-size: 1.5rem; font-size: 1.75rem;
font-weight: 600; background: linear-gradient(90deg, #4285f4, #7c4dff);
margin-bottom: 0.5rem; -webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 1rem;
} }
.chart-subtitle { .chart-subtitle {
font-size: 1rem; font-size: 1.1rem;
color: #666; color: var(--text-secondary);
font-weight: normal; margin-bottom: 1.5rem;
margin-bottom: 1rem;
} }
.legend { .legend {
...@@ -26,6 +32,15 @@ ...@@ -26,6 +32,15 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
background: rgba(255,255,255,0.1);
padding: 0.5rem 1rem;
border-radius: 8px;
transition: all 0.3s ease;
}
.legend-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
} }
.legend-color { .legend-color {
...@@ -83,7 +98,7 @@ ...@@ -83,7 +98,7 @@
position: absolute; position: absolute;
left: 0; left: 0;
right: 0; right: 0;
border-top: 1px solid #e0e0e0; border-top: 1px dashed rgba(224,224,224,0.3);
} }
.chart-column { .chart-column {
...@@ -110,15 +125,17 @@ ...@@ -110,15 +125,17 @@
width: 15px; /* 减小柱状图的宽度 */ width: 15px; /* 减小柱状图的宽度 */
position: relative; position: relative;
border-radius: 4px 4px 0 0; border-radius: 4px 4px 0 0;
transition: height 0.3s ease; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
background: linear-gradient(180deg, rgba(66,133,244,0.9), rgba(66,133,244,0.7));
box-shadow: 0 4px 12px rgba(66,133,244,0.2);
} }
.input-bar { .input-bar {
background-color: #4285f4; background: linear-gradient(180deg, #4285f4, #2b579a);
} }
.output-bar { .output-bar {
background-color: #7c4dff; background: linear-gradient(180deg, #7c4dff, #4a2b9a);
margin-left: 5px; margin-left: 5px;
} }
...@@ -145,6 +162,12 @@ ...@@ -145,6 +162,12 @@
width: 20px; /* 缩小Logo尺寸 */ width: 20px; /* 缩小Logo尺寸 */
height: 20px; height: 20px;
object-fit: contain; object-fit: contain;
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1));
transition: transform 0.3s ease;
}
.provider-logo:hover {
transform: scale(1.1);
} }
.provider-name { .provider-name {
...@@ -164,14 +187,15 @@ ...@@ -164,14 +187,15 @@
} }
.pricing-charts-container { .pricing-charts-container {
padding: 20px; padding: 2rem;
background-color: #f9f9f9; background: var(--bg-primary);
min-height: 100vh;
} }
.update-time { .update-time {
text-align: right; background: rgba(255,255,255,0.05);
font-size: 0.9rem; padding: 0.5rem 1rem;
color: #555; border-radius: 8px;
margin-bottom: 15px; margin-bottom: 2rem;
font-style: italic; display: inline-block;
} }
.container { .container {
font-family: 'Helvetica Neue', Arial, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6; line-height: 1.6;
color: #333; max-width: 1000px;
max-width: 800px;
margin: 0 auto; margin: 0 auto;
padding: 20px; padding: 2rem;
background-color: #ffffff; position: relative;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.9), rgba(255, 255, 255, 0.95));
min-height: 100vh;
}
.container::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;
} }
.timeline-title { .timeline-title {
text-align: center; text-align: center;
color: #4560f5; font-size: 2.5rem;
margin-bottom: 40px; margin-bottom: 3rem;
font-size: 60px; background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: 700;
letter-spacing: -0.02em;
} }
.timeline { .timeline {
...@@ -21,40 +41,51 @@ ...@@ -21,40 +41,51 @@
list-style: none; list-style: none;
} }
.timeline:before { .timeline::before {
content: ""; content: "";
position: absolute; position: absolute;
top: 0; top: 0;
left: 50%; left: 50%;
width: 2px; width: 2px;
height: 100%; height: 100%;
background: #4560f5; background: linear-gradient(180deg, #6366F1 0%, #4F46E5 100%);
transform: translateX(-50%);
} }
.event { .event {
position: relative; position: relative;
margin-bottom: 50px; margin-bottom: 3rem;
} }
.event:before { .event::before {
content: ""; content: "";
position: absolute; position: absolute;
top: 0;
left: 50%; left: 50%;
width: 20px; width: 20px;
height: 20px; height: 20px;
background: #4560f5; background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
border-radius: 50%; border-radius: 50%;
transform: translateX(-50%); transform: translateX(-50%);
box-shadow: 0 0 20px rgba(99, 102, 241, 0.3);
z-index: 2;
} }
.event-content { .event-content {
position: relative; position: relative;
width: 45%; width: 45%;
padding: 15px; padding: 1.5rem;
background: #fff; background: rgba(255, 255, 255, 0.9);
border-radius: 5px; backdrop-filter: blur(10px);
box-shadow: 0 3px 10px rgba(0,0,0,0.1); border-radius: 16px;
box-shadow: 0 8px 32px rgba(99, 102, 241, 0.1);
border: 1px solid rgba(99, 102, 241, 0.1);
transition: all 0.3s ease;
}
.event-content:hover {
transform: translateY(-5px);
box-shadow: 0 12px 24px rgba(99, 102, 241, 0.15);
border-color: rgba(99, 102, 241, 0.3);
} }
.event:nth-child(odd) .event-content { .event:nth-child(odd) .event-content {
...@@ -62,27 +93,51 @@ ...@@ -62,27 +93,51 @@
} }
.event:nth-child(even) .event-content { .event:nth-child(even) .event-content {
left: 0%; left: 0;
} }
.event-date { .event-date {
font-weight: bold; font-weight: 600;
color: #4560f5; color: #6366F1;
margin-bottom: 5px; margin-bottom: 0.5rem;
font-size: 1.1rem;
} }
.event-title { .event-title {
font-weight: bold; font-weight: 700;
margin-bottom: 5px; margin-bottom: 0.5rem;
font-size: 1.3rem;
color: #1a1a1a;
}
.event-feature {
color: #4F46E5;
margin-bottom: 0.5rem;
font-size: 1rem;
font-weight: 500;
} }
.event-description { .event-description {
font-size: 0.9em; color: #4B5563;
color: #666; font-size: 0.95rem;
margin-top: 5px; line-height: 1.6;
} }
.event-feature {
font-style: italic; @media (max-width: 768px) {
color: #888; .timeline::before {
margin-top: 3px; left: 30px;
}
.event::before {
left: 30px;
}
.event-content {
width: calc(100% - 80px);
left: 80px !important;
}
.timeline-title {
font-size: 2rem;
}
} }
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment