From 1ff0782399f3efb3e3b96d6e9d39d77155a68665 Mon Sep 17 00:00:00 2001 From: Orangeeee Date: Wed, 23 Sep 2026 03:53:57 +0800 Subject: [PATCH] =?UTF-8?q?=E5=89=8D=E7=AB=AF=E6=B5=8B=E8=AF=95=E9=80=9A?= =?UTF-8?q?=E8=BF=87=EF=BC=8C=E9=83=A8=E5=88=86=E6=8E=A5=E5=8F=A3=E6=B2=A1?= =?UTF-8?q?=E6=9C=89=E6=95=B0=E6=8D=AE=EF=BC=8C=E8=BF=90=E8=A1=8C=E6=B2=A1?= =?UTF-8?q?=E9=97=AE=E9=A2=98=E4=BA=86=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- StatCalc/api/statcalc_api.py | 2 +- frontend/index.html | 335 ++++++++++++++++++++++++++++++++++- main.py | 10 ++ seed_data.py | 56 +++--- students/StudentsQuery.sql | 28 ++- 5 files changed, 393 insertions(+), 38 deletions(-) diff --git a/StatCalc/api/statcalc_api.py b/StatCalc/api/statcalc_api.py index 0f58740..ce6dc98 100644 --- a/StatCalc/api/statcalc_api.py +++ b/StatCalc/api/statcalc_api.py @@ -66,7 +66,7 @@ def salarytop5(db=Depends(get_db)): .filter(Employments.is_deleted == 0)\ .order_by(Employments.employment_salary.desc())\ .limit(5) .all() - return [{'学生姓名':i.sname,'学生班级编号':i.class_num,'就业时间':i.offer_recived_time,'公司':employment_company} for i,employment_company in q] + return [{'学生姓名':i.stuid,'学生班级编号':i.class_id,'就业时间':i.offer_recived_time,'公司':employment_company} for i,employment_company in q] @statcalc_api.get('/statcalc/job',summary='学生就业时长') def worktime(db=Depends(get_db)): diff --git a/frontend/index.html b/frontend/index.html index bf7bcfc..ae9a01e 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -668,6 +668,83 @@ 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%); @@ -817,6 +894,14 @@ 就业管理 + + + + + + + 统计分析 + @@ -1010,6 +1095,96 @@ + +
+

统计分析

+ +
+
+
👥
+
+

班级人数统计

+ +
+
+
+ +
+
📊
+
+

优秀成绩学生

+

所有考试都在80分以上

+
+ +
+
+ +
+
⚠️
+
+

不及格学生查询

+

两次以上不及格

+
+ +
+
+ +
+
📈
+
+

班级平均分

+

每次考试各班级平均分

+
+ +
+
+ +
+
🎓
+
+

就业薪资Top5

+

薪资最高的前5名学生

+
+ +
+
+ +
+
💼
+
+

就业时长统计

+

每位学生的就业时长

+
+ +
+
+ +
+
⏱️
+
+

平均就业时长

+

所有就业学生的平均时长

+
+ +
+
+ +
+
👴
+
+

30岁以上学员

+

年龄超过30岁的学生

