From: root Date: Thu, 20 Aug 2026 09:27:44 +0000 (+0800) Subject: 领域方案库 Cube 页面支持选择页面样式库样式后生成(方案级,镜像站点机制) X-Git-Url: http://acesimba.cloud/gitweb/?a=commitdiff_plain;h=04fba162b2066f13b1807fcb69c3e1fd7d187e66;p=StationBuilder.git 领域方案库 Cube 页面支持选择页面样式库样式后生成(方案级,镜像站点机制) - 后端 domain_templates 新增 style_id 列(ensureColumn 增量加列);handleDomainTemplateUpdate 支持写入/显式置空;hydrateDomainTemplateCtx 返回真实 style_id - 前端领域方案详情「功能与菜单 → Cube 扩展页面」顶部新增「领域方案页面样式」选择器(复用 .style-picker 与样式库缩略图),选中即持久化,生成/AI 修改经共享 buildStylePrompt 自动注入所选样式主题 - 设计文档升 v0.21,补充 changelog 与交互说明 --- 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 4dab6bc..2ea3a88 100644 Binary files a/data/db/StationBuilder.sqlite and b/data/db/StationBuilder.sqlite differ diff --git a/db-sidecar.mjs b/db-sidecar.mjs index 93478fb..a7109b9 100644 --- a/db-sidecar.mjs +++ b/db-sidecar.mjs @@ -339,6 +339,7 @@ function ensureColumn(table, column, def) { ensureColumn("cubes", "prompt", "TEXT"); ensureColumn("sites", "style_id", "INTEGER"); ensureColumn("domain_templates", "supplement", "TEXT"); +ensureColumn("domain_templates", "style_id", "INTEGER"); ensureColumn("domain_pages", "status", "TEXT DEFAULT 'pending'"); ensureColumn("domain_pages", "gen_filename", "TEXT"); ensureColumn("domain_pages", "prompt", "TEXT"); @@ -1682,8 +1683,9 @@ async function handleDomainTemplateUpdate(req, res, id) { const version = body.version != null ? body.version.toString() : row.version; const creator = body.creator != null ? body.creator.toString() : row.creator; const supplement = body.supplement != null ? body.supplement : row.supplement; + const style_id = body.style_id !== undefined ? (body.style_id != null ? Number(body.style_id) : null) : row.style_id; if (!name) return respondError(res, 400, "模板名称不能为空"); - db.prepare("UPDATE domain_templates SET domain_id=?,name=?,description=?,status=?,version=?,creator=?,supplement=? WHERE id=?").run(domain_id, name, description, status, version, creator, supplement, id); + db.prepare("UPDATE domain_templates SET domain_id=?,name=?,description=?,status=?,version=?,creator=?,supplement=?,style_id=? WHERE id=?").run(domain_id, name, description, status, version, creator, supplement, style_id, id); if (body.device_template_ids !== undefined) { db.prepare("DELETE FROM device_template_domain_templates WHERE domain_template_id=?").run(id); for (const devId of Array.isArray(body.device_template_ids) ? body.device_template_ids : []) { @@ -1720,7 +1722,7 @@ async function hydrateDomainTemplateCtx(tmplId) { const dt = db.prepare("SELECT dt.*, d.name AS domain_name FROM domain_templates dt JOIN domains d ON d.id=dt.domain_id WHERE dt.id=?").get(tmplId); if (!dt) return null; const assets = db.prepare("SELECT a.device_template_id, dt.name AS type, a.count, a.rule FROM domain_template_assets a JOIN device_templates dt ON dt.id=a.device_template_id WHERE a.domain_template_id=?").all(tmplId); - return { id: dt.id, name: dt.name, domain: dt.domain_name, description: dt.description || "", supplement: dt.supplement || "", assets, style_id: null, cubes: [] }; + return { id: dt.id, name: dt.name, domain: dt.domain_name, description: dt.description || "", supplement: dt.supplement || "", assets, style_id: dt.style_id != null ? dt.style_id : null, cubes: [] }; } async function resolveDeviceTemplate(name, domainId, tmplId) { diff --git a/public/design-doc.html b/public/design-doc.html index 4e4bf06..52f4f87 100644 --- a/public/design-doc.html +++ b/public/design-doc.html @@ -115,7 +115,7 @@
-

文档版本: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)

+
版本日期修改人说明
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 使其可收缩、内容正常折行,消除水平滚动,目录任意宽度常驻可见。