Browse Source

最新版

main
lyq0314 6 days ago
parent
commit
d15c0a23c4
  1. 270
      README.md
  2. 1
      app.html
  3. 783
      assets/css/style.css
  4. 1591
      assets/js/app-main.js
  5. 760
      assets/js/app.js
  6. 29
      assets/js/common.js
  7. 7
      assets/js/config.js
  8. 139
      assets/js/login.js
  9. 10
      assets/js/register.js
  10. 38
      login.html
  11. 11
      register.html

270
README.md

@ -1,53 +1,107 @@
# 运动会报名系统 # 运动会报名系统
## 项目简介 ## 项目简介
这是一个基于 Spring Boot 和前端技术栈开发的运动会报名系统,用于管理学校运动会的项目报名工作。 这是一个基于 Spring Boot + 原生 Web 前端技术栈开发的运动会报名管理系统,支持学生在线报名运动会项目、管理员统一管理用户与报名数据。
---
## 技术栈 ## 技术栈
### 后端 ### 后端
- Java 8+ | 技术 | 版本 | 说明 |
- Spring Boot 2.x |------|------|------|
- MyBatis | Java | 8+ | 开发语言 |
- MySQL 数据库 | Spring Boot | 2.x | 核心框架 |
| MyBatis | 3.x | ORM 框架 |
| MySQL | 5.7+ | 关系型数据库 |
| Maven | 3.6+ | 项目构建 |
### 前端 ### 前端
- HTML5 / CSS3 / JavaScript | 技术 | 说明 |
- Bootstrap 样式框架 |------|------|
| HTML5 | 页面结构 |
| CSS3 | 样式设计(纯原生,无框架依赖) |
| JavaScript (ES5) | 交互逻辑(纯原生,无框架依赖) |
---
## 项目结构 ## 项目结构
``` ```
运动会报名/ 运动会报名/
├── backend/ # 后端代码 ├── backend/ # 后端 Spring Boot 项目
│ ├── src/main/java/ # Java 源代码 │ ├── src/main/java/ # Java 源代码
│ ├── src/main/resources/ # 配置文件 │ │ └── com/sports/ # 主包
│ └── pom.xml # Maven 配置 │ │ ├── controller/ # 控制器层
├── frontend/ # 前端代码 │ │ ├── service/ # 业务逻辑层
│ ├── assets/ # 静态资源 │ │ ├── mapper/ # 数据访问层
│ │ ├── css/ # 样式文件 │ │ ├── entity/ # 实体类
│ │ └── js/ # JavaScript 文件 │ │ ├── vo/ # 视图对象
│ ├── index.html # 首页 │ │ ├── dto/ # 数据传输对象
│ ├── login.html # 登录页 │ │ └── config/ # 配置类
│ ├── register.html # 注册页 │ ├── src/main/resources/ # 配置文件
│ └── app.html # 主应用页面 │ │ ├── application.properties # 应用配置
└── README.md # 项目说明 │ │ ├── schema.sql # 数据库表结构
│ │ └── data.sql # 初始数据
│ ├── API文档.md # 接口文档
│ └── pom.xml # Maven 配置
└── frontend/ # 前端项目
├── assets/
│ ├── css/
│ │ ├── style.css # 全局样式(830 行)
│ │ ├── login.css # 登录/注册页样式
│ │ └── app.css # 主应用页样式
│ └── js/
│ ├── common.js # 公共工具库(API 请求、消息提示等)
│ ├── login.js # 登录页逻辑
│ ├── register.js # 注册页逻辑
│ ├── app-main.js # 主应用页核心逻辑(714 行)
│ └── app.js # 备用页面逻辑
├── login.html # 登录页面
├── register.html # 注册页面
├── app.html # 主应用页面(登录后跳转)
└── README.md # 项目说明(本文件)
``` ```
## 功能模块 ---
## 页面说明
### 用户功能 ### login.html — 登录页
- 用户注册与登录 - 用户输入 **登录账号** + **密码** + **图形验证码**
- 个人信息管理 - 验证码为 Canvas 绘制的 4 位随机字符
- 运动会项目浏览 - 登录成功后将用户信息存入 `sessionStorage`,跳转至 `app.html`
- 项目报名与取消 - 包含"去注册"按钮跳转至注册页
### 管理员功能 ### register.html — 注册页
- 用户信息管理 - 填写完整的个人信息(身份证号、姓名、手机号、性别、学院、班级、学号、类别)
- 报名总览查看 - 注册成功后自动登录并跳转至 `app.html`
- 报名记录统计 - 包含"返回登录"按钮
### app.html — 主应用页
- **顶栏**:系统标题 + 当前用户名 + 退出登录按钮
- **左侧导航**:根据角色(学生/管理员)显示对应菜单
- **右侧内容区**:动态渲染对应功能模块
#### 学生菜单
| 菜单项 | 功能 |
|--------|------|
| 个人信息 | 查看并维护当前登录人员的基础资料 |
| 运动会报名 | 浏览所有项目并完成报名,也可查看自己的报名信息 |
#### 管理员菜单
| 菜单项 | 功能 |
|--------|------|
| 运动会管理 | 查看后台首页和系统概览 |
| 团体信息管理 | 维护参赛团体与组织信息 |
| 用户信息管理 | 查看、维护用户资料并执行账号管理操作 |
| 报名管理 | 查看全部报名记录并进行管理 |
| 赛事管理 | 管理所有运动会赛事项目 |
| 系统管理 | 系统配置、权限设置和运行维护 |
---
## 快速开始 ## 快速开始
@ -55,53 +109,161 @@
- JDK 8 或更高版本 - JDK 8 或更高版本
- Maven 3.6+ - Maven 3.6+
- MySQL 5.7+ - MySQL 5.7+
- 任意现代浏览器(推荐 Chrome / Edge)
### 数据库配置 ### 1. 数据库配置
创建数据库并导入初始数据:
```sql ```sql
CREATE DATABASE sports_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE DATABASE IF NOT EXISTS sports_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE sports_db; USE sports_db;
``` ```
导入 `backend/src/main/resources/schema.sql``backend/src/main/resources/data.sql` 然后导入 `backend/src/main/resources/schema.sql``data.sql` 初始化表结构和数据。
### 启动后端服务 ### 2. 修改后端配置
编辑 `backend/src/main/resources/application.properties`
```properties
spring.datasource.url=jdbc:mysql://localhost:3306/sports_db?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
spring.datasource.username=你的数据库用户名
spring.datasource.password=你的数据库密码
server.port=8080
```
### 3. 启动后端
```bash ```bash
cd backend cd backend
mvn spring-boot:run mvn spring-boot:run
``` ```
### 启动前端服务 后端默认运行在 `http://localhost:8080`
使用任意 HTTP 服务器启动前端,例如: ### 4. 启动前端
**方式一:Python HTTP 服务器**
```bash ```bash
cd frontend cd frontend
python -m http.server 8080 python -m http.server 5500
``` ```
访问 `http://localhost:5500/login.html`
**方式二:VS Code Live Server**
- 安装 Live Server 插件
- 右键 `login.html` → "Open with Live Server"
### 访问地址 **方式三:直接打开文件(file:// 协议)**
- 直接双击 `login.html`
- 前端会自动将 API 请求指向 `http://localhost:8080`
- 前端页面: http://localhost:8080 > **注意**:前端 JS 会自动检测协议——如果通过 `file://` 打开,API 地址自动指向 `http://localhost:8080`;否则指向当前域名 + 端口 8080。
- 后端 API: http://localhost:8081
---
## 默认账号 ## 默认账号
| 账号 | 密码 | 角色 | | 登录账号 | 密码 | 角色 |
|------|------|------| |----------|------|------|
| admin | admin | 管理员 | | admin | admin | 管理员 |
| student | student | 普通用户 | | student | student | 普通学生 |
---
## 系统架构
```
┌─────────────────────────────────────┐
│ 前端 (Native Web) │
│ login.html → app.html │
│ common.js 提供 Ajax 工具库 │
└──────────────┬──────────────────────┘
│ HTTP (JSON)
│ Session + Cookie 认证
┌──────────────▼──────────────────────┐
│ 后端 (Spring Boot) │
│ Controller → Service → Mapper │
│ /api/auth/* 无需认证 │
│ /api/** 需登录 │
│ /api/admin/** 需 ADMIN 角色 │
└──────────────┬──────────────────────┘
│ JDBC
┌──────────────▼──────────────────────┐
│ MySQL 数据库 │
└─────────────────────────────────────┘
```
### 认证流程
1. 用户在登录页提交账号密码
2. 后端验证通过后写入 Session(`session.currentUserId`)
3. 前端收到用户信息后存入 `sessionStorage`,跳转至 `app.html`
4. `app.html` 加载后调用 `/api/auth/me` 验证登录状态
5. 验证通过则初始化用户数据、加载侧边栏和主页内容
6. 后续 API 请求自动携带 Cookie(Session)
### 前端核心逻辑 (app-main.js)
- `loadCurrentUser()` → 调用 `/api/auth/me` 获取当前用户
- `buildApp()` → 渲染侧边栏 + 初始化数据
- `renderSidebar()` → 根据角色(学生/管理员)渲染导航菜单
- `renderCurrentView()` → 根据当前选中菜单渲染内容区域
- `loadEventData()` → 加载赛事数据
- `loadAdminData()` → 加载管理员相关数据
---
## API 接口概览
详细的接口文档请参考 `backend/API文档.md`
### 认证接口 `/api/auth`
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | /api/auth/login | 用户登录 |
| POST | /api/auth/register | 用户注册 |
| GET | /api/auth/me | 获取当前登录用户信息 |
| POST | /api/auth/logout | 退出登录 |
### 赛事接口 `/api/events`
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /api/events | 获取所有赛事 |
| GET | /api/events/my | 获取我的报名 |
| POST | /api/events/{id}/register | 报名赛事 |
| DELETE | /api/events/{id}/register | 取消报名 |
### 管理员接口 `/api/admin`
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /api/admin/users | 获取所有用户 |
| GET | /api/admin/registrations | 获取所有报名记录 |
| GET | /api/admin/events | 获取赛事管理数据 |
| POST | /api/admin/users/{id}/reset-password | 重置用户密码 |
| POST | /api/admin/users/{id}/disable | 禁用用户 |
| POST | /api/admin/users/{id}/enable | 启用用户 |
| DELETE | /api/admin/users/{id} | 删除用户 |
---
## 编码说明
- 所有前端 HTML/CSS/JS 文件均使用 **UTF-8 编码**
- 如遇中文乱码,请检查文件是否以 UTF-8 格式保存
- 后端使用 UTF-8 编码,数据库使用 utf8mb4
---
## 常见问题
## 开发说明 ### Q: 登录后页面卡在"加载中..."
**A:** 请检查:
1. 后端服务是否已启动(`http://localhost:8080`)
2. 浏览器控制台是否有跨域或网络错误
3. JS 文件编码是否为 UTF-8(不是 GBK)
### 代码规范 ### Q: 注册时身份证号怎么填?
- Java 代码遵循 Spring 编码规范 **A:** 15 位或 18 位的有效身份证号,如 `110101200001010000`
- JavaScript 代码使用 ES6+ 语法
- 数据库表名使用下划线命名
### 注意事项 ### Q: 如何添加新的赛事项目?
- 开发环境下请确保 MySQL 服务已启动 **A:** 使用管理员账号登录,在"赛事管理"模块中操作,或直接在数据库中插入数据。
- 修改配置文件后需要重启服务才能生效

