CCSWebsite/a.html
2025-07-04 23:49:18 +00:00

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>