Varinput组件添加选择窗口按钮,可以快速填入窗口及元素的句柄、标题等属性

This commit is contained in:
fofolee 2025-01-18 13:39:54 +08:00
parent 0ee3647261
commit a8e0fe9e8f
6 changed files with 490 additions and 400 deletions

View File

@ -9,154 +9,39 @@
>
<template v-slot:append>
<q-btn
v-if="hasSelectedVariable"
flat
dense
icon="close"
size="sm"
class="clear-btn prepend-btn"
@click="clearVariable"
>
<q-tooltip>清除选中的变量</q-tooltip>
</q-btn>
<q-btn
flat
dense
:icon="isString ? 'format_quote' : 'data_object'"
size="sm"
class="string-toggle prepend-btn"
:icon="isString ? 'format_quote' : 'data_object'"
@click="toggleType"
v-if="!hasSelectedVariable"
>
<q-tooltip>{{
(isString
? "当前类型是:字符串"
: "当前类型是:变量、数字、表达式等") +
(disableToggleType ? "" : ",点击切换")
}}</q-tooltip>
<q-tooltip>{{ typeToggleTooltip }}</q-tooltip>
</q-btn>
<!-- 选项下拉按钮 -->
<q-btn-dropdown
v-if="options.items && !hasSelectedVariable"
flat
dense
size="sm"
dropdown-icon="menu"
no-icon-animation
class="options-dropdown prepend-btn"
>
<q-list class="options-item-list">
<template v-if="options.multiSelect">
<q-item
v-for="item in normalizedItems"
:key="getItemValue(item)"
clickable
class="option-item"
@click="toggleSelectItem(item)"
>
<q-checkbox
size="xs"
:model-value="isItemSelected(item)"
@update:model-value="toggleSelectItem(item)"
/>
<div class="option-item-label">{{ getItemLabel(item) }}</div>
</q-item>
<q-separator />
<q-item
clickable
class="option-item"
@click="confirmMultiSelect"
v-close-popup
>
<q-item-section class="text-primary">
确定 (已选择 {{ selectedItems.length }} )
</q-item-section>
</q-item>
</template>
<template v-else>
<q-item
v-for="item in normalizedItems"
:key="getItemValue(item)"
clickable
v-close-popup
@click="selectItem(item)"
class="option-item"
>
<q-item-section>
{{ getItemLabel(item) }}
</q-item-section>
</q-item>
</template>
</q-list>
</q-btn-dropdown>
<q-btn
v-if="!hasSelectedVariable && options.dialog"
flat
dense
icon="file_open"
size="sm"
<ItemList
v-if="options.items"
:items="options.items"
:multiSelect="options.multiSelect"
@emit-value="updateValBySelect"
class="prepend-btn"
@click="handleFileOpen(options.dialog)"
>
<q-tooltip>选择文件</q-tooltip>
</q-btn>
/>
<!-- 文件选择按钮 -->
<FileSelector
v-if="options.dialog"
:dialog="options.dialog"
@emit-value="updateValBySelect"
class="prepend-btn"
/>
<!-- 窗口选择按钮 -->
<WindowSelector
v-if="options.window"
:window="options.window"
@emit-value="updateValBySelect"
class="prepend-btn"
/>
<!-- 变量选择下拉 -->
<q-btn-dropdown
flat
dense
stretch
class="variable-dropdown prepend-btn"
size="sm"
@click="variables = getAvailableVariables()"
>
<q-list class="variable-list">
<q-item-label header class="variable-label">
<q-icon name="functions" size="15px" />
选择变量
</q-item-label>
<q-separator class="q-my-xs" />
<template v-if="variables.length">
<q-item
v-for="variable in variables"
:key="variable.name"
clickable
v-close-popup
@click="insertVariable(variable)"
class="variable-item"
>
<q-item-section>
<q-item-label class="variable-name">
{{ variable.name }}
</q-item-label>
<q-item-label caption class="variable-source">
来自: {{ variable.sourceCommand.label }}
</q-item-label>
</q-item-section>
</q-item>
</template>
<template v-else>
<q-item>
<q-item-section>
<q-item-label class="empty-variables-tip">
<div class="q-gutter-md">
<div class="row items-center justify-center text-grey-6">
<q-icon name="info" size="20px" class="q-mr-sm" />
<span>当前命令没有可用变量</span>
</div>
<div class="row items-center justify-center text-grey-7">
<div class="text-grey-7">点击其他命令卡片右上角的</div>
<q-icon name="output" size="16px" class="q-mx-xs" />
<div>按钮添加输出变量</div>
</div>
</div>
</q-item-label>
</q-item-section>
</q-item>
</template>
</q-list>
</q-btn-dropdown>
<VariableList @emit-value="updateValBySelect" class="prepend-btn" />
</template>
<template v-slot:prepend>
<q-icon v-if="!noIcon" :name="icon || 'code'" />
@ -165,10 +50,17 @@
</template>
<script>
import { defineComponent, inject } from "vue";
import { defineComponent } from "vue";
import { newVarInputVal } from "js/composer/varInputValManager";
import ItemList from "./varinput/ItemList.vue";
import FileSelector from "./varinput/FileSelector.vue";
import VariableList from "./varinput/VariableList.vue";
import WindowSelector from "./varinput/WindowSelector.vue";
/**
* 变量输入框组件
* @description 支持变量选择和字符串输入的输入框组件
*
* @property {Object} modelValue - 输入框的值对象
@ -206,6 +98,13 @@ import { newVarInputVal } from "js/composer/varInputValManager";
export default defineComponent({
name: "VariableInput",
components: {
ItemList,
FileSelector,
VariableList,
WindowSelector,
},
props: {
modelValue: {
type: Object,
@ -227,36 +126,14 @@ export default defineComponent({
},
emits: ["update:modelValue"],
setup() {
const getCurrentVariables = inject("getCurrentVariables");
const commandIndex = inject("commandIndex", null);
const getAvailableVariables = () => {
// commandIndex 使 value
return getCurrentVariables().filter(
(variable) => variable.sourceCommand.index < commandIndex.value
);
};
return {
getAvailableVariables,
};
},
data() {
return {
selectedVariable: null,
variables: [],
selectedItems: [],
};
},
computed: {
// UI
hasSelectedVariable() {
return this.selectedVariable !== null;
},
isString: {
get() {
return this.modelValue.isString;
@ -267,6 +144,11 @@ export default defineComponent({
isString: value,
});
},
typeToggleTooltip() {
const currentType = this.isString ? "字符串" : "变量、数字、表达式等";
const toggleText = this.disableToggleType ? "" : ",点击切换";
return `当前类型是:${currentType}${toggleText}`;
},
},
inputValue: {
@ -280,27 +162,9 @@ export default defineComponent({
});
},
},
//
normalizedItems() {
if (!this.options.items) return [];
return this.options.items.map((item) => {
if (typeof item === "string") {
return { label: item, value: item };
}
return item;
});
},
},
methods: {
//
insertVariable(variable) {
this.selectedVariable = variable;
this.isString = false; //
this.$emit("update:modelValue", newVarInputVal("var", variable.name));
},
//
clearVariable() {
this.selectedVariable = null;
@ -317,75 +181,12 @@ export default defineComponent({
});
},
getItemLabel(option) {
return typeof option === "string" ? option : option.label;
updateValBySelect(type, value) {
const newValue = this.options.appendItem
? this.inputValue + value
: value;
this.$emit("update:modelValue", newVarInputVal(type, newValue));
},
getItemValue(option) {
return typeof option === "string" ? option : option.value;
},
selectItem(option) {
if (this.options.multiSelect) {
this.toggleSelectItem(option);
} else {
const value = this.options.appendItem
? `${this.inputValue}${this.getItemValue(option)}`
: this.getItemValue(option);
this.$emit("update:modelValue", newVarInputVal("str", value));
}
},
handleFileOpen(dialog) {
let { type, options } = window.lodashM.cloneDeep(dialog);
if (!type) type = "open";
if (type === "open") {
const files = utools.showOpenDialog(options);
if (!files) return;
if (files.length > 1) {
this.$emit("update:modelValue", newVarInputVal("var", files));
} else if (files.length === 1) {
this.$emit("update:modelValue", newVarInputVal("str", files[0]));
}
} else {
const file = utools.showSaveDialog(options);
if (!file) return;
this.$emit("update:modelValue", newVarInputVal("str", file));
}
},
//
isItemSelected(item) {
return this.selectedItems.includes(this.getItemValue(item));
},
//
toggleSelectItem(item) {
const value = this.getItemValue(item);
const index = this.selectedItems.indexOf(value);
if (index === -1) {
this.selectedItems.push(value);
} else {
this.selectedItems.splice(index, 1);
}
},
//
confirmMultiSelect() {
if (this.selectedItems.length === 0) return;
this.$emit(
"update:modelValue",
newVarInputVal(
"var",
JSON.stringify(this.selectedItems).replace(/,/g, ", ")
)
);
this.selectedItems = []; //
},
},
//
beforeUnmount() {
this.selectedItems = [];
},
});
</script>
@ -427,106 +228,4 @@ export default defineComponent({
margin-left: 5px;
transition: all 0.6s ease;
}
.variable-dropdown.prepend-btn {
background-color: rgba(0, 0, 0, 0.02);
}
.body--dark .variable-dropdown.prepend-btn {
background-color: rgba(255, 255, 255, 0.02);
}
.clear-btn:hover {
color: var(--q-negative);
}
/* 变量列表样式 */
.variable-list {
min-width: 200px;
padding: 4px;
}
.variable-item {
border-radius: 4px;
padding: 0px 16px;
transition: all 0.3s ease;
min-height: 40px;
}
.variable-item:hover {
background-color: var(--q-primary-opacity-10);
}
.variable-label {
padding: 4px 8px;
display: flex;
align-items: center;
gap: 4px;
}
.variable-name {
font-size: 12px;
font-weight: 500;
}
.variable-source {
font-size: 11px;
opacity: 0.7;
}
/* 暗色模式适配 */
.body--dark .variable-item:hover {
background: rgba(255, 255, 255, 0.1);
}
.options-item-list {
min-width: 120px;
padding: 4px;
}
.option-item {
border-radius: 4px;
padding: 0px 16px;
transition: all 0.3s ease;
min-height: 40px;
font-size: 12px;
display: flex;
align-items: center;
}
.option-item:hover {
background-color: var(--q-primary-opacity-10);
}
.option-item-label {
text-align: center;
flex: 1;
}
/* 暗色模式适配 */
.body--dark .option-item:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.empty-variables-tip {
text-align: center;
font-size: 13px;
opacity: 0.9;
transition: opacity 0.3s ease;
}
.empty-variables-tip:hover {
opacity: 1;
}
/* 多选确认按钮样式 */
.option-item.text-primary {
justify-content: center;
font-weight: 500;
}
/* 多选项样式 */
.option-item .q-checkbox {
margin-right: 4px;
}
</style>

