]> acesimba.cloud Git - StationBuilder.git/commitdiff
fix: 设计方案导出修复(PDF 真实文字/页脚页码/目录;Word 页脚页码/静态目录/截图300px)
authorroot <root@localhost.localdomain>
Fri, 21 Aug 2026 01:00:52 +0000 (09:00 +0800)
committerroot <root@localhost.localdomain>
Fri, 21 Aug 2026 01:00:52 +0000 (09:00 +0800)
- PDF 改 window.print 原生打印,真实文字无乱码;打印时注入静态目录(h2/h3/h4),@page 页脚页码
- Word footer 补 id + 规范 PAGE/NUMPAGES 域标记,每页底部页码;目录改可见静态目录
- 截图限宽 300px 等比例(修复 .figure 类选择器未命中 <figure> 元素导致 PDF 图片溢出)
- 图后插 <br> 使图注换行;设计文档 v0.26 + 顶部说明同步

public/design-doc.html

index d9bf7f6d46ae09ee0e4e4b60b12cd27e61981dbe..fad881d3a67b6f5e18042b299db7130800a4de9c 100644 (file)
@@ -4,8 +4,8 @@
   - 本文档是 StationBuilder 的产品/交互设计文档,单一 HTML 源,位于 public/design-doc.html,
     运行时通过顶栏「设计方案」链接(/design-doc.html)打开。
   - 每次功能改动后,请同步更新对应章节;新增/遗留的未实现能力用 <span class="todo">尚未实现</span> 标注。
-  - 顶部三个按钮分别导出:PDF(html2pdf.js 生成,带「第 X 页 / 共 Y 页」页脚页码,截图限 240px 宽)、
-    Word(.doc:微软雅黑、纯黑文字、黑色表格边框、1.5 倍行距、页脚页码域、默认 TOC 目录域、截图 240px 宽)、
+  - 顶部三个按钮分别导出:PDF(调用浏览器打印另存为 PDF——真实文字、微软雅黑、纯黑、1.5 倍行距;打印对话框勾选「页眉和页脚」即得页码;截图限 300px 宽)、
+    Word(.doc:微软雅黑、纯黑文字、黑色表格边框、1.5 倍行距、页脚页码域(第 X / 共 Y 页)、默认可见静态目录、截图 300px 宽)、
     Markdown(由本页 DOM 实时转换,不含图片)。
 -->
 <html lang="zh-CN">
     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:240px !important; width:auto !important; height:auto !important; }
+    figure{ page-break-inside:avoid; break-inside:avoid; }
+    figure img{ max-width:300px !important; width:auto !important; height:auto !important; display:block !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; }
     img{ -webkit-print-color-adjust:exact; print-color-adjust:exact; }
+    .doc-toc{ margin:0 0 16px; page-break-after:avoid; break-after:avoid; }
+    .doc-toc h2{ border-bottom:2px solid #000; }
+    .doc-toc ul{ list-style:none; padding-left:0; margin:8px 0; }
+    .doc-toc li{ margin:4px 0; }
+    .doc-toc li.sub{ padding-left:22px; font-size:12.5px; }
+    .doc-toc li.sub2{ padding-left:40px; font-size:12px; }
   }
