From b3894756c0098bc37ea0d8e59ff9ba2f604256e1 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 21 Aug 2026 17:13:29 +0800 Subject: [PATCH] =?utf8?q?v0.32=20=E9=A2=86=E5=9F=9F=E6=96=B9=E6=A1=88?= =?utf8?q?=E3=80=8C=E6=96=B9=E6=A1=88=E6=89=93=E5=8C=85=E3=80=8D=E6=AD=A5?= =?utf8?q?=E9=AA=A4=E9=87=8D=E6=9E=84=EF=BC=9A=E6=96=B9=E6=A1=88=E6=A6=82?= =?utf8?q?=E8=A7=88=E5=A4=9A=E6=A0=87=E7=AD=BE=E6=80=BB=E8=A7=88=20+=20?= =?utf8?q?=E6=89=A9=E5=B1=95=E9=A1=B5=E9=9D=A2=E9=A2=84=E8=A7=88=E6=95=B4?= =?utf8?q?=E9=A1=B5=E4=BF=AE=E5=A4=8D=20+=20=E6=89=93=E5=8C=85=E4=B8=8B?= =?utf8?q?=E8=BD=BD=E7=A4=BA=E6=84=8F=E5=BC=B9=E7=AA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- app/globals.css | 49 ++++++++++++++- app/page.tsx | 136 +++++++++++++++++++++++++++++++---------- public/design-doc.html | 3 +- 3 files changed, 153 insertions(+), 35 deletions(-) diff --git a/app/globals.css b/app/globals.css index a28cbec..4864e99 100644 --- a/app/globals.css +++ b/app/globals.css @@ -232,8 +232,8 @@ .badge.src-upload{display:inline-flex;align-items:center;gap:2px;background:#e7f5fc;color:#007cc3;border:1px solid #b9e0f4;border-radius:10px;padding:2px 9px;font-size:10px} .badge.src-ai{display:inline-flex;align-items:center;gap:2px;background:#f3e8ff;color:#7a3cc0;border:1px solid #dcc4f3;border-radius:10px;padding:2px 9px;font-size:10px} .badge.src-manual{display:inline-flex;align-items:center;gap:2px;background:#f1f2f3;color:#6b7177;border:1px solid #dfe2e4;border-radius:10px;padding:2px 9px;font-size:10px} -.domain-preview-canvas{background:#1e2630;min-height:430px;border-radius:6px;overflow:hidden} -.domain-preview-canvas .dashboard-thumb-live{min-height:430px} +.domain-preview-canvas{background:#1e2630;height:64vh;border-radius:6px;overflow:auto} +.domain-preview-canvas .dashboard-thumb-live{min-height:0} .domain-preview-feedback{margin-top:14px} .domain-preview-feedback textarea{width:100%;border:1px solid #cfd3d7;border-radius:3px;padding:9px;resize:vertical;line-height:1.6;margin-top:6px} .domain-review-pages{display:grid;grid-template-columns:1fr;gap:12px} @@ -321,3 +321,48 @@ .domain-nav-input{flex:1;min-width:0;margin:0 4px;height:28px;border:1px solid #cfd4d9;border-radius:3px;padding:0 8px;font-size:12px;background:#fff} .domain-nav-input:focus{outline:1px solid var(--blue);border-color:var(--blue)} +/* ===== 方案打包 / 方案概览(v0.32) ===== */ +.gen-overview{display:block} +.gen-overview .panel{margin-bottom:0} +.overview-info{display:grid;grid-template-columns:1fr 1fr;gap:10px 18px;padding:4px 2px 14px;border-bottom:1px solid var(--line);margin-bottom:12px} +.overview-info>div{display:flex;flex-direction:column;gap:3px} +.overview-info span{font-size:10px;color:#8b9094;font-weight:bold;letter-spacing:.3px} +.overview-info b{font-size:14px;color:#2b2f33} +.overview-info .overview-desc{grid-column:1 / -1} +.overview-info .overview-desc p{margin:0;font-size:12px;color:#555;line-height:1.6;background:#f8fafb;border:1px solid #eef1f3;border-radius:4px;padding:7px 9px} +.overview-tabs{margin:0 0 12px} +.overview-tabs button{padding:0 14px;font-size:12px} +.overview-body{min-height:120px} +.overview-body .muted{padding:10px 2px} +.dev-list{display:flex;flex-direction:column;gap:8px} +.dev-row{border:1px solid var(--line);border-radius:6px;overflow:hidden;background:#fff} +.dev-row-head{width:100%;border:0;background:transparent;display:flex;align-items:center;gap:10px;padding:11px 13px;text-align:left;cursor:pointer} +.dev-row-head:hover{background:#f6fafe} +.dev-row-head b{font-size:13px;flex:1} +.dev-count{font-size:11px;color:#008ed2;background:#e7f3f8;border-radius:10px;padding:1px 9px} +.dev-caret{color:#9aa1a5;font-size:11px} +.dev-detail{border-top:1px solid #eef1f3;padding:11px 13px;background:#fbfcfd} +.dev-desc{margin:0 0 8px;font-size:12px;color:#555;line-height:1.7} +.dev-cp b{font-size:11px;color:#333;display:block;margin-bottom:4px} +.dev-cp ul{margin:0;padding-left:18px;color:#666;font-size:11px;line-height:1.8} +.doc-list{display:flex;flex-direction:column;gap:7px} +.doc-item{display:flex;align-items:baseline;gap:10px;padding:9px 11px;border:1px solid var(--line);border-radius:6px;background:#fff} +.doc-item b{font-size:13px;flex:1} +.doc-item .muted{font-size:11px;white-space:nowrap} +.ops-placeholder{display:flex;flex-direction:column;gap:10px;padding:18px;border:1px dashed #cfd6da;border-radius:8px;background:#fafbfc} +.ops-placeholder .status{align-self:flex-start} +.ops-placeholder p{margin:0;font-size:12px;color:#777;line-height:1.7} +/* ===== 方案打包示意图弹窗(v0.32) ===== */ +.pkg-modal{width:560px} +.pkg-body{padding:18px} +.pkg-body .hint{margin:0 0 14px} +.pkg-box{display:flex;justify-content:center;padding:6px 0 16px} +.pkg-svg{width:240px;height:auto;filter:drop-shadow(0 8px 16px rgba(120,80,20,.18))} +.pkg-items{display:grid;grid-template-columns:1fr 1fr;gap:10px} +.pkg-item{display:flex;align-items:flex-start;gap:10px;padding:11px 12px;border:1px solid var(--line);border-radius:8px;background:#fff;box-shadow:0 1px 3px #0000000a} +.pkg-item:hover{border-color:#9ed3ec;box-shadow:0 2px 8px #008fd21f} +.pkg-icon{width:34px;height:34px;flex:0 0 auto;border-radius:7px;background:linear-gradient(135deg,#e8f4fb,#d6ecf7);color:#008ed2;display:grid;place-items:center;font-size:17px} +.pkg-item-text{display:flex;flex-direction:column;gap:3px;min-width:0} +.pkg-item-text b{font-size:13px;color:#2b2f33} +.pkg-item-text small{font-size:10px;color:#888;line-height:1.5} + diff --git a/app/page.tsx b/app/page.tsx index 4c8d998..210b016 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1392,7 +1392,7 @@ const DOMAIN_STEPS:{key:DomainStepKey;label:string}[]=[ {key:"assets",label:"资产设置"}, {key:"functions",label:"功能与菜单"}, {key:"opsflow",label:"运维流程"}, - {key:"generate",label:"生成保存"}, + {key:"generate",label:"方案打包"}, {key:"expert",label:"专家确认"}, ]; @@ -1419,7 +1419,7 @@ async function streamInto(res:Response, setLog:(f:(p:string)=>string)=>void, onE for(;;){ const {done,value}=await reader.read(); if(done) break; buf+=dec.decode(value,{stream:true}); const lines=buf.split("\n"); buf=lines.pop()||""; for(const line of lines){ if(!line.trim()) continue; try{ const ev=JSON.parse(line); if(ev.log) setLog(prev=>prev+ev.log); onEv(ev); }catch{} } } } -function DomainPageThumb({jsonText,emptyLabel="未生成"}:{jsonText:string; emptyLabel?:string}){ +function DomainPageThumb({jsonText,emptyLabel="未生成",fit=false}:{jsonText:string; emptyLabel?:string; fit?:boolean}){ const outer=useRef(null); const inner=useRef(null); useEffect(()=>{ @@ -1428,22 +1428,24 @@ function DomainPageThumb({jsonText,emptyLabel="未生成"}:{jsonText:string; emp (async()=>{ try{ const json=JSON.parse(jsonText); - if(cancelled||!inner.current) return; + if(cancelled||!inner.current||!outer.current) return; const renderDashboard:any=await loadRenderer(); renderDashboard(json, inner.current, {}); - const w=outer.current?.clientWidth||300; + const w=outer.current.clientWidth||300; const pw=json.grid?.pageWidth||1920; const rootEl=inner.current.firstElementChild as HTMLElement|null; const rh=rootEl? rootEl.offsetHeight : (json.grid?.pageHeight||1080); - const s=Math.max(0.02, w/pw); + // fit 模式:同时按宽、高缩放,整页一次可见(修复“只能预览上半部分”) + const availH = fit ? (outer.current.parentElement?.clientHeight || rh) : Infinity; + const s = fit ? Math.max(0.02, Math.min(w/pw, availH/rh)) : Math.max(0.02, w/pw); inner.current.style.height=`${rh}px`; inner.current.style.transform=`scale(${s})`; inner.current.style.transformOrigin="top left"; - if(outer.current) outer.current.style.height=`${rh*s}px`; + outer.current.style.height=`${rh*s}px`; }catch(e){ if(inner.current) inner.current.innerHTML='
预览不可用
'; } })(); return ()=>{cancelled=true;}; - },[jsonText, emptyLabel]); + },[jsonText, emptyLabel, fit]); return
; } @@ -1470,7 +1472,7 @@ function DomainPagePreviewModal({page,tmplId,notify,onClose}:{page:DomainPage;tm return
onClose(false)}>
e.stopPropagation()}>

