From d15c0a23c43946813ced0a169f7b3cc1feac55cd Mon Sep 17 00:00:00 2001 From: lyq0314 <2169694180@qq.com> Date: Wed, 10 Jun 2026 08:14:26 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=80=E6=96=B0=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 270 +++++-- app.html | 1 + assets/css/style.css | 783 +++++++++++++++++++- assets/js/app-main.js | 1591 ++++++++++++++++++++++++++++++++++------- assets/js/app.js | 760 -------------------- assets/js/common.js | 29 +- assets/js/config.js | 7 + assets/js/login.js | 139 +++- assets/js/register.js | 10 +- login.html | 38 +- register.html | 11 +- 11 files changed, 2468 insertions(+), 1171 deletions(-) delete mode 100644 assets/js/app.js create mode 100644 assets/js/config.js diff --git a/README.md b/README.md index 2c86140..e252409 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,107 @@ - # 运动会报名系统 ## 项目简介 -这是一个基于 Spring Boot 和前端技术栈开发的运动会报名系统,用于管理学校运动会的项目报名工作。 +这是一个基于 Spring Boot + 原生 Web 前端技术栈开发的运动会报名管理系统,支持学生在线报名运动会项目、管理员统一管理用户与报名数据。 + +--- ## 技术栈 ### 后端 -- Java 8+ -- Spring Boot 2.x -- MyBatis -- MySQL 数据库 +| 技术 | 版本 | 说明 | +|------|------|------| +| Java | 8+ | 开发语言 | +| Spring Boot | 2.x | 核心框架 | +| MyBatis | 3.x | ORM 框架 | +| MySQL | 5.7+ | 关系型数据库 | +| Maven | 3.6+ | 项目构建 | ### 前端 -- HTML5 / CSS3 / JavaScript -- Bootstrap 样式框架 +| 技术 | 说明 | +|------|------| +| HTML5 | 页面结构 | +| CSS3 | 样式设计(纯原生,无框架依赖) | +| JavaScript (ES5) | 交互逻辑(纯原生,无框架依赖) | + +--- ## 项目结构 ``` 运动会报名/ -├── backend/ # 后端代码 -│ ├── src/main/java/ # Java 源代码 -│ ├── src/main/resources/ # 配置文件 -│ └── pom.xml # Maven 配置 -├── frontend/ # 前端代码 -│ ├── assets/ # 静态资源 -│ │ ├── css/ # 样式文件 -│ │ └── js/ # JavaScript 文件 -│ ├── index.html # 首页 -│ ├── login.html # 登录页 -│ ├── register.html # 注册页 -│ └── app.html # 主应用页面 -└── README.md # 项目说明 +├── backend/ # 后端 Spring Boot 项目 +│ ├── src/main/java/ # Java 源代码 +│ │ └── com/sports/ # 主包 +│ │ ├── controller/ # 控制器层 +│ │ ├── service/ # 业务逻辑层 +│ │ ├── mapper/ # 数据访问层 +│ │ ├── entity/ # 实体类 +│ │ ├── vo/ # 视图对象 +│ │ ├── dto/ # 数据传输对象 +│ │ └── config/ # 配置类 +│ ├── src/main/resources/ # 配置文件 +│ │ ├── application.properties # 应用配置 +│ │ ├── 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 或更高版本 - Maven 3.6+ - MySQL 5.7+ +- 任意现代浏览器(推荐 Chrome / Edge) -### 数据库配置 - -创建数据库并导入初始数据: +### 1. 数据库配置 ```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; ``` -导入 `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 cd backend mvn spring-boot:run ``` -### 启动前端服务 +后端默认运行在 `http://localhost:8080`。 -使用任意 HTTP 服务器启动前端,例如: +### 4. 启动前端 +**方式一:Python HTTP 服务器** ```bash 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 -- 后端 API: http://localhost:8081 +> **注意**:前端 JS 会自动检测协议——如果通过 `file://` 打开,API 地址自动指向 `http://localhost:8080`;否则指向当前域名 + 端口 8080。 + +--- ## 默认账号 -| 账号 | 密码 | 角色 | -|------|------|------| +| 登录账号 | 密码 | 角色 | +|----------|------|------| | 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) -### 代码规范 -- Java 代码遵循 Spring 编码规范 -- JavaScript 代码使用 ES6+ 语法 -- 数据库表名使用下划线命名 +### Q: 注册时身份证号怎么填? +**A:** 15 位或 18 位的有效身份证号,如 `110101200001010000`。 -### 注意事项 -- 开发环境下请确保 MySQL 服务已启动 -- 修改配置文件后需要重启服务才能生效 +### Q: 如何添加新的赛事项目? +**A:** 使用管理员账号登录,在"赛事管理"模块中操作,或直接在数据库中插入数据。 \ No newline at end of file diff --git a/app.html b/app.html index a239687..c54263b 100644 --- a/app.html +++ b/app.html @@ -35,6 +35,7 @@ + diff --git a/assets/css/style.css b/assets/css/style.css index 4fcf920..c44a56f 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -1,4 +1,4 @@ -:root { +:root { --surface: rgba(255, 255, 255, 0.92); --text: #1f2937; --muted: #6b7280; @@ -38,6 +38,7 @@ select { display: none !important; } +/* ========== 认证页面 ========== */ .auth-page { display: flex; align-items: center; @@ -135,6 +136,7 @@ select { margin-bottom: 24px; } +/* ========== 表单 ========== */ .form-grid { display: grid; gap: 18px; @@ -168,7 +170,6 @@ select { outline: none; } -/* 下拉列表通用样式 */ select { appearance: none; -webkit-appearance: none; @@ -179,24 +180,20 @@ select { background-size: 16px; } -/* 学院下拉列表选项样式 */ select[name="college"] option { padding: 6px 16px; line-height: 1.3; } -/* 学院下拉列表占位符样式 */ select[name="college"] option[value=""] { color: var(--muted); display: none; } -/* 未选择时显示占位符颜色 */ select[name="college"]:invalid { color: var(--muted); } -/* 选择后显示正常颜色 */ select[name="college"]:valid { color: var(--text); } @@ -263,6 +260,7 @@ select[name="college"]:valid { color: var(--danger); } +/* ========== 应用布局 ========== */ .app-body { padding: 24px; } @@ -411,49 +409,208 @@ select[name="college"]:valid { margin-bottom: 18px; } -.event-table { +/* ========== 通用数据表格 ========== */ +.table-wrapper { + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +.data-table { width: 100%; border-collapse: collapse; - border-radius: 18px; + border-radius: 14px; 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, -.event-table td { - padding: 15px 14px; - text-align: left; - border-bottom: 1px solid rgba(226, 232, 240, 0.9); - font-size: 14px; +.data-table thead { + background: linear-gradient(135deg, #0f766e, #14b8a6); + color: #fff; } -.event-table th:last-child, -.event-table td:last-child { +.data-table thead th { + padding: 14px 12px; 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 { - background: rgba(15, 118, 110, 0.08); - color: #115e59; +.data-table thead th:last-child { + border-right: none; +} + +.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; } +.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 { - padding: 10px 16px; - border-radius: 12px; - background: linear-gradient(135deg, var(--primary), var(--primary-light)); - color: #fff; + padding: 8px 14px; + border-radius: 8px; + border: none; + cursor: pointer; 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] { background: rgba(148, 163, 184, 0.6); 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 { padding: 32px 20px; text-align: center; @@ -462,7 +619,73 @@ select[name="college"]:valid { 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 { display: flex; justify-content: center; @@ -509,28 +732,513 @@ select[name="college"]:valid { 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, .register-shell, .main-layout, .profile-grid, .summary-grid, + .dashboard-top, + .dashboard-bottom, + .dashboard-metrics, .two-columns { grid-template-columns: 1fr; } .topbar, + .dashboard-hero-card, .content-header, .section-head { flex-direction: column; align-items: flex-start; 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, .app-body { padding: 14px; @@ -541,8 +1249,21 @@ select[name="college"]:valid { align-items: stretch; } - .event-table { - display: block; - overflow-x: auto; + .data-table { + min-width: 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; } } diff --git a/assets/js/app-main.js b/assets/js/app-main.js index ce2b005..40f5fac 100644 --- a/assets/js/app-main.js +++ b/assets/js/app-main.js @@ -14,9 +14,27 @@ document.addEventListener('DOMContentLoaded', function () { events: [], myEvents: [], adminUsers: [], + allAdminUsers: [], adminRegistrations: [], adminEvents: [], - editingEventId: null + dashboardStats: { + onlineUsers: 0 + }, + editingEventId: null, + editingUserId: null, + userPage: 1, + userPageSize: 8, + userKeyword: '', + userTotal: 0, + eventPage: 1, + eventPageSize: 8, + eventTotal: 0, + adminEventPage: 1, + adminEventPageSize: 8, + adminEventTotal: 0, + athletePage: 1, + athletePageSize: 8, + athleteTotal: 0 }; var genderOptions = ['男', '女']; @@ -43,18 +61,31 @@ document.addEventListener('DOMContentLoaded', function () { var categoryOptions = ['青年组', '老年组']; var studentMenus = [ - { key: 'profile', label: '个人信息', desc: '查看并修改个人信息。' }, + { key: 'profile-view', label: '查看信息', desc: '查看当前账号信息。' }, + { key: 'profile-edit', label: '修改信息', desc: '修改个人资料。' }, { key: 'events', label: '运动会报名', desc: '查看项目、报名和取消报名。' } ]; var adminMenus = [ { key: 'admin-home', label: '运动会管理', desc: '查看系统概览。' }, + { + label: '用户信息管理', + children: [ + { key: 'user-list', label: '用户列表', desc: '查询、导出、重置密码及禁用等操作。' }, + { key: 'user-add', label: '新增用户', desc: '新增系统用户或批量上传。' } + ] + }, { key: 'team-info', label: '团体信息管理', desc: '管理学院、班级等团体信息。' }, - { key: 'user-manage', label: '用户信息管理', desc: '管理用户资料、权限和状态。' }, - { key: 'event-manage', label: '项目管理', desc: '新增、编辑和删除比赛项目。' }, - { key: 'athlete-manage', label: '参赛运动员管理', desc: '查看所有已报名人员和项目。' }, + { + label: '项目管理', + children: [ + { key: 'event-list', label: '项目列表', desc: '查看、编辑和删除比赛项目。' }, + { key: 'event-add', label: '新增项目', desc: '新增比赛项目。' } + ] + }, + { key: 'athlete-manage', label: '参赛运动员管理', desc: '查看所有已报名人员。' }, { key: 'score-manage', label: '参赛成绩管理', desc: '录入和维护成绩。' }, - { key: 'record-manage', label: '项目记录管理', desc: '维护项目记录和赛事资料。' }, + { key: 'record-manage', label: '项目记录管理', desc: '维护项目记录。' }, { key: 'system-manage', label: '系统管理', desc: '维护系统配置。' } ]; @@ -72,20 +103,83 @@ document.addEventListener('DOMContentLoaded', function () { } function getMeta(viewKey) { - var menu = getMenus().find(function (item) { - return item.key === viewKey; - }); - return menu || { label: '系统首页', desc: '欢迎进入运动会报名系统。' }; + var menus = getMenus(); + for (var i = 0; i < menus.length; i++) { + var item = menus[i]; + if (item.key === viewKey) return item; + if (item.children) { + for (var j = 0; j < item.children.length; j++) { + if (item.children[j].key === viewKey) return item.children[j]; + } + } + } + return { label: '系统首页', desc: '欢迎进入运动会报名系统。' }; + } + + function isParentActive(item) { + if (item.key === state.currentView) return true; + if (item.children) { + for (var i = 0; i < item.children.length; i++) { + if (item.children[i].key === state.currentView) return true; + } + } + return false; } function renderSidebar() { - sidebarNav.innerHTML = getMenus().map(function (item) { - return ''; - }).join(''); + var expandedParent = null; + var menus = getMenus(); + for (var i = 0; i < menus.length; i++) { + if (menus[i].children && isParentActive(menus[i])) { + expandedParent = menus[i].label; + break; + } + } - Array.prototype.slice.call(sidebarNav.querySelectorAll('.nav-item')).forEach(function (button) { - button.addEventListener('click', function () { - switchView(button.getAttribute('data-view')); + var html = ''; + for (var p = 0; p < menus.length; p++) { + var item = menus[p]; + if (item.children) { + var isExpanded = (item.label === expandedParent); + html += ''; + } else { + html += ''; + } + } + sidebarNav.innerHTML = html; + + Array.prototype.slice.call(sidebarNav.querySelectorAll('.nav-item[data-view]')).forEach(function (btn) { + btn.addEventListener('click', function () { + switchView(btn.getAttribute('data-view')); + }); + }); + + Array.prototype.slice.call(sidebarNav.querySelectorAll('.nav-parent')).forEach(function (btn) { + btn.addEventListener('click', function () { + var parentLabel = btn.getAttribute('data-parent'); + var childGroup = sidebarNav.querySelector('[data-parent-group="' + CSS.escape(parentLabel) + '"]'); + if (childGroup) { + var isOpen = childGroup.classList.contains('open'); + Array.prototype.slice.call(sidebarNav.querySelectorAll('.nav-children.open')).forEach(function (g) { + g.classList.remove('open'); + }); + Array.prototype.slice.call(sidebarNav.querySelectorAll('.nav-parent.expanded')).forEach(function (p) { + p.classList.remove('expanded'); + }); + if (!isOpen) { + childGroup.classList.add('open'); + btn.classList.add('expanded'); + } + } }); }); } @@ -111,7 +205,8 @@ document.addEventListener('DOMContentLoaded', function () { }); } - function renderProfile() { + // ===================== 查看信息(独立页面) ===================== + function renderProfileView() { mainView.innerHTML = '' + '
' + '