+  /* 打印 PDF 页码:A4 页脚居中「第 X 页 / 共 Y 页」;Chrome 另存为 PDF 时若未显示,请在打印对话框勾选「页眉和页脚」 */
+  @page{ size:A4; margin:16mm 14mm 16mm 14mm;
+    @bottom-center{ content:"第 " counter(page) " 页 / 共 " counter(pages) " 页"; font-size:9pt; color:#000; font-family:"Microsoft YaHei",SimSun,sans-serif; } }
   /* 页面截图(参考 MachineCube 手册撰写方式:小节标题 + 说明 + 下图 + 配图 + 图注)*/
   body{ counter-reset: fig; }
   .figure{ margin:14px 0; border:1px solid var(--line); background:#fbfcfd; }
   .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>
   <h3>10.1 版本变更记录(Changelog)</h3>
   <table class="changelog">
     <tr><th>版本</th><th>日期</th><th>修改人</th><th>说明</th></tr>
+    <tr><td>v0.26</td><td>2026-08-21</td><td>wang.yu</td><td>导出修复(设计方案 Word / PDF):① PDF 由 html2pdf(jsPDF+html2canvas,中文乱码且长文档卡死)改为调用浏览器原生打印(window.print),输出真实文字、微软雅黑、纯黑、1.5 倍行距,无乱码;打印时临时注入静态目录(buildStaticToc,含 h2/h3/h4)于文首,打印后移除、不污染屏幕 UI;屏幕 UI 截图尺寸不变(仍 360px)。② Word 页脚页码修复:footer 元素补 id 并改用规范 Word 域标记(PAGE / NUMPAGES 字段,含 field-begin/separator/end),由「仅首页顶部显示 1」修正为每页底部「第 X 页 / 共 Y 页」。③ Word 目录由 TOC 域(打开时常为空)改为默认插入可见的静态目录(buildStaticToc,含 h2/h3/h4 三级)。④ 截图限宽 300px(等比例):Word 设 width=300 属性 + 行内样式;PDF 原 @media print 用 `.figure img` 类选择器未命中正文 `<figure>` 元素导致约束不生效、图片按原始像素溢出页面(即此前「240 显示不下」的真相),改为 `figure img` 元素选择器 + max-width:300px 后正常限宽并完整显示;图后插入回车(&lt;br&gt;)使图注文字换行到下一行。⑤ 补充 @page 打印页码(A4 页脚居中「第 X 页 / 共 Y 页」;Chrome 另存为 PDF 若未显示,在打印对话框勾选「页眉和页脚」兜底)。</td></tr>
     <tr><td>v0.24</td><td>2026-08-20</td><td>wang.yu</td><td>AI 并发调用统一限流:新增全局信号量 <code>AI_CONCURRENCY = 2</code>,包裹 <code>runCodebuddy</code>(db-sidecar.mjs),使站点生成(含批内并行)、各项补全(complete / complete-assets / complete-cubes)、领域方案补全(dt-complete 系列)等全部 AI 功能任意时刻最多 2 个 codebuddy 子进程并发,避免无脑全并行抢占资源导致超时。站点「生成全部」批内并行数 <code>GEN_CONCURRENCY</code> 由硬编码 3 改为复用 <code>AI_CONCURRENCY</code>,对应日志文案同步为「最多 2 个并行调用」。AI 聊天侧(ai-sidecar.mjs /api/chat)本身以 <code>running</code> 布尔量串行(同一时刻仅 1 个),已满足 ≤2。</td></tr>
     <tr><td>v0.25</td><td>2026-08-20</td><td>wang.yu</td><td>领域方案「已完成态」步进导航增强:判定「已完成」= 方案存在至少一个已生成(非空 <code>json_text</code>)的 Cube 页面(<code>DomainWizard</code> 内 <code>pageGenerated</code>)。完成后:① 步进器每个「已配置」的 Step 前置圆圈显示绿色对号(复用 <code>.steps button.done</code> 绿色样式,<code>configured</code> 由各步数据驱动——基本属性 name+description、领域知识 docs、资产设置 有 device_template_id、功能与菜单 有页面或已选 MU、运维流程 flows、生成保存 已生成页面、专家确认 status=available);② 取消「仅可点已访问步骤」限制,任意 Step 直接点击即可跳转对应页(不再需连点「下一步」);③ 未完成(草稿)方案保持原逐步引导行为(仅已访问步骤可点、后续步骤加 <code>.locked</code> 不可点)。<code>DomainSteps</code> 新增 <code>completed</code> / <code>configured</code> 入参,<code>globals.css</code> 增补 <code>.steps button.locked</code> 光标态。Playwright 验证:已完成方案绿对号渲染正确、点击直达生效、0 控制台报错;草稿方案锁定与不可跳转保持。</td></tr>
     <tr><td>v0.23</td><td>2026-08-20</td><td>wang.yu</td><td>领域知识上传格式扩展:<code>KnowledgeStep</code> 上传 <code>accept</code> 由纯文本类放开至 Word / Excel / PDF / 图片及常见文本(<code>.pdf,.doc,.docx,.xls,.xlsx,image/*</code> 等);提示文案补充「Word、Excel、PDF 会提取正文注入,图片仅作参考登记」,列表文件类徽标「文档」改为「文件」以兼容图片。后端 <code>extractDocText(filePath,mime,filename)</code> 替换原 <code>isTextMime</code>/<code>readKnowledgeFile</code>:文本类(txt/md/json/csv/html/xml/yaml/yml 等)直接读取;PDF(<code>pdf-parse</code>)、Word(仅 <code>.docx</code>,<code>mammoth</code>)、Excel(<code>.xls</code>/<code>.xlsx</code>,SheetJS 转 CSV)提取正文并 16k 截断后注入提示词;图片及老 <code>.doc</code> 等无法纯 JS 提取的格式返回空串,<code>buildKnowledgePrompt</code> 中仍登记为参考来源(<code>(参考来源:文件名)</code>)。新增依赖 <code>pdf-parse</code> / <code>mammoth</code> / <code>xlsx</code>(纯 JS,无原生编译)。</td></tr>
 
   <ul>
     <li>功能变更后请同步更新本文档对应章节,未实现项持续标注 <span class="todo">尚未实现</span>,落地后移除。</li>
-    <li><b>下载 PDF</b>:调用浏览器打印,请在打印对话框选择「另存为 PDF」。</li>
-    <li><b>下载 Word</b>:将本文档内容以 Word 兼容的 <code>.doc</code> 格式导出(Word 可直接打开)。</li>
+    <li><b>下载 PDF</b>:调用浏览器打印,在打印对话框选择「另存为 PDF」;勾选「页眉和页脚」可加页码,输出真实文字无乱码。</li>
+    <li><b>ä¸\8bè½½ Word</b>ï¼\9aå°\86æ\9c¬æ\96\87æ¡£å\86\85容以 Word å\85¼å®¹ç\9a\84 <code>.doc</code> æ ¼å¼\8f导å\87ºï¼\88Word å\8f¯ç\9b´æ\8e¥æ\89\93å¼\80ï¼\8cå\90«é¡µè\84\9a页ç \81ä¸\8eé»\98认ç\9b®å½\95ï¼\89ã\80\82</li>
     <li><b>下载 Markdown</b>:由本页 DOM 实时转换为 Markdown 文本导出。</li>
   </ul>
 </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(){
@@ -835,10 +830,11 @@ function saveBlob(blob, filename){
 function buildStaticToc(){
   const main = document.getElementById('doc');
   if(!main) return '';
-  const heads = main.querySelectorAll('h2, h3');
+  const heads = main.querySelectorAll('h2, h3, h4');
   let html = '<div class="doc-toc"><h2>目录</h2><ul>';
   heads.forEach(function(h){
-    const sub = h.tagName.toLowerCase()==='h3' ? ' class="sub"' : '';
+    const tag = h.tagName.toLowerCase();
+    const sub = tag==='h3' ? ' class="sub"' : (tag==='h4' ? ' class="sub2"' : '');
     const text = (h.textContent||'').replace(/</g,'&lt;');
     html += '<li'+sub+'>'+text+'</li>';
   });
@@ -846,49 +842,20 @@ function buildStaticToc(){
   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'] }
+  const main = document.getElementById('doc');
+  if(!main) return;
+  // 打印专用目录:仅在打印时插入文首,打印结束后移除,不污染屏幕 UI
+  const tocWrap = document.createElement('div');
+  tocWrap.id = 'print-toc';
+  tocWrap.innerHTML = buildStaticToc();
+  main.insertBefore(tocWrap, main.firstChild);
+  const cleanup = function(){
+    if(tocWrap.parentNode) tocWrap.parentNode.removeChild(tocWrap);
+    window.removeEventListener('afterprint', cleanup);
   };
-  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);
-  });
+  window.addEventListener('afterprint', cleanup);
+  // 调用浏览器原生打印(另存为 PDF):真实文字、微软雅黑、纯黑、1.5 倍行距,无中文乱码
+  window.print();
 }
 function blobToDataURL(blob){
   return new Promise((resolve,reject)=>{
@@ -910,18 +877,19 @@ async function downloadWord(){
       if(!resp.ok) return;
       const du = await blobToDataURL(await resp.blob());
       img.setAttribute('src', du);
-      img.style.maxWidth = '240px';
-      img.style.height = 'auto';
-      img.style.display = 'block';
-      img.style.margin = '0 auto';
     }catch(e){ /* 失败则保留原相对路径,不影响导出其它内容 */ }
   }));
