重构登录页面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) 在此代码仓库中工作时提供指导。
|
本文件为 Claude Code (claude.ai/code) 在此代码仓库中工作时提供指导。
|
||||||
|
|
||||||
|
## 重要开发规范
|
||||||
|
|
||||||
|
### 文档同步更新规则
|
||||||
|
**关键规则**: 当实现新功能、修改现有功能或更改项目配置时,**必须同步更新相关文档**。
|
||||||
|
|
||||||
|
- **README.md 更新时机**:
|
||||||
|
- 添加新的核心功能或特性
|
||||||
|
- 修改项目安装、配置或运行方式
|
||||||
|
- 更改技术栈或主要依赖
|
||||||
|
- 添加新的 API 端点或修改现有端点
|
||||||
|
- 更新项目架构或目录结构
|
||||||
|
|
||||||
|
- **CLAUDE.md 更新时机**:
|
||||||
|
- 添加新的开发规范或最佳实践
|
||||||
|
- 修改项目配置(如代理、构建工具等)
|
||||||
|
- 更改目录结构或文件组织方式
|
||||||
|
- 引入新的工具或技术
|
||||||
|
- 更新常用命令或开发流程
|
||||||
|
|
||||||
|
- **更新原则**:
|
||||||
|
- 代码修改和文档更新应在同一次提交中完成
|
||||||
|
- 文档要清晰、准确,反映当前代码状态
|
||||||
|
- 使用 markdown 链接引用具体文件位置
|
||||||
|
- 及时移除过时的说明和示例
|
||||||
|
|
||||||
## 项目概述
|
## 项目概述
|
||||||
|
|
||||||
AnKao 是一个使用 **Gin 框架**构建的 Go Web 应用程序。该项目遵循整洁架构模式,具有清晰的关注点分离,并设计为支持数据库集成。
|
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 组件使用原则
|
||||||
**重要**: 在开发前端页面时,必须优先使用 UI 框架的组件。
|
**重要**: 在开发前端页面时,必须优先使用 UI 框架的组件。
|
||||||
|
|
||||||
@ -125,4 +178,10 @@ go test -v ./...
|
|||||||
- **web/src/pages/** - 页面组件
|
- **web/src/pages/** - 页面组件
|
||||||
- **web/src/components/** - 可复用组件
|
- **web/src/components/** - 可复用组件
|
||||||
- **web/src/pages/*.module.less** - 页面样式文件 (CSS Modules)
|
- **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/ # 数据模型
|
│ └── models/ # 数据模型
|
||||||
├── pkg/ # 公共库代码
|
├── pkg/ # 公共库代码
|
||||||
│ └── config/ # 配置管理
|
│ └── config/ # 配置管理
|
||||||
|
├── web/ # 前端项目目录
|
||||||
|
│ ├── src/ # 源代码
|
||||||
|
│ │ ├── pages/ # 页面组件
|
||||||
|
│ │ ├── components/ # 可复用组件
|
||||||
|
│ │ └── main.tsx # 入口文件
|
||||||
|
│ ├── vite.config.ts # Vite配置(含API代理)
|
||||||
|
│ └── package.json # 前端依赖
|
||||||
├── go.mod # Go模块文件
|
├── go.mod # Go模块文件
|
||||||
|
├── CLAUDE.md # 开发规范文档
|
||||||
└── README.md # 项目说明
|
└── README.md # 项目说明
|
||||||
```
|
```
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
### 运行服务器
|
### 后端服务器
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# 安装依赖
|
||||||
|
go mod tidy
|
||||||
|
|
||||||
|
# 运行服务器
|
||||||
go run main.go
|
go run main.go
|
||||||
```
|
```
|
||||||
|
|
||||||
服务器将在 `http://localhost:8080` 启动
|
服务器将在 `http://localhost:8080` 启动
|
||||||
|
|
||||||
|
### 前端开发服务器
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 进入前端目录
|
||||||
|
cd web
|
||||||
|
|
||||||
|
# 安装依赖 (使用 yarn)
|
||||||
|
yarn install
|
||||||
|
|
||||||
|
# 运行开发服务器
|
||||||
|
yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
前端开发服务器将在 `http://localhost:3000` 启动
|
||||||
|
|
||||||
|
**注意**: 前端使用 Vite 代理,所有 `/api/*` 请求会自动转发到后端服务器,无需配置 CORS。
|
||||||
|
|
||||||
### API端点
|
### API端点
|
||||||
|
|
||||||
#### 用户相关
|
#### 用户相关
|
||||||
- `POST /api/login` - 用户登录
|
- `POST /api/login` - 用户登录
|
||||||
|
- `POST /api/register` - 用户注册
|
||||||
|
|
||||||
#### 题目相关
|
#### 题目相关
|
||||||
- `GET /api/questions` - 获取题目列表
|
- `GET /api/questions` - 获取题目列表
|
||||||
@ -100,30 +130,70 @@ go build -o bin/server.exe main.go
|
|||||||
|
|
||||||
## 前端开发
|
## 前端开发
|
||||||
|
|
||||||
|
前端项目位于 `web/` 目录,使用 **yarn** 作为包管理工具。
|
||||||
|
|
||||||
### 安装依赖
|
### 安装依赖
|
||||||
```bash
|
```bash
|
||||||
cd web
|
cd web
|
||||||
npm install
|
yarn install
|
||||||
```
|
```
|
||||||
|
|
||||||
### 运行开发服务器
|
### 运行开发服务器
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### 构建
|
开发服务器运行在 `http://localhost:3000`
|
||||||
|
|
||||||
|
### 构建生产版本
|
||||||
```bash
|
```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
|
- **Go** 1.25.1
|
||||||
- **Gin** v1.11.0 - Web 框架
|
- **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;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
// 防止移动端缩放
|
||||||
|
touch-action: pan-x pan-y;
|
||||||
|
-ms-touch-action: pan-x pan-y;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: @font-family;
|
font-family: @font-family;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
background-color: @bg-color;
|
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 {
|
#root {
|
||||||
min-height: 100vh;
|
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 App from './App'
|
||||||
import './index.less'
|
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(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App />
|
||||||
|
|||||||
@ -4,116 +4,75 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: white;
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
padding: 40px 20px;
|
padding: 40px 20px;
|
||||||
max-width: 400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 40px;
|
margin-bottom: 48px;
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #888;
|
|
||||||
margin: 0 0 10px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 48px;
|
font-size: 56px;
|
||||||
font-weight: 700;
|
font-weight: 800;
|
||||||
color: var(--adm-color-primary);
|
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;
|
margin: 0;
|
||||||
text-align: center;
|
font-weight: 400;
|
||||||
letter-spacing: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form {
|
.form {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.formGroup {
|
.inputGroup {
|
||||||
display: flex;
|
margin-bottom: 24px;
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
label {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #333;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
.label {
|
||||||
padding: 12px 16px;
|
display: block;
|
||||||
border: 2px solid #e1e8ed;
|
margin-bottom: 10px;
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 15px;
|
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;
|
outline: none;
|
||||||
background: white;
|
box-sizing: border-box;
|
||||||
|
|
||||||
&:focus {
|
|
||||||
border-color: #667eea;
|
|
||||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: #aaa;
|
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) {
|
&:hover:not(:disabled) {
|
||||||
color: #764ba2;
|
border-color: rgba(255, 255, 255, 0.6);
|
||||||
text-decoration: underline;
|
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 {
|
&:disabled {
|
||||||
@ -121,4 +80,102 @@
|
|||||||
cursor: not-allowed;
|
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 React, { useState, useEffect } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
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'
|
import styles from './Login.module.less'
|
||||||
|
|
||||||
interface LoginResponse {
|
interface LoginResponse {
|
||||||
@ -20,6 +21,7 @@ const Login: React.FC = () => {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [isLogin, setIsLogin] = useState(true)
|
const [isLogin, setIsLogin] = useState(true)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [showPassword, setShowPassword] = useState(false)
|
||||||
|
|
||||||
// 表单字段
|
// 表单字段
|
||||||
const [username, setUsername] = useState('')
|
const [username, setUsername] = useState('')
|
||||||
@ -46,7 +48,7 @@ const Login: React.FC = () => {
|
|||||||
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
const response = await fetch('http://localhost:8080/api/login', {
|
const response = await fetch('/api/login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -57,7 +59,6 @@ const Login: React.FC = () => {
|
|||||||
const data: LoginResponse = await response.json()
|
const data: LoginResponse = await response.json()
|
||||||
|
|
||||||
if (data.success && data.data) {
|
if (data.success && data.data) {
|
||||||
// 保存token到localStorage
|
|
||||||
localStorage.setItem('token', data.data.token)
|
localStorage.setItem('token', data.data.token)
|
||||||
localStorage.setItem('user', JSON.stringify(data.data.user))
|
localStorage.setItem('user', JSON.stringify(data.data.user))
|
||||||
|
|
||||||
@ -66,7 +67,6 @@ const Login: React.FC = () => {
|
|||||||
content: '登录成功',
|
content: '登录成功',
|
||||||
})
|
})
|
||||||
|
|
||||||
// 跳转到首页
|
|
||||||
navigate('/')
|
navigate('/')
|
||||||
} else {
|
} else {
|
||||||
Toast.show({
|
Toast.show({
|
||||||
@ -105,7 +105,7 @@ const Login: React.FC = () => {
|
|||||||
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
const response = await fetch('http://localhost:8080/api/register', {
|
const response = await fetch('/api/register', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -116,7 +116,6 @@ const Login: React.FC = () => {
|
|||||||
const data: LoginResponse = await response.json()
|
const data: LoginResponse = await response.json()
|
||||||
|
|
||||||
if (data.success && data.data) {
|
if (data.success && data.data) {
|
||||||
// 保存token到localStorage
|
|
||||||
localStorage.setItem('token', data.data.token)
|
localStorage.setItem('token', data.data.token)
|
||||||
localStorage.setItem('user', JSON.stringify(data.data.user))
|
localStorage.setItem('user', JSON.stringify(data.data.user))
|
||||||
|
|
||||||
@ -125,7 +124,6 @@ const Login: React.FC = () => {
|
|||||||
content: '注册成功',
|
content: '注册成功',
|
||||||
})
|
})
|
||||||
|
|
||||||
// 跳转到首页
|
|
||||||
navigate('/')
|
navigate('/')
|
||||||
} else {
|
} else {
|
||||||
Toast.show({
|
Toast.show({
|
||||||
@ -144,49 +142,68 @@ const Login: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (isLogin) {
|
||||||
|
handleLogin()
|
||||||
|
} else {
|
||||||
|
handleRegister()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<p className={styles.subtitle}>欢迎使用AnKao题库系统</p>
|
|
||||||
<h1 className={styles.title}>AnKao</h1>
|
<h1 className={styles.title}>AnKao</h1>
|
||||||
|
<p className={styles.subtitle}>欢迎使用题库系统</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form
|
<form onSubmit={handleSubmit} className={styles.form}>
|
||||||
layout="vertical"
|
<div className={styles.inputGroup}>
|
||||||
onFinish={isLogin ? handleLogin : handleRegister}
|
<label className={styles.label}>用户名</label>
|
||||||
className={styles.form}
|
<input
|
||||||
>
|
type="text"
|
||||||
<Form.Item label="用户名">
|
|
||||||
<Input
|
|
||||||
value={username}
|
value={username}
|
||||||
onChange={setUsername}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
placeholder="请输入用户名"
|
placeholder="请输入用户名"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
clearable
|
className={styles.input}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</div>
|
||||||
|
|
||||||
<Form.Item label="密码">
|
<div className={styles.inputGroup}>
|
||||||
<Input
|
<label className={styles.label}>密码</label>
|
||||||
type="password"
|
<div className={styles.passwordWrapper}>
|
||||||
|
<input
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
value={password}
|
value={password}
|
||||||
onChange={setPassword}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
placeholder={isLogin ? '请输入密码' : '请输入密码(至少6位)'}
|
placeholder={isLogin ? '请输入密码' : '请输入密码(至少6位)'}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
clearable
|
className={styles.input}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.eyeButton}
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
>
|
||||||
|
{showPassword ? <EyeOutline /> : <EyeInvisibleOutline />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{!isLogin && (
|
{!isLogin && (
|
||||||
<Form.Item label="昵称(可选)">
|
<div className={styles.inputGroup}>
|
||||||
<Input
|
<label className={styles.label}>昵称(可选)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
value={nickname}
|
value={nickname}
|
||||||
onChange={setNickname}
|
onChange={(e) => setNickname(e.target.value)}
|
||||||
placeholder="请输入昵称"
|
placeholder="请输入昵称"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
clearable
|
className={styles.input}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@ -195,16 +212,18 @@ const Login: React.FC = () => {
|
|||||||
block
|
block
|
||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
|
className={styles.submitButton}
|
||||||
>
|
>
|
||||||
{isLogin ? '登录' : '注册'}
|
{isLogin ? '登录' : '注册'}
|
||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</form>
|
||||||
|
|
||||||
<div className={styles.switchMode}>
|
<div className={styles.switchMode}>
|
||||||
{isLogin ? (
|
{isLogin ? (
|
||||||
<p>
|
<p>
|
||||||
还没有账号?
|
还没有账号?
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => setIsLogin(false)}
|
onClick={() => setIsLogin(false)}
|
||||||
className={styles.linkBtn}
|
className={styles.linkBtn}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
@ -216,11 +235,12 @@ const Login: React.FC = () => {
|
|||||||
<p>
|
<p>
|
||||||
已有账号?
|
已有账号?
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => setIsLogin(true)}
|
onClick={() => setIsLogin(true)}
|
||||||
className={styles.linkBtn}
|
className={styles.linkBtn}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
立即登录
|
返回登录
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user