feat: 添加学员详情/方案编辑/方案列表新页面
- student.html: 学员详情页,支持编辑/添加/删除问题 - plan_edit.html: 方案编辑页 - plans.html: 方案列表页 - home.html: 首页
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}方案管理 - 钢琴练习方案系统{% endblock %}
|
||||
|
||||
{% block page_css %}
|
||||
<style>
|
||||
.plan-table th { white-space: nowrap; }
|
||||
.plan-problem-text { font-weight: 600; color: #2c3e50; }
|
||||
.plan-meta-text { color: #95a5a6; font-size: 0.85rem; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h4><i class="bi bi-clipboard-check"></i> 方案管理</h4>
|
||||
</div>
|
||||
|
||||
<!-- 筛选器 -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">班级</label>
|
||||
<select class="form-select" id="filterClass" onchange="loadPlans()">
|
||||
<option value="">全部班级</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">问题</label>
|
||||
<select class="form-select" id="filterProblem" onchange="loadPlans()">
|
||||
<option value="">全部问题</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">模板</label>
|
||||
<select class="form-select" id="filterTemplate" onchange="loadPlans()">
|
||||
<option value="">全部模板</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">典型方案</label>
|
||||
<select class="form-select" id="filterTypical" onchange="loadPlans()">
|
||||
<option value="">全部</option>
|
||||
<option value="true">仅典型</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">学员姓名</label>
|
||||
<input type="text" class="form-control" id="filterStudentName" placeholder="模糊搜索..." oninput="debounceLoad()">
|
||||
</div>
|
||||
<div class="col-md-1 d-flex align-items-end">
|
||||
<button class="btn btn-outline-secondary w-100" onclick="clearFilters()">清空</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 方案列表 -->
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div id="plansContainer">
|
||||
<div class="text-center text-muted py-5">
|
||||
<i class="bi bi-hourglass-split fs-1"></i>
|
||||
<p class="mt-2">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// 防抖定时器
|
||||
let debounceTimer = null;
|
||||
|
||||
function debounceLoad() {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(loadPlans, 300);
|
||||
}
|
||||
|
||||
// 页面初始化
|
||||
window.pageInit = function() {
|
||||
loadFilters();
|
||||
loadPlans();
|
||||
};
|
||||
|
||||
// 加载筛选器选项
|
||||
async function loadFilters() {
|
||||
// 加载班级
|
||||
try {
|
||||
const resp = await fetch('/api/classes');
|
||||
const classes = await resp.json();
|
||||
const classSelect = document.getElementById('filterClass');
|
||||
classes.forEach(c => {
|
||||
classSelect.innerHTML += `<option value="${c.id}">${c.name}</option>`;
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('加载班级失败', e);
|
||||
}
|
||||
|
||||
// 加载问题
|
||||
try {
|
||||
const resp = await fetch('/api/problems');
|
||||
const problems = await resp.json();
|
||||
const problemSelect = document.getElementById('filterProblem');
|
||||
problems.forEach(p => {
|
||||
problemSelect.innerHTML += `<option value="${p.id}">${p.name}</option>`;
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('加载问题失败', e);
|
||||
}
|
||||
|
||||
// 加载模板
|
||||
try {
|
||||
const resp = await fetch('/templates/templates');
|
||||
const templates = await resp.json();
|
||||
const templateSelect = document.getElementById('filterTemplate');
|
||||
templates.filter(t => t.type === 'ai_prompt').forEach(t => {
|
||||
templateSelect.innerHTML += `<option value="${t.id}">${t.name}</option>`;
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('加载模板失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载方案列表
|
||||
async function loadPlans() {
|
||||
const container = document.getElementById('plansContainer');
|
||||
container.innerHTML = '<div class="text-center text-muted py-5"><i class="bi bi-hourglass fs-4"></i><p class="mt-2">加载中...</p></div>';
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
const classId = document.getElementById('filterClass').value;
|
||||
if (classId) params.append('class_id', classId);
|
||||
|
||||
const templateId = document.getElementById('filterTemplate').value;
|
||||
if (templateId) params.append('template_id', templateId);
|
||||
|
||||
const isTypical = document.getElementById('filterTypical').value;
|
||||
if (isTypical === 'true') params.append('is_typical', 'true');
|
||||
|
||||
const studentName = document.getElementById('filterStudentName').value.trim();
|
||||
if (studentName) params.append('student_name', studentName);
|
||||
|
||||
const problemId = document.getElementById('filterProblem').value;
|
||||
if (problemId) {
|
||||
params.append('problem_ids', parseInt(problemId));
|
||||
}
|
||||
|
||||
const resp = await fetch(`/api/plans?${params}`);
|
||||
const plans = await resp.json();
|
||||
|
||||
if (plans.length === 0) {
|
||||
container.innerHTML = '<div class="text-center text-muted py-5"><i class="bi bi-inbox fs-4"></i><p class="mt-2">暂无方案</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = `
|
||||
<table class="table table-hover table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>学员</th>
|
||||
<th>班级</th>
|
||||
<th>问题</th>
|
||||
<th>模板</th>
|
||||
<th>典型</th>
|
||||
<th>时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
|
||||
plans.forEach(p => {
|
||||
const problems = (p.problem_names || []).slice(0, 3).join('、');
|
||||
const moreProblems = (p.problem_names || []).length > 3 ? `等${p.problem_names.length}个` : '';
|
||||
const template = p.template_name || '无模板';
|
||||
const studentName = p.student_name || '未知';
|
||||
const className = p.class_name || '-';
|
||||
const isTypical = p.is_typical ? '✓' : '';
|
||||
|
||||
html += `
|
||||
<tr>
|
||||
<td><strong>${studentName}</strong></td>
|
||||
<td>${className}</td>
|
||||
<td><span class="plan-problem-text">${problems}${moreProblems}</span></td>
|
||||
<td class="text-muted small">${template}</td>
|
||||
<td class="text-center">${isTypical ? '<span class="text-warning">★</span>' : ''}</td>
|
||||
<td class="text-muted small">${p.created_at || ''}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-primary" onclick="viewPlan(${p.id})">查看</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
html += '</tbody></table>';
|
||||
container.innerHTML = html;
|
||||
|
||||
} catch (e) {
|
||||
container.innerHTML = `<div class="text-center text-danger py-5"><i class="bi bi-exclamation-circle fs-4"></i><p class="mt-2">加载失败: ${e.message}</p></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 清空筛选器
|
||||
function clearFilters() {
|
||||
document.getElementById('filterClass').value = '';
|
||||
document.getElementById('filterProblem').value = '';
|
||||
document.getElementById('filterTemplate').value = '';
|
||||
document.getElementById('filterTypical').value = '';
|
||||
document.getElementById('filterStudentName').value = '';
|
||||
loadPlans();
|
||||
}
|
||||
|
||||
// 查看方案 - 跳转到方案详情页
|
||||
function viewPlan(planId) {
|
||||
window.location.href = `/plan/${planId}`;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user