node-red-contrib-my-tools 1.0.0
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.
- package/README.txt +69 -0
- package/node-red-contrib-mysql-extended-test/mysql-extended.html +443 -0
- package/node-red-contrib-mysql-extended-test/mysql-extended.js +355 -0
- package/node-red-contrib-rfid-helper/rfid-lazy.html +153 -0
- package/node-red-contrib-rfid-helper/rfid-lazy.js +361 -0
- package/node-red-contrib-s7-plus/LICENSE +235 -0
- package/node-red-contrib-s7-plus/README.md +141 -0
- package/node-red-contrib-s7-plus/examples/read-multiple-values-flow.json +346 -0
- package/node-red-contrib-s7-plus/examples/read-write-test-flow.json +172 -0
- package/node-red-contrib-s7-plus/examples/write-single-values-flow.json +5583 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/README.md +72 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/areas.js +36 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/datatypes.js +91 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/flat-browser.js +433 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/index.js +22 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/lazy.js +397 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/node-id.js +22 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/resolve-symbolic.js +181 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/vte-helpers.js +49 -0
- package/node-red-contrib-s7-plus/lib/s7plus/buffer-stream.js +96 -0
- package/node-red-contrib-s7-plus/lib/s7plus/client.js +1238 -0
- package/node-red-contrib-s7-plus/lib/s7plus/constants.js +155 -0
- package/node-red-contrib-s7-plus/lib/s7plus/crc/index.js +3 -0
- package/node-red-contrib-s7-plus/lib/s7plus/crc/s7-crc32.js +70 -0
- package/node-red-contrib-s7-plus/lib/s7plus/crc/symbol-crc.js +271 -0
- package/node-red-contrib-s7-plus/lib/s7plus/debug.js +88 -0
- package/node-red-contrib-s7-plus/lib/s7plus/explore-result.js +41 -0
- package/node-red-contrib-s7-plus/lib/s7plus/item-address.js +55 -0
- package/node-red-contrib-s7-plus/lib/s7plus/pdu-explore.js +77 -0
- package/node-red-contrib-s7-plus/lib/s7plus/pdu-messages.js +359 -0
- package/node-red-contrib-s7-plus/lib/s7plus/pobject.js +197 -0
- package/node-red-contrib-s7-plus/lib/s7plus/pvalue-codec.js +435 -0
- package/node-red-contrib-s7-plus/lib/s7plus/pvalue.js +653 -0
- package/node-red-contrib-s7-plus/lib/s7plus/pvar-lists.js +336 -0
- package/node-red-contrib-s7-plus/lib/s7plus/read-result.js +91 -0
- package/node-red-contrib-s7-plus/lib/s7plus/s7p.js +266 -0
- package/node-red-contrib-s7-plus/lib/s7plus/tag-routing.js +30 -0
- package/node-red-contrib-s7-plus/lib/s7plus/transport/cotp-stream.js +335 -0
- package/node-red-contrib-s7-plus/lib/s7plus/transport/s7-transport.js +335 -0
- package/node-red-contrib-s7-plus/nodes/icons/s7complus.png +0 -0
- package/node-red-contrib-s7-plus/nodes/icons/s7complus.svg +17 -0
- package/node-red-contrib-s7-plus/nodes/s7complus-endpoint.js +825 -0
- package/node-red-contrib-s7-plus/nodes/s7complus-explore.js +91 -0
- package/node-red-contrib-s7-plus/nodes/s7complus-in.js +255 -0
- package/node-red-contrib-s7-plus/nodes/s7complus-out.js +182 -0
- package/node-red-contrib-s7-plus/nodes/s7complus.html +1541 -0
- package/node-red-contrib-s7-plus/nodes/s7complus.js +10 -0
- package/node-red-contrib-s7-plus/package.json +50 -0
- package/node-red-contrib-s7-plus/plc/s7-1500/DB_Arrays.db +17 -0
- package/node-red-contrib-s7-plus/plc/s7-1500/DB_Binary.db +16 -0
- package/node-red-contrib-s7-plus/plc/s7-1500/DB_BitStrings.db +36 -0
- package/node-red-contrib-s7-plus/plc/s7-1500/DB_CharacterStrings.db +42 -0
- package/node-red-contrib-s7-plus/plc/s7-1500/DB_DateAndTime.db +70 -0
- package/node-red-contrib-s7-plus/plc/s7-1500/DB_FloatingPoint.db +42 -0
- package/node-red-contrib-s7-plus/plc/s7-1500/DB_Integers.db +72 -0
- package/node-red-contrib-s7-plus/plc/s7-1500/DB_Timers.db +40 -0
- package/node-red-contrib-s7-plus/scripts/example-flow-shared.js +39 -0
- package/node-red-contrib-s7-plus/scripts/generate-read-multiple-flow.js +331 -0
- package/node-red-contrib-s7-plus/scripts/generate-rw-test-flow.js +690 -0
- package/node-red-contrib-s7-plus/scripts/generate-write-flow.js +476 -0
- package/node-red-contrib-s7-plus/scripts/layout-write-flow.js +156 -0
- package/node-red-contrib-s7-plus/scripts/run-tests.js +16 -0
- package/node-red-flowgobal-change/variable-change.html +425 -0
- package/node-red-flowgobal-change/variable-change.js +92 -0
- package/node-red-ui_media_viewer/ui_media_viewer.html +87 -0
- package/node-red-ui_media_viewer/ui_media_viewer.js +136 -0
- package/package.json +12 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('variable-change',{
|
|
3
|
+
category: 'mytools',
|
|
4
|
+
color: '#6699cc',
|
|
5
|
+
defaults: {
|
|
6
|
+
name: {value:""},
|
|
7
|
+
mode: {value:"realtime"},
|
|
8
|
+
initData: {value:"[]"}
|
|
9
|
+
},
|
|
10
|
+
inputs:1,
|
|
11
|
+
outputs:1,
|
|
12
|
+
icon: "arrow-in-out.png",
|
|
13
|
+
label: function() {
|
|
14
|
+
return this.name || "全自動變數管理";
|
|
15
|
+
},
|
|
16
|
+
oneditprepare: function() {
|
|
17
|
+
var node = this;
|
|
18
|
+
|
|
19
|
+
// 人性化 JSON 轉譯器 (允許單引號、無引號 key、結尾逗號等寬鬆寫法)
|
|
20
|
+
function parseRelaxedJSON(str) {
|
|
21
|
+
if (!str || str.trim() === "") return undefined;
|
|
22
|
+
return (new Function("return " + str.trim()))();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 建立列的 HTML (加入拖曳手把與直觀按鈕)
|
|
26
|
+
function createRowHtml(v, scope) {
|
|
27
|
+
var displayVal = (typeof v.value === 'object') ? JSON.stringify(v.value) : v.value;
|
|
28
|
+
if(displayVal === undefined) displayVal = "";
|
|
29
|
+
var isJson = (v.type === 'object' || v.type === 'json');
|
|
30
|
+
var currentMode = $("#node-input-mode").val();
|
|
31
|
+
|
|
32
|
+
// 按鈕文字依據模式改變
|
|
33
|
+
var saveText = currentMode === 'init' ? "✓ 確認" : "💾 寫入";
|
|
34
|
+
var saveColor = currentMode === 'init' ? "#2196f3" : "#4caf50";
|
|
35
|
+
|
|
36
|
+
return `
|
|
37
|
+
<tr class="var-main-row" data-scope="${scope}" style="border-bottom: 1px solid #ddd; background: #fff;">
|
|
38
|
+
<td class="drag-handle" style="padding:6px; cursor:grab; text-align:center; color:#999;" title="按住拖曳排序"><i class="fa fa-bars"></i></td>
|
|
39
|
+
|
|
40
|
+
<td style="padding:6px;"><span class="red-ui-node-input-type" style="background:#eee; padding:2px 6px; border-radius:3px;">${scope.toUpperCase()}</span></td>
|
|
41
|
+
<td style="padding:6px;"><input type="text" class="var-key-input" value="${v.key}" data-original="${v.key}" style="width:90%;"></td>
|
|
42
|
+
<td style="padding:6px; display:flex; align-items:center;">
|
|
43
|
+
<input type="text" class="var-val-input" value='${displayVal}' style="width:calc(100% - 35px);">
|
|
44
|
+
<button type="button" class="btn btn-small edit-json-btn" title="放大編輯 JSON/陣列" style="display:${isJson ? 'inline-block' : 'none'}; margin-left:5px;"><i class="fa fa-expand"></i></button>
|
|
45
|
+
</td>
|
|
46
|
+
<td style="padding:6px;">
|
|
47
|
+
<select class="var-type-select" style="width:90px;">
|
|
48
|
+
<option value="string" ${v.type==='string'?'selected':''}>String</option>
|
|
49
|
+
<option value="number" ${v.type==='number'?'selected':''}>Number</option>
|
|
50
|
+
<option value="boolean" ${v.type==='boolean'?'selected':''}>Boolean</option>
|
|
51
|
+
<option value="json" ${isJson?'selected':''}>JSON</option>
|
|
52
|
+
</select>
|
|
53
|
+
</td>
|
|
54
|
+
<td style="padding:6px; text-align:center;">
|
|
55
|
+
<button type="button" class="btn btn-small save-var-btn" style="background:${saveColor}; color:white; border:none; border-radius:4px; padding:4px 8px; margin-right:4px;">${saveText}</button>
|
|
56
|
+
<button type="button" class="btn btn-small del-var-btn" style="background:#f44336; color:white; border:none; border-radius:4px; padding:4px 8px;"><i class="fa fa-trash"></i> 刪除</button>
|
|
57
|
+
</td>
|
|
58
|
+
</tr>
|
|
59
|
+
`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function refreshVariables() {
|
|
63
|
+
var currentMode = $("#node-input-mode").val();
|
|
64
|
+
$("#var-table-body").html('<tr><td colspan="6" style="text-align:center;">正在載入資料...</td></tr>');
|
|
65
|
+
|
|
66
|
+
if (currentMode === 'init') {
|
|
67
|
+
$("#init-mode-warning").show();
|
|
68
|
+
$("#import-backend-btn").show(); // 顯示手動匯入按鈕
|
|
69
|
+
} else {
|
|
70
|
+
$("#init-mode-warning").hide();
|
|
71
|
+
$("#import-backend-btn").hide();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
$.getJSON('variable-change/all', { nodeId: node.id }, function(data) {
|
|
75
|
+
var container = $("#var-table-body").empty();
|
|
76
|
+
var displayVars = [];
|
|
77
|
+
|
|
78
|
+
if (currentMode === 'init') {
|
|
79
|
+
// 【初始化模式】:嚴格隔離!只讀取之前存好的 initData,不再自動匯入。
|
|
80
|
+
try { displayVars = JSON.parse(node.initData || "[]"); } catch(e) {}
|
|
81
|
+
} else {
|
|
82
|
+
// 【即時模式】:自動讀取後台
|
|
83
|
+
displayVars = data.flow.concat(data.global);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
displayVars.forEach(function(v) {
|
|
87
|
+
container.append(createRowHtml(v, v.scope));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if(displayVars.length === 0) {
|
|
91
|
+
container.html('<tr><td colspan="6" style="text-align:center; color:#999; padding:15px;">目前沒有變數,請點擊上方按鈕新增!</td></tr>');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 啟用拖曳排序
|
|
95
|
+
if ($.fn.sortable) {
|
|
96
|
+
$("#var-table-body").sortable({
|
|
97
|
+
handle: ".drag-handle",
|
|
98
|
+
axis: "y",
|
|
99
|
+
cursor: "grabbing",
|
|
100
|
+
opacity: 0.8
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}).fail(function(xhr) {
|
|
104
|
+
$("#var-table-body").html(`<tr><td colspan="6" style="text-align:center; color:#ff9800; padding:15px;">
|
|
105
|
+
<strong>提示:</strong> 請先點擊右上角的 <b>Deploy (部署)</b> 此節點,後台系統才能與之連線並讀取變數!
|
|
106
|
+
</td></tr>`);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
refreshVariables();
|
|
111
|
+
|
|
112
|
+
$("#node-input-mode").change(function() { refreshVariables(); });
|
|
113
|
+
|
|
114
|
+
// 監聽:手動從後台匯入現有變數 (僅初始化模式)
|
|
115
|
+
$("#import-backend-btn").click(function() {
|
|
116
|
+
if(confirm("這將會把系統當前所有的 Flow/Global 變數匯入到清單的最下方,確定匯入嗎?")) {
|
|
117
|
+
$.getJSON('variable-change/all', { nodeId: node.id }, function(data) {
|
|
118
|
+
var realTimeVars = data.flow.concat(data.global);
|
|
119
|
+
var existingKeys = [];
|
|
120
|
+
$("#var-table-body .var-main-row").each(function() {
|
|
121
|
+
existingKeys.push($(this).data('scope') + '-' + $(this).find('.var-key-input').val());
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
var added = 0;
|
|
125
|
+
realTimeVars.forEach(function(rt) {
|
|
126
|
+
if (!existingKeys.includes(rt.scope + '-' + rt.key)) {
|
|
127
|
+
$("#var-table-body").append(createRowHtml(rt, rt.scope));
|
|
128
|
+
added++;
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
if (added > 0) RED.notify(`已匯入 ${added} 個新變數,請記得確認修改。`, "success");
|
|
132
|
+
else RED.notify("沒有發現不在清單中的新變數。", "info");
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// ---------------- [ 人性化 JSON 展開編輯邏輯 ] ----------------
|
|
138
|
+
$(document).on('change', '.var-type-select, .new-var-type', function() {
|
|
139
|
+
var btn = $(this).closest('tr').find('.edit-json-btn');
|
|
140
|
+
if ($(this).val() === 'json') btn.show();
|
|
141
|
+
else { btn.hide(); $(this).closest('tr').next('.json-editor-row').remove(); }
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
$(document).off('click', '.edit-json-btn').on('click', '.edit-json-btn', function() {
|
|
145
|
+
var tr = $(this).closest('tr');
|
|
146
|
+
if (tr.next().hasClass('json-editor-row')) { tr.next().remove(); return; }
|
|
147
|
+
|
|
148
|
+
var currentVal = tr.find('.var-val-input').val();
|
|
149
|
+
var formattedJson = currentVal;
|
|
150
|
+
try {
|
|
151
|
+
// 人性化讀取:允許非標準 JSON
|
|
152
|
+
var obj = parseRelaxedJSON(currentVal);
|
|
153
|
+
if(obj !== undefined) {
|
|
154
|
+
formattedJson = JSON.stringify(obj, null, 4); // 轉成標準格式顯示
|
|
155
|
+
} else {
|
|
156
|
+
formattedJson = "[\n \n]";
|
|
157
|
+
}
|
|
158
|
+
} catch(e) {}
|
|
159
|
+
|
|
160
|
+
var editorRow = $(`
|
|
161
|
+
<tr class="json-editor-row" style="background:#f9f9f9; border-bottom:2px solid #ccc;">
|
|
162
|
+
<td colspan="6" style="padding:10px 15px;">
|
|
163
|
+
<div style="display:flex; justify-content:space-between; margin-bottom:8px; align-items:center;">
|
|
164
|
+
<strong style="color:#333;"><i class="fa fa-code"></i> JSON / 陣列人性化編輯器 (自動糾正格式)</strong>
|
|
165
|
+
<button type="button" class="btn btn-small add-array-item-btn" style="background:#2196f3; color:white; border:none; padding:4px 10px;">
|
|
166
|
+
<i class="fa fa-plus"></i> 複製前筆格式
|
|
167
|
+
</button>
|
|
168
|
+
</div>
|
|
169
|
+
<textarea class="json-edit-textarea" placeholder="在此處編輯。支援寬鬆格式 (如單引號、無引號 key),儲存時會自動轉化為標準 JSON。" style="width:100%; height:200px; font-family:monospace; font-size:14px; padding:10px; border:1px solid #aaa; border-radius:4px; resize:vertical;">${formattedJson}</textarea>
|
|
170
|
+
<div style="text-align:right; margin-top:8px;">
|
|
171
|
+
<button type="button" class="btn btn-small apply-json-btn" style="background:#4caf50; color:white; border:none; padding:5px 15px;"><i class="fa fa-check"></i> 確認修改並格式化</button>
|
|
172
|
+
</div>
|
|
173
|
+
</td>
|
|
174
|
+
</tr>
|
|
175
|
+
`);
|
|
176
|
+
tr.after(editorRow);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// 儲存編輯器:寬鬆轉標準
|
|
180
|
+
$(document).off('click', '.apply-json-btn').on('click', '.apply-json-btn', function() {
|
|
181
|
+
var editorRow = $(this).closest('.json-editor-row');
|
|
182
|
+
var textareaVal = editorRow.find('.json-edit-textarea').val();
|
|
183
|
+
try {
|
|
184
|
+
var obj = parseRelaxedJSON(textareaVal); // 容錯解析
|
|
185
|
+
var minified = JSON.stringify(obj); // 轉為嚴格單行 JSON
|
|
186
|
+
var mainRow = editorRow.prev('.var-main-row');
|
|
187
|
+
mainRow.find('.var-val-input').val(minified);
|
|
188
|
+
editorRow.remove();
|
|
189
|
+
} catch(e) {
|
|
190
|
+
alert("語法解析嚴重錯誤,請檢查陣列/物件括號是否成對!\n錯誤: " + e.message);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// 複製前筆格式
|
|
195
|
+
$(document).off('click', '.add-array-item-btn').on('click', '.add-array-item-btn', function() {
|
|
196
|
+
var textarea = $(this).closest('td').find('.json-edit-textarea');
|
|
197
|
+
try {
|
|
198
|
+
var arr = parseRelaxedJSON(textarea.val()); // 容錯解析
|
|
199
|
+
if (!Array.isArray(arr)) { alert("目前最外層不是陣列 [ ],無法新增陣列項目!"); return; }
|
|
200
|
+
if (arr.length === 0) arr.push({});
|
|
201
|
+
else {
|
|
202
|
+
var lastItem = arr[arr.length - 1];
|
|
203
|
+
if (typeof lastItem === 'object' && lastItem !== null && !Array.isArray(lastItem)) {
|
|
204
|
+
var newItem = {};
|
|
205
|
+
for (var key in lastItem) {
|
|
206
|
+
var valType = typeof lastItem[key];
|
|
207
|
+
if (valType === 'string') newItem[key] = "";
|
|
208
|
+
else if (valType === 'number') newItem[key] = 0;
|
|
209
|
+
else if (valType === 'boolean') newItem[key] = false;
|
|
210
|
+
else if (Array.isArray(lastItem[key])) newItem[key] = [];
|
|
211
|
+
else newItem[key] = null;
|
|
212
|
+
}
|
|
213
|
+
arr.push(newItem);
|
|
214
|
+
} else if (Array.isArray(lastItem)) arr.push([]);
|
|
215
|
+
else arr.push(typeof lastItem === 'string' ? "" : (typeof lastItem === 'number' ? 0 : false));
|
|
216
|
+
}
|
|
217
|
+
textarea.val(JSON.stringify(arr, null, 4));
|
|
218
|
+
textarea.scrollTop(textarea[0].scrollHeight);
|
|
219
|
+
} catch(e) { alert("文字區塊格式損毀過於嚴重,請先手動修復語法錯誤。"); }
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// ---------------- [ 表格按鈕邏輯 ] ----------------
|
|
223
|
+
$("#add-var-btn").click(function() {
|
|
224
|
+
var newRow = $(`
|
|
225
|
+
<tr class="var-main-row new-var-row" style="border-bottom: 1px solid #ddd; background:#f9fff9;">
|
|
226
|
+
<td class="drag-handle" style="padding:6px; cursor:grab; text-align:center; color:#999;"><i class="fa fa-bars"></i></td>
|
|
227
|
+
<td style="padding:6px;">
|
|
228
|
+
<select class="new-var-scope" style="width:85px;">
|
|
229
|
+
<option value="flow">FLOW</option>
|
|
230
|
+
<option value="global">GLOBAL</option>
|
|
231
|
+
</select>
|
|
232
|
+
</td>
|
|
233
|
+
<td style="padding:6px;"><input type="text" class="var-key-input new-var-key" placeholder="輸入變數名稱" style="width:90%;"></td>
|
|
234
|
+
<td style="padding:6px; display:flex; align-items:center;">
|
|
235
|
+
<input type="text" class="var-val-input new-var-val" placeholder="初始值" style="width:calc(100% - 35px);">
|
|
236
|
+
<button type="button" class="btn btn-small edit-json-btn" title="放大編輯 JSON/陣列" style="display:none; margin-left:5px;"><i class="fa fa-expand"></i></button>
|
|
237
|
+
</td>
|
|
238
|
+
<td style="padding:6px;">
|
|
239
|
+
<select class="var-type-select new-var-type" style="width:90px;">
|
|
240
|
+
<option value="string">String</option>
|
|
241
|
+
<option value="number">Number</option>
|
|
242
|
+
<option value="boolean">Boolean</option>
|
|
243
|
+
<option value="json">JSON</option>
|
|
244
|
+
</select>
|
|
245
|
+
</td>
|
|
246
|
+
<td style="padding:6px; text-align:center;">
|
|
247
|
+
<button type="button" class="btn btn-small submit-new-btn" style="background:#2196f3; color:white; border:none; border-radius:4px; padding:4px 8px; margin-right:4px;">✓ 確認</button>
|
|
248
|
+
<button type="button" class="btn btn-small cancel-new-btn" style="background:#bbb; color:white; border:none; border-radius:4px; padding:4px 8px;"><i class="fa fa-times"></i></button>
|
|
249
|
+
</td>
|
|
250
|
+
</tr>
|
|
251
|
+
`);
|
|
252
|
+
$("#var-table-body").append(newRow);
|
|
253
|
+
|
|
254
|
+
// 新增列後重新啟動拖曳
|
|
255
|
+
if ($.fn.sortable) $("#var-table-body").sortable('refresh');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
$(document).off('click', '.save-var-btn').on('click', '.save-var-btn', function() {
|
|
259
|
+
var currentMode = $("#node-input-mode").val();
|
|
260
|
+
var row = $(this).closest('tr');
|
|
261
|
+
var scope = row.data('scope');
|
|
262
|
+
var oldKey = row.find('.var-key-input').data('original');
|
|
263
|
+
var newKey = row.find('.var-key-input').val().trim();
|
|
264
|
+
var value = row.find('.var-val-input').val();
|
|
265
|
+
var type = row.find('.var-type-select').val();
|
|
266
|
+
|
|
267
|
+
if (!newKey) { alert("變數名稱不能為空!"); return; }
|
|
268
|
+
|
|
269
|
+
// 人性化解析:如果選 JSON,把寬鬆輸入轉成標準 string 確保安全寫入
|
|
270
|
+
if (type === 'json') {
|
|
271
|
+
try {
|
|
272
|
+
var obj = parseRelaxedJSON(value);
|
|
273
|
+
value = JSON.stringify(obj);
|
|
274
|
+
row.find('.var-val-input').val(value); // 畫面上也更新為標準格式
|
|
275
|
+
} catch(e) {
|
|
276
|
+
alert("JSON 語法解析錯誤!\n" + e.message); return;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (currentMode === 'init') {
|
|
281
|
+
row.find('.var-key-input').data('original', newKey);
|
|
282
|
+
RED.notify("已確認變更(請記得點擊 Node-RED 右上角部署)", "info");
|
|
283
|
+
} else {
|
|
284
|
+
$.post('variable-change/save', { nodeId: node.id, scope: scope, oldKey: oldKey, key: newKey, value: value, type: type }, function() {
|
|
285
|
+
RED.notify("即時寫入後台成功!", "success");
|
|
286
|
+
row.find('.var-key-input').data('original', newKey);
|
|
287
|
+
}).fail(function(err) { alert("寫入失敗: " + err.responseText); });
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
$(document).off('click', '.del-var-btn').on('click', '.del-var-btn', function() {
|
|
292
|
+
var currentMode = $("#node-input-mode").val();
|
|
293
|
+
var row = $(this).closest('tr');
|
|
294
|
+
var scope = row.data('scope');
|
|
295
|
+
var key = row.find('.var-key-input').data('original');
|
|
296
|
+
|
|
297
|
+
if (currentMode === 'init') {
|
|
298
|
+
row.next('.json-editor-row').remove();
|
|
299
|
+
row.remove();
|
|
300
|
+
} else {
|
|
301
|
+
if (confirm(`確定要完全從系統中刪除 ${scope.toUpperCase()} 變數 [ ${key} ] 嗎?`)) {
|
|
302
|
+
$.post('variable-change/delete', { nodeId: node.id, scope: scope, key: key }, function() {
|
|
303
|
+
RED.notify("變數已即時刪除", "success");
|
|
304
|
+
row.next('.json-editor-row').remove();
|
|
305
|
+
row.remove();
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
$(document).off('click', '.submit-new-btn').on('click', '.submit-new-btn', function() {
|
|
312
|
+
var currentMode = $("#node-input-mode").val();
|
|
313
|
+
var row = $(this).closest('tr');
|
|
314
|
+
var scope = row.find('.new-var-scope').val();
|
|
315
|
+
var key = row.find('.new-var-key').val().trim();
|
|
316
|
+
var value = row.find('.new-var-val').val();
|
|
317
|
+
var type = row.find('.new-var-type').val();
|
|
318
|
+
|
|
319
|
+
if (!key) { alert("請輸入變数名稱!"); return; }
|
|
320
|
+
|
|
321
|
+
if (type === 'json') {
|
|
322
|
+
try {
|
|
323
|
+
var obj = parseRelaxedJSON(value);
|
|
324
|
+
value = JSON.stringify(obj);
|
|
325
|
+
} catch(e) {
|
|
326
|
+
alert("JSON 語法解析錯誤!\n" + e.message); return;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (currentMode === 'init') {
|
|
331
|
+
var newMainRow = createRowHtml({key: key, value: value, type: type}, scope);
|
|
332
|
+
row.replaceWith(newMainRow);
|
|
333
|
+
} else {
|
|
334
|
+
$.post('variable-change/save', { nodeId: node.id, scope: scope, oldKey: null, key: key, value: value, type: type }, function() {
|
|
335
|
+
RED.notify("新變數已即時寫入!", "success");
|
|
336
|
+
var newMainRow = createRowHtml({key: key, value: value, type: type}, scope);
|
|
337
|
+
row.replaceWith(newMainRow);
|
|
338
|
+
}).fail(function(err) { alert("寫入失敗: " + err.responseText); });
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
$(document).off('click', '.cancel-new-btn').on('click', '.cancel-new-btn', function() {
|
|
343
|
+
$(this).closest('tr').remove();
|
|
344
|
+
});
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
// ---------------- [ Node-RED 核心儲存事件 ] ----------------
|
|
348
|
+
oneditsave: function() {
|
|
349
|
+
var node = this;
|
|
350
|
+
var mode = $("#node-input-mode").val();
|
|
351
|
+
|
|
352
|
+
// 將「畫面上目前排序好」的內容搜集起來,存入 Node 設定中
|
|
353
|
+
if (mode === 'init') {
|
|
354
|
+
var initArr = [];
|
|
355
|
+
$("#var-table-body .var-main-row").each(function() {
|
|
356
|
+
var scope = $(this).attr('data-scope');
|
|
357
|
+
var key = $(this).find('.var-key-input').val().trim();
|
|
358
|
+
var value = $(this).find('.var-val-input').val();
|
|
359
|
+
var type = $(this).find('.var-type-select').val();
|
|
360
|
+
if (key) {
|
|
361
|
+
initArr.push({scope: scope, key: key, value: value, type: type});
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
node.initData = JSON.stringify(initArr);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
</script>
|
|
369
|
+
|
|
370
|
+
<script type="text/html" data-template-name="variable-change">
|
|
371
|
+
<div class="form-row">
|
|
372
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> 節點名稱</label>
|
|
373
|
+
<input type="text" id="node-input-name" placeholder="名稱">
|
|
374
|
+
</div>
|
|
375
|
+
|
|
376
|
+
<div class="form-row">
|
|
377
|
+
<label for="node-input-mode"><i class="fa fa-sliders"></i> 運作模式</label>
|
|
378
|
+
<select id="node-input-mode" style="width:70%;">
|
|
379
|
+
<option value="realtime">即時調整模式 (直接改動後台變數)</option>
|
|
380
|
+
<option value="init">初始化模式 (接收 Input 時再賦予值)</option>
|
|
381
|
+
</select>
|
|
382
|
+
</div>
|
|
383
|
+
<hr>
|
|
384
|
+
|
|
385
|
+
<div id="init-mode-warning" style="display:none; background:#e8f4fd; color:#0d47a1; padding:10px; margin-bottom:15px; border-radius:4px; border:1px solid #b3d4fc;">
|
|
386
|
+
<i class="fa fa-info-circle"></i> <b>初始化嚴格模式:</b>
|
|
387
|
+
目前只會顯示並載入您在此設定過的變數(其他未設定的變數不會被自動帶入)。<br>
|
|
388
|
+
修改完畢後,請點擊外側的「完成」與「部署」,當流程進入此節點時才會寫入變數。
|
|
389
|
+
</div>
|
|
390
|
+
|
|
391
|
+
<div class="form-row" style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px;">
|
|
392
|
+
<h4 style="margin:0;"><i class="fa fa-database"></i> 變數管理清單</h4>
|
|
393
|
+
<div>
|
|
394
|
+
<button type="button" class="btn" id="import-backend-btn" style="display:none; background:#ff9800; color:white; border:none; padding:5px 10px; margin-right:5px;"><i class="fa fa-download"></i> 匯入現有變數</button>
|
|
395
|
+
<button type="button" class="btn" id="add-var-btn" style="background:#6699cc; color:white; border:none; padding:5px 10px;"><i class="fa fa-plus"></i> 新增變數</button>
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
<div class="form-row">
|
|
399
|
+
<table style="width:100%; border: 1px solid #ccc; border-collapse:collapse; font-size:13px;">
|
|
400
|
+
<thead>
|
|
401
|
+
<tr style="background:#f3f3f3; text-align:left; border-bottom: 2px solid #ddd;">
|
|
402
|
+
<th style="padding:8px; width:5%; text-align:center;"><i class="fa fa-arrows-v"></i></th>
|
|
403
|
+
<th style="padding:8px; width:10%;">範圍</th>
|
|
404
|
+
<th style="padding:8px; width:22%;">變數名稱 (Key)</th>
|
|
405
|
+
<th style="padding:8px; width:33%;">數值 (Value)</th>
|
|
406
|
+
<th style="padding:8px; width:12%;">型態</th>
|
|
407
|
+
<th style="padding:8px; width:18%; text-align:center;">操作</th>
|
|
408
|
+
</tr>
|
|
409
|
+
</thead>
|
|
410
|
+
<tbody id="var-table-body">
|
|
411
|
+
</tbody>
|
|
412
|
+
</table>
|
|
413
|
+
</div>
|
|
414
|
+
</script>
|
|
415
|
+
|
|
416
|
+
<script type="text/html" data-help-name="variable-change">
|
|
417
|
+
<p>支援進階陣列與 JSON 修改的視覺化變數管理器。</p>
|
|
418
|
+
|
|
419
|
+
<h3>💡 本次升級特色</h3>
|
|
420
|
+
<ul>
|
|
421
|
+
<li><b>拖曳排序:</b> 滑鼠按住最左側的三條線圖示 <i class="fa fa-bars"></i>,即可上下拖拉改變變數順序!系統會自動記憶你當前的順序。</li>
|
|
422
|
+
<li><b>初始化隔離:</b> 在初始化模式中,你刪除的變數絕對不會再幽靈般跑回來。若需要,可以點擊上方的「匯入現有變數」手動補充。</li>
|
|
423
|
+
<li><b>人性化 JSON:</b> 不必再寫死板的標準 JSON!你可以直接輸入 <code>[{name: 'Tom', age: 20}]</code>(無雙引號),按下寫入或確認後,系統會自動轉譯為嚴格標準格式。</li>
|
|
424
|
+
</ul>
|
|
425
|
+
</script>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
function VariableChangeNode(config) {
|
|
3
|
+
RED.nodes.createNode(this, config);
|
|
4
|
+
var node = this;
|
|
5
|
+
|
|
6
|
+
node.mode = config.mode || "realtime";
|
|
7
|
+
node.initData = config.initData || "[]";
|
|
8
|
+
|
|
9
|
+
node.on('input', function(msg) {
|
|
10
|
+
if (node.mode === 'init') {
|
|
11
|
+
try {
|
|
12
|
+
var initVars = JSON.parse(node.initData);
|
|
13
|
+
initVars.forEach(function(item) {
|
|
14
|
+
var parsedValue = item.value;
|
|
15
|
+
if (item.type === "number") parsedValue = Number(item.value);
|
|
16
|
+
if (item.type === "boolean") parsedValue = (item.value === "true" || item.value === true);
|
|
17
|
+
if (item.type === "json") {
|
|
18
|
+
try { parsedValue = JSON.parse(item.value); } catch(e) {}
|
|
19
|
+
}
|
|
20
|
+
node.context()[item.scope].set(item.key, parsedValue);
|
|
21
|
+
});
|
|
22
|
+
} catch(e) {
|
|
23
|
+
node.error("初始化資料解析錯誤: " + e.message, msg);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
node.send(msg);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
RED.nodes.registerType("variable-change", VariableChangeNode);
|
|
30
|
+
|
|
31
|
+
RED.httpAdmin.get("/variable-change/all", RED.auth.needsPermission('settings.read'), function(req, res) {
|
|
32
|
+
var nodeId = req.query.nodeId;
|
|
33
|
+
var targetNode = RED.nodes.getNode(nodeId);
|
|
34
|
+
|
|
35
|
+
if (!targetNode) return res.status(404).send("請先部署節點以啟用自動讀取功能。");
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
var flowCtx = targetNode.context().flow;
|
|
39
|
+
var globalCtx = targetNode.context().global;
|
|
40
|
+
|
|
41
|
+
var flowVars = (flowCtx.keys() || []).map(k => ({ scope: 'flow', key: k, value: flowCtx.get(k), type: typeof flowCtx.get(k) }));
|
|
42
|
+
var globalVars = (globalCtx.keys() || []).map(k => ({ scope: 'global', key: k, value: globalCtx.get(k), type: typeof globalCtx.get(k) }));
|
|
43
|
+
|
|
44
|
+
res.json({ flow: flowVars, global: globalVars });
|
|
45
|
+
} catch(err) {
|
|
46
|
+
res.status(500).send(err.toString());
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
RED.httpAdmin.post("/variable-change/save", RED.auth.needsPermission('settings.write'), function(req, res) {
|
|
51
|
+
var nodeId = req.body.nodeId;
|
|
52
|
+
var scope = req.body.scope;
|
|
53
|
+
var oldKey = req.body.oldKey;
|
|
54
|
+
var newKey = req.body.key;
|
|
55
|
+
var value = req.body.value;
|
|
56
|
+
var type = req.body.type;
|
|
57
|
+
|
|
58
|
+
var targetNode = RED.nodes.getNode(nodeId);
|
|
59
|
+
if (!targetNode) return res.status(404).send("找不到節點實例。");
|
|
60
|
+
|
|
61
|
+
var parsedValue = value;
|
|
62
|
+
if (type === "number") parsedValue = Number(value);
|
|
63
|
+
if (type === "boolean") parsedValue = (value === "true" || value === true);
|
|
64
|
+
if (type === "json") {
|
|
65
|
+
// 前端已經轉換為標準格式,這裡直接 parse 即可
|
|
66
|
+
try { parsedValue = JSON.parse(value); } catch(e) { return res.status(400).send("JSON 格式錯誤"); }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
var ctx = targetNode.context()[scope];
|
|
71
|
+
if (oldKey && oldKey !== newKey) ctx.set(oldKey, undefined);
|
|
72
|
+
ctx.set(newKey, parsedValue);
|
|
73
|
+
res.sendStatus(200);
|
|
74
|
+
} catch(err) {
|
|
75
|
+
res.status(500).send(err.toString());
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
RED.httpAdmin.post("/variable-change/delete", RED.auth.needsPermission('settings.write'), function(req, res) {
|
|
80
|
+
var nodeId = req.body.nodeId;
|
|
81
|
+
var scope = req.body.scope;
|
|
82
|
+
var key = req.body.key;
|
|
83
|
+
|
|
84
|
+
var targetNode = RED.nodes.getNode(nodeId);
|
|
85
|
+
if (targetNode) {
|
|
86
|
+
targetNode.context()[scope].set(key, undefined);
|
|
87
|
+
res.sendStatus(200);
|
|
88
|
+
} else {
|
|
89
|
+
res.status(404).send("找不到節點");
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('ui_media_viewer', {
|
|
3
|
+
category: 'mytools',
|
|
4
|
+
color: 'rgb(119, 198, 204)',
|
|
5
|
+
defaults: {
|
|
6
|
+
group: { type: 'ui_group', required: true },
|
|
7
|
+
name: { value: '' },
|
|
8
|
+
order: { value: 0 },
|
|
9
|
+
width: {
|
|
10
|
+
value: 0, validate: function (v) {
|
|
11
|
+
var width = v || 0;
|
|
12
|
+
var currentGroup = $('#node-input-group').val() || this.group;
|
|
13
|
+
var groupNode = RED.nodes.node(currentGroup);
|
|
14
|
+
var valid = !groupNode || +width <= +groupNode.width;
|
|
15
|
+
$("#node-input-size").toggleClass("input-error", !valid);
|
|
16
|
+
return valid;
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
height: { value: 0 },
|
|
20
|
+
filepath: { value: '', required: false },
|
|
21
|
+
mediaType: { value: 'image' },
|
|
22
|
+
displayWidth: { value: '100%' }
|
|
23
|
+
},
|
|
24
|
+
inputs: 1,
|
|
25
|
+
outputs: 0,
|
|
26
|
+
icon: "font-awesome/fa-picture-o",
|
|
27
|
+
paletteLabel: "media viewer",
|
|
28
|
+
label: function () { return this.name || "Media Viewer"; },
|
|
29
|
+
oneditprepare: function () {
|
|
30
|
+
$("#node-input-size").elementSizer({
|
|
31
|
+
width: "#node-input-width",
|
|
32
|
+
height: "#node-input-height",
|
|
33
|
+
group: "#node-input-group"
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<script type="text/html" data-template-name="ui_media_viewer">
|
|
40
|
+
<div class="form-row" id="template-row-group">
|
|
41
|
+
<label for="node-input-group"><i class="fa fa-table"></i> Group</label>
|
|
42
|
+
<input type="text" id="node-input-group">
|
|
43
|
+
</div>
|
|
44
|
+
<div class="form-row" id="template-row-size">
|
|
45
|
+
<label><i class="fa fa-object-group"></i> Size</label>
|
|
46
|
+
<input type="hidden" id="node-input-width">
|
|
47
|
+
<input type="hidden" id="node-input-height">
|
|
48
|
+
<button class="editor-button" id="node-input-size"></button>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="form-row">
|
|
51
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
52
|
+
<input type="text" id="node-input-name" placeholder="Name">
|
|
53
|
+
</div>
|
|
54
|
+
<hr>
|
|
55
|
+
<div class="form-row">
|
|
56
|
+
<label for="node-input-filepath"><i class="fa fa-file"></i> 檔案路徑</label>
|
|
57
|
+
<input type="text" id="node-input-filepath" placeholder="例如: C:\Users\bryan\Pictures\test.png">
|
|
58
|
+
<div style="margin-top: 5px; font-size: 12px; color: #666;">*可留空。由前方的 inject 直接傳入路徑。</div>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="form-row">
|
|
61
|
+
<label for="node-input-mediaType"><i class="fa fa-film"></i> 檔案格式</label>
|
|
62
|
+
<select id="node-input-mediaType" style="width:70%;">
|
|
63
|
+
<option value="image">圖片 (JPG / PNG / GIF)</option>
|
|
64
|
+
<option value="video">影片 (MP4)</option>
|
|
65
|
+
</select>
|
|
66
|
+
<div style="margin-top: 5px; font-size: 12px; color: #666;">*具備自動偵測功能,副檔名為 mp4 會自動切換為影片</div>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="form-row">
|
|
69
|
+
<label for="node-input-displayWidth"><i class="fa fa-arrows-h"></i> 顯示大小</label>
|
|
70
|
+
<input type="text" id="node-input-displayWidth" placeholder="例如: 300px 或 100%">
|
|
71
|
+
</div>
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
<script type="text/html" data-help-name="ui_media_viewer">
|
|
75
|
+
<p>整合讀取檔案與 Dashboard 顯示的萬用媒體節點。</p>
|
|
76
|
+
<h3>輸入觸發機制</h3>
|
|
77
|
+
<dl class="message-properties">
|
|
78
|
+
<dt>顯示媒體 (動態傳入路徑)</dt>
|
|
79
|
+
<dd>直接在 <code>msg.payload</code> 或是 <code>msg.filepath</code> 傳入你的檔案絕對路徑 (例如 <code>C:\test.mp4</code>),即可動態切換顯示該檔案。</dd>
|
|
80
|
+
|
|
81
|
+
<dt>關閉/隱藏媒體 (Reset)</dt>
|
|
82
|
+
<dd>若 <code>msg.payload</code> 為字串 <code>"reset"</code> 或是 <code>"clear"</code>,前端的圖片或影片就會立刻消失。</dd>
|
|
83
|
+
|
|
84
|
+
<dt class="optional">msg.mediaType <span class="property-type">string</span></dt>
|
|
85
|
+
<dd>若輸入此屬性(<code>"image"</code> 或 <code>"video"</code>),將會動態切換格式(通常不需設定,節點會根據副檔名自動判斷)。</dd>
|
|
86
|
+
</dl>
|
|
87
|
+
</script>
|