1196 lines
49 KiB
HTML
1196 lines
49 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}钢琴练习方案管理系统{% endblock %}
|
||
|
||
{% block page_css %}
|
||
<style>
|
||
.student-card { cursor: pointer; transition: transform 0.2s; }
|
||
.student-card:hover { transform: translateY(-3px); }
|
||
.problem-tag { font-size: 12px; padding: 3px 8px; margin: 2px; display: inline-block; }
|
||
.severity-轻微 { background: #d4edda; color: #155724; }
|
||
.severity-中等 { background: #fff3cd; color: #856404; }
|
||
.severity-严重 { background: #f8d7da; color: #721c24; }
|
||
.modal-body { max-height: 70vh; overflow-y: auto; }
|
||
.problem-checkbox { margin-bottom: 8px; }
|
||
.problem-row { padding: 10px; border-bottom: 1px solid #eee; }
|
||
.problem-row:last-child { border-bottom: none; }
|
||
.markdown-body h1 { font-size: 1.5rem; margin-bottom: 1rem; border-bottom: 2px solid #dee2e6; padding-bottom: 0.5rem; }
|
||
.markdown-body h2 { font-size: 1.25rem; margin: 1.5rem 0 0.75rem; }
|
||
.markdown-body h3 { font-size: 1.1rem; margin: 1rem 0 0.5rem; }
|
||
.markdown-body p { margin: 0.5rem 0; line-height: 1.6; }
|
||
.markdown-body ul, .markdown-body ol { padding-left: 1.5rem; margin: 0.5rem 0; }
|
||
.markdown-body table { border-collapse: collapse; width: 100%; margin: 1rem 0; }
|
||
.markdown-body th, .markdown-body td { border: 1px solid #dee2e6; padding: 8px 12px; text-align: left; }
|
||
.markdown-body th { background: #f8f9fa; }
|
||
.markdown-body code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; font-size: 0.9em; }
|
||
.markdown-body blockquote { border-left: 4px solid #dee2e6; margin: 1rem 0; padding: 0.5rem 1rem; background: #f8f9fa; }
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block sidebar_nav %}
|
||
<a class="nav-link active" href="#" onclick="showStudentList()">
|
||
<i class="bi bi-people"></i> 学员管理
|
||
</a>
|
||
<a class="nav-link" href="#" onclick="showSettings()" id="settingsNav">
|
||
<i class="bi bi-gear"></i> 问题配置
|
||
</a>
|
||
<a class="nav-link" href="/api-settings" id="apiSettingsNav" style="display:none;">
|
||
<i class="bi bi-key"></i> API设置
|
||
</a>
|
||
<a class="nav-link" href="/templates" id="templatesNav" style="display:none;">
|
||
<i class="bi bi-file-earmark-text"></i> 模板管理
|
||
</a>
|
||
<a class="nav-link" href="/classes" id="classesNav" style="display:none;">
|
||
<i class="bi bi-collection"></i> 班级管理
|
||
</a>
|
||
<a class="nav-link" href="/users" id="usersNav" style="display:none;">
|
||
<i class="bi bi-person-badge"></i> 用户管理
|
||
</a>
|
||
<hr>
|
||
<a class="nav-link" href="#" onclick="showChangePasswordModal()">
|
||
<i class="bi bi-key"></i> 修改密码
|
||
</a>
|
||
<a class="nav-link" href="#" onclick="logout()">
|
||
<i class="bi bi-box-arrow-right"></i> 退出登录
|
||
</a>
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<!-- 学员列表页面 -->
|
||
<div id="studentListPage">
|
||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||
<div class="d-flex align-items-center gap-3">
|
||
<h4><i class="bi bi-people"></i> 学员列表</h4>
|
||
<select class="form-select form-select-sm" style="width:auto;" id="classFilter" onchange="loadStudents()">
|
||
<option value="">全部班级</option>
|
||
</select>
|
||
<input type="text" class="form-control form-control-sm" style="width:150px;" placeholder="搜索姓名..." id="nameFilter" oninput="loadStudents()">
|
||
</div>
|
||
<div class="btn-group">
|
||
<button class="btn btn-outline-secondary" onclick="downloadTemplate()">
|
||
<i class="bi bi-download"></i> 模板下载
|
||
</button>
|
||
<button class="btn btn-outline-primary" onclick="document.getElementById('importFileInput').click()">
|
||
<i class="bi bi-upload"></i> CSV导入
|
||
</button>
|
||
<input type="file" id="importFileInput" accept=".csv" style="display:none" onchange="importStudents(this)">
|
||
<button class="btn btn-outline-success" onclick="exportStudents()">
|
||
<i class="bi bi-file-earmark-arrow-up"></i> 导出学员
|
||
</button>
|
||
<button class="btn btn-primary" onclick="showAddStudentModal()">
|
||
<i class="bi bi-plus-lg"></i> 新增学员
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row" id="studentList">
|
||
<!-- 学员卡片将通过JS动态加载 -->
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 学员详情页面 -->
|
||
<div id="studentDetailPage" style="display: none;">
|
||
<button class="btn btn-link mb-3" onclick="showStudentList()">
|
||
<i class="bi bi-arrow-left"></i> 返回列表
|
||
</button>
|
||
|
||
<div class="row">
|
||
<div class="col-md-4">
|
||
<div class="card mb-3">
|
||
<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">学员姓名</h5>
|
||
<p class="text-muted" id="detailWechatNickname">微信昵称</p>
|
||
<p class="text-muted" id="detailPhone">电话</p>
|
||
<p class="small"><span class="badge bg-info" id="detailPracticeTime">30分钟</span></p>
|
||
<p class="small" id="detailClass"></p>
|
||
<p class="small text-muted" id="detailNotes">备注</p>
|
||
<button type="button" class="btn btn-sm btn-outline-primary" onclick="editStudent()">
|
||
<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-3">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<h6 class="mb-0">问题记录</h6>
|
||
<button class="btn btn-sm btn-primary" onclick="showProblemsModal()">
|
||
<i class="bi bi-plus-lg"></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">练习方案</h6>
|
||
<div class="d-flex gap-2 align-items-center">
|
||
<select class="form-select form-select-sm" id="aiTemplateSelect" style="width: auto;">
|
||
<option value="">加载中...</option>
|
||
</select>
|
||
<button class="btn btn-sm btn-success" onclick="generatePlan()">
|
||
<i class="bi bi-magic"></i> 生成方案
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="card-body" id="planList">
|
||
<p class="text-muted">暂无练习方案</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 新增/编辑学员模态框 -->
|
||
<div class="modal fade" id="addStudentModal" tabindex="-1">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="studentModalTitle">新增学员</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>
|
||
<input type="text" class="form-control" id="studentName" required>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">电话</label>
|
||
<input type="text" class="form-control" id="studentPhone">
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">微信昵称</label>
|
||
<input type="text" class="form-control" id="studentWechatNickname">
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">每日练习时间</label>
|
||
<select class="form-select" id="studentPracticeTime">
|
||
<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>
|
||
<small class="text-muted">生成方案时将使用此练习时间</small>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">班级</label>
|
||
<select class="form-select" id="studentClassId">
|
||
<option value="">未分配班级</option>
|
||
</select>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">备注</label>
|
||
<textarea class="form-control" id="studentNotes" 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="saveStudent()">保存</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 问题记录模态框 -->
|
||
<div class="modal fade" id="problemsModal" tabindex="-1">
|
||
<div class="modal-dialog modal-lg">
|
||
<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="alert alert-info">
|
||
<i class="bi bi-info-circle"></i>
|
||
练习时间请在学员信息中统一设置
|
||
</div>
|
||
<div id="problemCheckboxes">
|
||
<!-- 问题复选框将通过JS动态生成 -->
|
||
</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="saveProblems()">保存</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 方案查看模态框 -->
|
||
<div class="modal fade" id="planDetailModal" tabindex="-1">
|
||
<div class="modal-dialog modal-lg">
|
||
<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" id="planDetailContent">
|
||
<!-- 方案内容将通过JS动态生成 -->
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||
<button type="button" class="btn btn-warning" onclick="editPlanContent()">
|
||
<i class="bi bi-edit"></i> 编辑内容
|
||
</button>
|
||
<select class="form-select form-select-sm" id="reportTemplateSelect" style="width: auto;">
|
||
<option value="">加载中...</option>
|
||
</select>
|
||
<button type="button" class="btn btn-primary" onclick="downloadMD()">
|
||
<i class="bi bi-file-markdown"></i> 下载MD
|
||
</button>
|
||
<button type="button" class="btn btn-success" onclick="downloadPDF()">
|
||
<i class="bi bi-download"></i> 下载PDF
|
||
</button>
|
||
<button type="button" class="btn btn-info" onclick="showWechatCard()">
|
||
<i class="bi bi-wechat"></i> 微信卡片
|
||
</button>
|
||
<button type="button" class="btn btn-outline-secondary" onclick="previewReportTemplate()">
|
||
<i class="bi bi-eye"></i> 预览报告
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 编辑方案内容弹窗 -->
|
||
<div class="modal fade" id="editPlanContentModal" tabindex="-1">
|
||
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title"><i class="bi bi-edit"></i> 编辑方案内容</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-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>
|
||
<li class="nav-item" role="presentation">
|
||
<button class="nav-link" id="basicInfo-tab" data-bs-toggle="tab" data-bs-target="#basicInfoPane" 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="max-height: 400px;"></div>
|
||
<button type="button" class="btn btn-outline-primary btn-sm mt-2" onclick="addScheduleRow()">
|
||
<i class="bi bi-plus"></i> 添加一行
|
||
</button>
|
||
</div>
|
||
<div class="tab-pane fade" id="basicInfoPane" role="tabpanel">
|
||
<div class="row">
|
||
<div class="col-md-6 mb-3">
|
||
<label class="form-label">练习时间</label>
|
||
<input type="text" class="form-control" id="editPracticeTime" readonly>
|
||
</div>
|
||
<div class="col-md-6 mb-3">
|
||
<label class="form-label">生成时间</label>
|
||
<input type="text" class="form-control" id="editGeneratedAt" readonly>
|
||
</div>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">问题数量</label>
|
||
<input type="text" class="form-control" id="editProblemsCount" readonly>
|
||
</div>
|
||
</div>
|
||
</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="savePlanContent()">
|
||
<i class="bi bi-save"></i> 保存
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 报告模板预览模态框 -->
|
||
<div class="modal fade" id="reportPreviewModal" tabindex="-1">
|
||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title"><i class="bi bi-eye"></i> 报告预览</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div id="reportPreviewContent" class="markdown-body" style="background: #f8f9fa; padding: 15px; border-radius: 8px; min-height: 200px;"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 进度显示模态框 -->
|
||
<div class="modal fade" id="progressModal" tabindex="-1" data-bs-backdrop="static">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title">生成练习方案</h5>
|
||
</div>
|
||
<div class="modal-body">
|
||
<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: 150px; overflow-y: auto; font-family: monospace;">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/tabulator-tables@5/dist/js/tabulator.min.js"></script>
|
||
<script>
|
||
let currentStudentId = null;
|
||
let currentPlanId = null;
|
||
|
||
// 问题列表(从后端传入)
|
||
const problemList = {{ problem_list | tojson }};
|
||
const severityLevels = {{ severity_levels | tojson }};
|
||
const practiceTimeOptions = {{ practice_time_options | tojson }};
|
||
|
||
// 初始化
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
fetch('/api/check-login').then(r => r.json()).then(data => {
|
||
if (!data.logged_in) {
|
||
window.location.href = '/login';
|
||
return;
|
||
}
|
||
const ROLE_LABELS = { 'admin': '管理员', 'user': '普通用户' };
|
||
|
||
const userDisplay = data.username + ' (' + (ROLE_LABELS[data.role] || data.role) + ')';
|
||
document.getElementById('currentUserDisplay').textContent = userDisplay;
|
||
const mobileDisplay = document.getElementById('mobileUserDisplay');
|
||
if (mobileDisplay) mobileDisplay.textContent = userDisplay;
|
||
|
||
if (data.role === 'admin') {
|
||
document.getElementById('usersNav').style.display = '';
|
||
document.getElementById('settingsNav').style.display = '';
|
||
document.getElementById('apiSettingsNav').style.display = '';
|
||
document.getElementById('templatesNav').style.display = '';
|
||
} else {
|
||
document.getElementById('settingsNav').style.display = '';
|
||
document.getElementById('apiSettingsNav').style.display = 'none';
|
||
document.getElementById('templatesNav').style.display = 'none';
|
||
}
|
||
document.getElementById('classesNav').style.display = '';
|
||
loadAiTemplates();
|
||
loadReportTemplates();
|
||
}).catch(() => {
|
||
window.location.href = '/login';
|
||
});
|
||
|
||
loadClassFilter();
|
||
loadStudents();
|
||
initProblemCheckboxes();
|
||
});
|
||
|
||
// 加载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('');
|
||
}
|
||
} catch (e) {
|
||
console.error('加载AI模板失败:', e);
|
||
}
|
||
}
|
||
|
||
// 加载报告模板列表
|
||
async function loadReportTemplates() {
|
||
try {
|
||
const resp = await fetch('/templates/templates?type=report');
|
||
if (resp.ok) {
|
||
const templates = await resp.json();
|
||
const select = document.getElementById('reportTemplateSelect');
|
||
select.innerHTML = templates.map(t =>
|
||
`<option value="${t.id}">${t.name}</option>`
|
||
).join('');
|
||
}
|
||
} catch (e) {
|
||
console.error('加载报告模板失败:', e);
|
||
}
|
||
}
|
||
|
||
// 下载CSV模板
|
||
function downloadTemplate() {
|
||
window.location.href = '/api/students/template';
|
||
}
|
||
|
||
// 导出学员CSV
|
||
function exportStudents() {
|
||
window.location.href = '/api/students/export';
|
||
}
|
||
|
||
// 导入CSV学员
|
||
function importStudents(input) {
|
||
const file = input.files[0];
|
||
if (!file) return;
|
||
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
fetch('/api/students/import', {
|
||
method: 'POST',
|
||
body: formData
|
||
})
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
if (data.error) {
|
||
alert(data.error);
|
||
if (data.error_details) {
|
||
alert('错误详情:\n' + data.error_details.join('\n'));
|
||
}
|
||
} else {
|
||
alert(data.message);
|
||
loadStudents();
|
||
}
|
||
})
|
||
.catch(err => {
|
||
console.error(err);
|
||
alert('导入失败');
|
||
});
|
||
|
||
input.value = '';
|
||
}
|
||
|
||
// 加载学员列表
|
||
async function loadStudents() {
|
||
const classId = document.getElementById('classFilter').value;
|
||
const name = document.getElementById('nameFilter').value;
|
||
|
||
let url = '/api/students?';
|
||
if (classId) url += 'class_id=' + classId + '&';
|
||
if (name) url += 'name=' + encodeURIComponent(name);
|
||
|
||
const response = await fetch(url);
|
||
if (response.status === 401) {
|
||
window.location.href = '/login';
|
||
return;
|
||
}
|
||
const students = await response.json();
|
||
renderStudentList(students);
|
||
}
|
||
|
||
// 加载班级筛选选项
|
||
async function loadClassFilter() {
|
||
try {
|
||
const response = await fetch('/api/classes');
|
||
const classes = await response.json();
|
||
const select = document.getElementById('classFilter');
|
||
classes.forEach(c => {
|
||
const option = document.createElement('option');
|
||
option.value = c.id;
|
||
option.textContent = c.name;
|
||
select.appendChild(option);
|
||
});
|
||
} catch (e) {
|
||
console.error('加载班级失败', e);
|
||
}
|
||
}
|
||
|
||
// 渲染学员列表
|
||
function renderStudentList(students) {
|
||
const container = document.getElementById('studentList');
|
||
if (students.length === 0) {
|
||
container.innerHTML = '<div class="col-12 text-center text-muted py-5"><p>暂无学员,请添加</p></div>';
|
||
return;
|
||
}
|
||
|
||
let html = '';
|
||
students.forEach(s => {
|
||
html += `
|
||
<div class="col-md-4 col-sm-6 mb-3">
|
||
<div class="card student-card" onclick="showStudentDetail(${s.id})">
|
||
<div class="card-body">
|
||
<h5 class="card-title">${s.name}</h5>
|
||
<p class="card-text text-muted small">${s.wechat_nickname || ''} ${s.phone ? '| ' + s.phone : ''}</p>
|
||
<span class="badge bg-info">${s.practice_time}</span>
|
||
<span class="badge bg-secondary">${s.problem_count} 个问题</span>
|
||
<span class="badge bg-primary">${s.plan_count} 个方案</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
// 初始化问题复选框
|
||
function initProblemCheckboxes() {
|
||
const container = document.getElementById('problemCheckboxes');
|
||
const levelOptions = ['启蒙', '入门', '进阶', '熟练', '精通'];
|
||
let html = '';
|
||
|
||
problemList.forEach(p => {
|
||
html += `
|
||
<div class="problem-row">
|
||
<div class="form-check">
|
||
<input class="form-check-input problem-checkbox-input" type="checkbox" value="${p.id}" id="prob_${p.id}" data-name="${p.name}">
|
||
<label class="form-check-label fw-bold" for="prob_${p.id}">${p.name}</label>
|
||
</div>
|
||
<div class="mt-2 ps-4" id="severity_${p.id}" style="display:none;">
|
||
<div class="mb-2">
|
||
<small class="text-muted">严重程度:</small>
|
||
<div class="btn-group btn-group-sm" role="group">
|
||
${severityLevels.map((sev, idx) => `
|
||
<input type="radio" class="btn-check" name="severity_${p.id}" id="sev_${p.id}_${idx}" value="${sev}" ${idx===1 ? 'checked' : ''}>
|
||
<label class="btn btn-outline-${idx===2 ? 'danger' : idx===1 ? 'warning' : 'success'}" for="sev_${p.id}_${idx}">${sev}</label>
|
||
`).join('')}
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<small class="text-muted">当前级别:</small>
|
||
<div class="btn-group btn-group-sm" role="group">
|
||
${levelOptions.map((lvl, idx) => `
|
||
<input type="radio" class="btn-check" name="level_${p.id}" id="lvl_${p.id}_${idx}" value="${lvl}" ${idx===1 ? 'checked' : ''}>
|
||
<label class="btn btn-outline-secondary" for="lvl_${p.id}_${idx}">${lvl}</label>
|
||
`).join('')}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
container.innerHTML = html;
|
||
|
||
document.querySelectorAll('.problem-checkbox-input').forEach(cb => {
|
||
cb.addEventListener('change', function() {
|
||
const problemId = this.value;
|
||
const severityDiv = document.getElementById(`severity_${problemId}`);
|
||
severityDiv.style.display = this.checked ? 'block' : 'none';
|
||
});
|
||
});
|
||
}
|
||
|
||
// 显示新增学员模态框
|
||
function showAddStudentModal() {
|
||
document.getElementById('studentModalTitle').textContent = '新增学员';
|
||
document.getElementById('studentName').value = '';
|
||
document.getElementById('studentPhone').value = '';
|
||
document.getElementById('studentWechatNickname').value = '';
|
||
document.getElementById('studentPracticeTime').value = '30分钟';
|
||
document.getElementById('studentNotes').value = '';
|
||
document.getElementById('studentClassId').value = '';
|
||
currentStudentId = null;
|
||
fetch('/api/classes').then(r => r.json()).then(classes => {
|
||
const select = document.getElementById('studentClassId');
|
||
select.innerHTML = '<option value="">未分配班级</option>' +
|
||
classes.map(c => `<option value="${c.id}">${c.name}</option>`).join('');
|
||
});
|
||
new bootstrap.Modal(document.getElementById('addStudentModal')).show();
|
||
}
|
||
|
||
// 编辑学员
|
||
function editStudent() {
|
||
document.getElementById('studentModalTitle').textContent = '编辑学员';
|
||
document.getElementById('studentName').value = document.getElementById('detailName').textContent;
|
||
document.getElementById('studentPhone').value = document.getElementById('detailPhone').textContent === '未填写' ? '' : document.getElementById('detailPhone').textContent;
|
||
fetch(`/api/students/${currentStudentId}`)
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
document.getElementById('studentWechatNickname').value = data.student.wechat_nickname || '';
|
||
document.getElementById('studentPracticeTime').value = data.student.practice_time || '30分钟';
|
||
fetch('/api/classes').then(r => r.json()).then(classes => {
|
||
const select = document.getElementById('studentClassId');
|
||
select.innerHTML = '<option value="">未分配班级</option>' +
|
||
classes.map(c => `<option value="${c.id}">${c.name}</option>`).join('');
|
||
select.value = data.student.class_id || '';
|
||
});
|
||
});
|
||
document.getElementById('studentNotes').value = document.getElementById('detailNotes').textContent === '无备注' ? '' : document.getElementById('detailNotes').textContent;
|
||
new bootstrap.Modal(document.getElementById('addStudentModal')).show();
|
||
}
|
||
|
||
// 保存学员
|
||
async function saveStudent() {
|
||
const name = document.getElementById('studentName').value.trim();
|
||
if (!name) { alert('请输入姓名'); return; }
|
||
|
||
const classId = document.getElementById('studentClassId').value;
|
||
const data = {
|
||
name: name,
|
||
phone: document.getElementById('studentPhone').value.trim(),
|
||
wechat_nickname: document.getElementById('studentWechatNickname').value.trim(),
|
||
practice_time: document.getElementById('studentPracticeTime').value,
|
||
notes: document.getElementById('studentNotes').value.trim(),
|
||
class_id: classId ? parseInt(classId) : null
|
||
};
|
||
|
||
let response;
|
||
if (currentStudentId) {
|
||
response = await fetch(`/api/students/${currentStudentId}`, {
|
||
method: 'PUT',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify(data)
|
||
});
|
||
} else {
|
||
response = await fetch('/api/students', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify(data)
|
||
});
|
||
}
|
||
|
||
if (response.ok) {
|
||
bootstrap.Modal.getInstance(document.getElementById('addStudentModal')).hide();
|
||
if (currentStudentId) {
|
||
showStudentDetail(currentStudentId);
|
||
} else {
|
||
loadStudents();
|
||
}
|
||
}
|
||
}
|
||
|
||
// 显示学员详情
|
||
async function showStudentDetail(studentId) {
|
||
currentStudentId = studentId;
|
||
|
||
const response = await fetch(`/api/students/${studentId}`);
|
||
const data = await response.json();
|
||
|
||
document.getElementById('detailName').textContent = data.student.name;
|
||
document.getElementById('detailWechatNickname').textContent = data.student.wechat_nickname || '未填写';
|
||
document.getElementById('detailPhone').textContent = data.student.phone || '未填写';
|
||
document.getElementById('detailPracticeTime').textContent = data.student.practice_time || '30分钟';
|
||
|
||
const classEl = document.getElementById('detailClass');
|
||
if (data.student.class_name) {
|
||
classEl.innerHTML = '<span class="badge bg-secondary">' + data.student.class_name + '</span>';
|
||
} else {
|
||
classEl.innerHTML = '<span class="text-muted">未分配班级</span>';
|
||
}
|
||
|
||
document.getElementById('detailNotes').textContent = data.student.notes || '无备注';
|
||
|
||
renderProblemList(data.problems);
|
||
renderPlanList(data.plans);
|
||
|
||
document.getElementById('studentListPage').style.display = 'none';
|
||
document.getElementById('studentDetailPage').style.display = 'block';
|
||
}
|
||
|
||
// 返回学员列表
|
||
function showStudentList() {
|
||
document.getElementById('studentListPage').style.display = 'block';
|
||
document.getElementById('studentDetailPage').style.display = 'none';
|
||
currentStudentId = null;
|
||
loadStudents();
|
||
}
|
||
|
||
// 渲染问题列表
|
||
function renderProblemList(problems) {
|
||
const container = document.getElementById('problemList');
|
||
if (problems.length === 0) {
|
||
container.innerHTML = '<p class="text-muted">暂无问题记录</p>';
|
||
return;
|
||
}
|
||
|
||
const levelOptions = ['启蒙', '入门', '进阶', '熟练', '精通'];
|
||
const severityLevels = ['轻微', '中等', '严重'];
|
||
|
||
let html = '';
|
||
problems.forEach(p => {
|
||
const currentLevel = p.level || '入门';
|
||
const currentSeverity = p.severity || '中等';
|
||
|
||
const levelSelect = levelOptions.map(l =>
|
||
`<option value="${l}" ${l === currentLevel ? 'selected' : ''}>${l}</option>`
|
||
).join('');
|
||
|
||
const severitySelect = severityLevels.map((s, idx) => {
|
||
const color = idx === 2 ? 'danger' : idx === 1 ? 'warning' : 'success';
|
||
return `<option value="${s}" ${s === currentSeverity ? 'selected' : ''}>${s}</option>`;
|
||
}).join('');
|
||
|
||
html += `
|
||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||
<span>${p.problem_name}</span>
|
||
<div class="d-flex align-items-center gap-2">
|
||
<select class="form-select form-select-sm" style="width: 85px;" onchange="updateProblemField(${p.id}, 'level', this.value)">
|
||
${levelSelect}
|
||
</select>
|
||
<select class="form-select form-select-sm" style="width: 70px;" onchange="updateProblemField(${p.id}, 'severity', this.value)">
|
||
${severitySelect}
|
||
</select>
|
||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteProblem(${p.id})"><i class="bi bi-trash"></i></button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
// 更新问题字段
|
||
async function updateProblemField(problemId, field, value) {
|
||
const response = await fetch(`/api/students/${currentStudentId}/problems`);
|
||
const problems = await response.json();
|
||
|
||
const problem = problems.find(p => p.id === problemId);
|
||
if (!problem) return;
|
||
|
||
await fetch(`/api/students/${currentStudentId}/problems/${problem.problem_id}`, {method: 'DELETE'});
|
||
|
||
await fetch(`/api/students/${currentStudentId}/problems`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({
|
||
problems: [{
|
||
problem_id: problem.problem_id,
|
||
problem_name: problem.problem_name,
|
||
severity: field === 'severity' ? value : problem.severity,
|
||
level: field === 'level' ? value : (problem.level || '入门')
|
||
}]
|
||
})
|
||
});
|
||
}
|
||
|
||
// 删除单个问题
|
||
async function deleteProblem(problemId) {
|
||
const response = await fetch(`/api/students/${currentStudentId}/problems`);
|
||
const problems = await response.json();
|
||
const problem = problems.find(p => p.id === problemId);
|
||
if (!problem) return;
|
||
|
||
if (!confirm(`确定要删除问题"${problem.problem_name}"吗?`)) return;
|
||
|
||
await fetch(`/api/students/${currentStudentId}/problems/${problem.problem_id}`, {method: 'DELETE'});
|
||
showStudentDetail(currentStudentId);
|
||
}
|
||
|
||
// 渲染方案列表
|
||
function renderPlanList(plans) {
|
||
const container = document.getElementById('planList');
|
||
if (plans.length === 0) {
|
||
container.innerHTML = '<p class="text-muted">暂无练习方案</p>';
|
||
return;
|
||
}
|
||
|
||
let html = '';
|
||
plans.forEach(p => {
|
||
html += `
|
||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||
<span>生成于 ${p.created_at}</span>
|
||
<div>
|
||
<button class="btn btn-sm btn-outline-primary" onclick="viewPlan(${p.id})">查看</button>
|
||
<button class="btn btn-sm btn-outline-danger" onclick="deletePlan(${p.id})">删除</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
// 显示问题记录模态框
|
||
function showProblemsModal() {
|
||
fetch(`/api/students/${currentStudentId}/problems`)
|
||
.then(r => r.json())
|
||
.then(currentProblems => {
|
||
const currentProblemIds = currentProblems.map(p => p.problem_id);
|
||
|
||
document.querySelectorAll('.problem-checkbox-input').forEach(cb => {
|
||
const isChecked = currentProblemIds.includes(cb.value);
|
||
cb.checked = isChecked;
|
||
|
||
const severityDiv = document.getElementById(`severity_${cb.value}`);
|
||
severityDiv.style.display = isChecked ? 'block' : 'none';
|
||
|
||
if (isChecked) {
|
||
const problem = currentProblems.find(p => p.problem_id === cb.value);
|
||
if (problem) {
|
||
const levelRadios = document.querySelectorAll(`input[name="level_${cb.value}"]`);
|
||
levelRadios.forEach(r => {
|
||
r.checked = r.value === (problem.level || '入门');
|
||
});
|
||
const severityRadios = document.querySelectorAll(`input[name="severity_${cb.value}"]`);
|
||
severityRadios.forEach(r => {
|
||
r.checked = r.value === (problem.severity || '中等');
|
||
});
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
new bootstrap.Modal(document.getElementById('problemsModal')).show();
|
||
}
|
||
|
||
// 保存问题
|
||
async function saveProblems() {
|
||
const problems = [];
|
||
|
||
document.querySelectorAll('.problem-checkbox-input:checked').forEach(cb => {
|
||
const problemId = cb.value;
|
||
const problemName = cb.dataset.name;
|
||
const severityEl = document.querySelector(`input[name="severity_${problemId}"]:checked`);
|
||
const levelEl = document.querySelector(`input[name="level_${problemId}"]:checked`);
|
||
|
||
problems.push({
|
||
problem_id: problemId,
|
||
problem_name: problemName,
|
||
severity: severityEl ? severityEl.value : '中等',
|
||
level: levelEl ? levelEl.value : '入门'
|
||
});
|
||
});
|
||
|
||
if (problems.length === 0) {
|
||
alert('请至少选择一个 问题');
|
||
return;
|
||
}
|
||
|
||
const response = await fetch(`/api/students/${currentStudentId}/problems`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({problems})
|
||
});
|
||
|
||
if (response.ok) {
|
||
bootstrap.Modal.getInstance(document.getElementById('problemsModal')).hide();
|
||
showStudentDetail(currentStudentId);
|
||
}
|
||
}
|
||
|
||
// 生成方案 - 使用SSE实时显示进度
|
||
async function generatePlan() {
|
||
if (!confirm('确定要生成练习方案吗?')) return;
|
||
|
||
const progressModal = new bootstrap.Modal(document.getElementById('progressModal'));
|
||
progressModal.show();
|
||
|
||
const progressBar = document.getElementById('progressBar');
|
||
const progressText = document.getElementById('progressText');
|
||
const progressLog = document.getElementById('progressLog');
|
||
|
||
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 templateId = document.getElementById('aiTemplateSelect').value;
|
||
const response = await fetch('/api/generate-plan', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({student_id: currentStudentId, use_ai: true, template_id: templateId || null})
|
||
});
|
||
|
||
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(() => {
|
||
progressModal.hide();
|
||
alert('方案生成成功!');
|
||
showStudentDetail(currentStudentId);
|
||
}, 500);
|
||
}
|
||
|
||
if (data.error) {
|
||
addLog(data.error, true);
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
}
|
||
}
|
||
} catch (err) {
|
||
addLog('请求失败:' + err.message, true);
|
||
progressText.textContent = '生成失败';
|
||
}
|
||
}
|
||
|
||
// 查看方案
|
||
async function viewPlan(planId) {
|
||
currentPlanId = planId;
|
||
const response = await fetch(`/api/plans/${planId}`);
|
||
const data = await response.json();
|
||
|
||
let html = `
|
||
<div class="mb-3">
|
||
<strong>学员:</strong>${data.student_name}
|
||
<strong>练习时间:</strong>${data.content.practice_time}
|
||
<strong>生成时间:</strong>${data.created_at}
|
||
</div>
|
||
<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" style="max-height: 500px; overflow-y: auto;">${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('planDetailContent').innerHTML = html;
|
||
new bootstrap.Modal(document.getElementById('planDetailModal')).show();
|
||
}
|
||
|
||
// 下载PDF
|
||
function downloadPDF() {
|
||
const templateId = document.getElementById('reportTemplateSelect').value;
|
||
const url = templateId ? `/api/plans/${currentPlanId}/pdf?template_id=${templateId}` : `/api/plans/${currentPlanId}/pdf`;
|
||
window.open(url, '_blank');
|
||
}
|
||
|
||
// 下载MD
|
||
function downloadMD() {
|
||
const templateId = document.getElementById('reportTemplateSelect').value;
|
||
const url = templateId ? `/api/plans/${currentPlanId}/md?template_id=${templateId}` : `/api/plans/${currentPlanId}/md`;
|
||
window.open(url, '_blank');
|
||
}
|
||
|
||
// 预览报告模板
|
||
async function previewReportTemplate() {
|
||
if (!currentPlanId) {
|
||
alert('请先选择一个方案');
|
||
return;
|
||
}
|
||
const templateId = document.getElementById('reportTemplateSelect').value;
|
||
const url = templateId ? `/api/plans/${currentPlanId}/md?template_id=${templateId}` : `/api/plans/${currentPlanId}/md`;
|
||
try {
|
||
const resp = await fetch(url);
|
||
if (resp.ok) {
|
||
const md = await resp.text();
|
||
document.getElementById('reportPreviewContent').innerHTML = marked.parse(md);
|
||
new bootstrap.Modal(document.getElementById('reportPreviewModal')).show();
|
||
} else {
|
||
alert('预览失败');
|
||
}
|
||
} catch (e) {
|
||
alert('预览失败: ' + e.message);
|
||
}
|
||
}
|
||
|
||
// 编辑方案内容
|
||
let planContentEditor = null;
|
||
let scheduleTable = null;
|
||
async function editPlanContent() {
|
||
const resp = await fetch(`/api/plans/${currentPlanId}`);
|
||
const data = await resp.json();
|
||
const content = typeof data.content === 'string' ? JSON.parse(data.content) : data.content;
|
||
|
||
document.getElementById('editPracticeTime').value = content.practice_time || '';
|
||
document.getElementById('editGeneratedAt').value = content.generated_at || '';
|
||
document.getElementById('editProblemsCount').value = (content.problems || []).length + ' 个问题';
|
||
|
||
if (planContentEditor) {
|
||
planContentEditor.toTextArea();
|
||
planContentEditor = null;
|
||
}
|
||
document.getElementById('editAiReport').value = content.ai_report || '';
|
||
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 || ''
|
||
});
|
||
|
||
if (scheduleTable) {
|
||
scheduleTable.destroy();
|
||
scheduleTable = null;
|
||
}
|
||
|
||
const scheduleData = (content.daily_schedule || []).map(item => ({
|
||
phase: item.phase || '',
|
||
duration: item.duration || '',
|
||
content: item.content || '',
|
||
purpose: item.purpose || ''
|
||
}));
|
||
|
||
scheduleTable = new Tabulator("#editDailyScheduleTable", {
|
||
data: scheduleData,
|
||
layout: "fitDataFill",
|
||
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();
|
||
}
|
||
}
|
||
]
|
||
});
|
||
|
||
new bootstrap.Modal(document.getElementById('editPlanContentModal')).show();
|
||
}
|
||
|
||
// 添加一行
|
||
function addScheduleRow() {
|
||
if (scheduleTable) {
|
||
scheduleTable.addRow({ phase: "新环节", duration: "5", content: "练习内容", purpose: "练习目的" });
|
||
}
|
||
}
|
||
|
||
// 保存方案内容
|
||
async function savePlanContent() {
|
||
try {
|
||
const resp = await fetch(`/api/plans/${currentPlanId}`);
|
||
const data = await resp.json();
|
||
const content = typeof data.content === 'string' ? JSON.parse(data.content) : data.content;
|
||
|
||
content.ai_report = planContentEditor ? planContentEditor.value() : document.getElementById('editAiReport').value;
|
||
|
||
if (scheduleTable) {
|
||
content.daily_schedule = scheduleTable.getData();
|
||
}
|
||
|
||
const saveResp = await fetch(`/api/plans/${currentPlanId}/content`, {
|
||
method: 'PUT',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({content: JSON.stringify(content)})
|
||
});
|
||
if (saveResp.ok) {
|
||
if (planContentEditor) {
|
||
planContentEditor.toTextArea();
|
||
planContentEditor = null;
|
||
}
|
||
if (scheduleTable) {
|
||
scheduleTable.destroy();
|
||
scheduleTable = null;
|
||
}
|
||
bootstrap.Modal.getInstance(document.getElementById('editPlanContentModal')).hide();
|
||
alert('保存成功');
|
||
showStudentDetail(currentStudentId);
|
||
} else {
|
||
alert('保存失败');
|
||
}
|
||
} catch (e) {
|
||
alert('保存失败: ' + e.message);
|
||
}
|
||
}
|
||
|
||
// 显示微信卡片
|
||
function showWechatCard() {
|
||
window.open(`/plans/${currentPlanId}/wechat`, '_blank');
|
||
}
|
||
|
||
// 删除方案
|
||
async function deletePlan(planId) {
|
||
if (!confirm('确定要删除这个方案吗?')) return;
|
||
|
||
await fetch(`/api/plans/${planId}`, {method: 'DELETE'});
|
||
showStudentDetail(currentStudentId);
|
||
}
|
||
|
||
// 删除学员
|
||
async function deleteStudent() {
|
||
if (!confirm('确定要删除该学员吗?所有相关记录将被删除!')) return;
|
||
|
||
await fetch(`/api/students/${currentStudentId}`, {method: 'DELETE'});
|
||
showStudentList();
|
||
}
|
||
|
||
// 显示设置
|
||
function showSettings() {
|
||
window.location.href = '/settings';
|
||
}
|
||
</script>
|
||
{% endblock %} |