mirror of
https://github.com/fofolee/uTools-quickcommand.git
synced 2025-06-29 20:32:44 +08:00
模拟操作添加屏幕找图(Mac)
This commit is contained in:
parent
02c1574b5b
commit
ef4726049e
@ -157,6 +157,21 @@ const quickcommand = {
|
|||||||
writeClipboard: function (text) {
|
writeClipboard: function (text) {
|
||||||
electron.clipboard.writeText(text.toString());
|
electron.clipboard.writeText(text.toString());
|
||||||
},
|
},
|
||||||
|
|
||||||
|
readClipboardImage: function () {
|
||||||
|
// 从剪贴板获取图片
|
||||||
|
const image = electron.clipboard.readImage();
|
||||||
|
if (!image.isEmpty()) {
|
||||||
|
return image.toDataURL();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试获取文本(可能是base64格式的图片)
|
||||||
|
const clipboardText = electron.clipboard.readText();
|
||||||
|
if (clipboardText && clipboardText.startsWith("data:image")) {
|
||||||
|
return clipboardText;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
const quickcomposer = {
|
const quickcomposer = {
|
||||||
textProcessing: require("./quickcomposer/textProcessing"),
|
textProcessing: require("./quickcomposer/textProcessing"),
|
||||||
|
simulate: require("./quickcomposer/simulate"),
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = quickcomposer;
|
module.exports = quickcomposer;
|
||||||
|
238
plugin/lib/quickcomposer/simulate/imageFinder.js
Normal file
238
plugin/lib/quickcomposer/simulate/imageFinder.js
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
const { nativeImage } = require("electron");
|
||||||
|
const { captureScreen } = require("./screenCapture");
|
||||||
|
|
||||||
|
// 将颜色值映射到8个区间
|
||||||
|
function mapColorValue(val) {
|
||||||
|
if (val > 223) return 7; // [224 ~ 255]
|
||||||
|
if (val > 191) return 6; // [192 ~ 223]
|
||||||
|
if (val > 159) return 5; // [160 ~ 191]
|
||||||
|
if (val > 127) return 4; // [128 ~ 159]
|
||||||
|
if (val > 95) return 3; // [96 ~ 127]
|
||||||
|
if (val > 63) return 2; // [64 ~ 95]
|
||||||
|
if (val > 31) return 1; // [32 ~ 63]
|
||||||
|
return 0; // [0 ~ 31]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算图像特征向量
|
||||||
|
function calculateFeatureVector(
|
||||||
|
buffer,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
startX = 0,
|
||||||
|
startY = 0,
|
||||||
|
w = width,
|
||||||
|
h = height
|
||||||
|
) {
|
||||||
|
// 8^4 = 4096 维向量,表示RGBA各8个区间的组合
|
||||||
|
const vector = new Array(8 * 8 * 8 * 8).fill(0);
|
||||||
|
|
||||||
|
for (let y = startY; y < startY + h; y++) {
|
||||||
|
for (let x = startX; x < startX + w; x++) {
|
||||||
|
const idx = (y * width + x) * 4;
|
||||||
|
// 计算四个通道的量化值
|
||||||
|
const r = mapColorValue(buffer[idx]);
|
||||||
|
const g = mapColorValue(buffer[idx + 1]);
|
||||||
|
const b = mapColorValue(buffer[idx + 2]);
|
||||||
|
const a = mapColorValue(buffer[idx + 3]);
|
||||||
|
|
||||||
|
// 计算在向量中的位置
|
||||||
|
const vectorIdx = r * 512 + g * 64 + b * 8 + a;
|
||||||
|
vector[vectorIdx]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return vector;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算余弦相似度
|
||||||
|
function calculateCosineSimilarity(v1, v2) {
|
||||||
|
let dotProduct = 0;
|
||||||
|
let norm1 = 0;
|
||||||
|
let norm2 = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < v1.length; i++) {
|
||||||
|
dotProduct += v1[i] * v2[i];
|
||||||
|
norm1 += v1[i] * v1[i];
|
||||||
|
norm2 += v2[i] * v2[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取显示器缩放比例
|
||||||
|
function getDisplayScale() {
|
||||||
|
if (process.platform === "darwin") {
|
||||||
|
// 在 macOS 上,通过比较实际分辨率和报告的分辨率来计算缩放比例
|
||||||
|
const primaryDisplay = utools.getPrimaryDisplay();
|
||||||
|
const { scaleFactor } = primaryDisplay;
|
||||||
|
return scaleFactor;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在屏幕上查找图片
|
||||||
|
async function findImage(targetImageData, options = {}) {
|
||||||
|
try {
|
||||||
|
// 获取屏幕截图
|
||||||
|
const screenDataUrl = await captureScreen();
|
||||||
|
if (!screenDataUrl) return null;
|
||||||
|
|
||||||
|
// 获取显示器缩放比例
|
||||||
|
const scale = getDisplayScale();
|
||||||
|
|
||||||
|
// 读取屏幕截图
|
||||||
|
const screenImage = nativeImage.createFromDataURL(screenDataUrl);
|
||||||
|
const screenBuffer = screenImage.toBitmap();
|
||||||
|
const { width: actualWidth, height: actualHeight } = screenImage.getSize();
|
||||||
|
|
||||||
|
// 计算缩放后的实际尺寸
|
||||||
|
const screenWidth = Math.round(actualWidth / scale);
|
||||||
|
const screenHeight = Math.round(actualHeight / scale);
|
||||||
|
|
||||||
|
// 从 base64 字符串创建目标图片
|
||||||
|
const targetImage = nativeImage.createFromDataURL(targetImageData);
|
||||||
|
const targetBuffer = targetImage.toBitmap();
|
||||||
|
const { width: targetWidth, height: targetHeight } = targetImage.getSize();
|
||||||
|
|
||||||
|
// 计算目标图片的特征向量
|
||||||
|
const targetVector = calculateFeatureVector(
|
||||||
|
targetBuffer,
|
||||||
|
targetWidth,
|
||||||
|
targetHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
// 设置匹配阈值
|
||||||
|
const threshold = options.threshold || 0.9;
|
||||||
|
|
||||||
|
let bestMatch = null;
|
||||||
|
let bestSimilarity = 0;
|
||||||
|
|
||||||
|
// 使用滑动窗口搜索
|
||||||
|
const stepSize = Math.round(8 * scale); // 根据缩放比例调整步长
|
||||||
|
for (let y = 0; y <= actualHeight - targetHeight; y += stepSize) {
|
||||||
|
for (let x = 0; x <= actualWidth - targetWidth; x += stepSize) {
|
||||||
|
// 计算当前区域的特征向量
|
||||||
|
const regionVector = calculateFeatureVector(
|
||||||
|
screenBuffer,
|
||||||
|
actualWidth,
|
||||||
|
actualHeight,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
targetWidth,
|
||||||
|
targetHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
// 计算相似度
|
||||||
|
const similarity = calculateCosineSimilarity(
|
||||||
|
targetVector,
|
||||||
|
regionVector
|
||||||
|
);
|
||||||
|
|
||||||
|
// 更新最佳匹配
|
||||||
|
if (similarity > bestSimilarity) {
|
||||||
|
bestSimilarity = similarity;
|
||||||
|
bestMatch = { x: Math.round(x / scale), y: Math.round(y / scale) };
|
||||||
|
|
||||||
|
// 如果相似度已经很高,进行精确搜索
|
||||||
|
if (similarity >= threshold) {
|
||||||
|
// 在周围进行精确搜索,注意搜索范围也要考虑缩放
|
||||||
|
const searchRange = Math.round(4 * scale);
|
||||||
|
for (let dy = -searchRange; dy <= searchRange; dy++) {
|
||||||
|
for (let dx = -searchRange; dx <= searchRange; dx++) {
|
||||||
|
const newX = x + dx;
|
||||||
|
const newY = y + dy;
|
||||||
|
|
||||||
|
if (
|
||||||
|
newX < 0 ||
|
||||||
|
newY < 0 ||
|
||||||
|
newX > actualWidth - targetWidth ||
|
||||||
|
newY > actualHeight - targetHeight
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const preciseVector = calculateFeatureVector(
|
||||||
|
screenBuffer,
|
||||||
|
actualWidth,
|
||||||
|
actualHeight,
|
||||||
|
newX,
|
||||||
|
newY,
|
||||||
|
targetWidth,
|
||||||
|
targetHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
const preciseSimilarity = calculateCosineSimilarity(
|
||||||
|
targetVector,
|
||||||
|
preciseVector
|
||||||
|
);
|
||||||
|
if (preciseSimilarity > bestSimilarity) {
|
||||||
|
bestSimilarity = preciseSimilarity;
|
||||||
|
bestMatch = {
|
||||||
|
x: Math.round(newX / scale),
|
||||||
|
y: Math.round(newY / scale),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果找到足够好的匹配,提前返回
|
||||||
|
if (bestSimilarity >= threshold) {
|
||||||
|
const position = {
|
||||||
|
x: bestMatch.x,
|
||||||
|
y: bestMatch.y,
|
||||||
|
width: Math.round(targetWidth / scale),
|
||||||
|
height: Math.round(targetHeight / scale),
|
||||||
|
confidence: bestSimilarity,
|
||||||
|
};
|
||||||
|
|
||||||
|
clickImage(position, options.mouseAction);
|
||||||
|
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有找到足够好的匹配,但有最佳匹配且相似度不太低,也返回
|
||||||
|
if (bestMatch && bestSimilarity > threshold * 0.8) {
|
||||||
|
const position = {
|
||||||
|
x: bestMatch.x,
|
||||||
|
y: bestMatch.y,
|
||||||
|
width: Math.round(targetWidth / scale),
|
||||||
|
height: Math.round(targetHeight / scale),
|
||||||
|
confidence: bestSimilarity,
|
||||||
|
};
|
||||||
|
clickImage(position, options.mouseAction);
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("查找图片失败:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clickImage = (position, mouseAction) => {
|
||||||
|
// 计算中心点
|
||||||
|
const centerX = position.x + position.width / 2;
|
||||||
|
const centerY = position.y + position.height / 2;
|
||||||
|
|
||||||
|
// 根据配置执行鼠标动作
|
||||||
|
switch (mouseAction) {
|
||||||
|
case "none":
|
||||||
|
break;
|
||||||
|
case "click":
|
||||||
|
window.utools.simulateMouseClick(centerX, centerY);
|
||||||
|
break;
|
||||||
|
case "dblclick":
|
||||||
|
window.utools.simulateMouseDoubleClick(centerX, centerY);
|
||||||
|
break;
|
||||||
|
case "rightclick":
|
||||||
|
window.utools.simulateMouseRightClick(centerX, centerY);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { findImage };
|
7
plugin/lib/quickcomposer/simulate/index.js
Normal file
7
plugin/lib/quickcomposer/simulate/index.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
const { findImage } = require("./imageFinder");
|
||||||
|
const { captureScreen } = require("./screenCapture");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
findImage,
|
||||||
|
captureScreen,
|
||||||
|
};
|
189
plugin/lib/quickcomposer/simulate/screenCapture.js
Normal file
189
plugin/lib/quickcomposer/simulate/screenCapture.js
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
const { execFile } = require("child_process");
|
||||||
|
const { promisify } = require("util");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const os = require("os");
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile);
|
||||||
|
const readFileAsync = promisify(fs.readFile);
|
||||||
|
const unlinkAsync = promisify(fs.unlink);
|
||||||
|
|
||||||
|
// Windows C# 截图代码
|
||||||
|
const csharpScript = Buffer.from(
|
||||||
|
`
|
||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Drawing.Imaging;
|
||||||
|
|
||||||
|
public class ScreenCapture {
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
static extern IntPtr GetDC(IntPtr hwnd);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
static extern int ReleaseDC(IntPtr hwnd, IntPtr hdc);
|
||||||
|
|
||||||
|
[DllImport("gdi32.dll")]
|
||||||
|
static extern IntPtr CreateCompatibleDC(IntPtr hdc);
|
||||||
|
|
||||||
|
[DllImport("gdi32.dll")]
|
||||||
|
static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int width, int height);
|
||||||
|
|
||||||
|
[DllImport("gdi32.dll")]
|
||||||
|
static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);
|
||||||
|
|
||||||
|
[DllImport("gdi32.dll")]
|
||||||
|
static extern bool BitBlt(IntPtr hdcDest, int xDest, int yDest, int width, int height,
|
||||||
|
IntPtr hdcSrc, int xSrc, int ySrc, int rop);
|
||||||
|
|
||||||
|
[DllImport("gdi32.dll")]
|
||||||
|
static extern bool DeleteDC(IntPtr hdc);
|
||||||
|
|
||||||
|
[DllImport("gdi32.dll")]
|
||||||
|
static extern bool DeleteObject(IntPtr hObject);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
static extern bool GetCursorPos(ref Point point);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
static extern bool GetWindowRect(IntPtr hwnd, ref Rectangle rect);
|
||||||
|
|
||||||
|
public static void CaptureScreen(string outputPath) {
|
||||||
|
IntPtr desktopDC = GetDC(IntPtr.Zero);
|
||||||
|
Rectangle bounds = System.Windows.Forms.Screen.PrimaryScreen.Bounds;
|
||||||
|
|
||||||
|
IntPtr memoryDC = CreateCompatibleDC(desktopDC);
|
||||||
|
IntPtr bitmap = CreateCompatibleBitmap(desktopDC, bounds.Width, bounds.Height);
|
||||||
|
IntPtr oldBitmap = SelectObject(memoryDC, bitmap);
|
||||||
|
|
||||||
|
try {
|
||||||
|
BitBlt(memoryDC, 0, 0, bounds.Width, bounds.Height, desktopDC, 0, 0, 0x00CC0020);
|
||||||
|
|
||||||
|
using (var bmp = Image.FromHbitmap(bitmap)) {
|
||||||
|
bmp.Save(outputPath, ImageFormat.Png);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
SelectObject(memoryDC, oldBitmap);
|
||||||
|
DeleteObject(bitmap);
|
||||||
|
DeleteDC(memoryDC);
|
||||||
|
ReleaseDC(IntPtr.Zero, desktopDC);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
).toString("base64");
|
||||||
|
|
||||||
|
// Windows 截图实现
|
||||||
|
async function captureWindowsScreen() {
|
||||||
|
const tmpFile = path.join(os.tmpdir(), `screen-${Date.now()}.png`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 使用base64编码的C#代码执行
|
||||||
|
const command = `
|
||||||
|
$code = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${csharpScript}'));
|
||||||
|
Add-Type -TypeDefinition $code;
|
||||||
|
[ScreenCapture]::CaptureScreen('${tmpFile.replace(/\\/g, "\\\\")}');
|
||||||
|
`;
|
||||||
|
|
||||||
|
await execFileAsync("powershell", [
|
||||||
|
"-NoProfile",
|
||||||
|
"-NonInteractive",
|
||||||
|
"-ExecutionPolicy",
|
||||||
|
"Bypass",
|
||||||
|
"-Command",
|
||||||
|
command,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 读取截图
|
||||||
|
const imageBuffer = await readFileAsync(tmpFile);
|
||||||
|
return `data:image/png;base64,${imageBuffer.toString("base64")}`;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Windows截图失败:", error);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
// 清理临时文件
|
||||||
|
await unlinkAsync(tmpFile).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// macOS 截图实现
|
||||||
|
async function captureMacScreen() {
|
||||||
|
const tmpFile = path.join(os.tmpdir(), `screen-${Date.now()}.png`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await execFileAsync("screencapture", ["-x", "-C", "-T", "0", tmpFile]);
|
||||||
|
const imageBuffer = await readFileAsync(tmpFile);
|
||||||
|
return `data:image/png;base64,${imageBuffer.toString("base64")}`;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("macOS截图失败:", error);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
await unlinkAsync(tmpFile).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linux 截图实现
|
||||||
|
async function captureLinuxScreen() {
|
||||||
|
const tmpFile = path.join(os.tmpdir(), `screen-${Date.now()}.png`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 检查可用的截图工具
|
||||||
|
let tool = null;
|
||||||
|
try {
|
||||||
|
await execFileAsync("which", ["gnome-screenshot"]);
|
||||||
|
tool = "gnome-screenshot";
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
await execFileAsync("which", ["scrot"]);
|
||||||
|
tool = "scrot";
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
await execFileAsync("which", ["import"]);
|
||||||
|
tool = "import";
|
||||||
|
} catch {
|
||||||
|
console.error("未找到可用的Linux截图工具");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据可用工具执行截图
|
||||||
|
switch (tool) {
|
||||||
|
case "gnome-screenshot":
|
||||||
|
await execFileAsync("gnome-screenshot", ["-f", tmpFile]);
|
||||||
|
break;
|
||||||
|
case "scrot":
|
||||||
|
await execFileAsync("scrot", [tmpFile]);
|
||||||
|
break;
|
||||||
|
case "import":
|
||||||
|
await execFileAsync("import", ["-window", "root", tmpFile]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageBuffer = await readFileAsync(tmpFile);
|
||||||
|
return `data:image/png;base64,${imageBuffer.toString("base64")}`;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Linux截图失败:", error);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
await unlinkAsync(tmpFile).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一的截图接口
|
||||||
|
async function captureScreen() {
|
||||||
|
try {
|
||||||
|
if (process.platform === "darwin") {
|
||||||
|
return await captureMacScreen();
|
||||||
|
} else if (process.platform === "win32") {
|
||||||
|
return await captureWindowsScreen();
|
||||||
|
} else if (process.platform === "linux") {
|
||||||
|
return await captureLinuxScreen();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("截图失败:", error);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { captureScreen };
|
@ -85,7 +85,6 @@ export default defineComponent({
|
|||||||
...action,
|
...action,
|
||||||
id: this.nextId++,
|
id: this.nextId++,
|
||||||
argv: "",
|
argv: "",
|
||||||
argvType: "string",
|
|
||||||
saveOutput: false,
|
saveOutput: false,
|
||||||
outputVariable: null,
|
outputVariable: null,
|
||||||
cmd: action.value || action.cmd,
|
cmd: action.value || action.cmd,
|
||||||
|
@ -137,32 +137,12 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
argvLocal: {
|
argvLocal: {
|
||||||
get() {
|
get() {
|
||||||
if (this.command.hasAxiosEditor) {
|
|
||||||
// 如果是编辑现有配置
|
|
||||||
if (
|
|
||||||
this.command.argv &&
|
|
||||||
!this.command.argv.includes("axios.") &&
|
|
||||||
!this.command.argv.includes("fetch(")
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(this.command.argv);
|
|
||||||
} catch (e) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 如果已经是格式化的代码,直接返回
|
|
||||||
return this.command.argv || {};
|
|
||||||
}
|
|
||||||
return this.command.argv;
|
return this.command.argv;
|
||||||
},
|
},
|
||||||
set(value) {
|
set(value) {
|
||||||
const updatedCommand = {
|
const updatedCommand = {
|
||||||
...this.command,
|
...this.command,
|
||||||
argv: this.command.hasAxiosEditor
|
argv: value,
|
||||||
? typeof value === "string"
|
|
||||||
? value
|
|
||||||
: JSON.stringify(value)
|
|
||||||
: value,
|
|
||||||
};
|
};
|
||||||
this.$emit("update:command", updatedCommand);
|
this.$emit("update:command", updatedCommand);
|
||||||
},
|
},
|
||||||
|
@ -360,8 +360,7 @@ export default defineComponent({
|
|||||||
const tempFlow = [
|
const tempFlow = [
|
||||||
command,
|
command,
|
||||||
{
|
{
|
||||||
value: "console.log",
|
argv: `console.log(${command.outputVariable})`,
|
||||||
argv: command.outputVariable,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
// 触发运行事件
|
// 触发运行事件
|
||||||
|
@ -466,3 +466,10 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.body--dark .q-tab,
|
||||||
|
.body--dark .q-tab-panel {
|
||||||
|
background-color: #303133;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
305
src/components/composer/simulate/ImageSearchEditor.vue
Normal file
305
src/components/composer/simulate/ImageSearchEditor.vue
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
<template>
|
||||||
|
<div class="image-search-editor">
|
||||||
|
<div class="row q-col-gutter-sm">
|
||||||
|
<!-- 图片预览区域 -->
|
||||||
|
<div class="col-12 col-sm-8">
|
||||||
|
<div
|
||||||
|
class="image-preview q-pa-md"
|
||||||
|
:class="{ 'has-image': !!imagePreview }"
|
||||||
|
@click="triggerImageUpload"
|
||||||
|
@paste="handlePaste"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<template v-if="imagePreview">
|
||||||
|
<img :src="imagePreview" class="preview-image" />
|
||||||
|
<q-btn
|
||||||
|
round
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
icon="close"
|
||||||
|
class="remove-image"
|
||||||
|
@click.stop="clearImage"
|
||||||
|
>
|
||||||
|
<q-tooltip>移除图片</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="upload-placeholder">
|
||||||
|
<q-icon name="add_photo_alternate" size="48px" color="grey-6" />
|
||||||
|
<div class="text-grey-6 q-mt-sm">
|
||||||
|
点击上传或粘贴图片<br />支持从剪贴板读取
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 配置区域 -->
|
||||||
|
<div class="col-12 col-sm-4">
|
||||||
|
<div class="row q-col-gutter-sm">
|
||||||
|
<!-- 从剪贴板读取按钮 -->
|
||||||
|
<div class="col-12">
|
||||||
|
<q-btn
|
||||||
|
outline
|
||||||
|
color="primary"
|
||||||
|
class="full-width"
|
||||||
|
@click="pasteFromClipboard"
|
||||||
|
>
|
||||||
|
<q-icon name="content_paste" class="q-mr-sm" />
|
||||||
|
从剪贴板读取
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 匹配阈值设置 -->
|
||||||
|
<div class="col-12">
|
||||||
|
<VariableInput
|
||||||
|
v-model="threshold"
|
||||||
|
label="匹配阈值"
|
||||||
|
class="border-primary"
|
||||||
|
:command="{
|
||||||
|
inputType: 'number',
|
||||||
|
icon: 'tune',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 鼠标动作选择 -->
|
||||||
|
<div class="col-12">
|
||||||
|
<q-select
|
||||||
|
v-model="mouseAction"
|
||||||
|
:options="mouseActionOptions"
|
||||||
|
label="找到后"
|
||||||
|
class="border-primary"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<q-icon name="mouse" />
|
||||||
|
</template>
|
||||||
|
</q-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 隐藏的文件上传input -->
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref="fileInput"
|
||||||
|
accept="image/*"
|
||||||
|
style="display: none"
|
||||||
|
@change="handleFileUpload"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import VariableInput from "../ui/VariableInput.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "ImageSearchEditor",
|
||||||
|
components: {
|
||||||
|
VariableInput,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ["update:modelValue"],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
imagePreview: "",
|
||||||
|
threshold: 0.9,
|
||||||
|
mouseAction: "none",
|
||||||
|
mouseActionOptions: [
|
||||||
|
{ label: "不处理", value: "none" },
|
||||||
|
{ label: "单击", value: "click" },
|
||||||
|
{ label: "双击", value: "dblclick" },
|
||||||
|
{ label: "右击", value: "rightclick" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
modelValue: {
|
||||||
|
immediate: true,
|
||||||
|
handler(val) {
|
||||||
|
if (!val) {
|
||||||
|
// 如果是空字符串,初始化为默认值
|
||||||
|
this.imagePreview = "";
|
||||||
|
this.threshold = 0.9;
|
||||||
|
this.mouseAction = "none";
|
||||||
|
this.updateValue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从代码字符串解析配置
|
||||||
|
try {
|
||||||
|
const imageDataMatch = val.match(/"data:image\/png;base64,([^"]+)"/);
|
||||||
|
const thresholdMatch = val.match(/threshold:\s*([\d.]+)/);
|
||||||
|
const mouseActionMatch = val.match(/mouseAction:\s*"([^"]+)"/);
|
||||||
|
|
||||||
|
if (imageDataMatch) {
|
||||||
|
this.imagePreview = `data:image/png;base64,${imageDataMatch[1]}`;
|
||||||
|
}
|
||||||
|
if (thresholdMatch) {
|
||||||
|
this.threshold = parseFloat(thresholdMatch[1]);
|
||||||
|
}
|
||||||
|
if (mouseActionMatch) {
|
||||||
|
this.mouseAction = mouseActionMatch[1];
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to parse config from code string");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
threshold(val) {
|
||||||
|
this.updateValue();
|
||||||
|
},
|
||||||
|
mouseAction(val) {
|
||||||
|
this.updateValue();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
// 触发文件上传
|
||||||
|
triggerImageUpload() {
|
||||||
|
this.$refs.fileInput.click();
|
||||||
|
},
|
||||||
|
|
||||||
|
// 处理文件上传
|
||||||
|
async handleFileUpload(event) {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
try {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
this.imagePreview = e.target.result;
|
||||||
|
this.updateValue();
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
} catch (error) {
|
||||||
|
quickcommand.showMessageBox("读取图片失败", "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 处理粘贴事件
|
||||||
|
async handlePaste(event) {
|
||||||
|
const items = (event.clipboardData || event.originalEvent.clipboardData)
|
||||||
|
.items;
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.type.indexOf("image") === 0) {
|
||||||
|
const blob = item.getAsFile();
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
this.imagePreview = e.target.result;
|
||||||
|
this.updateValue();
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 从剪贴板读取
|
||||||
|
async pasteFromClipboard() {
|
||||||
|
const clipboardImage = quickcommand.readClipboardImage();
|
||||||
|
if (!clipboardImage)
|
||||||
|
return quickcommand.showMessageBox("剪贴板中没有图片", "warning");
|
||||||
|
this.imagePreview = clipboardImage;
|
||||||
|
this.updateValue();
|
||||||
|
},
|
||||||
|
|
||||||
|
// 清除图片
|
||||||
|
clearImage() {
|
||||||
|
this.imagePreview = "";
|
||||||
|
this.updateValue();
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新值
|
||||||
|
updateValue() {
|
||||||
|
const imageData = this.imagePreview.split(",")[1] || "";
|
||||||
|
const config = {
|
||||||
|
imageData,
|
||||||
|
threshold: this.threshold,
|
||||||
|
mouseAction: this.mouseAction,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生成代码
|
||||||
|
const code = `quickcomposer.simulate.findImage("data:image/png;base64,${config.imageData}", { threshold: ${config.threshold}, mouseAction: "${config.mouseAction}" })`;
|
||||||
|
this.$emit("update:modelValue", code);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.image-search-editor {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview {
|
||||||
|
border: 2px dashed var(--q-primary);
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
max-height: 128px;
|
||||||
|
min-height: 128px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview:hover {
|
||||||
|
border-color: var(--q-primary);
|
||||||
|
background: rgba(var(--q-primary), 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview.has-image {
|
||||||
|
border-style: solid;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-image {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 120px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-image {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-image:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-placeholder {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色模式适配 */
|
||||||
|
:deep(.dark) .image-preview:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-primary {
|
||||||
|
border: 1px solid var(--q-primary);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
0
src/components/composer/simulate/KeyComboEditor.vue
Normal file
0
src/components/composer/simulate/KeyComboEditor.vue
Normal file
@ -4,6 +4,9 @@ import { defineAsyncComponent } from "vue";
|
|||||||
export const KeyEditor = defineAsyncComponent(() =>
|
export const KeyEditor = defineAsyncComponent(() =>
|
||||||
import("components/composer/ui/KeyEditor.vue")
|
import("components/composer/ui/KeyEditor.vue")
|
||||||
);
|
);
|
||||||
|
export const ImageSearchEditor = defineAsyncComponent(() =>
|
||||||
|
import("components/composer/simulate/ImageSearchEditor.vue")
|
||||||
|
);
|
||||||
|
|
||||||
// Control Flow Components
|
// Control Flow Components
|
||||||
export const ConditionalJudgment = defineAsyncComponent(() =>
|
export const ConditionalJudgment = defineAsyncComponent(() =>
|
||||||
|
@ -10,7 +10,7 @@ export const simulateCommands = {
|
|||||||
component: "KeyEditor",
|
component: "KeyEditor",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "utools",
|
value: "utools.simulateMouseClick",
|
||||||
label: "鼠标点击",
|
label: "鼠标点击",
|
||||||
allowEmptyArgv: true,
|
allowEmptyArgv: true,
|
||||||
config: [
|
config: [
|
||||||
@ -77,5 +77,12 @@ export const simulateCommands = {
|
|||||||
config: [],
|
config: [],
|
||||||
allowEmptyArgv: true,
|
allowEmptyArgv: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: "quickcomposer.simulate.findImage",
|
||||||
|
label: "屏幕找图",
|
||||||
|
component: "ImageSearchEditor",
|
||||||
|
config: [],
|
||||||
|
isAsync: true,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
@ -406,6 +406,11 @@ interface quickcommandApi {
|
|||||||
*/
|
*/
|
||||||
readClipboard(): text<string>;
|
readClipboard(): text<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读剪贴板图片
|
||||||
|
*/
|
||||||
|
readClipboardImage(): text<string>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 写剪贴板
|
* 写剪贴板
|
||||||
*
|
*
|
||||||
|
Loading…
x
Reference in New Issue
Block a user