|
|
@@ -67,7 +67,7 @@
|
|
|
|
|
|
<a-divider orientation="left">实体关联</a-divider>
|
|
|
|
|
|
- <a-form-item label="快速筛选 (关联主实体)">
|
|
|
+ <a-form-item label="设备主实体 (Main Entity)">
|
|
|
<a-select v-model="compositeForm.BaseEntity" allow-search placeholder="请选择 HA 设备 (先选设备可自动筛选关联实体)" @change="handleDeviceChange" allow-clear>
|
|
|
<a-option v-for="dev in haDevices" :key="dev.id" :value="dev.id">
|
|
|
{{ dev.name }}
|
|
|
@@ -80,7 +80,7 @@
|
|
|
<div v-for="(label, key) in currentTemplate" :key="key" style="margin-bottom: 16px">
|
|
|
<a-form-item :label="label">
|
|
|
<a-select v-model="compositeForm.AttributeMapping[key]" allow-search placeholder="选择关联的 HA 实体" allow-clear>
|
|
|
- <a-option v-for="item in getFilteredCandidates()" :key="item.entity_id" :value="item.entity_id">
|
|
|
+ <a-option v-for="item in getFilteredCandidates(key)" :key="item.entity_id" :value="item.entity_id">
|
|
|
{{ item.entity_id }} ({{ item.attributes?.friendly_name || item.state }})
|
|
|
</a-option>
|
|
|
</a-select>
|
|
|
@@ -201,12 +201,12 @@ const filterKeyword = ref('');
|
|
|
|
|
|
const templates: Record<string, Record<string, string>> = {
|
|
|
'ELEC': {
|
|
|
- 'switch': '开关控制 (Switch)',
|
|
|
- 'status': '开关状态 (Status)',
|
|
|
+ 'switch': '开关 (Switch)',
|
|
|
+ 'power': '电功率 (Power, W)',
|
|
|
'voltage': '电压 (Voltage, V)',
|
|
|
'current': '电流 (Current, A)',
|
|
|
- 'power': '功率 (Power, W)',
|
|
|
- 'energy': '累计用电量 (Energy, kWh)'
|
|
|
+ 'energy': '耗电量 (Energy, kWh)',
|
|
|
+ 'temperature': '温度 (Temperature, °C)'
|
|
|
},
|
|
|
'WATER': {
|
|
|
'flow_rate': '瞬时流量 (m³/h)',
|
|
|
@@ -422,9 +422,39 @@ const handleDeviceChange = async (val: any) => {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-const getFilteredCandidates = () => {
|
|
|
- // Since we load entities only for the selected device, just return them.
|
|
|
- return candidateData.value;
|
|
|
+const getFilteredCandidates = (attrKey?: string) => {
|
|
|
+ let data = [...candidateData.value];
|
|
|
+ if (!attrKey) return data;
|
|
|
+
|
|
|
+ const keywords: Record<string, string[]> = {
|
|
|
+ 'switch': ['switch', '开关'],
|
|
|
+ 'power': ['power', '功率'],
|
|
|
+ 'voltage': ['voltage', '电压'],
|
|
|
+ 'current': ['current', '电流'],
|
|
|
+ 'energy': ['energy', '耗电', '电量'],
|
|
|
+ 'temperature': ['temperature', 'temp', '温度'],
|
|
|
+ 'flow_rate': ['flow', '流量'],
|
|
|
+ 'consumption': ['consumption', '用量'],
|
|
|
+ 'pressure': ['pressure', '压']
|
|
|
+ };
|
|
|
+
|
|
|
+ const targetKeywords = keywords[attrKey] || [attrKey];
|
|
|
+
|
|
|
+ // Sort: matching keywords first
|
|
|
+ data.sort((a, b) => {
|
|
|
+ const aStr = (a.entity_id + (a.attributes?.friendly_name || '')).toLowerCase();
|
|
|
+ const bStr = (b.entity_id + (b.attributes?.friendly_name || '')).toLowerCase();
|
|
|
+
|
|
|
+ const aScore = targetKeywords.some(k => aStr.includes(k.toLowerCase())) ? 1 : 0;
|
|
|
+ const bScore = targetKeywords.some(k => bStr.includes(k.toLowerCase())) ? 1 : 0;
|
|
|
+
|
|
|
+ if (aScore !== bScore) {
|
|
|
+ return bScore - aScore; // Match first
|
|
|
+ }
|
|
|
+ return a.entity_id.localeCompare(b.entity_id); // Then alphabetical
|
|
|
+ });
|
|
|
+
|
|
|
+ return data;
|
|
|
};
|
|
|
|
|
|
const handleCompositeImport = async () => {
|
|
|
@@ -440,11 +470,15 @@ const handleCompositeImport = async () => {
|
|
|
try {
|
|
|
// Determine ExternalID
|
|
|
let externalID = '';
|
|
|
- const mappedValues = Object.values(compositeForm.AttributeMapping).filter(v => !!v);
|
|
|
- if (mappedValues.length > 0) {
|
|
|
- externalID = mappedValues[0];
|
|
|
+ if (compositeForm.BaseEntity) {
|
|
|
+ externalID = compositeForm.BaseEntity;
|
|
|
} else {
|
|
|
- externalID = `virtual-${Date.now()}`;
|
|
|
+ const mappedValues = Object.values(compositeForm.AttributeMapping).filter(v => !!v);
|
|
|
+ if (mappedValues.length > 0) {
|
|
|
+ externalID = mappedValues[0];
|
|
|
+ } else {
|
|
|
+ externalID = `virtual-${Date.now()}`;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
await createDevice({
|