node-red-contrib-symi-mesh 1.7.1 → 1.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,13 +10,14 @@
10
10
  },
11
11
  inputs: 1,
12
12
  outputs: 1,
13
- icon: 'bridge.png',
13
+ icon: 'font-awesome/fa-exchange',
14
+ align: 'left',
14
15
  paletteLabel: 'RS485桥接',
15
16
  label: function() {
16
17
  if (this.name) return this.name;
17
18
  try {
18
19
  var m = JSON.parse(this.mappings || '[]');
19
- if (m.length > 0) return 'RS485桥接 (' + m.length + '组)';
20
+ if (m.length > 0) return 'RS485桥接(' + m.length + '组)';
20
21
  } catch(e) {}
21
22
  return 'RS485桥接';
22
23
  },
@@ -83,8 +84,10 @@
83
84
  // 构建Mesh设备选项(不包含按键,按键单独选择)
84
85
  function getMeshOptions(selectedMac) {
85
86
  var html = '<option value="">-- 选择 --</option>';
87
+ var selMacNorm = (selectedMac || '').toLowerCase().replace(/:/g, '');
86
88
  meshDevices.forEach(function(d) {
87
- var selected = (d.mac === selectedMac) ? ' selected' : '';
89
+ var devMacNorm = (d.mac || '').toLowerCase().replace(/:/g, '');
90
+ var selected = (devMacNorm === selMacNorm && selMacNorm !== '') ? ' selected' : '';
88
91
  html += '<option value="' + d.mac + '" data-channels="' + (d.channels || 1) + '"' + selected + '>' + d.name + '</option>';
89
92
  });
90
93
  return html;
@@ -92,7 +95,10 @@
92
95
 
93
96
  // 构建Mesh按键选项(仅当开关设备时显示)
94
97
  function getMeshChannelOptions(mac, selectedChannel) {
95
- var device = meshDevices.find(function(d) { return d.mac === mac; });
98
+ var macNorm = (mac || '').toLowerCase().replace(/:/g, '');
99
+ var device = meshDevices.find(function(d) {
100
+ return (d.mac || '').toLowerCase().replace(/:/g, '') === macNorm;
101
+ });
96
102
  var channels = device ? (device.channels || 1) : 0;
97
103
  if (channels <= 1) return '';
98
104
  var html = '<select class="mesh-channel">';
@@ -106,7 +112,10 @@
106
112
 
107
113
  // 获取Mesh设备的按键数
108
114
  function getMeshDeviceChannels(mac) {
109
- var device = meshDevices.find(function(d) { return d.mac === mac; });
115
+ var macNorm = (mac || '').toLowerCase().replace(/:/g, '');
116
+ var device = meshDevices.find(function(d) {
117
+ return (d.mac || '').toLowerCase().replace(/:/g, '') === macNorm;
118
+ });
110
119
  return device ? (device.channels || 1) : 1;
111
120
  }
112
121
 
@@ -159,7 +168,7 @@
159
168
  }
160
169
 
161
170
  mappings.forEach(function(m, idx) {
162
- var row = $('<div class="mapping-row" data-idx="' + idx + '" style="flex-wrap:wrap;"></div>');
171
+ var row = $('<div class="mapping-row" data-idx="' + idx + '"></div>');
163
172
  // 地址输入框:杜亚2字节地址,自定义模式隐藏,其他显示
164
173
  var addrHtml = '';
165
174
  if (m.brand === 'custom') {
@@ -174,7 +183,29 @@
174
183
  '<input type="number" class="addr-input" value="' + (m.address !== undefined ? m.address : 1) + '" min="0" max="255" title="Modbus从机地址(0-255)">' +
175
184
  '</div>';
176
185
  }
186
+ // 生成自定义码摘要信息
187
+ var codeSummary = '';
188
+ if (m.brand === 'custom' && m.customCodes) {
189
+ var codes = m.customCodes;
190
+ if (m.device === 'custom_switch') {
191
+ codeSummary = (codes.sendOn || codes.recvOn || codes.sendOff || codes.recvOff) ? '已配置' : '未配置';
192
+ } else if (m.device === 'custom_curtain') {
193
+ codeSummary = (codes.sendOpen || codes.sendClose || codes.recvOpen || codes.recvClose) ? '已配置' : '未配置';
194
+ } else if (m.device === 'custom_climate') {
195
+ codeSummary = (codes.acSendOn || codes.fanSendHigh || codes.modeSendCool) ? '已配置' : '未配置';
196
+ } else {
197
+ codeSummary = codes.trigger ? '已配置' : '未配置';
198
+ }
199
+ }
200
+ // 自定义码折叠按钮和反馈选项(放在同一行)
201
+ var toggleBtn = '';
202
+ if (m.brand === 'custom' && m.device) {
203
+ var feedbackChecked = (m.feedback !== false) ? ' checked' : '';
204
+ toggleBtn = '<label class="feedback-label" title="勾选:RS485收码后Mesh执行并反馈发码;不勾选:RS485收码后Mesh执行但不反馈" style="font-size:10px;cursor:pointer;white-space:nowrap;"><input type="checkbox" class="feedback-checkbox"' + feedbackChecked + ' style="margin-right:1px;vertical-align:middle;">反馈</label>';
205
+ toggleBtn += '<button type="button" class="red-ui-button red-ui-button-small btn-toggle-codes" title="展开/收起自定义码" style="margin-left:2px;background:#fff8e1;border-color:#ffa726;color:#f57c00;padding:2px 4px;"><i class="fa fa-code"></i>' + codeSummary + '</button>';
206
+ }
177
207
  row.html(
208
+ '<div class="mapping-main">' +
178
209
  '<div class="mesh-col">' +
179
210
  ' <select class="mesh-select">' + getMeshOptions(m.meshMac) + '</select>' +
180
211
  ' <span class="mesh-ch-wrap">' + getMeshChannelOptions(m.meshMac, m.meshChannel || 1) + '</span>' +
@@ -188,16 +219,22 @@
188
219
  ' <span class="rs485-ch-wrap">' + getRS485ChannelOptions(m.brand, m.device, m.rs485Channel || 1) + '</span>' +
189
220
  '</div>' +
190
221
  addrHtml +
191
- '<div class="del-col"><button type="button" class="red-ui-button red-ui-button-small btn-remove" title="删除"><i class="fa fa-times"></i></button></div>'
222
+ '<div class="del-col">' + toggleBtn + '<button type="button" class="red-ui-button red-ui-button-small btn-remove" title="删除" style="padding:2px 6px;"><i class="fa fa-times"></i></button></div>' +
223
+ '</div>'
192
224
  );
193
225
  container.append(row);
194
226
  // 如果是自定义协议,显示自定义码输入框
195
227
  if (m.brand === 'custom' && m.device) {
196
228
  updateCustomCodesVisibility(row, m.brand, m.device, idx);
197
229
  }
230
+ // 如果是SYMI或中弘协议,显示桥接配置
231
+ if ((m.brand === 'symi' || m.brand === 'zhonghong') && m.device) {
232
+ updateCustomCodesVisibility(row, m.brand, m.device, idx);
233
+ }
198
234
  });
199
235
 
200
236
  bindEvents();
237
+ bindToggleEvents();
201
238
  }
