90 lines
5.4 KiB
TypeScript
90 lines
5.4 KiB
TypeScript
import { BrowserWindow } from "electron";
|
|
import { register } from "../engine/registry";
|
|
import { RuntimeError } from "../engine/types";
|
|
|
|
interface FormFieldSpec { key?: string; name?: string; label?: string; type?: string; value?: string; options?: unknown[] }
|
|
|
|
function parseFields(raw: unknown, nodeId: string): FormFieldSpec[] {
|
|
if (raw === undefined || raw === null || raw === "") {
|
|
throw new RuntimeError("validation_error", "表单配置 jsonlist 为空", { nodeId, componentId: nodeId, recommendation: "填写 JSON 数组,例如 [{\"key\":\"name\",\"label\":\"名称\",\"type\":\"text\"}]" });
|
|
}
|
|
if (Array.isArray(raw)) return raw as FormFieldSpec[];
|
|
const text = String(raw).trim();
|
|
if (text.startsWith("[")) {
|
|
try {
|
|
const parsed = JSON.parse(text) as unknown;
|
|
if (Array.isArray(parsed)) return parsed as FormFieldSpec[];
|
|
} catch { /* fall through */ }
|
|
}
|
|
throw new RuntimeError("validation_error", "表单配置 jsonlist 无效", { nodeId, componentId: nodeId, recommendation: "填写 JSON 数组,例如 [{\"key\":\"name\",\"label\":\"名称\",\"type\":\"text\"}]" });
|
|
}
|
|
|
|
function fieldKey(f: FormFieldSpec, index: number): string {
|
|
return String(f.key ?? f.name ?? f.label ?? "field" + (index + 1));
|
|
}
|
|
|
|
function escapeHtml(s: string): string {
|
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
}
|
|
|
|
async function showForm(options: { title: string; fields: FormFieldSpec[]; timeoutSec: number }): Promise<Record<string, string>> {
|
|
return new Promise((resolve) => {
|
|
const rows = options.fields.map((f, i) => {
|
|
const key = escapeHtml(fieldKey(f, i));
|
|
const label = escapeHtml(f.label ?? fieldKey(f, i));
|
|
const value = escapeHtml(String(f.value ?? ""));
|
|
const type = String(f.type ?? "text").toLowerCase();
|
|
if (type === "select" || (Array.isArray(f.options) && f.options.length)) {
|
|
const opts = (f.options ?? []).map(o => {
|
|
const ov = typeof o === "string" ? o : String((o as { value?: string }).value ?? (o as { label?: string }).label ?? "");
|
|
const ol = typeof o === "string" ? o : String((o as { label?: string }).label ?? ov);
|
|
return '<option value="' + escapeHtml(ov) + '"' + (ov === String(f.value ?? "") ? " selected" : "") + ">" + escapeHtml(ol) + "</option>";
|
|
}).join("");
|
|
return "<label>" + label + '<br/><select data-key="' + key + '">' + opts + "</select></label><br/>";
|
|
}
|
|
if (type === "textarea") {
|
|
return "<label>" + label + '<br/><textarea data-key="' + key + '" rows="3">' + value + "</textarea></label><br/>";
|
|
}
|
|
if (type === "checkbox") {
|
|
const checked = String(f.value ?? "") === "true" || String(f.value ?? "") === "1" ? " checked" : "";
|
|
return '<label><input type="checkbox" data-key="' + key + '"' + checked + " /> " + label + "</label><br/>";
|
|
}
|
|
const inputType = type === "password" ? "password" : "text";
|
|
return "<label>" + label + '<br/><input type="' + inputType + '" data-key="' + key + '" value="' + value + '" /></label><br/>';
|
|
}).join("\n");
|
|
const html = '<!DOCTYPE html><html><head><meta charset="utf-8"><style>body{font-family:Microsoft YaHei,sans-serif;margin:16px;font-size:14px;color:#1f2937}label{display:block;margin-bottom:10px}input,select,textarea{width:100%;box-sizing:border-box;padding:6px;margin-top:4px}#ok{margin-top:8px;padding:8px 24px;background:#2563eb;color:#fff;border:none;border-radius:6px;cursor:pointer}</style></head><body><h3>' + escapeHtml(options.title) + "</h3>" + rows + '<button id="ok">确认</button><script>document.getElementById("ok").onclick=function(){var out={};document.querySelectorAll("[data-key]").forEach(function(el){out[el.dataset.key]=el.type==="checkbox"?el.checked:el.value});window.__formValues=out;};</script></body></html>';
|
|
const win = new BrowserWindow({
|
|
width: 480, height: 0, useContentSize: true, autoHideMenuBar: true,
|
|
title: options.title, alwaysOnTop: true,
|
|
webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true }
|
|
});
|
|
let settled = false;
|
|
const timer = setInterval(async () => {
|
|
try {
|
|
const values = await win.webContents.executeJavaScript("window.__formValues || null", true);
|
|
if (values) { clearInterval(timer); win.destroy(); resolve(values as Record<string, string>); }
|
|
} catch { clearInterval(timer); resolve({}); }
|
|
}, 250);
|
|
const timeout = setTimeout(() => {
|
|
try { void win.webContents.executeJavaScript("document.getElementById(\"ok\").click()"); } catch {}
|
|
}, Math.max(5, options.timeoutSec) * 1000);
|
|
win.on("closed", () => { clearInterval(timer); clearTimeout(timeout); resolve({}); });
|
|
void win.loadURL("data:text/html;charset=utf-8," + encodeURIComponent(html));
|
|
});
|
|
}
|
|
|
|
const DIALOG_IDS = [
|
|
"Dialog_GetConfig", "Dialog_GetConfigV1", "Dialog_GetConfigV2", "Dialog_GetConfigV3",
|
|
"Dialog_GetConfigV4", "Dialog_GetConfigV5", "Dialog_GetConfigV6", "Dialog_GetConfigV7", "Dialog_GetConfigV8"
|
|
];
|
|
|
|
register(DIALOG_IDS, async (ctx, node, args) => {
|
|
const fields = parseFields(args.jsonlist, node.id);
|
|
const title = String(args.title ?? args.configName ?? "请输入");
|
|
const timeoutSec = Number(args.wait_time ?? 30) || 30;
|
|
const values = await showForm({ title, fields, timeoutSec });
|
|
ctx.log("已获取人机交互表单输入:" + JSON.stringify(values), "info", node.id);
|
|
return values;
|
|
});
|
|
|