fix: 补充用户不便输入的displayType控件(mouseRecord/clickPosition坐标拾取、excelRange文本、USB串口下拉、键鼠驱动下拉、邮箱email输入、previewDataTable只读、customModal/itemGroup JSON textarea); pickPointToField函数; flowValidation match_type优先级修复

This commit is contained in:
yu 2026-11-03 13:46:27 +08:00
parent 4824ac85f2
commit 346e2ad342
3 changed files with 182 additions and 6 deletions

View File

@ -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

View File

@ -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;
}

View 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);
});
});