基础资料

当前账号信息如下。

' @@ -127,7 +222,12 @@ document.addEventListener('DOMContentLoaded', function () { + infoCard('类别', state.user.category) + infoCard('角色', state.user.role) + '
' - + '' + + ''; + } + + // ===================== 修改信息(独立页面) ===================== + function renderProfileEdit() { + mainView.innerHTML = '' + '
' + '

修改个人信息

可在此更新姓名、电话、学院、班级、学号等信息。

' + '
' @@ -140,6 +240,7 @@ document.addEventListener('DOMContentLoaded', function () { + fieldSelect('类别', 'category', state.user.category, categoryOptions, '请选择类别', true) + '
' + ' ' + + ' ' + '
' + '

' + '
' @@ -147,6 +248,8 @@ document.addEventListener('DOMContentLoaded', function () { var form = document.getElementById('profileForm'); var messageEl = document.getElementById('profileMessage'); + var cancelBtn = document.getElementById('cancelProfileEdit'); + form.addEventListener('submit', function (event) { event.preventDefault(); var formData = new FormData(form); @@ -169,30 +272,45 @@ document.addEventListener('DOMContentLoaded', function () { } state.user = response.data; updateCurrentUserName(); - renderProfile(); - appUtils.showMessage(document.getElementById('profileMessage'), '个人信息修改成功', true); + appUtils.showMessage(messageEl, '个人信息修改成功', true); + setTimeout(function () { + switchView('profile-view'); + }, 800); }, error: function (xhr, response) { appUtils.showMessage(messageEl, (response && response.message) || '保存失败', false); } }); }); + + if (cancelBtn) { + cancelBtn.addEventListener('click', function () { + switchView('profile-view'); + }); + } } + // ===================== 运动会报名 ===================== function renderEventTable(list, isMine) { + state.eventTotal = list.length; + var totalPages = Math.ceil(state.eventTotal / state.eventPageSize) || 1; + if (state.eventPage > totalPages) state.eventPage = totalPages; + var start = (state.eventPage - 1) * state.eventPageSize; + var pageItems = list.slice(start, start + state.eventPageSize); + if (!list.length) { mainView.innerHTML = '

暂无数据

' + (isMine ? '你还没有报名任何项目。' : '当前暂无可展示项目。') + '

'; return; } mainView.innerHTML = '' - + '' + + '
' + ' ' + ' ' - + list.map(function (item) { + + pageItems.map(function (item) { var buttonHtml = isMine - ? '' - : ''; + ? '' + : ''; return '' + '' + '' @@ -200,13 +318,36 @@ document.addEventListener('DOMContentLoaded', function () { + '' + '' + '' - + '' + + '' + ''; }).join('') + ' ' - + '
项目名称项目分类比赛时间比赛地点报名情况项目说明操作
' + escapeHtml(item.eventName) + '' + escapeHtml(item.eventCategory) + '' + escapeHtml(item.location) + '' + escapeHtml((item.registeredCount || 0) + '/' + (item.quota || 0)) + '' + escapeHtml(item.description) + '' + buttonHtml + '' + buttonHtml + '
'; + + '' + + renderEventPagination(totalPages); - Array.prototype.slice.call(document.querySelectorAll('.register-btn')).forEach(function (button) { + bindEventTableActions(); + } + + function renderEventPagination(totalPages) { + var html = ''; + html += '

共 ' + state.eventTotal + ' 条记录,第 ' + state.eventPage + ' / ' + totalPages + ' 页

'; + return html; + } + + function bindEventTableActions() { + Array.prototype.slice.call(document.querySelectorAll('.register-btn:not([disabled])')).forEach(function (button) { button.addEventListener('click', function () { var eventId = button.getAttribute('data-id'); appUtils.ajax({ @@ -245,204 +386,1171 @@ document.addEventListener('DOMContentLoaded', function () { }); }); }); + + bindEventPaginationActions(); + } + + function bindEventPaginationActions() { + Array.prototype.slice.call(document.querySelectorAll('.page-btn[data-type="event"]:not([disabled])')).forEach(function (button) { + button.addEventListener('click', function () { + var page = Number(button.getAttribute('data-page')); + if (page >= 1) { + state.eventPage = page; + renderCurrentView(); + } + }); + }); } + // ===================== 管理员首页 ===================== function renderAdminHome() { + var totalUsers = state.allAdminUsers.length; + var totalRegistrations = state.adminRegistrations.length; + var totalEvents = state.adminEvents.length; + var activeUsers = state.allAdminUsers.filter(function (item) { + return item.status !== 'DISABLED'; + }).length; + var disabledUsers = totalUsers - activeUsers; + var avgFillRate = totalEvents + ? Math.round(state.adminEvents.reduce(function (sum, item) { + var quota = Number(item.quota || 0); + var registered = Number(item.registeredCount || 0); + return sum + (quota > 0 ? Math.min(registered / quota, 1) : 0); + }, 0) / totalEvents * 100) + : 0; + var onlineUsers = Number((state.dashboardStats && state.dashboardStats.onlineUsers) || 0); + var collegeStats = buildCountStats(state.adminRegistrations, function (item) { + return item.college; + }); + var categoryStats = buildCountStats(state.adminEvents, function (item) { + return item.eventCategory; + }); + var compactCategoryStats = compressStats(categoryStats, 4, '其他分类'); + var statusStats = [ + { label: '正常用户', count: activeUsers, color: '#0f766e' }, + { label: '禁用用户', count: Math.max(disabledUsers, 0), color: '#f97316' } + ].filter(function (item) { + return item.count > 0; + }); + var latestRegistrations = state.adminRegistrations + .slice() + .sort(function (a, b) { + return parseDateValue(b.createdAt) - parseDateValue(a.createdAt); + }) + .slice(0, 4); + var upcomingEvents = state.adminEvents + .slice() + .sort(function (a, b) { + return parseDateValue(a.eventTime) - parseDateValue(b.eventTime); + }) + .filter(function (item) { + return parseDateValue(item.eventTime) > 0; + }) + .slice(0, 4); + var hotEvents = state.adminEvents + .slice() + .sort(function (a, b) { + return Number(b.registeredCount || 0) - Number(a.registeredCount || 0); + }) + .slice(0, 4); + mainView.innerHTML = '' - + '
' - + '

