]> acesimba.cloud Git - StationBuilder.git/commitdiff
站点编辑Step4更名为菜单设置;移除默认菜单结构展示区
authorroot <root@localhost.localdomain>
Mon, 14 Sep 2026 06:44:14 +0000 (14:44 +0800)
committerroot <root@localhost.localdomain>
Mon, 14 Sep 2026 06:44:14 +0000 (14:44 +0800)
app/page.tsx

index 036e122d86026b3d25ed06ea258331928f50db34..15a13485ac1b80fd42a88761b5af5d8d852cb70f 100644 (file)
@@ -14,7 +14,7 @@ const meta: Record<PageKey, { label: string; icon: string; subtitle: string }> =
   basic:{label:"站点基本属性",icon:"▤",subtitle:"描述站点业务背景,信息越完整,生成结果越符合预期"},
   assets:{label:"站点资产设置",icon:"▧",subtitle:"通过快速配置或手动配置建立场站资产结构"},
   pageConfig:{label:"页面配置",icon:"",subtitle:""},
-  functions:{label:"功能与菜单",icon:"☷",subtitle:"选择 MU 标准页面,定义 Cube 扩展页面及菜单结构"},
+  functions:{label:"菜单设置",icon:"☷",subtitle:"选择 MU 标准页面,定义 Cube 扩展页面及菜单结构"},
   generate:{label:"生成确认",icon:"↻",subtitle:"确认生成范围,调用 Cube Skill 生成页面和模拟数据"},
   result:{label:"站点结果",icon:"◫",subtitle:"集中查看、预览、校验并下载站点交付物"},
   preview:{label:"Cube 页面预览",icon:"▣",subtitle:"检查单个 Cube 页面效果,并通过反馈快速重新生成"},
@@ -56,6 +56,7 @@ type SiteDetail = {
   id:number; name:string; domain:string; description:string; supplement:string; status:string;
   style_id?: number | null;
   parent_id?: number | null;
+  app_name?: string; app_icon?: string; app_icon_filename?: string;
   created_at:string; updated_at:string;
   assets: Asset[]; cubes: Cube[]; mu: string[]; menu: MenuItem[]; files: ReferenceFile[];
 };
@@ -1052,25 +1053,6 @@ function Assets({site,onChange,saving,notify,onViewCompletePrompt,mode,onModeCha
           </tr>})}</tbody>
         </table>
       </Panel>
-      <Panel title="资产层级预览"><div className="asset-tree-preview">
-        <div className="tree-root">⌄ <b>{site.name || "未命名站点"}</b></div>
-        {rows.filter(r=>r.type).map((row,i)=><div key={i} className="tree-branch">
-          <div className="tree-node">⌄ {row.type} <span className="tree-count">{row.count}</span></div>
-          {row.count>0&&<div className="tree-leaves">{formatInstances(row.rule, row.count)}</div>}
-        </div>)}
-      </div></Panel>
-    </div>
-    <div className="stack">
-      <Panel title="填写建议"><ul className="tips"><li>列出站点可能涉及的主要设备类型(泵、风机、换热器、仪表等)。</li><li>为每种设备估算典型数量与命名规则。</li><li>命名规则用 <code>#</code> 表示序号,如 <code>CH-##</code> 生成 CH-01/CH-02;纯前缀如 <code>beng-</code> 会自动补 -01/-02。</li><li>资产层级按「站点 → 类型 → 实例」三级展开预览。</li></ul></Panel>
-      <Panel title="AI 智能补全">
-        <p className="hint">基于站点名称 / 领域 / 描述 / 补充要求,由 AI 分析并补充设备类型、数量与命名规则到资产配置列表(位于您已填项下方)。</p>
-        <div style={{display:"flex",gap:"8px",alignItems:"center"}}>
-          <button className="button" disabled={completing} onClick={runCompleteAssets}>{completing?"补全中…":"开始补全"}</button>
-          <button className="text-button" onClick={onViewCompletePrompt}>查看提示词</button>
-        </div>
-        {(completing||completeLog)&&<div className="modal-console"><div className="modal-console-head">AI 智能补全</div><pre className="modal-console-body" ref={completeConsoleRef}>{completeLog||"(等待 AI 分析…)"}</pre></div>}
-      </Panel>
-      {saving&&<Panel><div className="empty-state">正在保存…</div></Panel>}
     </div>
   </div>}
   </div>;
