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:
@@ -80,6 +80,20 @@
|
||||
<p class="text-muted">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 推荐方案区块 -->
|
||||
<div class="card mb-4 border-warning">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">📋 推荐方案</h5>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-outline-secondary active" id="filterAll" onclick="setRecommendedFilter('all')">全部</button>
|
||||
<button class="btn btn-outline-secondary" id="filterMine" onclick="setRecommendedFilter('mine')">我的</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body" id="recommendedPlanList">
|
||||
<p class="text-muted">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -359,6 +373,11 @@ const studentName = "{{ student.name }}";
|
||||
let problemModal, editProblemModal, generateModal, editStudentModal, assignGoalModal;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initPage();
|
||||
});
|
||||
|
||||
// 页面初始化
|
||||
function initPage() {
|
||||
problemModal = new bootstrap.Modal(document.getElementById('addProblemModal'));
|
||||
editProblemModal = new bootstrap.Modal(document.getElementById('editProblemModal'));
|
||||
generateModal = new bootstrap.Modal(document.getElementById('generatePlanModal'));
|
||||
@@ -372,10 +391,29 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
document.getElementById('editStudentPracticeTime').value = document.getElementById('detailPracticeTime').textContent;
|
||||
document.getElementById('editStudentNotes').value = document.getElementById('detailNotes').textContent === '无备注' ? '' : document.getElementById('detailNotes').textContent;
|
||||
|
||||
// 检查是否需要刷新推荐方案
|
||||
const needsRefreshRecommended = sessionStorage.getItem('needs_refresh_recommended') === 'true';
|
||||
if (needsRefreshRecommended) {
|
||||
sessionStorage.removeItem('needs_refresh_recommended');
|
||||
// 恢复推荐方案筛选状态
|
||||
const savedFilter = sessionStorage.getItem('recommended_filter') || 'all';
|
||||
loadRecommendedPlans(savedFilter);
|
||||
} else {
|
||||
loadRecommendedPlans('all');
|
||||
}
|
||||
|
||||
loadProblems();
|
||||
loadPlans();
|
||||
loadProblemOptions();
|
||||
loadStudentGoals();
|
||||
}
|
||||
|
||||
// pageshow 事件处理 bfcache 恢复的情况
|
||||
window.addEventListener('pageshow', function(event) {
|
||||
if (event.persisted) {
|
||||
// 页面从 bfcache 恢复,需要重新检查刷新标记
|
||||
initPage();
|
||||
}
|
||||
});
|
||||
|
||||
async function loadProblemOptions() {
|
||||
@@ -590,13 +628,17 @@ function renderTimeline(timeline) {
|
||||
</div>`;
|
||||
} else {
|
||||
const p = entry.plan;
|
||||
const adoptedFrom = p.adopted_from;
|
||||
const editInfo = p.updated_at ? `<span class="text-muted small">(于${p.updated_at}编辑)</span>` : '';
|
||||
return `
|
||||
<div class="d-flex align-items-start mb-2 p-2 border rounded ${p.is_typical ? 'border-warning bg-light' : ''}">
|
||||
<div class="flex-grow-1">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<a href="/plan/${p.id}" class="fw-bold text-decoration-none">${formatDate(entry.date)}</a>
|
||||
${editInfo}
|
||||
${p.is_typical ? '<span class="badge bg-warning text-dark ms-1">典型</span>' : ''}
|
||||
${adoptedFrom ? `<span class="badge bg-info ms-1">采纳自${escapeHtml(adoptedFrom.student_name)}的方案</span>` : ''}
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<a href="/plan/${p.id}" class="btn btn-sm btn-outline-primary" title="查看">
|
||||
@@ -682,6 +724,7 @@ async function saveAddProblem() {
|
||||
if (resp.ok) {
|
||||
problemModal.hide();
|
||||
loadProblems();
|
||||
loadRecommendedPlans(currentRecommendedFilter);
|
||||
} else {
|
||||
const err = await resp.json();
|
||||
alert('添加失败: ' + (err.error || '未知错误'));
|
||||
@@ -723,6 +766,7 @@ async function saveProblemEdit() {
|
||||
if (updateResp.ok) {
|
||||
editProblemModal.hide();
|
||||
loadProblems();
|
||||
loadRecommendedPlans(currentRecommendedFilter);
|
||||
} else {
|
||||
alert('更新失败');
|
||||
}
|
||||
@@ -739,6 +783,7 @@ async function deleteProblem(id) {
|
||||
});
|
||||
if (resp.ok) {
|
||||
loadProblems();
|
||||
loadRecommendedPlans(currentRecommendedFilter);
|
||||
} else {
|
||||
alert('删除失败');
|
||||
}
|
||||
@@ -970,6 +1015,84 @@ async function loadStudentGoals() {
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// ===== 推荐方案相关 =====
|
||||
let currentRecommendedFilter = 'all';
|
||||
|
||||
function setRecommendedFilter(filter) {
|
||||
currentRecommendedFilter = filter;
|
||||
sessionStorage.setItem('recommended_filter', filter);
|
||||
document.getElementById('filterAll').classList.toggle('active', filter === 'all');
|
||||
document.getElementById('filterMine').classList.toggle('active', filter === 'mine');
|
||||
loadRecommendedPlans(filter);
|
||||
}
|
||||
|
||||
async function loadRecommendedPlans(filter) {
|
||||
const container = document.getElementById('recommendedPlanList');
|
||||
container.innerHTML = '<p class="text-muted">加载中...</p>';
|
||||
|
||||
try {
|
||||
const url = `/api/students/${currentStudentId}/recommended-plans${filter === 'mine' ? '?mine=true' : ''}`;
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error('API error');
|
||||
const plans = await res.json();
|
||||
|
||||
if (plans.length === 0) {
|
||||
container.innerHTML = '<p class="text-muted">暂无匹配的推荐方案</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = plans.map(p => `
|
||||
<div class="d-flex justify-content-between align-items-start mb-2 p-2 border rounded bg-light">
|
||||
<div class="flex-grow-1">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<strong>${escapeHtml(p.student_name)}的方案</strong>
|
||||
<span class="badge bg-warning text-dark">典型</span>
|
||||
</div>
|
||||
<div class="small text-muted mt-1">
|
||||
问题匹配: ${p.matched_problems ? p.matched_problems.join(', ') : ''}
|
||||
<span class="text-info">(${p.matched_count || 0}个)</span>
|
||||
</div>
|
||||
<div class="small text-muted">
|
||||
${p.created_at ? '创建: ' + formatDate(p.created_at) : ''}
|
||||
${p.template_name ? ' | 模板: ' + p.template_name : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm ms-2">
|
||||
<a href="/plan/${p.id}" class="btn btn-outline-primary">查看</a>
|
||||
${p.can_adopt
|
||||
? `<button class="btn btn-success" onclick="adoptTypicalPlan(${p.id})">采纳</button>`
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} catch (e) {
|
||||
console.error('加载推荐方案失败', e);
|
||||
container.innerHTML = '<p class="text-danger">加载失败</p>';
|
||||
}
|
||||
}
|
||||
|
||||
async function adoptTypicalPlan(planId) {
|
||||
if (!confirm('确定采纳此典型方案?系统将复制该方案到当前学员。')) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/students/${currentStudentId}/plans/from-typical/${planId}`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'}
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
loadPlans(); // 刷新方案列表
|
||||
loadRecommendedPlans(currentRecommendedFilter); // 刷新推荐列表
|
||||
} else {
|
||||
const err = await res.json();
|
||||
alert('采纳失败: ' + (err.error || '未知错误'));
|
||||
}
|
||||
} catch (e) {
|
||||
alert('采纳失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return '';
|
||||
const d = new Date(dateStr);
|
||||
|
||||
Reference in New Issue
Block a user