feat: 学员详情页改为学习历程时间线,显示目标启动/达成和方案生成记录

This commit is contained in:
hmo
2026-04-24 00:33:30 +08:00
parent a133f26fd5
commit 587aa79c16
+106 -16
View File
@@ -46,7 +46,7 @@
<div class="col-md-8">
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0"><i class="bi bi-list-check"></i> 问题记录</h6>
<h6 class="mb-0"><i class="bi bi-list-check"></i> 当前问题</h6>
<button class="btn btn-sm btn-primary" onclick="showAddProblemModal()">
<i class="bi bi-plus"></i> 添加问题
</button>
@@ -474,23 +474,112 @@ function renderProblemList(problems) {
`).join('');
}
// 学习历程时间线(替代原 loadPlans)
async function loadPlans() {
try {
const resp = await fetch(`/api/students/${currentStudentId}/plans`);
const plans = await resp.json();
renderPlanList(plans);
} catch (e) {
document.getElementById('planList').innerHTML = '<p class="text-danger">加载失败</p>';
}
await loadTimeline();
}
function renderPlanList(plans) {
// 学习历程时间线
async function loadTimeline() {
const [plansRes, goalsRes] = await Promise.all([
fetch(`/api/students/${currentStudentId}/plans`),
fetch(`/api/students/${currentStudentId}/goals`)
]);
const plans = await plansRes.json();
const goals = await goalsRes.json();
// 构建时间线条目
const timeline = [];
// 添加目标开始记录
goals.forEach(g => {
if (g.start_date) {
const startDate = new Date(g.start_date);
const endDate = g.assessment_date ? new Date(g.assessment_date) : null;
const days = endDate ? Math.ceil((endDate - startDate) / (1000*60*60*24)) : null;
timeline.push({
date: startDate,
type: 'goal_start',
goal: g,
days: days,
endDate: endDate
});
}
// 添加目标达成记录
if (g.achievement_date) {
timeline.push({
date: new Date(g.achievement_date),
type: 'goal_achieved',
goal: g
});
}
});
// 添加方案生成记录
plans.forEach(p => {
timeline.push({
date: new Date(p.created_at),
type: 'plan',
plan: p
});
});
// 按时间逆序排序
timeline.sort((a, b) => b.date - a.date);
renderTimeline(timeline);
}
function renderTimeline(timeline) {
const container = document.getElementById('planList');
if (plans.length === 0) {
container.innerHTML = '<p class="text-muted">暂无练习方案</p>';
if (timeline.length === 0) {
container.innerHTML = '<p class="text-muted">暂无学习记录</p>';
return;
}
container.innerHTML = plans.map(p => `
container.innerHTML = timeline.map(entry => {
if (entry.type === 'goal_start') {
const g = entry.goal;
return `
<div class="d-flex align-items-start mb-2 p-2 border rounded border-primary">
<div class="me-2">
<span class="badge bg-primary">目标启动</span>
</div>
<div class="flex-grow-1">
<div class="d-flex justify-content-between align-items-start">
<div>
<strong>${escapeHtml(g.goal_name)}</strong>
<span class="text-muted ms-2">${formatDate(entry.date)}</span>
</div>
<span class="badge bg-secondary">预期 ${entry.days} 天</span>
</div>
<div class="small text-muted">
${g.goal_level || '入门'} | ${g.goal_category || '综合'} | 评估日期: ${entry.endDate ? formatDate(entry.endDate) : '未设置'}
</div>
</div>
</div>`;
} else if (entry.type === 'goal_achieved') {
const g = entry.goal;
const stars = '⭐'.repeat(g.mastery_level || 1);
return `
<div class="d-flex align-items-start mb-2 p-2 border rounded border-success">
<div class="me-2">
<span class="badge bg-success">目标达成</span>
</div>
<div class="flex-grow-1">
<div class="d-flex justify-content-between align-items-start">
<div>
<strong>${escapeHtml(g.goal_name)}</strong>
<span class="text-muted ms-2">${formatDate(entry.date)}</span>
</div>
<span>${stars}</span>
</div>
${g.comment ? `<div class="small text-muted mt-1"><em>"${escapeHtml(g.comment)}"</em></div>` : ''}
</div>
</div>`;
} else {
const p = entry.plan;
return `
<div class="d-flex align-items-start mb-2 p-2 border rounded ${p.is_typical ? 'border-warning bg-light' : ''}">
<div class="form-check me-2">
<input class="form-check-input" type="checkbox" id="typical-${p.id}" ${p.is_typical ? 'checked' : ''} onchange="toggleTypical(${p.id})">
@@ -499,7 +588,7 @@ function renderPlanList(plans) {
<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">${p.created_at ? p.created_at.substring(0, 16) : '未知'}</a>
<a href="/plan/${p.id}" class="fw-bold text-decoration-none">${formatDate(entry.date)}</a>
${p.is_typical ? '<span class="badge bg-warning text-dark ms-1">典型</span>' : ''}
</div>
<button class="btn btn-sm btn-outline-danger" onclick="deletePlan(${p.id})">
@@ -511,13 +600,14 @@ function renderPlanList(plans) {
${p.template_name ? ' | 模板: ' + p.template_name : ''}
</div>
</div>
</div>
`).join('');
</div>`;
}
}).join('');
}
async function toggleTypical(planId) {
await fetch(`/api/plans/${planId}/typical`, {method: 'POST'});
loadPlans();
loadTimeline();
}
function showEditStudentModal() {