系统概览

管理员可在此查看系统整体情况。

' - + '
' - + summaryCard('用户总数', state.adminUsers.length) - + summaryCard('报名记录', state.adminRegistrations.length) - + summaryCard('比赛项目', state.adminEvents.length) - + '
' + + '
' + + '
' + + '
' + + '
' + + '

Sports Meet Admin

' + + '

运动会管理仪表盘

' + + '

查看系统运行概况、项目热度和最新报名动态。

' + + '
' + + ' ' + + ' ' + + ' ' + + '
' + + '
' + + renderDonutPanel('用户状态分布', totalUsers, statusStats, '账号状态') + + '
' + + '
' + + renderCategoryOverviewPanel('项目分类概览', compactCategoryStats) + + '
' + + '
' + + '
' + + dashboardMetricCard('实时在线', onlineUsers, onlineUsers > 0 ? '当前会话在线用户数' : '当前暂无在线用户') + + dashboardMetricCard('用户总数', totalUsers, activeUsers + ' 正常 / ' + disabledUsers + ' 禁用') + + dashboardMetricCard('报名记录', totalRegistrations, totalEvents ? '覆盖 ' + totalEvents + ' 个比赛项目' : '暂无比赛项目') + + dashboardMetricCard('项目总数', totalEvents, categoryStats.length + ' 个项目分类') + + dashboardMetricCard('平均满额率', avgFillRate + '%', '便于掌握项目热度与容量') + + '
' + + '
' + + '
' + + '

热门项目排行

按报名人数排序,突出最受关注项目。

' + + renderCompactEventRanking(hotEvents, '当前没有项目数据') + + '
' + + '
' + + '

最新动态

把报名时间和即将开始的项目压缩展示。

' + + renderCompactFeed(latestRegistrations, upcomingEvents) + + '
' + + '
' + + '
'; + + bindDashboardActions(); + } + + function dashboardMetricCard(label, value, hint) { + return '' + + '
' + + ' ' + escapeHtml(label) + '' + + ' ' + escapeHtml(value) + '' + + '

' + escapeHtml(hint) + '

' + + '
'; + } + + function dashboardStatRow(label, value) { + return '' + + '
' + + ' ' + escapeHtml(label) + '' + + ' ' + escapeHtml(value) + '' + '
'; } - function renderUserManage() { - if (!state.adminUsers.length) { - mainView.innerHTML = '

暂无用户

当前系统中还没有用户数据。

'; - return; + function renderDonutPanel(title, total, items, centerLabel) { + return '' + + '
' + + '

' + escapeHtml(title) + '

当前数据分布概览。

' + + renderDonutChart(items, total, centerLabel) + + '
'; + } + + function renderCategoryOverviewPanel(title, items) { + return '' + + '
' + + '

' + escapeHtml(title) + '

按项目分类查看当前数量分布。

' + + renderCategoryOverview(items) + + '
'; + } + + function buildCountStats(list, getter) { + var map = {}; + list.forEach(function (item) { + var key = String(getter(item) || '').trim(); + if (!key) { + key = '未分类'; + } + map[key] = (map[key] || 0) + 1; + }); + return Object.keys(map).map(function (key) { + return { label: key, count: map[key] }; + }).sort(function (a, b) { + return b.count - a.count; + }); + } + + function compressStats(items, maxItems, otherLabel) { + if (!items.length || items.length <= maxItems) { + return items; } - mainView.innerHTML = '' - + '
' - + '

数据概览

查看当前系统内用户整体情况。

' - + '
' - + summaryCard('用户总数', state.adminUsers.length) - + summaryCard('管理员数量', state.adminUsers.filter(function (item) { return item.role === 'ADMIN'; }).length) - + summaryCard('禁用账号数量', state.adminUsers.filter(function (item) { return item.status === 'DISABLED'; }).length) - + '
' - + '
' - + '
' - + '

用户列表

支持重置密码、禁用、解除禁用和删除账号。

' - + ' ' - + ' ' - + ' ' - + state.adminUsers.map(function (item) { - var isAdmin = item.role === 'ADMIN'; - var statusText = item.status === 'DISABLED' ? '已禁用' : '正常'; - var enableOrDisable = isAdmin ? '' : (item.status === 'DISABLED' - ? '' - : ''); - var deleteAction = isAdmin ? '' : ''; - return '' - + '' - + '' - + '' - + '' - + '' - + '' - + '' - + '' - + '' - + '' - + '' - + '' - + ''; + var kept = items.slice(0, maxItems - 1); + var rest = items.slice(maxItems - 1); + var restCount = rest.reduce(function (sum, item) { + return sum + Number(item.count || 0); + }, 0); + + kept.push({ + label: otherLabel || '其他', + count: restCount, + color: '#94a3b8' + }); + + return kept; + } + + function parseDateValue(value) { + var text = String(value || '').trim(); + if (!text) { + return 0; + } + var normalized = text.replace(/-/g, '/'); + var time = new Date(normalized).getTime(); + return isNaN(time) ? 0 : time; + } + + function renderMiniBars(items, emptyText) { + if (!items.length) { + return '
' + escapeHtml(emptyText) + '
'; + } + var max = items[0].count || 1; + return '
' + + items.map(function (item) { + var width = Math.max(14, Math.round((item.count / max) * 100)); + return '' + + '
' + + '
' + escapeHtml(item.label) + '' + escapeHtml(item.count) + '
' + + '
' + + '
'; }).join('') - + '
' - + '
姓名账号身份证号电话性别学院班级学号类别角色状态操作
' + escapeHtml(item.name) + '' + escapeHtml(item.username) + '' + escapeHtml(item.idCard) + '' + escapeHtml(item.phone) + '' + escapeHtml(item.gender) + '' + escapeHtml(item.college) + '' + escapeHtml(item.className) + '' + escapeHtml(item.studentNo) + '' + escapeHtml(item.category) + '' + escapeHtml(item.role) + '' + escapeHtml(statusText) + ' ' + enableOrDisable + ' ' + deleteAction + '
' + '
'; + } - bindAdminUserActions(); + function renderRegistrationTimeline(list) { + if (!list.length) { + return '
当前还没有报名记录
'; + } + return '
' + + list.map(function (item) { + return '' + + '
' + + '
' + + '
' + + ' ' + escapeHtml(item.studentName) + ' 报名了 ' + escapeHtml(item.eventName) + '' + + '

' + escapeHtml((item.college || '未填写学院') + ' · ' + (item.eventCategory || '未分类')) + '

' + + ' ' + escapeHtml(item.createdAt || '时间未知') + '' + + '
' + + '
'; + }).join('') + + '
'; } - function renderAthleteManage() { - if (!state.adminRegistrations.length) { - mainView.innerHTML = '

暂无报名记录

当前还没有参赛人员数据。

'; - return; + function renderDonutChart(items, total, centerLabel) { + if (!items.length || total <= 0) { + return '
当前没有可统计的数据
'; } + var palette = ['#0f766e', '#2563eb', '#f97316', '#e11d48', '#14b8a6', '#8b5cf6']; + var current = 0; + var gradientParts = []; + var legendItems = []; - var coveredEvents = {}; - state.adminRegistrations.forEach(function (item) { - coveredEvents[item.eventName] = true; + items.forEach(function (item, index) { + var count = Number(item.count || 0); + if (count <= 0) { + return; + } + var color = item.color || palette[index % palette.length]; + var percent = total > 0 ? (count / total) * 100 : 0; + var start = current; + var end = current + percent; + gradientParts.push(color + ' ' + start + '% ' + end + '%'); + current = end; + legendItems.push({ + label: item.label, + count: count, + color: color, + percent: Math.round(percent) + }); }); - mainView.innerHTML = '' - + '
' - + '

数据概览

查看当前参赛人员和报名记录。

' - + '
' - + summaryCard('报名记录总数', state.adminRegistrations.length) - + summaryCard('已覆盖项目', Object.keys(coveredEvents).length) - + summaryCard('系统用户数', state.adminUsers.length) + return '' + + '
' + + '
' + + '
' + + ' ' + escapeHtml(total) + '' + + ' ' + escapeHtml(centerLabel || '总计') + '' + + '
' + '
' - + '
' - + '
' - + '

参赛名单

显示所有已报名人员和项目详情。

' - + ' ' - + ' ' - + ' ' - + state.adminRegistrations.map(function (item) { - return '' - + '' - + '' - + '' - + '' - + '' - + '' - + '' - + '' - + '' - + '' - + ''; + + '
' + + legendItems.map(function (item) { + return '' + + '
' + + ' ' + + '
' + escapeHtml(item.label) + '

