feat: 添加学员详情/方案编辑/方案列表新页面
- student.html: 学员详情页,支持编辑/添加/删除问题 - plan_edit.html: 方案编辑页 - plans.html: 方案列表页 - home.html: 首页
This commit is contained in:
@@ -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 %}
|
||||
Reference in New Issue
Block a user