367 lines
11 KiB
HTML
367 lines
11 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}方案详情 - 钢琴练习方案系统{% endblock %}
|
||
|
||
{% block extra_css %}
|
||
<style>
|
||
/* 导出预览样式 - 镜像PDF效果 */
|
||
.preview-content {
|
||
font-family: "Microsoft YaHei", "微软雅黑", sans-serif;
|
||
font-size: 12px;
|
||
line-height: 1.6;
|
||
color: #333;
|
||
padding: 20px;
|
||
background: white;
|
||
}
|
||
|
||
.preview-content h1 {
|
||
font-size: 24px;
|
||
font-weight: bold;
|
||
margin: 0 0 16px 0;
|
||
text-align: center;
|
||
}
|
||
|
||
.preview-content h2 {
|
||
font-size: 20px;
|
||
font-weight: bold;
|
||
margin: 16px 0 12px 0;
|
||
border-bottom: 1px solid #ddd;
|
||
padding-bottom: 4px;
|
||
}
|
||
|
||
.preview-content h3 {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
margin: 12px 0 8px 0;
|
||
}
|
||
|
||
.preview-content p {
|
||
margin: 8px 0;
|
||
}
|
||
|
||
.preview-content ul, .preview-content ol {
|
||
margin: 8px 0;
|
||
padding-left: 24px;
|
||
}
|
||
|
||
.preview-content li {
|
||
margin: 4px 0;
|
||
}
|
||
|
||
.preview-content table {
|
||
border-collapse: collapse;
|
||
width: 100%;
|
||
margin: 12px 0;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.preview-content table th,
|
||
.preview-content table td {
|
||
border: 1px solid #ddd;
|
||
padding: 4px 8px;
|
||
text-align: left;
|
||
}
|
||
|
||
.preview-content table th {
|
||
background: #f5f5f5;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.preview-content strong {
|
||
font-weight: bold;
|
||
}
|
||
|
||
.preview-content em {
|
||
font-style: italic;
|
||
}
|
||
|
||
.preview-content code {
|
||
font-family: Consolas, monospace;
|
||
background: #f5f5f5;
|
||
padding: 1px 4px;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.preview-content blockquote {
|
||
border-left: 3px solid #ddd;
|
||
margin: 8px 0;
|
||
padding-left: 12px;
|
||
color: #666;
|
||
}
|
||
|
||
.preview-watermark-overlay {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
pointer-events: none;
|
||
z-index: 10;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.preview-watermark-overlay::before {
|
||
content: attr(data-watermark);
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%) rotate(-45deg);
|
||
font-size: 48px;
|
||
font-weight: bold;
|
||
color: rgba(200, 200, 200, 0.3);
|
||
white-space: nowrap;
|
||
z-index: 11;
|
||
display: none;
|
||
}
|
||
|
||
.preview-watermark-overlay.watermark-active::before {
|
||
display: block;
|
||
}
|
||
</style>
|
||
{% 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="goBack()" 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();
|
||
// 清除编辑页标记(从编辑页返回后不要再跳回去)
|
||
sessionStorage.removeItem('fromEdit');
|
||
// 记录来源页面
|
||
const referrer = document.referrer;
|
||
if (referrer.includes('/student/')) {
|
||
sessionStorage.setItem('plan_detail_referrer', 'student');
|
||
} else if (referrer.includes('/plans')) {
|
||
sessionStorage.setItem('plan_detail_referrer', 'plans');
|
||
} else {
|
||
sessionStorage.setItem('plan_detail_referrer', 'unknown');
|
||
}
|
||
|
||
// 如果是从编辑页返回(plan_detail_reload被设置),强制刷新
|
||
const needsReload = sessionStorage.getItem('plan_detail_reload') === 'true';
|
||
if (needsReload) {
|
||
sessionStorage.removeItem('plan_detail_reload');
|
||
}
|
||
|
||
try {
|
||
const resp = await fetch(`/api/plans/${currentPlanId}`);
|
||
const data = await resp.json();
|
||
window.currentStudentId = data.student_id;
|
||
|
||
let editInfo = '';
|
||
if (data.updated_at) {
|
||
const editor = data.updated_by_name ? ` by ${data.updated_by_name}` : '';
|
||
editInfo = `<span class="text-muted">(于${data.updated_at}${editor}编辑)</span>`;
|
||
}
|
||
|
||
let html = `
|
||
<div class="mb-3">
|
||
<strong>学员:</strong>${data.student_name}
|
||
<strong>练习时间:</strong>${data.content.practice_time}
|
||
<strong>生成时间:</strong>${data.created_at} ${editInfo}
|
||
<strong>模板:</strong>${data.template_name || '无'}
|
||
<div class="mt-2">
|
||
<a href="/student/${data.student_id}" class="btn btn-sm btn-outline-primary">
|
||
<i class="bi bi-person"></i> 查看学员
|
||
</a>
|
||
<div class="form-check form-check-inline">
|
||
<input class="form-check-input" type="checkbox" id="typicalToggle" ${data.is_typical ? 'checked' : ''} onchange="toggleTypical(${currentPlanId}, this.checked)">
|
||
<label class="form-check-label" for="typicalToggle">典型方案</label>
|
||
</div>
|
||
<a href="/plan/${currentPlanId}/edit" class="btn btn-sm btn-warning" onclick="markFromEdit()">
|
||
<i class="bi bi-edit"></i> 编辑
|
||
</a>
|
||
<div class="form-check form-check-inline">
|
||
<select id="reportTemplateSelect" class="form-select form-select-sm" style="width: auto;" onchange="updateDownloadLinks()">
|
||
</select>
|
||
</div>
|
||
<button onclick="downloadPDFWithTemplate()" class="btn btn-sm btn-primary">
|
||
<i class="bi bi-download"></i> 下载PDF
|
||
</button>
|
||
<button onclick="showPreview()" class="btn btn-sm btn-outline-success">
|
||
<i class="bi bi-eye"></i> 预览
|
||
</button>
|
||
<button onclick="downloadMDWithTemplate()" class="btn btn-sm btn-outline-primary" style="display:none">
|
||
<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>
|
||
`;
|
||
}
|
||
|
||
document.getElementById('planContent').innerHTML = html;
|
||
loadTemplates();
|
||
} 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 updateDownloadLinks() {
|
||
// No longer needed - buttons now use downloadPDFWithTemplate/downloadMDWithTemplate directly
|
||
}
|
||
|
||
function downloadPDFWithTemplate() {
|
||
const templateId = document.getElementById('reportTemplateSelect')?.value;
|
||
const suffix = templateId ? `?template_id=${templateId}` : '';
|
||
window.open(`/api/plans/${currentPlanId}/pdf${suffix}`, '_blank');
|
||
}
|
||
|
||
function downloadMDWithTemplate() {
|
||
const templateId = document.getElementById('reportTemplateSelect')?.value;
|
||
const suffix = templateId ? `?template_id=${templateId}` : '';
|
||
window.open(`/api/plans/${currentPlanId}/md${suffix}`, '_blank');
|
||
}
|
||
|
||
async function showPreview() {
|
||
const templateId = document.getElementById('reportTemplateSelect')?.value;
|
||
const planId = currentPlanId;
|
||
const suffix = templateId ? `?template_id=${templateId}` : '';
|
||
|
||
try {
|
||
const resp = await fetch(`/api/plans/${planId}/preview${suffix}`);
|
||
if (!resp.ok) {
|
||
alert('预览加载失败');
|
||
return;
|
||
}
|
||
const html = await resp.text();
|
||
document.getElementById('previewContent').innerHTML = html;
|
||
|
||
// 加载水印配置
|
||
try {
|
||
const cfgResp = await fetch('/api/config');
|
||
if (cfgResp.ok) {
|
||
const cfg = await cfgResp.json();
|
||
const wmText = cfg.watermark_text || '';
|
||
const wmEl = document.getElementById('previewWatermark');
|
||
if (wmText) {
|
||
wmEl.setAttribute('data-watermark', wmText);
|
||
wmEl.classList.add('watermark-active');
|
||
} else {
|
||
wmEl.classList.remove('watermark-active');
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.log('水印配置加载失败', e);
|
||
}
|
||
|
||
// 显示模态框
|
||
const modal = new bootstrap.Modal(document.getElementById('previewModal'));
|
||
modal.show();
|
||
} catch (err) {
|
||
console.error('预览错误:', err);
|
||
alert('预览加载失败');
|
||
}
|
||
}
|
||
|
||
async function loadTemplates() {
|
||
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);
|
||
}
|
||
}
|
||
|
||
// 设为典型
|
||
async function toggleTypical(planId, isTypical) {
|
||
try {
|
||
await fetch(`/api/plans/${planId}/typical`, {method: 'POST'});
|
||
// 标记需要刷新方案列表
|
||
sessionStorage.setItem('plans_needs_refresh', 'true');
|
||
} catch (e) {
|
||
alert('设置失败: ' + e.message);
|
||
}
|
||
}
|
||
|
||
// 返回按钮处理
|
||
function goBack() {
|
||
// 标记需要刷新推荐方案列表
|
||
sessionStorage.setItem('needs_refresh_recommended', 'true');
|
||
history.back();
|
||
}
|
||
|
||
// 处理 bfcache - 页面从缓存恢复时需要重新加载以获取最新数据
|
||
window.addEventListener('pageshow', function(event) {
|
||
if (event.persisted) {
|
||
// 页面从 bfcache 恢复,需要重新加载
|
||
loadPlan();
|
||
}
|
||
});
|
||
|
||
// 标记来源为编辑页(编辑页点击"返回详情"前设置)
|
||
function markFromEdit() {
|
||
sessionStorage.setItem('fromEdit', 'true');
|
||
}
|
||
|
||
window.currentStudentId = null;
|
||
|
||
loadPlan();
|
||
</script>
|
||
|
||
<!-- 导出预览模态框 -->
|
||
<div class="modal fade" id="previewModal" tabindex="-1" aria-labelledby="previewModalLabel" aria-hidden="true">
|
||
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="previewModalLabel">导出预览</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="关闭"></button>
|
||
</div>
|
||
<div class="modal-body" style="position: relative; overflow: auto;">
|
||
<div id="previewWatermark" class="preview-watermark-overlay"></div>
|
||
<div id="previewContent" class="preview-content"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %} |