View File

@ -0,0 +1,39 @@
<template>
<q-btn flat dense icon="file_open" size="sm" @click="handleFileOpen">
<q-tooltip>选择文件</q-tooltip>
</q-btn>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "FileSelector",
props: {
dialog: {
type: Object,
required: true,
},
},
emits: ["emitValue"],
methods: {
handleFileOpen() {
let { type, options } = window.lodashM.cloneDeep(this.dialog);
if (!type) type = "open";
if (type === "open") {
const files = utools.showOpenDialog(options);
if (!files) return;
if (files.length > 1) {
this.$emit("emitValue", "var", files);
} else if (files.length === 1) {
this.$emit("emitValue", "str", files[0]);
}
} else {
const file = utools.showSaveDialog(options);
if (!file) return;
this.$emit("emitValue", "str", file);
}
},
},
});
</script>

View File

@ -0,0 +1,174 @@
<template>
<!-- 选项下拉按钮 -->
<q-btn-dropdown flat dense size="sm" dropdown-icon="menu" no-icon-animation>
<q-list class="options-item-list">
<template v-if="multiSelect">
<q-item
v-for="item in normalizedItems"
:key="getItemValue(item)"
clickable
class="option-item"
@click="toggleSelectItem(item)"
>
<q-checkbox
size="xs"
:model-value="isItemSelected(item)"
@update:model-value="toggleSelectItem(item)"
/>
<div class="option-item-label">{{ getItemLabel(item) }}</div>
</q-item>
<q-separator />
<q-item
clickable
class="option-item"
@click="confirmMultiSelect"
v-close-popup
>
<q-item-section class="text-primary">
确定 (已选择 {{ selectedItems.length }} )
</q-item-section>
</q-item>
</template>
<template v-else>
<q-item
v-for="item in normalizedItems"
:key="getItemValue(item)"
clickable
v-close-popup
@click="selectItem(item)"
class="option-item"
>
<q-item-section>
{{ getItemLabel(item) }}
</q-item-section>
</q-item>
</template>
</q-list>
</q-btn-dropdown>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "ItemList",
emits: ["emitValue"],
props: {
items: {
type: Array,
required: true,
},
multiSelect: {
type: Boolean,
default: false,
},
},
data() {
return {
selectedItems: [],
};
},
computed: {
//
normalizedItems() {
if (!this.items) return [];
return this.items.map((item) => {
if (typeof item === "string") {
return { label: item, value: item };
}
return item;
});
},
},
methods: {
getItemLabel(item) {
return typeof item === "string" ? item : item.label;
},
getItemValue(item) {
return typeof item === "string" ? item : item.value;
},
selectItem(item) {
if (this.multiSelect) {
this.toggleSelectItem(item);
} else {
this.$emit("emitValue", "str", this.getItemValue(item));
}
},
//
isItemSelected(item) {
return this.selectedItems.includes(this.getItemValue(item));
},
//
toggleSelectItem(item) {
const value = this.getItemValue(item);
const index = this.selectedItems.indexOf(value);
if (index === -1) {
this.selectedItems.push(value);
} else {
this.selectedItems.splice(index, 1);
}
},
//
confirmMultiSelect() {
if (this.selectedItems.length === 0) return;
this.$emit(
"emitValue",
"var",
JSON.stringify(this.selectedItems).replace(/,/g, ", ")
);
this.selectedItems = []; //
},
},
//
beforeUnmount() {
this.selectedItems = [];
},
});
</script>
<style scoped>
.options-item-list {
min-width: 120px;
padding: 4px;
}
.option-item {
border-radius: 4px;
padding: 0px 16px;
transition: all 0.3s ease;
min-height: 40px;
font-size: 12px;
display: flex;
align-items: center;
}
.option-item:hover {
background-color: var(--q-primary-opacity-10);
}
.option-item-label {
text-align: center;
flex: 1;
}
/* 暗色模式适配 */
.body--dark .option-item:hover {
background-color: rgba(255, 255, 255, 0.1);
}
/* 多选确认按钮样式 */
.option-item.text-primary {
justify-content: center;
font-weight: 500;
}
/* 多选项样式 */
.option-item .q-checkbox {
margin-right: 4px;
}
</style>