' + escapeHtml(item.count + ' · ' + item.percent + '%') + '

' + + '
'; }).join('') - + '
' - + '
姓名账号电话学院类别项目名称项目分类时间地点状态报名时间
' + escapeHtml(item.studentName) + '' + escapeHtml(item.username) + '' + escapeHtml(item.phone) + '' + escapeHtml(item.college) + '' + escapeHtml(item.category) + '' + escapeHtml(item.eventName) + '' + escapeHtml(item.eventCategory) + '' + escapeHtml((item.eventTime || '') + ' / ' + (item.location || '')) + '' + escapeHtml(item.status) + '' + escapeHtml(item.createdAt) + '
' + + '
' + '
'; } - function renderEventManage() { - var editingItem = state.adminEvents.find(function (item) { - return item.id === state.editingEventId; - }) || { - eventName: '', - eventCategory: '', - location: '', - quota: '', - eventTime: '', - description: '' - }; + function renderCategoryOverview(items) { + if (!items.length) { + return '
当前没有可统计的数据
'; + } + var max = Math.max.apply(null, items.map(function (item) { + return Number(item.count || 0); + })); + return '
' + + items.map(function (item, index) { + var count = Number(item.count || 0); + var width = max > 0 ? Math.max(12, Math.round((count / max) * 100)) : 0; + return '' + + '
' + + '
' + + ' ' + escapeHtml(item.label) + '' + + ' ' + escapeHtml(count + ' 个项目') + '' + + '
' + + '
' + + '
'; + }).join('') + + '
'; + } + + function renderEventRanking(list, emptyText) { + if (!list.length) { + return '
' + escapeHtml(emptyText) + '
'; + } + var max = Number(list[0].registeredCount || 0) || 1; + return '
' + + list.map(function (item, index) { + var registered = Number(item.registeredCount || 0); + var quota = Number(item.quota || 0); + var width = Math.max(10, Math.round((registered / max) * 100)); + return '' + + '
' + + '
TOP ' + (index + 1) + '' + escapeHtml(item.eventName) + '
' + + '

' + escapeHtml((item.eventCategory || '未分类') + ' · ' + (item.location || '地点待定')) + '

' + + '
' + + ' ' + escapeHtml(registered + ' / ' + quota + ' 人') + '' + + '
'; + }).join('') + + '
'; + } + + function renderCompactEventRanking(list, emptyText) { + if (!list.length) { + return '
' + escapeHtml(emptyText) + '
'; + } + return '
' + + list.map(function (item, index) { + var registered = Number(item.registeredCount || 0); + var quota = Number(item.quota || 0); + return '' + + '
' + + ' ' + (index + 1) + '' + + '
' + + ' ' + escapeHtml(item.eventName) + '' + + '

' + escapeHtml((item.eventCategory || '未分类') + ' · ' + registered + '/' + quota + ' 人') + '

' + + '
' + + ' ' + escapeHtml(item.location || '地点待定') + '' + + '
'; + }).join('') + + '
'; + } + + function renderUpcomingEvents(list) { + if (!list.length) { + return '
当前没有可展示的比赛时间数据
'; + } + return '
' + + list.map(function (item) { + return '' + + '
' + + ' ' + escapeHtml(item.eventName) + '' + + '

' + escapeHtml(item.eventCategory || '未分类') + '

' + + ' ' + escapeHtml((item.eventTime || '时间待定') + ' · ' + (item.location || '地点待定')) + '' + + '
'; + }).join('') + + '
'; + } + + function renderCompactFeed(registrations, events) { + var feedItems = []; + + registrations.forEach(function (item) { + feedItems.push({ + time: item.createdAt || '', + title: (item.studentName || '有用户') + ' 报名了 ' + (item.eventName || '项目'), + desc: (item.college || '未填写学院') + ' · ' + (item.eventCategory || '未分类'), + type: '报名' + }); + }); + + events.forEach(function (item) { + feedItems.push({ + time: item.eventTime || '', + title: (item.eventName || '项目') + ' 即将开始', + desc: (item.location || '地点待定') + ' · ' + (item.eventCategory || '未分类'), + type: '赛程' + }); + }); + + feedItems = feedItems + .sort(function (a, b) { + return parseDateValue(b.time) - parseDateValue(a.time); + }) + .slice(0, 6); + + if (!feedItems.length) { + return '
当前没有最新动态
'; + } + + return '
' + + feedItems.map(function (item) { + return '' + + '
' + + ' ' + escapeHtml(item.type) + '' + + '
' + + ' ' + escapeHtml(item.title) + '' + + '

' + escapeHtml(item.desc) + '

' + + '
' + + ' ' + escapeHtml(item.time || '时间待定') + '' + + '
'; + }).join('') + + '
'; + } + + function bindDashboardActions() { + Array.prototype.slice.call(document.querySelectorAll('.dashboard-link')).forEach(function (button) { + button.addEventListener('click', function () { + switchView(button.getAttribute('data-view')); + }); + }); + } + + // ===================== 用户列表 ===================== + function getPagedUsers() { + var filtered = state.allAdminUsers; + if (state.userKeyword) { + var kw = state.userKeyword.toLowerCase(); + filtered = state.allAdminUsers.filter(function (u) { + return (u.name && u.name.toLowerCase().indexOf(kw) >= 0) + || (u.username && u.username.toLowerCase().indexOf(kw) >= 0) + || (u.studentNo && u.studentNo.toLowerCase().indexOf(kw) >= 0) + || (u.college && u.college.toLowerCase().indexOf(kw) >= 0) + || (u.idCard && u.idCard.toLowerCase().indexOf(kw) >= 0); + }); + } + state.userTotal = filtered.length; + var start = (state.userPage - 1) * state.userPageSize; + return filtered.slice(start, start + state.userPageSize); + } + + function renderPagination() { + var totalPages = Math.ceil(state.userTotal / state.userPageSize) || 1; + var html = ''; + html += '

共 ' + state.userTotal + ' 条记录,第 ' + state.userPage + ' / ' + totalPages + ' 页

'; + return html; + } + + function renderUserList() { + var pageUsers = getPagedUsers(); mainView.innerHTML = '' + '
' - + '

' + (state.editingEventId ? '编辑项目' : '新增项目') + '

在这里维护比赛项目基础信息。

' - + '
' - + fieldInput('项目名称', 'eventName', editingItem.eventName, '请输入项目名称') - + fieldInput('项目分类', 'eventCategory', editingItem.eventCategory, '请输入项目分类') - + fieldInput('比赛地点', 'location', editingItem.location, '请输入比赛地点') - + ' ' - + fieldInput('比赛时间', 'eventTime', editingItem.eventTime, '例如 2026-05-20 08:30', true) - + fieldInput('项目说明', 'description', editingItem.description, '请输入项目说明', true) - + '
' - + ' ' - + ' ' - + '
' - + '

' - + '
' + + '

用户列表

支持搜索、分页,以及重置密码、禁用/解除禁用和删除操作。

' + + '
' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + '
' + '
' - + '
' - + '

项目列表

查看、编辑和删除当前项目。

' - + (state.adminEvents.length ? '' - + '' - + ' ' + + '
' + + (pageUsers.length ? '' + + '
项目名称项目分类比赛时间比赛地点人数上限已报名项目说明操作
' + + ' ' + ' ' - + state.adminEvents.map(function (item) { + + pageUsers.map(function (item) { + var isAdmin = item.role === 'ADMIN'; + var statusText = item.status === 'DISABLED' ? '已禁用' : '正常'; + var statusClass = item.status === 'DISABLED' ? 'status-disabled' : 'status-active'; + + var actionBtns = ''; + actionBtns += ''; + actionBtns += ''; + if (!isAdmin) { + if (item.status === 'DISABLED') { + actionBtns += ''; + } else { + actionBtns += ''; + } + actionBtns += ''; + } + return '' - + '' - + '' - + '' - + '' - + '' - + '' - + '' - + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + ''; }).join('') + ' ' + '
姓名账号身份证号电话性别学院班级学号类别角色状态操作
' + escapeHtml(item.eventName) + '' + escapeHtml(item.eventCategory) + '' + escapeHtml(item.eventTime) + '' + escapeHtml(item.location) + '' + escapeHtml(item.quota) + '' + escapeHtml(item.registeredCount || 0) + '' + escapeHtml(item.description) + ' ' + escapeHtml(item.name) + '' + escapeHtml(item.username) + '' + escapeHtml(item.idCard) + '' + escapeHtml(item.phone) + '' + escapeHtml(item.gender) + '' + escapeHtml(item.college) + '' + escapeHtml(item.className) + '' + escapeHtml(item.studentNo) + '' + escapeHtml(item.category) + '' + escapeHtml(item.role) + '' + escapeHtml(statusText) + '' + actionBtns + '
' - : '

暂无项目

请先新增比赛项目。

') + + renderPagination() + : '

暂无用户

当前系统中还没有用户数据,或搜索结果为空。

') + '
'; - bindEventManageActions(); + bindUserListActions(); + bindPaginationActions(); } - function renderPlaceholder(title, desc) { - mainView.innerHTML = '' - + '
' - + '

' + escapeHtml(title) + '

' + escapeHtml(desc) + '

' - + '

功能持续完善中

这个模块的详细功能可以继续在现有框架上补充。

