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[];
};
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[]>([]);
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);
}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");
return;
}
if(curId && (!site || site.id!==curId)){
+ if(!(await confirmUnsaved())) return;
await loadSite(curId);
}
setPage(target);
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); }
};
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">
</>
)}
</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"/>}
{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}/>}
</>
}
-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);
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">
</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>