View File

@ -0,0 +1,150 @@
<template>
<q-btn-dropdown
flat
dense
stretch
size="sm"
class="variable-dropdown"
@click="variables = getAvailableVariables()"
>
<q-list class="variable-list">
<q-item-label header class="variable-label">
<q-icon name="functions" />
<span>选择变量</span>
</q-item-label>
<q-separator class="q-my-xs" />
<template v-if="variables.length">
<q-item
v-for="variable in variables"
:key="variable.name"
clickable
v-close-popup
@click="insertVariable(variable)"
class="variable-item"
>
<q-item-section>
<q-item-label class="variable-name">
{{ variable.name }}
</q-item-label>
<q-item-label caption class="variable-source">
来自: {{ variable.sourceCommand.label }}
</q-item-label>
</q-item-section>
</q-item>
</template>
<template v-else>
<q-item>
<q-item-section>
<q-item-label class="empty-variables-tip">
<div class="q-gutter-md">
<div class="row items-center justify-center text-grey-6">
<q-icon name="info" size="20px" class="q-mr-sm" />
<span>当前命令没有可用变量</span>
</div>
<div class="row items-center justify-center text-grey-7">
<div class="text-grey-7">点击其他命令卡片右上角的</div>
<q-icon name="output" size="16px" class="q-mx-xs" />
<div>按钮添加输出变量</div>
</div>
</div>
</q-item-label>
</q-item-section>
</q-item>
</template>
</q-list>
</q-btn-dropdown>
</template>
<script>
import { defineComponent, inject } from "vue";
export default defineComponent({
name: "VariableList",
emits: ["emitValue"],
setup() {
const getCurrentVariables = inject("getCurrentVariables");
const commandIndex = inject("commandIndex", null);
const getAvailableVariables = () => {
return getCurrentVariables().filter(
(variable) => variable.sourceCommand.index < commandIndex.value
);
};
return {
getAvailableVariables,
};
},
data() {
return {
variables: [],
};
},
methods: {
insertVariable(variable) {
this.$emit("emitValue", "var", variable.name);
},
},
});
</script>
<style scoped>
.variable-dropdown {
background-color: rgba(0, 0, 0, 0.02);
}
.body--dark .variable-dropdown {
background-color: rgba(255, 255, 255, 0.02);
}
/* 变量列表样式 */
.variable-list {
min-width: 200px;
padding: 4px;
}
.variable-item {
border-radius: 4px;
padding: 0px 16px;
transition: all 0.3s ease;
min-height: 40px;
}
.variable-item:hover {
background-color: var(--q-primary-opacity-10);
}
.variable-label {
padding: 4px 8px;
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
}
.variable-name {
font-size: 12px;
font-weight: 500;
}
.variable-source {
font-size: 11px;
opacity: 0.7;
}
/* 暗色模式适配 */
.body--dark .variable-item:hover {
background: rgba(255, 255, 255, 0.1);
}
.empty-variables-tip {
text-align: center;
font-size: 13px;
opacity: 0.9;
transition: opacity 0.3s ease;
}
.empty-variables-tip:hover {
opacity: 1;
}
</style>

