Files
piano-plan/app/templates/classes.html
T

441 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}班级管理 - 钢琴练习方案系统{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<div class="d-flex align-items-center gap-3">
<h4><i class="bi bi-collection"></i> 班级管理</h4>
<select class="form-select form-select-sm" style="width:auto;" id="activeFilter" onchange="loadClasses()">
<option value="">全部班级</option>
<option value="true" selected>进行中</option>
<option value="false">已结束</option>
</select>
</div>
<button class="btn btn-primary" id="addClassBtn" style="display:none;">
<i class="bi bi-plus-circle"></i> 新增班级
</button>
</div>
<div class="card">
<div class="card-body">
<table class="table table-hover" id="classesTable">
<thead>
<tr>
<th>ID</th>
<th>班级名称</th>
<th>描述</th>
<th>进行中</th>
<th>学员数</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<style>
.modal-footer-with-top {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
padding: 1rem;
border-top: 1px solid #dee2e6;
background: #f8f9fa;
position: sticky;
bottom: 0;
}
</style>
<!-- 新增/编辑班级弹窗 -->
<div class="modal fade" id="classModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="classModalTitle">新增班级</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="classForm">
<input type="hidden" id="classId">
<div class="mb-3">
<label class="form-label">班级名称</label>
<input type="text" class="form-control" id="className" required>
</div>
<div class="mb-3">
<label class="form-label">描述</label>
<textarea class="form-control" id="classDesc" rows="2"></textarea>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="classActive" checked>
<label class="form-check-label" for="classActive">进行中</label>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="saveClassBtn">保存</button>
</div>
</div>
</div>
</div>
<!-- 班级学员弹窗 -->
<div class="modal fade" id="classStudentsModal" tabindex="-1">
<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"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<button type="button" class="btn btn-sm btn-success" id="assignStudentsBtn">分配学员</button>
</div>
<table class="table" id="classStudentsTable">
<thead>
<tr>
<th>姓名</th>
<th>手机</th>
<th>练习时间</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 分配学员弹窗 -->
<div class="modal fade" id="assignModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">分配学员到班级</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="studentCheckboxes"></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" id="confirmAssignBtn">确认分配</button>
</div>
</div>
</div>
</div>
<!-- 分配目标弹窗 -->
<div class="modal fade" id="classAssignGoalModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">分配目标 - <span id="classAssignGoalClassName"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">选择目标 *</label>
<select class="form-select" id="class-assign-goal-select"></select>
</div>
<div class="mb-3">
<label class="form-label">评估日期</label>
<div class="row g-2">
<div class="col-auto">
<select class="form-select" id="class-assign-assessment-days">
<option value="">指定天数</option>
<option value="15">15天后</option>
<option value="30">30天后</option>
<option value="60">60天后</option>
<option value="90" selected>90天后</option>
<option value="180">180天后</option>
</select>
</div>
<div class="col-auto">
<input type="date" class="form-control" id="class-assign-assessment-date">
</div>
</div>
</div>
<div class="mb-3">
<a class="text-decoration-none" data-bs-toggle="collapse" href="#classAssignMoreSettings" role="button">
更多设置 ▼
</a>
<div class="collapse" id="classAssignMoreSettings">
<div class="card card-body mt-2">
<div class="mb-3">
<label class="form-label">开始日期</label>
<input type="date" class="form-control" id="class-assign-start-date">
<small class="text-muted">默认立即开始</small>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer-with-top">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="confirm-class-assign-goal">分配</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
let currentUserRole = 'user';
let currentClassId = null;
let allStudents = [];
// Toast 通知函数
function showToast(message, isError = true) {
const toast = document.createElement('div');
toast.className = `toast-container position-fixed top-50 start-50 translate-middle`;
toast.style.zIndex = '9999';
toast.innerHTML = `
<div class="toast show" role="alert">
<div class="toast-header ${isError ? 'bg-danger' : 'bg-success'} text-white">
<strong class="me-auto">${isError ? '错误' : '成功'}</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast"></button>
</div>
<div class="toast-body">${message}</div>
</div>
`;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
}
window.pageInit = function(data) {
currentUserRole = data.role;
if (data.role === 'admin') {
const addClassBtn = document.getElementById('addClassBtn');
if (addClassBtn) addClassBtn.style.display = 'inline-block';
}
loadClasses();
};
// 加载班级列表
function loadClasses() {
const activeFilter = document.getElementById('activeFilter').value;
const url = activeFilter ? '/api/classes?active=' + activeFilter : '/api/classes';
fetch(url).then(r => r.json()).then(classes => {
const tbody = document.querySelector('#classesTable tbody');
const isAdmin = currentUserRole === 'admin';
tbody.innerHTML = classes.map(c => `
<tr>
<td>${c.id}</td>
<td>${c.name}</td>
<td>${c.description || '-'}</td>
<td>${c.active ? '<span class="badge bg-success">进行中</span>' : '<span class="badge bg-secondary">已结束</span>'}</td>
<td><a href="#" onclick="viewClassStudents(${c.id})">${c.student_count}</a></td>
<td>${c.created_at}</td>
<td>
<button type="button" class="btn btn-sm btn-success me-1" onclick="openAssignGoalModal(${c.id}, '${c.name}')">分配目标</button>
${isAdmin ? `<button type="button" class="btn btn-sm btn-primary me-1" onclick="editClass(${c.id}, '${c.name}', '${c.description || ''}', ${c.active})">编辑</button>
<button type="button" class="btn btn-sm btn-danger" onclick="deleteClass(${c.id})">删除</button>` : ''}
</td>
</tr>
`).join('');
});
}
// 保存班级
document.getElementById('saveClassBtn').onclick = () => {
const id = document.getElementById('classId').value;
const name = document.getElementById('className').value.trim();
const description = document.getElementById('classDesc').value;
const active = document.getElementById('classActive').checked;
if (!name) {
alert('请输入班级名称');
return;
}
const method = id ? 'PUT' : 'POST';
fetch('/api/classes' + (id ? '/' + id : ''), {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, description, active })
}).then(r => r.json()).then(data => {
if (data.error) {
showToast(data.error);
} else {
const modal = bootstrap.Modal.getInstance(document.getElementById('classModal'));
if (modal) modal.hide();
document.querySelectorAll('.modal-backdrop').forEach(el => el.remove());
document.body.classList.remove('modal-open');
loadClasses();
}
});
};
// 编辑班级
function editClass(id, name, desc, active) {
document.getElementById('classId').value = id;
document.getElementById('className').value = name;
document.getElementById('classDesc').value = desc;
document.getElementById('classActive').checked = active !== false;
document.getElementById('classModalTitle').textContent = '编辑班级';
new bootstrap.Modal(document.getElementById('classModal')).show();
}
// 删除班级
function deleteClass(id) {
if (!confirm('确定要删除该班级吗?学员不会被删除。')) return;
fetch('/api/classes/' + id, { method: 'DELETE' }).then(r => r.json()).then(data => {
if (data.error) {
showToast(data.error);
} else {
loadClasses();
}
});
}
// 查看班级学员
function viewClassStudents(classId) {
currentClassId = classId;
fetch('/api/classes/' + classId + '/students').then(r => r.json()).then(students => {
const tbody = document.querySelector('#classStudentsTable tbody');
tbody.innerHTML = students.length ? students.map(s => `
<tr><td>${s.name}</td><td>${s.phone || '-'}</td><td>${s.practice_time}</td></tr>
`).join('') : '<tr><td colspan="3" class="text-center">暂无学员</td></tr>';
new bootstrap.Modal(document.getElementById('classStudentsModal')).show();
});
}
// 分配学员
document.getElementById('assignStudentsBtn').onclick = () => {
fetch('/api/students').then(r => r.json()).then(students => {
allStudents = students;
const container = document.getElementById('studentCheckboxes');
container.innerHTML = students.map(s => `
<div class="form-check">
<input class="form-check-input" type="checkbox" value="${s.id}" ${s.class_id == currentClassId ? 'checked' : ''}>
<label class="form-check-label">${s.name} (${s.practice_time})</label>
</div>
`).join('');
new bootstrap.Modal(document.getElementById('assignModal')).show();
});
};
document.getElementById('confirmAssignBtn').onclick = () => {
const checked = Array.from(document.querySelectorAll('#studentCheckboxes input:checked')).map(c => parseInt(c.value));
fetch('/api/classes/' + currentClassId + '/assign', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ student_ids: checked })
}).then(r => r.json()).then(data => {
if (data.error) {
showToast(data.error);
} else {
const modal = bootstrap.Modal.getInstance(document.getElementById('assignModal'));
if (modal) modal.hide();
document.querySelectorAll('.modal-backdrop').forEach(el => el.remove());
document.body.classList.remove('modal-open');
loadClasses();
viewClassStudents(currentClassId);
}
});
};
// 新增班级按钮
document.getElementById('addClassBtn').onclick = () => {
document.getElementById('classId').value = '';
document.getElementById('className').value = '';
document.getElementById('classDesc').value = '';
document.getElementById('classActive').checked = true;
document.getElementById('classModalTitle').textContent = '新增班级';
new bootstrap.Modal(document.getElementById('classModal')).show();
};
// ========== 分配目标功能 ==========
let classAssignGoalModal;
document.getElementById('classAssignGoalModal').addEventListener('show.bs.modal', function () {
loadClassGoalOptions();
});
function openAssignGoalModal(classId, className) {
currentClassId = classId;
document.getElementById('classAssignGoalClassName').textContent = className;
// 重置表单
document.getElementById('class-assign-assessment-days').value = '';
document.getElementById('class-assign-assessment-date').value = '';
document.getElementById('class-assign-start-date').value = '';
classAssignGoalModal = new bootstrap.Modal(document.getElementById('classAssignGoalModal'));
classAssignGoalModal.show();
}
async function loadClassGoalOptions() {
const res = await fetch('/api/goals');
const goals = await res.json();
const select = document.getElementById('class-assign-goal-select');
select.innerHTML = goals.map(g => `<option value="${g.id}">${escapeHtml(g.name)} [${g.level || '入门'} - ${g.category || '综合'}]</option>`).join('');
// 设置默认开始日期为今天
document.getElementById('class-assign-start-date').value = new Date().toISOString().split('T')[0];
}
// 评估日期联动
document.getElementById('class-assign-assessment-days').addEventListener('change', function() {
const days = parseInt(this.value);
if (days) {
const d = new Date();
d.setDate(d.getDate() + days);
document.getElementById('class-assign-assessment-date').value = d.toISOString().split('T')[0];
}
});
document.getElementById('class-assign-assessment-date').addEventListener('change', function() {
if (this.value) {
document.getElementById('class-assign-assessment-days').value = '';
}
});
// 确认分配目标
document.getElementById('confirm-class-assign-goal').addEventListener('click', async () => {
const goalId = document.getElementById('class-assign-goal-select').value;
const assessmentDays = document.getElementById('class-assign-assessment-days').value;
const assessmentDate = document.getElementById('class-assign-assessment-date').value;
const startDate = document.getElementById('class-assign-start-date').value;
if (!goalId) { alert('请选择目标'); return; }
if (!assessmentDays && !assessmentDate) { alert('请选择评估方式'); return; }
// 弹出确认框
if (!confirm('将给班级所有学员分配此目标,确定吗?')) return;
const res = await fetch(`/api/classes/${currentClassId}/goals`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
goal_id: parseInt(goalId),
assessment_days: assessmentDays || null,
assessment_date: assessmentDate || null,
start_date: startDate || null,
start_now: !startDate
})
});
if (res.ok) {
const data = await res.json();
classAssignGoalModal.hide();
alert(data.message + (data.skipped_count ? `${data.skipped_count}个学员已分配此目标,跳过)` : ''));
} else {
const err = await res.json();
alert(err.error || '分配失败');
}
});
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
{% endblock %}