-  // 默认插入目录: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]-->';
+  // 图片统一限宽 300px(等比例),图后插入回车使图注文字换行到下一行
+  clone.querySelectorAll('img').forEach(function(img){
+    img.setAttribute('width', '300');
+    img.style.maxWidth = '300px';
+    img.style.height = 'auto';
+    img.style.display = 'block';
+    img.style.margin = '0 auto';
+    img.insertAdjacentHTML('afterend', '<br>');
+  });
+  // 默认插入可见的静态目录(不依赖 Word 域更新,确保打开即有目录;含 h2/h3/h4)
+  const tocHtml = buildStaticToc();
   const styleCss = [
     'body, div, p, span, td, th, li, h1, h2, h3, h4, a {',
     '  font-family:"Microsoft YaHei","微软雅黑",SimSun,sans-serif;',
@@ -932,17 +900,34 @@ async function downloadWord(){
     '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; }',
+    'img{ max-width:300px; height:auto; display:block; margin:0 auto; }',
+    '.doc-toc{ margin:0 0 16px; page-break-after:avoid; }',
+    '.doc-toc h2{ border-bottom:2px solid #000; }',
+    '.doc-toc ul{ list-style:none; padding-left:0; margin:8px 0; }',
+    '.doc-toc li{ margin:4px 0; }',
+    '.doc-toc li.sub{ padding-left:24px; font-size:12.5px; }',
+    '.doc-toc li.sub2{ padding-left:48px; font-size:12px; }',
     '@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]-->';
+  // 页脚页码:规范 Word 域标记(PAGE / NUMPAGES),每页自动填充、显示在页尾
+  const footer =
+    '<div style="mso-element:footer" id="footer">' +
+    '  <p class="MsoFooter">' +
+    '第 ' +
+    '<span style="mso-field-code:PAGE"><!--[if supportFields]><span style="mso-element:field-begin"></span>PAGE<span style="mso-element:field-separator"></span><![endif]-->1<!--[if supportFields]><span style="mso-element:field-end"></span><![endif]--></span>' +
+    ' 页 / 共 ' +
+    '<span style="mso-field-code:NUMPAGES"><!--[if supportFields]><span style="mso-element:field-begin"></span>NUMPAGES<span style="mso-element:field-separator"></span><![endif]-->1<!--[if supportFields]><span style="mso-element:field-end"></span><![endif]--></span>' +
+    ' 页' +
+    '  </p>' +
+    '</div>';
   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>' +
+    footer +
+    '<div class="Section1">' + tocHtml + clone.innerHTML + '</div>' +
     '</body></html>';
   const blob = new Blob([html], {type:'application/msword;charset=utf-8'});
   saveBlob(blob, 'StationBuilder-设计方案.doc');