feat: 添加学员详情/方案编辑/方案列表新页面
- student.html: 学员详情页,支持编辑/添加/删除问题 - plan_edit.html: 方案编辑页 - plans.html: 方案列表页 - home.html: 首页
This commit is contained in:
@@ -0,0 +1,46 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}首页 - 钢琴练习方案系统{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row g-4 mb-4">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card text-center h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<i class="bi bi-people text-primary fs-1"></i>
|
||||||
|
<h2 class="mt-2">{{ student_count }}</h2>
|
||||||
|
<p class="text-muted">学员</p>
|
||||||
|
<a href="/students" class="btn btn-outline-primary btn-sm">查看</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card text-center h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<i class="bi bi-collection text-success fs-1"></i>
|
||||||
|
<h2 class="mt-2">{{ class_count }}</h2>
|
||||||
|
<p class="text-muted">班级</p>
|
||||||
|
<a href="/classes" class="btn btn-outline-success btn-sm">查看</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card text-center h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<i class="bi bi-clipboard-check text-warning fs-1"></i>
|
||||||
|
<h2 class="mt-2">{{ plan_count }}</h2>
|
||||||
|
<p class="text-muted">方案</p>
|
||||||
|
<a href="/plans" class="btn btn-outline-warning btn-sm">查看</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body text-center py-5">
|
||||||
|
<i class="bi bi-music-note-beamed text-muted" style="font-size: 48px;"></i>
|
||||||
|
<h4 class="mt-3 text-muted">欢迎使用钢琴练习方案管理系统</h4>
|
||||||
|
<p class="text-muted">从左侧菜单选择功能</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}方案详情 - 钢琴练习方案系统{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h4><i class="bi bi-file-text"></i> 方案详情</h4>
|
||||||
|
<div>
|
||||||
|
<button onclick="history.back()" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-arrow-left"></i> 返回
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="planContent" class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="text-center text-muted py-5">
|
||||||
|
<i class="bi bi-hourglass fs-4"></i>
|
||||||
|
<p class="mt-2">加载中...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
var currentPlanId = null;
|
||||||
|
|
||||||
|
async function loadPlan() {
|
||||||
|
currentPlanId = window.location.pathname.split('/').pop();
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/plans/${currentPlanId}`);
|
||||||
|
const data = await resp.json();
|
||||||
|
window.currentStudentId = data.student_id;
|
||||||
|
|
||||||
|
let html = `
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>学员:</strong>${data.student_name}
|
||||||
|
<strong>练习时间:</strong>${data.content.practice_time}
|
||||||
|
<strong>生成时间:</strong>${data.created_at}
|
||||||
|
<strong>模板:</strong>${data.template_name || '无'}
|
||||||
|
<div class="mt-2">
|
||||||
|
<a href="/?student_id=${data.student_id}&from=${encodeURIComponent(window.location.href)}" class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="bi bi-person"></i> 查看学员
|
||||||
|
</a>
|
||||||
|
<a href="/plan/${currentPlanId}/edit" class="btn btn-sm btn-warning">
|
||||||
|
<i class="bi bi-edit"></i> 编辑
|
||||||
|
</a>
|
||||||
|
<button onclick="downloadPDF()" class="btn btn-sm btn-primary">
|
||||||
|
<i class="bi bi-download"></i> 下载PDF
|
||||||
|
</button>
|
||||||
|
<button onclick="downloadMD()" class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="bi bi-file-markdown"></i> 下载MD
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<h6>问题诊断</h6>
|
||||||
|
<div class="mb-3">
|
||||||
|
`;
|
||||||
|
|
||||||
|
data.content.problems.forEach(p => {
|
||||||
|
html += `<span class="problem-tag severity-${p.severity}">${p.name}(${p.severity})</span> `;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += `</div>`;
|
||||||
|
|
||||||
|
if (data.content.ai_report) {
|
||||||
|
const aiReportHtml = marked.parse(data.content.ai_report);
|
||||||
|
html += `
|
||||||
|
<h6>AI个性化练习报告</h6>
|
||||||
|
<div class="mb-3 p-3 bg-light rounded">${aiReportHtml}</div>
|
||||||
|
`;
|
||||||
|
} else if (data.content.ai_report_error) {
|
||||||
|
html += `
|
||||||
|
<h6>AI报告</h6>
|
||||||
|
<div class="mb-3 p-3 bg-warning rounded">AI生成失败: ${data.content.ai_report_error}</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<h6>每日练习计划(共${data.content.total_daily_minutes}分钟)</h6>
|
||||||
|
<table class="table table-sm">
|
||||||
|
<thead><tr><th>环节</th><th>时长</th><th>内容</th><th>目的</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
`;
|
||||||
|
|
||||||
|
data.content.daily_schedule.forEach(item => {
|
||||||
|
html += `<tr><td>${item.phase}</td><td>${item.duration}</td><td>${item.content}</td><td>${item.purpose}</td></tr>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += '</tbody></table>';
|
||||||
|
|
||||||
|
document.getElementById('planContent').innerHTML = html;
|
||||||
|
} catch (e) {
|
||||||
|
document.getElementById('planContent').innerHTML = `
|
||||||
|
<div class="card-body text-center text-danger py-5">
|
||||||
|
<i class="bi bi-exclamation-triangle fs-4"></i>
|
||||||
|
<p class="mt-2">加载失败: ${e.message}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadPDF() {
|
||||||
|
window.open(`/api/plans/${currentPlanId}/pdf`, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadMD() {
|
||||||
|
window.open(`/api/plans/${currentPlanId}/md`, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
window.currentStudentId = null;
|
||||||
|
|
||||||
|
loadPlan();
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}编辑方案 - 钢琴练习方案系统{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h4><i class="bi bi-edit"></i> 编辑方案</h4>
|
||||||
|
<div>
|
||||||
|
<a href="/plan/{{ plan_id }}" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-arrow-left"></i> 返回详情
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<ul class="nav nav-tabs" id="editContentTabs" role="tablist">
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link active" id="aiReport-tab" data-bs-toggle="tab" data-bs-target="#aiReportPane" type="button">AI报告</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="dailySchedule-tab" data-bs-toggle="tab" data-bs-target="#dailySchedulePane" type="button">每日练习计划</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="tab-content mt-3" id="editContentTabContent">
|
||||||
|
<div class="tab-pane fade show active" id="aiReportPane" role="tabpanel">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="bi bi-info-circle"></i> 使用 Markdown 格式编辑AI报告,支持标题、列表、加粗等格式
|
||||||
|
</div>
|
||||||
|
<textarea id="editAiReport" style="display:none;"></textarea>
|
||||||
|
<div id="editAiReportEditor"></div>
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane fade" id="dailySchedulePane" role="tabpanel">
|
||||||
|
<div class="alert alert-info mb-2">
|
||||||
|
<i class="bi bi-info-circle"></i> 支持拖拽排序,双击单元格编辑、点击+添加行、点击×删除行
|
||||||
|
</div>
|
||||||
|
<div id="editDailyScheduleTable" style="height: 400px; max-height: 600px;"></div>
|
||||||
|
<button type="button" class="btn btn-outline-primary btn-sm mt-2" onclick="addScheduleRow()">
|
||||||
|
<i class="bi bi-plus"></i> 添加一行
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="history.back()">取消</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="savePlanContent()">
|
||||||
|
<i class="bi bi-save"></i> 保存
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
let planContentEditor = null;
|
||||||
|
let scheduleTable = null;
|
||||||
|
let editPlanOriginalState = { ai_report: '', scheduleData: [] };
|
||||||
|
|
||||||
|
window.pageInit = function() {
|
||||||
|
loadPlanForEdit();
|
||||||
|
};
|
||||||
|
|
||||||
|
async function loadPlanForEdit() {
|
||||||
|
const planId = {{ plan_id }};
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/plans/${planId}`);
|
||||||
|
const data = await resp.json();
|
||||||
|
const content = typeof data.content === 'string' ? JSON.parse(data.content) : data.content;
|
||||||
|
|
||||||
|
document.getElementById('editAiReport').value = content.ai_report || '';
|
||||||
|
|
||||||
|
if (planContentEditor) {
|
||||||
|
planContentEditor.toTextArea();
|
||||||
|
planContentEditor = null;
|
||||||
|
}
|
||||||
|
planContentEditor = new EasyMDE({
|
||||||
|
element: document.getElementById('editAiReport'),
|
||||||
|
spellChecker: false,
|
||||||
|
status: false,
|
||||||
|
toolbar: ['bold', 'italic', 'heading', '|', 'code', 'quote', 'unordered-list', '|', 'side-by-side', 'fullscreen'],
|
||||||
|
initialValue: content.ai_report || ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const scheduleData = (content.daily_schedule || []).map(item => ({
|
||||||
|
phase: item.phase || '',
|
||||||
|
duration: item.duration || '',
|
||||||
|
content: item.content || '',
|
||||||
|
purpose: item.purpose || ''
|
||||||
|
}));
|
||||||
|
|
||||||
|
editPlanOriginalState.ai_report = content.ai_report || '';
|
||||||
|
editPlanOriginalState.scheduleData = JSON.parse(JSON.stringify(scheduleData));
|
||||||
|
|
||||||
|
if (scheduleTable) {
|
||||||
|
scheduleTable.destroy();
|
||||||
|
scheduleTable = null;
|
||||||
|
}
|
||||||
|
scheduleTable = new Tabulator("#editDailyScheduleTable", {
|
||||||
|
height: "100%",
|
||||||
|
data: scheduleData,
|
||||||
|
layout: "fitColumns",
|
||||||
|
movableRows: true,
|
||||||
|
editable: true,
|
||||||
|
addRowPos: "bottom",
|
||||||
|
columns: [
|
||||||
|
{ title: "环节", field: "phase", editor: "input", width: 120 },
|
||||||
|
{ title: "时长", field: "duration", editor: "input", width: 100 },
|
||||||
|
{ title: "内容", field: "content", editor: "input", minWidth: 200 },
|
||||||
|
{ title: "目的", field: "purpose", editor: "input", minWidth: 150 },
|
||||||
|
{
|
||||||
|
title: "操作",
|
||||||
|
width: 80,
|
||||||
|
formatter: function(cell) {
|
||||||
|
return "<button type='button' class='btn btn-sm btn-outline-danger'><i class='bi bi-trash'></i></button>";
|
||||||
|
},
|
||||||
|
cellClick: function(e, cell) {
|
||||||
|
cell.getRow().delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
alert('加载方案失败: ' + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addScheduleRow() {
|
||||||
|
if (scheduleTable) {
|
||||||
|
scheduleTable.addRow({ phase: '', duration: '', content: '', purpose: '' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function savePlanContent() {
|
||||||
|
const planId = {{ plan_id }};
|
||||||
|
const currentAiReport = planContentEditor ? planContentEditor.value() : document.getElementById('editAiReport').value;
|
||||||
|
const tableData = scheduleTable ? scheduleTable.getData() : [];
|
||||||
|
|
||||||
|
const hasChanges = currentAiReport !== editPlanOriginalState.ai_report ||
|
||||||
|
JSON.stringify(tableData) !== JSON.stringify(editPlanOriginalState.scheduleData);
|
||||||
|
|
||||||
|
if (!hasChanges) {
|
||||||
|
alert('没有修改,无需保存');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm('确定要保存修改吗?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/plans/${planId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
ai_report: currentAiReport,
|
||||||
|
daily_schedule: tableData
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resp.ok) {
|
||||||
|
alert('保存成功');
|
||||||
|
window.location.href = `/plan/${planId}`;
|
||||||
|
} else {
|
||||||
|
alert('保存失败');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('保存失败: ' + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}方案管理 - 钢琴练习方案系统{% endblock %}
|
||||||
|
|
||||||
|
{% block page_css %}
|
||||||
|
<style>
|
||||||
|
.plan-table th { white-space: nowrap; }
|
||||||
|
.plan-problem-text { font-weight: 600; color: #2c3e50; }
|
||||||
|
.plan-meta-text { color: #95a5a6; font-size: 0.85rem; }
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h4><i class="bi bi-clipboard-check"></i> 方案管理</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 筛选器 -->
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label small">班级</label>
|
||||||
|
<select class="form-select" id="filterClass" onchange="loadPlans()">
|
||||||
|
<option value="">全部班级</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label small">问题</label>
|
||||||
|
<select class="form-select" id="filterProblem" onchange="loadPlans()">
|
||||||
|
<option value="">全部问题</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label small">模板</label>
|
||||||
|
<select class="form-select" id="filterTemplate" onchange="loadPlans()">
|
||||||
|
<option value="">全部模板</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label small">典型方案</label>
|
||||||
|
<select class="form-select" id="filterTypical" onchange="loadPlans()">
|
||||||
|
<option value="">全部</option>
|
||||||
|
<option value="true">仅典型</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label small">学员姓名</label>
|
||||||
|
<input type="text" class="form-control" id="filterStudentName" placeholder="模糊搜索..." oninput="debounceLoad()">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-1 d-flex align-items-end">
|
||||||
|
<button class="btn btn-outline-secondary w-100" onclick="clearFilters()">清空</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 方案列表 -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="plansContainer">
|
||||||
|
<div class="text-center text-muted py-5">
|
||||||
|
<i class="bi bi-hourglass-split fs-1"></i>
|
||||||
|
<p class="mt-2">加载中...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
// 防抖定时器
|
||||||
|
let debounceTimer = null;
|
||||||
|
|
||||||
|
function debounceLoad() {
|
||||||
|
clearTimeout(debounceTimer);
|
||||||
|
debounceTimer = setTimeout(loadPlans, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面初始化
|
||||||
|
window.pageInit = function() {
|
||||||
|
loadFilters();
|
||||||
|
loadPlans();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载筛选器选项
|
||||||
|
async function loadFilters() {
|
||||||
|
// 加载班级
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/api/classes');
|
||||||
|
const classes = await resp.json();
|
||||||
|
const classSelect = document.getElementById('filterClass');
|
||||||
|
classes.forEach(c => {
|
||||||
|
classSelect.innerHTML += `<option value="${c.id}">${c.name}</option>`;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载班级失败', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载问题
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/api/problems');
|
||||||
|
const problems = await resp.json();
|
||||||
|
const problemSelect = document.getElementById('filterProblem');
|
||||||
|
problems.forEach(p => {
|
||||||
|
problemSelect.innerHTML += `<option value="${p.id}">${p.name}</option>`;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载问题失败', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载模板
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/templates/templates');
|
||||||
|
const templates = await resp.json();
|
||||||
|
const templateSelect = document.getElementById('filterTemplate');
|
||||||
|
templates.filter(t => t.type === 'ai_prompt').forEach(t => {
|
||||||
|
templateSelect.innerHTML += `<option value="${t.id}">${t.name}</option>`;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载模板失败', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载方案列表
|
||||||
|
async function loadPlans() {
|
||||||
|
const container = document.getElementById('plansContainer');
|
||||||
|
container.innerHTML = '<div class="text-center text-muted py-5"><i class="bi bi-hourglass fs-4"></i><p class="mt-2">加载中...</p></div>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
const classId = document.getElementById('filterClass').value;
|
||||||
|
if (classId) params.append('class_id', classId);
|
||||||
|
|
||||||
|
const templateId = document.getElementById('filterTemplate').value;
|
||||||
|
if (templateId) params.append('template_id', templateId);
|
||||||
|
|
||||||
|
const isTypical = document.getElementById('filterTypical').value;
|
||||||
|
if (isTypical === 'true') params.append('is_typical', 'true');
|
||||||
|
|
||||||
|
const studentName = document.getElementById('filterStudentName').value.trim();
|
||||||
|
if (studentName) params.append('student_name', studentName);
|
||||||
|
|
||||||
|
const problemId = document.getElementById('filterProblem').value;
|
||||||
|
if (problemId) {
|
||||||
|
params.append('problem_ids', parseInt(problemId));
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = await fetch(`/api/plans?${params}`);
|
||||||
|
const plans = await resp.json();
|
||||||
|
|
||||||
|
if (plans.length === 0) {
|
||||||
|
container.innerHTML = '<div class="text-center text-muted py-5"><i class="bi bi-inbox fs-4"></i><p class="mt-2">暂无方案</p></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = `
|
||||||
|
<table class="table table-hover table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>学员</th>
|
||||||
|
<th>班级</th>
|
||||||
|
<th>问题</th>
|
||||||
|
<th>模板</th>
|
||||||
|
<th>典型</th>
|
||||||
|
<th>时间</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
`;
|
||||||
|
|
||||||
|
plans.forEach(p => {
|
||||||
|
const problems = (p.problem_names || []).slice(0, 3).join('、');
|
||||||
|
const moreProblems = (p.problem_names || []).length > 3 ? `等${p.problem_names.length}个` : '';
|
||||||
|
const template = p.template_name || '无模板';
|
||||||
|
const studentName = p.student_name || '未知';
|
||||||
|
const className = p.class_name || '-';
|
||||||
|
const isTypical = p.is_typical ? '✓' : '';
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<tr>
|
||||||
|
<td><strong>${studentName}</strong></td>
|
||||||
|
<td>${className}</td>
|
||||||
|
<td><span class="plan-problem-text">${problems}${moreProblems}</span></td>
|
||||||
|
<td class="text-muted small">${template}</td>
|
||||||
|
<td class="text-center">${isTypical ? '<span class="text-warning">★</span>' : ''}</td>
|
||||||
|
<td class="text-muted small">${p.created_at || ''}</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-outline-primary" onclick="viewPlan(${p.id})">查看</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += '</tbody></table>';
|
||||||
|
container.innerHTML = html;
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
container.innerHTML = `<div class="text-center text-danger py-5"><i class="bi bi-exclamation-circle fs-4"></i><p class="mt-2">加载失败: ${e.message}</p></div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空筛选器
|
||||||
|
function clearFilters() {
|
||||||
|
document.getElementById('filterClass').value = '';
|
||||||
|
document.getElementById('filterProblem').value = '';
|
||||||
|
document.getElementById('filterTemplate').value = '';
|
||||||
|
document.getElementById('filterTypical').value = '';
|
||||||
|
document.getElementById('filterStudentName').value = '';
|
||||||
|
loadPlans();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看方案 - 跳转到方案详情页
|
||||||
|
function viewPlan(planId) {
|
||||||
|
window.location.href = `/plan/${planId}`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,583 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ student.name }} - 学员详情 - 钢琴练习方案系统{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h4><i class="bi bi-person"></i> 学员详情</h4>
|
||||||
|
<div>
|
||||||
|
<a href="/students" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-arrow-left"></i> 返回列表
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="avatar mb-3">
|
||||||
|
<i class="bi bi-person-circle" style="font-size: 60px; color: #6c757d;"></i>
|
||||||
|
</div>
|
||||||
|
<h5 id="detailName">{{ student.name }}</h5>
|
||||||
|
<p class="text-muted" id="detailWechatNickname">{{ student.wechat_nickname or '未填写' }}</p>
|
||||||
|
<p class="text-muted" id="detailPhone">{{ student.phone or '未填写' }}</p>
|
||||||
|
<p class="small">
|
||||||
|
<span class="badge bg-info" id="detailPracticeTime">{{ student.practice_time or '30分钟' }}</span>
|
||||||
|
</p>
|
||||||
|
<p class="small" id="detailClass">
|
||||||
|
{% if student.class_obj %}
|
||||||
|
<span class="badge bg-secondary">{{ student.class_obj.name }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">未分配班级</span>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
<p class="small text-muted" id="detailNotes">{{ student.notes or '无备注' }}</p>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary" onclick="showEditStudentModal()">
|
||||||
|
<i class="bi bi-pencil"></i> 编辑
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteStudent()">
|
||||||
|
<i class="bi bi-trash"></i> 删除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<button class="btn btn-sm btn-primary" onclick="showAddProblemModal()">
|
||||||
|
<i class="bi bi-plus"></i> 添加问题
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body" id="problemList">
|
||||||
|
<p class="text-muted">加载中...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h6 class="mb-0"><i class="bi bi-clipboard-check"></i> 练习方案</h6>
|
||||||
|
<button class="btn btn-sm btn-primary" onclick="generatePlan()">
|
||||||
|
<i class="bi bi-plus"></i> 生成方案
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body" id="planList">
|
||||||
|
<p class="text-muted">加载中...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 编辑学员 Modal -->
|
||||||
|
<div class="modal fade" id="editStudentModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">编辑学员</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" id="editStudentId" value="{{ student.id }}">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">姓名 *</label>
|
||||||
|
<input type="text" class="form-control" id="editStudentName" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">手机号</label>
|
||||||
|
<input type="text" class="form-control" id="editStudentPhone">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">微信昵称</label>
|
||||||
|
<input type="text" class="form-control" id="editStudentWechat">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">每日练习时间</label>
|
||||||
|
<select class="form-select" id="editStudentPracticeTime">
|
||||||
|
<option value="15分钟">15分钟</option>
|
||||||
|
<option value="30分钟">30分钟</option>
|
||||||
|
<option value="45分钟">45分钟</option>
|
||||||
|
<option value="60分钟">60分钟</option>
|
||||||
|
<option value="90分钟">90分钟</option>
|
||||||
|
<option value="120分钟">120分钟</option>
|
||||||
|
<option value="150分钟以上">150分钟以上</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">备注</label>
|
||||||
|
<textarea class="form-control" id="editStudentNotes" rows="2"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="saveStudentEdit()">保存</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 添加问题 Modal -->
|
||||||
|
<div class="modal fade" id="addProblemModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">添加问题</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">问题 *</label>
|
||||||
|
<select class="form-select" id="addProblemSelect" required>
|
||||||
|
<option value="">选择问题...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">严重程度 *</label>
|
||||||
|
<select class="form-select" id="addProblemSeverity" required>
|
||||||
|
<option value="轻微">轻微</option>
|
||||||
|
<option value="中等" selected>中等</option>
|
||||||
|
<option value="严重">严重</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<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="进阶">进阶</option>
|
||||||
|
<option value="熟练">熟练</option>
|
||||||
|
<option value="精通">精通</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="saveAddProblem()">添加</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 编辑问题 Modal -->
|
||||||
|
<div class="modal fade" id="editProblemModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">编辑问题</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" id="editProblemId">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">问题</label>
|
||||||
|
<input type="text" class="form-control" id="editProblemName" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">严重程度 *</label>
|
||||||
|
<select class="form-select" id="editProblemSeverity" required>
|
||||||
|
<option value="轻微">轻微</option>
|
||||||
|
<option value="中等">中等</option>
|
||||||
|
<option value="严重">严重</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">级别 *</label>
|
||||||
|
<select class="form-select" id="editProblemLevel" required>
|
||||||
|
<option value="启蒙">启蒙</option>
|
||||||
|
<option value="入门">入门</option>
|
||||||
|
<option value="进阶">进阶</option>
|
||||||
|
<option value="熟练">熟练</option>
|
||||||
|
<option value="精通">精通</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="saveProblemEdit()">保存</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 生成方案 Modal -->
|
||||||
|
<div class="modal fade" id="generatePlanModal" tabindex="-1" data-bs-backdrop="static">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">生成练习方案</h5>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="progress" style="height: 25px;">
|
||||||
|
<div id="progressBar" class="progress-bar progress-bar-striped progress-bar-animated"
|
||||||
|
role="progressbar" style="width: 0%">0%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong id="progressText">准备中...</strong>
|
||||||
|
</div>
|
||||||
|
<div id="progressLog" class="bg-light p-3 rounded" style="height: 200px; overflow-y: auto; font-family: monospace;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="startGenerateBtn" onclick="startGeneratePlan()">开始生成</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
const currentStudentId = {{ student.id }};
|
||||||
|
const studentName = "{{ student.name }}";
|
||||||
|
let problemModal, editProblemModal, generateModal, editStudentModal;
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
problemModal = new bootstrap.Modal(document.getElementById('addProblemModal'));
|
||||||
|
editProblemModal = new bootstrap.Modal(document.getElementById('editProblemModal'));
|
||||||
|
generateModal = new bootstrap.Modal(document.getElementById('generatePlanModal'));
|
||||||
|
editStudentModal = new bootstrap.Modal(document.getElementById('editStudentModal'));
|
||||||
|
|
||||||
|
// 填充编辑学员表单初始值
|
||||||
|
document.getElementById('editStudentName').value = document.getElementById('detailName').textContent;
|
||||||
|
document.getElementById('editStudentPhone').value = document.getElementById('detailPhone').textContent === '未填写' ? '' : document.getElementById('detailPhone').textContent;
|
||||||
|
document.getElementById('editStudentWechat').value = document.getElementById('detailWechatNickname').textContent === '未填写' ? '' : document.getElementById('detailWechatNickname').textContent;
|
||||||
|
document.getElementById('editStudentPracticeTime').value = document.getElementById('detailPracticeTime').textContent;
|
||||||
|
document.getElementById('editStudentNotes').value = document.getElementById('detailNotes').textContent === '无备注' ? '' : document.getElementById('detailNotes').textContent;
|
||||||
|
|
||||||
|
loadProblems();
|
||||||
|
loadPlans();
|
||||||
|
loadProblemOptions();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadProblemOptions() {
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/api/problems');
|
||||||
|
const problems = await resp.json();
|
||||||
|
const select = document.getElementById('addProblemSelect');
|
||||||
|
select.innerHTML = '<option value="">选择问题...</option>';
|
||||||
|
problems.forEach(p => {
|
||||||
|
select.innerHTML += `<option value="${p.id}">${p.no} - ${p.name}</option>`;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载问题列表失败', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadProblems() {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/students/${currentStudentId}/problems`);
|
||||||
|
const problems = await resp.json();
|
||||||
|
renderProblemList(problems);
|
||||||
|
} catch (e) {
|
||||||
|
document.getElementById('problemList').innerHTML = '<p class="text-danger">加载失败</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderProblemList(problems) {
|
||||||
|
const container = document.getElementById('problemList');
|
||||||
|
if (problems.length === 0) {
|
||||||
|
container.innerHTML = '<p class="text-muted">暂无问题记录</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
container.innerHTML = problems.map(p => `
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2 p-2 border rounded">
|
||||||
|
<div>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-sm btn-outline-primary me-1" onclick="showEditProblemModal(${p.id}, '${p.problem_name}', '${p.severity}', '${p.level}')">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-danger" onclick="deleteProblem(${p.id})">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
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>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPlanList(plans) {
|
||||||
|
const container = document.getElementById('planList');
|
||||||
|
if (plans.length === 0) {
|
||||||
|
container.innerHTML = '<p class="text-muted">暂无练习方案</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
container.innerHTML = plans.map(p => `
|
||||||
|
<div class="d-flex align-items-center mb-2 p-2 border rounded">
|
||||||
|
<a href="/plan/${p.id}" class="btn btn-sm btn-outline-primary me-2">查看</a>
|
||||||
|
<span class="flex-grow-1">${p.created_at ? p.created_at.substring(0, 10) : '未知'}</span>
|
||||||
|
<button class="btn btn-sm btn-outline-danger" onclick="deletePlan(${p.id})">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showEditStudentModal() {
|
||||||
|
editStudentModal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveStudentEdit() {
|
||||||
|
const data = {
|
||||||
|
name: document.getElementById('editStudentName').value,
|
||||||
|
phone: document.getElementById('editStudentPhone').value,
|
||||||
|
wechat_nickname: document.getElementById('editStudentWechat').value,
|
||||||
|
practice_time: document.getElementById('editStudentPracticeTime').value,
|
||||||
|
notes: document.getElementById('editStudentNotes').value,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/students/${currentStudentId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
if (resp.ok) {
|
||||||
|
editStudentModal.hide();
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('保存失败');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('保存失败: ' + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteStudent() {
|
||||||
|
if (!confirm('确定删除该学员?所有问题和方案都将被删除!')) return;
|
||||||
|
fetch(`/api/students/${currentStudentId}`, {method: 'DELETE'})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(() => window.location.href = '/students');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAddProblemModal() {
|
||||||
|
document.getElementById('addProblemSelect').value = '';
|
||||||
|
document.getElementById('addProblemSeverity').value = '中等';
|
||||||
|
document.getElementById('addProblemLevel').value = '入门';
|
||||||
|
problemModal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveAddProblem() {
|
||||||
|
const problemId = document.getElementById('addProblemSelect').value;
|
||||||
|
const severity = document.getElementById('addProblemSeverity').value;
|
||||||
|
const level = document.getElementById('addProblemLevel').value;
|
||||||
|
|
||||||
|
if (!problemId) {
|
||||||
|
alert('请选择问题');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/students/${currentStudentId}/problems`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({
|
||||||
|
problems: [{problem_id: parseInt(problemId), severity, level}]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if (resp.ok) {
|
||||||
|
problemModal.hide();
|
||||||
|
loadProblems();
|
||||||
|
} else {
|
||||||
|
const err = await resp.json();
|
||||||
|
alert('添加失败: ' + (err.error || '未知错误'));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('添加失败: ' + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showEditProblemModal(id, name, severity, level) {
|
||||||
|
document.getElementById('editProblemId').value = id;
|
||||||
|
document.getElementById('editProblemName').value = name;
|
||||||
|
document.getElementById('editProblemSeverity').value = severity;
|
||||||
|
document.getElementById('editProblemLevel').value = level;
|
||||||
|
editProblemModal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveProblemEdit() {
|
||||||
|
const id = document.getElementById('editProblemId').value;
|
||||||
|
const severity = document.getElementById('editProblemSeverity').value;
|
||||||
|
const level = document.getElementById('editProblemLevel').value;
|
||||||
|
|
||||||
|
// 获取当前问题列表找到对应的 problem_db_id
|
||||||
|
const resp = await fetch(`/api/students/${currentStudentId}/problems`);
|
||||||
|
const problems = await resp.json();
|
||||||
|
const problem = problems.find(p => p.id === parseInt(id));
|
||||||
|
if (!problem) {
|
||||||
|
alert('问题不存在');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用更新 API
|
||||||
|
try {
|
||||||
|
const updateResp = await fetch(`/api/students/${currentStudentId}/problems/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({severity, level})
|
||||||
|
});
|
||||||
|
if (updateResp.ok) {
|
||||||
|
editProblemModal.hide();
|
||||||
|
loadProblems();
|
||||||
|
} else {
|
||||||
|
alert('更新失败');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('更新失败: ' + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteProblem(id) {
|
||||||
|
if (!confirm('确定删除该问题?')) return;
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/students/${currentStudentId}/problems/${id}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
if (resp.ok) {
|
||||||
|
loadProblems();
|
||||||
|
} else {
|
||||||
|
alert('删除失败');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('删除失败: ' + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generatePlan() {
|
||||||
|
const progressBar = document.getElementById('progressBar');
|
||||||
|
const progressText = document.getElementById('progressText');
|
||||||
|
const progressLog = document.getElementById('progressLog');
|
||||||
|
|
||||||
|
progressBar.style.width = '0%';
|
||||||
|
progressBar.textContent = '0%';
|
||||||
|
progressText.textContent = '准备中...';
|
||||||
|
progressLog.innerHTML = '';
|
||||||
|
generateModal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startGeneratePlan() {
|
||||||
|
const progressBar = document.getElementById('progressBar');
|
||||||
|
const progressText = document.getElementById('progressText');
|
||||||
|
const progressLog = document.getElementById('progressLog');
|
||||||
|
const useAi = document.getElementById('useAiReport').checked;
|
||||||
|
|
||||||
|
progressBar.style.width = '0%';
|
||||||
|
progressText.textContent = '准备中...';
|
||||||
|
progressLog.innerHTML = '';
|
||||||
|
|
||||||
|
function addLog(msg, isError = false) {
|
||||||
|
const time = new Date().toLocaleTimeString();
|
||||||
|
progressLog.innerHTML += `<div class="${isError ? 'text-danger' : 'text-muted'} small">${time} ${msg}</div>`;
|
||||||
|
progressLog.scrollTop = progressLog.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/generate-plan', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({student_id: currentStudentId, use_ai: useAi})
|
||||||
|
});
|
||||||
|
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const {done, value} = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
const text = decoder.decode(value);
|
||||||
|
const lines = text.split('\n');
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith('data: ')) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(line.slice(6));
|
||||||
|
|
||||||
|
progressBar.style.width = data.progress + '%';
|
||||||
|
progressBar.textContent = data.progress + '%';
|
||||||
|
progressText.textContent = data.message;
|
||||||
|
|
||||||
|
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 └ ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addLog(logMsg);
|
||||||
|
|
||||||
|
if (data.step === 'complete') {
|
||||||
|
setTimeout(() => {
|
||||||
|
generateModal.hide();
|
||||||
|
alert('方案生成成功!');
|
||||||
|
loadPlans();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
addLog(data.error, true);
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
addLog('请求失败:' + err.message, true);
|
||||||
|
progressText.textContent = '生成失败';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deletePlan(id) {
|
||||||
|
if (!confirm('确定删除该方案?')) return;
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/plans/${id}`, {method: 'DELETE'});
|
||||||
|
if (resp.ok) {
|
||||||
|
loadPlans();
|
||||||
|
} else {
|
||||||
|
alert('删除失败');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('删除失败: ' + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showToast(message, type) {
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = 'toast-message alert alert-' + (type === 'success' ? 'success' : 'danger');
|
||||||
|
toast.style.cssText = 'position:fixed;top:20px;right:20px;z-index:9999;min-width:200px;';
|
||||||
|
toast.textContent = message;
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
setTimeout(() => toast.remove(), 3000);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user