- 初始化项目
- 添加登录注册功能
Change-Id: I4ceb5400dca3042f2f31232eaf246df83d57b9be
登录注册
Change-Id: Ibd4868d02f2f2b51b9cf645c5b56cb31adae6a1d
登录注册
Change-Id: Iee4aca2a0871ab46a95208ece13053e92b615b2e
login and register
Change-Id: Idb1ca43081e39c982a508b36ab1d80907b63a412
diff --git a/src/components/Auth/Auth.css b/src/components/Auth/Auth.css
new file mode 100644
index 0000000..5073ce3
--- /dev/null
+++ b/src/components/Auth/Auth.css
@@ -0,0 +1,96 @@
+.auth-container {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end; /* 使卡片靠右 */
+ min-height: 100vh;
+ font-family: Arial, sans-serif;
+ background-color: #5F4437; /* 设置背景颜色 */
+ padding: 0 2rem; /* 添加左右内边距 */
+}
+
+.auth-container img {
+ margin-left: 250px;
+}
+
+.auth-form-section {
+ background: #E4D8C9; /* 米白色 */
+ padding: 2rem;
+ border-radius: 10px;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+ width: 300px;
+ margin: 0 auto;
+ max-width: 400px; /* 限制卡片最大宽度 */
+}
+
+.form-group {
+ margin-bottom: 1.2rem;
+ display: flex;
+ flex-direction: column;
+}
+
+label {
+ display: block;
+ font-size: 0.9rem;
+ margin-bottom: 0.5rem;
+ color: #4A3B34; /* 棕色 */
+}
+
+.form-input {
+ width: 100%;
+ padding: 0.8rem;
+ border: 1px solid #ddd;
+ border-radius: 5px;
+ font-size: 0.9rem;
+ box-sizing: border-box;
+}
+
+.auth-button {
+ padding: 0.8rem 8.4rem;
+ background: #BA929A; /* 粉色 */
+ color: #4A3B34; /* 棕色 */
+ border: none;
+ border-radius: 5px;
+ cursor: pointer;
+ font-size: 0.9rem;
+ display: inline-block;
+}
+
+.verify-button {
+ padding: 0.5rem 1rem; /* 更小的内边距 */
+ background: #BA929A; /* 粉色 */
+ color: #4A3B34; /* 棕色 */
+ border: none;
+ border-radius: 5px;
+ cursor: pointer;
+ font-size: 0.8rem; /* 更小的字体 */
+ display: inline-block;
+}
+
+.link-button {
+ background: none;
+ border: none;
+ color: #4A3B34; /* 棕色 */
+ /* text-decoration: underline; */
+ cursor: pointer;
+ font-size: 0.8rem;
+ padding: 0;
+}
+
+.forgot-password {
+ position: absolute;
+ right: 10px; /* 让按钮靠右 */
+ bottom: 5px; /* 调整到底部 */
+ font-size: 12px;
+ background: none;
+ border: none;
+ color: #4A3B34; /* 颜色与 "点击注册" 一致 */
+ cursor: pointer;
+ text-decoration: underline;
+}
+
+
+.register-link, .login-link {
+ text-align: center;
+ font-size: 0.8rem;
+ margin-top: 1rem;
+}
\ No newline at end of file
diff --git a/src/components/Auth/Login.jsx b/src/components/Auth/Login.jsx
new file mode 100644
index 0000000..e7495b5
--- /dev/null
+++ b/src/components/Auth/Login.jsx
@@ -0,0 +1,95 @@
+import React, { useState } from 'react';
+import './Auth.css';
+import image from './logo.svg'; // 引入图片
+const Login = ({ onRegisterClick }) => {
+ const [formData, setFormData] = useState({
+ username: '',
+ password: ''
+ });
+
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+ setFormData(prev => ({ ...prev, [name]: value }));
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ try {
+ const response = await fetch('http://localhost:8080/user/login', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(formData),
+ });
+
+ const result = await response.json();
+ if (response.ok && result.code === "0") {
+ console.log('登录成功:', result);
+ // 处理成功逻辑
+ } else {
+ console.error('登录失败:', result);
+ // 处理失败逻辑
+ }
+ } catch (error) {
+ console.error('请求错误:', error);
+ // 处理请求错误
+ }
+ };
+
+ return (
+ <div className="auth-container">
+ <img
+ src={image}
+ alt="描述"
+ style={{ width: '30%', height: 'auto'}}
+ />
+ <div className="auth-form-section">
+ {/* <h3>欢迎来到EchoTorent !</h3> */}
+ <h3>用户登录</h3>
+ <form onSubmit={handleSubmit}>
+ <div className="form-group">
+ <label>用户名</label>
+ <input
+ type="text"
+ name="username"
+ placeholder="请输入用户名"
+ value={formData.username}
+ onChange={handleChange}
+ className="form-input"
+ />
+ </div>
+ <div className="form-group">
+ <label>密码</label>
+ <input
+ type="password"
+ name="password"
+ placeholder="请输入密码"
+ value={formData.password}
+ onChange={handleChange}
+ className="form-input"
+ />
+ <button
+ type="button"
+ className="link-button forgot-password"
+ onClick={() => console.log('跳转到忘记密码页面')}
+ >
+ 忘记密码?
+ </button>
+ </div>
+ <button type="submit" className="auth-button">
+ 登录
+ </button>
+ <p className="register-link">
+ 没有账号?
+ <button onClick={onRegisterClick} className="link-button">
+ 点击注册
+ </button>
+ </p>
+ </form>
+ </div>
+ </div>
+ );
+};
+
+export default Login;
\ No newline at end of file
diff --git a/src/components/Auth/Login.test.jsx b/src/components/Auth/Login.test.jsx
new file mode 100644
index 0000000..6da7659
--- /dev/null
+++ b/src/components/Auth/Login.test.jsx
@@ -0,0 +1,28 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import Login from './Login';
+
+describe('Login component', () => {
+ test('renders login form', () => {
+ const onRegisterClick = jest.fn();
+ render(<Login onRegisterClick={onRegisterClick} />);
+
+ const usernameInput = screen.getByPlaceholderText('请输入用户名');
+ const passwordInput = screen.getByPlaceholderText('请输入密码');
+ const loginButton = screen.getByText('登录');
+
+ expect(usernameInput).toBeInTheDocument();
+ expect(passwordInput).toBeInTheDocument();
+ expect(loginButton).toBeInTheDocument();
+ });
+
+ test('calls onRegisterClick when "点击注册" is clicked', () => {
+ const onRegisterClick = jest.fn();
+ render(<Login onRegisterClick={onRegisterClick} />);
+
+ const registerButton = screen.getByText('点击注册');
+ fireEvent.click(registerButton);
+
+ expect(onRegisterClick).toHaveBeenCalled();
+ });
+});
\ No newline at end of file
diff --git a/src/components/Auth/Register.jsx b/src/components/Auth/Register.jsx
new file mode 100644
index 0000000..417864f
--- /dev/null
+++ b/src/components/Auth/Register.jsx
@@ -0,0 +1,121 @@
+import React, { useState } from 'react';
+import './Auth.css';
+import image from './logo.svg'; // 引入图片
+
+const Register = ({ onLoginClick }) => {
+ const [formData, setFormData] = useState({
+ username: '',
+ password: '',
+ email: ''
+ });
+ const [verificationCode, setVerificationCode] = useState('');
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ // 注册逻辑
+ };
+
+ const verifyEmailCode = async () => {
+ try {
+ const response = await fetch('http://localhost:8080/user/verify-code', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ email: formData.email,
+ code: verificationCode,
+ }),
+ });
+
+ const result = await response.json();
+ if (response.ok && result.code === "0") {
+ console.log('邮箱验证成功:', result.msg);
+ // 处理成功逻辑
+ } else {
+ console.error('邮箱验证失败:', result.msg);
+ // 处理失败逻辑
+ }
+ } catch (error) {
+ console.error('请求错误:', error);
+ // 处理请求错误
+ }
+ };
+
+
+ return (
+ <div className="auth-container">
+ <img
+ src={image}
+ alt="描述"
+ style={{ width: '30%', height: 'auto'}}
+ />
+ <div className="auth-form-section">
+ <h3>用户注册</h3>
+ <form onSubmit={handleSubmit}>
+ <div className="form-group">
+ <label>用户名</label>
+ <input
+ type="text"
+ className="form-input"
+ placeholder="请输入用户名"
+ value={formData.username}
+ onChange={(e) => setFormData({ ...formData, username: e.target.value })}
+ required
+ />
+ </div>
+ <div className="form-group">
+ <label>密码</label>
+ <input
+ type="password"
+ className="form-input"
+ placeholder="请输入密码"
+ value={formData.password}
+ onChange={(e) => setFormData({ ...formData, password: e.target.value })}
+ required
+ />
+ </div>
+ <div className="form-group">
+ <label>邮箱</label>
+ <div style={{ display: 'flex', alignItems: 'center' }}>
+ <input
+ type="email"
+ className="form-input"
+ placeholder="请输入邮箱"
+ value={formData.email}
+ onChange={(e) => setFormData({ ...formData, email: e.target.value })}
+ required
+ style={{ flex: 1, marginRight: '10px' }}
+ />
+ <button type="button" onClick={verifyEmailCode} className="verify-button">
+ 验证邮箱
+ </button>
+ </div>
+ </div>
+ <div className="form-group">
+ <label>验证码</label>
+ <input
+ type="text"
+ className="form-input"
+ placeholder="请输入邮箱验证码"
+ value={verificationCode}
+ onChange={(e) => setVerificationCode(e.target.value)}
+ required
+ />
+ </div>
+ <button type="submit" className="auth-button">
+ 注册
+ </button>
+ <p className="login-link">
+ 已有账号?
+ <button onClick={onLoginClick} className="link-button">
+ 点击登录
+ </button>
+ </p>
+ </form>
+ </div>
+ </div>
+ );
+};
+
+export default Register;
\ No newline at end of file
diff --git a/src/components/Auth/Register.test.jsx b/src/components/Auth/Register.test.jsx
new file mode 100644
index 0000000..969e464
--- /dev/null
+++ b/src/components/Auth/Register.test.jsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import Register from './Register';
+
+describe('Register component', () => {
+ test('renders register form', () => {
+ const onLoginClick = jest.fn();
+ render(<Register onLoginClick={onLoginClick} />);
+
+ const usernameInput = screen.getByPlaceholderText('请输入用户名');
+ const passwordInput = screen.getByPlaceholderText('请输入密码');
+ const emailInput = screen.getByPlaceholderText('请输入邮箱');
+ const verifyButton = screen.getByText('验证邮箱');
+ const registerButton = screen.getByText('注册');
+
+ expect(usernameInput).toBeInTheDocument();
+ expect(passwordInput).toBeInTheDocument();
+ expect(emailInput).toBeInTheDocument();
+ expect(verifyButton).toBeInTheDocument();
+ expect(registerButton).toBeInTheDocument();
+ });
+
+ test('calls onLoginClick when "点击登录" is clicked', () => {
+ const onLoginClick = jest.fn();
+ render(<Register onLoginClick={onLoginClick} />);
+
+ const loginButton = screen.getByText('点击登录');
+ fireEvent.click(loginButton);
+
+ expect(onLoginClick).toHaveBeenCalled();
+ });
+});
\ No newline at end of file
diff --git a/src/components/Auth/logo.svg b/src/components/Auth/logo.svg
new file mode 100644
index 0000000..38d62ff
--- /dev/null
+++ b/src/components/Auth/logo.svg
@@ -0,0 +1,24 @@
+<svg width="81" height="81" viewBox="0 0 81 81" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M40.5 6.74951V74.2496" stroke="#FBF2E3" stroke-width="4" stroke-linecap="round"/>
+<path d="M57.375 20.2498V60.7498" stroke="#FBF2E3" stroke-width="4" stroke-linecap="round"/>
+<path d="M6.75 30.3748V50.6248" stroke="#FBF2E3" stroke-width="4" stroke-linecap="round"/>
+<path d="M74.25 30.3748V50.6248" stroke="#FBF2E3" stroke-width="4" stroke-linecap="round"/>
+<path d="M23.625 20.2498V60.7498" stroke="#FBF2E3" stroke-width="4" stroke-linecap="round"/>
+<g clip-path="url(#clip0_193_2)">
+<mask id="mask0_193_2" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="28" y="27" width="26" height="26">
+<path d="M54 27H28V53H54V27Z" fill="white"/>
+</mask>
+<g mask="url(#mask0_193_2)">
+<path d="M30.1665 43.7918C30.1666 40.271 32.3334 38.937 33.9583 38.3852C35.5831 37.8335 36.6667 37.8335 38.2915 36.2085C39.9163 34.5835 39.9168 31.3335 43.1666 30.2501C46.4163 29.1668 50.2575 30.7918 51.2914 34.5835C52.3253 38.3751 49.3961 43.2501 48.0419 44.8751C46.6877 46.5001 43.9784 49.2085 39.3747 49.7501C34.7711 50.2918 30.1664 47.3126 30.1665 43.7918Z" fill="#4A3B34" stroke="#FBF2E3" stroke-width="2" stroke-linejoin="round"/>
+<path d="M34.5 38.3986C38.2917 43.25 43.7083 38.3986 39.9167 33.5Z" fill="#4A3B34"/>
+<path d="M34.5 38.3986C38.2917 43.25 43.7083 38.3986 39.9167 33.5" stroke="#FBF2E3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M33.9585 38.3851C35.5833 37.8333 36.667 37.8333 38.2918 36.2083C39.9166 34.5833 39.917 31.3333 43.1668 30.25" fill="#4A3B34"/>
+<path d="M33.9585 38.3851C35.5833 37.8333 36.667 37.8333 38.2918 36.2083C39.9166 34.5833 39.917 31.3333 43.1668 30.25" stroke="#FBF2E3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+</g>
+<defs>
+<clipPath id="clip0_193_2">
+<rect width="26" height="26" fill="white" transform="translate(28 27)"/>
+</clipPath>
+</defs>
+</svg>