const toggleAll=()=>setCps(cps.map(c=>({...c,checked:!allChecked})));
const delCpRows=async()=>{ const sel=cps.filter(c=>c.checked); if(sel.length===0){ notify("请先勾选要删除的采集点"); return; } if(!(await confirm(`确定删除选中的 ${sel.length} 个采集点?`))) return; for(const c of sel){ if(c.id) await fetchDb(`/collection-points/${c.id}`,{method:"DELETE"}); } setCps(cps.filter(c=>!c.checked)); onChange(); };
const saveCpRows=async()=>{ try{ for(const c of cps){ const payload={name:c.name,collect_note:c.collect_note,unit:c.unit,value_range:c.value_range,description:c.description}; if(c.id){ await fetchDb(`/collection-points/${c.id}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload)}); } else { await fetchDb(`/device-templates/${dev.id}/collection-points`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload)}); } } notify("采集点已保存"); onChange(); }catch(e:any){ notify("保存失败:"+e.message); } };
+ const [attrs,setAttrs]=useState<any[]>((dev.attributes||[]).map((a:any)=>({...a,key:a.id,checked:false})));
+ const addAttrRow=()=>setAttrs([...attrs,{key:-(Date.now()),keyword:"",name:"",value:"",unit:"",checked:false}]);
+ const updAttr=(k:number,field:string,val:any)=>setAttrs(attrs.map(a=>a.key===k?{...a,[field]:val}:a));
+ const toggleAttr=(k:number)=>setAttrs(attrs.map(a=>a.key===k?{...a,checked:!a.checked}:a));
+ const allAttrChecked=attrs.length>0&&attrs.every(a=>a.checked);
+ const toggleAllAttr=()=>setAttrs(attrs.map(a=>({...a,checked:!allAttrChecked})));
+ const delAttrRows=async()=>{ const sel=attrs.filter(a=>a.checked); if(sel.length===0){ notify("请先勾选要删除的属性"); return; } if(!(await confirm(`确定删除选中的 ${sel.length} 个属性?`))) return; for(const a of sel){ if(a.id) await fetchDb(`/attributes/${a.id}`,{method:"DELETE"}); } setAttrs(attrs.filter(a=>!a.checked)); onChange(); };
+ const saveAttrRows=async()=>{ try{ for(const a of attrs){ const payload={keyword:a.keyword,name:a.name,value:a.value,unit:a.unit}; if(a.id){ await fetchDb(`/attributes/${a.id}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload)}); } else { await fetchDb(`/device-templates/${dev.id}/attributes`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload)}); } } notify("属性已保存"); onChange(); }catch(e:any){ notify("保存失败:"+e.message); } };
const cpAi=useStreamComplete();
const runCpComplete=async()=>{
if(cpAi.completing) return;
))}
</tbody></table>
</div>
+ <div className="cp-list">
+ <div className="cp-list-head"><b>属性列表</b><span><button className="text-button" onClick={addAttrRow}>+ 新增一行</button><button className="text-button" onClick={delAttrRows}>删除选中</button><button className="text-button" onClick={saveAttrRows}>保存属性</button></span></div>
+ <table className="data-table cp-editor"><thead><tr><th style={{width:34}}><input type="checkbox" checked={allAttrChecked} onChange={toggleAllAttr}/></th><th>关键字</th><th>属性名称</th><th>属性值</th><th>单位</th></tr></thead>
+ <tbody>
+ {attrs.length===0 && <tr><td colSpan={5} className="muted">暂无属性,点「+ 新增一行」添加</td></tr>}
+ {attrs.map((a:any)=>(
+ <tr key={a.key}>
+ <td><input type="checkbox" checked={!!a.checked} onChange={()=>toggleAttr(a.key)}/></td>
+ <td><input value={a.keyword||""} onChange={e=>updAttr(a.key,"keyword",e.target.value)} placeholder="关键字"/></td>
+ <td><input value={a.name||""} onChange={e=>updAttr(a.key,"name",e.target.value)} placeholder="属性名称"/></td>
+ <td><input value={a.value||""} onChange={e=>updAttr(a.key,"value",e.target.value)} placeholder="属性值"/></td>
+ <td><input value={a.unit||""} onChange={e=>updAttr(a.key,"unit",e.target.value)} placeholder="单位"/></td>
+ </tr>
+ ))}
+ </tbody></table>
+ </div>
<div className="field-assist" style={{marginTop:16}}>
<Button secondary onClick={()=>setAiOpen(true)}><RobotOutlined/> AI 智能补全</Button>
<span className="muted">基于设备名称 / 领域 / 说明,AI 推荐合适的采集点并追加</span>
description TEXT,
FOREIGN KEY(device_template_id) REFERENCES device_templates(id) ON DELETE CASCADE
);
+ CREATE TABLE IF NOT EXISTS device_template_attributes (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ device_template_id INTEGER NOT NULL,
+ keyword TEXT,
+ name TEXT,
+ value TEXT,
+ unit TEXT,
+ FOREIGN KEY(device_template_id) REFERENCES device_templates(id) ON DELETE CASCADE
+ );
CREATE TABLE IF NOT EXISTS device_template_domains (
device_template_id INTEGER NOT NULL,
domain_id INTEGER NOT NULL,
FROM device_templates dt2
WHERE dt2.id IN (SELECT device_template_id FROM device_template_domain_templates WHERE domain_template_id=?)
`).all(id);
- for (const dev of devices) { dev.collection_points = db.prepare("SELECT * FROM device_collection_points WHERE device_template_id=? ORDER BY id").all(dev.id); try { dev.domain_ids = JSON.parse(dev.domain_ids || "[]"); } catch { dev.domain_ids = []; } try { dev.domain_names = JSON.parse(dev.domain_names || "[]"); } catch { dev.domain_names = []; } }
+ for (const dev of devices) { dev.collection_points = db.prepare("SELECT * FROM device_collection_points WHERE device_template_id=? ORDER BY id").all(dev.id); dev.attributes = db.prepare("SELECT * FROM device_template_attributes WHERE device_template_id=? ORDER BY id").all(dev.id); try { dev.domain_ids = JSON.parse(dev.domain_ids || "[]"); } catch { dev.domain_ids = []; } try { dev.domain_names = JSON.parse(dev.domain_names || "[]"); } catch { dev.domain_names = []; } }
const pages = db.prepare("SELECT * FROM domain_pages WHERE domain_template_id=? ORDER BY id").all(id);
const menu = db.prepare("SELECT * FROM template_menu_items WHERE domain_template_id=? ORDER BY sort_order,id").all(id);
const docs = db.prepare("SELECT id,name,doc_type,url,mime,size,created_at FROM knowledge_docs WHERE domain_template_id=? ORDER BY id").all(id);
const rows = db.prepare(`
SELECT dt.*, (SELECT json_group_array(d.name) FROM device_template_domains dtd JOIN domains d ON d.id=dtd.domain_id WHERE dtd.device_template_id=dt.id) AS domain_names
FROM device_templates dt ORDER BY dt.id DESC`).all();
- for (const row of rows) row.collection_points = db.prepare("SELECT * FROM device_collection_points WHERE device_template_id=? ORDER BY id").all(row.id);
+ for (const row of rows) { row.collection_points = db.prepare("SELECT * FROM device_collection_points WHERE device_template_id=? ORDER BY id").all(row.id); row.attributes = db.prepare("SELECT * FROM device_template_attributes WHERE device_template_id=? ORDER BY id").all(row.id); }
respondJson(res, 200, rows);
}
const dev = db.prepare("SELECT * FROM device_templates WHERE id=?").get(id);
if (!dev) return respondError(res, 404, "设备模板不存在");
dev.collection_points = db.prepare("SELECT * FROM device_collection_points WHERE device_template_id=? ORDER BY id").all(id);
+ dev.attributes = db.prepare("SELECT * FROM device_template_attributes WHERE device_template_id=? ORDER BY id").all(id);
dev.domain_ids = db.prepare("SELECT domain_id FROM device_template_domains WHERE device_template_id=?").all(id).map(r => r.domain_id);
dev.domain_template_ids = db.prepare("SELECT domain_template_id FROM device_template_domain_templates WHERE device_template_id=?").all(id).map(r => r.domain_template_id);
respondJson(res, 200, dev);
respondJson(res, 200, { ok: true });
}
+async function handleAttributeCreate(req, res, devId) {
+ const body = await parseBody(req);
+ const name = (body.name || "").toString().trim();
+ if (!name) return respondError(res, 400, "属性名称不能为空");
+ const info = db.prepare("INSERT INTO device_template_attributes (device_template_id,keyword,name,value,unit) VALUES (?,?,?,?,?)").run(devId, body.keyword || "", name, body.value || "", body.unit || "");
+ respondJson(res, 200, { ok: true, id: info.lastInsertRowid });
+}
+
+async function handleAttributeUpdate(req, res, id) {
+ const row = db.prepare("SELECT * FROM device_template_attributes WHERE id=?").get(id);
+ if (!row) return respondError(res, 404, "属性不存在");
+ const body = await parseBody(req);
+ const keyword = body.keyword != null ? body.keyword : row.keyword;
+ const name = body.name != null ? body.name.toString().trim() : row.name;
+ const value = body.value != null ? body.value : row.value;
+ const unit = body.unit != null ? body.unit : row.unit;
+ if (!name) return respondError(res, 400, "属性名称不能为空");
+ db.prepare("UPDATE device_template_attributes SET keyword=?,name=?,value=?,unit=? WHERE id=?").run(keyword, name, value, unit, id);
+ respondJson(res, 200, { ok: true, id });
+}
+
+async function handleAttributeDelete(res, id) {
+ db.prepare("DELETE FROM device_template_attributes WHERE id=?").run(id);
+ respondJson(res, 200, { ok: true });
+}
+
async function handleDeviceCompleteCollectionPoints(req, res, devId) {
res.writeHead(200, { "Content-Type": "application/x-ndjson; charset=utf-8", "Cache-Control": "no-cache" });
const emit = (o) => streamEvent(res, o);
const mDtPageRefine = path.match(/^\/api\/domain-templates\/(\d+)\/pages\/(\d+)\/refine$/);
const mDevList = path === "/api/device-templates";
const mDevCp = path.match(/^\/api\/device-templates\/(\d+)\/collection-points$/);
+ const mDevAttr = path.match(/^\/api\/device-templates\/(\d+)\/attributes$/);
const mDev = path.match(/^\/api\/device-templates\/(\d+)$/);
const mDevImage = path.match(/^\/api\/device-templates\/(\d+)\/image$/);
const mDevCpComplete = path.match(/^\/api\/device-templates\/(\d+)\/complete-collection-points$/);
const mDevCpCompleteStop = path.match(/^\/api\/device-templates\/(\d+)\/complete-collection-points\/stop$/);
const mDevCpCompletePrompt = path.match(/^\/api\/device-templates\/(\d+)\/complete-collection-points-prompt$/);
const mCp = path.match(/^\/api\/collection-points\/(\d+)$/);
+ const mAttr = path.match(/^\/api\/attributes\/(\d+)$/);
const mDp = path.match(/^\/api\/domain-pages\/(\d+)$/);
const mMenu = path.match(/^\/api\/template-menu-items\/(\d+)$/);
const mDoc = path.match(/^\/api\/knowledge-docs\/(\d+)$/);
if (req.method === "POST") return handleDeviceTemplateCreate(req, res);
}
if (mDevCp) { if (req.method === "POST") return handleCollectionPointCreate(req, res, Number(mDevCp[1])); }
+ if (mDevAttr) { if (req.method === "POST") return handleAttributeCreate(req, res, Number(mDevAttr[1])); }
if (mDev) {
if (req.method === "GET") return handleDeviceTemplateDetail(res, Number(mDev[1]));
if (req.method === "PUT") return handleDeviceTemplateUpdate(req, res, Number(mDev[1]));
if (req.method === "PUT") return handleCollectionPointUpdate(req, res, Number(mCp[1]));
if (req.method === "DELETE") return handleCollectionPointDelete(res, Number(mCp[1]));
}
+ if (mAttr) {
+ if (req.method === "PUT") return handleAttributeUpdate(req, res, Number(mAttr[1]));
+ if (req.method === "DELETE") return handleAttributeDelete(res, Number(mAttr[1]));
+ }
if (mDevCpComplete) { if (req.method === "POST") return handleDeviceCompleteCollectionPoints(req, res, Number(mDevCpComplete[1])); }
if (mDevCpCompleteStop) { if (req.method === "POST") return handleDeviceCompleteCollectionPointsStop(res, Number(mDevCpCompleteStop[1])); }
if (mDevCpCompletePrompt) { if (req.method === "GET") return handleDeviceCompleteCollectionPointsPrompt(req, res, Number(mDevCpCompletePrompt[1])); }