Commit 2d5293e7 authored by 青山's avatar 青山

新增登录云函数接口

parent 85f9803e
......@@ -8,6 +8,7 @@
"preview": "vite preview"
},
"dependencies": {
"@cloudbase/js-sdk": "^2.17.4",
"@imgly/background-removal": "^1.5.5",
"@react-oauth/google": "^0.12.1",
"@tailwindcss/typography": "^0.5.16",
......
......@@ -5,6 +5,7 @@ import Header from './components/Header';
import Footer from './components/Footer';
import NotFound from './pages/NotFound';
import Login from './pages/Login';
import PrivateRoute from './components/PrivateRoute';
const DevTools = lazy(() => import('./pages/DevTools'));
const ImageTools = lazy(() => import('./pages/ImageTools'));
......@@ -47,40 +48,255 @@ function App() {
<main>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/about" element={<About />} />
<Route path="/dev-tools" element={<DevTools />} />
<Route path="/image-tools" element={<ImageTools />} />
<Route path="/ai-products" element={<AIProduct />} />
<Route path="/blog" element={<Blog />} />
<Route path="/translator" element={<Translator />} />
<Route path="/document-translator" element={<DocumentTranslator />} />
<Route path="/markdown-to-image" element={<MarkdownToImage />} />
<Route path="/json-formatter" element={<JsonFormatter />} />
<Route path="/url-encode-and-decode" element={<UrlEnDecode />} />
<Route path="/openai-timeline" element={<OpenAITimeline />} />
<Route path="/anthropic-timeline" element={<AnthropicTimeline />} />
<Route path="/llm-model-price" element={<PricingCharts />} />
<Route path="/drugs-list" element={<DrugsList />} />
<Route path="/handwriting" element={<HandwriteGen />} />
<Route path="/image-base64" element={<ImageBase64Converter />} />
<Route path="/quote-card" element={<QuoteCard />} />
<Route path="/latex-to-image" element={<LatexToImage />} />
<Route path="/text-diff" element={<TextDiff />} />
<Route path="/subtitle-to-image" element={<SubtitleGenerator />} />
<Route path="/image-compressor" element={<ImageCompressor />} />
<Route path="/image-watermark" element={<ImageWatermark />} />
<Route path="/text-behind-image" element={<TextBehindImage />} />
<Route path="/background-remover" element={<BackgroundRemover />} />
<Route path="/id-photo-maker" element={<IDPhotoMaker />} />
<Route path="/deepseek-timeline" element={<DeepSeekTimeline />} />
<Route path="/wechat-formatter" element={<WechatFormatter />} />
<Route path="/image-annotator" element={<ImageAnnotator />} />
<Route path="/ai-timeline" element={<AITimelinePage />} />
<Route path="/perpetual-calendar" element={<PerpetualCalendar />} />
<Route
path="/"
element={
<PrivateRoute>
<Home />
</PrivateRoute>
}
/>
<Route
path="/about"
element={
<PrivateRoute>
<About />
</PrivateRoute>
}
/>
<Route
path="/dev-tools"
element={
<PrivateRoute>
<DevTools />
</PrivateRoute>
}
/>
<Route
path="/image-tools"
element={
<PrivateRoute>
<ImageTools />
</PrivateRoute>
}
/>
<Route
path="/ai-products"
element={
<PrivateRoute>
<AIProduct />
</PrivateRoute>
}
/>
<Route
path="/blog"
element={
<PrivateRoute>
<Blog />
</PrivateRoute>
}
/>
<Route
path="/translator"
element={
<PrivateRoute>
<Translator />
</PrivateRoute>
}
/>
<Route
path="/document-translator"
element={
<PrivateRoute>
<DocumentTranslator />
</PrivateRoute>
}
/>
<Route
path="/markdown-to-image"
element={
<PrivateRoute>
<MarkdownToImage />
</PrivateRoute>
}
/>
<Route
path="/json-formatter"
element={
<PrivateRoute>
<JsonFormatter />
</PrivateRoute>
}
/>
<Route
path="/url-encode-and-decode"
element={
<PrivateRoute>
<UrlEnDecode />
</PrivateRoute>
}
/>
<Route
path="/openai-timeline"
element={
<PrivateRoute>
<OpenAITimeline />
</PrivateRoute>
}
/>
<Route
path="/anthropic-timeline"
element={
<PrivateRoute>
<AnthropicTimeline />
</PrivateRoute>
}
/>
<Route
path="/llm-model-price"
element={
<PrivateRoute>
<PricingCharts />
</PrivateRoute>
}
/>
<Route
path="/drugs-list"
element={
<PrivateRoute>
<DrugsList />
</PrivateRoute>
}
/>
<Route
path="/handwriting"
element={
<PrivateRoute>
<HandwriteGen />
</PrivateRoute>
}
/>
<Route
path="/image-base64"
element={
<PrivateRoute>
<ImageBase64Converter />
</PrivateRoute>
}
/>
<Route
path="/quote-card"
element={
<PrivateRoute>
<QuoteCard />
</PrivateRoute>
}
/>
<Route
path="/latex-to-image"
element={
<PrivateRoute>
<LatexToImage />
</PrivateRoute>
}
/>
<Route
path="/text-diff"
element={
<PrivateRoute>
<TextDiff />
</PrivateRoute>
}
/>
<Route
path="/subtitle-to-image"
element={
<PrivateRoute>
<SubtitleGenerator />
</PrivateRoute>
}
/>
<Route
path="/image-compressor"
element={
<PrivateRoute>
<ImageCompressor />
</PrivateRoute>
}
/>
<Route
path="/image-watermark"
element={
<PrivateRoute>
<ImageWatermark />
</PrivateRoute>
}
/>
<Route
path="/text-behind-image"
element={
<PrivateRoute>
<TextBehindImage />
</PrivateRoute>
}
/>
<Route
path="/background-remover"
element={
<PrivateRoute>
<BackgroundRemover />
</PrivateRoute>
}
/>
<Route
path="/id-photo-maker"
element={
<PrivateRoute>
<IDPhotoMaker />
</PrivateRoute>
}
/>
<Route
path="/deepseek-timeline"
element={
<PrivateRoute>
<DeepSeekTimeline />
</PrivateRoute>
}
/>
<Route
path="/wechat-formatter"
element={
<PrivateRoute>
<WechatFormatter />
</PrivateRoute>
}
/>
<Route
path="/image-annotator"
element={
<PrivateRoute>
<ImageAnnotator />
</PrivateRoute>
}
/>
<Route
path="/ai-timeline"
element={
<PrivateRoute>
<AITimelinePage />
</PrivateRoute>
}
/>
<Route
path="/perpetual-calendar"
element={
<PrivateRoute>
<PerpetualCalendar />
</PrivateRoute>
}
/>
<Route path="*" element={<NotFound />} />
</Routes>
</Suspense>
......
......@@ -229,7 +229,7 @@ function Header() {
className="flex items-center space-x-3 focus:outline-none"
>
<img
src={user.picture}
src="https://th.bing.com/th/id/OIP.nl9qIl6NYdJmv6jeMd8H7gAAAA?rs=1&pid=ImgDetMain&cb=idpwebpc2"
alt="User 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"
/>
......
import React from 'react';
import { Navigate, useLocation } from 'react-router-dom';
/**
* 用于保护需要登录才能访问的页面
* 未登录时自动跳转到 /login,并记录原始跳转路径
*/
const PrivateRoute = ({ children }) => {
const location = useLocation();
const user = localStorage.getItem('user');
if (!user) {
// 记录跳转前的路径,登录后可重定向回来
sessionStorage.setItem('redirectAfterLogin', location.pathname + location.search);
return <Navigate to="/login" replace />;
}
return children;
};
export default PrivateRoute;
// main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import cloudbase from '@cloudbase/js-sdk';
const cloudbaseApp = cloudbase.init({
env: 'xingzhi-authing-5gkb8pggc4cf2edf',
});
window.cloudbase = cloudbaseApp;
import { BrowserRouter as Router } from 'react-router-dom';
import { GoogleOAuthProvider } from '@react-oauth/google';
import App from './App';
......
// src/pages/Login.jsx
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { GoogleLogin } from '@react-oauth/google';
import { useTranslation } from '../js/i18n';
import '../styles/Login.css';
......@@ -9,46 +8,73 @@ const Login = () => {
const navigate = useNavigate();
const { t } = useTranslation();
const handleLoginSuccess = (credentialResponse) => {
const { credential } = credentialResponse;
const base64Url = credential.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const decodedPayload = JSON.parse(window.atob(base64));
// Save user data to localStorage
localStorage.setItem('user', JSON.stringify(decodedPayload));
// Check if there's a redirect path saved
const redirectPath = sessionStorage.getItem('redirectAfterLogin');
// 使用直接的 window.location 跳转而不是 React Router 导航
// 这样可以确保页面完全重新加载,刷新登录状态
if (redirectPath) {
sessionStorage.removeItem('redirectAfterLogin');
window.location.href = redirectPath;
// 新增:用户名密码登录相关状态
const [username, setUsername] = useState('testuser');
const [password, setPassword] = useState('testpass');
const [loginLoading, setLoginLoading] = useState(false);
const [loginError, setLoginError] = useState('');
const [rememberMe, setRememberMe] = useState(false);
// 新增:腾讯云开发用户名密码登录
const handleCloudbaseLogin = async (e) => {
e.preventDefault();
setLoginError('');
setLoginLoading(true);
// 记住密码逻辑
if (rememberMe) {
localStorage.setItem('rememberedLogin', JSON.stringify({ username, password }));
} else {
window.location.href = '/';
localStorage.removeItem('rememberedLogin');
}
};
const handleLoginError = () => {
console.log('Login failed');
};
const app = cloudbase.init({
env: 'xingzhi-authing-5gkb8pggc4cf2edf',
});
useEffect(() => {
// Redirect if user is already logged in
const userData = localStorage.getItem('user');
if (userData) {
// Check if there's a redirect path saved
const redirectPath = sessionStorage.getItem('redirectAfterLogin');
if (redirectPath) {
sessionStorage.removeItem('redirectAfterLogin');
window.location.href = redirectPath;
} else {
try {
if (!app) {
setLoginError('CloudBase SDK 未初始化');
setLoginLoading(false);
return;
}
// 0. 先进行匿名登录,避免 "you can't request without auth" 报错
const auth = app.auth();
await auth.signInAnonymously();
// 1. 调用云函数 login 校验用户名密码,返回 ticket
const res = await app.callFunction({
name: 'login',
data: {
username,
password,
},
});
const { code } = res.result || {};
if (code == 200) {
localStorage.setItem('user', JSON.stringify({ username }));
window.location.href = '/';
} else {
setLoginError('用户名或密码错误');
}
} catch (err) {
setLoginError(err.message || '用户名或密码错误');
setLoginLoading(false);
}
}, [navigate]);
};
// 记住密码功能:初始化时自动填充
useEffect(() => {
const saved = localStorage.getItem('rememberedLogin');
if (saved) {
try {
const { username: savedUser, password: savedPass } = JSON.parse(saved);
setUsername(savedUser || '');
setPassword(savedPass || '');
setRememberMe(true);
} catch {}
}
}, []);
return (
<div className="login-container">
......@@ -58,19 +84,44 @@ const Login = () => {
{t('loginSubtitle', '欢迎使用 AI 工具箱,请登录以获得完整体验')}
</p>
<div className="login-options">
<div className="google-login-wrapper">
<GoogleLogin
onSuccess={handleLoginSuccess}
onError={handleLoginError}
theme="outline"
size="large"
width="100%"
text="signin_with"
shape="rectangular"
locale="zh_CN"
useOneTap
{/* 腾讯云开发用户名密码登录表单 */}
<form className="cloudbase-login-form" onSubmit={handleCloudbaseLogin}>
<input
type="text"
placeholder="用户名"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
className="login-input enhanced-input"
autoComplete="username"
/>
<input
type="password"
placeholder="密码"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="login-input enhanced-input"
autoComplete="current-password"
/>
</div>
<label style={{ display: 'flex', alignItems: 'center', marginBottom: '0.5rem', fontSize: '0.98rem', color: '#6B7280', userSelect: 'none' }}>
<input
type="checkbox"
checked={rememberMe}
onChange={e => setRememberMe(e.target.checked)}
style={{ marginRight: '8px', accentColor: '#6366f1' }}
/>
记住密码
</label>
<button
type="submit"
className="login-btn enhanced-btn"
disabled={loginLoading}
>
{loginLoading ? '登录中...' : '用户名密码登录'}
</button>
{loginError && <div className="login-error">{loginError}</div>}
</form>
</div>
</div>
</div>
......
......@@ -7,6 +7,64 @@
background: linear-gradient(135deg, #f8faff 0%, #f3f6ff 100%);
padding: 1rem;
margin-top: -44px;
}
/* 美化用户名密码登录输入框和按钮 */
.cloudbase-login-form {
display: flex;
flex-direction: column;
gap: 1.2rem;
margin-top: 0.5rem;
}
.enhanced-input {
width: 100%;
padding: 12px 16px;
font-size: 1rem;
border: 1.5px solid #c7d2fe;
border-radius: 8px;
background: #f3f6ff;
box-shadow: 0 2px 8px rgba(99, 102, 241, 0.06);
transition: border 0.2s, box-shadow 0.2s;
outline: none;
}
.enhanced-input:focus {
border-color: #6366f1;
background: #fff;
box-shadow: 0 4px 16px rgba(99, 102, 241, 0.13);
}
.enhanced-btn {
width: 100%;
padding: 12px 0;
font-size: 1.08rem;
font-weight: 600;
color: #fff;
background: linear-gradient(90deg, #6366f1 0%, #4f46e5 100%);
border: none;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(99, 102, 241, 0.10);
cursor: pointer;
transition: background 0.2s, transform 0.15s, box-shadow 0.2s;
letter-spacing: 0.02em;
}
.enhanced-btn:hover, .enhanced-btn:focus {
background: linear-gradient(90deg, #4f46e5 0%, #6366f1 100%);
transform: translateY(-2px) scale(1.03);
box-shadow: 0 6px 24px rgba(99, 102, 241, 0.18);
}
.login-error {
color: #ef4444;
margin-top: 0.5rem;
font-size: 0.98rem;
background: #fef2f2;
border-radius: 6px;
padding: 0.5rem 0.8rem;
border: 1px solid #fecaca;
box-shadow: 0 1px 4px rgba(239, 68, 68, 0.06);
}
.login-card {
......@@ -108,4 +166,4 @@
.login-title {
font-size: 1.5rem;
}
}
\ No newline at end of file
}
This source diff could not be displayed because it is too large. You can view the blob instead.
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