diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..b6b1ecf
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,10 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
+# 已忽略包含查询文件的默认文件夹
+/queries/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
+# 基于编辑器的 HTTP 客户端请求
+/httpRequests/
diff --git a/.idea/MarsCodeWorkspaceAppSettings.xml b/.idea/MarsCodeWorkspaceAppSettings.xml
new file mode 100644
index 0000000..b26fdc6
--- /dev/null
+++ b/.idea/MarsCodeWorkspaceAppSettings.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml
new file mode 100644
index 0000000..bfd6cbf
--- /dev/null
+++ b/.idea/dataSources.xml
@@ -0,0 +1,19 @@
+
+
+
+
+ mysql.8
+ true
+ com.mysql.cj.jdbc.Driver
+ jdbc:mysql://192.168.5.8:3306
+ $ProjectFileDir$
+
+
+ mysql.8
+ true
+ com.mysql.cj.jdbc.Driver
+ jdbc:mysql://localhost:3306
+ $ProjectFileDir$
+
+
+
\ No newline at end of file
diff --git a/.idea/db-forest-config.xml b/.idea/db-forest-config.xml
new file mode 100644
index 0000000..fcb1ba6
--- /dev/null
+++ b/.idea/db-forest-config.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..47c3e24
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..5fbb48d
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..0a98a9b
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/students-system.iml b/.idea/students-system.iml
new file mode 100644
index 0000000..fca1449
--- /dev/null
+++ b/.idea/students-system.iml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/xs_system.iml b/.idea/xs_system.iml
new file mode 100644
index 0000000..fca1449
--- /dev/null
+++ b/.idea/xs_system.iml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/api/statistics_api.py b/api/statistics_api.py
index 466a027..9939884 100644
--- a/api/statistics_api.py
+++ b/api/statistics_api.py
@@ -1,7 +1,7 @@
from fastapi import APIRouter, Depends, Query
from database import get_db
from dao.statistics_dao import StatisticsDao
-from schema.statistics_schema import ResponseModel,ResponseModel1,ResponseModel2,ResponseModel3,ResponseModel4,ResponseModel5,ResponseModel6
+from schema.statistics_schema import ResponseModel,ResponseModel1,ResponseModel2,ResponseModel3,ResponseModel4,ResponseModel5,ResponseModel6,ResponseModel7,ResponseModel8
from math import ceil
BasicInformationAPI = APIRouter(tags=['统计分析'])
@@ -24,11 +24,20 @@ def get_students_by_age_range_api(
total_pages=ceil(total/m) if total > 0 else 0,
data=req)
-@BasicInformationAPI.get('/basic-information/{class_id}',summary='统计每个班级的⼈数以及男⽣/⼥⽣的⼈数')
+@BasicInformationAPI.get('/basic-information/{class_id}',summary='统计指定班级的⼈数以及男⽣/⼥⽣的⼈数')
def get_students_by_class_id_api(class_id:str,
db = Depends(get_db)):
- total_count , male_count , female_count = StatisticsDao.get_students_by_class_id_dao(class_id,db)
- return {f'code:200,message=查询成功, 全班总人数:{total_count}, 班级男生人数:{male_count}, 班级女生人数:{female_count}'}
+ data = StatisticsDao.get_students_by_class_id_dao(class_id,db)
+ return ResponseModel7(code=200,
+ message='查询成功',
+ data=data)
+
+@BasicInformationAPI.get('/class-stats',summary='统计所有班级的人数以及男⽣/⼥⽣的⼈数')
+def get_class_stats_api(db = Depends(get_db)):
+ data = StatisticsDao.get_class_stats_dao(db)
+ return ResponseModel8(code=200,
+ message='查询成功',
+ data=data)
@BasicInformationAPI.get('/scores',summary='在某个分数段的学生')
diff --git a/dao/statistics_dao.py b/dao/statistics_dao.py
index f146577..231b8e8 100644
--- a/dao/statistics_dao.py
+++ b/dao/statistics_dao.py
@@ -1,7 +1,8 @@
from model.statistics_model import StudentInfo, StudentScore, ClassInfo, EmploymentInfo
from datetime import date
from dateutil.relativedelta import relativedelta
-from sqlalchemy import func # SQL 内置函数生成器(MIN/MAX/COUNT 等)
+from sqlalchemy import func, case
+from fastapi import HTTPException
class StatisticsDao:
@staticmethod
@@ -24,18 +25,59 @@ class StatisticsDao:
return req,total
@staticmethod
- def get_students_by_class_id_dao(class_id:str,db):
+ def get_class_stats_dao(db):
try:
- q = db.query(StudentInfo).filter(StudentInfo.class_id == class_id).\
- filter(StudentInfo.is_deleted == '0')
- total_count = q.count() # 总人数
- male_count = q.filter(StudentInfo.gender == '男').count()
- female_count = q.filter(StudentInfo.gender == '女').count()
+ q = (db.query(
+ ClassInfo.class_id,
+ ClassInfo.class_name,
+ func.count(StudentInfo.student_id).label('total_count'),
+ func.sum(case((StudentInfo.gender == '男', 1), else_=0)).label('male_count'),
+ func.sum(case((StudentInfo.gender == '女', 1), else_=0)).label('female_count'),
+ ).join(StudentInfo, ClassInfo.class_id == StudentInfo.class_id)
+ .filter(ClassInfo.is_deleted == '0')
+ .filter(StudentInfo.is_deleted == '0')
+ .group_by(ClassInfo.class_id, ClassInfo.class_name))
+ req = q.all()
except Exception as e:
db.rollback()
raise e
else:
- return total_count, male_count , female_count
+ return [{
+ 'class_id': i.class_id,
+ 'class_name': i.class_name,
+ 'total_count': i.total_count,
+ 'male_count': i.male_count or 0,
+ 'female_count': i.female_count or 0,
+ } for i in req]
+
+ @staticmethod
+ def get_students_by_class_id_dao(class_id:str,db):
+ try:
+ class_info = db.query(ClassInfo).filter(
+ ClassInfo.class_id == class_id,
+ ClassInfo.is_deleted == '0'
+ ).first()
+ if not class_info:
+ raise HTTPException(status_code=404, detail=f'班级 {class_id} 不存在')
+
+ q = db.query(StudentInfo).filter(StudentInfo.class_id == class_id).\
+ filter(StudentInfo.is_deleted == '0')
+ total_count = q.count()
+ male_count = q.filter(StudentInfo.gender == '男').count()
+ female_count = q.filter(StudentInfo.gender == '女').count()
+ except HTTPException:
+ raise
+ except Exception as e:
+ db.rollback()
+ raise e
+ else:
+ return {
+ 'class_id': class_id,
+ 'class_name': class_info.class_name,
+ 'total_count': total_count,
+ 'male_count': male_count,
+ 'female_count': female_count
+ }
@staticmethod
def get_students_by_score_dao(n:int,m:int,score:float,db):
diff --git a/frontend/css/style.css b/frontend/css/style.css
new file mode 100644
index 0000000..b1eacc0
--- /dev/null
+++ b/frontend/css/style.css
@@ -0,0 +1,352 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
+ "Hiragino Sans GB", "Microsoft YaHei", Arial, sans-serif;
+ background-color: #f0f2f5;
+ color: #333;
+ height: 100vh;
+ overflow: hidden;
+}
+
+.layout {
+ display: flex;
+ height: 100vh;
+}
+
+.sidebar {
+ width: 240px;
+ background: linear-gradient(180deg, #1e3c72 0%, #2a5298 100%);
+ color: #fff;
+ display: flex;
+ flex-direction: column;
+ flex-shrink: 0;
+}
+
+.sidebar-header {
+ padding: 24px 20px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.15);
+}
+
+.sidebar-header h1 {
+ font-size: 20px;
+ font-weight: 600;
+ letter-spacing: 1px;
+}
+
+.sidebar-header p {
+ font-size: 12px;
+ color: rgba(255, 255, 255, 0.7);
+ margin-top: 6px;
+}
+
+.nav {
+ flex: 1;
+ padding: 12px 0;
+ overflow-y: auto;
+}
+
+.nav-item {
+ padding: 12px 24px;
+ cursor: pointer;
+ font-size: 14px;
+ transition: all 0.2s;
+ border-left: 3px solid transparent;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.nav-item:hover {
+ background: rgba(255, 255, 255, 0.08);
+}
+
+.nav-item.active {
+ background: rgba(255, 255, 255, 0.15);
+ border-left-color: #ffd700;
+ font-weight: 500;
+}
+
+.nav-item .icon {
+ font-size: 16px;
+ width: 20px;
+ text-align: center;
+}
+
+.main-content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.top-bar {
+ height: 56px;
+ background: #fff;
+ border-bottom: 1px solid #e8e8e8;
+ display: flex;
+ align-items: center;
+ padding: 0 24px;
+ font-size: 16px;
+ font-weight: 500;
+ color: #1e3c72;
+ flex-shrink: 0;
+}
+
+.page-container {
+ flex: 1;
+ padding: 24px;
+ overflow-y: auto;
+}
+
+.page {
+ display: none;
+}
+
+.page.active {
+ display: block;
+}
+
+.card {
+ background: #fff;
+ border-radius: 8px;
+ padding: 24px;
+ margin-bottom: 20px;
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
+}
+
+.card-title {
+ font-size: 15px;
+ font-weight: 600;
+ margin-bottom: 16px;
+ color: #1e3c72;
+ padding-bottom: 12px;
+ border-bottom: 1px solid #f0f0f0;
+}
+
+.form-row {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ flex-wrap: wrap;
+}
+
+.form-group {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.form-group label {
+ font-size: 14px;
+ color: #555;
+ white-space: nowrap;
+}
+
+input[type="number"],
+input[type="text"] {
+ padding: 8px 12px;
+ border: 1px solid #d9d9d9;
+ border-radius: 4px;
+ font-size: 14px;
+ width: 140px;
+ outline: none;
+ transition: border-color 0.2s;
+}
+
+input[type="number"]:focus,
+input[type="text"]:focus {
+ border-color: #2a5298;
+}
+
+.btn {
+ padding: 8px 20px;
+ border: none;
+ border-radius: 4px;
+ font-size: 14px;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.btn-primary {
+ background: #2a5298;
+ color: #fff;
+}
+
+.btn-primary:hover {
+ background: #1e3c72;
+}
+
+.btn-default {
+ background: #fff;
+ color: #555;
+ border: 1px solid #d9d9d9;
+}
+
+.btn-default:hover {
+ border-color: #2a5298;
+ color: #2a5298;
+}
+
+.table-wrapper {
+ overflow-x: auto;
+}
+
+table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 14px;
+}
+
+thead th {
+ background: #fafafa;
+ padding: 12px 16px;
+ text-align: left;
+ font-weight: 600;
+ color: #333;
+ border-bottom: 2px solid #f0f0f0;
+ white-space: nowrap;
+}
+
+tbody td {
+ padding: 12px 16px;
+ border-bottom: 1px solid #f0f0f0;
+ color: #555;
+}
+
+tbody tr:hover {
+ background: #fafbfc;
+}
+
+.pagination {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ padding: 16px 0 4px;
+}
+
+.pagination .page-info {
+ margin-right: 16px;
+ color: #888;
+ font-size: 13px;
+}
+
+.pagination button {
+ min-width: 32px;
+ height: 32px;
+ padding: 0 10px;
+ border: 1px solid #d9d9d9;
+ background: #fff;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 13px;
+ color: #555;
+}
+
+.pagination button:hover:not(:disabled) {
+ border-color: #2a5298;
+ color: #2a5298;
+}
+
+.pagination button:disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+}
+
+.pagination button.active {
+ background: #2a5298;
+ border-color: #2a5298;
+ color: #fff;
+}
+
+.stats-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 16px;
+}
+
+.stat-card {
+ background: #fff;
+ border-radius: 8px;
+ padding: 20px;
+ text-align: center;
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
+ border-top: 3px solid #2a5298;
+}
+
+.stat-card .stat-label {
+ font-size: 13px;
+ color: #888;
+ margin-bottom: 8px;
+}
+
+.stat-card .stat-value {
+ font-size: 28px;
+ font-weight: 600;
+ color: #1e3c72;
+}
+
+.stat-card.male {
+ border-top-color: #1890ff;
+}
+.stat-card.female {
+ border-top-color: #eb2f96;
+}
+.stat-card.total {
+ border-top-color: #52c41a;
+}
+
+.chart-container {
+ width: 100%;
+ height: 400px;
+}
+
+.loading {
+ text-align: center;
+ padding: 40px;
+ color: #999;
+}
+
+.empty {
+ text-align: center;
+ padding: 40px;
+ color: #999;
+}
+
+.toast {
+ position: fixed;
+ top: 20px;
+ left: 50%;
+ transform: translateX(-50%);
+ padding: 12px 24px;
+ border-radius: 4px;
+ font-size: 14px;
+ z-index: 9999;
+ animation: slideDown 0.3s ease;
+}
+
+.toast.success {
+ background: #52c41a;
+ color: #fff;
+}
+
+.toast.error {
+ background: #ff4d4f;
+ color: #fff;
+}
+
+@keyframes slideDown {
+ from {
+ opacity: 0;
+ transform: translate(-50%, -20px);
+ }
+ to {
+ opacity: 1;
+ transform: translate(-50%, 0);
+ }
+}
\ No newline at end of file
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..fab41e7
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+ 学生管理系统 - 统计分析
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/js/api.js b/frontend/js/api.js
new file mode 100644
index 0000000..2201c8d
--- /dev/null
+++ b/frontend/js/api.js
@@ -0,0 +1,49 @@
+const BASE_URL = 'http://127.0.0.1:12345';
+
+async function request(path, params = {}) {
+ const url = new URL(BASE_URL + path);
+ Object.keys(params).forEach(key => {
+ if (params[key] !== undefined && params[key] !== null && params[key] !== '') {
+ url.searchParams.append(key, params[key]);
+ }
+ });
+
+ try {
+ const res = await fetch(url.toString());
+ if (!res.ok) throw new Error('HTTP ' + res.status);
+ const data = await res.json();
+ return data;
+ } catch (e) {
+ console.error('请求失败:', url.toString(), e);
+ throw e;
+ }
+}
+
+const API = {
+ getStudentsByAgeRange: (n, m, minAge, maxAge) =>
+ request('/basic-information', { n, m, min_age: minAge, max_age: maxAge }),
+
+ getClassInfo: (classId) =>
+ request('/basic-information/' + classId),
+
+ getAllClassStats: () =>
+ request('/class-stats'),
+
+ getStudentsByScore: (n, m, score) =>
+ request('/scores', { n, m, score }),
+
+ getNoPassStudents: (n, m, failCount) =>
+ request('/scores_no_pass', { n, m, fail_count: failCount }),
+
+ getClassExamAvg: (n, m) =>
+ request('/scores_avg', { n, m }),
+
+ getSalaryTop: (m = 5) =>
+ request('/salary_top', { m }),
+
+ getTimeSize: (n, m) =>
+ request('/time_size', { n, m }),
+
+ getClassAvgTimeSize: (n, m) =>
+ request('/class_avg_time_size', { n, m })
+};
\ No newline at end of file
diff --git a/frontend/js/main.js b/frontend/js/main.js
new file mode 100644
index 0000000..9f3329b
--- /dev/null
+++ b/frontend/js/main.js
@@ -0,0 +1,714 @@
+const state = {
+ age: { page: 1, size: 10, totalPages: 0, total: 0 },
+ score: { page: 1, size: 10, totalPages: 0, total: 0 },
+ nopass: { page: 1, size: 10, totalPages: 0, total: 0 },
+ examAvg: { page: 1, size: 10, totalPages: 0, total: 0 },
+ timeSize: { page: 1, size: 10, totalPages: 0, total: 0 },
+ classAvgTime: { page: 1, size: 10, totalPages: 0, total: 0 }
+};
+
+const pages = [
+ { id: 'age', name: '按年龄区间查询', icon: '👤' },
+ { id: 'class-stats', name: '指定班级人数统计', icon: '🏫' },
+ { id: 'all-class-stats', name: '所有班级人数统计', icon: '📊' },
+ { id: 'score', name: '按分数段查询', icon: '📝' },
+ { id: 'no-pass', name: '不合格学生查询', icon: '⚠️' },
+ { id: 'exam-avg', name: '班级考试平均分', icon: '📈' },
+ { id: 'salary-top', name: '薪资TOP排行', icon: '💰' },
+ { id: 'time-size', name: '就业时长查询', icon: '⏱️' },
+ { id: 'class-avg-time', name: '班级平均就业时长', icon: '📅' }
+];
+
+function renderSidebar() {
+ const nav = document.getElementById('nav');
+ nav.innerHTML = pages.map((p, i) =>
+ `
+ ${p.icon}${p.name}
+
`
+ ).join('');
+
+ nav.querySelectorAll('.nav-item').forEach(item => {
+ item.addEventListener('click', () => {
+ nav.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
+ item.classList.add('active');
+ showPage(item.dataset.page);
+ });
+ });
+}
+
+function showPage(id) {
+ document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
+ const target = document.getElementById('page-' + id);
+ if (target) target.classList.add('active');
+ document.getElementById('top-title').textContent =
+ pages.find(p => p.id === id)?.name || '';
+
+ if (id === 'all-class-stats') loadAllClassStats();
+ if (id === 'salary-top') loadSalaryTop();
+ if (id === 'exam-avg') loadExamAvg();
+ if (id === 'time-size') loadTimeSize();
+ if (id === 'class-avg-time') loadClassAvgTime();
+}
+
+function toast(msg, type = 'success') {
+ const div = document.createElement('div');
+ div.className = 'toast ' + type;
+ div.textContent = msg;
+ document.body.appendChild(div);
+ setTimeout(() => div.remove(), 2000);
+}
+
+function renderPagination(container, s, onChange) {
+ if (s.totalPages <= 1) {
+ container.innerHTML = '';
+ return;
+ }
+ let html = `共 ${s.total} 条 / 第 ${s.page}/${s.totalPages} 页`;
+ html += ``;
+
+ const range = [];
+ const start = Math.max(1, s.page - 2);
+ const end = Math.min(s.totalPages, start + 4);
+ for (let i = start; i <= end; i++) range.push(i);
+
+ range.forEach(p => {
+ html += ``;
+ });
+
+ html += ``;
+ html += `跳至`;
+ html += ``;
+ html += `页`;
+
+ container.innerHTML = html;
+
+ container.querySelectorAll('button').forEach(btn => {
+ btn.addEventListener('click', () => {
+ let p = btn.dataset.p ? parseInt(btn.dataset.p) : null;
+ if (btn.dataset.act === 'prev') p = s.page - 1;
+ if (btn.dataset.act === 'next') p = s.page + 1;
+ if (p && p !== s.page) onChange(p);
+ });
+ });
+
+ const jumpInput = container.querySelector('input[data-act="jumpto"]');
+ if (jumpInput) {
+ jumpInput.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter') {
+ let p = parseInt(jumpInput.value);
+ if (!isNaN(p)) {
+ p = Math.max(1, Math.min(s.totalPages, p));
+ if (p !== s.page) onChange(p);
+ }
+ }
+ });
+ }
+}
+
+function formatDate(d) {
+ if (!d) return '-';
+ return d.toString().slice(0, 10);
+}
+
+function createPages() {
+ const container = document.getElementById('pages-container');
+ container.innerHTML = `
+
+
+
+
+
查询结果
+
+
+
+
+ | 学号 | 姓名 | 性别 | 身份证号 |
+ 出生日期 | 民族 | 专业 | 班级 |
+ 入学日期 | 学籍状态 | 学历 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | 班级编号 | 班级名称 | 总人数 | 男生 | 女生 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
每次考试每个班级平均分(从高到低)
+
+
+
+
+
+
+
+ | 课程编号 | 班级编号 | 班级名称 | 平均分 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | 姓名 | 班级 | 公司 | 入职时间 | 薪资(元/月) |
+
+
+
+
+
+
+
+
+
+
+
每个学生的就业时长
+
+
+
+
+ | 学号 | 姓名 | 开放简历时间 | Offer时间 | 就业时长(天) |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | 班级编号 | 班级名称 | 平均就业时长(天) |
+
+
+
+
+
+
+
+ `;
+}
+
+// ============ 各页面加载函数 ============
+
+async function searchAge() {
+ const min = parseInt(document.getElementById('age-min').value);
+ const max = parseInt(document.getElementById('age-max').value);
+ state.age.page = 1;
+ await loadAge(min, max);
+}
+
+async function loadAge(min, max) {
+ const s = state.age;
+ s.size = parseInt(document.getElementById('age-size').value) || 10;
+ document.querySelector('#age-table tbody').innerHTML = '| 加载中... |
';
+ try {
+ const res = await API.getStudentsByAgeRange(s.page, s.size, min, max);
+ if (res.code !== 200) { toast(res.message, 'error'); return; }
+ s.total = res.total;
+ s.totalPages = res.total_pages;
+
+ const tbody = document.querySelector('#age-table tbody');
+ if (!res.data || res.data.length === 0) {
+ tbody.innerHTML = '| 暂无数据 |
';
+ } else {
+ tbody.innerHTML = res.data.map(r => `
+
+ | ${r.student_id} | ${r.student_name} | ${r.gender} |
+ ${r.id_card} | ${formatDate(r.birthday)} | ${r.ethnicity} |
+ ${r.major} | ${r.class_id} | ${formatDate(r.enrollment_date)} |
+ ${r.student_status} | ${r.education_level} |
+
`).join('');
+ }
+ renderPagination(document.getElementById('age-pagination'), s, (p) => {
+ s.page = p; loadAge(min, max);
+ });
+ } catch (e) {
+ document.querySelector('#age-table tbody').innerHTML = '| 请求失败,请检查后端服务是否启动 |
';
+ }
+}
+
+async function searchClassInfo() {
+ const classId = document.getElementById('class-id').value.trim();
+ if (!classId) { toast('请输入班级编号', 'error'); return; }
+ const box = document.getElementById('class-stats-result');
+ box.innerHTML = '加载中...
';
+ try {
+ const res = await API.getClassInfo(classId);
+ if (res.code !== 200) { toast(res.message, 'error'); box.innerHTML = ''; return; }
+ const d = res.data;
+ box.innerHTML = `
+
+
班级:${d.class_id} - ${d.class_name}
+
+
+
总人数
+
${d.total_count}
+
+
+
+
女生
+
${d.female_count}
+
+
+
+
`;
+
+ const chart = echarts.init(document.getElementById('class-info-chart'));
+ chart.setOption({
+ tooltip: { trigger: 'item' },
+ legend: { bottom: 10 },
+ series: [{
+ type: 'pie',
+ radius: ['40%', '70%'],
+ avoidLabelOverlap: false,
+ itemStyle: { borderRadius: 6, borderColor: '#fff', borderWidth: 2 },
+ label: { show: true, formatter: '{b}: {c} ({d}%)' },
+ data: [
+ { value: d.male_count, name: '男生', itemStyle: { color: '#1890ff' } },
+ { value: d.female_count, name: '女生', itemStyle: { color: '#eb2f96' } }
+ ]
+ }]
+ });
+ } catch (e) {
+ box.innerHTML = '请求失败,请检查后端服务是否启动
';
+ }
+}
+
+async function loadAllClassStats() {
+ try {
+ const res = await API.getAllClassStats();
+ if (res.code !== 200) { toast(res.message, 'error'); return; }
+ const data = res.data || [];
+
+ const tbody = document.querySelector('#all-class-table tbody');
+ if (!data.length) {
+ tbody.innerHTML = '| 暂无数据 |
';
+ } else {
+ tbody.innerHTML = data.map(r => `
+
+ | ${r.class_id} | ${r.class_name} |
+ ${r.total_count} | ${r.male_count} | ${r.female_count} |
+
`).join('');
+ }
+
+ const chart = echarts.init(document.getElementById('all-class-chart'));
+ chart.setOption({
+ tooltip: { trigger: 'axis' },
+ legend: { data: ['男生', '女生', '总人数'] },
+ grid: { left: 50, right: 30, top: 50, bottom: 80 },
+ xAxis: {
+ type: 'category',
+ data: data.map(d => d.class_name),
+ axisLabel: { rotate: 30 }
+ },
+ yAxis: { type: 'value' },
+ series: [
+ { name: '男生', type: 'bar', data: data.map(d => d.male_count), itemStyle: { color: '#1890ff' } },
+ { name: '女生', type: 'bar', data: data.map(d => d.female_count), itemStyle: { color: '#eb2f96' } },
+ { name: '总人数', type: 'bar', data: data.map(d => d.total_count), itemStyle: { color: '#52c41a' } }
+ ]
+ });
+ } catch (e) {
+ console.error(e);
+ }
+}
+
+async function searchScore() {
+ state.score.page = 1;
+ await loadScore();
+}
+
+async function loadScore() {
+ const s = state.score;
+ s.size = parseInt(document.getElementById('score-size').value) || 10;
+ const score = parseFloat(document.getElementById('score-val').value);
+ try {
+ const res = await API.getStudentsByScore(s.page, s.size, score);
+ if (res.code !== 200) { toast(res.message, 'error'); return; }
+ s.total = res.total; s.totalPages = res.total_pages;
+
+ const tbody = document.querySelector('#score-table tbody');
+ if (!res.data.length) {
+ tbody.innerHTML = '| 暂无数据 |
';
+ } else {
+ tbody.innerHTML = res.data.map(r =>
+ `| ${r.student_id} | ${r.student_name} | ${r.score} |
`
+ ).join('');
+ }
+ renderPagination(document.getElementById('score-pagination'), s, (p) => {
+ s.page = p; loadScore();
+ });
+ } catch (e) { toast('请求失败', 'error'); }
+}
+
+async function searchNoPass() {
+ state.nopass.page = 1;
+ await loadNoPass();
+}
+
+async function loadNoPass() {
+ const s = state.nopass;
+ s.size = parseInt(document.getElementById('nopass-size').value) || 10;
+ const fc = parseInt(document.getElementById('fail-count').value);
+ try {
+ const res = await API.getNoPassStudents(s.page, s.size, fc);
+ if (res.code !== 200) { toast(res.message, 'error'); return; }
+ s.total = res.total; s.totalPages = res.total_pages;
+
+ const tbody = document.querySelector('#nopass-table tbody');
+ if (!res.data.length) {
+ tbody.innerHTML = '| 暂无数据 |
';
+ } else {
+ tbody.innerHTML = res.data.map(r =>
+ `| ${r.student_id} | ${r.student_name} | ${r.fail_count} |
`
+ ).join('');
+ }
+ renderPagination(document.getElementById('nopass-pagination'), s, (p) => {
+ s.page = p; loadNoPass();
+ });
+ } catch (e) { toast('请求失败', 'error'); }
+}
+
+async function loadExamAvg() {
+ const s = state.examAvg;
+ s.size = parseInt(document.getElementById('exam-avg-size').value) || 10;
+ try {
+ const res = await API.getClassExamAvg(s.page, s.size);
+ if (res.code !== 200) { toast(res.message, 'error'); return; }
+ s.total = res.total; s.totalPages = res.total_pages;
+
+ const data = res.data || [];
+
+ const tbody = document.querySelector('#exam-avg-table tbody');
+ if (!data.length) {
+ tbody.innerHTML = '| 暂无数据 |
';
+ } else {
+ tbody.innerHTML = data.map(r =>
+ `| ${r.course_id} | ${r.class_id} | ${r.class_name} | ${r.avg_score.toFixed(2)} |
`
+ ).join('');
+ }
+
+ const chart = echarts.init(document.getElementById('exam-avg-chart'));
+ chart.setOption({
+ tooltip: { trigger: 'axis' },
+ grid: { left: 50, right: 30, top: 30, bottom: 80 },
+ xAxis: {
+ type: 'category',
+ data: data.map(d => d.class_name + '-' + d.course_id),
+ axisLabel: { rotate: 40 }
+ },
+ yAxis: { type: 'value', min: 0, max: 100 },
+ series: [{
+ type: 'bar',
+ data: data.map(d => d.avg_score.toFixed(2)),
+ itemStyle: {
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+ { offset: 0, color: '#2a5298' },
+ { offset: 1, color: '#1e3c72' }
+ ])
+ },
+ label: { show: true, position: 'top', formatter: '{c}' }
+ }]
+ });
+
+ renderPagination(document.getElementById('exam-avg-pagination'), s, (p) => {
+ s.page = p; loadExamAvg();
+ });
+ } catch (e) { toast('请求失败', 'error'); }
+}
+
+async function loadSalaryTop() {
+ const m = parseInt(document.getElementById('salary-n').value) || 5;
+ try {
+ const res = await API.getSalaryTop(m);
+ if (res.code !== 200) { toast(res.message, 'error'); return; }
+ const data = res.data || [];
+
+ const tbody = document.querySelector('#salary-table tbody');
+ if (!data.length) {
+ tbody.innerHTML = '| 暂无数据 |
';
+ } else {
+ tbody.innerHTML = data.map(r =>
+ `| ${r.student_name} | ${r.class_name} | ${r.company_name} |
+ ${formatDate(r.part_time)} | ${r.salary} |
`
+ ).join('');
+ }
+
+ const chart = echarts.init(document.getElementById('salary-chart'));
+ chart.setOption({
+ tooltip: { trigger: 'axis' },
+ grid: { left: 80, right: 30, top: 30, bottom: 30 },
+ xAxis: { type: 'value' },
+ yAxis: { type: 'category', data: data.map(d => d.student_name).reverse() },
+ series: [{
+ type: 'bar',
+ data: data.map(d => d.salary).reverse(),
+ itemStyle: {
+ color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
+ { offset: 0, color: '#fa709a' },
+ { offset: 1, color: '#fee140' }
+ ])
+ },
+ label: { show: true, position: 'right', formatter: '{c}' }
+ }]
+ });
+ } catch (e) { toast('请求失败', 'error'); }
+}
+
+async function loadTimeSize() {
+ const s = state.timeSize;
+ s.size = parseInt(document.getElementById('time-size').value) || 10;
+ try {
+ const res = await API.getTimeSize(s.page, s.size);
+ if (res.code !== 200) { toast(res.message, 'error'); return; }
+ s.total = res.total; s.totalPages = res.total_pages;
+
+ const tbody = document.querySelector('#time-table tbody');
+ if (!res.data.length) {
+ tbody.innerHTML = '| 暂无数据 |
';
+ } else {
+ tbody.innerHTML = res.data.map(r =>
+ `| ${r.student_id} | ${r.student_name} |
+ ${formatDate(r.resume_open_date)} |
+ ${formatDate(r.offer_date)} | ${r.time_size} |
`
+ ).join('');
+ }
+ renderPagination(document.getElementById('time-pagination'), s, (p) => {
+ s.page = p; loadTimeSize();
+ });
+ } catch (e) { toast('请求失败', 'error'); }
+}
+
+async function loadClassAvgTime() {
+ const s = state.classAvgTime;
+ s.size = parseInt(document.getElementById('class-avg-time-size').value) || 10;
+ try {
+ const res = await API.getClassAvgTimeSize(s.page, s.size);
+ if (res.code !== 200) { toast(res.message, 'error'); return; }
+ s.total = res.total; s.totalPages = res.total_pages;
+
+ const data = res.data || [];
+
+ const tbody = document.querySelector('#class-avg-time-table tbody');
+ if (!data.length) {
+ tbody.innerHTML = '| 暂无数据 |
';
+ } else {
+ tbody.innerHTML = data.map(r =>
+ `| ${r.class_id} | ${r.class_name} | ${r.avg_time_size.toFixed(1)} |
`
+ ).join('');
+ }
+
+ const chart = echarts.init(document.getElementById('class-avg-time-chart'));
+ chart.setOption({
+ tooltip: { trigger: 'axis' },
+ grid: { left: 50, right: 30, top: 30, bottom: 80 },
+ xAxis: {
+ type: 'category',
+ data: data.map(d => d.class_name),
+ axisLabel: { rotate: 30 }
+ },
+ yAxis: { type: 'value', name: '天数' },
+ series: [{
+ type: 'bar',
+ data: data.map(d => d.avg_time_size.toFixed(1)),
+ itemStyle: {
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+ { offset: 0, color: '#43e97b' },
+ { offset: 1, color: '#38f9d7' }
+ ])
+ },
+ label: { show: true, position: 'top', formatter: '{c}天' }
+ }]
+ });
+
+ renderPagination(document.getElementById('class-avg-time-pagination'), s, (p) => {
+ s.page = p; loadClassAvgTime();
+ });
+ } catch (e) { toast('请求失败', 'error'); }
+}
+
+function reloadExamAvg() { state.examAvg.page = 1; loadExamAvg(); }
+function reloadTimeSize() { state.timeSize.page = 1; loadTimeSize(); }
+function reloadClassAvgTime() { state.classAvgTime.page = 1; loadClassAvgTime(); }
+
+// ============ 启动 ============
+window.addEventListener('DOMContentLoaded', () => {
+ createPages();
+ renderSidebar();
+ window.searchAge = searchAge;
+ window.searchClassInfo = searchClassInfo;
+ window.searchScore = searchScore;
+ window.searchNoPass = searchNoPass;
+ window.loadSalaryTop = loadSalaryTop;
+ window.reloadExamAvg = reloadExamAvg;
+ window.reloadTimeSize = reloadTimeSize;
+ window.reloadClassAvgTime = reloadClassAvgTime;
+ window.addEventListener('resize', () => {
+ document.querySelectorAll('.chart-container').forEach(el => {
+ const inst = echarts.getInstanceByDom(el);
+ if (inst) inst.resize();
+ });
+ });
+});
+
+// 页面切换时也触发对应数据加载
+const origShowPage = showPage;
\ No newline at end of file
diff --git a/main.py b/main.py
index 4b09230..86eff53 100644
--- a/main.py
+++ b/main.py
@@ -1,4 +1,5 @@
from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
from database import engine, Base
from api.statistics_api import BasicInformationAPI
from model import statistics_model #不可以删
@@ -6,6 +7,13 @@ Base.metadata.create_all(engine) #创建所有的表
app = FastAPI()
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
app.include_router(BasicInformationAPI)
diff --git a/schema/statistics_schema.py b/schema/statistics_schema.py
index b7f4a64..8396b2a 100644
--- a/schema/statistics_schema.py
+++ b/schema/statistics_schema.py
@@ -101,4 +101,21 @@ class ResponseModel6(BaseModel):
message: str = 'ok'
total: int
total_pages: int
- data: List[ClassAvgTimeSize]
\ No newline at end of file
+ data: List[ClassAvgTimeSize]
+
+class ClassStats(BaseModel):
+ class_id: str
+ class_name: str
+ total_count: int
+ male_count: int
+ female_count: int
+
+class ResponseModel7(BaseModel):
+ code: int
+ message: str = 'ok'
+ data: ClassStats
+
+class ResponseModel8(BaseModel):
+ code: int
+ message: str = 'ok'
+ data: List[ClassStats]
\ No newline at end of file