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

340 lines
12 KiB
HTML

{% 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-1 d-flex align-items-end">
<button class="btn btn-primary active w-100" id="minePlansBtn" onclick="toggleMinePlans()">我的</button>
</div>
<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-2">
<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;
const STORAGE_KEY = 'plans_filters';
function debounceLoad() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(loadPlans, 300);
}
// 保存筛选状态到 sessionStorage
function saveFilterState() {
const state = {
classId: document.getElementById('filterClass').value,
templateId: document.getElementById('filterTemplate').value,
isTypical: document.getElementById('filterTypical').value,
studentName: document.getElementById('filterStudentName').value,
problemId: document.getElementById('filterProblem').value,
mineActive: document.getElementById('minePlansBtn')?.classList.contains('active')
};
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}
// 恢复筛选状态
function restoreFilterState() {
const saved = sessionStorage.getItem(STORAGE_KEY);
if (!saved) return;
try {
const state = JSON.parse(saved);
if (state.classId) document.getElementById('filterClass').value = state.classId;
if (state.templateId) document.getElementById('filterTemplate').value = state.templateId;
if (state.isTypical) document.getElementById('filterTypical').value = state.isTypical;
if (state.studentName) document.getElementById('filterStudentName').value = state.studentName;
if (state.problemId) document.getElementById('filterProblem').value = state.problemId;
const btn = document.getElementById('minePlansBtn');
if (btn) {
if (state.mineActive) {
btn.classList.add('active', 'btn-primary');
btn.classList.remove('btn-outline-secondary');
} else {
btn.classList.remove('active', 'btn-primary');
btn.classList.add('btn-outline-secondary');
}
}
// 确保状态同步保存
saveFilterState();
} catch (e) {
console.error('恢复筛选状态失败', e);
}
}
// 页面初始化
window.pageInit = function() {
checkAndRefresh();
};
// 检查是否需要刷新(从详情页返回)
function checkAndRefresh() {
const needsRefresh = sessionStorage.getItem('plans_needs_refresh') === 'true';
if (needsRefresh) {
sessionStorage.removeItem('plans_needs_refresh');
loadFilters().then(() => {
restoreFilterState();
loadPlans(true);
});
} else {
loadFilters().then(() => {
restoreFilterState();
loadPlans(false);
});
}
}
// pageshow 事件处理 bfcache 恢复的情况
window.addEventListener('pageshow', function(event) {
if (event.persisted) {
// 页面从 bfcache 恢复,需要重新检查刷新标记
checkAndRefresh();
}
});
// 加载筛选器选项
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.no} - ${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() {
// 保存当前筛选状态
saveFilterState();
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 mineBtn = document.getElementById('minePlansBtn');
if (mineBtn && mineBtn.classList.contains('active')) {
params.append('mine', 'true');
}
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 details = p.problem_details || [];
const problemText = details.slice(0, 3).map(d => `${d.name}[${d.level}/${d.severity}]`).join('、');
const moreProblems = details.length > 3 ? `${details.length}` : '';
const template = p.template_name || '无模板';
const studentName = p.student_name || '未知';
const className = p.class_name || '-';
const isTypical = p.is_typical ? '✓' : '';
html += `
<tr>
<td><a href="/student/${p.student_id}" class="text-decoration-none"><strong>${studentName}</strong></a></td>
<td>${className}</td>
<td><span class="plan-problem-text">${problemText}${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>
<button class="btn btn-sm btn-outline-danger" onclick="deletePlan(${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 = '';
const mineBtn = document.getElementById('minePlansBtn');
if (mineBtn) {
mineBtn.classList.remove('active', 'btn-primary');
mineBtn.classList.add('btn-outline-secondary');
}
sessionStorage.removeItem(STORAGE_KEY);
loadPlans();
}
// 我的筛选
function toggleMinePlans() {
const btn = document.getElementById('minePlansBtn');
if (!btn) return;
btn.classList.toggle('active');
if (btn.classList.contains('active')) {
btn.classList.remove('btn-outline-secondary');
btn.classList.add('btn-primary');
} else {
btn.classList.remove('btn-primary');
btn.classList.add('btn-outline-secondary');
}
saveFilterState();
loadPlans();
}
// 查看方案 - 跳转到方案详情页
function viewPlan(planId) {
window.location.href = `/plan/${planId}`;
}
// 删除方案
async function deletePlan(planId) {
if (!confirm('确定删除该方案?删除后无法恢复。')) return;
try {
const resp = await fetch(`/api/plans/${planId}`, { method: 'DELETE' });
if (resp.ok) {
loadPlans(); // 刷新列表
} else {
const err = await resp.json();
alert('删除失败: ' + (err.error || '未知错误'));
}
} catch (e) {
alert('删除失败: ' + e.message);
}
}
</script>
{% endblock %}