今天继续介绍DeepSeek的“不会编程也能做应用”的事情。之前介绍的都是一些相对较小众的的东西,今天想折腾一个大一点的,给大家打个样。比如,让DeepSeek做一个班级学生成长档案管理系统。

是的,胆子就是这么大。这么专业的东西敢让DeepSeek做!做出来的东西居然还能用!






DeepSeek的野心完全藏不住了!想用一下试试?下面给你地址——
https://www.32kw.com/view/ebce066:一行代码不写,就能让DeepSeek生成学生成长档案管理器!那么,用的是什么提示词呢?大家都知道,这个东西我也不知道怎么做,于是反手就甩给DeepSeek,让他帮我想提示词,等他想出来后我再改改。

下面是第一次提示词全文

等到DeepSeek给我答案后,我又稍作了修改,形成了最终的提示词。
你是H5开发专家和教育管理专家,请设计一个完整的班级学生成长档案管理H5单页应用。该应用将帮助班主任高效管理6年(12个学期)的学生数据、成绩记录和分析报告。请按以下要求生成独立的H5页面应用(即单个H5网页),强烈建议优先使用优秀的第三方前端js组件和CSS库,以减少代码量,但使用第三方库时禁止使用cdn.jsdelivr.net的库文件,优先使用BootCDN、Staticfile CDN和unpkg的库文件。 ## 技术选型建议 1. **UI框架**:使用Bootstrap 5(来自BootCDN)构建响应式界面 2. **数据处理**:使用Vue.js 3(来自unpkg)实现数据绑定和交互 3. **表格组件**:采用Tabulator.js(来自Staticfile CDN)处理学生名单和成绩展示 4. **图表库**:使用Chart.js(来自unpkg)进行成绩统计分析可视化 5. **文件处理**:SheetJS(来自unpkg)处理Excel导入导出 6. **PDF生成**:jsPDF(来自unpkg)创建学生报告单 7. 无后端交互,全前端,数据从本地手动加载本地Excel文件 ## 核心功能模块 ### 1. 学生名单管理 – 支持Excel批量导入学生基本信息(姓名、学号、性别等) – 增删改查单个学生信息 – 按学期分类展示(1-12学期标签页) – 支持学生升学/留级等状态变更 ### 2. 成绩管理 – 多学期成绩存储结构(12学期独立数据) – 支持Excel批量导入各科成绩 – 单科/多科成绩录入界面 – 成绩异常值自动检测提醒 ### 3. 统计分析 – 单科成绩分析(平均分、最高分、分数段分布) – 学生个人成绩趋势图(跨学期对比) – 班级成绩排名与进步分析 – 多科目成绩相关性分析 ### 4. 报告生成 – 自定义报告模板设计器 – 自动生成学生个人学期报告(含各科成绩、评语、图表) – 批量生成全班报告单 – PDF导出功能 ## 界面设计建议 1. **响应式布局**:适配电脑、平板和手机 2. **数据看板**:首页展示班级关键指标 3. **导航结构**:左侧菜单+顶部学期切换 4. **操作流程**:清晰的导入-处理-分析-导出工作流 ## 安全与性能 1. 纯前端实现,数据存储在浏览器IndexedDB中 2. 定期数据导出提醒功能 3. 大数据量下的性能优化(虚拟滚动、分页加载) ## 开发建议 1. 采用模块化开发,保持代码结构清晰 2. 实现完善的错误处理和用户引导 3. 添加操作日志记录关键动作
由于众所周知的原因,经过多次中断生成以后,生成了这样的代码,一共有3391行。下面我把代码都给你
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>刚哥帮EDU@班级学生成长档案管理系统</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.2.3/css/bootstrap.min.css" rel="stylesheet">
<!-- Tabulator CSS -->
<link href="https://cdn.staticfile.org/tabulator/5.4.3/css/tabulator.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
:root {
--primary-color: #3498db;
--secondary-color: #2ecc71;
--danger-color: #e74c3c;
--warning-color: #f39c12;
--info-color: #9b59b6;
}
body {
font-family: 'Microsoft YaHei', sans-serif;
background-color: #f8f9fa;
}
.sidebar {
height: 100vh;
background: linear-gradient(135deg, #2c3e50, #34495e);
color: white;
position: fixed;
padding-top: 20px;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
transition: all 0.3s;
z-index: 1000;
}
.sidebar .nav-link {
color: rgba(255, 255, 255, 0.8);
margin-bottom: 5px;
border-radius: 5px;
padding: 10px 15px;
transition: all 0.3s;
}
.sidebar .nav-link:hover, .sidebar .nav-link.active {
background-color: rgba(255, 255, 255, 0.1);
color: white;
}
.sidebar .nav-link i {
margin-right: 10px;
width: 20px;
text-align: center;
}
.main-content {
margin-left: 250px;
padding: 20px;
transition: all 0.3s;
}
.card {
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
border: none;
}
.card-header {
background-color: white;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
font-weight: bold;
border-radius: 10px 10px 0 0 !important;
}
.semester-tabs {
background-color: white;
border-radius: 10px;
padding: 15px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.nav-tabs .nav-link {
border: none;
color: #555;
font-weight: 500;
padding: 8px 15px;
}
.nav-tabs .nav-link.active {
color: var(--primary-color);
border-bottom: 3px solid var(--primary-color);
background-color: transparent;
}
.dashboard-card {
border-left: 4px solid;
transition: transform 0.3s;
}
.dashboard-card:hover {
transform: translateY(-5px);
}
.card-primary {
border-left-color: var(--primary-color);
}
.card-success {
border-left-color: var(--secondary-color);
}
.card-danger {
border-left-color: var(--danger-color);
}
.card-warning {
border-left-color: var(--warning-color);
}
.card-info {
border-left-color: var(--info-color);
}
.btn-primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
.btn-success {
background-color: var(--secondary-color);
border-color: var(--secondary-color);
}
.btn-danger {
background-color: var(--danger-color);
border-color: var(--danger-color);
}
.btn-warning {
background-color: var(--warning-color);
border-color: var(--warning-color);
}
.btn-info {
background-color: var(--info-color);
border-color: var(--info-color);
}
.badge-primary {
background-color: var(--primary-color);
}
.badge-success {
background-color: var(--secondary-color);
}
.badge-danger {
background-color: var(--danger-color);
}
.badge-warning {
background-color: var(--warning-color);
}
.badge-info {
background-color: var(--info-color);
}
.progress {
height: 10px;
border-radius: 5px;
}
.progress-bar {
background-color: var(--primary-color);
}
.student-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
background-color: #eee;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
color: #555;
}
.action-buttons .btn {
padding: 5px 10px;
font-size: 12px;
margin-right: 5px;
}
.tabulator {
border-radius: 10px;
overflow: hidden;
}
.tabulator .tabulator-header {
background-color: #f8f9fa;
font-weight: bold;
}
.tabulator .tabulator-tableHolder {
background-color: white;
}
.tabulator-row.tabulator-selectable:hover {
background-color: #f0f7ff !important;
}
.tabulator-row.tabulator-row-even {
background-color: #f9f9f9;
}
.modal-content {
border-radius: 15px;
overflow: hidden;
}
.form-control, .form-select {
border-radius: 8px;
padding: 10px 15px;
}
.form-floating>label {
padding: 10px 15px;
}
.toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 1100;
}
.chart-container {
position: relative;
height: 300px;
margin-bottom: 20px;
}
.sidebar-collapsed {
width: 80px;
overflow: hidden;
}
.sidebar-collapsed .nav-link span {
display: none;
}
.sidebar-collapsed .nav-link i {
margin-right: 0;
font-size: 1.2rem;
}
.sidebar-collapsed .sidebar-brand span {
display: none;
}
.sidebar-collapsed + .main-content {
margin-left: 80px;
}
.sidebar-toggle {
position: fixed;
left: 235px;
top: 10px;
z-index: 1100;
background-color: white;
border-radius: 50%;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
cursor: pointer;
transition: all 0.3s;
}
.sidebar-collapsed .sidebar-toggle {
left: 65px;
}
@media (max-width: 992px) {
.sidebar {
left: -250px;
}
.sidebar-collapsed {
left: 0;
width: 80px;
}
.main-content {
margin-left: 0;
}
.sidebar-toggle {
left: 10px;
}
.sidebar-collapsed .sidebar-toggle {
left: 10px;
}
}
.drag-drop-area {
border: 2px dashed #ccc;
border-radius: 10px;
padding: 30px;
text-align: center;
background-color: #f9f9f9;
cursor: pointer;
transition: all 0.3s;
}
.drag-drop-area:hover {
border-color: var(--primary-color);
background-color: #f0f7ff;
}
.drag-drop-area.active {
border-color: var(--primary-color);
background-color: #e6f2ff;
}
.report-template {
border: 1px solid #eee;
border-radius: 10px;
padding: 15px;
margin-bottom: 15px;
background-color: white;
transition: all 0.3s;
cursor: pointer;
}
.report-template:hover {
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
transform: translateY(-3px);
}
.report-template.active {
border-color: var(--primary-color);
background-color: #f0f7ff;
}
.log-item {
padding: 10px 15px;
border-bottom: 1px solid #eee;
font-size: 14px;
}
.log-item:last-child {
border-bottom: none;
}
.log-time {
font-size: 12px;
color: #777;
}
.empty-state {
text-align: center;
padding: 40px 20px;
color: #777;
}
.empty-state i {
font-size: 50px;
margin-bottom: 20px;
color: #ddd;
}
</style>
</head>
<body>
<div id="app">
<!-- 侧边栏导航 -->
<div class="sidebar" :class="{ 'sidebar-collapsed': isSidebarCollapsed }">
<div class="sidebar-brand p-3 mb-4 d-flex align-items-center">
<i class="fas fa-graduation-cap me-3 fs-4"></i>
<span class="fs-5 fw-bold">成长档案系统</span>
</div>
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link active" href="#" @click="changeTab('dashboard')">
<i class="fas fa-tachometer-alt"></i>
<span>数据看板</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" @click="changeTab('students')">
<i class="fas fa-users"></i>
<span>学生管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" @click="changeTab('grades')">
<i class="fas fa-book"></i>
<span>成绩管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" @click="changeTab('analysis')">
<i class="fas fa-chart-bar"></i>
<span>统计分析</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" @click="changeTab('reports')">
<i class="fas fa-file-pdf"></i>
<span>报告生成</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" @click="changeTab('settings')">
<i class="fas fa-cog"></i>
<span>系统设置</span>
</a>
</li>
</ul>
</div>
<div class="sidebar-toggle" @click="toggleSidebar">
<i class="fas" :class="isSidebarCollapsed ? 'fa-chevron-right' : 'fa-chevron-left'"></i>
</div>
<!-- 主内容区 -->
<div class="main-content">
<!-- 顶部学期导航 -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="mb-0">{{ currentTabName }}</h4>
<div class="semester-tabs">
<ul class="nav nav-tabs">
<li class="nav-item" v-for="semester in 12" :key="semester">
<a class="nav-link" :class="{ 'active': currentSemester === semester }"
href="#" @click="changeSemester(semester)">
第{{ semester }}学期
</a>
</li>
</ul>
</div>
</div>
<!-- 数据看板 -->
<div v-if="currentTab === 'dashboard'" class="tab-content">
<div class="row">
<div class="col-md-3">
<div class="card dashboard-card card-primary">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-2">班级人数</h6>
<h3 class="mb-0">{{ studentCount }}</h3>
</div>
<div class="bg-primary bg-opacity-10 p-3 rounded">
<i class="fas fa-users text-primary"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card dashboard-card card-success">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-2">平均成绩</h6>
<h3 class="mb-0">{{ averageGrade }}</h3>
</div>
<div class="bg-success bg-opacity-10 p-3 rounded">
<i class="fas fa-book text-success"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card dashboard-card card-warning">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-2">最高分</h6>
<h3 class="mb-0">{{ maxGrade }}</h3>
</div>
<div class="bg-warning bg-opacity-10 p-3 rounded">
<i class="fas fa-star text-warning"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card dashboard-card card-danger">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-2">最低分</h6>
<h3 class="mb-0">{{ minGrade }}</h3>
</div>
<div class="bg-danger bg-opacity-10 p-3 rounded">
<i class="fas fa-exclamation-triangle text-danger"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-8">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span>成绩趋势分析</span>
<div>
<select class="form-select form-select-sm" style="width: 150px;" v-model="trendSubject">
<option value="all">全部科目</option>
<option v-for="subject in subjects" :value="subject">{{ subject }}</option>
</select>
</div>
</div>
<div class="card-body">
<div class="chart-container">
<canvas id="trendChart"></canvas>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<span>成绩分布</span>
</div>
<div class="card-body">
<div class="chart-container">
<canvas id="distributionChart"></canvas>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<span>操作日志</span>
</div>
<div class="card-body p-0">
<div v-if="logs.length === 0" class="empty-state">
<i class="fas fa-clipboard-list"></i>
<p>暂无操作记录</p>
</div>
<div v-else>
<div class="log-item" v-for="log in logs.slice(0, 5)" :key="log.id">
<div class="d-flex justify-content-between">
<span>{{ log.action }}</span>
<span class="log-time">{{ formatTime(log.time) }}</span>
</div>
</div>
<div class="p-3 text-center" v-if="logs.length > 5">
<button class="btn btn-sm btn-outline-primary" @click="viewAllLogs">查看全部</button>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<span>数据备份</span>
</div>
<div class="card-body">
<p>上次备份时间: {{ lastBackupTime || '暂无备份' }}</p>
<div class="d-flex gap-2">
<button class="btn btn-primary" @click="exportData">
<i class="fas fa-download me-2"></i>导出数据
</button>
<button class="btn btn-outline-secondary" @click="importData">
<i class="fas fa-upload me-2"></i>导入数据
</button>
</div>
<div class="alert alert-warning mt-3">
<i class="fas fa-exclamation-triangle me-2"></i>
建议定期导出数据备份,防止数据丢失
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 学生管理 -->
<div v-if="currentTab === 'students'" class="tab-content">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span>学生名单管理</span>
<div>
<button class="btn btn-primary btn-sm" @click="showAddStudentModal">
<i class="fas fa-plus me-1"></i>添加学生
</button>
<button class="btn btn-success btn-sm ms-2" @click="showImportModal('students')">
<i class="fas fa-file-import me-1"></i>Excel导入
</button>
</div>
</div>
<div class="card-body">
<div id="studentsTable"></div>
</div>
</div>
</div>
<!-- 成绩管理 -->
<div v-if="currentTab === 'grades'" class="tab-content">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span>成绩管理</span>
<div>
<button class="btn btn-primary btn-sm" @click="showAddGradeModal">
<i class="fas fa-plus me-1"></i>添加成绩
</button>
<button class="btn btn-success btn-sm ms-2" @click="showImportModal('grades')">
<i class="fas fa-file-import me-1"></i>Excel导入
</button>
<button class="btn btn-info btn-sm ms-2" @click="exportGradeExcel">
<i class="fas fa-file-export me-1"></i>Excel导出
</button>
</div>
</div>
<div class="card-body">
<div class="alert alert-warning" v-if="gradeWarnings.length > 0">
<i class="fas fa-exclamation-triangle me-2"></i>
<strong>发现 {{ gradeWarnings.length }} 条成绩异常:</strong>
<ul class="mb-0 mt-2">
<li v-for="warning in gradeWarnings" :key="warning.id">
{{ warning.message }}
</li>
</ul>
</div>
<div id="gradesTable"></div>
</div>
</div>
</div>
<!-- 统计分析 -->
<div v-if="currentTab === 'analysis'" class="tab-content">
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span>单科成绩分析</span>
<select class="form-select form-select-sm" style="width: 150px;" v-model="analysisSubject">
<option v-for="subject in subjects" :value="subject">{{ subject }}</option>
</select>
</div>
<div class="card-body">
<div class="row mb-4">
<div class="col-md-4">
<div class="d-flex align-items-center">
<div class="bg-primary bg-opacity-10 p-2 rounded me-3">
<i class="fas fa-calculator text-primary"></i>
</div>
<div>
<small class="text-muted">平均分</small>
<h5 class="mb-0">{{ subjectStats.avg || '--' }}</h5>
</div>
</div>
</div>
<div class="col-md-4">
<div class="d-flex align-items-center">
<div class="bg-success bg-opacity-10 p-2 rounded me-3">
<i class="fas fa-arrow-up text-success"></i>
</div>
<div>
<small class="text-muted">最高分</small>
<h5 class="mb-0">{{ subjectStats.max || '--' }}</h5>
</div>
</div>
</div>
<div class="col-md-4">
<div class="d-flex align-items-center">
<div class="bg-danger bg-opacity-10 p-2 rounded me-3">
<i class="fas fa-arrow-down text-danger"></i>
</div>
<div>
<small class="text-muted">最低分</small>
<h5 class="mb-0">{{ subjectStats.min || '--' }}</h5>
</div>
</div>
</div>
</div>
<div class="chart-container">
<canvas id="subjectAnalysisChart"></canvas>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span>学生进步分析</span>
<select class="form-select form-select-sm" style="width: 150px;" v-model="progressStudent">
<option v-for="student in students" :value="student.id">{{ student.name }}</option>
</select>
</div>
<div class="card-body">
<div class="row mb-4">
<div class="col-md-6">
<div class="d-flex align-items-center">
<div class="bg-info bg-opacity-10 p-2 rounded me-3">
<i class="fas fa-chart-line text-info"></i>
</div>
<div>
<small class="text-muted">进步幅度</small>
<h5 class="mb-0">{{ studentProgress.rate || '--' }}%</h5>
</div>
</div>
</div>
<div class="col-md-6">
<div class="d-flex align-items-center">
<div class="bg-warning bg-opacity-10 p-2 rounded me-3">
<i class="fas fa-medal text-warning"></i>
</div>
<div>
<small class="text-muted">班级排名</small>
<h5 class="mb-0">{{ studentProgress.rank || '--' }}</h5>
</div>
</div>
</div>
</div>
<div class="chart-container">
<canvas id="studentProgressChart"></canvas>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<span>科目相关性分析</span>
</div>
<div class="card-body">
<div class="chart-container" style="height: 400px;">
<canvas id="correlationChart"></canvas>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 报告生成 -->
<div v-if="currentTab === 'reports'" class="tab-content">
<div class="row">
<div class="col-md-4">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span>报告模板</span>
<button class="btn btn-primary btn-sm" @click="showAddTemplateModal">
<i class="fas fa-plus me-1"></i>新建模板
</button>
</div>
<div class="card-body">
<div class="report-template"
v-for="template in reportTemplates"
:key="template.id"
:class="{ 'active': selectedTemplate === template.id }"
@click="selectTemplate(template.id)">
<h6>{{ template.name }}</h6>
<small class="text-muted">最后修改: {{ formatTime(template.updatedAt) }}</small>
</div>
<div v-if="reportTemplates.length === 0" class="empty-state">
<i class="fas fa-file-alt"></i>
<p>暂无报告模板</p>
<button class="btn btn-primary btn-sm" @click="showAddTemplateModal">
<i class="fas fa-plus me-1"></i>创建第一个模板
</button>
</div>
</div>
</div>
</div>
<div class="col-md-8">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span>报告生成器</span>
<div v-if="selectedTemplate">
<button class="btn btn-outline-secondary btn-sm" @click="editTemplate">
<i class="fas fa-edit me-1"></i>编辑模板
</button>
<button class="btn btn-success btn-sm ms-2" @click="generateReport">
<i class="fas fa-file-pdf me-1"></i>生成报告
</button>
</div>
</div>
<div class="card-body">
<div v-if="!selectedTemplate" class="empty-state">
<i class="fas fa-file-pdf"></i>
<p>请从左侧选择一个模板</p>
</div>
<div v-else>
<div class="mb-3">
<label class="form-label">选择学生</label>
<select class="form-select" v-model="reportStudent">
<option v-for="student in students" :value="student.id">{{ student.name }} ({{ student.id }})</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">选择学期</label>
<select class="form-select" v-model="reportSemester">
<option v-for="semester in 12" :value="semester">第{{ semester }}学期</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">报告预览</label>
<div class="border p-3" style="min-height: 300px; background-color: white;">
<h5 class="text-center mb-4">{{ getTemplate(selectedTemplate).name }}</h5>
<div v-html="previewReportContent"></div>
</div>
</div>
<div class="d-flex justify-content-between">
<button class="btn btn-outline-primary" @click="batchGenerateReports">
<i class="fas fa-bolt me-1"></i>批量生成全班报告
</button>
<button class="btn btn-primary" @click="generateReport">
<i class="fas fa-download me-1"></i>下载PDF报告
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 系统设置 -->
<div v-if="currentTab === 'settings'" class="tab-content">
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<span>科目设置</span>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">当前科目</label>
<div class="d-flex gap-2 mb-2">
<input type="text" class="form-control" v-model="newSubject" placeholder="输入新科目名称">
<button class="btn btn-primary" @click="addSubject">添加</button>
</div>
<div class="d-flex flex-wrap gap-2">
<span class="badge bg-primary d-flex align-items-center" v-for="subject in subjects" :key="subject">
{{ subject }}
<button class="btn btn-sm p-0 ms-2" @click="removeSubject(subject)">
<i class="fas fa-times text-white"></i>
</button>
</span>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<span>系统设置</span>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">班级名称</label>
<input type="text" class="form-control" v-model="className">
</div>
<div class="mb-3">
<label class="form-label">班主任</label>
<input type="text" class="form-control" v-model="teacherName">
</div>
<div class="mb-3">
<label class="form-label">学年设置</label>
<input type="text" class="form-control" v-model="academicYear">
</div>
<button class="btn btn-primary" @click="saveSettings">保存设置</button>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<span>数据管理</span>
</div>
<div class="card-body">
<div class="alert alert-danger">
<i class="fas fa-exclamation-triangle me-2"></i>
<strong>危险操作</strong> - 以下操作将永久删除数据,请谨慎操作
</div>
<div class="d-flex gap-2">
<button class="btn btn-outline-danger" @click="confirmClearData('students')">
<i class="fas fa-trash me-1"></i>清空学生数据
</button>
<button class="btn btn-outline-danger" @click="confirmClearData('grades')">
<i class="fas fa-trash me-1"></i>清空成绩数据
</button>
<button class="btn btn-outline-danger" @click="confirmClearData('all')">
<i class="fas fa-trash me-1"></i>清空所有数据
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 添加学生模态框 -->
<div class="modal fade" id="addStudentModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ editStudentMode ? '编辑学生信息' : '添加新学生' }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">学号</label>
<input type="text" class="form-control" v-model="currentStudent.id" :disabled="editStudentMode">
</div>
<div class="mb-3">
<label class="form-label">姓名</label>
<input type="text" class="form-control" v-model="currentStudent.name">
</div>
<div class="mb-3">
<label class="form-label">性别</label>
<select class="form-select" v-model="currentStudent.gender">
<option value="男">男</option>
<option value="女">女</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">出生日期</label>
<input type="date" class="form-control" v-model="currentStudent.birthday">
</div>
<div class="mb-3">
<label class="form-label">联系电话</label>
<input type="text" class="form-control" v-model="currentStudent.phone">
</div>
<div class="mb-3">
<label class="form-label">家庭住址</label>
<input type="text" class="form-control" v-model="currentStudent.address">
</div>
<div class="mb-3">
<label class="form-label">状态</label>
<select class="form-select" v-model="currentStudent.status">
<option value="在读">在读</option>
<option value="休学">休学</option>
<option value="退学">退学</option>
<option value="毕业">毕业</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" @click="saveStudent">保存</button>
</div>
</div>
</div>
</div>
<!-- 添加成绩模态框 -->
<div class="modal fade" id="addGradeModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ editGradeMode ? '编辑成绩' : '添加成绩' }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">学生</label>
<select class="form-select" v-model="currentGrade.studentId">
<option v-for="student in students" :value="student.id">{{ student.name }} ({{ student.id }})</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">学期</label>
<select class="form-select" v-model="currentGrade.semester">
<option v-for="semester in 12" :value="semester">第{{ semester }}学期</option>
</select>
</div>
<div class="mb-3" v-for="subject in subjects" :key="subject">
<label class="form-label">{{ subject }}成绩</label>
<input type="number" class="form-control" v-model="currentGrade.subjects[subject]" min="0" max="100" step="0.1">
</div>
<div class="mb-3">
<label class="form-label">评语</label>
<textarea class="form-control" v-model="currentGrade.comment" rows="3"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" @click="saveGrade">保存</button>
</div>
</div>
</div>
</div>
<!-- 导入数据模态框 -->
<div class="modal fade" id="importModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ importType === 'students' ? '导入学生数据' : '导入成绩数据' }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>
请下载模板文件,按照格式填写数据后导入
<a href="#" @click.prevent="downloadTemplate" class="ms-2">下载模板</a>
</div>
<div class="drag-drop-area"
@dragover.prevent="dragOver = true"
@dragleave="dragOver = false"
@drop.prevent="handleFileDrop"
:class="{ 'active': dragOver }">
<i class="fas fa-file-excel fa-3x mb-3 text-success"></i>
<h5>拖放Excel文件到此处</h5>
<p class="text-muted">或</p>
<input type="file" id="fileInput" class="d-none" accept=".xlsx,.xls" @change="handleFileSelect">
<label for="fileInput" class="btn btn-primary">
<i class="fas fa-folder-open me-2"></i>选择文件
</label>
</div>
<div class="alert alert-warning mt-3" v-if="importType === 'grades'">
<i class="fas fa-exclamation-triangle me-2"></i>
注意:导入成绩数据会覆盖同学生同学期的已有成绩
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" @click="confirmImport" :disabled="!importFile">导入</button>
</div>
</div>
</div>
</div>
<!-- 添加模板模态框 -->
<div class="modal fade" id="addTemplateModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ editTemplateMode ? '编辑报告模板' : '新建报告模板' }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">模板名称</label>
<input type="text" class="form-control" v-model="currentTemplate.name">
</div>
<div class="mb-3">
<label class="form-label">模板内容</label>
<div class="border p-2 mb-2">
<button class="btn btn-sm btn-outline-secondary me-2" @click="insertVariable('studentName')">
<i class="fas fa-user me-1"></i>学生姓名
</button>
<button class="btn btn-sm btn-outline-secondary me-2" @click="insertVariable('semester')">
<i class="fas fa-calendar me-1"></i>学期
</button>
<button class="btn btn-sm btn-outline-secondary me-2" @click="insertVariable('gradesTable')">
<i class="fas fa-table me-1"></i>成绩表格
</button>
<button class="btn btn-sm btn-outline-secondary me-2" @click="insertVariable('progressChart')">
<i class="fas fa-chart-line me-1"></i>进步图表
</button>
</div>
<textarea class="form-control" v-model="currentTemplate.content" rows="10"></textarea>
</div>
<div class="mb-3">
<label class="form-label">预览</label>
<div class="border p-3" style="min-height: 200px; background-color: white;">
<div v-html="previewTemplateContent"></div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" @click="saveTemplate">保存</button>
</div>
</div>
</div>
</div>
<!-- 确认清除数据模态框 -->
<div class="modal fade" id="confirmClearModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title">确认清除数据</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="alert alert-danger">
<i class="fas fa-exclamation-triangle me-2"></i>
您确定要清除{{ clearDataType === 'students' ? '所有学生数据' : clearDataType === 'grades' ? '所有成绩数据' : '所有数据' }}吗?此操作不可撤销!
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="confirmCheck" v-model="confirmChecked">
<label class="form-check-label" for="confirmCheck">
我确认要删除数据
</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-danger" @click="clearData" :disabled="!confirmChecked">确认清除</button>
</div>
</div>
</div>
</div>
<!-- 查看所有日志模态框 -->
<div class="modal fade" id="allLogsModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">操作日志</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="log-item border-bottom" v-for="log in logs" :key="log.id">
<div class="d-flex justify-content-between">
<span>{{ log.action }}</span>
<span class="log-time">{{ formatTime(log.time) }}</span>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
<!-- Toast通知 -->
<div class="toast-container">
<div class="toast align-items-center" :class="`text-bg-${toast.type}`" role="alert" aria-live="assertive" aria-atomic="true" v-if="toast.show">
<div class="d-flex">
<div class="toast-body">
<i class="fas me-2" :class="toastIcon"></i>
{{ toast.message }}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close" @click="toast.show = false"></button>
</div>
</div>
</div>
</div>
<!-- 第三方库 -->
<!-- Vue 3 -->
<script src="https://unpkg.com/vue@3.2.47/dist/vue.global.prod.js"></script>
<!-- Bootstrap 5 JS -->
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.2.3/js/bootstrap.bundle.min.js"></script>
<!-- Tabulator -->
<script src="https://cdn.staticfile.org/tabulator/5.4.3/js/tabulator.min.js"></script>
<!-- Chart.js -->
<script src="https://unpkg.com/chart.js@4.2.1/dist/chart.umd.min.js"></script>
<!-- SheetJS -->
<script src="https://unpkg.com/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
<!-- jsPDF -->
<script src="https://unpkg.com/jspdf@2.5.1/dist/jspdf.umd.min.js"></script>
<!-- 自动表格插件 -->
<script src="https://unpkg.com/jspdf-autotable@3.5.28/dist/jspdf.plugin.autotable.min.js"></script>
<script>
const { createApp, ref, reactive, computed, onMounted, watch, nextTick } = Vue;
// 生成UUID
function generateId() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
// 格式化时间
function formatTime(date) {
if (!date) return '';
const d = new Date(date);
return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')} ${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
}
createApp({
setup() {
// 状态管理
const isSidebarCollapsed = ref(false);
const currentTab = ref('dashboard');
const currentSemester = ref(1);
const students = ref([]);
const grades = ref([]);
const reportTemplates = ref([]);
const selectedTemplate = ref(null);
const reportStudent = ref(null);
const reportSemester = ref(1);
const subjects = ref(['语文', '数学', '英语', '物理', '化学', '生物']);
const className = ref('六年级一班');
const teacherName = ref('张老师');
const academicYear = ref('2022-2023');
const logs = ref([]);
const lastBackupTime = ref(null);
const toast = reactive({
show: false,
message: '',
type: 'success'
});
// 当前操作对象
const currentStudent = reactive({
id: '',
name: '',
gender: '男',
birthday: '',
phone: '',
address: '',
status: '在读'
});
const currentGrade = reactive({
id: '',
studentId: '',
semester: 1,
subjects: {},
comment: ''
});
const currentTemplate = reactive({
id: '',
name: '',
content: ''
});
// 模态框状态
const editStudentMode = ref(false);
const editGradeMode = ref(false);
const editTemplateMode = ref(false);
const importType = ref('students');
const importFile = ref(null);
const dragOver = ref(false);
const clearDataType = ref('all');
const confirmChecked = ref(false);
// 分析相关状态
const trendSubject = ref('all');
const analysisSubject = ref('语文');
const progressStudent = ref(null);
// 表格实例
const studentsTable = ref(null);
const gradesTable = ref(null);
// 图表实例
const trendChart = ref(null);
const distributionChart = ref(null);
const subjectAnalysisChart = ref(null);
const studentProgressChart = ref(null);
const correlationChart = ref(null);
// 计算属性
const currentTabName = computed(() => {
const names = {
'dashboard': '数据看板',
'students': '学生管理',
'grades': '成绩管理',
'analysis': '统计分析',
'reports': '报告生成',
'settings': '系统设置'
};
return names[currentTab.value];
});
const studentCount = computed(() => {
return students.value.length;
});
const averageGrade = computed(() => {
const currentGrades = grades.value.filter(g => g.semester === currentSemester.value);
if (currentGrades.length === 0) return '--';
let total = 0;
let count = 0;
currentGrades.forEach(grade => {
Object.values(grade.subjects).forEach(score => {
if (score !== null && score !== undefined) {
total += parseFloat(score);
count++;
}
});
});
return count > 0 ? (total / count).toFixed(1) : '--';
});
const maxGrade = computed(() => {
const currentGrades = grades.value.filter(g => g.semester === currentSemester.value);
if (currentGrades.length === 0) return '--';
let max = -Infinity;
currentGrades.forEach(grade => {
Object.values(grade.subjects).forEach(score => {
if (score !== null && score !== undefined) {
const num = parseFloat(score);
if (num > max) max = num;
}
});
});
return max !== -Infinity ? max.toFixed(1) : '--';
});
const minGrade = computed(() => {
const currentGrades = grades.value.filter(g => g.semester === currentSemester.value);
if (currentGrades.length === 0) return '--';
let min = Infinity;
currentGrades.forEach(grade => {
Object.values(grade.subjects).forEach(score => {
if (score !== null && score !== undefined) {
const num = parseFloat(score);
if (num < min) min = num;
}
});
});
return min !== Infinity ? min.toFixed(1) : '--';
});
const gradeWarnings = computed(() => {
const warnings = [];
grades.value.forEach(grade => {
Object.entries(grade.subjects).forEach(([subject, score]) => {
if (score !== null && score !== undefined) {
const num = parseFloat(score);
if (num < 0 || num > 100) {
const student = students.value.find(s => s.id === grade.studentId);
warnings.push({
id: `${grade.id}-${subject}`,
message: `${student?.name || grade.studentId} 第${grade.semester}学期 ${subject}成绩异常: ${score}分`
});
}
}
});
});
return warnings;
});
const subjectStats = computed(() => {
const currentGrades = grades.value.filter(g => g.semester === currentSemester.value);
if (currentGrades.length === 0) return {};
const subject = analysisSubject.value;
const scores = currentGrades.map(g => g.subjects[subject]).filter(s => s !== null && s !== undefined).map(s => parseFloat(s));
if (scores.length === 0) return {};
const sum = scores.reduce((a, b) => a + b, 0);
const avg = sum / scores.length;
const max = Math.max(...scores);
const min = Math.min(...scores);
// 分数段统计
const segments = [0, 60, 70, 80, 90, 100];
const segmentCounts = {};
for (let i = 0; i < segments.length - 1; i++) {
const lower = segments[i];
const upper = segments[i + 1];
const key = `${lower}-${upper}`;
segmentCounts[key] = scores.filter(s => s >= lower && s < upper).length;
}
// 特殊处理100分
segmentCounts['100'] = scores.filter(s => s === 100).length;
return {
avg: avg.toFixed(1),
max: max.toFixed(1),
min: min.toFixed(1),
segmentCounts
};
});
const studentProgress = computed(() => {
if (!progressStudent.value) return {};
const studentGrades = grades.value.filter(g => g.studentId === progressStudent.value);
if (studentGrades.length < 2) return {};
// 计算平均分变化
const firstSemester = Math.min(...studentGrades.map(g => g.semester));
const lastSemester = Math.max(...studentGrades.map(g => g.semester));
const firstGrade = studentGrades.find(g => g.semester === firstSemester);
const lastGrade = studentGrades.find(g => g.semester === lastSemester);
let firstAvg = 0;
let firstCount = 0;
Object.values(firstGrade.subjects).forEach(score => {
if (score !== null && score !== undefined) {
firstAvg += parseFloat(score);
firstCount++;
}
});
firstAvg = firstCount > 0 ? firstAvg / firstCount : 0;
let lastAvg = 0;
let lastCount = 0;
Object.values(lastGrade.subjects).forEach(score => {
if (score !== null && score !== undefined) {
lastAvg += parseFloat(score);
lastCount++;
}
});
lastAvg = lastCount > 0 ? lastAvg / lastCount : 0;
const rate = ((lastAvg - firstAvg) / firstAvg * 100).toFixed(1);
// 计算当前排名
const currentGrades = grades.value.filter(g => g.semester === currentSemester.value);
const averages = [];
currentGrades.forEach(grade => {
let sum = 0;
let count = 0;
Object.values(grade.subjects).forEach(score => {
if (score !== null && score !== undefined) {
sum += parseFloat(score);
count++;
}
});
if (count > 0) {
averages.push({
studentId: grade.studentId,
avg: sum / count
});
}
});
// 按平均分排序
averages.sort((a, b) => b.avg - a.avg);
const rank = averages.findIndex(a => a.studentId === progressStudent.value) + 1;
return {
rate,
rank
};
});
const previewTemplateContent = computed(() => {
if (!currentTemplate.content) return '请输入模板内容';
// 简单替换变量为示例文本
return currentTemplate.content
.replace(/\{\{studentName\}\}/g, '张三')
.replace(/\{\{semester\}\}/g, '第1学期')
.replace(/\{\{gradesTable\}\}/g, '<div class="alert alert-info">此处将显示成绩表格</div>')
.replace(/\{\{progressChart\}\}/g, '<div class="alert alert-info">此处将显示进步图表</div>');
});
const previewReportContent = computed(() => {
if (!selectedTemplate.value) return '';
const template = getTemplate(selectedTemplate.value);
if (!template) return '';
const student = students.value.find(s => s.id === reportStudent.value);
const grade = grades.value.find(g => g.studentId === reportStudent.value && g.semester === reportSemester.value);
if (!student || !grade) return template.content;
// 替换变量为实际内容
let content = template.content.replace(/\{\{studentName\}\}/g, student.name);
content = content.replace(/\{\{semester\}\}/g, `第${reportSemester.value}学期`);
// 生成成绩表格
if (content.includes('{{gradesTable}}')) {
let tableHtml = '<table class="table table-bordered mt-3"><thead><tr><th>科目</th><th>成绩</th></tr></thead><tbody>';
Object.entries(grade.subjects).forEach(([subject, score]) => {
tableHtml += `<tr><td>${subject}</td><td>${score || '--'}</td></tr>`;
});
tableHtml += '</tbody></table>';
content = content.replace(/\{\{gradesTable\}\}/g, tableHtml);
}
// 生成进步图表占位
if (content.includes('{{progressChart}}')) {
content = content.replace(/\{\{progressChart\}\}/g, '<div class="chart-container" style="height: 200px;"><canvas></canvas></div>');
}
return content;
});
const toastIcon = computed(() => {
const icons = {
'success': 'fa-check-circle',
'danger': 'fa-exclamation-circle',
'warning': 'fa-exclamation-triangle',
'info': 'fa-info-circle'
};
return icons[toast.type] || 'fa-info-circle';
});
// 方法
const toggleSidebar = () => {
isSidebarCollapsed.value = !isSidebarCollapsed.value;
};
const changeTab = (tab) => {
currentTab.value = tab;
// 初始化相关组件
nextTick(() => {
if (tab === 'students' && !studentsTable.value) {
initStudentsTable();
} else if (tab === 'grades' && !gradesTable.value) {
initGradesTable();
} else if (tab === 'dashboard') {
renderCharts();
} else if (tab === 'analysis') {
renderAnalysisCharts();
}
});
};
const changeSemester = (semester) => {
currentSemester.value = semester;
if (currentTab.value === 'grades' && gradesTable.value) {
gradesTable.value.setData(getGradesForSemester(semester));
}
if (currentTab.value === 'dashboard') {
renderCharts();
}
};
const showToast = (message, type = 'success', duration = 3000) => {
toast.message = message;
toast.type = type;
toast.show = true;
if (duration > 0) {
setTimeout(() => {
toast.show = false;
}, duration);
}
};
const logAction = (action) => {
logs.value.unshift({
id: generateId(),
action,
time: new Date()
});
// 限制日志数量
if (logs.value.length > 100) {
logs.value.pop();
}
saveLogs();
};
const formatTime = (date) => {
return formatTime(date);
};
// 学生管理方法
const showAddStudentModal = () => {
editStudentMode.value = false;
Object.assign(currentStudent, {
id: '',
name: '',
gender: '男',
birthday: '',
phone: '',
address: '',
status: '在读'
});
const modal = new bootstrap.Modal(document.getElementById('addStudentModal'));
modal.show();
};
const editStudent = (student) => {
editStudentMode.value = true;
Object.assign(currentStudent, JSON.parse(JSON.stringify(student)));
const modal = new bootstrap.Modal(document.getElementById('addStudentModal'));
modal.show();
};
const saveStudent = () => {
if (!currentStudent.id || !currentStudent.name) {
showToast('学号和姓名不能为空', 'danger');
return;
}
if (editStudentMode.value) {
// 更新学生
const index = students.value.findIndex(s => s.id === currentStudent.id);
if (index !== -1) {
students.value[index] = JSON.parse(JSON.stringify(currentStudent));
showToast('学生信息更新成功');
logAction(`更新学生信息: ${currentStudent.name}`);
}
} else {
// 检查学号是否已存在
if (students.value.some(s => s.id === currentStudent.id)) {
showToast('该学号已存在', 'danger');
return;
}
// 添加新学生
students.value.push(JSON.parse(JSON.stringify(currentStudent)));
showToast('学生添加成功');
logAction(`添加新学生: ${currentStudent.name}`);
}
saveStudents();
studentsTable.value.setData(students.value);
const modal = bootstrap.Modal.getInstance(document.getElementById('addStudentModal'));
modal.hide();
};
const deleteStudent = (studentId) => {
if (confirm('确定要删除这个学生吗?同时会删除该学生的所有成绩记录!')) {
const student = students.value.find(s => s.id === studentId);
students.value = students.value.filter(s => s.id !== studentId);
// 删除相关成绩
grades.value = grades.value.filter(g => g.studentId !== studentId);
saveStudents();
saveGrades();
if (studentsTable.value) {
studentsTable.value.setData(students.value);
}
if (gradesTable.value) {
gradesTable.value.setData(getGradesForSemester(currentSemester.value));
}
showToast('学生删除成功');
logAction(`删除学生: ${student?.name || studentId}`);
}
};
const initStudentsTable = () => {
studentsTable.value = new Tabulator('#studentsTable', {
data: students.value,
layout: 'fitColumns',
responsiveLayout: true,
pagination: 'local',
paginationSize: 10,
paginationSizeSelector: [5, 10, 20, 50],
columns: [
{
title: '学号',
field: 'id',
sorter: 'string',
width: 120
},
{
title: '姓名',
field: 'name',
sorter: 'string',
formatter: (cell) => {
const row = cell.getRow().getData();
return `<div class="d-flex align-items-center">
<div class="student-avatar me-2">${row.name.charAt(0)}</div>
<span>${row.name}</span>
</div>`;
}
},
{
title: '性别',
field: 'gender',
sorter: 'string',
width: 80
},
{
title: '状态',
field: 'status',
sorter: 'string',
width: 100,
formatter: (cell) => {
const status = cell.getValue();
let badgeClass = 'bg-secondary';
if (status === '在读') badgeClass = 'bg-success';
else if (status === '休学') badgeClass = 'bg-warning';
else if (status === '退学') badgeClass = 'bg-danger';
else if (status === '毕业') badgeClass = 'bg-info';
return `<span class="badge ${badgeClass}">${status}</span>`;
}
},
{
title: '联系电话',
field: 'phone',
sorter: 'string'
},
{
title: '操作',
field: 'id',
width: 150,
formatter: (cell) => {
const id = cell.getValue();
return `<div class="action-buttons">
<button class="btn btn-sm btn-outline-primary" onclick="app.editStudent(${JSON.stringify(cell.getRow().getData())})">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="app.deleteStudent('${id}')">
<i class="fas fa-trash"></i>
</button>
<button class="btn btn-sm btn-outline-info" onclick="app.viewStudentDetails('${id}')">
<i class="fas fa-info-circle"></i>
</button>
</div>`;
}
}
]
});
};
const viewStudentDetails = (studentId) => {
const student = students.value.find(s => s.id === studentId);
if (!student) return;
alert(`学生详情:\n\n姓名: ${student.name}\n学号: ${student.id}\n性别: ${student.gender}\n出生日期: ${student.birthday || '无'}\n联系电话: ${student.phone || '无'}\n家庭住址: ${student.address || '无'}\n状态: ${student.status}`);
};
// 成绩管理方法
const getGradesForSemester = (semester) => {
return grades.value.filter(g => g.semester === semester).map(grade => {
const student = students.value.find(s => s.id === grade.studentId);
return {
...grade,
studentName: student?.name || '未知',
studentGender: student?.gender || '未知'
};
});
};
const showAddGradeModal = () => {
editGradeMode.value = false;
Object.assign(currentGrade, {
id: '',
studentId: students.value[0]?.id || '',
semester: currentSemester.value,
subjects: {},
comment: ''
});
// 初始化科目成绩
subjects.value.forEach(subject => {
currentGrade.subjects[subject] = '';
});
const modal = new bootstrap.Modal(document.getElementById('addGradeModal'));
modal.show();
};
const editGrade = (grade) => {
editGradeMode.value = true;
Object.assign(currentGrade, JSON.parse(JSON.stringify(grade)));
const modal = new bootstrap.Modal(document.getElementById('addGradeModal'));
modal.show();
};
const saveGrade = () => {
if (!currentGrade.studentId) {
showToast('请选择学生', 'danger');
return;
}
// 验证成绩
for (const [subject, score] of Object.entries(currentGrade.subjects)) {
if (score !== '' && (isNaN(score) || score < 0 || score > 100)) {
showToast(`${subject}成绩必须在0-100之间`, 'danger');
return;
}
}
if (editGradeMode.value) {
// 更新成绩
const index = grades.value.findIndex(g => g.id === currentGrade.id);
if (index !== -1) {
grades.value[index] = JSON.parse(JSON.stringify(currentGrade));
showToast('成绩更新成功');
const student = students.value.find(s => s.id === currentGrade.studentId);
logAction(`更新成绩: ${student?.name || currentGrade.studentId} 第${currentGrade.semester}学期`);
}
} else {
// 检查是否已存在该学生该学期的成绩
const exists = grades.value.some(g => g.studentId === currentGrade.studentId && g.semester === currentGrade.semester);
if (exists) {
showToast('该学生本学期的成绩已存在,请编辑现有记录', 'danger');
return;
}
// 添加新成绩
currentGrade.id = generateId();
grades.value.push(JSON.parse(JSON.stringify(currentGrade)));
showToast('成绩添加成功');
const student = students.value.find(s => s.id === currentGrade.studentId);
logAction(`添加成绩: ${student?.name || currentGrade.studentId} 第${currentGrade.semester}学期`);
}
saveGrades();
if (gradesTable.value) {
gradesTable.value.setData(getGradesForSemester(currentGrade.semester));
}
const modal = bootstrap.Modal.getInstance(document.getElementById('addGradeModal'));
modal.hide();
};
const deleteGrade = (gradeId) => {
if (confirm('确定要删除这条成绩记录吗?')) {
const grade = grades.value.find(g => g.id === gradeId);
grades.value = grades.value.filter(g => g.id !== gradeId);
saveGrades();
if (gradesTable.value) {
gradesTable.value.setData(getGradesForSemester(currentSemester.value));
}
const student = students.value.find(s => s.id === grade?.studentId);
showToast('成绩删除成功');
logAction(`删除成绩: ${student?.name || grade?.studentId || '未知'} 第${grade?.semester || '未知'}学期`);
}
};
const initGradesTable = () => {
gradesTable.value = new Tabulator('#gradesTable', {
data: getGradesForSemester(currentSemester.value),
layout: 'fitColumns',
responsiveLayout: true,
pagination: 'local',
paginationSize: 10,
paginationSizeSelector: [5, 10, 20, 50],
columns: [
{
title: '学号',
field: 'studentId',
sorter: 'string',
width: 120
},
{
title: '姓名',
field: 'studentName',
sorter: 'string'
},
...subjects.value.map(subject => ({
title: subject,
field: `subjects.${subject}`,
sorter: 'number',
width: 100,
formatter: (cell) => {
const value = cell.getValue();
if (value === null || value === undefined || value === '') return '--';
const num = parseFloat(value);
if (num < 60) return `<span class="text-danger">${num}</span>`;
if (num >= 90) return `<span class="text-success">${num}</span>`;
return num;
}
})),
{
title: '操作',
field: 'id',
width: 120,
formatter: (cell) => {
const id = cell.getValue();
return `<div class="action-buttons">
<button class="btn btn-sm btn-outline-primary" onclick="app.editGrade(${JSON.stringify(cell.getRow().getData())})">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="app.deleteGrade('${id}')">
<i class="fas fa-trash"></i>
</button>
</div>`;
}
}
]
});
};
// 导入导出方法
const showImportModal = (type) => {
importType.value = type;
importFile.value = null;
dragOver.value = false;
const modal = new bootstrap.Modal(document.getElementById('importModal'));
modal.show();
};
const handleFileDrop = (e) => {
dragOver.value = false;
const file = e.dataTransfer.files[0];
if (file) {
importFile.value = file;
}
};
const handleFileSelect = (e) => {
const file = e.target.files[0];
if (file) {
importFile.value = file;
}
};
const downloadTemplate = () => {
if (importType.value === 'students') {
// 学生模板
const templateData = [
['学号', '姓名', '性别', '出生日期', '联系电话', '家庭住址', '状态'],
['1001', '张三', '男', '2010-05-15', '13800138000', '北京市海淀区', '在读'],
['1002', '李四', '女', '2010-08-22', '13900139000', '北京市朝阳区', '在读']
];
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.aoa_to_sheet(templateData);
XLSX.utils.book_append_sheet(wb, ws, '学生模板');
XLSX.writeFile(wb, '学生信息导入模板.xlsx');
} else {
// 成绩模板
const headers = ['学号', '学期', ...subjects.value, '评语'];
const templateData = [
headers,
['1001', '1', '85', '92', '88', '90', '95', '89', '该生表现优秀'],
['1002', '1', '78', '85', '82', '76', '80', '84', '该生需加强练习']
];
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.aoa_to_sheet(templateData);
XLSX.utils.book_append_sheet(wb, ws, '成绩模板');
XLSX.writeFile(wb, '成绩信息导入模板.xlsx');
}
};
const confirmImport = async () => {
if (!importFile.value) {
showToast('请先选择文件', 'danger');
return;
}
try {
const data = await readExcelFile(importFile.value);
if (importType.value === 'students') {
importStudents(data);
} else {
importGrades(data);
}
const modal = bootstrap.Modal.getInstance(document.getElementById('importModal'));
modal.hide();
} catch (error) {
showToast(`导入失败: ${error.message}`, 'danger');
}
};
const readExcelFile = (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, { type: 'array' });
const firstSheet = workbook.Sheets[workbook.SheetNames[0]];
const jsonData = XLSX.utils.sheet_to_json(firstSheet, { header: 1 });
resolve(jsonData);
} catch (error) {
reject(error);
}
};
reader.onerror = () => {
reject(new Error('文件读取失败'));
};
reader.readAsArrayBuffer(file);
});
};
const importStudents = (data) => {
if (data.length < 2) {
showToast('Excel文件中没有数据', 'danger');
return;
}
const headers = data[0].map(h => h.trim());
const requiredHeaders = ['学号', '姓名'];
// 检查必要的表头
for (const reqHeader of requiredHeaders) {
if (!headers.includes(reqHeader)) {
showToast(`Excel文件缺少必要的列: ${reqHeader}`, 'danger');
return;
}
}
const newStudents = [];
const existingIds = students.value.map(s => s.id);
for (let i = 1; i < data.length; i++) {
const row = data[i];
if (row.length === 0) continue;
const student = {
id: String(row[headers.indexOf('学号')]),
name: String(row[headers.indexOf('姓名')]),
gender: headers.includes('性别') ? String(row[headers.indexOf('性别')] || '男') : '男',
birthday: headers.includes('出生日期') ? String(row[headers.indexOf('出生日期')]) : '',
phone: headers.includes('联系电话') ? String(row[headers.indexOf('联系电话')]) : '',
address: headers.includes('家庭住址') ? String(row[headers.indexOf('家庭住址')]) : '',
status: headers.includes('状态') ? String(row[headers.indexOf('状态')] || '在读') : '在读'
};
// 检查学号是否已存在
if (existingIds.includes(student.id)) {
// 更新现有学生
const index = students.value.findIndex(s => s.id === student.id);
if (index !== -1) {
students.value[index] = student;
}
} else {
// 添加新学生
newStudents.push(student);
existingIds.push(student.id);
}
}
students.value = [...students.value, ...newStudents];
saveStudents();
if (studentsTable.value) {
studentsTable.value.setData(students.value);
}
showToast(`成功导入 ${newStudents.length} 条新学生数据,更新了 ${students.value.length - newStudents.length - existingIds.length + newStudents.length} 条现有数据`);
logAction(`导入学生数据: ${newStudents.length} 新增, ${students.value.length - newStudents.length - existingIds.length + newStudents.length} 更新`);
};
const importGrades = (data) => {
if (data.length < 2) {
showToast('Excel文件中没有数据', 'danger');
return;
}
const headers = data[0].map(h => h.trim());
const requiredHeaders = ['学号', '学期'];
// 检查必要的表头
for (const reqHeader of requiredHeaders) {
if (!headers.includes(reqHeader)) {
showToast(`Excel文件缺少必要的列: ${reqHeader}`, 'danger');
return;
}
}
const newGrades = [];
const updatedGrades = [];
for (let i = 1; i < data.length; i++) {
const row = data[i];
if (row.length === 0) continue;
const studentId = String(row[headers.indexOf('学号')]);
const semester = parseInt(row[headers.indexOf('学期')]);
if (!studentId || isNaN(semester)) {
continue;
}
// 检查学生是否存在
if (!students.value.some(s => s.id === studentId)) {
continue;
}
// 创建成绩对象
const grade = {
id: generateId(),
studentId,
semester,
subjects: {},
comment: headers.includes('评语') ? String(row[headers.indexOf('评语')] || '') : ''
};
// 添加科目成绩
subjects.value.forEach(subject => {
if (headers.includes(subject)) {
const value = row[headers.indexOf(subject)];
grade.subjects[subject] = value !== undefined && value !== '' ? parseFloat(value) : null;
} else {
grade.subjects[subject] = null;
}
});
// 检查是否已存在该学生该学期的成绩
const existingIndex = grades.value.findIndex(g => g.studentId === studentId && g.semester === semester);
if (existingIndex !== -1) {
// 更新现有成绩
grades.value[existingIndex] = grade;
updatedGrades.push(grade);
} else {
// 添加新成绩
newGrades.push(grade);
}
}
grades.value = [...grades.value, ...newGrades];
saveGrades();
if (gradesTable.value) {
gradesTable.value.setData(getGradesForSemester(currentSemester.value));
}
showToast(`成功导入 ${newGrades.length} 条新成绩数据,更新了 ${updatedGrades.length} 条现有数据`);
logAction(`导入成绩数据: ${newGrades.length} 新增, ${updatedGrades.length} 更新`);
};
const exportGradeExcel = () => {
const gradesToExport = grades.value.filter(g => g.semester === currentSemester.value);
if (gradesToExport.length === 0) {
showToast('当前学期没有成绩数据可导出', 'warning');
return;
}
const headers = ['学号', '姓名', '学期', ...subjects.value, '评语'];
const data = [headers];
gradesToExport.forEach(grade => {
const student = students.value.find(s => s.id === grade.studentId);
const row = [
grade.studentId,
student?.name || '未知',
grade.semester
];
subjects.value.forEach(subject => {
row.push(grade.subjects[subject] || '');
});
row.push(grade.comment || '');
data.push(row);
});
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.aoa_to_sheet(data);
XLSX.utils.book_append_sheet(wb, ws, '成绩数据');
XLSX.writeFile(wb, `第${currentSemester.value}学期成绩.xlsx`);
showToast('成绩导出成功');
logAction(`导出成绩数据: 第${currentSemester.value}学期`);
};
const exportData = () => {
const data = {
students: students.value,
grades: grades.value,
reportTemplates: reportTemplates.value,
subjects: subjects.value,
className: className.value,
teacherName: teacherName.value,
academicYear: academicYear.value,
lastBackupTime: new Date()
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `班级成长档案备份_${formatTime(new Date()).replace(/[: ]/g, '-')}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
lastBackupTime.value = new Date();
saveLastBackupTime();
showToast('数据导出成功');
logAction('导出所有数据备份');
};
const importData = () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target.result);
if (confirm('导入数据将覆盖当前所有数据,确定要继续吗?')) {
students.value = data.students || [];
grades.value = data.grades || [];
reportTemplates.value = data.reportTemplates || [];
subjects.value = data.subjects || ['语文', '数学', '英语'];
className.value = data.className || '六年级一班';
teacherName.value = data.teacherName || '张老师';
academicYear.value = data.academicYear || '2022-2023';
lastBackupTime.value = data.lastBackupTime ? new Date(data.lastBackupTime) : null;
saveAllData();
if (studentsTable.value) {
studentsTable.value.setData(students.value);
}
if (gradesTable.value) {
gradesTable.value.setData(getGradesForSemester(currentSemester.value));
}
showToast('数据导入成功');
logAction('导入所有数据备份');
}
} catch (error) {
showToast('导入失败: 文件格式不正确', 'danger');
}
};
reader.readAsText(file);
};
input.click();
};
// 报告管理方法
const showAddTemplateModal = () => {
editTemplateMode.value = false;
Object.assign(currentTemplate, {
id: '',
name: '',
content: ''
});
const modal = new bootstrap.Modal(document.getElementById('addTemplateModal'));
modal.show();
};
const editTemplate = () => {
const template = getTemplate(selectedTemplate.value);
if (!template) return;
editTemplateMode.value = true;
Object.assign(currentTemplate, JSON.parse(JSON.stringify(template)));
const modal = new bootstrap.Modal(document.getElementById('addTemplateModal'));
modal.show();
};
const selectTemplate = (templateId) => {
selectedTemplate.value = templateId;
if (students.value.length > 0) {
reportStudent.value = students.value[0].id;
}
reportSemester.value = currentSemester.value;
};
const getTemplate = (templateId) => {
return reportTemplates.value.find(t => t.id === templateId);
};
const saveTemplate = () => {
if (!currentTemplate.name || !currentTemplate.content) {
showToast('模板名称和内容不能为空', 'danger');
return;
}
if (editTemplateMode.value) {
// 更新模板
const index = reportTemplates.value.findIndex(t => t.id === currentTemplate.id);
if (index !== -1) {
currentTemplate.updatedAt = new Date();
reportTemplates.value[index] = JSON.parse(JSON.stringify(currentTemplate));
showToast('模板更新成功');
logAction(`更新报告模板: ${currentTemplate.name}`);
}
} else {
// 添加新模板
currentTemplate.id = generateId();
currentTemplate.createdAt = new Date();
currentTemplate.updatedAt = new Date();
reportTemplates.value.push(JSON.parse(JSON.stringify(currentTemplate)));
showToast('模板添加成功');
logAction(`添加报告模板: ${currentTemplate.name}`);
// 自动选择新模板
selectedTemplate.value = currentTemplate.id;
}
saveReportTemplates();
const modal = bootstrap.Modal.getInstance(document.getElementById('addTemplateModal'));
modal.hide();
};
const insertVariable = (variable) => {
const textarea = document.querySelector('#addTemplateModal textarea');
const startPos = textarea.selectionStart;
const endPos = textarea.selectionEnd;
const variableText = `{{${variable}}}`;
currentTemplate.content = currentTemplate.content.substring(0, startPos) +
variableText +
currentTemplate.content.substring(endPos);
// 移动光标到变量后面
nextTick(() => {
textarea.selectionStart = startPos + variableText.length;
textarea.selectionEnd = startPos + variableText.length;
textarea.focus();
});
};
const generateReport = () => {
if (!selectedTemplate.value || !reportStudent.value) {
showToast('请选择模板和学生', 'danger');
return;
}
const template = getTemplate(selectedTemplate.value);
const student = students.value.find(s => s.id === reportStudent.value);
const grade = grades.value.find(g => g.studentId === reportStudent.value && g.semester === reportSemester.value);
if (!template || !student || !grade) {
showToast('无法生成报告,数据不完整', 'danger');
return;
}
// 创建PDF
const doc = new jspdf.jsPDF();
// 添加标题
doc.setFontSize(18);
doc.text(template.name, 105, 20, { align: 'center' });
// 添加基本信息
doc.setFontSize(12);
doc.text(`学生: ${student.name}`, 20, 40);
doc.text(`学号: ${student.id}`, 20, 50);
doc.text(`学期: 第${reportSemester.value}学期`, 20, 60);
// 添加成绩表格
const gradesData = [];
Object.entries(grade.subjects).forEach(([subject, score]) => {
gradesData.push([subject, score || '--']);
});
doc.autoTable({
startY: 70,
head: [['科目', '成绩']],
body: gradesData,
theme: 'grid',
headStyles: {
fillColor: [52, 152, 219],
textColor: 255
}
});
// 添加评语
if (grade.comment) {
doc.text('教师评语:', 20, doc.autoTable.previous.finalY + 20);
doc.text(grade.comment, 20, doc.autoTable.previous.finalY + 30, { maxWidth: 170 });
}
// 保存PDF
doc.save(`${student.name}_第${reportSemester.value}学期报告.pdf`);
showToast('报告生成成功');
logAction(`生成报告: ${student.name} 第${reportSemester.value}学期`);
};
const batchGenerateReports = () => {
if (!selectedTemplate.value) {
showToast('请先选择报告模板', 'danger');
return;
}
if (students.value.length === 0) {
showToast('没有学生数据', 'warning');
return;
}
if (confirm(`确定要为全班${students.value.length}名学生生成第${reportSemester.value}学期报告吗?`)) {
showToast('开始批量生成报告,请稍候...', 'info', 0);
// 使用setTimeout让UI有机会更新
setTimeout(() => {
const template = getTemplate(selectedTemplate.value);
students.value.forEach(student => {
const grade = grades.value.find(g => g.studentId === student.id && g.semester === reportSemester.value);
if (!grade) return;
// 创建PDF
const doc = new jspdf.jsPDF();
// 添加标题
doc.setFontSize(18);
doc.text(template.name, 105, 20, { align: 'center' });
// 添加基本信息
doc.setFontSize(12);
doc.text(`学生: ${student.name}`, 20, 40);
doc.text(`学号: ${student.id}`, 20, 50);
doc.text(`学期: 第${reportSemester.value}学期`, 20, 60);
// 添加成绩表格
const gradesData = [];
Object.entries(grade.subjects).forEach(([subject, score]) => {
gradesData.push([subject, score || '--']);
});
doc.autoTable({
startY: 70,
head: [['科目', '成绩']],
body: gradesData,
theme: 'grid',
headStyles: {
fillColor: [52, 152, 219],
textColor: 255
}
});
// 添加评语
if (grade.comment) {
doc.text('教师评语:', 20, doc.autoTable.previous.finalY + 20);
doc.text(grade.comment, 20, doc.autoTable.previous.finalY + 30, { maxWidth: 170 });
}
// 保存PDF
doc.save(`${student.name}_第${reportSemester.value}学期报告.pdf`);
});
toast.show = false;
showToast(`成功为${students.value.length}名学生生成报告`);
logAction(`批量生成报告: ${students.value.length}名学生 第${reportSemester.value}学期`);
}, 100);
}
};
// 图表方法
const renderCharts = () => {
renderTrendChart();
renderDistributionChart();
};
const renderAnalysisCharts = () => {
renderSubjectAnalysisChart();
renderStudentProgressChart();
renderCorrelationChart();
};
const renderTrendChart = () => {
const ctx = document.getElementById('trendChart');
if (!ctx) return;
// 销毁旧图表
if (trendChart.value) {
trendChart.value.destroy();
}
// 准备数据
const labels = Array.from({ length: 12 }, (_, i) => `第${i + 1}学期`);
const datasets = [];
if (trendSubject.value === 'all') {
// 显示所有科目平均分趋势
subjects.value.forEach(subject => {
const data = [];
for (let i = 1; i <= 12; i++) {
const semesterGrades = grades.value.filter(g => g.semester === i);
if (semesterGrades.length === 0) {
data.push(null);
continue;
}
let sum = 0;
let count = 0;
semesterGrades.forEach(grade => {
if (grade.subjects[subject] !== null && grade.subjects[subject] !== undefined) {
sum += parseFloat(grade.subjects[subject]);
count++;
}
});
data.push(count > 0 ? sum / count : null);
}
datasets.push({
label: subject,
data,
borderWidth: 2,
tension: 0.3,
fill: false
});
});
} else {
// 显示单个科目平均分趋势
const subject = trendSubject.value;
const data = [];
for (let i = 1; i <= 12; i++) {
const semesterGrades = grades.value.filter(g => g.semester === i);
if (semesterGrades.length === 0) {
data.push(null);
continue;
}
let sum = 0;
let count = 0;
semesterGrades.forEach(grade => {
if (grade.subjects[subject] !== null && grade.subjects[subject] !== undefined) {
sum += parseFloat(grade.subjects[subject]);
count++;
}
});
data.push(count > 0 ? sum / count : null);
}
datasets.push({
label: subject,
data,
borderWidth: 2,
borderColor: '#3498db',
backgroundColor: '#3498db',
tension: 0.3,
fill: false
});
}
trendChart.value = new Chart(ctx, {
type: 'line',
data: {
labels,
datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: trendSubject.value === 'all' ? '各科目平均分趋势' : `${trendSubject.value}平均分趋势`,
font: {
size: 16
}
},
tooltip: {
mode: 'index',
intersect: false
},
legend: {
position: 'bottom'
}
},
scales: {
y: {
beginAtZero: false,
min: 0,
max: 100,
title: {
display: true,
text: '分数'
}
}
}
}
});
};
const renderDistributionChart = () => {
const ctx = document.getElementById('distributionChart');
if (!ctx) return;
// 销毁旧图表
if (distributionChart.value) {
distributionChart.value.destroy();
}
// 准备数据
const currentGrades = grades.value.filter(g => g.semester === currentSemester.value);
if (currentGrades.length === 0) return;
const segments = [
{ min: 0, max: 60, label: '不及格' },
{ min: 60, max: 70, label: '及格' },
{ min: 70, max: 80, label: '中等' },
{ min: 80, max: 90, label: '良好' },
{ min: 90, max: 100, label: '优秀' }
];
const data = segments.map(segment => {
let count = 0;
currentGrades.forEach(grade => {
Object.values(grade.subjects).forEach(score => {
if (score !== null && score !== undefined) {
const num = parseFloat(score);
if (num >= segment.min && num < segment.max) {
count++;
}
}
});
});
return count;
});
const labels = segments.map(s => s.label);
const backgroundColors = [
'rgba(231, 76, 60, 0.7)',
'rgba(241, 196, 15, 0.7)',
'rgba(52, 152, 219, 0.7)',
'rgba(46, 204, 113, 0.7)',
'rgba(155, 89, 182, 0.7)'
];
distributionChart.value = new Chart(ctx, {
type: 'doughnut',
data: {
labels,
datasets: [{
data,
backgroundColor: backgroundColors,
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: '成绩分布',
font: {
size: 16
}
},
legend: {
position: 'bottom'
}
}
}
});
};
const renderSubjectAnalysisChart = () => {
const ctx = document.getElementById('subjectAnalysisChart');
if (!ctx) return;
// 销毁旧图表
if (subjectAnalysisChart.value) {
subjectAnalysisChart.value.destroy();
}
const stats = subjectStats.value;
if (!stats || !stats.segmentCounts) return;
const labels = Object.keys(stats.segmentCounts).map(key => {
if (key === '0-60') return '不及格(<60)';
if (key === '60-70') return '及格(60-69)';
if (key === '70-80') return '中等(70-79)';
if (key === '80-90') return '良好(80-89)';
if (key === '90-100') return '优秀(90-99)';
if (key === '100') return '满分(100)';
return key;
});
const data = Object.values(stats.segmentCounts);
const backgroundColors = [
'rgba(231, 76, 60, 0.7)',
'rgba(241, 196, 15, 0.7)',
'rgba(52, 152, 219, 0.7)',
'rgba(46, 204, 113, 0.7)',
'rgba(155, 89, 182, 0.7)',
'rgba(26, 188, 156, 0.7)'
];
subjectAnalysisChart.value = new Chart(ctx, {
type: 'bar',
data: {
labels,
datasets: [{
label: '人数',
data,
backgroundColor: backgroundColors,
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: `${analysisSubject.value}成绩分布`,
font: {
size: 16
}
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 1
}
}
}
}
});
};
const renderStudentProgressChart = () => {
const ctx = document.getElementById('studentProgressChart');
if (!ctx || !progressStudent.value) return;
// 销毁旧图表
if (studentProgressChart.value) {
studentProgressChart.value.destroy();
}
const studentGrades = grades.value.filter(g => g.studentId === progressStudent.value);
if (studentGrades.length === 0) return;
const student = students.value.find(s => s.id === progressStudent.value);
const labels = [];
const subjectData = {};
// 初始化科目数据
subjects.value.forEach(subject => {
subjectData[subject] = [];
});
// 按学期排序
studentGrades.sort((a, b) => a.semester - b.semester);
// 填充数据
studentGrades.forEach(grade => {
labels.push(`第${grade.semester}学期`);
subjects.value.forEach(subject => {
subjectData[subject].push(grade.subjects[subject] || null);
});
});
// 创建数据集
const datasets = subjects.value.map(subject => ({
label: subject,
data: subjectData[subject],
borderWidth: 2,
tension: 0.3,
fill: false
}));
studentProgressChart.value = new Chart(ctx, {
type: 'line',
data: {
labels,
datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: `${student?.name || progressStudent.value}成绩趋势`,
font: {
size: 16
}
},
tooltip: {
mode: 'index',
intersect: false
},
legend: {
position: 'bottom'
}
},
scales: {
y: {
beginAtZero: false,
min: 0,
max: 100,
title: {
display: true,
text: '分数'
}
}
}
}
});
};
const renderCorrelationChart = () => {
const ctx = document.getElementById('correlationChart');
if (!ctx) return;
// 销毁旧图表
if (correlationChart.value) {
correlationChart.value.destroy();
}
const currentGrades = grades.value.filter(g => g.semester === currentSemester.value);
if (currentGrades.length === 0) return;
// 计算相关系数矩阵
const matrix = [];
const subjectList = subjects.value;
// 初始化矩阵
for (let i = 0; i < subjectList.length; i++) {
matrix[i] = [];
for (let j = 0; j < subjectList.length; j++) {
matrix[i][j] = 0;
}
}
// 计算每对科目的相关系数
for (let i = 0; i < subjectList.length; i++) {
for (let j = i; j < subjectList.length; j++) {
const subject1 = subjectList[i];
const subject2 = subjectList[j];
// 收集两科目的成绩对
const pairs = [];
currentGrades.forEach(grade => {
const score1 = grade.subjects[subject1];
const score2 = grade.subjects[subject2];
if (score1 !== null && score1 !== undefined &&
score2 !== null && score2 !== undefined) {
pairs.push({
x: parseFloat(score1),
y: parseFloat(score2)
});
}
});
if (pairs.length > 1) {
// 计算相关系数
const n = pairs.length;
let sumX = 0, sumY = 0, sumXY = 0;
let sumX2 = 0, sumY2 = 0;
pairs.forEach(pair => {
sumX += pair.x;
sumY += pair.y;
sumXY += pair.x * pair.y;
sumX2 += pair.x * pair.x;
sumY2 += pair.y * pair.y;
});
const numerator = sumXY - (sumX * sumY / n);
const denominator = Math.sqrt((sumX2 - (sumX * sumX / n)) * (sumY2 - (sumY * sumY / n)));
const r = denominator !== 0 ? numerator / denominator : 0;
matrix[i][j] = r;
matrix[j][i] = r;
}
}
}
// 准备热力图数据
const data = {
labels: subjectList,
datasets: [{
data: matrix.flatMap((row, i) => row.map((value, j) => ({ x: subjectList[j], y: subjectList[i], v: value })))
}]
};
correlationChart.value = new Chart(ctx, {
type: 'matrix',
data,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: '科目成绩相关性分析',
font: {
size: 16
}
},
tooltip: {
callbacks: {
title: () => '', // 禁用标题
label: (context) => {
const { x, y, v } = context.raw;
return `${x} & ${y}: ${v.toFixed(2)}`;
}
}
},
legend: {
display: false
}
},
scales: {
x: {
type: 'category',
offset: true,
grid: {
display: false
},
position: 'bottom'
},
y: {
type: 'category',
offset: true,
grid: {
display: false
}
}
}
}
});
};
// 系统设置方法
const addSubject = () => {
if (!newSubject.value.trim()) {
showToast('科目名称不能为空', 'danger');
return;
}
if (subjects.value.includes(newSubject.value.trim())) {
showToast('该科目已存在', 'danger');
return;
}
subjects.value.push(newSubject.value.trim());
newSubject.value = '';
saveSubjects();
showToast('科目添加成功');
logAction(`添加科目: ${newSubject.value}`);
};
const removeSubject = (subject) => {
if (confirm(`确定要删除科目"${subject}"吗?这将删除所有相关的成绩记录!`)) {
subjects.value = subjects.value.filter(s => s !== subject);
// 删除相关成绩
grades.value.forEach(grade => {
delete grade.subjects[subject];
});
saveSubjects();
saveGrades();
if (gradesTable.value) {
gradesTable.value.setData(getGradesForSemester(currentSemester.value));
}
showToast('科目删除成功');
logAction(`删除科目: ${subject}`);
}
};
const saveSettings = () => {
localStorage.setItem('className', className.value);
localStorage.setItem('teacherName', teacherName.value);
localStorage.setItem('academicYear', academicYear.value);
showToast('系统设置保存成功');
logAction('更新系统设置');
};
const confirmClearData = (type) => {
clearDataType.value = type;
confirmChecked.value = false;
const modal = new bootstrap.Modal(document.getElementById('confirmClearModal'));
modal.show();
};
const clearData = () => {
if (clearDataType.value === 'students') {
if (confirm('确定要清空所有学生数据吗?这将同时删除所有成绩记录!')) {
students.value = [];
grades.value = [];
saveStudents();
saveGrades();
if (studentsTable.value) {
studentsTable.value.setData(students.value);
}
if (gradesTable.value) {
gradesTable.value.setData(getGradesForSemester(currentSemester.value));
}
showToast('学生数据已清空');
logAction('清空所有学生数据');
}
} else if (clearDataType.value === 'grades') {
grades.value = [];
saveGrades();
if (gradesTable.value) {
gradesTable.value.setData(getGradesForSemester(currentSemester.value));
}
showToast('成绩数据已清空');
logAction('清空所有成绩数据');
} else if (clearDataType.value === 'all') {
if (confirm('确定要清空所有数据吗?这将删除学生、成绩和所有设置!')) {
students.value = [];
grades.value = [];
reportTemplates.value = [];
subjects.value = ['语文', '数学', '英语'];
className.value = '六年级一班';
teacherName.value = '张老师';
academicYear.value = '2022-2023';
saveAllData();
if (studentsTable.value) {
studentsTable.value.setData(students.value);
}
if (gradesTable.value) {
gradesTable.value.setData(getGradesForSemester(currentSemester.value));
}
showToast('所有数据已清空');
logAction('清空所有数据');
}
}
const modal = bootstrap.Modal.getInstance(document.getElementById('confirmClearModal'));
modal.hide();
};
// 数据持久化方法
const saveStudents = () => {
localStorage.setItem('students', JSON.stringify(students.value));
};
const saveGrades = () => {
localStorage.setItem('grades', JSON.stringify(grades.value));
};
const saveReportTemplates = () => {
localStorage.setItem('reportTemplates', JSON.stringify(reportTemplates.value));
};
const saveSubjects = () => {
localStorage.setItem('subjects', JSON.stringify(subjects.value));
};
const saveLogs = () => {
localStorage.setItem('logs', JSON.stringify(logs.value));
};
const saveLastBackupTime = () => {
localStorage.setItem('lastBackupTime', lastBackupTime.value ? lastBackupTime.value.toISOString() : '');
};
const saveAllData = () => {
saveStudents();
saveGrades();
saveReportTemplates();
saveSubjects();
saveLogs();
saveLastBackupTime();
localStorage.setItem('className', className.value);
localStorage.setItem('teacherName', teacherName.value);
localStorage.setItem('academicYear', academicYear.value);
};
const loadAllData = () => {
// 加载学生数据
const studentsData = localStorage.getItem('students');
if (studentsData) {
students.value = JSON.parse(studentsData);
}
// 加载成绩数据
const gradesData = localStorage.getItem('grades');
if (gradesData) {
grades.value = JSON.parse(gradesData);
}
// 加载报告模板
const templatesData = localStorage.getItem('reportTemplates');
if (templatesData) {
reportTemplates.value = JSON.parse(templatesData);
}
// 加载科目
const subjectsData = localStorage.getItem('subjects');
if (subjectsData) {
subjects.value = JSON.parse(subjectsData);
}
// 加载日志
const logsData = localStorage.getItem('logs');
if (logsData) {
logs.value = JSON.parse(logsData);
}
// 加载备份时间
const backupTime = localStorage.getItem('lastBackupTime');
if (backupTime) {
lastBackupTime.value = new Date(backupTime);
}
// 加载系统设置
const savedClassName = localStorage.getItem('className');
if (savedClassName) {
className.value = savedClassName;
}
const savedTeacherName = localStorage.getItem('teacherName');
if (savedTeacherName) {
teacherName.value = savedTeacherName;
}
const savedAcademicYear = localStorage.getItem('academicYear');
if (savedAcademicYear) {
academicYear.value = savedAcademicYear;
}
};
// 其他方法
const viewAllLogs = () => {
const modal = new bootstrap.Modal(document.getElementById('allLogsModal'));
modal.show();
};
// 初始化
onMounted(() => {
loadAllData();
// 设置默认选中的学生和模板
if (students.value.length > 0) {
progressStudent.value = students.value[0].id;
reportStudent.value = students.value[0].id;
}
if (reportTemplates.value.length > 0) {
selectedTemplate.value = reportTemplates.value[0].id;
}
// 初始化图表
renderCharts();
// 监听窗口大小变化,重新渲染图表
window.addEventListener('resize', () => {
if (currentTab.value === 'dashboard') {
renderCharts();
} else if (currentTab.value === 'analysis') {
renderAnalysisCharts();
}
});
});
// 监听相关状态变化
watch(trendSubject, () => {
if (currentTab.value === 'dashboard') {
renderTrendChart();
}
});
watch(currentSemester, () => {
if (currentTab.value === 'dashboard') {
renderDistributionChart();
}
});
watch(analysisSubject, () => {
if (currentTab.value === 'analysis') {
renderSubjectAnalysisChart();
}
});
watch(progressStudent, () => {
if (currentTab.value === 'analysis') {
renderStudentProgressChart();
}
});
watch(currentTemplate, () => {
// 模板内容变化时更新预览
}, { deep: true });
// 暴露方法给模板
return {
isSidebarCollapsed,
currentTab,
currentSemester,
students,
grades,
reportTemplates,
selectedTemplate,
reportStudent,
reportSemester,
subjects,
className,
teacherName,
academicYear,
logs,
lastBackupTime,
toast,
currentStudent,
currentGrade,
currentTemplate,
editStudentMode,
editGradeMode,
editTemplateMode,
importType,
importFile,
dragOver,
clearDataType,
confirmChecked,
trendSubject,
analysisSubject,
progressStudent,
newSubject: ref(''),
currentTabName,
studentCount,
averageGrade,
maxGrade,
minGrade,
gradeWarnings,
subjectStats,
studentProgress,
previewTemplateContent,
previewReportContent,
toastIcon,
toggleSidebar,
changeTab,
changeSemester,
showToast,
logAction,
formatTime,
showAddStudentModal,
editStudent,
saveStudent,
deleteStudent,
viewStudentDetails,
showAddGradeModal,
editGrade,
saveGrade,
deleteGrade,
showImportModal,
handleFileDrop,
handleFileSelect,
downloadTemplate,
confirmImport,
exportGradeExcel,
exportData,
importData,
showAddTemplateModal,
editTemplate,
selectTemplate,
saveTemplate,
insertVariable,
generateReport,
batchGenerateReports,
addSubject,
removeSubject,
saveSettings,
confirmClearData,
clearData,
viewAllLogs,
getTemplate
};
}
}).mount('#app');
// 注册矩阵图表类型
Chart.register({
id: 'matrix',
defaults: {
width: 400,
height: 400
},
afterInit(chart) {
const { ctx, data, chartArea: { top, bottom, left, right }, scales: { x, y } } = chart;
// 计算每个单元格的大小
const xAxis = x.getLabels();
const yAxis = y.getLabels();
const cellSize = (right - left) / xAxis.length;
// 存储单元格位置
chart._metasets[0]._grids = data.datasets[0].data.map(({ x: xVal, y: yVal, v }) => {
const xIndex = xAxis.indexOf(xVal);
const yIndex = yAxis.indexOf(yVal);
return {
x: left + xIndex * cellSize + cellSize / 2,
y: top + yIndex * cellSize + cellSize / 2,
width: cellSize - 2,
height: cellSize - 2,
value: v
};
});
},
draw(chart) {
const { ctx, chartArea: { top, bottom, left, right }, _metasets: [meta] } = chart;
// 绘制热力图
meta._grids.forEach(({ x, y, width, height, value }) => {
// 根据值计算颜色 (从蓝到红)
const colorValue = (value + 1) / 2; // 将范围从[-1,1]映射到[0,1]
const r = Math.floor(255 * colorValue);
const b = Math.floor(255 * (1 - colorValue));
const color = `rgb(${r}, 0, ${b})`;
ctx.fillStyle = color;
ctx.fillRect(x - width / 2, y - height / 2, width, height);
// 显示数值
ctx.fillStyle = 'white';
ctx.font = '10px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(value.toFixed(2), x, y);
});
}
});
</script>
</body>
</html>
原文链接:https://dnsy.nxskgs.com/huodong%e4%bb%8b%e7%bb%8d/1479.html