Spaces:
Running
Running
| <html lang="zh-CN"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>学生成绩比较系统</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;600;700&display=swap" rel="stylesheet"> | |
| <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <style> | |
| :root { | |
| --primary-color: #4361ee; | |
| --primary-hover: #3a51db; | |
| --secondary-color: #6c757d; | |
| --success-color: #28a745; | |
| --danger-color: #dc3545; | |
| --warning-color: #ffc107; | |
| --info-color: #0dcaf0; | |
| --light-color: #f8f9fa; | |
| --dark-color: #343a40; | |
| --white: #ffffff; | |
| --body-bg: #f0f2f5; | |
| --card-bg: var(--white); | |
| --border-radius: 12px; | |
| --box-shadow: 0 6px 20px rgba(0, 0, 0, 0.07); | |
| --transition: all 0.3s ease; | |
| /* 新增对比颜色 */ | |
| --student-a-color: #4361ee; | |
| --student-a-light: rgba(67, 97, 238, 0.2); | |
| --student-b-color: #ff6b6b; | |
| --student-b-light: rgba(255, 107, 107, 0.2); | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Nunito', Arial, sans-serif; | |
| background-color: var(--body-bg); | |
| color: #333; | |
| line-height: 1.6; | |
| padding: 0; | |
| margin: 0; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 20px auto; | |
| padding: 0 15px; | |
| } | |
| .header { | |
| text-align: center; | |
| padding: 30px 0 20px; | |
| } | |
| .header h1 { | |
| color: var(--primary-color); | |
| font-size: 2.4rem; | |
| margin-bottom: 10px; | |
| font-weight: 700; | |
| text-shadow: 1px 1px 2px rgba(0,0,0,0.1); | |
| } | |
| .header p { | |
| color: var(--secondary-color); | |
| font-size: 1.1rem; | |
| } | |
| .nav-links { | |
| text-align: center; | |
| margin-bottom: 20px; | |
| } | |
| .nav-links a { | |
| color: var(--primary-color); | |
| text-decoration: none; | |
| margin: 0 15px; | |
| font-weight: 600; | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 5px; | |
| padding: 8px 16px; | |
| border-radius: 20px; | |
| transition: var(--transition); | |
| } | |
| .nav-links a:hover { | |
| background-color: rgba(67, 97, 238, 0.1); | |
| } | |
| .card { | |
| background-color: var(--card-bg); | |
| border-radius: var(--border-radius); | |
| box-shadow: var(--box-shadow); | |
| padding: 25px; | |
| margin-bottom: 30px; | |
| transition: var(--transition); | |
| } | |
| .student-selection { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 30px; | |
| margin-bottom: 30px; | |
| } | |
| .student-panel { | |
| border: 2px solid #e1e5ea; | |
| border-radius: var(--border-radius); | |
| padding: 20px; | |
| position: relative; | |
| transition: all 0.3s ease; | |
| } | |
| .student-panel.active-a { | |
| border-color: var(--student-a-color); | |
| background-color: rgba(67, 97, 238, 0.05); | |
| } | |
| .student-panel.active-b { | |
| border-color: var(--student-b-color); | |
| background-color: rgba(255, 107, 107, 0.05); | |
| } | |
| .student-panel h3 { | |
| margin-bottom: 20px; | |
| text-align: center; | |
| } | |
| .student-panel h3.student-a { | |
| color: var(--student-a-color); | |
| } | |
| .student-panel h3.student-b { | |
| color: var(--student-b-color); | |
| } | |
| .search-container { | |
| position: relative; | |
| margin-bottom: 15px; | |
| } | |
| .search-input { | |
| width: 100%; | |
| padding: 12px 20px; | |
| padding-left: 45px; | |
| border: 2px solid #e1e5ea; | |
| border-radius: 25px; | |
| font-size: 14px; | |
| transition: var(--transition); | |
| outline: none; | |
| } | |
| .search-input:focus { | |
| border-color: var(--primary-color); | |
| box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.2); | |
| } | |
| .search-icon { | |
| position: absolute; | |
| left: 15px; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| color: #aab0b7; | |
| font-size: 18px; | |
| } | |
| .student-list { | |
| background-color: white; | |
| border-radius: 8px; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.1); | |
| margin-top: 10px; | |
| max-height: 200px; | |
| overflow-y: auto; | |
| display: none; | |
| position: absolute; | |
| top: 100%; | |
| left: 0; | |
| right: 0; | |
| z-index: 1000; | |
| -webkit-overflow-scrolling: touch; | |
| touch-action: pan-y; | |
| overscroll-behavior-y: contain; | |
| scrollbar-width: thin; | |
| scrollbar-color: #c1c1c1 #f1f1f1; | |
| } | |
| .student-list::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| .student-list::-webkit-scrollbar-track { | |
| background: #f1f1f1; | |
| border-radius: 3px; | |
| } | |
| .student-list::-webkit-scrollbar-thumb { | |
| background: #c1c1c1; | |
| border-radius: 3px; | |
| } | |
| .student-list::-webkit-scrollbar-thumb:hover { | |
| background: #a8a8a8; | |
| } | |
| .student-item { | |
| padding: 12px 15px; | |
| border-bottom: 1px solid #f0f0f0; | |
| cursor: pointer; | |
| transition: background-color 0.2s; | |
| -webkit-tap-highlight-color: transparent; | |
| } | |
| .student-item:hover { | |
| background-color: #f8faff; | |
| } | |
| .student-item:last-child { | |
| border-bottom: none; | |
| } | |
| .student-name { | |
| font-weight: 600; | |
| color: #333; | |
| margin-bottom: 3px; | |
| pointer-events: none; | |
| } | |
| .student-id { | |
| color: #777; | |
| font-size: 12px; | |
| pointer-events: none; | |
| } | |
| .selected-student { | |
| background-color: #f8faff; | |
| border: 1px solid #e1e5ea; | |
| border-radius: 8px; | |
| padding: 15px; | |
| margin-top: 10px; | |
| display: none; | |
| transition: all 0.3s ease; | |
| animation: fadeIn 0.5s ease; | |
| } | |
| .selected-student.student-a { | |
| border-color: var(--student-a-color); | |
| background-color: var(--student-a-light); | |
| } | |
| .selected-student.student-b { | |
| border-color: var(--student-b-color); | |
| background-color: var(--student-b-light); | |
| } | |
| .selected-student.show { | |
| display: block; | |
| } | |
| .compare-btn { | |
| background-color: var(--primary-color); | |
| color: white; | |
| border: none; | |
| border-radius: 25px; | |
| padding: 12px 30px; | |
| font-size: 16px; | |
| cursor: pointer; | |
| transition: var(--transition); | |
| font-weight: 600; | |
| display: block; | |
| margin: 20px auto; | |
| min-width: 150px; | |
| } | |
| .compare-btn:hover:not(:disabled) { | |
| background-color: var(--primary-hover); | |
| transform: translateY(-2px); | |
| } | |
| .compare-btn:disabled { | |
| background-color: #ccc; | |
| cursor: not-allowed; | |
| } | |
| .comparison-section { | |
| display: none; | |
| } | |
| .comparison-section.show { | |
| display: block; | |
| animation: fadeIn 0.5s ease; | |
| } | |
| /* 优化考试选择部分 */ | |
| .exam-selector { | |
| display: flex; | |
| gap: 10px; | |
| margin-bottom: 20px; | |
| flex-wrap: wrap; | |
| background-color: #f8f9fa; | |
| border-radius: 12px; | |
| padding: 15px; | |
| max-height: 200px; | |
| overflow-y: auto; | |
| } | |
| .exam-btn { | |
| padding: 8px 16px; | |
| border: 2px solid #e1e5ea; | |
| background: white; | |
| border-radius: 20px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| font-size: 14px; | |
| font-weight: 600; | |
| box-shadow: 0 2px 5px rgba(0,0,0,0.05); | |
| margin-bottom: 5px; | |
| } | |
| .exam-btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 5px 10px rgba(0,0,0,0.1); | |
| } | |
| .exam-btn.active { | |
| border-color: var(--primary-color); | |
| background-color: var(--primary-color); | |
| color: white; | |
| } | |
| .comparison-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| margin: 20px 0; | |
| border-radius: 8px; | |
| overflow: hidden; | |
| box-shadow: 0 0 10px rgba(0,0,0,0.03); | |
| table-layout: fixed; /* 固定表格布局 */ | |
| } | |
| .comparison-table th, | |
| .comparison-table td { | |
| border: 1px solid #eaecef; | |
| padding: 12px 15px; | |
| text-align: center; | |
| word-wrap: break-word; /* 允许长内容换行 */ | |
| overflow-wrap: break-word; | |
| } | |
| .comparison-table th { | |
| background-color: #f6f8fa; | |
| font-weight: 600; | |
| color: #444; | |
| position: sticky; | |
| top: 0; | |
| z-index: 1; | |
| } | |
| .comparison-table tr:nth-child(even) { | |
| background-color: #fafbff; | |
| } | |
| .better-score { | |
| background-color: rgba(40, 167, 69, 0.05); /* 降低透明度防止色彩干扰 */ | |
| color: var(--success-color); | |
| font-weight: 600; | |
| position: relative; | |
| border-left: 3px solid var(--success-color); /* 添加边框增强视觉效果 */ | |
| } | |
| .worse-score { | |
| background-color: rgba(220, 53, 69, 0.05); /* 降低透明度防止色彩干扰 */ | |
| color: var(--danger-color); | |
| font-weight: 600; | |
| position: relative; | |
| border-left: 3px solid var(--danger-color); /* 添加边框增强视觉效果 */ | |
| } | |
| /* 排名差距指示器 */ | |
| .rank-diff-indicator { | |
| height: 4px; | |
| background-color: #e9ecef; | |
| border-radius: 2px; | |
| margin-top: 5px; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .rank-diff-bar { | |
| height: 100%; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| border-radius: 2px; | |
| transition: width 0.6s ease; | |
| } | |
| .rank-diff-bar.better { | |
| background-color: var(--success-color); | |
| } | |
| .rank-diff-bar.worse { | |
| background-color: var(--danger-color); | |
| } | |
| .chart-container { | |
| width: 100%; | |
| height: 400px; | |
| margin: 20px 0; | |
| position: relative; | |
| } | |
| /* 图表容器布局 */ | |
| .charts-row { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 30px; | |
| margin: 30px 0; | |
| } | |
| /* 移动设备优化 */ | |
| @media (max-width: 992px) { | |
| .charts-row { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| @media (max-width: 768px) { | |
| .container { | |
| padding: 0 10px; | |
| margin: 10px auto; | |
| } | |
| .header h1 { | |
| font-size: 1.8rem; | |
| margin-bottom: 8px; | |
| } | |
| .header p { | |
| font-size: 1rem; | |
| } | |
| .nav-links { | |
| margin-bottom: 15px; | |
| } | |
| .nav-links a { | |
| margin: 0 8px; | |
| padding: 6px 12px; | |
| font-size: 14px; | |
| } | |
| .card { | |
| padding: 20px 15px; | |
| margin-bottom: 20px; | |
| } | |
| /* 学生选择面板移动端布局 */ | |
| .student-selection { | |
| grid-template-columns: 1fr; | |
| gap: 20px; | |
| margin-bottom: 20px; | |
| } | |
| .student-panel { | |
| padding: 15px; | |
| } | |
| .student-panel h3 { | |
| font-size: 1.2rem; | |
| margin-bottom: 15px; | |
| } | |
| .search-input { | |
| padding: 10px 18px; | |
| padding-left: 40px; | |
| font-size: 16px; /* 防止iOS缩放 */ | |
| border-radius: 20px; | |
| } | |
| .search-icon { | |
| left: 12px; | |
| } | |
| /* 学生列表移动端优化 */ | |
| .student-list { | |
| max-height: 250px; /* 增加高度 */ | |
| -webkit-overflow-scrolling: touch; | |
| overflow-y: auto; | |
| position: absolute; /* 改回 absolute */ | |
| top: 100%; | |
| left: 0; | |
| right: 0; | |
| z-index: 10000; /* 提高层级 */ | |
| background-color: white; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.15); | |
| /* 修复移动设备滚动问题 */ | |
| touch-action: pan-y; | |
| overscroll-behavior: contain; | |
| } | |
| .student-item { | |
| padding: 10px 12px; | |
| } | |
| .student-name { | |
| font-size: 14px; | |
| } | |
| .student-id { | |
| font-size: 11px; | |
| } | |
| .selected-student { | |
| padding: 12px; | |
| margin-top: 8px; | |
| } | |
| .compare-btn { | |
| padding: 10px 25px; | |
| font-size: 15px; | |
| margin: 15px auto; | |
| } | |
| /* 考试选择器移动端优化 */ | |
| .exam-selector { | |
| max-height: 120px; | |
| padding: 12px; | |
| gap: 8px; | |
| } | |
| .exam-btn { | |
| padding: 6px 12px; | |
| font-size: 13px; | |
| margin-bottom: 4px; | |
| } | |
| /* 对比表格移动端优化 */ | |
| .comparison-table { | |
| font-size: 12px; | |
| margin: 15px 0; | |
| } | |
| .comparison-table th, | |
| .comparison-table td { | |
| padding: 8px 6px; | |
| word-wrap: break-word; | |
| max-width: 100px; | |
| } | |
| .comparison-table th { | |
| font-size: 11px; | |
| } | |
| /* 亮点卡片移动端优化 */ | |
| .comparison-highlights { | |
| grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); | |
| gap: 15px; | |
| margin: 20px 0; | |
| } | |
| .highlight-card { | |
| padding: 15px; | |
| } | |
| .highlight-title { | |
| font-size: 0.8rem; | |
| } | |
| .highlight-value { | |
| font-size: 1.4rem; | |
| } | |
| .highlight-sub { | |
| font-size: 0.8rem; | |
| } | |
| /* 图表容器移动端优化 */ | |
| .chart-card { | |
| height: 300px; | |
| padding: 15px; | |
| overflow: hidden; /* 防止内容溢出 */ | |
| } | |
| .chart-card h4 { | |
| font-size: 1rem; | |
| margin-bottom: 15px; | |
| } | |
| .chart-area { | |
| min-height: 200px; | |
| max-height: 220px; | |
| width: 100%; | |
| overflow: hidden; /* 防止图表超出边界 */ | |
| position: relative; | |
| } | |
| /* 强制图表在移动设备上响应式缩放 */ | |
| .chart-area canvas { | |
| max-width: 100% ; | |
| max-height: 100% ; | |
| width: auto ; | |
| height: auto ; | |
| } | |
| .chart-legend { | |
| margin-top: 10px; | |
| gap: 15px; | |
| } | |
| .legend-item { | |
| font-size: 12px; | |
| } | |
| /* 标签按钮移动端优化 */ | |
| .tabs-header { | |
| margin-bottom: 15px; | |
| } | |
| .tab-button { | |
| padding: 10px 15px; | |
| font-size: 14px; | |
| } | |
| /* 科目选择器移动端优化 */ | |
| .subject-toggles { | |
| gap: 6px; | |
| margin: 12px 0; | |
| } | |
| .subject-toggle { | |
| padding: 5px 10px; | |
| font-size: 12px; | |
| } | |
| /* 排名差距指示器移动端优化 */ | |
| .rank-diff-indicator { | |
| height: 3px; | |
| margin-top: 3px; | |
| } | |
| } | |
| @media (max-width: 480px) { | |
| .container { | |
| padding: 0 8px; | |
| margin: 8px auto; | |
| } | |
| .header { | |
| padding: 20px 0 15px; | |
| } | |
| .header h1 { | |
| font-size: 1.5rem; | |
| } | |
| .header p { | |
| font-size: 0.9rem; | |
| } | |
| .card { | |
| padding: 15px 10px; | |
| margin-bottom: 15px; | |
| } | |
| .student-panel { | |
| padding: 12px; | |
| } | |
| .student-panel h3 { | |
| font-size: 1.1rem; | |
| margin-bottom: 12px; | |
| } | |
| .search-input { | |
| padding: 8px 15px; | |
| padding-left: 35px; | |
| font-size: 16px; | |
| } | |
| .search-icon { | |
| left: 10px; | |
| font-size: 16px; | |
| } | |
| /* 更小屏幕的学生列表优化 */ | |
| .student-list { | |
| max-height: 200px; | |
| /* 强化移动设备滚动支持 */ | |
| overflow-y: auto; | |
| -webkit-overflow-scrolling: touch; | |
| touch-action: pan-y; | |
| overscroll-behavior-y: contain; | |
| position: absolute; | |
| z-index: 10000; | |
| } | |
| .student-item { | |
| padding: 8px 10px; | |
| } | |
| .student-name { | |
| font-size: 13px; | |
| } | |
| .student-id { | |
| font-size: 10px; | |
| } | |
| .selected-student { | |
| padding: 10px; | |
| } | |
| .compare-btn { | |
| padding: 8px 20px; | |
| font-size: 14px; | |
| min-width: 120px; | |
| } | |
| /* 超小屏幕表格优化 */ | |
| .comparison-table { | |
| font-size: 10px; | |
| display: block; | |
| overflow-x: auto; | |
| white-space: nowrap; | |
| -webkit-overflow-scrolling: touch; | |
| } | |
| .comparison-table thead, | |
| .comparison-table tbody, | |
| .comparison-table th, | |
| .comparison-table td, | |
| .comparison-table tr { | |
| display: block; | |
| } | |
| .comparison-table thead tr { | |
| position: absolute; | |
| top: -9999px; | |
| left: -9999px; | |
| } | |
| .comparison-table tr { | |
| border: 1px solid #ccc; | |
| margin-bottom: 10px; | |
| padding: 10px; | |
| border-radius: 8px; | |
| background: white; | |
| } | |
| .comparison-table td { | |
| border: none; | |
| position: relative; | |
| padding: 8px 8px 8px 50%; | |
| text-align: left; | |
| white-space: normal; | |
| max-width: none; | |
| } | |
| .comparison-table td:before { | |
| content: attr(data-label) ": "; | |
| position: absolute; | |
| left: 6px; | |
| width: 45%; | |
| padding-right: 10px; | |
| white-space: nowrap; | |
| font-weight: 600; | |
| color: #333; | |
| } | |
| /* 亮点卡片超小屏幕优化 */ | |
| .comparison-highlights { | |
| grid-template-columns: 1fr 1fr; | |
| gap: 10px; | |
| margin: 15px 0; | |
| } | |
| .highlight-card { | |
| padding: 12px; | |
| } | |
| .highlight-title { | |
| font-size: 0.75rem; | |
| } | |
| .highlight-value { | |
| font-size: 1.2rem; | |
| } | |
| .highlight-sub { | |
| font-size: 0.75rem; | |
| } | |
| /* 图表容器超小屏幕优化 */ | |
| .chart-card { | |
| height: 250px; | |
| padding: 12px; | |
| } | |
| .chart-card h4 { | |
| font-size: 0.9rem; | |
| margin-bottom: 12px; | |
| } | |
| .chart-area { | |
| min-height: 160px; | |
| max-height: 180px; | |
| } | |
| .chart-legend { | |
| margin-top: 8px; | |
| gap: 10px; | |
| } | |
| .legend-item { | |
| font-size: 11px; | |
| } | |
| .legend-color { | |
| width: 8px; | |
| height: 8px; | |
| } | |
| /* 标签优化 */ | |
| .tab-button { | |
| padding: 8px 10px; | |
| font-size: 12px; | |
| } | |
| /* 科目选择器超小屏幕优化 */ | |
| .subject-toggle { | |
| padding: 4px 8px; | |
| font-size: 11px; | |
| } | |
| /* 考试选择器超小屏幕优化 */ | |
| .exam-selector { | |
| max-height: 100px; | |
| padding: 10px; | |
| } | |
| .exam-btn { | |
| padding: 5px 10px; | |
| font-size: 12px; | |
| } | |
| } | |
| /* 触摸设备滚动优化 */ | |
| @media (pointer: coarse) { | |
| .student-list, | |
| .exam-selector, | |
| .comparison-table { | |
| -webkit-overflow-scrolling: touch; | |
| scrollbar-width: thin; | |
| } | |
| .student-list::-webkit-scrollbar, | |
| .exam-selector::-webkit-scrollbar { | |
| width: 4px; | |
| } | |
| .student-list::-webkit-scrollbar-track, | |
| .exam-selector::-webkit-scrollbar-track { | |
| background: #f1f1f1; | |
| border-radius: 2px; | |
| } | |
| .student-list::-webkit-scrollbar-thumb, | |
| .exam-selector::-webkit-scrollbar-thumb { | |
| background: #c1c1c1; | |
| border-radius: 2px; | |
| } | |
| .student-list::-webkit-scrollbar-thumb:hover, | |
| .exam-selector::-webkit-scrollbar-thumb:hover { | |
| background: #a8a8a8; | |
| } | |
| } | |
| .chart-card { | |
| background-color: var(--white); | |
| border-radius: var(--border-radius); | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.08); | |
| padding: 20px; | |
| height: 400px; /* 降低高度,从450px改为400px */ | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .chart-card h4 { | |
| margin: 0 0 20px 0; | |
| color: var(--dark-color); | |
| font-size: 1.1rem; | |
| text-align: center; | |
| } | |
| .chart-area { | |
| flex: 1; | |
| position: relative; | |
| min-height: 280px; /* 添加最小高度确保图表显示 */ | |
| max-height: 320px; /* 限制最大高度 */ | |
| width: 100%; | |
| overflow: hidden; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| /* 图例样式 */ | |
| .chart-legend { | |
| display: flex; | |
| justify-content: center; | |
| margin-top: 15px; | |
| gap: 20px; | |
| } | |
| .legend-item { | |
| display: flex; | |
| align-items: center; | |
| font-size: 14px; | |
| } | |
| .legend-color { | |
| width: 12px; | |
| height: 12px; | |
| border-radius: 50%; | |
| margin-right: 5px; | |
| } | |
| /* 动画效果 */ | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| @keyframes pulse { | |
| 0% { transform: scale(1); } | |
| 50% { transform: scale(1.05); } | |
| 100% { transform: scale(1); } | |
| } | |
| .comparison-highlights { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | |
| gap: 20px; | |
| margin: 30px 0; | |
| } | |
| .highlight-card { | |
| background-color: var(--white); | |
| border-radius: var(--border-radius); | |
| box-shadow: 0 4px 15px rgba(0,0,0,0.05); | |
| padding: 20px; | |
| text-align: center; | |
| transition: all 0.3s ease; | |
| } | |
| .highlight-card:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 8px 25px rgba(0,0,0,0.1); | |
| } | |
| .highlight-title { | |
| font-size: 0.9rem; | |
| color: var(--secondary-color); | |
| margin-bottom: 10px; | |
| } | |
| .highlight-value { | |
| font-size: 2rem; | |
| font-weight: 700; | |
| margin-bottom: 5px; | |
| } | |
| .highlight-sub { | |
| font-size: 0.9rem; | |
| color: var(--secondary-color); | |
| margin-bottom: 15px; | |
| } | |
| .highlight-card.student-a .highlight-value { | |
| color: var(--student-a-color); | |
| } | |
| .highlight-card.student-b .highlight-value { | |
| color: var(--student-b-color); | |
| } | |
| /* 科目选择器 */ | |
| .subject-toggles { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| margin: 15px 0; | |
| justify-content: center; | |
| } | |
| .subject-toggle { | |
| padding: 6px 12px; | |
| border-radius: 20px; | |
| font-size: 13px; | |
| background-color: #f0f2f8; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| border: 1px solid transparent; | |
| } | |
| .subject-toggle:hover { | |
| background-color: #e9ecf1; | |
| } | |
| .subject-toggle.active { | |
| background-color: var(--primary-color); | |
| color: white; | |
| } | |
| .tab-container { | |
| margin: 20px 0; | |
| } | |
| .tabs-header { | |
| display: flex; | |
| border-bottom: 2px solid #eaecef; | |
| margin-bottom: 20px; | |
| } | |
| .tab-button { | |
| padding: 12px 20px; | |
| background: transparent; | |
| border: none; | |
| cursor: pointer; | |
| font-weight: 600; | |
| color: var(--secondary-color); | |
| position: relative; | |
| transition: all 0.2s ease; | |
| } | |
| .tab-button:after { | |
| content: ''; | |
| position: absolute; | |
| bottom: -2px; | |
| left: 0; | |
| width: 100%; | |
| height: 3px; | |
| background-color: var(--primary-color); | |
| transform: scaleX(0); | |
| transition: transform 0.3s ease; | |
| } | |
| .tab-button.active { | |
| color: var(--primary-color); | |
| } | |
| .tab-button.active:after { | |
| transform: scaleX(1); | |
| } | |
| .tab-content { | |
| display: none; | |
| animation: fadeIn 0.4s ease; | |
| } | |
| .tab-content.active { | |
| display: block; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>学生成绩比较系统</h1> | |
| <p>选择两名学生进行成绩对比分析</p> | |
| </div> | |
| <div class="nav-links"> | |
| <a href="/"><span class="material-icons">home</span>返回查询页面</a> | |
| </div> | |
| <div class="card"> | |
| <div class="student-selection"> | |
| <div class="student-panel" id="student-panel-a"> | |
| <h3 class="student-a">学生 A</h3> | |
| <div class="search-container"> | |
| <span class="material-icons search-icon">search</span> | |
| <input type="text" id="search-input-a" class="search-input" placeholder="输入学生姓名搜索..."> | |
| <div class="student-list" id="student-list-a"></div> | |
| </div> | |
| <div class="selected-student student-a" id="selected-student-a"></div> | |
| </div> | |
| <div class="student-panel" id="student-panel-b"> | |
| <h3 class="student-b">学生 B</h3> | |
| <div class="search-container"> | |
| <span class="material-icons search-icon">search</span> | |
| <input type="text" id="search-input-b" class="search-input" placeholder="输入学生姓名搜索..."> | |
| <div class="student-list" id="student-list-b"></div> | |
| </div> | |
| <div class="selected-student student-b" id="selected-student-b"></div> | |
| </div> | |
| </div> | |
| <button id="compare-btn" class="compare-btn" onclick="startComparison()" disabled>开始比较</button> | |
| </div> | |
| <!-- 比较结果区域 --> | |
| <div class="comparison-section" id="comparison-section"> | |
| <div class="card"> | |
| <h3>考试选择</h3> | |
| <div class="exam-selector" id="exam-selector"> | |
| <!-- 动态生成考试选择按钮 --> | |
| </div> | |
| </div> | |
| <!-- 主要比较结果卡片 --> | |
| <div class="card" id="comparison-results"> | |
| <!-- 比较结果将在这里显示 --> | |
| </div> | |
| <!-- 可视化图表区域 --> | |
| <div class="card"> | |
| <div class="tab-container"> | |
| <div class="tabs-header"> | |
| <button class="tab-button active" data-tab="tab-charts">综合图表分析</button> | |
| <button class="tab-button" data-tab="tab-trends">成绩趋势对比</button> | |
| </div> | |
| <!-- 图表标签内容 --> | |
| <div id="tab-charts" class="tab-content active"> | |
| <div class="charts-row"> | |
| <!-- 雷达图 --> | |
| <div class="chart-card"> | |
| <h4>各科能力对比雷达图</h4> | |
| <div class="chart-area"> | |
| <canvas id="radar-chart"></canvas> | |
| </div> | |
| <div class="chart-legend"> | |
| <div class="legend-item"> | |
| <div class="legend-color" style="background-color: var(--student-a-color)"></div> | |
| <span id="legend-name-a">学生A</span> | |
| </div> | |
| <div class="legend-item"> | |
| <div class="legend-color" style="background-color: var(--student-b-color)"></div> | |
| <span id="legend-name-b">学生B</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- 柱状图 --> | |
| <div class="chart-card"> | |
| <h4>历史平均排名对比</h4> | |
| <div class="subject-toggles" id="subject-toggles-bar"> | |
| <!-- 科目选择器将在JS中动态生成 --> | |
| </div> | |
| <div class="chart-area"> | |
| <canvas id="bar-chart"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- 趋势标签内容 --> | |
| <div id="tab-trends" class="tab-content"> | |
| <div class="subject-toggles" id="subject-toggles-trend"> | |
| <!-- 科目选择器将在JS中动态生成 --> | |
| </div> | |
| <div class="chart-card" style="height: 450px;"> <!-- 趋势图表高度单独设置 --> | |
| <h4>排名趋势对比</h4> | |
| <div class="chart-area" style="min-height: 350px; max-height: 380px;"> | |
| <canvas id="trend-chart"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| let studentsDataA = null; | |
| let studentsDataB = null; | |
| let selectedStudentA = null; | |
| let selectedStudentB = null; | |
| let activeSubject = '总分'; // 默认显示总分对比 | |
| let charts = {}; // 存储所有图表实例 | |
| document.addEventListener('DOMContentLoaded', function() { | |
| setupStudentSearch('a'); | |
| setupStudentSearch('b'); | |
| setupTabs(); | |
| }); | |
| // 设置标签切换 | |
| function setupTabs() { | |
| const tabButtons = document.querySelectorAll('.tab-button'); | |
| tabButtons.forEach(button => { | |
| button.addEventListener('click', () => { | |
| const tabId = button.dataset.tab; | |
| // 移除所有激活类 | |
| document.querySelectorAll('.tab-button').forEach(b => b.classList.remove('active')); | |
| document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); | |
| // 激活当前标签 | |
| button.classList.add('active'); | |
| document.getElementById(tabId).classList.add('active'); | |
| // 重新绘制图表 | |
| if (tabId === 'tab-trends' && charts.trendChart) { | |
| setTimeout(() => { | |
| charts.trendChart.resize(); | |
| }, 100); | |
| } | |
| }); | |
| }); | |
| } | |
| function setupStudentSearch(student) { | |
| const searchInput = document.getElementById(`search-input-${student}`); | |
| const studentList = document.getElementById(`student-list-${student}`); | |
| let typingTimer; | |
| if (!searchInput || !studentList) { | |
| console.error(`❌ 无法找到学生${student}的搜索元素`); | |
| return; | |
| } | |
| searchInput.addEventListener('input', function() { | |
| clearTimeout(typingTimer); | |
| const inputValue = this.value.trim(); | |
| console.log(`📝 学生${student}输入: "${inputValue}"`); | |
| if (inputValue) { | |
| typingTimer = setTimeout(() => { | |
| console.log(`⏰ 开始搜索学生${student}: "${inputValue}"`); | |
| searchStudents(student, inputValue); | |
| }, 500); | |
| } else { | |
| studentList.style.display = 'none'; | |
| } | |
| }); | |
| // 点击外部关闭列表 | |
| document.addEventListener('click', function(event) { | |
| if (!searchInput.contains(event.target) && !studentList.contains(event.target)) { | |
| studentList.style.display = 'none'; | |
| } | |
| }); | |
| } | |
| function searchStudents(student, name) { | |
| console.log(`🔍 开始搜索学生: ${name} (学生${student})`); | |
| const studentList = document.getElementById(`student-list-${student}`); | |
| studentList.innerHTML = '<div style="text-align:center;padding:15px;">正在搜索...</div>'; | |
| studentList.style.display = 'block'; | |
| const formData = new FormData(); | |
| formData.append('name', name); | |
| fetch('/search_student', { | |
| method: 'POST', | |
| body: formData | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.success && data.students && data.students.length > 0) { | |
| studentList.innerHTML = ''; | |
| data.students.forEach((studentInfo, index) => { | |
| const div = document.createElement('div'); | |
| div.className = 'student-item'; | |
| div.innerHTML = ` | |
| <div class="student-name">${studentInfo.姓名}</div> | |
| <div class="student-id">${studentInfo.身份证号}</div> | |
| `; | |
| div.addEventListener('click', function(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| // 统一用身份证号字段 | |
| selectStudent(student, { | |
| ...studentInfo, | |
| '显示身份证号': studentInfo.身份证号 | |
| }); | |
| studentList.style.display = 'none'; | |
| }); | |
| // 移动端触摸优化 | |
| div.addEventListener('touchstart', function(e) { | |
| e.stopPropagation(); | |
| }, { passive: true }); | |
| studentList.appendChild(div); | |
| }); | |
| } else { | |
| studentList.innerHTML = '<div style="text-align:center;padding:15px;color:#888;">未找到匹配的学生</div>'; | |
| } | |
| }) | |
| .catch(error => { | |
| studentList.innerHTML = '<div style="text-align:center;padding:15px;color:red;">搜索出错,请稍后再试</div>'; | |
| }); | |
| } | |
| function selectStudent(student, studentInfo) { | |
| if (student === 'a') { | |
| selectedStudentA = studentInfo; | |
| const selectedDiv = document.getElementById('selected-student-a'); | |
| selectedDiv.innerHTML = ` | |
| <div class="student-name">${studentInfo.姓名}</div> | |
| <div class="student-id">${studentInfo.显示身份证号}</div> | |
| `; | |
| selectedDiv.classList.add('show'); | |
| document.getElementById('student-panel-a').classList.add('active-a'); | |
| document.getElementById('legend-name-a').textContent = studentInfo.姓名; | |
| // 重置数据 | |
| studentsDataA = null; | |
| } else { | |
| selectedStudentB = studentInfo; | |
| const selectedDiv = document.getElementById('selected-student-b'); | |
| selectedDiv.innerHTML = ` | |
| <div class="student-name">${studentInfo.姓名}</div> | |
| <div class="student-id">${studentInfo.显示身份证号}</div> | |
| `; | |
| selectedDiv.classList.add('show'); | |
| document.getElementById('student-panel-b').classList.add('active-b'); | |
| document.getElementById('legend-name-b').textContent = studentInfo.姓名; | |
| // 重置数据 | |
| studentsDataB = null; | |
| } | |
| // 检查是否可以开始比较 | |
| const compareBtn = document.getElementById('compare-btn'); | |
| if (selectedStudentA && selectedStudentB) { | |
| compareBtn.disabled = false; | |
| } | |
| // 隐藏比较结果区域 | |
| const comparisonSection = document.getElementById('comparison-section'); | |
| if (comparisonSection) { | |
| comparisonSection.classList.remove('show'); | |
| } | |
| } | |
| async function startComparison() { | |
| const compareBtn = document.getElementById('compare-btn'); | |
| compareBtn.textContent = '正在加载数据...'; | |
| compareBtn.disabled = true; | |
| try { | |
| // 清理旧图表 | |
| Object.keys(charts).forEach(key => { | |
| if (charts[key] && typeof charts[key].destroy === 'function') { | |
| charts[key].destroy(); | |
| } | |
| }); | |
| charts = {}; | |
| // 获取两个学生的成绩数据 | |
| const [dataA, dataB] = await Promise.all([ | |
| getStudentScores(selectedStudentA.身份证号), | |
| getStudentScores(selectedStudentB.身份证号) | |
| ]); | |
| if (dataA.success && dataB.success) { | |
| studentsDataA = dataA.data; | |
| studentsDataB = dataB.data; | |
| showComparisonSection(); | |
| } else { | |
| showError(`获取成绩数据失败: ${dataA.error || dataB.error}`); | |
| } | |
| } catch (error) { | |
| showError(`加载数据时出错: ${error.message}`); | |
| } | |
| compareBtn.textContent = '开始比较'; | |
| compareBtn.disabled = false; | |
| } | |
| async function getStudentScores(idNumber) { | |
| const formData = new FormData(); | |
| formData.append('id_number', idNumber); | |
| const response = await fetch('/api/get_student_scores', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| return await response.json(); | |
| } | |
| function showComparisonSection() { | |
| const examSelector = document.getElementById('exam-selector'); | |
| examSelector.innerHTML = ''; | |
| // 添加考试选择说明 | |
| const selectionTitle = document.createElement('p'); | |
| selectionTitle.style.width = '100%'; | |
| selectionTitle.style.marginBottom = '10px'; | |
| selectionTitle.style.fontWeight = '600'; | |
| selectionTitle.style.color = '#666'; | |
| selectionTitle.textContent = '选择要比较的考试:'; | |
| examSelector.appendChild(selectionTitle); | |
| // 创建"历史所有考试"按钮 | |
| const allExamsBtn = document.createElement('button'); | |
| allExamsBtn.className = 'exam-btn active'; | |
| allExamsBtn.textContent = '历史所有考试对比'; | |
| allExamsBtn.onclick = () => { | |
| setActiveExamBtn(allExamsBtn); | |
| showHistoricalComparison(); | |
| }; | |
| examSelector.appendChild(allExamsBtn); | |
| // 获取所有考试列表并排序(按时间倒序) | |
| const allExams = [...studentsDataA, ...studentsDataB]; | |
| const uniqueExams = [...new Map(allExams.map(exam => [exam.考试名称, exam])).values()] | |
| .sort((a, b) => (b.sort_index || 0) - (a.sort_index || 0)); | |
| uniqueExams.forEach((exam, index) => { | |
| const examBtn = document.createElement('button'); | |
| examBtn.className = 'exam-btn'; | |
| // 添加日期信息 | |
| const examDate = exam.考试时间 ? ` (${exam.考试时间.split(' ')[0]})` : ''; | |
| examBtn.textContent = (exam.考试名称 || `考试${index + 1}`) + examDate; | |
| examBtn.onclick = () => { | |
| setActiveExamBtn(examBtn); | |
| showSingleExamComparison(exam.考试名称); | |
| }; | |
| examSelector.appendChild(examBtn); | |
| }); | |
| document.getElementById('comparison-section').classList.add('show'); | |
| showHistoricalComparison(); // 默认显示历史对比 | |
| initCharts(); // 初始化图表 | |
| } | |
| function setActiveExamBtn(activeBtn) { | |
| document.querySelectorAll('.exam-btn').forEach(btn => btn.classList.remove('active')); | |
| activeBtn.classList.add('active'); | |
| } | |
| function showSingleExamComparison(examName) { | |
| const examA = studentsDataA.find(exam => exam.考试名称 === examName); | |
| const examB = studentsDataB.find(exam => exam.考试名称 === examName); | |
| const resultsDiv = document.getElementById('comparison-results'); | |
| if (!examA || !examB) { | |
| resultsDiv.innerHTML = '<div class="info-message">其中一名学生没有该考试的成绩记录</div>'; | |
| return; | |
| } | |
| const subjects = ['语文', '数学', '英语', '物理', '化学', '生物', '历史', '政治', '地理']; | |
| // 创建亮点数据 | |
| let highlights = createExamHighlights(examA, examB); | |
| let tableHTML = ` | |
| <h3>${examName} - 单次考试对比</h3> | |
| <div class="comparison-highlights"> | |
| ${highlights} | |
| </div> | |
| <table class="comparison-table"> | |
| <thead> | |
| <tr> | |
| <th>科目</th> | |
| <th>${selectedStudentA.姓名} (成绩/排名)</th> | |
| <th>${selectedStudentB.姓名} (成绩/排名)</th> | |
| <th>对比结果</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| `; | |
| subjects.forEach(subject => { | |
| const scoreA = examA[subject] || '无数据'; | |
| const rankA = examA[`${subject}校次`] || '无数据'; | |
| const scoreB = examB[subject] || '无数据'; | |
| const rankB = examB[`${subject}校次`] || '无数据'; | |
| if (scoreA !== '无数据' || scoreB !== '无数据') { | |
| let comparison = ''; | |
| let classA = '', classB = ''; | |
| let diffIndicator = ''; | |
| if (rankA !== '无数据' && rankB !== '无数据') { | |
| const rankNumA = parseInt(rankA); | |
| const rankNumB = parseInt(rankB); | |
| const diff = Math.abs(rankNumA - rankNumB); | |
| const maxRank = Math.max(rankNumA, rankNumB) * 1.5; // 用于计算差距比例 | |
| const diffPercent = Math.min(100, Math.round((diff / maxRank) * 100)); | |
| if (rankNumA < rankNumB) { | |
| comparison = `${selectedStudentA.姓名} 领先 ${diff} 名`; | |
| classA = 'better-score'; | |
| classB = 'worse-score'; | |
| diffIndicator = ` | |
| <div class="rank-diff-indicator"> | |
| <div class="rank-diff-bar better" style="width: ${diffPercent}%"></div> | |
| </div> | |
| `; | |
| } else if (rankNumA > rankNumB) { | |
| comparison = `${selectedStudentB.姓名} 领先 ${diff} 名`; | |
| classA = 'worse-score'; | |
| classB = 'better-score'; | |
| diffIndicator = ` | |
| <div class="rank-diff-indicator"> | |
| <div class="rank-diff-bar worse" style="width: ${diffPercent}%"></div> | |
| </div> | |
| `; | |
| } else { | |
| comparison = '排名相同'; | |
| } | |
| } | |
| tableHTML += ` | |
| <tr> | |
| <td data-label="科目">${subject}</td> | |
| <td data-label="${selectedStudentA.姓名}" class="${classA}">${scoreA} / ${rankA}</td> | |
| <td data-label="${selectedStudentB.姓名}" class="${classB}">${scoreB} / ${rankB}</td> | |
| <td data-label="对比结果"> | |
| ${comparison} | |
| ${diffIndicator} | |
| </td> | |
| </tr> | |
| `; | |
| } | |
| }); | |
| // 总分对比 | |
| const totalA = examA['总分'] || '无数据'; | |
| const totalRankA = examA['大类排名'] || '无数据'; | |
| const totalB = examB['总分'] || '无数据'; | |
| const totalRankB = examB['大类排名'] || '无数据'; | |
| let totalComparison = ''; | |
| let totalClassA = '', totalClassB = ''; | |
| let totalDiffIndicator = ''; | |
| if (totalRankA !== '无数据' && totalRankB !== '无数据') { | |
| const rankNumA = parseInt(totalRankA); | |
| const rankNumB = parseInt(totalRankB); | |
| const diff = Math.abs(rankNumA - rankNumB); | |
| const maxRank = Math.max(rankNumA, rankNumB) * 1.5; | |
| const diffPercent = Math.min(100, Math.round((diff / maxRank) * 100)); | |
| if (rankNumA < rankNumB) { | |
| totalComparison = `${selectedStudentA.姓名} 领先 ${diff} 名`; | |
| totalClassA = 'better-score'; | |
| totalClassB = 'worse-score'; | |
| totalDiffIndicator = ` | |
| <div class="rank-diff-indicator"> | |
| <div class="rank-diff-bar better" style="width: ${diffPercent}%"></div> | |
| </div> | |
| `; | |
| } else if (rankNumA > rankNumB) { | |
| totalComparison = `${selectedStudentB.姓名} 领先 ${diff} 名`; | |
| totalClassA = 'worse-score'; | |
| totalClassB = 'better-score'; | |
| totalDiffIndicator = ` | |
| <div class="rank-diff-indicator"> | |
| <div class="rank-diff-bar worse" style="width: ${diffPercent}%"></div> | |
| </div> | |
| `; | |
| } else { | |
| totalComparison = '排名相同'; | |
| } | |
| } | |
| tableHTML += ` | |
| <tr> | |
| <td data-label="科目"><strong>总分</strong></td> | |
| <td data-label="${selectedStudentA.姓名}" class="${totalClassA}"><strong>${totalA} / ${totalRankA}</strong></td> | |
| <td data-label="${selectedStudentB.姓名}" class="${totalClassB}"><strong>${totalB} / ${totalRankB}</strong></td> | |
| <td data-label="对比结果"> | |
| <strong>${totalComparison}</strong> | |
| ${totalDiffIndicator} | |
| </td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| `; | |
| resultsDiv.innerHTML = tableHTML; | |
| // 更新单次考试的雷达图 | |
| updateRadarChart(examA, examB); | |
| } | |
| // 创建考试亮点数据 | |
| function createExamHighlights(examA, examB) { | |
| // 总分对比 | |
| const totalScoreA = examA['总分'] !== '无数据' ? parseFloat(examA['总分']) : null; | |
| const totalScoreB = examB['总分'] !== '无数据' ? parseFloat(examB['总分']) : null; | |
| // 总排名对比 | |
| const totalRankA = examA['大类排名'] !== '无数据' ? parseInt(examA['大类排名']) : null; | |
| const totalRankB = examB['大类排名'] !== '无数据' ? parseInt(examB['大类排名']) : null; | |
| // 找出A学生最好的科目 | |
| let bestSubjectA = '无'; | |
| let bestRankA = Infinity; | |
| // 找出B学生最好的科目 | |
| let bestSubjectB = '无'; | |
| let bestRankB = Infinity; | |
| const subjects = ['语文', '数学', '英语', '物理', '化学', '生物', '历史', '政治', '地理']; | |
| subjects.forEach(subject => { | |
| const rankKeyA = `${subject}校次`; | |
| if (examA[rankKeyA] && examA[rankKeyA] !== '无数据') { | |
| const rank = parseInt(examA[rankKeyA]); | |
| if (rank < bestRankA) { | |
| bestRankA = rank; | |
| bestSubjectA = subject; | |
| } | |
| } | |
| const rankKeyB = `${subject}校次`; | |
| if (examB[rankKeyB] && examB[rankKeyB] !== '无数据') { | |
| const rank = parseInt(examB[rankKeyB]); | |
| if (rank < bestRankB) { | |
| bestRankB = rank; | |
| bestSubjectB = subject; | |
| } | |
| } | |
| }); | |
| return ` | |
| <div class="highlight-card student-a"> | |
| <div class="highlight-title">总分</div> | |
| <div class="highlight-value">${totalScoreA !== null ? totalScoreA : 'N/A'}</div> | |
| <div class="highlight-sub">${selectedStudentA.姓名}</div> | |
| </div> | |
| <div class="highlight-card student-b"> | |
| <div class="highlight-title">总分</div> | |
| <div class="highlight-value">${totalScoreB !== null ? totalScoreB : 'N/A'}</div> | |
| <div class="highlight-sub">${selectedStudentB.姓名}</div> | |
| </div> | |
| <div class="highlight-card student-a"> | |
| <div class="highlight-title">总排名</div> | |
| <div class="highlight-value">${totalRankA !== null ? totalRankA : 'N/A'}</div> | |
| <div class="highlight-sub">${selectedStudentA.姓名}</div> | |
| </div> | |
| <div class="highlight-card student-b"> | |
| <div class="highlight-title">总排名</div> | |
| <div class="highlight-value">${totalRankB !== null ? totalRankB : 'N/A'}</div> | |
| <div class="highlight-sub">${selectedStudentB.姓名}</div> | |
| </div> | |
| `; | |
| } | |
| function showHistoricalComparison() { | |
| const resultsDiv = document.getElementById('comparison-results'); | |
| // 计算历史平均排名 | |
| const subjects = ['语文', '数学', '英语', '物理', '化学', '生物', '历史', '政治', '地理', '总分']; | |
| // 找出学生A和B的优势科目 | |
| const advantagesA = []; | |
| const advantagesB = []; | |
| subjects.forEach(subject => { | |
| const ranksA = collectRanks(studentsDataA, subject); | |
| const ranksB = collectRanks(studentsDataB, subject); | |
| if (ranksA.length > 0 && ranksB.length > 0) { | |
| const avgA = (ranksA.reduce((a, b) => a + b, 0) / ranksA.length); | |
| const avgB = (ranksB.reduce((a, b) => a + b, 0) / ranksB.length); | |
| if (avgA < avgB) { | |
| advantagesA.push({subject, diff: avgB - avgA}); | |
| } else if (avgB < avgA) { | |
| advantagesB.push({subject, diff: avgA - avgB}); | |
| } | |
| } | |
| }); | |
| // 按差距排序 | |
| advantagesA.sort((a, b) => b.diff - a.diff); | |
| advantagesB.sort((a, b) => b.diff - a.diff); | |
| // 取前三个优势科目 | |
| const topAdvantagesA = advantagesA.slice(0, 3).map(a => a.subject).join('、') || '无明显优势科目'; | |
| const topAdvantagesB = advantagesB.slice(0, 3).map(a => a.subject).join('、') || '无明显优势科目'; | |
| let highlightsHTML = ` | |
| <div class="comparison-highlights"> | |
| <div class="highlight-card student-a"> | |
| <div class="highlight-title">优势科目</div> | |
| <div class="highlight-value">${topAdvantagesA}</div> | |
| <div class="highlight-sub">${selectedStudentA.姓名}</div> | |
| </div> | |
| <div class="highlight-card student-b"> | |
| <div class="highlight-title">优势科目</div> | |
| <div class="highlight-value">${topAdvantagesB}</div> | |
| <div class="highlight-sub">${selectedStudentB.姓名}</div> | |
| </div> | |
| <div class="highlight-card"> | |
| <div class="highlight-title">考试记录数</div> | |
| <div class="highlight-value">${studentsDataA.length} / ${studentsDataB.length}</div> | |
| <div class="highlight-sub">${selectedStudentA.姓名} / ${selectedStudentB.姓名}</div> | |
| </div> | |
| </div> | |
| `; | |
| let tableHTML = ` | |
| <h3>历史所有考试对比分析</h3> | |
| ${highlightsHTML} | |
| <table class="comparison-table"> | |
| <thead> | |
| <tr> | |
| <th>科目/项目</th> | |
| <th>${selectedStudentA.姓名} (平均排名)</th> | |
| <th>${selectedStudentB.姓名} (平均排名)</th> | |
| <th>对比结果</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| `; | |
| subjects.forEach(subject => { | |
| const ranksA = collectRanks(studentsDataA, subject); | |
| const ranksB = collectRanks(studentsDataB, subject); | |
| if (ranksA.length > 0 || ranksB.length > 0) { | |
| const avgA = ranksA.length > 0 ? (ranksA.reduce((a, b) => a + b, 0) / ranksA.length).toFixed(2) : 'N/A'; | |
| const avgB = ranksB.length > 0 ? (ranksB.reduce((a, b) => a + b, 0) / ranksB.length).toFixed(2) : 'N/A'; | |
| let comparison = ''; | |
| let classA = '', classB = ''; | |
| let diffIndicator = ''; | |
| if (avgA !== 'N/A' && avgB !== 'N/A') { | |
| const numA = parseFloat(avgA); | |
| const numB = parseFloat(avgB); | |
| const diff = Math.abs(numA - numB).toFixed(2); | |
| const maxRank = Math.max(numA, numB) * 1.5; | |
| const diffPercent = Math.min(100, Math.round((diff / maxRank) * 100)); | |
| if (numA < numB) { | |
| comparison = `${selectedStudentA.姓名} 平均领先 ${diff} 名`; | |
| classA = 'better-score'; | |
| classB = 'worse-score'; | |
| diffIndicator = ` | |
| <div class="rank-diff-indicator"> | |
| <div class="rank-diff-bar better" style="width: ${diffPercent}%"></div> | |
| </div> | |
| `; | |
| } else if (numA > numB) { | |
| comparison = `${selectedStudentB.姓名} 平均领先 ${diff} 名`; | |
| classA = 'worse-score'; | |
| classB = 'better-score'; | |
| diffIndicator = ` | |
| <div class="rank-diff-indicator"> | |
| <div class="rank-diff-bar worse" style="width: ${diffPercent}%"></div> | |
| </div> | |
| `; | |
| } else { | |
| comparison = '平均排名相同'; | |
| } | |
| } | |
| tableHTML += ` | |
| <tr> | |
| <td data-label="科目/项目">${subject}</td> | |
| <td data-label="${selectedStudentA.姓名}" class="${classA}">${avgA} (${ranksA.length}次考试)</td> | |
| <td data-label="${selectedStudentB.姓名}" class="${classB}">${avgB} (${ranksB.length}次考试)</td> | |
| <td data-label="对比结果"> | |
| ${comparison} | |
| ${diffIndicator} | |
| </td> | |
| </tr> | |
| `; | |
| } | |
| }); | |
| tableHTML += ` | |
| </tbody> | |
| </table> | |
| `; | |
| resultsDiv.innerHTML = tableHTML; | |
| // 更新历史比较的图表 | |
| updateBarChart(); | |
| updateRadarChart(); | |
| } | |
| function collectRanks(examData, subject) { | |
| const ranks = []; | |
| examData.forEach(exam => { | |
| let rankStr; | |
| if (subject === '总分') { | |
| rankStr = exam['大类排名']; | |
| } else { | |
| rankStr = exam[`${subject}校次`]; | |
| } | |
| if (rankStr && rankStr !== '无数据' && rankStr.trim() !== '') { | |
| const rankVal = parseInt(rankStr); | |
| if (!isNaN(rankVal)) { | |
| ranks.push(rankVal); | |
| } | |
| } | |
| }); | |
| return ranks; | |
| } | |
| // 初始化所有图表 | |
| function initCharts() { | |
| initRadarChart(); | |
| initBarChart(); | |
| initTrendChart(); | |
| initSubjectToggles(); | |
| } | |
| // 初始化科目选择器 | |
| function initSubjectToggles() { | |
| const subjects = ['总分', '语文', '数学', '英语', '物理', '化学', '生物', '历史', '政治', '地理']; | |
| const barToggleContainer = document.getElementById('subject-toggles-bar'); | |
| const trendToggleContainer = document.getElementById('subject-toggles-trend'); | |
| // 清空现有内容 | |
| barToggleContainer.innerHTML = ''; | |
| trendToggleContainer.innerHTML = ''; | |
| subjects.forEach(subject => { | |
| // 为柱状图创建选择器 | |
| const barToggle = document.createElement('div'); | |
| barToggle.className = `subject-toggle ${subject === activeSubject ? 'active' : ''}`; | |
| barToggle.textContent = subject; | |
| barToggle.dataset.subject = subject; | |
| barToggle.addEventListener('click', function() { | |
| document.querySelectorAll('#subject-toggles-bar .subject-toggle').forEach( | |
| t => t.classList.remove('active') | |
| ); | |
| this.classList.add('active'); | |
| activeSubject = this.dataset.subject; | |
| updateBarChart(); | |
| }); | |
| barToggleContainer.appendChild(barToggle); | |
| // 为趋势图创建选择器 | |
| const trendToggle = document.createElement('div'); | |
| trendToggle.className = `subject-toggle ${subject === activeSubject ? 'active' : ''}`; | |
| trendToggle.textContent = subject; | |
| trendToggle.dataset.subject = subject; | |
| trendToggle.addEventListener('click', function() { | |
| document.querySelectorAll('#subject-toggles-trend .subject-toggle').forEach( | |
| t => t.classList.remove('active') | |
| ); | |
| this.classList.add('active'); | |
| activeSubject = this.dataset.subject; | |
| updateTrendChart(); | |
| }); | |
| trendToggleContainer.appendChild(trendToggle); | |
| }); | |
| } | |
| // 初始化雷达图 | |
| function initRadarChart() { | |
| const ctx = document.getElementById('radar-chart').getContext('2d'); | |
| if (charts.radarChart) { | |
| charts.radarChart.destroy(); | |
| } | |
| charts.radarChart = new Chart(ctx, { | |
| type: 'radar', | |
| data: { | |
| labels: ['语文', '数学', '英语', '物理', '化学', '生物', '历史', '政治', '地理'], | |
| datasets: [] | |
| }, | |
| options: { | |
| scales: { | |
| r: { | |
| angleLines: { | |
| display: true | |
| }, | |
| suggestedMin: 0, | |
| suggestedMax: 100, | |
| ticks: { | |
| display: false | |
| }, | |
| pointLabels: { | |
| font: { | |
| size: 12, | |
| weight: 'bold' | |
| } | |
| }, | |
| reverse: true | |
| } | |
| }, | |
| plugins: { | |
| legend: { | |
| display: false | |
| }, | |
| tooltip: { | |
| callbacks: { | |
| label: function(context) { | |
| let label = context.dataset.label || ''; | |
| if (label) { | |
| label += ': '; | |
| } | |
| if (context.parsed.r !== null) { | |
| label += `排名 ${context.parsed.r}`; | |
| } | |
| return label; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| updateRadarChart(); | |
| } | |
| // 更新雷达图数据 | |
| function updateRadarChart(examA, examB) { | |
| if (!charts.radarChart) return; | |
| const subjects = ['语文', '数学', '英语', '物理', '化学', '生物', '历史', '政治', '地理']; | |
| // 如果提供了特定考试数据,使用该考试的数据 | |
| // 否则使用所有考试的平均排名 | |
| let dataA, dataB; | |
| if (examA && examB) { | |
| dataA = subjects.map(subject => { | |
| const rank = examA[`${subject}校次`]; | |
| return rank && rank !== '无数据' ? parseInt(rank) : null; | |
| }); | |
| dataB = subjects.map(subject => { | |
| const rank = examB[`${subject}校次`]; | |
| return rank && rank !== '无数据' ? parseInt(rank) : null; | |
| }); | |
| } else { | |
| dataA = subjects.map(subject => { | |
| const ranks = collectRanks(studentsDataA, subject); | |
| return ranks.length > 0 ? | |
| (ranks.reduce((a, b) => a + b, 0) / ranks.length) : null; | |
| }); | |
| dataB = subjects.map(subject => { | |
| const ranks = collectRanks(studentsDataB, subject); | |
| return ranks.length > 0 ? | |
| (ranks.reduce((a, b) => a + b, 0) / ranks.length) : null; | |
| }); | |
| } | |
| // 更新图表数据 | |
| charts.radarChart.data.datasets = [ | |
| { | |
| label: selectedStudentA.姓名, | |
| data: dataA, | |
| backgroundColor: 'rgba(67, 97, 238, 0.3)', | |
| borderColor: 'rgb(67, 97, 238)', | |
| pointBackgroundColor: 'rgb(67, 97, 238)', | |
| pointBorderColor: '#fff', | |
| pointHoverBackgroundColor: '#fff', | |
| pointHoverBorderColor: 'rgb(67, 97, 238)' | |
| }, | |
| { | |
| label: selectedStudentB.姓名, | |
| data: dataB, | |
| backgroundColor: 'rgba(255, 107, 107, 0.3)', | |
| borderColor: 'rgb(255, 107, 107)', | |
| pointBackgroundColor: 'rgb(255, 107, 107)', | |
| pointBorderColor: '#fff', | |
| pointHoverBackgroundColor: '#fff', | |
| pointHoverBorderColor: 'rgb(255, 107, 107)' | |
| } | |
| ]; | |
| charts.radarChart.update(); | |
| } | |
| // 初始化柱状图 | |
| function initBarChart() { | |
| const ctx = document.getElementById('bar-chart').getContext('2d'); | |
| if (charts.barChart) { | |
| charts.barChart.destroy(); | |
| } | |
| charts.barChart = new Chart(ctx, { | |
| type: 'bar', | |
| data: { | |
| labels: ['平均排名', '最高排名', '最低排名'], | |
| datasets: [] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| scales: { | |
| y: { | |
| beginAtZero: true, | |
| reverse: true, // 排名越小越好 | |
| title: { | |
| display: true, | |
| text: '排名' | |
| } | |
| }, | |
| x: { | |
| grid: { | |
| display: false | |
| } | |
| } | |
| }, | |
| plugins: { | |
| legend: { | |
| position: 'top', | |
| } | |
| }, | |
| animation: { | |
| duration: 1000 | |
| } | |
| } | |
| }); | |
| updateBarChart(); | |
| } | |
| // 更新柱状图数据 | |
| function updateBarChart() { | |
| if (!charts.barChart) return; | |
| // 获取选中科目的数据 | |
| const subject = activeSubject; | |
| // 收集A学生的排名数据 | |
| const ranksA = collectRanks(studentsDataA, subject); | |
| const avgA = ranksA.length > 0 ? (ranksA.reduce((a, b) => a + b, 0) / ranksA.length) : null; | |
| const minA = ranksA.length > 0 ? Math.min(...ranksA) : null; // 最高排名 | |
| const maxA = ranksA.length > 0 ? Math.max(...ranksA) : null; // 最低排名 | |
| // 收集B学生的排名数据 | |
| const ranksB = collectRanks(studentsDataB, subject); | |
| const avgB = ranksB.length > 0 ? (ranksB.reduce((a, b) => a + b, 0) / ranksB.length) : null; | |
| const minB = ranksB.length > 0 ? Math.min(...ranksB) : null; // 最高排名 | |
| const maxB = ranksB.length > 0 ? Math.max(...ranksB) : null; // 最低排名 | |
| // 更新图表数据 | |
| charts.barChart.data.datasets = [ | |
| { | |
| label: selectedStudentA.姓名, | |
| data: [avgA, minA, maxA], | |
| backgroundColor: 'rgba(67, 97, 238, 0.7)', | |
| borderColor: 'rgb(67, 97, 238)', | |
| borderWidth: 1, | |
| maxBarThickness: 50 // 限制柱状图最大宽度 | |
| }, | |
| { | |
| label: selectedStudentB.姓名, | |
| data: [avgB, minB, maxB], | |
| backgroundColor: 'rgba(255, 107, 107, 0.7)', | |
| borderColor: 'rgb(255, 107, 107)', | |
| borderWidth: 1, | |
| maxBarThickness: 50 // 限制柱状图最大宽度 | |
| } | |
| ]; | |
| // 更新标题和选项 | |
| charts.barChart.options.plugins.title = { | |
| display: true, | |
| text: `${subject} 排名统计对比`, | |
| font: { | |
| size: 14 // 移动端减小字体 | |
| } | |
| }; | |
| // 移动端优化 | |
| if (window.innerWidth <= 768) { | |
| charts.barChart.options.plugins.legend.labels.font = { | |
| size: 12 | |
| }; | |
| charts.barChart.options.scales.x.ticks.font = { | |
| size: 10 | |
| }; | |
| charts.barChart.options.scales.y.ticks.font = { | |
| size: 10 | |
| }; | |
| } | |
| charts.barChart.update(); | |
| } | |
| // 初始化趋势图 | |
| function initTrendChart() { | |
| const ctx = document.getElementById('trend-chart').getContext('2d'); | |
| if (charts.trendChart) { | |
| charts.trendChart.destroy(); | |
| } | |
| charts.trendChart = new Chart(ctx, { | |
| type: 'line', | |
| data: { | |
| labels: [], | |
| datasets: [] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| scales: { | |
| y: { | |
| reverse: true, // 排名越小越好 | |
| title: { | |
| display: true, | |
| text: '排名' | |
| } | |
| }, | |
| x: { | |
| title: { | |
| display: true, | |
| text: '考试时间' | |
| } | |
| } | |
| }, | |
| plugins: { | |
| legend: { | |
| position: 'top', | |
| }, | |
| tooltip: { | |
| mode: 'index', | |
| intersect: false, | |
| } | |
| }, | |
| elements: { | |
| line: { | |
| tension: 0.3 | |
| }, | |
| point: { | |
| radius: 5, | |
| hitRadius: 10, | |
| hoverRadius: 7 | |
| } | |
| } | |
| } | |
| }); | |
| updateTrendChart(); | |
| } | |
| // 更新趋势图 | |
| function updateTrendChart() { | |
| if (!charts.trendChart) return; | |
| // 获取选中科目 | |
| const subject = activeSubject; | |
| // 按时间排序的所有考试 | |
| const allExamsA = [...studentsDataA].sort((a, b) => | |
| (a.sort_index || 0) - (b.sort_index || 0) | |
| ); | |
| const allExamsB = [...studentsDataB].sort((a, b) => | |
| (a.sort_index || 0) - (b.sort_index || 0) | |
| ); | |
| // 收集考试名称和日期作为标签,并按 sort_index 排序 | |
| const allExams = [...studentsDataA, ...studentsDataB]; | |
| const uniqueExamsMap = new Map(); | |
| allExams.forEach(exam => { | |
| const name = exam.考试名称 || exam.考试时间; | |
| if (!uniqueExamsMap.has(name)) { | |
| uniqueExamsMap.set(name, exam); | |
| } | |
| }); | |
| const sortedUniqueExams = Array.from(uniqueExamsMap.values()).sort((a, b) => | |
| (a.sort_index || 0) - (b.sort_index || 0) | |
| ); | |
| const uniqueLabels = sortedUniqueExams.map(exam => exam.考试名称 || exam.考试时间); | |
| // 收集排名数据 | |
| const dataA = uniqueLabels.map(label => { | |
| const exam = allExamsA.find(e => (e.考试名称 || e.考试时间) === label); | |
| if (!exam) return null; | |
| const rankKey = subject === '总分' ? '大类排名' : `${subject}校次`; | |
| const rankStr = exam[rankKey]; | |
| return rankStr && rankStr !== '无数据' ? parseInt(rankStr) : null; | |
| }); | |
| const dataB = uniqueLabels.map(label => { | |
| const exam = allExamsB.find(e => (e.考试名称 || e.考试时间) === label); | |
| if (!exam) return null; | |
| const rankKey = subject === '总分' ? '大类排名' : `${subject}校次`; | |
| const rankStr = exam[rankKey]; | |
| return rankStr && rankStr !== '无数据' ? parseInt(rankStr) : null; | |
| }); | |
| // 更新图表数据 | |
| charts.trendChart.data.labels = uniqueLabels; | |
| charts.trendChart.data.datasets = [ | |
| { | |
| label: selectedStudentA.姓名, | |
| data: dataA, | |
| borderColor: 'rgb(67, 97, 238)', | |
| backgroundColor: 'rgba(67, 97, 238, 0.1)', | |
| fill: false, | |
| pointBackgroundColor: 'rgb(67, 97, 238)' | |
| }, | |
| { | |
| label: selectedStudentB.姓名, | |
| data: dataB, | |
| borderColor: 'rgb(255, 107, 107)', | |
| backgroundColor: 'rgba(255, 107, 107, 0.1)', | |
| fill: false, | |
| pointBackgroundColor: 'rgb(255, 107, 107)' | |
| } | |
| ]; | |
| // 更新标题 | |
| charts.trendChart.options.plugins.title = { | |
| display: true, | |
| text: `${subject} 排名趋势对比`, | |
| font: { | |
| size: 16 | |
| } | |
| }; | |
| charts.trendChart.update(); | |
| } | |
| function showError(message) { | |
| const resultsDiv = document.getElementById('comparison-results'); | |
| resultsDiv.innerHTML = `<div class="error-message">${message}</div>`; | |
| } | |
| </script> | |
| </body> | |
| </html> | |