|
|
@@ -435,6 +435,50 @@
|
|
|
stream.scrollTop = stream.scrollHeight;
|
|
|
}
|
|
|
|
|
|
+ /** 把一次问数导出为自包含 HTML 报告(问题/元信息 + markdown 正文),纯前端生成。 */
|
|
|
+ function exportRunHtml(rec) {
|
|
|
+ const result = rec.result || {};
|
|
|
+ const entity = (state.entities.find((e) => e.key === (rec.entity || state.entity)) || {});
|
|
|
+ const styles = [
|
|
|
+ 'body{font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;color:#2b2a28;margin:0;background:#f4f2ef;}',
|
|
|
+ '.wrap{max-width:860px;margin:0 auto;padding:40px 32px 56px;}',
|
|
|
+ 'h1{font-size:22px;margin:0 0 8px;line-height:1.4;}',
|
|
|
+ '.meta{color:#6b6560;font-size:12.5px;margin-bottom:24px;}',
|
|
|
+ '.card{background:#fff;border:1px solid #e8e4df;border-radius:14px;padding:26px 30px;}',
|
|
|
+ '.answer{font-size:14px;line-height:1.8;}',
|
|
|
+ '.answer>*:first-child{margin-top:0}.answer>*:last-child{margin-bottom:0}',
|
|
|
+ '.answer p{margin:10px 0;}',
|
|
|
+ '.answer h3,.answer h4,.answer h5,.answer h6{margin:18px 0 8px;font-size:14px;font-weight:700;}',
|
|
|
+ '.answer h3{font-size:16px;}',
|
|
|
+ '.answer ul,.answer ol{margin:10px 0;padding-left:22px;display:grid;gap:5px;}',
|
|
|
+ '.answer blockquote{margin:10px 0;padding:8px 14px;border-left:3px solid #e8e4df;color:#6b6560;background:#f4f2ef;border-radius:0 8px 8px 0;}',
|
|
|
+ '.answer code{padding:1px 5px;border-radius:5px;background:#f0ede8;font-size:12.5px;font-family:ui-monospace,Menlo,Consolas,monospace;}',
|
|
|
+ '.answer pre.md-code{margin:10px 0;padding:12px 14px;border-radius:8px;background:#1d1c1a;color:#f4f2ef;font-size:12.5px;overflow:auto;line-height:1.6;}',
|
|
|
+ '.answer pre.md-code code{background:transparent;color:inherit;padding:0;}',
|
|
|
+ '.answer table{margin:12px 0;border-collapse:collapse;font-size:13px;width:100%;}',
|
|
|
+ '.answer th,.answer td{border:1px solid #e8e4df;padding:7px 11px;text-align:left;vertical-align:top;}',
|
|
|
+ '.answer th{background:#f0ede8;font-weight:650;white-space:nowrap;}',
|
|
|
+ '.answer hr{border:0;border-top:1px solid #e8e4df;margin:14px 0;}',
|
|
|
+ '.answer a{color:#ff6b35;}',
|
|
|
+ 'footer{margin-top:22px;color:#a5a09a;font-size:12px;text-align:center;}',
|
|
|
+ '@media print{body{background:#fff;}.card{border:none;padding:0;}}',
|
|
|
+ ].join('\n');
|
|
|
+ const doc = '<!doctype html>\n<html lang="zh-CN">\n<head>\n<meta charset="utf-8">\n'
|
|
|
+ + '<meta name="viewport" content="width=device-width, initial-scale=1">\n'
|
|
|
+ + `<title>${esc(rec.question || '问数报告')}</title>\n<style>${styles}</style>\n</head>\n<body>\n<div class="wrap">\n`
|
|
|
+ + `<h1>${esc(rec.question || '')}</h1>\n`
|
|
|
+ + `<div class="meta">智能问数 · ${esc(entity.name || rec.entity || state.entity || '—')} · ${esc(fmtClock(rec.startedAt))}${rec.skill ? ` · 技能「${esc(rec.skill)}」` : ''}</div>\n`
|
|
|
+ + `<div class="card"><div class="answer">${renderMarkdown(result.answer || '')}</div></div>\n`
|
|
|
+ + '<footer>由 炘和数智智慧平台 · 智能问数 生成</footer>\n</div>\n</body>\n</html>';
|
|
|
+ const blob = new Blob([doc], { type: 'text/html;charset=utf-8' });
|
|
|
+ const a = document.createElement('a');
|
|
|
+ a.href = URL.createObjectURL(blob);
|
|
|
+ a.download = `问数报告-${String(rec.question || '报告').slice(0, 16).replace(/[\\/:*?"<>|\s]+/g, '-')}-${Date.now()}.html`;
|
|
|
+ a.click();
|
|
|
+ URL.revokeObjectURL(a.href);
|
|
|
+ showToast('报告已导出(.html,可直接打印为 PDF)', 'success');
|
|
|
+ }
|
|
|
+
|
|
|
/** 把一次运行渲染进容器(轮询时整卡重绘,数据量小无压力)。 */
|
|
|
function renderRunInto(wrap, rec) {
|
|
|
const TOOL_LABEL = {
|
|
|
@@ -493,7 +537,14 @@ ${esc(step.summary || step.error || '')}</pre>
|
|
|
parts.push(`<div class="ask-q">${esc(rec.question)}</div>`);
|
|
|
parts.push('<div class="ask-card">');
|
|
|
parts.push(head.join(''));
|
|
|
- if (steps.length) parts.push(`<div class="ask-steps">${steps.join('')}</div>`);
|
|
|
+ // 思考过程:Codex 式一行摘要,运行中自动展开、完成后收起,点开才看逐步明细
|
|
|
+ if (steps.length) {
|
|
|
+ const running = rec.status === 'running';
|
|
|
+ const label = running
|
|
|
+ ? `思考中 · 已 ${steps.length} 步 · ${toolCount} 个工具`
|
|
|
+ : `思考过程 · ${steps.length} 步${totalMs != null ? ` · ${fmtDuration(totalMs)}` : ''} · ${toolCount} 个工具`;
|
|
|
+ parts.push(`<details class="ask-loop"${running ? ' open' : ''}><summary><span class="ask-dot ${running ? 'running' : 'done'}"></span>${esc(label)}</summary><div class="ask-steps">${steps.join('')}</div></details>`);
|
|
|
+ }
|
|
|
// verdict 卡:数据体检/层间核对/恒等式/完整性等校验结论
|
|
|
const verdicts = rec.verdicts || [];
|
|
|
if (verdicts.length) {
|
|
|
@@ -521,9 +572,7 @@ ${esc(step.summary || step.error || '')}</pre>
|
|
|
parts.push(`<div class="ask-chart" data-run="${esc(rec.runId)}"></div>`);
|
|
|
if (chart.title) parts.push(`<div class="ask-chart-title">${esc(chart.title)}</div>`);
|
|
|
}
|
|
|
- if (result.appliedFrom) {
|
|
|
- parts.push('<div class="ask-actions"><button type="button" class="btn btn-ghost btn-small" data-apply="' + esc(rec.runId) + '">应用到报表</button></div>');
|
|
|
- }
|
|
|
+ parts.push('<div class="ask-actions"><button type="button" class="btn btn-ghost btn-small" data-export="' + esc(rec.runId) + '">导出为报告(HTML)</button></div>');
|
|
|
if (result.reportUrl) {
|
|
|
parts.push(`<div class="ask-actions"><a class="btn btn-ghost btn-small" href="${esc(result.reportUrl)}" target="_blank" rel="noopener">查看分析报告</a></div>`);
|
|
|
}
|
|
|
@@ -549,13 +598,8 @@ ${esc(step.summary || step.error || '')}</pre>
|
|
|
if (wrapEl && rows && rows.length) renderChart(wrapEl, result.chart, rows);
|
|
|
else if (wrapEl) wrapEl.parentNode.removeChild(wrapEl);
|
|
|
}
|
|
|
- const applyBtn = wrap.querySelector('[data-apply]');
|
|
|
- if (applyBtn) {
|
|
|
- applyBtn.addEventListener('click', () => {
|
|
|
- const applied = rec.result && rec.result.appliedFrom;
|
|
|
- if (applied && window.shanReportsApplyQuery) window.shanReportsApplyQuery(applied);
|
|
|
- });
|
|
|
- }
|
|
|
+ const exportBtn = wrap.querySelector('[data-export]');
|
|
|
+ if (exportBtn) exportBtn.addEventListener('click', () => exportRunHtml(rec));
|
|
|
wrap.querySelectorAll('[data-fb]').forEach((btn) => {
|
|
|
btn.addEventListener('click', async () => {
|
|
|
const kind = btn.dataset.fb;
|