- 本文档是 StationBuilder 的产品/交互设计文档,单一 HTML 源,位于 public/design-doc.html,
运行时通过顶栏「设计方案」链接(/design-doc.html)打开。
- 每次功能改动后,请同步更新对应章节;新增/遗留的未实现能力用 <span class="todo">尚未实现</span> 标注。
- - 顶部三个按钮可分别导出 PDF(浏览器打印)、Word(.doc)、Markdown(由本页 DOM 实时转换)。
+ - 顶部三个按钮分别导出:PDF(html2pdf.js 生成,带「第 X 页 / 共 Y 页」页脚页码,截图限 240px 宽)、
+ Word(.doc:微软雅黑、纯黑文字、黑色表格边框、1.5 倍行距、页脚页码域、默认 TOC 目录域、截图 240px 宽)、
+ Markdown(由本页 DOM 实时转换,不含图片)。
-->
<html lang="zh-CN">
<head>
main#doc{ padding:18px 14px 80px; }
}
@media print{ .doc-toolbar, .doc-nav{ display:none; } main#doc{ max-width:none; padding:0; width:100%; }
+ body, main#doc, main#doc *{ font-family:"Microsoft YaHei","微软雅黑",SimSun,sans-serif !important; color:#000 !important; line-height:1.5 !important; }
+ table{ border-collapse:collapse; border:1px solid #000 !important; }
+ th, td{ border:1px solid #000 !important; }
+ th{ background:#f2f2f2 !important; }
.figure{ page-break-inside:avoid; break-inside:avoid; }
- .figure img{ max-width:100%; width:auto; }
+ .figure img{ max-width:240px !important; width:auto !important; height:auto !important; }
table{ page-break-inside:avoid; break-inside:avoid; }
tr{ page-break-inside:avoid; break-inside:avoid; }
h2,h3,h4{ page-break-after:avoid; break-after:avoid; }
.changelog th{ background:#f6f7f8; color:#4b4f53; text-align:left; font-size:11px; font-weight:bold; border-bottom:1px solid var(--line); padding:8px 10px; }
.changelog td{ border-bottom:1px solid #e3e5e7; padding:9px 10px; font-size:11px; vertical-align:top; }
.changelog td:first-child{ white-space:nowrap; font-weight:bold; color:#008ed2; }
+ /* 导出 PDF 时作用于克隆容器,不影响屏幕 UI */
+ .export-pdf{ background:#fff; padding:20px 22px; width:780px; box-sizing:border-box; }
+ .export-pdf, .export-pdf *{ font-family:"Microsoft YaHei","微软雅黑",SimSun,sans-serif !important; color:#000 !important; line-height:1.5 !important; }
+ .export-pdf table{ border-collapse:collapse; border:1px solid #000; width:100%; margin:10px 0; }
+ .export-pdf th, .export-pdf td{ border:1px solid #000; padding:6px 9px; text-align:left; vertical-align:top; }
+ .export-pdf th{ font-weight:bold; background:#f2f2f2; }
+ .export-pdf .figure{ border:1px solid #000; background:#fff; }
+ .export-pdf .figure img{ max-width:240px; width:auto; height:auto; }
+ .export-pdf .doc-toc{ margin:0 0 18px; page-break-after:avoid; }
+ .export-pdf .doc-toc h2{ border-bottom:2px solid #000; }
+ .export-pdf .doc-toc ul{ list-style:none; padding-left:0; margin:8px 0; }
+ .export-pdf .doc-toc li{ margin:4px 0; }
+ .export-pdf .doc-toc li.sub{ padding-left:22px; font-size:12.5px; }
</style>
</head>
<body>
</main>
</div>
+<!-- PDF 导出依赖 html2pdf(jsPDF + html2canvas);若加载失败,downloadPDF 会回退到浏览器打印 -->
+<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.2/html2pdf.bundle.min.js"></script>
<script>
// 自动生成左侧目录(TOC)并实现滚动高亮
(function buildToc(){
document.body.appendChild(a); a.click(); a.remove();
setTimeout(()=>URL.revokeObjectURL(url), 60000);
}
-function downloadPDF(){ window.print(); }
+function buildStaticToc(){
+ const main = document.getElementById('doc');
+ if(!main) return '';
+ const heads = main.querySelectorAll('h2, h3');
+ let html = '<div class="doc-toc"><h2>目录</h2><ul>';
+ heads.forEach(function(h){
+ const sub = h.tagName.toLowerCase()==='h3' ? ' class="sub"' : '';
+ const text = (h.textContent||'').replace(/</g,'<');
+ html += '<li'+sub+'>'+text+'</li>';
+ });
+ html += '</ul></div>';
+ return html;
+}
+function downloadPDF(){
+ if(typeof html2pdf === 'undefined'){
+ alert('PDF 生成库未加载(可能无网络),已改用浏览器打印;如需自动页码,请在打印对话框勾选「页眉和页脚」。');
+ window.print();
+ return;
+ }
+ const src = document.getElementById('doc');
+ const clone = src.cloneNode(true);
+ clone.removeAttribute('id');
+ clone.classList.add('export-pdf');
+ clone.querySelectorAll('[id]').forEach(e=>e.removeAttribute('id'));
+ clone.querySelectorAll('img').forEach(function(img){
+ img.style.maxWidth='240px'; img.style.height='auto'; img.style.display='block'; img.style.margin='0 auto';
+ });
+ clone.insertAdjacentHTML('afterbegin', buildStaticToc());
+ const holder = document.createElement('div');
+ holder.style.position='fixed'; holder.style.left='-10000px'; holder.style.top='0';
+ holder.appendChild(clone);
+ document.body.appendChild(holder);
+
+ const opt = {
+ margin: [12, 10, 16, 10],
+ filename: 'StationBuilder-设计方案.pdf',
+ image: { type:'jpeg', quality:0.98 },
+ html2canvas: { scale:2, useCORS:true, backgroundColor:'#ffffff', logging:false },
+ jsPDF: { unit:'mm', format:'a4', orientation:'portrait' },
+ pagebreak: { mode:['avoid-all','css','legacy'] }
+ };
+ html2pdf().set(opt).from(clone).toPdf().get('pdf').then(function(pdf){
+ const total = pdf.internal.getNumberOfPages();
+ const w = pdf.internal.pageSize.getWidth();
+ const h = pdf.internal.pageSize.getHeight();
+ for(let i=1;i<=total;i++){
+ pdf.setPage(i);
+ pdf.setFontSize(9);
+ pdf.setTextColor(0,0,0);
+ pdf.text('第 '+i+' 页 / 共 '+total+' 页', w/2, h-8, { align:'center' });
+ }
+ pdf.save('StationBuilder-设计方案.pdf');
+ }).catch(function(e){
+ alert('PDF 生成失败:'+(e&&e.message||e));
+ }).finally(function(){
+ if(holder.parentNode) holder.parentNode.removeChild(holder);
+ });
+}
function blobToDataURL(blob){
return new Promise((resolve,reject)=>{
const r=new FileReader();
if(!resp.ok) return;
const du = await blobToDataURL(await resp.blob());
img.setAttribute('src', du);
- img.style.maxWidth = '360px';
+ img.style.maxWidth = '240px';
+ img.style.height = 'auto';
+ img.style.display = 'block';
+ img.style.margin = '0 auto';
}catch(e){ /* 失败则保留原相对路径,不影响导出其它内容 */ }
}));
- const head = '<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word" xmlns="http://www.w3.org/TR/REC-html40"><head><meta charset="utf-8"></head><body>';
- const blob = new Blob([''+head+clone.innerHTML+'</body></html>'], {type:'application/msword;charset=utf-8'});
+ // 默认插入目录:Word TOC 域,打开后自动生成带页码、可点击跳转的目录
+ const tocField =
+ '<h2 style="border:none">目录</h2>' +
+ '<!--[if supportFields]><span style="mso-element:field-begin"></span>TOC \\o "1-3" \\h \\z \\u<span style="mso-element:field-separator"></span><![endif]-->' +
+ '(若目录为空,请右键「更新域」或按 F9 刷新)' +
+ '<!--[if supportFields]><span style="mso-element:field-end"></span><![endif]-->';
+ const styleCss = [
+ 'body, div, p, span, td, th, li, h1, h2, h3, h4, a {',
+ ' font-family:"Microsoft YaHei","微软雅黑",SimSun,sans-serif;',
+ ' color:#000000; line-height:1.5;',
+ '}',
+ 'body{ font-size:10.5pt; }',
+ 'h1{ font-size:22pt; } h2{ font-size:15pt; } h3{ font-size:13pt; } h4{ font-size:11pt; }',
+ 'table{ border-collapse:collapse; border:1px solid #000000; width:100%; }',
+ 'td, th{ border:1px solid #000000; padding:4pt 6pt; text-align:left; vertical-align:top; }',
+ 'th{ font-weight:bold; background:#f2f2f2; }',
+ 'img{ max-width:240px; height:auto; }',
+ '@page Section1{ size:595.45pt 841.7pt; margin:36.0pt 36.0pt 40.0pt 36.0pt; mso-header-margin:35.4pt; mso-footer-margin:35.4pt; mso-paper-source:0; }',
+ 'div.Section1{ page:Section1; }',
+ 'p.MsoFooter{ text-align:center; font-size:9.0pt; color:#000000; }'
+ ].join('\n');
+ const mso = '<!--[if gte mso 9]><xml><w:WordDocument><w:View>Print</w:View><w:Zoom>90</w:Zoom><w:UpdateFields>true</w:UpdateFields></w:WordDocument></xml><![endif]-->';
+ const html =
+ '<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word" xmlns="http://www.w3.org/TR/REC-html40">' +
+ '<head><meta charset="utf-8"><style>' + styleCss + '</style>' + mso + '</head><body>' +
+ '<div style="mso-element:footer" class="MsoFooter"><p class="MsoFooter">第 <span style="mso-field-code:PAGE">1</span> 页 / 共 <span style="mso-field-code:NUMPAGES">1</span> 页</p></div>' +
+ '<div class="Section1">' + tocField + clone.innerHTML + '</div>' +
+ '</body></html>';
+ const blob = new Blob([html], {type:'application/msword;charset=utf-8'});
saveBlob(blob, 'StationBuilder-设计方案.doc');
}
function inlineOf(node){