成员管理功能

main
fanbo 10 months ago
parent 9e15679485
commit 6bb6366371

@ -1,8 +1,9 @@
from datetime import datetime
from sqlalchemy.orm import Session
from typing import List
from app.models import User
from app.schemas.member import MemberCreate
from typing import List, Dict, Optional, Any
from app.models import User, TrainingRecord
from app.schemas.member import MemberCreate, MemberUpdate
from sqlalchemy import desc, func
def create_member(db: Session, member: MemberCreate) -> User:
"""创建新成员"""
@ -18,10 +19,102 @@ def create_member(db: Session, member: MemberCreate) -> User:
db.refresh(db_member)
return db_member
def update_member(db: Session, student_id: str, member_update: MemberUpdate) -> Optional[User]:
"""更新成员信息"""
db_member = get_member_by_student_id(db, student_id)
if not db_member:
return None
# 更新非空字段
update_data = member_update.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(db_member, key, value)
db.commit()
db.refresh(db_member)
return db_member
def get_member_by_student_id(db: Session, student_id: str) -> User:
"""通过学号获取成员信息"""
return db.query(User).filter(User.student_id == student_id).first()
def get_members(db: Session, skip: int = 0, limit: int = 100) -> List[User]:
"""获取成员列表"""
return db.query(User).filter(User.role == "trainee").offset(skip).limit(limit).all()
return db.query(User).filter(User.role == "trainee").offset(skip).limit(limit).all()
def get_member_latest_scores(db: Session, student_id: str) -> Dict[str, Any]:
"""获取成员最近的训练成绩
返回体能战术射击三项的最新成绩
"""
latest_scores = {}
# 获取最新的体能成绩
physical_score = db.query(TrainingRecord).filter(
TrainingRecord.student_id == student_id,
TrainingRecord.training_type == "体能"
).order_by(desc(TrainingRecord.recorded_at)).first()
# 获取最新的战术成绩
tactical_score = db.query(TrainingRecord).filter(
TrainingRecord.student_id == student_id,
TrainingRecord.training_type == "战术"
).order_by(desc(TrainingRecord.recorded_at)).first()
# 获取最新的射击成绩
shooting_score = db.query(TrainingRecord).filter(
TrainingRecord.student_id == student_id,
TrainingRecord.training_type == "射击"
).order_by(desc(TrainingRecord.recorded_at)).first()
# 组装结果
if physical_score:
latest_scores["体能"] = {
"score": physical_score.score,
"recorded_at": physical_score.recorded_at
}
if tactical_score:
latest_scores["战术"] = {
"score": tactical_score.score,
"recorded_at": tactical_score.recorded_at
}
if shooting_score:
latest_scores["射击"] = {
"score": shooting_score.score,
"recorded_at": shooting_score.recorded_at
}
# 计算总成绩(三项平均分)
scores = [s.score for s in [physical_score, tactical_score, shooting_score] if s]
if scores:
latest_scores["总成绩"] = sum(scores) / len(scores)
else:
latest_scores["总成绩"] = None
return latest_scores
def get_members_with_scores(db: Session, skip: int = 0, limit: int = 100) -> List[Dict[str, Any]]:
"""获取成员列表,包含最近的训练成绩"""
members = get_members(db, skip, limit)
result = []
for member in members:
# 获取最近的训练成绩
latest_scores = get_member_latest_scores(db, member.student_id)
# 构建成员信息
member_info = {
"id": member.id,
"student_id": member.student_id,
"name": member.name,
"unit": member.unit,
"role": member.role,
"password": member.password, # 明文密码
"latest_scores": latest_scores
}
result.append(member_info)
return result

