feat: v1.4.0 - 典型方案采纳、推荐方案列表、审计字段、导航优化

- 添加典型方案采纳功能 (POST /api/plans/<id>/adopt)
- 添加推荐方案列表 (GET /api/students/<id>/recommended-plans)
- PracticePlan 新增 created_by/updated_by/updated_at 审计字段
- 方案编辑/详情页导航优化 (bfcache 处理、pageshow 事件)
- 方案列表支持删除功能
- 学员列表'暂无方案/问题'样式统一
- 更新文档:问题文件已废弃(迁移到数据库)
- 更新部署脚本和验证清单
This commit is contained in:
hmo
2026-04-27 02:01:22 +08:00
parent 6abdd49c04
commit e50a9207b4
20 changed files with 873 additions and 88 deletions
+51 -5
View File
@@ -389,12 +389,49 @@ const problemList = {{ problem_list | tojson }};
const severityLevels = {{ severity_levels | tojson }};
const practiceTimeOptions = {{ practice_time_options | tojson }};
// 学员列表筛选状态管理
const STUDENT_FILTER_KEY = 'index_student_filters';
function saveStudentFilterState() {
const state = {
classId: document.getElementById('classFilter').value,
name: document.getElementById('nameFilter').value,
mineActive: document.getElementById('mineStudentFilterBtn').classList.contains('active')
};
sessionStorage.setItem(STUDENT_FILTER_KEY, JSON.stringify(state));
}
function restoreStudentFilterState() {
const saved = sessionStorage.getItem(STUDENT_FILTER_KEY);
if (!saved) return;
try {
const state = JSON.parse(saved);
if (state.classId) document.getElementById('classFilter').value = state.classId;
if (state.name) document.getElementById('nameFilter').value = state.name;
const btn = document.getElementById('mineStudentFilterBtn');
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');
}
}
saveStudentFilterState();
} catch (e) {
console.error('恢复学员筛选状态失败', e);
}
}
// 页面初始化(base.html 统一登录检查后调用)
window.pageInit = function(data) {
loadAiTemplates();
loadReportTemplates();
loadClassFilter();
loadStudents();
loadClassFilter().then(() => {
restoreStudentFilterState();
loadStudents();
});
initProblemCheckboxes();
// 检查 URL 参数,自动打开学员详情
@@ -481,6 +518,8 @@ function importStudents(input) {
// 加载学员列表
async function loadStudents() {
saveStudentFilterState();
const classId = document.getElementById('classFilter').value;
const name = document.getElementById('nameFilter').value;
const mineFilter = document.getElementById('mineStudentFilterBtn').classList.contains('active');
@@ -552,10 +591,17 @@ function renderStudentList(students) {
} else {
problemText = s.problem_names.join('、');
}
} else {
} else if (s.problem_count > 0) {
problemText = `${s.problem_count} 个问题`;
} else {
problemText = '<span class="text-muted">暂无问题</span>';
}
// 构建方案数量显示(样式与问题一致)
const planCount = s.plan_count > 0;
const planBadgeText = planCount ? `${s.plan_count} 个方案` : '暂无方案';
const planBadgeClass = planCount ? 'bg-primary' : 'bg-light text-muted';
html += `
<div class="col-md-4 col-sm-6 mb-3">
<div class="card">
@@ -564,8 +610,8 @@ function renderStudentList(students) {
<h5 class="card-title">${s.name}</h5>
<p class="card-text text-muted small">${s.wechat_nickname || ''} ${s.phone ? '| ' + s.phone : ''}</p>
<span class="badge bg-info">${s.practice_time}</span>
<span class="badge bg-secondary">${problemText}</span>
<span class="badge bg-primary">${s.plan_count} 个方案</span>
<span class="badge ${s.problem_count > 0 ? 'bg-secondary' : 'bg-light text-muted'}">${problemText}</span>
<span class="badge ${planBadgeClass}">${planBadgeText}</span>
${s.goal_count > 0 ? `<span class="badge bg-success">${s.goal_count}个目标(${s.completed_goal_count}已达成)</span>` : ''}
</div>
</a>