fix: 补充用户不便输入的displayType控件(mouseRecord/clickPosition坐标拾取、excelRange文本、USB串口下拉、键鼠驱动下拉、邮箱email输入、previewDataTable只读、customModal/itemGroup JSON textarea); pickPointToField函数; flowValidation match_type优先级修复
This commit is contained in:
parent
4824ac85f2
commit
346e2ad342
@ -655,6 +655,21 @@ function FieldControl({
|
||||
};
|
||||
|
||||
|
||||
const pickPointToField = async () => {
|
||||
setPicking(true);
|
||||
try {
|
||||
const region = await window.rpa.desktop.pickRegion();
|
||||
const cx = region.x + Math.floor(region.width / 2);
|
||||
const cy = region.y + Math.floor(region.height / 2);
|
||||
onChange(field.key, JSON.stringify({ x: cx, y: cy }));
|
||||
onToast("已拾取坐标:" + cx + " , " + cy);
|
||||
} catch (error) {
|
||||
onToast(error instanceof Error ? error.message : String(error), "error");
|
||||
} finally {
|
||||
setPicking(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (field.widget === "boolean") {
|
||||
return (
|
||||
<label className="checkbox-row">
|
||||
@ -767,7 +782,49 @@ function FieldControl({
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : field.widget === "secret" ? (
|
||||
) : field.displayType === "colorPicker" ? (
|
||||
<div className="field-row">
|
||||
<input className="text-input" value={textOf(raw)} placeholder="#FF0000" onChange={(e) => onChange(field.key, e.target.value)} />
|
||||
<input type="color" value={/^#[0-9a-fA-F]{6}$/.test(textOf(raw)) ? textOf(raw) : "#000000"} style={{ width: 32, height: 28, padding: 0, border: "none", cursor: "pointer" }} onChange={(e) => onChange(field.key, e.target.value)} title="选择颜色" />
|
||||
<VarMenu variables={variables} onPick={insertVariable} />
|
||||
</div>
|
||||
) : field.displayType === "similaritySlider" ? (
|
||||
<div className="field-row">
|
||||
<input type="range" min={0} max={1} step={0.01} value={Number(textOf(raw)) || 0.8} style={{ flex: 1 }} onChange={(e) => onChange(field.key, e.target.value)} />
|
||||
<input className="text-input" style={{ width: 72 }} type="number" min={0} max={1} step={0.01} value={textOf(raw)} placeholder="0.8" onChange={(e) => onChange(field.key, e.target.value)} />
|
||||
</div>
|
||||
) : field.displayType === "dateFormatSelector" ? (
|
||||
<div className="field-row">
|
||||
<input className="text-input" value={textOf(raw)} placeholder="%Y-%m-%d %H:%M:%S" onChange={(e) => onChange(field.key, e.target.value)} />
|
||||
<select className="select-input" style={{ width: 160 }} value="" onChange={(e) => { if (e.target.value) onChange(field.key, e.target.value); }}>
|
||||
<option value="">选择常用格式…</option>
|
||||
{["%Y-%m-%d %H:%M:%S", "%Y-%m-%d", "%Y/%m/%d", "%H:%M:%S", "%Y年%m月%d日", "%Y%m%d%H%M%S"].map((f) => (
|
||||
<option key={f} value={f}>{f}</option>
|
||||
))}
|
||||
</select>
|
||||
<VarMenu variables={variables} onPick={insertVariable} />
|
||||
</div>
|
||||
) : field.displayType === "variableTypeSelect" ? (
|
||||
<select className="select-input" value={textOf(raw)} onChange={(e) => onChange(field.key, e.target.value)}>
|
||||
<option value="">请选择类型</option>
|
||||
{["rpa_str", "rpa_int", "rpa_double", "rpa_bool", "rpa_list", "rpa_dict", "rpa_datetimes", "rpa_any"].map((v) => (
|
||||
<option key={v} value={v}>{v.replace("rpa_", "")}</option>
|
||||
))}
|
||||
</select>
|
||||
) : field.displayType === "LLMModelSelect" ? (
|
||||
<div className="field-row">
|
||||
<input className="text-input" value={textOf(raw)} placeholder="输入模型名称" onChange={(e) => onChange(field.key, e.target.value)} />
|
||||
<select className="select-input" style={{ width: 180 }} value="" onChange={(e) => { if (e.target.value) onChange(field.key, e.target.value); }}>
|
||||
<option value="">选择常用模型…</option>
|
||||
{["gpt-4o", "gpt-4o-mini", "claude-3.5-sonnet", "deepseek-chat", "qwen-plus", "glm-4", "moonshot-v1-8k"].map((m) => (
|
||||
<option key={m} value={m}>{m}</option>
|
||||
))}
|
||||
</select>
|
||||
<VarMenu variables={variables} onPick={insertVariable} />
|
||||
</div>
|
||||
) : field.displayType === "databaseConfig" || field.displayType === "dictConfig" ? (
|
||||
<textarea className="textarea-input" value={textOf(raw)} placeholder={field.placeholder || '{"key": "value"}'} rows={3} onChange={(e) => onChange(field.key, e.target.value)} />
|
||||
) : field.widget === "secret" ? (
|
||||
<div className="field-row">
|
||||
<input
|
||||
className="text-input"
|
||||
@ -818,6 +875,31 @@ function FieldControl({
|
||||
{picking ? "框选中" : "屏幕框选"}
|
||||
</button>
|
||||
</div>
|
||||
) : field.widget === "region" && (field.displayType === "mouseRecord" || field.displayType === "clickPosition" || field.displayType === "clickPositionV2" || field.displayType === "clickPositionV3") ? (
|
||||
// 坐标点类字段: 用户需要的是(x,y)坐标, 不是矩形区域, 用拾取坐标按钮
|
||||
<div className="field-row">
|
||||
<input
|
||||
className="text-input"
|
||||
value={textOf(raw)}
|
||||
placeholder={field.placeholder || "填坐标对象如 {\"x\":100,\"y\":200},或点「拾取坐标」在屏幕上点选"}
|
||||
onChange={(event) => onChange(field.key, event.target.value)}
|
||||
/>
|
||||
<VarMenu variables={variables} onPick={insertVariable} />
|
||||
<button type="button" className="mini-btn" disabled={picking} onClick={() => void pickPointToField()} title="在屏幕上点击拾取一个坐标点">
|
||||
{picking ? "拾取中" : "拾取坐标"}
|
||||
</button>
|
||||
</div>
|
||||
) : field.widget === "region" && field.displayType === "excelRange" ? (
|
||||
// Excel 区域字段: 值是 A1:B10 这样的单元格范围, 不是屏幕区域, 不应显示框选按钮
|
||||
<div className="field-row">
|
||||
<input
|
||||
className="text-input"
|
||||
value={textOf(raw)}
|
||||
placeholder={field.placeholder || "如 A1:B10 或 $A$1:$B$10"}
|
||||
onChange={(event) => onChange(field.key, event.target.value)}
|
||||
/>
|
||||
<VarMenu variables={variables} onPick={insertVariable} />
|
||||
</div>
|
||||
) : field.widget === "region" ? (
|
||||
<div className="field-row">
|
||||
<input
|
||||
@ -831,6 +913,35 @@ function FieldControl({
|
||||
{picking ? "框选中" : "屏幕框选"}
|
||||
</button>
|
||||
</div>
|
||||
) : field.displayType === "USBGetComs" ? (
|
||||
<div className="field-row">
|
||||
<input className="text-input" value={textOf(raw)} placeholder={field.placeholder || "如 COM3"} onChange={(e) => onChange(field.key, e.target.value)} />
|
||||
<select className="select-input" style={{ width: 100 }} value="" onChange={(e) => { if (e.target.value) onChange(field.key, e.target.value); }}>
|
||||
<option value="">选择串口…</option>
|
||||
{["COM1","COM2","COM3","COM4","COM5","COM6","COM7","COM8"].map((v) => (<option key={v} value={v}>{v}</option>))}
|
||||
</select>
|
||||
<VarMenu variables={variables} onPick={insertVariable} />
|
||||
</div>
|
||||
) : field.displayType === "keyboardDriverSelect" ? (
|
||||
<div className="field-row">
|
||||
<input className="text-input" value={textOf(raw)} placeholder={field.placeholder || "驱动名称"} onChange={(e) => onChange(field.key, e.target.value)} />
|
||||
<select className="select-input" style={{ width: 120 }} value="" onChange={(e) => { if (e.target.value) onChange(field.key, e.target.value); }}>
|
||||
<option value="">选择驱动…</option>
|
||||
{["win32", "winio", "sendinput"].map((v) => (<option key={v} value={v}>{v}</option>))}
|
||||
</select>
|
||||
<VarMenu variables={variables} onPick={insertVariable} />
|
||||
</div>
|
||||
) : field.displayType === "mailSelection" ? (
|
||||
<div className="field-row">
|
||||
<input className="text-input" type="email" value={textOf(raw)} placeholder={field.placeholder || "user@example.com"} onChange={(e) => onChange(field.key, e.target.value)} />
|
||||
<VarMenu variables={variables} onPick={insertVariable} />
|
||||
</div>
|
||||
) : field.displayType === "previewDataTable" ? (
|
||||
<textarea className="textarea-input" value={textOf(raw)} readOnly disabled rows={2} placeholder="(运行时自动生成)" style={{ opacity: 0.5 }} />
|
||||
) : field.displayType === "customModal" || field.displayType === "customModalV4" || field.displayType === "customModalLocal" ? (
|
||||
<textarea className="textarea-input" value={textOf(raw)} placeholder={field.placeholder || "[{\"label\":\"\u5B57\u6BB5\u540D\",\"type\":\"text\",\"key\":\"field1\"}]"} rows={4} onChange={(e) => onChange(field.key, e.target.value)} />
|
||||
) : field.displayType === "itemGroup" ? (
|
||||
<textarea className="textarea-input" value={textOf(raw)} placeholder={field.placeholder || "筛选条件 JSON"} rows={3} onChange={(e) => onChange(field.key, e.target.value)} />
|
||||
) : (
|
||||
<div className="field-row">
|
||||
<input
|
||||
|
||||
@ -32,16 +32,18 @@ export function fieldIsActive(componentId: string, key: string, values: Record<s
|
||||
// Window_*_v1 series: conditional fields based on get_type mode
|
||||
if (/^Window_.*_v1$/.test(componentId) || componentId === "Window_WindowExists" || componentId === "Window_WaitWindow") {
|
||||
const gt = String(values.get_type ?? "");
|
||||
if (gt === "2") {
|
||||
// get_type=2: match_type determines which title field is active
|
||||
if (key === "match_type") return true;
|
||||
const mt = String(values.match_type ?? "0");
|
||||
if (key === "select_title") return mt === "0";
|
||||
if (key === "match_title") return mt === "1";
|
||||
}
|
||||
if (gt !== "") {
|
||||
if (key === "target_window") return gt === "0";
|
||||
if (key === "window_obj") return gt === "1";
|
||||
if (key === "match_type" || key === "select_title" || key === "match_title") return gt === "2";
|
||||
}
|
||||
if (gt === "2") {
|
||||
const mt = String(values.match_type ?? "0");
|
||||
if (key === "select_title") return mt === "0";
|
||||
if (key === "match_title") return mt === "1";
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
63
src/renderer/src/validation/window-fields.test.ts
Normal file
63
src/renderer/src/validation/window-fields.test.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { fieldIsActive } from "./flowValidation";
|
||||
|
||||
describe("Window component conditional fields", () => {
|
||||
const componentId = "Window_CloseWindow_v1";
|
||||
|
||||
it("get_type=0 (拾取窗口): only target_window visible", () => {
|
||||
const values = { get_type: "0" };
|
||||
expect(fieldIsActive(componentId, "target_window", values)).toBe(true);
|
||||
expect(fieldIsActive(componentId, "window_obj", values)).toBe(false);
|
||||
expect(fieldIsActive(componentId, "match_type", values)).toBe(false);
|
||||
expect(fieldIsActive(componentId, "select_title", values)).toBe(false);
|
||||
expect(fieldIsActive(componentId, "match_title", values)).toBe(false);
|
||||
expect(fieldIsActive(componentId, "isProcess", values)).toBe(true);
|
||||
});
|
||||
|
||||
it("get_type=1 (选择窗口对象): only window_obj visible", () => {
|
||||
const values = { get_type: "1" };
|
||||
expect(fieldIsActive(componentId, "target_window", values)).toBe(false);
|
||||
expect(fieldIsActive(componentId, "window_obj", values)).toBe(true);
|
||||
expect(fieldIsActive(componentId, "match_type", values)).toBe(false);
|
||||
expect(fieldIsActive(componentId, "select_title", values)).toBe(false);
|
||||
expect(fieldIsActive(componentId, "match_title", values)).toBe(false);
|
||||
});
|
||||
|
||||
it("get_type=2 + match_type=0: match_type + select_title visible", () => {
|
||||
const values = { get_type: "2", match_type: "0" };
|
||||
expect(fieldIsActive(componentId, "match_type", values)).toBe(true);
|
||||
expect(fieldIsActive(componentId, "select_title", values)).toBe(true);
|
||||
expect(fieldIsActive(componentId, "match_title", values)).toBe(false);
|
||||
expect(fieldIsActive(componentId, "target_window", values)).toBe(false);
|
||||
expect(fieldIsActive(componentId, "window_obj", values)).toBe(false);
|
||||
});
|
||||
|
||||
it("get_type=2 + match_type=1: match_type + match_title visible", () => {
|
||||
const values = { get_type: "2", match_type: "1" };
|
||||
expect(fieldIsActive(componentId, "match_type", values)).toBe(true);
|
||||
expect(fieldIsActive(componentId, "select_title", values)).toBe(false);
|
||||
expect(fieldIsActive(componentId, "match_title", values)).toBe(true);
|
||||
});
|
||||
|
||||
it("no get_type: all fields default visible", () => {
|
||||
const values = {};
|
||||
expect(fieldIsActive(componentId, "target_window", values)).toBe(true);
|
||||
expect(fieldIsActive(componentId, "window_obj", values)).toBe(true);
|
||||
expect(fieldIsActive(componentId, "match_type", values)).toBe(true);
|
||||
});
|
||||
|
||||
it("Window_WindowExists also follows the pattern", () => {
|
||||
expect(fieldIsActive("Window_WindowExists", "window_obj", { get_type: "1" })).toBe(true);
|
||||
expect(fieldIsActive("Window_WindowExists", "target_window", { get_type: "1" })).toBe(false);
|
||||
});
|
||||
|
||||
it("Window_WaitWindow also follows the pattern", () => {
|
||||
expect(fieldIsActive("Window_WaitWindow", "match_title", { get_type: "2", match_type: "1" })).toBe(true);
|
||||
expect(fieldIsActive("Window_WaitWindow", "select_title", { get_type: "2", match_type: "1" })).toBe(false);
|
||||
});
|
||||
|
||||
it("non-window components unaffected", () => {
|
||||
expect(fieldIsActive("System_ExecuteCommandV1", "dos_command", { command_type: "1" })).toBe(true);
|
||||
expect(fieldIsActive("App_KillV1", "pid", { close_type: "id" })).toBe(true);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user