@@ -1238,153 +1220,80 @@ function AssetTreeBranch({node,selectedId,search,onSelect,onOpen,onAddChild}:{no
 }
 
 function Functions({site,muTree,onMuChange,onSiteChange,pageStyles,notify,onViewCompletePrompt}:{site:SiteDetail|null;muTree:MuNode[];onMuChange:(s:string[])=>void;onSiteChange:(s:SiteDetail)=>void;pageStyles:PageStyle[];notify:(m:string)=>void;onViewCompletePrompt:()=>void}){
-  const [tab,setTab]=useState<"mu"|"cube"|"menu">("cube");
   const leaves = useMemo(()=>buildMuLeaves(muTree),[muTree]);
   const allMuKeys = useMemo(()=>leaves.map(l=>l.key),[leaves]);
   const selectedMu = site?.mu || [];
-  // 默认全选中:site.mu 为空时按全部叶子键显示,首次勾选才落地保存
   const effSelected = selectedMu.length ? selectedMu : allMuKeys;
+  const [appName,setAppName]=useState(site?.app_name||"");
+  const [appIcon,setAppIcon]=useState<string|undefined>(site?.app_icon);
+  const [appIconFile,setAppIconFile]=useState<string|undefined>(site?.app_icon_filename);
   if(!site) return <Panel><div className="empty-state">加载中…</div></Panel>;
-  const updateCube=(idx:number, patch:Partial<Cube>)=>{
-    const next = site.cubes.map((c,i)=>i===idx?{...c,...patch}:c);
-    onSiteChange({...site, cubes:next});
-  };
-  const saveCube=async(idx:number)=>{
-    const cube = site.cubes[idx];
-    try{
-      if(cube.id){
-        await fetchDb(`/sites/${site.id}/cubes/${cube.id}`, {method:"PUT", headers:{"Content-Type":"application/json"}, body:JSON.stringify(cube)});
-      } else {
-        const created = await fetchDb(`/sites/${site.id}/cubes`, {method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify(cube)}) as Cube;
-        const next = [...site.cubes]; next[idx] = {...cube, id:created.id!}; onSiteChange({...site, cubes:next});
-      }
-      notify("Cube 页面已保存");
-    }catch(e:any){ notify("保存失败: "+e.message); }
-  };
-  const addCube=()=>onSiteChange({...site, cubes:[...site.cubes, {name:"",content:"",status:"pending",sort_order:site.cubes.length}]});
-  const removeCube=async(idx:number)=>{
-    const cube = site.cubes[idx];
-    if(cube.id) try{ await fetchDb(`/sites/${site.id}/cubes/${cube.id}`, {method:"DELETE"}); }catch(e:any){ notify("删除失败: "+e.message); return; }
-    onSiteChange({...site, cubes:site.cubes.filter((_,i)=>i!==idx)});
-  };
-  const selectStyle=async(id:number|null)=>{
-    try{
-      const refreshed:SiteDetail = await fetchDb(`/sites/${site.id}`, {method:"PUT", headers:{"Content-Type":"application/json"}, body:JSON.stringify({
-        name:site.name, domain:site.domain, description:site.description, supplement:site.supplement, style_id: id
-      })}) as SiteDetail;
-      onSiteChange(refreshed);
-      notify(id ? "已应用页面样式" : "已清除页面样式");
-    }catch(e:any){ notify("应用样式失败: "+e.message); }
-  };
-  const generateCube=async(idx:number)=>{
-    const cube = site.cubes[idx];
-    if(!cube.id) await saveCube(idx);
-    notify(`开始生成 ${cube.name}…`);
-    try{
-      await fetchDb(`/sites/${site.id}/cubes/${cube.id}/generate`, {method:"POST"});
-      notify(`${cube.name} 生成完成`);
-      const refreshed:SiteDetail = await fetchDb(`/sites/${site.id}`);
-      onSiteChange(refreshed);
-    }catch(e:any){ notify("生成失败: "+e.message); }
-  };
-  const downloadCube=(cube:Cube)=>{
-    if(!cube.id) return;
-    fetch(`/api/db/sites/${site.id}/cubes/${cube.id}/file`).then(r=>r.blob()).then(b=>downloadBlob(b, cube.gen_filename || `${cube.name}.json`)).catch(e=>notify("下载失败"));
-  };
-  const previewCube=(cube:Cube)=>{
-    notify(`预览 ${cube.name}`);
+  const toggleMu=(key:string)=>{
+    const base = selectedMu.length ? selectedMu : allMuKeys;
+    onMuChange(base.includes(key)? base.filter(k=>k!==key) : [...base, key]);
   };
-
-  // ===== AI Cube 页面补全 =====
-  const [completing,setCompleting]=useState(false);
-  const [completeLog,setCompleteLog]=useState("");
-  const completeConsoleRef=useRef<HTMLPreElement>(null);
-  useEffect(()=>{ if(completeConsoleRef.current) completeConsoleRef.current.scrollTop=completeConsoleRef.current.scrollHeight; },[completeLog]);
-  const appendCubes=(items:Array<{name:string;content:string}>)=>{
-    const base = site?.cubes || [];
-    const next = [...base, ...items.map((it,i)=>({name:it.name, content:it.content||"", status:"pending", sort_order: base.length+i}))];
-    onSiteChange({...site, cubes:next});
-    notify(`AI 已补充 ${items.length} 个 Cube 页面(位于您已填项下方)`);
+  const pickAppIcon=(e:React.ChangeEvent<HTMLInputElement>)=>{
+    const f=e.target.files?.[0]; if(!f) return;
+    if(!["image/png","image/svg+xml","image/jpeg"].includes(f.type)||f.size>100*1024){ notify("仅支持 png/svg 格式,且小于 100KB"); return; }
+    const reader=new FileReader(); reader.onload=()=>{ setAppIcon(reader.result as string); setAppIconFile(f.name); }; reader.readAsDataURL(f);
   };
-  const runCompleteCubes=async()=>{
-    if(!site?.id || completing) return;
-    setCompleting(true); setCompleteLog("");
-    const watchdog=setTimeout(()=>{ setCompleting(false); notify("AI Cube 补全超时(>6 分钟),请重试"); }, 360000);
+  const saveAppInfo=async()=>{
     try{
-      const res=await fetch(`/api/db/sites/${site.id}/complete-cubes`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:site.name,domain:site.domain,description:site.description,supplement:site.supplement,assets:site.assets})});
-      if(!res.ok||!res.body) throw new Error("HTTP "+res.status);
-      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) setCompleteLog(prev=>prev+ev.log);
-            if(Array.isArray(ev.result)) appendCubes(ev.result);
-          }catch{}
-        }
-      }
-      notify("AI Cube 页面补全已完成");
-    }catch(e:any){ notify("AI Cube 页面补全失败:"+(e?.message||e)); }
-    finally{ clearTimeout(watchdog); setCompleting(false); }
+      await fetchDb(`/sites/${site.id}`, {method:"PUT", headers:{"Content-Type":"application/json"}, body:JSON.stringify({
+        name:site.name, domain:site.domain, description:site.description, supplement:site.supplement,
+        style_id:site.style_id, parent_id:site.parent_id,
+        app_name: appName, app_icon: appIcon, app_icon_filename: appIconFile
+      })});
+      onSiteChange({...site, app_name: appName, app_icon: appIcon, app_icon_filename: appIconFile});
+      notify("应用信息已保存");
+    }catch(e:any){ notify("保存失败: "+e.message); }
   };
 
