更新:models/routes/services/templates/docs
This commit is contained in:
+284
-84
@@ -156,8 +156,8 @@
|
||||
<div class="mb-3">
|
||||
<label class="form-label">级别 *</label>
|
||||
<select class="form-select" id="addProblemLevel" required>
|
||||
<option value="启蒙">启蒙</option>
|
||||
<option value="入门" selected>入门</option>
|
||||
<option value="启蒙" selected>启蒙</option>
|
||||
<option value="入门">入门</option>
|
||||
<option value="进阶">进阶</option>
|
||||
<option value="熟练">熟练</option>
|
||||
<option value="精通">精通</option>
|
||||
@@ -225,12 +225,10 @@
|
||||
<label class="form-label">学员:{{ student.name }}</label>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="useAiReport" checked>
|
||||
<label class="form-check-label" for="useAiReport">
|
||||
生成AI个性化报告(需要配置API)
|
||||
</label>
|
||||
</div>
|
||||
<label class="form-label">AI提示词模板</label>
|
||||
<select class="form-select" id="aiTemplateSelect">
|
||||
<option value="">加载中...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="progress" style="height: 25px;">
|
||||
@@ -296,10 +294,15 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="assess-goal-id">
|
||||
<input type="hidden" id="assess-evaluation-id">
|
||||
<p class="fw-bold mb-3" id="assess-goal-name"></p>
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">评估日期</label>
|
||||
<input type="date" class="form-control" id="assess-date">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">掌握程度</label>
|
||||
<select class="form-select" id="assess-mastery">
|
||||
<option value="1">⭐ 入门</option>
|
||||
@@ -309,14 +312,18 @@
|
||||
<option value="5">⭐⭐⭐⭐⭐ 精通</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">达成日期</label>
|
||||
<input type="date" class="form-control" id="assess-achievement-date">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">当前状态</label>
|
||||
<input type="text" class="form-control" id="assess-current-status" readonly>
|
||||
</div>
|
||||
<div class="col-md-3 d-flex align-items-end">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="assess-is-final">
|
||||
<label class="form-check-label" for="assess-is-final">
|
||||
最终评估
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">评语</label>
|
||||
@@ -407,6 +414,7 @@ function renderProblemList(problems) {
|
||||
<strong>${p.problem_name}</strong>
|
||||
<span class="badge bg-${p.severity === '严重' ? 'danger' : p.severity === '中等' ? 'warning' : 'info'} ms-2">${p.severity}</span>
|
||||
<span class="badge bg-secondary ms-1">${p.level}</span>
|
||||
<span class="text-muted ms-2" style="font-size: 0.8em;">添加: ${p.created_at ? p.created_at.split('T')[0] : ''}</span>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-sm btn-outline-primary me-1" onclick="showEditProblemModal(${p.id}, '${p.problem_name}', '${p.severity}', '${p.level}')">
|
||||
@@ -427,39 +435,56 @@ async function loadPlans() {
|
||||
|
||||
// 学习历程时间线
|
||||
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
|
||||
});
|
||||
try {
|
||||
const [plansRes, goalsRes, evalsRes] = await Promise.all([
|
||||
fetch(`/api/students/${currentStudentId}/plans`),
|
||||
fetch(`/api/students/${currentStudentId}/goals`),
|
||||
fetch(`/api/students/${currentStudentId}/evaluations`)
|
||||
]);
|
||||
if (!plansRes.ok || !goalsRes.ok || !evalsRes.ok) {
|
||||
throw new Error('API request failed');
|
||||
}
|
||||
// 添加目标达成记录
|
||||
if (g.achievement_date) {
|
||||
const plans = await plansRes.json();
|
||||
const goals = await goalsRes.json();
|
||||
const evaluations = await evalsRes.json();
|
||||
|
||||
// 构建时间线条目
|
||||
const timeline = [];
|
||||
const today = new Date();
|
||||
|
||||
// 添加所有评估记录
|
||||
evaluations.forEach(e => {
|
||||
if (!e.assessment_date) return;
|
||||
timeline.push({
|
||||
date: new Date(g.achievement_date),
|
||||
type: 'goal_achieved',
|
||||
goal: g
|
||||
date: new Date(e.assessment_date),
|
||||
type: 'evaluation',
|
||||
evaluation: e,
|
||||
goalName: e.goal_name,
|
||||
goalLevel: e.goal_level
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 添加目标开始记录
|
||||
goals.forEach(g => {
|
||||
if (g.start_date) {
|
||||
const startDate = new Date(g.start_date);
|
||||
const assessmentDate = g.assessment_date ? new Date(g.assessment_date) : null;
|
||||
const days = assessmentDate ? Math.ceil((assessmentDate - startDate) / (1000*60*60*24)) : null;
|
||||
// 计算尚余天数(对于未完成的目标)
|
||||
let daysRemaining = null;
|
||||
if (days && g.status !== '已完成') {
|
||||
daysRemaining = Math.ceil((assessmentDate - today) / (1000*60*60*24));
|
||||
}
|
||||
timeline.push({
|
||||
date: startDate,
|
||||
type: 'goal_start',
|
||||
goal: g,
|
||||
days: days,
|
||||
daysRemaining: daysRemaining,
|
||||
assessmentDate: assessmentDate
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 添加方案生成记录
|
||||
plans.forEach(p => {
|
||||
@@ -474,6 +499,10 @@ async function loadTimeline() {
|
||||
timeline.sort((a, b) => b.date - a.date);
|
||||
|
||||
renderTimeline(timeline);
|
||||
} catch (err) {
|
||||
console.error('loadTimeline error:', err);
|
||||
document.getElementById('planList').innerHTML = '<p class="text-danger">加载失败</p>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderTimeline(timeline) {
|
||||
@@ -486,6 +515,17 @@ function renderTimeline(timeline) {
|
||||
container.innerHTML = timeline.map(entry => {
|
||||
if (entry.type === 'goal_start') {
|
||||
const g = entry.goal;
|
||||
const isCompleted = g.status === '已完成';
|
||||
let durationBadge = '';
|
||||
if (entry.days) {
|
||||
if (isCompleted) {
|
||||
durationBadge = `<span class="badge bg-secondary">预期 ${entry.days} 天</span>`;
|
||||
} else if (entry.daysRemaining !== null) {
|
||||
durationBadge = `<span class="badge bg-secondary">预期 ${entry.days} 天</span> <span class="badge bg-info">尚余 ${entry.daysRemaining} 天</span>`;
|
||||
} else {
|
||||
durationBadge = `<span class="badge bg-secondary">预期 ${entry.days} 天</span>`;
|
||||
}
|
||||
}
|
||||
return `
|
||||
<div class="d-flex align-items-start mb-2 p-2 border rounded border-primary">
|
||||
<div class="me-2">
|
||||
@@ -497,52 +537,78 @@ function renderTimeline(timeline) {
|
||||
<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>
|
||||
${durationBadge}
|
||||
</div>
|
||||
<div class="small text-muted">
|
||||
${g.goal_level || '入门'} | ${g.goal_category || '综合'} | 评估日期: ${entry.endDate ? formatDate(entry.endDate) : '未设置'}
|
||||
${g.goal_level || '入门'} | ${g.goal_category || '综合'} | 评估日期: ${entry.assessmentDate ? formatDate(entry.assessmentDate) : '未设置'}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
} else if (entry.type === 'goal_achieved') {
|
||||
const g = entry.goal;
|
||||
const stars = '⭐'.repeat(g.mastery_level || 1);
|
||||
} else if (entry.type === 'evaluation') {
|
||||
const e = entry.evaluation;
|
||||
const stars = '⭐'.repeat(e.mastery_level || 1);
|
||||
const badge = e.is_final
|
||||
? '<span class="badge bg-warning text-dark">最终评估</span>'
|
||||
: '<span class="badge bg-info">阶段评估</span>';
|
||||
let timingBadge = '';
|
||||
if (e.is_final && e.goal_start_date && e.goal_assessment_date) {
|
||||
const startDate = new Date(e.goal_start_date);
|
||||
const achievementDate = new Date(e.assessment_date);
|
||||
const assessmentDate = new Date(e.goal_assessment_date);
|
||||
const actualDays = Math.ceil((achievementDate - startDate) / (1000*60*60*24));
|
||||
const expectedDays = Math.ceil((assessmentDate - startDate) / (1000*60*60*24));
|
||||
const diff = actualDays - expectedDays;
|
||||
let timingInfo;
|
||||
if (diff < 0) {
|
||||
timingInfo = `提前${Math.abs(diff)}天达成`;
|
||||
} else if (diff > 0) {
|
||||
timingInfo = `延迟${diff}天达成`;
|
||||
} else {
|
||||
timingInfo = '按期达成';
|
||||
}
|
||||
timingBadge = `<div class="mt-1"><span class="badge bg-success">${timingInfo},共耗时 ${actualDays} 天</span></div>`;
|
||||
}
|
||||
return `
|
||||
<div class="d-flex align-items-start mb-2 p-2 border rounded border-success">
|
||||
<div class="d-flex align-items-start mb-2 p-2 border rounded ${e.is_final ? 'border-success' : 'border-info'}">
|
||||
<div class="me-2">
|
||||
<span class="badge bg-success">目标达成</span>
|
||||
${badge}
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="flex-grow-1" style="cursor:pointer" onclick='openAssessGoalFromEvaluation(${JSON.stringify(e)})'>
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<strong>${escapeHtml(g.goal_name)}</strong>
|
||||
<strong>${escapeHtml(e.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>` : ''}
|
||||
${timingBadge}
|
||||
${e.comment ? `<div class="small text-muted mt-1"><em>"${escapeHtml(e.comment)}"</em></div>` : ''}
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-danger ms-2" onclick="event.stopPropagation(); deleteEvaluation(${e.id})" title="删除">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</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})">
|
||||
<label class="form-check-label" for="typical-${p.id}" title="设为典型"></label>
|
||||
</div>
|
||||
<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>
|
||||
${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})">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
<div class="btn-group">
|
||||
<a href="/plan/${p.id}" class="btn btn-sm btn-outline-primary" title="查看">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="deletePlan(${p.id})" title="删除">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="small text-muted">
|
||||
${p.problem_names && p.problem_names.length > 0 ? '问题: ' + p.problem_names.join(', ') : ''}
|
||||
${p.problem_details && p.problem_details.length > 0 ? '问题: ' + p.problem_details.map(d => d.name + '[' + d.level + '/' + d.severity + ']').join(', ') : ''}
|
||||
${p.template_name ? ' | 模板: ' + p.template_name : ''}
|
||||
</div>
|
||||
</div>
|
||||
@@ -551,11 +617,6 @@ function renderTimeline(timeline) {
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function toggleTypical(planId) {
|
||||
await fetch(`/api/plans/${planId}/typical`, {method: 'POST'});
|
||||
loadTimeline();
|
||||
}
|
||||
|
||||
function showEditStudentModal() {
|
||||
editStudentModal.show();
|
||||
}
|
||||
@@ -596,7 +657,7 @@ function deleteStudent() {
|
||||
function showAddProblemModal() {
|
||||
document.getElementById('addProblemSelect').value = '';
|
||||
document.getElementById('addProblemSeverity').value = '中等';
|
||||
document.getElementById('addProblemLevel').value = '入门';
|
||||
document.getElementById('addProblemLevel').value = '启蒙';
|
||||
problemModal.show();
|
||||
}
|
||||
|
||||
@@ -695,14 +756,80 @@ function generatePlan() {
|
||||
progressBar.textContent = '0%';
|
||||
progressText.textContent = '准备中...';
|
||||
progressLog.innerHTML = '';
|
||||
|
||||
// 加载AI模板列表
|
||||
loadAiTemplates();
|
||||
|
||||
generateModal.show();
|
||||
}
|
||||
|
||||
// 模态框显示后加载提示词预览
|
||||
document.getElementById('generatePlanModal').addEventListener('shown.bs.modal', function () {
|
||||
loadPromptPreview();
|
||||
});
|
||||
|
||||
// 模板切换时更新预览
|
||||
document.getElementById('aiTemplateSelect').addEventListener('change', function () {
|
||||
loadPromptPreview();
|
||||
});
|
||||
|
||||
// 加载提示词预览
|
||||
async function loadPromptPreview() {
|
||||
const progressLog = document.getElementById('progressLog');
|
||||
const progressText = document.getElementById('progressText');
|
||||
|
||||
progressText.textContent = '正在生成提示词预览...';
|
||||
progressLog.innerHTML = '<div class="text-muted small">加载中...</div>';
|
||||
|
||||
try {
|
||||
const templateId = document.getElementById('aiTemplateSelect').value || null;
|
||||
const resp = await fetch('/api/generate-plan/preview', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ student_id: currentStudentId, template_id: templateId })
|
||||
});
|
||||
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
// 显示完整提示词
|
||||
progressLog.innerHTML = `<div class="small" style="white-space: pre-wrap; font-family: monospace;">${escapeHtml(data.prompt)}</div>`;
|
||||
progressText.textContent = `提示词预览(共${data.prompt_length}字)`;
|
||||
} else {
|
||||
const err = await resp.json();
|
||||
progressLog.innerHTML = `<div class="text-danger small">加载失败: ${err.error || '未知错误'}</div>`;
|
||||
progressText.textContent = '加载失败';
|
||||
}
|
||||
} catch (e) {
|
||||
progressLog.innerHTML = `<div class="text-danger small">加载失败: ${e.message}</div>`;
|
||||
progressText.textContent = '加载失败';
|
||||
}
|
||||
}
|
||||
|
||||
// 加载AI模板列表
|
||||
async function loadAiTemplates() {
|
||||
try {
|
||||
const resp = await fetch('/templates/templates?type=ai_prompt');
|
||||
if (resp.ok) {
|
||||
const templates = await resp.json();
|
||||
const select = document.getElementById('aiTemplateSelect');
|
||||
select.innerHTML = templates.map(t => `<option value="${t.id}">${t.name}</option>`).join('');
|
||||
// 模板加载完成后自动显示预览
|
||||
loadPromptPreview();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载AI模板失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function startGeneratePlan() {
|
||||
const progressBar = document.getElementById('progressBar');
|
||||
const progressText = document.getElementById('progressText');
|
||||
const progressLog = document.getElementById('progressLog');
|
||||
const useAi = document.getElementById('useAiReport').checked;
|
||||
const startBtn = document.getElementById('startGenerateBtn');
|
||||
|
||||
// 禁用开始按钮
|
||||
startBtn.disabled = true;
|
||||
startBtn.textContent = '生成中...';
|
||||
|
||||
progressBar.style.width = '0%';
|
||||
progressText.textContent = '准备中...';
|
||||
@@ -718,7 +845,7 @@ async function startGeneratePlan() {
|
||||
const response = await fetch('/api/generate-plan', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({student_id: currentStudentId, use_ai: useAi})
|
||||
body: JSON.stringify({student_id: currentStudentId, use_ai: true, template_id: document.getElementById('aiTemplateSelect').value || null})
|
||||
});
|
||||
|
||||
const reader = response.body.getReader();
|
||||
@@ -742,19 +869,35 @@ async function startGeneratePlan() {
|
||||
|
||||
let logMsg = data.message;
|
||||
if (data.detail) {
|
||||
if (data.step === 'ai_prompt') {
|
||||
logMsg = '【AI提示词】\n' + data.detail;
|
||||
addLog(logMsg);
|
||||
continue;
|
||||
} else {
|
||||
logMsg += '\n └ ' + data.detail.replace(/\n/g, '\n └ ');
|
||||
}
|
||||
logMsg += '\n └ ' + data.detail.replace(/\n/g, '\n └ ');
|
||||
}
|
||||
addLog(logMsg);
|
||||
|
||||
// AI报告完成后显示字数统计
|
||||
if (data.step === 'complete') {
|
||||
addLog('─'.repeat(40));
|
||||
if (data.prompt_length) {
|
||||
addLog(`📤 提示词:${data.prompt_length} 字`);
|
||||
}
|
||||
if (data.student_problems_length) {
|
||||
addLog(` └ 问题摘要:${data.student_problems_length} 字`);
|
||||
}
|
||||
if (data.problems_length) {
|
||||
addLog(` └ 问题详情:${data.problems_length} 字`);
|
||||
}
|
||||
if (data.student_goals_length) {
|
||||
addLog(` └ 学员目标:${data.student_goals_length} 字`);
|
||||
}
|
||||
if (data.ai_report_length) {
|
||||
addLog(`📥 AI报告:${data.ai_report_length} 字`);
|
||||
}
|
||||
}
|
||||
|
||||
if (data.step === 'complete') {
|
||||
setTimeout(() => {
|
||||
generateModal.hide();
|
||||
startBtn.disabled = false;
|
||||
startBtn.textContent = '开始生成';
|
||||
alert('方案生成成功!');
|
||||
loadPlans();
|
||||
}, 500);
|
||||
@@ -762,6 +905,8 @@ async function startGeneratePlan() {
|
||||
|
||||
if (data.error) {
|
||||
addLog(data.error, true);
|
||||
startBtn.disabled = false;
|
||||
startBtn.textContent = '开始生成';
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
@@ -770,6 +915,8 @@ async function startGeneratePlan() {
|
||||
} catch (err) {
|
||||
addLog('请求失败:' + err.message, true);
|
||||
progressText.textContent = '生成失败';
|
||||
startBtn.disabled = false;
|
||||
startBtn.textContent = '开始生成';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -849,6 +996,7 @@ document.getElementById('remove-assigned-goal').addEventListener('click', () =>
|
||||
.then(() => {
|
||||
bootstrap.Modal.getInstance(document.getElementById('adjustGoalModal')).hide();
|
||||
loadStudentGoals();
|
||||
loadTimeline();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -876,35 +1024,69 @@ async function openAssessGoal(goalId) {
|
||||
if (!goal) return;
|
||||
|
||||
document.getElementById('assess-goal-id').value = goalId;
|
||||
document.getElementById('assess-evaluation-id').value = '';
|
||||
document.getElementById('assess-goal-name').textContent = goal.goal_name;
|
||||
document.getElementById('assess-date').value = new Date().toISOString().split('T')[0];
|
||||
document.getElementById('assess-mastery').value = goal.mastery_level || '1';
|
||||
document.getElementById('assess-achievement-date').value = goal.achievement_date ? goal.achievement_date.split('T')[0] : '';
|
||||
document.getElementById('assess-current-status').value = goal.status;
|
||||
document.getElementById('assess-comment').value = goal.comment || '';
|
||||
document.getElementById('assess-is-final').checked = false;
|
||||
|
||||
new bootstrap.Modal(document.getElementById('assessGoalModal')).show();
|
||||
}
|
||||
|
||||
function openAssessGoalFromEvaluation(evaluation) {
|
||||
// evaluation has: id, student_goal_id, mastery_level, comment, is_final, goal_name, assessment_date, etc.
|
||||
document.getElementById('assess-goal-id').value = evaluation.student_goal_goal_id;
|
||||
document.getElementById('assess-evaluation-id').value = evaluation.id;
|
||||
document.getElementById('assess-goal-name').textContent = evaluation.goal_name || '未知目标';
|
||||
document.getElementById('assess-date').value = evaluation.assessment_date ? evaluation.assessment_date.split('T')[0] : new Date().toISOString().split('T')[0];
|
||||
document.getElementById('assess-mastery').value = evaluation.mastery_level || '1';
|
||||
document.getElementById('assess-comment').value = evaluation.comment || '';
|
||||
document.getElementById('assess-is-final').checked = evaluation.is_final || false;
|
||||
|
||||
// Status cannot be edited from evaluation modal, show current status
|
||||
document.getElementById('assess-current-status').value = evaluation.is_final ? '已完成' : '进行中';
|
||||
|
||||
new bootstrap.Modal(document.getElementById('assessGoalModal')).show();
|
||||
}
|
||||
|
||||
document.getElementById('confirm-assess-goal').addEventListener('click', async () => {
|
||||
const goalId = document.getElementById('assess-goal-id').value;
|
||||
const evaluationId = document.getElementById('assess-evaluation-id').value;
|
||||
const assessmentDate = document.getElementById('assess-date').value;
|
||||
const masteryLevel = document.getElementById('assess-mastery').value;
|
||||
const achievementDate = document.getElementById('assess-achievement-date').value;
|
||||
const comment = document.getElementById('assess-comment').value;
|
||||
const isFinal = document.getElementById('assess-is-final').checked;
|
||||
|
||||
const body = {
|
||||
mastery_level: parseInt(masteryLevel),
|
||||
comment: comment,
|
||||
is_final: isFinal,
|
||||
assessment_date: assessmentDate || null
|
||||
};
|
||||
|
||||
if (evaluationId) {
|
||||
body.evaluation_id = parseInt(evaluationId);
|
||||
}
|
||||
|
||||
await fetch(`/api/students/${currentStudentId}/goals/${goalId}`, {
|
||||
method: 'PUT',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
mastery_level: parseInt(masteryLevel),
|
||||
achievement_date: achievementDate || null,
|
||||
comment: comment
|
||||
})
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
bootstrap.Modal.getInstance(document.getElementById('assessGoalModal')).hide();
|
||||
loadStudentGoals();
|
||||
loadTimeline();
|
||||
});
|
||||
|
||||
async function deleteEvaluation(evaluationId) {
|
||||
if (!confirm('确定要删除这条评估记录吗?')) return;
|
||||
await fetch(`/api/evaluations/${evaluationId}`, { method: 'DELETE' });
|
||||
loadTimeline();
|
||||
}
|
||||
|
||||
// 加载可选目标列表到 Modal
|
||||
async function loadGoalOptions() {
|
||||
const res = await fetch('/api/goals');
|
||||
@@ -946,6 +1128,24 @@ document.getElementById('assign-assessment-date').addEventListener('change', fun
|
||||
}
|
||||
});
|
||||
|
||||
// 开始日期联动:修改开始日期后,如果使用"XX天后"评估,自动重新计算评估日期
|
||||
function updateAssessmentDateFromStartDate() {
|
||||
const startDateStr = document.getElementById('assign-start-date').value;
|
||||
const daysStr = document.getElementById('assign-assessment-days').value;
|
||||
if (startDateStr && daysStr) {
|
||||
const days = parseInt(daysStr);
|
||||
const [y, m, d] = startDateStr.split('-').map(Number);
|
||||
const startDate = new Date(y, m - 1, d);
|
||||
startDate.setDate(startDate.getDate() + days);
|
||||
const yy = startDate.getFullYear();
|
||||
const mm = String(startDate.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(startDate.getDate()).padStart(2, '0');
|
||||
document.getElementById('assign-assessment-date').value = `${yy}-${mm}-${dd}`;
|
||||
}
|
||||
}
|
||||
document.getElementById('assign-start-date').addEventListener('change', updateAssessmentDateFromStartDate);
|
||||
document.getElementById('assign-start-date').addEventListener('input', updateAssessmentDateFromStartDate);
|
||||
|
||||
document.getElementById('confirm-assign-goal').addEventListener('click', async () => {
|
||||
const goalId = document.getElementById('assign-goal-select').value;
|
||||
const assessmentDays = document.getElementById('assign-assessment-days').value;
|
||||
|
||||
Reference in New Issue
Block a user