' - + '
'; - } + function bindUserListActions() { + var searchInput = document.getElementById('userSearchInput'); + var searchBtn = document.getElementById('userSearchBtn'); + var clearSearchBtn = document.getElementById('clearSearchBtn'); + var exportBtn = document.getElementById('exportUsersBtn'); - function renderCurrentView() { - var meta = getMeta(state.currentView); - sectionTitle.textContent = meta.label; - sectionDesc.textContent = meta.desc; - renderTabs(); + if (searchBtn) { + searchBtn.addEventListener('click', function () { + state.userKeyword = String(searchInput ? searchInput.value : '').trim(); + state.userPage = 1; + renderCurrentView(); + }); + } - if (state.currentView === 'profile') { - renderProfile(); - return; + if (clearSearchBtn) { + clearSearchBtn.addEventListener('click', function () { + state.userKeyword = ''; + state.userPage = 1; + if (searchInput) { searchInput.value = ''; } + renderCurrentView(); + }); } - if (state.currentView === 'events') { - renderEventTable(state.currentTab === 'mine' ? state.myEvents : state.events, state.currentTab === 'mine'); - return; + + if (searchInput) { + searchInput.addEventListener('keydown', function (e) { + if (e.key === 'Enter') { + state.userKeyword = String(searchInput.value).trim(); + state.userPage = 1; + renderCurrentView(); + } + }); + } + + // 导出用户 — 前端生成 CSV 下载 + if (exportBtn) { + exportBtn.addEventListener('click', function () { + var exportData = state.userKeyword ? getPagedUsers() : state.allAdminUsers; + if (!exportData.length) { + window.alert('没有可导出的用户数据'); + return; + } + var headers = ['姓名', '账号', '身份证号', '电话', '性别', '学院', '班级', '学号', '类别', '角色', '状态']; + var csvContent = '\uFEFF' + headers.join(',') + '\n'; + exportData.forEach(function (u) { + var row = [ + u.name || '', + u.username || '', + u.idCard || '', + u.phone || '', + u.gender || '', + u.college || '', + u.className || '', + u.studentNo || '', + u.category || '', + u.role || '', + u.status || '' + ]; + csvContent += row.map(function (v) { return '"' + String(v).replace(/"/g, '""') + '"'; }).join(',') + '\n'; + }); + var blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + var url = window.URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = '用户列表.csv'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + }); + } + + bindAdminUserActions(); + } + + // ===================== 新增用户(独立页面) ===================== + function renderUserAdd() { + var editingUser = null; + if (state.editingUserId) { + editingUser = state.allAdminUsers.find(function (u) { return u.id === state.editingUserId; }) || null; + } + var isEditing = !!editingUser; + + mainView.innerHTML = '' + + '
' + + '

' + (isEditing ? '编辑用户信息' : '新增用户') + '

' + (isEditing ? '修改现有用户的基本信息。' : '管理员可在此新增系统用户。') + '

' + + '
' + + fieldInput('身份证号', 'idCard', isEditing ? editingUser.idCard : '', '请输入身份证号') + + fieldInput('邮箱', 'email', isEditing ? editingUser.username : '', '请输入邮箱地址') + + (!isEditing ? fieldInput('密码', 'password', '', '请输入密码(不少于6位)') + fieldInput('确认密码', 'confirmPassword', '', '请再次输入密码') : '') + + fieldInput('姓名', 'name', isEditing ? editingUser.name : '', '请输入姓名') + + fieldInput('联系电话', 'phone', isEditing ? editingUser.phone : '', '请输入电话') + + fieldSelect('性别', 'gender', isEditing ? editingUser.gender : '', genderOptions, '请选择性别') + + fieldSelect('学院', 'college', isEditing ? editingUser.college : '', collegeOptions, '请选择学院') + + fieldInput('班级', 'className', isEditing ? editingUser.className : '', '请输入班级') + + fieldInput('学号', 'studentNo', isEditing ? editingUser.studentNo : '', '请输入学号') + + fieldSelect('类别', 'category', isEditing ? editingUser.category : '', categoryOptions, '请选择类别') + + '
' + + ' ' + + ' ' + + '
' + + '

' + + '
' + + '
' + + (!isEditing ? '' + // 批量上传区域(仅新增时显示) + + '
' + + '

批量上传用户

通过 Excel 或 CSV 文件批量导入用户信息。

' + + '
' + + '
' + + '

支持格式:.csv

' + + '

📝 文件要求(列顺序固定):

' + + '

身份证号 | 邮箱 | 密码 | 姓名 | 电话 | 性别 | 学院 | 班级 | 学号 | 类别

' + + '

第一行为表头,数据从第二行开始。性别:男/女,类别:青年组/老年组。

' + + '

注意:当前不支持直接上传 Excel,也不要直接把用户导出文件回传导入。

' + + '
' + + '
' + + ' ' + + ' ' + + ' ' + + '
' + + '

' + + '
' + + '
' : ''); + + bindUserAddActions(); + } + + function bindUserAddActions() { + var form = document.getElementById('userForm'); + var clearBtn = document.getElementById('clearUserForm'); + var messageEl = document.getElementById('userFormMessage'); + var uploadBtn = document.getElementById('uploadUsersBtn'); + var fileInput = document.getElementById('uploadFileInput'); + var fileNameEl = document.getElementById('uploadFileName'); + var uploadMsgEl = document.getElementById('uploadMessage'); + var isEditing = !!state.editingUserId; + + if (form) { + form.addEventListener('submit', function (event) { + event.preventDefault(); + var formData = new FormData(form); + + var payload = { + idCard: String(formData.get('idCard') || '').trim(), + email: String(formData.get('email') || '').trim(), + 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() + }; + + if (!isEditing) { + var pwd = String(formData.get('password') || ''); + var confirmPwd = String(formData.get('confirmPassword') || ''); + if (pwd !== confirmPwd) { + appUtils.showMessage(messageEl, '两次输入的密码不一致', false); + return; + } + if (pwd.length < 6) { + appUtils.showMessage(messageEl, '密码长度不能少于6位', false); + return; + } + payload.password = pwd; + } + + appUtils.ajax({ + method: isEditing ? 'PUT' : 'POST', + url: isEditing ? '/api/admin/users/' + state.editingUserId : '/api/admin/users', + data: payload, + success: function (response) { + if (!response.success) { + appUtils.showMessage(messageEl, response.message || '保存失败', false); + return; + } + appUtils.showMessage(messageEl, isEditing ? '用户信息已更新' : '用户新增成功', true); + state.editingUserId = null; + switchView('user-list'); + loadAllAdminUsers(); + }, + error: function (xhr, response) { + appUtils.showMessage(messageEl, (response && response.message) || '保存失败', false); + } + }); + }); + } + + if (clearBtn) { + clearBtn.addEventListener('click', function () { + state.editingUserId = null; + switchView('user-list'); + }); + } + + var isEditingLocal = !!state.editingUserId; + if (!isEditingLocal && uploadBtn && fileInput) { + uploadBtn.addEventListener('click', function () { + fileInput.click(); + }); + fileInput.addEventListener('change', function () { + var file = fileInput.files[0]; + if (!file) return; + if (fileNameEl) { + fileNameEl.textContent = '已选择:' + file.name; + } + var formDataUpload = new FormData(); + formDataUpload.append('file', file); + appUtils.ajax({ + method: 'POST', + url: '/api/admin/users/import', + data: formDataUpload, + success: function (response) { + if (!response.success) { + appUtils.showMessage(uploadMsgEl, response.message || '上传失败', false); + return; + } + appUtils.showMessage(uploadMsgEl, response.message || '用户信息上传成功', true); + state.userKeyword = ''; + state.userPage = 1; + loadAllAdminUsers(); + setTimeout(function () { + switchView('user-list'); + }, 800); + }, + error: function (xhr, response) { + appUtils.showMessage(uploadMsgEl, (response && response.message) || '上传失败', false); + } + }); + fileInput.value = ''; + if (fileNameEl) fileNameEl.textContent = ''; + }); + } + + } + + function bindPaginationActions() { + Array.prototype.slice.call(document.querySelectorAll('.page-btn:not([disabled])')).forEach(function (button) { + button.addEventListener('click', function () { + var page = Number(button.getAttribute('data-page')); + if (page >= 1) { + state.userPage = page; + renderCurrentView(); + } + }); + }); + } + + function bindAdminUserActions() { + Array.prototype.slice.call(document.querySelectorAll('.edit-user-btn')).forEach(function (button) { + button.addEventListener('click', function () { + state.editingUserId = Number(button.getAttribute('data-id')); + switchView('user-add'); + }); + }); + bindUserAction('.reset-password-btn', 'POST', '/api/admin/users/{id}/reset-password', '重置密码成功'); + bindUserAction('.disable-user-btn', 'POST', '/api/admin/users/{id}/disable', '账号已禁用'); + bindUserAction('.enable-user-btn', 'POST', '/api/admin/users/{id}/enable', '账号已解除禁用'); + bindUserAction('.delete-user-btn', 'DELETE', '/api/admin/users/{id}', '用户已删除'); + } + + function bindUserAction(selector, method, urlTemplate, successText) { + Array.prototype.slice.call(document.querySelectorAll(selector)).forEach(function (button) { + button.addEventListener('click', function () { + if (!confirm('确定要执行此操作吗?')) return; + var userId = button.getAttribute('data-id'); + appUtils.ajax({ + method: method, + url: urlTemplate.replace('{id}', userId), + success: function (response) { + if (!response.success) { + window.alert(response.message || '操作失败'); + return; + } + if (successText) { + window.alert(successText); + } + loadAllAdminUsers(); + }, + error: function (xhr, response) { + window.alert((response && response.message) || '操作失败'); + } + }); + }); + }); + } + + // ===================== 参赛运动员管理 ===================== + function renderAthleteManage() { + if (!state.adminRegistrations.length) { + mainView.innerHTML = '

