feat: 学员详情页改为学习历程时间线,显示目标启动/达成和方案生成记录
This commit is contained in:
+106
-16
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user