- 本文档是 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 后正常限宽并完整显示;图后插入回车(<br>)使图注文字换行到下一行。⑤ 补充 @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(){
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,'<');
html += '<li'+sub+'>'+text+'</li>';
});
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)=>{
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;',
'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');