暂无报名记录

当前还没有参赛人员数据。

'; + return; + } + + state.athleteTotal = state.adminRegistrations.length; + var totalPages = Math.ceil(state.athleteTotal / state.athletePageSize) || 1; + if (state.athletePage > totalPages) state.athletePage = totalPages; + var start = (state.athletePage - 1) * state.athletePageSize; + var pageItems = state.adminRegistrations.slice(start, start + state.athletePageSize); + var coveredEvents = {}; + state.adminRegistrations.forEach(function (item) { + coveredEvents[item.eventName] = true; + }); + + mainView.innerHTML = '' + + '
' + + '

数据概览

查看当前参赛人员和报名记录。

' + + '
' + + summaryCard('报名记录总数', state.adminRegistrations.length) + + summaryCard('已覆盖项目', Object.keys(coveredEvents).length) + + summaryCard('系统用户数', state.allAdminUsers.length) + + '
' + + '
' + + '
' + + '

参赛名单

显示所有已报名人员和项目详情。

' + + '
' + + ' ' + + ' ' + + ' ' + + '
' + + ' ' + + ' ' + + ' ' + + pageItems.map(function (item) { + return '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + ''; + }).join('') + + ' ' + + '
姓名账号电话学院类别项目名称项目分类时间地点状态报名时间
' + escapeHtml(item.studentName) + '' + escapeHtml(item.username) + '' + escapeHtml(item.phone) + '' + escapeHtml(item.college) + '' + escapeHtml(item.category) + '' + escapeHtml(item.eventName) + '' + escapeHtml(item.eventCategory) + '' + escapeHtml((item.eventTime || '') + ' / ' + (item.location || '')) + '' + escapeHtml(item.status) + '' + escapeHtml(item.createdAt) + '
' + + renderAthletePagination(totalPages) + + '
'; + + bindAthleteActions(); + } + + function renderAthletePagination(totalPages) { + var html = ''; + html += '

共 ' + state.athleteTotal + ' 条记录,第 ' + state.athletePage + ' / ' + totalPages + ' 页

'; + return html; + } + + function bindAthleteActions() { + var exportBtn = document.getElementById('exportAthleteBtn'); + if (exportBtn) { + exportBtn.addEventListener('click', function () { + var exportData = state.adminRegistrations; + if (!exportData.length) { + window.alert('没有可导出的参赛名单数据'); + return; + } + var headers = ['姓名', '账号', '电话', '学院', '类别', '项目名称', '项目分类', '比赛时间', '比赛地点', '状态', '报名时间']; + var csvContent = '\uFEFF' + headers.join(',') + '\n'; + exportData.forEach(function (item) { + var row = [ + item.studentName || '', + item.username || '', + item.phone || '', + item.college || '', + item.category || '', + item.eventName || '', + item.eventCategory || '', + item.eventTime || '', + item.location || '', + item.status || '', + item.createdAt || '' + ]; + csvContent += row.map(function (v) { return '"' + String(v).replace(/"/g, '""') + '"'; }).join(',') + '\n'; + }); + var blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + var url = window.URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = '参赛名单.csv'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + }); + } + + bindAthletePaginationActions(); + } + + function bindAthletePaginationActions() { + Array.prototype.slice.call(document.querySelectorAll('.page-btn[data-type="athlete"]:not([disabled])')).forEach(function (button) { + button.addEventListener('click', function () { + var page = Number(button.getAttribute('data-page')); + if (page >= 1) { + state.athletePage = page; + renderCurrentView(); + } + }); + }); + } + + // ===================== 项目列表 ===================== + function renderEventList() { + state.adminEventTotal = state.adminEvents.length; + var totalPages = Math.ceil(state.adminEventTotal / state.adminEventPageSize) || 1; + if (state.adminEventPage > totalPages) state.adminEventPage = totalPages; + var start = (state.adminEventPage - 1) * state.adminEventPageSize; + var pageItems = state.adminEvents.slice(start, start + state.adminEventPageSize); + + mainView.innerHTML = '' + + '
' + + '

项目列表

查看、编辑和删除当前项目。

' + + (state.adminEvents.length ? '' + + '' + + ' ' + + ' ' + + pageItems.map(function (item) { + return '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + ''; + }).join('') + + ' ' + + '
项目名称项目分类比赛时间比赛地点人数上限已报名项目说明操作
' + escapeHtml(item.eventName) + '' + escapeHtml(item.eventCategory) + '' + escapeHtml(item.eventTime) + '' + escapeHtml(item.location) + '' + escapeHtml(item.quota) + '' + escapeHtml(item.registeredCount || 0) + '' + escapeHtml(item.description) + '' + + '' + + '' + + '
' + + renderAdminEventPagination(totalPages) + : '

暂无项目

请先到新增项目页面添加比赛项目。

') + + '
'; + + bindEventListActions(); + } + + function renderAdminEventPagination(totalPages) { + var html = ''; + html += '

共 ' + state.adminEventTotal + ' 条记录,第 ' + state.adminEventPage + ' / ' + totalPages + ' 页

'; + return html; + } + + function bindEventListActions() { + Array.prototype.slice.call(document.querySelectorAll('.edit-event-btn')).forEach(function (button) { + button.addEventListener('click', function () { + state.editingEventId = Number(button.getAttribute('data-id')); + switchView('event-add'); + }); + }); + + Array.prototype.slice.call(document.querySelectorAll('.delete-event-btn')).forEach(function (button) { + button.addEventListener('click', function () { + if (!confirm('确定要删除该项目吗?')) return; + var eventId = button.getAttribute('data-id'); + 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) || '删除失败'); + } + }); + }); + }); + + bindAdminEventPaginationActions(); + } + + function bindAdminEventPaginationActions() { + Array.prototype.slice.call(document.querySelectorAll('.page-btn[data-type="adminEvent"]:not([disabled])')).forEach(function (button) { + button.addEventListener('click', function () { + var page = Number(button.getAttribute('data-page')); + if (page >= 1) { + state.adminEventPage = page; + renderCurrentView(); + } + }); + }); + } + + // ===================== 新增项目 ===================== + function renderEventAdd() { + var editingItem = state.adminEvents.find(function (item) { + return item.id === state.editingEventId; + }) || { + eventName: '', + eventCategory: '', + location: '', + quota: '', + eventTime: '', + description: '' + }; + + mainView.innerHTML = '' + + '
' + + '

' + (state.editingEventId ? '编辑项目' : '新增项目') + '

在这里维护比赛项目基础信息。

' + + '
' + + fieldInput('项目名称', 'eventName', editingItem.eventName, '请输入项目名称') + + fieldInput('项目分类', 'eventCategory', editingItem.eventCategory, '请输入项目分类') + + fieldInput('比赛地点', 'location', editingItem.location, '请输入比赛地点') + + ' ' + + fieldInput('比赛时间', 'eventTime', editingItem.eventTime, '例如 2026-05-20 08:30', true) + + fieldInput('项目说明', 'description', editingItem.description, '请输入项目说明', true) + + '
' + + ' ' + + ' ' + + '
' + + '

' + + '
' + + '
'; + + bindEventAddActions(); + } + + function bindEventAddActions() { + 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), + eventTime: String(formData.get('eventTime') || '').trim(), + description: String(formData.get('description') || '').trim() + }; + + 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; + appUtils.showMessage(messageEl, '项目保存成功', true); + loadAdminData(); + setTimeout(function () { + renderCurrentView(); + }, 800); + }, + error: function (xhr, response) { + appUtils.showMessage(messageEl, (response && response.message) || '保存失败', false); + } + }); + }); + } + + if (resetButton) { + resetButton.addEventListener('click', function () { + state.editingEventId = null; + renderCurrentView(); + }); + } + } + + // ===================== 占位页面 ===================== + function renderPlaceholder(title, desc) { + mainView.innerHTML = '' + + '
' + + '

' + escapeHtml(title) + '

' + escapeHtml(desc) + '

' + + '

功能持续完善中

这个模块的详细功能可以继续在现有框架上补充。

