Commit e321ffa6 authored by fisherdaddy's avatar fisherdaddy

feature: add google login

parent 60a3d72a
...@@ -8,8 +8,10 @@ ...@@ -8,8 +8,10 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@react-oauth/google": "^0.12.1",
"antd": "^5.21.6", "antd": "^5.21.6",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"jwt-decode": "^4.0.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
......
...@@ -4,6 +4,7 @@ import Home from './pages/Home'; ...@@ -4,6 +4,7 @@ import Home from './pages/Home';
import Header from './components/Header'; import Header from './components/Header';
import Footer from './components/Footer'; import Footer from './components/Footer';
import NotFound from './pages/NotFound'; import NotFound from './pages/NotFound';
import Login from './pages/Login';
const JsonFormatter = lazy(() => import('./components/JsonFormatter')); const JsonFormatter = lazy(() => import('./components/JsonFormatter'));
const TextToImage = lazy(() => import('./components/TextToImage')); const TextToImage = lazy(() => import('./components/TextToImage'));
...@@ -25,6 +26,7 @@ function App() { ...@@ -25,6 +26,7 @@ function App() {
<Suspense fallback={<div>Loading...</div>}> <Suspense fallback={<div>Loading...</div>}>
<Routes> <Routes>
<Route path="/" element={<Home />} /> <Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/text2image" element={<TextToImage />} /> <Route path="/text2image" element={<TextToImage />} />
<Route path="/json-formatter" element={<JsonFormatter />} /> <Route path="/json-formatter" element={<JsonFormatter />} />
<Route path="/url-decode" element={<UrlDecode />} /> <Route path="/url-decode" element={<UrlDecode />} />
...@@ -36,6 +38,7 @@ function App() { ...@@ -36,6 +38,7 @@ function App() {
<Route path="/image-base64" element={<ImageBase64Converter />} /> <Route path="/image-base64" element={<ImageBase64Converter />} />
<Route path="/quote-card" element={<QuoteCard />} /> <Route path="/quote-card" element={<QuoteCard />} />
<Route path="*" element={<NotFound />} /> <Route path="*" element={<NotFound />} />
</Routes> </Routes>
</Suspense> </Suspense>
</main>- </main>-
......
// src/components/Header.jsx
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import { Link, 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'; // 确保创建并导入这个CSS文件 import '../styles/Header.css';
// 导入 logo 图片 import logo from '/assets/logo.png';
import logo from '/assets/logo.png'; // 请确保路径正确
function Header() { function Header() {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate();
const user = JSON.parse(localStorage.getItem('user'));
const handleLogout = () => {
localStorage.removeItem('user');
navigate('/login');
};
return ( return (
<header> <header>
...@@ -18,10 +25,24 @@ function Header() { ...@@ -18,10 +25,24 @@ function Header() {
{t('title')} {t('title')}
</Link> </Link>
</div> </div>
<LanguageSelector /> <div className="right-container">
<LanguageSelector />
<div className="auth-container">
{user ? (
<div className="user-info">
<span>
{t('welcome')}, {user.name || user.given_name}
</span>
<button onClick={handleLogout}>{t('logout')}</button>
</div>
) : (
<Link to="/login">{t('login')}</Link>
)}
</div>
</div>
</nav> </nav>
</header> </header>
); );
} }
export default Header; export default Header;
\ No newline at end of file
...@@ -4,6 +4,9 @@ ...@@ -4,6 +4,9 @@
"description": "AI Toolbox - A collection of AI tools including text cards, JSON formatter, URL decoder, OpenAI product releases summary, and global model price comparisons to help you accomplish various tasks effortlessly.", "description": "AI Toolbox - A collection of AI tools including text cards, JSON formatter, URL decoder, OpenAI product releases summary, and global model price comparisons to help you accomplish various tasks effortlessly.",
"slogan": "Your intelligent development toolset, a one-stop solution for all AI tool needs.", "slogan": "Your intelligent development toolset, a one-stop solution for all AI tool needs.",
"keywords": "AI Toolbox, AI tools, text cards, JSON formatter, URL decoder, OpenAI products, model price comparison, online tools, free tools", "keywords": "AI Toolbox, AI tools, text cards, JSON formatter, URL decoder, OpenAI products, model price comparison, online tools, free tools",
"welcome": "Welcome",
"login": "Login",
"logout": "Logout",
"tools": { "tools": {
"text2image": { "text2image": {
"title": "Text to Image Card", "title": "Text to Image Card",
...@@ -93,6 +96,9 @@ ...@@ -93,6 +96,9 @@
"description": "AI工具箱 - 集合了多种 AI 工具,如文字卡片、JSON 格式化、URL 解码器、OpenAI 产品发布汇总、全球各大模型价格对比,帮助您轻松完成各类任务。", "description": "AI工具箱 - 集合了多种 AI 工具,如文字卡片、JSON 格式化、URL 解码器、OpenAI 产品发布汇总、全球各大模型价格对比,帮助您轻松完成各类任务。",
"slogan": "您的智能开发工具集合,一站式解决各种 AI 工具需求。", "slogan": "您的智能开发工具集合,一站式解决各种 AI 工具需求。",
"keywords": "AI工具箱,AI 工具,文字卡片,JSON 格式化,URL 解码器,OpenAI 产品,模型价格对比,在线工具,免费工具", "keywords": "AI工具箱,AI 工具,文字卡片,JSON 格式化,URL 解码器,OpenAI 产品,模型价格对比,在线工具,免费工具",
"welcome": "欢迎",
"login": "登录",
"logout": "退出登录",
"tools": { "tools": {
"text2image": { "text2image": {
"title": "文字卡片", "title": "文字卡片",
...@@ -182,6 +188,9 @@ ...@@ -182,6 +188,9 @@
"description": "AIツールボックス - テキストカード、JSONフォーマッター、URLデコーダー、OpenAI製品のリリースまとめ、世界のモデル価格比較など、多様なAIツールを集めたサイトです。さまざまなタスクを簡単にこなせます。", "description": "AIツールボックス - テキストカード、JSONフォーマッター、URLデコーダー、OpenAI製品のリリースまとめ、世界のモデル価格比較など、多様なAIツールを集めたサイトです。さまざまなタスクを簡単にこなせます。",
"slogan": "あなたのスマートな開発ツールセット、あらゆるAIツールのニーズをワンストップで解決します。", "slogan": "あなたのスマートな開発ツールセット、あらゆるAIツールのニーズをワンストップで解決します。",
"keywords": "AIツールボックス、AIツール、テキストカード、JSONフォーマッター、URLデコーダー、OpenAI製品、モデル価格比較、オンラインツール、無料ツール", "keywords": "AIツールボックス、AIツール、テキストカード、JSONフォーマッター、URLデコーダー、OpenAI製品、モデル価格比較、オンラインツール、無料ツール",
"welcome": "ようこそ",
"login": "ログイン",
"logout": "ログアウト",
"tools": { "tools": {
"text2image": { "text2image": {
"title": "テキストから画像", "title": "テキストから画像",
...@@ -271,6 +280,9 @@ ...@@ -271,6 +280,9 @@
"description": "AI 도구상자 - 텍스트 카드, JSON 포매터, URL 디코더, OpenAI 제품 출시 요약, 글로벌 모델 가격 비교 등 다양한 AI 도구를 모아 다양한 작업을 손쉽게 수행할 수 있도록 도와드립니다.", "description": "AI 도구상자 - 텍스트 카드, JSON 포매터, URL 디코더, OpenAI 제품 출시 요약, 글로벌 모델 가격 비교 등 다양한 AI 도구를 모아 다양한 작업을 손쉽게 수행할 수 있도록 도와드립니다.",
"slogan": "당신의 지능형 개발 도구 모음, 모든 AI 도구 요구 사항을 원스톱으로 해결합니다.", "slogan": "당신의 지능형 개발 도구 모음, 모든 AI 도구 요구 사항을 원스톱으로 해결합니다.",
"keywords": "AI 도구상자, AI 도구, 텍스트 카드, JSON 포매터, URL 디코더, OpenAI 제품, 모델 가격 비교, 온라인 도구, 무료 도구", "keywords": "AI 도구상자, AI 도구, 텍스트 카드, JSON 포매터, URL 디코더, OpenAI 제품, 모델 가격 비교, 온라인 도구, 무료 도구",
"welcome": "환영합니다",
"login": "로그인",
"logout": "로그아웃",
"tools": { "tools": {
"text2image": { "text2image": {
"title": "텍스트를 이미지로", "title": "텍스트를 이미지로",
......
import React from 'react' // main.jsx
import ReactDOM from 'react-dom/client' import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom' import ReactDOM from 'react-dom/client';
import App from './App' import { BrowserRouter as Router } from 'react-router-dom';
import './styles/main.css' import { GoogleOAuthProvider } from '@react-oauth/google';
import App from './App';
import './styles/main.css';
ReactDOM.createRoot(document.getElementById('root')).render( ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode> <React.StrictMode>
<Router> <GoogleOAuthProvider clientId="100542027781-fsbqmfab5rmcjmvngu42bace31bn70d6.apps.googleusercontent.com">
<App /> <Router>
</Router> <App />
</Router>
</GoogleOAuthProvider>
</React.StrictMode> </React.StrictMode>
) );
// src/pages/Login.jsx
import React, { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { GoogleLogin } from '@react-oauth/google';
import * as jwt_decode from 'jwt-decode';
const Login = () => {
const navigate = useNavigate();
const handleLoginSuccess = (credentialResponse) => {
const { credential } = credentialResponse;
const decoded = jwt_decode.default(credential);
// 将用户信息保存到 localStorage 或上下文
localStorage.setItem('user', JSON.stringify(decoded));
navigate('/'); // 登录成功后重定向到首页
};
const handleLoginError = () => {
console.log('登录失败');
};
useEffect(() => {
const user = localStorage.getItem('user');
if (user) {
navigate('/'); // 如果已登录,直接跳转到首页
}
}, [navigate]);
return (
<div className="login-container">
<h1>登录</h1>
<GoogleLogin
onSuccess={handleLoginSuccess}
onError={handleLoginError}
/>
</div>
);
};
export default Login;
.no-underline { .no-underline {
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
} }
.no-underline:hover { .no-underline:hover {
text-decoration: none; text-decoration: none;
} }
.logo { .logo {
width: 40px; width: 40px;
height: 40px; height: 40px;
margin-right: 15px; margin-right: 15px;
object-fit: contain; object-fit: contain;
} }
.title {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', sans-serif;
font-size: 1.5rem;
font-weight: 500;
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 { .title {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', sans-serif; font-size: 1.3rem;
font-size: 1.5rem;
font-weight: 500;
letter-spacing: 0.02em;
color: #6366F1; /* 使用与图片中紫色渐变相近的颜色 */
transition: color 0.2s ease;
}
header nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
}
.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; /* 悬停时稍微深一点的紫色 */ header nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
}
.right-container {
display: flex;
align-items: center;
}
.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 > * {
@keyframes fadeIn { margin-left: 0;
from { opacity: 0; } margin-top: 10px;
to { opacity: 1; }
} }
}
/* 为移动设备优化 */
@media (max-width: 768px) {
.title {
font-size: 1.3rem;
}
}
\ 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