jzyg123's picture
Update src/templates/index.html
e2d68d2 verified
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>学生成绩查询系统</title>
<!-- 引入Google字体和图标 -->
<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">
<!-- 引入Chart.js库 -->
<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;
/* 新增对比颜色 - 与compare页面一致 */
--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);
}
/* 趋势图标样式 */
.meta-value-wrapper {
display: flex;
align-items: center;
gap: 4px;
}
.trend-icon {
font-size: 18px;
font-weight: bold;
}
.trend-icon.up, .trend-text.up {
color: var(--success-color);
}
.trend-icon.down, .trend-text.down {
color: var(--danger-color);
}
* {
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: 1100px;
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;
}
.card {
background-color: var(--card-bg);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
padding: 25px;
margin-bottom: 30px;
transition: var(--transition);
animation: fadeIn 0.3s ease;
}
.card:hover {
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
}
.search-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.search-section {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 30px;
margin-bottom: 20px;
position: relative;
}
.search-container {
position: relative;
margin: 0 auto;
max-width: 500px;
width: 100%;
}
.search-input {
width: 100%;
padding: 16px 20px;
padding-left: 50px;
border: 2px solid #e1e5ea;
border-radius: 30px;
font-size: 16px;
transition: var(--transition);
outline: none;
background-color: var(--white);
}
.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: 16px;
top: 50%;
transform: translateY(-50%);
color: #aab0b7;
}
.search-btn {
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 30px;
padding: 14px 30px;
font-size: 16px;
cursor: pointer;
transition: var(--transition);
font-weight: 600;
display: block;
margin: 0 auto;
min-width: 150px;
}
.search-btn:hover {
background-color: var(--primary-hover);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(67, 97, 238, 0.3);
}
.search-btn:active {
transform: translateY(0);
box-shadow: none;
}
/* 搜索模式切换 */
.search-toggle {
display: flex;
background-color: #edf2ff;
border-radius: 50px;
padding: 3px;
margin-bottom: 20px;
width: 400px;
max-width: 100%;
margin: 0 auto 20px auto;
}
.search-toggle-btn {
flex: 1;
border: none;
padding: 10px 15px;
font-size: 14px;
font-weight: 600;
background: transparent;
color: var(--secondary-color);
cursor: pointer;
border-radius: 50px;
transition: all 0.2s ease;
}
.search-toggle-btn.active {
background-color: var(--primary-color);
color: white;
box-shadow: 0 3px 8px rgba(67, 97, 238, 0.25);
}
/* 姓名搜索区域 */
.name-search-section, .idcard-search-section {
display: none;
width: 100%;
max-width: 500px;
margin: 0 auto;
}
.name-search-section.active, .idcard-search-section.active {
display: block;
}
.name-search-input {
width: 100%;
padding: 16px 20px;
padding-left: 50px;
border: 2px solid #e1e5ea;
border-radius: 30px;
font-size: 16px;
transition: var(--transition);
outline: none;
background-color: var(--white);
}
.name-search-input:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.2);
}
.student-list {
background-color: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
margin-top: 10px;
max-height: 300px;
overflow-y: auto;
overflow-x: hidden;
display: none;
position: relative;
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: 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: 5px;
pointer-events: none;
}
.student-id {
color: #777;
font-size: 14px;
pointer-events: none;
}
#loading-message {
display: none;
text-align: center;
padding: 30px;
}
.loader {
width: 48px;
height: 48px;
border: 5px solid var(--light-color);
border-bottom-color: var(--primary-color);
border-radius: 50%;
display: inline-block;
box-sizing: border-box;
animation: rotation 1s linear infinite;
margin: 0 auto;
}
@keyframes rotation {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
margin-top: 15px;
color: var(--primary-color);
font-weight: 600;
}
.error-message {
color: var(--danger-color);
background-color: rgba(220, 53, 69, 0.1);
border-left: 4px solid var(--danger-color);
padding: 15px;
border-radius: 5px;
margin: 20px 0;
font-weight: 600;
}
.success-message {
color: var(--success-color);
background-color: rgba(40, 167, 69, 0.1);
border-left: 4px solid var(--success-color);
padding: 15px;
border-radius: 5px;
margin: 20px 0;
font-weight: 600;
display: flex;
align-items: center;
gap: 10px;
}
.info-message {
color: var(--secondary-color);
background-color: rgba(108, 117, 125, 0.1);
border-left: 4px solid var(--secondary-color);
padding: 15px;
border-radius: 5px;
margin: 20px 0;
}
.tabs {
display: flex;
flex-wrap: wrap;
gap: 2px;
margin-top: 20px;
border-radius: 8px 8px 0 0;
overflow: hidden;
}
.tab-button {
background-color: #f0f2f8;
border: none;
padding: 14px 20px;
cursor: pointer;
flex-grow: 1;
text-align: center;
transition: var(--transition);
font-weight: 600;
border-bottom: 3px solid transparent;
color: var(--secondary-color);
}
.tab-button:hover {
background-color: #e9ecf1;
}
.tab-button.active {
background-color: var(--white);
color: var(--primary-color);
border-bottom: 3px solid var(--primary-color);
}
.tab-content {
display: none;
padding: 25px;
background-color: var(--white);
border-radius: 0 0 12px 12px;
animation: fadeIn 0.5s ease-in-out;
}
.tab-content.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.score-header {
border-bottom: 1px solid #eaecef;
padding-bottom: 15px;
margin-bottom: 25px;
}
.score-header h3 {
color: var(--primary-color);
font-size: 1.6rem;
margin-bottom: 5px;
}
.score-meta {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 25px;
background-color: #f8faff;
padding: 20px;
border-radius: 12px;
}
.meta-item {
display: flex;
flex-direction: column;
}
.meta-label {
font-size: 0.9rem;
color: var(--secondary-color);
margin-bottom: 5px;
}
.meta-value {
font-size: 1.2rem;
font-weight: 600;
color: #333;
}
.score-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);
}
.score-table th,
.score-table td {
border: 1px solid #eaecef;
padding: 14px 15px;
text-align: left;
}
.score-table th {
background-color: #f6f8fa;
font-weight: 600;
color: #444;
position: sticky;
top: 0;
z-index: 1;
}
.score-table tr:nth-child(even) {
background-color: #fafbff;
}
.score-table tr:hover {
background-color: rgba(67, 97, 238, 0.05);
}
.summary-box {
background-color: #f8fafd;
border-radius: 12px;
padding: 20px;
margin-top: 25px;
border: 1px solid #eaecef;
}
.summary-title {
font-weight: 600;
color: #455a64;
margin-bottom: 15px;
font-size: 1.1rem;
}
.summary-content {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
gap: 20px;
}
.summary-item {
display: flex;
flex-direction: column;
}
.tips {
list-style-type: none;
padding-left: 0;
}
.tips li {
padding: 7px 0;
position: relative;
padding-left: 25px;
}
.tips li:before {
content: "•";
position: absolute;
left: 10px;
color: var(--primary-color);
font-weight: bold;
}
.material-icons {
vertical-align: middle;
font-size: 20px;
}
/* 趋势图相关样式 */
.trend-section {
margin-top: 30px;
}
.single-exam-analysis {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #e1e5ea;
}
.single-exam-analysis h4 {
color: var(--dark-color);
margin-bottom: 20px;
text-align: center;
font-size: 1.1rem;
}
.chart-card {
background-color: var(--white);
border-radius: var(--border-radius);
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
padding: 20px;
min-height: 300px;
display: flex;
flex-direction: column;
}
.chart-area {
flex: 1;
position: relative;
min-height: 250px;
width: 100%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.no-chart-data {
color: var(--secondary-color);
text-align: center;
font-style: italic;
padding: 40px 20px;
}
.trend-section {
margin-top: 35px;
padding-top: 25px;
border-top: 1px dashed #e0e4e7;
}
.chart-container {
width: 100%;
height: 400px;
margin-top: 20px;
position: relative;
}
/* 图表区域样式 - 与compare页面保持一致 */
.chart-area {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
position: relative;
min-height: 300px;
}
.chart-card {
background: white;
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
margin-bottom: 20px;
padding: 20px;
display: flex;
flex-direction: column;
height: 400px;
}
.chart-card h4 {
margin: 0 0 20px 0;
color: var(--dark-color);
font-size: 1.1rem;
text-align: center;
}
}
.chart-tabs {
display: flex;
background-color: #f1f3f5;
border-radius: 10px;
margin-bottom: 20px;
overflow: hidden;
width: 400px;
max-width: 100%;
}
.chart-tab {
flex: 1;
text-align: center;
padding: 12px 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
border: none;
background: transparent;
color: #495057;
}
.chart-tab.active {
background-color: var(--primary-color);
color: white;
box-shadow: 0 2px 10px rgba(67, 97, 238, 0.2);
}
.chart-view {
display: none;
}
.chart-view.active {
display: block;
animation: fadeIn 0.5s ease;
}
.no-trend-data {
text-align: center;
padding: 30px 20px;
color: var(--secondary-color);
font-style: italic;
background-color: #f8fafd;
border-radius: 10px;
border: 1px dashed #e0e4e7;
}
/* 统计区域样式更新为与compare.html一致 */
.statistics-section {
margin-top: 35px;
padding-top: 25px;
border-top: 1px dashed #e0e4e7;
animation: fadeIn 0.5s ease;
}
.statistics-section h4 {
color: var(--primary-color);
font-size: 1.3rem;
margin-bottom: 20px;
font-weight: 600;
}
.statistics-table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 0 10px rgba(0,0,0,0.05);
}
.statistics-table th,
.statistics-table td {
border: 1px solid #eaecef;
padding: 12px 15px;
text-align: left;
font-size: 0.95rem;
}
.statistics-table th {
background-color: #f6f8fa;
font-weight: 600;
color: #444;
position: sticky;
top: 0;
z-index: 1;
}
.statistics-table tr:nth-child(even) {
background-color: #fafbff;
}
.statistics-table tr:hover {
background-color: rgba(67, 97, 238, 0.05);
}
.no-stats-data {
text-align: center;
padding: 30px 20px;
color: var(--secondary-color);
font-style: italic;
background-color: #f8fafd;
border-radius: 10px;
border: 1px dashed #e0e4e7;
margin-top: 15px;
}
/* 科目选择器 */
.subject-toggles {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 15px 0;
justify-content: center;
}
.subject-toggle {
display: inline-flex;
align-items: center;
background-color: #f0f2f8;
padding: 6px 12px;
border-radius: 20px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
user-select: none;
border: 1px solid transparent;
}
.subject-toggle.active {
background-color: var(--primary-color);
color: white;
}
.subject-toggle.active:before {
content: "✓";
margin-right: 5px;
font-weight: bold;
}
.color-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
margin-right: 8px;
}
/* 移动设备优化 */
@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;
}
.card {
padding: 20px 15px;
margin-bottom: 20px;
}
.search-form {
flex-direction: column;
}
.score-meta {
grid-template-columns: 1fr;
}
.summary-content {
grid-template-columns: 1fr;
}
.tabs {
flex-direction: column;
}
.search-toggle {
width: 100%;
}
.chart-container {
height: 340px;
}
.student-list {
max-height: 200px;
-webkit-overflow-scrolling: touch;
touch-action: pan-y;
overscroll-behavior-y: contain;
position: fixed;
z-index: 9999;
left: 10px;
right: 10px;
width: auto;
}
.chart-card {
height: 300px;
padding: 15px;
}
.chart-card h4 {
font-size: 1rem;
margin-bottom: 15px;
}
.chart-area {
min-height: 200px;
max-height: 220px;
width: 100%;
}
.subject-toggles {
gap: 6px;
margin: 12px 0;
}
.subject-toggle {
padding: 5px 10px;
font-size: 12px;
}
}
@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;
}
.search-container {
width: 100%;
}
.summary-content {
grid-template-columns: 1fr;
}
.score-table {
font-size: 0.9rem;
}
.chart-container {
height: 280px;
}
.student-list {
max-height: 150px;
overflow-y: scroll;
-webkit-overflow-scrolling: touch;
touch-action: pan-y;
overscroll-behavior-y: contain;
position: fixed;
z-index: 9999;
left: 8px;
right: 8px;
width: auto;
}
.chart-card {
height: 250px;
padding: 12px;
}
.chart-card h4 {
font-size: 0.9rem;
margin-bottom: 12px;
}
.chart-area {
min-height: 160px;
max-height: 180px;
}
.subject-toggle {
padding: 4px 8px;
font-size: 11px;
}
.statistics-table {
font-size: 12px;
}
.statistics-table th,
.statistics-table td {
padding: 8px 6px;
}
}
/* 触摸设备滚动优化 */
@media (pointer: coarse) {
.student-list {
-webkit-overflow-scrolling: touch;
scrollbar-width: thin;
}
.student-list::-webkit-scrollbar {
width: 4px;
}
.student-list::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 2px;
}
.student-list::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 2px;
}
.student-list::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>学生成绩查询系统</h1>
<p>输入姓名或身份证号查询考试成绩</p>
<div style="margin-top: 15px;">
<a href="/compare" style="color: var(--primary-color); text-decoration: none; font-weight: 600; display: inline-flex; align-items: center; gap: 5px; padding: 8px 16px; border-radius: 20px; border: 2px solid var(--primary-color); transition: var(--transition);"
onmouseover="this.style.backgroundColor='var(--primary-color)'; this.style.color='white';"
onmouseout="this.style.backgroundColor='transparent'; this.style.color='var(--primary-color)';">
<span class="material-icons">compare_arrows</span>成绩比较
</a>
</div>
</div>
<div class="card">
<!-- 搜索方式切换 -->
<div class="search-toggle">
<button type="button" class="search-toggle-btn active" data-target="name-search">按姓名查询</button>
<button type="button" class="search-toggle-btn" data-target="idcard-search">按身份证号查询</button>
</div>
<!-- 姓名搜索区域 -->
<div class="name-search-section active">
<div class="search-container">
<input type="text" id="name-input" class="name-search-input" placeholder="请输入学生姓名">
<span class="material-icons search-icon">person_search</span>
</div>
<div id="student-list" class="student-list" style="display:none;"></div>
</div>
<!-- 身份证搜索区域 -->
<div class="idcard-search-section">
<form action="/query" method="post" id="queryForm" class="search-form">
<div class="search-container">
<input type="text" name="id_number" id="id-number-input" class="search-input" placeholder="请输入18位身份证号" value="{{ result.id_number_submitted if result and result.id_number_submitted else '' }}" required>
<span class="material-icons search-icon">pin</span>
</div>
<button type="submit" class="search-btn">查询成绩</button>
</form>
</div>
</div>
<div id="loading-message">
<span class="loader"></span>
<p class="loading-text">正在查询成绩,请稍候...</p>
</div>
{% if result %}
<div class="result">
{% if result.error_message %}
<div class="card">
<div class="error-message">
<span class="material-icons">error</span> 查询失败: {{ result.error_message }}
</div>
{% if 'cookie' in result.error_message.lower() %}
<div class="info-message">
<strong>提示:</strong>
<ul class="tips">
<li>请确保服务器上的 cookies.txt 文件存在且有效</li>
<li>如果问题持续,请联系管理员更新Cookie</li>
</ul>
</div>
{% endif %}
</div>
{% elif result.scores_data %}
<div class="card">
<div class="success-message">
<span class="material-icons">check_circle</span> 成功查询到 {{ result.scores_data|length }} 条成绩记录
</div>
{% if result.scores_data|length > 1 %}
<div class="tabs">
{% for record in result.scores_data %}
<button class="tab-button {% if loop.first %}active{% endif %}" onclick="openTab(event, 'record-{{ loop.index }}')">
{{ record.get('考试名称', '考试') }}
</button>
{% endfor %}
</div>
{% endif %}
{% for record in result.scores_data %}
<div id="record-{{ loop.index }}" class="tab-content {% if loop.first %}active{% endif %}">
<div class="score-header">
<h3>{{ record.get('考试名称', '考试成绩') }}</h3>
<div class="score-meta">
<div class="meta-item">
<span class="meta-label">考试时间</span>
<span class="meta-value">{{ record.get('考试时间', '未知') }}</span>
</div>
<div class="meta-item">
<span class="meta-label">学生姓名</span>
<span class="meta-value">{{ record.get('姓名', '未知') }}</span>
</div>
<div class="meta-item">
<span class="meta-label">学号</span>
<span class="meta-value">{{ record.get('学号', '未知') }}</span>
</div>
<div class="meta-item">
<span class="meta-label">总分</span>
<span class="meta-value">{{ record.get('总分', '未知') }}</span>
</div>
<div class="meta-item">
<span class="meta-label">大类排名</span>
<span class="meta-value">{{ record.get('大类排名', '未知') }}</span>
</div>
<div class="meta-item">
<span class="meta-label">组合排名</span>
<span class="meta-value">{{ record.get('组合排名', '未知') }}</span>
</div>
<div class="meta-item">
<span class="meta-label">排名变动</span>
<div class="meta-value-wrapper">
{% set rank_trend = record.get('rank_trend', '无数据') %}
{% if '↑' in rank_trend %}
<span class="material-icons trend-icon up">arrow_upward</span>
<span class="trend-text up">{{ rank_trend|replace('↑', '') }}</span>
{% elif '↓' in rank_trend %}
<span class="material-icons trend-icon down">arrow_downward</span>
<span class="trend-text down">{{ rank_trend|replace('↓', '') }}</span>
{% else %}
<span class="trend-text">{{ rank_trend }}</span>
{% endif %}
</div>
</div>
<div class="meta-item">
<span class="meta-label">总分变动</span>
<div class="meta-value-wrapper">
{% set score_trend = record.get('score_trend', '无数据') %}
{% if '↑' in score_trend %}
<span class="material-icons trend-icon up">arrow_upward</span>
<span class="trend-text up">{{ score_trend|replace('↑', '') }}</span>
{% elif '↓' in score_trend %}
<span class="material-icons trend-icon down">arrow_downward</span>
<span class="trend-text down">{{ score_trend|replace('↓', '') }}</span>
{% else %}
<span class="trend-text">{{ score_trend }}</span>
{% endif %}
</div>
</div>
</div>
</div>
<table class="score-table">
<thead>
<tr>
<th>科目</th>
<th>成绩</th>
<th>校次</th>
</tr>
</thead>
<tbody>
{% for subject in ['语文', '数学', '英语', '物理', '化学', '生物', '历史', '政治', '地理'] %}
{% if record.get(subject) and record.get(subject) != '无数据' %}
<tr>
<td>{{ subject }}</td>
<td>{{ record.get(subject, '-') }}</td>
<td>{{ record.get(subject ~ '校次', '-') }}</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
<!-- 单次考试雷达图分析 -->
<div class="single-exam-analysis">
<h4>科目能力雷达图</h4>
<div class="chart-card">
<div class="chart-area">
<canvas id="radar-chart-{{ loop.index }}"></canvas>
<div id="no-radar-data-{{ loop.index }}" class="no-chart-data" style="display:none;">
暂无完整科目数据显示雷达图
</div>
</div>
</div>
</div>
</div>
{% endfor %}
{% if result.scores_data|length > 1 %}
<div class="fade-in" style="padding: 20px 0;">
<!-- 趋势图部分 -->
<div class="trend-section">
<h4 style="text-align: center; font-size: 1.5rem; color: var(--primary-color); margin-bottom: 20px;">历史成绩趋势分析</h4>
<!-- 科目选择器 -->
<div class="subject-toggles" id="subject-toggles">
<!-- 科目选择器将由JavaScript动态生成 -->
</div>
<!-- 趋势图 -->
<div class="chart-card">
<h4>排名趋势对比</h4>
<div class="chart-area">
<canvas id="trend-chart-canvas"></canvas>
<div id="no-trend-data" class="no-trend-data" style="display:none;">
暂无足够数据显示趋势,至少需要两次考试记录。
</div>
</div>
</div>
</div>
<!-- 统计数据部分 -->
<div class="statistics-section" id="statistics-section">
<!-- 统计内容将由JavaScript动态生成 -->
</div>
</div>
{% endif %}
</div>
{% else %}
{% if result.message %}
<div class="card">
<div class="info-message">
<span class="material-icons">info</span> {{ result.message }}
</div>
<p class="info-message">请检查身份证号是否正确。</p>
</div>
{% endif %}
{% endif %}
</div>
{% endif %}
</div>
<script>
// 全局变量
let examDataCache = null;
let charts = {}; // 用于存储图表实例
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('queryForm');
const loadingMessage = document.getElementById('loading-message');
const nameInput = document.getElementById('name-input');
const idNumberInput = document.getElementById('id-number-input');
const studentListDiv = document.getElementById('student-list');
// 搜索方式切换
const toggleBtns = document.querySelectorAll('.search-toggle-btn');
const nameSearchSection = document.querySelector('.name-search-section');
const idcardSearchSection = document.querySelector('.idcard-search-section');
toggleBtns.forEach(btn => {
btn.addEventListener('click', function() {
// 取消所有按钮的激活状态
toggleBtns.forEach(b => b.classList.remove('active'));
// 激活当前按钮
this.classList.add('active');
// 显示对应的搜索区域
const target = this.dataset.target;
if (target === 'name-search') {
if (nameSearchSection) nameSearchSection.classList.add('active');
if (idcardSearchSection) idcardSearchSection.classList.remove('active');
} else {
if (nameSearchSection) nameSearchSection.classList.remove('active');
if (idcardSearchSection) idcardSearchSection.classList.add('active');
}
});
});
// 姓名搜索功能
let typingTimer;
const doneTypingInterval = 500;
if (nameInput) {
nameInput.addEventListener('input', function() {
clearTimeout(typingTimer);
if (this.value.trim()) {
typingTimer = setTimeout(searchStudents, doneTypingInterval);
} else {
if (studentListDiv) {
studentListDiv.style.display = 'none';
}
}
});
}
// 姓名搜索函数
function searchStudents() {
const name = nameInput ? nameInput.value.trim() : '';
if (!name || !studentListDiv) return;
studentListDiv.innerHTML = '<div style="text-align:center;padding:15px;">正在搜索...</div>';
studentListDiv.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) {
studentListDiv.innerHTML = '';
data.students.forEach(student => {
const div = document.createElement('div');
div.className = 'student-item fade-in';
div.innerHTML = `
<div class="student-name">${student.姓名}</div>
<div class="student-id">${student.身份证号}</div>
`;
div.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
if (idNumberInput) {
idNumberInput.value = student.身份证号;
if (form) {
form.submit();
}
}
studentListDiv.style.display = 'none';
if (loadingMessage) loadingMessage.style.display = 'block';
});
// 移动端触摸优化
div.addEventListener('touchstart', function(e) {
e.stopPropagation();
}, { passive: true });
studentListDiv.appendChild(div);
});
} else {
studentListDiv.innerHTML = '<div style="text-align:center;padding:15px;color:#888;">未找到匹配的学生</div>';
}
})
.catch(error => {
console.error('搜索学生出错:', error);
studentListDiv.innerHTML = '<div style="text-align:center;padding:15px;color:red;">搜索出错,请稍后再试</div>';
});
}
// 点击页面其他地方关闭学生列表
document.addEventListener('click', function(event) {
if (nameInput && studentListDiv &&
!nameInput.contains(event.target) &&
!studentListDiv.contains(event.target)) {
studentListDiv.style.display = 'none';
}
});
// 阻止学生列表的滚动事件冒泡
if (studentListDiv) {
studentListDiv.addEventListener('touchmove', function(e) {
e.stopPropagation();
}, { passive: true });
}
// 表单提交处理
if (form && loadingMessage) {
form.addEventListener('submit', function() {
const resultDiv = document.querySelector('.result');
if (resultDiv) {
resultDiv.style.display = 'none';
}
loadingMessage.style.display = 'block';
});
}
// 如果服务器返回了结果,隐藏加载提示
{% if result %}
if (loadingMessage) {
loadingMessage.style.display = 'none';
}
{% endif %}
// 初始化所有图表
initAllCharts();
});
// 切换考试记录标签页
function openTab(evt, tabName) {
const tabcontents = document.getElementsByClassName("tab-content");
for (let i = 0; i < tabcontents.length; i++) {
tabcontents[i].classList.remove("active");
}
const tablinks = document.getElementsByClassName("tab-button");
for (let i = 0; i < tablinks.length; i++) {
tablinks[i].classList.remove("active");
}
document.getElementById(tabName).classList.add("active");
evt.currentTarget.classList.add("active");
// Re-initialize radar chart for the newly shown tab
setTimeout(() => {
const examId = tabName.replace('record-', '');
renderRadarChart(examId);
}, 100);
}
// 渲染历史成绩排名统计
function renderStatistics() {
const statsContainer = document.getElementById(`statistics-section`);
if (!statsContainer) return;
const allExams = collectAllExamData();
statsContainer.innerHTML = '';
if (allExams.length === 0) {
const noDataP = document.createElement('p');
noDataP.className = 'no-stats-data';
noDataP.textContent = '无历史数据可供统计。';
statsContainer.appendChild(noDataP);
return;
}
const sectionTitle = document.createElement('h4');
sectionTitle.textContent = '历史成绩排名统计';
statsContainer.appendChild(sectionTitle);
const table = document.createElement('table');
table.className = 'statistics-table';
const thead = table.createTHead();
const headerRow = thead.insertRow();
['科目/项目', '平均排名', '最高排名', '最低排名', '有效考试次数'].forEach(text => {
const th = document.createElement('th');
th.textContent = text;
headerRow.appendChild(th);
});
const tbody = table.createTBody();
const itemsToStat = ['大类排名', '语文', '数学', '英语', '物理', '化学', '生物', '历史', '政治', '地理'];
itemsToStat.forEach(item => {
const ranks = [];
allExams.forEach(exam => {
let rankStr;
if (item === '大类排名') {
rankStr = exam.typeRanking;
} else {
rankStr = exam[`${item}校次`];
}
if (rankStr && rankStr !== '无数据' && rankStr.trim() !== '') {
const rankVal = parseInt(rankStr);
if (!isNaN(rankVal)) {
ranks.push(rankVal);
}
}
});
if (ranks.length > 0) {
const row = tbody.insertRow();
row.insertCell().textContent = item;
const sum = ranks.reduce((acc, val) => acc + val, 0);
const avg = (sum / ranks.length).toFixed(2);
const minRank = Math.min(...ranks);
const maxRank = Math.max(...ranks);
row.insertCell().textContent = avg;
row.insertCell().textContent = minRank;
row.insertCell().textContent = maxRank;
row.insertCell().textContent = ranks.length;
}
});
statsContainer.appendChild(table);
}
// 图表的全局配置
const chartOptions = {
activeSubject: '总分', // 当前激活的科目
subjects: {},
colors: {
'总分': 'rgb(255, 99, 132)', // 大类排名用红色
'语文': 'rgb(255, 159, 64)',
'数学': 'rgb(75, 192, 192)',
'英语': 'rgb(255, 206, 86)',
'物理': 'rgb(153, 102, 255)',
'化学': 'rgb(199, 199, 199)',
'生物': 'rgb(83, 102, 255)',
'历史': 'rgb(255, 99, 132)',
'政治': 'rgb(54, 162, 235)',
'地理': 'rgb(40, 159, 64)'
}
};
// 移除切换趋势图表视图函数,因为不再需要
// 初始化所有图表
function initAllCharts() {
const allExams = collectAllExamData();
// 为每个标签页初始化雷达图
const tabContents = document.querySelectorAll('.tab-content');
tabContents.forEach(tab => {
if (tab.id.startsWith('record-')) {
const examId = tab.id.replace('record-', '');
renderRadarChart(examId);
}
});
if (allExams.length > 1) {
initSubjectToggles();
renderTrendChart();
renderStatistics();
}
}
// 初始化科目选择器
function initSubjectToggles() {
const allExams = collectAllExamData();
if (allExams.length < 2) return;
const subjects = ['总分', '语文', '数学', '英语', '物理', '化学', '生物', '历史', '政治', '地理'];
const availableSubjects = [];
subjects.forEach(subject => {
let hasData = false;
if (subject === '总分') {
hasData = allExams.some(exam => exam.typeRanking && exam.typeRanking !== '无数据');
} else {
hasData = allExams.some(exam => exam[`${subject}校次`] && exam[`${subject}校次`] !== '无数据');
}
if (hasData) {
availableSubjects.push(subject);
}
});
const toggleContainer = document.getElementById(`subject-toggles`);
if (!toggleContainer) return;
toggleContainer.innerHTML = '';
availableSubjects.forEach(subject => {
const toggle = document.createElement('div');
toggle.className = `subject-toggle ${subject === chartOptions.activeSubject ? 'active' : ''}`;
toggle.dataset.subject = subject;
const colorIndicator = document.createElement('span');
colorIndicator.className = 'color-indicator';
colorIndicator.style.backgroundColor = chartOptions.colors[subject];
toggle.appendChild(colorIndicator);
toggle.appendChild(document.createTextNode(subject));
toggle.addEventListener('click', function() {
// 移除所有active类
document.querySelectorAll('#subject-toggles .subject-toggle').forEach(
t => t.classList.remove('active')
);
// 激活当前选择
this.classList.add('active');
chartOptions.activeSubject = this.dataset.subject;
renderTrendChart();
});
toggleContainer.appendChild(toggle);
});
}
// 渲染趋势图表
function renderTrendChart() {
const chartCanvas = document.getElementById(`trend-chart-canvas`);
if (!chartCanvas) return;
const allExams = collectAllExamData();
if (allExams.length < 2) {
const noDataElement = document.getElementById(`no-trend-data`);
if (noDataElement) noDataElement.style.display = 'block';
chartCanvas.style.display = 'none';
return;
}
const noDataElement = document.getElementById(`no-trend-data`);
if (noDataElement) noDataElement.style.display = 'none';
chartCanvas.style.display = 'block';
// 使用后端提供的排序索引进行排序
allExams.sort((a, b) => a.sortIndex - b.sortIndex);
const labels = allExams.map(exam => formatExamName(exam.examName));
const subject = chartOptions.activeSubject;
let data, label, color;
if (subject === '总分') {
data = allExams.map(exam => parseInt(exam.typeRanking) || null);
label = '大类排名';
color = chartOptions.colors['总分'];
} else {
data = allExams.map(exam => {
const rank = exam[`${subject}校次`];
return (rank && rank !== '无数据') ? parseInt(rank) : null;
});
label = `${subject}校次`;
color = chartOptions.colors[subject];
}
if (charts.trendChart) {
charts.trendChart.destroy();
}
const ctx = chartCanvas.getContext('2d');
charts.trendChart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: label,
data: data,
borderColor: color,
backgroundColor: color.replace('rgb', 'rgba').replace(')', ', 0.1)'),
tension: 0.3,
fill: false,
pointBackgroundColor: color,
pointBorderColor: '#fff',
pointHoverBackgroundColor: '#fff',
pointHoverBorderColor: color,
pointRadius: 5,
pointHoverRadius: 7,
borderWidth: 3
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
reverse: true,
title: {
display: true,
text: '排名',
font: {
size: 14,
weight: 'bold'
}
},
grid: {
color: 'rgba(0,0,0,0.1)'
}
},
x: {
title: {
display: true,
text: '考试',
font: {
size: 14,
weight: 'bold'
}
},
grid: {
display: false
}
}
},
plugins: {
title: {
display: true,
text: `${subject} 排名趋势变化`,
font: {
size: 16,
weight: 'bold'
}
},
legend: {
display: false
},
tooltip: {
mode: 'index',
intersect: false,
backgroundColor: 'rgba(0,0,0,0.8)',
titleColor: '#fff',
bodyColor: '#fff',
borderColor: color,
borderWidth: 2,
cornerRadius: 6
}
},
elements: {
line: {
tension: 0.3
},
point: {
radius: 5,
hitRadius: 15,
hoverRadius: 8
}
},
animation: {
duration: 1000,
easing: 'easeInOutQuart'
},
interaction: {
intersect: false,
mode: 'index'
}
}
});
}
// 移除原来的渲染排名趋势图表和渲染学科排名趋势图表函数
// 渲染雷达图
function renderRadarChart(examId) {
const chartCanvas = document.getElementById(`radar-chart-${examId}`);
if (!chartCanvas) return;
const tabContent = document.getElementById(`record-${examId}`);
if (!tabContent) return;
const scoreTable = tabContent.querySelector('.score-table');
if (!scoreTable) return;
const subjects = ['语文', '数学', '英语', '物理', '化学', '生物', '历史', '政治', '地理'];
const data = [];
const validSubjects = [];
const tableRows = scoreTable.querySelectorAll('tbody tr');
tableRows.forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3) {
const subject = cells[0].textContent.trim();
const rank = cells[2].textContent.trim();
if (subjects.includes(subject) && rank && rank !== '-' && rank !== '无数据') {
const rankNum = parseInt(rank);
if (!isNaN(rankNum)) {
data.push(rankNum);
validSubjects.push(subject);
}
}
}
});
if (validSubjects.length < 3) {
const noDataElement = document.getElementById(`no-radar-data-${examId}`);
if (noDataElement) noDataElement.style.display = 'block';
chartCanvas.style.display = 'none';
return;
}
const noDataElement = document.getElementById(`no-radar-data-${examId}`);
if (noDataElement) noDataElement.style.display = 'none';
chartCanvas.style.display = 'block';
const chartKey = `radarChart${examId}`;
if (charts[chartKey]) {
charts[chartKey].destroy();
}
const ctx = chartCanvas.getContext('2d');
charts[chartKey] = new Chart(ctx, {
type: 'radar',
data: {
labels: validSubjects,
datasets: [{
label: '校内排名',
data: data,
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)',
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
r: {
angleLines: {
display: true
},
suggestedMin: 0,
ticks: {
display: false
},
pointLabels: {
font: {
size: 12,
weight: 'bold'
}
},
reverse: true
}
},
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: function(context) {
return `${context.label}: 排名 ${context.parsed.r}`;
}
}
}
}
}
});
}
// 收集所有考试数据
function collectAllExamData() {
if (examDataCache) return examDataCache;
const allExams = [];
{% if result and result.scores_data %}
const examData = [
{% for record in result.scores_data %}
{
examName: "{{ record.get('考试名称', '未知')|escape }}",
examTime: "{{ record.get('考试时间', '')|escape }}",
sortIndex: {{ record.get('sort_index', 0) }},
typeRanking: "{{ record.get('大类排名', '')|escape }}",
comboRanking: "{{ record.get('组合排名', '')|escape }}",
"语文": "{{ record.get('语文', '无数据')|escape }}",
"语文校次": "{{ record.get('语文校次', '无数据')|escape }}",
"数学": "{{ record.get('数学', '无数据')|escape }}",
"数学校次": "{{ record.get('数学校次', '无数据')|escape }}",
"英语": "{{ record.get('英语', '无数据')|escape }}",
"英语校次": "{{ record.get('英语校次', '无数据')|escape }}",
"物理": "{{ record.get('物理', '无数据')|escape }}",
"物理校次": "{{ record.get('物理校次', '无数据')|escape }}",
"化学": "{{ record.get('化学', '无数据')|escape }}",
"化学校次": "{{ record.get('化学校次', '无数据')|escape }}",
"生物": "{{ record.get('生物', '无数据')|escape }}",
"生物校次": "{{ record.get('生物校次', '无数据')|escape }}",
"历史": "{{ record.get('历史', '无数据')|escape }}",
"历史校次": "{{ record.get('历史校次', '无数据')|escape }}",
"政治": "{{ record.get('政治', '无数据')|escape }}",
"政治校次": "{{ record.get('政治校次', '无数据')|escape }}",
"地理": "{{ record.get('地理', '无数据')|escape }}",
"地理校次": "{{ record.get('地理校次', '无数据')|escape }}"
}{% if not loop.last %},{% endif %}
{% endfor %}
];
allExams.push(...examData);
{% endif %}
examDataCache = allExams;
return allExams;
}
// 格式化考试名称
function formatExamName(name) {
if (!name) return "未知";
if (name.length > 10) {
return name.substring(0, 10) + '...';
}
return name;
}
</script>
</body>
</html>