Files
student_manage_system/frontend/index.html
T
2026-09-23 15:43:09 +08:00

2138 lines
79 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>学生管理系统</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary: #1E88E5;
--primary-dark: #1565C0;
--primary-light: #E3F2FD;
--bg: #F5F7FA;
--white: #ffffff;
--text: #333333;
--text-secondary: #666666;
--text-muted: #999999;
--border: #E0E0E0;
--success: #4CAF50;
--warning: #FF9800;
--danger: #F44336;
--shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
--shadow-hover: 0 8px 24px rgba(0, 0, 0, 0.12);
--radius: 8px;
--sidebar-width: 240px;
--header-height: 64px;
}
body {
font-family: 'PingFang SC', 'Microsoft YaHei', -apple-system, BlinkMacSystemFont, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
}
/* 侧边栏 */
.sidebar {
position: fixed;
left: 0;
top: 0;
width: var(--sidebar-width);
height: 100vh;
background: var(--white);
border-right: 1px solid var(--border);
padding: 20px 0;
z-index: 100;
}
.logo {
padding: 0 24px 24px;
font-size: 20px;
font-weight: 600;
color: var(--primary);
border-bottom: 1px solid var(--border);
margin-bottom: 16px;
}
.nav-item {
display: flex;
align-items: center;
padding: 14px 24px;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s;
text-decoration: none;
}
.nav-item:hover {
background: var(--primary-light);
color: var(--primary);
}
.nav-item.active {
background: var(--primary-light);
color: var(--primary);
border-left: 3px solid var(--primary);
padding-left: 21px;
}
.nav-icon {
width: 20px;
height: 20px;
margin-right: 12px;
opacity: 0.7;
}
.nav-item.active .nav-icon {
opacity: 1;
}
/* 主内容区 */
.main-content {
margin-left: var(--sidebar-width);
min-height: 100vh;
}
/* 顶部导航 */
.header {
height: var(--header-height);
background: var(--white);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
padding: 0 32px;
position: sticky;
top: 0;
z-index: 50;
}
.breadcrumb {
display: flex;
align-items: center;
gap: 8px;
color: var(--text-muted);
font-size: 14px;
}
.breadcrumb .current {
color: var(--text);
font-weight: 500;
}
.header-right {
margin-left: auto;
display: flex;
align-items: center;
gap: 16px;
}
.user-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: var(--primary);
color: var(--white);
display: flex;
align-items: center;
justify-content: center;
font-weight: 500;
cursor: pointer;
}
/* 页面内容 */
.page {
display: none;
padding: 32px;
animation: slideIn 0.3s ease;
}
.page.active {
display: block;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* 卡片 */
.card {
background: var(--white);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 24px;
transition: all 0.2s;
}
.card:hover {
box-shadow: var(--shadow-hover);
}
.card-click {
cursor: pointer;
transition: transform 0.15s;
}
.card-click:active {
transform: scale(0.95);
}
/* 首页 */
.welcome-banner {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color: var(--white);
padding: 32px;
border-radius: var(--radius);
margin-bottom: 24px;
}
.welcome-banner h1 {
font-size: 28px;
margin-bottom: 8px;
}
.welcome-banner .streak {
font-size: 14px;
opacity: 0.9;
}
.quick-actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 16px;
margin-bottom: 32px;
}
.quick-card {
background: var(--white);
border-radius: var(--radius);
padding: 24px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
box-shadow: var(--shadow);
}
.quick-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-hover);
}
.quick-card:active {
transform: scale(0.95);
}
.quick-card .icon {
font-size: 32px;
margin-bottom: 12px;
}
.quick-card .label {
font-size: 14px;
color: var(--text-secondary);
}
.news-list {
background: var(--white);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: hidden;
}
.news-header {
padding: 16px 24px;
border-bottom: 1px solid var(--border);
font-weight: 500;
}
.news-item {
padding: 16px 24px;
border-bottom: 1px solid var(--border);
cursor: pointer;
transition: background 0.2s;
}
.news-item:last-child {
border-bottom: none;
}
.news-item:hover {
background: var(--primary-light);
}
.news-item .time {
font-size: 12px;
color: var(--text-muted);
margin-bottom: 4px;
}
.news-item .title {
font-size: 14px;
color: var(--text);
}
/* 表格 */
.table-container {
background: var(--white);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: hidden;
}
.table-header {
padding: 20px 24px;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
}
.table-title {
font-size: 16px;
font-weight: 500;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 14px 24px;
text-align: left;
border-bottom: 1px solid var(--border);
}
th {
background: var(--bg);
font-weight: 500;
color: var(--text-secondary);
cursor: pointer;
user-select: none;
}
th:hover {
background: #E8EBF0;
}
tr:nth-child(even) {
background: #FAFBFC;
}
tr:hover {
background: var(--primary-light);
}
/* 按钮 */
.btn {
padding: 8px 20px;
border-radius: 6px;
border: none;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 6px;
}
.btn-primary {
background: var(--primary);
color: var(--white);
}
.btn-primary:hover {
background: var(--primary-dark);
}
.btn-outline {
background: transparent;
color: var(--primary);
border: 1px solid var(--primary);
}
.btn-outline:hover {
background: var(--primary-light);
}
.btn-danger {
background: var(--danger);
color: var(--white);
}
.btn-danger:hover {
background: #D32F2F;
}
.btn-sm {
padding: 6px 12px;
font-size: 12px;
}
/* 表单 */
.form-group {
margin-bottom: 16px;
}
.form-label {
display: block;
margin-bottom: 6px;
font-size: 14px;
color: var(--text-secondary);
}
.form-input {
width: 100%;
padding: 10px 14px;
border: 1px solid var(--border);
border-radius: 6px;
font-size: 14px;
transition: border-color 0.2s;
}
.form-input:focus {
outline: none;
border-color: var(--primary);
}
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
/* 模态框 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: all 0.3s;
}
.modal-overlay.active {
opacity: 1;
visibility: visible;
}
.modal {
background: var(--white);
border-radius: var(--radius);
padding: 24px;
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
transform: scale(0.9);
transition: transform 0.3s;
}
.modal-overlay.active .modal {
transform: scale(1);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.modal-title {
font-size: 18px;
font-weight: 500;
}
.modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: var(--text-muted);
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 20px;
}
/* Toast */
.toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 2000;
display: flex;
flex-direction: column;
gap: 10px;
}
.toast {
background: var(--white);
border-radius: 6px;
padding: 14px 20px;
box-shadow: var(--shadow-hover);
display: flex;
align-items: center;
gap: 10px;
animation: toastIn 0.3s ease;
min-width: 280px;
}
.toast.success {
border-left: 4px solid var(--success);
}
.toast.error {
border-left: 4px solid var(--danger);
}
.toast.info {
border-left: 4px solid var(--primary);
}
@keyframes toastIn {
from {
opacity: 0;
transform: translateX(100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* 标签 */
.tag {
display: inline-block;
padding: 4px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.tag-success {
background: #E8F5E9;
color: var(--success);
}
.tag-warning {
background: #FFF3E0;
color: var(--warning);
}
.tag-danger {
background: #FFEBEE;
color: var(--danger);
}
.tag-primary {
background: var(--primary-light);
color: var(--primary);
}
/* 搜索栏 */
.search-bar {
display: flex;
gap: 12px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.search-input {
flex: 1;
min-width: 200px;
padding: 10px 14px;
border: 1px solid var(--border);
border-radius: 6px;
font-size: 14px;
}
.search-input:focus {
outline: none;
border-color: var(--primary);
}
/* 分页 */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
padding: 20px;
}
.page-btn {
padding: 8px 14px;
border: 1px solid var(--border);
background: var(--white);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.page-btn:hover {
background: var(--primary-light);
border-color: var(--primary);
color: var(--primary);
}
.page-btn.active {
background: var(--primary);
border-color: var(--primary);
color: var(--white);
}
/* 成绩卡片 */
.score-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
margin-top: 20px;
}
.score-card {
background: var(--white);
border-radius: var(--radius);
padding: 20px;
box-shadow: var(--shadow);
transition: all 0.2s;
}
.score-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-hover);
}
.score-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.score-card-title {
font-weight: 500;
}
.score-card-value {
font-size: 24px;
font-weight: 600;
color: var(--primary);
}
.score-card-value.pass {
color: var(--success);
}
.score-card-value.fail {
color: var(--danger);
}
/* 图表容器 */
.chart-container {
background: var(--white);
border-radius: var(--radius);
padding: 24px;
box-shadow: var(--shadow);
margin-top: 20px;
}
.chart-title {
font-size: 16px;
font-weight: 500;
margin-bottom: 16px;
}
/* 统计卡片 */
.stats-overview {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
margin-top: 20px;
}
.stat-card {
background: var(--white);
border-radius: var(--radius);
padding: 20px;
box-shadow: var(--shadow);
transition: all 0.2s;
display: flex;
flex-direction: column;
gap: 12px;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-hover);
}
.stat-card-primary { border-left: 4px solid var(--primary); }
.stat-card-success { border-left: 4px solid var(--success); }
.stat-card-warning { border-left: 4px solid var(--warning); }
.stat-card-info { border-left: 4px solid var(--info); }
.stat-icon {
font-size: 32px;
}
.stat-content h3 {
margin: 0 0 4px 0;
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
}
.stat-content p {
margin: 0;
font-size: 13px;
color: var(--text-secondary);
}
.stat-content select {
margin-top: 8px;
padding: 8px 12px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--white);
color: var(--text-primary);
font-size: 14px;
width: 100%;
}
.stat-data {
margin-top: 8px;
padding: 12px;
background: var(--bg-secondary);
border-radius: var(--radius-sm);
font-size: 13px;
color: var(--text-secondary);
max-height: 150px;
overflow-y: auto;
}
.stat-data div {
padding: 4px 0;
border-bottom: 1px solid var(--border);
}
.stat-data div:last-child {
border-bottom: none;
}
/* 个人中心 */
.profile-header {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color: var(--white);
padding: 40px 32px;
border-radius: var(--radius);
margin-bottom: 24px;
}
.profile-info {
display: flex;
align-items: center;
gap: 24px;
}
.profile-avatar {
width: 80px;
height: 80px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
font-weight: 600;
}
.profile-details h2 {
font-size: 24px;
margin-bottom: 8px;
}
.profile-details p {
opacity: 0.9;
font-size: 14px;
}
.profile-menu {
background: var(--white);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: hidden;
}
.profile-menu-item {
display: flex;
align-items: center;
padding: 16px 24px;
border-bottom: 1px solid var(--border);
cursor: pointer;
transition: background 0.2s;
}
.profile-menu-item:last-child {
border-bottom: none;
}
.profile-menu-item:hover {
background: var(--primary-light);
}
.profile-menu-item .icon {
width: 24px;
height: 24px;
margin-right: 16px;
opacity: 0.7;
}
.profile-menu-item .label {
flex: 1;
font-size: 14px;
}
.profile-menu-item .arrow {
color: var(--text-muted);
}
/* 响应式 */
@media (max-width: 768px) {
.sidebar {
width: 60px;
}
.sidebar .logo,
.sidebar .nav-text {
display: none;
}
.nav-item {
justify-content: center;
padding: 14px 0;
}
.nav-icon {
margin-right: 0;
}
.main-content {
margin-left: 60px;
}
.page {
padding: 16px;
}
}
</style>
</head>
<body>
<!-- 侧边栏 -->
<aside class="sidebar">
<div class="logo">📚 学生管理系统</div>
<nav>
<a class="nav-item active" data-page="home">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"></path>
<polyline points="9 22 9 12 15 12 15 22"></polyline>
</svg>
<span class="nav-text">首页</span>
</a>
<a class="nav-item" data-page="students">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
<path d="M23 21v-2a4 4 0 00-4-4H-3"></path>
<circle cx="17" cy="7" r="4"></circle>
</svg>
<span class="nav-text">学生管理</span>
</a>
<a class="nav-item" data-page="scores">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 11l3 3L22 4"></path>
<path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"></path>
</svg>
<span class="nav-text">成绩查询</span>
</a>
<a class="nav-item" data-page="teachers">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
<span class="nav-text">教师管理</span>
</a>
<a class="nav-item" data-page="employment">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="7" width="20" height="14" rx="2" ry="2"></rect>
<path d="M16 21V5a2 2 0 00-2-2h-4a2 2 0 00-2 2v16"></path>
</svg>
<span class="nav-text">就业管理</span>
</a>
<a class="nav-item" data-page="stats">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="20" x2="18" y2="10"></line>
<line x1="12" y1="20" x2="12" y2="4"></line>
<line x1="6" y1="20" x2="6" y2="14"></line>
</svg>
<span class="nav-text">统计分析</span>
</a>
<a class="nav-item" data-page="profile">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<path d="M12 16v-4"></path>
<path d="M12 8h.01"></path>
</svg>
<span class="nav-text">个人中心</span>
</a>
</nav>
</aside>
<!-- 主内容区 -->
<main class="main-content">
<!-- 顶部导航 -->
<header class="header">
<div class="breadcrumb">
<span>首页</span>
<span>/</span>
<span class="current" id="breadcrumb-current">控制台</span>
</div>
<div class="header-right">
<div class="user-avatar">学</div>
</div>
</header>
<!-- 首页 -->
<div class="page active" id="page-home">
<div class="welcome-banner">
<h1>Hi,同学 👋</h1>
<p class="streak">已连续学习 <strong id="streak-days">3</strong> 天 | 今日是第 3 个学习日</p>
</div>
<div class="quick-actions">
<div class="quick-card card-click" onclick="navigateTo('scores')">
<div class="icon">📊</div>
<div class="label">查课表</div>
</div>
<div class="quick-card card-click" onclick="navigateTo('scores')">
<div class="icon">📝</div>
<div class="label">查成绩</div>
</div>
<div class="quick-card card-click" onclick="navigateTo('employment')">
<div class="icon">💳</div>
<div class="label">校园卡</div>
</div>
<div class="quick-card card-click" onclick="showToast('报修功能开发中', 'info')">
<div class="icon">🔧</div>
<div class="label">报修</div>
</div>
</div>
<div class="news-list">
<div class="news-header">📢 校园动态</div>
<div class="news-item">
<div class="time">2026-09-22 10:00</div>
<div class="title">【教务处】2026年秋季学期期末考试安排通知</div>
</div>
<div class="news-item">
<div class="time">2026-09-21 15:30</div>
<div class="title">【就业中心】秋季招聘会即将举行,欢迎投递简历</div>
</div>
<div class="news-item">
<div class="time">2026-09-20 09:00</div>
<div class="title">【图书馆】延长开放时间至24:00</div>
</div>
<div class="news-item">
<div class="time">2026-09-19 14:00</div>
<div class="title">【学术报告】人工智能前沿技术讲座</div>
</div>
</div>
</div>
<!-- 学生管理 -->
<div class="page" id="page-students">
<div class="table-container">
<div class="table-header">
<span class="table-title">学生列表</span>
<button class="btn btn-primary" onclick="openModal('student-modal')">+ 新增学生</button>
</div>
<div class="search-bar">
<input type="text" class="search-input" id="student-search" placeholder="搜索学生姓名、学号..." onkeyup="searchStudents()">
<select class="search-input" style="flex: 0 0 150px" id="student-class-filter" onchange="searchStudents()">
<option value="">全部班级</option>
<option value="1">计算机科学2401班</option>
<option value="2">计算机科学2402班</option>
<option value="3">人工智能2501班</option>
</select>
</div>
<table>
<thead>
<tr>
<th onclick="sortTable('students-table', 0)">学号 ↕</th>
<th onclick="sortTable('students-table', 1)">姓名 ↕</th>
<th onclick="sortTable('students-table', 2)">性别 ↕</th>
<th onclick="sortTable('students-table', 3)">班级 ↕</th>
<th onclick="sortTable('students-table', 4)">专业 ↕</th>
<th onclick="sortTable('students-table', 5)">手机号 ↕</th>
<th>操作</th>
</tr>
</thead>
<tbody id="students-tbody">
<!-- 动态生成 -->
</tbody>
</table>
<div class="pagination">
<button class="page-btn" onclick="changePage('students', -1)">上一页</button>
<span id="students-page-info">第 1 页 / 共 1 页</span>
<button class="page-btn" onclick="changePage('students', 1)">下一页</button>
</div>
</div>
</div>
<!-- 成绩查询 -->
<div class="page" id="page-scores">
<div class="search-bar">
<select class="search-input" style="flex: 0 0 150px" id="score-student-filter" onchange="loadScores()">
<option value="">选择学生</option>
</select>
<select class="search-input" style="flex: 0 0 150px" id="score-term-filter" onchange="loadScores()">
<option value="1">第一学期</option>
<option value="2">第二学期</option>
</select>
<button class="btn btn-primary" onclick="loadScores()">查询</button>
</div>
<div class="score-cards" id="score-cards">
<!-- 动态生成 -->
</div>
<div class="chart-container">
<div class="chart-title">成绩趋势图</div>
<canvas id="score-chart" height="100"></canvas>
</div>
</div>
<!-- 教师管理 -->
<div class="page" id="page-teachers">
<div class="table-container">
<div class="table-header">
<span class="table-title">教师列表</span>
<button class="btn btn-primary" onclick="openModal('teacher-modal')">+ 新增教师</button>
</div>
<div class="search-bar">
<input type="text" class="search-input" id="teacher-search" placeholder="搜索教师姓名..." onkeyup="searchTeachers()">
</div>
<table>
<thead>
<tr>
<th>教师ID</th>
<th>姓名</th>
<th>性别</th>
<th>擅长领域</th>
<th>学历</th>
<th>毕业院校</th>
<th>操作</th>
</tr>
</thead>
<tbody id="teachers-tbody">
<!-- 动态生成 -->
</tbody>
</table>
</div>
</div>
<!-- 就业管理 -->
<div class="page" id="page-employment">
<div class="table-container">
<div class="table-header">
<span class="table-title">就业信息</span>
<button class="btn btn-primary" onclick="openModal('employment-modal')">+ 新增就业信息</button>
</div>
<div class="search-bar">
<input type="text" class="search-input" id="employment-search" placeholder="搜索学生姓名或公司..." onkeyup="searchEmployment()">
</div>
<table>
<thead>
<tr>
<th>学生姓名</th>
<th>班级</th>
<th>公司</th>
<th>薪资</th>
<th>offer下发时间</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody id="employment-tbody">
<!-- 动态生成 -->
</tbody>
</table>
</div>
</div>
<!-- 统计分析 -->
<div class="page" id="page-stats">
<h2 class="page-title">统计分析</h2>
<div class="stats-overview">
<div class="stat-card stat-card-primary">
<div class="stat-icon">👥</div>
<div class="stat-content">
<h3>班级人数统计</h3>
<select id="stat-class-type" onchange="loadClassStats()">
<option value="班级人数">按班级统计</option>
<option value="男女人数">按性别统计</option>
<option value="班级中男女人数">班级性别分布</option>
</select>
</div>
<div id="class-stats-result" class="stat-data"></div>
</div>
<div class="stat-card stat-card-success">
<div class="stat-icon">📊</div>
<div class="stat-content">
<h3>优秀成绩学生</h3>
<p>所有考试都在80分以上</p>
</div>
<button class="btn btn-success" onclick="loadExcellentStats()">查询</button>
<div id="excellent-stats-result" class="stat-data"></div>
</div>
<div class="stat-card stat-card-warning">
<div class="stat-icon">⚠️</div>
<div class="stat-content">
<h3>不及格学生查询</h3>
<p>两次以上不及格</p>
</div>
<button class="btn btn-warning" onclick="loadFailStats()">查询</button>
<div id="fail-stats-result" class="stat-data"></div>
</div>
<div class="stat-card stat-card-info">
<div class="stat-icon">📈</div>
<div class="stat-content">
<h3>班级平均分</h3>
<p>每次考试各班级平均分</p>
</div>
<button class="btn btn-info" onclick="loadAvgStats()">查询</button>
<div id="avg-stats-result" class="stat-data"></div>
</div>
<div class="stat-card stat-card-primary">
<div class="stat-icon">🎓</div>
<div class="stat-content">
<h3>就业薪资Top5</h3>
<p>薪资最高的前5名学生</p>
</div>
<button class="btn btn-primary" onclick="loadTopStats()">查询</button>
<div id="top-stats-result" class="stat-data"></div>
</div>
<div class="stat-card stat-card-success">
<div class="stat-icon">💼</div>
<div class="stat-content">
<h3>就业时长统计</h3>
<p>每位学生的就业时长</p>
</div>
<button class="btn btn-success" onclick="loadJobStats()">查询</button>
<div id="job-stats-result" class="stat-data"></div>
</div>
<div class="stat-card stat-card-warning">
<div class="stat-icon">⏱️</div>
<div class="stat-content">
<h3>平均就业时长</h3>
<p>所有就业学生的平均时长</p>
</div>
<button class="btn btn-warning" onclick="loadAvgJobStats()">查询</button>
<div id="avgjob-stats-result" class="stat-data"></div>
</div>
<div class="stat-card stat-card-info">
<div class="stat-icon">👴</div>
<div class="stat-content">
<h3>30岁以上学员</h3>
<p>年龄超过30岁的学生</p>
</div>
<button class="btn btn-info" onclick="loadAgeStats()">查询</button>
<div id="age-stats-result" class="stat-data"></div>
</div>
</div>
</div>
<!-- 个人中心 -->
<div class="page" id="page-profile">
<div class="profile-header">
<div class="profile-info">
<div class="profile-avatar">学</div>
<div class="profile-details">
<h2>张同学</h2>
<p>学号:20241001 | 计算机科学2401班</p>
<p>联系方式:13900001001</p>
</div>
</div>
</div>
<div class="profile-menu">
<div class="profile-menu-item" onclick="showToast('个人信息页面开发中', 'info')">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
<span class="label">个人信息</span>
<span class="arrow">›</span>
</div>
<div class="profile-menu-item" onclick="showToast('设置页面开发中', 'info')">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 01-2-2 2 2 0 012-2h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z"></path>
</svg>
<span class="label">设置</span>
<span class="arrow">›</span>
</div>
<div class="profile-menu-item" onclick="showToast('帮助中心开发中', 'info')">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<path d="M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3"></path>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
<span class="label">帮助</span>
<span class="arrow">›</span>
</div>
<div class="profile-menu-item" onclick="showConfirm('确定要退出登录吗?', () => { showToast('已退出登录', 'success'); })">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4"></path>
<polyline points="16 17 21 12 16 7"></polyline>
<line x1="21" y1="12" x2="9" y2="12"></line>
</svg>
<span class="label">退出登录</span>
<span class="arrow">›</span>
</div>
</div>
</div>
</main>
<!-- 学生模态框 -->
<div class="modal-overlay" id="student-modal">
<div class="modal">
<div class="modal-header">
<h3 class="modal-title" id="student-modal-title">新增学生</h3>
<button class="modal-close" onclick="closeModal('student-modal')">&times;</button>
</div>
<form id="student-form">
<input type="hidden" id="student-edit-id">
<div class="form-row">
<div class="form-group">
<label class="form-label">姓名 *</label>
<input type="text" class="form-input" id="student-name" required>
</div>
<div class="form-group">
<label class="form-label">性别 *</label>
<select class="form-input" id="student-sex" required>
<option value="男">男</option>
<option value="女">女</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">年龄 *</label>
<input type="number" class="form-input" id="student-age" min="16" max="60" required>
</div>
<div class="form-group">
<label class="form-label">手机号 *</label>
<input type="text" class="form-input" id="student-phone" maxlength="11" required>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">班级 *</label>
<select class="form-input" id="student-class" required>
<option value="1">计算机科学2401班</option>
<option value="2">计算机科学2402班</option>
<option value="3">人工智能2501班</option>
</select>
</div>
<div class="form-group">
<label class="form-label">专业</label>
<select class="form-input" id="student-major">
<option value="计算机科学">计算机科学</option>
<option value="软件工程">软件工程</option>
<option value="数据科学">数据科学</option>
<option value="人工智能">人工智能</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">学历 *</label>
<select class="form-input" id="student-education" required>
<option value="本科">本科</option>
<option value="专科">专科</option>
<option value="硕士">硕士</option>
<option value="博士">博士</option>
</select>
</div>
<div class="form-group">
<label class="form-label">籍贯</label>
<input type="text" class="form-input" id="student-home">
</div>
<div class="form-group">
<label class="form-label">毕业院校</label>
<input type="text" class="form-input" id="student-college">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" onclick="closeModal('student-modal')">取消</button>
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
</div>
<!-- 教师模态框 -->
<div class="modal-overlay" id="teacher-modal">
<div class="modal">
<div class="modal-header">
<h3 class="modal-title">新增教师</h3>
<button class="modal-close" onclick="closeModal('teacher-modal')">&times;</button>
</div>
<form id="teacher-form">
<div class="form-row">
<div class="form-group">
<label class="form-label">姓名 *</label>
<input type="text" class="form-input" id="teacher-name" required>
</div>
<div class="form-group">
<label class="form-label">性别</label>
<select class="form-input" id="teacher-sex">
<option value="男">男</option>
<option value="女">女</option>
<option value="未知">未知</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">手机号 *</label>
<input type="text" class="form-input" id="teacher-phone" required>
</div>
<div class="form-group">
<label class="form-label">班级ID *</label>
<input type="number" class="form-input" id="teacher-class-id" required>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">科目ID *</label>
<input type="number" class="form-input" id="teacher-subject-id" required>
</div>
<div class="form-group">
<label class="form-label">专业方向</label>
<input type="text" class="form-input" id="teacher-specialty">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">学历</label>
<select class="form-input" id="teacher-education">
<option value="博士">博士</option>
<option value="硕士">硕士</option>
<option value="本科">本科</option>
</select>
</div>
<div class="form-group">
<label class="form-label">毕业院校</label>
<input type="text" class="form-input" id="teacher-collage">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" onclick="closeModal('teacher-modal')">取消</button>
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
</div>
<!-- 就业模态框 -->
<div class="modal-overlay" id="employment-modal">
<div class="modal">
<div class="modal-header">
<h3 class="modal-title">新增就业信息</h3>
<button class="modal-close" onclick="closeModal('employment-modal')">&times;</button>
</div>
<form id="employment-form">
<div class="form-row">
<div class="form-group">
<label class="form-label">学生ID *</label>
<input type="number" class="form-input" id="emp-student-id" required>
</div>
<div class="form-group">
<label class="form-label">班级ID *</label>
<input type="number" class="form-input" id="emp-class-id" required>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">公司ID</label>
<input type="number" class="form-input" id="emp-company-id">
</div>
<div class="form-group">
<label class="form-label">薪资</label>
<input type="number" class="form-input" id="emp-salary">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">就业开放时间</label>
<input type="date" class="form-input" id="emp-open-time">
</div>
<div class="form-group">
<label class="form-label">offer下发时间</label>
<input type="date" class="form-input" id="emp-offer-time">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" onclick="closeModal('employment-modal')">取消</button>
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
</div>
<!-- 确认对话框 -->
<div class="modal-overlay" id="confirm-modal">
<div class="modal" style="max-width: 400px;">
<div class="modal-header">
<h3 class="modal-title">确认操作</h3>
</div>
<p id="confirm-message" style="margin: 20px 0; color: var(--text-secondary);"></p>
<div class="modal-footer">
<button class="btn btn-outline" onclick="closeModal('confirm-modal')">取消</button>
<button class="btn btn-danger" id="confirm-btn">确认</button>
</div>
</div>
</div>
<!-- Toast 容器 -->
<div class="toast-container" id="toast-container"></div>
<script>
// API 基础地址
const API_BASE = 'http://localhost:58000';
// 全局状态
let currentPage = 'home';
let sortState = {};
let studentData = [];
let teacherData = [];
let employmentData = [];
let scoreData = {};
// 页面加载
document.addEventListener('DOMContentLoaded', () => {
loadStudents();
loadTeachers();
loadEmployment();
initNavigation();
});
// 初始化导航
function initNavigation() {
document.querySelectorAll('.nav-item').forEach(item => {
item.addEventListener('click', (e) => {
const page = e.currentTarget.dataset.page;
if (page) navigateTo(page);
});
});
}
// 页面导航
function navigateTo(page) {
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
document.getElementById(`page-${page}`).classList.add('active');
document.querySelector(`.nav-item[data-page="${page}"]`).classList.add('active');
currentPage = page;
document.getElementById('breadcrumb-current').textContent = getPageName(page);
if (page === 'scores') {
loadScores();
}
}
// 获取页面名称
function getPageName(page) {
const names = {
'home': '控制台',
'students': '学生管理',
'scores': '成绩查询',
'teachers': '教师管理',
'employment': '就业管理',
'stats': '统计分析',
'profile': '个人中心'
};
return names[page] || page;
}
// ── 学生管理 ──────────────────────────────────────
async function loadStudents() {
try {
console.log('[DEBUG] 开始加载学生...');
const res = await fetch(`${API_BASE}/students`);
console.log('[DEBUG] 响应状态:', res.status);
if (res.ok) {
const resp = await res.json();
console.log('[DEBUG] 响应数据类型:', Array.isArray(resp) ? 'array' : 'object');
studentData = Array.isArray(resp) ? resp : (resp.data || []);
console.log('[DEBUG] 学生数据条数:', studentData.length);
renderStudents(studentData);
updateStudentSelect();
} else {
console.error('[DEBUG] 请求失败:', res.status, res.statusText);
}
} catch (err) {
console.error('加载学生失败:', err);
showToast('加载学生数据失败', 'error');
}
}
function renderStudents(data) {
const tbody = document.getElementById('students-tbody');
tbody.innerHTML = data.map(s => `
<tr>
<td>${s.num}</td>
<td>${s.name}</td>
<td><span class="tag ${s.sex === '男' ? 'tag-primary' : s.sex === '女' ? 'tag-warning' : ''}">${s.sex || '-'}</span></td>
<td>班级${s.class_id}</td>
<td>${s.specialty || '-'}</td>
<td>${s.phone}</td>
<td>
<button class="btn btn-sm btn-outline" onclick="editStudent(${s.id})">编辑</button>
<button class="btn btn-sm btn-danger" onclick="deleteStudent(${s.id}, '${s.name}')">删除</button>
</td>
</tr>
`).join('');
}
function searchStudents() {
const keyword = document.getElementById('student-search').value.toLowerCase();
const classFilter = document.getElementById('student-class-filter').value;
let filtered = studentData.filter(s => {
const matchKeyword = !keyword || s.name.toLowerCase().includes(keyword) || s.num.includes(keyword);
const matchClass = !classFilter || s.class_id === parseInt(classFilter);
return matchKeyword && matchClass;
});
renderStudents(filtered);
}
function editStudent(id) {
const student = studentData.find(s => s.id === id);
if (!student) return;
document.getElementById('student-modal-title').textContent = '编辑学生';
document.getElementById('student-edit-id').value = id;
document.getElementById('student-name').value = student.name;
document.getElementById('student-sex').value = student.sex;
document.getElementById('student-age').value = student.age;
document.getElementById('student-phone').value = student.phone;
document.getElementById('student-class').value = student.class_id;
document.getElementById('student-major').value = student.specialty || '';
document.getElementById('student-home').value = student.home_Place || '';
document.getElementById('student-college').value = student.college || '';
// 回填学历(后端存储的是中文,下拉框也是中文)
const eduSel = document.getElementById('student-education');
if (student.education) {
eduSel.value = student.education;
}
openModal('student-modal');
}
document.getElementById('student-form').addEventListener('submit', async (e) => {
e.preventDefault();
const id = document.getElementById('student-edit-id').value;
const data = {
name: document.getElementById('student-name').value,
sex: document.getElementById('student-sex').value,
age: parseInt(document.getElementById('student-age').value),
phone: document.getElementById('student-phone').value,
class_id: parseInt(document.getElementById('student-class').value),
specialty: document.getElementById('student-major').value,
education: document.getElementById('student-education').value,
// 默认 advisor_id=1,绕过后端 dao 查 advisor 的 bug
advisor_id: 1,
home_Place: document.getElementById('student-home').value,
college: document.getElementById('student-college').value,
};
if (id) {
data.id = parseInt(id);
await updateStudent(data);
} else {
// 根据当前最大id自动生成学号
const maxId = studentData.reduce((max, s) => Math.max(max, s.id || 0), 0);
data.num = '2024' + String(maxId + 1001).padStart(4, '0');
await addStudent(data);
}
});
async function addStudent(data) {
try {
const res = await fetch(`${API_BASE}/students`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (res.ok) {
showToast('添加学生成功', 'success');
closeModal('student-modal');
loadStudents();
} else {
const err = await res.json();
showToast(err.detail || '添加失败', 'error');
}
} catch (err) {
showToast('网络错误', 'error');
}
}
async function updateStudent(data) {
try {
const res = await fetch(`${API_BASE}/students`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (res.ok) {
showToast('更新学生成功', 'success');
closeModal('student-modal');
loadStudents();
} else {
const err = await res.json();
showToast(err.detail || '更新失败', 'error');
}
} catch (err) {
showToast('网络错误', 'error');
}
}
function deleteStudent(id, name) {
showConfirm(`确定要删除学生 ${name} 吗?此操作不可恢复。`, async () => {
try {
const res = await fetch(`${API_BASE}/students`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id })
});
if (res.ok) {
showToast('删除成功', 'success');
loadStudents();
} else {
showToast('删除失败', 'error');
}
} catch (err) {
showToast('网络错误', 'error');
}
});
}
function updateStudentSelect() {
const select = document.getElementById('score-student-filter');
select.innerHTML = '<option value="">选择学生</option>' +
studentData.map(s => `<option value="${s.id}">${s.name} (${s.num})</option>`).join('');
}
// ── 教师管理 ──────────────────────────────────────
async function loadTeachers() {
try {
console.log('[DEBUG] 开始加载教师...');
const res = await fetch(`${API_BASE}/tea`);
console.log('[DEBUG] 响应状态:', res.status);
if (res.ok) {
const data = await res.json();
console.log('[DEBUG] 教师数据:', JSON.stringify(data).substring(0, 200));
teacherData = data.data || [];
console.log('[DEBUG] 教师条数:', teacherData.length);
renderTeachers(teacherData);
} else {
console.error('[DEBUG] 请求失败:', res.status, res.statusText);
}
} catch (err) {
console.error('加载教师失败:', err);
}
}
function renderTeachers(data) {
const tbody = document.getElementById('teachers-tbody');
tbody.innerHTML = data.map(t => `
<tr>
<td>${t.id}</td>
<td>${t.name}</td>
<td><span class="tag ${t.sex === '男' ? 'tag-primary' : t.sex === '女' ? 'tag-warning' : ''}">${t.sex || '-'}</span></td>
<td>${t.specialty || '-'}</td>
<td>${t.work_experience || '-'}</td>
<td>${t.collage || '-'}</td>
<td>
<button class="btn btn-sm btn-outline">编辑</button>
<button class="btn btn-sm btn-danger" onclick="deleteTeacher(${t.id}, '${t.name}')">删除</button>
</td>
</tr>
`).join('');
}
function searchTeachers() {
const keyword = document.getElementById('teacher-search').value.toLowerCase();
const filtered = teacherData.filter(t => t.name.toLowerCase().includes(keyword));
renderTeachers(filtered);
}
document.getElementById('teacher-form').addEventListener('submit', async (e) => {
e.preventDefault();
const data = {
name: document.getElementById('teacher-name').value,
sex: document.getElementById('teacher-sex').value,
phone: document.getElementById('teacher-phone').value,
class_id: parseInt(document.getElementById('teacher-class-id').value),
sub: document.getElementById('teacher-subject-id').value,
specialty: document.getElementById('teacher-specialty').value,
education: document.getElementById('teacher-education').value,
collage: document.getElementById('teacher-collage').value,
};
try {
const res = await fetch(`${API_BASE}/tea`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (res.ok) {
showToast('添加教师成功', 'success');
closeModal('teacher-modal');
loadTeachers();
} else {
showToast('添加失败', 'error');
}
} catch (err) {
showToast('网络错误', 'error');
}
});
function deleteTeacher(id, name) {
showConfirm(`确定要删除教师 ${name} 吗?`, async () => {
try {
const res = await fetch(`${API_BASE}/tea/${id}`, { method: 'DELETE' });
if (res.ok) {
showToast('删除成功', 'success');
loadTeachers();
} else {
showToast('删除失败', 'error');
}
} catch (err) {
showToast('网络错误', 'error');
}
});
}
// ── 就业管理 ──────────────────────────────────────
async function loadEmployment() {
try {
console.log('[DEBUG] 开始加载就业...');
const res = await fetch(`${API_BASE}/emp`);
console.log('[DEBUG] 响应状态:', res.status);
if (res.ok) {
const resp = await res.json();
console.log('[DEBUG] 就业响应类型:', Array.isArray(resp) ? 'array' : 'object');
employmentData = Array.isArray(resp) ? resp : (resp.data || []);
console.log('[DEBUG] 就业条数:', employmentData.length);
renderEmployment(employmentData);
} else {
console.error('[DEBUG] 请求失败:', res.status, res.statusText);
}
} catch (err) {
console.error('加载就业数据失败:', err);
}
}
function renderEmployment(data) {
const tbody = document.getElementById('employment-tbody');
tbody.innerHTML = data.map(e => `
<tr>
<td>${e['学生姓名'] || '-'}</td>
<td>班级${e['学生班级编号'] || '-'}</td>
<td>${e['公司'] || '-'}</td>
<td>¥${(e['薪资'] || 0).toLocaleString()}</td>
<td>${e['offer下发时间'] || '待定'}</td>
<td><span class="tag ${e['offer下发时间'] ? 'tag-success' : 'tag-warning'}">${e['offer下发时间'] ? '已就业' : '待就业'}</span></td>
<td>
<button class="btn btn-sm btn-outline">编辑</button>
<button class="btn btn-sm btn-danger" onclick="deleteEmployment(${e.id})">删除</button>
</td>
</tr>
`).join('');
}
function searchEmployment() {
const keyword = document.getElementById('employment-search').value.toLowerCase();
const filtered = employmentData.filter(e =>
((e['学生姓名'] && e['学生姓名'].toLowerCase().includes(keyword)) ||
(e['公司'] && e['公司'].toLowerCase().includes(keyword)))
);
renderEmployment(filtered);
}
document.getElementById('employment-form').addEventListener('submit', async (e) => {
e.preventDefault();
const data = {
sname: '',
class_id: parseInt(document.getElementById('emp-class-id').value),
company_id: parseInt(document.getElementById('emp-company-id').value) || null,
employment_salary: parseInt(document.getElementById('emp-salary').value) || null,
employment_open_time: document.getElementById('emp-open-time').value || null,
offer_recived_time: document.getElementById('emp-offer-time').value || null,
};
try {
const res = await fetch(`${API_BASE}/emp`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (res.ok) {
showToast('添加就业信息成功', 'success');
closeModal('employment-modal');
loadEmployment();
} else {
showToast('添加失败', 'error');
}
} catch (err) {
showToast('网络错误', 'error');
}
});
function deleteEmployment(id) {
showConfirm('确定要删除这条就业记录吗?', async () => {
try {
const res = await fetch(`${API_BASE}/emp/${id}`, { method: 'DELETE' });
if (res.ok) {
showToast('删除成功', 'success');
loadEmployment();
} else {
showToast('删除失败', 'error');
}
} catch (err) {
showToast('网络错误', 'error');
}
});
}
// ── 成绩查询 ──────────────────────────────────────
async function loadScores() {
const studentId = document.getElementById('score-student-filter').value;
const term = document.getElementById('score-term-filter').value;
if (!studentId) {
document.getElementById('score-cards').innerHTML = '<p style="color: var(--text-muted); padding: 20px;">请选择学生进行查询</p>';
return;
}
try {
const res = await fetch(`${API_BASE}/Score?sid=${studentId}&num=${term}`);
if (res.ok) {
const data = await res.json();
renderScores(data);
}
} catch (err) {
showToast('加载成绩失败', 'error');
}
}
function renderScores(scores) {
const container = document.getElementById('score-cards');
if (!scores || scores.length === 0) {
container.innerHTML = '<p style="color: var(--text-muted); padding: 20px;">暂无成绩数据</p>';
return;
}
container.innerHTML = scores.map(s => `
<div class="score-card card-click">
<div class="score-card-header">
<span class="score-card-title">课程ID: ${s.t_subject}</span>
<span class="tag ${s.score >= 60 ? 'tag-success' : 'tag-danger'}">${s.score >= 60 ? '及格' : '不及格'}</span>
</div>
<div class="score-card-value ${s.score >= 60 ? 'pass' : 'fail'}">${s.score}分</div>
</div>
`).join('');
renderScoreChart(scores);
}
let scoreChart = null;
function renderScoreChart(scores) {
const ctx = document.getElementById('score-chart').getContext('2d');
if (scoreChart) {
scoreChart.destroy();
}
scoreChart = new Chart(ctx, {
type: 'bar',
data: {
labels: scores.map((s, i) => `课程${i + 1}`),
datasets: [{
label: '成绩',
data: scores.map(s => s.score),
backgroundColor: scores.map(s => s.score >= 60 ? '#4CAF50' : '#F44336'),
borderRadius: 4
}]
},
options: {
responsive: true,
plugins: {
legend: { display: false }
},
scales: {
y: {
beginAtZero: true,
max: 100
}
}
}
});
}
// ── 通用函数 ──────────────────────────────────────
function openModal(id) {
document.getElementById(id).classList.add('active');
}
function closeModal(id) {
document.getElementById(id).classList.remove('active');
if (id === 'student-modal') {
document.getElementById('student-form').reset();
document.getElementById('student-edit-id').value = '';
document.getElementById('student-modal-title').textContent = '新增学生';
}
}
function showToast(message, type = 'info') {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.innerHTML = `
<span>${type === 'success' ? '✓' : type === 'error' ? '✗' : 'ℹ'}</span>
<span>${message}</span>
`;
container.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateX(100%)';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
function showConfirm(message, onConfirm) {
document.getElementById('confirm-message').textContent = message;
openModal('confirm-modal');
document.getElementById('confirm-btn').onclick = () => {
closeModal('confirm-modal');
onConfirm();
};
}
function sortTable(tableId, colIndex) {
const key = `${tableId}-${colIndex}`;
sortState[key] = !sortState[key];
const asc = sortState[key];
// 简化排序逻辑
showToast(`按第${colIndex + 1}列${asc ? '升序' : '降序'}排序`, 'info');
}
function changePage(type, delta) {
showToast(`${type}分页功能开发中`, 'info');
}
// ── 统计分析 ──────────────────────────────────────
async function loadClassStats() {
try {
const type = document.getElementById('stat-class-type').value;
const res = await fetch(`${API_BASE}/statcalc/students?headcount=${encodeURIComponent(type)}`);
if (res.ok) {
const data = await res.json();
const resultDiv = document.getElementById('class-stats-result');
resultDiv.innerHTML = data.length > 0
? data.map(row => `<div>${Object.values(row).join(' | ')}</div>`).join('')
: '<div>暂无数据</div>';
}
} catch (err) {
console.error('加载班级统计失败:', err);
}
}
async function loadExcellentStats() {
try {
const res = await fetch(`${API_BASE}/statcalc/scores`);
if (res.ok) {
const data = await res.json();
const resultDiv = document.getElementById('excellent-stats-result');
if (data.detail) {
resultDiv.innerHTML = `<div>${data.detail}</div>`;
} else {
resultDiv.innerHTML = data.length > 0
? data.map(row => `<div>${row.name} - ${row.score}分</div>`).join('')
: '<div>暂无数据</div>';
}
}
} catch (err) {
console.error('加载优秀成绩统计失败:', err);
}
}
async function loadFailStats() {
try {
const res = await fetch(`${API_BASE}/statcalc/fail`);
if (res.ok) {
const data = await res.json();
const resultDiv = document.getElementById('fail-stats-result');
if (data.detail) {
resultDiv.innerHTML = `<div>${data.detail}</div>`;
} else {
resultDiv.innerHTML = data.length > 0
? data.map(row => `<div>${row.name} | ${row.class_name} | 不及格${row.score}分</div>`).join('')
: '<div>无不及格学生</div>';
}
}
} catch (err) {
console.error('加载不及格统计失败:', err);
}
}
async function loadAvgStats() {
try {
const res = await fetch(`${API_BASE}/statcalc/average`);
if (res.ok) {
const data = await res.json();
const resultDiv = document.getElementById('avg-stats-result');
resultDiv.innerHTML = data.length > 0
? data.map(row => `<div>${row.class_name}班 - 平均分: ${row.avg_score}</div>`).join('')
: '<div>暂无数据</div>';
}
} catch (err) {
console.error('加载平均分统计失败:', err);
}
}
async function loadTopStats() {
try {
const res = await fetch(`${API_BASE}/statcalc/top`);
if (res.ok) {
const data = await res.json();
const resultDiv = document.getElementById('top-stats-result');
resultDiv.innerHTML = data.length > 0
? data.map((row, i) => `<div>${i+1}. ${row['学生姓名']} - ${row['公司']} - ¥${row['employment_salary']}</div>`).join('')
: '<div>暂无就业数据</div>';
}
} catch (err) {
console.error('加载Top5统计失败:', err);
}
}
async function loadJobStats() {
try {
const res = await fetch(`${API_BASE}/statcalc/job`);
if (res.ok) {
const data = await res.json();
const resultDiv = document.getElementById('job-stats-result');
resultDiv.innerHTML = data.length > 0
? data.map(row => `<div>${row['学生姓名']} - ${row['就业时常']}</div>`).join('')
: '<div>暂无数据</div>';
}
} catch (err) {
console.error('加载就业时长统计失败:', err);
}
}
async function loadAvgJobStats() {
try {
const res = await fetch(`${API_BASE}/statcalc/avgjob`);
if (res.ok) {
const data = await res.json();
const resultDiv = document.getElementById('avgjob-stats-result');
resultDiv.innerHTML = `
<div>就业人数: ${data['人数']}人</div>
<div>平均就业时长: ${data['平均就业时常(天)']}天</div>
`;
}
} catch (err) {
console.error('加载平均就业时长失败:', err);
}
}
async function loadAgeStats() {
try {
const res = await fetch(`${API_BASE}/statcalc/age`);
if (res.ok) {
const data = await res.json();
const resultDiv = document.getElementById('age-stats-result');
resultDiv.innerHTML = data.length > 0
? data.map(row => `<div>${row.name} - ${row.age}岁 - ${row.num}</div>`).join('')
: '<div>无30岁以上学员</div>';
}
} catch (err) {
console.error('加载年龄统计失败:', err);
}
}
// 点击模态框外部关闭
document.querySelectorAll('.modal-overlay').forEach(overlay => {
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
overlay.classList.remove('active');
}
});
});
</script>
</body>
</html>