From 8647b130a61ce4a0b5b7cdf4f130f788cd8c927a Mon Sep 17 00:00:00 2001 From: root Date: Thu, 10 Sep 2026 14:24:22 +0800 Subject: [PATCH] =?utf8?q?=E8=AE=BE=E5=A4=87=E6=A8=A1=E6=9D=BF=E6=96=B0?= =?utf8?q?=E5=A2=9E=E3=80=8C=E5=B1=9E=E6=80=A7=E5=88=97=E8=A1=A8=E3=80=8D?= =?utf8?q?=E5=8C=BA=E5=9D=97=EF=BC=9A=E5=85=B3=E9=94=AE=E5=AD=97/=E5=B1=9E?= =?utf8?q?=E6=80=A7=E5=90=8D=E7=A7=B0/=E5=B1=9E=E6=80=A7=E5=80=BC/?= =?utf8?q?=E5=8D=95=E4=BD=8D=EF=BC=8C=E6=94=AF=E6=8C=81=E8=A1=8C=E5=86=85?= =?utf8?q?=E7=BC=96=E8=BE=91=E3=80=81=E5=8B=BE=E9=80=89=E5=88=A0=E9=99=A4?= =?utf8?q?=E4=B8=8E=E4=BF=9D=E5=AD=98=EF=BC=88=E5=A4=8D=E7=94=A8=E9=87=87?= =?utf8?q?=E9=9B=86=E7=82=B9=E8=A1=A8=E6=A0=BC=E7=BC=96=E8=BE=91=E6=A0=B7?= =?utf8?q?=E5=BC=8F=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- app/page.tsx | 24 +++++++++++++++++ data/db/StationBuilder.sqlite | Bin 11657216 -> 11677696 bytes db-sidecar.mjs | 47 ++++++++++++++++++++++++++++++++-- 3 files changed, 69 insertions(+), 2 deletions(-) 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 70c7efabc6b5c220932d2f33da6b7d7950e8827b..a8677d504d944c87d8fa11efb37869afbc92a0db 100644 GIT binary patch delta 6874 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 delta 1483 zcmY+^TW}Lq9LMq9P0}WtW>2^-NgJA$t4K;NO&bDjRiNI`auEfAN}&Z?XhJW52vVT1 z5fEBfBp@IKqoQ0(CR2G4d~nop7^>rp;{yyc4iui4(H9?_(QkcG&&+3L_w4U~&VP1i z&(3?_+IHUj#@6c{shyJ~=XmMgQF?fKSkcb)ugc7@xV4O#A1uj^Sg>8Q`J9$2CB5Tr z`?!3?=M|Qja=Q4Z_Oa&O>TX#vSJy1}kL?h@OE$ScwiGIj$<~JEM2CFCp;&aeqq(!G zqsY8dGi9{ZS}&$rIn}7*evV^|EYepewinu0x3mRBkulr+GU-!{ zS>~PO1WSn#H*>9}iZRptrIp#3`A4fyH7cpBlyxe9C|1U(_V%{UH35gus4%M&;}s)n z)+fACqnuQ+JW3W1a=RX-dLCXrt88cqS4W}H1vUs%j;Ly?BgEOZ#HZ?akHzX{6BWV7bDDfCGhdB}$y4ybUV04`_{aAO2UViZPW z48}soIC$ViA&TI`cof5r5|m;BCSnpMV+y9?2~5M2n2rF-Fatq^5Jm*$h@t|Oh+!t; zn1$J>LN#je6rRR2n1i{Phgv*~=kPpUz>AoVm+&&`umB6O2#fIwmf%&qhNW1B*RdQc zuoCrn1FO)0)mVc@G+`~);Y~E71?#Z^tw1nbpmGHKq9UbVzCTzwQyoIfJ z8{4oQ?_dYIuoJtm8{OE0y)cl%yVy67@a{i0X|&yzle9#^d8g^f*z4?$ynrpoUwYb> z%FWx419%SyaR@!=#bF$QiKBQQ$B@QxoWKV-iBmX@4{-*4IE!=mXdpK)GoO<~lB`;n zB+iJK_OsSS3rQ{AmB?JrYg(EoRc=eW6B#|VB$>g-_ynKgJU+wcxPXhegv+>ss{>1t w{a5@J&0dzy@9p=ouxcMDJKq0ee&(9HKxJP=+>*pCF~ouVl^D2nqd(&R8+Vm$aR2}S 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])); } -- 2.43.0