Browse Source

first commit

main
lyq0314 1 month ago
commit
e1ccdd140b
  1. 67
      .gitignore
  2. 107
      README.md
  3. 51
      app.html
  4. 522
      assets/css/style.css
  5. 590
      assets/js/app.js
  6. 71
      assets/js/common.js
  7. 55
      assets/js/login.js
  8. 45
      assets/js/register.js
  9. 53
      login.html
  10. 100
      register.html

67
.gitignore

@ -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

107
README.md

@ -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 服务已启动
- 修改配置文件后需要重启服务才能生效

51
app.html

@ -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>

522
assets/css/style.css

@ -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;
}
}

590
assets/js/app.js

@ -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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
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');
});

71
assets/js/common.js

@ -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
};
})();

55
assets/js/login.js

@ -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);
}
});
});
});

45
assets/js/register.js

@ -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);
}
});
});
});

53
login.html

@ -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>

100
register.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…
Cancel
Save