重构CommandEditor组件

This commit is contained in:
fofolee 2025-02-15 11:28:26 +08:00
parent 9ffa941e72
commit c6b511696a
22 changed files with 1038 additions and 1776 deletions

View File

@ -1,73 +1,50 @@
<template> <template>
<!-- 命令设置栏 --> <div class="command-editor">
<CommandSideBar
ref="sidebar"
:canCommandSave="canCommandSave"
:quickcommandInfo="quickcommandInfo"
:allQuickCommandTags="allQuickCommandTags"
class="absolute-left shadow-1"
:style="{
zIndex: 1,
transform: isFullscreen ? 'translateX(-100%)' : 'translateX(0)',
transition: 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
}"
:sideBarWidth="sideBarWidth"
v-if="showSidebar"
@back="handleBack"
></CommandSideBar>
<!-- 编程语言栏 --> <!-- 编程语言栏 -->
<CommandLanguageBar <CommandLanguageBar
class="absolute-top"
:style="{
left: showSidebar ? sideBarWidth + 'px' : 65,
zIndex: 1,
transform: isFullscreen ? 'translateY(-100%)' : 'translateY(0)',
opacity: isFullscreen ? 0 : 1,
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
}"
v-model="quickcommandInfo" v-model="quickcommandInfo"
:height="languageBarHeight"
:canCommandSave="canCommandSave" :canCommandSave="canCommandSave"
:isRunCodePage="isRunCodePage" :isRunCodePage="isRunCodePage"
@program-changed="programChanged" @action="handleAction"
@run="runCurrentCommand" />
@save="saveCurrentCommand"
@show-composer="showComposer = true" <!-- 命令设置栏 -->
<CommandConfig
v-if="!isRunCodePage"
:model-value="commandConfig"
@update:is-expanded="isConfigExpanded = $event"
:expand-on-focus="true"
class="command-config"
@update:model-value="updateCommandConfig"
/> />
<!-- 编辑器 --> <!-- 编辑器 -->
<MonacoEditor <CodeEditor
class="editor-transition" v-model="quickcommandInfo.cmd"
:placeholder="true" :language="getLanguage()"
:cursor-position="quickcommandInfo.cursorPosition"
@update:cursor-position="quickcommandInfo.cursorPosition = $event"
placeholder="请输入代码"
class="codeEditor"
ref="editor" ref="editor"
@loaded="monacoInit"
@typing="(val) => monacoTyping(val)"
@keyStroke="monacoKeyStroke"
:style="{
position: 'absolute',
top: isFullscreen ? 0 : languageBarHeight + 'px',
left: isFullscreen ? 0 : action.type === 'run' ? 0 : sideBarWidth + 'px',
right: 0,
bottom: 0,
}"
/> />
</div>
<!-- 编辑器工具按钮组 --> <!-- 编辑器工具按钮组 -->
<EditorTools <EditorTools
ref="editorTools" ref="editorTools"
v-show="!isConfigExpanded"
:commandCode="quickcommandInfo?.features?.code || 'temp'" :commandCode="quickcommandInfo?.features?.code || 'temp'"
:isFullscreen="isFullscreen"
@restore="restoreHistory" @restore="restoreHistory"
@toggle-fullscreen="toggleFullscreen"
/> />
<!-- 可视化编排 --> <!-- 可视化编排 -->
<q-dialog v-model="showComposer" maximized> <q-dialog v-model="showComposer" maximized>
<CommandComposer <CommandComposer
ref="composer" ref="composer"
v-model="composerInfo"
@action="handleComposerAction" @action="handleComposerAction"
:model-value="{ flows }" :disabled-control-buttons="['save']"
/> />
</q-dialog> </q-dialog>
@ -76,8 +53,8 @@
</template> </template>
<script> <script>
import { defineAsyncComponent } from "vue"; import { defineAsyncComponent, ref, computed } from "vue";
import CommandSideBar from "components/editor/CommandSideBar"; import CommandConfig from "./editor/CommandConfig.vue";
import CommandLanguageBar from "components/editor/CommandLanguageBar"; import CommandLanguageBar from "components/editor/CommandLanguageBar";
import EditorTools from "components/editor/EditorTools"; import EditorTools from "components/editor/EditorTools";
import CommandRunResult from "components/CommandRunResult"; import CommandRunResult from "components/CommandRunResult";
@ -86,53 +63,72 @@ import programs from "js/options/programs.js";
import { dbManager } from "js/utools.js"; import { dbManager } from "js/utools.js";
// MonacoEditor // MonacoEditor
const MonacoEditorPromise = import("components/editor/MonacoEditor"); const CodeEditorPromise = import("components/editor/CodeEditor.vue");
// //
if (window.requestIdleCallback) { if (window.requestIdleCallback) {
window.requestIdleCallback(() => { window.requestIdleCallback(() => {
MonacoEditorPromise; CodeEditorPromise;
}); });
} else { } else {
setTimeout(() => { setTimeout(() => {
MonacoEditorPromise; CodeEditorPromise;
}, 0); }, 0);
} }
// Performance Scripting > 500ms // Performance Scripting > 500ms
const MonacoEditor = defineAsyncComponent({ const CodeEditor = defineAsyncComponent({
loader: () => MonacoEditorPromise, loader: () => CodeEditorPromise,
timeout: 3000, timeout: 3000,
}); });
// TODO:
export default { export default {
components: { components: {
MonacoEditor, CodeEditor,
CommandSideBar, CommandConfig,
CommandRunResult, CommandRunResult,
CommandLanguageBar, CommandLanguageBar,
CommandComposer, CommandComposer,
EditorTools, EditorTools,
}, },
emits: ["editorEvent"],
data() { data() {
return { return {
programLanguages: Object.keys(programs), programLanguages: Object.keys(programs),
sideBarWidth: 200,
languageBarHeight: 40,
showComposer: false, showComposer: false,
isRunCodePage: this.action.type === "run", listener: null,
canCommandSave: this.action.type !== "run", isConfigExpanded: false,
showSidebar: this.action.type !== "run", composerInfo: {
flows: [ program: "quickcomposer",
{
id: "main",
name: "main",
label: "主流程",
commands: [],
customVariables: [],
}, },
], };
quickcommandInfo: { },
props: {
action: {
type: Object,
required: true,
},
},
setup(props) {
const isRunCodePage = ref(props.action.type === "run");
const canCommandSave = ref(!isRunCodePage.value);
const commandAction = window.lodashM.cloneDeep(props.action);
const savedCommand = isRunCodePage.value
? dbManager.getDB("cfg_codeHistory")
: commandAction.data || {};
const defaultCommand = {
program: "quickcommand", program: "quickcommand",
features: {
icon: programs.quickcommand.icon,
explain: "",
platform: ["win32", "linux", "darwin"],
mainPush: false,
cmds: [],
},
output: "text",
tags: [],
cmd: "", cmd: "",
scptarg: "", scptarg: "",
charset: { charset: {
@ -144,136 +140,73 @@ export default {
argv: "", argv: "",
ext: "", ext: "",
}, },
}, };
resultMaxLength: 10000, const quickcommandInfo = ref({
listener: null, ...defaultCommand,
isFullscreen: false, ...savedCommand,
});
//
if (quickcommandInfo.value.tags?.includes("默认") && !utools.isDev()) {
canCommandSave.value = false;
}
const commandConfig = computed(() => {
const { tags, output, features, program } = quickcommandInfo.value;
return { tags, output, features, program };
});
return {
quickcommandInfo,
isRunCodePage,
canCommandSave,
commandConfig,
}; };
}, },
props: {
action: {
type: Object,
required: true,
},
allQuickCommandTags: Array,
},
mounted() { mounted() {
this.commandInit(); this.saveToHistory();
this.sidebarInit(); document.addEventListener("keydown", this.handleKeydown);
},
beforeUnmount() {
document.removeEventListener("keydown", this.handleKeydown);
}, },
methods: { methods: {
//
commandInit() {
let quickCommandInfo = this.isRunCodePage
? dbManager.getDB("cfg_codeHistory")
: this.action.data;
quickCommandInfo?.program &&
Object.assign(
this.quickcommandInfo,
window.lodashM.cloneDeep(quickCommandInfo)
);
//
if (this.quickcommandInfo.tags?.includes("默认") && !utools.isDev()) {
this.canCommandSave = false;
}
},
//
sidebarInit() {
this.$refs.sidebar?.init();
},
// MonacoMonaco
monacoInit() {
this.$refs.editor.setEditorValue(this.quickcommandInfo.cmd);
this.setLanguage(this.quickcommandInfo.program);
this.$refs.editor.setCursorPosition(this.quickcommandInfo.cursorPosition);
//
setTimeout(() => {
this.saveToHistory();
}, 1000); //
},
programChanged(value) {
this.setLanguage(value);
if (value === "custom") this.$refs.settings.show();
this.$refs.sidebar?.setIcon(value);
},
//
matchLanguage() {
if (!this.quickcommandInfo.customOptions.ext) return;
let language = Object.values(programs).filter(
(program) => program.ext === this.quickcommandInfo.customOptions.ext
);
if (language.length) {
this.setLanguage(language[0].name);
}
},
//
setLanguage(language) {
let highlight = programs[language].highlight;
this.$refs.editor.setEditorLanguage(highlight ? highlight : language);
},
insertText(text) {
this.$refs.editor.repacleEditorSelection(text);
this.$refs.editor.formatDocument();
},
replaceText(text) {
this.$refs.editor.setEditorValue(text);
this.$refs.editor.formatDocument();
},
handleComposerAction(actionType, actionData) { handleComposerAction(actionType, actionData) {
switch (actionType) { switch (actionType) {
case "run": case "run":
return this.runCurrentCommand(actionData); // actionData
case "insert": this.runCurrentCommand(actionData);
return this.insertText(actionData); break;
case "apply": case "apply":
return this.replaceText(actionData); // actionData cmd
console.log(actionData);
this.showComposer = false;
this.quickcommandInfo.cmd = actionData;
break;
case "close": case "close":
return (this.showComposer = false); this.showComposer = false;
break;
} }
}, },
// //
saveCurrentCommand(message = "保存成功") { saveCurrentCommand() {
let updatedData = this.$refs.sidebar?.SaveMenuData(); this.$emit("editorEvent", "save", this.quickcommandInfo);
if (!updatedData) return;
Object.assign(
this.quickcommandInfo,
window.lodashM.cloneDeep(updatedData)
);
let newQuickcommandInfo = window.lodashM.cloneDeep(this.quickcommandInfo);
dbManager.putDB(
newQuickcommandInfo,
"qc_" + this.quickcommandInfo.features.code
);
this.$emit("editorEvent", {
type: "save",
data: newQuickcommandInfo,
});
this.saveToHistory(); // this.saveToHistory(); //
if (!message) return;
quickcommand.showMessageBox(message, "success", 1000, "bottom-right");
}, },
// //
runCurrentCommand(cmd) { runCurrentCommand(command) {
if (!command) {
this.saveToHistory(); // this.saveToHistory(); //
let command = window.lodashM.cloneDeep(this.quickcommandInfo); command = { ...this.quickcommandInfo };
if (cmd) command.cmd = cmd; }
command.output =
this.$refs.sidebar?.currentCommand.output ||
(command.program === "html" ? "html" : "text");
command.cmdType = this.$refs.sidebar?.cmdType.name;
this.$refs.result.runCurrentCommand(command); this.$refs.result.runCurrentCommand(command);
}, },
saveCodeHistory() { saveCodeHistory() {
if (this.action.type !== "run") return; if (!this.isRunCodePage) return;
let command = window.lodashM.cloneDeep(this.quickcommandInfo); let command = window.lodashM.cloneDeep(this.quickcommandInfo);
command.cursorPosition = this.$refs.editor.getCursorPosition();
dbManager.putDB(command, "cfg_codeHistory"); dbManager.putDB(command, "cfg_codeHistory");
}, },
monacoTyping(val) { handleAction(event, data) {
this.quickcommandInfo.cmd = val;
},
monacoKeyStroke(event, data) {
switch (event) { switch (event) {
case "run": case "run":
this.runCurrentCommand(); this.runCurrentCommand();
@ -281,28 +214,22 @@ export default {
case "save": case "save":
this.saveCurrentCommand(); this.saveCurrentCommand();
break; break;
case "log": case "back":
if (this.quickcommandInfo.program !== "quickcommand") return; this.$emit("editorEvent", "back");
this.runCurrentCommand(`console.log(${data})`);
break; break;
case "fullscreen": case "show-composer":
this.toggleFullscreen(); this.showComposer = true;
break;
case "insert-text":
this.$refs.editor.repacleEditorSelection(data);
break; break;
default: default:
break; break;
} }
}, },
toggleFullscreen() {
this.isFullscreen = !this.isFullscreen;
//
setTimeout(() => {
this.$refs.editor.resizeEditor();
}, 300);
},
saveToHistory() { saveToHistory() {
this.$refs.editorTools.tryToSave( this.$refs.editorTools.tryToSave(
this.$refs.editor.getEditorValue(), this.quickcommandInfo.cmd,
this.quickcommandInfo.program this.quickcommandInfo.program
); );
}, },
@ -311,27 +238,72 @@ export default {
this.saveToHistory(); this.saveToHistory();
// //
this.$refs.editor.setEditorValue(item.content); this.quickcommandInfo.cmd = item.content;
this.quickcommandInfo.program = item.program;
}, },
handleBack() { updateCommandConfig(value) {
// this.quickcommandInfo = {
this.$emit("editorEvent", { type: "back" }); ...this.quickcommandInfo,
...value,
};
},
getLanguage() {
if (this.quickcommandInfo.program !== "custom") {
return this.quickcommandInfo.program;
}
if (!this.quickcommandInfo.customOptions.ext) return;
let language = Object.values(programs).find(
(program) => program.ext === this.quickcommandInfo.customOptions.ext
);
if (!language) return;
return language.name;
},
//
handleKeydown(e) {
// Ctrl (Windows) Command (Mac)
const isCmdOrCtrl = window.utools.isMacOS() ? e.metaKey : e.ctrlKey;
if (!isCmdOrCtrl) return;
switch (e.key.toLowerCase()) {
case "s":
e.preventDefault();
if (!this.canCommandSave) return;
this.saveCurrentCommand();
break;
case "b":
e.preventDefault();
this.runCurrentCommand();
break;
}
}, },
}, },
}; };
</script> </script>
<style scoped> <style scoped>
/* 统一过渡效果 */ .command-editor {
.sidebar-transition, display: flex;
.language-bar-transition { flex-direction: column;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); height: 100%;
will-change: transform, left, top, opacity; border-radius: 10px;
overflow: hidden;
background-color: #fffffe;
position: fixed;
inset: 0;
} }
/* 编辑器动画不一致,可以产生一个回弹效果 */ .body--dark .command-editor {
.editor-transition { background-color: #1e1e1e;
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); }
will-change: transform, left, top, opacity;
.codeEditor {
flex: 1;
min-height: 0;
border-radius: 0 0 10px 10px;
overflow: hidden;
}
.command-config {
padding: 4px 10px;
} }
</style> </style>

