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",
......
This diff is collapsed.
......@@ -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));
// 新增:用户名密码登录相关状态
const [username, setUsername] = useState('testuser');
const [password, setPassword] = useState('testpass');
const [loginLoading, setLoginLoading] = useState(false);
const [loginError, setLoginError] = useState('');
const [rememberMe, setRememberMe] = useState(false);
// Save user data to localStorage
localStorage.setItem('user', JSON.stringify(decodedPayload));
// Check if there's a redirect path saved
const redirectPath = sessionStorage.getItem('redirectAfterLogin');
// 新增:腾讯云开发用户名密码登录
const handleCloudbaseLogin = async (e) => {
e.preventDefault();
setLoginError('');
setLoginLoading(true);
// 使用直接的 window.location 跳转而不是 React Router 导航
// 这样可以确保页面完全重新加载,刷新登录状态
if (redirectPath) {
sessionStorage.removeItem('redirectAfterLogin');
window.location.href = redirectPath;
// 记住密码逻辑
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"
/>
</div>
<input
type="password"
placeholder="密码"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="login-input enhanced-input"
autoComplete="current-password"
/>
<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>
......
......@@ -9,6 +9,64 @@
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 {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
......
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