-  return <Panel><div className="tabs"><button className={tab==="cube"?"active":""} onClick={()=>setTab("cube")}>领域扩展页面 <i>{site.cubes.length}</i></button><button className={tab==="menu"?"active":""} onClick={()=>setTab("menu")}>菜单结构</button></div>
-    {/* Cube 扩展页面:左右两栏,右栏为填写建议 + AI 智能补全 */}
-    {tab==="cube"&&<div className="two-column">
-      <div>
-        <div className="cube-editor"><table className="data-table editable cube-editor-table"><thead><tr><th>页面名称 <small>最多6字</small></th><th>页面内容</th><th>生成文件</th><th>操作</th></tr></thead><tbody>{site.cubes.map((cube,i)=><tr key={i}>
-          <td><input value={cube.name} maxLength={6} placeholder="例如:运行总览" onChange={e=>updateCube(i,{name:e.target.value})}/></td>
-          <td><input value={cube.content} placeholder="描述该页面要展示的内容和指标" onChange={e=>updateCube(i,{content:e.target.value})}/></td>
-          <td>{cube.status==="generated"? <button className="link" onClick={()=>downloadCube(cube)}>下载 {cube.gen_filename}</button> : <span className="muted">未生成</span>}</td>
-          <td className="cube-actions"><button onClick={()=>previewCube(cube)}>预览</button><button className="btn-ai" onClick={()=>generateCube(i)}>AI生成</button><button onClick={()=>saveCube(i)}>保存</button><button className="trash" onClick={()=>removeCube(i)}>删除</button></td>
-        </tr>)}</tbody></table><Button onClick={addCube}>+ 添加 Cube 页面</Button></div>
-        <div className="style-picker">
-          <h4>站点页面样式 <small>站点级 · 全部 Cube 页面共用</small></h4>
-          <p className="hint">选择一个样式库方案作为本站点所有 Cube 页面的视觉风格(生成时参考其 JSON 主题参数)。</p>
-          <div className="style-pick-list">
-            <button className={"style-pick"+(!site.style_id?" active":"")} onClick={()=>selectStyle(null)}>
-              <div className="style-pick-thumb none">无</div><span>不使用样式</span>
-            </button>
-            {pageStyles.map(s=><button key={s.id} className={"style-pick"+(site.style_id===s.id?" active":"")} onClick={()=>selectStyle(s.id)}>
-              <div className="style-pick-thumb"><img src={`/api/db/page-styles/${s.id}/image?w=360`} alt={s.name} loading="lazy"/></div>
-              <span>{s.name}</span>
-            </button>)}
-          </div>
+  // 设备运维页面(复用 pageConfig-设备运维 的 MU 表格)
+  const opLeaves = useMemo(()=>buildMuLeaves(muTree),[muTree]);
+  const opGrouped = useMemo(()=>{
+    const map=new Map<string,{parentMenu:string,items:typeof opLeaves}>();
+    for(const l of opLeaves){ if(!map.has(l.parentMenu)) map.set(l.parentMenu,{parentMenu:l.parentMenu,items:[]}); map.get(l.parentMenu)!.items.push(l); }
+    return Array.from(map.values());
+  },[opLeaves]);
+
+  // 仪表盘示例名称
+  const dashboardExamples = ["运行总览","能耗分析","设备健康度","报警事件"];
+  // 数字孪生场景示例名称
+  const twinExamples = ["3D 厂区漫游","设备数字孪生","管线可视化","实时数据大屏"];
+
+  return <Panel>
+    {/* 应用信息 */}
+    <div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:"16px",marginBottom:"16px"}}>
+      <Field label="应用名称"><input value={appName} placeholder="例如:智慧水务平台" onChange={e=>setAppName(e.target.value)} onBlur={saveAppInfo}/></Field>
+      <Field label="应用Icon" hint="支持 png、svg,小于 100KB">
+        <div style={{display:"flex",alignItems:"center",gap:"10px"}}>
+          {appIcon?<img src={appIcon} alt="应用图标" style={{width:40,height:40,borderRadius:6,objectFit:"cover",border:"1px solid #ddd"}}/>:<div style={{width:40,height:40,borderRadius:6,border:"1px dashed #ddd",display:"flex",alignItems:"center",justifyContent:"center",color:"#bbb",fontSize:"12px"}}>无</div>}
+          <label className="text-button" style={{cursor:"pointer"}}>选择文件<input type="file" accept=".png,.svg,.jpeg,image/png,image/svg+xml" hidden onChange={pickAppIcon}/></label>
+          {appIconFile&&<span className="muted">{appIconFile}</span>}
         </div>
