commit
e1ccdd140b
10 changed files with 1661 additions and 0 deletions
@ -0,0 +1,67 @@ |
|||||
|
|
||||
|
# Compiled class files |
||||
|
*.class |
||||
|
|
||||
|
# Log files |
||||
|
*.log |
||||
|
|
||||
|
# BlueJ files |
||||
|
*.ctxt |
||||
|
|
||||
|
# Mobile Tools for Java (J2ME) |
||||
|
.mtj.tmp/ |
||||
|
|
||||
|
# Package files |
||||
|
*.jar |
||||
|
*.war |
||||
|
*.nar |
||||
|
*.ear |
||||
|
*.zip |
||||
|
*.tar.gz |
||||
|
*.rar |
||||
|
|
||||
|
# virtual machine crash logs |
||||
|
hs_err_pid* |
||||
|
replay_pid* |
||||
|
|
||||
|
# Maven |
||||
|
target/ |
||||
|
pom.xml.tag |
||||
|
pom.xml.releaseBackup |
||||
|
pom.xml.versionsBackup |
||||
|
pom.xml.next |
||||
|
release.properties |
||||
|
dependency-reduced-pom.xml |
||||
|
buildNumber.properties |
||||
|
.mvn/timing.properties |
||||
|
.mvn/wrapper/maven-wrapper.jar |
||||
|
|
||||
|
# IDE |
||||
|
.idea/ |
||||
|
*.iml |
||||
|
*.ipr |
||||
|
*.iws |
||||
|
.vscode/ |
||||
|
*.swp |
||||
|
*.swo |
||||
|
*~ |
||||
|
|
||||
|
# OS |
||||
|
.DS_Store |
||||
|
Thumbs.db |
||||
|
|
||||
|
# Frontend |
||||
|
node_modules/ |
||||
|
dist/ |
||||
|
build/ |
||||
|
npm-debug.log |
||||
|
yarn-error.log |
||||
|
|
||||
|
# Database |
||||
|
*.db |
||||
|
*.sqlite |
||||
|
|
||||
|
# SSL certificates |
||||
|
*.pem |
||||
|
*.key |
||||
|
*.crt |
||||
@ -0,0 +1,107 @@ |
|||||
|
|
||||
|
# 运动会报名系统 |
||||
|
|
||||
|
## 项目简介 |
||||
|
|
||||
|
这是一个基于 Spring Boot 和前端技术栈开发的运动会报名系统,用于管理学校运动会的项目报名工作。 |
||||
|
|
||||
|
## 技术栈 |
||||
|
|
||||
|
### 后端 |
||||
|
- Java 8+ |
||||
|
- Spring Boot 2.x |
||||
|
- MyBatis |
||||
|
- MySQL 数据库 |
||||
|
|
||||
|
### 前端 |
||||
|
- HTML5 / CSS3 / JavaScript |
||||
|
- Bootstrap 样式框架 |
||||
|
|
||||
|
## 项目结构 |
||||
|
|
||||
|
``` |
||||
|
运动会报名/ |
||||
|
├── 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 # 项目说明 |
||||
|
``` |
||||
|
|
||||
|
## 功能模块 |
||||
|
|
||||
|
### 用户功能 |
||||
|
- 用户注册与登录 |
||||
|
- 个人信息管理 |
||||
|
- 运动会项目浏览 |
||||
|
- 项目报名与取消 |
||||
|
|
||||
|
### 管理员功能 |
||||
|
- 用户信息管理 |
||||
|
- 报名总览查看 |
||||
|
- 报名记录统计 |
||||
|
|
||||
|
## 快速开始 |
||||
|
|
||||
|
### 环境要求 |
||||
|
- JDK 8 或更高版本 |
||||
|
- Maven 3.6+ |
||||
|
- MySQL 5.7+ |
||||
|
|
||||
|
### 数据库配置 |
||||
|
|
||||
|
创建数据库并导入初始数据: |
||||
|
|
||||
|
```sql |
||||
|
CREATE DATABASE sports_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; |
||||
|
USE sports_db; |
||||
|
``` |
||||
|
|
||||
|
导入 `backend/src/main/resources/schema.sql` 和 `backend/src/main/resources/data.sql` |
||||
|
|
||||
|
### 启动后端服务 |
||||
|
|
||||
|
```bash |
||||
|
cd backend |
||||
|
mvn spring-boot:run |
||||
|
``` |
||||
|
|
||||
|
### 启动前端服务 |
||||
|
|
||||
|
使用任意 HTTP 服务器启动前端,例如: |
||||
|
|
||||
|
```bash |
||||
|
cd frontend |
||||
|
python -m http.server 8080 |
||||
|
``` |
||||
|
|
||||
|
### 访问地址 |
||||
|
|
||||
|
- 前端页面: http://localhost:8080 |
||||
|
- 后端 API: http://localhost:8081 |
||||
|
|
||||
|
## 默认账号 |
||||
|
|
||||
|
| 账号 | 密码 | 角色 | |
||||
|
|------|------|------| |
||||
|
| admin | admin | 管理员 | |
||||
|
| student | student | 普通用户 | |
||||
|
|
||||
|
## 开发说明 |
||||
|
|
||||
|
### 代码规范 |
||||
|
- Java 代码遵循 Spring 编码规范 |
||||
|
- JavaScript 代码使用 ES6+ 语法 |
||||
|
- 数据库表名使用下划线命名 |
||||
|
|
||||
|
### 注意事项 |
||||
|
- 开发环境下请确保 MySQL 服务已启动 |
||||
|
- 修改配置文件后需要重启服务才能生效 |
||||
@ -0,0 +1,51 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html lang="zh-CN"> |
||||
|
|
||||
|
<head> |
||||
|
<meta charset="UTF-8"> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
|
<title>运动会报名系统</title> |
||||
|
<link rel="stylesheet" href="./assets/css/style.css"> |
||||
|
</head> |
||||
|
|
||||
|
<body class="app-body"> |
||||
|
<div class="app-layout"> |
||||
|
<header class="topbar"> |
||||
|
<div> |
||||
|
<p class="topbar-tag">Sports Meet Workspace</p> |
||||
|
<h1>运动会报名系统</h1> |
||||
|
</div> |
||||
|
<div class="topbar-user"> |
||||
|
<span id="currentUserName">加载中...</span> |
||||
|
<button type="button" class="ghost-btn small" id="logoutBtn">退出登录</button> |
||||
|
</div> |
||||
|
</header> |
||||
|
|
||||
|
<main class="main-layout"> |
||||
|
<aside class="sidebar"> |
||||
|
<button class="nav-item active" data-view="profile">个人信息</button> |
||||
|
<button class="nav-item" data-view="events">运动会报名</button> |
||||
|
<button class="nav-item hidden" data-view="admin" id="adminNavBtn">管理员后台</button> |
||||
|
<button class="nav-item hidden" data-view="registrations" id="registrationsNavBtn">报名总览</button> |
||||
|
</aside> |
||||
|
|
||||
|
<section class="content-area"> |
||||
|
<div class="content-header"> |
||||
|
<div> |
||||
|
<h2 id="sectionTitle">个人信息</h2> |
||||
|
<p id="sectionDesc">查看并维护当前登录人员的基础资料。</p> |
||||
|
</div> |
||||
|
<div class="content-tabs hidden" id="subTabs"></div> |
||||
|
</div> |
||||
|
<section id="profileView" class="content-card"></section> |
||||
|
<section id="eventsView" class="content-card hidden"></section> |
||||
|
<section id="adminView" class="content-card hidden"></section> |
||||
|
<section id="registrationsView" class="content-card hidden"></section> |
||||
|
</section> |
||||
|
</main> |
||||
|
</div> |
||||
|
<script src="./assets/js/common.js"></script> |
||||
|
<script src="./assets/js/app.js"></script> |
||||
|
</body> |
||||
|
|
||||
|
</html> |
||||
@ -0,0 +1,522 @@ |
|||||
|
:root { |
||||
|
--surface: rgba(255, 255, 255, 0.92); |
||||
|
--text: #1f2937; |
||||
|
--muted: #6b7280; |
||||
|
--line: rgba(148, 163, 184, 0.24); |
||||
|
--primary: #0f766e; |
||||
|
--primary-light: #14b8a6; |
||||
|
--success: #059669; |
||||
|
--danger: #dc2626; |
||||
|
--shadow: 0 24px 60px rgba(15, 23, 42, 0.14); |
||||
|
--radius-xl: 28px; |
||||
|
--radius-lg: 20px; |
||||
|
--radius-md: 14px; |
||||
|
} |
||||
|
|
||||
|
* { |
||||
|
box-sizing: border-box; |
||||
|
} |
||||
|
|
||||
|
body { |
||||
|
margin: 0; |
||||
|
min-height: 100vh; |
||||
|
font-family: "Microsoft YaHei", "PingFang SC", sans-serif; |
||||
|
color: var(--text); |
||||
|
background: |
||||
|
radial-gradient(circle at top left, rgba(15, 118, 110, 0.18), transparent 28%), |
||||
|
radial-gradient(circle at bottom right, rgba(245, 158, 11, 0.16), transparent 20%), |
||||
|
linear-gradient(180deg, #f8fbff 0%, #eef4f8 100%); |
||||
|
} |
||||
|
|
||||
|
button, |
||||
|
input, |
||||
|
select { |
||||
|
font: inherit; |
||||
|
} |
||||
|
|
||||
|
.hidden { |
||||
|
display: none !important; |
||||
|
} |
||||
|
|
||||
|
.auth-page { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
padding: 32px 20px; |
||||
|
} |
||||
|
|
||||
|
.auth-shell { |
||||
|
width: min(1180px, 100%); |
||||
|
display: grid; |
||||
|
gap: 24px; |
||||
|
} |
||||
|
|
||||
|
.login-shell { |
||||
|
grid-template-columns: 1.2fr 0.9fr; |
||||
|
} |
||||
|
|
||||
|
.register-shell { |
||||
|
grid-template-columns: 1.1fr 0.8fr; |
||||
|
} |
||||
|
|
||||
|
.auth-hero, |
||||
|
.auth-panel, |
||||
|
.register-side-card, |
||||
|
.topbar, |
||||
|
.sidebar, |
||||
|
.content-card { |
||||
|
background: var(--surface); |
||||
|
border: 1px solid rgba(255, 255, 255, 0.55); |
||||
|
border-radius: var(--radius-xl); |
||||
|
box-shadow: var(--shadow); |
||||
|
backdrop-filter: blur(18px); |
||||
|
} |
||||
|
|
||||
|
.auth-hero, |
||||
|
.auth-panel, |
||||
|
.register-side-card, |
||||
|
.content-card { |
||||
|
padding: 32px; |
||||
|
} |
||||
|
|
||||
|
.auth-badge, |
||||
|
.topbar-tag, |
||||
|
.pill { |
||||
|
display: inline-flex; |
||||
|
align-items: center; |
||||
|
padding: 8px 14px; |
||||
|
border-radius: 999px; |
||||
|
background: rgba(15, 118, 110, 0.12); |
||||
|
color: #115e59; |
||||
|
font-size: 13px; |
||||
|
font-weight: 700; |
||||
|
} |
||||
|
|
||||
|
.auth-hero h1, |
||||
|
.register-side-card h1 { |
||||
|
margin: 18px 0 12px; |
||||
|
line-height: 1.08; |
||||
|
font-size: clamp(32px, 4vw, 52px); |
||||
|
} |
||||
|
|
||||
|
.auth-subtitle, |
||||
|
.panel-head p, |
||||
|
.register-side-card p, |
||||
|
.notice-card li, |
||||
|
.field span, |
||||
|
.content-header p, |
||||
|
.info-meta, |
||||
|
.empty-state p { |
||||
|
color: var(--muted); |
||||
|
} |
||||
|
|
||||
|
.notice-card { |
||||
|
margin-top: 24px; |
||||
|
padding: 24px; |
||||
|
border-radius: var(--radius-lg); |
||||
|
background: linear-gradient(135deg, rgba(15, 118, 110, 0.08), rgba(255, 255, 255, 0.85)); |
||||
|
border: 1px solid rgba(15, 118, 110, 0.14); |
||||
|
} |
||||
|
|
||||
|
.notice-card h2, |
||||
|
.panel-head h2, |
||||
|
.content-header h2 { |
||||
|
margin: 0 0 10px; |
||||
|
} |
||||
|
|
||||
|
.notice-card ul { |
||||
|
margin: 0; |
||||
|
padding-left: 18px; |
||||
|
display: grid; |
||||
|
gap: 10px; |
||||
|
} |
||||
|
|
||||
|
.panel-head { |
||||
|
margin-bottom: 24px; |
||||
|
} |
||||
|
|
||||
|
.form-grid { |
||||
|
display: grid; |
||||
|
gap: 18px; |
||||
|
} |
||||
|
|
||||
|
.single-column { |
||||
|
grid-template-columns: 1fr; |
||||
|
} |
||||
|
|
||||
|
.two-columns { |
||||
|
grid-template-columns: repeat(2, minmax(0, 1fr)); |
||||
|
} |
||||
|
|
||||
|
.field { |
||||
|
display: grid; |
||||
|
gap: 10px; |
||||
|
} |
||||
|
|
||||
|
.field span { |
||||
|
font-size: 14px; |
||||
|
font-weight: 700; |
||||
|
} |
||||
|
|
||||
|
.field input, |
||||
|
.field select { |
||||
|
width: 100%; |
||||
|
border: 1px solid var(--line); |
||||
|
background: rgba(255, 255, 255, 0.95); |
||||
|
border-radius: 16px; |
||||
|
padding: 10px 16px; |
||||
|
outline: none; |
||||
|
} |
||||
|
|
||||
|
/* 下拉列表通用样式 */ |
||||
|
select { |
||||
|
appearance: none; |
||||
|
-webkit-appearance: none; |
||||
|
-moz-appearance: none; |
||||
|
background-image: url('data:image/svg+xml;charset=US-ASCII,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path fill="%236b7280" d="M7 10l5 5 5-5z"/></svg>'); |
||||
|
background-repeat: no-repeat; |
||||
|
background-position: right 16px center; |
||||
|
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); |
||||
|
} |
||||
|
|
||||
|
.form-actions { |
||||
|
display: flex; |
||||
|
gap: 14px; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.form-actions.stacked { |
||||
|
flex-direction: column; |
||||
|
} |
||||
|
|
||||
|
.full-row { |
||||
|
grid-column: 1 / -1; |
||||
|
} |
||||
|
|
||||
|
.primary-btn, |
||||
|
.ghost-btn, |
||||
|
.nav-item, |
||||
|
.tab-item, |
||||
|
.action-btn { |
||||
|
border: none; |
||||
|
cursor: pointer; |
||||
|
transition: transform 0.2s ease, background 0.2s ease; |
||||
|
} |
||||
|
|
||||
|
.primary-btn, |
||||
|
.ghost-btn { |
||||
|
min-height: 48px; |
||||
|
padding: 0 22px; |
||||
|
border-radius: 16px; |
||||
|
font-weight: 700; |
||||
|
} |
||||
|
|
||||
|
.primary-btn { |
||||
|
background: linear-gradient(135deg, var(--primary), var(--primary-light)); |
||||
|
color: #fff; |
||||
|
} |
||||
|
|
||||
|
.ghost-btn { |
||||
|
background: rgba(255, 255, 255, 0.8); |
||||
|
color: var(--text); |
||||
|
border: 1px solid var(--line); |
||||
|
} |
||||
|
|
||||
|
.small { |
||||
|
min-height: 40px; |
||||
|
padding: 0 16px; |
||||
|
} |
||||
|
|
||||
|
.form-message { |
||||
|
min-height: 22px; |
||||
|
margin: 0; |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
|
||||
|
.form-message.success { |
||||
|
color: var(--success); |
||||
|
} |
||||
|
|
||||
|
.form-message.error { |
||||
|
color: var(--danger); |
||||
|
} |
||||
|
|
||||
|
.app-body { |
||||
|
padding: 24px; |
||||
|
} |
||||
|
|
||||
|
.app-layout { |
||||
|
display: grid; |
||||
|
gap: 18px; |
||||
|
} |
||||
|
|
||||
|
.topbar { |
||||
|
padding: 22px 28px; |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.topbar h1 { |
||||
|
margin: 10px 0 0; |
||||
|
font-size: 30px; |
||||
|
} |
||||
|
|
||||
|
.topbar-user { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 12px; |
||||
|
font-weight: 700; |
||||
|
} |
||||
|
|
||||
|
.main-layout { |
||||
|
display: grid; |
||||
|
grid-template-columns: 220px 1fr; |
||||
|
gap: 18px; |
||||
|
} |
||||
|
|
||||
|
.sidebar { |
||||
|
padding: 18px; |
||||
|
display: grid; |
||||
|
gap: 10px; |
||||
|
align-content: start; |
||||
|
} |
||||
|
|
||||
|
.nav-item { |
||||
|
width: 100%; |
||||
|
text-align: left; |
||||
|
padding: 16px 18px; |
||||
|
border-radius: 18px; |
||||
|
background: transparent; |
||||
|
color: var(--text); |
||||
|
font-weight: 700; |
||||
|
} |
||||
|
|
||||
|
.nav-item.active, |
||||
|
.tab-item.active { |
||||
|
background: linear-gradient(135deg, rgba(15, 118, 110, 0.14), rgba(20, 184, 166, 0.24)); |
||||
|
color: #115e59; |
||||
|
} |
||||
|
|
||||
|
.content-area { |
||||
|
display: grid; |
||||
|
gap: 18px; |
||||
|
} |
||||
|
|
||||
|
.content-header, |
||||
|
.section-head { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.content-tabs { |
||||
|
display: flex; |
||||
|
gap: 10px; |
||||
|
} |
||||
|
|
||||
|
.tab-item { |
||||
|
min-width: 110px; |
||||
|
padding: 12px 18px; |
||||
|
border-radius: 14px; |
||||
|
background: rgba(255, 255, 255, 0.72); |
||||
|
font-weight: 700; |
||||
|
} |
||||
|
|
||||
|
.profile-grid, |
||||
|
.summary-grid { |
||||
|
display: grid; |
||||
|
grid-template-columns: repeat(2, minmax(0, 1fr)); |
||||
|
gap: 18px; |
||||
|
} |
||||
|
|
||||
|
.summary-grid { |
||||
|
grid-template-columns: repeat(3, minmax(0, 1fr)); |
||||
|
margin-bottom: 22px; |
||||
|
} |
||||
|
|
||||
|
.info-card, |
||||
|
.summary-card { |
||||
|
padding: 18px; |
||||
|
border-radius: 20px; |
||||
|
background: rgba(248, 250, 252, 0.96); |
||||
|
border: 1px solid rgba(226, 232, 240, 0.9); |
||||
|
} |
||||
|
|
||||
|
.summary-card strong, |
||||
|
.info-card strong { |
||||
|
display: block; |
||||
|
margin-bottom: 8px; |
||||
|
} |
||||
|
|
||||
|
.summary-card p, |
||||
|
.info-card p { |
||||
|
margin: 0; |
||||
|
font-size: 16px; |
||||
|
} |
||||
|
|
||||
|
.section-block+.section-block { |
||||
|
margin-top: 28px; |
||||
|
} |
||||
|
|
||||
|
.section-head { |
||||
|
margin-bottom: 18px; |
||||
|
} |
||||
|
|
||||
|
.event-table { |
||||
|
width: 100%; |
||||
|
border-collapse: collapse; |
||||
|
border-radius: 18px; |
||||
|
overflow: hidden; |
||||
|
background: rgba(255, 255, 255, 0.86); |
||||
|
} |
||||
|
|
||||
|
.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; |
||||
|
} |
||||
|
|
||||
|
.event-table th:last-child, |
||||
|
.event-table td:last-child { |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
.event-table th { |
||||
|
background: rgba(15, 118, 110, 0.08); |
||||
|
color: #115e59; |
||||
|
} |
||||
|
|
||||
|
.event-table tr:last-child td { |
||||
|
border-bottom: none; |
||||
|
} |
||||
|
|
||||
|
.action-btn { |
||||
|
padding: 10px 16px; |
||||
|
border-radius: 12px; |
||||
|
background: linear-gradient(135deg, var(--primary), var(--primary-light)); |
||||
|
color: #fff; |
||||
|
font-weight: 700; |
||||
|
} |
||||
|
|
||||
|
.action-btn[disabled] { |
||||
|
background: rgba(148, 163, 184, 0.6); |
||||
|
cursor: not-allowed; |
||||
|
} |
||||
|
|
||||
|
.empty-state { |
||||
|
padding: 32px 20px; |
||||
|
text-align: center; |
||||
|
border-radius: 22px; |
||||
|
background: rgba(248, 250, 252, 0.86); |
||||
|
border: 1px dashed rgba(148, 163, 184, 0.45); |
||||
|
} |
||||
|
|
||||
|
/* 分页样式 */ |
||||
|
.pagination { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
gap: 8px; |
||||
|
margin-top: 20px; |
||||
|
flex-wrap: wrap; |
||||
|
} |
||||
|
|
||||
|
.page-btn { |
||||
|
min-width: 36px; |
||||
|
height: 36px; |
||||
|
padding: 0 12px; |
||||
|
border: 1px solid var(--line); |
||||
|
border-radius: 8px; |
||||
|
background: rgba(255, 255, 255, 0.95); |
||||
|
color: var(--text); |
||||
|
font-size: 14px; |
||||
|
font-weight: 600; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.2s ease; |
||||
|
} |
||||
|
|
||||
|
.page-btn:hover:not(:disabled) { |
||||
|
background: rgba(15, 118, 110, 0.08); |
||||
|
border-color: var(--primary); |
||||
|
} |
||||
|
|
||||
|
.page-btn.active { |
||||
|
background: linear-gradient(135deg, var(--primary), var(--primary-light)); |
||||
|
color: #fff; |
||||
|
border-color: transparent; |
||||
|
} |
||||
|
|
||||
|
.page-btn:disabled { |
||||
|
opacity: 0.5; |
||||
|
cursor: not-allowed; |
||||
|
} |
||||
|
|
||||
|
.page-info { |
||||
|
text-align: center; |
||||
|
margin-top: 12px; |
||||
|
font-size: 14px; |
||||
|
color: var(--muted); |
||||
|
} |
||||
|
|
||||
|
@media (max-width: 960px) { |
||||
|
|
||||
|
.login-shell, |
||||
|
.register-shell, |
||||
|
.main-layout, |
||||
|
.profile-grid, |
||||
|
.summary-grid, |
||||
|
.two-columns { |
||||
|
grid-template-columns: 1fr; |
||||
|
} |
||||
|
|
||||
|
.topbar, |
||||
|
.content-header, |
||||
|
.section-head { |
||||
|
flex-direction: column; |
||||
|
align-items: flex-start; |
||||
|
gap: 14px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@media (max-width: 640px) { |
||||
|
|
||||
|
body, |
||||
|
.app-body { |
||||
|
padding: 14px; |
||||
|
} |
||||
|
|
||||
|
.form-actions { |
||||
|
flex-direction: column; |
||||
|
align-items: stretch; |
||||
|
} |
||||
|
|
||||
|
.event-table { |
||||
|
display: block; |
||||
|
overflow-x: auto; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,590 @@ |
|||||
|
document.addEventListener('DOMContentLoaded', function () { |
||||
|
var currentUserName = document.getElementById('currentUserName'); |
||||
|
var logoutBtn = document.getElementById('logoutBtn'); |
||||
|
var navItems = document.querySelectorAll('.nav-item'); |
||||
|
var sectionTitle = document.getElementById('sectionTitle'); |
||||
|
var sectionDesc = document.getElementById('sectionDesc'); |
||||
|
var subTabs = document.getElementById('subTabs'); |
||||
|
var profileView = document.getElementById('profileView'); |
||||
|
var eventsView = document.getElementById('eventsView'); |
||||
|
var adminView = document.getElementById('adminView'); |
||||
|
var registrationsView = document.getElementById('registrationsView'); |
||||
|
var adminNavBtn = document.getElementById('adminNavBtn'); |
||||
|
var registrationsNavBtn = document.getElementById('registrationsNavBtn'); |
||||
|
|
||||
|
var state = { |
||||
|
currentView: 'profile', |
||||
|
currentTab: 'all', |
||||
|
user: null, |
||||
|
events: [], |
||||
|
myEvents: [], |
||||
|
adminUsers: [], |
||||
|
adminRegistrations: [], |
||||
|
eventPage: 1, |
||||
|
myEventPage: 1, |
||||
|
registrationsPage: 1, |
||||
|
pageSize: 7 |
||||
|
}; |
||||
|
|
||||
|
function escapeHtml(value) { |
||||
|
return String(value == null ? '' : value) |
||||
|
.replace(/&/g, '&') |
||||
|
.replace(/</g, '<') |
||||
|
.replace(/>/g, '>') |
||||
|
.replace(/"/g, '"') |
||||
|
.replace(/'/g, '''); |
||||
|
} |
||||
|
|
||||
|
function buildOptions(list, current) { |
||||
|
return list.map(function (item) { |
||||
|
return '<option value="' + item + '"' + (item === current ? ' selected' : '') + '>' + item + '</option>'; |
||||
|
}).join(''); |
||||
|
} |
||||
|
|
||||
|
function buildCollegeOptions(current) { |
||||
|
var colleges = [ |
||||
|
'文学与文化传播学院', |
||||
|
'马克思主义学院', |
||||
|
'教育学院', |
||||
|
'外国语学院', |
||||
|
'历史文化学院', |
||||
|
'商学院', |
||||
|
'化学工程与技术学院', |
||||
|
'电子信息与电气工程学院', |
||||
|
'数学与统计学院', |
||||
|
'生物工程与技术学院', |
||||
|
'机电工程学院', |
||||
|
'土木工程学院', |
||||
|
'资源与环境工程学院', |
||||
|
'体育学院', |
||||
|
'美术与设计学院', |
||||
|
'音乐舞蹈学院', |
||||
|
'卫生健康学院', |
||||
|
'继续教育学院(培训中心)' |
||||
|
]; |
||||
|
return '<option value="">请选择学院</option>' + colleges.map(function (item) { |
||||
|
return '<option value="' + item + '"' + (item === current ? ' selected' : '') + '>' + item + '</option>'; |
||||
|
}).join(''); |
||||
|
} |
||||
|
|
||||
|
function getPaginatedList(list, page) { |
||||
|
var start = (page - 1) * state.pageSize; |
||||
|
var end = start + state.pageSize; |
||||
|
return list.slice(start, end); |
||||
|
} |
||||
|
|
||||
|
function getTotalPages(list) { |
||||
|
return Math.ceil(list.length / state.pageSize); |
||||
|
} |
||||
|
|
||||
|
function renderPagination(totalPages, currentPage, type) { |
||||
|
if (totalPages <= 1) { |
||||
|
return ''; |
||||
|
} |
||||
|
var html = '<div class="pagination">'; |
||||
|
html += '<button class="page-btn" data-page="prev" ' + (currentPage <= 1 ? 'disabled' : '') + '>上一页</button>'; |
||||
|
for (var i = 1; i <= totalPages; i++) { |
||||
|
html += '<button class="page-btn ' + (i === currentPage ? 'active' : '') + '" data-page="' + i + '">' + i + '</button>'; |
||||
|
} |
||||
|
html += '<button class="page-btn" data-page="next" ' + (currentPage >= totalPages ? 'disabled' : '') + '>下一页</button>'; |
||||
|
html += '</div>'; |
||||
|
return html; |
||||
|
} |
||||
|
|
||||
|
function renderTabs() { |
||||
|
var html = ''; |
||||
|
if (state.currentView === 'events') { |
||||
|
html = '' |
||||
|
+ '<button class="tab-item ' + (state.currentTab === 'all' ? 'active' : '') + '" data-tab="all">报名</button>' |
||||
|
+ '<button class="tab-item ' + (state.currentTab === 'mine' ? 'active' : '') + '" data-tab="mine">报名信息</button>'; |
||||
|
} else if (state.currentView === 'admin') { |
||||
|
html = '<button class="tab-item active" data-tab="users">用户信息</button>'; |
||||
|
} |
||||
|
subTabs.innerHTML = html; |
||||
|
subTabs.classList.toggle('hidden', !html); |
||||
|
Array.prototype.slice.call(subTabs.querySelectorAll('.tab-item')).forEach(function (item) { |
||||
|
item.addEventListener('click', function () { |
||||
|
state.currentTab = item.getAttribute('data-tab'); |
||||
|
renderTabs(); |
||||
|
renderCurrentView(); |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function renderProfile() { |
||||
|
if (!state.user) { |
||||
|
return; |
||||
|
} |
||||
|
profileView.innerHTML = '' |
||||
|
+ '<div class="section-block">' |
||||
|
+ ' <div class="section-head"><div><h3>基础资料</h3><p class="info-meta">支持在线更新姓名、电话、性别、学院和类别信息。</p></div></div>' |
||||
|
+ ' <div class="profile-grid">' |
||||
|
+ ' <div class="info-card"><strong>身份证号</strong><p>' + escapeHtml(state.user.idCard) + '</p></div>' |
||||
|
+ ' <div class="info-card"><strong>登录账号</strong><p>' + escapeHtml(state.user.username) + '</p></div>' |
||||
|
+ ' <div class="info-card"><strong>姓名</strong><p>' + escapeHtml(state.user.name) + '</p></div>' |
||||
|
+ ' <div class="info-card"><strong>联系电话</strong><p>' + escapeHtml(state.user.phone) + '</p></div>' |
||||
|
+ ' <div class="info-card"><strong>性别</strong><p>' + escapeHtml(state.user.gender) + '</p></div>' |
||||
|
+ ' <div class="info-card"><strong>学院</strong><p>' + escapeHtml(state.user.college) + '</p></div>' |
||||
|
+ ' <div class="info-card"><strong>类别</strong><p>' + escapeHtml(state.user.category) + '</p></div>' |
||||
|
+ ' </div>' |
||||
|
+ '</div>' |
||||
|
+ '<div class="section-block">' |
||||
|
+ ' <div class="section-head"><div><h3>修改信息</h3><p class="info-meta">请保持信息真实准确,方便赛事组织和通知。</p></div></div>' |
||||
|
+ ' <form id="profileForm" class="form-grid two-columns">' |
||||
|
+ ' <label class="field"><span>姓名</span><input name="name" value="' + escapeHtml(state.user.name) + '"></label>' |
||||
|
+ ' <label class="field"><span>电话</span><input name="phone" value="' + escapeHtml(state.user.phone) + '"></label>' |
||||
|
+ ' <label class="field"><span>性别</span><select name="gender">' + buildOptions(['男', '女'], state.user.gender) + '</select></label>' |
||||
|
+ ' <label class="field"><span>学院</span><select name="college">' + buildCollegeOptions(state.user.college) + '</select></label>' |
||||
|
+ ' <label class="field full-row"><span>类别</span><select name="category">' + buildOptions(['青年组', '老年组'], state.user.category) + '</select></label>' |
||||
|
+ ' <div class="form-actions full-row"><button type="submit" class="primary-btn">保存修改</button></div>' |
||||
|
+ ' <p id="profileMessage" class="form-message full-row"></p>' |
||||
|
+ ' </form>' |
||||
|
+ '</div>'; |
||||
|
|
||||
|
document.getElementById('profileForm').addEventListener('submit', function (event) { |
||||
|
event.preventDefault(); |
||||
|
var formData = new FormData(event.target); |
||||
|
appUtils.ajax({ |
||||
|
method: 'PUT', |
||||
|
url: '/api/users/me', |
||||
|
data: { |
||||
|
name: String(formData.get('name') || '').trim(), |
||||
|
phone: String(formData.get('phone') || '').trim(), |
||||
|
gender: String(formData.get('gender') || ''), |
||||
|
college: String(formData.get('college') || '').trim(), |
||||
|
category: String(formData.get('category') || '') |
||||
|
}, |
||||
|
success: function (response) { |
||||
|
if (!response.success) { |
||||
|
appUtils.showMessage(document.getElementById('profileMessage'), response.message || '保存失败', false); |
||||
|
return; |
||||
|
} |
||||
|
state.user = response.data; |
||||
|
currentUserName.textContent = state.user.name + (state.user.role === 'ADMIN' ? ' 管理员' : ' 老师'); |
||||
|
renderProfile(); |
||||
|
appUtils.showMessage(document.getElementById('profileMessage'), '个人信息已更新', true); |
||||
|
}, |
||||
|
error: function (xhr, response) { |
||||
|
appUtils.showMessage(document.getElementById('profileMessage'), (response && response.message) || '保存失败', false); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function renderEventTable(list, isMine) { |
||||
|
if (!list.length) { |
||||
|
return '<div class="empty-state"><h3>暂无数据</h3><p>' + (isMine ? '你还没有报名任何项目。' : '当前暂无可报名项目。') + '</p></div>'; |
||||
|
} |
||||
|
var currentPage = isMine ? state.myEventPage : state.eventPage; |
||||
|
var totalPages = getTotalPages(list); |
||||
|
var paginatedList = getPaginatedList(list, currentPage); |
||||
|
|
||||
|
var html = '' |
||||
|
+ '<table class="event-table">' |
||||
|
+ ' <thead><tr><th>项目名称</th><th>项目类别</th><th>比赛时间</th><th>比赛地点</th><th>报名情况</th><th>项目说明</th><th>操作</th></tr></thead>' |
||||
|
+ ' <tbody>' |
||||
|
+ paginatedList.map(function (item) { |
||||
|
var actionHtml = isMine |
||||
|
? '<button class="action-btn cancel-btn" data-id="' + item.id + '">取消报名</button>' |
||||
|
: '<button class="action-btn register-btn" data-id="' + item.id + '" ' + (item.registered ? 'disabled' : '') + '>' + (item.registered ? '已报名' : '报名') + '</button>'; |
||||
|
return '' |
||||
|
+ '<tr>' |
||||
|
+ '<td>' + escapeHtml(item.eventName) + '</td>' |
||||
|
+ '<td>' + escapeHtml(item.eventCategory) + '</td>' |
||||
|
+ '<td>' + escapeHtml(item.eventTime) + '</td>' |
||||
|
+ '<td>' + escapeHtml(item.location) + '</td>' |
||||
|
+ '<td>' + escapeHtml(item.registeredCount + '/' + item.quota) + '</td>' |
||||
|
+ '<td>' + escapeHtml(item.description) + '</td>' |
||||
|
+ '<td>' + actionHtml + '</td>' |
||||
|
+ '</tr>'; |
||||
|
}).join('') |
||||
|
+ ' </tbody>' |
||||
|
+ '</table>'; |
||||
|
|
||||
|
html += renderPagination(totalPages, currentPage, isMine); |
||||
|
html += '<div class="page-info">第 ' + currentPage + ' 页 / 共 ' + totalPages + ' 页,共 ' + list.length + ' 条记录</div>'; |
||||
|
return html; |
||||
|
} |
||||
|
|
||||
|
function renderUserTable() { |
||||
|
if (!state.adminUsers.length) { |
||||
|
return '<div class="empty-state"><h3>暂无用户</h3><p>当前系统还没有用户数据。</p></div>'; |
||||
|
} |
||||
|
return '' |
||||
|
+ '<table class="event-table">' |
||||
|
+ '<thead><tr><th>姓名</th><th>账号</th><th>身份证号</th><th>电话</th><th>性别</th><th>学院</th><th>类别</th><th>角色</th><th>操作</th></tr></thead>' |
||||
|
+ '<tbody>' |
||||
|
+ state.adminUsers.map(function (item) { |
||||
|
return '' |
||||
|
+ '<tr>' |
||||
|
+ '<td>' + escapeHtml(item.name) + '</td>' |
||||
|
+ '<td>' + escapeHtml(item.username) + '</td>' |
||||
|
+ '<td>' + escapeHtml(item.idCard) + '</td>' |
||||
|
+ '<td>' + escapeHtml(item.phone) + '</td>' |
||||
|
+ '<td>' + escapeHtml(item.gender) + '</td>' |
||||
|
+ '<td>' + escapeHtml(item.college) + '</td>' |
||||
|
+ '<td>' + escapeHtml(item.category) + '</td>' |
||||
|
+ '<td>' + escapeHtml(item.role === 'ADMIN' ? '管理员' : '普通用户') + '</td>' |
||||
|
+ '<td>' |
||||
|
+ '<button class="action-btn reset-btn" data-id="' + item.id + '">重置密码</button> ' |
||||
|
+ '<button class="action-btn delete-btn" data-id="' + item.id + '">删除账号</button>' |
||||
|
+ '</td>' |
||||
|
+ '</tr>'; |
||||
|
}).join('') |
||||
|
+ '</tbody></table>'; |
||||
|
} |
||||
|
|
||||
|
function bindUserActions() { |
||||
|
// 绑定重置密码按钮事件
|
||||
|
Array.prototype.slice.call(document.querySelectorAll('.reset-btn')).forEach(function (button) { |
||||
|
button.addEventListener('click', function () { |
||||
|
var userId = button.getAttribute('data-id'); |
||||
|
if (confirm('确定要重置该用户的密码吗?重置后密码将变为默认值。')) { |
||||
|
appUtils.ajax({ |
||||
|
method: 'POST', |
||||
|
url: '/api/admin/users/' + userId + '/reset-password', |
||||
|
success: function (response) { |
||||
|
if (response.success) { |
||||
|
alert('密码重置成功!'); |
||||
|
} else { |
||||
|
alert('密码重置失败:' + (response.message || '未知错误')); |
||||
|
} |
||||
|
}, |
||||
|
error: function (xhr, response) { |
||||
|
alert('密码重置失败:' + (response && response.message) || '网络异常'); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
// 绑定删除账号按钮事件
|
||||
|
Array.prototype.slice.call(document.querySelectorAll('.delete-btn')).forEach(function (button) { |
||||
|
button.addEventListener('click', function () { |
||||
|
var userId = button.getAttribute('data-id'); |
||||
|
if (confirm('确定要删除该账号吗?此操作不可恢复。')) { |
||||
|
appUtils.ajax({ |
||||
|
method: 'DELETE', |
||||
|
url: '/api/admin/users/' + userId, |
||||
|
success: function (response) { |
||||
|
if (response.success) { |
||||
|
alert('账号删除成功!'); |
||||
|
loadAdminData(); |
||||
|
} else { |
||||
|
alert('账号删除失败:' + (response.message || '未知错误')); |
||||
|
} |
||||
|
}, |
||||
|
error: function (xhr, response) { |
||||
|
alert('账号删除失败:' + (response && response.message) || '网络异常'); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function uniqueEventCount() { |
||||
|
var map = {}; |
||||
|
state.adminRegistrations.forEach(function (item) { |
||||
|
map[item.eventName] = true; |
||||
|
}); |
||||
|
return Object.keys(map).length; |
||||
|
} |
||||
|
|
||||
|
function renderRegistrationTable() { |
||||
|
var paginatedRegistrations = getPaginatedList(state.adminRegistrations, state.registrationsPage); |
||||
|
if (!paginatedRegistrations.length) { |
||||
|
return '<div class="empty-state"><h3>暂无报名记录</h3><p>目前还没有用户完成项目报名。</p></div>'; |
||||
|
} |
||||
|
return '' |
||||
|
+ '<table class="event-table">' |
||||
|
+ '<thead><tr><th>姓名</th><th>账号</th><th>电话</th><th>学院</th><th>类别</th><th>项目名称</th><th>项目类别</th><th>时间地点</th><th>状态</th><th>报名时间</th></tr></thead>' |
||||
|
+ '<tbody>' |
||||
|
+ paginatedRegistrations.map(function (item) { |
||||
|
return '' |
||||
|
+ '<tr>' |
||||
|
+ '<td>' + escapeHtml(item.studentName) + '</td>' |
||||
|
+ '<td>' + escapeHtml(item.username) + '</td>' |
||||
|
+ '<td>' + escapeHtml(item.phone) + '</td>' |
||||
|
+ '<td>' + escapeHtml(item.college) + '</td>' |
||||
|
+ '<td>' + escapeHtml(item.category) + '</td>' |
||||
|
+ '<td>' + escapeHtml(item.eventName) + '</td>' |
||||
|
+ '<td>' + escapeHtml(item.eventCategory) + '</td>' |
||||
|
+ '<td>' + escapeHtml(item.eventTime + ' / ' + item.location) + '</td>' |
||||
|
+ '<td>' + escapeHtml(item.status) + '</td>' |
||||
|
+ '<td>' + escapeHtml(item.createdAt) + '</td>' |
||||
|
+ '</tr>'; |
||||
|
}).join('') |
||||
|
+ '</tbody></table>'; |
||||
|
} |
||||
|
|
||||
|
function renderAdminContent() { |
||||
|
adminView.innerHTML = '' |
||||
|
+ '<div class="summary-grid">' |
||||
|
+ ' <div class="summary-card"><strong>用户总数</strong><p>' + state.adminUsers.length + '</p></div>' |
||||
|
+ ' <div class="summary-card"><strong>管理员数量</strong><p>' + state.adminUsers.filter(function (item) { return item.role === "ADMIN"; }).length + '</p></div>' |
||||
|
+ ' <div class="summary-card"><strong>普通用户数量</strong><p>' + state.adminUsers.filter(function (item) { return item.role !== "ADMIN"; }).length + '</p></div>' |
||||
|
+ '</div>' |
||||
|
+ renderUserTable(); |
||||
|
bindUserActions(); |
||||
|
} |
||||
|
|
||||
|
function bindRegisterButtons() { |
||||
|
// 绑定报名按钮事件
|
||||
|
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 () { |
||||
|
if (confirm('确定要取消报名吗?')) { |
||||
|
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 bindPaginationButtons(type) { |
||||
|
var totalPages, currentPage; |
||||
|
if (type === 'registrations') { |
||||
|
totalPages = getTotalPages(state.adminRegistrations); |
||||
|
currentPage = state.registrationsPage; |
||||
|
} else { |
||||
|
var isMine = type === 'mine'; |
||||
|
totalPages = getTotalPages(isMine ? state.myEvents : state.events); |
||||
|
currentPage = isMine ? state.myEventPage : state.eventPage; |
||||
|
} |
||||
|
|
||||
|
Array.prototype.slice.call(document.querySelectorAll('.pagination .page-btn')).forEach(function (button) { |
||||
|
button.addEventListener('click', function () { |
||||
|
var page = button.getAttribute('data-page'); |
||||
|
|
||||
|
if (page === 'prev') { |
||||
|
currentPage = Math.max(1, currentPage - 1); |
||||
|
} else if (page === 'next') { |
||||
|
currentPage = Math.min(totalPages, currentPage + 1); |
||||
|
} else { |
||||
|
currentPage = parseInt(page, 10); |
||||
|
} |
||||
|
|
||||
|
if (type === 'registrations') { |
||||
|
state.registrationsPage = currentPage; |
||||
|
var totalPages = getTotalPages(state.adminRegistrations); |
||||
|
registrationsView.innerHTML = '' |
||||
|
+ '<div class="summary-grid">' |
||||
|
+ ' <div class="summary-card"><strong>报名记录总数</strong><p>' + state.adminRegistrations.length + '</p></div>' |
||||
|
+ ' <div class="summary-card"><strong>已报名状态</strong><p>' + state.adminRegistrations.filter(function (item) { return item.status === "已报名"; }).length + '</p></div>' |
||||
|
+ ' <div class="summary-card"><strong>覆盖项目数</strong><p>' + uniqueEventCount() + '</p></div>' |
||||
|
+ '</div>' |
||||
|
+ renderRegistrationTable() |
||||
|
+ renderPagination(totalPages, state.registrationsPage, 'registrations'); |
||||
|
bindPaginationButtons('registrations'); |
||||
|
} else { |
||||
|
var isMine = type === 'mine'; |
||||
|
if (isMine) { |
||||
|
state.myEventPage = currentPage; |
||||
|
} else { |
||||
|
state.eventPage = currentPage; |
||||
|
} |
||||
|
|
||||
|
eventsView.innerHTML = renderEventTable(isMine ? state.myEvents : state.events, isMine); |
||||
|
bindRegisterButtons(); |
||||
|
bindPaginationButtons(type); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function renderCurrentView() { |
||||
|
profileView.classList.toggle('hidden', state.currentView !== 'profile'); |
||||
|
eventsView.classList.toggle('hidden', state.currentView !== 'events'); |
||||
|
adminView.classList.toggle('hidden', state.currentView !== 'admin'); |
||||
|
registrationsView.classList.toggle('hidden', state.currentView !== 'registrations'); |
||||
|
|
||||
|
if (state.currentView === 'profile') { |
||||
|
sectionTitle.textContent = '个人信息'; |
||||
|
sectionDesc.textContent = '查看并维护当前登录人员的基础资料。'; |
||||
|
renderTabs(); |
||||
|
renderProfile(); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (state.currentView === 'events') { |
||||
|
sectionTitle.textContent = '运动会报名'; |
||||
|
sectionDesc.textContent = '浏览所有项目并完成报名,也可以查看自己的报名信息。'; |
||||
|
renderTabs(); |
||||
|
eventsView.innerHTML = renderEventTable(state.currentTab === 'mine' ? state.myEvents : state.events, state.currentTab === 'mine'); |
||||
|
bindRegisterButtons(); |
||||
|
bindPaginationButtons(state.currentTab === 'mine' ? 'mine' : 'all'); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (state.currentView === 'admin') { |
||||
|
sectionTitle.textContent = '管理员后台'; |
||||
|
sectionDesc.textContent = '查看系统内所有用户资料。'; |
||||
|
renderTabs(); |
||||
|
renderAdminContent(); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (state.currentView === 'registrations') { |
||||
|
sectionTitle.textContent = '报名总览'; |
||||
|
sectionDesc.textContent = '查看所有用户的报名记录。'; |
||||
|
subTabs.classList.add('hidden'); |
||||
|
var totalPages = getTotalPages(state.adminRegistrations); |
||||
|
registrationsView.innerHTML = '' |
||||
|
+ '<div class="summary-grid">' |
||||
|
+ ' <div class="summary-card"><strong>报名记录总数</strong><p>' + state.adminRegistrations.length + '</p></div>' |
||||
|
+ ' <div class="summary-card"><strong>已报名状态</strong><p>' + state.adminRegistrations.filter(function (item) { return item.status === "已报名"; }).length + '</p></div>' |
||||
|
+ ' <div class="summary-card"><strong>覆盖项目数</strong><p>' + uniqueEventCount() + '</p></div>' |
||||
|
+ '</div>' |
||||
|
+ renderRegistrationTable() |
||||
|
+ renderPagination(totalPages, state.registrationsPage, 'registrations'); |
||||
|
bindPaginationButtons('registrations'); |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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(); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function switchView(view) { |
||||
|
state.currentView = view; |
||||
|
if (view === 'events') { |
||||
|
state.currentTab = state.currentTab === 'mine' ? 'mine' : 'all'; |
||||
|
} |
||||
|
navItems.forEach(function (item) { |
||||
|
item.classList.toggle('active', item.getAttribute('data-view') === view); |
||||
|
}); |
||||
|
renderCurrentView(); |
||||
|
} |
||||
|
|
||||
|
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' ? ' 管理员' : ' 同学'); |
||||
|
if (state.user.role === 'ADMIN') { |
||||
|
adminNavBtn.classList.remove('hidden'); |
||||
|
registrationsNavBtn.classList.remove('hidden'); |
||||
|
// 隐藏个人信息和运动会报名菜单
|
||||
|
document.querySelector('[data-view="profile"]').classList.add('hidden'); |
||||
|
document.querySelector('[data-view="events"]').classList.add('hidden'); |
||||
|
// 自动切换到管理员后台视图
|
||||
|
switchView('admin'); |
||||
|
} |
||||
|
renderProfile(); |
||||
|
loadEventData(); |
||||
|
loadAdminData(); |
||||
|
}, |
||||
|
error: function () { |
||||
|
window.location.href = './login.html'; |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
navItems.forEach(function (item) { |
||||
|
item.addEventListener('click', function () { |
||||
|
switchView(item.getAttribute('data-view')); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
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(); |
||||
|
switchView('profile'); |
||||
|
}); |
||||
@ -0,0 +1,71 @@ |
|||||
|
(function () { |
||||
|
function resolveApiBase() { |
||||
|
if (window.location.protocol === 'file:') { |
||||
|
return 'http://localhost:8080'; |
||||
|
} |
||||
|
return window.location.protocol + '//' + window.location.hostname + ':8080'; |
||||
|
} |
||||
|
|
||||
|
var API_BASE = resolveApiBase(); |
||||
|
|
||||
|
function buildUrl(url) { |
||||
|
if (/^https?:\/\//.test(url)) { |
||||
|
return url; |
||||
|
} |
||||
|
return API_BASE + url; |
||||
|
} |
||||
|
|
||||
|
function ajax(options) { |
||||
|
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'); |
||||
|
xhr.onreadystatechange = function () { |
||||
|
var response; |
||||
|
if (xhr.readyState !== 4) { |
||||
|
return; |
||||
|
} |
||||
|
try { |
||||
|
response = xhr.responseText ? JSON.parse(xhr.responseText) : {}; |
||||
|
} catch (error) { |
||||
|
response = { success: false, message: '响应数据解析失败' }; |
||||
|
} |
||||
|
if (xhr.status >= 200 && xhr.status < 300) { |
||||
|
options.success && options.success(response); |
||||
|
return; |
||||
|
} |
||||
|
options.error && options.error(xhr, response); |
||||
|
}; |
||||
|
xhr.onerror = function () { |
||||
|
options.error && options.error(xhr, { success: false, message: '网络异常,请确认后端已启动' }); |
||||
|
}; |
||||
|
xhr.send(options.data ? JSON.stringify(options.data) : null); |
||||
|
} |
||||
|
|
||||
|
function showMessage(element, message, isSuccess) { |
||||
|
if (!element) { |
||||
|
return; |
||||
|
} |
||||
|
element.textContent = message || ''; |
||||
|
element.className = 'form-message ' + (isSuccess ? 'success' : 'error'); |
||||
|
} |
||||
|
|
||||
|
function redirectIfLoggedIn() { |
||||
|
ajax({ |
||||
|
method: 'GET', |
||||
|
url: '/api/users/me', |
||||
|
success: function (response) { |
||||
|
if (response.success && response.data) { |
||||
|
window.location.href = './app.html'; |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
window.appUtils = { |
||||
|
ajax: ajax, |
||||
|
showMessage: showMessage, |
||||
|
redirectIfLoggedIn: redirectIfLoggedIn, |
||||
|
apiBase: API_BASE |
||||
|
}; |
||||
|
})(); |
||||
@ -0,0 +1,55 @@ |
|||||
|
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); |
||||
|
|
||||
|
goRegister.addEventListener('click', function () { |
||||
|
window.location.href = './register.html'; |
||||
|
}); |
||||
|
|
||||
|
form.addEventListener('submit', function (event) { |
||||
|
event.preventDefault(); |
||||
|
var formData = new FormData(form); |
||||
|
|
||||
|
appUtils.ajax({ |
||||
|
method: 'POST', |
||||
|
url: '/api/auth/login', |
||||
|
data: { |
||||
|
username: String(formData.get('username') || '').trim(), |
||||
|
password: String(formData.get('password') || '').trim() |
||||
|
}, |
||||
|
success: function (response) { |
||||
|
if (!response.success) { |
||||
|
appUtils.showMessage(message, response.message || '登录失败', false); |
||||
|
return; |
||||
|
} |
||||
|
appUtils.showMessage(message, '登录成功,正在进入系统...', true); |
||||
|
setTimeout(function () { |
||||
|
window.location.href = './app.html'; |
||||
|
}, 400); |
||||
|
}, |
||||
|
error: function (xhr, response) { |
||||
|
appUtils.showMessage(message, (response && response.message) || '网络异常,请确认后端已启动', false); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,45 @@ |
|||||
|
document.addEventListener('DOMContentLoaded', function () { |
||||
|
appUtils.redirectIfLoggedIn(); |
||||
|
|
||||
|
var form = document.getElementById('registerForm'); |
||||
|
var message = document.getElementById('registerMessage'); |
||||
|
var backLogin = document.getElementById('backLogin'); |
||||
|
|
||||
|
backLogin.addEventListener('click', function () { |
||||
|
window.location.href = './login.html'; |
||||
|
}); |
||||
|
|
||||
|
form.addEventListener('submit', function (event) { |
||||
|
event.preventDefault(); |
||||
|
var formData = new FormData(form); |
||||
|
|
||||
|
appUtils.ajax({ |
||||
|
method: 'POST', |
||||
|
url: '/api/auth/register', |
||||
|
data: { |
||||
|
idCard: String(formData.get('idCard') || '').trim(), |
||||
|
username: String(formData.get('username') || '').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') || ''), |
||||
|
college: String(formData.get('college') || '').trim(), |
||||
|
category: String(formData.get('category') || '') |
||||
|
}, |
||||
|
success: function (response) { |
||||
|
if (!response.success) { |
||||
|
appUtils.showMessage(message, response.message || '注册失败', false); |
||||
|
return; |
||||
|
} |
||||
|
appUtils.showMessage(message, '注册成功,正在进入系统...', true); |
||||
|
setTimeout(function () { |
||||
|
window.location.href = './app.html'; |
||||
|
}, 400); |
||||
|
}, |
||||
|
error: function (xhr, response) { |
||||
|
appUtils.showMessage(message, (response && response.message) || '网络异常,请确认后端已启动', false); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,53 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html lang="zh-CN"> |
||||
|
|
||||
|
<head> |
||||
|
<meta charset="UTF-8"> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
|
<title>运动会报名系统 - 登录</title> |
||||
|
<link rel="stylesheet" href="./assets/css/style.css"> |
||||
|
</head> |
||||
|
|
||||
|
<body class="auth-page"> |
||||
|
<div class="auth-shell login-shell"> |
||||
|
<section class="auth-hero"> |
||||
|
<div class="auth-badge">Campus Sports</div> |
||||
|
<h1>运动会报名系统</h1> |
||||
|
<p class="auth-subtitle">统一完成赛事报名、个人信息维护与报名记录查看,界面简洁,流程顺畅。</p> |
||||
|
<div class="notice-card"> |
||||
|
<h2>报名须知</h2> |
||||
|
<ul> |
||||
|
<li>请使用真实身份信息注册,身份证号在系统中唯一。</li> |
||||
|
<li>报名成功后可在“报名信息”中查看已选项目。</li> |
||||
|
<li>如项目名额已满,将无法继续报名该项目。</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
</section> |
||||
|
|
||||
|
<section class="auth-panel"> |
||||
|
<div class="panel-head"> |
||||
|
<h2>账号登录</h2> |
||||
|
<p>请输入账号和密码进入系统。</p> |
||||
|
</div> |
||||
|
<form id="loginForm" class="form-grid single-column" autocomplete="off"> |
||||
|
<label class="field"> |
||||
|
<span>登录账号</span> |
||||
|
<input type="text" name="username" placeholder="请输入登录账号" autocomplete="off"> |
||||
|
</label> |
||||
|
<label class="field"> |
||||
|
<span>登录密码</span> |
||||
|
<input type="password" name="password" placeholder="请输入登录密码" autocomplete="off"> |
||||
|
</label> |
||||
|
<div class="form-actions stacked"> |
||||
|
<button type="submit" class="primary-btn">登录</button> |
||||
|
<button type="button" class="ghost-btn" id="goRegister">注册</button> |
||||
|
</div> |
||||
|
<p id="loginMessage" class="form-message"></p> |
||||
|
</form> |
||||
|
</section> |
||||
|
</div> |
||||
|
<script src="./assets/js/common.js"></script> |
||||
|
<script src="./assets/js/login.js"></script> |
||||
|
</body> |
||||
|
|
||||
|
</html> |
||||
@ -0,0 +1,100 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html lang="zh-CN"> |
||||
|
|
||||
|
<head> |
||||
|
<meta charset="UTF-8"> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
|
<title>运动会报名系统 - 注册</title> |
||||
|
<link rel="stylesheet" href="./assets/css/style.css"> |
||||
|
</head> |
||||
|
|
||||
|
<body class="auth-page register-page"> |
||||
|
<div class="auth-shell register-shell"> |
||||
|
<section class="auth-panel wide-panel"> |
||||
|
<div class="panel-head"> |
||||
|
<h2>新用户注册</h2> |
||||
|
<p>请完整填写个人信息,注册成功后将自动登录。</p> |
||||
|
</div> |
||||
|
<form id="registerForm" class="form-grid two-columns"> |
||||
|
<label class="field"> |
||||
|
<span>身份证号</span> |
||||
|
<input type="text" name="idCard" placeholder="请输入身份证号"> |
||||
|
</label> |
||||
|
<label class="field"> |
||||
|
<span>登录账号</span> |
||||
|
<input type="text" name="username" placeholder="请输入登录账号"> |
||||
|
</label> |
||||
|
<label class="field"> |
||||
|
<span>密码</span> |
||||
|
<input type="password" name="password" placeholder="请输入密码"> |
||||
|
</label> |
||||
|
<label class="field"> |
||||
|
<span>确认密码</span> |
||||
|
<input type="password" name="confirmPassword" placeholder="请再次输入密码"> |
||||
|
</label> |
||||
|
<label class="field"> |
||||
|
<span>姓名</span> |
||||
|
<input type="text" name="name" placeholder="请输入姓名"> |
||||
|
</label> |
||||
|
<label class="field"> |
||||
|
<span>电话</span> |
||||
|
<input type="text" name="phone" placeholder="请输入联系电话"> |
||||
|
</label> |
||||
|
<label class="field"> |
||||
|
<span>性别</span> |
||||
|
<select name="gender"> |
||||
|
<option value="">请选择性别</option> |
||||
|
<option value="男">男</option> |
||||
|
<option value="女">女</option> |
||||
|
</select> |
||||
|
</label> |
||||
|
<label class="field"> |
||||
|
<span>学院</span> |
||||
|
<select name="college" required> |
||||
|
<option value="">请选择学院</option> |
||||
|
<option value="文学与文化传播学院">文学与文化传播学院</option> |
||||
|
<option value="马克思主义学院">马克思主义学院</option> |
||||
|
<option value="教育学院">教育学院</option> |
||||
|
<option value="外国语学院">外国语学院</option> |
||||
|
<option value="历史文化学院">历史文化学院</option> |
||||
|
<option value="商学院">商学院</option> |
||||
|
<option value="化学工程与技术学院">化学工程与技术学院</option> |
||||
|
<option value="电子信息与电气工程学院">电子信息与电气工程学院</option> |
||||
|
<option value="数学与统计学院">数学与统计学院</option> |
||||
|
<option value="生物工程与技术学院">生物工程与技术学院</option> |
||||
|
<option value="机电工程学院">机电工程学院</option> |
||||
|
<option value="土木工程学院">土木工程学院</option> |
||||
|
<option value="资源与环境工程学院">资源与环境工程学院</option> |
||||
|
<option value="体育学院">体育学院</option> |
||||
|
<option value="美术与设计学院">美术与设计学院</option> |
||||
|
<option value="音乐舞蹈学院">音乐舞蹈学院</option> |
||||
|
<option value="卫生健康学院">卫生健康学院</option> |
||||
|
<option value="继续教育学院(培训中心)">继续教育学院(培训中心)</option> |
||||
|
</select> |
||||
|
</label> |
||||
|
<label class="field full-row"> |
||||
|
<span>类别</span> |
||||
|
<select name="category"> |
||||
|
<option value="">请选择类别</option> |
||||
|
<option value="青年组">青年组</option> |
||||
|
<option value="老年组">老年组</option> |
||||
|
</select> |
||||
|
</label> |
||||
|
<div class="form-actions full-row"> |
||||
|
<button type="submit" class="primary-btn">提交注册</button> |
||||
|
<button type="button" class="ghost-btn" id="backLogin">返回登录</button> |
||||
|
</div> |
||||
|
<p id="registerMessage" class="form-message full-row"></p> |
||||
|
</form> |
||||
|
</section> |
||||
|
<section class="register-side-card"> |
||||
|
<div class="auth-badge">Join The Meet</div> |
||||
|
<h1>青春赛场,即刻出发</h1> |
||||
|
<p>注册后可在线报名项目、维护个人资料,并查看所有已报名赛事。</p> |
||||
|
</section> |
||||
|
</div> |
||||
|
<script src="./assets/js/common.js"></script> |
||||
|
<script src="./assets/js/register.js"></script> |
||||
|
</body> |
||||
|
|
||||
|
</html> |
||||
Loading…
Reference in new issue