202
239
 
203
240
  // 绑定事件
@@ -224,17 +261,13 @@
224
261
  });
225
262
 
226
263
  container.find('.device-select').off('change').on('change', function() {
227
- var row = $(this).closest('.mapping-row');
228
- var idx = row.data('idx');
264
+ var idx = $(this).closest('.mapping-row').data('idx');
229
265
  var deviceId = $(this).val();
230
- var brandId = row.find('.brand-select').val();
266
+ var brandId = $(this).closest('.mapping-row').find('.brand-select').val();
231
267
  mappings[idx].device = deviceId;
232
- // 更新RS485按键选择
233
- row.find('.rs485-ch-wrap').html(getRS485ChannelOptions(brandId, deviceId, 1));
234
268
  mappings[idx].rs485Channel = 1;
235
- // 显示/隐藏自定义码输入区域
236
- updateCustomCodesVisibility(row, brandId, deviceId, idx);
237
- bindEvents(); // 重新绑定新添加的元素事件
269
+ // 重新渲染以更新反馈/折叠按钮和录码区域
270
+ renderMappings();
238
271
  });
239
272
 
240
273
  container.find('.rs485-channel').off('change').on('change', function() {
@@ -281,37 +314,43 @@
281
314
  renderMappings();
282
315
  });
283
316
 