-      </div>
-      <div className="stack">
-        <Panel title="填写建议"><ul className="tips"><li>从站点业务与设备资产出发,规划需要的监控维度(综合总览、能耗能效、设备健康、报警事件、环境监测、生产产量等)。</li><li>每个 Cube 页面写明:页面目标、关键指标/图表、数据来源与模拟数据构造思路。</li><li>页面名称简洁,不超过 6 个汉字。</li><li>AI 补充的页面会追加在您已创建页面下方,不会改动您原有的内容。</li></ul></Panel>
-        <Panel title="AI 智能补全">
-          <p className="hint">基于站点名称 / 领域 / 描述 / 补充要求,以及设备资产层级,由 AI 分析并补充站点所需的 Cube 扩展页面(页面名称 + 详细内容),追加在您已建页面下方。</p>
-          <div style={{display:"flex",gap:"8px",alignItems:"center"}}>
-            <button className="button" disabled={completing} onClick={runCompleteCubes}>{completing?"补全中…":"开始补全"}</button>
-            <button className="text-button" onClick={onViewCompletePrompt}>查看提示词</button>
-          </div>
-          {(completing||completeLog)&&<div className="modal-console"><div className="modal-console-head">AI 智能补全</div><pre className="modal-console-body" ref={completeConsoleRef}>{completeLog||"(等待 AI 分析…)"}</pre></div>}
-        </Panel>
-      </div>
-    </div>}
-    {/* 菜单结构:只读默认预览(MU 在上,Cube 全部归入「综合监控」一级菜单) */}
-    {tab==="menu"&&<div className="menu-preview">
-      <p className="hint">默认菜单结构(展示用,无需编辑):MU 标准页面在上,所有 Cube 扩展页面归入「综合监控」一级菜单下。</p>
-      <div className="menu-tree">
-        <div className="menu-root">⌄ {site.name || "未命名站点"}</div>
-        <div className="menu-section">
-          <div className="menu-section-title">MU 标准页面</div>
-          {leaves.filter(l=>effSelected.includes(l.key)).map(l=><div key={l.key} className="menu-leaf">· {l.parentMenu}/{l.label}</div>)}
-          {leaves.filter(l=>effSelected.includes(l.key)).length===0&&<div className="menu-leaf muted">(未选择 MU 页面)</div>}
-        </div>
-        <div className="menu-section">
-          <div className="menu-section-title">综合监控</div>
-          {site.cubes.filter(c=>c.name&&c.name.trim()).map(c=><div key={c.id||c.name} className="menu-leaf cube">· {c.name}</div>)}
-          {site.cubes.filter(c=>c.name&&c.name.trim()).length===0&&<div className="menu-leaf muted">(暂无 Cube 页面)</div>}
-        </div>
-      </div>
-    </div>}
+      </Field>
+    </div>
+
+    {/* 设备运维页面 */}
+    <div style={{marginTop:"20px"}}>
+      <h3 style={{fontSize:"15px",fontWeight:600,marginBottom:"10px"}}>设备运维页面</h3>
+      <div className="mu-tree">{opGrouped.map(g=><div key={g.parentMenu} className="mu-group"><b>{g.parentMenu}</b><div className="mu-items">{g.items.map(item=><label key={item.key} className={item.disabled?"disabled":""}><input type="checkbox" disabled={item.disabled} checked={effSelected.includes(item.key)} onChange={()=>toggleMu(item.key)}/><span>{item.label}</span></label>)}</div></div>)}</div>
+    </div>
+
+    {/* 仪表盘页面 */}
+    <div style={{marginTop:"20px"}}>
+      <h3 style={{fontSize:"15px",fontWeight:600,marginBottom:"10px"}}>仪表盘页面</h3>
+      <p className="hint">以下为预设仪表盘模板示例(Demo),实际内容待配置。</p>
+      <div style={{display:"flex",flexWrap:"wrap",gap:"8px"}}>{dashboardExamples.map((name,i)=><div key={i} style={{padding:"8px 14px",border:"1px solid #d9dee2",borderRadius:"4px",fontSize:"13px",color:"#555"}}>{name}</div>)}</div>
+    </div>
+
+    {/* 数字孪生场景 */}
+    <div style={{marginTop:"20px"}}>
+      <h3 style={{fontSize:"15px",fontWeight:600,marginBlur:"10px"}}>数字孪生场景</h3>
+      <p className="hint">以下为预设数字孪生场景示例(Demo),实际内容待配置。</p>
+      <div style={{display:"flex",flexWrap:"wrap",gap:"8px"}}>{twinExamples.map((name,i)=><div key={i} style={{padding:"8px 14px",border:"1px solid #d9dee2",borderRadius:"4px",fontSize:"13px",color:"#555"}}>{name}</div>)}</div>
+    </div>
   </Panel>;
 }