{page.name||"页面预览"}

-
{hasJson? :
尚未生成,请点击「生成」后再预览
}
+
{hasJson? :
尚未生成,请点击「生成」后再预览
}

修改意见

输入对当前页面的修改意见,AI 将据此重新生成该页面。

@@ -1496,6 +1498,7 @@ function DomainWizard({id,doms,go,notify,onBack,viewCompletePrompt,viewCompleteA const [devCreateOpen,setDevCreateOpen]=useState(false); const [pendingDevRow,setPendingDevRow]=useState(null); const [previewPage,setPreviewPage]=useState(null); + const [pkgOpen,setPkgOpen]=useState(false); const basicAi=useStreamComplete(); const assetsAi=useStreamComplete(); const cubesAi=useStreamComplete(); @@ -1742,10 +1745,11 @@ function DomainWizard({id,doms,go,notify,onBack,viewCompletePrompt,viewCompleteA pageStyles={pageStyles} styleId={m.style_id??null} onSelectStyle={selectStyle} />} {step==="opsflow" && } - {step==="generate" && goStep("functions")} onKnowledge={()=>goStep("knowledge")} onConfirm={()=>notify("方案已生成并保存,可返回上一步继续调整")} onPreviewPage={setPreviewPage}/>} + {step==="generate" && goStep("opsflow")} onPreviewPage={setPreviewPage}/>} {step==="expert" && }
-
{curIdxgoStep(DOMAIN_STEPS[curIdx+1].key)}>下一步}{curIdx===DOMAIN_STEPS.length-1 && }
+
{step==="generate" && }{step!=="generate" && curIdxgoStep(DOMAIN_STEPS[curIdx+1].key)}>下一步}{curIdx===DOMAIN_STEPS.length-1 && }
+ {pkgOpen && setPkgOpen(false)} notify={notify}/>} ; } @@ -1821,34 +1825,102 @@ function FunctionsStep({muTree,grouped,effSelected,onToggleMu,pages,onAddPage,on ; } -function DomainGenerateReview({tName,assets,effSelected,grouped,pages,docs,onBack,onKnowledge,onConfirm,onPreviewPage}:{tName:string;assets:DomainAsset[];effSelected:string[];grouped:any[];pages:DomainPage[];docs:any[];onBack:()=>void;onKnowledge?:()=>void;onConfirm:()=>void;onPreviewPage?:(p:DomainPage)=>void}){ - const reviewRows:[string,string,string,string?,(()=>void)?][]=[ - ["方案",tName,"","返回修改",onBack], - ["资产",`${assets.filter(a=>a.device_template_id).length} 类 / ${assets.reduce((n,a)=>n+Number(a.count||0),0)} 个设备`,"3 层资产结构",""], - ["MU 页面",`${effSelected.length} 个标准页面`,"复用并配置菜单",""], - ["Cube 页面",`${pages.length} 个扩展页面`,`已生成 ${pages.filter(p=>p.status==="generated").length} 个`,""], - ["菜单结构","MU + 综合监控",`共 ${effSelected.length+pages.length} 项`,""], - ["领域知识", docs.length?`${docs.length} 份(${docs.slice(0,3).map((d:any)=>d.name).join("、")}${docs.length>3?" 等":""})`:"暂无知识", docs.length?"已沉淀领域知识,将作为 AI 补全优先参考":"可上传文档 / 配置站点", "返回领域知识", onKnowledge], - ["运维流程","标准 SOP","(本期占位,待开发)",""], +function DomainGenerateReview({m,domainName,assets,effSelected,grouped,pages,docs,onBack,onPreviewPage}:{ + m:any;domainName:string;assets:DomainAsset[];effSelected:string[];grouped:any[];pages:DomainPage[];docs:any[];onBack:()=>void;onPreviewPage?:(p:DomainPage)=>void +}){ + const [tab,setTab]=useState("dev"); + const [devOpen,setDevOpen]=useState(null); + const [devDetail,setDevDetail]=useState(null); + const [devLoading,setDevLoading]=useState(false); + const openDev=async(id:number)=>{ + if(devOpen===id){ setDevOpen(null); return; } + setDevOpen(id); setDevLoading(true); setDevDetail(null); + try{ const d=await fetchDb(`/device-templates/${id}`) as any; setDevDetail(d); }catch(e:any){} finally{ setDevLoading(false); } + }; + const devList=Array.from(new Map(assets.filter(a=>a.device_template_id).map(a=>[a.device_template_id as number,a])).values()); + const assetGroups=assets.filter(a=>a.device_template_id); + const muItems=grouped.flatMap((g:any)=>g.items).filter((l:any)=>effSelected.includes(l.key)); + const tabs:{key:string;label:string}[]=[ + {key:"dev",label:`设备模板 ${devList.length}`}, + {key:"asset",label:`资产结构 ${assetGroups.length}`}, + {key:"mu",label:`MU页面 ${effSelected.length}`}, + {key:"cube",label:`扩展页面 ${pages.length}`}, + {key:"knowledge",label:`领域知识 ${docs.length}`}, + {key:"ops",label:"运维流程"}, ]; - return
-
- {reviewRows.map(r=>
{r[0]}{r[1]}{r[2]}{r[3]&&}
)} -
-
⌄ {tName||"未命名方案"}
{assets.filter(a=>a.device_template_id).map((row,i)=>
⌄ {row.device_name}{row.count}
{row.count>0&&
{formatInstances(row.rule,row.count)}
}
)}
-
⌄ {tName||"未命名方案"}
MU 标准页面
{effSelected.length===0?
(未选择)
:grouped.flatMap((g:any)=>g.items).filter((l:any)=>effSelected.includes(l.key)).map((l:any)=>
· {l.parentMenu}/{l.label}
)}
综合监控
{pages.filter(p=>p.name).map(p=>
· {p.name}
)}
-
-
- -

以下为方案中已配置的 Cube 扩展页面预览(点击可放大查看)。如需调整,请返回「功能与菜单」。

-
{pages.length===0?
暂无 Cube 页面
: pages.map(p=>
onPreviewPage&&onPreviewPage(p)}>
{p.status==="generated"?"AI已生成":p.status==="uploaded"?"已上传":"未生成"}{p.source?` · ${p.source==="ai"?"AI":p.source==="upload"?"上传":"手动"}`:""}

{p.name}

⛶ 点击放大
)}
-
+ return
+ +
+
所属领域{domainName||"—"}
+
方案名称{m.name||"—"}
+
方案描述

{m.description||"—"}

+
补充描述

{m.supplement||"—"}

+
+
+ {tabs.map(t=>)} +
+
+ {tab==="dev" &&
{devList.length===0?

本方案未选用设备模板。

: + devList.map((a:any)=>( +
+ + {devOpen===a.device_template_id &&
{devLoading?

加载中…

: + devDetail? <>

{devDetail.description||"(无说明)"}

+
采集点({(devDetail.collection_points||[]).length}){(devDetail.collection_points||[]).length===0? 无 : +
    {(devDetail.collection_points||[]).slice(0,40).map((cp:any,i:number)=>
  • {cp.name}{cp.unit?`(${cp.unit})`:""}
  • )}
}
: +

无法加载详情。

}
} +
+ ))}
} + {tab==="asset" &&
⌄ {m.name||"未命名方案"}
{assetGroups.map((row,i)=>
⌄ {row.device_name}{row.count}
{row.count>0&&
{formatInstances(row.rule,row.count)}
}
)}
} + {tab==="mu" &&
{grouped.length===0?

(未加载 MU 树)

: + grouped.map(g=>
{g.parentMenu}
{g.items.map((item:any)=>)}
)}
} + {tab==="cube" &&
{pages.length===0?
暂无扩展页面,返回「功能与菜单 → 领域扩展页面」生成。
: + pages.map(p=>
onPreviewPage&&onPreviewPage(p)}>
{p.status==="generated"?"AI已生成":p.status==="uploaded"?"已上传":"未生成"}{p.source?` · ${p.source==="ai"?"AI":p.source==="upload"?"上传":"手动"}`:""}

{p.name}

⛶ 点击放大
)}
} + {tab==="knowledge" &&
{docs.length===0?

未上传领域知识,返回「领域知识」步骤上传。

: + docs.map((d:any,i:number)=>
{d.name}{d.type==="link"?"参考站点":(d.mime||"文档")}{(d.size||d.file_size)?` · ${(Number(d.size||d.file_size)/1024).toFixed(1)}KB`:""}
)}
} + {tab==="ops" &&
待开发

运维流程编辑功能待开发,后续版本将支持在方案中配置标准运维 SOP 与告警处理流程。

} +
-
; +
; } +function PackageModal({name,onClose,notify}:{name?:string;onClose:()=>void;notify:(m:string)=>void}){ + const items=[ + {icon:"⬡",label:"设备模板",desc:"本方案选用的设备模版及采集点"}, + {icon:"▦",label:"资产结构",desc:"默认资产组与设备实例层级"}, + {icon:"❖",label:"扩展页面文件",desc:"已生成的 Cube 扩展页面文件"}, + {icon:"☰",label:"默认菜单",desc:"MU + 综合监控 菜单结构"}, + {icon:"📄",label:"领域知识",desc:"上传的领域知识文档"}, + ]; + return
e.stopPropagation()}> +

方案打包

+
+

即将把方案「{name||"未命名方案"}」打包为可交付成果,包含以下组成部分:

+
+ + + + + + + + + + + +
+
+ {items.map(it=>
{it.icon}
{it.label}{it.desc}
)} +
+
+
+
; +} + function KnowledgeStep({id,docs,notify,reload,confirm}:{id:number;docs:any[];notify:(m:string)=>void;reload:()=>void;confirm:(msg:string)=>Promise}){ const [busy,setBusy]=useState(false); const [urlName,setUrlName]=useState(""); diff --git a/public/design-doc.html b/public/design-doc.html index 1923eee..1b95cfb 100644 --- a/public/design-doc.html +++ b/public/design-doc.html @@ -111,7 +111,7 @@
-

文档版本:v0.31 最后更新:2026-08-21 修改人:wang.yu 状态:开发中(演示数据已标注)

+

文档版本:v0.32 最后更新:2026-08-21 修改人:wang.yu 状态:开发中(演示数据已标注)

StationBuilder 产品与交互设计方案

1. 文档说明

@@ -759,6 +759,7 @@

10.1 版本变更记录(Changelog)

+ -- 2.43.0
版本日期修改人说明
v0.322026-08-21wang.yu领域方案「生成保存」步骤重构为「方案打包」:① 步骤改名:DOMAIN_STEPS 末步标签「生成保存」→「方案打包」;该步骤不再提供「下一步」,仅保留「保存草稿」与本步专属「📦 打包下载」按钮(末步另保留「提交确认」以提交专家确认)。② 「生成范围确认」→「方案概览」多标签总览:DomainGenerateReview 重写为单 Panel「方案概览」+ 六标签(标签上带数量徽标):设备模板 N(方案去重用到的设备模板列表,点击行内展开详情——调 GET /api/db/device-templates/:id 显示说明与采集点清单)、资产结构 N(按设备模板聚合的默认资产组树状预览,使用 formatInstances(rule,count) 展开实例)、MU页面 N(复用功能与菜单的 MU 树,禁用态勾选框展示已选页面)、扩展页面 N(已生成 Cube 页面卡片,点击整卡经 onPreviewPage 打开全屏预览)、领域知识 N(列举本方案上传的知识文档与 URL 站点)、运维流程(占位「待开发」+ 固定文案「运维流程编辑功能待开发,后续版本将支持在方案中配置标准运维 SOP 与告警处理流程。」)。概览头部固定展示所属领域 / 方案名称 / 方案描述 / 补充描述。③ 修复扩展页面预览「只能看到上半部分」:DomainPageThumb 新增 fit 模式——由「仅按宽度缩放」改为「按宽高较小比例缩放」s = min(w/pw, availH/rh)(availH 取预览 canvas 父容器高度),canvas 高度由固定 430px 改为 height:64vh; overflow:auto,整页一次可见不再裁切。④ 「📦 打包下载」示意弹窗:新增 PackageModal——SVG 立体包装箱(堆叠文档矩形,带投影)+ 5 张打包组成卡片(设备模板 / 资产结构 / 扩展页面文件 / 默认菜单 / 领域知识),「确认打包下载」提示「方案已打包(示意),可交付给实施团队」后关闭(本期为示意,真实文件导出待后续)。globals.css 增补 .gen-overview / .overview-info / .overview-tabs / .dev-list / .dev-detail / .asset-tree-preview / .mu-tree / .doc-list / .ops-placeholder / .pkg-modal / .pkg-svg / .pkg-items 等样式。Playwright E2E(污水厂方案,34 设备 / 11 页面):步骤标签=方案打包、Panel=方案概览、6 标签徽标正确、设备行展开显示 12 个采集点、资产树 / MU 树 / 页面卡片渲染、预览 canvas 渲染高度 576px = 缩放后整页高度(证明整页可见,不再上半部裁切)、知识 / 运维标签文案正确、本步无「下一步」、打包弹窗含 SVG + 5 组成项,0 控制台报错;另以数字机房方案验证领域知识列表确实展示已上传文档。
v0.312026-08-21wang.yuUI 风格对齐 KB Insight 门户(Ant Design):参照 https://portal-kbinsight-wiseiot-ensaas.practice.cloud.advantech.com.cn/kb/homepage 调整 ① 字体:由 Arial,"Microsoft YaHei" 改为 AntD 系统字体栈 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Microsoft YaHei",与门户一致(中文保留雅黑 / Noto)。② 顶部 Header:高度 42px→56px,去除 4px 实色顶边,改为 3px 蓝白渐变顶线(模拟门户 .bd-linear,用 background-clip:padding-box,border-box 双层背景实现);同步 .app-shell 顶部内边距与 .side-nav 顶部偏移 42px→56px,避免布局错位。③ 左侧菜单选中/悬停对齐 AntD:选中背景由 #dff1fa/#e9f6fc 改为 rgba(0,140,214,.1)(对应门户 --color-blue-tree/--color-menu-bk),选中文字 #008cd6,悬停浅灰 rgba(0,0,0,.04)(对应 --color-hover-gray);覆盖 .side-nav>button、.module-switch>button、.domain-nav-row 三处,菜单字号提到 14px;修复 .domain-nav-row>button:not() 高优先级规则覆盖选中态的问题(给选中规则补 :not(.domain-nav-del):not(.domain-nav-ok) 提权)。主蓝 token 维持 #009fe3(与目标 #008CD6 肉眼无差,避免大范围改色不一致)。Playwright 断言:body 字体栈含 Segoe UI/Roboto、header 高度 56 且 backgroundImage 为双层渐变、三类菜单选中背景均为 rgba(0,140,214,.1)、0 控制台报错。
v0.302026-08-21wang.yu修复领域方案「生成保存」页面预览全部空白:根因为 public/dashboard-renderer.js 的 renderDashboard 每次渲染开头执行 renderDashboard._instances.forEach(i=>i.dispose()) 把全部全局 echarts 实例销毁——同一方案 11 张页面缩略图顺序渲染时,后一张的渲染把前一张的图表实例 dispose 掉,仅最后一张卡片保留图表、其余均为空白(DOM 有内容但 0 个 <canvas>)。修复:废弃全局实例数组,改为按容器隔离——每个容器记录自身 container.__dashInst,渲染前仅销毁本容器旧实例并清空 innerHTML,渲染后回挂本容器实例;新增模块级 _curInst 收集本次渲染的实例。现已验证污水厂(橙色风格)方案 11 张卡片全部渲染出图表(共 43 个 canvas,每卡 ≥0)。附带增强:① 生成确认页「领域方案页面」列表卡片改为整宽单列大图(.domain-review-pages 由两列改 1 列,.dashboard-thumb-live 最小高度 115px→240px),并整体可点击——点击卡片经新增 onPreviewPage 入参打开 DomainPagePreviewModal 全屏预览(含图表),卡片右下角加「⛶ 点击放大」提示。② globals.css 增补 .domain-review-page hover 描边与 .review-zoom 提示样式。Playwright E2E:11 卡全部有 canvas、点开预览 modal 含 2 个 canvas + 361 字正文,0 控制台报错。
v0.292026-08-21wang.yu领域方案库 / 站点「功能与菜单 → Cube 扩展页面」遗留问题修复与体验提升:① 页签改名与排序:站点与领域两处「Cube 扩展页面」页签统一更名为「领域扩展页面」,并调整顺序置于「MU 标准页面」之前(默认进入该页签);文档同步。② 生成按钮醒目化:两处扩展页面表格的「生成」按钮文案升级为「AI生成」、生成中显示「AI生成中」,并采用醒目渐变蓝紫样式(.btn-ai,覆盖 .cube-actions button 白底默认态)。③ 统一生成:领域方案「领域扩展页面」表格上方新增「⚡ AI 统一生成全部(N)」按钮(N = 未生成页面数),一键对所有「未生成」且未在生成中的页面并发触发 generatePage;后端按每页独立锁 + AI_CONCURRENCY=2 信号量排队,各自页面按钮同步显示「AI生成中」,免去逐个点击(仅保留单页生成)。④ 图片加载提速:后端 db-sidecar 的 handlePageStyleImage / handleDeviceTemplateImage 新增 ?w= 宽度参数,经 sharp 生成 WebP 缩略图并落盘缓存(mtime 校验失效重生成),原图约 1.4MB → 缩略图约 9KB(提速约 150 倍);前端样式选择器(?systemctl restart stationbuilder-db 生效(新增 sharp 依赖,Node 原生模块已就绪)。Playwright E2E:领域方案进入「领域扩展页面」页签确认置首、10 个页面按钮显示「AI生成」、统一按钮计数正确;新增待生成页面后点「AI 统一生成全部」其按钮即时翻为「AI生成中」、其余已生成页保持「AI生成」,0 控制台报错;页面样式库 5 张缩略图均带 ?w=、缩略图接口实测 1355KB→8.9KB。