mirror of
https://github.com/fofolee/uTools-quickcommand.git
synced 2025-06-29 12:22:44 +08:00
模拟操作添加屏幕找图(Mac)
This commit is contained in:
parent
02c1574b5b
commit
ef4726049e
@ -157,6 +157,21 @@ const quickcommand = {
|
||||
writeClipboard: function (text) {
|
||||
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") {
|
||||
|
@ -1,5 +1,6 @@
|
||||
const quickcomposer = {
|
||||
textProcessing: require("./quickcomposer/textProcessing"),
|
||||
simulate: require("./quickcomposer/simulate"),
|
||||
};
|
||||
|
||||
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,
|
||||
id: this.nextId++,
|
||||
argv: "",
|
||||
argvType: "string",
|
||||
saveOutput: false,
|
||||
outputVariable: null,
|
||||
cmd: action.value || action.cmd,
|
||||
|
@ -137,32 +137,12 @@ export default defineComponent({
|
||||
},
|
||||
argvLocal: {
|
||||
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;
|
||||
},
|
||||
set(value) {
|
||||
const updatedCommand = {
|
||||
...this.command,
|
||||
argv: this.command.hasAxiosEditor
|
||||
? typeof value === "string"
|
||||
? value
|
||||
: JSON.stringify(value)
|
||||
: value,
|
||||
argv: value,
|
||||
};
|
||||
this.$emit("update:command", updatedCommand);
|
||||
},
|
||||
|
@ -360,8 +360,7 @@ export default defineComponent({
|
||||
const tempFlow = [
|
||||
command,
|
||||
{
|
||||
value: "console.log",
|
||||
argv: command.outputVariable,
|
||||
argv: `console.log(${command.outputVariable})`,
|
||||
},
|
||||
];
|
||||
// 触发运行事件
|
||||
|
@ -466,3 +466,10 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
</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(() =>
|
||||
import("components/composer/ui/KeyEditor.vue")
|
||||
);
|
||||
export const ImageSearchEditor = defineAsyncComponent(() =>
|
||||
import("components/composer/simulate/ImageSearchEditor.vue")
|
||||
);
|
||||
|
||||
// Control Flow Components
|
||||
export const ConditionalJudgment = defineAsyncComponent(() =>
|
||||
|
@ -10,7 +10,7 @@ export const simulateCommands = {
|
||||
component: "KeyEditor",
|
||||
},
|
||||
{
|
||||
value: "utools",
|
||||
value: "utools.simulateMouseClick",
|
||||
label: "鼠标点击",
|
||||
allowEmptyArgv: true,
|
||||
config: [
|
||||
@ -77,5 +77,12 @@ export const simulateCommands = {
|
||||
config: [],
|
||||
allowEmptyArgv: true,
|
||||
},
|
||||
{
|
||||
value: "quickcomposer.simulate.findImage",
|
||||
label: "屏幕找图",
|
||||
component: "ImageSearchEditor",
|
||||
config: [],
|
||||
isAsync: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -406,6 +406,11 @@ interface quickcommandApi {
|
||||
*/
|
||||
readClipboard(): text<string>;
|
||||
|
||||
/**
|
||||
* 读剪贴板图片
|
||||
*/
|
||||
readClipboardImage(): text<string>;
|
||||
|
||||
/**
|
||||
* 写剪贴板
|
||||
*
|
||||
|
Loading…
x
Reference in New Issue
Block a user