mirror of
https://github.com/fofolee/uTools-quickcommand.git
synced 2025-06-08 06:16:27 +08:00
470 lines
12 KiB
Vue
470 lines
12 KiB
Vue
<template>
|
||
<div
|
||
class="composer-card"
|
||
:class="{
|
||
collapsed: isCollapsed && !command.isControlFlow,
|
||
'drag-handle': !isLastCommandInChain,
|
||
'no-animation': isClickingControl,
|
||
}"
|
||
v-bind="$attrs"
|
||
@mousedown="handleMouseDown"
|
||
@mouseup="handleMouseUp"
|
||
>
|
||
<q-card class="command-item">
|
||
<q-card-section
|
||
class="card-section"
|
||
:class="{ collapsed: isCollapsed || command.isControlFlow }"
|
||
>
|
||
<CommandHead
|
||
:command="command"
|
||
:is-collapsed="isCollapsed"
|
||
@update:outputVariable="handleOutputVariableUpdate"
|
||
@toggle-output="handleToggleOutput"
|
||
@toggle-collapse="handleToggleCollapse"
|
||
@run="runCommand"
|
||
@remove="$emit('remove')"
|
||
>
|
||
<!-- 控制流程组件,直接把组件放在head中 -->
|
||
<template v-if="command.isControlFlow">
|
||
<component
|
||
:is="command.component"
|
||
v-model="argvLocal"
|
||
:command="command"
|
||
v-bind="command.componentProps || {}"
|
||
:type="command.commandType"
|
||
@addBranch="(chainInfo) => $emit('addBranch', chainInfo)"
|
||
/>
|
||
</template>
|
||
|
||
<!-- 非控制流程组件,使用正常布局 -->
|
||
<template v-else>
|
||
<q-space />
|
||
</template>
|
||
</CommandHead>
|
||
|
||
<!-- 非控制流程组件的参数输入 -->
|
||
<div
|
||
v-if="!command.isControlFlow"
|
||
class="command-content-wrapper"
|
||
:class="{ collapsed: isCollapsed }"
|
||
>
|
||
<div class="command-content">
|
||
<component
|
||
v-if="!!command.component"
|
||
:is="command.component"
|
||
v-model="argvLocal"
|
||
:command="command"
|
||
class="col"
|
||
v-bind="command.componentProps || {}"
|
||
/>
|
||
<MultiParamInput
|
||
v-else
|
||
v-model="argvLocal"
|
||
:command="command"
|
||
class="col"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</q-card-section>
|
||
</q-card>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { defineComponent, inject, defineAsyncComponent } from "vue";
|
||
import { validateVariableName } from "js/common/variableValidator";
|
||
import VariableInput from "components/composer/ui/VariableInput.vue";
|
||
import MultiParamInput from "components/composer/ui/MultiParamInput.vue";
|
||
import CommandHead from "components/composer/card/CommandHead.vue";
|
||
|
||
export default defineComponent({
|
||
name: "ComposerCard",
|
||
components: {
|
||
VariableInput,
|
||
MultiParamInput,
|
||
CommandHead,
|
||
KeyEditor: defineAsyncComponent(() =>
|
||
import("components/composer/ui/KeyEditor.vue")
|
||
),
|
||
UBrowserEditor: defineAsyncComponent(() =>
|
||
import("components/composer/ubrowser/UBrowserEditor.vue")
|
||
),
|
||
AxiosConfigEditor: defineAsyncComponent(() =>
|
||
import("components/composer/http/AxiosConfigEditor.vue")
|
||
),
|
||
SymmetricCryptoEditor: defineAsyncComponent(() =>
|
||
import("components/composer/crypto/SymmetricCryptoEditor.vue")
|
||
),
|
||
AsymmetricCryptoEditor: defineAsyncComponent(() =>
|
||
import("components/composer/crypto/AsymmetricCryptoEditor.vue")
|
||
),
|
||
FunctionSelector: defineAsyncComponent(() =>
|
||
import("components/composer/ui/FunctionSelector.vue")
|
||
),
|
||
RegexEditor: defineAsyncComponent(() =>
|
||
import("components/composer/regex/RegexEditor.vue")
|
||
),
|
||
ConditionalJudgment: defineAsyncComponent(() =>
|
||
import("components/composer/control/ConditionalJudgment.vue")
|
||
),
|
||
LoopControl: defineAsyncComponent(() =>
|
||
import("components/composer/control/LoopControl.vue")
|
||
),
|
||
},
|
||
props: {
|
||
command: {
|
||
type: Object,
|
||
required: true,
|
||
},
|
||
availableOutputs: {
|
||
type: Array,
|
||
default: () => [],
|
||
},
|
||
placeholder: {
|
||
type: String,
|
||
default: "",
|
||
},
|
||
isDragging: {
|
||
type: Boolean,
|
||
default: false,
|
||
},
|
||
},
|
||
data() {
|
||
return {
|
||
showKeyRecorder: false,
|
||
isCollapsed: false,
|
||
isClickingControl: false,
|
||
};
|
||
},
|
||
watch: {
|
||
"command.isCollapsed": {
|
||
immediate: true,
|
||
handler(val) {
|
||
if (val !== undefined) {
|
||
this.isCollapsed = val;
|
||
}
|
||
},
|
||
},
|
||
},
|
||
emits: [
|
||
"remove",
|
||
"toggle-output",
|
||
"update:argv",
|
||
"update:command",
|
||
"run",
|
||
"addBranch",
|
||
"toggle-collapse",
|
||
],
|
||
computed: {
|
||
saveOutputLocal: {
|
||
get() {
|
||
return this.command.saveOutput;
|
||
},
|
||
set(value) {
|
||
this.$emit("toggle-output");
|
||
},
|
||
},
|
||
argvLocal: {
|
||
get() {
|
||
if (this.command.hasAxiosEditor) {
|
||
// 如果是编辑现有配置
|
||
if (
|
||
this.command.argv &&
|
||
!this.command.argv.includes("axios.") &&
|
||
!this.command.argv.includes("fetch(")
|
||
) {
|
||
try {
|
||
return JSON.parse(this.command.argv);
|
||
} catch (e) {
|
||
return {};
|
||
}
|
||
}
|
||
// 如果已经是格式化的代码,直接返回
|
||
return this.command.argv || {};
|
||
}
|
||
return this.command.argv;
|
||
},
|
||
set(value) {
|
||
const updatedCommand = {
|
||
...this.command,
|
||
argv: this.command.hasAxiosEditor
|
||
? typeof value === "string"
|
||
? value
|
||
: JSON.stringify(value)
|
||
: value,
|
||
};
|
||
this.$emit("update:command", updatedCommand);
|
||
},
|
||
},
|
||
showRunBtn() {
|
||
return !this.command.isControlFlow;
|
||
},
|
||
showOutputBtn() {
|
||
return !this.command.isControlFlow;
|
||
},
|
||
isLastCommandInChain() {
|
||
if (!this.command.commandChain) return false;
|
||
return (
|
||
this.command.commandType === this.command.commandChain?.slice(-1)[0]
|
||
);
|
||
},
|
||
},
|
||
setup() {
|
||
const addVariable = inject("addVariable");
|
||
const removeVariable = inject("removeVariable");
|
||
const variables = inject("composerVariables", []);
|
||
|
||
return {
|
||
addVariable,
|
||
removeVariable,
|
||
variables,
|
||
};
|
||
},
|
||
methods: {
|
||
handleKeyRecord(keys) {
|
||
this.showKeyRecorder = false;
|
||
// 从keyTap("a","control")格式中提取参数
|
||
const matches = keys.match(/keyTap\((.*)\)/);
|
||
if (matches && matches[1]) {
|
||
this.$emit("update:argv", matches[1]);
|
||
}
|
||
},
|
||
handleOutputVariableChange(value) {
|
||
if (this.command.outputVariable) {
|
||
this.removeVariable(this.command.outputVariable);
|
||
}
|
||
if (value) {
|
||
this.addVariable(value, this.command);
|
||
}
|
||
},
|
||
handleOutputVariableUpdate(value) {
|
||
// 检查变量名是否合法
|
||
const validation = validateVariableName(value);
|
||
if (!validation.isValid) {
|
||
quickcommand.showMessageBox(validation.error, "warning");
|
||
return;
|
||
}
|
||
|
||
// 检查变量名是否重复
|
||
if (this.variables.some((v) => v.name === value)) {
|
||
quickcommand.showMessageBox(`变量名 "${value}" 已经存在`, "warning");
|
||
return;
|
||
}
|
||
|
||
// 创建命令的副本并更新
|
||
const updatedCommand = {
|
||
...this.command,
|
||
outputVariable: value,
|
||
};
|
||
// 发出更新事件
|
||
this.$emit("update:command", updatedCommand);
|
||
// 处理变量管理
|
||
this.handleOutputVariableChange(value);
|
||
},
|
||
handleToggleOutput() {
|
||
// 创建命令的副本
|
||
const updatedCommand = {
|
||
...this.command,
|
||
saveOutput: !this.command.saveOutput,
|
||
};
|
||
|
||
// 如果关闭输出,清空变量名
|
||
if (!updatedCommand.saveOutput && updatedCommand.outputVariable) {
|
||
this.removeVariable(updatedCommand.outputVariable);
|
||
updatedCommand.outputVariable = null;
|
||
}
|
||
|
||
// 发出更新事件
|
||
this.$emit("update:command", updatedCommand);
|
||
},
|
||
handleArgvChange(key, value) {
|
||
// 收集所有参数的当前值
|
||
const args = this.command.config.reduce((acc, item) => {
|
||
acc[item.key] = item.key === key ? value : item.value || "";
|
||
return acc;
|
||
}, {});
|
||
|
||
// 按照配置顺序拼接参数值
|
||
const argv = this.command.config
|
||
.map((item) => args[item.key])
|
||
.filter(Boolean)
|
||
.join(",");
|
||
|
||
this.$emit("update:argv", argv);
|
||
},
|
||
runCommand() {
|
||
// 创建一个带临时变量的命令副本
|
||
const tempCommand = {
|
||
...this.command,
|
||
outputVariable: this.command.outputVariable || `temp_${Date.now()}`,
|
||
saveOutput: true,
|
||
};
|
||
this.$emit("run", tempCommand);
|
||
},
|
||
handleToggleCollapse() {
|
||
if (this.command.isControlFlow) {
|
||
// 控制命令的折叠,直接传递给父组件处理
|
||
this.$emit("toggle-collapse", {
|
||
isCollapsed: this.isCollapsed,
|
||
chainId: this.command.chainId,
|
||
});
|
||
} else {
|
||
// 非控制命令的折叠,更新自身状态
|
||
this.isCollapsed = !this.isCollapsed;
|
||
}
|
||
},
|
||
handleMouseDown(event) {
|
||
// 检查点击的元素是否是控制元素(按钮、输入框等)
|
||
const isControlElement = event.target.closest(
|
||
".q-btn, .q-field, .q-icon, button, input, .border-label"
|
||
);
|
||
this.isClickingControl = !!isControlElement;
|
||
},
|
||
handleMouseUp() {
|
||
// 延迟重置状态,以确保动画不会立即触发
|
||
setTimeout(() => {
|
||
this.isClickingControl = false;
|
||
}, 100);
|
||
},
|
||
},
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* 卡片基础样式 */
|
||
.composer-card {
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
transform-origin: center;
|
||
opacity: 1;
|
||
transform: translateY(0) scale(1);
|
||
border-radius: inherit;
|
||
position: relative;
|
||
}
|
||
|
||
.composer-card.no-animation,
|
||
.composer-card.no-animation::before,
|
||
.composer-card.no-animation .command-item {
|
||
transition: none !important;
|
||
transform: none !important;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05) !important;
|
||
}
|
||
|
||
.composer-card.drag-handle {
|
||
cursor: grab;
|
||
}
|
||
|
||
.composer-card.no-animation.drag-handle {
|
||
cursor: default;
|
||
}
|
||
|
||
.composer-card.drag-handle:active {
|
||
cursor: grabbing;
|
||
}
|
||
|
||
.composer-card.drag-handle:hover::before {
|
||
content: "";
|
||
position: absolute;
|
||
inset: 0;
|
||
background: var(--q-primary);
|
||
opacity: 0.03;
|
||
border-radius: inherit;
|
||
transition: all 0.3s ease;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.composer-card.drag-handle:active::before {
|
||
opacity: 0.06;
|
||
transform: scale(0.99);
|
||
}
|
||
|
||
.command-item {
|
||
transition: all 0.3s ease;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||
border-radius: inherit;
|
||
user-select: none;
|
||
transform: translateZ(0);
|
||
will-change: transform;
|
||
}
|
||
|
||
.drag-handle .command-item {
|
||
transition: transform 0.2s ease, box-shadow 0.3s ease;
|
||
}
|
||
|
||
.drag-handle:hover .command-item {
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||
}
|
||
|
||
.drag-handle:active .command-item {
|
||
transform: translateY(0) scale(0.98);
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||
}
|
||
|
||
/* 暗色模式适配 */
|
||
.body--dark .command-item {
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
.body--dark .drag-handle:hover .command-item {
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.body--dark .drag-handle:active .command-item {
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
|
||
}
|
||
|
||
.body--dark .composer-card.drag-handle:hover::before {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.body--dark .composer-card.drag-handle:active::before {
|
||
background: rgba(255, 255, 255, 0.15);
|
||
}
|
||
|
||
/* 收起状态样式 */
|
||
.composer-card.collapsed {
|
||
transform-origin: top;
|
||
}
|
||
|
||
.composer-card.collapsed .command-item {
|
||
min-height: 20px;
|
||
}
|
||
|
||
/* 卡片内容区域动画 */
|
||
.card-section {
|
||
transition: padding 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||
padding: 8px;
|
||
}
|
||
|
||
.card-section.collapsed {
|
||
padding: 2px 8px;
|
||
}
|
||
|
||
/* 命令内容动画 */
|
||
.command-content-wrapper {
|
||
display: grid;
|
||
grid-template-rows: 1fr;
|
||
transition: grid-template-rows 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.command-content {
|
||
min-height: 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.command-content-wrapper.collapsed {
|
||
grid-template-rows: 0fr;
|
||
margin-top: 0;
|
||
}
|
||
|
||
.command-content-wrapper.collapsed .command-content {
|
||
opacity: 0;
|
||
}
|
||
|
||
/* 调整控制流程组件的样式 */
|
||
.command-item :deep(.condition-type-btn) {
|
||
margin-left: -8px;
|
||
}
|
||
</style>
|