+
+ +
+
+
+
+
@@ -1312,6 +1487,7 @@ 'scores': '成绩查询', 'teachers': '教师管理', 'employment': '就业管理', + 'stats': '统计分析', 'profile': '个人中心' }; return names[page] || page; @@ -1320,11 +1496,18 @@ // ── 学生管理 ────────────────────────────────────── async function loadStudents() { try { + console.log('[DEBUG] 开始加载学生...'); const res = await fetch(`${API_BASE}/students`); + console.log('[DEBUG] 响应状态:', res.status); if (res.ok) { - studentData = await res.json(); + 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); @@ -1338,7 +1521,7 @@ ${s.num} ${s.name} - ${s.sex === 'm' ? '男' : s.sex === 'f' ? '女' : s.sex} + ${s.sex || '-'} 班级${s.class_id} ${s.specialty || '-'} ${s.phone} @@ -1477,11 +1660,17 @@ // ── 教师管理 ────────────────────────────────────── 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); @@ -1494,7 +1683,7 @@ ${t.id} ${t.name} - ${t.sex === 'm' ? '男' : t.sex === 'f' ? '女' : t.sex} + ${t.sex || '-'} ${t.specialty || '-'} ${t.work_experience || '-'} ${t.collage || '-'} @@ -1564,10 +1753,17 @@ // ── 就业管理 ────────────────────────────────────── async function loadEmployment() { try { + console.log('[DEBUG] 开始加载就业...'); const res = await fetch(`${API_BASE}/emp`); + console.log('[DEBUG] 响应状态:', res.status); if (res.ok) { - employmentData = await res.json(); + 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); @@ -1778,6 +1974,137 @@ 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 => `
${Object.values(row).join(' | ')}
`).join('') + : '
暂无数据
'; + } + } 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 = `
${data.detail}
`; + } else { + resultDiv.innerHTML = data.length > 0 + ? data.map(row => `
${row['students.name']} - ${row.scores.score}分
`).join('') + : '
暂无数据
'; + } + } + } 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 = `
${data.detail}
`; + } else { + resultDiv.innerHTML = data.length > 0 + ? data.map(row => `
${row['students.name']} | ${row['class_management.name']} | 不及格${row.scores.score}分
`).join('') + : '
无不及格学生
'; + } + } + } 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 => `
${row.class_name}班 - 平均分: ${row.avg_score}
`).join('') + : '
暂无数据
'; + } + } 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) => `
${i+1}. ${row['学生姓名']} - ${row['公司']} - ¥${row['employment_salary']}
`).join('') + : '
暂无就业数据
'; + } + } 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 => `
${row['学生姓名']} - ${row['就业时常']}
`).join('') + : '
暂无数据
'; + } + } 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 = ` +
就业人数: ${data['人数']}人
+
平均就业时长: ${data['平均就业时常(天)']}天
+ `; + } + } 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 => `
${row.name} - ${row.age}岁 - ${row.num}
`).join('') + : '
无30岁以上学员
'; + } + } catch (err) { + console.error('加载年龄统计失败:', err); + } + } + // 点击模态框外部关闭 document.querySelectorAll('.modal-overlay').forEach(overlay => { overlay.addEventListener('click', (e) => { diff --git a/main.py b/main.py index bf703ed..f182df7 100644 --- a/main.py +++ b/main.py @@ -9,6 +9,16 @@ from databases import * from seed_data import seed_test_data app = FastAPI(title="学生管理系统") +from fastapi.middleware.cors import CORSMiddleware + +# 配置CORS - 接前端 +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # 允许所有来源 + allow_credentials=True, + allow_methods=["*"], # 允许所有方法 + allow_headers=["*"], # 允许所有头部 +) app.include_router(Classes_API,tags=["班级管理模块"]) app.include_router(Scores_API,tags=["学生考核成绩管理模块"]) diff --git a/seed_data.py b/seed_data.py index b8d85f4..add3706 100644 --- a/seed_data.py +++ b/seed_data.py @@ -144,6 +144,8 @@ def seed_test_data(): enrollment = date(2024, 9, 1) if class_idx != 2 else date(2025, 9, 1) graduate = date(2028, 6, 30) if class_idx != 2 else date(2029, 6, 30) phone = f"1390000{1000 + i}" + # 前3个学生年龄设大一些,用于30岁以上统计测试 + age = 30 + i if i < 3 else 18 + (i % 4) db.execute(text(""" INSERT INTO students (num, name, age, sex, home_Place, college, specialty, enrollment_time, graduate_time, @@ -154,7 +156,7 @@ def seed_test_data(): """), { "num": f"2024{1001 + i}", "name": first_names[i], - "age": 18 + (i % 4), + "age": age, "sex": "m" if i % 2 == 0 else "f", "home_Place": home_places[i % 8], "college": colleges[i % 8], @@ -176,10 +178,20 @@ def seed_test_data(): for idx, st in enumerate(all_students): for subj_idx, sub in enumerate(all_subjects): - score_val = 60 + (idx * 7 + subj_idx * 13) % 41 - # 前3名学生部分课程不及格(供统计接口测试) - if idx < 3 and subj_idx < 2: - score_val = 30 + (idx * 5) % 25 + # 前3名学生设大年龄(用于30岁以上统计) + # 前2名学生所有科目80分以上(用于优秀成绩统计) + # 最后2名学生部分科目不及格(用于不及格统计) + if idx < 2: + # 优秀学生:所有科目85-95分 + score_val = 85 + (idx * 5 + subj_idx * 3) % 11 + elif idx >= 18: + # 不及格学生:部分科目50-60分 + if subj_idx < 3: + score_val = 50 + (idx * 3 + subj_idx) % 11 + else: + score_val = 75 + (idx * 2 + subj_idx) % 16 + else: + score_val = 60 + (idx * 7 + subj_idx * 13) % 41 db.execute(text(""" INSERT INTO scores (sid, cid, num, score, t_subject, is_deleted) VALUES (:sid, :cid, 1, :score, :t_subject, 0) @@ -210,24 +222,18 @@ def seed_test_data(): company_ids.append(result.lastrowid) # ── 8. 就业信息 ─────────────────────────────────── - employment_data = [ - (all_students[0][0], all_students[0][1], company_ids[0], - 35000, date(2028, 3, 1), date(2028, 4, 15)), - (all_students[1][0], all_students[1][1], company_ids[1], - 32000, date(2028, 3, 1), date(2028, 5, 1)), - (all_students[2][0], all_students[2][1], company_ids[2], - 30000, date(2028, 3, 1), date(2028, 4, 20)), - (all_students[3][0], all_students[3][1], company_ids[3], - 28000, date(2028, 3, 1), date(2028, 5, 10)), - (all_students[4][0], all_students[4][1], company_ids[4], - 25000, date(2028, 3, 1), date(2028, 6, 1)), - (all_students[5][0], all_students[5][1], company_ids[0], - 33000, date(2028, 3, 1), date(2028, 4, 25)), - (all_students[6][0], all_students[6][1], company_ids[1], - 29000, date(2028, 3, 1), None), - (all_students[7][0], all_students[7][1], company_ids[2], - 31000, date(2028, 3, 1), date(2028, 5, 5)), - ] + # 添加更多就业记录,确保统计接口有数据 + employment_data = [] + for i in range(min(15, len(all_students))): + company_idx = i % len(company_ids) + address_idx = i % len(address_ids) + salary = 25000 + (i * 2000) % 15000 + # 前10个学生有offer时间,后5个没有(测试不同情况) + offer_time = date(2028, 4, 15 + i) if i < 10 else None + employment_data.append(( + all_students[i][0], all_students[i][1], company_ids[company_idx], + address_ids[address_idx], salary, date(2028, 3, 1), offer_time + )) for idx, emp in enumerate(employment_data): db.execute(text(""" INSERT INTO Employments (stuid, class_id, company_id, address_id, @@ -237,8 +243,8 @@ def seed_test_data(): :open_time, :offer_time, 0) """), { "stuid": emp[0], "class_id": emp[1], - "company_id": emp[2], "addr_id": address_ids[idx % len(address_ids)], - "salary": emp[3], "open_time": emp[4], "offer_time": emp[5], + "company_id": emp[2], "addr_id": emp[3], + "salary": emp[4], "open_time": emp[5], "offer_time": emp[6], }) db.commit() diff --git a/students/StudentsQuery.sql b/students/StudentsQuery.sql index b8baef8..9cd99fe 100644 --- a/students/StudentsQuery.sql +++ b/students/StudentsQuery.sql @@ -14,14 +14,26 @@ select * from students limit 200; select * from scores limit 200; +select * from address limit 200; + +select * from classes limit 200; + +select * from company limit 200; + +select * from employments limit 200; + +select * from teachers limit 200; + +select * from subject limit 200; + desc students; -select * -from cast(student_manage_system as BINARY) -where age >30 -join on -group by -having -order by -limit \ No newline at end of file +# select * +# from cast(student_manage_system as BINARY) +# where age >30 +# join on +# group by +# having +# order by +# limit \ No newline at end of file