' + + '
'; + } + + // ===================== 视图路由 ===================== + function renderCurrentView() { + var meta = getMeta(state.currentView); + sectionTitle.textContent = meta.label; + sectionDesc.textContent = meta.desc; + renderTabs(); + + if (state.currentView === 'profile-view') { + renderProfileView(); + return; + } + if (state.currentView === 'profile-edit') { + renderProfileEdit(); + return; + } + if (state.currentView === 'events') { + renderEventTable(state.currentTab === 'mine' ? state.myEvents : state.events, state.currentTab === 'mine'); + return; } if (state.currentView === 'admin-home') { renderAdminHome(); return; } - if (state.currentView === 'user-manage') { - renderUserManage(); + if (state.currentView === 'user-list') { + renderUserList(); return; } - if (state.currentView === 'athlete-manage') { - renderAthleteManage(); + if (state.currentView === 'user-add') { + renderUserAdd(); + return; + } + if (state.currentView === 'event-list') { + renderEventList(); return; } - if (state.currentView === 'event-manage') { - renderEventManage(); + if (state.currentView === 'event-add') { + renderEventAdd(); + return; + } + if (state.currentView === 'athlete-manage') { + renderAthleteManage(); return; } if (state.currentView === 'team-info') { @@ -470,10 +1578,14 @@ document.addEventListener('DOMContentLoaded', function () { if (view === 'events' && state.currentTab !== 'mine') { state.currentTab = 'all'; } + if (view === 'user-list') { + state.userPage = 1; + } renderSidebar(); renderCurrentView(); } + // ===================== 数据加载 ===================== function loadEventData() { appUtils.ajax({ method: 'GET', @@ -512,6 +1624,7 @@ document.addEventListener('DOMContentLoaded', function () { url: '/api/admin/users', success: function (response) { if (response.success) { + state.allAdminUsers = response.data || []; state.adminUsers = response.data || []; renderCurrentView(); } @@ -539,115 +1652,37 @@ document.addEventListener('DOMContentLoaded', function () { } } }); - } - function bindAdminUserActions() { - bindUserAction('.reset-password-btn', 'POST', '/api/admin/users/{id}/reset-password', '重置密码成功'); - bindUserAction('.disable-user-btn', 'POST', '/api/admin/users/{id}/disable', '账号已禁用'); - bindUserAction('.enable-user-btn', 'POST', '/api/admin/users/{id}/enable', '账号已解除禁用'); - bindUserAction('.delete-user-btn', 'DELETE', '/api/admin/users/{id}', '用户已删除'); - } - - function bindUserAction(selector, method, urlTemplate, successText) { - Array.prototype.slice.call(document.querySelectorAll(selector)).forEach(function (button) { - button.addEventListener('click', function () { - var userId = button.getAttribute('data-id'); - appUtils.ajax({ - method: method, - url: urlTemplate.replace('{id}', userId), - success: function (response) { - if (!response.success) { - window.alert(response.message || '操作失败'); - return; - } - if (successText) { - window.alert(successText); - } - loadAdminData(); - }, - error: function (xhr, response) { - window.alert((response && response.message) || '操作失败'); - } - }); - }); + appUtils.ajax({ + method: 'GET', + url: '/api/admin/dashboard/stats', + success: function (response) { + if (response.success && response.data) { + state.dashboardStats = 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), - eventTime: String(formData.get('eventTime') || '').trim(), - description: String(formData.get('description') || '').trim() - }; - - 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; - loadAdminData(); - }, - error: function (xhr, response) { - appUtils.showMessage(messageEl, (response && response.message) || '保存失败', false); - } - }); - }); - } - - if (resetButton) { - resetButton.addEventListener('click', function () { - state.editingEventId = null; - renderCurrentView(); - }); + function loadAllAdminUsers() { + if (!state.user || state.user.role !== 'ADMIN') { + return; } - - 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'); - 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) || '删除失败'); - } - }); - }); + appUtils.ajax({ + method: 'GET', + url: '/api/admin/users', + success: function (response) { + if (response.success) { + state.allAdminUsers = response.data || []; + state.adminUsers = response.data || []; + renderCurrentView(); + } + } }); } + // ===================== 辅助函数 ===================== function loadCurrentUser() { appUtils.ajax({ method: 'GET', @@ -659,7 +1694,7 @@ document.addEventListener('DOMContentLoaded', function () { } state.user = response.data; - state.currentView = state.user.role === 'ADMIN' ? 'admin-home' : 'profile'; + state.currentView = state.user.role === 'ADMIN' ? 'admin-home' : 'profile-view'; updateCurrentUserName(); renderSidebar(); renderCurrentView(); diff --git a/assets/js/app.js b/assets/js/app.js deleted file mode 100644 index 661d585..0000000 --- a/assets/js/app.js +++ /dev/null @@ -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, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - } - - function getMenus() { - return state.user && state.user.role === 'ADMIN' ? adminMenus : studentMenus; - } - - function renderSidebar() { - var menus = getMenus(); - sidebarNav.innerHTML = menus.map(function (item, index) { - return ''; - }).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 = '' - + '' - + ''; - } - 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 = '' - + '
' - + '

鍩虹璧勬枡

褰撳墠璐﹀彿鐨勪釜浜轰俊鎭涓嬨€?/p>

' - + '
' - + '
韬唤璇佸彿

' + escapeHtml(state.user.idCard) + '

' - + '
鐧诲綍璐﹀彿

' + escapeHtml(state.user.username) + '

' - + '
濮撳悕

' + escapeHtml(state.user.name) + '

' - + '
鑱旂郴鐢佃瘽

' + escapeHtml(state.user.phone) + '

' - + '
鎬у埆

' + escapeHtml(state.user.gender) + '

' - + '
瀛﹂櫌

' + escapeHtml(state.user.college) + '

' - + '
鐝骇

' + escapeHtml(state.user.className) + '

' - + '
瀛﹀彿

' + escapeHtml(state.user.studentNo) + '

' - + '
绫诲埆

' + escapeHtml(state.user.category) + '

' - + '
瑙掕壊

' + escapeHtml(state.user.role) + '

' - + '
' - + '
'; - } - - function renderProfile() { - mainView.innerHTML = '' - + '
' - + '

鍩虹璧勬枡

褰撳墠璐﹀彿鐨勪釜浜轰俊鎭涓嬨€?/p>

' - + '
' - + '
韬唤璇佸彿

' + escapeHtml(state.user.idCard) + '

' - + '
鐧诲綍璐﹀彿

' + escapeHtml(state.user.username) + '

' - + '
濮撳悕

' + escapeHtml(state.user.name) + '

' - + '
鑱旂郴鐢佃瘽

' + escapeHtml(state.user.phone) + '

' - + '
鎬у埆

' + escapeHtml(state.user.gender) + '

' - + '
瀛﹂櫌

' + escapeHtml(state.user.college) + '

' - + '
鐝骇

' + escapeHtml(state.user.className) + '

' - + '
瀛﹀彿

' + escapeHtml(state.user.studentNo) + '

' - + '
绫诲埆

' + escapeHtml(state.user.category) + '

' - + '
瑙掕壊

' + escapeHtml(state.user.role) + '

' - + '
' - + '
' - + '
' - + '

淇敼涓汉淇℃伅

鍙互鍦ㄨ繖閲屾洿鏂板鍚嶃€佺數璇濄€佸闄€佺彮绾у拰瀛﹀彿绛変俊鎭€?/p>

' - + '
' - + ' ' - + '
' - + '

' - + ' ' - + '
'; - - 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 = '

鏆傛棤鏁版嵁

' + (isMine ? '褰撳墠杩樻病鏈夋姤鍚嶈褰曘€? : '褰撳墠娌℃湁鍙睍绀洪」鐩€?) + '

'; - return; - } - - mainView.innerHTML = '' - + '' - + ' ' - + ' ' - + list.map(function (item) { - var actionHtml = isMine - ? '' - : ''; - return '' - + '' - + '' - + '' - + '' - + '' - + '' - + '' - + '' - + ''; - }).join('') - + ' ' - + '
椤圭洰鍚嶇О椤圭洰绫诲埆姣旇禌鏃堕棿姣旇禌鍦扮偣鎶ュ悕鎯呭喌椤圭洰璇存槑鎿嶄綔
' + escapeHtml(item.eventName) + '' + escapeHtml(item.eventCategory) + '' + escapeHtml(item.eventTime) + '' + escapeHtml(item.location) + '' + escapeHtml(item.registeredCount + '/' + item.quota) + '' + escapeHtml(item.description) + '' + actionHtml + '
'; - - bindEventButtons(); - } - - function renderUserManage() { - if (!state.adminUsers.length) { - mainView.innerHTML = '

鏆傛棤鐢ㄦ埛

褰撳墠绯荤粺杩樻病鏈夌敤鎴锋暟鎹€?/p>

'; - return; - } - - mainView.innerHTML = '' - + '
' - + '

鏁版嵁姒傝

鏌ョ湅褰撳墠绯荤粺鍐呯敤鎴锋€讳綋鎯呭喌銆?/p>

' - + '
' - + '
鐢ㄦ埛鎬绘暟

' + state.adminUsers.length + '

' - + '
绠$悊鍛樻暟閲?/strong>

' + state.adminUsers.filter(function (item) { return item.role === "ADMIN"; }).length + '

' - + '
绂佺敤璐﹀彿鏁伴噺

' + state.adminUsers.filter(function (item) { return item.status === "DISABLED"; }).length + '

' - + '
' - + '
' - + '
' - + '

鐢ㄦ埛鍒楄〃

鏌ョ湅绯荤粺鍐呮墍鏈夌敤鎴蜂俊鎭€?/p>

' - + ' ' - + ' ' - + ' ' - + state.adminUsers.map(function (item) { - var isAdmin = item.role === 'ADMIN'; - var statusText = item.status === 'DISABLED' ? '宸茬鐢? : '姝e父'; - var statusAction = isAdmin - ? '' - : (item.status === 'DISABLED' - ? '' - : ''); - var deleteAction = isAdmin ? '' : ''; - return '' - + '' - + '' - + '' - + '' - + '' - + '' - + '' - + '' - + '' - + '' - + '' - + ''; - }).join('') - + ' ' - + '
濮撳悕璐﹀彿韬唤璇佸彿鐢佃瘽鎬у埆瀛﹂櫌绫诲埆瑙掕壊鐘舵€?/th>鎿嶄綔
' + escapeHtml(item.name) + '' + escapeHtml(item.username) + '' + escapeHtml(item.idCard) + '' + escapeHtml(item.phone) + '' + escapeHtml(item.gender) + '' + escapeHtml(item.college) + '' + escapeHtml(item.category) + '' + escapeHtml(isAdmin ? '绠$悊鍛? : '鏅€氱敤鎴?) + '' + escapeHtml(statusText) + ' ' + statusAction + ' ' + deleteAction + '
' - + '
'; - - bindUserManageActions(); - } - - function renderAthleteManage() { - if (!state.adminRegistrations.length) { - mainView.innerHTML = '

鏆傛棤鎶ュ悕璁板綍

