761 lines
38 KiB
HTML
761 lines
38 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>教学日程查询 - {{dateRange}}</title>
|
|
<!-- Tailwind CSS -->
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<!-- Font Awesome -->
|
|
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
|
<!-- SheetJS for Excel export -->
|
|
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
|
|
<!-- Day.js for date handling -->
|
|
<script src="https://cdn.jsdelivr.net/npm/dayjs@1.11.10/dayjs.min.js"></script>
|
|
|
|
<script>
|
|
tailwind.config = {
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
primary: '#f97316',
|
|
success: '#22c55e',
|
|
warning: '#eab308',
|
|
danger: '#ef4444',
|
|
info: '#fb923c',
|
|
},
|
|
fontFamily: {
|
|
sans: ['Inter', 'system-ui', 'sans-serif'],
|
|
},
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style type="text/tailwindcss">
|
|
@layer utilities {
|
|
.content-auto {
|
|
content-visibility: auto;
|
|
}
|
|
.card-shadow {
|
|
box-shadow: 0 4px 24px rgba(249, 115, 22, 0.08);
|
|
}
|
|
.card-shadow-strong {
|
|
box-shadow: 0 8px 32px rgba(249, 115, 22, 0.12);
|
|
}
|
|
.card-hover {
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
.card-hover:hover {
|
|
transform: translateY(-4px);
|
|
box-shadow: 0 12px 40px rgba(249, 115, 22, 0.16);
|
|
}
|
|
.animate-fade-in {
|
|
animation: fadeIn 0.4s ease-out;
|
|
}
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; transform: translateY(12px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
.date-group-header {
|
|
position: sticky;
|
|
top: 90px;
|
|
z-index: 10;
|
|
backdrop-filter: blur(10px);
|
|
background-color: rgba(255, 247, 237, 0.85);
|
|
}
|
|
.glass-card {
|
|
background: linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(255,247,237,0.9) 100%);
|
|
border: 1px solid rgba(249, 115, 22, 0.08);
|
|
}
|
|
.stat-card {
|
|
background: linear-gradient(145deg, #ffffff 0%, #fff7ed 100%);
|
|
border: 1px solid rgba(249, 115, 22, 0.06);
|
|
}
|
|
.orange-glow {
|
|
box-shadow: 0 0 20px rgba(249, 115, 22, 0.15);
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="bg-orange-50/40 min-h-screen font-sans">
|
|
<!-- 头部导航 -->
|
|
<header class="bg-white/90 backdrop-blur-md shadow-sm sticky top-0 z-50 border-b border-orange-100">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
|
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-10 h-10 bg-gradient-to-br from-orange-400 to-orange-600 rounded-xl flex items-center justify-center text-white shadow-lg shadow-orange-200">
|
|
<i class="fa fa-calendar-check-o text-lg"></i>
|
|
</div>
|
|
<h1 class="text-xl font-bold text-gray-800">教学日程查询</h1>
|
|
<span class="px-3 py-1 bg-orange-100 text-orange-600 rounded-full text-sm font-semibold border border-orange-200">
|
|
{{dateRange}}
|
|
</span>
|
|
</div>
|
|
<div class="flex flex-wrap items-center gap-3 w-full sm:w-auto">
|
|
<!-- 日期范围显示 -->
|
|
<div class="flex items-center gap-2 text-sm text-gray-600">
|
|
<span>共 {{totalDays}} 天</span>
|
|
</div>
|
|
<!-- 搜索框 -->
|
|
<div class="relative flex-1 sm:flex-initial min-w-[200px]">
|
|
<i class="fa fa-search absolute left-3 top-1/2 -translate-y-1/2 text-orange-300"></i>
|
|
<input
|
|
type="text"
|
|
id="searchInput"
|
|
placeholder="搜索班级、学生、日期..."
|
|
class="w-full pl-10 pr-4 py-2 border border-orange-200 rounded-xl focus:ring-2 focus:ring-orange-300/50 focus:border-orange-400 outline-none transition-all bg-white/80"
|
|
>
|
|
</div>
|
|
<!-- 导出按钮 -->
|
|
<button
|
|
id="exportBtn"
|
|
class="px-4 py-2 bg-gradient-to-r from-orange-400 to-orange-500 hover:from-orange-500 hover:to-orange-600 text-white rounded-xl flex items-center gap-2 transition-all shadow-lg shadow-orange-200 hover:shadow-xl hover:shadow-orange-300"
|
|
>
|
|
<i class="fa fa-download"></i>
|
|
<span>导出Excel</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
<!-- 统计信息 -->
|
|
<div id="statsSection" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4 mb-8">
|
|
<div class="stat-card rounded-2xl p-6 card-shadow animate-fade-in">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<p class="text-sm text-gray-500 mb-1">总天数</p>
|
|
<p id="totalDays" class="text-3xl font-bold text-gray-800">0</p>
|
|
</div>
|
|
<div class="w-12 h-12 bg-gradient-to-br from-orange-100 to-orange-200 rounded-2xl flex items-center justify-center text-orange-500">
|
|
<i class="fa fa-calendar text-xl"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="stat-card rounded-2xl p-6 card-shadow animate-fade-in" style="animation-delay: 0.1s">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<p class="text-sm text-gray-500 mb-1">总课程</p>
|
|
<p id="totalClasses" class="text-3xl font-bold text-gray-800">0</p>
|
|
</div>
|
|
<div class="w-12 h-12 bg-gradient-to-br from-orange-100 to-amber-100 rounded-2xl flex items-center justify-center text-amber-500">
|
|
<i class="fa fa-book text-xl"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="stat-card rounded-2xl p-6 card-shadow animate-fade-in" style="animation-delay: 0.2s">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<p class="text-sm text-gray-500 mb-1">学生人次</p>
|
|
<p id="totalStudents" class="text-3xl font-bold text-gray-800">0</p>
|
|
</div>
|
|
<div class="w-12 h-12 bg-gradient-to-br from-orange-100 to-rose-100 rounded-2xl flex items-center justify-center text-rose-400">
|
|
<i class="fa fa-users text-xl"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="stat-card rounded-2xl p-6 card-shadow animate-fade-in" style="animation-delay: 0.3s">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<p class="text-sm text-gray-500 mb-1">出勤人次</p>
|
|
<p id="presentStudents" class="text-3xl font-bold text-green-500">0</p>
|
|
</div>
|
|
<div class="w-12 h-12 bg-gradient-to-br from-green-100 to-emerald-100 rounded-2xl flex items-center justify-center text-green-500">
|
|
<i class="fa fa-check-circle text-xl"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="stat-card rounded-2xl p-6 card-shadow animate-fade-in" style="animation-delay: 0.4s">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<p class="text-sm text-gray-500 mb-1">请假人次</p>
|
|
<p id="leaveStudents" class="text-3xl font-bold text-amber-500">0</p>
|
|
</div>
|
|
<div class="w-12 h-12 bg-gradient-to-br from-amber-100 to-yellow-100 rounded-2xl flex items-center justify-center text-amber-500">
|
|
<i class="fa fa-pause-circle text-xl"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 筛选栏 -->
|
|
<div id="filterSection" class="glass-card rounded-2xl p-4 mb-6 card-shadow animate-fade-in" style="animation-delay: 0.5s">
|
|
<div class="flex flex-wrap items-center gap-4">
|
|
<span class="text-sm font-semibold text-gray-700">出勤筛选:</span>
|
|
<div class="flex flex-wrap gap-2">
|
|
<button class="filter-btn px-3 py-1.5 bg-gray-100 hover:bg-gray-200 rounded-full text-sm transition-all active" data-filter="all">
|
|
全部
|
|
</button>
|
|
<button class="filter-btn px-3 py-1.5 bg-green-50 hover:bg-green-100 text-green-600 rounded-full text-sm transition-all" data-filter="present">
|
|
✅ 出勤
|
|
</button>
|
|
<button class="filter-btn px-3 py-1.5 bg-amber-50 hover:bg-amber-100 text-amber-600 rounded-full text-sm transition-all" data-filter="leave">
|
|
⏸ 请假
|
|
</button>
|
|
<button class="filter-btn px-3 py-1.5 bg-red-50 hover:bg-red-100 text-red-500 rounded-full text-sm transition-all" data-filter="absent">
|
|
❌ 缺勤
|
|
</button>
|
|
</div>
|
|
<div class="flex items-center gap-2 ml-auto">
|
|
<span class="text-sm font-semibold text-gray-700">视图:</span>
|
|
<button id="viewByDate" class="px-3 py-1.5 bg-gradient-to-r from-orange-400 to-orange-500 text-white rounded-full text-sm transition-all shadow-md shadow-orange-200">
|
|
按日期
|
|
</button>
|
|
<button id="viewByClass" class="px-3 py-1.5 bg-gray-100 hover:bg-gray-200 rounded-full text-sm transition-all">
|
|
按班级
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 课程列表 -->
|
|
<div id="classesContainer" class="space-y-8">
|
|
<!-- 课程会通过JavaScript动态生成 -->
|
|
<div id="emptyState" class="text-center py-16">
|
|
<div class="w-24 h-24 bg-gradient-to-br from-orange-100 to-orange-200 rounded-2xl flex items-center justify-center mx-auto mb-4 shadow-lg shadow-orange-100">
|
|
<i class="fa fa-calendar-o text-4xl text-orange-400"></i>
|
|
</div>
|
|
<h3 class="text-lg font-semibold text-gray-700 mb-2">暂无课程安排</h3>
|
|
<p class="text-gray-500">选择其他日期范围查看日程</p>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- 页脚 -->
|
|
<footer class="bg-white/80 backdrop-blur-sm border-t border-orange-100 mt-16">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
|
<div class="text-center text-sm text-gray-500">
|
|
<p>© 2026 穹狼科创 · 教学日程查询系统</p>
|
|
<p class="mt-1">数据更新时间:{{updateTime}}</p>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
|
|
<script>
|
|
// 原始数据,会在生成网页时注入
|
|
// 格式:数组,每个元素是单天的日程数据 {teaching_date, items: [...]}
|
|
const scheduleData = {{scheduleData}};
|
|
|
|
// 当前配置
|
|
let currentFilter = 'all';
|
|
let searchKeyword = '';
|
|
let currentView = 'date'; // date 按日期, class 按班级
|
|
|
|
// 初始化页面
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
renderPage();
|
|
setupEventListeners();
|
|
});
|
|
|
|
// 设置事件监听器
|
|
function setupEventListeners() {
|
|
// 搜索框事件
|
|
document.getElementById('searchInput').addEventListener('input', function(e) {
|
|
searchKeyword = e.target.value.toLowerCase();
|
|
renderClasses();
|
|
});
|
|
|
|
// 筛选按钮事件
|
|
document.querySelectorAll('.filter-btn').forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active', 'bg-primary', 'text-white'));
|
|
this.classList.add('active', 'bg-primary', 'text-white');
|
|
currentFilter = this.dataset.filter;
|
|
renderClasses();
|
|
});
|
|
});
|
|
|
|
// 视图切换事件
|
|
document.getElementById('viewByDate').addEventListener('click', function() {
|
|
document.querySelectorAll('#filterSection button[id^="viewBy"]').forEach(b => b.classList.remove('bg-primary', 'text-white'));
|
|
document.querySelectorAll('#filterSection button[id^="viewBy"]').forEach(b => b.classList.add('bg-gray-100', 'hover:bg-gray-200'));
|
|
this.classList.remove('bg-gray-100', 'hover:bg-gray-200');
|
|
this.classList.add('bg-primary', 'text-white');
|
|
currentView = 'date';
|
|
renderClasses();
|
|
});
|
|
|
|
document.getElementById('viewByClass').addEventListener('click', function() {
|
|
document.querySelectorAll('#filterSection button[id^="viewBy"]').forEach(b => b.classList.remove('bg-primary', 'text-white'));
|
|
document.querySelectorAll('#filterSection button[id^="viewBy"]').forEach(b => b.classList.add('bg-gray-100', 'hover:bg-gray-200'));
|
|
this.classList.remove('bg-gray-100', 'hover:bg-gray-200');
|
|
this.classList.add('bg-primary', 'text-white');
|
|
currentView = 'class';
|
|
renderClasses();
|
|
});
|
|
|
|
// 导出按钮事件
|
|
document.getElementById('exportBtn').addEventListener('click', exportToExcel);
|
|
}
|
|
|
|
// 渲染整个页面
|
|
function renderPage() {
|
|
updateStats();
|
|
renderClasses();
|
|
}
|
|
|
|
// 更新统计信息
|
|
function updateStats() {
|
|
if (!scheduleData || scheduleData.length === 0) {
|
|
document.getElementById('totalDays').textContent = '0';
|
|
document.getElementById('totalClasses').textContent = '0';
|
|
document.getElementById('totalStudents').textContent = '0';
|
|
document.getElementById('presentStudents').textContent = '0';
|
|
document.getElementById('leaveStudents').textContent = '0';
|
|
return;
|
|
}
|
|
|
|
const totalDays = scheduleData.length;
|
|
let totalClasses = 0;
|
|
let totalStudents = 0;
|
|
let presentStudents = 0;
|
|
let leaveStudents = 0;
|
|
|
|
scheduleData.forEach(dayData => {
|
|
if (dayData.items) {
|
|
totalClasses += dayData.items.length;
|
|
dayData.items.forEach(cls => {
|
|
if (cls.students) {
|
|
totalStudents += cls.students.length;
|
|
cls.students.forEach(student => {
|
|
const status = student.attendance_status || '';
|
|
if (status.includes('出勤') || status === '✅ 出勤') {
|
|
presentStudents++;
|
|
} else if (status.includes('请假') || status === '⏸ 请假') {
|
|
leaveStudents++;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
document.getElementById('totalDays').textContent = totalDays;
|
|
document.getElementById('totalClasses').textContent = totalClasses;
|
|
document.getElementById('totalStudents').textContent = totalStudents;
|
|
document.getElementById('presentStudents').textContent = presentStudents;
|
|
document.getElementById('leaveStudents').textContent = leaveStudents;
|
|
|
|
// 如果没有数据,显示空状态
|
|
if (totalClasses === 0) {
|
|
document.getElementById('emptyState').style.display = 'block';
|
|
document.getElementById('filterSection').style.display = 'none';
|
|
} else {
|
|
document.getElementById('emptyState').style.display = 'none';
|
|
document.getElementById('filterSection').style.display = 'block';
|
|
}
|
|
}
|
|
|
|
// 获取所有课程的打平数据
|
|
function getAllFlattenClasses() {
|
|
const allClasses = [];
|
|
scheduleData.forEach(dayData => {
|
|
if (dayData.items) {
|
|
dayData.items.forEach(cls => {
|
|
allClasses.push({
|
|
...cls,
|
|
teaching_date: dayData.teaching_date
|
|
});
|
|
});
|
|
}
|
|
});
|
|
return allClasses;
|
|
}
|
|
|
|
// 渲染课程列表
|
|
function renderClasses() {
|
|
const container = document.getElementById('classesContainer');
|
|
const allClasses = getAllFlattenClasses();
|
|
|
|
if (allClasses.length === 0) {
|
|
container.innerHTML = `
|
|
<div id="emptyState" class="text-center py-16">
|
|
<div class="w-24 h-24 bg-gradient-to-br from-orange-100 to-orange-200 rounded-2xl flex items-center justify-center mx-auto mb-4 shadow-lg shadow-orange-100">
|
|
<i class="fa fa-calendar-o text-4xl text-orange-400"></i>
|
|
</div>
|
|
<h3 class="text-lg font-semibold text-gray-700 mb-2">暂无课程安排</h3>
|
|
<p class="text-gray-500">选择其他日期范围查看日程</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
// 过滤和搜索课程
|
|
const filteredClasses = allClasses.filter(cls => {
|
|
// 搜索匹配:日期、班级名称、学生姓名
|
|
const matchSearch =
|
|
searchKeyword === '' ||
|
|
cls.teaching_date.includes(searchKeyword) ||
|
|
cls.class_name.toLowerCase().includes(searchKeyword) ||
|
|
(cls.students && cls.students.some(s => s.student_name.toLowerCase().includes(searchKeyword)));
|
|
|
|
return matchSearch;
|
|
});
|
|
|
|
if (filteredClasses.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="text-center py-16">
|
|
<div class="w-24 h-24 bg-gradient-to-br from-orange-100 to-orange-200 rounded-2xl flex items-center justify-center mx-auto mb-4 shadow-lg shadow-orange-100">
|
|
<i class="fa fa-search text-4xl text-orange-400"></i>
|
|
</div>
|
|
<h3 class="text-lg font-semibold text-gray-700 mb-2">没有找到匹配的结果</h3>
|
|
<p class="text-gray-500">请尝试其他搜索关键词</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
|
|
if (currentView === 'date') {
|
|
// 按日期分组
|
|
const classesByDate = {};
|
|
filteredClasses.forEach(cls => {
|
|
if (!classesByDate[cls.teaching_date]) {
|
|
classesByDate[cls.teaching_date] = [];
|
|
}
|
|
classesByDate[cls.teaching_date].push(cls);
|
|
});
|
|
|
|
// 按日期排序
|
|
const sortedDates = Object.keys(classesByDate).sort();
|
|
|
|
sortedDates.forEach((date, dateIndex) => {
|
|
const dayClasses = classesByDate[date].sort((a, b) => a.start_time.localeCompare(b.start_time));
|
|
|
|
// 计算当天的统计
|
|
let dayTotalStudents = 0;
|
|
let dayPresent = 0;
|
|
let dayLeave = 0;
|
|
dayClasses.forEach(cls => {
|
|
if (cls.students) {
|
|
dayTotalStudents += cls.students.length;
|
|
cls.students.forEach(student => {
|
|
const status = student.attendance_status || '';
|
|
if (status.includes('出勤') || status === '✅ 出勤') dayPresent++;
|
|
else if (status.includes('请假') || status === '⏸ 请假') dayLeave++;
|
|
});
|
|
}
|
|
});
|
|
|
|
// 格式化日期,显示星期
|
|
const dateObj = new Date(date);
|
|
const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
|
|
const weekDay = weekDays[dateObj.getDay()];
|
|
const formattedDate = `${date} (${weekDay})`;
|
|
|
|
html += `
|
|
<div class="date-group animate-fade-in" style="animation-delay: ${dateIndex * 0.1}s">
|
|
<!-- 日期分组头部 -->
|
|
<div class="date-group-header rounded-2xl p-4 mb-4 flex items-center justify-between border border-orange-100 shadow-sm">
|
|
<div class="flex items-center gap-3">
|
|
<h2 class="text-lg font-bold text-gray-800">${formattedDate}</h2>
|
|
<span class="px-2 py-1 bg-gradient-to-r from-orange-100 to-amber-100 text-orange-600 rounded-full text-xs font-semibold border border-orange-200">
|
|
${dayClasses.length} 节课
|
|
</span>
|
|
<span class="px-2 py-1 bg-gray-100 text-gray-600 rounded-full text-xs font-medium">
|
|
${dayTotalStudents} 人次
|
|
</span>
|
|
</div>
|
|
<div class="flex items-center gap-3 text-sm">
|
|
<div class="flex items-center gap-1">
|
|
<span class="w-2 h-2 bg-green-400 rounded-full"></span>
|
|
<span class="text-green-600 font-medium">${dayPresent}</span>
|
|
</div>
|
|
<div class="flex items-center gap-1">
|
|
<span class="w-2 h-2 bg-amber-400 rounded-full"></span>
|
|
<span class="text-amber-600 font-medium">${dayLeave}</span>
|
|
</div>
|
|
<div class="flex items-center gap-1">
|
|
<span class="w-2 h-2 bg-gray-300 rounded-full"></span>
|
|
<span class="text-gray-500">${dayTotalStudents - dayPresent - dayLeave}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- 当天的课程列表 -->
|
|
<div class="space-y-4 pl-2 border-l-2 border-orange-200">
|
|
`;
|
|
|
|
// 渲染当天的课程
|
|
dayClasses.forEach((cls, classIndex) => {
|
|
html += renderClassCard(cls, classIndex);
|
|
});
|
|
|
|
html += `
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
} else {
|
|
// 按班级分组
|
|
const classesByClassName = {};
|
|
filteredClasses.forEach(cls => {
|
|
if (!classesByClassName[cls.class_name]) {
|
|
classesByClassName[cls.class_name] = [];
|
|
}
|
|
classesByClassName[cls.class_name].push(cls);
|
|
});
|
|
|
|
// 按班级名称排序
|
|
const sortedClassNames = Object.keys(classesByClassName).sort();
|
|
|
|
sortedClassNames.forEach((className, classIndex) => {
|
|
const classSessions = classesByClassName[className].sort((a, b) => a.teaching_date.localeCompare(b.teaching_date));
|
|
|
|
html += `
|
|
<div class="class-group animate-fade-in" style="animation-delay: ${classIndex * 0.1}s">
|
|
<!-- 班级分组头部 -->
|
|
<div class="date-group-header rounded-2xl p-4 mb-4 flex items-center justify-between border border-orange-100 shadow-sm">
|
|
<div class="flex items-center gap-3">
|
|
<h2 class="text-lg font-bold text-gray-800">${className}</h2>
|
|
<span class="px-2 py-1 bg-gradient-to-r from-orange-100 to-amber-100 text-orange-600 rounded-full text-xs font-semibold border border-orange-200">
|
|
${classSessions.length} 次课
|
|
</span>
|
|
</div>
|
|
<div class="text-sm text-gray-600">
|
|
${classSessions[0].teaching_date} 至 ${classSessions[classSessions.length - 1].teaching_date}
|
|
</div>
|
|
</div>
|
|
<!-- 该班级的所有课程 -->
|
|
<div class="space-y-4 pl-2 border-l-2 border-orange-200">
|
|
`;
|
|
|
|
// 渲染该班级的每一次课
|
|
classSessions.forEach((cls, sessionIndex) => {
|
|
html += renderClassCard(cls, sessionIndex, true);
|
|
});
|
|
|
|
html += `
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
}
|
|
|
|
container.innerHTML = html;
|
|
|
|
// 添加卡片展开/收起事件
|
|
document.querySelectorAll('.class-header').forEach(header => {
|
|
header.addEventListener('click', function() {
|
|
const content = this.nextElementSibling;
|
|
const icon = this.querySelector('.expand-icon');
|
|
|
|
if (content.classList.contains('hidden')) {
|
|
content.classList.remove('hidden');
|
|
icon.classList.add('rotate-180');
|
|
} else {
|
|
content.classList.add('hidden');
|
|
icon.classList.remove('rotate-180');
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// 渲染单个课程卡片
|
|
function renderClassCard(cls, index, showDate = false) {
|
|
// 计算班级出勤统计
|
|
let presentCount = 0;
|
|
let leaveCount = 0;
|
|
let absentCount = 0;
|
|
let totalCount = cls.students ? cls.students.length : 0;
|
|
|
|
if (cls.students) {
|
|
cls.students.forEach(student => {
|
|
const status = student.attendance_status || '';
|
|
if (status.includes('出勤') || status === '✅ 出勤') presentCount++;
|
|
else if (status.includes('请假') || status === '⏸ 请假') leaveCount++;
|
|
else if (status.includes('缺勤') || status === '❌ 缺勤') absentCount++;
|
|
});
|
|
}
|
|
|
|
// 过滤学生列表
|
|
let filteredStudents = cls.students || [];
|
|
if (currentFilter !== 'all') {
|
|
filteredStudents = filteredStudents.filter(student => {
|
|
const status = student.attendance_status || '';
|
|
if (currentFilter === 'present') return status.includes('出勤') || status === '✅ 出勤';
|
|
if (currentFilter === 'leave') return status.includes('请假') || status === '⏸ 请假';
|
|
if (currentFilter === 'absent') return status.includes('缺勤') || status === '❌ 缺勤';
|
|
return true;
|
|
});
|
|
}
|
|
|
|
// 搜索学生
|
|
if (searchKeyword) {
|
|
filteredStudents = filteredStudents.filter(s =>
|
|
s.student_name.toLowerCase().includes(searchKeyword)
|
|
);
|
|
}
|
|
|
|
// 生成学生表格HTML
|
|
const studentsTableHTML = filteredStudents.length > 0 ? `
|
|
<div class="overflow-x-auto mt-4">
|
|
<table class="w-full text-sm">
|
|
<thead>
|
|
<tr class="border-b border-orange-100">
|
|
<th class="text-left py-3 px-4 font-semibold text-gray-600">序号</th>
|
|
<th class="text-left py-3 px-4 font-semibold text-gray-600">学生姓名</th>
|
|
<th class="text-left py-3 px-4 font-semibold text-gray-600">出勤状态</th>
|
|
<th class="text-left py-3 px-4 font-semibold text-gray-600">备注</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${filteredStudents.map((student, sIndex) => {
|
|
let statusClass = '';
|
|
let statusText = student.attendance_status || '✅ 出勤';
|
|
|
|
if (statusText.includes('出勤') || statusText === '✅ 出勤') {
|
|
statusClass = 'text-green-600 bg-green-50 border border-green-100';
|
|
} else if (statusText.includes('请假') || statusText === '⏸ 请假') {
|
|
statusClass = 'text-amber-600 bg-amber-50 border border-amber-100';
|
|
} else if (statusText.includes('缺勤') || statusText === '❌ 缺勤') {
|
|
statusClass = 'text-red-500 bg-red-50 border border-red-100';
|
|
}
|
|
|
|
return `
|
|
<tr class="border-b border-orange-50 hover:bg-orange-50/30 transition-colors">
|
|
<td class="py-3 px-4 text-gray-500">${sIndex + 1}</td>
|
|
<td class="py-3 px-4 font-semibold text-gray-800">${student.student_name || '-'}</td>
|
|
<td class="py-3 px-4">
|
|
<span class="px-2.5 py-1 rounded-full text-xs font-semibold ${statusClass}">
|
|
${statusText}
|
|
</span>
|
|
</td>
|
|
<td class="py-3 px-4 text-gray-500">${student.remark || '-'}</td>
|
|
</tr>
|
|
`;
|
|
}).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
` : `
|
|
<div class="text-center py-8 text-gray-500">
|
|
<i class="fa fa-info-circle mr-2 text-orange-400"></i>没有匹配的学生数据
|
|
</div>
|
|
`;
|
|
|
|
return `
|
|
<div class="glass-card rounded-2xl overflow-hidden card-shadow card-hover">
|
|
<!-- 课程卡片头部 -->
|
|
<div class="p-6 cursor-pointer class-header" data-class-id="${cls.class_id || index}">
|
|
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
|
<div class="flex-1">
|
|
<div class="flex items-center gap-3 mb-2">
|
|
<h3 class="text-lg font-bold text-gray-800">${cls.class_name || '未命名班级'}</h3>
|
|
${showDate ? `
|
|
<span class="px-2 py-1 bg-gradient-to-r from-blue-50 to-indigo-50 text-blue-600 rounded-full text-xs font-semibold border border-blue-100">
|
|
${cls.teaching_date}
|
|
</span>
|
|
` : ''}
|
|
<span class="px-2.5 py-1 bg-gradient-to-r from-orange-100 to-amber-100 text-orange-600 rounded-full text-xs font-semibold border border-orange-200">
|
|
${totalCount}人
|
|
</span>
|
|
</div>
|
|
<div class="flex flex-wrap items-center gap-4 text-sm text-gray-600">
|
|
<div class="flex items-center gap-1.5">
|
|
<i class="fa fa-clock-o text-orange-300"></i>
|
|
<span>${cls.start_time || '-'} - ${cls.end_time || '-'}</span>
|
|
</div>
|
|
<div class="flex items-center gap-1.5">
|
|
<i class="fa fa-user text-orange-300"></i>
|
|
<span>授课老师:${cls.teacher_name || '-'}</span>
|
|
</div>
|
|
<div class="flex items-center gap-1.5">
|
|
<i class="fa fa-map-marker text-orange-300"></i>
|
|
<span>教室:${cls.classroom || '-'}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-4">
|
|
<!-- 出勤统计 -->
|
|
<div class="flex items-center gap-3 bg-gray-50 rounded-xl px-3 py-2">
|
|
<div class="flex items-center gap-1 text-sm">
|
|
<span class="w-2 h-2 bg-green-400 rounded-full"></span>
|
|
<span class="text-green-600 font-semibold">${presentCount}</span>
|
|
</div>
|
|
<div class="flex items-center gap-1 text-sm">
|
|
<span class="w-2 h-2 bg-amber-400 rounded-full"></span>
|
|
<span class="text-amber-600 font-semibold">${leaveCount}</span>
|
|
</div>
|
|
<div class="flex items-center gap-1 text-sm">
|
|
<span class="w-2 h-2 bg-red-400 rounded-full"></span>
|
|
<span class="text-red-500 font-semibold">${absentCount}</span>
|
|
</div>
|
|
</div>
|
|
<!-- 展开/收起图标 -->
|
|
<div class="w-8 h-8 bg-orange-50 rounded-full flex items-center justify-center">
|
|
<i class="fa fa-chevron-down text-orange-400 transition-transform transform rotate-0 expand-icon"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- 学生列表内容(默认隐藏) -->
|
|
<div class="class-content border-t border-orange-100 px-6 pb-6 hidden">
|
|
${studentsTableHTML}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// 导出到Excel
|
|
function exportToExcel() {
|
|
const allClasses = getAllFlattenClasses();
|
|
|
|
if (allClasses.length === 0) {
|
|
alert('没有数据可以导出');
|
|
return;
|
|
}
|
|
|
|
// 准备导出数据
|
|
const exportData = [];
|
|
allClasses.forEach(cls => {
|
|
if (cls.students) {
|
|
cls.students.forEach(student => {
|
|
exportData.push({
|
|
'日期': cls.teaching_date,
|
|
'班级名称': cls.class_name || '-',
|
|
'上课时间': `${cls.start_time || '-'} - ${cls.end_time || '-'}`,
|
|
'授课老师': cls.teacher_name || '-',
|
|
'教室': cls.classroom || '-',
|
|
'学生姓名': student.student_name || '-',
|
|
'出勤状态': student.attendance_status || '✅ 出勤',
|
|
'备注': student.remark || '-'
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
if (exportData.length === 0) {
|
|
alert('没有学生数据可以导出');
|
|
return;
|
|
}
|
|
|
|
// 创建工作簿
|
|
const ws = XLSX.utils.json_to_sheet(exportData);
|
|
const wb = XLSX.utils.book_new();
|
|
XLSX.utils.book_append_sheet(wb, ws, "教学日程");
|
|
|
|
// 设置列宽
|
|
const wscols = [
|
|
{wch: 12}, // 日期
|
|
{wch: 20}, // 班级名称
|
|
{wch: 18}, // 上课时间
|
|
{wch: 12}, // 授课老师
|
|
{wch: 10}, // 教室
|
|
{wch: 12}, // 学生姓名
|
|
{wch: 10}, // 出勤状态
|
|
{wch: 20}, // 备注
|
|
];
|
|
ws['!cols'] = wscols;
|
|
|
|
// 导出文件
|
|
const dateRange = document.title.replace('教学日程查询 - ', '');
|
|
XLSX.writeFile(wb, `教学日程_${dateRange}.xlsx`);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|