|
|
|
|
@ -420,6 +420,60 @@
|
|
|
|
|
.notice-item button:hover {
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 自定义复选框样式 */
|
|
|
|
|
.custom-checkbox {
|
|
|
|
|
position: relative;
|
|
|
|
|
width: 20px;
|
|
|
|
|
height: 20px;
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.custom-checkbox input[type="checkbox"] {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
width: 0;
|
|
|
|
|
height: 0;
|
|
|
|
|
position: absolute;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.checkmark {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
width: 20px;
|
|
|
|
|
height: 20px;
|
|
|
|
|
background-color: #fff;
|
|
|
|
|
border: 1px solid #ccc;
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.custom-checkbox input[type="checkbox"]:checked ~ .checkmark {
|
|
|
|
|
background-color: #2c3e50;
|
|
|
|
|
border-color: #2c3e50;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.checkmark:after {
|
|
|
|
|
content: "";
|
|
|
|
|
position: absolute;
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.custom-checkbox input[type="checkbox"]:checked ~ .checkmark:after {
|
|
|
|
|
display: block;
|
|
|
|
|
left: 7px;
|
|
|
|
|
top: 3px;
|
|
|
|
|
width: 5px;
|
|
|
|
|
height: 10px;
|
|
|
|
|
border: solid white;
|
|
|
|
|
border-width: 0 2px 2px 0;
|
|
|
|
|
transform: rotate(45deg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 全选复选框样式 */
|
|
|
|
|
th .custom-checkbox .checkmark {
|
|
|
|
|
background-color: #f8f9fa;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
@ -487,7 +541,7 @@
|
|
|
|
|
<i class="fas fa-users"></i>
|
|
|
|
|
<h4>总学员数</h4>
|
|
|
|
|
<h2 id="total-trainees">0</h2>
|
|
|
|
|
<small class="text-success">
|
|
|
|
|
<small id="trainees-change" class="text-success">
|
|
|
|
|
<i class="fas fa-arrow-up"></i> 较上月增长5%
|
|
|
|
|
</small>
|
|
|
|
|
</div>
|
|
|
|
|
@ -497,7 +551,7 @@
|
|
|
|
|
<i class="fas fa-running"></i>
|
|
|
|
|
<h4>进行中的训练</h4>
|
|
|
|
|
<h2 id="active-trainings">0</h2>
|
|
|
|
|
<small class="text-primary">
|
|
|
|
|
<small id="trainings-pending" class="text-primary">
|
|
|
|
|
<i class="fas fa-clock"></i> 2个计划待审核
|
|
|
|
|
</small>
|
|
|
|
|
</div>
|
|
|
|
|
@ -507,7 +561,7 @@
|
|
|
|
|
<i class="fas fa-clipboard-list"></i>
|
|
|
|
|
<h4>待发布通知</h4>
|
|
|
|
|
<h2 id="pending-notices">0</h2>
|
|
|
|
|
<small class="text-warning">
|
|
|
|
|
<small id="urgent-notices" class="text-warning">
|
|
|
|
|
<i class="fas fa-exclamation-circle"></i> 3条紧急通知
|
|
|
|
|
</small>
|
|
|
|
|
</div>
|
|
|
|
|
@ -517,7 +571,7 @@
|
|
|
|
|
<i class="fas fa-trophy"></i>
|
|
|
|
|
<h4>本周最佳成绩</h4>
|
|
|
|
|
<h2 id="best-score">95</h2>
|
|
|
|
|
<small class="text-success">
|
|
|
|
|
<small id="score-level" class="text-success">
|
|
|
|
|
<i class="fas fa-star"></i> 优秀
|
|
|
|
|
</small>
|
|
|
|
|
</div>
|
|
|
|
|
@ -719,15 +773,21 @@
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th>
|
|
|
|
|
<input type="checkbox" class="form-check-input" id="selectAll">
|
|
|
|
|
<div class="custom-checkbox">
|
|
|
|
|
<input type="checkbox" id="selectAll">
|
|
|
|
|
<span class="checkmark"></span>
|
|
|
|
|
</div>
|
|
|
|
|
</th>
|
|
|
|
|
<th>学号</th>
|
|
|
|
|
<th>姓名</th>
|
|
|
|
|
<th>单位</th>
|
|
|
|
|
<th>状态</th>
|
|
|
|
|
<th>训练进度</th>
|
|
|
|
|
<th>体能成绩</th>
|
|
|
|
|
<th>战术成绩</th>
|
|
|
|
|
<th>射击成绩</th>
|
|
|
|
|
<th>总成绩</th>
|
|
|
|
|
<th>最近登录</th>
|
|
|
|
|
<th>等级</th>
|
|
|
|
|
<th>操作</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
@ -828,7 +888,10 @@
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th>
|
|
|
|
|
<input type="checkbox" class="form-check-input" id="selectAllTrainings">
|
|
|
|
|
<div class="custom-checkbox">
|
|
|
|
|
<input type="checkbox" id="selectAllTrainings">
|
|
|
|
|
<span class="checkmark"></span>
|
|
|
|
|
</div>
|
|
|
|
|
</th>
|
|
|
|
|
<th>训练名称</th>
|
|
|
|
|
<th>类型</th>
|
|
|
|
|
@ -872,66 +935,215 @@
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 添加/编辑成员模态框 -->
|
|
|
|
|
<div class="modal fade" id="addMemberModal" tabindex="-1">
|
|
|
|
|
<!-- 添加成员模态框 -->
|
|
|
|
|
<div class="modal fade" id="addMemberModal" tabindex="-1" aria-labelledby="addMemberModalLabel" aria-hidden="true">
|
|
|
|
|
<div class="modal-dialog">
|
|
|
|
|
<div class="modal-content">
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
<h5 class="modal-title" id="memberModalTitle">添加成员</h5>
|
|
|
|
|
<h5 class="modal-title" id="addMemberModalLabel">添加新成员</h5>
|
|
|
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
<form id="addMemberForm">
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
<label for="student_id" class="form-label">学号</label>
|
|
|
|
|
<input type="text" class="form-control" id="student_id" name="student_id" required minlength="6" maxlength="20">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
<label for="name" class="form-label">姓名</label>
|
|
|
|
|
<input type="text" class="form-control" id="name" name="name" required minlength="2" maxlength="50">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
<label for="unit" class="form-label">单位</label>
|
|
|
|
|
<input type="text" class="form-control" id="unit" name="unit" required maxlength="50">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
<label for="password" class="form-label">密码</label>
|
|
|
|
|
<input type="password" class="form-control" id="password" name="password" required minlength="6">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-footer">
|
|
|
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
|
|
|
|
<button type="submit" class="btn btn-primary">添加</button>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 查看成员详情模态框 -->
|
|
|
|
|
<div class="modal fade" id="viewMemberModal" tabindex="-1" aria-labelledby="viewMemberModalLabel" aria-hidden="true">
|
|
|
|
|
<div class="modal-dialog modal-lg">
|
|
|
|
|
<div class="modal-content">
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
<h5 class="modal-title" id="viewMemberModalLabel">成员详情</h5>
|
|
|
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
<div class="row">
|
|
|
|
|
<div class="col-md-6">
|
|
|
|
|
<p><strong>学号:</strong> <span id="view_student_id"></span></p>
|
|
|
|
|
<p><strong>姓名:</strong> <span id="view_name"></span></p>
|
|
|
|
|
<p><strong>单位:</strong> <span id="view_unit"></span></p>
|
|
|
|
|
<p><strong>状态:</strong> <span id="view_status"></span></p>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-md-6">
|
|
|
|
|
<p><strong>训练进度:</strong> <span id="view_progress"></span></p>
|
|
|
|
|
<p><strong>总成绩:</strong> <span id="view_score"></span></p>
|
|
|
|
|
<p><strong>成绩等级:</strong> <span id="view_score_level"></span></p>
|
|
|
|
|
<p><strong>创建时间:</strong> <span id="view_created_at"></span></p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="mt-4">
|
|
|
|
|
<h6>训练成绩详情</h6>
|
|
|
|
|
<div class="table-responsive">
|
|
|
|
|
<table class="table table-hover table-sm">
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th>训练类型</th>
|
|
|
|
|
<th>成绩</th>
|
|
|
|
|
<th>时间</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody id="memberScoresList">
|
|
|
|
|
<!-- 成绩数据将通过JavaScript动态加载 -->
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-footer">
|
|
|
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 已读名单模态框 -->
|
|
|
|
|
<div class="modal fade" id="readListModal" tabindex="-1">
|
|
|
|
|
<div class="modal-dialog">
|
|
|
|
|
<div class="modal-content">
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
<h5 class="modal-title">阅读情况</h5>
|
|
|
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
<form id="memberForm">
|
|
|
|
|
<ul class="nav nav-tabs" id="readListTabs">
|
|
|
|
|
<li class="nav-item">
|
|
|
|
|
<a class="nav-link active" data-bs-toggle="tab" href="#readTab">已读名单</a>
|
|
|
|
|
</li>
|
|
|
|
|
<li class="nav-item">
|
|
|
|
|
<a class="nav-link" data-bs-toggle="tab" href="#unreadTab">未读名单</a>
|
|
|
|
|
</li>
|
|
|
|
|
<li class="nav-item">
|
|
|
|
|
<a class="nav-link" data-bs-toggle="tab" href="#confirmedTab">已确认</a>
|
|
|
|
|
</li>
|
|
|
|
|
</ul>
|
|
|
|
|
<div class="tab-content mt-3">
|
|
|
|
|
<div class="tab-pane fade show active" id="readTab">
|
|
|
|
|
<div class="list-group" id="readList">
|
|
|
|
|
<!-- 已读名单将动态加载 -->
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="tab-pane fade" id="unreadTab">
|
|
|
|
|
<div class="list-group" id="unreadList">
|
|
|
|
|
<!-- 未读名单将动态加载 -->
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="tab-pane fade" id="confirmedTab">
|
|
|
|
|
<div class="list-group" id="confirmedList">
|
|
|
|
|
<!-- 已确认名单将动态加载 -->
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-footer">
|
|
|
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
|
|
|
|
<button type="button" class="btn btn-primary" id="exportReadList">
|
|
|
|
|
导出名单
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 编辑成员模态框 -->
|
|
|
|
|
<div class="modal fade" id="editMemberModal" tabindex="-1">
|
|
|
|
|
<div class="modal-dialog">
|
|
|
|
|
<div class="modal-content">
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
<h5 class="modal-title">编辑成员信息</h5>
|
|
|
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
<form id="editMemberForm">
|
|
|
|
|
<input type="hidden" id="edit-student-id">
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
<label class="form-label">学号</label>
|
|
|
|
|
<input type="text" class="form-control" name="student_id" required>
|
|
|
|
|
<label for="edit-name" class="form-label">姓名</label>
|
|
|
|
|
<input type="text" class="form-control" id="edit-name" required>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
<label class="form-label">姓名</label>
|
|
|
|
|
<input type="text" class="form-control" name="name" required>
|
|
|
|
|
<label for="edit-unit" class="form-label">单位</label>
|
|
|
|
|
<input type="text" class="form-control" id="edit-unit" required>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
<label class="form-label">单位</label>
|
|
|
|
|
<input type="text" class="form-control" name="unit" required>
|
|
|
|
|
<label for="edit-status" class="form-label">状态</label>
|
|
|
|
|
<select class="form-select" id="edit-status" required>
|
|
|
|
|
<option value="active">在训</option>
|
|
|
|
|
<option value="graduated">已结业</option>
|
|
|
|
|
<option value="suspended">暂停训练</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
<label class="form-label">密码</label>
|
|
|
|
|
<input type="password" class="form-control" name="password" required>
|
|
|
|
|
<div class="form-text">初始密码将用于成员首次登录</div>
|
|
|
|
|
<label for="edit-password" class="form-label">密码</label>
|
|
|
|
|
<input type="password" class="form-control" id="edit-password" placeholder="不修改请留空">
|
|
|
|
|
<small class="form-text text-muted">如不需要修改密码,请留空</small>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-footer">
|
|
|
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
|
|
|
|
<button type="button" class="btn btn-primary" id="saveMember">保存</button>
|
|
|
|
|
<button type="button" class="btn btn-primary" id="saveMemberEdit">保存</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 批量导入模态框 -->
|
|
|
|
|
<div class="modal fade" id="importModal" tabindex="-1">
|
|
|
|
|
<div class="modal fade" id="importMembersModal" tabindex="-1" aria-labelledby="importMembersModalLabel" aria-hidden="true">
|
|
|
|
|
<div class="modal-dialog">
|
|
|
|
|
<div class="modal-content">
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
<h5 class="modal-title">批量导入成员</h5>
|
|
|
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
|
|
|
<h5 class="modal-title" id="importMembersModalLabel">批量导入成员</h5>
|
|
|
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
<form id="importForm">
|
|
|
|
|
<form id="importMembersForm">
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
<label class="form-label">Excel文件</label>
|
|
|
|
|
<input type="file" class="form-control" accept=".xlsx,.xls" required>
|
|
|
|
|
<label for="importFile" class="form-label">选择Excel文件</label>
|
|
|
|
|
<input type="file" class="form-control" id="importFile" name="importFile" accept=".xlsx,.xls" required>
|
|
|
|
|
<div class="form-text">请上传包含学号、姓名、单位、密码的Excel文件</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="alert alert-info">
|
|
|
|
|
<i class="fas fa-info-circle"></i> 请下载模板文件,按照模板格式填写后上传
|
|
|
|
|
<a href="#" class="alert-link">下载模板</a>
|
|
|
|
|
<i class="fas fa-info-circle me-2"></i>
|
|
|
|
|
<span>Excel文件格式要求:</span>
|
|
|
|
|
<ul class="mb-0 mt-2">
|
|
|
|
|
<li>第一行为表头:学号、姓名、单位、密码</li>
|
|
|
|
|
<li>学号必须唯一且不能为空</li>
|
|
|
|
|
<li>姓名和单位不能为空</li>
|
|
|
|
|
<li>密码如果为空,将使用默认密码:123456</li>
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
<a href="#" id="downloadTemplate" class="btn btn-outline-primary btn-sm">
|
|
|
|
|
<i class="fas fa-download me-1"></i> 下载导入模板
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-footer">
|
|
|
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
|
|
|
|
<button type="button" class="btn btn-primary" id="startImport">开始导入</button>
|
|
|
|
|
<button type="submit" class="btn btn-primary">开始导入</button>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
@ -1210,567 +1422,11 @@
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 已读名单模态框 -->
|
|
|
|
|
<div class="modal fade" id="readListModal" tabindex="-1">
|
|
|
|
|
<div class="modal-dialog">
|
|
|
|
|
<div class="modal-content">
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
<h5 class="modal-title">阅读情况</h5>
|
|
|
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
<ul class="nav nav-tabs" id="readListTabs">
|
|
|
|
|
<li class="nav-item">
|
|
|
|
|
<a class="nav-link active" data-bs-toggle="tab" href="#readTab">已读名单</a>
|
|
|
|
|
</li>
|
|
|
|
|
<li class="nav-item">
|
|
|
|
|
<a class="nav-link" data-bs-toggle="tab" href="#unreadTab">未读名单</a>
|
|
|
|
|
</li>
|
|
|
|
|
<li class="nav-item">
|
|
|
|
|
<a class="nav-link" data-bs-toggle="tab" href="#confirmedTab">已确认</a>
|
|
|
|
|
</li>
|
|
|
|
|
</ul>
|
|
|
|
|
<div class="tab-content mt-3">
|
|
|
|
|
<div class="tab-pane fade show active" id="readTab">
|
|
|
|
|
<div class="list-group" id="readList">
|
|
|
|
|
<!-- 已读名单将动态加载 -->
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="tab-pane fade" id="unreadTab">
|
|
|
|
|
<div class="list-group" id="unreadList">
|
|
|
|
|
<!-- 未读名单将动态加载 -->
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="tab-pane fade" id="confirmedTab">
|
|
|
|
|
<div class="list-group" id="confirmedList">
|
|
|
|
|
<!-- 已确认名单将动态加载 -->
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-footer">
|
|
|
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
|
|
|
|
<button type="button" class="btn btn-primary" id="exportReadList">
|
|
|
|
|
导出名单
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
|
|
|
|
|
<script src="/static/js/member.js"></script>
|
|
|
|
|
<script src="/static/js/training.js"></script>
|
|
|
|
|
<script>
|
|
|
|
|
// 检查登录状态和角色
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
|
const token = localStorage.getItem('access_token');
|
|
|
|
|
const role = localStorage.getItem('user_role');
|
|
|
|
|
|
|
|
|
|
if (!token || role !== 'admin') {
|
|
|
|
|
window.location.href = 'login.html';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 设置当前日期
|
|
|
|
|
const now = new Date();
|
|
|
|
|
document.getElementById('current-date').textContent = now.toLocaleDateString('zh-CN', {
|
|
|
|
|
year: 'numeric',
|
|
|
|
|
month: 'long',
|
|
|
|
|
day: 'numeric',
|
|
|
|
|
weekday: 'long'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 侧边栏导航
|
|
|
|
|
document.querySelectorAll('.nav-link').forEach(link => {
|
|
|
|
|
link.addEventListener('click', function(e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
// 移除所有active类
|
|
|
|
|
document.querySelectorAll('.nav-link').forEach(l => l.classList.remove('active'));
|
|
|
|
|
|
|
|
|
|
// 添加active类到当前点击的链接
|
|
|
|
|
this.classList.add('active');
|
|
|
|
|
|
|
|
|
|
// 隐藏所有section
|
|
|
|
|
document.querySelectorAll('main > div').forEach(section => {
|
|
|
|
|
section.style.display = 'none';
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 显示对应的section
|
|
|
|
|
const sectionId = this.getAttribute('data-section');
|
|
|
|
|
document.getElementById(sectionId + '-section').style.display = 'block';
|
|
|
|
|
|
|
|
|
|
// 如果是通知管理页面,加载通知列表
|
|
|
|
|
if (sectionId === 'notices') {
|
|
|
|
|
loadNotices();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 退出登录
|
|
|
|
|
document.getElementById('logout-btn').addEventListener('click', function(e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
localStorage.removeItem('access_token');
|
|
|
|
|
localStorage.removeItem('user_role');
|
|
|
|
|
window.location.href = 'login.html';
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 初始化图表
|
|
|
|
|
initCharts();
|
|
|
|
|
|
|
|
|
|
// 初始化成员管理模块
|
|
|
|
|
initMemberManagement();
|
|
|
|
|
|
|
|
|
|
// 为保存通知按钮添加点击事件
|
|
|
|
|
document.addEventListener('click', function(e) {
|
|
|
|
|
// 保存通知按钮
|
|
|
|
|
if (e.target.id === 'saveNotice') {
|
|
|
|
|
handleSaveNotice(e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 编辑通知按钮
|
|
|
|
|
if (e.target.classList.contains('edit-notice')) {
|
|
|
|
|
const noticeId = e.target.getAttribute('data-notice-id');
|
|
|
|
|
editNotice(noticeId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 删除通知按钮
|
|
|
|
|
if (e.target.classList.contains('delete-notice')) {
|
|
|
|
|
const noticeId = e.target.getAttribute('data-notice-id');
|
|
|
|
|
deleteNotice(noticeId);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 当前编辑的通知ID
|
|
|
|
|
let currentEditId = null;
|
|
|
|
|
|
|
|
|
|
// 加载通知列表
|
|
|
|
|
async function loadNotices() {
|
|
|
|
|
try {
|
|
|
|
|
console.log("开始加载通知列表");
|
|
|
|
|
const token = localStorage.getItem('access_token');
|
|
|
|
|
|
|
|
|
|
// 调试令牌
|
|
|
|
|
console.log("使用的Token:", token ? token.substring(0, 20) + "..." : "无");
|
|
|
|
|
|
|
|
|
|
if (!token) {
|
|
|
|
|
console.error("未找到访问令牌,请重新登录");
|
|
|
|
|
alert("您的登录已过期,请重新登录");
|
|
|
|
|
window.location.href = 'login.html';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const response = await fetch('/api/notices?page=1&size=100', {
|
|
|
|
|
method: 'GET',
|
|
|
|
|
headers: {
|
|
|
|
|
'Authorization': `Bearer ${token}`,
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log("获取通知列表响应状态:", response.status);
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
// 尝试读取错误内容
|
|
|
|
|
let errorText = "";
|
|
|
|
|
try {
|
|
|
|
|
const errorData = await response.text();
|
|
|
|
|
errorText = errorData;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
errorText = "无法读取错误详情";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.error("响应内容:", errorText);
|
|
|
|
|
throw new Error(`获取通知列表失败,状态码:${response.status}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const notices = await response.json();
|
|
|
|
|
console.log("获取到通知数据:", notices);
|
|
|
|
|
|
|
|
|
|
const noticeList = document.getElementById('notice-list');
|
|
|
|
|
|
|
|
|
|
if (!noticeList) {
|
|
|
|
|
console.warn('找不到notice-list元素');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 生成通知列表HTML
|
|
|
|
|
noticeList.innerHTML = notices.length > 0 ?
|
|
|
|
|
notices.map(notice => {
|
|
|
|
|
// 检查通知对象及其属性
|
|
|
|
|
if (!notice) {
|
|
|
|
|
console.warn("收到空通知对象");
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log("处理通知:", notice);
|
|
|
|
|
|
|
|
|
|
let importanceClass = '';
|
|
|
|
|
switch (notice.importance) {
|
|
|
|
|
case 'high':
|
|
|
|
|
importanceClass = 'border-danger';
|
|
|
|
|
break;
|
|
|
|
|
case 'medium':
|
|
|
|
|
importanceClass = 'border-warning';
|
|
|
|
|
break;
|
|
|
|
|
case 'normal':
|
|
|
|
|
default:
|
|
|
|
|
importanceClass = 'border-info';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 格式化创建时间(安全处理)
|
|
|
|
|
let createTimeStr = "未知时间";
|
|
|
|
|
try {
|
|
|
|
|
if (notice.create_time) {
|
|
|
|
|
const createTime = new Date(notice.create_time);
|
|
|
|
|
if (!isNaN(createTime.getTime())) {
|
|
|
|
|
createTimeStr = createTime.toLocaleString('zh-CN', {
|
|
|
|
|
year: 'numeric',
|
|
|
|
|
month: '2-digit',
|
|
|
|
|
day: '2-digit',
|
|
|
|
|
hour: '2-digit',
|
|
|
|
|
minute: '2-digit'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn("日期格式化错误:", e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 安全获取属性,避免空值错误
|
|
|
|
|
const id = notice.id || 0;
|
|
|
|
|
const title = notice.title || "无标题";
|
|
|
|
|
const content = notice.content || "无内容";
|
|
|
|
|
const type = notice.type || "未知";
|
|
|
|
|
const isActive = notice.is_active !== undefined ? notice.is_active : false;
|
|
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
<div class="notice-item ${importanceClass} mb-3 p-3 border rounded shadow-sm" data-notice-id="${id}">
|
|
|
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
|
|
|
<div class="notice-title fw-bold fs-5">
|
|
|
|
|
${title}
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<span class="badge bg-${isActive ? 'success' : 'secondary'} me-2">
|
|
|
|
|
${isActive ? '已发布' : '未发布'}
|
|
|
|
|
</span>
|
|
|
|
|
<span class="badge bg-info">${type}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="notice-content my-2">${content}</div>
|
|
|
|
|
<div class="d-flex justify-content-between align-items-center mt-2">
|
|
|
|
|
<small class="text-muted">
|
|
|
|
|
发布时间:${createTimeStr}
|
|
|
|
|
</small>
|
|
|
|
|
<div>
|
|
|
|
|
<button class="btn btn-sm btn-outline-primary edit-notice"
|
|
|
|
|
data-notice-id="${id}">
|
|
|
|
|
编辑
|
|
|
|
|
</button>
|
|
|
|
|
<button class="btn btn-sm btn-outline-danger delete-notice"
|
|
|
|
|
data-notice-id="${id}">
|
|
|
|
|
删除
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}).join('') :
|
|
|
|
|
`<div class="alert alert-info">暂无通知</div>`;
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('加载通知列表失败:', error);
|
|
|
|
|
const noticeList = document.getElementById('notice-list');
|
|
|
|
|
if (noticeList) {
|
|
|
|
|
noticeList.innerHTML = `
|
|
|
|
|
<div class="alert alert-danger">
|
|
|
|
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
|
|
|
|
加载通知列表失败: ${error.message}
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理保存通知按钮点击
|
|
|
|
|
async function handleSaveNotice(e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
const form = document.getElementById('noticeForm');
|
|
|
|
|
if (!form) {
|
|
|
|
|
console.error('找不到通知表单');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 表单验证
|
|
|
|
|
if (!form.checkValidity()) {
|
|
|
|
|
form.reportValidity();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const noticeData = {
|
|
|
|
|
title: document.getElementById('noticeTitle')?.value || '',
|
|
|
|
|
type: document.getElementById('noticeType')?.value || '',
|
|
|
|
|
importance: document.getElementById('noticeImportance')?.value || 'normal',
|
|
|
|
|
content: document.getElementById('noticeContent')?.value || '',
|
|
|
|
|
is_active: true
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
console.log("通知数据:", noticeData);
|
|
|
|
|
|
|
|
|
|
// 验证重要性字段
|
|
|
|
|
if (!['high', 'medium', 'normal'].includes(noticeData.importance.toLowerCase())) {
|
|
|
|
|
alert('请选择有效的重要性级别');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let success = false;
|
|
|
|
|
|
|
|
|
|
if (currentEditId) {
|
|
|
|
|
// 更新通知
|
|
|
|
|
success = await updateNotice(currentEditId, noticeData);
|
|
|
|
|
} else {
|
|
|
|
|
// 创建新通知
|
|
|
|
|
success = await createNotice(noticeData);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (success) {
|
|
|
|
|
// 关闭模态框
|
|
|
|
|
const modalElement = document.getElementById('createNoticeModal');
|
|
|
|
|
if (modalElement) {
|
|
|
|
|
const modal = bootstrap.Modal.getInstance(modalElement);
|
|
|
|
|
if (modal) modal.hide();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 重置编辑状态
|
|
|
|
|
currentEditId = null;
|
|
|
|
|
|
|
|
|
|
// 重新加载通知列表
|
|
|
|
|
await loadNotices();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 创建通知
|
|
|
|
|
async function createNotice(noticeData) {
|
|
|
|
|
try {
|
|
|
|
|
console.log("创建通知:", noticeData);
|
|
|
|
|
const token = localStorage.getItem('access_token');
|
|
|
|
|
const response = await fetch('/api/admin/notices', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Authorization': `Bearer ${token}`,
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify(noticeData)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log("创建通知响应状态:", response.status);
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
let errorMessage = '创建通知失败';
|
|
|
|
|
try {
|
|
|
|
|
const errorData = await response.text();
|
|
|
|
|
console.error("错误详情:", errorData);
|
|
|
|
|
errorMessage += `: ${errorData}`;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error("无法解析错误详情");
|
|
|
|
|
}
|
|
|
|
|
throw new Error(errorMessage);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
console.log("创建通知成功:", result);
|
|
|
|
|
alert('通知创建成功!');
|
|
|
|
|
return true;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('创建通知失败:', error);
|
|
|
|
|
alert(`创建通知失败: ${error.message}`);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 编辑通知
|
|
|
|
|
async function editNotice(noticeId) {
|
|
|
|
|
try {
|
|
|
|
|
console.log("编辑通知:", noticeId);
|
|
|
|
|
const token = localStorage.getItem('access_token');
|
|
|
|
|
const response = await fetch(`/api/notices/${noticeId}`, {
|
|
|
|
|
headers: {
|
|
|
|
|
'Authorization': `Bearer ${token}`,
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error('获取通知详情失败');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const notice = await response.json();
|
|
|
|
|
|
|
|
|
|
// 记录当前编辑的通知ID
|
|
|
|
|
currentEditId = noticeId;
|
|
|
|
|
|
|
|
|
|
// 填充表单
|
|
|
|
|
const titleInput = document.getElementById('noticeTitle');
|
|
|
|
|
const typeSelect = document.getElementById('noticeType');
|
|
|
|
|
const importanceSelect = document.getElementById('noticeImportance');
|
|
|
|
|
const contentTextarea = document.getElementById('noticeContent');
|
|
|
|
|
|
|
|
|
|
if (titleInput) titleInput.value = notice.title || '';
|
|
|
|
|
if (typeSelect) typeSelect.value = notice.type || '';
|
|
|
|
|
if (importanceSelect) importanceSelect.value = notice.importance || '';
|
|
|
|
|
if (contentTextarea) contentTextarea.value = notice.content || '';
|
|
|
|
|
|
|
|
|
|
// 显示模态框
|
|
|
|
|
const modalElement = document.getElementById('createNoticeModal');
|
|
|
|
|
if (modalElement) {
|
|
|
|
|
const modal = new bootstrap.Modal(modalElement);
|
|
|
|
|
modal.show();
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('获取通知详情失败:', error);
|
|
|
|
|
alert('获取通知详情失败,请重试');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新通知
|
|
|
|
|
async function updateNotice(noticeId, noticeData) {
|
|
|
|
|
try {
|
|
|
|
|
console.log("更新通知:", noticeId, noticeData);
|
|
|
|
|
const token = localStorage.getItem('access_token');
|
|
|
|
|
const response = await fetch(`/api/admin/notices/${noticeId}`, {
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
headers: {
|
|
|
|
|
'Authorization': `Bearer ${token}`,
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify(noticeData)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error('更新通知失败');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
alert('通知更新成功!');
|
|
|
|
|
return true;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('更新通知失败:', error);
|
|
|
|
|
alert(`更新通知失败: ${error.message}`);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 删除通知
|
|
|
|
|
async function deleteNotice(noticeId) {
|
|
|
|
|
if (!confirm('确定要删除这条通知吗?')) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
console.log("删除通知:", noticeId);
|
|
|
|
|
const token = localStorage.getItem('access_token');
|
|
|
|
|
const response = await fetch(`/api/admin/notices/${noticeId}`, {
|
|
|
|
|
method: 'DELETE',
|
|
|
|
|
headers: {
|
|
|
|
|
'Authorization': `Bearer ${token}`,
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error('删除通知失败');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
alert('通知已删除!');
|
|
|
|
|
await loadNotices();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('删除通知失败:', error);
|
|
|
|
|
alert(`删除通知失败: ${error.message}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function initCharts() {
|
|
|
|
|
// 训练完成情况趋势图
|
|
|
|
|
const trainingTrendOptions = {
|
|
|
|
|
series: [{
|
|
|
|
|
name: '完成率',
|
|
|
|
|
data: [65, 75, 82, 78, 88, 92, 85]
|
|
|
|
|
}],
|
|
|
|
|
chart: {
|
|
|
|
|
height: 350,
|
|
|
|
|
type: 'line',
|
|
|
|
|
toolbar: {
|
|
|
|
|
show: false
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
stroke: {
|
|
|
|
|
curve: 'smooth',
|
|
|
|
|
width: 3
|
|
|
|
|
},
|
|
|
|
|
colors: ['#3498db'],
|
|
|
|
|
xaxis: {
|
|
|
|
|
categories: ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
|
|
|
|
|
},
|
|
|
|
|
yaxis: {
|
|
|
|
|
title: {
|
|
|
|
|
text: '完成率 (%)'
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
markers: {
|
|
|
|
|
size: 4,
|
|
|
|
|
colors: ['#3498db'],
|
|
|
|
|
strokeColors: '#fff',
|
|
|
|
|
strokeWidth: 2
|
|
|
|
|
},
|
|
|
|
|
tooltip: {
|
|
|
|
|
y: {
|
|
|
|
|
formatter: function(value) {
|
|
|
|
|
return value + '%';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const trainingTrendChart = new ApexCharts(
|
|
|
|
|
document.querySelector("#training-trend-chart"),
|
|
|
|
|
trainingTrendOptions
|
|
|
|
|
);
|
|
|
|
|
trainingTrendChart.render();
|
|
|
|
|
|
|
|
|
|
// 成绩分布图
|
|
|
|
|
const scoreDistributionOptions = {
|
|
|
|
|
series: [44, 55, 13, 43],
|
|
|
|
|
chart: {
|
|
|
|
|
type: 'donut',
|
|
|
|
|
height: 350
|
|
|
|
|
},
|
|
|
|
|
labels: ['优秀', '良好', '及格', '待提高'],
|
|
|
|
|
colors: ['#2ecc71', '#3498db', '#f1c40f', '#e74c3c'],
|
|
|
|
|
legend: {
|
|
|
|
|
position: 'bottom'
|
|
|
|
|
},
|
|
|
|
|
responsive: [{
|
|
|
|
|
breakpoint: 480,
|
|
|
|
|
options: {
|
|
|
|
|
chart: {
|
|
|
|
|
width: 200
|
|
|
|
|
},
|
|
|
|
|
legend: {
|
|
|
|
|
position: 'bottom'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}]
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const scoreDistributionChart = new ApexCharts(
|
|
|
|
|
document.querySelector("#score-distribution-chart"),
|
|
|
|
|
scoreDistributionOptions
|
|
|
|
|
);
|
|
|
|
|
scoreDistributionChart.render();
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/apexcharts@3.35.3/dist/apexcharts.min.js"></script>
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/file-saver@2.0.5/dist/FileSaver.min.js"></script>
|
|
|
|
|
<script src="js/admin.js"></script>
|
|
|
|
|
<script src="js/notice.js"></script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|