mirror of
https://github.com/fofolee/uTools-quickcommand.git
synced 2025-06-28 11:52:46 +08:00
实现基本的命令可视化编排
This commit is contained in:
parent
dece5dbd11
commit
57de0b651b
@ -38,7 +38,12 @@
|
||||
<q-btn-group unelevated class="button-group">
|
||||
<template v-if="modelValue.program === 'quickcommand'">
|
||||
<q-btn
|
||||
v-for="(item, index) in ['keyboard', 'rocket_launch', 'help_center']"
|
||||
v-for="(item, index) in [
|
||||
'keyboard',
|
||||
'rocket_launch',
|
||||
'help_center',
|
||||
'view_timeline',
|
||||
]"
|
||||
:key="index"
|
||||
dense
|
||||
flat
|
||||
@ -48,7 +53,7 @@
|
||||
@click="handleQuickCommandAction(index)"
|
||||
>
|
||||
<q-tooltip>
|
||||
{{ ["录制按键", "快捷动作", "查看文档"][index] }}
|
||||
{{ ["录制按键", "快捷动作", "查看文档", "可视化编排"][index] }}
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
</template>
|
||||
@ -163,18 +168,32 @@
|
||||
<q-dialog v-model="showRecorder" position="bottom">
|
||||
<KeyRecorder @sendKeys="addAction" />
|
||||
</q-dialog>
|
||||
<q-dialog v-model="showComposer" maximized>
|
||||
<q-card class="full-height">
|
||||
<q-card-section class="q-pa-md full-height">
|
||||
<CommandComposer
|
||||
ref="composer"
|
||||
@run="handleComposerRun"
|
||||
@apply="handleComposerApply"
|
||||
@update:model-value="showComposer = false"
|
||||
/>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import QuickAction from "components/popup/QuickAction";
|
||||
import KeyRecorder from "components/popup/KeyRecorder";
|
||||
import CommandComposer from "components/editor/composer/CommandComposer.vue";
|
||||
|
||||
export default {
|
||||
name: "CommandLanguageBar",
|
||||
components: {
|
||||
QuickAction,
|
||||
KeyRecorder,
|
||||
CommandComposer,
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
@ -198,6 +217,7 @@ export default {
|
||||
return {
|
||||
showActions: false,
|
||||
showRecorder: false,
|
||||
showComposer: false,
|
||||
};
|
||||
},
|
||||
emits: [
|
||||
@ -267,6 +287,7 @@ export default {
|
||||
() => (this.showRecorder = true),
|
||||
() => (this.showActions = true),
|
||||
() => this.showHelp(),
|
||||
() => (this.showComposer = true),
|
||||
];
|
||||
actions[index]();
|
||||
},
|
||||
@ -276,6 +297,20 @@ export default {
|
||||
showHelp() {
|
||||
window.showUb.docs();
|
||||
},
|
||||
handleComposerRun(code) {
|
||||
this.$emit('add-action', code);
|
||||
},
|
||||
handleComposerApply(code) {
|
||||
this.$emit('add-action', code);
|
||||
this.showComposer = false;
|
||||
},
|
||||
applyComposerCommands() {
|
||||
if (this.$refs.composer) {
|
||||
const code = this.$refs.composer.generateCode();
|
||||
this.$emit('add-action', code);
|
||||
}
|
||||
this.showComposer = false;
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
159
src/components/editor/composer/CommandComposer.vue
Normal file
159
src/components/editor/composer/CommandComposer.vue
Normal file
@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<div class="command-composer">
|
||||
<!-- 固定头部 -->
|
||||
<div class="composer-header q-pa-sm row items-center bg-white">
|
||||
<div class="text-h6 text-weight-medium">可视化命令编排</div>
|
||||
<q-space />
|
||||
<q-btn flat round dense icon="close" v-close-popup />
|
||||
</div>
|
||||
|
||||
<!-- 主体内容 -->
|
||||
<div class="composer-body row no-wrap q-pa-sm">
|
||||
<!-- 左侧命令列表 -->
|
||||
<div class="col-3">
|
||||
<div class="text-subtitle1 q-pb-sm">可用命令</div>
|
||||
<q-scroll-area style="height: calc(100vh - 200px)">
|
||||
<ComposerList
|
||||
:commands="availableCommands"
|
||||
@add-command="addCommand"
|
||||
/>
|
||||
</q-scroll-area>
|
||||
</div>
|
||||
|
||||
<!-- 右侧命令流程 -->
|
||||
<div class="col q-pl-md">
|
||||
<div class="text-subtitle1 q-pb-sm">命令流程</div>
|
||||
<q-scroll-area style="height: calc(100vh - 200px)">
|
||||
<ComposerFlow v-model="commandFlow" @add-command="addCommand" />
|
||||
</q-scroll-area>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 固定底部 -->
|
||||
<div class="composer-footer q-pa-sm row items-center justify-end bg-white">
|
||||
<q-btn
|
||||
outline
|
||||
color="primary"
|
||||
label="运行"
|
||||
icon="play_arrow"
|
||||
class="q-mr-sm"
|
||||
@click="runCommands"
|
||||
/>
|
||||
<q-btn flat label="取消" v-close-popup />
|
||||
<q-btn unelevated color="primary" label="确认" @click="applyCommands" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "vue";
|
||||
import ComposerList from "./ComposerList.vue";
|
||||
import ComposerFlow from "./ComposerFlow.vue";
|
||||
import { commandCategories } from "./composerConfig";
|
||||
|
||||
// 从commandCategories中提取所有命令
|
||||
const availableCommands = commandCategories.reduce((commands, category) => {
|
||||
return commands.concat(
|
||||
category.commands.map((cmd) => ({
|
||||
type: category.label,
|
||||
...cmd,
|
||||
}))
|
||||
);
|
||||
}, []);
|
||||
|
||||
export default defineComponent({
|
||||
name: "CommandComposer",
|
||||
components: {
|
||||
ComposerList,
|
||||
ComposerFlow,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
commandFlow: [],
|
||||
nextId: 1,
|
||||
availableCommands,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
addCommand(action) {
|
||||
this.commandFlow.push({
|
||||
...action,
|
||||
id: this.nextId++,
|
||||
argv: "",
|
||||
saveOutput: false,
|
||||
useOutput: null,
|
||||
cmd: action.value || action.cmd,
|
||||
value: action.value || action.cmd,
|
||||
});
|
||||
},
|
||||
generateCode() {
|
||||
let code = [];
|
||||
let outputVars = new Map();
|
||||
|
||||
this.commandFlow.forEach((cmd, index) => {
|
||||
let line = "";
|
||||
if (cmd.saveOutput) {
|
||||
const varName = `output${index}`;
|
||||
outputVars.set(index, varName);
|
||||
line += `let ${varName} = `;
|
||||
}
|
||||
|
||||
if (cmd.useOutput !== null) {
|
||||
const inputVar = outputVars.get(cmd.useOutput);
|
||||
line += `${cmd.value}(${inputVar})`;
|
||||
} else {
|
||||
const argv =
|
||||
cmd.value !== "quickcommand.sleep" ? `"${cmd.argv}"` : cmd.argv;
|
||||
line += `${cmd.value}(${argv})`;
|
||||
}
|
||||
|
||||
code.push(line);
|
||||
});
|
||||
|
||||
return code.join("\n");
|
||||
},
|
||||
runCommands() {
|
||||
const code = this.generateCode();
|
||||
this.$emit("run", code);
|
||||
},
|
||||
applyCommands() {
|
||||
const code = this.generateCode();
|
||||
this.$emit("apply", code);
|
||||
this.$emit("update:modelValue", false);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.command-composer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.composer-header {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.composer-body {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.composer-footer {
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
/* 滚动美化 */
|
||||
:deep(.q-scrollarea__thumb) {
|
||||
width: 6px;
|
||||
opacity: 0.4;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
:deep(.q-scrollarea__thumb:hover) {
|
||||
opacity: 0.8;
|
||||
}
|
||||
</style>
|
176
src/components/editor/composer/ComposerCard.vue
Normal file
176
src/components/editor/composer/ComposerCard.vue
Normal file
@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<div class="composer-card q-mb-sm">
|
||||
<q-card flat bordered>
|
||||
<q-card-section horizontal class="q-pa-sm">
|
||||
<!-- 拖拽手柄 -->
|
||||
<div class="drag-handle cursor-move q-mr-sm">
|
||||
<q-icon name="drag_indicator" size="24px" class="text-grey-6" />
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<!-- 命令标题和描述 -->
|
||||
<div class="row items-center q-mb-sm">
|
||||
<div class="text-subtitle1">{{ command.label }}</div>
|
||||
<q-space />
|
||||
<!-- 输出开关 -->
|
||||
<q-toggle
|
||||
v-if="hasOutput"
|
||||
v-model="saveOutputLocal"
|
||||
label="保存输出"
|
||||
dense
|
||||
/>
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
dense
|
||||
icon="close"
|
||||
@click="$emit('remove')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 参数输入 -->
|
||||
<div class="row items-center">
|
||||
<!-- 使用上一个命令的输出 -->
|
||||
<template v-if="canUseOutput && availableOutputs.length > 0">
|
||||
<q-select
|
||||
v-model="useOutputLocal"
|
||||
:options="availableOutputs"
|
||||
dense
|
||||
outlined
|
||||
class="col"
|
||||
emit-value
|
||||
map-options
|
||||
clearable
|
||||
:label="placeholder"
|
||||
@clear="handleClearOutput"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="input" />
|
||||
</template>
|
||||
<template v-slot:selected-item="scope">
|
||||
<div class="row items-center">
|
||||
<q-icon name="output" color="primary" size="xs" class="q-mr-xs" />
|
||||
{{ scope.opt.label }}
|
||||
</div>
|
||||
</template>
|
||||
</q-select>
|
||||
</template>
|
||||
<!-- 按键编辑器 -->
|
||||
<template v-else-if="command.hasKeyRecorder">
|
||||
<KeyEditor v-model="argvLocal" class="col" />
|
||||
</template>
|
||||
<!-- 普通参数输入 -->
|
||||
<template v-else>
|
||||
<q-input
|
||||
v-model="argvLocal"
|
||||
dense
|
||||
outlined
|
||||
class="col"
|
||||
:label="placeholder"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="code" />
|
||||
</template>
|
||||
</q-input>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
import KeyEditor from './KeyEditor.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ComposerCard',
|
||||
components: {
|
||||
KeyEditor
|
||||
},
|
||||
props: {
|
||||
command: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
hasOutput: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
canUseOutput: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
availableOutputs: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showKeyRecorder: false
|
||||
}
|
||||
},
|
||||
emits: ['remove', 'toggle-output', 'update:argv', 'update:use-output'],
|
||||
computed: {
|
||||
saveOutputLocal: {
|
||||
get() {
|
||||
return this.command.saveOutput
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('toggle-output')
|
||||
}
|
||||
},
|
||||
argvLocal: {
|
||||
get() {
|
||||
return this.command.argv
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:argv', value)
|
||||
}
|
||||
},
|
||||
useOutputLocal: {
|
||||
get() {
|
||||
return this.command.useOutput
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:use-output', value)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClearOutput() {
|
||||
this.$emit('update:use-output', null)
|
||||
},
|
||||
handleKeyRecord(keys) {
|
||||
this.showKeyRecorder = false;
|
||||
// 从keyTap("a","control")格式中提取参数
|
||||
const matches = keys.match(/keyTap\((.*)\)/)
|
||||
if (matches && matches[1]) {
|
||||
this.$emit('update:argv', matches[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.composer-card {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.drag-handle:hover {
|
||||
color: var(--q-primary);
|
||||
}
|
||||
</style>
|
204
src/components/editor/composer/ComposerFlow.vue
Normal file
204
src/components/editor/composer/ComposerFlow.vue
Normal file
@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<div class="composer-flow">
|
||||
<div
|
||||
class="command-flow-container"
|
||||
@dragover.prevent
|
||||
@drop="onDrop"
|
||||
>
|
||||
<draggable
|
||||
v-model="commands"
|
||||
group="commands"
|
||||
item-key="id"
|
||||
class="flow-list"
|
||||
handle=".drag-handle"
|
||||
:animation="200"
|
||||
>
|
||||
<template #item="{ element, index }">
|
||||
<transition
|
||||
name="slide-fade"
|
||||
mode="out-in"
|
||||
appear
|
||||
>
|
||||
<div :key="element.id" class="flow-item">
|
||||
<ComposerCard
|
||||
:command="element"
|
||||
:has-output="hasOutput(element)"
|
||||
:can-use-output="canUseOutput(element, index)"
|
||||
:available-outputs="getAvailableOutputs(index)"
|
||||
:placeholder="getPlaceholder(element, index)"
|
||||
@remove="removeCommand(index)"
|
||||
@toggle-output="toggleSaveOutput(index)"
|
||||
@update:argv="(val) => handleArgvChange(index, val)"
|
||||
@update:use-output="(val) => handleUseOutputChange(index, val)"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
</draggable>
|
||||
<div v-if="commands.length === 0" class="empty-flow">
|
||||
<div class="text-center text-grey-6">
|
||||
<q-icon name="drag_indicator" size="32px" />
|
||||
<div class="text-body2 q-mt-sm">从左侧拖拽命令到这里开始编排</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
import draggable from 'vuedraggable'
|
||||
import ComposerCard from './ComposerCard.vue'
|
||||
import { commandsWithOutput, commandsAcceptOutput } from './composerConfig'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ComposerFlow',
|
||||
components: {
|
||||
draggable,
|
||||
ComposerCard
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'add-command'],
|
||||
computed: {
|
||||
commands: {
|
||||
get() {
|
||||
return this.modelValue
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:modelValue', value)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onDrop(event) {
|
||||
const actionData = JSON.parse(event.dataTransfer.getData('action'))
|
||||
this.$emit('add-command', actionData)
|
||||
document.querySelectorAll('.dragging').forEach(el => {
|
||||
el.classList.remove('dragging')
|
||||
})
|
||||
},
|
||||
removeCommand(index) {
|
||||
const newCommands = [...this.commands]
|
||||
newCommands.splice(index, 1)
|
||||
this.$emit('update:modelValue', newCommands)
|
||||
},
|
||||
hasOutput(command) {
|
||||
return commandsWithOutput[command.value] || false
|
||||
},
|
||||
canUseOutput(command, index) {
|
||||
return commandsAcceptOutput[command.value] && this.getAvailableOutputs(index).length > 0
|
||||
},
|
||||
getAvailableOutputs(currentIndex) {
|
||||
return this.commands
|
||||
.slice(0, currentIndex)
|
||||
.map((cmd, index) => ({
|
||||
label: `${cmd.label} 的输出`,
|
||||
value: index,
|
||||
disable: !cmd.saveOutput
|
||||
}))
|
||||
.filter(item => !item.disable)
|
||||
},
|
||||
toggleSaveOutput(index) {
|
||||
const newCommands = [...this.commands]
|
||||
newCommands[index].saveOutput = !newCommands[index].saveOutput
|
||||
if (!newCommands[index].saveOutput) {
|
||||
newCommands.forEach((cmd, i) => {
|
||||
if (i > index && cmd.useOutput === index) {
|
||||
cmd.useOutput = null
|
||||
}
|
||||
})
|
||||
}
|
||||
this.$emit('update:modelValue', newCommands)
|
||||
},
|
||||
handleArgvChange(index, value) {
|
||||
const newCommands = [...this.commands]
|
||||
newCommands[index].argv = value
|
||||
this.$emit('update:modelValue', newCommands)
|
||||
},
|
||||
handleUseOutputChange(index, value) {
|
||||
const newCommands = [...this.commands]
|
||||
newCommands[index].useOutput = value
|
||||
if (value !== null) {
|
||||
newCommands[index].argv = ''
|
||||
}
|
||||
this.$emit('update:modelValue', newCommands)
|
||||
},
|
||||
getPlaceholder(element, index) {
|
||||
if (element.useOutput !== null) {
|
||||
return `使用 ${this.commands[element.useOutput].label} 的输出`
|
||||
}
|
||||
return element.desc
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.composer-flow {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.command-flow-container {
|
||||
min-height: 100px;
|
||||
padding: 8px;
|
||||
background-color: #fafafa;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.command-flow-container:empty {
|
||||
border: 2px dashed #e0e0e0;
|
||||
}
|
||||
|
||||
.flow-list {
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
.empty-flow {
|
||||
height: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2px dashed #e0e0e0;
|
||||
border-radius: 4px;
|
||||
margin: 8px 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.empty-flow:hover {
|
||||
border-color: #bdbdbd;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
/* 拖拽时的视觉反馈 */
|
||||
.command-flow-container.drag-over {
|
||||
background-color: #f0f4ff;
|
||||
border-color: #2196f3;
|
||||
}
|
||||
|
||||
/* 滑动淡出动画 */
|
||||
.slide-fade-enter-active,
|
||||
.slide-fade-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.slide-fade-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
|
||||
.slide-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
|
||||
.flow-item {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
</style>
|
118
src/components/editor/composer/ComposerList.vue
Normal file
118
src/components/editor/composer/ComposerList.vue
Normal file
@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<div class="composer-list">
|
||||
<q-list bordered separator class="rounded-borders">
|
||||
<template v-for="category in commandCategories" :key="category.label">
|
||||
<q-item-label header class="q-py-sm bg-grey-2">
|
||||
<div class="row items-center">
|
||||
<q-icon :name="category.icon" color="primary" size="sm" class="q-mr-sm" />
|
||||
<span class="text-weight-medium">{{ category.label }}</span>
|
||||
</div>
|
||||
</q-item-label>
|
||||
|
||||
<q-item
|
||||
v-for="command in getCategoryCommands(category)"
|
||||
:key="command.value"
|
||||
class="command-item q-py-xs"
|
||||
draggable="true"
|
||||
@dragstart="onDragStart($event, command)"
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label class="text-weight-medium">{{ command.label }}</q-item-label>
|
||||
<q-item-label caption>{{ command.desc }}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-icon name="drag_indicator" color="grey-6" size="16px" />
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-list>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
import { commandCategories } from './composerConfig'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ComposerList',
|
||||
props: {
|
||||
commands: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
commandCategories
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getCategoryCommands(category) {
|
||||
return this.commands.filter(cmd =>
|
||||
category.commands.some(catCmd => catCmd.value === cmd.value || catCmd.value === cmd.cmd)
|
||||
)
|
||||
},
|
||||
onDragStart(event, command) {
|
||||
event.dataTransfer.setData('action', JSON.stringify({
|
||||
...command,
|
||||
cmd: command.value || command.cmd
|
||||
}))
|
||||
event.target.classList.add('dragging')
|
||||
event.target.addEventListener('dragend', () => {
|
||||
event.target.classList.remove('dragging')
|
||||
}, { once: true })
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.composer-list {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.command-item {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
margin: 4px 8px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
background: white;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.command-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
transform: translateX(4px);
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.command-item.dragging {
|
||||
opacity: 0.5;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* 分类标题样式 */
|
||||
.q-item-label.header {
|
||||
min-height: 32px;
|
||||
padding: 4px 12px;
|
||||
font-size: 0.9rem;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* 暗色模式适配 */
|
||||
.body--dark .command-item {
|
||||
border-color: #424242;
|
||||
background: #1d1d1d;
|
||||
}
|
||||
|
||||
.body--dark .command-item:hover {
|
||||
background-color: #2d2d2d;
|
||||
}
|
||||
|
||||
.body--dark .q-item-label.header {
|
||||
border-color: #424242;
|
||||
background-color: #2d2d2d;
|
||||
}
|
||||
</style>
|
362
src/components/editor/composer/KeyEditor.vue
Normal file
362
src/components/editor/composer/KeyEditor.vue
Normal file
@ -0,0 +1,362 @@
|
||||
<template>
|
||||
<div class="key-editor">
|
||||
<!-- 按键输入框 -->
|
||||
<q-input
|
||||
dense
|
||||
outlined
|
||||
readonly
|
||||
class="col"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<!-- 修饰键 -->
|
||||
<div class="row items-center q-gutter-x-xs">
|
||||
<q-chip
|
||||
v-for="(active, key) in modifiers"
|
||||
:key="key"
|
||||
:color="active ? 'primary' : 'grey-4'"
|
||||
:text-color="active ? 'white' : 'grey-7'"
|
||||
dense
|
||||
clickable
|
||||
class="modifier-chip"
|
||||
@click="toggleModifier(key)"
|
||||
>
|
||||
{{ modifierLabels[key] }}
|
||||
</q-chip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 主按键显示 -->
|
||||
<template v-slot:default>
|
||||
<div class="main-key-container">
|
||||
<div class="main-key">
|
||||
{{ mainKeyDisplay }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:append>
|
||||
<q-separator vertical inset />
|
||||
<!-- 选择按钮 -->
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
dense
|
||||
icon="edit"
|
||||
@click="showKeySelect = true"
|
||||
>
|
||||
<q-tooltip>选择按键</q-tooltip>
|
||||
</q-btn>
|
||||
<q-separator vertical inset />
|
||||
<!-- 录制按钮 -->
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
dense
|
||||
:icon="isRecording ? 'fiber_manual_record' : 'radio_button_unchecked'"
|
||||
:color="isRecording ? 'negative' : 'primary'"
|
||||
@click="toggleRecording"
|
||||
>
|
||||
<q-tooltip>{{ isRecording ? '停止录制' : '开始录制' }}</q-tooltip>
|
||||
</q-btn>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<!-- 按键选择对话框 -->
|
||||
<q-dialog v-model="showKeySelect">
|
||||
<q-card style="min-width: 300px">
|
||||
<q-card-section>
|
||||
<div class="text-h6">选择按键</div>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-gutter-y-md">
|
||||
<!-- 主按键选择 -->
|
||||
<q-select
|
||||
v-model="mainKey"
|
||||
:options="commonKeys"
|
||||
label="选择用按键"
|
||||
dense
|
||||
outlined
|
||||
emit-value
|
||||
map-options
|
||||
/>
|
||||
<q-input
|
||||
v-model="customKey"
|
||||
label="或输入自定义按键"
|
||||
dense
|
||||
outlined
|
||||
@update:model-value="mainKey = $event"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat label="确定" color="primary" v-close-popup />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
// 检测操作系统
|
||||
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0
|
||||
|
||||
export default defineComponent({
|
||||
name: 'KeyEditor',
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isRecording: false,
|
||||
showKeySelect: false,
|
||||
mainKey: '',
|
||||
customKey: '',
|
||||
modifiers: {
|
||||
control: false,
|
||||
alt: false,
|
||||
shift: false,
|
||||
command: false
|
||||
},
|
||||
modifierLabels: isMac ? {
|
||||
control: '⌃',
|
||||
alt: '⌥',
|
||||
shift: '⇧',
|
||||
command: '⌘'
|
||||
} : {
|
||||
control: 'Ctrl',
|
||||
alt: 'Alt',
|
||||
shift: 'Shift',
|
||||
command: 'Win'
|
||||
},
|
||||
commonKeys: [
|
||||
{ label: 'Enter ↵', value: 'enter' },
|
||||
{ label: 'Tab ⇥', value: 'tab' },
|
||||
{ label: 'Space', value: 'space' },
|
||||
{ label: 'Backspace ⌫', value: 'backspace' },
|
||||
{ label: 'Delete ⌦', value: 'delete' },
|
||||
{ label: 'Escape ⎋', value: 'escape' },
|
||||
{ label: '↑', value: 'up' },
|
||||
{ label: '↓', value: 'down' },
|
||||
{ label: '←', value: 'left' },
|
||||
{ label: '→', value: 'right' },
|
||||
{ label: 'Home', value: 'home' },
|
||||
{ label: 'End', value: 'end' },
|
||||
{ label: 'Page Up', value: 'pageup' },
|
||||
{ label: 'Page Down', value: 'pagedown' }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
mainKeyDisplay() {
|
||||
if (!this.mainKey) return ''
|
||||
// 特殊按键映射表
|
||||
const specialKeyMap = {
|
||||
'enter': '↵',
|
||||
'tab': '⇥',
|
||||
'space': '␣',
|
||||
'backspace': '⌫',
|
||||
'delete': '⌦',
|
||||
'escape': '⎋',
|
||||
'up': '↑',
|
||||
'down': '↓',
|
||||
'left': '←',
|
||||
'right': '→'
|
||||
}
|
||||
return specialKeyMap[this.mainKey] ||
|
||||
(this.mainKey.length === 1 ? this.mainKey.toUpperCase() :
|
||||
this.mainKey.charAt(0).toUpperCase() + this.mainKey.slice(1))
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
modelValue: {
|
||||
immediate: true,
|
||||
handler(val) {
|
||||
if (val) {
|
||||
this.parseKeyString(val)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleModifier(key) {
|
||||
this.modifiers[key] = !this.modifiers[key]
|
||||
this.updateValue()
|
||||
},
|
||||
toggleRecording() {
|
||||
if (!this.isRecording) {
|
||||
this.startRecording()
|
||||
} else {
|
||||
this.stopRecording()
|
||||
}
|
||||
},
|
||||
startRecording() {
|
||||
this.isRecording = true
|
||||
let lastKeyTime = 0
|
||||
let lastKey = null
|
||||
|
||||
this.recordEvent = (event) => {
|
||||
event.preventDefault()
|
||||
const currentTime = Date.now()
|
||||
|
||||
// 重置所有修饰键状态
|
||||
Object.keys(this.modifiers).forEach(key => {
|
||||
this.modifiers[key] = false
|
||||
})
|
||||
|
||||
// 根据操作系统设置修饰键
|
||||
if (isMac) {
|
||||
if (event.metaKey) this.modifiers.command = true
|
||||
if (event.ctrlKey) this.modifiers.control = true
|
||||
} else {
|
||||
if (event.ctrlKey) this.modifiers.control = true
|
||||
if (event.metaKey || event.winKey) this.modifiers.command = true
|
||||
}
|
||||
if (event.altKey) this.modifiers.alt = true
|
||||
if (event.shiftKey) this.modifiers.shift = true
|
||||
|
||||
// 设置主按键
|
||||
let key = null
|
||||
|
||||
// 处理字母键
|
||||
if (event.code.startsWith('Key')) {
|
||||
key = event.code.slice(-1).toLowerCase()
|
||||
}
|
||||
// 处理数字键
|
||||
else if (event.code.startsWith('Digit')) {
|
||||
key = event.code.slice(-1)
|
||||
}
|
||||
// 处理功能键
|
||||
else if (event.code.startsWith('F') && !isNaN(event.code.slice(1))) {
|
||||
key = event.code.toLowerCase()
|
||||
}
|
||||
// 处理其他特殊键
|
||||
else {
|
||||
const keyMap = {
|
||||
'ArrowUp': 'up',
|
||||
'ArrowDown': 'down',
|
||||
'ArrowLeft': 'left',
|
||||
'ArrowRight': 'right',
|
||||
'Enter': 'enter',
|
||||
'Space': 'space',
|
||||
'Escape': 'escape',
|
||||
'Delete': 'delete',
|
||||
'Backspace': 'backspace',
|
||||
'Tab': 'tab',
|
||||
'Home': 'home',
|
||||
'End': 'end',
|
||||
'PageUp': 'pageup',
|
||||
'PageDown': 'pagedown',
|
||||
'Control': 'control',
|
||||
'Alt': 'alt',
|
||||
'Shift': 'shift',
|
||||
'Meta': 'command'
|
||||
}
|
||||
key = keyMap[event.code] || event.key.toLowerCase()
|
||||
}
|
||||
|
||||
// 处理双击修饰键
|
||||
if (['control', 'alt', 'shift', 'command'].includes(key)) {
|
||||
if (key === lastKey && (currentTime - lastKeyTime) < 500) {
|
||||
this.mainKey = key
|
||||
this.modifiers[key] = false // 清除修饰键状态
|
||||
this.stopRecording()
|
||||
this.updateValue()
|
||||
return
|
||||
}
|
||||
lastKey = key
|
||||
lastKeyTime = currentTime
|
||||
return
|
||||
}
|
||||
|
||||
// 处理空格键和其他按键
|
||||
if (key === 'space' || !['meta', 'control', 'shift', 'alt', 'command'].includes(key)) {
|
||||
this.mainKey = key
|
||||
this.stopRecording()
|
||||
this.updateValue()
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', this.recordEvent)
|
||||
},
|
||||
stopRecording() {
|
||||
this.isRecording = false
|
||||
document.removeEventListener('keydown', this.recordEvent)
|
||||
},
|
||||
updateValue() {
|
||||
if (!this.mainKey) return
|
||||
const activeModifiers = Object.entries(this.modifiers)
|
||||
.filter(([_, active]) => active)
|
||||
.map(([key]) => key)
|
||||
// 在非 Mac 系统上,将 command 转换为 meta
|
||||
.map(key => !isMac && key === 'command' ? 'meta' : key)
|
||||
|
||||
const args = [this.mainKey, ...activeModifiers]
|
||||
this.$emit('update:modelValue', args.join('","'))
|
||||
},
|
||||
parseKeyString(val) {
|
||||
try {
|
||||
const args = val.split('","')
|
||||
if (args.length > 0) {
|
||||
this.mainKey = args[0]
|
||||
Object.keys(this.modifiers).forEach(key => {
|
||||
// 在非 Mac 系统上,将 meta 转换为 command
|
||||
const modKey = !isMac && args.includes('meta') ? 'command' : key
|
||||
this.modifiers[key] = args.includes(modKey)
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to parse key string:', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.key-editor {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.modifier-chip {
|
||||
height: 24px;
|
||||
font-size: 13px;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.main-key-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.main-key {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--q-primary);
|
||||
line-height: 24px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
:deep(.q-field__control) {
|
||||
padding: 0 8px;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
:deep(.q-field__prepend) {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
:deep(.q-field__native) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.q-field__control-container) {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
132
src/components/editor/composer/composerConfig.js
Normal file
132
src/components/editor/composer/composerConfig.js
Normal file
@ -0,0 +1,132 @@
|
||||
// 定义命令图标映射
|
||||
export const commandIcons = {
|
||||
'open': 'folder_open',
|
||||
'locate': 'location_on',
|
||||
'visit': 'language',
|
||||
'utools.ubrowser.goto': 'public',
|
||||
'system': 'terminal',
|
||||
'copyTo': 'content_copy',
|
||||
'message': 'message',
|
||||
'alert': 'warning',
|
||||
'send': 'send',
|
||||
'utools.redirect': 'alt_route',
|
||||
'quickcommand.sleep': 'schedule',
|
||||
'keyTap': 'keyboard'
|
||||
}
|
||||
|
||||
// 定义命令分类
|
||||
export const commandCategories = [
|
||||
{
|
||||
label: '文件操作',
|
||||
icon: 'folder',
|
||||
commands: [
|
||||
{
|
||||
value: "open",
|
||||
label: "打开文件/文件夹/软件",
|
||||
desc: "文件、文件夹或软件的绝对路径",
|
||||
},
|
||||
{
|
||||
value: "locate",
|
||||
label: "在文件管理器中定位文件",
|
||||
desc: "要在文件管理器里显示的文件路径",
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '网络操作',
|
||||
icon: 'language',
|
||||
commands: [
|
||||
{
|
||||
value: "visit",
|
||||
label: "用默认浏览器打开网址",
|
||||
desc: "要访问的网址链接",
|
||||
},
|
||||
{
|
||||
value: "utools.ubrowser.goto",
|
||||
label: "用ubrowser打开网址",
|
||||
desc: "要访问的网址链接",
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '系统操作',
|
||||
icon: 'computer',
|
||||
commands: [
|
||||
{
|
||||
value: "system",
|
||||
label: "执行系统命令",
|
||||
desc: "要执行的命令行",
|
||||
},
|
||||
{
|
||||
value: "copyTo",
|
||||
label: "将内容写入剪贴板",
|
||||
desc: "要写入剪切板的内容",
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '消息通知',
|
||||
icon: 'notifications',
|
||||
commands: [
|
||||
{
|
||||
value: "message",
|
||||
label: "发送系统消息",
|
||||
desc: "要发送的系统消息文本",
|
||||
},
|
||||
{
|
||||
value: "alert",
|
||||
label: "弹窗显示消息",
|
||||
desc: "要弹窗显示的消息文本",
|
||||
},
|
||||
{
|
||||
value: "send",
|
||||
label: "发送文本到活动窗口",
|
||||
desc: "要发送到窗口的文本内容",
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '其他功能',
|
||||
icon: 'more_horiz',
|
||||
commands: [
|
||||
{
|
||||
value: "utools.redirect",
|
||||
label: "转至指定插件",
|
||||
desc: "要跳转至的插件名称",
|
||||
},
|
||||
{
|
||||
value: "quickcommand.sleep",
|
||||
label: "添加延时",
|
||||
desc: "延迟的毫秒数",
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '按键操作',
|
||||
icon: 'keyboard',
|
||||
commands: [
|
||||
{
|
||||
value: "keyTap",
|
||||
label: "模拟按键",
|
||||
desc: "模拟键盘按键",
|
||||
hasKeyRecorder: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
// 定义哪些命令可以产生输出
|
||||
export const commandsWithOutput = {
|
||||
'system': true,
|
||||
'open': true,
|
||||
'locate': true,
|
||||
'copyTo': true,
|
||||
}
|
||||
|
||||
// 定义哪些命令可以接收输出
|
||||
export const commandsAcceptOutput = {
|
||||
'message': true,
|
||||
'alert': true,
|
||||
'send': true,
|
||||
'copyTo': true,
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user