From 04fba162b2066f13b1807fcb69c3e1fd7d187e66 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 20 Aug 2026 17:27:44 +0800 Subject: [PATCH] =?utf8?q?=E9=A2=86=E5=9F=9F=E6=96=B9=E6=A1=88=E5=BA=93=20?= =?utf8?q?Cube=20=E9=A1=B5=E9=9D=A2=E6=94=AF=E6=8C=81=E9=80=89=E6=8B=A9?= =?utf8?q?=E9=A1=B5=E9=9D=A2=E6=A0=B7=E5=BC=8F=E5=BA=93=E6=A0=B7=E5=BC=8F?= =?utf8?q?=E5=90=8E=E7=94=9F=E6=88=90=EF=BC=88=E6=96=B9=E6=A1=88=E7=BA=A7?= =?utf8?q?=EF=BC=8C=E9=95=9C=E5=83=8F=E7=AB=99=E7=82=B9=E6=9C=BA=E5=88=B6?= =?utf8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit - 后端 domain_templates 新增 style_id 列(ensureColumn 增量加列);handleDomainTemplateUpdate 支持写入/显式置空;hydrateDomainTemplateCtx 返回真实 style_id - 前端领域方案详情「功能与菜单 → Cube 扩展页面」顶部新增「领域方案页面样式」选择器(复用 .style-picker 与样式库缩略图),选中即持久化,生成/AI 修改经共享 buildStylePrompt 自动注入所选样式主题 - 设计文档升 v0.21,补充 changelog 与交互说明 --- app/page.tsx | 37 +++++++++++++++++++++++++++------- data/db/StationBuilder.sqlite | Bin 9060352 -> 9064448 bytes db-sidecar.mjs | 6 ++++-- public/design-doc.html | 4 +++- 4 files changed, 37 insertions(+), 10 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index 87ed177..a2c7c09 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -500,7 +500,7 @@ export default function Home(){
iStation StationBuilder / {["domains","domainDetail","styleLibrary","deviceTemplates"].includes(page)?meta[page].label:page==="projects"?"站点列表":`站点详情 / ${meta[page].label}`}

{meta[page].label}

{meta[page].subtitle}

{!["projects","domains","domainDetail","styleLibrary","deviceTemplates"].includes(page)&&}{page==="projects"&&}{page==="domains"&&}{page==="domains"&&openDomainTemplateId!=null&&}{domainFilter!=null&&page==="domains"&&}
{(["basic","assets","functions","generate"] as PageKey[]).includes(page)&&}
{page==="projects"&&} {(page==="domains"||page==="domainDetail")&&(openDomainTemplateId!=null - ? setOpenDomainTemplateId(null)} onCreated={setOpenDomainTemplateId} viewCompletePrompt={viewDtCompletePrompt} viewCompleteAssetsPrompt={viewDtCompleteAssetsPrompt} viewCompleteCubesPrompt={viewDtCompleteCubesPrompt}/> + ? setOpenDomainTemplateId(null)} onCreated={setOpenDomainTemplateId} viewCompletePrompt={viewDtCompletePrompt} viewCompleteAssetsPrompt={viewDtCompleteAssetsPrompt} viewCompleteCubesPrompt={viewDtCompleteCubesPrompt} pageStyles={pageStyles}/> : setOpenDomainTemplateId(id)} domainFilter={domainFilter} domainOptions={domainOptions} onDomainDeleted={handleDomainDeleted}/>)} {page==="styleLibrary"&&} {page==="deviceTemplates"&&} @@ -1338,13 +1338,13 @@ function PageRow({p,notify,onChange}:{p:any;notify:(m:string)=>void;onChange:()= function OpsFlowsViewer(){ const [flows,setFlows]=useState([]); useEffect(()=>{ fetchDb("/ops-flows").then((d:any)=>setFlows(d||[])).catch(()=>{}); },[]); return {flows.length===0? : flows.map((f:any)=>)}
流程名称内容
暂无
{f.name}{f.content}

运维流程功能尚未实现,当前仅保留占位记录。

; } -function DomainDetail({id,go,notify,onBack,onCreated,viewCompletePrompt,viewCompleteAssetsPrompt,viewCompleteCubesPrompt}:{id:number|null;go:(p:PageKey)=>void;notify:(m:string)=>void;onBack:()=>void;onCreated:(nid:number)=>void;viewCompletePrompt:(id:number,name:string,domain:string,description:string)=>void;viewCompleteAssetsPrompt:(id:number,name:string,domain:string,description:string,supplement:string)=>void;viewCompleteCubesPrompt:(id:number,name:string,domain:string,description:string,supplement:string,assets:any[])=>void}){ +function DomainDetail({id,go,notify,onBack,onCreated,viewCompletePrompt,viewCompleteAssetsPrompt,viewCompleteCubesPrompt,pageStyles}:{id:number|null;go:(p:PageKey)=>void;notify:(m:string)=>void;onBack:()=>void;onCreated:(nid:number)=>void;viewCompletePrompt:(id:number,name:string,domain:string,description:string)=>void;viewCompleteAssetsPrompt:(id:number,name:string,domain:string,description:string,supplement:string)=>void;viewCompleteCubesPrompt:(id:number,name:string,domain:string,description:string,supplement:string,assets:any[])=>void;pageStyles:PageStyle[]}){ const [doms,setDoms]=useState([]); const [loading,setLoading]=useState(false); const loadDoms=async()=>{ try{ setDoms(await fetchDb("/domains") as any); }catch{} }; useEffect(()=>{ loadDoms(); },[]); if(id==null) return ; - return ; + return ; } const DOMAIN_STEPS:{key:DomainStepKey;label:string}[]=[ @@ -1438,7 +1438,7 @@ function DomainPagePreviewModal({page,tmplId,notify,onClose}:{page:DomainPage;tm
; } -function DomainWizard({id,doms,go,notify,onBack,viewCompletePrompt,viewCompleteAssetsPrompt,viewCompleteCubesPrompt}:{id:number;doms:any[];go:(p:PageKey)=>void;notify:(m:string)=>void;onBack:()=>void;viewCompletePrompt:(id:number,name:string,domain:string,description:string)=>void;viewCompleteAssetsPrompt:(id:number,name:string,domain:string,description:string,supplement:string)=>void;viewCompleteCubesPrompt:(id:number,name:string,domain:string,description:string,supplement:string,assets:any[])=>void}){ +function DomainWizard({id,doms,go,notify,onBack,viewCompletePrompt,viewCompleteAssetsPrompt,viewCompleteCubesPrompt,pageStyles}:{id:number;doms:any[];go:(p:PageKey)=>void;notify:(m:string)=>void;onBack:()=>void;viewCompletePrompt:(id:number,name:string,domain:string,description:string)=>void;viewCompleteAssetsPrompt:(id:number,name:string,domain:string,description:string,supplement:string)=>void;viewCompleteCubesPrompt:(id:number,name:string,domain:string,description:string,supplement:string,assets:any[])=>void;pageStyles:PageStyle[]}){ const [t,setT]=useState(null); const [m,setM]=useState(null); const [assets,setAssets]=useState([]); @@ -1471,7 +1471,7 @@ function DomainWizard({id,doms,go,notify,onBack,viewCompletePrompt,viewCompleteA const [d, mt]=await Promise.all([ fetchDb(`/domain-templates/${id}`) as any, fetchDb(`/mu-tree`) as Promise ]); setMuTreeLocal(mt); setT(d); - setM({domain_id:d.domain_id,name:d.name,description:d.description||"",status:d.status,version:d.version,creator:d.creator||"",supplement:d.supplement||""}); + setM({domain_id:d.domain_id,name:d.name,description:d.description||"",status:d.status,version:d.version,creator:d.creator||"",supplement:d.supplement||"",style_id:d.style_id!=null?Number(d.style_id):null}); setAssets((d.assets||[]).map((a:any)=>({id:a.id,device_template_id:a.device_template_id,device_name:a.device_name||"",count:Number(a.count)||0,rule:a.rule||"",sort_order:a.sort_order||0}))); setPages((d.pages||[]).map((p:any)=>({id:p.id,name:p.name,description:p.description||"",json_text:p.json_text||"",status:p.status||"pending",gen_filename:p.gen_filename||null,prompt:p.prompt||null,source:p.source||null,sort_order:p.sort_order||0}))); setMu(d.mu && d.mu.length ? d.mu : []); @@ -1491,6 +1491,15 @@ function DomainWizard({id,doms,go,notify,onBack,viewCompletePrompt,viewCompleteA const saveMu=async(next?:string[])=>{ const sel=next??mu; setMu(sel); await patch({mu:sel}, "MU 选择已保存"); }; const toggleMu=(key:string)=> saveMu(effSelected.includes(key)? effSelected.filter(k=>k!==key) : [...effSelected, key]); + // ===== 领域方案页面样式(镜像站点侧 style_id 机制)===== + const selectStyle=async(sid:number|null)=>{ + try{ + await fetchDb(`/domain-templates/${id}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({style_id:sid})}); + setM((prev:any)=>({...prev,style_id:sid})); + notify(sid?"已应用页面样式":"已清除页面样式"); + }catch(e:any){ notify("应用样式失败:"+e.message); } + }; + // ===== Step1 AI 智能补全(写入补充描述)===== const runBasicComplete=async()=>{ if(basicAi.completing) return; @@ -1662,7 +1671,8 @@ function DomainWizard({id,doms,go,notify,onBack,viewCompletePrompt,viewCompleteA genIds={genIds} genLog={genLog} cubesAi={cubesAi} onRunCubesComplete={runCubesComplete} onViewCompleteCubesPrompt={()=>viewCompleteCubesPrompt(id,m.name,domainName,m.description,m.supplement||"",assets.map(a=>({type:a.device_name,count:a.count,rule:a.rule})))} updatePageField={(i,patchRow)=>setPages(prev=>prev.map((p,idx)=>idx===i?{...p,...patchRow}:p))} - tName={m.name} saving={saving} notify={notify} />} + tName={m.name} saving={saving} notify={notify} + pageStyles={pageStyles} styleId={m.style_id??null} onSelectStyle={selectStyle} />} {step==="opsflow" && } {step==="knowledge" && } @@ -1677,7 +1687,7 @@ function PlaceholderStep({title,note}:{title:string;note:string}){ return
待开发

{note}

; } -function FunctionsStep({muTree,grouped,effSelected,onToggleMu,pages,onAddPage,onUploadPageJson,onSavePageMeta,onDeletePage,onGeneratePage,onDownloadPage,onPreviewPage,genIds,genLog,cubesAi,onRunCubesComplete,onViewCompleteCubesPrompt,updatePageField,tName,saving,notify}:{muTree:MuNode[];grouped:any[];effSelected:string[];onToggleMu:(k:string)=>void;pages:DomainPage[];onAddPage:()=>Promise;onUploadPageJson:(f:File)=>Promise;onSavePageMeta:(p:DomainPage)=>Promise;onDeletePage:(p:DomainPage)=>Promise;onGeneratePage:(p:DomainPage)=>Promise;onDownloadPage:(p:DomainPage)=>void;onPreviewPage:(p:DomainPage)=>void;genIds:Set;genLog:string;cubesAi:ReturnType;onRunCubesComplete:()=>Promise;onViewCompleteCubesPrompt:()=>void;updatePageField:(i:number,patch:Partial)=>void;tName:string;saving:boolean;notify:(m:string)=>void}){ +function FunctionsStep({muTree,grouped,effSelected,onToggleMu,pages,onAddPage,onUploadPageJson,onSavePageMeta,onDeletePage,onGeneratePage,onDownloadPage,onPreviewPage,genIds,genLog,cubesAi,onRunCubesComplete,onViewCompleteCubesPrompt,updatePageField,tName,saving,notify,pageStyles,styleId,onSelectStyle}:{muTree:MuNode[];grouped:any[];effSelected:string[];onToggleMu:(k:string)=>void;pages:DomainPage[];onAddPage:()=>Promise;onUploadPageJson:(f:File)=>Promise;onSavePageMeta:(p:DomainPage)=>Promise;onDeletePage:(p:DomainPage)=>Promise;onGeneratePage:(p:DomainPage)=>Promise;onDownloadPage:(p:DomainPage)=>void;onPreviewPage:(p:DomainPage)=>void;genIds:Set;genLog:string;cubesAi:ReturnType;onRunCubesComplete:()=>Promise;onViewCompleteCubesPrompt:()=>void;updatePageField:(i:number,patch:Partial)=>void;tName:string;saving:boolean;notify:(m:string)=>void;pageStyles:PageStyle[];styleId:number|null;onSelectStyle:(id:number|null)=>void}){ const sourceBadge=(src?:string|null)=>{ if(src==="upload") return ↑ 上传; if(src==="ai") return ✦ AI生成; @@ -1690,6 +1700,19 @@ function FunctionsStep({muTree,grouped,effSelected,onToggleMu,pages,onAddPage,on {tab==="mu" &&
{grouped.map(g=>
{g.parentMenu}
{g.items.map((item:any)=>)}
)}
} {tab==="cube" &&
+
+

领域方案页面样式 方案级 · 本方案全部 Cube 页面共用

+

选择一个样式库方案作为本方案所有 Cube 页面的视觉风格(生成时参考其 JSON 主题参数)。

+
+ + {pageStyles.map(s=>)} +
+
{pages.map((p,i)=> diff --git a/data/db/StationBuilder.sqlite b/data/db/StationBuilder.sqlite index 4dab6bc5924dce30c82ed8decfed7d20b8fe8fed..2ea3a88066e67f590e795e72623ad8718c544a4e 100644 GIT binary patch delta 988 zcmYk*$xl;J6vy%VUQ1v5T6nEmK%h)2PPDcK5m3YdM^v0gREi*?rQib`K(LOLs1egh z|Zv4tdZ}K^dliXi&ZtjUjht1K!VRL_`%O!+$ zNc=n54-1RjF5$cXD~YD8{iIrlwBOp#gmXl9zv^qzR3Q{!Dy7iz+BxHsl=zReyDW|@ zyQxA-^nTORC8EWko6LIDukR`1~C~f3U>sFZBlfx&E^9U~ze%C^nXH-Wbc! zs~?#h=Di7JMEA%dU=)n#s>Gi%<)?YQ^6i?@YU!M0487KCJvnlxP{pKBGi_N)siiEX z)iLE5P(P|+F)6>5JL7rI_r~P7J|HRF|8OWE$32?Ics!vGnw|$nbjcRdl-bnw+0+E7 zE=z8E$K`NiJWrXM7@pLV6aAlc%jmd#U5bUj=x9d=I&lFPaS4~vg>GCy4|;JGQC!1y z+`vuT!fhk`CHBNEo8Rz`!{#Lg4pm5tl3kfX%vYx0_fJ2)Hx+$5{rpaMPg`T8p*hmj z5{h(&!m+P2G2Ah}&h#y)o|nuPTC6sa5^=n+$8FE7HC7veKNO`Z%aZor+@F*`{kVy7 delta 839 zcmYMy$xjn;7{>AWm6m~_w1cbyWf8%R)@fNRAk-MNyFoI4tf^MJbw?Xd0QD z!Ndy(69nT(Joq0NQZI%CxtRV17!x_@*OPDZc@EF*N#4lhlr<8WvOX`!5R#PgM*4rQ z7nN0+Lh?NOZKD*<6;k#%R}9Crp7Uhg%g@QELkq!r(P?3RE$V3*hk%L_1VKwqmfI_%X zgkqGS6l+k1wJ1jg)?qz1U?Vo65^i{~85+Fsp$gTgK`pkxk2=(&0b9|CCj8sJ4coB; zJFyG9u?Kt6jD6UTAP(RlTF{C^IE*7WieosAHk`mooWg0G!C9Qcd0ap{Lg>InTtX)< z;|i{#3)j$%Fs`Eqy=FYnS2sWF?dw+>>THT+DNirXc2-HE{l~}m3FAXzVlMG|JTw&Q z3Wqwvoo!x^HsI5IwQkMt_Gu-WzrNaEUsDsE%fDgH;stt&zq zZFS1IZntHubv7uvh4J>Ao)If?$VO3I&ot*}bg!$J_p?h&lB3t2uumvol(4jvvLI*K z{#aGZC(B^0RQ+r&&Fd4wOl^MpOOA#Ybo65YgBZe1+`?_#!Cl
-

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

+

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

StationBuilder 产品与交互设计方案

1. 文档说明

@@ -506,6 +506,7 @@
+
页面名称 最多6字页面描述来源Json文件操作
updatePageField(i,{name:e.target.value})}/> updatePageField(i,{description:e.target.value})}/>
筛选栏(查找 / 所属领域 / 状态 / 条数)列表实时筛选与计数本地 state(q / domainSel / statusSel),作用于 GET /api/db/domain-templates 结果查找框按 方案名称/创建人 模糊匹配;所属领域、状态下拉精确过滤;右上角显示「共 N 条」
领域方案列表表格展示全部领域方案(所属领域 / 方案名称 / 版本 / 状态 / 创建人 / 设备·页面·文档数 / 操作)GET /api/db/domain-templates(含 domain_name、device_count、page_count、doc_count)每行「编辑」→ 打开该方案详情编辑器;「删除」→ 确认后 DELETE /api/db/domain-templates/:id(级联删除 device_template_domain_templates)并刷新;空列表提示「点击右上角新增」
管理领域面板领域分类管理:列出全部领域及方案数,支持删除领域GET /api/domains(领域列表);删除调用 DELETE /api/domains/:id「其他」标注「(默认保留)」且无删除按钮;其余领域显示「删除」按钮 → 自定义确认弹窗二次确认 → 调用 DELETE /api/domains/:id(后端先将其下 domain_templates / device_template_domains 迁移至「其他」,再删除领域)→ 前端刷新领域列表、若当前筛选命中该领域则清除筛选
领域方案页面样式(Cube 扩展页面顶部)为方案选择页面样式库样式,作用于本方案全部 Cube 页面GET /api/page-styles(样式列表 + 缩略图);选中 PUT /api/db/domain-templates/:id(style_id)持久化;生成经共享 buildStylePrompt 注入样式主题「功能与菜单 → Cube 扩展页面」顶部 .style-picker:含「不使用样式」与全部页面样式缩略图按钮,当前选中高亮;点击即写回 domain_templates.style_id,生成/AI 修改时自动带入该样式视觉主题(与站点侧机制一致)
「+ 新增领域方案」新建领域方案,进入新增表单—同页切到 CreateTemplate 表单:所属领域(select 来自 GET /api/domains)、模板名称、版本、状态(可用/不可用)、创建人、领域描述;POST /api/db/domain-templates 成功后自动打开该方案详情编辑器
搜索 / 筛选 / 同步方案库(历史功能)—已按计划 Phase 0 移除整行(含「同步方案库」占位按钮) 已移除
@@ -752,6 +753,7 @@

11.1 版本变更记录(Changelog)

+ -- 2.43.0
版本日期修改人说明
v0.212026-08-20wang.yu领域方案库 Cube 扩展页面支持选择页面样式库样式后生成(镜像站点侧 sites.style_id 机制,方案级):① 后端 domain_templates 新增 style_id 列(ensureColumn 增量加列);handleDomainTemplateUpdate 支持写入/显式置空;hydrateDomainTemplateCtx 返回真实 style_id(此前硬编码 null)。② 前端领域方案详情「功能与菜单 → Cube 扩展页面」顶部新增「领域方案页面样式」选择器(复用 .style-picker 与页面样式库缩略图),选中即 PUT /domain-templates/:id 持久化;生成/AI 修改经共享的 buildStylePrompt 自动注入所选样式的 JSON 主题,无需改动提示词构造。
v0.202026-08-20wang.yu领域分类管理增强:① 默认分类调整——移除「工业制造 / 工业园区 / 智慧楼宇」,新增「其他」(DEFAULT_DOMAINS 同步更新,重启不再复活被删分类);② 领域方案库新增「管理领域」面板,列出全部领域及其方案数,「其他」标注「默认保留」且不可删除,其余领域提供「删除」按钮,经自定义确认弹窗二次确认后调用 DELETE /api/domains/:id;③ 后端新增 DELETE /api/domains/:id:禁止删除「其他」,删除前将待删领域的 domain_templates 与 device_template_domains 关联迁移至「其他」(避免 ON DELETE CASCADE 误删方案),再删除领域;④ 删除后前端刷新领域列表,并在当前筛选命中该领域时自动清除筛选。
v0.192026-08-20wang.yu修复领域方案「功能与菜单 → Cube 扩展页面」两处问题:①「生成」按钮改为流式请求(消费 NDJSON 响应体以保持连接),生成期间按钮显示「生成中…」并禁用、底部「AI 生成页面」控制台实时输出进度并据服务端 {status:failed} 事件弹失败提示;此前前端用普通 fetch 不读取响应体,连接被提前关闭导致服务端 AI 生成中断(status='failed')、Json 未写入。② AI 智能补全(complete-cubes)由「逐条静默 .catch(()=>{}) 插入」改为「收集结果后逐个 await 插入、失败显式 notify」,并捕获服务端 {status:failed} 事件弹失败提示,避免补全内容静默丢失、列表无变化。
v0.172026-08-19wang.yu修复左侧目录(TOC)点击章节后消失:根因为顶部版本说明段落含全角空格不换行,在 flex 布局下把 main#doc 撑到约 2238px 宽(flex item 默认 min-width:auto 不收缩),点击锚点时浏览器水平滚动将 sticky 目录推出左边界。给 main#doc 增加 min-width:0 使其可收缩、内容正常折行,消除水平滚动,目录任意宽度常驻可见。