949 lines
44 KiB
HTML
Executable File
949 lines
44 KiB
HTML
Executable File
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>成绩追踪应用</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
|
|
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.8/dist/chart.umd.min.js"></script>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
|
|
<!-- Tailwind 配置 -->
|
|
<script>
|
|
tailwind.config = {
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
primary: '#165DFF',
|
|
secondary: '#7B61FF',
|
|
success: '#00B42A',
|
|
warning: '#FF7D00',
|
|
danger: '#F53F3F',
|
|
dark: '#1D2129',
|
|
light: '#F2F3F5'
|
|
},
|
|
fontFamily: {
|
|
inter: ['Inter', 'sans-serif'],
|
|
},
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style type="text/tailwindcss">
|
|
@layer utilities {
|
|
.content-auto {
|
|
content-visibility: auto;
|
|
}
|
|
.card-shadow {
|
|
box-shadow: 0 10px 30px -5px rgba(0, 0, 0, 0.1);
|
|
}
|
|
.input-focus {
|
|
@apply focus:ring-2 focus:ring-primary/50 focus:border-primary;
|
|
}
|
|
.btn-hover {
|
|
@apply hover:shadow-lg transform hover:-translate-y-0.5 transition-all duration-300;
|
|
}
|
|
.animate-fade-in {
|
|
animation: fadeIn 0.5s ease-in-out;
|
|
}
|
|
.animate-slide-up {
|
|
animation: slideUp 0.5s ease-out;
|
|
}
|
|
.toggle-checkbox:checked {
|
|
right: 0;
|
|
border-color: #165DFF;
|
|
}
|
|
.toggle-checkbox:checked + .toggle-label {
|
|
background-color: #165DFF;
|
|
}
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; }
|
|
to { opacity: 1; }
|
|
}
|
|
|
|
@keyframes slideUp {
|
|
from { transform: translateY(20px); opacity: 0; }
|
|
to { transform: translateY(0); opacity: 1; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="font-inter bg-gradient-to-br from-light to-white min-h-screen text-dark">
|
|
<div class="container mx-auto px-4 py-8 max-w-6xl">
|
|
<!-- 页面标题 -->
|
|
<header class="text-center mb-12 animate-fade-in">
|
|
<h1 class="text-[clamp(2rem,5vw,3.5rem)] font-bold text-primary mb-4">
|
|
<i class="fa fa-line-chart mr-3"></i>成绩追踪
|
|
</h1>
|
|
<p class="text-gray-600 text-lg max-w-2xl mx-auto">
|
|
记录、分析和可视化你的预估成绩与实际成绩,帮助你更好地掌握学习情况
|
|
</p>
|
|
</header>
|
|
|
|
<main class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
<!-- 左侧:成绩输入表单 -->
|
|
<section class="lg:col-span-1 animate-slide-up" style="animation-delay: 0.1s">
|
|
<div class="bg-white rounded-xl p-6 card-shadow h-full">
|
|
<h2 class="text-xl font-semibold mb-6 flex items-center">
|
|
<i class="fa fa-pencil-square-o text-primary mr-2"></i>添加成绩
|
|
</h2>
|
|
|
|
<form id="gradeForm" class="space-y-4">
|
|
<div>
|
|
<label for="course" class="block text-sm font-medium text-gray-700 mb-1">课程名称</label>
|
|
<div class="relative">
|
|
<select id="course" name="course"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg input-focus transition-all appearance-none"
|
|
required>
|
|
<option value="">请选择课程</option>
|
|
<!-- 课程选项会通过JavaScript动态添加 -->
|
|
</select>
|
|
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-3 text-gray-400">
|
|
<i class="fa fa-chevron-down"></i>
|
|
</div>
|
|
</div>
|
|
<button type="button" id="addCourseBtn" class="mt-2 text-sm text-primary hover:text-primary/80">
|
|
<i class="fa fa-plus-circle mr-1"></i> 添加新课程
|
|
</button>
|
|
</div>
|
|
|
|
<div id="newCourseInput" class="hidden">
|
|
<label for="newCourse" class="block text-sm font-medium text-gray-700 mb-1">新课程名称</label>
|
|
<input type="text" id="newCourse" name="newCourse"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg input-focus transition-all"
|
|
placeholder="输入新课程名称">
|
|
<div class="flex space-x-2 mt-2">
|
|
<button type="button" id="cancelAddCourse" class="px-3 py-1 text-sm border border-gray-300 rounded hover:bg-gray-50">
|
|
取消
|
|
</button>
|
|
<button type="button" id="confirmAddCourse" class="px-3 py-1 text-sm bg-primary text-white rounded hover:bg-primary/90">
|
|
确认添加
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="estimatedScore" class="block text-sm font-medium text-gray-700 mb-1">预估</label>
|
|
<input type="number" id="estimatedScore" name="estimatedScore" min="0" max="100" step="0.1"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg input-focus transition-all"
|
|
placeholder="0-100" required>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="actualScore" class="block text-sm font-medium text-gray-700 mb-1">实际</label>
|
|
<input type="number" id="actualScore" name="actualScore" min="0" max="100" step="0.1"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg input-focus transition-all"
|
|
placeholder="0-100" required>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="description" class="block text-sm font-medium text-gray-700 mb-1">备注</label>
|
|
<textarea id="description" name="description" rows="3"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg input-focus transition-all"
|
|
placeholder="考试类型、难度等信息(选填)"></textarea>
|
|
</div>
|
|
|
|
<button type="submit"
|
|
class="w-full bg-primary hover:bg-primary/90 text-white font-medium py-3 px-6 rounded-lg
|
|
btn-hover flex items-center justify-center">
|
|
<i class="fa fa-plus-circle mr-2"></i>添加成绩
|
|
</button>
|
|
</form>
|
|
|
|
<!-- 统计卡片 -->
|
|
<div class="mt-8 grid grid-cols-2 gap-4">
|
|
<div class="bg-light rounded-lg p-4">
|
|
<p class="text-sm text-gray-500">平均预估</p>
|
|
<p id="averageEstimated" class="text-2xl font-bold">--</p>
|
|
</div>
|
|
<div class="bg-light rounded-lg p-4">
|
|
<p class="text-sm text-gray-500">平均实际</p>
|
|
<p id="averageActual" class="text-2xl font-bold">--</p>
|
|
</div>
|
|
<div class="bg-light rounded-lg p-4">
|
|
<p class="text-sm text-gray-500">预估偏差</p>
|
|
<p id="averageDeviation" class="text-2xl font-bold">--</p>
|
|
</div>
|
|
<div class="bg-light rounded-lg p-4">
|
|
<p class="text-sm text-gray-500">成绩总数</p>
|
|
<p id="totalGrades" class="text-2xl font-bold">0</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- 右侧:图表和成绩列表 -->
|
|
<section class="lg:col-span-2 space-y-8 animate-slide-up" style="animation-delay: 0.3s">
|
|
<!-- 图表区域 -->
|
|
<div class="bg-white rounded-xl p-6 card-shadow">
|
|
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 gap-4">
|
|
<h2 class="text-xl font-semibold flex items-center">
|
|
<i class="fa fa-bar-chart text-primary mr-2"></i>成绩趋势
|
|
</h2>
|
|
<div class="flex flex-wrap gap-2">
|
|
<button id="chartTypeLine" class="px-3 py-1 text-sm bg-primary text-white rounded-md">
|
|
折线图
|
|
</button>
|
|
<button id="chartTypeBar" class="px-3 py-1 text-sm bg-gray-200 hover:bg-gray-300 rounded-md">
|
|
柱状图
|
|
</button>
|
|
<button id="toggleAllCourses" class="px-3 py-1 text-sm bg-gray-200 hover:bg-gray-300 rounded-md">
|
|
全部显示
|
|
</button>
|
|
<select id="courseSelector" class="px-3 py-1 text-sm border border-gray-300 rounded-md">
|
|
<option value="">全部科目</option>
|
|
<!-- 科目选项会通过JavaScript动态添加 -->
|
|
</select>
|
|
<button id="exportChart" class="px-3 py-1 text-sm bg-success/10 text-success rounded-md hover:bg-success/20">
|
|
<i class="fa fa-download mr-1"></i>导出图片
|
|
</button>
|
|
<!-- 新增导出数据按钮 -->
|
|
<button id="exportData" class="px-3 py-1 text-sm bg-secondary/10 text-secondary rounded-md hover:bg-secondary/20">
|
|
<i class="fa fa-file-text-o mr-1"></i>导出数据
|
|
</button>
|
|
<button id="exportCode" class="px-3 py-2 bg-secondary/10 text-secondary rounded-lg hover:bg-secondary/20 transition-colors">
|
|
<i class="fa fa-share-alt mr-1"></i>生成分享代码
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 科目显示控制 -->
|
|
<div id="courseVisibilityControls" class="mb-4 flex flex-wrap gap-2">
|
|
<!-- 科目控制按钮会通过JavaScript动态添加 -->
|
|
</div>
|
|
|
|
<div class="h-80">
|
|
<canvas id="gradeChart"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 成绩列表 -->
|
|
<div class="bg-white rounded-xl p-6 card-shadow">
|
|
<div class="flex justify-between items-center mb-6">
|
|
<h2 class="text-xl font-semibold flex items-center">
|
|
<i class="fa fa-list-alt text-primary mr-2"></i>成绩记录
|
|
</h2>
|
|
<div class="flex space-x-2">
|
|
<button id="clearAll" class="px-3 py-2 bg-danger/10 text-danger rounded-lg hover:bg-danger/20 transition-colors">
|
|
<i class="fa fa-trash-o mr-1"></i>清空
|
|
</button>
|
|
<button id="exportRecordImage" class="px-3 py-2 bg-success/10 text-success rounded-lg hover:bg-success/20 transition-colors">
|
|
<i class="fa fa-download mr-1"></i>导出为图片
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full divide-y divide-gray-200">
|
|
<thead>
|
|
<tr>
|
|
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
课程
|
|
</th>
|
|
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
预估
|
|
</th>
|
|
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
实际
|
|
</th>
|
|
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
偏差
|
|
</th>
|
|
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
备注
|
|
</th>
|
|
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
操作
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="gradesList" class="bg-white divide-y divide-gray-200">
|
|
<!-- 成绩记录将通过 JavaScript 动态添加 -->
|
|
<tr class="text-center">
|
|
<td colspan="6" class="px-6 py-12 text-gray-500">
|
|
<div class="flex flex-col items-center">
|
|
<i class="fa fa-file-text-o text-4xl mb-3 text-gray-300"></i>
|
|
<p>暂无成绩记录</p>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- 分页控件 -->
|
|
<div class="flex justify-between items-center mt-4 text-sm">
|
|
<div id="paginationInfo" class="text-gray-500">
|
|
显示 0-0 条,共 0 条
|
|
</div>
|
|
<div class="flex space-x-1">
|
|
<button id="prevPage" class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed" disabled>
|
|
上一页
|
|
</button>
|
|
<span id="currentPage" class="px-3 py-1">1</span>
|
|
<button id="nextPage" class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed" disabled>
|
|
下一页
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<!-- 页脚 -->
|
|
<footer class="mt-16 text-center text-gray-500 text-sm py-4 border-t border-gray-200">
|
|
<p>Copyright © 2023-2025 CCSIT Network.All rights reserved. © 圆周云境信息技术 保留所有权利。</p>
|
|
</footer>
|
|
</div>
|
|
|
|
<!-- 编辑模态框 -->
|
|
<div id="editModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
|
|
<div class="bg-white rounded-xl p-6 max-w-md w-full mx-4 animate-fade-in">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h3 class="text-xl font-semibold">编辑成绩</h3>
|
|
<button id="closeModal" class="text-gray-400 hover:text-gray-500">
|
|
<i class="fa fa-times"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<form id="editForm" class="space-y-4">
|
|
<input type="hidden" id="editId">
|
|
|
|
<div>
|
|
<label for="editCourse" class="block text-sm font-medium text-gray-700 mb-1">课程名称</label>
|
|
<select id="editCourse" name="editCourse"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg input-focus transition-all" required>
|
|
<!-- 课程选项会通过JavaScript动态添加 -->
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="editEstimatedScore" class="block text-sm font-medium text-gray-700 mb-1">预估</label>
|
|
<input type="number" id="editEstimatedScore" name="editEstimatedScore" min="0" max="100" step="0.1"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg input-focus transition-all" required>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="editActualScore" class="block text-sm font-medium text-gray-700 mb-1">实际</label>
|
|
<input type="number" id="editActualScore" name="editActualScore" min="0" max="100" step="0.1"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg input-focus transition-all" required>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="editDescription" class="block text-sm font-medium text-gray-700 mb-1">备注</label>
|
|
<textarea id="editDescription" name="editDescription" rows="3"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg input-focus transition-all"></textarea>
|
|
</div>
|
|
|
|
<div class="flex space-x-3">
|
|
<button type="button" id="cancelEdit" class="flex-1 bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-2 px-4 rounded-lg btn-hover">
|
|
取消
|
|
</button>
|
|
<button type="submit" class="flex-1 bg-primary hover:bg-primary/90 text-white font-medium py-2 px-4 rounded-lg btn-hover">
|
|
保存修改
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 删除确认模态框 -->
|
|
<div id="deleteModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
|
|
<div class="bg-white rounded-xl p-6 max-w-md w-full mx-4 animate-fade-in">
|
|
<div class="text-center mb-4">
|
|
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-danger/10 text-danger mb-4">
|
|
<i class="fa fa-exclamation-triangle text-2xl"></i>
|
|
</div>
|
|
<h3 class="text-xl font-semibold">确认删除</h3>
|
|
<p class="text-gray-500 mt-2">你确定要删除这条成绩记录吗?此操作无法撤销。</p>
|
|
</div>
|
|
|
|
<input type="hidden" id="deleteId">
|
|
|
|
<div class="flex space-x-3 mt-6">
|
|
<button id="cancelDelete" class="flex-1 bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-2 px-4 rounded-lg btn-hover">
|
|
取消
|
|
</button>
|
|
<button id="confirmDelete" class="flex-1 bg-danger hover:bg-danger/90 text-white font-medium py-2 px-4 rounded-lg btn-hover">
|
|
确认删除
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="codeModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
|
|
<div class="bg-white rounded-xl p-6 max-w-md w-full mx-4 animate-fade-in">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h3 class="text-xl font-semibold">输入成绩代码</h3>
|
|
<button onclick="closeCodeModal()" class="text-gray-400 hover:text-gray-500">
|
|
<i class="fa fa-times"></i>
|
|
</button>
|
|
</div>
|
|
<div class="space-y-4">
|
|
<textarea id="importCode" class="w-full h-32 p-3 border rounded-lg" placeholder="粘贴成绩代码..."></textarea>
|
|
<button onclick="loadFromCode()" class="w-full bg-primary text-white py-2 rounded-lg hover:bg-primary/90">
|
|
加载成绩
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- 清空确认模态框 -->
|
|
<script>
|
|
// 模拟数据
|
|
let grades = [];
|
|
let chartInstance;
|
|
let chartType = 'line';
|
|
let currentPage = 1;
|
|
const itemsPerPage = 10;
|
|
|
|
// DOM 元素
|
|
const gradeForm = document.getElementById('gradeForm');
|
|
const courseSelect = document.getElementById('course');
|
|
const addCourseBtn = document.getElementById('addCourseBtn');
|
|
const newCourseInput = document.getElementById('newCourseInput');
|
|
const cancelAddCourse = document.getElementById('cancelAddCourse');
|
|
const confirmAddCourse = document.getElementById('confirmAddCourse');
|
|
const gradesList = document.getElementById('gradesList');
|
|
const averageEstimated = document.getElementById('averageEstimated');
|
|
const averageActual = document.getElementById('averageActual');
|
|
const averageDeviation = document.getElementById('averageDeviation');
|
|
const totalGrades = document.getElementById('totalGrades');
|
|
const chartTypeLine = document.getElementById('chartTypeLine');
|
|
const chartTypeBar = document.getElementById('chartTypeBar');
|
|
const toggleAllCourses = document.getElementById('toggleAllCourses');
|
|
const courseVisibilityControls = document.getElementById('courseVisibilityControls');
|
|
const searchInput = document.getElementById('searchInput');
|
|
const clearAll = document.getElementById('clearAll');
|
|
const prevPage = document.getElementById('prevPage');
|
|
const nextPage = document.getElementById('nextPage');
|
|
const currentPageDisplay = document.getElementById('currentPage');
|
|
const paginationInfo = document.getElementById('paginationInfo');
|
|
const editModal = document.getElementById('editModal');
|
|
const editForm = document.getElementById('editForm');
|
|
const closeModal = document.getElementById('closeModal');
|
|
const cancelEdit = document.getElementById('cancelEdit');
|
|
const deleteModal = document.getElementById('deleteModal');
|
|
const cancelDelete = document.getElementById('cancelDelete');
|
|
const confirmDelete = document.getElementById('confirmDelete');
|
|
const courseSelector = document.getElementById('courseSelector');
|
|
const exportCodeBtn = document.getElementById('exportCode');
|
|
const codeModal = document.getElementById('codeModal');
|
|
const importCode = document.getElementById('importCode');
|
|
|
|
// 保存数据到本地存储
|
|
function saveDataToLocalStorage() {
|
|
localStorage.setItem('grades', JSON.stringify(grades));
|
|
}
|
|
|
|
// 从本地存储加载数据
|
|
function loadDataFromLocalStorage() {
|
|
const savedGrades = localStorage.getItem('grades');
|
|
if (savedGrades) {
|
|
grades = JSON.parse(savedGrades);
|
|
}
|
|
}
|
|
|
|
// 检查 URL 中是否有成绩代码参数
|
|
function getGradeCodeFromURL() {
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const gradeCode = urlParams.get('code');
|
|
|
|
if (gradeCode) {
|
|
try {
|
|
// 解码成绩代码
|
|
const decodedData = decodeURIComponent(escape(atob(gradeCode)));
|
|
const loadedGrades = JSON.parse(decodedData);
|
|
|
|
// 替换当前成绩数据
|
|
grades = loadedGrades;
|
|
|
|
// 保存数据到本地存储
|
|
saveDataToLocalStorage();
|
|
|
|
alert('成绩代码加载成功!');
|
|
} catch (error) {
|
|
alert('加载成绩代码失败:' + error.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 初始化
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// 从本地存储加载数据
|
|
loadDataFromLocalStorage();
|
|
|
|
// 检查 URL 中是否有成绩代码参数
|
|
getGradeCodeFromURL();
|
|
|
|
// 初始化课程选择器
|
|
updateCourseSelect();
|
|
// 初始化下拉框选项
|
|
updateCourseSelector();
|
|
// 初始化图表
|
|
updateChart();
|
|
// 初始化成绩列表
|
|
updateGradesList();
|
|
|
|
// 绑定事件监听器
|
|
gradeForm.addEventListener('submit', handleGradeSubmit);
|
|
addCourseBtn.addEventListener('click', showNewCourseInput);
|
|
cancelAddCourse.addEventListener('click', hideNewCourseInput);
|
|
confirmAddCourse.addEventListener('click', addNewCourse);
|
|
chartTypeLine.addEventListener('click', () => changeChartType('line'));
|
|
chartTypeBar.addEventListener('click', () => changeChartType('bar'));
|
|
toggleAllCourses.addEventListener('click', toggleAllCoursesVisibility);
|
|
searchInput.addEventListener('input', handleSearch);
|
|
clearAll.addEventListener('click', confirmClearAll);
|
|
prevPage.addEventListener('click', () => changePage(currentPage - 1));
|
|
nextPage.addEventListener('click', () => changePage(currentPage + 1));
|
|
closeModal.addEventListener('click', closeEditModal);
|
|
cancelEdit.addEventListener('click', closeEditModal);
|
|
cancelDelete.addEventListener('click', closeDeleteModal);
|
|
confirmDelete.addEventListener('click', handleDelete);
|
|
editForm.addEventListener('submit', handleEditSubmit);
|
|
courseSelector.addEventListener('change', handleCourseSelect);
|
|
exportCodeBtn.addEventListener('click', exportGradeCode);
|
|
});
|
|
|
|
// 更新课程选择器
|
|
function updateCourseSelect(selectElement = courseSelect) {
|
|
const courses = [...new Set(grades.map(grade => grade.course))];
|
|
selectElement.innerHTML = '<option value="">请选择课程</option>';
|
|
courses.forEach(course => {
|
|
const option = document.createElement('option');
|
|
option.value = course;
|
|
option.textContent = course;
|
|
selectElement.appendChild(option);
|
|
});
|
|
}
|
|
|
|
// 更新下拉框选项
|
|
function updateCourseSelector() {
|
|
while (courseSelector.options.length > 1) {
|
|
courseSelector.remove(1);
|
|
}
|
|
|
|
const courses = [...new Set(grades.map(grade => grade.course))];
|
|
|
|
courses.forEach(course => {
|
|
const option = document.createElement('option');
|
|
option.value = course;
|
|
option.textContent = course;
|
|
courseSelector.appendChild(option);
|
|
});
|
|
}
|
|
|
|
// 处理科目选择事件
|
|
function handleCourseSelect() {
|
|
const selectedCourse = courseSelector.value;
|
|
if (selectedCourse === '') {
|
|
updateChart();
|
|
} else {
|
|
updateChart(selectedCourse);
|
|
}
|
|
}
|
|
|
|
// 处理成绩提交
|
|
function handleGradeSubmit(e) {
|
|
e.preventDefault();
|
|
const course = courseSelect.value;
|
|
const estimatedScore = parseFloat(document.getElementById('estimatedScore').value);
|
|
const actualScore = parseFloat(document.getElementById('actualScore').value);
|
|
const description = document.getElementById('description').value;
|
|
const id = grades.length + 1;
|
|
|
|
grades.push({
|
|
id,
|
|
course,
|
|
estimatedScore,
|
|
actualScore,
|
|
description
|
|
});
|
|
|
|
saveDataToLocalStorage();
|
|
|
|
gradeForm.reset();
|
|
updateCourseSelect();
|
|
updateCourseSelector();
|
|
updateChart();
|
|
updateGradesList();
|
|
}
|
|
|
|
// 显示新增课程输入框
|
|
function showNewCourseInput() {
|
|
newCourseInput.classList.remove('hidden');
|
|
addCourseBtn.classList.add('hidden');
|
|
}
|
|
|
|
// 隐藏新增课程输入框
|
|
function hideNewCourseInput() {
|
|
newCourseInput.classList.add('hidden');
|
|
addCourseBtn.classList.remove('hidden');
|
|
document.getElementById('newCourse').value = '';
|
|
}
|
|
|
|
// 添加新课程
|
|
function addNewCourse() {
|
|
const newCourse = document.getElementById('newCourse').value;
|
|
if (newCourse) {
|
|
const option = document.createElement('option');
|
|
option.value = newCourse;
|
|
option.textContent = newCourse;
|
|
courseSelect.appendChild(option);
|
|
courseSelect.value = newCourse;
|
|
hideNewCourseInput();
|
|
}
|
|
}
|
|
|
|
// 更新图表
|
|
function updateChart(selectedCourse = null) {
|
|
if (chartInstance) {
|
|
chartInstance.destroy();
|
|
}
|
|
|
|
let dataToChart = grades;
|
|
if (selectedCourse) {
|
|
dataToChart = grades.filter(grade => grade.course === selectedCourse);
|
|
}
|
|
|
|
const labels = dataToChart.map(grade => grade.course);
|
|
const estimatedScores = dataToChart.map(grade => grade.estimatedScore);
|
|
const actualScores = dataToChart.map(grade => grade.actualScore);
|
|
|
|
const ctx = document.getElementById('gradeChart').getContext('2d');
|
|
chartInstance = new Chart(ctx, {
|
|
type: chartType,
|
|
data: {
|
|
labels: labels,
|
|
datasets: [
|
|
{
|
|
label: '预估',
|
|
data: estimatedScores,
|
|
borderColor: 'rgba(22, 93, 255, 1)',
|
|
backgroundColor: 'rgba(22, 93, 255, 0.2)',
|
|
borderWidth: 1
|
|
},
|
|
{
|
|
label: '实际',
|
|
data: actualScores,
|
|
borderColor: 'rgba(0, 180, 42, 1)',
|
|
backgroundColor: 'rgba(0, 180, 42, 0.2)',
|
|
borderWidth: 1
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// 更改图表类型
|
|
function changeChartType(type) {
|
|
chartType = type;
|
|
if (type === 'line') {
|
|
chartTypeLine.classList.add('bg-primary', 'text-white');
|
|
chartTypeBar.classList.remove('bg-primary', 'text-white');
|
|
chartTypeBar.classList.add('bg-gray-200', 'hover:bg-gray-300');
|
|
} else {
|
|
chartTypeBar.classList.add('bg-primary', 'text-white');
|
|
chartTypeLine.classList.remove('bg-primary', 'text-white');
|
|
chartTypeLine.classList.add('bg-gray-200', 'hover:bg-gray-300');
|
|
}
|
|
updateChart();
|
|
}
|
|
|
|
// 切换所有科目可见性
|
|
function toggleAllCoursesVisibility() {
|
|
updateChart();
|
|
}
|
|
|
|
|
|
// 更新成绩列表
|
|
function updateGradesList(filteredGrades = grades) {
|
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
|
const endIndex = startIndex + itemsPerPage;
|
|
const currentGrades = filteredGrades.slice(startIndex, endIndex);
|
|
|
|
gradesList.innerHTML = '';
|
|
if (currentGrades.length === 0) {
|
|
gradesList.innerHTML = `
|
|
<tr class="text-center">
|
|
<td colspan="6" class="px-6 py-12 text-gray-500">
|
|
<div class="flex flex-col items-center">
|
|
<i class="fa fa-file-text-o text-4xl mb-3 text-gray-300"></i>
|
|
<p>暂无成绩记录</p>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
} else {
|
|
currentGrades.forEach(grade => {
|
|
const deviation = grade.estimatedScore - grade.actualScore;
|
|
const row = document.createElement('tr');
|
|
row.innerHTML = `
|
|
<td class="px-6 py-4 whitespace-nowrap">${grade.course}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">${grade.estimatedScore}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">${grade.actualScore}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">${deviation.toFixed(1)}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">${(grade.description || '-').replace(/`/g, '\\`')}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<button class="text-primary hover:text-primary/80 mr-2" onclick="openEditModal(${grade.id})">
|
|
<i class="fa fa-pencil"></i> 编辑
|
|
</button>
|
|
<button class="text-danger hover:text-danger/80" onclick="openDeleteModal(${grade.id})">
|
|
<i class="fa fa-trash-o"></i> 删除
|
|
</button>
|
|
</td>
|
|
`;
|
|
gradesList.appendChild(row);
|
|
});
|
|
}
|
|
|
|
const totalEstimated = filteredGrades.reduce((sum, grade) => sum + grade.estimatedScore, 0);
|
|
const totalActual = filteredGrades.reduce((sum, grade) => sum + grade.actualScore, 0);
|
|
const totalDeviation = filteredGrades.reduce((sum, grade) => sum + (grade.estimatedScore - grade.actualScore), 0);
|
|
averageEstimated.textContent = filteredGrades.length > 0 ? (totalEstimated / filteredGrades.length).toFixed(1) : '--';
|
|
averageActual.textContent = filteredGrades.length > 0 ? (totalActual / filteredGrades.length).toFixed(1) : '--';
|
|
averageDeviation.textContent = filteredGrades.length > 0 ? (totalDeviation / filteredGrades.length).toFixed(1) : '--';
|
|
totalGrades.textContent = filteredGrades.length;
|
|
|
|
const totalPages = Math.ceil(filteredGrades.length / itemsPerPage);
|
|
prevPage.disabled = currentPage === 1;
|
|
nextPage.disabled = currentPage === totalPages;
|
|
currentPageDisplay.textContent = currentPage;
|
|
const start = startIndex + 1;
|
|
const end = Math.min(endIndex, filteredGrades.length);
|
|
paginationInfo.textContent = `显示 ${start}-${end} 条,共 ${filteredGrades.length} 条`;
|
|
}
|
|
|
|
// 打开编辑模态框
|
|
function openEditModal(id) {
|
|
const grade = grades.find(g => g.id === id);
|
|
if (grade) {
|
|
document.getElementById('editId').value = id;
|
|
const editCourseSelect = document.getElementById('editCourse');
|
|
updateCourseSelect(editCourseSelect);
|
|
editCourseSelect.value = grade.course;
|
|
document.getElementById('editEstimatedScore').value = grade.estimatedScore;
|
|
document.getElementById('editActualScore').value = grade.actualScore;
|
|
document.getElementById('editDescription').value = grade.description;
|
|
editModal.classList.remove('hidden');
|
|
}
|
|
}
|
|
|
|
// 关闭编辑模态框
|
|
function closeEditModal() {
|
|
editModal.classList.add('hidden');
|
|
}
|
|
|
|
// 处理编辑提交
|
|
function handleEditSubmit(e) {
|
|
e.preventDefault();
|
|
const id = parseInt(document.getElementById('editId').value);
|
|
const course = document.getElementById('editCourse').value;
|
|
const estimatedScore = parseFloat(document.getElementById('editEstimatedScore').value);
|
|
const actualScore = parseFloat(document.getElementById('editActualScore').value);
|
|
const description = document.getElementById('editDescription').value;
|
|
|
|
const index = grades.findIndex(grade => grade.id === id);
|
|
if (index !== -1) {
|
|
grades[index] = {
|
|
id,
|
|
course,
|
|
estimatedScore,
|
|
actualScore,
|
|
description
|
|
};
|
|
|
|
saveDataToLocalStorage();
|
|
}
|
|
|
|
closeEditModal();
|
|
updateCourseSelect();
|
|
updateCourseSelector();
|
|
updateChart();
|
|
updateGradesList();
|
|
}
|
|
|
|
// 打开删除确认模态框
|
|
function openDeleteModal(id) {
|
|
document.getElementById('deleteId').value = id;
|
|
deleteModal.classList.remove('hidden');
|
|
}
|
|
|
|
// 关闭删除确认模态框
|
|
function closeDeleteModal() {
|
|
deleteModal.classList.add('hidden');
|
|
}
|
|
|
|
// 处理删除
|
|
function handleDelete() {
|
|
const id = parseInt(document.getElementById('deleteId').value);
|
|
grades = grades.filter(grade => grade.id !== id);
|
|
|
|
saveDataToLocalStorage();
|
|
|
|
closeDeleteModal();
|
|
updateCourseSelect();
|
|
updateCourseSelector();
|
|
updateChart();
|
|
updateGradesList();
|
|
}
|
|
|
|
// 确认清空所有记录
|
|
function confirmClearAll() {
|
|
if (confirm('你确定要清空所有成绩记录吗?此操作无法撤销。')) {
|
|
grades = [];
|
|
|
|
saveDataToLocalStorage();
|
|
|
|
updateCourseSelect();
|
|
updateCourseSelector();
|
|
updateChart();
|
|
updateGradesList();
|
|
}
|
|
}
|
|
|
|
// 更改页面
|
|
function changePage(page) {
|
|
if (page > 0) {
|
|
currentPage = page;
|
|
updateGradesList();
|
|
}
|
|
}
|
|
|
|
// 导出成绩数据为CSV文件
|
|
function exportGradeData() {
|
|
const selectedCourse = document.getElementById('courseSelector').value;
|
|
const allGrades = JSON.parse(localStorage.getItem('grades') || '[]');
|
|
let filteredGrades = allGrades;
|
|
|
|
if (selectedCourse) {
|
|
filteredGrades = allGrades.filter(grade => grade.course === selectedCourse);
|
|
}
|
|
|
|
if (filteredGrades.length === 0) {
|
|
alert('没有可导出的成绩数据');
|
|
return;
|
|
}
|
|
|
|
const headers = ['课程', '预估', '实际', '偏差', '备注', '添加时间'];
|
|
let csvContent = headers.join(',') + '\n';
|
|
|
|
filteredGrades.forEach(grade => {
|
|
const deviation = (grade.actualScore - grade.estimatedScore).toFixed(1);
|
|
const row = [
|
|
`"${grade.course}"`,
|
|
grade.estimatedScore,
|
|
grade.actualScore,
|
|
deviation,
|
|
`"${grade.description || '-'}"`
|
|
].join(',');
|
|
csvContent += row + '\n';
|
|
});
|
|
|
|
const bom = new Uint8Array([0xEF, 0xBB, 0xBF]);
|
|
const blob = new Blob([bom, csvContent], { type: 'text/csv;charset=utf-8' });
|
|
|
|
const link = document.createElement('a');
|
|
link.href = URL.createObjectURL(blob);
|
|
const timestamp = new Date().toLocaleString().replace(/[\/:*?"<>|]/g, '-');
|
|
link.download = `成绩_${selectedCourse || '全部科目'}_${timestamp}.csv`;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
URL.revokeObjectURL(link.href);
|
|
}
|
|
|
|
// 导出图表为图片
|
|
function exportChartImage() {
|
|
if (!chartInstance) {
|
|
alert('请先添加成绩数据并生成图表');
|
|
return;
|
|
}
|
|
|
|
const selectedCourse = document.getElementById('courseSelector').value;
|
|
const courseName = selectedCourse || '所有科目';
|
|
const timestamp = new Date().toLocaleString().replace(/[\/:*?"<>|]/g, '-');
|
|
const fileName = `${courseName}_成绩图表_${timestamp}.png`;
|
|
const imageURL = chartInstance.toBase64Image();
|
|
|
|
const link = document.createElement('a');
|
|
link.href = imageURL;
|
|
link.download = fileName;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
}
|
|
|
|
// 导出成绩代码
|
|
function exportGradeCode() {
|
|
try {
|
|
const gradeData = JSON.stringify(grades);
|
|
const encodedData = btoa(unescape(encodeURIComponent(gradeData)));
|
|
|
|
const link = document.createElement('a');
|
|
link.href = 'data:text/plain;charset=utf-8,' + encodeURIComponent(encodedData);
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
|
|
const currentUrl = window.location.origin + window.location.pathname;
|
|
const shareUrl = `${currentUrl}?code=${encodeURIComponent(encodedData)}`;
|
|
prompt('复制以下链接分享你的成绩:', shareUrl);
|
|
|
|
alert('成绩分享代码已生成!');
|
|
} catch (error) {
|
|
alert('导出成绩代码失败:' + error.message);
|
|
}
|
|
}
|
|
|
|
// 加载成绩代码
|
|
function loadFromCode() {
|
|
const code = importCode.value;
|
|
if (!code) return;
|
|
|
|
try {
|
|
const decodedData = decodeURIComponent(escape(atob(code)));
|
|
const loadedGrades = JSON.parse(decodedData);
|
|
|
|
grades = loadedGrades;
|
|
|
|
saveDataToLocalStorage();
|
|
|
|
closeCodeModal();
|
|
|
|
updateCourseSelect();
|
|
updateCourseSelector();
|
|
updateChart();
|
|
updateGradesList();
|
|
|
|
alert('成绩代码加载成功!');
|
|
} catch (error) {
|
|
alert('加载成绩代码失败:' + error.message);
|
|
}
|
|
}
|
|
|
|
// 打开代码模态框
|
|
function openCodeModal() {
|
|
codeModal.classList.remove('hidden');
|
|
}
|
|
|
|
// 关闭代码模态框
|
|
function closeCodeModal() {
|
|
codeModal.classList.add('hidden');
|
|
importCode.value = '';
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |