340 lines
12 KiB
HTML
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 %} |