From: root Date: Thu, 10 Sep 2026 06:24:22 +0000 (+0800) Subject: 设备模板新增「属性列表」区块:关键字/属性名称/属性值/单位,支持行内编辑、勾选删除与保存(复用采集点表格编辑样式) X-Git-Url: http://acesimba.cloud/gitweb/?a=commitdiff_plain;h=8647b130a61ce4a0b5b7cdf4f130f788cd8c927a;p=StationBuilder.git 设备模板新增「属性列表」区块:关键字/属性名称/属性值/单位,支持行内编辑、勾选删除与保存(复用采集点表格编辑样式) --- diff --git a/app/page.tsx b/app/page.tsx index bebcd4f..c91a773 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1604,6 +1604,14 @@ function DeviceTemplateCard({dev,notify,onChange,onBack,onViewCpPrompt}:{dev:any 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((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; @@ -1661,6 +1669,22 @@ function DeviceTemplateCard({dev,notify,onChange,onBack,onViewCpPrompt}:{dev:any ))} +
+
属性列表
+ + + {attrs.length===0 && } + {attrs.map((a:any)=>( + + + + + + + + ))} +
关键字属性名称属性值单位
暂无属性,点「+ 新增一行」添加
toggleAttr(a.key)}/>updAttr(a.key,"keyword",e.target.value)} placeholder="关键字"/>updAttr(a.key,"name",e.target.value)} placeholder="属性名称"/>updAttr(a.key,"value",e.target.value)} placeholder="属性值"/>updAttr(a.key,"unit",e.target.value)} placeholder="单位"/>
+
基于设备名称 / 领域 / 说明,AI 推荐合适的采集点并追加 diff --git a/data/db/StationBuilder.sqlite b/data/db/StationBuilder.sqlite index 70c7efa..a8677d5 100644 Binary files a/data/db/StationBuilder.sqlite and b/data/db/StationBuilder.sqlite differ diff --git a/db-sidecar.mjs b/db-sidecar.mjs index 76e27a3..6005ac0 100644 --- a/db-sidecar.mjs +++ b/db-sidecar.mjs @@ -268,6 +268,15 @@ function initSchema() { 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, @@ -1881,7 +1890,7 @@ async function handleDomainTemplateDetail(res, id) { 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); @@ -2181,7 +2190,7 @@ async function handleDeviceTemplatesList(res) { 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); } @@ -2200,6 +2209,7 @@ async function handleDeviceTemplateDetail(res, id) { 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); @@ -2255,6 +2265,32 @@ async function handleCollectionPointDelete(res, id) { 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); @@ -2485,12 +2521,14 @@ async function route(req, res) { 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+)$/); @@ -2770,6 +2808,7 @@ async function route(req, res) { 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])); @@ -2779,6 +2818,10 @@ async function route(req, res) { 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])); }