]> acesimba.cloud Git - StationBuilder.git/commitdiff
站点结构树(侧栏) + 基本情况描述非必填 + 页面配置新增四个Tab
authorroot <root@localhost.localdomain>
Fri, 11 Sep 2026 09:49:17 +0000 (17:49 +0800)
committerroot <root@localhost.localdomain>
Fri, 11 Sep 2026 09:49:17 +0000 (17:49 +0800)
app/globals.css
app/page.tsx

index e2bdc19357a9cf4e41ad5edbc6ed94a115f66f9e..f1b5605b49d22c013a0f34ab97b48d67fd088eb0 100644 (file)
 .tree-root{font-weight:bold;color:#33414a;margin-bottom:8px;display:flex;align-items:center;gap:5px}
 .tree-branch{margin:4px 0 4px 14px;padding-left:12px;border-left:1px dashed #cdd5d9}
 .tree-node{display:flex;align-items:center;gap:6px;font-weight:bold;color:#3a4750}
+.site-structure-title{margin-top:18px}
+.site-structure{grid-column:2;margin-top:6px}
+.site-structure .tree-branch{margin:4px 0 4px 0;padding-left:12px;border-left:1px solid #ddd}
+.site-structure .tree-node{font-weight:normal;color:#555;font-size:14px}
+.site-structure .tree-node.current-site{color:var(--blue);background:rgba(0,140,214,.08);border-radius:3px;padding:1px 4px;margin-left:-4px}
 .tree-count{background:var(--pale);color:var(--blue2);border-radius:9px;padding:0 7px;font-size:10px;font-weight:normal}
 .tree-leaves{color:#788187;font-size:11px;line-height:1.7;margin:3px 0 0 20px;word-break:break-all}
 
index de8d92a9386cdcd77d4635ad0b3fa144db24df67..f4ac85f05fd4b808a723f005ca440113e984fd29 100644 (file)
@@ -55,6 +55,7 @@ type ReferenceFile = { id:number; filename:string; mime:string; size:number; cre
 type SiteDetail = {
   id:number; name:string; domain:string; description:string; supplement:string; status:string;
   style_id?: number | null;
+  parent_id?: number | null;
   created_at:string; updated_at:string;
   assets: Asset[]; cubes: Cube[]; mu: string[]; menu: MenuItem[]; files: ReferenceFile[];
 };
@@ -203,6 +204,7 @@ export default function Home(){
   const [site,setSite]=useState<SiteDetail|null>(null);
   const [loading,setLoading]=useState(true);
   const [saving,setSaving]=useState(false);
+  const [dirty,setDirty]=useState(false);
   const [domains,setDomains]=useState<string[]>([]);
   const [muTree,setMuTree]=useState<MuNode[]>([]);
   const [pageStyles,setPageStyles]=useState<PageStyle[]>([]);
@@ -215,6 +217,8 @@ export default function Home(){
   const [view,setView]=useState<"table"|"card">("table");
   const [siteEditSection,setSiteEditSection]=useState<SiteEditSection>("basic");
   const [assetConfigMode,setAssetConfigMode]=useState<"quick"|"manual">("quick");
+  const [pageConfigTab,setPageConfigTab]=useState<"ops"|"dashboard"|"report"|"twin">("ops");
+  const PAGE_CONFIG_LABEL:Record<string,string>={ops:"设备运维",dashboard:"仪表板",report:"报表",twin:"数字孪生场景"};
   const [functionTab,setFunctionTab]=useState<"mu"|"cube"|"menu">("mu");
   const [previewTab,setPreviewTab]=useState<"info"|"json"|"feedback">("info");
   const [feedbackCube,setFeedbackCube]=useState<Cube|null>(null);
@@ -422,6 +426,13 @@ export default function Home(){
     }catch(e:any){ notify("删除失败: "+e.message); }
   };
 
+  const confirmUnsaved=async():Promise<boolean>=>{
+    if(!dirty) return true;
+    return new Promise(resolve=>{
+      const ok=window.confirm("当前站点有未保存的修改,切换站点将丢失这些修改。是否继续?");
+      if(ok){ setDirty(false); resolve(true); } else resolve(false);
+    });
+  };
   const go=async(target:PageKey, sid?:number|"new")=>{
     try{
       if(target==="siteEdit")setSiteEditSection("basic");
@@ -447,6 +458,7 @@ export default function Home(){
         return;
       }
       if(curId && (!site || site.id!==curId)){
+        if(!(await confirmUnsaved())) return;
         await loadSite(curId);
       }
       setPage(target);
@@ -470,10 +482,12 @@ export default function Home(){
     if(!site) return;
     const next = {...site, ...patch};
     setSite(next);
+    setDirty(true);
     setSaving(true);
     try{
       await fetchDb(`/sites/${site.id}`, {method:"PUT", headers:{"Content-Type":"application/json"}, body:JSON.stringify(next)});
       setSites(prev=>prev.map(s=>s.id===site.id?{...s, name:next.name, domain:next.domain, status:next.status}:s));
+      setDirty(false);
     }catch(e:any){ notify("保存失败: "+e.message); }
     finally{ setSaving(false); }
   };
@@ -557,6 +571,18 @@ export default function Home(){
   const systemArea = (["toolSettings","domainSettings"] as PageKey[]).includes(page);
   const extensionArea = (["dashboard","report-dashboard","report-doc","basic-components","func-components","filters","datasource"] as PageKey[]).includes(page);
   const siteEditMeta = meta[siteEditSection];
+
+  const siteHierarchy=useMemo(()=>{
+    if(!siteId) return null;
+    const byId=new Map(sites.map(s=>[s.id,s]));
+    const current=byId.get(siteId);
+    if(!current) return null;
+    const ancestors:SiteSummary[]=[];
+    let p:SiteSummary|undefined=current;
+    while(p){ ancestors.unshift(p); p=p.parent_id?byId.get(p.parent_id):undefined; }
+    const children=(sites.filter(s=>s.parent_id===siteId));
+    return {ancestors,children,current};
+  },[sites,siteId]);
   const manualAssetActive = assetConfigMode==="manual" && (page==="assets" || (page==="siteEdit"&&siteEditSection==="assets"));
 
   return <div className="app-shell">
@@ -651,7 +677,7 @@ export default function Home(){
       </>
     )}
     </div></aside>
-    <main className="workspace"><div className="crumb">iStation StationBuilder / <b>{["domains","domainDetail","styleLibrary","deviceTemplates","toolSettings","domainSettings"].includes(page)?meta[page].label:page==="projects"?"站点列表":page==="siteEdit"?`站点管理 / 站点编辑 / ${siteEditMeta.label}`:`站点详情 / ${meta[page].label}`}</b></div><div className="page-heading"><div><h1>{page==="siteEdit"?siteEditMeta.label:meta[page].label}</h1><p>{page==="siteEdit"?siteEditMeta.subtitle:meta[page].subtitle}</p></div><div>{page==="projects"&&<Button onClick={()=>go("basic","new")}>+ 新建站点</Button>}{page==="siteEdit"&&<label className="site-edit-switcher"><span>当前站点</span><select value={siteId??""} onChange={async e=>{const id=Number(e.target.value);await loadSite(id);window.history.replaceState({},"",`?page=siteEdit&id=${id}`);}}>{sites.map(item=><option key={item.id} value={item.id}>{item.name||"未命名站点"}</option>)}</select></label>}{page==="domains"&&openDomainTemplateId==null&&<Button onClick={()=>setOpenDomainTemplateId("create")}>+ 新增领域方案</Button>}{page==="domains"&&openDomainTemplateId!=null&&<Button secondary onClick={()=>setOpenDomainTemplateId(null)}>← 返回方案列表</Button>}{domainFilter!=null&&page==="domains"&&openDomainTemplateId==null&&<Button secondary onClick={()=>{setDomainFilter(null);}}>清除领域筛选:{domainOptions.find(d=>d.id===domainFilter)?.name||""}</Button>}{page==="domainSettings"&&<Button onClick={()=>{setNewDomainName("");setAddingDomain(true);}}>+ 新增领域</Button>}</div></div>{page==="siteEdit"&&<SiteEditTabs section={siteEditSection} onChange={setSiteEditSection}/>} {SITE_EDIT_STEPS.includes(page as SiteEditSection)&&<Steps page={page} go={go}/>}<div className="content">
+    <main className="workspace"><div className="crumb">iStation StationBuilder / <b>{["domains","domainDetail","styleLibrary","deviceTemplates","toolSettings","domainSettings"].includes(page)?meta[page].label:page==="projects"?"站点列表":page==="siteEdit"?`站点管理 / 站点编辑 / ${siteEditMeta.label}`:`站点详情 / ${meta[page].label}`}</b></div><div className="page-heading"><div><h1>{page==="siteEdit"?siteEditMeta.label:meta[page].label}</h1><p>{page==="siteEdit"?siteEditMeta.subtitle:meta[page].subtitle}</p></div><div>{page==="projects"&&<Button onClick={()=>go("basic","new")}>+ 新建站点</Button>}{page==="siteEdit"&&<label className="site-edit-switcher"><span>当前站点</span><select value={siteId??""} onChange={async e=>{const id=Number(e.target.value);if(id!==siteId&&!(await confirmUnsaved())){e.target.value=String(siteId??'');return;}await loadSite(id);window.history.replaceState({},"",`?page=siteEdit&id=${id}`);}}>{sites.map(item=><option key={item.id} value={item.id}>{item.name||"未命名站点"}</option>)}</select></label>}{page==="domains"&&openDomainTemplateId==null&&<Button onClick={()=>setOpenDomainTemplateId("create")}>+ 新增领域方案</Button>}{page==="domains"&&openDomainTemplateId!=null&&<Button secondary onClick={()=>setOpenDomainTemplateId(null)}>← 返回方案列表</Button>}{domainFilter!=null&&page==="domains"&&openDomainTemplateId==null&&<Button secondary onClick={()=>{setDomainFilter(null);}}>清除领域筛选:{domainOptions.find(d=>d.id===domainFilter)?.name||""}</Button>}{page==="domainSettings"&&<Button onClick={()=>{setNewDomainName("");setAddingDomain(true);}}>+ 新增领域</Button>}</div></div>{page==="siteEdit"&&<SiteEditTabs section={siteEditSection} onChange={setSiteEditSection}/>} {SITE_EDIT_STEPS.includes(page as SiteEditSection)&&<Steps page={page} go={go}/>}<div className="content">
       {page==="projects"&&<Projects view={view} setView={setView} sites={sites} loading={loading} go={go} loadSite={loadSite} notify={notify} onDelete={deleteSites} onCreateSub={createSubSite}/>}
       {page==="dashboard"&&<Dashboard/>}
       {page==="report-dashboard"&&<ImagePage src="/t/stationbuilder/ext-pages/report-dashboard.jpg"/>}
@@ -667,10 +693,18 @@ export default function Home(){
       {page==="deviceTemplates"&&<CompanyDeviceTemplates notify={notify} onViewCpPrompt={viewDeviceCpCompletePrompt}/>}
       {page==="toolSettings"&&<InternalToolSettings model={model} onModelChange={setModel} notify={notify}/>}
       {page==="domainSettings"&&<DomainSettings domains={domainOptions} notify={notify} onDeleted={handleDomainDeleted}/>}
-      {(page==="basic"||(page==="siteEdit"&&siteEditSection==="basic"))&&<Basic site={site} domains={domains} onChange={saveSiteBasic} onAddDomain={addDomain} onUpload={uploadFile} saving={saving} notify={notify} onViewCompletePrompt={viewCompletePrompt}/>}
+      {(page==="basic"||(page==="siteEdit"&&siteEditSection==="basic"))&&<Basic site={site} domains={domains} onChange={saveSiteBasic} onAddDomain={addDomain} onUpload={uploadFile} saving={saving} notify={notify} onViewCompletePrompt={viewCompletePrompt} go={go} siteId={siteId} siteHierarchy={siteHierarchy}/>}
       {page==="assets"&&<Assets site={site} onChange={saveAssets} saving={saving} notify={notify} onViewCompletePrompt={viewCompleteAssetsPrompt} mode={assetConfigMode} onModeChange={setAssetConfigMode}/>}
       {page==="siteEdit"&&siteEditSection==="assets"&&<Assets site={site} onChange={saveAssets} saving={saving} notify={notify} onViewCompletePrompt={viewCompleteAssetsPrompt} mode={assetConfigMode} onModeChange={setAssetConfigMode}/>}
-      {(page==="pageConfig"||(page==="siteEdit"&&siteEditSection==="pageConfig"))&&<div className="site-step-blank" aria-label="页面配置内容区域"/>}
+      {(page==="pageConfig"||(page==="siteEdit"&&siteEditSection==="pageConfig"))&&<div className="page-config-shell">
+        <div className="asset-config-tabs" role="tablist" aria-label="页面配置">
+          <button role="tab" aria-selected={pageConfigTab==="ops"} className={pageConfigTab==="ops"?"active":""} onClick={()=>setPageConfigTab("ops")}><span>设备运维</span><small>设备运行维护相关配置</small></button>
+          <button role="tab" aria-selected={pageConfigTab==="dashboard"} className={pageConfigTab==="dashboard"?"active":""} onClick={()=>setPageConfigTab("dashboard")}><span>仪表板</span><small>可视化仪表板配置</small></button>
+          <button role="tab" aria-selected={pageConfigTab==="report"} className={pageConfigTab==="report"?"active":""} onClick={()=>setPageConfigTab("report")}><span>报表</span><small>报表配置</small></button>
+          <button role="tab" aria-selected={pageConfigTab==="twin"} className={pageConfigTab==="twin"?"active":""} onClick={()=>setPageConfigTab("twin")}><span>数字孪生场景</span><small>数字孪生场景配置</small></button>
+        </div>
+        <Panel title={PAGE_CONFIG_LABEL[pageConfigTab]}><div className="muted">内容待补充</div></Panel>
+      </div>}
       {page==="functions"&&<Functions site={site} muTree={muTree} onMuChange={saveMu} onSiteChange={setSite} pageStyles={pageStyles} notify={notify} onViewCompletePrompt={viewCompleteCubesPrompt}/>}
       {page==="siteEdit"&&siteEditSection==="functions"&&<Functions site={site} muTree={muTree} onMuChange={saveMu} onSiteChange={setSite} pageStyles={pageStyles} notify={notify} onViewCompletePrompt={viewCompleteCubesPrompt}/>}
       {page==="generate"&&<Generate site={site} onSiteChange={setSite} notify={notify} viewPrompt={viewPrompt} viewSitePrompt={viewSitePrompt}/>}
@@ -846,7 +880,7 @@ function Projects({view,setView,sites,loading,go,loadSite,notify,onDelete,onCrea
   </>
 }
 
-function Basic({site,domains,onChange,onAddDomain,onUpload,saving,notify,onViewCompletePrompt}:{site:SiteDetail|null;domains:string[];onChange:(p:Partial<SiteDetail>)=>void;onAddDomain:(n:string)=>void;onUpload:(f:File)=>void;saving:boolean;notify:(m:string)=>void;onViewCompletePrompt:()=>void}){
+function Basic({site,domains,onChange,onAddDomain,onUpload,saving,notify,onViewCompletePrompt,go,siteId,siteHierarchy}:{site:SiteDetail|null;domains:string[];onChange:(p:Partial<SiteDetail>)=>void;onAddDomain:(n:string)=>void;onUpload:(f:File)=>void;saving:boolean;notify:(m:string)=>void;onViewCompletePrompt:()=>void;go:(p:PageKey,sid?:number|"new")=>void;siteId:number|null|undefined;siteHierarchy:{ancestors:SiteSummary[];children:SiteSummary[];current:SiteSummary|null}|null}){
   const [newDomain,setNewDomain]=useState("");
   const [domainInput,setDomainInput]=useState(site?.domain||"");
   const [showAdd,setShowAdd]=useState(false);
@@ -884,7 +918,23 @@ function Basic({site,domains,onChange,onAddDomain,onUpload,saving,notify,onViewC
   if(!site) return <Panel><div className="empty-state">加载中…</div></Panel>;
   const nameTooLong = countChars(site.name)>20;
   const domainTooLong = newDomain.length>6;
-  return <><div className="company-form-page"><aside className="form-section-nav"><b>1</b><span>站点信息</span><a href="#site-basic">基本信息</a><a href="#site-description">情况描述</a></aside><div className="single-column-form"><Panel title="站点基本信息"><div className="form-grid" id="site-basic">
+  const renderSiteBranch=(idx:number):JSX.Element|null=>{
+    if(!siteHierarchy||idx>=siteHierarchy.ancestors.length) return null;
+    const a=siteHierarchy.ancestors[idx];
+    const isLast=idx===siteHierarchy.ancestors.length-1;
+    return (
+      <div className="tree-branch">
+        <div className={"tree-node"+(a.id===siteId?" current-site":"")} style={{cursor:"pointer"}} onClick={()=>go("siteEdit",a.id)}>{a.name||"未命名站点"}</div>
+        {isLast && siteHierarchy.children.map(c=>(
+          <div key={c.id} className="tree-branch">
+            <div className={"tree-node"+(c.id===siteId?" current-site":"")} style={{cursor:"pointer"}} onClick={()=>go("siteEdit",c.id)}>{c.name||"未命名站点"}</div>
+          </div>
+        ))}
+        {renderSiteBranch(idx+1)}
+      </div>
+    );
+  };
+  return <><div className="company-form-page"><aside className="form-section-nav"><b>1</b><span>站点信息</span><a href="#site-basic">基本信息</a><a href="#site-description">情况描述</a>{siteHierarchy&&<><span className="site-structure-title">站点结构</span><div className="site-structure">{renderSiteBranch(0)}</div></>}</aside><div className="single-column-form"><Panel title="站点基本信息"><div className="form-grid" id="site-basic">
     <Field label="站点名称" required hint="最多 20 个汉字"><input value={site.name} maxLength={20} onChange={e=>onChange({name:e.target.value})} className={nameTooLong?"invalid":""}/></Field>
     <Field label="所属领域" required>
       <div className="domain-select">
@@ -898,7 +948,7 @@ function Basic({site,domains,onChange,onAddDomain,onUpload,saving,notify,onViewC
       </div>}
     </Field>
     <div className="form-section-heading" id="site-description"><h3>情况描述</h3><p>说明站点用途、重点设备、管理目标及关注指标。</p></div>
-    <Field label="站点基本情况描述" required><textarea value={site.description} onChange={e=>onChange({description:e.target.value})}/></Field>
+    <Field label="站点基本情况描述"><textarea value={site.description} onChange={e=>onChange({description:e.target.value})}/></Field>
     <div className="field-assist"><Button secondary onClick={()=>setAiOpen(true)}><RobotOutlined/> AI 智能补全</Button><span className="muted">补充要求、填写建议与参考资料均在右侧抽屉中维护</span></div>
     {saving&&<div className="save-indicator">正在保存…</div>}
   </div></Panel></div></div>