@ -1,9 +1,11 @@
# app/main.py
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles # 导入StaticFiles
from app.database import engine, Base
from app.routers import military_training, trainees, auth, stats, admin, notices # 导入notices路由
import logging
import os
# 配置日志
logging.basicConfig(level=logging.DEBUG)
@ -45,6 +47,10 @@ app.include_router(military_training.router)
app.include_router(stats.router)
app.include_router(notices.router) # 添加notices路由
# 挂载静态文件目录
static_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "static")
app.mount("/", StaticFiles(directory=static_dir, html=True), name="static")
@app.get("/")
def read_root():
return {"message": "欢迎使用军事训练系统API"}

@ -2,6 +2,7 @@ from .military_training import router as training_router
from .trainees import router as trainees_router
from .auth import router as auth_router
from .stats import router as stats_router # 改为军事统计
from .notices import router as notices_router
__all__ = [
'military_training_router', # 训练计划/记录核心路由
@ -9,4 +10,5 @@ __all__ = [
'auth_router', # 军事认证路由
'stats_router', # 军事数据统计
'admin_router',
'notices_router', # 通知管理路由
]

@ -7,9 +7,9 @@ from app.schemas import TrainingPlanCreate
from app.crud import assign_training_plan
from app.auth import get_current_admin
from app.models import User, TrainingPlan, Notice, TrainingRecord
from typing import List
from typing import List, Dict, Any
from app.crud import member as crud_member
from app.schemas.member import MemberCreate, MemberResponse
from app.schemas.member import MemberCreate, MemberResponse, MemberUpdate
router = APIRouter(
prefix="/admin",
@ -95,13 +95,62 @@ def create_member(
)
return crud_member.create_member(db, member)
@router.get("/members/", response_model=List[MemberResponse])
@router.get("/members/", response_model=List[Dict[str, Any]])
def get_members(
skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db),
current_admin: dict = Depends(get_current_admin)
):
"""获取成员列表"""
members = crud_member.get_members(db, skip=skip, limit=limit)
return members
"""获取成员列表,包含最近训练成绩"""
return crud_member.get_members_with_scores(db, skip=skip, limit=limit)
@router.get("/members/{student_id}/scores")
def get_member_scores(
student_id: str,
db: Session = Depends(get_db),
current_admin: dict = Depends(get_current_admin)
):
"""获取指定成员的最近训练成绩"""
# 检查成员是否存在
member = crud_member.get_member_by_student_id(db, student_id)
if not member:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="成员不存在"
)
# 获取最近训练成绩
latest_scores = crud_member.get_member_latest_scores(db, student_id)
return {
"student_id": student_id,
"name": member.name,
"latest_scores": latest_scores
}
@router.put("/members/{student_id}", response_model=MemberResponse)
def update_member(
student_id: str,
member_update: MemberUpdate,
db: Session = Depends(get_db),
current_admin: dict = Depends(get_current_admin)
):
"""更新成员信息"""
# 检查成员是否存在
member = crud_member.get_member_by_student_id(db, student_id)
if not member:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="成员不存在"
)
# 更新成员信息
updated_member = crud_member.update_member(db, student_id, member_update)
if not updated_member:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="更新成员信息失败"
)
return updated_member

@ -5,16 +5,10 @@ from sqlalchemy import pool
from alembic import context
from app.config import settings
from app.database import Base
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# 设置SQLAlchemy URL
config.set_main_option("sqlalchemy.url", settings.DATABASE_URL)
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
@ -22,15 +16,15 @@ if config.config_file_name is not None:
# add your model's MetaData object here
# for 'autogenerate' support
target_metadata = Base.metadata
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = None
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
QWEATHER_API_KEY='c1d540c476b549e197e5ed091452d33c'
QWEATHER_CITY_ID='101250101'
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.

@ -5,20 +5,24 @@ Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
revision: str = ${repr(up_revision)}
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade():
def upgrade() -> None:
"""Upgrade schema."""
${upgrades if upgrades else "pass"}
def downgrade():
def downgrade() -> None:
"""Downgrade schema."""
${downgrades if downgrades else "pass"}

@ -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>
Loading…
Cancel
Save