317
+ // 反馈选项
318
+ container.find('.feedback-checkbox').off('change').on('change', function() {
319
+ var idx = $(this).closest('.mapping-row').data('idx');
320
+ mappings[idx].feedback = $(this).is(':checked');
321
+ });
322
+
284
323
  // 自定义码输入事件 - 开关
285
- container.find('.custom-send-on').off('change').on('change', function() {
324
+ container.find('.custom-code-send-on').off('change').on('change', function() {
286
325
  var idx = $(this).closest('.mapping-row').data('idx');
287
326
  mappings[idx].customCodes = mappings[idx].customCodes || {};
288
327
  mappings[idx].customCodes.sendOn = $(this).val();
289
328
  });
290
- container.find('.custom-send-off').off('change').on('change', function() {
329
+ container.find('.custom-code-send-off').off('change').on('change', function() {
291
330
  var idx = $(this).closest('.mapping-row').data('idx');
292
331
  mappings[idx].customCodes = mappings[idx].customCodes || {};
293
332
  mappings[idx].customCodes.sendOff = $(this).val();
294
333
  });
295
- container.find('.custom-recv-on').off('change').on('change', function() {
334
+ container.find('.custom-code-recv-on').off('change').on('change', function() {
296
335
  var idx = $(this).closest('.mapping-row').data('idx');
297
336
  mappings[idx].customCodes = mappings[idx].customCodes || {};
298
337
  mappings[idx].customCodes.recvOn = $(this).val();
299
338
  });
300
- container.find('.custom-recv-off').off('change').on('change', function() {
339
+ container.find('.custom-code-recv-off').off('change').on('change', function() {
301
340
  var idx = $(this).closest('.mapping-row').data('idx');
302
341
  mappings[idx].customCodes = mappings[idx].customCodes || {};
303
342
  mappings[idx].customCodes.recvOff = $(this).val();
304
343
  });
305
344
  // 窗帘码
306
- container.find('.custom-send-open, .custom-send-close, .custom-send-stop, .custom-recv-open, .custom-recv-close, .custom-recv-stop').off('change').on('change', function() {
345
+ container.find('.custom-code-send-open, .custom-code-send-close, .custom-code-send-stop, .custom-code-recv-open, .custom-code-recv-close, .custom-code-recv-stop').off('change').on('change', function() {
307
346
  var idx = $(this).closest('.mapping-row').data('idx');
308
347
  mappings[idx].customCodes = mappings[idx].customCodes || {};
309
- if ($(this).hasClass('custom-send-open')) mappings[idx].customCodes.sendOpen = $(this).val();
310
- if ($(this).hasClass('custom-send-close')) mappings[idx].customCodes.sendClose = $(this).val();
311
- if ($(this).hasClass('custom-send-stop')) mappings[idx].customCodes.sendStop = $(this).val();
312
- if ($(this).hasClass('custom-recv-open')) mappings[idx].customCodes.recvOpen = $(this).val();
313
- if ($(this).hasClass('custom-recv-close')) mappings[idx].customCodes.recvClose = $(this).val();
314
- if ($(this).hasClass('custom-recv-stop')) mappings[idx].customCodes.recvStop = $(this).val();
348
+ if ($(this).hasClass('custom-code-send-open')) mappings[idx].customCodes.sendOpen = $(this).val();
349
+ if ($(this).hasClass('custom-code-send-close')) mappings[idx].customCodes.sendClose = $(this).val();
350
+ if ($(this).hasClass('custom-code-send-stop')) mappings[idx].customCodes.sendStop = $(this).val();
351
+ if ($(this).hasClass('custom-code-recv-open')) mappings[idx].customCodes.recvOpen = $(this).val();
352
+ if ($(this).hasClass('custom-code-recv-close')) mappings[idx].customCodes.recvClose = $(this).val();
353
+ if ($(this).hasClass('custom-code-recv-stop')) mappings[idx].customCodes.recvStop = $(this).val();
315
354
  });
316
355
  // 空调码
317
356
  container.find('[class^="custom-ac-"], [class^="custom-fan-"], [class^="custom-mode-"], [class^="custom-temp-"]').off('change').on('change', function() {
@@ -327,9 +366,11 @@
327
366
  else if (cls.includes('fan-send-high')) mappings[idx].customCodes.fanSendHigh = $(this).val();
328
367
  else if (cls.includes('fan-send-mid')) mappings[idx].customCodes.fanSendMid = $(this).val();
329
368
  else if (cls.includes('fan-send-low')) mappings[idx].customCodes.fanSendLow = $(this).val();
369
+ else if (cls.includes('fan-send-auto')) mappings[idx].customCodes.fanSendAuto = $(this).val();
330
370
  else if (cls.includes('fan-recv-high')) mappings[idx].customCodes.fanRecvHigh = $(this).val();
331
371
  else if (cls.includes('fan-recv-mid')) mappings[idx].customCodes.fanRecvMid = $(this).val();
332
372
  else if (cls.includes('fan-recv-low')) mappings[idx].customCodes.fanRecvLow = $(this).val();
373
+ else if (cls.includes('fan-recv-auto')) mappings[idx].customCodes.fanRecvAuto = $(this).val();
333
374
  else if (cls.includes('mode-send-cool')) mappings[idx].customCodes.modeSendCool = $(this).val();
334
375
  else if (cls.includes('mode-send-heat')) mappings[idx].customCodes.modeSendHeat = $(this).val();
335
376
  else if (cls.includes('mode-send-dry')) mappings[idx].customCodes.modeSendDry = $(this).val();
@@ -369,14 +410,112 @@
369
410
  });
370
411
  }
371
412
 
413
+ // 绑定SYMI/Zhonghong桥接配置事件
414
+ function bindBridgeEvents() {
415
+ var container = $('#mapping-list');
416
+
417
+ // SYMI配置
418
+ container.find('.symi-local-addr').off('change').on('change', function() {
419
+ var idx = $(this).closest('.mapping-row').data('idx');
420
+ mappings[idx].symiLocalAddr = parseInt($(this).val()) || 1;
421
+ });
422
+ container.find('.symi-device-addr').off('change').on('change', function() {
423
+ var idx = $(this).closest('.mapping-row').data('idx');
424
+ mappings[idx].symiDeviceAddr = parseInt($(this).val()) || 1;
425
+ });
426
+ container.find('.symi-device-channel').off('change').on('change', function() {
427
+ var idx = $(this).closest('.mapping-row').data('idx');
428
+ mappings[idx].symiDeviceChannel = parseInt($(this).val()) || 0;
429
+ });
430
+ container.find('.symi-brand-id').off('change').on('change', function() {
431
+ var idx = $(this).closest('.mapping-row').data('idx');
432
+ mappings[idx].symiBrandId = parseInt($(this).val()) || 0;
433
+ });
434
+
435
+ // Zhonghong配置
436
+ container.find('.zh-slave-addr').off('change').on('change', function() {
437
+ var idx = $(this).closest('.mapping-row').data('idx');
438
+ mappings[idx].zhSlaveAddr = parseInt($(this).val()) || 1;
439
+ });
440
+ container.find('.zh-outdoor-addr').off('change').on('change', function() {
441
+ var idx = $(this).closest('.mapping-row').data('idx');
442
+ mappings[idx].zhOutdoorAddr = parseInt($(this).val()) || 1;
443
+ });
444
+ container.find('.zh-indoor-addr').off('change').on('change', function() {
445
+ var idx = $(this).closest('.mapping-row').data('idx');
446
+ mappings[idx].zhIndoorAddr = parseInt($(this).val()) || 1;
447
+ });
448
+
449
+ // 桥接启用
450
+ container.find('.zh-bridge-target').off('change').on('change', function() {
451
+ var idx = $(this).closest('.mapping-row').data('idx');
452
+ mappings[idx].zhBridgeTarget = $(this).is(':checked');
453
+ });
454
+ container.find('.symi-bridge-target').off('change').on('change', function() {
455
+ var idx = $(this).closest('.mapping-row').data('idx');
456
+ mappings[idx].symiBridgeTarget = $(this).is(':checked');
457
+ });
458
+ }
459
+
372
460
  // 更新自定义码输入区域的显示
373
461
  function updateCustomCodesVisibility(row, brandId, deviceId, idx) {
374
462
  row.find('.custom-codes-row').remove();
463
+ row.find('.bridge-config-row').remove();
464
+
465
+ // SYMI空调面板配置
466
+ if (brandId === 'symi' && deviceId === 'climate') {
467
+ var m = mappings[idx];
468
+ var html = '<div class="bridge-config-row" style="width:100%;padding:8px;margin-top:6px;background:#e3f2fd;border:1px dashed #2196f3;border-radius:4px;">';
469
+ html += '<div style="font-size:11px;color:#1976d2;margin-bottom:6px;"><i class="fa fa-cog"></i> SYMI面板配置</div>';
470
+ html += '<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:6px;">';
471
+ html += '<label style="font-size:10px;">本机地址: <input type="number" class="symi-local-addr" value="' + (m.symiLocalAddr || 1) + '" min="0" max="255" style="width:100%;padding:2px 4px;border:1px solid #64b5f6;border-radius:2px;font-size:10px;"></label>';
472
+ html += '<label style="font-size:10px;">设备地址: <input type="number" class="symi-device-addr" value="' + (m.symiDeviceAddr || 1) + '" min="0" max="255" style="width:100%;padding:2px 4px;border:1px solid #64b5f6;border-radius:2px;font-size:10px;"></label>';
473
+ html += '<label style="font-size:10px;">设备通道: <input type="number" class="symi-device-channel" value="' + (m.symiDeviceChannel || 0) + '" min="0" max="255" style="width:100%;padding:2px 4px;border:1px solid #64b5f6;border-radius:2px;font-size:10px;"></label>';
474
+ html += '<label style="font-size:10px;">品牌ID: <input type="number" class="symi-brand-id" value="' + (m.symiBrandId || 0) + '" min="0" max="255" style="width:100%;padding:2px 4px;border:1px solid #64b5f6;border-radius:2px;font-size:10px;"></label>';
475
+ html += '</div>';
476
+ html += '<div style="font-size:11px;color:#1976d2;margin:8px 0 6px 0;"><i class="fa fa-link"></i> 桥接到中弘VRF</div>';
477
+ html += '<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:6px;">';
478
+ html += '<label style="font-size:10px;"><input type="checkbox" class="zh-bridge-target" ' + (m.zhBridgeTarget ? 'checked' : '') + '> 启用桥接</label>';
479
+ html += '<label style="font-size:10px;">从机地址: <input type="number" class="zh-slave-addr" value="' + (m.zhSlaveAddr || 1) + '" min="0" max="255" style="width:100%;padding:2px 4px;border:1px solid #64b5f6;border-radius:2px;font-size:10px;"></label>';
480
+ html += '<label style="font-size:10px;">外机地址: <input type="number" class="zh-outdoor-addr" value="' + (m.zhOutdoorAddr || 1) + '" min="0" max="255" style="width:100%;padding:2px 4px;border:1px solid #64b5f6;border-radius:2px;font-size:10px;"></label>';
481
+ html += '<label style="font-size:10px;">内机地址: <input type="number" class="zh-indoor-addr" value="' + (m.zhIndoorAddr || 1) + '" min="0" max="255" style="width:100%;padding:2px 4px;border:1px solid #64b5f6;border-radius:2px;font-size:10px;"></label>';
482
+ html += '</div>';
483
+ html += '</div>';
484
+ row.append(html);
485
+ bindBridgeEvents();
486
+ return;
487
+ }
488
+
489
+ // 中弘VRF配置
490
+ if (brandId === 'zhonghong' && deviceId === 'climate') {
491
+ var m = mappings[idx];
492
+ var html = '<div class="bridge-config-row" style="width:100%;padding:8px;margin-top:6px;background:#fff3e0;border:1px dashed #ff9800;border-radius:4px;">';
493
+ html += '<div style="font-size:11px;color:#e65100;margin-bottom:6px;"><i class="fa fa-cog"></i> 中弘VRF配置</div>';
494
+ html += '<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px;">';
495
+ html += '<label style="font-size:10px;">从机地址: <input type="number" class="zh-slave-addr" value="' + (m.zhSlaveAddr || 1) + '" min="0" max="255" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:10px;"></label>';
496
+ html += '<label style="font-size:10px;">外机地址: <input type="number" class="zh-outdoor-addr" value="' + (m.zhOutdoorAddr || 1) + '" min="0" max="255" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:10px;"></label>';
497
+ html += '<label style="font-size:10px;">内机地址: <input type="number" class="zh-indoor-addr" value="' + (m.zhIndoorAddr || 1) + '" min="0" max="255" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:10px;"></label>';
498
+ html += '</div>';
499
+ html += '<div style="font-size:11px;color:#e65100;margin:8px 0 6px 0;"><i class="fa fa-link"></i> 桥接到SYMI面板</div>';
500
+ html += '<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr 1fr;gap:6px;">';
501
+ html += '<label style="font-size:10px;"><input type="checkbox" class="symi-bridge-target" ' + (m.symiBridgeTarget ? 'checked' : '') + '> 启用桥接</label>';
502
+ html += '<label style="font-size:10px;">本机地址: <input type="number" class="symi-local-addr" value="' + (m.symiLocalAddr || 1) + '" min="0" max="255" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:10px;"></label>';
503
+ html += '<label style="font-size:10px;">设备地址: <input type="number" class="symi-device-addr" value="' + (m.symiDeviceAddr || 1) + '" min="0" max="255" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:10px;"></label>';
504
+ html += '<label style="font-size:10px;">设备通道: <input type="number" class="symi-device-channel" value="' + (m.symiDeviceChannel || 0) + '" min="0" max="255" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:10px;"></label>';
505
+ html += '<label style="font-size:10px;">品牌ID: <input type="number" class="symi-brand-id" value="' + (m.symiBrandId || 0) + '" min="0" max="255" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:10px;"></label>';
506
+ html += '</div>';
507
+ html += '</div>';
508
+ row.append(html);
509
+ bindBridgeEvents();
510
+ return;
511
+ }
512
+
375
513
  if (brandId !== 'custom') return;
376
514
 
377
515
  var codes = mappings[idx].customCodes || {};
378
- var html = '<div class="custom-codes-row" style="width:100%;padding:8px;margin-top:6px;background:#fff8e1;border:1px dashed #ffa726;border-radius:4px;">';
379
- html += '<div style="font-size:11px;color:#f57c00;margin-bottom:6px;"><i class="fa fa-code"></i> 自定义RS485码(十六进制,如: 55 01 01 03 01 B9 00)</div>';
516
+ // 内容区域(默认折叠)
517
+ var html = '<div class="custom-codes-row" style="width:100%;">';
518
+ html += '<div class="custom-codes-content" style="display:none;padding:8px;background:#fff8e1;border:1px dashed #ffa726;border-radius:4px;margin-top:4px;">';
380
519
 
381
520
  if (deviceId === 'custom_switch') {
382
521
  html += '<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;">';
@@ -405,13 +544,15 @@
405
544
  html += '</div>';
406
545
 
407
546
  html += '<div style="font-size:10px;color:#f57c00;margin-bottom:4px;">风速控制</div>';
408
- html += '<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:4px;margin-bottom:6px;">';
547
+ html += '<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:4px;margin-bottom:6px;">';
409
548
  html += '<label style="font-size:10px;">发高风: <input type="text" class="custom-fan-send-high" value="' + (codes.fanSendHigh || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
410
549
  html += '<label style="font-size:10px;">发中风: <input type="text" class="custom-fan-send-mid" value="' + (codes.fanSendMid || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
411
550
  html += '<label style="font-size:10px;">发低风: <input type="text" class="custom-fan-send-low" value="' + (codes.fanSendLow || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
551
+ html += '<label style="font-size:10px;">发自动: <input type="text" class="custom-fan-send-auto" value="' + (codes.fanSendAuto || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
412
552
  html += '<label style="font-size:10px;">收高风: <input type="text" class="custom-fan-recv-high" value="' + (codes.fanRecvHigh || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
413
553
  html += '<label style="font-size:10px;">收中风: <input type="text" class="custom-fan-recv-mid" value="' + (codes.fanRecvMid || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
414
554
  html += '<label style="font-size:10px;">收低风: <input type="text" class="custom-fan-recv-low" value="' + (codes.fanRecvLow || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
555
+ html += '<label style="font-size:10px;">收自动: <input type="text" class="custom-fan-recv-auto" value="' + (codes.fanRecvAuto || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
415
556
  html += '</div>';
416
557
 
417
558
  html += '<div style="font-size:10px;color:#f57c00;margin-bottom:4px;">模式控制</div>';
@@ -458,9 +599,44 @@
458
599
  html += '<label style="font-size:11px;">触发码: <input type="text" class="custom-code-trigger" value="' + (codes.trigger || '') + '" placeholder="55 01 01..." style="width:200px;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:11px;font-family:monospace;"></label>';
459
600
  html += '</div>';
460
601
  }
602
+ html += '</div>'; // 关闭 custom-codes-content
461
603
  html += '</div>';
462
604
  row.append(html);
605
+
463
606
  bindEvents();
607
+ bindToggleEvents();
608
+ }
609
+
610
+ // 绑定折叠按钮事件
611
+ function bindToggleEvents() {
612
+ $('#mapping-list').find('.btn-toggle-codes').off('click').on('click', function(e) {
613
+ e.preventDefault();
614
+ e.stopPropagation();
615
+ var row = $(this).closest('.mapping-row');
616
+ var content = row.find('.custom-codes-content');
617
+ var btn = $(this);
618
+ if (content.is(':visible')) {
619
+ content.slideUp(200);
620
+ // 更新按钮文字
621
+ var idx = row.data('idx');
622
+ var m = mappings[idx];
623
+ var codes = m.customCodes || {};
624
+ var summary = '未配置';
625
+ if (m.device === 'custom_switch') {
626
+ summary = (codes.sendOn || codes.recvOn || codes.sendOff || codes.recvOff) ? '已配置' : '未配置';
627
+ } else if (m.device === 'custom_curtain') {
628
+ summary = (codes.sendOpen || codes.sendClose || codes.recvOpen || codes.recvClose) ? '已配置' : '未配置';
629
+ } else if (m.device === 'custom_climate') {
630
+ summary = (codes.acSendOn || codes.fanSendHigh || codes.modeSendCool) ? '已配置' : '未配置';
631
+ } else {
632
+ summary = codes.trigger ? '已配置' : '未配置';
633
+ }
634
+ btn.html('<i class="fa fa-code"></i> ' + summary);
635
+ } else {
636
+ content.slideDown(200);
637
+ btn.html('<i class="fa fa-code"></i> 收起');
638
+ }
639
+ });
464
640
  }
465
641
 
466
642
  // 添加新映射
@@ -493,6 +669,30 @@
493
669
  m.addrHigh = parseInt($(this).find('.addr-high').val()) || 1;
494
670
  m.addrLow = parseInt($(this).find('.addr-low').val()) || 1;
495
671
  }
672
+ // 保存SYMI空调面板配置
673
+ if (m.brand === 'symi') {
674
+ var row = $(this);
675
+ m.symiLocalAddr = parseInt(row.find('.symi-local-addr').val()) || 1;
676
+ m.symiDeviceAddr = parseInt(row.find('.symi-device-addr').val()) || 1;
677
+ m.symiDeviceChannel = parseInt(row.find('.symi-device-channel').val()) || 0;
678
+ m.symiBrandId = parseInt(row.find('.symi-brand-id').val()) || 0;
679
+ m.zhBridgeTarget = row.find('.zh-bridge-target').is(':checked');
680
+ m.zhSlaveAddr = parseInt(row.find('.zh-slave-addr').val()) || 1;
681
+ m.zhOutdoorAddr = parseInt(row.find('.zh-outdoor-addr').val()) || 1;
682
+ m.zhIndoorAddr = parseInt(row.find('.zh-indoor-addr').val()) || 1;
683
+ }
684
+ // 保存中弘VRF配置
685
+ if (m.brand === 'zhonghong') {
686
+ var row = $(this);
687
+ m.zhSlaveAddr = parseInt(row.find('.zh-slave-addr').val()) || 1;
688
+ m.zhOutdoorAddr = parseInt(row.find('.zh-outdoor-addr').val()) || 1;
689
+ m.zhIndoorAddr = parseInt(row.find('.zh-indoor-addr').val()) || 1;
690
+ m.symiBridgeTarget = row.find('.symi-bridge-target').is(':checked');
691
+ m.symiLocalAddr = parseInt(row.find('.symi-local-addr').val()) || 1;
692
+ m.symiDeviceAddr = parseInt(row.find('.symi-device-addr').val()) || 1;
693
+ m.symiDeviceChannel = parseInt(row.find('.symi-device-channel').val()) || 0;
694
+ m.symiBrandId = parseInt(row.find('.symi-brand-id').val()) || 0;
695
+ }
496
696
  // 保存自定义码
497
697
  if (m.brand === 'custom') {
498
698
  var row = $(this);
@@ -577,8 +777,9 @@
577
777
 
578
778
  #mapping-list { max-height: calc(100vh - 380px); min-height: 200px; overflow-y: auto; }
579
779
  .mapping-empty { padding: 15px; text-align: center; color: #999; font-size: 12px; }
580
- .mapping-row { display: flex; align-items: center; padding: 6px 8px; margin-bottom: 6px; background: #fff; border: 1px solid #e0e0e0; border-radius: 4px; gap: 6px; }
581
- .mesh-col { flex: 0 0 25%; display: flex; gap: 4px; }
780
+ .mapping-row { display: flex; flex-direction: column; padding: 6px 8px; margin-bottom: 6px; background: #fff; border: 1px solid #e0e0e0; border-radius: 4px; gap: 6px; }
781
+ .mapping-main { display: flex; align-items: center; width: 100%; gap: 6px; min-width: 0; }
782
+ .mesh-col { flex: 1 1 20%; min-width: 0; display: flex; gap: 4px; }
582
783
  .mesh-col .mesh-select { flex: 1; padding: 4px; border: 1px solid #81c784; border-radius: 3px; background: #e8f5e9; font-size: 12px; }
583
784
  .mesh-col .mesh-channel { width: 58px; padding: 4px; border: 1px solid #81c784; border-radius: 3px; background: #c8e6c9; font-size: 11px; font-weight: bold; }
584
785
  .arrow-col { flex: 0 0 20px; text-align: center; color: #999; }
@@ -590,7 +791,7 @@
590
791
  .device-col .rs485-channel { width: 58px; background: #bbdefb; font-size: 11px; font-weight: bold; }
591
792
  .addr-col { flex: 0 0 50px; }
592
793
  .addr-col input { width: 100%; padding: 4px; border: 1px solid #ccc; border-radius: 3px; text-align: center; font-size: 12px; }
593
- .del-col { flex: 0 0 32px; text-align: center; }
794
+ .del-col { flex: 0 0 auto; display: flex; align-items: center; gap: 4px; margin-left: auto; white-space: nowrap; }
594
795
  .btn-remove { color: #d32f2f !important; padding: 2px 6px !important; }
595
796
  .form-tips { font-size: 11px; }
596
797
  .form-tips p { margin: 3px 0; }