Merge branch 'main' of http://43.140.215.244:7040/wangyukun/PRA-only-demo
This commit is contained in:
commit
afa32133ec
@ -6,13 +6,34 @@
|
||||
"defaultValue": "number",
|
||||
"description": "选择要判断的目标类型。历史默认值 1 兼容数值类型。",
|
||||
"options": [
|
||||
{ "label": "数值", "value": "number" },
|
||||
{ "label": "整数", "value": "int" },
|
||||
{ "label": "空值", "value": "null" },
|
||||
{ "label": "字典", "value": "dict" },
|
||||
{ "label": "数组", "value": "list" },
|
||||
{ "label": "文本", "value": "string" },
|
||||
{ "label": "布尔", "value": "bool" }
|
||||
{
|
||||
"label": "数值",
|
||||
"value": "number"
|
||||
},
|
||||
{
|
||||
"label": "整数",
|
||||
"value": "int"
|
||||
},
|
||||
{
|
||||
"label": "空值",
|
||||
"value": "null"
|
||||
},
|
||||
{
|
||||
"label": "字典",
|
||||
"value": "dict"
|
||||
},
|
||||
{
|
||||
"label": "数组",
|
||||
"value": "list"
|
||||
},
|
||||
{
|
||||
"label": "文本",
|
||||
"value": "string"
|
||||
},
|
||||
{
|
||||
"label": "布尔",
|
||||
"value": "bool"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -24,10 +45,22 @@
|
||||
"defaultValue": "number",
|
||||
"description": "选择目标类型。历史默认值 1 兼容数值;其他数字别名 2=整数、3=布尔、4=文本。",
|
||||
"options": [
|
||||
{ "label": "数值", "value": "number" },
|
||||
{ "label": "整数", "value": "int" },
|
||||
{ "label": "布尔", "value": "bool" },
|
||||
{ "label": "文本", "value": "string" }
|
||||
{
|
||||
"label": "数值",
|
||||
"value": "number"
|
||||
},
|
||||
{
|
||||
"label": "整数",
|
||||
"value": "int"
|
||||
},
|
||||
{
|
||||
"label": "布尔",
|
||||
"value": "bool"
|
||||
},
|
||||
{
|
||||
"label": "文本",
|
||||
"value": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -36,8 +69,14 @@
|
||||
"fields": {
|
||||
"close_type": {
|
||||
"options": [
|
||||
{ "label": "按进程 PID", "value": "id" },
|
||||
{ "label": "按进程名称", "value": "name" }
|
||||
{
|
||||
"label": "按进程 PID",
|
||||
"value": "id"
|
||||
},
|
||||
{
|
||||
"label": "按进程名称",
|
||||
"value": "name"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -47,9 +86,18 @@
|
||||
"command_type": {
|
||||
"label": "命令类型",
|
||||
"options": [
|
||||
{ "label": "DOS 命令", "value": "1" },
|
||||
{ "label": "BAT 文件", "value": "2" },
|
||||
{ "label": "PowerShell 命令", "value": "3" }
|
||||
{
|
||||
"label": "DOS 命令",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"label": "BAT 文件",
|
||||
"value": "2"
|
||||
},
|
||||
{
|
||||
"label": "PowerShell 命令",
|
||||
"value": "3"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -57,12 +105,70 @@
|
||||
"System_ScreenSaverShowV1": {
|
||||
"description": "开启 Windows 已配置的系统屏保。当前组件不支持自定义屏保文字、透明度或解锁密码;登录保护遵循 Windows 系统设置。",
|
||||
"messageTemplate": "开启 Windows 系统屏保",
|
||||
"removeFields": ["text", "effect", "status", "set_password", "password"]
|
||||
"removeFields": [
|
||||
"text",
|
||||
"effect",
|
||||
"status",
|
||||
"set_password",
|
||||
"password"
|
||||
]
|
||||
},
|
||||
"ScreenControl_ScreenUnlock": {
|
||||
"name": "唤醒屏幕(非登录解锁)",
|
||||
"description": "仅尝试唤醒屏幕,不支持通过账号密码自动完成 Windows 登录解锁。锁屏后仍需本人手动登录。",
|
||||
"messageTemplate": "尝试唤醒屏幕;Windows 登录锁屏需人工解锁",
|
||||
"removeFields": ["username", "pwd"]
|
||||
"removeFields": [
|
||||
"username",
|
||||
"pwd"
|
||||
]
|
||||
},
|
||||
"Mail_DoGetMailMessage_v2": {
|
||||
"fields": {
|
||||
"attachment": {
|
||||
"defaultValue": "2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Mail_DoMoveMail_v1": {
|
||||
"fields": {
|
||||
"email_folder": {
|
||||
"defaultValue": "INBOX",
|
||||
"options": [
|
||||
{
|
||||
"label": "收件箱",
|
||||
"value": "INBOX"
|
||||
},
|
||||
{
|
||||
"label": "已发送",
|
||||
"value": "Sent"
|
||||
},
|
||||
{
|
||||
"label": "其他文件夹",
|
||||
"value": "Other"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Mail_FindMail_v2": {
|
||||
"fields": {
|
||||
"attachment": {
|
||||
"defaultValue": "2",
|
||||
"options": [
|
||||
{
|
||||
"label": "包含附件",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"label": "不包含附件",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"label": "不限",
|
||||
"value": "2"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
13124
resources/catalog.json
13124
resources/catalog.json
File diff suppressed because it is too large
Load Diff
@ -136,6 +136,20 @@ function groupFor(component) {
|
||||
return "data";
|
||||
}
|
||||
|
||||
function normalizeOptions(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => {
|
||||
if (typeof item === "string") return { label: item, value: item };
|
||||
if (typeof item !== "object" || item === null) return null;
|
||||
const label = String(item.label ?? item.text ?? item.value ?? "");
|
||||
const rawValue = item.value ?? item.label ?? "";
|
||||
return { label, value: cleanDefault(rawValue) };
|
||||
}).filter((item) => item.label || item.value);
|
||||
}
|
||||
if (Array.isArray(value?.options)) return normalizeOptions(value.options);
|
||||
return [];
|
||||
}
|
||||
|
||||
function normalizeVendorField(field) {
|
||||
const widget = widgetFor(field.displayType, field.widget);
|
||||
const valueTypes = Array.isArray(field.valueTypes) ? [...field.valueTypes] : [];
|
||||
|
||||
89
src/main/capabilities/catalog-properties.test.ts
Normal file
89
src/main/capabilities/catalog-properties.test.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import type { Catalog, CatalogComponent, CatalogField } from "../../shared/types";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { hasCapability } from "../engine/registry";
|
||||
import "./index";
|
||||
|
||||
const widgets = new Set(["text","textarea","number","select","radio","boolean","file","folder","element","code","secret","region","hidden"]);
|
||||
const runtimeContainers = new Set([
|
||||
"logic_control_if_else","logic_control_count_for","logic_control_array_for_v3","logic_control_dictionary_for_v1",
|
||||
"logic_control_while","infinite_loop","logic_control_try_catch"
|
||||
]);
|
||||
// 这些属性在运行时由上游对象 / 外部系统动态提供,组件库中退化为表达式输入。
|
||||
const dynamicSelects = new Set([
|
||||
"Database_BatchSQLExecute|connectionID","Database_BatchInsertExecute|connectionID","Database_SQLExecute|connectionID",
|
||||
"DataService_Review|reviewTemplate","DataService_ChatBot|chatbot","DataService_ChatBot|botChannel",
|
||||
"DataService_KeyWord|keyWordTemplate","DataService_ZBrain|z_barin","Element_Sibling_V2|position",
|
||||
"Element_SetCheck|isChecked","Element_ElementAarray|browser_type","Excel_Cs_Interop|obj","Excel_Cs_Interop|property",
|
||||
"Mail_DoReplyMail_v1|account_obj","Mail_FindMail_v1|email_num","Mail_DoGetMailMessage_v2|account_obj",
|
||||
"Mail_DoGetMailMessage_v2|email_num","Mail_DoGetMailMessage_v1|account_obj","Mail_DoGetMailMessage_v1|email_num",
|
||||
"Mail_FindMail_v2|email_num","Mail_DoMoveMail_v1|account_obj","Mail_DoSendMail_v1|account_obj",
|
||||
"OCR_MergeCardOCR|category","OCR_MergeOtherOCR|category","OCR_MergePaperOCR|category","OCR_MergeCarOCR|category",
|
||||
"OCR_MergeEducationOCR|category","WebBrowser_GetTAB|type","WebBrowser_GetTAB|isActive","WebBrowser_GetTAB|isRemote",
|
||||
"WebBrowser_BatchDataCrawl|browserType"
|
||||
]);
|
||||
|
||||
describe("catalog capability properties", () => {
|
||||
it("exposes complete metadata for every canonical capability", async () => {
|
||||
const catalog: Catalog = JSON.parse(await readFile("resources/catalog.json", "utf8"));
|
||||
const excluded = JSON.parse(await readFile("resources/excluded.json", "utf8"));
|
||||
const duplicates = JSON.parse(await readFile("resources/capability-duplicates.json", "utf8"));
|
||||
const excludedIds = new Set(excluded.ids);
|
||||
const hidden = new Set((duplicates.decisions as Array<{ id: string; status: string; canonical?: string }>).filter((item) => item.status === "hide").map((item) => item.id));
|
||||
const visible: CatalogComponent[] = catalog.components.filter((component) => !excludedIds.has(component.id) && !hidden.has(component.id));
|
||||
expect(visible).toHaveLength(658);
|
||||
|
||||
const ids = visible.map((component) => component.id);
|
||||
expect(new Set(ids).size).toBe(ids.length);
|
||||
|
||||
for (const component of visible) {
|
||||
expect(component.id, component.id).toBeTruthy();
|
||||
expect(component.name, component.id).toBeTruthy();
|
||||
expect(component.group, component.id).toBeTruthy();
|
||||
expect(component.kind, component.id).toBeTruthy();
|
||||
expect(component.icon, component.id).toBeTruthy();
|
||||
expect(["low","high"], component.id).toContain(component.risk);
|
||||
expect(Array.isArray(component.fields), component.id).toBe(true);
|
||||
expect(Array.isArray(component.slots), component.id).toBe(true);
|
||||
expect(component.output, component.id).toBeTruthy();
|
||||
|
||||
const fieldKeys = component.fields.map((field: CatalogField) => field.key);
|
||||
expect(new Set(fieldKeys).size, component.id).toBe(fieldKeys.length);
|
||||
for (const field of component.fields as CatalogField[]) {
|
||||
expect(field.key, component.id).toBeTruthy();
|
||||
expect(field.label, component.id + ":" + field.key).toBeTruthy();
|
||||
expect(widgets.has(field.widget), component.id + ":" + field.key + ":" + field.widget).toBe(true);
|
||||
expect(["string","number","boolean","object"], component.id + ":" + field.key).toContain(field.type);
|
||||
if ((field.widget === "select" || field.widget === "radio") && !field.options?.length) {
|
||||
expect(dynamicSelects.has(component.id + "|" + field.key), component.id + ":" + field.key).toBe(true);
|
||||
}
|
||||
if (Array.isArray(field.options) && field.options.length) {
|
||||
const values = field.options.map((option: string | { label: string; value: string }) => typeof option === "string" ? option : option.value);
|
||||
expect(values.every((value) => typeof value === "string"), component.id + ":" + field.key).toBe(true);
|
||||
expect(new Set(values).size, component.id + ":" + field.key).toBe(values.length);
|
||||
if (field.defaultValue) expect(values, component.id + ":" + field.key).toContain(field.defaultValue);
|
||||
}
|
||||
if (field.widget === "number" && field.defaultValue) expect(Number.isFinite(Number(field.defaultValue)), component.id + ":" + field.key).toBe(true);
|
||||
if (field.widget === "boolean" && field.defaultValue) {
|
||||
expect(["true","false","1","0"], component.id + ":" + field.key).toContain(String(field.defaultValue).toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
const outputKeys = component.output.fields.map((field: { key: string }) => field.key);
|
||||
expect(new Set(outputKeys).size).toBe(outputKeys.length);
|
||||
}
|
||||
});
|
||||
|
||||
it("binds every canonical capability to a runtime handler or container", async () => {
|
||||
const catalog: Catalog = JSON.parse(await readFile("resources/catalog.json", "utf8"));
|
||||
const excluded = JSON.parse(await readFile("resources/excluded.json", "utf8"));
|
||||
const duplicates = JSON.parse(await readFile("resources/capability-duplicates.json", "utf8"));
|
||||
const excludedIds = new Set(excluded.ids);
|
||||
const hidden = new Set((duplicates.decisions as Array<{ id: string; status: string; canonical?: string }>).filter((item) => item.status === "hide").map((item) => item.id));
|
||||
const visible: CatalogComponent[] = catalog.components.filter((component) => !excludedIds.has(component.id) && !hidden.has(component.id));
|
||||
const missing = visible.filter((component) =>
|
||||
!runtimeContainers.has(component.id) && !hasCapability(component.id) && !hasCapability(component.functionName)
|
||||
);
|
||||
expect(missing.map((component) => component.id)).toEqual([]);
|
||||
});
|
||||
});
|
||||
@ -27,6 +27,7 @@ async function grabScreen(imagePath: string): Promise<void> {
|
||||
fs.mkdirSync(path.dirname(imagePath), { recursive: true });
|
||||
const psScript = [
|
||||
"Add-Type -AssemblyName System.Drawing | Out-Null;",
|
||||
"Add-Type -AssemblyName System.Windows.Forms | Out-Null;",
|
||||
"$b = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds;",
|
||||
"$bmp = New-Object System.Drawing.Bitmap($b.Width, $b.Height);",
|
||||
"$g = [System.Drawing.Graphics]::FromImage($bmp);",
|
||||
|
||||
@ -5,6 +5,7 @@ import {
|
||||
CircleAlert,
|
||||
CirclePlay,
|
||||
Clock3,
|
||||
History,
|
||||
ListChecks,
|
||||
Pencil,
|
||||
Plus,
|
||||
@ -44,8 +45,6 @@ function formatDuration(value?: number): string {
|
||||
return `${(value / 1000).toFixed(value < 10000 ? 1 : 0)} s`;
|
||||
}
|
||||
|
||||
const RUN_LABEL = { succeeded: "成功", failed: "失败", cancelled: "已取消", never: "未运行" } as const;
|
||||
|
||||
export function TasksPage({ flows, onCreate, onCreateExamples, onOpenEditor, onRun, onDelete, onRefresh, onToast }: TasksPageProps) {
|
||||
const [query, setQuery] = useState("");
|
||||
const [selectedId, setSelectedId] = useState<string | null>(flows[0]?.id || null);
|
||||
@ -129,9 +128,9 @@ export function TasksPage({ flows, onCreate, onCreateExamples, onOpenEditor, onR
|
||||
|
||||
<div className="task-table-head" aria-hidden="true">
|
||||
<span>任务</span>
|
||||
<span>状态</span>
|
||||
<span>触发方式</span>
|
||||
<span>最近运行</span>
|
||||
<span>更新时间</span>
|
||||
<span />
|
||||
</div>
|
||||
|
||||
@ -151,14 +150,15 @@ export function TasksPage({ flows, onCreate, onCreateExamples, onOpenEditor, onR
|
||||
<span className="task-icon"><ListChecks className="icon" /></span>
|
||||
<span className="task-copy"><strong>{flow.name}</strong><small>{flow.nodeCount} 个节点</small></span>
|
||||
</div>
|
||||
<span className={`task-status ${flow.enabled === false ? "paused" : "enabled"}`}>
|
||||
<span className="status-dot" />{flow.enabled === false ? "未启用" : "已启用"}
|
||||
</span>
|
||||
<span className="task-trigger"><CalendarClock className="icon" />{flow.triggerLabel || "手动运行"}</span>
|
||||
<span className={`task-updated run-result ${flow.lastRunStatus || "never"}`} title={flow.lastRunError || ""}>
|
||||
<Clock3 className="icon" />
|
||||
<span>{flow.lastRunAt ? formatTime(flow.lastRunAt) : "尚未运行"}{flow.lastRunDurationMs !== undefined ? ` · ${formatDuration(flow.lastRunDurationMs)}` : ""}</span>
|
||||
</span>
|
||||
<span className="task-updated" title={`最近修改于 ${formatTime(flow.updatedAt)}`}>
|
||||
<History className="icon" />
|
||||
<span>{formatTime(flow.updatedAt)}</span>
|
||||
</span>
|
||||
<span className="task-row-actions">
|
||||
<button type="button" className="icon-btn" title="运行任务" onClick={(event) => { event.stopPropagation(); onRun(flow.id); }}>
|
||||
<CirclePlay className="icon" />
|
||||
@ -172,38 +172,13 @@ export function TasksPage({ flows, onCreate, onCreateExamples, onOpenEditor, onR
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{!filtered.length && (
|
||||
{!flows.length ? (
|
||||
<div className="task-empty"><ListChecks className="icon" /><strong>还没有任务</strong><span>新建一个任务开始搭建自动化流程。</span><button type="button" className="btn btn-primary" onClick={onCreate}><Plus className="icon" />新建任务</button></div>
|
||||
) : !filtered.length ? (
|
||||
<div className="task-empty"><ListChecks className="icon" /><strong>没有匹配的任务</strong><span>调整搜索条件,或新建一个自动化任务。</span></div>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside className="panel task-overview" aria-label="任务概览">
|
||||
{selected ? (
|
||||
<>
|
||||
<div className="task-overview-head">
|
||||
<span className="task-icon large"><ListChecks className="icon" /></span>
|
||||
<div><span className="eyebrow">任务概览</span><h2>{selected.name}</h2></div>
|
||||
</div>
|
||||
<div className="overview-status"><span className="status-dot" />{selected.enabled === false ? "未启用" : "已启用"}<span className="overview-sep" />本地执行</div>
|
||||
<div className="overview-actions">
|
||||
<button type="button" className="btn btn-primary" onClick={() => onRun(selected.id)}><CirclePlay className="icon" />运行任务</button>
|
||||
<button type="button" className="btn" onClick={() => onOpenEditor(selected.id)}><Pencil className="icon" />编辑流程</button>
|
||||
</div>
|
||||
<dl className="overview-meta">
|
||||
<div><dt>触发方式</dt><dd><CalendarClock className="icon" />{selected.triggerLabel || "手动运行"}</dd></div>
|
||||
<div><dt>流程节点</dt><dd>{selected.nodeCount} 个</dd></div>
|
||||
<div><dt>最近运行</dt><dd className={`overview-run ${selected.lastRunStatus || "never"}`}>{RUN_LABEL[selected.lastRunStatus || "never"]} · {selected.lastRunAt ? formatTime(selected.lastRunAt) : "尚未运行"}</dd></div>
|
||||
<div><dt>运行耗时</dt><dd>{selected.lastRunDurationMs === undefined ? "—" : formatDuration(selected.lastRunDurationMs)}</dd></div>
|
||||
{selected.lastRunError && <div><dt>失败原因</dt><dd className="overview-error" title={selected.lastRunError}>{selected.lastRunError}</dd></div>}
|
||||
<div><dt>最近修改</dt><dd>{formatTime(selected.updatedAt)}</dd></div>
|
||||
</dl>
|
||||
<div className="overview-note"><Zap className="icon" /><span>任务打开后先查看概览;只有点击“编辑流程”才进入画布。</span></div>
|
||||
</>
|
||||
) : (
|
||||
<div className="task-empty overview-empty"><ListChecks className="icon" /><strong>还没有任务</strong><span>新建一个任务开始搭建自动化流程。</span><button type="button" className="btn btn-primary" onClick={onCreate}><Plus className="icon" />新建任务</button></div>
|
||||
)}
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -56,13 +56,8 @@ export function Palette({ catalog, onAdd }: PaletteProps) {
|
||||
}, [catalog.groups, filtered]);
|
||||
|
||||
useEffect(() => {
|
||||
if (query.trim()) return;
|
||||
setExpanded((current) => current.size ? current : new Set(catalog.groups.slice(0, 2).map((group) => group.id)));
|
||||
}, [catalog.groups, query]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!query.trim()) return;
|
||||
setExpanded(new Set(groups.map((group) => group.id)));
|
||||
if (query.trim()) setExpanded(new Set(groups.map((group) => group.id)));
|
||||
else setExpanded(new Set());
|
||||
}, [groups, query]);
|
||||
|
||||
const allExpanded = groups.length > 0 && groups.every((group) => expanded.has(group.id));
|
||||
|
||||
@ -700,45 +700,33 @@ function FieldControl({
|
||||
<VarMenu variables={variables} onPick={insertVariable} />
|
||||
<button type="button" className="mini-btn" onClick={() => void pickPath(field.widget === "folder")} title="浏览">
|
||||
<FolderOpen className="icon" style={{ width: 13, height: 13 }} />
|
||||
{isVisionTarget && /image/i.test(field.key) && (
|
||||
<button type="button" className="mini-btn" disabled={picking} onClick={() => void pickRegionToField()} title="屏幕框选区域并截图">
|
||||
{picking ? "框选中" : "屏幕框选"}
|
||||
</button>
|
||||
)}
|
||||
{isWindowTarget && /title/i.test(field.key) && (
|
||||
<button type="button" className="mini-btn" disabled={picking} onClick={() => void pickWindowToField()} title="选择窗口">
|
||||
{picking ? "选择中" : "选窗口"}
|
||||
</button>
|
||||
)}
|
||||
{isVisionTarget && /image/i.test(field.key) && (
|
||||
<button type="button" className="mini-btn" disabled={picking} onClick={() => void pickRegionToField()} title="屏幕框选区域并截图">
|
||||
{picking ? "框选中" : "屏幕框选"}
|
||||
</button>
|
||||
)}
|
||||
{isWindowTarget && /title/i.test(field.key) && (
|
||||
<button type="button" className="mini-btn" disabled={picking} onClick={() => void pickWindowToField()} title="选择窗口">
|
||||
{picking ? "选择中" : "选窗口"}
|
||||
</button>
|
||||
)}
|
||||
</button>
|
||||
{isVisionTarget && /image/i.test(field.key) && (
|
||||
<button type="button" className="mini-btn" disabled={picking} onClick={() => void pickRegionToField()} title="屏幕框选区域并截图">
|
||||
{picking ? "框选中" : "屏幕框选"}
|
||||
</button>
|
||||
)}
|
||||
{isWindowTarget && /title/i.test(field.key) && (
|
||||
<button type="button" className="mini-btn" disabled={picking} onClick={() => void pickWindowToField()} title="选择窗口">
|
||||
{picking ? "选择中" : "选窗口"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : field.widget === "element" ? (
|
||||
<div className="field-row">
|
||||
<input
|
||||
className="text-input"
|
||||
value={textOf(raw)}
|
||||
placeholder="css:#submit / aria:button"
|
||||
placeholder={isVisionTarget ? "屏幕上可见的目标文本" : "css:#submit / aria:button"}
|
||||
onChange={(event) => onChange(field.key, event.target.value)}
|
||||
/>
|
||||
{isGroundTarget ? (
|
||||
<>
|
||||
<button type="button" className="mini-btn" disabled={picking} onClick={() => void pickDesktop()} title="从桌面拾取元素(移到目标按空格)">
|
||||
{picking ? "桌面拾取中" : "桌面拾取"}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button type="button" className="mini-btn" disabled={picking} onClick={() => void pickDesktop()} title="从桌面拾取元素(移到目标按空格)">
|
||||
{picking ? "桌面拾取中" : "桌面拾取"}
|
||||
</button>
|
||||
) : isVisionTarget ? null : (
|
||||
<button type="button" className="mini-btn" disabled={picking} onClick={() => void pickElement()} title="从浏览器拾取元素(移到目标按空格锁定,PageUp/PageDown 调层级)">
|
||||
{picking ? "浏览器拾取中" : "浏览器拾取"}
|
||||
{picking ? "浏览器拾取中" : "浏览器拾取"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -360,6 +360,13 @@ textarea:focus-visible {
|
||||
color: var(--faint);
|
||||
}
|
||||
|
||||
/* 搜索容器自带聚焦光圈, 内层 input 不再叠加自己的 focus 框 */
|
||||
.search-box input:focus-visible,
|
||||
.task-search input:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.seg {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
@ -538,14 +545,6 @@ textarea:focus-visible {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--success);
|
||||
box-shadow: 0 0 0 3px rgba(7, 148, 85, 0.08);
|
||||
}
|
||||
|
||||
.palette-more {
|
||||
padding: 8px 14px 12px;
|
||||
display: flex;
|
||||
@ -766,8 +765,6 @@ textarea:focus-visible {
|
||||
|
||||
.flow-canvas .react-flow {
|
||||
background-color: #f8fafc;
|
||||
background-image: radial-gradient(circle, rgba(138, 153, 172, 0.33) 1px, transparent 1px);
|
||||
background-size: 20px 20px;
|
||||
}
|
||||
|
||||
.react-flow__node-rpa {
|
||||
@ -2705,8 +2702,7 @@ textarea:focus-visible {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.page-heading-actions,
|
||||
.overview-actions {
|
||||
.page-heading-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
@ -2756,12 +2752,10 @@ textarea:focus-visible {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.65fr) minmax(300px, 0.75fr);
|
||||
gap: 12px;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.task-list-panel,
|
||||
.task-overview {
|
||||
.task-list-panel {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
@ -2821,7 +2815,7 @@ textarea:focus-visible {
|
||||
.task-table-head,
|
||||
.task-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(190px, 1.35fr) 90px minmax(100px, 0.8fr) minmax(100px, 0.85fr) 106px;
|
||||
grid-template-columns: minmax(190px, 1.35fr) minmax(100px, 0.8fr) minmax(110px, 0.85fr) minmax(100px, 0.75fr) 106px;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
@ -2862,10 +2856,7 @@ textarea:focus-visible {
|
||||
|
||||
.task-name-cell,
|
||||
.task-trigger,
|
||||
.task-updated,
|
||||
.task-status,
|
||||
.overview-status,
|
||||
.overview-meta dd {
|
||||
.task-updated {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
@ -2883,22 +2874,11 @@ textarea:focus-visible {
|
||||
color: var(--blue-dark);
|
||||
}
|
||||
|
||||
.task-icon.large {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 11px;
|
||||
}
|
||||
|
||||
.task-icon .icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.task-icon.large .icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.task-copy {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
@ -2933,56 +2913,18 @@ textarea:focus-visible {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.run-result.failed,
|
||||
.overview-run.failed,
|
||||
.overview-error {
|
||||
.run-result.failed {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.run-result.succeeded,
|
||||
.overview-run.succeeded {
|
||||
.run-result.succeeded {
|
||||
color: var(--success-deep);
|
||||
}
|
||||
|
||||
.run-result.cancelled,
|
||||
.overview-run.cancelled {
|
||||
.run-result.cancelled {
|
||||
color: var(--warn-deep);
|
||||
}
|
||||
|
||||
.overview-error {
|
||||
max-width: 210px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.task-status {
|
||||
font-size: 10.5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
flex: none;
|
||||
border-radius: 50%;
|
||||
background: var(--success);
|
||||
box-shadow: 0 0 0 3px rgba(7, 148, 85, 0.08);
|
||||
}
|
||||
|
||||
.task-status.paused .status-dot {
|
||||
background: var(--faint);
|
||||
box-shadow: 0 0 0 3px rgba(161, 172, 186, 0.12);
|
||||
}
|
||||
|
||||
.task-status.paused {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.task-status.enabled {
|
||||
color: var(--success-deep);
|
||||
}
|
||||
|
||||
.task-row-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@ -3036,108 +2978,6 @@ textarea:focus-visible {
|
||||
line-height: 17px;
|
||||
}
|
||||
|
||||
.overview-empty {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.task-overview {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.task-overview-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 11px;
|
||||
}
|
||||
|
||||
.task-overview-head h2 {
|
||||
max-width: 230px;
|
||||
margin-top: 3px;
|
||||
overflow: hidden;
|
||||
color: var(--ink);
|
||||
font-size: 17px;
|
||||
line-height: 23px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.overview-status {
|
||||
margin-top: 16px;
|
||||
color: var(--success-deep);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.overview-sep {
|
||||
width: 1px;
|
||||
height: 14px;
|
||||
background: var(--line-strong);
|
||||
}
|
||||
|
||||
.overview-actions {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.overview-actions .btn {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.overview-meta {
|
||||
margin-top: 20px;
|
||||
display: grid;
|
||||
gap: 0;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.overview-meta > div {
|
||||
min-height: 48px;
|
||||
padding: 8px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.overview-meta dt {
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.overview-meta dd {
|
||||
justify-content: flex-end;
|
||||
color: var(--ink-2);
|
||||
font-size: 11px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.overview-meta dd .icon {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
.overview-note {
|
||||
margin-top: auto;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
border: 1px solid rgba(34, 100, 229, 0.1);
|
||||
border-radius: 9px;
|
||||
background: var(--blue-mist);
|
||||
color: var(--ink-2);
|
||||
font-size: 10.5px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.overview-note .icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex: none;
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
/* ---------- 组件树 ---------- */
|
||||
.palette-expand-all {
|
||||
min-height: 24px;
|
||||
@ -3333,13 +3173,9 @@ textarea:focus-visible {
|
||||
}
|
||||
|
||||
@media (max-width: 1250px) {
|
||||
.tasks-content {
|
||||
grid-template-columns: minmax(0, 1fr) 300px;
|
||||
}
|
||||
|
||||
.task-table-head,
|
||||
.task-row {
|
||||
grid-template-columns: minmax(170px, 1.3fr) 82px minmax(90px, 0.8fr) 96px 96px;
|
||||
grid-template-columns: minmax(170px, 1.3fr) minmax(90px, 0.8fr) 96px 96px 96px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user