View File

@ -0,0 +1,38 @@
<template>
<q-btn @click="inspectWindow" icon="my_location" flat dense size="sm" />
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "WindowSelector",
emits: ["emitValue"],
props: {
window: {
type: Object,
required: true,
},
},
methods: {
async inspectWindow() {
const { props } = this.window;
window.utools.hideMainWindow();
const selectWindow = await quickcomposer.windows.automation.inspect();
window.utools.showMainWindow()
let propKeys = [];
if (typeof props === "object") {
const result = await quickcommand.showButtonBox(
props.map((item) => item.label),
"请选择要填入的属性"
);
propKeys = props[result.id].value.split(".");
} else {
propKeys = props.split(".");
}
const propValue = propKeys.reduce((acc, key) => acc[key], selectWindow);
this.$emit("emitValue", "str", propValue || "");
},
},
});
</script>

View File

@ -1,52 +1,5 @@
import { newVarInputVal } from "js/composer/varInputValManager.js";
const controlClass = [
// 基础控件
{ value: "Button", label: "按钮 (Button)" },
{ value: "Edit", label: "编辑框 (Edit)" },
{ value: "Static", label: "静态文本 (Static)" },
{ value: "ComboBox", label: "下拉框 (ComboBox)" },
{ value: "ListBox", label: "列表框 (ListBox)" },
{ value: "CheckBox", label: "复选框 (CheckBox)" },
{ value: "RadioButton", label: "单选框 (RadioButton)" },
// 常见对话框控件
{ value: "SysListView32", label: "列表视图 (SysListView32)" },
{ value: "SysTreeView32", label: "树形视图 (SysTreeView32)" },
{ value: "SysTabControl32", label: "选项卡 (SysTabControl32)" },
{ value: "msctls_progress32", label: "进度条 (msctls_progress32)" },
{ value: "msctls_trackbar32", label: "滑块 (msctls_trackbar32)" },
{ value: "msctls_updown32", label: "数字调节器 (msctls_updown32)" },
// 文件对话框相关
{ value: "DirectUIHWND", label: "文件浏览器 (DirectUIHWND)" },
{ value: "ToolbarWindow32", label: "工具栏 (ToolbarWindow32)" },
{ value: "ComboBoxEx32", label: "扩展下拉框 (ComboBoxEx32)" },
// 常见应用程序控件
{ value: "RICHEDIT50W", label: "富文本编辑框 (RICHEDIT50W)" },
{ value: "Scintilla", label: "代码编辑器 (Scintilla)" },
{ value: "WebView2", label: "Edge浏览器 (WebView2)" },
{
value: "Chrome_RenderWidgetHostHWND",
label: "Chrome渲染 (Chrome_RenderWidgetHostHWND)",
},
// 系统控件
{ value: "Shell_TrayWnd", label: "任务栏 (Shell_TrayWnd)" },
{ value: "TrayNotifyWnd", label: "通知区域 (TrayNotifyWnd)" },
{ value: "ReBarWindow32", label: "工具条容器 (ReBarWindow32)" },
{ value: "TaskListThumbnailWnd", label: "任务预览 (TaskListThumbnailWnd)" },
// 通用容器
{ value: "Window", label: "窗口 (Window)" },
{ value: "Dialog", label: "对话框 (Dialog)" },
{ value: "#32770", label: "标准对话框 (#32770)" },
{ value: "MDIClient", label: "MDI客户区 (MDIClient)" },
{ value: "ScrollBar", label: "滚动条 (ScrollBar)" },
{ value: "GroupBox", label: "分组框 (GroupBox)" },
];
const sendKeys = [
// 特殊按键
{ value: "{ENTER}", label: "回车键 (Enter)" },
@ -174,6 +127,16 @@ const searchWindowConfig = [
icon: "title",
width: 9,
placeholder: "标题、类名支持模糊匹配,选择活动窗口无需输入",
options: {
window: {
props: [
{ label: "标题", value: "title" },
{ label: "类名", value: "class" },
{ label: "句柄", value: "handle" },
{ label: "进程名", value: "processName" },
],
},
},
},
];
@ -185,6 +148,11 @@ const windowHandleConfig = [
width: 12,
placeholder: "可从搜索/选择窗口获取,留空则使用当前活动窗口",
defaultValue: newVarInputVal("str", ""),
options: {
window: {
props: "handle",
},
},
},
];
@ -207,6 +175,15 @@ const searchElementConfig = [
label: "查找值",
component: "VariableInput",
icon: "account_tree",
options: {
window: {
props: [
{ label: "XPath", value: "element.xpath" },
{ label: "AutomationId", value: "element.automationId" },
{ label: "Name", value: "element.name" },
],
},
},
width: 8,
placeholder: "XPath: /Pane[3]/Edit[2], 组合条件: name=按钮&type=Button",
},
@ -656,7 +633,9 @@ export const windowsCommands = {
component: "VariableInput",
icon: "filter_alt",
options: {
items: controlClass,
window: {
props: "element.type",
},
},
width: 8,
placeholder: "可选,输入要过滤的控件类型或文本",
@ -698,7 +677,9 @@ export const windowsCommands = {
component: "VariableInput",
icon: "class",
options: {
items: controlClass,
window: {
props: "element.type",
},
},
width: 6,
placeholder: "可选,和文本至少输入一个",
@ -707,6 +688,11 @@ export const windowsCommands = {
label: "控件文本",
component: "VariableInput",
icon: "text_fields",
options: {
window: {
props: "element.name",
},
},
width: 6,
placeholder: "可选,和控件类型至少输入一个",
},
@ -750,7 +736,9 @@ export const windowsCommands = {
label: "目标控件",
component: "VariableInput",
options: {
items: controlClass,
window: {
props: "element.type",
},
},
icon: "class",
width: 8,
@ -793,7 +781,9 @@ export const windowsCommands = {
label: "目标控件",
component: "VariableInput",
options: {
items: controlClass,
window: {
props: "element.type",
},
},
icon: "class",
width: 8,