褰撳墠杩樻病鏈夊弬璧涜繍鍔ㄥ憳鏁版嵁銆?/p>

'; - return; - } - - mainView.innerHTML = '' - + '
' - + '

鏁版嵁姒傝

鏌ョ湅褰撳墠鍙傝禌杩愬姩鍛樼殑鎬讳綋缁熻銆?/p>

' - + '
' - + '
鎶ュ悕璁板綍鎬绘暟

' + state.adminRegistrations.length + '

' - + '
宸叉姤鍚嶇姸鎬?/strong>

' + state.adminRegistrations.filter(function (item) { return item.status === "宸叉姤鍚?; }).length + '

' - + '
瑕嗙洊椤圭洰鏁?/strong>

' + uniqueEventCount() + '

' - + '
' - + '
' - + '
' - + '

鍙傝禌鍚嶅崟

鏌ョ湅鎵€鏈夊凡鎶ュ悕杩愬姩鍛樺拰椤圭洰鏄庣粏銆?/p>

' - + ' ' - + ' ' - + ' ' - + state.adminRegistrations.map(function (item) { - return '' - + '' - + '' - + '' - + '' - + '' - + '' - + '' - + '' - + '' - + '' - + '' - + ''; - }).join('') - + ' ' - + '
濮撳悕璐﹀彿鐢佃瘽瀛﹂櫌绫诲埆椤圭洰鍚嶇О椤圭洰绫诲埆鏃堕棿鍦扮偣鐘舵€?/th>鎶ュ悕鏃堕棿
' + escapeHtml(item.studentName) + '' + escapeHtml(item.username) + '' + escapeHtml(item.phone) + '' + escapeHtml(item.college) + '' + escapeHtml(item.category) + '' + escapeHtml(item.eventName) + '' + escapeHtml(item.eventCategory) + '' + escapeHtml(item.eventTime + ' / ' + item.location) + '' + escapeHtml(item.status) + '' + escapeHtml(item.createdAt) + '
' - + '
'; - } - - 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 = '' - + '
' - + '

' + formTitle + '

鍙湪杩欓噷缁存姢姣旇禌椤圭洰鐨勫熀纭€淇℃伅銆?/p>

' - + '
' - + ' ' - + '
' - + '

' - + ' ' - + '
' - + '
' - + '

椤圭洰鍒楄〃

褰撳墠绯荤粺鍐呯殑鍏ㄩ儴姣旇禌椤圭洰銆?/p>

' - + (state.adminEvents.length ? '' - + '' - + ' ' - + ' ' - + state.adminEvents.map(function (item) { - return '' - + '' - + '' - + '' - + '' - + '' - + '' - + '' - + '' - + '' - + ''; - }).join('') - + ' ' - + '
椤圭洰鍚嶇О椤圭洰鍒嗙被姣旇禌鏃堕棿姣旇禌鍦扮偣浜烘暟涓婇檺宸叉姤鍚?/th>椤圭洰璇存槑鎿嶄綔
' + escapeHtml(item.eventName) + '' + escapeHtml(item.eventCategory) + '' + escapeHtml(item.eventTime) + '' + escapeHtml(item.location) + '' + escapeHtml(item.quota) + '' + escapeHtml(item.registeredCount || 0) + '' + escapeHtml(item.description) + '
' - : '

鏆傛棤椤圭洰

褰撳墠杩樻病鏈夋瘮璧涢」鐩紝璇峰厛鏂板椤圭洰銆?/p>

') - + '
'; - - bindEventManageActions(); - } - - function renderPlaceholder(title, text) { - mainView.innerHTML = '' - + '
' - + '

' + title + '

' - + '

' + text + '

' - + '
'; - } - - 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(); -}); diff --git a/assets/js/common.js b/assets/js/common.js index 8ebb044..f334e68 100644 --- a/assets/js/common.js +++ b/assets/js/common.js @@ -1,9 +1,25 @@ (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() { - if (window.location.protocol === 'file:') { - return 'http://localhost:8080'; + if (window.APP_CONFIG && window.APP_CONFIG.API_BASE) { + 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(); @@ -19,7 +35,10 @@ var xhr = new XMLHttpRequest(); xhr.open(options.method || 'GET', buildUrl(options.url), 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 () { var response; if (xhr.readyState !== 4) { @@ -39,7 +58,7 @@ xhr.onerror = function () { 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) { diff --git a/assets/js/config.js b/assets/js/config.js new file mode 100644 index 0000000..4102f37 --- /dev/null +++ b/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' +}; diff --git a/assets/js/login.js b/assets/js/login.js index 02489d9..8bb2981 100644 --- a/assets/js/login.js +++ b/assets/js/login.js @@ -1,55 +1,154 @@ -document.addEventListener('DOMContentLoaded', function () { +document.addEventListener('DOMContentLoaded', function () { appUtils.redirectIfLoggedIn(); var form = document.getElementById('loginForm'); var message = document.getElementById('loginMessage'); var goRegister = document.getElementById('goRegister'); - var usernameInput = document.querySelector('input[name="username"]'); - var passwordInput = document.querySelector('input[name="password"]'); - - // 强制清空输入字段,确保不保留历史登录信息 - setTimeout(function() { - usernameInput.value = ''; - passwordInput.value = ''; - // 移除可能的自动填充样式 - usernameInput.style.background = 'rgba(255, 255, 255, 0.95)'; - passwordInput.style.background = 'rgba(255, 255, 255, 0.95)'; - }, 100); - - // 再次清空,确保浏览器自动填充后也能被清空 - setTimeout(function() { - usernameInput.value = ''; - passwordInput.value = ''; - }, 500); + var refreshCaptchaBtn = document.getElementById('refreshCaptcha'); + var captchaCanvas = document.getElementById('captchaCanvas'); + var captchaCtx = captchaCanvas.getContext('2d'); + var captchaCode = ''; + var rememberCheckbox = document.getElementById('rememberAccount'); + var loginIdInput = form.querySelector('[name="loginId"]'); + + // 记住账号:页面加载时填充 + var remembered = localStorage.getItem('rememberedAccount'); + if (remembered) { + loginIdInput.value = remembered; + rememberCheckbox.checked = true; + } + + // 勾选时立即保存 + rememberCheckbox.addEventListener('change', function () { + 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 () { window.location.href = './register.html'; }); + refreshCaptchaBtn.addEventListener('click', function () { + drawCaptcha(); + }); + + captchaCanvas.addEventListener('click', function () { + drawCaptcha(); + }); + form.addEventListener('submit', function (event) { event.preventDefault(); var formData = new FormData(form); + if (!validateCaptcha(formData.get('captcha'))) { + appUtils.showMessage(message, '图像验证码错误,请重新输入', false); + drawCaptcha(); + return; + } + appUtils.ajax({ method: 'POST', url: '/api/auth/login', data: { - username: String(formData.get('username') || '').trim(), + loginId: String(formData.get('loginId') || '').trim(), password: String(formData.get('password') || '').trim() }, success: function (response) { if (!response.success) { appUtils.showMessage(message, response.message || '登录失败', false); + drawCaptcha(); return; } + + // 记住账号:登录成功时再次确认保存 + if (rememberCheckbox.checked) { + localStorage.setItem('rememberedAccount', String(formData.get('loginId') || '').trim()); + } + appUtils.showMessage(message, '登录成功,正在进入系统...', true); setTimeout(function () { window.location.href = './app.html'; }, 400); }, error: function (xhr, response) { - appUtils.showMessage(message, (response && response.message) || '网络异常,请确认后端已启动', false); + appUtils.showMessage(message, (response && response.message) || '网络异常,请检查后端服务', false); + drawCaptcha(); } }); }); + + drawCaptcha(); }); diff --git a/assets/js/register.js b/assets/js/register.js index a1646ed..827e3a4 100644 --- a/assets/js/register.js +++ b/assets/js/register.js @@ -1,4 +1,4 @@ -document.addEventListener('DOMContentLoaded', function () { +document.addEventListener('DOMContentLoaded', function () { appUtils.redirectIfLoggedIn(); var form = document.getElementById('registerForm'); @@ -18,16 +18,16 @@ document.addEventListener('DOMContentLoaded', function () { url: '/api/auth/register', data: { idCard: String(formData.get('idCard') || '').trim(), - username: String(formData.get('username') || '').trim(), + email: String(formData.get('email') || '').trim(), password: String(formData.get('password') || '').trim(), confirmPassword: String(formData.get('confirmPassword') || '').trim(), name: String(formData.get('name') || '').trim(), phone: String(formData.get('phone') || '').trim(), - gender: String(formData.get('gender') || ''), + 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') || '') + category: String(formData.get('category') || '').trim() }, success: function (response) { if (!response.success) { @@ -40,7 +40,7 @@ document.addEventListener('DOMContentLoaded', function () { }, 400); }, error: function (xhr, response) { - appUtils.showMessage(message, (response && response.message) || '网络异常,请确认后端已启动', false); + appUtils.showMessage(message, (response && response.message) || '网络异常,请检查后端服务', false); } }); }); diff --git a/login.html b/login.html index 11b48da..0a5156b 100644 --- a/login.html +++ b/login.html @@ -1,25 +1,24 @@ - + - 运动会报名系统 - 登录 - + + - - \ No newline at end of file + diff --git a/register.html b/register.html index b930506..24f7965 100644 --- a/register.html +++ b/register.html @@ -1,4 +1,4 @@ - + @@ -11,7 +11,7 @@

新用户注册

-

请完整填写个人信息,注册成功后将自动登录。

+

请完整填写个人信息,邮箱将用于登录,注册成功后可直接进入系统。