|
|
@@ -76,7 +76,7 @@
|
|
|
</div>
|
|
|
<div class="rp-actions">
|
|
|
<button type="button" class="btn btn-ghost btn-small" id="rpArchive">历史报告</button>
|
|
|
- <button type="button" class="btn btn-ghost btn-small" id="rpSave">保存报告</button>
|
|
|
+ <button type="button" class="btn btn-ghost btn-small" id="rpSave" disabled>保存报告</button>
|
|
|
<button type="button" class="btn btn-ghost btn-small" id="rpRefresh">重新生成</button>
|
|
|
<button type="button" class="btn btn-primary btn-small" id="rpPrint">下载 PDF</button>
|
|
|
</div>
|
|
|
@@ -245,6 +245,8 @@
|
|
|
|
|
|
function renderReport(report) {
|
|
|
const page = $('rpPage');
|
|
|
+ const saveBtn = $('rpSave');
|
|
|
+ if (saveBtn) saveBtn.disabled = false;
|
|
|
page.innerHTML = [
|
|
|
renderCover(report),
|
|
|
renderSummary(report),
|
|
|
@@ -354,7 +356,11 @@
|
|
|
const { res, body } = await api('/api/ops/reports', {
|
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
- body: JSON.stringify({ from: state.from, to: state.to, schools: state.schools.join(','), shops: state.shops.join(',') }),
|
|
|
+ body: JSON.stringify({
|
|
|
+ from: state.from, to: state.to,
|
|
|
+ schools: state.schools.join(','), shops: state.shops.join(','),
|
|
|
+ report: state.report,
|
|
|
+ }),
|
|
|
});
|
|
|
if (!res.ok) throw new Error((body && (body.detail || body.error)) || ('HTTP ' + res.status));
|
|
|
btn.textContent = '已保存';
|
|
|
@@ -385,14 +391,19 @@
|
|
|
return;
|
|
|
}
|
|
|
list.innerHTML = `<table class="rp-table">
|
|
|
- <thead><tr><th>保存时间</th><th>区间</th><th>来源</th><th>关键指标</th><th class="rp-col-actions">操作</th></tr></thead>
|
|
|
+ <thead><tr><th>保存时间</th><th>区间</th><th>学校</th><th>店铺</th><th>来源</th><th class="rp-col-actions">操作</th></tr></thead>
|
|
|
<tbody>${reports.map((r) => `<tr>
|
|
|
<td>${esc(new Date(r.createdAt).toLocaleString('zh-CN'))}</td>
|
|
|
<td>${esc(r.from)} ~ ${esc(r.to)}</td>
|
|
|
+ <td>${(r.schoolNames || []).length ? esc(r.schoolNames.join('、')) : '<span class="rp-muted">全部</span>'}</td>
|
|
|
+ <td title="${esc((r.shopNames || []).join('、'))}">${(r.shopNames || []).length
|
|
|
+ ? esc(r.shopNames.slice(0, 3).join('、')) + (r.shopNames.length > 3 ? ` 等 ${r.shopNames.length} 家` : '')
|
|
|
+ : '<span class="rp-muted">全部</span>'}</td>
|
|
|
<td>${r.source === 'llm' ? 'AI 生成' : '规则生成'}</td>
|
|
|
- <td class="rp-history-kpis">${(r.kpis || []).map((k) => `${esc(k.label)} ${esc(k.current)}`).join(' · ')}</td>
|
|
|
<td class="rp-col-actions">
|
|
|
<button type="button" class="btn btn-ghost btn-small" data-view="${esc(r.id)}">查看</button>
|
|
|
+ <button type="button" class="btn btn-ghost btn-small" data-pdf="${esc(r.id)}">PDF</button>
|
|
|
+ <button type="button" class="btn btn-ghost btn-small" data-json="${esc(r.id)}">JSON</button>
|
|
|
<button type="button" class="btn btn-ghost btn-small" data-del="${esc(r.id)}">删除</button>
|
|
|
</td>
|
|
|
</tr>`).join('')}</tbody>
|
|
|
@@ -400,6 +411,12 @@
|
|
|
list.querySelectorAll('[data-view]').forEach((btn) => {
|
|
|
btn.addEventListener('click', () => loadSaved(btn.dataset.view));
|
|
|
});
|
|
|
+ list.querySelectorAll('[data-pdf]').forEach((btn) => {
|
|
|
+ btn.addEventListener('click', () => downloadSavedPdf(btn.dataset.pdf));
|
|
|
+ });
|
|
|
+ list.querySelectorAll('[data-json]').forEach((btn) => {
|
|
|
+ btn.addEventListener('click', () => downloadSavedJson(btn.dataset.json));
|
|
|
+ });
|
|
|
list.querySelectorAll('[data-del]').forEach((btn) => {
|
|
|
btn.addEventListener('click', async () => {
|
|
|
if (!confirm('删除这份历史报告?')) return;
|
|
|
@@ -410,6 +427,44 @@
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+ async function fetchSaved(id) {
|
|
|
+ const { res, body } = await api('/api/ops/reports/' + encodeURIComponent(id));
|
|
|
+ if (!res.ok || !body || !body.report) {
|
|
|
+ alert('读取失败');
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return body.report;
|
|
|
+ }
|
|
|
+
|
|
|
+ async function downloadSavedPdf(id) {
|
|
|
+ const report = await fetchSaved(id);
|
|
|
+ if (!report) return;
|
|
|
+ state.report = report;
|
|
|
+ $('rpHistoryModal').hidden = true;
|
|
|
+ renderReport(report);
|
|
|
+ const meta = report.meta || {};
|
|
|
+ const prevTitle = document.title;
|
|
|
+ document.title = `经营分析报告_${String(meta.period_start || '').replace(/-/g, '')}-${String(meta.period_end || '').replace(/-/g, '')}`;
|
|
|
+ setTimeout(() => {
|
|
|
+ window.print();
|
|
|
+ document.title = prevTitle;
|
|
|
+ }, 500);
|
|
|
+ }
|
|
|
+
|
|
|
+ async function downloadSavedJson(id) {
|
|
|
+ const report = await fetchSaved(id);
|
|
|
+ if (!report) return;
|
|
|
+ const blob = new Blob([JSON.stringify(report, null, 2)], { type: 'application/json' });
|
|
|
+ const url = URL.createObjectURL(blob);
|
|
|
+ const a = document.createElement('a');
|
|
|
+ a.href = url;
|
|
|
+ a.download = `经营分析报告_${id}.json`;
|
|
|
+ document.body.appendChild(a);
|
|
|
+ a.click();
|
|
|
+ a.remove();
|
|
|
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
|
+ }
|
|
|
+
|
|
|
async function loadSaved(id) {
|
|
|
const { res, body } = await api('/api/ops/reports/' + encodeURIComponent(id));
|
|
|
if (!res.ok || !body || !body.report) {
|
|
|
@@ -434,6 +489,8 @@
|
|
|
if (state.loading) return;
|
|
|
state.loading = true;
|
|
|
const page = $('rpPage');
|
|
|
+ const saveBtn = $('rpSave');
|
|
|
+ if (saveBtn) saveBtn.disabled = true;
|
|
|
if (page && !state.report) page.innerHTML = '<div class="rp-loading">正在生成报告…</div>';
|
|
|
try {
|
|
|
const params = new URLSearchParams();
|