1
app.html

@ -35,6 +35,7 @@
</main> </main>
</div> </div>
<script src="./assets/js/config.js"></script>
<script src="./assets/js/common.js"></script> <script src="./assets/js/common.js"></script>
<script src="./assets/js/app-main.js"></script> <script src="./assets/js/app-main.js"></script>
</body> </body>

783
assets/css/style.css

@ -1,4 +1,4 @@
:root { :root {
--surface: rgba(255, 255, 255, 0.92); --surface: rgba(255, 255, 255, 0.92);
--text: #1f2937; --text: #1f2937;
--muted: #6b7280; --muted: #6b7280;
@ -38,6 +38,7 @@ select {
display: none !important; display: none !important;
} }
/* ========== 认证页面 ========== */
.auth-page { .auth-page {
display: flex; display: flex;
align-items: center; align-items: center;
@ -135,6 +136,7 @@ select {
margin-bottom: 24px; margin-bottom: 24px;
} }
/* ========== 表单 ========== */
.form-grid { .form-grid {
display: grid; display: grid;
gap: 18px; gap: 18px;
@ -168,7 +170,6 @@ select {
outline: none; outline: none;
} }
/* 下拉列表通用样式 */
select { select {
appearance: none; appearance: none;
-webkit-appearance: none; -webkit-appearance: none;
@ -179,24 +180,20 @@ select {
background-size: 16px; background-size: 16px;
} }
/* 学院下拉列表选项样式 */
select[name="college"] option { select[name="college"] option {
padding: 6px 16px; padding: 6px 16px;
line-height: 1.3; line-height: 1.3;
} }
/* 学院下拉列表占位符样式 */
select[name="college"] option[value=""] { select[name="college"] option[value=""] {
color: var(--muted); color: var(--muted);
display: none; display: none;
} }
/* 未选择时显示占位符颜色 */
select[name="college"]:invalid { select[name="college"]:invalid {
color: var(--muted); color: var(--muted);
} }
/* 选择后显示正常颜色 */
select[name="college"]:valid { select[name="college"]:valid {
color: var(--text); color: var(--text);
} }
@ -263,6 +260,7 @@ select[name="college"]:valid {
color: var(--danger); color: var(--danger);
} }
/* ========== 应用布局 ========== */
.app-body { .app-body {
padding: 24px; padding: 24px;
} }
@ -411,49 +409,208 @@ select[name="college"]:valid {
margin-bottom: 18px; margin-bottom: 18px;
} }
.event-table { /* ========== 通用数据表格 ========== */
.table-wrapper {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.data-table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
border-radius: 18px; border-radius: 14px;
overflow: hidden; overflow: hidden;
background: rgba(255, 255, 255, 0.86); background: #fff;
font-size: 13px;
min-width: 700px;
box-shadow: 0 2px 12px rgba(15, 23, 42, 0.06);
} }
.event-table th, .data-table thead {
.event-table td { background: linear-gradient(135deg, #0f766e, #14b8a6);
padding: 15px 14px; color: #fff;
text-align: left;
border-bottom: 1px solid rgba(226, 232, 240, 0.9);
font-size: 14px;
} }
.event-table th:last-child, .data-table thead th {
.event-table td:last-child { padding: 14px 12px;
text-align: center; text-align: center;
font-weight: 700;
font-size: 13px;
white-space: nowrap;
border-right: 1px solid rgba(255, 255, 255, 0.15);
letter-spacing: 0.5px;
} }
.event-table th { .data-table thead th:last-child {
background: rgba(15, 118, 110, 0.08); border-right: none;
color: #115e59; }
.data-table tbody td {
padding: 10px 12px;
border-bottom: 1px solid #e2e8f0;
vertical-align: middle;
white-space: nowrap;
}
.data-table tbody tr:nth-child(even) {
background: #f8fafc;
}
.data-table tbody tr:nth-child(odd) {
background: #ffffff;
}
.data-table tbody tr:hover {
background: #e6f7f5;
} }
.event-table tr:last-child td { .data-table tbody tr:last-child td {
border-bottom: none; border-bottom: none;
} }
.data-table .actions-cell {
white-space: nowrap;
text-align: center;
min-width: 200px;
display: flex;
flex-wrap: wrap;
gap: 6px;
justify-content: center;
align-items: center;
padding: 10px 6px !important;
}
/* ========== 状态标签 ========== */
.status-active {
display: inline-block;
padding: 4px 12px;
border-radius: 999px;
background: #d1fae5;
color: #065f46;
font-size: 12px;
font-weight: 700;
}
.status-disabled {
display: inline-block;
padding: 4px 12px;
border-radius: 999px;
background: #fee2e2;
color: #991b1b;
font-size: 12px;
font-weight: 700;
}
/* ========== 按钮样式 ========== */
.action-btn { .action-btn {
padding: 10px 16px; padding: 8px 14px;
border-radius: 12px; border-radius: 8px;
background: linear-gradient(135deg, var(--primary), var(--primary-light)); border: none;
color: #fff; cursor: pointer;
font-weight: 700; font-weight: 700;
font-size: 12px;
white-space: nowrap;
transition: all 0.2s ease;
color: #fff;
background: linear-gradient(135deg, var(--primary), var(--primary-light));
}
.action-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(15, 118, 110, 0.25);
} }
.action-btn[disabled] { .action-btn[disabled] {
background: rgba(148, 163, 184, 0.6); background: rgba(148, 163, 184, 0.6);
cursor: not-allowed; cursor: not-allowed;
transform: none;
box-shadow: none;
}
.action-btn.small-btn {
padding: 7px 14px;
font-size: 12px;
border-radius: 8px;
min-width: 56px;
text-align: center;
} }
.action-btn.cancel-btn {
background: linear-gradient(135deg, #f59e0b, #d97706);
}
.action-btn.register-btn {
background: linear-gradient(135deg, var(--primary), var(--primary-light));
}
.action-btn.danger-btn {
background: linear-gradient(135deg, #ef4444, #dc2626);
}
.edit-user-btn {
background: linear-gradient(135deg, #3b82f6, #2563eb) !important;
}
.reset-password-btn {
background: linear-gradient(135deg, #8b5cf6, #7c3aed) !important;
}
.disable-user-btn {
background: linear-gradient(135deg, #f59e0b, #d97706) !important;
}
.enable-user-btn {
background: linear-gradient(135deg, #10b981, #059669) !important;
}
.delete-user-btn {
background: linear-gradient(135deg, #ef4444, #dc2626) !important;
}
.edit-event-btn {
background: linear-gradient(135deg, #3b82f6, #2563eb) !important;
}
.delete-event-btn {
background: linear-gradient(135deg, #ef4444, #dc2626) !important;
}
/* ========== 上传区域 ========== */
.upload-area {
padding: 18px;
border-radius: 18px;
background: rgba(248, 250, 252, 0.96);
border: 1px dashed rgba(148, 163, 184, 0.55);
}
.upload-hint {
margin-bottom: 18px;
padding: 14px 18px;
background: #fefce8;
border-left: 4px solid #f59e0b;
border-radius: 10px;
font-size: 13px;
line-height: 1.8;
}
.upload-hint p {
margin: 4px 0;
color: #78350f;
}
.upload-actions {
display: flex;
align-items: center;
gap: 14px;
flex-wrap: wrap;
}
.upload-file-name {
font-size: 13px;
color: var(--muted);
}
/* ========== 空状态 ========== */
.empty-state { .empty-state {
padding: 32px 20px; padding: 32px 20px;
text-align: center; text-align: center;
@ -462,7 +619,73 @@ select[name="college"]:valid {
border: 1px dashed rgba(148, 163, 184, 0.45); border: 1px dashed rgba(148, 163, 184, 0.45);
} }
/* 分页样式 */ /* ========== 可折叠导航菜单 ========== */
.nav-group {
display: grid;
gap: 4px;
}
.nav-parent {
width: 100%;
text-align: left;
padding: 14px 16px;
border-radius: 18px;
background: transparent;
color: rgba(255, 255, 255, 0.9);
font-weight: 700;
border: 1px solid transparent;
cursor: pointer;
transition: background 0.2s ease;
display: flex;
align-items: center;
gap: 8px;
}
.nav-parent:hover,
.nav-parent.expanded {
background: rgba(255, 255, 255, 0.1);
}
.nav-arrow {
display: inline-block;
font-size: 10px;
transition: transform 0.2s ease;
}
.nav-parent.expanded .nav-arrow {
transform: rotate(90deg);
}
.nav-children {
display: none;
padding-left: 12px;
margin: 2px 0 6px;
border-left: 2px solid rgba(255, 255, 255, 0.15);
}
.nav-children.open {
display: grid;
gap: 4px;
}
.nav-sub {
background: rgba(255, 255, 255, 0.06);
border: 1px solid transparent;
padding: 12px 16px;
font-size: 14px;
}
.nav-sub:hover {
background: rgba(255, 255, 255, 0.12);
}
.nav-sub.active {
background: rgba(255, 255, 255, 0.14);
color: #ffffff;
border-color: rgba(255, 255, 255, 0.2);
}
/* ========== 分页 ========== */
.pagination { .pagination {
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -509,28 +732,513 @@ select[name="college"]:valid {
color: var(--muted); color: var(--muted);
} }
@media (max-width: 960px) { /* ========== 工具栏 ========== */
.toolbar-row {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 12px;
}
.search-input {
flex: 1;
min-width: 200px;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 10px 16px;
outline: none;
font-size: 14px;
}
.search-input:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(15, 118, 110, 0.1);
}
/* ========== 仪表盘 ========== */
.dashboard-shell {
display: grid;
gap: 16px;
}
.dashboard-top,
.dashboard-bottom {
display: grid;
gap: 16px;
}
.dashboard-top {
grid-template-columns: minmax(0, 1.45fr) minmax(320px, 0.95fr);
}
.dashboard-bottom {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.dashboard-hero-card,
.metric-card,
.dashboard-panel,
.donut-panel {
border-radius: 22px;
background: rgba(255, 255, 255, 0.92);
border: 1px solid rgba(226, 232, 240, 0.92);
}
.dashboard-hero-card {
padding: 22px 24px;
display: grid;
grid-template-columns: minmax(0, 1fr) 280px;
gap: 20px;
align-items: center;
background:
radial-gradient(circle at top right, rgba(20, 184, 166, 0.18), transparent 35%),
linear-gradient(135deg, rgba(15, 118, 110, 0.08), rgba(255, 255, 255, 0.94));
}
.dashboard-hero-card.compact {
grid-template-columns: 1fr;
align-items: stretch;
}
.dashboard-hero-copy {
display: grid;
align-content: start;
gap: 10px;
}
.dashboard-kicker {
margin: 0 0 8px;
font-size: 13px;
font-weight: 700;
color: #0f766e;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.dashboard-hero h3 {
margin: 0 0 8px;
font-size: 28px;
}
.dashboard-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-top: 6px;
}
.dashboard-metrics {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 14px;
}
.metric-card {
padding: 18px 20px;
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.06);
}
.metric-label,
.metric-hint,
.ranking-item p,
.upcoming-item p,
.timeline-content p,
.timeline-content span {
color: var(--muted);
}
.metric-label {
display: block;
font-size: 13px;
font-weight: 700;
margin-bottom: 12px;
}
.metric-value {
display: block;
font-size: 32px;
line-height: 1;
margin-bottom: 10px;
}
.metric-hint {
margin: 0;
font-size: 13px;
}
.dashboard-panel {
padding: 20px 22px;
min-height: 0;
}
.dashboard-stat-list,
.mini-bars,
.ranking-list,
.upcoming-list,
.timeline-list {
display: grid;
gap: 14px;
}
.dashboard-stat-row,
.mini-bar-text,
.ranking-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.dashboard-stat-row {
padding: 14px 0;
border-bottom: 1px solid rgba(226, 232, 240, 0.88);
}
.dashboard-stat-row:last-child {
border-bottom: none;
padding-bottom: 0;
}
.mini-bar-track {
height: 10px;
border-radius: 999px;
overflow: hidden;
background: rgba(226, 232, 240, 0.92);
}
.mini-bar-track span {
display: block;
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, #0f766e, #14b8a6);
}
.timeline-item,
.upcoming-item,
.ranking-item {
border-radius: 18px;
background: rgba(248, 250, 252, 0.92);
border: 1px solid rgba(226, 232, 240, 0.88);
}
.timeline-item {
display: flex;
gap: 14px;
padding: 16px 18px;
}
.timeline-dot {
width: 12px;
min-width: 12px;
height: 12px;
margin-top: 6px;
border-radius: 50%;
background: linear-gradient(135deg, #0f766e, #14b8a6);
box-shadow: 0 0 0 6px rgba(20, 184, 166, 0.12);
}
.timeline-content strong,
.upcoming-item strong,
.ranking-item strong {
display: block;
}
.timeline-content p,
.timeline-content span,
.upcoming-item p,
.upcoming-item span,
.ranking-item p,
.ranking-meta {
margin: 6px 0 0;
font-size: 13px;
}
.upcoming-item,
.ranking-item {
padding: 16px 18px;
}
.ranking-index {
font-size: 12px;
font-weight: 700;
color: #0f766e;
}
.dashboard-empty {
padding: 26px 18px;
border-radius: 18px;
text-align: center;
color: var(--muted);
background: rgba(248, 250, 252, 0.86);
border: 1px dashed rgba(148, 163, 184, 0.45);
}
.donut-layout {
display: grid;
grid-template-columns: 160px minmax(0, 1fr);
gap: 16px;
align-items: center;
}
.donut-chart {
width: 160px;
height: 160px;
border-radius: 50%;
display: grid;
place-items: center;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.35);
}
.donut-hole {
width: 96px;
height: 96px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.96);
display: grid;
place-items: center;
text-align: center;
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.08);
}
.donut-hole strong {
font-size: 26px;
line-height: 1;
}
.donut-hole span {
font-size: 12px;
color: var(--muted);
}
.donut-legend {
display: grid;
gap: 10px;
}
.donut-legend-item {
display: flex;
align-items: center;
gap: 10px;
}
.donut-dot {
width: 12px;
height: 12px;
border-radius: 50%;
flex: 0 0 12px;
}
.donut-legend-text strong {
display: block;
font-size: 14px;
}
.donut-legend-text p {
margin: 4px 0 0;
color: var(--muted);
font-size: 12px;
}
.category-overview-list {
display: grid;
gap: 14px;
}
.category-overview-item {
display: grid;
gap: 8px;
}
.category-overview-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.category-overview-head strong {
font-size: 14px;
}
.category-overview-head span {
color: var(--muted);
font-size: 12px;
white-space: nowrap;
}
.category-overview-track {
height: 12px;
border-radius: 999px;
overflow: hidden;
background: rgba(226, 232, 240, 0.92);
}
.category-overview-fill {
display: block;
height: 100%;
border-radius: inherit;
}
.category-overview-fill.fill-0 {
background: linear-gradient(90deg, #0f766e, #14b8a6);
}
.category-overview-fill.fill-1 {
background: linear-gradient(90deg, #2563eb, #60a5fa);
}
.category-overview-fill.fill-2 {
background: linear-gradient(90deg, #f97316, #fb923c);
}
.category-overview-fill.fill-3 {
background: linear-gradient(90deg, #e11d48, #fb7185);
}
.category-overview-fill.fill-4 {
background: linear-gradient(90deg, #8b5cf6, #a78bfa);
}
.compact-ranking-list,
.compact-feed-list {
display: grid;
gap: 12px;
}
.compact-ranking-item,
.compact-feed-item {
display: grid;
align-items: center;
gap: 12px;
padding: 14px 16px;
border-radius: 18px;
background: rgba(248, 250, 252, 0.92);
border: 1px solid rgba(226, 232, 240, 0.88);
}
.compact-ranking-item {
grid-template-columns: 34px minmax(0, 1fr) auto;
}
.compact-rank-no {
width: 34px;
height: 34px;
border-radius: 50%;
display: grid;
place-items: center;
background: linear-gradient(135deg, #0f766e, #14b8a6);
color: #fff;
font-weight: 700;
}
.compact-ranking-main strong,
.compact-feed-body strong {
display: block;
}
.compact-ranking-main p,
.compact-feed-body p {
margin: 4px 0 0;
color: var(--muted);
font-size: 13px;
}
.compact-rank-badge,
.compact-feed-tag {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 28px;
padding: 0 10px;
border-radius: 999px;
background: rgba(15, 118, 110, 0.12);
color: #0f766e;
font-size: 12px;
font-weight: 700;
}
.compact-feed-item {
grid-template-columns: auto minmax(0, 1fr) auto;
}
.compact-feed-time {
color: var(--muted);
font-size: 12px;
white-space: nowrap;
}
/* ========== 记住账号 ========== */
.remember-inline {
display: flex;
align-items: center;
gap: 6px;
white-space: nowrap;
cursor: pointer;
font-size: 14px;
font-weight: 600;
}
/* ========== 验证码 ========== */
.captcha-row {
display: grid;
grid-template-columns: 1fr 140px auto;
gap: 12px;
align-items: center;
}
.captcha-row input {
min-width: 0;
}
.captcha-row canvas {
border-radius: 14px;
border: 1px solid var(--line);
cursor: pointer;
background: #fff;
}
/* ========== 响应式 ========== */
@media (max-width: 960px) {
.login-shell, .login-shell,
.register-shell, .register-shell,
.main-layout, .main-layout,
.profile-grid, .profile-grid,
.summary-grid, .summary-grid,
.dashboard-top,
.dashboard-bottom,
.dashboard-metrics,
.two-columns { .two-columns {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.topbar, .topbar,
.dashboard-hero-card,
.content-header, .content-header,
.section-head { .section-head {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
gap: 14px; gap: 14px;
} }
.dashboard-hero-card {
grid-template-columns: 1fr;
}
.donut-layout {
grid-template-columns: 1fr;
justify-items: center;
}
} }
@media (max-width: 640px) { @media (max-width: 720px) {
.captcha-row {
grid-template-columns: 1fr;
}
}
@media (max-width: 640px) {
body, body,
.app-body { .app-body {
padding: 14px; padding: 14px;
@ -541,8 +1249,21 @@ select[name="college"]:valid {
align-items: stretch; align-items: stretch;
} }
.event-table { .data-table {
display: block; min-width: auto;
overflow-x: auto; }
.dashboard-hero,
.dashboard-panel,
.dashboard-hero-card,
.metric-card,
.donut-panel {
padding: 18px;
}
.compact-ranking-item,
.compact-feed-item {
grid-template-columns: 1fr;
align-items: start;
} }
} }

1591
assets/js/app-main.js

File diff suppressed because it is too large

760
assets/js/app.js

@ -1,760 +0,0 @@
document.addEventListener('DOMContentLoaded', function () {
var currentUserName = document.getElementById('currentUserName');
var logoutBtn = document.getElementById('logoutBtn');
var sidebarNav = document.getElementById('sidebarNav');
var sectionTitle = document.getElementById('sectionTitle');
var sectionDesc = document.getElementById('sectionDesc');
var subTabs = document.getElementById('subTabs');
var mainView = document.getElementById('mainView');
var state = {
user: null,
currentView: '',
currentTab: 'all',
events: [],
myEvents: [],
adminUsers: [],
adminRegistrations: [],
adminEvents: [],
editingEventId: null
};
var studentMenus = [
{ key: 'profile', label: '涓汉淇℃伅', desc: '鏌ョ湅骞剁淮鎶ゅ綋鍓嶇櫥褰曚汉鍛樼殑鍩虹璧勬枡銆? },
{ key: 'events', label: '杩愬姩浼氭姤鍚?, desc: '娴忚鏈夐苟瀹屾垚鎶ュ悕锛屼篃鍙互鏌ョ湅鑷繁鐨勬姤鍚嶄俊鎭? }
];
var adminMenus = [
{ key: 'admin-home', label: '杩愬姩浼氱鐞?, desc: '鏌ョ湅鍚庡彴棣栭鍜岀郴缁熸瑙堛? },
{ key: 'team-info', label: '鍥綋淇℃伅绠$悊', desc: '缁存姢鍙傝禌鍥綋涓庣粍缁囦俊鎭? },
{ key: 'user-manage', label: '鐢ㄦ埛淇℃伅绠$悊', desc: '鏌ョ湅銆佺淮鎶ょ敤鎴疯祫鏂欏苟鎵ц彿绠悊鎿嶄綔銆? },
{ key: 'event-manage', label: '椤圭洰绠$悊', desc: '缁存姢璧涗簨椤圭洰銆佸垎绫汇佺粍鍒拰鎶ュ悕鍙傛暟銆? },
{ key: 'athlete-manage', label: '鍙傝禌杩愬姩鍛樼鐞?, desc: '鏌ョ湅鎵鏈夊凡鎶ュ悕杩愬姩鍛樺拰鍙傝禌鍚嶅崟銆? },
{ key: 'score-manage', label: '鍙傝禌鎴愮哗绠$悊', desc: '褰曞叆銆佹煡鐪嬪拰缁存姢姣旇禌鎴愮哗銆? },
{ key: 'record-manage', label: '椤圭洰璁板綍绠$悊', desc: '悊椤圭洰璁板綍銆佺З搴忓唽鍜岃禌浜嬭褰曘? },
{ key: 'system-manage', label: '绯荤粺绠$悊', desc: '缁存姢绯荤粺鍩虹閰嶇疆鍜屽悗鍙拌繍琛屼俊鎭? }
];
function escapeHtml(value) {
return String(value == null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function getMenus() {
return state.user && state.user.role === 'ADMIN' ? adminMenus : studentMenus;
}
function renderSidebar() {
var menus = getMenus();
sidebarNav.innerHTML = menus.map(function (item, index) {
return '<button class="nav-item ' + (item.key === state.currentView ? 'active' : '') + '" data-view="' + item.key + '">' + item.label + '</button>';
}).join('');
Array.prototype.slice.call(sidebarNav.querySelectorAll('.nav-item')).forEach(function (button) {
button.addEventListener('click', function () {
switchView(button.getAttribute('data-view'));
});
});
}
function renderTabs() {
var html = '';
if (state.currentView === 'events') {
html = ''
+ '<button class="tab-item ' + (state.currentTab === 'all' ? 'active' : '') + '" data-tab="all">鎶ュ悕</button>'
+ '<button class="tab-item ' + (state.currentTab === 'mine' ? 'active' : '') + '" data-tab="mine">鎶ュ悕淇℃伅</button>';
}
subTabs.innerHTML = html;
subTabs.classList.toggle('hidden', !html);
Array.prototype.slice.call(subTabs.querySelectorAll('.tab-item')).forEach(function (button) {
button.addEventListener('click', function () {
state.currentTab = button.getAttribute('data-tab');
renderTabs();
renderCurrentView();
});
});
}
function renderProfile() {
mainView.innerHTML = ''
+ '<div class="section-block">'
+ ' <div class="section-head"><div><h3>鍩虹璧勬枡</h3><p class="info-meta">褰撳墠璐﹀彿鐨勪釜浜轰俊鎭涓嬨€?/p></div></div>'
+ ' <div class="profile-grid">'
+ ' <div class="info-card"><strong>韬唤璇佸彿</strong><p>' + escapeHtml(state.user.idCard) + '</p></div>'
+ ' <div class="info-card"><strong>鐧诲綍璐﹀彿</strong><p>' + escapeHtml(state.user.username) + '</p></div>'
+ ' <div class="info-card"><strong>濮撳悕</strong><p>' + escapeHtml(state.user.name) + '</p></div>'
+ ' <div class="info-card"><strong>鑱旂郴鐢佃瘽</strong><p>' + escapeHtml(state.user.phone) + '</p></div>'
+ ' <div class="info-card"><strong>鎬у埆</strong><p>' + escapeHtml(state.user.gender) + '</p></div>'
+ ' <div class="info-card"><strong>瀛﹂櫌</strong><p>' + escapeHtml(state.user.college) + '</p></div>'
+ ' <div class="info-card"><strong>鐝骇</strong><p>' + escapeHtml(state.user.className) + '</p></div>'
+ ' <div class="info-card"><strong>瀛﹀彿</strong><p>' + escapeHtml(state.user.studentNo) + '</p></div>'
+ ' <div class="info-card"><strong>绫诲埆</strong><p>' + escapeHtml(state.user.category) + '</p></div>'
+ ' <div class="info-card"><strong>瑙掕壊</strong><p>' + escapeHtml(state.user.role) + '</p></div>'
+ ' </div>'
+ '</div>';
}
function renderProfile() {
mainView.innerHTML = ''
+ '<div class="section-block">'
+ ' <div class="section-head"><div><h3>鍩虹璧勬枡</h3><p class="info-meta">褰撳墠璐﹀彿鐨勪釜浜轰俊鎭涓嬨€?/p></div></div>'
+ ' <div class="profile-grid">'
+ ' <div class="info-card"><strong>韬唤璇佸彿</strong><p>' + escapeHtml(state.user.idCard) + '</p></div>'
+ ' <div class="info-card"><strong>鐧诲綍璐﹀彿</strong><p>' + escapeHtml(state.user.username) + '</p></div>'
+ ' <div class="info-card"><strong>濮撳悕</strong><p>' + escapeHtml(state.user.name) + '</p></div>'
+ ' <div class="info-card"><strong>鑱旂郴鐢佃瘽</strong><p>' + escapeHtml(state.user.phone) + '</p></div>'
+ ' <div class="info-card"><strong>鎬у埆</strong><p>' + escapeHtml(state.user.gender) + '</p></div>'
+ ' <div class="info-card"><strong>瀛﹂櫌</strong><p>' + escapeHtml(state.user.college) + '</p></div>'
+ ' <div class="info-card"><strong>鐝骇</strong><p>' + escapeHtml(state.user.className) + '</p></div>'
+ ' <div class="info-card"><strong>瀛﹀彿</strong><p>' + escapeHtml(state.user.studentNo) + '</p></div>'
+ ' <div class="info-card"><strong>绫诲埆</strong><p>' + escapeHtml(state.user.category) + '</p></div>'
+ ' <div class="info-card"><strong>瑙掕壊</strong><p>' + escapeHtml(state.user.role) + '</p></div>'
+ ' </div>'
+ '</div>'
+ '<div class="section-block">'
+ ' <div class="section-head"><div><h3>淇敼涓汉淇℃伅</h3><p class="info-meta">鍙互鍦ㄨ繖閲屾洿鏂板鍚嶃€佺數璇濄€佸闄€佺彮绾у拰瀛﹀彿绛変俊鎭€?/p></div></div>'
+ ' <form id="profileForm" class="form-grid two-columns">'
+ ' <label class="field"><span>濮撳悕</span><input name="name" value="' + escapeHtml(state.user.name) + '" placeholder="璇疯緭鍏ュ鍚?></label>'
+ ' <label class="field"><span>鑱旂郴鐢佃瘽</span><input name="phone" value="' + escapeHtml(state.user.phone) + '" placeholder="璇疯緭鍏ョ數璇?></label>'
+ ' <label class="field"><span>Gender</span><input name="gender" value="' + escapeHtml(state.user.gender) + '" placeholder="gender"></label>'
+ ' <label class="field"><span>???</span><input name="college" value="' + escapeHtml(state.user.college) + '" placeholder="????????></label>'
+ ' <label class="field"><span>???</span><input name="className" value="' + escapeHtml(state.user.className) + '" placeholder="????????></label>'
+ ' <label class="field"><span>???</span><input name="studentNo" value="' + escapeHtml(state.user.studentNo) + '" placeholder="????????></label>'
+ ' <label class="field full-row"><span>???</span><input name="category" value="' + escapeHtml(state.user.category) + '" placeholder="?????????????></label>'
+ ' </label>'
+ ' <label class="field"><span>瀛﹂櫌</span><input name="college" value="' + escapeHtml(state.user.college) + '" placeholder="璇疯緭鍏ュ闄?></label>'
+ ' <label class="field"><span>鐝骇</span><input name="className" value="' + escapeHtml(state.user.className) + '" placeholder="璇疯緭鍏ョ彮绾?></label>'
+ ' <label class="field"><span>瀛﹀彿</span><input name="studentNo" value="' + escapeHtml(state.user.studentNo) + '" placeholder="璇疯緭鍏ュ鍙?></label>'
+ ' <label class="field full-row"><span>绫诲埆</span><input name="category" value="' + escapeHtml(state.user.category) + '" placeholder="渚嬪锛氬鐢熴€佹暀甯?></label>'
+ ' <div class="form-actions full-row">'
+ ' <button type="submit" class="primary-btn">淇濆瓨淇敼</button>'
+ ' </div>'
+ ' <p id="profileMessage" class="form-message full-row"></p>'
+ ' </form>'
+ '</div>';
bindProfileForm();
}
function bindProfileForm() {
var form = document.getElementById('profileForm');
var messageEl = document.getElementById('profileMessage');
if (!form) {
return;
}
form.addEventListener('submit', function (event) {
event.preventDefault();
var formData = new FormData(form);
var payload = {
name: String(formData.get('name') || '').trim(),
phone: String(formData.get('phone') || '').trim(),
gender: String(formData.get('gender') || '').trim(),
college: String(formData.get('college') || '').trim(),
className: String(formData.get('className') || '').trim(),
studentNo: String(formData.get('studentNo') || '').trim(),
category: String(formData.get('category') || '').trim()
};
appUtils.ajax({
method: 'PUT',
url: '/api/users/me',
data: payload,
success: function (response) {
if (!response.success || !response.data) {
appUtils.showMessage(messageEl, response.message || '淇敼澶辫触', false);
return;
}
state.user = response.data;
currentUserName.textContent = state.user.name + (state.user.role === 'ADMIN' ? ' 绠$悊鍛? : ' 鐢ㄦ埛');
renderProfile();
appUtils.showMessage(document.getElementById('profileMessage'), 'Saved successfully', true);
},
error: function (xhr, response) {
appUtils.showMessage(messageEl, (response && response.message) || '淇敼澶辫触', false);
}
});
});
}
function renderEventTable(list, isMine) {
if (!list.length) {
mainView.innerHTML = '<div class="empty-state"><h3>鏆傛棤鏁版嵁</h3><p>' + (isMine ? '褰撳墠杩樻病鏈夋姤鍚嶈褰曘€? : '褰撳墠娌湁鍙睍绀洪?) + '</p></div>';
return;
}
mainView.innerHTML = ''
+ '<table class="event-table">'
+ ' <thead><tr><th>椤圭洰鍚嶇О</th><th>椤圭洰绫诲埆</th><th>姣旇禌鏃堕棿</th><th>姣旇禌鍦扮偣</th><th>鎶ュ悕鎯呭喌</th><th>椤圭洰璇存槑</th><th>鎿嶄綔</th></tr></thead>'
+ ' <tbody>'
+ list.map(function (item) {
var actionHtml = isMine
? '<button class="action-btn cancel-btn" data-id="' + item.id + '">鍙栨秷鎶ュ悕</button>'
: '<button class="action-btn register-btn" data-id="' + item.id + '" ' + (item.registered ? 'disabled' : '') + '>' + (item.registered ? '宸叉姤鍚? : '鎶ュ悕') + '</button>';
return ''
+ '<tr>'
+ '<td>' + escapeHtml(item.eventName) + '</td>'
+ '<td>' + escapeHtml(item.eventCategory) + '</td>'
+ '<td>' + escapeHtml(item.eventTime) + '</td>'
+ '<td>' + escapeHtml(item.location) + '</td>'
+ '<td>' + escapeHtml(item.registeredCount + '/' + item.quota) + '</td>'
+ '<td>' + escapeHtml(item.description) + '</td>'
+ '<td>' + actionHtml + '</td>'
+ '</tr>';
}).join('')
+ ' </tbody>'
+ '</table>';
bindEventButtons();
}
function renderUserManage() {
if (!state.adminUsers.length) {
mainView.innerHTML = '<div class="empty-state"><h3>鏆傛棤鐢ㄦ埛</h3><p>褰撳墠绯荤粺杩樻病鏈夌敤鎴锋暟鎹€?/p></div>';
return;
}
mainView.innerHTML = ''
+ '<div class="section-block">'
+ ' <div class="section-head"><div><h3>鏁版嵁姒傝</h3><p class="info-meta">鏌ョ湅褰撳墠绯荤粺鍐呯敤鎴锋€讳綋鎯呭喌銆?/p></div></div>'
+ ' <div class="summary-grid">'
+ ' <div class="summary-card"><strong>鐢ㄦ埛鎬绘暟</strong><p>' + state.adminUsers.length + '</p></div>'
+ ' <div class="summary-card"><strong>绠$悊鍛樻暟閲?/strong><p>' + state.adminUsers.filter(function (item) { return item.role === "ADMIN"; }).length + '</p></div>'
+ ' <div class="summary-card"><strong>绂佺敤璐﹀彿鏁伴噺</strong><p>' + state.adminUsers.filter(function (item) { return item.status === "DISABLED"; }).length + '</p></div>'
+ ' </div>'
+ '</div>'
+ '<div class="section-block">'
+ ' <div class="section-head"><div><h3>鐢ㄦ埛鍒楄〃</h3><p class="info-meta">鏌ョ湅绯荤粺鍐呮墍鏈夌敤鎴蜂俊鎭€?/p></div></div>'
+ ' <table class="event-table">'
+ ' <thead><tr><th>濮撳悕</th><th>璐﹀彿</th><th>韬唤璇佸彿</th><th>鐢佃瘽</th><th>鎬у埆</th><th>瀛﹂櫌</th><th>绫诲埆</th><th>瑙掕壊</th><th>鐘舵€?/th><th>鎿嶄綔</th></tr></thead>'
+ ' <tbody>'
+ state.adminUsers.map(function (item) {
var isAdmin = item.role === 'ADMIN';
var statusText = item.status === 'DISABLED' ? '宸茬鐢? : '姝e父';
var statusAction = isAdmin
? ''
: (item.status === 'DISABLED'
? '<button class="action-btn enable-user-btn" data-id="' + item.id + '">瑙i櫎绂佺敤</button>'
: '<button class="action-btn disable-user-btn" data-id="' + item.id + '">绂佺敤璐︽埛</button>');
var deleteAction = isAdmin ? '' : '<button class="action-btn delete-user-btn" data-id="' + item.id + '">鍒犻櫎</button>';
return ''
+ '<tr>'
+ '<td>' + escapeHtml(item.name) + '</td>'
+ '<td>' + escapeHtml(item.username) + '</td>'
+ '<td>' + escapeHtml(item.idCard) + '</td>'
+ '<td>' + escapeHtml(item.phone) + '</td>'
+ '<td>' + escapeHtml(item.gender) + '</td>'
+ '<td>' + escapeHtml(item.college) + '</td>'
+ '<td>' + escapeHtml(item.category) + '</td>'
+ '<td>' + escapeHtml(isAdmin ? '绠$悊鍛? : '氱敤鎴?) + '</td>'
+ '<td>' + escapeHtml(statusText) + '</td>'
+ '<td><button class="action-btn reset-password-btn" data-id="' + item.id + '">閲嶇疆瀵嗙爜</button> ' + statusAction + ' ' + deleteAction + '</td>'
+ '</tr>';
}).join('')
+ ' </tbody>'
+ ' </table>'
+ '</div>';
bindUserManageActions();
}
function renderAthleteManage() {
if (!state.adminRegistrations.length) {
mainView.innerHTML = '<div class="empty-state"><h3>鏆傛棤鎶ュ悕璁板綍</h3><p>褰撳墠杩樻病鏈夊弬璧涜繍鍔ㄥ憳鏁版嵁銆?/p></div>';
return;
}
mainView.innerHTML = ''
+ '<div class="section-block">'
+ ' <div class="section-head"><div><h3>鏁版嵁姒傝</h3><p class="info-meta">鏌ョ湅褰撳墠鍙傝禌杩愬姩鍛樼殑鎬讳綋缁熻銆?/p></div></div>'
+ ' <div class="summary-grid">'
+ ' <div class="summary-card"><strong>鎶ュ悕璁板綍鎬绘暟</strong><p>' + state.adminRegistrations.length + '</p></div>'
+ ' <div class="summary-card"><strong>宸叉姤鍚嶇姸鎬?/strong><p>' + state.adminRegistrations.filter(function (item) { return item.status === "宸叉姤鍚?; }).length + '</p></div>'
+ ' <div class="summary-card"><strong>瑕嗙洊椤圭洰鏁?/strong><p>' + uniqueEventCount() + '</p></div>'
+ ' </div>'
+ '</div>'
+ '<div class="section-block">'
+ ' <div class="section-head"><div><h3>鍙傝禌鍚嶅崟</h3><p class="info-meta">鏌ョ湅鎵€鏈夊凡鎶ュ悕杩愬姩鍛樺拰椤圭洰鏄庣粏銆?/p></div></div>'
+ ' <table class="event-table">'
+ ' <thead><tr><th>濮撳悕</th><th>璐﹀彿</th><th>鐢佃瘽</th><th>瀛﹂櫌</th><th>绫诲埆</th><th>椤圭洰鍚嶇О</th><th>椤圭洰绫诲埆</th><th>鏃堕棿鍦扮偣</th><th>鐘舵€?/th><th>鎶ュ悕鏃堕棿</th></tr></thead>'
+ ' <tbody>'
+ state.adminRegistrations.map(function (item) {
return ''
+ '<tr>'
+ '<td>' + escapeHtml(item.studentName) + '</td>'
+ '<td>' + escapeHtml(item.username) + '</td>'
+ '<td>' + escapeHtml(item.phone) + '</td>'
+ '<td>' + escapeHtml(item.college) + '</td>'
+ '<td>' + escapeHtml(item.category) + '</td>'
+ '<td>' + escapeHtml(item.eventName) + '</td>'
+ '<td>' + escapeHtml(item.eventCategory) + '</td>'
+ '<td>' + escapeHtml(item.eventTime + ' / ' + item.location) + '</td>'
+ '<td>' + escapeHtml(item.status) + '</td>'
+ '<td>' + escapeHtml(item.createdAt) + '</td>'
+ '</tr>';
}).join('')
+ ' </tbody>'
+ ' </table>'
+ '</div>';
}
function renderEventManage() {
var formTitle = state.editingEventId ? '缂栬緫椤圭洰' : '鏂板椤圭洰';
var editingItem = state.adminEvents.find(function (item) {
return item.id === state.editingEventId;
}) || {
eventName: '',
eventCategory: '',
location: '',
quota: '',
description: '',
eventTime: ''
};
mainView.innerHTML = ''
+ '<div class="section-block">'
+ ' <div class="section-head"><div><h3>' + formTitle + '</h3><p class="info-meta">鍙湪杩欓噷缁存姢姣旇禌椤圭洰鐨勫熀纭€淇℃伅銆?/p></div></div>'
+ ' <form id="eventForm" class="form-grid two-columns">'
+ ' <label class="field"><span>椤圭洰鍚嶇О</span><input name="eventName" value="' + escapeHtml(editingItem.eventName) + '" placeholder="渚嬪锛氱敺瀛?00绫?></label>'
+ ' <label class="field"><span>椤圭洰鍒嗙被</span><input name="eventCategory" value="' + escapeHtml(editingItem.eventCategory) + '" placeholder="渚嬪锛氱敯寰勭煭璺?></label>'
+ ' <label class="field"><span>姣旇禌鍦扮偣</span><input name="location" value="' + escapeHtml(editingItem.location) + '" placeholder="渚嬪锛氫笢鎿嶅満A鍖?></label>'
+ ' <label class="field"><span>浜烘暟涓婇檺</span><input name="quota" type="number" min="1" value="' + escapeHtml(editingItem.quota) + '" placeholder="璇疯緭鍏ヤ汉鏁颁笂闄?></label>'
+ ' <label class="field full-row"><span>姣旇禌鏃堕棿</span><input name="eventTime" value="' + escapeHtml(editingItem.eventTime) + '" placeholder="渚嬪锛?026-05-20 08:30"></label>'
+ ' <label class="field full-row"><span>椤圭洰璇存槑</span><input name="description" value="' + escapeHtml(editingItem.description) + '" placeholder="璇疯緭鍏ラ」鐩鏄?></label>'
+ ' <div class="form-actions full-row">'
+ ' <button type="submit" class="primary-btn">' + (state.editingEventId ? '淇濆瓨淇敼' : '鏂板椤圭洰') + '</button>'
+ ' <button type="button" class="ghost-btn" id="resetEventForm">娓呯┖琛ㄥ崟</button>'
+ ' </div>'
+ ' <p id="eventFormMessage" class="form-message full-row"></p>'
+ ' </form>'
+ '</div>'
+ '<div class="section-block">'
+ ' <div class="section-head"><div><h3>椤圭洰鍒楄〃</h3><p class="info-meta">褰撳墠绯荤粺鍐呯殑鍏ㄩ儴姣旇禌椤圭洰銆?/p></div></div>'
+ (state.adminEvents.length ? ''
+ '<table class="event-table">'
+ ' <thead><tr><th>椤圭洰鍚嶇О</th><th>椤圭洰鍒嗙被</th><th>姣旇禌鏃堕棿</th><th>姣旇禌鍦扮偣</th><th>浜烘暟涓婇檺</th><th>宸叉姤鍚?/th><th>椤圭洰璇存槑</th><th>鎿嶄綔</th></tr></thead>'
+ ' <tbody>'
+ state.adminEvents.map(function (item) {
return ''
+ '<tr>'
+ '<td>' + escapeHtml(item.eventName) + '</td>'
+ '<td>' + escapeHtml(item.eventCategory) + '</td>'
+ '<td>' + escapeHtml(item.eventTime) + '</td>'
+ '<td>' + escapeHtml(item.location) + '</td>'
+ '<td>' + escapeHtml(item.quota) + '</td>'
+ '<td>' + escapeHtml(item.registeredCount || 0) + '</td>'
+ '<td>' + escapeHtml(item.description) + '</td>'
+ '<td><button class="action-btn edit-event-btn" data-id="' + item.id + '">缂栬緫</button> <button class="action-btn delete-event-btn" data-id="' + item.id + '">鍒犻櫎</button></td>'
+ '</tr>';
}).join('')
+ ' </tbody>'
+ '</table>'
: '<div class="empty-state"><h3>鏆傛棤椤圭洰</h3><p>褰撳墠杩樻病鏈夋瘮璧涢」鐩紝璇峰厛鏂板椤圭洰銆?/p></div>')
+ '</div>';
bindEventManageActions();
}
function renderPlaceholder(title, text) {
mainView.innerHTML = ''
+ '<div class="empty-state">'
+ ' <h3>' + title + '</h3>'
+ ' <p>' + text + '</p>'
+ '</div>';
}
function uniqueEventCount() {
var map = {};
state.adminRegistrations.forEach(function (item) {
map[item.eventName] = true;
});
return Object.keys(map).length;
}
function bindEventButtons() {
Array.prototype.slice.call(document.querySelectorAll('.register-btn')).forEach(function (button) {
button.addEventListener('click', function () {
appUtils.ajax({
method: 'POST',
url: '/api/events/' + button.getAttribute('data-id') + '/register',
success: function (response) {
if (!response.success) {
window.alert(response.message || '鎶ュ悕澶辫触');
return;
}
window.alert('鎶ュ悕鎴愬姛');
loadEventData();
},
error: function (xhr, response) {
window.alert((response && response.message) || '鎶ュ悕澶辫触');
}
});
});
});
Array.prototype.slice.call(document.querySelectorAll('.cancel-btn')).forEach(function (button) {
button.addEventListener('click', function () {
appUtils.ajax({
method: 'DELETE',
url: '/api/events/' + button.getAttribute('data-id') + '/register',
success: function (response) {
if (!response.success) {
window.alert(response.message || '鍙栨秷鎶ュ悕澶辫触');
return;
}
window.alert('鍙栨秷鎶ュ悕鎴愬姛');
loadEventData();
},
error: function (xhr, response) {
window.alert((response && response.message) || '鍙栨秷鎶ュ悕澶辫触');
}
});
});
});
}
function bindUserManageActions() {
Array.prototype.slice.call(document.querySelectorAll('.reset-password-btn')).forEach(function (button) {
button.addEventListener('click', function () {
var id = button.getAttribute('data-id');
appUtils.ajax({
method: 'POST',
url: '/api/admin/users/' + id + '/reset-password',
success: function (response) {
if (!response.success) {
window.alert(response.message || '閲嶇疆瀵嗙爜澶辫触');
return;
}
window.alert('瀵嗙爜宸查噸缃负 123456');
},
error: function (xhr, response) {
window.alert((response && response.message) || '閲嶇疆瀵嗙爜澶辫触');
}
});
});
});
Array.prototype.slice.call(document.querySelectorAll('.disable-user-btn')).forEach(function (button) {
button.addEventListener('click', function () {
var id = button.getAttribute('data-id');
appUtils.ajax({
method: 'POST',
url: '/api/admin/users/' + id + '/disable',
success: function (response) {
if (!response.success) {
window.alert(response.message || '绂佺敤璐︽埛澶辫触');
return;
}
loadAdminData();
},
error: function (xhr, response) {
window.alert((response && response.message) || '绂佺敤璐︽埛澶辫触');
}
});
});
});
Array.prototype.slice.call(document.querySelectorAll('.enable-user-btn')).forEach(function (button) {
button.addEventListener('click', function () {
var id = button.getAttribute('data-id');
appUtils.ajax({
method: 'POST',
url: '/api/admin/users/' + id + '/enable',
success: function (response) {
if (!response.success) {
window.alert(response.message || '瑙i櫎绂佺敤澶辫触');
return;
}
loadAdminData();
},
error: function (xhr, response) {
window.alert((response && response.message) || '瑙i櫎绂佺敤澶辫触');
}
});
});
});
Array.prototype.slice.call(document.querySelectorAll('.delete-user-btn')).forEach(function (button) {
button.addEventListener('click', function () {
var id = button.getAttribute('data-id');
if (!window.confirm('畾瑕佸垹闄よ繖涓处鍙峰悧锛?)) {
return;
}
appUtils.ajax({
method: 'DELETE',
url: '/api/admin/users/' + id,
success: function (response) {
if (!response.success) {
window.alert(response.message || '鍒犻櫎鐢ㄦ埛澶辫触');
return;
}
loadAdminData();
},
error: function (xhr, response) {
window.alert((response && response.message) || '鍒犻櫎鐢ㄦ埛澶辫触');
}
});
});
});
}
function renderCurrentView() {
var currentMenu = getMenus().find(function (item) {
return item.key === state.currentView;
});
if (currentMenu) {
sectionTitle.textContent = currentMenu.label;
sectionDesc.textContent = currentMenu.desc;
}
renderTabs();
if (state.currentView === 'profile') {
renderProfile();
return;
}
if (state.currentView === 'events') {
renderEventTable(state.currentTab === 'mine' ? state.myEvents : state.events, state.currentTab === 'mine');
return;
}
if (state.currentView === 'user-manage') {
renderUserManage();
return;
}
if (state.currentView === 'athlete-manage') {
renderAthleteManage();
return;
}
if (state.currentView === 'admin-home') {
renderPlaceholder('杩愬姩浼氱鐞?, '杩欓噷灏嗕綔涓虹鐞嗗憳鍚庡彴棣栭锛屽彲灞曠ず鎶ュ悕鎬昏銆佺郴缁熷叕鍛娿佽繍鍔ㄤ細绠悊鍏ュ彛绛夊唴瀹广?);
return;
}
if (state.currentView === 'team-info') {
renderPlaceholder('鍥綋淇℃伅绠$悊', '杩欓噷灏嗙户缁ˉ鍏呭侀儴闂ㄣ佷唬琛ㄩ槦绛夊洟浣撲俊鎭鐞嗗姛鑳姐?);
return;
}
if (state.currentView === 'event-manage') {
renderEventManage();
return;
}
if (state.currentView === 'score-manage') {
renderPlaceholder('鍙傝禌鎴愮哗绠$悊', '杩欓噷灏嗙户缁ˉ鍏呮垚缁綍鍏ャ佹垚缁淮鎶ゃ佹垚缁煡璇瓑鍔熻兘銆?);
return;
}
if (state.currentView === 'record-manage') {
renderPlaceholder('椤圭洰璁板綍绠$悊', '杩欓噷灏嗙户缁ˉ鍏呴褰曘佽禌浜嬬З搴忓唽鍜岄妗堢鐞嗗姛鑳姐?);
return;
}
if (state.currentView === 'system-manage') {
renderPlaceholder('绯荤粺绠$悊', '杩欓噷灏嗙户缁ˉ鍏呯郴缁熼厤缃佽处鍙锋潈闄愬拰杩愯缁存姢鍔熻兘銆?);
}
}
function switchView(view) {
state.currentView = view;
if (view === 'events' && state.currentTab !== 'mine') {
state.currentTab = 'all';
}
renderSidebar();
renderCurrentView();
}
function loadEventData() {
appUtils.ajax({
method: 'GET',
url: '/api/events',
success: function (response) {
if (response.success) {
state.events = response.data || [];
renderCurrentView();
}
}
});
appUtils.ajax({
method: 'GET',
url: '/api/events/my',
success: function (response) {
if (response.success) {
state.myEvents = response.data || [];
renderCurrentView();
}
}
});
}
function loadAdminData() {
if (!state.user || state.user.role !== 'ADMIN') {
return;
}
appUtils.ajax({
method: 'GET',
url: '/api/admin/users',
success: function (response) {
if (response.success) {
state.adminUsers = response.data || [];
renderCurrentView();
}
}
});
appUtils.ajax({
method: 'GET',
url: '/api/admin/registrations',
success: function (response) {
if (response.success) {
state.adminRegistrations = response.data || [];
renderCurrentView();
}
}
});
appUtils.ajax({
method: 'GET',
url: '/api/admin/events',
success: function (response) {
if (response.success) {
state.adminEvents = response.data || [];
renderCurrentView();
}
}
});
}
function bindEventManageActions() {
var form = document.getElementById('eventForm');
var resetButton = document.getElementById('resetEventForm');
var messageEl = document.getElementById('eventFormMessage');
if (form) {
form.addEventListener('submit', function (event) {
event.preventDefault();
var formData = new FormData(form);
var payload = {
eventName: String(formData.get('eventName') || '').trim(),
eventCategory: String(formData.get('eventCategory') || '').trim(),
location: String(formData.get('location') || '').trim(),
quota: Number(formData.get('quota') || 0),
description: String(formData.get('description') || '').trim(),
eventTime: String(formData.get('eventTime') || '').trim()
};
var successText = state.editingEventId ? '椤圭洰淇敼鎴愬姛' : '椤圭洰鏂板鎴愬姛';
appUtils.ajax({
method: state.editingEventId ? 'PUT' : 'POST',
url: state.editingEventId ? '/api/admin/events/' + state.editingEventId : '/api/admin/events',
data: payload,
success: function (response) {
if (!response.success) {
appUtils.showMessage(messageEl, response.message || '淇濆瓨澶辫触', false);
return;
}
state.editingEventId = null;
window.alert(successText);
loadAdminData();
},
error: function (xhr, response) {
appUtils.showMessage(messageEl, (response && response.message) || '淇濆瓨澶辫触', false);
}
});
});
}
if (resetButton) {
resetButton.addEventListener('click', function () {
state.editingEventId = null;
renderCurrentView();
});
}
Array.prototype.slice.call(document.querySelectorAll('.edit-event-btn')).forEach(function (button) {
button.addEventListener('click', function () {
state.editingEventId = Number(button.getAttribute('data-id'));
renderCurrentView();
});
});
Array.prototype.slice.call(document.querySelectorAll('.delete-event-btn')).forEach(function (button) {
button.addEventListener('click', function () {
var eventId = button.getAttribute('data-id');
if (!window.confirm('畾瑕佸垹闄よ繖涓悧锛熷垹闄ゅ悗璇ラ殑鎶ュ悕璁板綍涔熶細涓骞舵竻闄ゃ?)) {
return;
}
appUtils.ajax({
method: 'DELETE',
url: '/api/admin/events/' + eventId,
success: function (response) {
if (!response.success) {
window.alert(response.message || '鍒犻櫎澶辫触');
return;
}
if (state.editingEventId === Number(eventId)) {
state.editingEventId = null;
}
loadAdminData();
},
error: function (xhr, response) {
window.alert((response && response.message) || '鍒犻櫎澶辫触');
}
});
});
});
}
function loadCurrentUser() {
appUtils.ajax({
method: 'GET',
url: '/api/users/me',
success: function (response) {
if (!response.success || !response.data) {
window.location.href = './login.html';
return;
}
state.user = response.data;
currentUserName.textContent = state.user.name + (state.user.role === 'ADMIN' ? ' 绠$悊鍛? : ' 鐢ㄦ埛');
state.currentView = state.user.role === 'ADMIN' ? 'admin-home' : 'profile';
renderSidebar();
renderCurrentView();
loadEventData();
loadAdminData();
},
error: function () {
window.location.href = './login.html';
}
});
}
logoutBtn.addEventListener('click', function () {
appUtils.ajax({
method: 'POST',
url: '/api/auth/logout',
success: function () {
window.location.href = './login.html';
},
error: function () {
window.location.href = './login.html';
}
});
});
loadCurrentUser();
});

29
assets/js/common.js

@ -1,9 +1,25 @@
(function () { (function () {
function normalizeBase(base) {
return String(base || '').replace(/\/+$/, '');
}
function normalizePath(path) {
var value = String(path || '').trim();
if (!value) {
return '';
}
return value.charAt(0) === '/' ? value : '/' + value;
}
function resolveApiBase() { function resolveApiBase() {
if (window.location.protocol === 'file:') { if (window.APP_CONFIG && window.APP_CONFIG.API_BASE) {
return 'http://localhost:8080'; return normalizeBase(window.APP_CONFIG.API_BASE);
} }
return window.location.protocol + '//' + window.location.hostname + ':8080'; var protocol = window.location.protocol === 'file:' ? 'http:' : window.location.protocol;
var host = window.location.hostname || 'localhost';
var port = (window.APP_CONFIG && window.APP_CONFIG.API_PORT) || '8080';
var contextPath = normalizePath(window.APP_CONFIG && window.APP_CONFIG.API_CONTEXT_PATH);
return normalizeBase(protocol + '//' + host + ':' + port + contextPath);
} }
var API_BASE = resolveApiBase(); var API_BASE = resolveApiBase();
@ -19,7 +35,10 @@
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();
xhr.open(options.method || 'GET', buildUrl(options.url), true); xhr.open(options.method || 'GET', buildUrl(options.url), true);
xhr.withCredentials = true; xhr.withCredentials = true;
xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8'); var isFormData = typeof FormData !== 'undefined' && options.data instanceof FormData;
if (!isFormData) {
xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
}
xhr.onreadystatechange = function () { xhr.onreadystatechange = function () {
var response; var response;
if (xhr.readyState !== 4) { if (xhr.readyState !== 4) {
@ -39,7 +58,7 @@
xhr.onerror = function () { xhr.onerror = function () {
options.error && options.error(xhr, { success: false, message: '网络异常,请确认后端已启动' }); options.error && options.error(xhr, { success: false, message: '网络异常,请确认后端已启动' });
}; };
xhr.send(options.data ? JSON.stringify(options.data) : null); xhr.send(options.data ? (isFormData ? options.data : JSON.stringify(options.data)) : null);
} }
function showMessage(element, message, isSuccess) { function showMessage(element, message, isSuccess) {

7
assets/js/config.js

@ -0,0 +1,7 @@
window.APP_CONFIG = {
// 留空时,前端会自动使用“当前页面的主机名 + 8080 + 上下文路径”访问后端。
// 例如页面从 http://127.0.0.1:5500 打开时,后端会自动请求 http://127.0.0.1:8080/sports-meet-signup
API_BASE: '',
API_PORT: '8080',
API_CONTEXT_PATH: '/sports-meet-signup'
};

139
assets/js/login.js

@ -1,55 +1,154 @@
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
appUtils.redirectIfLoggedIn(); appUtils.redirectIfLoggedIn();
var form = document.getElementById('loginForm'); var form = document.getElementById('loginForm');
var message = document.getElementById('loginMessage'); var message = document.getElementById('loginMessage');
var goRegister = document.getElementById('goRegister'); var goRegister = document.getElementById('goRegister');
var usernameInput = document.querySelector('input[name="username"]'); var refreshCaptchaBtn = document.getElementById('refreshCaptcha');
var passwordInput = document.querySelector('input[name="password"]'); var captchaCanvas = document.getElementById('captchaCanvas');
var captchaCtx = captchaCanvas.getContext('2d');
// 强制清空输入字段,确保不保留历史登录信息 var captchaCode = '';
setTimeout(function() { var rememberCheckbox = document.getElementById('rememberAccount');
usernameInput.value = ''; var loginIdInput = form.querySelector('[name="loginId"]');
passwordInput.value = '';
// 移除可能的自动填充样式 // 记住账号:页面加载时填充
usernameInput.style.background = 'rgba(255, 255, 255, 0.95)'; var remembered = localStorage.getItem('rememberedAccount');
passwordInput.style.background = 'rgba(255, 255, 255, 0.95)'; if (remembered) {
}, 100); loginIdInput.value = remembered;
rememberCheckbox.checked = true;
// 再次清空,确保浏览器自动填充后也能被清空 }
setTimeout(function() {
usernameInput.value = ''; // 勾选时立即保存
passwordInput.value = ''; rememberCheckbox.addEventListener('change', function () {
}, 500); if (rememberCheckbox.checked) {
var currentValue = String(loginIdInput.value || '').trim();
if (currentValue) {
localStorage.setItem('rememberedAccount', currentValue);
}
} else {
localStorage.removeItem('rememberedAccount');
}
});
// 输入时更新(如果已勾选记住账号)
loginIdInput.addEventListener('input', function () {
if (rememberCheckbox.checked) {
var val = String(loginIdInput.value || '').trim();
if (val) {
localStorage.setItem('rememberedAccount', val);
} else {
localStorage.removeItem('rememberedAccount');
}
}
});
function randomChar() {
var chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
return chars.charAt(Math.floor(Math.random() * chars.length));
}
function randomColor(min, max) {
var r = Math.floor(Math.random() * (max - min) + min);
var g = Math.floor(Math.random() * (max - min) + min);
var b = Math.floor(Math.random() * (max - min) + min);
return 'rgb(' + r + ',' + g + ',' + b + ')';
}
function drawCaptcha() {
captchaCode = '';
for (var i = 0; i < 4; i += 1) {
captchaCode += randomChar();
}
captchaCtx.clearRect(0, 0, captchaCanvas.width, captchaCanvas.height);
captchaCtx.fillStyle = '#f8fbff';
captchaCtx.fillRect(0, 0, captchaCanvas.width, captchaCanvas.height);
for (var i = 0; i < 6; i += 1) {
captchaCtx.strokeStyle = randomColor(160, 220);
captchaCtx.beginPath();
captchaCtx.moveTo(Math.random() * 140, Math.random() * 48);
captchaCtx.lineTo(Math.random() * 140, Math.random() * 48);
captchaCtx.stroke();
}
captchaCtx.font = 'bold 28px Arial';
captchaCtx.textBaseline = 'middle';
for (var j = 0; j < captchaCode.length; j += 1) {
var char = captchaCode.charAt(j);
var x = 18 + j * 30;
var y = 24 + (Math.random() * 6 - 3);
var angle = (Math.random() * 30 - 15) * Math.PI / 180;
captchaCtx.save();
captchaCtx.translate(x, y);
captchaCtx.rotate(angle);
captchaCtx.fillStyle = randomColor(40, 120);
captchaCtx.fillText(char, 0, 0);
captchaCtx.restore();
}
for (var k = 0; k < 20; k += 1) {
captchaCtx.fillStyle = randomColor(120, 220);
captchaCtx.fillRect(Math.random() * 140, Math.random() * 48, 2, 2);
}
}
function validateCaptcha(value) {
return String(value || '').trim().toUpperCase() === captchaCode;
}
goRegister.addEventListener('click', function () { goRegister.addEventListener('click', function () {
window.location.href = './register.html'; window.location.href = './register.html';
}); });
refreshCaptchaBtn.addEventListener('click', function () {
drawCaptcha();
});
captchaCanvas.addEventListener('click', function () {
drawCaptcha();
});
form.addEventListener('submit', function (event) { form.addEventListener('submit', function (event) {
event.preventDefault(); event.preventDefault();
var formData = new FormData(form); var formData = new FormData(form);
if (!validateCaptcha(formData.get('captcha'))) {
appUtils.showMessage(message, '图像验证码错误,请重新输入', false);
drawCaptcha();
return;
}
appUtils.ajax({ appUtils.ajax({
method: 'POST', method: 'POST',
url: '/api/auth/login', url: '/api/auth/login',
data: { data: {
username: String(formData.get('username') || '').trim(), loginId: String(formData.get('loginId') || '').trim(),
password: String(formData.get('password') || '').trim() password: String(formData.get('password') || '').trim()
}, },
success: function (response) { success: function (response) {
if (!response.success) { if (!response.success) {
appUtils.showMessage(message, response.message || '登录失败', false); appUtils.showMessage(message, response.message || '登录失败', false);
drawCaptcha();
return; return;
} }
// 记住账号:登录成功时再次确认保存
if (rememberCheckbox.checked) {
localStorage.setItem('rememberedAccount', String(formData.get('loginId') || '').trim());
}
appUtils.showMessage(message, '登录成功,正在进入系统...', true); appUtils.showMessage(message, '登录成功,正在进入系统...', true);
setTimeout(function () { setTimeout(function () {
window.location.href = './app.html'; window.location.href = './app.html';
}, 400); }, 400);
}, },
error: function (xhr, response) { error: function (xhr, response) {
appUtils.showMessage(message, (response && response.message) || '网络异常,请确认后端已启动', false); appUtils.showMessage(message, (response && response.message) || '网络异常,请检查后端服务', false);
drawCaptcha();
} }
}); });
}); });
drawCaptcha();
}); });

10
assets/js/register.js

@ -1,4 +1,4 @@
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
appUtils.redirectIfLoggedIn(); appUtils.redirectIfLoggedIn();
var form = document.getElementById('registerForm'); var form = document.getElementById('registerForm');
@ -18,16 +18,16 @@ document.addEventListener('DOMContentLoaded', function () {
url: '/api/auth/register', url: '/api/auth/register',
data: { data: {
idCard: String(formData.get('idCard') || '').trim(), idCard: String(formData.get('idCard') || '').trim(),
username: String(formData.get('username') || '').trim(), email: String(formData.get('email') || '').trim(),
password: String(formData.get('password') || '').trim(), password: String(formData.get('password') || '').trim(),
confirmPassword: String(formData.get('confirmPassword') || '').trim(), confirmPassword: String(formData.get('confirmPassword') || '').trim(),
name: String(formData.get('name') || '').trim(), name: String(formData.get('name') || '').trim(),
phone: String(formData.get('phone') || '').trim(), phone: String(formData.get('phone') || '').trim(),
gender: String(formData.get('gender') || ''), gender: String(formData.get('gender') || '').trim(),
college: String(formData.get('college') || '').trim(), college: String(formData.get('college') || '').trim(),
className: String(formData.get('className') || '').trim(), className: String(formData.get('className') || '').trim(),
studentNo: String(formData.get('studentNo') || '').trim(), studentNo: String(formData.get('studentNo') || '').trim(),
category: String(formData.get('category') || '') category: String(formData.get('category') || '').trim()
}, },
success: function (response) { success: function (response) {
if (!response.success) { if (!response.success) {
@ -40,7 +40,7 @@ document.addEventListener('DOMContentLoaded', function () {
}, 400); }, 400);
}, },
error: function (xhr, response) { error: function (xhr, response) {
appUtils.showMessage(message, (response && response.message) || '网络异常,请确认后端已启动', false); appUtils.showMessage(message, (response && response.message) || '网络异常,请检查后端服务', false);
} }
}); });
}); });

38
login.html

@ -1,25 +1,24 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>运动会报名系统 - 登录</title> <title>运动会报名系统 - 登录</title>
<link rel="stylesheet" href="./assets/css/style.css"> <link rel="stylesheet" href="./assets/css/style.css">
</head> </head>
<body class="auth-page"> <body class="auth-page">
<div class="auth-shell login-shell"> <div class="auth-shell login-shell">
<section class="auth-hero"> <section class="auth-hero">
<div class="auth-badge">Campus Sports</div> <div class="auth-badge">Campus Sports</div>
<h1>运动会报名系统</h1> <h1>运动会报名系统</h1>
<p class="auth-subtitle">统一完成赛事报名、个人信息维护与报名记录查看,界面简洁,流程顺畅</p> <p class="auth-subtitle">统一完成赛事报名、个人信息维护与报名记录查看,界面简洁,流程清晰</p>
<div class="notice-card"> <div class="notice-card">
<h2>报名须知</h2> <h2>报名须知</h2>
<ul> <ul>
<li>请使用真实身份信息注册,身份证号在系统中唯一。</li> <li>请使用真实身份信息注册,身份证号、邮箱和学号均需准确填写。</li>
<li>报名成功后可在“报名信息”中查看已选项目。</li> <li>普通用户仅能查看和修改自己的信息,并完成项目报名。</li>
<li>如项目名额已满,将无法继续报名该项目。</li> <li>管理员可以管理用户、项目和报名记录。</li>
<li>登录时需要输入图像验证码,请注意区分大小写。</li>
</ul> </ul>
</div> </div>
</section> </section>
@ -27,16 +26,28 @@
<section class="auth-panel"> <section class="auth-panel">
<div class="panel-head"> <div class="panel-head">
<h2>账号登录</h2> <h2>账号登录</h2>
<p>请输入账号和密码进入系统。</p> <p>请输入邮箱或学号、密码和图像验证码后进入系统。</p>
</div> </div>
<form id="loginForm" class="form-grid single-column" autocomplete="off"> <form id="loginForm" class="form-grid single-column" autocomplete="off">
<label class="field"> <label class="field">
<span>登录账</span> <span>邮箱 / 学</span>
<input type="text" name="username" placeholder="请输入登录账号" autocomplete="off"> <input type="text" name="loginId" placeholder="请输入邮箱或学号" autocomplete="off">
</label> </label>
<label class="field"> <label class="field">
<span>登录密码</span> <span>登录密码</span>
<input type="password" name="password" placeholder="请输入登录密码" autocomplete="off"> <input type="password" name="password" placeholder="请输入登录密码" autocomplete="new-password">
</label>
<label class="field">
<span>图像验证码</span>
<div class="captcha-row">
<input type="text" name="captcha" placeholder="请输入验证码" autocomplete="off">
<canvas id="captchaCanvas" width="140" height="48" aria-label="图像验证码"></canvas>
<button type="button" class="ghost-btn small" id="refreshCaptcha">刷新</button>
</div>
</label>
<label class="remember-inline" style="display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:600;font-size:14px;">
<input type="checkbox" id="rememberAccount">
<span>记住账号</span>
</label> </label>
<div class="form-actions stacked"> <div class="form-actions stacked">
<button type="submit" class="primary-btn">登录</button> <button type="submit" class="primary-btn">登录</button>
@ -46,8 +57,9 @@
</form> </form>
</section> </section>
</div> </div>
<script src="./assets/js/config.js"></script>
<script src="./assets/js/common.js"></script> <script src="./assets/js/common.js"></script>
<script src="./assets/js/login.js"></script> <script src="./assets/js/login.js"></script>
</body> </body>
</html>
</html>

11
register.html

@ -1,4 +1,4 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
@ -11,7 +11,7 @@
<section class="auth-panel wide-panel"> <section class="auth-panel wide-panel">
<div class="panel-head"> <div class="panel-head">
<h2>新用户注册</h2> <h2>新用户注册</h2>
<p>请完整填写个人信息,注册成功后将自动登录</p> <p>请完整填写个人信息,邮箱将用于登录,注册成功后可直接进入系统</p>
</div> </div>
<form id="registerForm" class="form-grid two-columns"> <form id="registerForm" class="form-grid two-columns">
<label class="field"> <label class="field">
@ -19,8 +19,8 @@
<input type="text" name="idCard" placeholder="请输入身份证号"> <input type="text" name="idCard" placeholder="请输入身份证号">
</label> </label>
<label class="field"> <label class="field">
<span>登录账号</span> <span>邮箱</span>
<input type="text" name="username" placeholder="请输入登录账号"> <input type="email" name="email" placeholder="请输入邮箱地址">
</label> </label>
<label class="field"> <label class="field">
<span>密码</span> <span>密码</span>
@ -50,7 +50,7 @@
<span>学院</span> <span>学院</span>
<select name="college" required> <select name="college" required>
<option value="">请选择学院</option> <option value="">请选择学院</option>
<option value="文学与文化传播学院">文学与文化传播学院</option> <option value="文学与文化传播学院">文学与文化传播学院</option>
<option value="马克思主义学院">马克思主义学院</option> <option value="马克思主义学院">马克思主义学院</option>
<option value="教育学院">教育学院</option> <option value="教育学院">教育学院</option>
<option value="外国语学院">外国语学院</option> <option value="外国语学院">外国语学院</option>
@ -99,6 +99,7 @@
<p>注册后可在线报名项目、维护个人资料,并查看所有已报名赛事。</p> <p>注册后可在线报名项目、维护个人资料,并查看所有已报名赛事。</p>
</section> </section>
</div> </div>
<script src="./assets/js/config.js"></script>
<script src="./assets/js/common.js"></script> <script src="./assets/js/common.js"></script>
<script src="./assets/js/register.js"></script> <script src="./assets/js/register.js"></script>
</body> </body>

Loading…
Cancel
Save