可视化编排添加命令配置功能

This commit is contained in:
fofolee 2025-02-12 23:41:49 +08:00
parent 8c564f8d97
commit 2cb0c6bb32
20 changed files with 1147 additions and 243 deletions

View File

@ -4,9 +4,9 @@
ref="sidebar"
:canCommandSave="canCommandSave"
:quickcommandInfo="quickcommandInfo"
:allQuickCommandTags="allQuickCommandTags"
class="absolute-left shadow-1"
:style="{
width: sideBarWidth + 'px',
zIndex: 1,
transform: isFullscreen ? 'translateX(-100%)' : 'translateX(0)',
transition: 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
@ -64,7 +64,11 @@
<!-- 可视化编排 -->
<q-dialog v-model="showComposer" maximized>
<CommandComposer ref="composer" @use-composer="handleComposer" />
<CommandComposer
ref="composer"
@action="handleComposerAction"
:model-value="{ flows }"
/>
</q-dialog>
<!-- 运行结果 -->
@ -78,6 +82,7 @@ import CommandLanguageBar from "components/editor/CommandLanguageBar";
import EditorTools from "components/editor/EditorTools";
import CommandRunResult from "components/CommandRunResult";
import CommandComposer from "components/composer/CommandComposer.vue";
import programs from "js/options/programs.js";
// MonacoEditor
const MonacoEditorPromise = import("components/editor/MonacoEditor");
@ -109,13 +114,22 @@ export default {
},
data() {
return {
programLanguages: Object.keys(this.$root.programs),
programLanguages: Object.keys(programs),
sideBarWidth: 200,
languageBarHeight: 40,
showComposer: false,
isRunCodePage: this.action.type === "run",
canCommandSave: this.action.type !== "run",
showSidebar: this.action.type !== "run",
flows: [
{
id: "main",
name: "main",
label: "主流程",
commands: [],
customVariables: [],
},
],
quickcommandInfo: {
program: "quickcommand",
cmd: "",
@ -141,15 +155,9 @@ export default {
required: true,
},
allQuickCommandTags: Array,
isLeaving: {
type: Boolean,
default: false,
},
},
created() {
this.commandInit();
},
mounted() {
this.commandInit();
this.sidebarInit();
},
computed: {
@ -196,7 +204,7 @@ export default {
//
matchLanguage() {
if (!this.quickcommandInfo.customOptions.ext) return;
let language = Object.values(this.$root.programs).filter(
let language = Object.values(programs).filter(
(program) => program.ext === this.quickcommandInfo.customOptions.ext
);
if (language.length) {
@ -205,7 +213,7 @@ export default {
},
//
setLanguage(language) {
let highlight = this.$root.programs[language].highlight;
let highlight = programs[language].highlight;
this.$refs.editor.setEditorLanguage(highlight ? highlight : language);
},
insertText(text) {
@ -216,14 +224,16 @@ export default {
this.$refs.editor.setEditorValue(text);
this.$refs.editor.formatDocument();
},
handleComposer({ type, code }) {
switch (type) {
handleComposerAction(actionType, actionData) {
switch (actionType) {
case "run":
return this.runCurrentCommand(code);
return this.runCurrentCommand(actionData);
case "insert":
return this.insertText(code);
return this.insertText(actionData);
case "apply":
return this.replaceText(code);
return this.replaceText(actionData);
case "close":
return this.showComposer = false;
}
},
//

View File

@ -75,6 +75,8 @@ import specialVars from "js/options/specialVars.js";
import commandTypes from "js/options/commandTypes.js";
import ResultArea from "components/ResultArea.vue";
import ResultMenu from "components/popup/ResultMenu.vue";
import { generateFlowsCode } from "js/composer/generateCode";
import { getValidCommand } from "js/commandManager";
export default {
components: { ResultArea, ResultMenu },
@ -127,11 +129,20 @@ export default {
methods: {
//
async runCurrentCommand(currentCommand) {
let command = window.lodashM.cloneDeep(currentCommand);
// composercmd
if (command.program === "quickcomposer") {
command.cmd = generateFlowsCode(command.flows);
}
this.$root.isRunningCommand = true;
await this.getTempPayload(currentCommand);
if (currentCommand.cmd.includes("{{subinput"))
return this.setSubInput(currentCommand);
this.fire(currentCommand);
try {
await this.getTempPayload(command);
} catch (error) {
return quickcommand.showMessageBox(error.toString(), "error");
}
//
if (command.cmd.includes("{{subinput")) return this.setSubInput(command);
this.fire(command);
},
async fire(currentCommand) {
currentCommand.cmd = this.assignSpecialVars(currentCommand.cmd);
@ -147,6 +158,7 @@ export default {
let resultOpts = { outPlugin, action, earlyExit };
switch (currentCommand.program) {
case "quickcommand":
case "quickcomposer":
window.runCodeInSandbox(
currentCommand.cmd,
(stdout, stderr) => this.handleResult(stdout, stderr, resultOpts),
@ -278,11 +290,15 @@ export default {
// payload
async getTempPayload(currentCommand) {
if (!this.needTempPayload) return;
let type =
currentCommand.cmdType || currentCommand.features?.cmds[0].type;
currentCommand = getValidCommand(currentCommand);
const firstCmd = currentCommand.features.cmds[0];
const type = firstCmd.type || "text";
this.$root.enterData = {
type: type || "text",
payload: await commandTypes[type]?.tempPayload?.(),
type,
payload:
type === "text"
? firstCmd
: (await commandTypes[type]?.tempPayload?.()) || {},
};
},
handleResult(stdout, stderr, options) {

View File

@ -1,8 +1,9 @@
<template>
<CommandComposer
ref="composer"
@use-composer="handleComposer"
:show-close-button="false"
@action="handleComposerAction"
v-model="quickcommandInfo"
:show-close-button="!isRunComposePage"
class="fixed-full"
/>
<!-- 运行结果 -->
@ -12,29 +13,131 @@
<script>
import CommandComposer from "components/composer/CommandComposer.vue";
import CommandRunResult from "components/CommandRunResult";
import { findCommandByValue } from "js/composer/composerConfig";
import programs from "js/options/programs.js";
import { provide, ref } from "vue";
export default {
components: { CommandComposer, CommandRunResult },
setup(props) {
provide("allQuickCommandTags", props.allQuickCommandTags);
const retoreToFullCommand = (command) => {
const { flows } = command;
if (!flows) return command;
const newFlows = flows.map((flow) => ({
...flow,
commands: flow.commands.map((cmd) => {
//
const command = findCommandByValue(cmd.value);
return {
...command,
...cmd,
};
}),
}));
return {
...command,
flows: newFlows,
};
};
const getLitedComposerCommand = (command) => {
const { flows } = command;
if (!flows) return command;
const newFlows = flows.map((flow) => ({
...flow,
commands: flow.commands.map((cmd) => {
const cmdCopy = { ...cmd };
//
const uselessProps = [
"config",
"label",
"component",
"subCommands",
"outputs",
"options",
"icon",
"width",
"placeholder",
"summary",
"type",
];
uselessProps.forEach((prop) => delete cmdCopy[prop]);
return cmdCopy;
}),
}));
return {
...command,
flows: newFlows,
};
};
const commandAction = window.lodashM.cloneDeep(props.action);
const savedCommand = commandAction.data || {};
const defaultCommand = {
program: "quickcomposer",
features: {
icon: programs.quickcommand.icon,
explain: "",
platform: ["win32", "linux", "darwin"],
mainPush: false,
cmds: [""],
},
flows: [
{
id: "main",
name: "main",
label: "主流程",
commands: [],
customVariables: [],
},
],
output: "text",
tags: [],
};
const quickcommandInfo = ref({
...defaultCommand,
...retoreToFullCommand(savedCommand),
});
return {
quickcommandInfo,
getLitedComposerCommand,
};
},
emits: ["editorEvent"],
props: {
action: {
type: Object,
required: true,
},
allQuickCommandTags: {
type: Array,
default: () => [],
},
},
data() {
return {
isRunComposePage: this.action.type === "composer",
};
},
methods: {
handleComposer({ type, code }) {
switch (type) {
handleComposerAction(actionType, command) {
switch (actionType) {
case "run":
return this.runCurrentCommand(code);
return this.runCurrentCommand(command);
case "close":
return this.$emit("editorEvent", {
type: "back",
});
case "save":
return this.$emit("editorEvent", {
type: "save",
data: this.getLitedComposerCommand(command),
});
}
},
runCurrentCommand(cmd) {
if (!cmd) return;
let command = {
cmd: cmd,
output: "text",
program: "quickcommand",
};
runCurrentCommand(command) {
this.$refs.result.runCurrentCommand(command);
},
},

View File

@ -46,7 +46,7 @@
width="16px"
/>
<div>|</div>
<q-img :src="programs[commandInfo.program].icon" width="16px" />
<q-img :src="program.icon" width="16px" />
<div class="text-subtitle2">{{ programName }}</div>
</div>
</q-card-section>
@ -75,6 +75,12 @@ export default {
},
computed: {
program() {
if (this.commandInfo.program === "quickcomposer") {
return {
...this.programs.quickcommand,
shortName: "可视化",
};
}
return this.programs[this.commandInfo.program];
},
programName() {

View File

@ -70,6 +70,12 @@ export default {
},
computed: {
program() {
if (this.commandInfo.program === "quickcomposer") {
return {
...this.programs.quickcommand,
name: "可视化编排",
};
}
return this.programs[this.commandInfo.program];
},
},

View File

@ -10,8 +10,10 @@
<!-- 右侧命令流程 -->
<div class="col command-section">
<FlowTabs
@action="handleComposer"
@action="handleAction"
:show-close-button="showCloseButton"
:model-value="modelValue"
@update:model-value="$emit('update:modelValue', $event)"
/>
</div>
</div>
@ -44,12 +46,16 @@ export default defineComponent({
type: Boolean,
default: true,
},
modelValue: {
type: Object,
default: () => ({}),
},
},
emits: ["use-composer"],
emits: ["action", "update:modelValue"],
methods: {
handleComposer(type, code) {
handleAction(actionType, actionData) {
//
this.$emit("use-composer", { type, code });
this.$emit("action", actionType, actionData);
},
findCommandNeedLoading(flow) {
//

View File

@ -197,7 +197,7 @@ export default defineComponent({
};
const consoleLogVars =
this.getAvailableOutputVariableName(outputVariable);
const tempFlow = {
const tempFlows = [{
name: "main",
commands: [
tempCommand,
@ -207,9 +207,10 @@ export default defineComponent({
console.log(${consoleLogVars})
}`,
},
],
};
this.$emit("run", tempFlow);
],
},
];
this.$emit("run", tempFlows);
},
handleToggleCollapse() {
if (this.localCommand.isControlFlow) {

View File

@ -82,10 +82,6 @@ export default defineComponent({
required: true,
default: () => [],
},
generateCode: {
type: Function,
required: true,
},
showCloseButton: {
type: Boolean,
default: true,

View File

@ -59,9 +59,9 @@
</div>
<ComposerButtons
:generate-code="generateAllFlowCode"
:is-all-collapsed="isAllCollapsed"
:show-close-button="showCloseButton"
:flows="flows"
@action="handleAction"
/>
</div>
@ -72,10 +72,15 @@
:key="flow.id"
v-show="activeTab === flow.id"
>
<CommandConfig
class="command-config-panel"
v-if="flow.id === 'main' && commandConfig.features"
:model-value="commandConfig"
@update:model-value="updateCommandConfig"
/>
<ComposerFlow
class="flow-wrapper"
v-model="flow.commands"
:generate-code="() => generateFlowCode(flow)"
:show-close-button="flows.length > 1"
@action="(type, payload) => handleAction(type, payload)"
ref="flowRefs"
@ -84,7 +89,7 @@
v-model="showVariableManager"
:flow="flow"
:variables="flow.customVariables"
@update-flow="Sub(flow)"
@update-flow="updateFlows(flow)"
:is-main-flow="flow.id === 'main'"
:output-variables="outputVariables"
class="variable-panel"
@ -99,10 +104,10 @@ import draggable from "vuedraggable";
import ComposerFlow from "components/composer/ComposerFlow.vue";
import ComposerButtons from "components/composer/flow/ComposerButtons.vue";
import FlowManager from "components/composer/flow/FlowManager.vue";
import { generateCode } from "js/composer/generateCode";
import { findCommandByValue } from "js/composer/composerConfig";
import CommandConfig from "components/editor/CommandConfig.vue";
import { generateUniqSuffix } from "js/composer/variableManager";
import { getUniqueId } from "js/common/uuid";
export default defineComponent({
name: "FlowTabs",
components: {
@ -110,23 +115,44 @@ export default defineComponent({
ComposerButtons,
draggable,
FlowManager,
CommandConfig,
},
emits: ["update:modelValue", "action"],
props: {
showCloseButton: {
type: Boolean,
default: true,
},
modelValue: {
type: Object,
default: () => ({}),
},
},
setup() {
const mainFlow = ref({
id: "main",
name: "main",
label: "主流程",
commands: [],
customVariables: [],
setup(props, { emit }) {
const updateFlows = (newFlows) => {
emit("update:modelValue", {
...props.modelValue,
flows: newFlows,
});
};
const flows = computed(() => props.modelValue.flows);
const mainFlow = computed({
get: () => flows.value[0],
set: (newVal) => {
const newFlows = [newVal, ...flows.value.slice(1)];
updateFlows(newFlows);
},
});
const subFlows = ref([]);
const subFlows = computed({
get: () => flows.value.slice(1),
set: (newVal) => {
const newFlows = [mainFlow.value, ...newVal];
updateFlows(newFlows);
},
});
//
const getCurrentFunctions = () => {
@ -140,8 +166,6 @@ export default defineComponent({
};
provide("getCurrentFunctions", getCurrentFunctions);
const flows = computed(() => [mainFlow.value, ...subFlows.value]);
const activeTab = ref("main");
const getCurrentFlow = () => {
@ -208,6 +232,7 @@ export default defineComponent({
subFlows,
activeTab,
getOutputVariables,
updateFlows,
};
},
data() {
@ -217,6 +242,12 @@ export default defineComponent({
outputVariables: [],
};
},
computed: {
commandConfig() {
const { tags, output, features } = this.modelValue;
return { tags, output, features };
},
},
methods: {
generateFlowName(baseName = "func_") {
return (
@ -249,7 +280,7 @@ export default defineComponent({
});
}
this.subFlows.push(newFlow);
this.subFlows = [...this.subFlows, newFlow];
if (options.silent) {
return;
@ -263,7 +294,10 @@ export default defineComponent({
removeFlow(flow) {
const index = this.subFlows.findIndex((f) => f.id === flow.id);
if (index > -1) {
this.subFlows.splice(index, 1);
this.subFlows = [
...this.subFlows.slice(0, index),
...this.subFlows.slice(index + 1),
];
this.activeTab = this.flows[0].id;
}
},
@ -279,25 +313,13 @@ export default defineComponent({
//
this.subFlows[index].customVariables = [...newParams, ...localVars];
},
generateFlowCode(flow) {
return generateCode(flow);
},
generateAllFlowCode() {
// flow
return [...this.subFlows, this.mainFlow]
.map((flow) => this.generateFlowCode(flow))
.join("\n\n");
},
handleAction(type, payload) {
switch (type) {
case "save":
this.saveFlows();
break;
case "load":
this.loadFlows();
break;
case "run":
this.runFlows(payload);
this.runCommand(payload);
break;
case "collapseAll":
this.collapseAll();
@ -308,6 +330,9 @@ export default defineComponent({
case "toggleVariableManager":
this.toggleVariableManager();
break;
case "close":
this.$emit("action", "close");
break;
case "addFlow":
//
const index = this.subFlows.findIndex((f) => f.name === payload.name);
@ -319,7 +344,7 @@ export default defineComponent({
}
break;
default:
this.$emit("action", type, this.generateAllFlowCode());
this.$emit("action", type, payload);
}
},
toggleVariableManager() {
@ -327,61 +352,17 @@ export default defineComponent({
this.outputVariables = this.getOutputVariables();
},
saveFlows() {
const flowsData = this.flows.map((flow) => ({
...flow,
commands: flow.commands.map((cmd) => {
const cmdCopy = { ...cmd };
//
const uselessProps = [
"config",
"code",
"label",
"component",
"subCommands",
"outputs",
"options",
"defaultValue",
"icon",
"width",
"placeholder",
"summary",
"type",
"defaultOutputVariable",
];
uselessProps.forEach((prop) => delete cmdCopy[prop]);
return cmdCopy;
}),
}));
localStorage.setItem("quickcomposer.flows", JSON.stringify(flowsData));
quickcommand.showMessageBox("保存成功");
this.$emit("action", "save", {
...this.modelValue,
flows: this.flows,
});
},
loadFlows() {
const savedFlows = localStorage.getItem("quickcomposer.flows");
if (!savedFlows) return;
const flowsData = JSON.parse(savedFlows);
const newFlows = flowsData.map((flow) => ({
...flow,
commands: flow.commands.map((cmd) => {
//
const command = findCommandByValue(cmd.value);
return {
...command,
...cmd,
};
}),
}));
this.Sub(newFlows);
this.activeTab = this.mainFlow.id;
},
runFlows(flow) {
const code = flow
? this.generateFlowCode(flow)
: this.generateAllFlowCode();
this.$emit("action", "run", code);
if (!code.includes("console.log")) {
quickcommand.showMessageBox("已运行");
}
runCommand(flows = this.flows) {
const command = {
...this.modelValue,
flows,
};
this.$emit("action", "run", command);
},
collapseAll() {
this.$refs.flowRefs.forEach((flow) => {
@ -399,9 +380,12 @@ export default defineComponent({
this.activeTab = flow.id;
this.toggleVariableManager();
},
Sub(flow) {
this.mainFlow = flow[0];
this.subFlows = flow.slice(1);
updateCommandConfig(newVal) {
const newModelValue = {
...this.modelValue,
...newVal,
};
this.$emit("update:modelValue", newModelValue);
},
},
});
@ -556,4 +540,8 @@ export default defineComponent({
border-bottom: 2px solid var(--q-primary);
transition: all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.command-config-panel {
padding: 8px;
}
</style>

View File

@ -57,9 +57,6 @@
<q-btn dense flat icon="save" @click="$emit('action', 'save')">
<q-tooltip>保存</q-tooltip>
</q-btn>
<q-btn dense flat icon="history" @click="$emit('action', 'load')">
<q-tooltip>载入</q-tooltip>
</q-btn>
<q-btn flat dense icon="preview" @click="isVisible = true">
<q-tooltip>预览代码</q-tooltip>
</q-btn>
@ -92,15 +89,12 @@
<script>
import { defineComponent } from "vue";
import { generateFlowsCode } from "js/composer/generateCode";
export default defineComponent({
name: "ComposerButtons",
props: {
generateCode: {
type: Function,
required: true,
},
isAllCollapsed: {
type: Boolean,
default: false,
@ -109,6 +103,10 @@ export default defineComponent({
type: Boolean,
default: true,
},
flows: {
type: Array,
default: () => [],
},
},
emits: ["action"],
@ -124,7 +122,7 @@ export default defineComponent({
watch: {
isVisible(val) {
if (val) {
this.code = this.generateCode();
this.code = generateFlowsCode(this.flows);
}
},
},
@ -172,16 +170,6 @@ export default defineComponent({
/* 自定义滚动条 */
.preview-code::-webkit-scrollbar {
width: 6px;
}
.preview-code::-webkit-scrollbar-thumb {
background: var(--q-primary-opacity-20);
border-radius: 3px;
transition: background 0.3s;
}
.preview-code::-webkit-scrollbar-thumb:hover {
background: var(--q-primary-opacity-30);
width: 5px;
}
</style>

View File

@ -0,0 +1,336 @@
<template>
<q-expansion-item
v-model="isExpanded"
class="command-config"
@dragover="isExpanded = false"
>
<template v-slot:header>
<div class="row q-col-gutter-sm basic-config">
<div class="col-auto">
<q-avatar size="36px" square class="featureIco">
<q-img
@click.stop="showIconPicker = true"
:src="
currentCommand.features.icon
"
/>
</q-avatar>
</div>
<div class="col">
<q-input
:model-value="currentCommand.features.explain"
filled
dense
@update:model-value="updateCommand('features.explain', $event)"
placeholder="名称"
@click.stop
>
<template v-slot:append>
<q-icon name="drive_file_rename_outline" />
</template>
</q-input>
</div>
</div>
</template>
<!-- 展开的配置项 -->
<div class="expanded-config">
<!-- 匹配规则 -->
<div class="config-section">
<div class="section-title">
<q-icon name="rule" size="16px" />
<span class="q-ml-sm">匹配规则</span>
<q-icon
name="help"
size="16px"
class="q-ml-sm cursor-pointer"
@click="showMatchRuleHelp"
>
<q-tooltip>查看帮助</q-tooltip>
</q-icon>
<q-icon
name="data_object"
size="16px"
class="q-ml-sm cursor-pointer"
:color="showMatchRuleJson ? 'primary' : 'grey'"
@click="showMatchRuleJson = !showMatchRuleJson"
>
<q-tooltip>编辑JSON配置</q-tooltip>
</q-icon>
</div>
<MatchRuleEditor
:showJson="showMatchRuleJson"
:model-value="currentCommand.features.cmds"
@update:model-value="updateCommand('features.cmds', $event)"
/>
</div>
<!-- 标签 -->
<div class="config-section">
<div class="section-title">
<q-icon name="label" size="16px" />
<span class="q-ml-sm">标签</span>
</div>
<q-select
:model-value="currentCommand.tags"
@update:model-value="updateCommand('tags', $event)"
:options="allQuickCommandTags"
dense
options-dense
filled
use-input
use-chips
multiple
hide-dropdown-icon
new-value-mode="add-unique"
placeholder="回车添加最多3个"
max-values="3"
@new-value="tagVerify"
input-debounce="0"
ref="commandTagRef"
@blur="(e) => autoAddInputVal(e, $refs.commandTagRef)"
/>
</div>
<!-- 输出 -->
<div class="config-section">
<div class="section-title">
<q-icon name="output" size="16px" />
<span class="q-ml-sm">输出</span>
</div>
<div class="row q-col-gutter-sm">
<div class="col-12">
<ButtonGroup
:model-value="currentCommand.output"
:options="outputTypesOptionsDy"
@update:model-value="updateCommand('output', $event)"
height="26px"
/>
</div>
<div class="col-12">
<div class="section-title">
<q-icon name="search" size="16px" />
<span class="q-ml-sm"
>搜索面板推送
<q-btn
flat
round
size="xs"
icon="help"
@click="showMainPushHelp"
/>
</span>
</div>
<ButtonGroup
:model-value="currentCommand.features.mainPush"
:options="searchPushOptions"
@update:model-value="handleMainPushChange"
height="30px"
/>
</div>
</div>
</div>
<!-- 平台 -->
<div class="config-section">
<div class="section-title">
<q-icon name="devices" size="16px" />
<span class="q-ml-sm">平台</span>
</div>
<CheckGroup
:model-value="currentCommand.features.platform"
:options="Object.values(platformTypes)"
@update:model-value="updateCommand('features.platform', $event)"
height="30px"
/>
</div>
</div>
<!-- 图标选择对话框 -->
<q-dialog v-model="showIconPicker" position="left">
<iconPicker
@iconChanged="(dataUrl) => updateCommand('features.icon', dataUrl)"
ref="icon"
/>
</q-dialog>
</q-expansion-item>
</template>
<script>
import { defineComponent, inject, computed } from "vue";
import iconPicker from "components/popup/IconPicker.vue";
import outputTypes from "js/options/outputTypes.js";
import platformTypes from "js/options/platformTypes.js";
import CheckGroup from "components/composer/common/CheckGroup.vue";
import ButtonGroup from "components/composer/common/ButtonGroup.vue";
import commandTypes from "js/options/commandTypes.js";
import MatchRuleEditor from "components/editor/MatchRuleEditor.vue";
export default defineComponent({
name: "CommandConfig",
components: {
iconPicker,
CheckGroup,
ButtonGroup,
MatchRuleEditor,
},
props: {
modelValue: {
type: Object,
required: true,
},
},
emits: ["update:modelValue"],
setup(props) {
const allQuickCommandTags = inject("allQuickCommandTags").filter(
(tag) => !["默认", "未分类", "搜索结果"].includes(tag)
);
const currentCommand = computed(() => props.modelValue);
return {
allQuickCommandTags,
currentCommand,
};
},
data() {
return {
isExpanded: false,
showIconPicker: false,
showMatchRuleJson: false,
commandTypes,
outputTypes,
platformTypes,
isFileTypeDirectory: false,
searchPushOptions: [
{ value: false, label: "禁用(进入插件后才执行命令)" },
{ value: true, label: "启用在uTools主搜索框直接执行命令" },
],
};
},
computed: {
commandTypesOptions() {
const options = Object.values(this.commandTypes);
return this.currentCommand.features.mainPush
? options.map((cmdType) =>
["regex", "over", "key"].includes(cmdType.name)
? cmdType
: { ...cmdType, disabled: true }
)
: options;
},
outputTypesOptionsDy() {
const options = Object.values(this.outputTypes);
return this.currentCommand.features.mainPush
? options.map((outputType) =>
outputType.name !== "text"
? { ...outputType, disabled: true }
: outputType
)
: options;
},
},
methods: {
updateCommand(path, value) {
const newCommand = { ...this.currentCommand };
const keys = path.split(".");
const lastKey = keys.pop();
const target = keys.reduce((obj, key) => obj[key], newCommand);
target[lastKey] = value;
this.$emit("update:modelValue", newCommand);
},
tagVerify(val, done) {
if (["默认", "未分类", "搜索结果"].includes(val)) {
return done(`_${val}_`);
}
done(val);
},
autoAddInputVal(e, ref) {
const inputValue = e.target.value;
if (!inputValue) return;
ref.add(inputValue, true);
},
handleMainPushChange(val) {
this.updateCommand("features.mainPush", val);
if (val) {
this.updateCommand("output", "text");
}
},
showMainPushHelp() {
window.showUb.help("#u0e9f1430");
},
showMatchRuleHelp() {
utools.ubrowser
.goto(
"https://www.u-tools.cn/docs/developer/information/plugin-json.html#%E5%8A%9F%E8%83%BD%E6%8C%87%E4%BB%A4"
)
.run();
},
},
});
</script>
<style scoped>
.command-config {
border-radius: 8px;
overflow: hidden;
}
.command-config :deep(.q-field__inner) {
font-family: "Consolas", "Monaco", monospace;
}
.basic-config {
width: 100%;
}
.command-config :deep(.q-item) {
padding: 0;
min-height: unset;
cursor: default;
}
.command-config :deep(.q-item__section--side) {
padding: 0;
}
.command-config :deep(.q-item:hover) {
background: transparent;
}
.command-config :deep(.q-focus-helper) {
display: none;
}
.config-section {
margin-bottom: 4px;
}
.section-title {
display: flex;
align-items: center;
font-size: 13px;
color: var(--q-primary);
padding: 4px 2px;
user-select: none;
}
.featureIco {
cursor: pointer;
transition: 0.2s;
}
.featureIco:hover {
transform: scale(1.02) translateY(-2px);
}
.expanded-config {
overflow-y: auto;
max-height: 450px;
padding: 0 5px;
}
::-webkit-scrollbar {
width: 1px;
}
</style>

View File

@ -0,0 +1,449 @@
<template>
<div class="match-rule-editor">
<!-- JSON编辑模式 -->
<div v-if="showJson" class="json-editor">
<q-input
:model-value="jsonText"
@update:model-value="updateJsonText($event)"
type="textarea"
filled
autogrow
/>
</div>
<!-- 可视化编辑模式 -->
<div v-else class="visual-editor">
<!-- 规则类型选择按钮组 -->
<div class="rule-type-buttons q-mb-sm q-mt-xs">
<div v-for="type in ruleTypeOptions" :key="type.value" class="col-auto">
<q-btn
:label="type.label"
:icon="type.icon"
:color="type.color"
outline
dense
class="rule-type-btn"
@click="addRuleByType(type.value)"
>
<q-badge
v-if="ruleTypeCounts[type.value]"
floating
:label="ruleTypeCounts[type.value]"
/>
</q-btn>
</div>
</div>
<!-- 规则列表 -->
<div class="rules-container">
<!-- 关键词匹配规则组 -->
<div v-if="keyRules.length" class="key-rules-row">
<div
v-for="(rule, index) in keyRules"
:key="'key-' + index"
class="key-input-wrapper"
>
<q-input
:model-value="rule"
@update:model-value="updateModelValueByIndex($event, index)"
dense
filled
:label="`关键词 ${index + 1}`"
>
<template v-slot:append>
<q-icon
name="cancel"
@click="removeRule(index)"
size="12px"
class="cursor-pointer text-grey"
/>
</template>
</q-input>
</div>
</div>
<!-- 其他类型规则 -->
<div
v-for="(rule, index) in nonKeyRules"
:key="'other-' + index"
class="rule-item"
>
<div class="row items-center">
<div class="col-auto q-mr-sm">
<q-field dense filled>
<template v-slot:control>
<q-icon :name="commandTypes[rule.type].icon" />
<div class="q-mx-xs" style="user-select: none">
{{ commandTypes[rule.type].label }}
</div>
<q-icon
name="cancel"
@click="removeRule(index + keyRules.length)"
size="12px"
class="cursor-pointer text-grey"
/>
</template>
</q-field>
</div>
<!-- 正则匹配 -->
<template v-if="rule.type === 'regex'">
<div class="col row q-gutter-sm">
<q-input
v-model="rule.match"
dense
filled
label="匹配文本正则表达式"
placeholder="例:/xxx/,任意匹配的正则会被 uTools 忽略"
class="col"
@blur="validateRegex(rule)"
/>
<q-input
v-model.number="rule.minLength"
type="number"
dense
filled
label="最小长度"
placeholder="可选"
style="width: 65px"
/>
<q-input
v-model.number="rule.maxLength"
type="number"
dense
filled
label="最大长度"
placeholder="可选"
style="width: 65px"
/>
</div>
</template>
<!-- 文件匹配 -->
<template v-else-if="rule.type === 'files'">
<div class="col row q-gutter-sm">
<q-input
v-model="rule.match"
dense
filled
label="匹配文件(夹)名正则表达式"
placeholder="可选,例:/xxx/"
class="col"
@blur="validateRegex(rule)"
/>
<q-select
:model-value="rule.fileType || 'file'"
@update:model-value="rule.fileType = $event"
:options="[
{ label: '文件', value: 'file' },
{ label: '文件夹', value: 'directory' },
]"
label="文件类型"
dense
options-dense
filled
emit-value
map-options
class="col-2"
/>
<q-input
v-model.number="rule.minLength"
type="number"
dense
filled
label="最小数量"
placeholder="可选"
style="width: 65px"
/>
<q-input
v-model.number="rule.maxLength"
type="number"
dense
filled
label="最大数量"
placeholder="可选"
style="width: 65px"
/>
</div>
</template>
<!-- 窗口匹配 -->
<template v-else-if="rule.type === 'window'">
<div class="col row q-gutter-sm">
<q-input
:model-value="rule.match.app.join(',')"
@update:model-value="
rule.match.app = getArrayFromString($event)
"
dense
filled
class="col"
label="程序/应用名,逗号隔开"
placeholder="例xxx.exe,xxx.app"
/>
<q-input
v-model="rule.match.title"
dense
filled
label="匹配窗口标题正则表达式"
placeholder="可选,例:/xxx/"
class="col-5"
@blur="validateRegex({ match: rule.match.title })"
/>
</div>
</template>
<!-- 图片匹配 -->
<template v-else-if="rule.type === 'img'">
<q-field v-model="rule.label" dense filled class="col">
<template v-slot:control>
<div>无需配置</div>
</template>
</q-field>
</template>
<!-- 所有文本匹配 -->
<template v-else-if="rule.type === 'over'">
<div class="col row q-gutter-sm">
<q-input
v-model="rule.exclude"
dense
filled
label="排除的正则表达式字符串"
placeholder="可选,例:/xxx/"
class="col"
@blur="validateRegex({ match: rule.exclude })"
/>
<q-input
v-model.number="rule.minLength"
type="number"
dense
filled
label="最小长度"
placeholder="可选"
style="width: 65px"
/>
<q-input
v-model.number="rule.maxLength"
type="number"
dense
filled
label="最大长度"
placeholder="可选"
style="width: 65px"
/>
</div>
</template>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { defineComponent } from "vue";
import commandTypes from "js/options/commandTypes.js";
export default defineComponent({
name: "MatchRuleEditor",
props: {
modelValue: {
type: Array,
required: true,
},
showJson: {
type: Boolean,
default: false,
},
},
emits: ["update:modelValue"],
data() {
return {
ruleTypeOptions: Object.values(commandTypes)
.filter((type) => type.name !== "professional")
.map((type) => ({
label: type.label,
value: type.name,
icon: type.icon,
color: type.color,
})),
commandTypes,
};
},
computed: {
localRules() {
return [...this.keyRules, ...this.nonKeyRules];
},
//
keyRules() {
return this.modelValue.filter((rule) => typeof rule === "string");
},
//
nonKeyRules() {
return this.modelValue.filter((rule) => typeof rule !== "string");
},
// JSON
jsonText() {
return JSON.stringify(this.modelValue, null, 2);
},
//
ruleTypeCounts() {
return this.localRules.reduce((counts, rule) => {
const type = typeof rule === "string" ? "key" : rule.type;
counts[type] = (counts[type] || 0) + 1;
return counts;
}, {});
},
},
methods: {
validateRegex(rule) {
const matchValue = rule.match;
if (!matchValue) return;
try {
if (!matchValue.startsWith("/")) {
rule.match = `/${matchValue}/`;
}
new RegExp(matchValue.replace(/^\/|\/[gimuy]*$/g, ""));
} catch (e) {
rule.match = "/./";
}
},
removeRule(index) {
const newRules = [...this.localRules];
newRules.splice(index, 1);
if (newRules.length == 0) {
return;
}
this.updateModelValue(newRules);
},
updateJsonText(newJsonText) {
try {
const parsed = JSON.parse(newJsonText);
const validConfig = parsed.filter((rule) => {
if (typeof rule === "string") return true;
return Object.values(commandTypes).some(
(type) => type.name === rule.type
);
});
this.updateModelValue(validConfig);
} catch (_) {}
},
updateModelValue(newRules) {
this.$emit("update:modelValue", newRules);
},
updateModelValueByIndex(newValue, index) {
const newRules = [...this.localRules];
newRules[index] = newValue;
this.updateModelValue(newRules);
},
getArrayFromString(str) {
return str
.split(",")
.filter(Boolean)
.map((item) => item.trim());
},
//
addRuleByType(type) {
const newRules = [...this.localRules];
if (type === "key") {
//
newRules.splice(this.keyRules.length, 0, "");
} else {
//
newRules.push({
type,
match: {
window: { app: [] },
regex: "",
}[type],
});
}
this.updateModelValue(newRules);
},
},
});
</script>
<style scoped>
.rule-type-buttons {
display: flex;
gap: 8px;
justify-content: space-between;
}
.rule-type-buttons :deep(.q-btn) {
padding: 0;
font-size: 12px;
height: 24px;
min-width: 85px;
}
.body--dark .rule-type-count {
background-color: rgba(255, 255, 255, 0.9);
}
.rules-container {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: -4px;
}
.key-rules-row {
display: flex;
gap: 8px;
}
.key-input-wrapper {
flex: 1;
min-width: 0;
}
.key-input-wrapper :deep(.q-field__append) {
padding: 0 4px;
}
.key-input-wrapper :deep(.q-icon) {
font-size: 16px;
opacity: 0.7;
transition: opacity 0.3s;
}
.key-input-wrapper :deep(.q-icon:hover) {
opacity: 1;
}
.json-editor {
font-family: monospace;
}
.json-editor :deep(.q-field__native) {
min-height: 200px;
font-family: monospace;
}
/* 隐藏默认的数字输入框箭头 - Chrome, Safari, Edge, Opera */
.match-rule-editor :deep(input[type="number"]::-webkit-outer-spin-button),
.match-rule-editor :deep(input[type="number"]::-webkit-inner-spin-button) {
-webkit-appearance: none;
margin: 0;
}
</style>

View File

@ -25,6 +25,10 @@
font-size: 12px;
}
.command-composer .q-field--filled.q-select--with-chips .q-field__control .q-chip {
margin: 0 4px;
}
/* 输入框图标大小 */
.command-composer .q-field--filled .q-field__control .q-icon {
font-size: 18px;

View File

@ -1,66 +0,0 @@
const winScpt = `Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
public class Win32 {
[StructLayout(LayoutKind.Sequential)]
public struct RECT {
public int Left;
public int Top;
public int Right;
public int Bottom;
}
[DllImport("user32.dll", SetLastError=true)]
public static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll", SetLastError=true)]
public static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect);
}
"@
$foregroundWindow = [Win32]::GetForegroundWindow()
$windowRect = New-Object Win32+RECT
$_ = [Win32]::GetWindowRect($foregroundWindow, [ref]$windowRect)
$result = New-Object PSObject
$result | Add-Member -Type NoteProperty -Name Left -Value $windowRect.Left
$result | Add-Member -Type NoteProperty -Name Top -Value $windowRect.Top
$result | Add-Member -Type NoteProperty -Name Right -Value $windowRect.Right
$result | Add-Member -Type NoteProperty -Name Bottom -Value $windowRect.Bottom
$result | ConvertTo-Json`;
const macScpt = `tell application "System Events"
set frontmostProcess to first application process where it is frontmost
set frontmostWindow to first window of frontmostProcess
set {windowLeft, windowTop} to position of frontmostWindow
set {windowWidth, windowHeight} to size of frontmostWindow
set windowRight to windowLeft + windowWidth
set windowBottom to windowTop + windowHeight
end tell
return "{ \\"Left\\": " & windowLeft & ", \\"Top\\": " & windowTop & ", \\"Right\\": " & windowRight & ", \\"Bottom\\": " &windowBottom & " }"`;
const getForegroundWindowPos = async () => {
let foregroundWindowPos;
try {
if (window.utools.isWindows()) {
foregroundWindowPos = await window.quickcommand.runPowerShell(winScpt);
} else if (window.utools.isMacOS()) {
foregroundWindowPos = await window.quickcommand.runAppleScript(macScpt);
}
} catch (error) {
console.log(error);
}
if (!foregroundWindowPos) return;
return JSON.parse(foregroundWindowPos);
};
let autoDetach = async () => {
const foregroundWindowPos = await getForegroundWindowPos();
console.log(foregroundWindowPos);
if (foregroundWindowPos) {
const { Left, Top, Right, Bottom } = foregroundWindowPos;
let { x, y } = window.utools.getCursorScreenPoint();
window.utools.simulateMouseDoubleClick(Left + 200, Top + 30);
window.utools.simulateMouseMove(x, y);
}
};
export default {
autoDetach,
};

View File

@ -2,6 +2,8 @@ import { reactive } from "vue";
import quickcommandParser from "js/common/quickcommandParser.js";
import importAll from "js/common/importAll.js";
import utoolsFull from "js/utools.js";
import { getUniqueId } from "js/common/uuid.js";
import outputTypes from "js/options/outputTypes.js";
// 默认命令
const defaultCommands = importAll(
@ -16,6 +18,50 @@ const state = reactive({
activatedQuickPanels: [],
});
const getCmdType = (cmds) => {
const firstCmdType = cmds[0].type || "key";
if (!cmds.find((x) => typeof x !== "string")) return "key";
if (!cmds.find((x) => x.type !== firstCmdType)) return firstCmdType;
return "professional";
};
const getFeatureCode = (cmds) => {
return `${getCmdType(cmds)}_${getUniqueId({ short: true })}`;
};
const getLabeledCmds = (cmds, explain) => {
return cmds.map((cmd) => {
if (typeof cmd === "string") {
return cmd || explain;
}
return {
...cmd,
label: cmd.label || explain,
};
});
};
export const getValidCommand = (command) => {
const { cmds, explain } = command.features;
if (!explain) throw "名称不能为空";
if (!Array.isArray(cmds)) throw "匹配规则格式错误";
// 未配置label或关键字时直接使用名称
command.features.cmds = getLabeledCmds(cmds, explain);
// 不需要显示输入框的输入类型添加mainHide属性
if (outputTypes[command.output].outPlugin) {
command.features.mainHide = true;
}
// 生成唯一code
if (!command.features.code) {
command.features.code = getFeatureCode(cmds);
}
return window.lodashM.cloneDeep(command);
};
// 使用函数工厂模式,确保每个组件获取自己的状态副本
export function useCommandManager() {
// 获取已启用的命令
@ -59,6 +105,11 @@ export function useCommandManager() {
// 保存命令
const saveCommand = (command) => {
try {
command = getValidCommand(command);
} catch (e) {
return quickcommand.showMessageBox(e.toString(), "error");
}
const code = command.features.code;
state.allQuickCommands[code] = command;
@ -70,7 +121,7 @@ export function useCommandManager() {
utoolsFull.whole.setFeature(command.features);
if (!isDefaultCommand(code)) {
utoolsFull.putDB(window.lodashM.cloneDeep(command), "qc_" + code);
utoolsFull.putDB(command, "qc_" + code);
}
getAllQuickCommandTags();

View File

@ -1,7 +1,9 @@
export const getUniqueId = () => {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
export const getUniqueId = (options = {}) => {
const { short = false } = options;
const uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
return short ? uuid.substring(0, 8) : uuid;
};

View File

@ -133,3 +133,10 @@ export function generateCode(flow) {
return finalCode;
}
export function generateFlowsCode(flows) {
const [mainFlow, ...subFlows] = flows;
return [...subFlows, mainFlow]
.map((flow) => generateCode(flow))
.join("\n\n");
}

View File

@ -3,7 +3,8 @@
*/
const jsonSample = [
"关键词",
"关键词1",
"关键词2",
{
type: "img",
label: "图片匹配",
@ -41,12 +42,12 @@ const jsonSample = [
},
];
const commandTypes = {
export default {
key: {
name: "key",
label: "关键词",
icon: "font_download",
color: "teal",
color: "blue",
matchLabel: "关键词",
desc: "直接在主输入框输入对应关键字,最通用的一种模式,关键字可以设置多个",
valueType: "array",
@ -57,7 +58,7 @@ const commandTypes = {
},
regex: {
name: "regex",
label: "正则/划词",
label: "正则",
icon: "rule",
color: "cyan",
matchLabel: "正则",
@ -82,7 +83,7 @@ const commandTypes = {
over: {
name: "over",
label: "所有文本",
matchLabel: "无需置",
matchLabel: "无需置",
icon: "emergency",
color: "light-green",
desc: "匹配主输入框的所有文本,但只有在该文本未设置对应的插件或功能时才生效",
@ -104,7 +105,7 @@ const commandTypes = {
},
window: {
name: "window",
label: "窗口/进程",
label: "窗口",
matchLabel: "进程名",
icon: "widgets",
color: "indigo",
@ -154,7 +155,7 @@ const commandTypes = {
},
files: {
name: "files",
label: "复制/选中文件",
label: "文件",
matchLabel: "正则",
icon: "description",
color: "light-blue",
@ -200,5 +201,3 @@ const commandTypes = {
jsonSample: jsonSample,
},
};
export default commandTypes;

View File

@ -27,7 +27,7 @@ export default {
},
methods: {
runCurrentCommand(command) {
this.$refs.result.runCurrentCommand(window.lodashM.cloneDeep(command));
this.$refs.result.runCurrentCommand(command);
},
},
};

View File

@ -221,9 +221,7 @@ export default {
}
},
runCommand(code) {
this.$refs.result.runCurrentCommand(
window.lodashM.cloneDeep(this.allQuickCommands[code])
);
this.$refs.result.runCurrentCommand(this.allQuickCommands[code]);
},
//
enableCommand(code) {
@ -241,13 +239,16 @@ export default {
}
},
//
editCommand(command) {
editCommand(commandOrCode) {
// code command
if (typeof command === "string") command = this.allQuickCommands[command];
const command =
typeof commandOrCode === "string"
? this.allQuickCommands[commandOrCode]
: commandOrCode;
this.commandEditorAction = {
type: "edit",
data: window.lodashM.cloneDeep(command),
component: "CommandEditor",
component: command.flows ? "ComposerEditor" : "CommandEditor",
};
this.isEditorShow = true;
},
@ -349,6 +350,7 @@ export default {
saveCommand(command) {
const code = this.commandManager.saveCommand(command);
this.locateToCommand(command.tags, code);
quickcommand.showMessageBox("保存成功!");
},
editorEvent(event) {
switch (event.type) {
@ -394,7 +396,7 @@ export default {
//
this.commandManager.state.allQuickCommands = {
...this.commandManager.state.allQuickCommands,
...this.allQuickCommands,
...tagCommands,
};