.resource-table th:nth-child(7),.resource-table td:nth-child(7){width:96px}.resource-table th:nth-child(8),.resource-table td:nth-child(8){width:180px}
.domain-resource-table th:first-child,.domain-resource-table td:first-child{width:260px;min-width:260px;text-align:left}.domain-resource-table .company-record-link{display:block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left}
.resource-detail>p{margin:8px 0;color:#666;line-height:1.8;white-space:pre-wrap}.resource-detail-image{display:block;width:100%;max-height:220px;object-fit:contain;border:1px solid var(--company-border);background:#f5f5f5;margin-bottom:20px}
-.device-resource-table{min-width:760px}.device-resource-table th:nth-child(1),.device-resource-table td:nth-child(1){width:280px;min-width:280px;text-align:left}.device-resource-table th:nth-child(2),.device-resource-table td:nth-child(2){width:auto}.device-resource-table th:nth-child(3),.device-resource-table td:nth-child(3){width:90px;text-align:center}.device-resource-table th:nth-child(4),.device-resource-table td:nth-child(4){width:110px}.device-resource-table th:nth-child(5),.device-resource-table td:nth-child(5){width:64px}.device-resource-table .company-record-link{display:block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left}
+.device-resource-table{min-width:860px}.device-resource-table th:nth-child(1),.device-resource-table td:nth-child(1){width:280px;min-width:280px;text-align:left}.device-resource-table th:nth-child(2),.device-resource-table td:nth-child(2){width:auto}.device-resource-table th:nth-child(3),.device-resource-table td:nth-child(3){width:90px;text-align:center}.device-resource-table th:nth-child(4),.device-resource-table td:nth-child(4){width:110px}.device-resource-table th:nth-child(5),.device-resource-table td.domain-cell{width:180px;min-width:140px;text-align:left;white-space:normal}.device-resource-table th:nth-child(6),.device-resource-table td:nth-child(6){width:64px}.device-resource-table .company-record-link{display:block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left}
.resource-name-cell{display:flex;align-items:center;gap:12px;min-width:0}.resource-name-cell>img,.resource-placeholder{flex:none;width:48px;height:36px;border:1px solid var(--company-border);background:#f5f5f5;object-fit:cover;border-radius:2px}.resource-placeholder{display:grid;place-items:center;color:#999}.resource-name-cell .company-record-link{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left}
.resource-point-list{border-top:1px solid #eee}.resource-point-list>div{height:40px;border-bottom:1px solid #eee;display:flex;align-items:center;justify-content:space-between}.resource-point-list span{color:var(--company-muted)}.resource-point-list>p{color:var(--company-muted)}
.resource-back{height:44px;display:flex;align-items:center;border-bottom:1px solid var(--company-border);margin:0 0 16px}.resource-back .text-button{margin:0}
function Button({children,secondary,onClick,disabled,title}:{children:React.ReactNode;secondary?:boolean;onClick?:()=>void;disabled?:boolean;title?:string}){return <AntButton title={title} disabled={disabled} onClick={onClick} type={secondary?"default":"primary"} className={"button "+(secondary?"secondary":"")}>{children}</AntButton>}
function Panel({title,tools,children,className=""}:{title?:string;tools?:React.ReactNode;children:React.ReactNode;className?:string}){return <section className={"panel "+className}>{title&&<div className="panel-head"><strong>{title}</strong><div>{tools}</div></div>}<div className="panel-body">{children}</div></section>}
function Field({label,required,hint,children}:{label:string;required?:boolean;hint?:string;children:React.ReactNode}){return <label className="field"><span>{required&&<i>*</i>}{label}</span>{children}{hint&&<small>{hint}</small>}</label>}
+function domainTagColor(name:string){
+ const map:Record<string,string>={"数据中心":"#009fe3","智慧水务":"#00b96b","燃气":"#ff990b","水务 · 自来水厂":"#42d067","新能源":"#7c3aed","其他":"#888888"};
+ if(map[name]) return map[name];
+ const palette=["#009fe3","#00b96b","#ff990b","#42d067","#7c3aed","#f05b49","#008cd6","#e58c35","#6d5ee9","#25a85b"];
+ let hash=0; for(let i=0;i<name.length;i++){ hash=((hash<<5)-hash)+name.charCodeAt(i); hash|=0; }
+ return palette[Math.abs(hash)%palette.length];
+}
+function DomainTag({name}:{name:string}){const color=domainTagColor(name); return <span className="ant-tag tag-new" style={{backgroundColor:`${color}32`,color,border:`1px solid ${color}`}}>{name}</span>;}
async function fetchDb(path:string, opts?:RequestInit){
const res = await fetch(`/api/db${path}`, opts);
const load=async()=>{ try{ setList(await fetchDb("/device-templates") as any[]); }catch(e:any){ notify("加载失败:"+e.message); }finally{ setLoading(false); } };
useEffect(()=>{ load(); },[]);
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); } };
const counts=useMemo(()=>({all:list.length,configured:list.filter(x=>pointCount(x)>0).length,empty:list.filter(x=>pointCount(x)===0).length}),[list]);
const filtered=useMemo(()=>list.filter(row=>(!pointFilter||(pointFilter==="configured"?pointCount(row)>0:pointCount(row)===0))&&(!query.trim()||`${row.name} ${row.description||""}`.toLowerCase().includes(query.trim().toLowerCase()))),[list,pointFilter,query]);
].map(item=><button key={item.key||"all"} role="tab" aria-selected={pointFilter===item.key} className={pointFilter===item.key?"active":""} onClick={()=>setPointFilter(item.key as ""|"configured"|"empty")}><span>{item.label}</span><b>{item.count}</b></button>)}</div>
<Panel className="company-list-panel resource-list-panel">
<div className="filterbar company-filterbar"><label className="search-field"><span aria-hidden>⌕</span><input value={query} onChange={e=>setQuery(e.target.value)} placeholder="搜索设备名称或说明"/></label><div className="filter-actions"><Button secondary onClick={()=>{setQuery("");setPointFilter("");}}>重置</Button><Button onClick={()=>setOpenId("create")}>+ 新增模板</Button></div></div>
- {loading?<div className="empty-state">å\8a è½½ä¸â\80¦</div>:<><table className="data-table company-table resource-table device-resource-table"><thead><tr><th>设å¤\87模æ\9d¿å\90\8dç§°</th><th>说æ\98\8e</th><th>é\87\87é\9b\86ç\82¹</th><th>é\85\8dç½®ç\8a¶æ\80\81</th><th>æ\93\8dä½\9c</th></tr></thead><tbody>{filtered.slice((pageIndex-1)*pageSize,pageIndex*pageSize).map(row=><tr key={row.id} onClick={()=>setPreview(row)}><td><button className="link company-record-link" title={row.name} onClick={e=>{e.stopPropagation();setPreview(row);}}><b>{row.name||"æ\9cªå\91½å\90\8d设å¤\87模æ\9d¿"}</b></button></td><td>{row.description||"â\80\94"}</td><td>{pointCount(row)}</td><td><Status>{pointCount(row)>0?"å·²é\85\8dç½®":"å¾\85é\85\8dç½®"}</Status></td><td onClick={e=>e.stopPropagation()}><Dropdown trigger={["click"]} menu={{items:[{key:"edit",label:"编辑模板",icon:<EditOutlined/>},{key:"delete",label:"删除",danger:true,icon:<DeleteOutlined/>}],onClick:({key})=>key==="edit"?setOpenId(row.id):remove(row)}}><AntButton type="text" aria-label="更多操作" icon={<EllipsisOutlined/>}/></Dropdown></td></tr>)}</tbody></table>{filtered.length===0&&<div className="empty-state">暂无符合条件的设备模板</div>}<div className="pagination"><span>{filtered.length} / 总数</span><button disabled={pageIndex<=1} onClick={()=>setPageIndex(p=>Math.max(1,p-1))}>‹</button><button className="active">{pageIndex}</button><button disabled={pageIndex>=Math.max(1,Math.ceil(filtered.length/pageSize))} onClick={()=>setPageIndex(p=>Math.min(Math.max(1,Math.ceil(filtered.length/pageSize)),p+1))}>›</button><select value={pageSize} onChange={e=>{setPageSize(Number(e.target.value));setPageIndex(1);}}><option value={10}>10 / 页</option><option value={20}>20 / 页</option><option value={50}>50 / 页</option></select></div></>}
+ {loading?<div className="empty-state">å\8a è½½ä¸â\80¦</div>:<><table className="data-table company-table resource-table device-resource-table"><thead><tr><th>设å¤\87模æ\9d¿å\90\8dç§°</th><th>说æ\98\8e</th><th>é\87\87é\9b\86ç\82¹</th><th>é\85\8dç½®ç\8a¶æ\80\81</th><th>æ\89\80å±\9eé¢\86å\9f\9f</th><th>æ\93\8dä½\9c</th></tr></thead><tbody>{filtered.slice((pageIndex-1)*pageSize,pageIndex*pageSize).map(row=><tr key={row.id} onClick={()=>setPreview(row)}><td><button className="link company-record-link" title={row.name} onClick={e=>{e.stopPropagation();setPreview(row);}}><b>{row.name||"æ\9cªå\91½å\90\8d设å¤\87模æ\9d¿"}</b></button></td><td>{row.description||"â\80\94"}</td><td>{pointCount(row)}</td><td><Status>{pointCount(row)>0?"å·²é\85\8dç½®":"å¾\85é\85\8dç½®"}</Status></td><td className="domain-cell">{domainNames(row).map(n=><DomainTag key={n} name={n}/>)}</td><td onClick={e=>e.stopPropagation()}><Dropdown trigger={["click"]} menu={{items:[{key:"edit",label:"编辑模板",icon:<EditOutlined/>},{key:"delete",label:"删除",danger:true,icon:<DeleteOutlined/>}],onClick:({key})=>key==="edit"?setOpenId(row.id):remove(row)}}><AntButton type="text" aria-label="更多操作" icon={<EllipsisOutlined/>}/></Dropdown></td></tr>)}</tbody></table>{filtered.length===0&&<div className="empty-state">暂无符合条件的设备模板</div>}<div className="pagination"><span>{filtered.length} / 总数</span><button disabled={pageIndex<=1} onClick={()=>setPageIndex(p=>Math.max(1,p-1))}>‹</button><button className="active">{pageIndex}</button><button disabled={pageIndex>=Math.max(1,Math.ceil(filtered.length/pageSize))} onClick={()=>setPageIndex(p=>Math.min(Math.max(1,Math.ceil(filtered.length/pageSize)),p+1))}>›</button><select value={pageSize} onChange={e=>{setPageSize(Number(e.target.value));setPageIndex(1);}}><option value={10}>10 / 页</option><option value={20}>20 / 页</option><option value={50}>50 / 页</option></select></div></>}
</Panel>
<Drawer title={preview?.name||"设备模板详情"} width={440} open={!!preview} onClose={()=>setPreview(null)} extra={preview&&<Button onClick={()=>{setPreview(null);setOpenId(preview.id);}}>编辑模板</Button>}>
{preview&&<div className="company-detail resource-detail">{preview.image_path&&<img className="resource-detail-image" src={`/api/db/device-templates/${preview.id}/image`} alt={preview.name}/>}<h3>基础信息</h3><dl><dt>模板名称</dt><dd>{preview.name}</dd><dt>采集点数量</dt><dd>{pointCount(preview)}</dd><dt>配置状态</dt><dd><Status>{pointCount(preview)>0?"已配置":"待配置"}</Status></dd></dl><h3>模板说明</h3><p>{preview.description||"暂无说明"}</p><h3>采集点预览</h3><div className="resource-point-list">{(preview.collection_points||[]).slice(0,8).map((cp:any)=><div key={cp.id||cp.name}><b>{cp.name}</b><span>{cp.unit||cp.value_range||"—"}</span></div>)}{pointCount(preview)===0&&<p>暂无采集点</p>}</div></div>}