From 7eb3c01e48863845815c856d4b1ed8891bd79de0 Mon Sep 17 00:00:00 2001 From: "wang.yu" Date: Wed, 19 Aug 2026 17:53:30 +0800 Subject: [PATCH] =?utf8?q?=E9=A2=86=E5=9F=9F=E6=96=B9=E6=A1=88=E5=BA=93?= =?utf8?q?=E9=87=8D=E6=9E=84=E4=B8=BA=207=20=E6=AD=A5=E5=90=91=E5=AF=BC?= =?utf8?q?=EF=BC=88=E4=BB=BF=20Station=20Builder=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit - 方案基本属性:补充描述输入 + 右侧填写建议 + AI 智能补全 - 资产设置:设备模板库下拉(禁止自由输入/+新增)、4 行可编辑资产表、 资产层级预览、AI 补全(自动建缺失设备模板)、新增设备模板弹窗 - 功能与菜单:MU(默认8项勾选)/Cube(上传Json+AI生成+实时预览+修改意见 精修)/菜单 三 Tab - 运维流程/领域知识/专家确认:待开发占位(待开发徽标) - 生成保存:资产+菜单+页面总览预览 + 专家确认方案 - 后端 db-sidecar:domain-templates 补全/资产/Cube/页面生成与精修接口, handleDomainTemplateDetail/Update 持久化补充描述+资产+MU --- app/globals.css | 14 ++ app/page.tsx | 440 +++++++++++++++++++++++++++++++++++++++++++----- db-sidecar.mjs | 257 +++++++++++++++++++++++++++- 3 files changed, 667 insertions(+), 44 deletions(-) diff --git a/app/globals.css b/app/globals.css index 6c91e25..d8f2158 100644 --- a/app/globals.css +++ b/app/globals.css @@ -217,6 +217,20 @@ .domain-card .badge{display:inline-block;align-self:flex-start;background:var(--pale);color:var(--blue2);border:1px solid #cfeaf6;border-radius:10px;padding:1px 10px;font-size:10px} .domain-card .meta{display:flex;gap:14px;color:#888;font-size:11px} .domain-card .meta b{color:#333;font-size:13px;margin-right:3px} + +/* 领域方案 7 步向导 */ +.placeholder-block{display:flex;flex-direction:column;gap:12px;padding:6px 2px} +.badge.todo{display:inline-block;align-self:flex-start;background:#fff4d8;color:#b37400;border:1px solid #f6bc44;border-radius:10px;padding:2px 10px;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-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:repeat(2,1fr);gap:12px} +.domain-review-page{border:1px solid #e0e3e5;background:#fff;overflow:hidden} +.domain-review-page .dashboard-thumb-live{border-bottom:0} +.domain-review-page>div{padding:9px 11px;display:flex;align-items:center;justify-content:space-between} +.domain-review-page h4{margin:0;font-size:13px} + .domain-card .card-actions{display:flex;gap:14px;margin-top:2px;padding-top:8px;border-top:1px solid #f0f2f3} /* ===== 设备模版 ===== */ .device-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:12px;padding:4px 0} diff --git a/app/page.tsx b/app/page.tsx index a1ef3a6..c6f7948 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -48,6 +48,9 @@ type SiteDetail = { type MuTab = string | { label:string; disabled?:boolean; note?:string }; type MuNode = { key:string; children?:MuNode[]; tabs?:MuTab[] }; type PageStyle = { id:number; name:string; created_at?:string }; +type DomainAsset = { id?:number; device_template_id:number; device_name:string; count:number; rule:string; sort_order:number }; +type DomainPage = { id:number; name:string; description:string; json_text:string; status:string; gen_filename?:string|null; prompt?:string|null; sort_order:number }; +type DomainStepKey = "basic" | "assets" | "functions" | "opsflow" | "knowledge" | "generate" | "expert"; // ---- helpers ---- function Status({children}:{children:React.ReactNode}){const v=String(children);const tone=/完成|正常|成功|通过|推荐|generated/.test(v)?"ok":/生成中|校验中|告警|running/.test(v)?"warn":/失败|异常|failed/.test(v)?"bad":"muted";return {children}} @@ -231,6 +234,30 @@ export default function Home(){ setModal("completePrompt"); }catch(e:any){ notify("读取提示词失败: "+e.message); } }; + const viewDtCompletePrompt=async(id:number,name:string,domain:string,description:string)=>{ + try{ + const qs=new URLSearchParams({name,domain,description}).toString(); + const data:{prompt:string}=await fetchDb(`/domain-templates/${id}/complete-prompt?${qs}`); + setPromptData({name:"AI 智能补全", text:data.prompt}); + setModal("completePrompt"); + }catch(e:any){ notify("读取提示词失败: "+e.message); } + }; + const viewDtCompleteAssetsPrompt=async(id:number,name:string,domain:string,description:string,supplement:string)=>{ + try{ + const qs=new URLSearchParams({name,domain,description,supplement}).toString(); + const data:{prompt:string}=await fetchDb(`/domain-templates/${id}/complete-assets-prompt?${qs}`); + setPromptData({name:"AI 资产补全", text:data.prompt}); + setModal("completePrompt"); + }catch(e:any){ notify("读取提示词失败: "+e.message); } + }; + const viewDtCompleteCubesPrompt=async(id:number,name:string,domain:string,description:string,supplement:string,assets:any[])=>{ + try{ + const qs=new URLSearchParams({name,domain,description,supplement}).toString(); + const data:{prompt:string}=await fetchDb(`/domain-templates/${id}/complete-cubes-prompt?${qs}`); + setPromptData({name:"AI Cube 页面补全", text:data.prompt}); + setModal("completePrompt"); + }catch(e:any){ notify("读取提示词失败: "+e.message); } + }; const openFeedback=(cube:Cube)=>{ if(!cube.id){ notify("请先生成该页面再提出修改意见"); return; } setFeedbackCube(cube); @@ -460,7 +487,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}/> + ? setOpenDomainTemplateId(null)} onCreated={setOpenDomainTemplateId} viewCompletePrompt={viewDtCompletePrompt} viewCompleteAssetsPrompt={viewDtCompleteAssetsPrompt} viewCompleteCubesPrompt={viewDtCompleteCubesPrompt}/> : setOpenDomainTemplateId(id)} domainFilter={domainFilter} domainOptions={domainOptions}/>)} {page==="styleLibrary"&&} {page==="deviceTemplates"&&} @@ -1282,57 +1309,386 @@ 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}:{id:number|null;go:(p:PageKey)=>void;notify:(m:string)=>void;onBack:()=>void;onCreated:(nid:number)=>void}){ +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}){ const [doms,setDoms]=useState([]); - const [t,setT]=useState(null); const [loading,setLoading]=useState(false); - const [m,setM]=useState(null); - const {confirm, el:confirmEl}=useConfirm(); - const {prompt, el:promptEl}=usePrompt(); const loadDoms=async()=>{ try{ setDoms(await fetchDb("/domains") as any); }catch{} }; - const load=async(silent=false)=>{ if(id==null){ setT(null); return; } if(!silent) setLoading(true); try{ setT(await fetchDb(`/domain-templates/${id}`) as any); }catch(e:any){ notify("加载失败:"+e.message); } finally{ if(!silent) setLoading(false); } }; - useEffect(()=>{ loadDoms(); load(); },[id]); - useEffect(()=>{ if(t) setM({domain_id:t.domain_id,name:t.name,description:t.description||"",status:t.status,version:t.version,creator:t.creator||""}); },[t?.id]); + useEffect(()=>{ loadDoms(); },[]); if(id==null) return ; - if(loading||!t) return

加载中…

; - if(!m) return

加载中…

; - const saveMeta=async()=>{ await fetchDb(`/domain-templates/${id}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(m)}); notify("已保存模板信息"); load(); }; - const addPage=async()=>{ await fetchDb(`/domain-templates/${id}/pages`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:"新页面",description:"",json_text:"{}"})}); load(); }; - const addMenu=async()=>{ await fetchDb(`/domain-templates/${id}/menu`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({label:"新菜单项"})}); load(); }; - const addDoc=async()=>{ const nm=await prompt("文档名称"); if(!nm) return; const link=await confirm("是否为超链?(确定=超链,取消=上传文件)"); if(link){ const u=await prompt("文档超链 URL"); if(!u) return; await fetchDb(`/domain-templates/${id}/docs`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:nm,doc_type:"link",url:u})}); } else { const f:any=await new Promise(res=>{ const input=document.createElement("input"); input.type="file"; input.onchange=()=>res(input.files?.[0]); input.click(); }); if(!f) return; const b=await fileToBase64(f); await fetchDb(`/domain-templates/${id}/docs`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:nm,doc_type:"file",file:{filename:b.filename,mime:b.mime,data:b.data}})}); } load(); }; - const delDoc=async(did:number)=>{ await fetchDb(`/knowledge-docs/${did}`,{method:"DELETE"}); load(); }; - const dlDoc=async(did:number)=>{ const r=await fetch(`/api/db/knowledge-docs/${did}/file`); const blob=await r.blob(); const d=t.docs.find((x:any)=>x.id===did); downloadBlob(blob, d?.name||"doc"); }; + return ; +} + +const DOMAIN_STEPS:{key:DomainStepKey;label:string}[]=[ + {key:"basic",label:"方案基本属性"}, + {key:"assets",label:"资产设置"}, + {key:"functions",label:"功能与菜单"}, + {key:"opsflow",label:"运维流程"}, + {key:"knowledge",label:"领域知识"}, + {key:"generate",label:"生成保存"}, + {key:"expert",label:"专家确认"}, +]; + +function DomainSteps({step,go}:{step:DomainStepKey;go:(s:DomainStepKey)=>void}){ + const current=DOMAIN_STEPS.findIndex(x=>x.key===step); + return
{DOMAIN_STEPS.map((item,index)=>)}
; +} + +function useStreamComplete(){ + const [completing,setCompleting]=useState(false); + const [log,setLog]=useState(""); + const ref=useRef(null); + useEffect(()=>{ if(ref.current) ref.current.scrollTop=ref.current.scrollHeight; },[log]); + return {completing,setCompleting,log,setLog,ref}; +} + +async function streamInto(res:Response, setLog:(f:(p:string)=>string)=>void, onEv:(ev:any)=>void){ + const reader=res.body!.getReader(); const dec=new TextDecoder(); let buf=""; + 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}){ + const outer=useRef(null); + const inner=useRef(null); + useEffect(()=>{ + let cancelled=false; + if(!jsonText || jsonText.trim()==="{}" || jsonText.trim()===""){ if(inner.current) inner.current.innerHTML='
'+emptyLabel+'
'; if(outer.current) outer.current.style.height="auto"; return; } + (async()=>{ + try{ + const json=JSON.parse(jsonText); + if(cancelled||!inner.current) return; + const renderDashboard:any=await loadRenderer(); + renderDashboard(json, inner.current, {}); + 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); + 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`; + }catch(e){ if(inner.current) inner.current.innerHTML='
预览不可用
'; } + })(); + return ()=>{cancelled=true;}; + },[jsonText, emptyLabel]); + return
; +} + +function DomainPagePreviewModal({page,tmplId,notify,onClose}:{page:DomainPage;tmplId:number;notify:(m:string)=>void;onClose:(reload:boolean)=>void}){ + const [feedback,setFeedback]=useState(""); + const [refining,setRefining]=useState(false); + const [refineLog,setRefineLog]=useState(""); + const consoleRef=useRef(null); + useEffect(()=>{ if(consoleRef.current) consoleRef.current.scrollTop=consoleRef.current.scrollHeight; },[refineLog]); + const hasJson = !!page.json_text && page.json_text.trim()!=="{}" && page.json_text.trim()!==""; + const regenerate=async()=>{ + if(!feedback.trim()){ notify("请填写修改意见"); return; } + setRefining(true); setRefineLog(""); + const watchdog=setTimeout(()=>{ setRefining(false); notify("重新生成超时(>10 分钟),请重试"); },600000); + try{ + const res=await fetch(`/api/db/domain-templates/${tmplId}/pages/${page.id}/refine`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({feedback})}); + if(!res.ok||!res.body) throw new Error("HTTP "+res.status); + await streamInto(res, setRefineLog, ()=>{}); + notify(`已根据修改意见重新生成「${page.name}」`); + onClose(true); + }catch(e:any){ notify("重新生成失败:"+(e?.message||e)); } + finally{ clearTimeout(watchdog); setRefining(false); } + }; + return
onClose(false)}>
e.stopPropagation()}> +

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

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

修改意见

+

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

+