重构登录页面UI并完善项目文档
主要改动: - 重新设计登录页面,使用原生input替代antd-mobile组件以更好控制样式 - 添加密码可见性切换功能(眼睛图标) - 实现登录/注册切换UI,注册链接移至底部 - 优化输入框样式,添加明显的边框和背景色 - 实现移动端防缩放功能(touch-action + JS事件监听) - 配置Vite代理解决CORS问题,API请求使用相对路径 - 完善CLAUDE.md和README.md文档,添加文档同步更新规范 - 更新技术栈说明,明确使用yarn作为包管理工具 技术细节: - 渐变背景(紫色主题) - 白色输入框带半透明边框,聚焦时显示光晕效果 - 防止移动端双指缩放和双击缩放 - Vite代理配置: /api/* -> http://localhost:8080 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
92e8c9a94c
commit
9540478583
59
CLAUDE.md
59
CLAUDE.md
@ -2,6 +2,31 @@
|
||||
|
||||
本文件为 Claude Code (claude.ai/code) 在此代码仓库中工作时提供指导。
|
||||
|
||||
## 重要开发规范
|
||||
|
||||
### 文档同步更新规则
|
||||
**关键规则**: 当实现新功能、修改现有功能或更改项目配置时,**必须同步更新相关文档**。
|
||||
|
||||
- **README.md 更新时机**:
|
||||
- 添加新的核心功能或特性
|
||||
- 修改项目安装、配置或运行方式
|
||||
- 更改技术栈或主要依赖
|
||||
- 添加新的 API 端点或修改现有端点
|
||||
- 更新项目架构或目录结构
|
||||
|
||||
- **CLAUDE.md 更新时机**:
|
||||
- 添加新的开发规范或最佳实践
|
||||
- 修改项目配置(如代理、构建工具等)
|
||||
- 更改目录结构或文件组织方式
|
||||
- 引入新的工具或技术
|
||||
- 更新常用命令或开发流程
|
||||
|
||||
- **更新原则**:
|
||||
- 代码修改和文档更新应在同一次提交中完成
|
||||
- 文档要清晰、准确,反映当前代码状态
|
||||
- 使用 markdown 链接引用具体文件位置
|
||||
- 及时移除过时的说明和示例
|
||||
|
||||
## 项目概述
|
||||
|
||||
AnKao 是一个使用 **Gin 框架**构建的 Go Web 应用程序。该项目遵循整洁架构模式,具有清晰的关注点分离,并设计为支持数据库集成。
|
||||
@ -107,6 +132,34 @@ go test -v ./...
|
||||
|
||||
## 前端开发规范
|
||||
|
||||
### 包管理和开发
|
||||
**重要**: 前端项目使用 **yarn** 作为包管理工具。
|
||||
|
||||
- **包管理器**: 使用 `yarn` 而非 `npm`
|
||||
- **常用命令**:
|
||||
```bash
|
||||
# 安装依赖
|
||||
yarn install
|
||||
|
||||
# 启动开发服务器 (运行在 http://localhost:3000)
|
||||
yarn dev
|
||||
|
||||
# 构建生产版本
|
||||
yarn build
|
||||
|
||||
# 添加依赖
|
||||
yarn add <package-name>
|
||||
|
||||
# 添加开发依赖
|
||||
yarn add -D <package-name>
|
||||
```
|
||||
|
||||
### API 代理配置
|
||||
- **开发环境**: Vite 已配置代理,前端请求 `/api/*` 会被代理到后端 `http://localhost:8080`
|
||||
- **配置位置**: [web/vite.config.ts](web/vite.config.ts)
|
||||
- **使用方式**: 前端代码中使用相对路径调用API,例如 `fetch('/api/login')`
|
||||
- **后端服务器**: 确保后端服务运行在 `http://localhost:8080`
|
||||
|
||||
### UI 组件使用原则
|
||||
**重要**: 在开发前端页面时,必须优先使用 UI 框架的组件。
|
||||
|
||||
@ -125,4 +178,10 @@ go test -v ./...
|
||||
- **web/src/pages/** - 页面组件
|
||||
- **web/src/components/** - 可复用组件
|
||||
- **web/src/pages/*.module.less** - 页面样式文件 (CSS Modules)
|
||||
- **web/vite.config.ts** - Vite 配置文件(包含代理配置)
|
||||
|
||||
### 移动端适配
|
||||
- **禁止缩放**: 应用已配置防止移动端双指缩放
|
||||
- **响应式设计**: 优先考虑移动端布局和交互
|
||||
- **触摸优化**: 使用合适的触摸区域大小(最小 44x44px)
|
||||
|
||||
|
||||
86
README.md
86
README.md
@ -13,24 +13,54 @@ AnKao/
|
||||
│ └── models/ # 数据模型
|
||||
├── pkg/ # 公共库代码
|
||||
│ └── config/ # 配置管理
|
||||
├── web/ # 前端项目目录
|
||||
│ ├── src/ # 源代码
|
||||
│ │ ├── pages/ # 页面组件
|
||||
│ │ ├── components/ # 可复用组件
|
||||
│ │ └── main.tsx # 入口文件
|
||||
│ ├── vite.config.ts # Vite配置(含API代理)
|
||||
│ └── package.json # 前端依赖
|
||||
├── go.mod # Go模块文件
|
||||
├── CLAUDE.md # 开发规范文档
|
||||
└── README.md # 项目说明
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 运行服务器
|
||||
### 后端服务器
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
go mod tidy
|
||||
|
||||
# 运行服务器
|
||||
go run main.go
|
||||
```
|
||||
|
||||
服务器将在 `http://localhost:8080` 启动
|
||||
|
||||
### 前端开发服务器
|
||||
|
||||
```bash
|
||||
# 进入前端目录
|
||||
cd web
|
||||
|
||||
# 安装依赖 (使用 yarn)
|
||||
yarn install
|
||||
|
||||
# 运行开发服务器
|
||||
yarn dev
|
||||
```
|
||||
|
||||
前端开发服务器将在 `http://localhost:3000` 启动
|
||||
|
||||
**注意**: 前端使用 Vite 代理,所有 `/api/*` 请求会自动转发到后端服务器,无需配置 CORS。
|
||||
|
||||
### API端点
|
||||
|
||||
#### 用户相关
|
||||
- `POST /api/login` - 用户登录
|
||||
- `POST /api/register` - 用户注册
|
||||
|
||||
#### 题目相关
|
||||
- `GET /api/questions` - 获取题目列表
|
||||
@ -100,30 +130,70 @@ go build -o bin/server.exe main.go
|
||||
|
||||
## 前端开发
|
||||
|
||||
前端项目位于 `web/` 目录,使用 **yarn** 作为包管理工具。
|
||||
|
||||
### 安装依赖
|
||||
```bash
|
||||
cd web
|
||||
npm install
|
||||
yarn install
|
||||
```
|
||||
|
||||
### 运行开发服务器
|
||||
```bash
|
||||
npm run dev
|
||||
yarn dev
|
||||
```
|
||||
|
||||
### 构建
|
||||
开发服务器运行在 `http://localhost:3000`
|
||||
|
||||
### 构建生产版本
|
||||
```bash
|
||||
npm run build
|
||||
yarn build
|
||||
```
|
||||
|
||||
### API代理
|
||||
开发环境下,Vite已配置代理,前端 `/api/*` 请求会自动转发到 `http://localhost:8080`,无需担心CORS问题。
|
||||
|
||||
## 页面结构
|
||||
|
||||
- **首页(刷题页)** - 题目练习、随机题目、题目列表、筛选等功能
|
||||
- **我的** - 用户信息、退出登录
|
||||
- **登录页** - 用户登录
|
||||
- **登录页** (`/login`) - 用户登录和注册,支持密码可见性切换
|
||||
- **首页** (`/`) - 题目练习、随机题目、题目列表、筛选等功能
|
||||
- **我的** (`/profile`) - 用户信息、退出登录
|
||||
|
||||
## 特性
|
||||
|
||||
### 后端特性
|
||||
- 基于 Gin 框架的高性能 HTTP 服务器
|
||||
- 自定义日志中间件
|
||||
- RESTful API 结构
|
||||
- 健康检查端点
|
||||
- 用户登录和注册系统(基于JSON文件存储)
|
||||
- 题目练习功能
|
||||
- 答题统计功能
|
||||
|
||||
### 前端特性
|
||||
- React + TypeScript + Vite 技术栈
|
||||
- Ant Design Mobile UI组件库
|
||||
- 移动端优化,防止双指缩放
|
||||
- CSS Modules 样式隔离
|
||||
- 现代化的登录/注册界面
|
||||
- 密码可见性切换功能
|
||||
|
||||
## 技术栈
|
||||
|
||||
### 后端
|
||||
- **Go** 1.25.1
|
||||
- **Gin** v1.11.0 - Web 框架
|
||||
|
||||
### 前端
|
||||
- **React** 18 - UI框架
|
||||
- **TypeScript** - 类型安全
|
||||
- **Vite** - 构建工具
|
||||
- **Ant Design Mobile** - UI组件库
|
||||
- **React Router** - 路由管理
|
||||
- **Less** - CSS预处理器
|
||||
- **Yarn** - 包管理工具
|
||||
|
||||
### 开发工具
|
||||
- API代理配置(Vite)
|
||||
- CSS Modules
|
||||
- 清晰的项目架构,易于扩展
|
||||
|
||||
@ -10,13 +10,39 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
// 防止移动端缩放
|
||||
touch-action: pan-x pan-y;
|
||||
-ms-touch-action: pan-x pan-y;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: @font-family;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: @bg-color;
|
||||
// 防止移动端缩放
|
||||
touch-action: pan-x pan-y;
|
||||
-ms-touch-action: pan-x pan-y;
|
||||
// 防止用户选择文本(可选,提升体验)
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#root {
|
||||
min-height: 100vh;
|
||||
// 防止移动端缩放
|
||||
touch-action: pan-x pan-y;
|
||||
-ms-touch-action: pan-x pan-y;
|
||||
}
|
||||
|
||||
// 允许输入框选择文本
|
||||
input,
|
||||
textarea {
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
@ -3,6 +3,44 @@ import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './index.less'
|
||||
|
||||
// 防止移动端双指缩放
|
||||
document.addEventListener('gesturestart', function (e) {
|
||||
e.preventDefault()
|
||||
})
|
||||
|
||||
document.addEventListener('gesturechange', function (e) {
|
||||
e.preventDefault()
|
||||
})
|
||||
|
||||
document.addEventListener('gestureend', function (e) {
|
||||
e.preventDefault()
|
||||
})
|
||||
|
||||
// 防止双击缩放
|
||||
let lastTouchEnd = 0
|
||||
document.addEventListener(
|
||||
'touchend',
|
||||
function (e) {
|
||||
const now = Date.now()
|
||||
if (now - lastTouchEnd <= 300) {
|
||||
e.preventDefault()
|
||||
}
|
||||
lastTouchEnd = now
|
||||
},
|
||||
false
|
||||
)
|
||||
|
||||
// 防止通过touch事件进行缩放
|
||||
document.addEventListener(
|
||||
'touchstart',
|
||||
function (e) {
|
||||
if (e.touches.length > 1) {
|
||||
e.preventDefault()
|
||||
}
|
||||
},
|
||||
{ passive: false }
|
||||
)
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
|
||||
@ -4,121 +4,178 @@
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: white;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 40px 20px;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
margin: 0 0 10px 0;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
color: var(--adm-color-primary);
|
||||
font-size: 56px;
|
||||
font-weight: 800;
|
||||
color: #ffffff;
|
||||
margin: 0 0 12px 0;
|
||||
letter-spacing: 3px;
|
||||
text-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 16px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
letter-spacing: 2px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.formGroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
.inputGroup {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 12px 16px;
|
||||
border: 2px solid #e1e8ed;
|
||||
border-radius: 8px;
|
||||
.label {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
font-size: 15px;
|
||||
transition: all 0.3s;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: 14px 16px;
|
||||
font-size: 15px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
color: #333;
|
||||
transition: all 0.3s ease;
|
||||
outline: none;
|
||||
background: white;
|
||||
|
||||
&:focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: #f5f5f5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
box-sizing: border-box;
|
||||
|
||||
&::placeholder {
|
||||
color: #aaa;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
padding: 12px;
|
||||
background-color: #fee;
|
||||
border: 2px solid #fcc;
|
||||
border-radius: 8px;
|
||||
color: #c33;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
animation: shake 0.5s;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
25% { transform: translateX(-10px); }
|
||||
75% { transform: translateX(10px); }
|
||||
}
|
||||
|
||||
.switchMode {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.linkBtn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #667eea;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
padding: 0 0 0 5px;
|
||||
transition: color 0.3s;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: #764ba2;
|
||||
text-decoration: underline;
|
||||
border-color: rgba(255, 255, 255, 0.6);
|
||||
background: #ffffff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: #ffffff;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.passwordWrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.passwordWrapper .input {
|
||||
padding-right: 48px;
|
||||
}
|
||||
|
||||
.eyeButton {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: color 0.2s ease;
|
||||
font-size: 20px;
|
||||
|
||||
&:hover {
|
||||
color: #667eea;
|
||||
}
|
||||
}
|
||||
|
||||
.submitButton {
|
||||
margin-top: 32px;
|
||||
height: 52px;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
border-radius: 12px;
|
||||
background: #ffffff;
|
||||
color: #667eea;
|
||||
border: none;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:not(:disabled):hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
&:not(:disabled):active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
:global {
|
||||
.adm-button {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
color: #667eea !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.switchMode {
|
||||
margin-top: 32px;
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.linkBtn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #ffffff;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
padding: 0 0 0 8px;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 15px;
|
||||
text-decoration: underline;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
opacity: 0.85;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Button, Input, Form, Toast } from 'antd-mobile'
|
||||
import { Button, Toast } from 'antd-mobile'
|
||||
import { EyeInvisibleOutline, EyeOutline } from 'antd-mobile-icons'
|
||||
import styles from './Login.module.less'
|
||||
|
||||
interface LoginResponse {
|
||||
@ -20,6 +21,7 @@ const Login: React.FC = () => {
|
||||
const navigate = useNavigate()
|
||||
const [isLogin, setIsLogin] = useState(true)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
|
||||
// 表单字段
|
||||
const [username, setUsername] = useState('')
|
||||
@ -46,7 +48,7 @@ const Login: React.FC = () => {
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await fetch('http://localhost:8080/api/login', {
|
||||
const response = await fetch('/api/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@ -57,7 +59,6 @@ const Login: React.FC = () => {
|
||||
const data: LoginResponse = await response.json()
|
||||
|
||||
if (data.success && data.data) {
|
||||
// 保存token到localStorage
|
||||
localStorage.setItem('token', data.data.token)
|
||||
localStorage.setItem('user', JSON.stringify(data.data.user))
|
||||
|
||||
@ -66,7 +67,6 @@ const Login: React.FC = () => {
|
||||
content: '登录成功',
|
||||
})
|
||||
|
||||
// 跳转到首页
|
||||
navigate('/')
|
||||
} else {
|
||||
Toast.show({
|
||||
@ -105,7 +105,7 @@ const Login: React.FC = () => {
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await fetch('http://localhost:8080/api/register', {
|
||||
const response = await fetch('/api/register', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@ -116,7 +116,6 @@ const Login: React.FC = () => {
|
||||
const data: LoginResponse = await response.json()
|
||||
|
||||
if (data.success && data.data) {
|
||||
// 保存token到localStorage
|
||||
localStorage.setItem('token', data.data.token)
|
||||
localStorage.setItem('user', JSON.stringify(data.data.user))
|
||||
|
||||
@ -125,7 +124,6 @@ const Login: React.FC = () => {
|
||||
content: '注册成功',
|
||||
})
|
||||
|
||||
// 跳转到首页
|
||||
navigate('/')
|
||||
} else {
|
||||
Toast.show({
|
||||
@ -144,49 +142,68 @@ const Login: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (isLogin) {
|
||||
handleLogin()
|
||||
} else {
|
||||
handleRegister()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<p className={styles.subtitle}>欢迎使用AnKao题库系统</p>
|
||||
<h1 className={styles.title}>AnKao</h1>
|
||||
<p className={styles.subtitle}>欢迎使用题库系统</p>
|
||||
</div>
|
||||
|
||||
<Form
|
||||
layout="vertical"
|
||||
onFinish={isLogin ? handleLogin : handleRegister}
|
||||
className={styles.form}
|
||||
>
|
||||
<Form.Item label="用户名">
|
||||
<Input
|
||||
<form onSubmit={handleSubmit} className={styles.form}>
|
||||
<div className={styles.inputGroup}>
|
||||
<label className={styles.label}>用户名</label>
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={setUsername}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
placeholder="请输入用户名"
|
||||
disabled={loading}
|
||||
clearable
|
||||
className={styles.input}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<Form.Item label="密码">
|
||||
<Input
|
||||
type="password"
|
||||
<div className={styles.inputGroup}>
|
||||
<label className={styles.label}>密码</label>
|
||||
<div className={styles.passwordWrapper}>
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={setPassword}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder={isLogin ? '请输入密码' : '请输入密码(至少6位)'}
|
||||
disabled={loading}
|
||||
clearable
|
||||
className={styles.input}
|
||||
/>
|
||||
</Form.Item>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.eyeButton}
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? <EyeOutline /> : <EyeInvisibleOutline />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isLogin && (
|
||||
<Form.Item label="昵称(可选)">
|
||||
<Input
|
||||
<div className={styles.inputGroup}>
|
||||
<label className={styles.label}>昵称(可选)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={nickname}
|
||||
onChange={setNickname}
|
||||
onChange={(e) => setNickname(e.target.value)}
|
||||
placeholder="请输入昵称"
|
||||
disabled={loading}
|
||||
clearable
|
||||
className={styles.input}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
@ -195,16 +212,18 @@ const Login: React.FC = () => {
|
||||
block
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
className={styles.submitButton}
|
||||
>
|
||||
{isLogin ? '登录' : '注册'}
|
||||
</Button>
|
||||
</Form>
|
||||
</form>
|
||||
|
||||
<div className={styles.switchMode}>
|
||||
{isLogin ? (
|
||||
<p>
|
||||
还没有账号?
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsLogin(false)}
|
||||
className={styles.linkBtn}
|
||||
disabled={loading}
|
||||
@ -216,11 +235,12 @@ const Login: React.FC = () => {
|
||||
<p>
|
||||
已有账号?
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsLogin(true)}
|
||||
className={styles.linkBtn}
|
||||
disabled={loading}
|
||||
>
|
||||
立即登录
|
||||
返回登录
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user