.data-table.editable{table-layout:fixed}
.data-table.editable th:nth-child(1){width:40%}
.data-table.editable th:nth-child(2){width:92px}
-.data-table.editable th:nth-child(3){width:34%}
-.data-table.editable th:nth-child(4){width:44px}
+.data-table.editable th:nth-child(3){width:56px}
+.data-table.editable th:nth-child(4){width:auto}
+.data-table.editable th:nth-child(5){width:48px}
.data-table.editable input,.data-table.editable select,.data-table.editable textarea{min-width:0;width:100%}
.data-table.editable .trash{border:0;background:transparent;color:var(--red);font-size:15px;cursor:pointer;line-height:1}
.data-table.editable .trash:hover{color:#d63723}
+.data-table.editable .asset-eye-btn{border:0;background:transparent;color:var(--blue);font-size:18px;cursor:pointer;line-height:1;padding:0;display:inline-flex;align-items:center}
+.data-table.editable .asset-eye-btn.disabled{color:#c4c9cc;cursor:default}
/* ===== 领域方案库:列表筛选栏 + 可排序列头 ===== */
.filter-bar{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:10px}
{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} 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==="assets"&&<Assets site={site} onChange={saveAssets} saving={saving} notify={notify} onViewCompletePrompt={viewCompleteAssetsPrompt} mode={assetConfigMode} onModeChange={setAssetConfigMode} go={go}/>}
+ {page==="siteEdit"&&siteEditSection==="assets"&&<Assets site={site} onChange={saveAssets} saving={saving} notify={notify} onViewCompletePrompt={viewCompleteAssetsPrompt} mode={assetConfigMode} onModeChange={setAssetConfigMode} go={go}/>}
{(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>
</Drawer></>;
}
-function Assets({site,onChange,saving,notify,onViewCompletePrompt,mode,onModeChange}:{site:SiteDetail|null;onChange:(assets:Asset[])=>void;saving:boolean;notify:(m:string)=>void;onViewCompletePrompt:()=>void;mode:"quick"|"manual";onModeChange:(mode:"quick"|"manual")=>void}){
+function Assets({site,onChange,saving,notify,onViewCompletePrompt,mode,onModeChange,go}:{site:SiteDetail|null;onChange:(assets:Asset[])=>void;saving:boolean;notify:(m:string)=>void;onViewCompletePrompt:()=>void;mode:"quick"|"manual";onModeChange:(mode:"quick"|"manual")=>void;go:(p:PageKey)=>void}){
const [completing,setCompleting]=useState(false);
const [completeLog,setCompleteLog]=useState("");
const completeConsoleRef=useRef<HTMLPreElement>(null);
+ const [deviceTemplates,setDeviceTemplates]=useState<any[]>([]);
useEffect(()=>{ if(completeConsoleRef.current) completeConsoleRef.current.scrollTop=completeConsoleRef.current.scrollHeight; },[completeLog]);
+ // 加载设备模板(按站点领域过滤)
+ useEffect(()=>{ (async()=>{ try{ const all=await fetchDb("/device-templates") as any[]; setDeviceTemplates(all.filter((t:any)=>{ try{ return JSON.parse(t.domain_names||"[]").includes(site?.domain); }catch{ return false; } })); }catch(e:any){ /* 静默失败,下拉框为空 */ } })(); },[site?.domain]);
const appendAssets=(items:Array<{type:string;count:number;rule:string}>)=>{
const base = site?.assets || [];
const next = [...base, ...items.map((it,i)=>({type:it.type, count:Number(it.count)||0, rule:it.rule||"", sort_order: base.length+i}))];
<div className="kpi-row">{[[rows.length,"设备类型","类"],[total,"设备实例","个"],[Math.max(2, rows.filter(r=>r.type).length+1),"资产层级","层"]].map(x=><div key={x[1]}><span>{x[1]}</span><b>{x[0]}</b><small>{x[2]}</small></div>)}{saving&&<div><span>保存</span><Status>保存中…</Status></div>}</div>
<Panel title="设备资产配置" tools={<Button onClick={add}>+ 添加设备类型</Button>}>
<table className="data-table editable">
- <thead><tr><th>设备类型 *</th><th>数量 *</th><th>命名规则</th><th>操作</th></tr></thead>
- <tbody>{rows.map((row,i)=><tr key={i} className={!row.type?"empty-row":""}>
- <td><input value={row.type} placeholder="例如:冷水机组" onChange={e=>update(i,{type:e.target.value})}/></td>
+ <thead><tr><th>设备类型 *</th><th></th><th>数量 *</th><th>命名规则</th><th>操作</th></tr></thead>
+ <tbody>{rows.map((row,i)=>{ const matchedTemplate = deviceTemplates.find(t=>t.name===row.type); return <tr key={i} className={!row.type?"empty-row":""}>
+ <td>{deviceTemplates.length > 0 ? <select value={row.type} onChange={e=>update(i,{type:e.target.value})}><option value="">请选择设备模板</option>{deviceTemplates.map(t=><option key={t.id} value={t.name}>{t.name}</option>)}</select> : <input value={row.type} placeholder="例如:冷水机组" onChange={e=>update(i,{type:e.target.value})}/>}</td>
+ <td><button className={"asset-eye-btn"+(matchedTemplate?"":" disabled")} title={matchedTemplate?`查看模板:${matchedTemplate.name}`:"请先选择设备模板"} onClick={()=>{ if(matchedTemplate){ go("deviceTemplates"); window.history.replaceState({}, "", "?page=deviceTemplates&openTemplate="+matchedTemplate.id); } }}><EyeOutlined/></button></td>
<td><input className="number" type="number" min={0} value={row.count||""} placeholder="0" onChange={e=>update(i,{count:Number(e.target.value)})}/></td>
<td><input value={row.rule} placeholder="例如:CH-##" onChange={e=>update(i,{rule:e.target.value})}/></td>
<td><button className="trash" onClick={()=>remove(i)}>⌫</button></td>
- </tr>)}</tbody>
+ </tr>})}</tbody>
</table>
</Panel>
<Panel title="资产层级预览"><div className="asset-tree-preview">
const {confirm,el:confirmEl}=useConfirm();
const load=async()=>{ try{ setList(await fetchDb("/device-templates") as any[]); }catch(e:any){ notify("加载失败:"+e.message); }finally{ setLoading(false); } };
useEffect(()=>{ load(); },[]);
+ // 支持从外部通过 URL 参数 openTemplate 自动打开指定模板编辑页
+ useEffect(()=>{ const p=new URLSearchParams(window.location.search); const tid=p.get("openTemplate"); if(tid){ setOpenId(Number(tid)); window.history.replaceState({}, "", "?page=deviceTemplates"); } },[]);
const pointCount=(row:any)=>(row.collection_points||[]).length;
const domainNames=(row:any)=>{ try{ return JSON.parse(row.domain_names||"[]") as string[]; }catch{ return []; } };
const remove=async(row:any)=>{ if(!(await confirm(`确定删除设备模板「${row.name}」?`))) return; try{ await fetchDb(`/device-templates/${row.id}`,{method:"DELETE"}); setPreview(null); await load(); notify("设备模板已删除"); }catch(e:any){ notify("删除失败:"+e.message); } };