Files
piano-plan/app/templates/plan_detail.html
T

367 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% 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} &nbsp;&nbsp;
<strong>练习时间:</strong>${data.content.practice_time} &nbsp;&nbsp;
<strong>生成时间:</strong>${data.created_at} ${editInfo} &nbsp;&nbsp;
<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 %}