View File

@ -78,6 +78,7 @@ import ResultMenu from "components/popup/ResultMenu.vue";
import { generateFlowsCode } from "js/composer/generateCode"; import { generateFlowsCode } from "js/composer/generateCode";
import { getValidCommand } from "js/commandManager"; import { getValidCommand } from "js/commandManager";
import { dbManager } from "js/utools.js"; import { dbManager } from "js/utools.js";
import programs from "js/options/programs.js";
export default { export default {
components: { ResultArea, ResultMenu }, components: { ResultArea, ResultMenu },

View File

@ -3,7 +3,7 @@
ref="composer" ref="composer"
@action="handleComposerAction" @action="handleComposerAction"
v-model="quickcommandInfo" v-model="quickcommandInfo"
:show-close-button="!isRunComposePage" :disabled-control-buttons="disabledControlButtons"
class="fixed-full" class="fixed-full"
/> />
<!-- 运行结果 --> <!-- 运行结果 -->
@ -15,7 +15,7 @@ import CommandComposer from "components/composer/CommandComposer.vue";
import CommandRunResult from "components/CommandRunResult"; import CommandRunResult from "components/CommandRunResult";
import { findCommandByValue } from "js/composer/composerConfig"; import { findCommandByValue } from "js/composer/composerConfig";
import programs from "js/options/programs.js"; import programs from "js/options/programs.js";
import { ref } from "vue"; import { ref, computed } from "vue";
export default { export default {
components: { CommandComposer, CommandRunResult }, components: { CommandComposer, CommandRunResult },
@ -82,15 +82,7 @@ export default {
mainPush: false, mainPush: false,
cmds: [], cmds: [],
}, },
flows: [ flows: [],
{
id: "main",
name: "main",
label: "主流程",
commands: [],
customVariables: [],
},
],
output: "text", output: "text",
tags: [], tags: [],
}; };
@ -99,12 +91,18 @@ export default {
...retoreToFullCommand(savedCommand), ...retoreToFullCommand(savedCommand),
}); });
const isRunComposePage = ref(props.action.type === "composer"); const isRunComposePage = computed(() => {
return props.action.type === "composer";
});
const disabledControlButtons = computed(() => {
return isRunComposePage.value ? ["close", "save", "apply"] : ["apply"];
});
return { return {
quickcommandInfo, quickcommandInfo,
getLitedComposerCommand, getLitedComposerCommand,
isRunComposePage, disabledControlButtons,
}; };
}, },
emits: ["editorEvent"], emits: ["editorEvent"],
@ -120,14 +118,13 @@ export default {
case "run": case "run":
return this.runCurrentCommand(command); return this.runCurrentCommand(command);
case "close": case "close":
return this.$emit("editorEvent", { return this.$emit("editorEvent", "back");
type: "back",
});
case "save": case "save":
return this.$emit("editorEvent", { return this.$emit(
type: "save", "editorEvent",
data: this.getLitedComposerCommand(command), "save",
}); this.getLitedComposerCommand(command)
);
} }
}, },
runCurrentCommand(command) { runCurrentCommand(command) {

View File

@ -11,7 +11,7 @@
<div class="col command-section"> <div class="col command-section">
<FlowTabs <FlowTabs
@action="handleAction" @action="handleAction"
:show-close-button="showCloseButton" :disabled-control-buttons="disabledControlButtons"
:model-value="modelValue" :model-value="modelValue"
@update:model-value="$emit('update:modelValue', $event)" @update:model-value="$emit('update:modelValue', $event)"
/> />
@ -42,9 +42,9 @@ export default defineComponent({
}; };
}, },
props: { props: {
showCloseButton: { disabledControlButtons: {
type: Boolean, type: Array,
default: true, default: () => [],
}, },
modelValue: { modelValue: {
type: Object, type: Object,

View File

@ -197,7 +197,8 @@ export default defineComponent({
}; };
const consoleLogVars = const consoleLogVars =
this.getAvailableOutputVariableName(outputVariable); this.getAvailableOutputVariableName(outputVariable);
const tempFlows = [{ const tempFlows = [
{
name: "main", name: "main",
commands: [ commands: [
tempCommand, tempCommand,
@ -314,6 +315,14 @@ export default defineComponent({
position: relative; position: relative;
} }
.composer-card .q-card {
background-color: rgba(0, 0, 0, 0.02);
}
.body--dark .composer-card .q-card {
background-color: rgba(255, 255, 255, 0.05);
}
/* 控制流程组件样式 */ /* 控制流程组件样式 */
.control-component { .control-component {
flex: 1; flex: 1;

View File

@ -60,7 +60,7 @@
<ComposerButtons <ComposerButtons
:is-all-collapsed="isAllCollapsed" :is-all-collapsed="isAllCollapsed"
:show-close-button="showCloseButton" :disabled-buttons="disabledControlButtons"
:flows="flows" :flows="flows"
@action="handleAction" @action="handleAction"
/> />
@ -86,7 +86,7 @@
ref="flowRefs" ref="flowRefs"
/> />
<FlowManager <FlowManager
v-model="showVariableManager" v-model="showFlowManager"
:flow="flow" :flow="flow"
:variables="flow.customVariables" :variables="flow.customVariables"
@update-flow="updateFlows(flow)" @update-flow="updateFlows(flow)"
@ -119,9 +119,9 @@ export default defineComponent({
}, },
emits: ["update:modelValue", "action"], emits: ["update:modelValue", "action"],
props: { props: {
showCloseButton: { disabledControlButtons: {
type: Boolean, type: Array,
default: true, default: () => [],
}, },
modelValue: { modelValue: {
type: Object, type: Object,
@ -136,7 +136,26 @@ export default defineComponent({
}); });
}; };
const flows = computed(() => props.modelValue.flows); const defaultFlow = [
{
id: "main",
name: "main",
label: "主流程",
commands: [],
customVariables: [],
},
];
if (!props.modelValue.flows || props.modelValue.flows.length === 0) {
updateFlows(defaultFlow);
}
const flows = computed(() => props.modelValue.flows || []);
const commandConfig = computed(() => {
const { tags, output, features, program } = props.modelValue;
return { tags, output, features, program };
});
const mainFlow = computed({ const mainFlow = computed({
get: () => flows.value[0], get: () => flows.value[0],
@ -235,6 +254,7 @@ export default defineComponent({
flows, flows,
mainFlow, mainFlow,
subFlows, subFlows,
commandConfig,
activeTab, activeTab,
getOutputVariables, getOutputVariables,
updateFlows, updateFlows,
@ -243,16 +263,10 @@ export default defineComponent({
data() { data() {
return { return {
isAllCollapsed: false, isAllCollapsed: false,
showVariableManager: false, showFlowManager: false,
outputVariables: [], outputVariables: [],
}; };
}, },
computed: {
commandConfig() {
const { tags, output, features } = this.modelValue;
return { tags, output, features };
},
},
methods: { methods: {
generateFlowName(baseName = "func_") { generateFlowName(baseName = "func_") {
return ( return (
@ -293,7 +307,7 @@ export default defineComponent({
this.activeTab = id; this.activeTab = id;
this.$nextTick(() => { this.$nextTick(() => {
this.toggleVariableManager(); this.toggleFlowManager();
}); });
}, },
removeFlow(flow) { removeFlow(flow) {
@ -332,12 +346,15 @@ export default defineComponent({
case "expandAll": case "expandAll":
this.expandAll(); this.expandAll();
break; break;
case "toggleVariableManager": case "toggleFlowManager":
this.toggleVariableManager(); this.toggleFlowManager();
break; break;
case "close": case "close":
this.$emit("action", "close"); this.$emit("action", "close");
break; break;
case "apply":
this.$emit("action", "apply", payload);
break;
case "addFlow": case "addFlow":
// //
const index = this.subFlows.findIndex((f) => f.name === payload.name); const index = this.subFlows.findIndex((f) => f.name === payload.name);
@ -352,8 +369,8 @@ export default defineComponent({
this.$emit("action", type, payload); this.$emit("action", type, payload);
} }
}, },
toggleVariableManager() { toggleFlowManager() {
this.showVariableManager = !this.showVariableManager; this.showFlowManager = !this.showFlowManager;
this.outputVariables = this.getOutputVariables(); this.outputVariables = this.getOutputVariables();
}, },
saveFlows() { saveFlows() {
@ -383,7 +400,7 @@ export default defineComponent({
}, },
editFunction(flow) { editFunction(flow) {
this.activeTab = flow.id; this.activeTab = flow.id;
this.toggleVariableManager(); this.toggleFlowManager();
}, },
updateCommandConfig(newVal) { updateCommandConfig(newVal) {
const newModelValue = { const newModelValue = {

View File

@ -5,8 +5,8 @@
icon="close" icon="close"
dense dense
flat flat
v-if="showCloseButton"
@click="$emit('action', 'close')" @click="$emit('action', 'close')"
v-if="!disabledButtons.includes('close')"
> >
<q-tooltip>退出可视化编排</q-tooltip> <q-tooltip>退出可视化编排</q-tooltip>
</q-btn> </q-btn>
@ -15,6 +15,7 @@
dense dense
flat flat
@click="$emit('action', isAllCollapsed ? 'expandAll' : 'collapseAll')" @click="$emit('action', isAllCollapsed ? 'expandAll' : 'collapseAll')"
v-if="!disabledButtons.includes('expand')"
> >
<q-tooltip>{{ isAllCollapsed ? "展开所有" : "折叠所有" }}</q-tooltip> <q-tooltip>{{ isAllCollapsed ? "展开所有" : "折叠所有" }}</q-tooltip>
</q-btn> </q-btn>
@ -22,7 +23,8 @@
icon="settings" icon="settings"
dense dense
flat flat
@click="$emit('action', 'toggleVariableManager')" @click="$emit('action', 'toggleFlowManager')"
v-if="!disabledButtons.includes('manager')"
> >
<q-tooltip>流程管理</q-tooltip> <q-tooltip>流程管理</q-tooltip>
</q-btn> </q-btn>
@ -34,33 +36,40 @@
v-if="isDev" v-if="isDev"
> >
</q-btn> </q-btn>
<!-- <q-btn <q-btn
dense
icon="read_more"
flat
v-close-popup
@click="$emit('action', 'insert')"
v-if="showCloseButton"
>
<q-tooltip>插入到编辑器光标处</q-tooltip>
</q-btn> -->
<!-- <q-btn
dense dense
flat flat
v-close-popup
icon="done_all" icon="done_all"
@click="$emit('action', 'apply')" @click="handleApply"
v-if="showCloseButton" v-if="!disabledButtons.includes('apply')"
> >
<q-tooltip>清空编辑器内容并插入</q-tooltip> <q-tooltip>清空编辑器内容并插入</q-tooltip>
</q-btn> --> </q-btn>
<q-btn dense flat icon="save" @click="$emit('action', 'save')"> <q-btn
dense
flat
icon="save"
@click="$emit('action', 'save')"
v-if="!disabledButtons.includes('save')"
>
<q-tooltip>保存</q-tooltip> <q-tooltip>保存</q-tooltip>
</q-btn> </q-btn>
<q-btn flat dense icon="preview" @click="isVisible = true"> <q-btn
flat
dense
icon="preview"
@click="handlePreviewCode"
v-if="!disabledButtons.includes('preview')"
>
<q-tooltip>预览代码</q-tooltip> <q-tooltip>预览代码</q-tooltip>
</q-btn> </q-btn>
<q-btn dense flat icon="play_circle" @click="$emit('action', 'run')"> <q-btn
dense
flat
icon="play_circle"
@click="$emit('action', 'run')"
v-if="!disabledButtons.includes('run')"
>
<q-tooltip>运行</q-tooltip> <q-tooltip>运行</q-tooltip>
</q-btn> </q-btn>
</div> </div>
@ -99,9 +108,9 @@ export default defineComponent({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
showCloseButton: { disabledButtons: {
type: Boolean, type: Array,
default: true, default: () => [],
}, },
flows: { flows: {
type: Array, type: Array,
@ -119,11 +128,15 @@ export default defineComponent({
}; };
}, },
watch: { methods: {
isVisible(val) { handlePreviewCode() {
if (val) {
this.code = generateFlowsCode(this.flows); this.code = generateFlowsCode(this.flows);
} this.isVisible = true;
},
//
handleApply() {
const code = generateFlowsCode(this.flows);
this.$emit("action", "apply", code);
}, },
}, },
}); });

View File

@ -26,7 +26,7 @@ import TimeInput from "components/composer/common/TimeInput.vue";
import FunctionInput from "components/composer/common/FunctionInput.vue"; import FunctionInput from "components/composer/common/FunctionInput.vue";
import { QInput, QSelect, QToggle, QCheckbox } from "quasar"; import { QInput, QSelect, QToggle, QCheckbox } from "quasar";
const CodeEditor = defineAsyncComponent(() => const CodeEditor = defineAsyncComponent(() =>
import("components/composer/common/CodeEditor.vue") import("components/editor/CodeEditor.vue")
); );
export default defineComponent({ export default defineComponent({

View File

@ -127,7 +127,7 @@
<script> <script>
import { defineComponent } from "vue"; import { defineComponent } from "vue";
import { newVarInputVal } from "js/composer/varInputValManager"; import { newVarInputVal } from "js/composer/varInputValManager";
import CodeEditor from "components/composer/common/CodeEditor.vue"; import CodeEditor from "components/editor/CodeEditor.vue";
import VariableInput from "components/composer/common/VariableInput.vue"; import VariableInput from "components/composer/common/VariableInput.vue";
import ArrayEditor from "components/composer/common/ArrayEditor.vue"; import ArrayEditor from "components/composer/common/ArrayEditor.vue";
import BorderLabel from "components/composer/common/BorderLabel.vue"; import BorderLabel from "components/composer/common/BorderLabel.vue";

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="code-editor" :style="{ height: height }"> <div class="code-editor" :style="{ height: height }">
<div ref="editorContainer" class="editor-container" /> <div ref="editorContainer" class="editor-container" />
<div class="placeholder-wrapper" v-show="!value && placeholder"> <div class="placeholder-wrapper" v-show="showPlaceholder">
<div class="placeholder"> <div class="placeholder">
{{ placeholder }} {{ placeholder }}
</div> </div>
@ -11,7 +11,6 @@
<script> <script>
import * as monaco from "monaco-editor"; import * as monaco from "monaco-editor";
import { toRaw } from "vue";
import importAll from "js/common/importAll.js"; import importAll from "js/common/importAll.js";
import { defineComponent } from "vue"; import { defineComponent } from "vue";
@ -21,6 +20,7 @@ let languageCompletions = importAll(
); );
let monacoCompletionProviders = {}; let monacoCompletionProviders = {};
let editor = null;
// //
const typeDefinitions = { const typeDefinitions = {
@ -70,12 +70,15 @@ export default defineComponent({
type: String, type: String,
default: "请输入...", default: "请输入...",
}, },
//
cursorPosition: {
type: Object,
default: () => ({}),
}, },
emits: ["update:modelValue", "change"], },
emits: ["update:modelValue", "update:cursorPosition"],
data() { data() {
return { return {
editor: null,
value: null,
resizeTimeout: null, resizeTimeout: null,
defaultOptions: { defaultOptions: {
value: "", value: "",
@ -138,12 +141,19 @@ export default defineComponent({
modelValue: { modelValue: {
immediate: true, immediate: true,
handler(newValue) { handler(newValue) {
if (this.value !== newValue) { if (!editor || editor.getValue() === newValue) return;
this.value = newValue; editor.setValue(newValue || "");
if (this.editor && this.editor.getValue() !== newValue) { },
this.editor.setValue(newValue || ""); },
} cursorPosition: {
} immediate: true,
handler(newValue) {
if (!editor) return;
const { lineNumber, column } = newValue;
if (!lineNumber || !column) return;
const pos = editor.getPosition();
if (pos.lineNumber === lineNumber && pos.column === column) return;
editor.setPosition(newValue);
}, },
}, },
"$q.dark.isActive": { "$q.dark.isActive": {
@ -155,11 +165,10 @@ export default defineComponent({
language: { language: {
immediate: true, immediate: true,
handler(newValue) { handler(newValue) {
if (this.editor) { if (!editor) return;
const language = this.getHighlighter(newValue); const language = this.getHighlighter(newValue);
monaco.editor.setModelLanguage(this.rawEditor().getModel(), language); monaco.editor.setModelLanguage(editor.getModel(), language);
this.loadTypes(); this.loadTypes();
}
}, },
}, },
}, },
@ -180,16 +189,17 @@ export default defineComponent({
const options = { const options = {
...this.defaultOptions, ...this.defaultOptions,
...this.options, ...this.options,
value: this.value || "", value: this.modelValue || "",
language, language,
theme: this.theme, theme: this.theme,
}; };
this.editor = monaco.editor.create(this.$refs.editorContainer, options); editor = monaco.editor.create(this.$refs.editorContainer, options);
this.listenEditorValue(); this.listenEditorValue();
this.loadTypes(); this.loadTypes();
this.registerLanguage(); this.registerLanguage();
this.bindKeys();
this.setCursorPosition(this.cursorPosition);
// //
this.$nextTick(() => { this.$nextTick(() => {
this.resizeEditor(); this.resizeEditor();
@ -197,11 +207,14 @@ export default defineComponent({
}, },
// //
listenEditorValue() { listenEditorValue() {
this.rawEditor().focus(); editor.focus();
this.rawEditor().onDidChangeModelContent(() => { editor.onDidChangeModelContent(() => {
this.value = this.getEditorValue(); this.$emit("update:modelValue", editor.getValue());
this.$emit("update:modelValue", this.value); });
this.$emit("change", this.value);
//
editor.onDidChangeCursorPosition((e) => {
this.$emit("update:cursorPosition", e.position);
}); });
}, },
// //
@ -210,50 +223,24 @@ export default defineComponent({
clearTimeout(this.resizeTimeout); clearTimeout(this.resizeTimeout);
} }
this.resizeTimeout = setTimeout(() => { this.resizeTimeout = setTimeout(() => {
this.rawEditor().layout(); editor.layout();
}, 50); }, 50);
}, },
// //
destroyEditor() { destroyEditor() {
if (this.editor) {
window.removeEventListener("resize", this.resizeEditor); window.removeEventListener("resize", this.resizeEditor);
this.rawEditor().dispose(); if (!editor) return;
this.editor = null; editor.dispose();
} editor = null;
},
//
rawEditor() {
return toRaw(this.editor);
},
//
getEditor() {
return this.editor;
},
//
setValue(value) {
if (this.editor) {
this.editor.setValue(value || "");
}
},
//
getValue() {
return this.editor ? this.editor.getValue() : "";
},
//
getEditorValue() {
return this.rawEditor().getValue();
}, },
// //
focus() { focus() {
if (this.editor) { editor && editor.focus();
this.editor.focus();
}
}, },
registerLanguage() { registerLanguage() {
let that = this;
const identifierPattern = "([a-zA-Z_]\\w*)"; const identifierPattern = "([a-zA-Z_]\\w*)";
let getTokens = (code) => { let getTokens = (code) => {
let identifier = new RegExp(identifierPattern, "g"); const identifier = new RegExp(identifierPattern, "g");
let tokens = []; let tokens = [];
let array1; let array1;
while ((array1 = identifier.exec(code)) !== null) { while ((array1 = identifier.exec(code)) !== null) {
@ -264,7 +251,7 @@ export default defineComponent({
let createDependencyProposals = (range, keyWords, editor, curWord) => { let createDependencyProposals = (range, keyWords, editor, curWord) => {
let keys = []; let keys = [];
// fix getValue of undefined // fix getValue of undefined
let tokens = getTokens(toRaw(editor).getModel()?.getValue()); const tokens = getTokens(editor.getModel()?.getValue());
// //
for (const item of tokens) { for (const item of tokens) {
if (item != curWord.word) { if (item != curWord.word) {
@ -312,7 +299,7 @@ export default defineComponent({
suggestions: createDependencyProposals( suggestions: createDependencyProposals(
range, range,
languageCompletions[language].default, languageCompletions[language].default,
toRaw(that.editor), editor,
word word
), ),
}; };
@ -366,11 +353,7 @@ export default defineComponent({
} }
}, },
getHighlighter(language) { getHighlighter(language) {
if ( if (["quickcommand", "javascript", "webjavascript"].includes(language)) {
["quickcommand", "javascript", "webjavascript"].includes(
language
)
) {
return "javascript"; return "javascript";
} }
if (language === "cmd") { if (language === "cmd") {
@ -378,10 +361,38 @@ export default defineComponent({
} }
return language; return language;
}, },
setCursorPosition(position) {
if (!position.lineNumber || !position.column) return;
editor.setPosition(position);
},
bindKeys() {
// alt + z
const revWordWrap = this.wordWrap === "on" ? "off" : "on";
editor.addCommand(monaco.KeyMod.Alt | monaco.KeyCode.KeyZ, () => {
editor.updateOptions({ wordWrap: revWordWrap });
});
},
repacleEditorSelection(text) {
var selection = editor.getSelection();
var range = new monaco.Range(
selection.startLineNumber,
selection.startColumn,
selection.endLineNumber,
selection.endColumn
);
var id = { major: 1, minor: 1 };
var op = {
identifier: id,
range: range,
text: text,
forceMoveMarkers: true,
};
editor.executeEdits("my-source", [op]);
},
}, },
computed: { computed: {
showPlaceholder() { showPlaceholder() {
return this.placeholder && (!this.value || this.value.trim() === ""); return this.placeholder && !this.modelValue;
}, },
}, },
}); });
@ -390,10 +401,9 @@ export default defineComponent({
<style scoped> <style scoped>
.code-editor { .code-editor {
width: 100%; width: 100%;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 4px;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
border-radius: 4px;
} }
.editor-container { .editor-container {
@ -406,7 +416,7 @@ export default defineComponent({
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
padding-left: 45px; padding-left: 40px;
pointer-events: none; pointer-events: none;
} }
@ -414,12 +424,6 @@ export default defineComponent({
font-size: 14px; font-size: 14px;
font-family: sans-serif; font-family: sans-serif;
user-select: none; user-select: none;
font-style: italic; opacity: 0.4;
opacity: 0;
transition: opacity 0.1s ease-in-out;
}
.code-editor:focus-within .placeholder {
opacity: 0.3;
} }
</style> </style>

View File

@ -1,34 +1,30 @@
<template> <template>
<q-expansion-item <q-expansion-item
v-model="isExpanded" v-model="isExpanded"
class="command-config" @update:model-value="$emit('update:is-expanded', $event)"
@dragover="isExpanded = false" class="command-composer command-config"
> >
<template v-slot:header> <template v-slot:header>
<div class="row q-col-gutter-sm basic-config"> <div class="row basic-config">
<div class="col-auto">
<q-avatar size="36px" square class="featureIco"> <q-avatar size="36px" square class="featureIco">
<q-img <q-img
@click.stop="showIconPicker = true" @click.stop="showIconPicker = true"
:src="currentCommand.features.icon" :src="currentCommand.features.icon"
/> />
</q-avatar> </q-avatar>
</div>
<div class="col">
<q-input <q-input
ref="explainInput"
:model-value="currentCommand.features.explain" :model-value="currentCommand.features.explain"
filled borderless
dense dense
@update:model-value="updateCommand('features.explain', $event)" @update:model-value="updateModelValue('features.explain', $event)"
placeholder="名称"
@click.stop @click.stop
placeholder="请输入名称"
@focus="expandOnFocus && updateExpanded(true)"
class="col"
> >
<template v-slot:append>
<q-icon name="drive_file_rename_outline" />
</template>
</q-input> </q-input>
</div> </div>
</div>
</template> </template>
<!-- 展开的配置项 --> <!-- 展开的配置项 -->
@ -59,7 +55,7 @@
<MatchRuleEditor <MatchRuleEditor
:showJson="showMatchRuleJson" :showJson="showMatchRuleJson"
:model-value="currentCommand.features.cmds" :model-value="currentCommand.features.cmds"
@update:model-value="updateCommand('features.cmds', $event)" @update:model-value="updateModelValue('features.cmds', $event)"
/> />
</div> </div>
@ -71,7 +67,7 @@
</div> </div>
<q-select <q-select
:model-value="currentCommand.tags" :model-value="currentCommand.tags"
@update:model-value="updateCommand('tags', $event)" @update:model-value="updateModelValue('tags', $event)"
:options="allQuickCommandTags" :options="allQuickCommandTags"
dense dense
options-dense options-dense
@ -81,6 +77,7 @@
multiple multiple
hide-dropdown-icon hide-dropdown-icon
new-value-mode="add-unique" new-value-mode="add-unique"
popup-content-class="command-tag-popup"
placeholder="回车添加最多3个" placeholder="回车添加最多3个"
max-values="3" max-values="3"
@new-value="tagVerify" @new-value="tagVerify"
@ -101,7 +98,7 @@
<ButtonGroup <ButtonGroup
:model-value="currentCommand.output" :model-value="currentCommand.output"
:options="outputTypesOptionsDy" :options="outputTypesOptionsDy"
@update:model-value="updateCommand('output', $event)" @update:model-value="updateModelValue('output', $event)"
height="26px" height="26px"
/> />
</div> </div>
@ -138,7 +135,7 @@
<CheckGroup <CheckGroup
:model-value="currentCommand.features.platform" :model-value="currentCommand.features.platform"
:options="Object.values(platformTypes)" :options="Object.values(platformTypes)"
@update:model-value="updateCommand('features.platform', $event)" @update:model-value="handlePlatformChange"
height="30px" height="30px"
/> />
</div> </div>
@ -147,7 +144,7 @@
<!-- 图标选择对话框 --> <!-- 图标选择对话框 -->
<q-dialog v-model="showIconPicker" position="left"> <q-dialog v-model="showIconPicker" position="left">
<iconPicker <iconPicker
@iconChanged="(dataUrl) => updateCommand('features.icon', dataUrl)" @iconChanged="(dataUrl) => updateModelValue('features.icon', dataUrl)"
ref="icon" ref="icon"
/> />
</q-dialog> </q-dialog>
@ -155,7 +152,7 @@
</template> </template>
<script> <script>
import { defineComponent, computed } from "vue"; import { defineComponent } from "vue";
import iconPicker from "components/popup/IconPicker.vue"; import iconPicker from "components/popup/IconPicker.vue";
import outputTypes from "js/options/outputTypes.js"; import outputTypes from "js/options/outputTypes.js";
import platformTypes from "js/options/platformTypes.js"; import platformTypes from "js/options/platformTypes.js";
@ -178,8 +175,12 @@ export default defineComponent({
type: Object, type: Object,
required: true, required: true,
}, },
expandOnFocus: {
type: Boolean,
default: false,
}, },
emits: ["update:modelValue"], },
emits: ["update:modelValue", "update:is-expanded"],
data() { data() {
return { return {
commandManager: useCommandManager(), commandManager: useCommandManager(),
@ -205,29 +206,42 @@ export default defineComponent({
currentCommand() { currentCommand() {
return this.modelValue; return this.modelValue;
}, },
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() { outputTypesOptionsDy() {
const options = Object.values(this.outputTypes); const options = Object.values(this.outputTypes);
return this.currentCommand.features.mainPush if (this.currentCommand.features.mainPush) {
? options.map((outputType) => return this.setOutputOptionDisabled(options, "text", false);
outputType.name !== "text" }
? { ...outputType, disabled: true } if (this.currentCommand.program === "html") {
: outputType return this.setOutputOptionDisabled(options, "html", false);
) }
: options; if (
["quickcommand", "quickcomposer"].includes(this.currentCommand.program)
) {
return this.setOutputOptionDisabled(options, "terminal", true);
}
return options;
}, },
}, },
mounted() {
if (!this.modelValue.features.explain) {
setTimeout(this.$refs.explainInput.focus);
}
//
document.addEventListener("click", this.handleOutsideClick);
},
beforeUnmount() {
//
document.removeEventListener("click", this.handleOutsideClick);
},
methods: { methods: {
updateCommand(path, value) { setOutputOptionDisabled(options, option, disabled = true) {
return options.map((opt) =>
opt.name === option
? { ...opt, disabled }
: { ...opt, disabled: !disabled }
);
},
updateModelValue(path, value) {
const newCommand = { ...this.currentCommand }; const newCommand = { ...this.currentCommand };
const keys = path.split("."); const keys = path.split(".");
const lastKey = keys.pop(); const lastKey = keys.pop();
@ -246,12 +260,6 @@ export default defineComponent({
if (!inputValue) return; if (!inputValue) return;
ref.add(inputValue, true); ref.add(inputValue, true);
}, },
handleMainPushChange(val) {
this.updateCommand("features.mainPush", val);
if (val) {
this.updateCommand("output", "text");
}
},
showMainPushHelp() { showMainPushHelp() {
window.showUb.help("#u0e9f1430"); window.showUb.help("#u0e9f1430");
}, },
@ -262,6 +270,33 @@ export default defineComponent({
) )
.run(); .run();
}, },
handleOutsideClick(event) {
//
if (!this.isExpanded) return;
//
const tagPopup = document.querySelector(".command-tag-popup");
if (tagPopup?.contains(event.target)) return;
//
const componentEl = this.$el;
if (componentEl.contains(event.target)) return;
this.updateExpanded(false);
},
handleMainPushChange(newMainPush) {
this.updateModelValue("features.mainPush", newMainPush);
if (newMainPush) {
this.updateModelValue("output", "text");
}
},
handlePlatformChange(newPlatform) {
if (newPlatform.length === 0) return;
this.updateModelValue("features.platform", newPlatform);
},
updateExpanded(value) {
this.isExpanded = value;
this.$emit("update:is-expanded", value);
},
}, },
}); });
</script> </script>
@ -280,6 +315,11 @@ export default defineComponent({
width: 100%; width: 100%;
} }
.basic-config :deep(.q-field__native),
.basic-config :deep(.q-field__control) {
height: 36px;
}
.command-config :deep(.q-item) { .command-config :deep(.q-item) {
padding: 0; padding: 0;
min-height: unset; min-height: unset;
@ -314,6 +354,7 @@ export default defineComponent({
.featureIco { .featureIco {
cursor: pointer; cursor: pointer;
transition: 0.2s; transition: 0.2s;
margin-right: 10px;
} }
.featureIco:hover { .featureIco:hover {

View File

@ -1,23 +1,31 @@
<template> <template>
<div class="row" v-show="!!height"> <div class="command-language-bar">
<div class="col">
<div>
<q-select <q-select
class="q-pl-xs"
dense dense
standout="bg-primary text-white" options-dense
borderless
square square
hide-bottom-space hide-bottom-space
color="primary" color="primary"
transition-show="jump-down" transition-show="jump-down"
transition-hide="jump-up" transition-hide="jump-up"
@update:model-value="updateProgram" :model-value="currentCommand.program"
:model-value="modelValue.program" @update:model-value="handleProgramChange"
:options="programLanguages" :options="Object.keys(programs)"
label="环境"
> >
<template v-slot:prepend>
<q-badge
label="环境"
color="primary"
text-color="white"
class="q-ml-sm"
style="height: 20px"
/>
</template>
<template v-slot:append> <template v-slot:append>
<q-avatar size="lg" square> <q-avatar size="20px" square v-if="isRunCodePage">
<img :src="programs[modelValue.program].icon" /> <img :src="programs[currentCommand.program].icon" />
</q-avatar> </q-avatar>
</template> </template>
<template v-slot:option="scope"> <template v-slot:option="scope">
@ -31,46 +39,77 @@
</q-item> </q-item>
</template> </template>
</q-select> </q-select>
</div>
</div>
<q-separator vertical />
<div class="col-auto justify-end flex">
<q-btn-group unelevated class="button-group"> <q-btn-group unelevated class="button-group">
<template v-if="modelValue.program === 'quickcommand'"> <q-btn-dropdown
class="special-var-btn"
dense
flat
label="变量"
color="primary"
icon="data_object"
>
<q-list>
<q-item
v-for="(item, index) in Object.values(specialVars)"
:key="index"
clickable
v-close-popup
@click="handleSpecialVarClick(item)"
>
<q-item-section>
<q-item-label class="row items-center justify-between">
<div v-text="item.label" />
<div v-if="item.onlyCmdTypes" class="row">
<q-badge color="grey-9" class="q-ml-xs"> </q-badge>
<q-badge
v-for="type in item.onlyCmdTypes"
:key="type"
class="q-ml-xs"
v-text="commandTypes[type].label"
color="grey-9"
/>
</div>
</q-item-label>
<q-tooltip v-if="item.tooltip">
{{ item.tooltip }}
</q-tooltip>
<q-item-label caption>{{ item.desc }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
<template v-if="currentCommand.program === 'quickcommand'">
<q-btn <q-btn
v-for="(item, index) in [ v-for="(item, index) in ['help_center', 'view_timeline']"
'help_center',
'view_timeline',
]"
:key="index" :key="index"
dense dense
flat flat
color="primary" color="primary"
class="settings-btn" class="settings-btn"
:label="['文档', '可视化'][index]"
:icon="item" :icon="item"
@click="handleQuickCommandAction(index)" @click="handleQuickCommandAction(index)"
> >
<q-tooltip>
{{ ["查看文档", "可视化编排"][index] }}
</q-tooltip>
</q-btn> </q-btn>
</template> </template>
<q-btn-dropdown <q-btn-dropdown
v-else-if="modelValue.program !== 'html'" v-model="isSettingsVisible"
v-else-if="currentCommand.program !== 'html'"
class="settings-btn" class="settings-btn"
dense dense
flat flat
ref="settings" label="设置"
color="primary" color="primary"
icon="settings" icon="settings"
> >
<q-list> <q-list>
<!-- 自定义解释器 --> <!-- 自定义解释器 -->
<q-item <q-item
v-for="(item, index) in Object.keys(modelValue.customOptions)" v-for="(item, index) in Object.keys(currentCommand.customOptions)"
:key="index" :key="index"
v-show="modelValue.program === 'custom'" v-show="currentCommand.program === 'custom'"
> >
<q-input <q-input
stack-label stack-label
@ -78,7 +117,6 @@
dense dense
outlined outlined
class="full-width" class="full-width"
@blur="matchLanguage"
:label=" :label="
[ [
'解释器路径,如:/opt/python', '解释器路径,如:/opt/python',
@ -86,8 +124,10 @@
'脚本后缀不含点py', '脚本后缀不含点py',
][index] ][index]
" "
:model-value="modelValue.customOptions[item]" :model-value="currentCommand.customOptions[item]"
@update:model-value="(val) => updateCustomOption(item, val)" @update:model-value="
(val) => updateModelValue(`customOptions.${item}`, val)
"
> >
<template v-slot:prepend> <template v-slot:prepend>
<q-icon name="code" /> <q-icon name="code" />
@ -95,15 +135,15 @@
</q-input> </q-input>
</q-item> </q-item>
<!-- 脚本参数 --> <!-- 脚本参数 -->
<q-item v-show="modelValue.program !== 'quickcommand'"> <q-item v-show="currentCommand.program !== 'quickcommand'">
<q-input <q-input
dense dense
stack-label stack-label
outlined outlined
label="脚本参数" label="脚本参数"
class="full-width" class="full-width"
:model-value="modelValue.scptarg" :model-value="currentCommand.scptarg"
@update:model-value="updateScptarg" @update:model-value="updateModelValue('scptarg', $event)"
> >
<template v-slot:prepend> <template v-slot:prepend>
<q-icon name="input" /> <q-icon name="input" />
@ -112,9 +152,9 @@
</q-item> </q-item>
<!-- 编码设置 --> <!-- 编码设置 -->
<q-item <q-item
v-for="(item, index) in Object.keys(modelValue.charset)" v-for="(item, index) in Object.keys(currentCommand.charset)"
:key="index" :key="index"
v-show="modelValue.program !== 'quickcommand'" v-show="currentCommand.program !== 'quickcommand'"
> >
<q-select <q-select
dense dense
@ -123,8 +163,10 @@
clearable clearable
class="full-width" class="full-width"
:label="['脚本编码', '输出编码'][index]" :label="['脚本编码', '输出编码'][index]"
:model-value="modelValue.charset[item]" :model-value="currentCommand.charset[item]"
@update:model-value="(val) => updateCharset(item, val)" @update:model-value="
(val) => updateModelValue(`charset.${item}`, val)
"
:options="['GBK', 'utf8', 'Big5']" :options="['GBK', 'utf8', 'Big5']"
type="text" type="text"
> >
@ -136,6 +178,16 @@
</q-list> </q-list>
</q-btn-dropdown> </q-btn-dropdown>
<q-separator vertical inset /> <q-separator vertical inset />
<q-btn
v-if="!isRunCodePage"
class="action-btn run-btn"
dense
flat
color="primary"
icon="arrow_back"
label="退出"
@click="$emit('action', 'back')"
></q-btn>
<q-btn <q-btn
class="action-btn run-btn" class="action-btn run-btn"
dense dense
@ -143,26 +195,37 @@
color="primary" color="primary"
icon="play_arrow" icon="play_arrow"
label="运行" label="运行"
@click="$emit('run')" @click="$emit('action', 'run')"
></q-btn> ></q-btn>
<q-btn <q-btn
class="action-btn save-btn"
flat
dense
v-if="!isRunCodePage" v-if="!isRunCodePage"
:disable="!canCommandSave" :disable="!canCommandSave"
:color="canCommandSave ? 'primary' : 'grey'" :color="canCommandSave ? 'primary' : 'grey'"
class="action-btn save-btn"
flat
dense
icon="save" icon="save"
label="保存" label="保存"
@click="$emit('save')" @click="$emit('action', 'save')"
></q-btn> ></q-btn>
</q-btn-group> </q-btn-group>
</div> <q-dialog v-model="showUserData">
<UserData
@insertText="
insertSpecialVar($event);
showUserData = false;
"
:showInsertBtn="true"
/>
</q-dialog>
</div> </div>
</template> </template>
<script> <script>
import programs from "js/options/programs.js"; import programs from "js/options/programs.js";
import specialVars from "js/options/specialVars.js";
import commandTypes from "js/options/commandTypes.js";
import UserData from "components/popup/UserData.vue";
export default { export default {
name: "CommandLanguageBar", name: "CommandLanguageBar",
@ -171,10 +234,6 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
height: {
type: Number,
default: 40,
},
canCommandSave: { canCommandSave: {
type: Boolean, type: Boolean,
default: true, default: true,
@ -184,149 +243,121 @@ export default {
default: false, default: false,
}, },
}, },
emits: [ emits: ["update:modelValue", "action"],
"update:modelValue", components: {
"program-changed", UserData,
"run", },
"save",
"show-composer",
],
data() { data() {
return { return {
programs, programs,
specialVars,
commandTypes,
isSettingsVisible: false,
showUserData: false,
}; };
}, },
computed: { computed: {
programLanguages() { currentCommand() {
return Object.keys(this.programs); return this.modelValue;
}, },
}, },
methods: { methods: {
updateProgram(value) { handleProgramChange(newProgram) {
this.$emit("update:modelValue", { const newCommand = { ...this.currentCommand };
...this.modelValue, newCommand.program = newProgram;
program: value, if (newProgram === "custom") {
}); this.isSettingsVisible = true;
this.programChanged(value);
},
updateCustomOption(key, value) {
this.$emit("update:modelValue", {
...this.modelValue,
customOptions: {
...this.modelValue.customOptions,
[key]: value,
},
});
},
updateScptarg(value) {
this.$emit("update:modelValue", {
...this.modelValue,
scptarg: value,
});
},
updateCharset(key, value) {
this.$emit("update:modelValue", {
...this.modelValue,
charset: {
...this.modelValue.charset,
[key]: value,
},
});
},
programChanged(value) {
this.$emit("program-changed", value);
if (value === "custom") {
this.$refs.settings.show();
} }
}, if (newProgram === "html") {
matchLanguage() { newCommand.output = "html";
if (!this.modelValue.customOptions.ext) return;
let language = Object.values(this.programs).filter(
(program) => program.ext === this.modelValue.customOptions.ext
);
if (language.length) {
this.$emit("program-changed", language[0].name);
} }
const featuresIcon = this.currentCommand.features.icon || "";
if (featuresIcon.slice(0, 10) !== "data:image") {
newCommand.features.icon = this.programs[newProgram].icon;
}
this.$emit("update:modelValue", newCommand);
},
updateModelValue(keyPath, value) {
const newModelValue = { ...this.modelValue };
const keys = keyPath.split(".");
const lastKey = keys.pop();
const target = keys.reduce((obj, key) => obj[key], newModelValue);
target[lastKey] = value;
this.$emit("update:modelValue", newModelValue);
}, },
handleQuickCommandAction(index) { handleQuickCommandAction(index) {
const actions = [ const actions = [
() => this.showHelp(), () => this.showHelp(),
() => this.$emit("show-composer"), () => this.$emit("action", "show-composer"),
]; ];
actions[index](); actions[index]();
}, },
showHelp() { showHelp() {
window.showUb.docs(); window.showUb.docs();
}, },
handleSpecialVarClick(item) {
if (item.label === "{{usr:}}") this.showUserData = true;
else this.insertSpecialVar(item.label);
},
insertSpecialVar(text) {
if (!text) return;
this.$emit("action", "insert-text", `"${text}"`);
},
}, },
}; };
</script> </script>
<style scoped> <style scoped>
.button-group { .button-group {
.action-btn { padding: 0 5px;
padding: 0 10px; }
}
.button-group :deep(.q-focus-helper) {
display: none;
}
.button-group :deep(.q-btn__content) {
font-size: 12px;
}
.button-group :deep(.q-btn-dropdown__arrow) {
margin-left: 0;
}
.button-group .q-btn:hover {
filter: brightness(1.2);
transition: all 0.2s ease;
} }
/* 运行按钮动画 */ /* 运行按钮动画 */
.run-btn:hover :deep(.q-icon) { .run-btn:hover :deep(.q-icon) {
display: inline-block; display: inline-block;
animation: slideRight 1.5s infinite; animation: leftRight 1.5s infinite;
} }
/* 保存按钮动画 */ /* 保存按钮动画 */
.save-btn:not([disabled]):hover :deep(.q-icon) { .save-btn:not([disabled]):hover :deep(.q-icon) {
display: inline-block; display: inline-block;
animation: saveAnimation 1.2s infinite; animation: upDown 1.2s infinite;
} }
/* 设置按钮动画 */ .command-language-bar {
.settings-btn :deep(.q-icon:first-child) { background-color: #fffffe;
display: inline-block; height: 30px;
transform: scale(1); margin-bottom: 2px;
transition: transform 0.5s ease; display: flex;
align-items: center;
justify-content: space-between;
} }
.settings-btn:hover :deep(.q-icon:first-child) { .body--dark .command-language-bar {
transform: scale(1.05); background-color: #1e1e1e;
} }
@keyframes slideRight { .command-language-bar :deep(.q-field__control),
0% { .command-language-bar :deep(.q-field__control > *),
transform: translateX(-2px); .command-language-bar :deep(.q-field__native) {
opacity: 0.7; max-height: 30px;
} min-height: 30px;
50% {
transform: translateX(2px);
opacity: 1;
}
100% {
transform: translateX(-2px);
opacity: 0.7;
}
}
@keyframes saveAnimation {
0% {
transform: translateY(-1px);
opacity: 1;
}
50% {
transform: translateY(1px);
opacity: 0.6;
}
75% {
transform: translateY(0.5px);
opacity: 0.8;
}
100% {
transform: translateY(-1px);
opacity: 1;
}
}
.settings-btn:hover :deep(.q-icon) {
display: inline-block;
} }
</style> </style>

View File

@ -1,739 +0,0 @@
<template>
<div
class="command-side-bar"
:style="{
width: sideBarWidth + 'px',
'--icon-url': `url(${currentCommand.features.icon})`,
}"
>
<!-- 头部区域 -->
<div class="header-section">
<div class="header-content">
<q-btn
dense
flat
color="grey"
icon="arrow_back_ios_new"
v-close-popup
class="back-btn"
@click="$emit('back')"
/>
<div class="logo-container">
<q-avatar size="64" square class="featureIco">
<q-img
@click="showIconPicker = true"
:src="currentCommand.features.icon"
/>
</q-avatar>
</div>
</div>
</div>
<!-- 可滚动的内容区域 -->
<q-scroll-area
:thumb-style="{
width: '3px',
}"
:horizontal-thumb-style="{
height: '5px',
}"
class="scroll-area"
>
<div
class="row"
:style="{
paddingLeft: sideBarPadding + 'px',
paddingRight: sideBarPadding + 'px',
paddingBottom: sideBarPadding + 'px',
}"
>
<div class="col-12">
<div class="row">
<div
class="command-side-bar-content"
:style="{ width: sideBarWidth - sideBarPadding * 2 + 'px' }"
>
<!-- 说明 -->
<q-input
:disable="!canCommandSave"
stack-label
label-color="primary"
borderless
square
v-model="currentCommand.features.explain"
type="text"
placeholder="请输入说明"
label="说明"
>
<template v-slot:prepend>
<q-icon
class="command-side-bar-icon"
name="drive_file_rename_outline"
/>
</template>
</q-input>
<!-- 匹配类型 -->
<q-select
:disable="!canCommandSave"
popup-content-class="side-bar-popup-content"
hide-dropdown-icon
stack-label
label-color="primary"
transition-show="jump-down"
transition-hide="jump-up"
borderless
square
@update:model-value="(val) => handleCmdTypeChange(val)"
:options="commandTypesOptions"
v-model="cmdType"
type="text"
label="匹配类型"
>
<template v-slot:prepend>
<q-icon class="command-side-bar-icon" :name="cmdType.icon" />
</template>
<template v-slot:option="scope">
<q-item v-bind="scope.itemProps" class="row items-center">
<q-icon :name="scope.opt.icon" class="q-mr-md" />
<div>
<q-item-label v-html="scope.opt.name" />
<q-item-label caption>{{ scope.opt.desc }}</q-item-label>
</div>
</q-item>
</template>
</q-select>
<!-- 匹配规则 -->
<q-select
v-if="cmdType.valueType === 'array'"
:disable="!canCommandSave"
popup-content-class="side-bar-popup-content"
hide-dropdown-icon
stack-label
label-color="primary"
transition-show="jump-down"
transition-hide="jump-up"
borderless
square
v-model="cmdMatch"
max-values="3"
type="text"
placeholder="回车添加"
use-input
use-chips
multiple
new-value-mode="add-unique"
input-debounce="0"
:label="cmdType.matchLabel"
ref="cmdMatchRef"
@blur="(e) => autoAddInputVal(e, $refs.cmdMatchRef)"
>
<template v-slot:prepend>
<q-icon class="command-side-bar-icon" name="square_foot" />
</template>
</q-select>
<q-input
v-else
:disable="!canCommandSave"
autogrow
borderless
square
v-model="cmdMatch"
hide-bottom-space
@blur="regexVerify"
:readonly="!cmdType.valueType"
type="text"
:label="cmdType.matchLabel"
>
<template v-slot:prepend>
<q-icon class="command-side-bar-icon" name="square_foot" />
</template>
<template v-slot:append>
<q-icon
v-if="cmdType.name === 'files'"
name="folder"
size="xs"
:color="isFileTypeDirectory ? 'primary' : ''"
@click="isFileTypeDirectory = !isFileTypeDirectory"
style="cursor: pointer"
>
<q-tooltip>
切换匹配类型当前{{
isFileTypeDirectory ? "文件夹" : "文件"
}}
</q-tooltip>
</q-icon>
</template>
</q-input>
<!-- 标签 -->
<q-select
:disable="!canCommandSave"
hide-dropdown-icon
stack-label
popup-content-class="side-bar-popup-content"
label-color="primary"
transition-show="jump-down"
transition-hide="jump-up"
borderless
square
v-model="currentCommand.tags"
max-values="3"
type="text"
label="标签"
placeholder="回车添加"
use-input
use-chips
multiple
new-value-mode="add-unique"
@new-value="tagVerify"
input-debounce="0"
:options="allQuickCommandTags"
ref="commandTagRef"
@blur="(e) => autoAddInputVal(e, $refs.commandTagRef)"
>
<template v-slot:prepend>
<q-icon class="command-side-bar-icon" name="label" />
</template>
</q-select>
<!-- 特殊变量 -->
<q-select
:disable="!canCommandSave"
hide-dropdown-icon
popup-content-class="side-bar-popup-content"
stack-label
label-color="primary"
transition-show="jump-down"
transition-hide="jump-up"
borderless
@popup-hide="
() => {
if (specialVar.label === '{{usr:}}') showUserData = true;
else insertSpecialVar(specialVar.label);
}
"
square
:options="specialVarsOptions"
v-model="specialVar"
input-debounce="0"
type="text"
label="特殊变量"
>
<template v-slot:prepend>
<q-icon class="command-side-bar-icon" name="attach_money" />
</template>
<template v-slot:option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section>
<q-item-label v-html="scope.opt.label" />
<q-tooltip v-if="scope.opt.tooltip">
{{ scope.opt.tooltip }}
</q-tooltip>
<q-item-label caption>{{ scope.opt.desc }}</q-item-label>
</q-item-section>
</q-item>
</template></q-select
>
<!-- 输出 -->
<q-select
:disable="!canCommandSave"
hide-dropdown-icon
stack-label
label-color="primary"
popup-content-class="side-bar-popup-content"
transition-show="jump-down"
transition-hide="jump-up"
borderless
square
color="primary"
v-model="currentCommand.output"
:display-value="outputTypes[currentCommand.output].label"
:options="outputTypesOptionsDy"
label="输出"
>
<template v-slot:prepend>
<q-icon
class="command-side-bar-icon"
:name="outputTypes[currentCommand.output].icon"
/>
</template>
<template v-slot:option="scope">
<q-item v-bind="scope.itemProps" class="row items-center">
<q-icon
:name="outputTypes[scope.opt].icon"
class="q-mr-md"
/>
<div>
<q-item-label v-html="outputTypes[scope.opt].label" />
</div>
</q-item>
</template>
</q-select>
<!-- 搜索面板推送 -->
<q-select
:disable="!canCommandSave"
hide-dropdown-icon
stack-label
label-color="primary"
popup-content-class="side-bar-popup-content"
transition-show="jump-down"
transition-hide="jump-up"
borderless
square
v-model="searchPushValue"
:options="searchPushOptions"
label="搜索面板推送"
>
<template v-slot:prepend>
<q-icon class="command-side-bar-icon" name="search" />
</template>
<template v-slot:option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section>
<q-item-label>{{ scope.opt.label }}</q-item-label>
<q-item-label caption>{{ scope.opt.desc }}</q-item-label>
</q-item-section>
<q-item-section side v-if="scope.opt.value">
<q-btn
flat
round
icon="help_outline"
size="xs"
dense
@click.stop="showMainPushHelp"
/>
</q-item-section>
</q-item>
</template>
</q-select>
<!-- 平台 -->
<q-select
:disable="!canCommandSave"
hide-dropdown-icon
stack-label
label-color="primary"
popup-content-class="side-bar-popup-content"
transition-show="jump-down"
transition-hide="jump-up"
borderless
square
:options="Object.keys(platformTypes)"
use-chips
@blur="platformVerify()"
v-model="currentCommand.features.platform"
multiple
label="平台"
>
<template v-slot:prepend>
<q-icon class="command-side-bar-icon" name="window" />
</template>
<template v-slot:selected-item="scope">
<q-chip
removable
dense
@remove="scope.removeAtIndex(scope.index)"
:tabindex="scope.tabindex"
>
{{ platformTypes[scope.opt].label }}
</q-chip>
</template>
<template v-slot:option="scope">
<q-item v-bind="scope.itemProps" class="row items-center">
<q-img
:src="platformTypes[scope.opt].icon"
width="24px"
class="q-mr-md"
/>
<div>
<q-item-label v-html="platformTypes[scope.opt].label" />
<q-item-label caption>{{ scope.opt.desc }}</q-item-label>
</div>
</q-item>
</template>
</q-select>
</div>
</div>
</div>
</div>
</q-scroll-area>
<!-- 对话框部分保持不变 -->
<q-dialog v-model="showIconPicker" position="left">
<iconPicker
@iconChanged="(dataUrl) => (currentCommand.features.icon = dataUrl)"
ref="icon"
/>
</q-dialog>
<q-dialog v-model="showUserData">
<UserData @insertText="insertSpecialVar" :showInsertBtn="true" />
</q-dialog>
</div>
</template>
<script>
import commandTypes from "js/options/commandTypes.js";
import outputTypes from "js/options/outputTypes.js";
import specialVars from "js/options/specialVars.js";
import platformTypes from "js/options/platformTypes.js";
import iconPicker from "components/popup/IconPicker.vue";
import UserData from "components/popup/UserData.vue";
export default {
components: { iconPicker, UserData },
data() {
return {
currentCommand: {
tags: [],
output: "text",
features: {
explain: "",
platform: ["win32", "linux", "darwin"],
icon: "",
mainPush: false,
},
},
searchPushOptions: [
{ value: false, label: "禁用", desc: "需要进入插件才能执行命令" },
{
value: true,
label: "启用",
desc: "可以在uTools主搜索框直接执行命令",
},
],
commandTypes: commandTypes,
platformTypes: platformTypes,
currentMatchType: "关键字",
cmdType: {},
cmdMatch: "",
outputTypes: outputTypes,
outputTypesOptions: Object.keys(outputTypes),
specialVar: "{{}}",
allQuickCommandTags: this.$parent.allQuickCommandTags,
showIconPicker: false,
showUserData: false,
sideBarPadding: 20,
isFileTypeDirectory: false,
};
},
props: {
quickcommandInfo: Object,
canCommandSave: Boolean,
sideBarWidth: Number,
},
computed: {
commandTypesOptions() {
return this.currentCommand.features.mainPush
? Object.values(commandTypes).filter((x) =>
["regex", "over", "key"].includes(x.name)
)
: Object.values(commandTypes);
},
specialVarsOptions() {
if (this.currentCommand.features.mainPush) return [specialVars.input];
let x = Object.values(specialVars).filter(
(x) => !x.label.match(this.cmdType.disabledSpecialVars)
);
return x;
},
outputTypesOptionsDy() {
if (this.currentCommand.features.mainPush) return ["text"];
switch (this.$parent.quickcommandInfo.program) {
case "quickcommand":
return window.lodashM.without(this.outputTypesOptions, "terminal");
case "html":
return ["html"];
default:
return this.outputTypesOptions;
}
},
searchPushValue: {
get() {
return (
this.searchPushOptions.find(
(opt) => opt.value === this.currentCommand.features.mainPush
) || this.searchPushOptions[0]
);
},
set(option) {
this.currentCommand.features.mainPush = option.value;
},
},
},
watch: {
outputTypesOptionsDy(val) {
if (!val.includes(this.currentCommand.output)) {
this.currentCommand.output = val[0];
}
},
commandTypesOptions(val) {
if (!val.map((x) => x.name).includes(this.cmdType.name)) {
this.cmdType = val[0];
}
},
},
methods: {
init() {
let currentQuickCommandCmds = this.getCommandType();
this.cmdType = this.commandTypes[currentQuickCommandCmds.type];
this.cmdMatch = currentQuickCommandCmds.match;
Object.assign(
this.currentCommand,
window.lodashM.cloneDeep(
window.lodashM.pick(
this.quickcommandInfo,
"tags",
"output",
"features"
)
)
);
this.setIcon(this.quickcommandInfo.program);
this.platformVerify();
},
setIcon(language) {
this.currentCommand.features.icon?.slice(0, 10) === "data:image" ||
(this.currentCommand.features.icon =
this.$root.programs[language].icon);
},
getCommandType() {
let data = { type: "key", match: [] };
let cmds = this.quickcommandInfo.features?.cmds;
if (!cmds) return data;
if (cmds.length === 1) {
let { type, match, fileType } = cmds[0];
data.type = type ? type : "key";
data.match =
data.type === "key" ? cmds : match?.app ? match.app : match;
this.isFileTypeDirectory = fileType === "directory";
} else {
data.type = cmds.filter((x) => !x.length).length
? "professional"
: "key";
data.match = data.type === "key" ? cmds : JSON.stringify(cmds, null, 4);
}
return data;
},
handleCmdTypeChange(val) {
this.cmdMatch =
val.name === "professional"
? JSON.stringify(val.jsonSample, null, 4)
: null;
},
tagVerify(val, done) {
if (
[
"默认",
"未分类",
"搜索结果",
// ""
].includes(val)
) {
return done(`_${val}_`);
}
done(val);
},
//
platformVerify() {
this.currentCommand.features.platform?.length > 0 ||
(this.currentCommand.features.platform = [window.processPlatform]);
},
//
regexVerify() {
if (
this.cmdType.valueType === "regex" &&
!/^\/.*?\/[igm]*$/.test(this.cmdMatch)
)
this.cmdMatch = `/${this.cmdMatch}/`;
},
autoAddInputVal(e, ref) {
let inputValue = e.target.value;
if (!inputValue) return;
ref.add(inputValue, true);
},
insertSpecialVar(text) {
if (!text) return;
this.$parent.$refs.editor.repacleEditorSelection(`"${text}"`);
},
showMainPushHelp() {
window.showUb.help("#u0e9f1430");
},
//
SaveMenuData() {
let updateData = {
features: this.currentCommand.features,
output: this.currentCommand.output,
tags: this.currentCommand.tags,
cmd: "",
};
//
updateData.features.explain || (updateData.features.explain = " ");
if (!updateData.features.code) {
// code
let uid = Number(
Math.random().toString().substr(3, 3) + Date.now()
).toString(36);
updateData.features.code = `${this.cmdType.name}_${uid}`;
}
let verify = this.cmdType.verify(this.cmdMatch);
if (verify !== true) {
return quickcommand.showMessageBox(verify, "error");
}
if (outputTypes[updateData.output].outPlugin) {
updateData.features.mainHide = true;
}
//
let rules = this.cmdMatch;
if (this.cmdType.name === "files") {
rules = {
fileType: this.isFileTypeDirectory ? "directory" : "file",
match: this.cmdMatch,
};
}
updateData.features.cmds = this.cmdType.matchToCmds(
rules,
updateData.features.explain
);
updateData.cmd = this.$parent.$refs.editor.getEditorValue();
let blackLisk = updateData.cmd.match(this.cmdType.disabledSpecialVars);
if (blackLisk) {
return quickcommand.showMessageBox(
`当前模式无法使用${[...new Set(blackLisk)].join("、")}`,
"error"
);
}
//
if (updateData.cmd.includes("{{subinput")) {
updateData.hasSubInput = true;
} else {
updateData.hasSubInput = false;
}
return updateData;
},
},
emits: ["back"],
};
</script>
<style scoped>
/* 其他样式从app.css中继承 */
.featureIco {
cursor: pointer;
}
.featureIco:hover {
transform: scale(1.02) translateY(-2px);
}
.featureIco:hover::after {
opacity: 0.8;
transform: scale(1.05);
}
.command-side-bar {
height: 100%;
background: #f4f4f4;
display: flex;
flex-direction: column;
}
.header-section {
height: 60px;
min-height: 60px;
background: inherit;
position: relative;
}
.header-content {
height: 100%;
display: flex;
align-items: center;
}
.back-btn {
position: absolute;
left: 20px;
top: 50%;
transform: translateY(-50%);
height: 48px;
width: 22px;
z-index: 1;
}
.logo-container {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
}
.scroll-area {
flex: 1;
background: inherit;
}
.body--dark .command-side-bar {
background: #303133;
}
.commandLogo {
cursor: pointer;
transition: 0.2s;
filter: drop-shadow(2px 1px 1px grey);
}
.commandLogo:hover {
transition: 0.5s;
transform: translateY(-1px);
filter: drop-shadow(2px 1px 5px grey);
}
/* 输入框图标基础样式 */
.command-side-bar-icon {
background: var(--q-primary);
border-radius: 8px;
padding: 4px;
color: #f4f4f4;
font-size: 14px;
/* 分开设置不同属性的过渡效果 */
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1),
box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
backface-visibility: hidden;
transform-style: preserve-3d;
will-change: transform;
-webkit-font-smoothing: subpixel-antialiased;
/* 添加初始transform状态 */
transform: translateZ(0);
}
/* 输入框容器悬浮效果 */
.q-field:hover .command-side-bar-icon {
transform: scale(1.05) translateY(-1px) translateZ(0);
background: var(--q-primary);
opacity: 0.9;
}
/* 输入框得焦点时的图标效果 */
.q-field--focused .command-side-bar-icon {
transform: scale(1.1) translateY(-1px) translateZ(0);
background: var(--q-primary);
opacity: 0.85;
}
.side-bar-popup-content .q-item .q-icon {
font-size: 20px;
}
.command-side-bar-content .q-field,
.side-bar-popup-content .q-item__label {
font-size: 13px;
}
.side-bar-popup-content .q-item__label--caption,
.command-side-bar-content :deep(.q-chip) {
font-size: 12px;
}
</style>

View File

@ -6,20 +6,6 @@
:commandCode="commandCode" :commandCode="commandCode"
@restore="$emit('restore', $event)" @restore="$emit('restore', $event)"
/> />
<!-- 全屏按钮 -->
<q-btn
round
dense
:icon="isFullscreen ? 'fullscreen_exit' : 'fullscreen'"
@click="$emit('toggle-fullscreen')"
class="fullscreen-btn"
:class="{ 'btn-fullscreen': isFullscreen }"
>
<q-tooltip>{{
isFullscreen ? "退出全屏 (F11)" : "全屏编辑 (F11)"
}}</q-tooltip>
</q-btn>
</div> </div>
</template> </template>
@ -36,12 +22,8 @@ export default {
type: String, type: String,
default: "temp", default: "temp",
}, },
isFullscreen: {
type: Boolean,
default: false,
}, },
}, emits: ["restore"],
emits: ["restore", "toggle-fullscreen"],
methods: { methods: {
showHistory() { showHistory() {
this.$refs.history.open(); this.$refs.history.open();
@ -58,46 +40,10 @@ export default {
position: fixed; position: fixed;
right: 24px; right: 24px;
bottom: 24px; bottom: 24px;
z-index: 1000; z-index: 500;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
} }
.fullscreen-btn {
z-index: 1000;
transform-origin: center;
color: #666;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.fullscreen-btn:hover {
transform: scale(1.1) translateY(-2px);
}
.fullscreen-btn:active {
transform: scale(0.95);
transition-duration: 0.1s;
}
.btn-fullscreen {
transform: rotate(180deg);
}
.btn-fullscreen:hover {
transform: rotate(180deg) scale(1.1);
}
.body--dark .fullscreen-btn {
background: rgba(255, 255, 255, 0.1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
color: #bbb;
}
.body--dark .fullscreen-btn:hover {
background: rgba(255, 255, 255, 0.15);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
</style> </style>

View File

@ -45,7 +45,6 @@
:class="{ 'buttons-visible': isHovering[type.value] }" :class="{ 'buttons-visible': isHovering[type.value] }"
@mouseleave="setHovering(type.value, false)" @mouseleave="setHovering(type.value, false)"
> >
<q-btn-group outline>
<q-btn <q-btn
dense dense
outline outline
@ -72,7 +71,6 @@
class="hover-btn" class="hover-btn"
@click="removeLastRuleByType(type.value)" @click="removeLastRuleByType(type.value)"
/> />
</q-btn-group>
</div> </div>
</div> </div>
</div> </div>
@ -140,7 +138,7 @@
label="匹配文本正则表达式" label="匹配文本正则表达式"
placeholder="例:/xxx/,任意匹配的正则会被 uTools 忽略" placeholder="例:/xxx/,任意匹配的正则会被 uTools 忽略"
class="col" class="col"
@blur="validateRegex(rule)" @blur="validateRegex('match', rule)"
/> />
<q-input <q-input
v-model.number="rule.minLength" v-model.number="rule.minLength"
@ -173,7 +171,7 @@
label="匹配文件(夹)名正则表达式" label="匹配文件(夹)名正则表达式"
placeholder="可选,例:/xxx/" placeholder="可选,例:/xxx/"
class="col" class="col"
@blur="validateRegex(rule)" @blur="validateRegex('match', rule)"
/> />
<q-select <q-select
:model-value="rule.fileType || 'file'" :model-value="rule.fileType || 'file'"
@ -232,7 +230,7 @@
label="匹配窗口标题正则表达式" label="匹配窗口标题正则表达式"
placeholder="可选,例:/xxx/" placeholder="可选,例:/xxx/"
class="col-5" class="col-5"
@blur="validateRegex({ match: rule.match.title })" @blur="validateRegex('match.title', rule)"
/> />
</div> </div>
</template> </template>
@ -256,7 +254,7 @@
label="排除的正则表达式字符串" label="排除的正则表达式字符串"
placeholder="可选,例:/xxx/" placeholder="可选,例:/xxx/"
class="col" class="col"
@blur="validateRegex({ match: rule.exclude })" @blur="validateRegex('exclude', rule)"
/> />
<q-input <q-input
v-model.number="rule.minLength" v-model.number="rule.minLength"
@ -350,17 +348,21 @@ export default defineComponent({
}, },
methods: { methods: {
validateRegex(rule) { validateRegex(keyPath, rule) {
const matchValue = rule.match; const keys = keyPath.split(".");
const lastKey = keys.pop();
const target = keys.reduce((obj, key) => obj[key], rule);
const matchValue = target[lastKey];
if (!matchValue) return; if (!matchValue) return;
try { try {
if (!matchValue.startsWith("/")) { if (!matchValue.startsWith("/")) {
rule.match = `/${matchValue}/`; target[lastKey] = `/${matchValue}/`;
} }
new RegExp(matchValue.replace(/^\/|\/[gimuy]*$/g, "")); new RegExp(matchValue.replace(/^\/|\/[gimuy]*$/g, ""));
} catch (e) { } catch (e) {
rule.match = "/./"; target[lastKey] = "/./";
} }
}, },
@ -450,7 +452,6 @@ export default defineComponent({
.rule-type-buttons { .rule-type-buttons {
display: flex; display: flex;
gap: 8px; gap: 8px;
justify-content: space-between;
} }
/* 合并按钮基础样式 */ /* 合并按钮基础样式 */
@ -490,7 +491,7 @@ export default defineComponent({
.key-input-wrapper :deep(.q-icon) { .key-input-wrapper :deep(.q-icon) {
font-size: 16px; font-size: 16px;
opacity: 0.7; opacity: 0.7;
transition: opacity 0.3s; transition: opacity 0.2s;
} }
.key-input-wrapper :deep(.q-icon:hover) { .key-input-wrapper :deep(.q-icon:hover) {
@ -518,8 +519,8 @@ export default defineComponent({
.rule-type-btn-wrapper, .rule-type-btn-wrapper,
.btn-container { .btn-container {
position: relative; position: relative;
width: 85px;
height: 24px; height: 24px;
flex: 1;
} }
.btn-container { .btn-container {
@ -531,7 +532,7 @@ export default defineComponent({
.rule-type-btn { .rule-type-btn {
width: 100%; width: 100%;
position: absolute; position: absolute;
transition: opacity 0.3s ease; transition: opacity 0.2s ease;
} }
.btn-hidden { .btn-hidden {
@ -544,9 +545,9 @@ export default defineComponent({
position: absolute; position: absolute;
inset: 0; inset: 0;
display: flex; display: flex;
gap: 4px;
justify-content: center; justify-content: center;
opacity: 0; opacity: 0;
transition: all 0.5s ease;
pointer-events: none; pointer-events: none;
} }

View File

@ -1,12 +1,17 @@
<template> <template>
<div class="monaco-container"> <div class="monaco-container">
<div id="monacoEditor" class="monaco-editor-instance"></div> <div id="monacoEditor" class="monaco-editor-instance"></div>
<div class="absolute-center flex" v-show="!value && placeholder"> <div class="placeholder-container" v-show="showPlaceholder">
<div class="placeholder text-center q-gutter-md"> {{ placeholder }}
<div v-for="shortCut in shortCuts" :key="shortCut"> </div>
<span>{{ shortCut[0] }}</span <div class="shortcut-container" v-show="showPlaceholder">
><span class="shortcut-key">{{ shortCut[1] }}</span <div class="shortcut text-center row q-gutter-md items-center">
><span class="shortcut-key">{{ shortCut[2] }}</span> <div
v-for="shortCut in shortCuts"
:key="shortCut"
class="row q-gutter-xs"
>
<q-badge v-for="item in shortCut" :key="item">{{ item }}</q-badge>
</div> </div>
</div> </div>
</div> </div>
@ -31,7 +36,6 @@ let languageCompletions = importAll(
let monacoCompletionProviders = {}; let monacoCompletionProviders = {};
let cmdCtrlKey = utools.isMacOs() ? "⌘" : "Ctrl"; let cmdCtrlKey = utools.isMacOs() ? "⌘" : "Ctrl";
let optAltKey = utools.isMacOs() ? "⌥" : "Alt";
export default { export default {
data() { data() {
@ -42,10 +46,14 @@ export default {
shortCuts: [ shortCuts: [
["保存", cmdCtrlKey, "S"], ["保存", cmdCtrlKey, "S"],
["运行", cmdCtrlKey, "B"], ["运行", cmdCtrlKey, "B"],
["换行", optAltKey, "Z"],
], ],
}; };
}, },
computed: {
showPlaceholder() {
return !this.value && !!this.placeholder;
},
},
mounted() { mounted() {
this.initEditor(); this.initEditor();
// MonacoResizeObserver loop limit exceeded // MonacoResizeObserver loop limit exceeded
@ -53,7 +61,7 @@ export default {
this.$emit("loaded"); this.$emit("loaded");
}, },
props: { props: {
placeholder: Boolean, placeholder: String,
}, },
methods: { methods: {
initEditor() { initEditor() {
@ -256,14 +264,7 @@ export default {
}); });
}, },
bindKeys() { bindKeys() {
let that = this; const that = this;
// ctrl + b
this.rawEditor().addCommand(
monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyB,
() => {
that.$emit("keyStroke", "run");
}
);
// alt + z // alt + z
this.rawEditor().addCommand( this.rawEditor().addCommand(
monaco.KeyMod.Alt | monaco.KeyCode.KeyZ, monaco.KeyMod.Alt | monaco.KeyCode.KeyZ,
@ -272,24 +273,6 @@ export default {
that.rawEditor().updateOptions({ wordWrap: that.wordWrap }); that.rawEditor().updateOptions({ wordWrap: that.wordWrap });
} }
); );
// ctrl + s
this.rawEditor().addCommand(
monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS,
() => {
that.$emit("keyStroke", "save");
}
);
// ctrl + e console.log
this.rawEditor().addCommand(
monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyE,
() => {
that.$emit("keyStroke", "log", that.getSelectionOrLineContent());
}
);
// F11
this.rawEditor().addCommand(monaco.KeyCode.F11, () => {
this.$emit("keyStroke", "fullscreen");
});
}, },
getSelectionOrLineContent() { getSelectionOrLineContent() {
let selection = this.rawEditor().getSelection(); let selection = this.rawEditor().getSelection();
@ -318,11 +301,7 @@ export default {
<style scoped> <style scoped>
.monaco-container { .monaco-container {
position: absolute; position: relative;
left: 0;
right: 0;
top: 0;
bottom: 0;
} }
.monaco-editor-instance { .monaco-editor-instance {
@ -330,22 +309,32 @@ export default {
height: 100%; height: 100%;
} }
.placeholder { .placeholder-container {
font-size: 14px; position: absolute;
font-family: sans-serif; top: 0;
color: #535353; left: 40px;
font-style: italic;
color: grey;
}
.shortcut-container {
position: absolute;
left: 0;
right: 0;
bottom: 10px;
display: flex;
justify-content: center;
align-items: center;
}
.shortcut .q-badge {
user-select: none; user-select: none;
background-color: rgba(0, 0, 0, 0.1);
color: rgba(0, 0, 0, 0.6);
} }
.shortcut-key { .body--dark .shortcut .q-badge {
background-color: #f3f4f6; background-color: rgba(255, 255, 255, 0.1);
border-radius: 0.25rem; color: rgba(255, 255, 255, 0.8);
margin-left: 0.5rem;
padding-left: 0.25rem;
padding-right: 0.25rem;
}
.body--dark .shortcut-key {
background-color: #262626;
} }
</style> </style>

View File

@ -59,7 +59,7 @@ export const getValidCommand = (command) => {
// 生成唯一code // 生成唯一code
if (!command.features.code) { if (!command.features.code) {
command.features.code = getFeatureCode(cmds); command.features.code = getFeatureCode(command.features.cmds);
} }
return window.lodashM.cloneDeep(command); return window.lodashM.cloneDeep(command);

View File

@ -5,7 +5,6 @@
const programs = { const programs = {
quickcommand: { quickcommand: {
name: "quickcommand", name: "quickcommand",
highlight: "javascript",
bin: "", bin: "",
argv: "", argv: "",
ext: "", ext: "",
@ -40,7 +39,6 @@ const programs = {
}, },
cmd: { cmd: {
name: "cmd", name: "cmd",
highlight: "bat",
bin: "", bin: "",
argv: "", argv: "",
ext: "bat", ext: "bat",

View File

@ -5,121 +5,123 @@
let handlingJsonVar = (jsonVar, name, payload) => { let handlingJsonVar = (jsonVar, name, payload) => {
try { try {
return window.evalCodeInSandbox(jsonVar.slice(2, -2), { return window.evalCodeInSandbox(jsonVar.slice(2, -2), {
[name]: payload [name]: payload,
}) });
} catch (e) { } catch (e) {
return utools.showNotification(e) return utools.showNotification(e);
} }
} };
let handlingJsExpression = js => { let handlingJsExpression = (js) => {
try { try {
return window.evalCodeInSandbox(js.slice(5, -2), { return window.evalCodeInSandbox(js.slice(5, -2), {
utools: window.getuToolsLite(), utools: window.getuToolsLite(),
}) });
} catch (e) { } catch (e) {
return utools.showNotification(e) return utools.showNotification(e);
} }
} };
const specialVars = { const specialVars = {
isWin: { isWin: {
name: "isWin", name: "isWin",
label: "{{isWin}}", label: "{{isWin}}",
desc: "是否为 windows 系统,返回 0 或 1", desc: "是否为 windows 系统,返回 0 或 1",
disabledType: [], match: /{{isWin}}/gm,
match: /{{isWin}}/mg, repl: () => (utools.isWindows() ? 1 : 0),
repl: () => utools.isWindows() ? 1 : 0
}, },
LocalId: { LocalId: {
name: "LocalId", name: "LocalId",
label: "{{LocalId}}", label: "{{LocalId}}",
desc: "本机唯一ID", desc: "本机唯一ID",
disabledType: [], match: /{{LocalId}}/gm,
match: /{{LocalId}}/mg, repl: () => utools.getNativeId(),
repl: () => utools.getNativeId()
}, },
BrowserUrl: { BrowserUrl: {
name: "BrowserUrl", name: "BrowserUrl",
label: "{{BrowserUrl}}", label: "{{BrowserUrl}}",
disabledType: [],
desc: "浏览器当前链接", desc: "浏览器当前链接",
match: /{{BrowserUrl}}/mg, match: /{{BrowserUrl}}/gm,
repl: () => utools.getCurrentBrowserUrl() repl: () => utools.getCurrentBrowserUrl(),
}, },
ClipText: { ClipText: {
name: "ClipText", name: "ClipText",
label: "{{ClipText}}", label: "{{ClipText}}",
disabledType: [],
desc: "剪贴板内容", desc: "剪贴板内容",
match: /{{ClipText}}/mg, match: /{{ClipText}}/gm,
repl: () => window.clipboardReadText() repl: () => window.clipboardReadText(),
}, },
subinput: { subinput: {
name: "subinput", name: "subinput",
label: "{{subinput:请输入}}", label: "{{subinput:请输入}}",
disabledType: [],
desc: "子输入框的文本,冒号后为占位符", desc: "子输入框的文本,冒号后为占位符",
match: /{{subinput(:.+?){0,1}}}/mg, match: /{{subinput(:.+?){0,1}}}/gm,
}, },
input: { input: {
name: "input", name: "input",
label: "{{input}}", label: "{{input}}",
desc: "主输入框的文本", desc: "主输入框的文本",
match: /{{input}}/mg, match: /{{input}}/gm,
repl: (text, enterData) => enterData.payload onlyCmdTypes: ["regex", "over"],
repl: (text, enterData) => enterData.payload,
}, },
pwd: { pwd: {
name: "pwd", name: "pwd",
label: "{{pwd}}", label: "{{pwd}}",
desc: "文件管理器当前目录", desc: "文件管理器当前目录",
match: /{{pwd}}/mg, match: /{{pwd}}/gm,
repl: () => window.getCurrentFolderPathFix() repl: () => window.getCurrentFolderPathFix(),
}, },
WindowInfo: { WindowInfo: {
name: "WindowInfo", name: "WindowInfo",
label: "{{WindowInfo}}", label: "{{WindowInfo}}",
desc: "当前窗口信息JSON格式可以指定键值如{{WindowInfo.id}}", desc: "当前窗口信息JSON格式可以指定键值如{{WindowInfo.id}}",
type: "json", type: "json",
match: /{{WindowInfo(.*?)}}/mg, match: /{{WindowInfo(.*?)}}/gm,
repl: (jsonVar, enterData) => handlingJsonVar(jsonVar, "WindowInfo", enterData.payload) onlyCmdTypes: ["window"],
repl: (jsonVar, enterData) =>
handlingJsonVar(jsonVar, "WindowInfo", enterData.payload),
}, },
MatchImage: { MatchImage: {
name: "MatchImage", name: "MatchImage",
label: "{{MatchImage}}", label: "{{MatchImage}}",
desc: "匹配到图片的 DataUrl", desc: "匹配到图片的 DataUrl",
match: /{{MatchImage}}/mg, match: /{{MatchImage}}/gm,
repl: (text, enterData) => enterData.payload onlyCmdTypes: ["img"],
repl: (text, enterData) => enterData.payload,
}, },
SelectFile: { SelectFile: {
name: "SelectFile", name: "SelectFile",
label: "{{SelectFile}}", label: "{{SelectFile}}",
desc: "文件管理器选中的文件不支持Linux", desc: "文件管理器选中的文件不支持Linux",
match: /{{SelectFile}}/mg, match: /{{SelectFile}}/gm,
repl: (text, enterData) => window.getSelectFile(enterData.payload.id) repl: (text, enterData) => window.getSelectFile(enterData.payload.id),
}, },
MatchedFiles: { MatchedFiles: {
name: "MatchedFiles", name: "MatchedFiles",
label: "{{MatchedFiles}}", label: "{{MatchedFiles}}",
desc: "匹配的文件JSON格式可以指定键值如{{MatchedFiles[0].path}}", desc: "匹配的文件JSON格式可以指定键值如{{MatchedFiles[0].path}}",
type: "json", type: "json",
match: /{{MatchedFiles(.*?)}}/mg, match: /{{MatchedFiles(.*?)}}/gm,
repl: (jsonVar, enterData) => handlingJsonVar(jsonVar, "MatchedFiles", enterData.payload) onlyCmdTypes: ["files"],
repl: (jsonVar, enterData) =>
handlingJsonVar(jsonVar, "MatchedFiles", enterData.payload),
}, },
type: { type: {
name: "type", name: "type",
label: "{{type}}", label: "{{type}}",
desc: "onPluginEnter的type匹配的类型", desc: "onPluginEnter的type匹配的类型",
match: /{{type}}/mg, match: /{{type}}/gm,
repl: (text, enterData) => enterData.type repl: (text, enterData) => enterData.type,
}, },
payload: { payload: {
name: "payload", name: "payload",
label: "{{payload}}", label: "{{payload}}",
desc: "onPluginEnter的payload,当为JSON时可以指定键值如{{payload.id}}", desc: "onPluginEnter的payload,当为JSON时可以指定键值如{{payload.id}}",
type: "json", type: "json",
match: /{{payload(.*?)}}/mg, match: /{{payload(.*?)}}/gm,
repl: (jsonVar, enterData) => handlingJsonVar(jsonVar, "payload", enterData.payload) repl: (jsonVar, enterData) =>
handlingJsonVar(jsonVar, "payload", enterData.payload),
}, },
js: { js: {
name: "js", name: "js",
@ -127,8 +129,8 @@ const specialVars = {
desc: "获取js表达式的值如{{js:utools.isMacOs()}}", desc: "获取js表达式的值如{{js:utools.isMacOs()}}",
tooltip: "注意必须为表达式而非语句类似Vue的文本插值", tooltip: "注意必须为表达式而非语句类似Vue的文本插值",
type: "command", type: "command",
match: /{{js:(.*?)}}/mg, match: /{{js:(.*?)}}/gm,
repl: js => handlingJsExpression(js) repl: (js) => handlingJsExpression(js),
}, },
python: { python: {
name: "python", name: "python",
@ -136,20 +138,20 @@ const specialVars = {
desc: "模拟python -c并获取返回值如{{py:print(1)}}", desc: "模拟python -c并获取返回值如{{py:print(1)}}",
tooltip: "只支持单行语句", tooltip: "只支持单行语句",
type: "command", type: "command",
match: /{{py:(.*?)}}/mg, match: /{{py:(.*?)}}/gm,
repl: py => window.runPythonCommand(py.slice(5, -2)) repl: (py) => window.runPythonCommand(py.slice(5, -2)),
}, },
userData: { userData: {
name: "userData", name: "userData",
label: "{{usr:}}", label: "{{usr:}}",
desc: "用户设置的变量,类似一个全局配置项", desc: "用户设置的变量,类似一个全局配置项",
match: /{{usr:(.*?)}}/mg, match: /{{usr:(.*?)}}/gm,
repl: (text, userData) => { repl: (text, userData) => {
let filterd = userData.filter(x => x.id === text.slice(6, -2)) let filterd = userData.filter((x) => x.id === text.slice(6, -2));
return filterd.length ? filterd[0].value : '' return filterd.length ? filterd[0].value : "";
}, },
tooltip: "仅本机可用时,该变量值只在本机有效,否则,所有电脑有效", tooltip: "仅本机可用时,该变量值只在本机有效,否则,所有电脑有效",
} },
} };
export default specialVars export default specialVars;

View File

@ -24,7 +24,7 @@
<component <component
:is="commandEditorAction.component" :is="commandEditorAction.component"
:action="commandEditorAction" :action="commandEditorAction"
@editorEvent="editorEvent" @editorEvent="handleEditorEvent"
/> />
</div> </div>
</transition> </transition>
@ -40,7 +40,7 @@
import { defineAsyncComponent } from "vue"; import { defineAsyncComponent } from "vue";
import { useCommandManager } from "js/commandManager.js"; import { useCommandManager } from "js/commandManager.js";
import changeLog from "js/options/changeLog.js"; import changeLog from "js/options/changeLog.js";
import { utoolsFull, dbManager } from "js/utools.js"; import { utoolsFull } from "js/utools.js";
import CommandEditor from "components/CommandEditor"; import CommandEditor from "components/CommandEditor";
import ComposerEditor from "components/ComposerEditor"; import ComposerEditor from "components/ComposerEditor";
import FooterBar from "src/components/config/FooterBar.vue"; import FooterBar from "src/components/config/FooterBar.vue";
@ -214,13 +214,14 @@ export default {
}, },
saveCommand(command) { saveCommand(command) {
const code = this.commandManager.saveCommand(command); const code = this.commandManager.saveCommand(command);
if (!code) return;
this.locateToCommand(command.tags, code); this.locateToCommand(command.tags, code);
quickcommand.showMessageBox("保存成功!"); quickcommand.showMessageBox("保存成功!");
}, },
editorEvent(event) { handleEditorEvent(event, data) {
switch (event.type) { switch (event) {
case "save": case "save":
this.saveCommand(event.data); this.saveCommand(data);
break; break;
case "back": case "back":
this.isEditorShow = false; this.isEditorShow = false;
@ -265,20 +266,12 @@ export default {
} }
.editor-container { .editor-container {
color: var(--utools-bright-font-color); color: var(--utools-font-color);
overflow: hidden; overflow: hidden;
position: fixed; position: fixed;
top: 0; inset: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 5000; z-index: 5000;
background: var(--utools-bright-bg); background: var(--utools-bg-color);
}
.body--dark .editor-container {
color: var(--utools-dark-font-color);
background: var(--utools-dark-bg);
} }
/* 编辑器容器动画 */ /* 编辑器容器动画 */

View File

@ -1,7 +1,5 @@
<template> <template>
<div> <CommandEditor ref="commandEditor" :action="action" />
<CommandEditor ref="commandEditor" :action="action"></CommandEditor>
</div>
</template> </template>
<script> <script>

View File

@ -1,26 +1,12 @@
<template> <template>
<div> <div class="server-page">
<MonacoEditor <CodeEditor
:placeholder="false" v-model="cmd"
class="absolute-top" @update:modelValue="saveCode"
ref="editor" language="quickcommand"
@typing=" style="flex: 1"
(val) => {
if (cmd === val) return;
cmd = val;
saveCode();
}
"
:style="{
bottom: bottomHeight + 'px',
}"
/> />
<div <div class="flex items-center justify-between q-px-md shadow-10">
class="absolute-bottom flex items-center justify-between q-px-md shadow-10"
:style="{
height: bottomHeight + 'px',
}"
>
<div class="q-gutter-xs flex items-center full-height content-center"> <div class="q-gutter-xs flex items-center full-height content-center">
<q-badge color="primary" dense square>POST</q-badge <q-badge color="primary" dense square>POST</q-badge
><q-badge color="primary" dense square>GET</q-badge> ><q-badge color="primary" dense square>GET</q-badge>
@ -73,22 +59,16 @@
</template> </template>
<script> <script>
import MonacoEditor from "components/editor/MonacoEditor"; import CodeEditor from "components/editor/CodeEditor.vue";
import { dbManager } from "js/utools.js"; import { dbManager } from "js/utools.js";
import { ref } from "vue";
export default { export default {
components: { MonacoEditor }, components: { CodeEditor },
data() { setup() {
return { const cmd = ref(dbManager.getStorage("cfg_serverCode") || "");
bottomHeight: 40, const saveCodeTimer = ref(null);
saveCodeTimer: null, return { cmd, saveCodeTimer };
cmd: null,
};
},
mounted() {
this.cmd = dbManager.getStorage("cfg_serverCode") || "";
this.$refs.editor.setEditorValue(this.cmd);
this.$refs.editor.setEditorLanguage("javascript");
}, },
methods: { methods: {
enableServer() { enableServer() {
@ -136,3 +116,12 @@ export default {
}, },
}; };
</script> </script>
<style scoped>
.server-page {
display: flex;
flex-direction: column;
position: fixed;
inset: 0;
}
</style>