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

View File

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

View File

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

View File

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

View File

@ -197,16 +197,17 @@ export default defineComponent({
};
const consoleLogVars =
this.getAvailableOutputVariableName(outputVariable);
const tempFlows = [{
name: "main",
commands: [
tempCommand,
{
//
code: `if(${consoleLogVars}!==undefined){
const tempFlows = [
{
name: "main",
commands: [
tempCommand,
{
//
code: `if(${consoleLogVars}!==undefined){
console.log(${consoleLogVars})
}`,
},
},
],
},
];
@ -314,6 +315,14 @@ export default defineComponent({
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 {
flex: 1;

View File

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

View File

@ -5,8 +5,8 @@
icon="close"
dense
flat
v-if="showCloseButton"
@click="$emit('action', 'close')"
v-if="!disabledButtons.includes('close')"
>
<q-tooltip>退出可视化编排</q-tooltip>
</q-btn>
@ -15,6 +15,7 @@
dense
flat
@click="$emit('action', isAllCollapsed ? 'expandAll' : 'collapseAll')"
v-if="!disabledButtons.includes('expand')"
>
<q-tooltip>{{ isAllCollapsed ? "展开所有" : "折叠所有" }}</q-tooltip>
</q-btn>
@ -22,7 +23,8 @@
icon="settings"
dense
flat
@click="$emit('action', 'toggleVariableManager')"
@click="$emit('action', 'toggleFlowManager')"
v-if="!disabledButtons.includes('manager')"
>
<q-tooltip>流程管理</q-tooltip>
</q-btn>
@ -34,33 +36,40 @@
v-if="isDev"
>
</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
<q-btn
dense
flat
v-close-popup
icon="done_all"
@click="$emit('action', 'apply')"
v-if="showCloseButton"
@click="handleApply"
v-if="!disabledButtons.includes('apply')"
>
<q-tooltip>清空编辑器内容并插入</q-tooltip>
</q-btn> -->
<q-btn dense flat icon="save" @click="$emit('action', 'save')">
</q-btn>
<q-btn
dense
flat
icon="save"
@click="$emit('action', 'save')"
v-if="!disabledButtons.includes('save')"
>
<q-tooltip>保存</q-tooltip>
</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-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-btn>
</div>
@ -99,9 +108,9 @@ export default defineComponent({
type: Boolean,
default: false,
},
showCloseButton: {
type: Boolean,
default: true,
disabledButtons: {
type: Array,
default: () => [],
},
flows: {
type: Array,
@ -119,11 +128,15 @@ export default defineComponent({
};
},
watch: {
isVisible(val) {
if (val) {
this.code = generateFlowsCode(this.flows);
}
methods: {
handlePreviewCode() {
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 { QInput, QSelect, QToggle, QCheckbox } from "quasar";
const CodeEditor = defineAsyncComponent(() =>
import("components/composer/common/CodeEditor.vue")
import("components/editor/CodeEditor.vue")
);
export default defineComponent({

View File

@ -127,7 +127,7 @@
<script>
import { defineComponent } from "vue";
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 ArrayEditor from "components/composer/common/ArrayEditor.vue";
import BorderLabel from "components/composer/common/BorderLabel.vue";

View File

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

View File

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

View File

@ -1,168 +1,231 @@
<template>
<div class="row" v-show="!!height">
<div class="col">
<div>
<q-select
dense
standout="bg-primary text-white"
square
hide-bottom-space
color="primary"
transition-show="jump-down"
transition-hide="jump-up"
@update:model-value="updateProgram"
:model-value="modelValue.program"
:options="programLanguages"
<div class="command-language-bar">
<q-select
class="q-pl-xs"
dense
options-dense
borderless
square
hide-bottom-space
color="primary"
transition-show="jump-down"
transition-hide="jump-up"
:model-value="currentCommand.program"
@update:model-value="handleProgramChange"
:options="Object.keys(programs)"
>
<template v-slot:prepend>
<q-badge
label="环境"
>
<template v-slot:append>
<q-avatar size="lg" square>
<img :src="programs[modelValue.program].icon" />
</q-avatar>
</template>
<template v-slot:option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<img width="32" :src="programs[scope.opt].icon" />
</q-item-section>
<q-item-section>
<q-item-label v-html="scope.opt" />
</q-item-section>
</q-item>
</template>
</q-select>
</div>
</div>
<q-separator vertical />
<div class="col-auto justify-end flex">
<q-btn-group unelevated class="button-group">
<template v-if="modelValue.program === 'quickcommand'">
<q-btn
v-for="(item, index) in [
'help_center',
'view_timeline',
]"
color="primary"
text-color="white"
class="q-ml-sm"
style="height: 20px"
/>
</template>
<template v-slot:append>
<q-avatar size="20px" square v-if="isRunCodePage">
<img :src="programs[currentCommand.program].icon" />
</q-avatar>
</template>
<template v-slot:option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<img width="32" :src="programs[scope.opt].icon" />
</q-item-section>
<q-item-section>
<q-item-label v-html="scope.opt" />
</q-item-section>
</q-item>
</template>
</q-select>
<q-btn-group unelevated class="button-group">
<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"
dense
flat
color="primary"
class="settings-btn"
:icon="item"
@click="handleQuickCommandAction(index)"
clickable
v-close-popup
@click="handleSpecialVarClick(item)"
>
<q-tooltip>
{{ ["查看文档", "可视化编排"][index] }}
</q-tooltip>
</q-btn>
</template>
<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>
<q-btn-dropdown
v-else-if="modelValue.program !== 'html'"
<template v-if="currentCommand.program === 'quickcommand'">
<q-btn
v-for="(item, index) in ['help_center', 'view_timeline']"
:key="index"
dense
flat
color="primary"
class="settings-btn"
dense
flat
ref="settings"
color="primary"
icon="settings"
:label="['文档', '可视化'][index]"
:icon="item"
@click="handleQuickCommandAction(index)"
>
<q-list>
<!-- 自定义解释器 -->
<q-item
v-for="(item, index) in Object.keys(modelValue.customOptions)"
:key="index"
v-show="modelValue.program === 'custom'"
</q-btn>
</template>
<q-btn-dropdown
v-model="isSettingsVisible"
v-else-if="currentCommand.program !== 'html'"
class="settings-btn"
dense
flat
label="设置"
color="primary"
icon="settings"
>
<q-list>
<!-- 自定义解释器 -->
<q-item
v-for="(item, index) in Object.keys(currentCommand.customOptions)"
:key="index"
v-show="currentCommand.program === 'custom'"
>
<q-input
stack-label
autofocus
dense
outlined
class="full-width"
:label="
[
'解释器路径,如:/opt/python',
'运行参数,如:-u',
'脚本后缀不含点py',
][index]
"
:model-value="currentCommand.customOptions[item]"
@update:model-value="
(val) => updateModelValue(`customOptions.${item}`, val)
"
>
<q-input
stack-label
autofocus
dense
outlined
class="full-width"
@blur="matchLanguage"
:label="
[
'解释器路径,如:/opt/python',
'运行参数,如:-u',
'脚本后缀不含点py',
][index]
"
:model-value="modelValue.customOptions[item]"
@update:model-value="(val) => updateCustomOption(item, val)"
>
<template v-slot:prepend>
<q-icon name="code" />
</template>
</q-input>
</q-item>
<!-- 脚本参数 -->
<q-item v-show="modelValue.program !== 'quickcommand'">
<q-input
dense
stack-label
outlined
label="脚本参数"
class="full-width"
:model-value="modelValue.scptarg"
@update:model-value="updateScptarg"
>
<template v-slot:prepend>
<q-icon name="input" />
</template>
</q-input>
</q-item>
<!-- 编码设置 -->
<q-item
v-for="(item, index) in Object.keys(modelValue.charset)"
:key="index"
v-show="modelValue.program !== 'quickcommand'"
<template v-slot:prepend>
<q-icon name="code" />
</template>
</q-input>
</q-item>
<!-- 脚本参数 -->
<q-item v-show="currentCommand.program !== 'quickcommand'">
<q-input
dense
stack-label
outlined
label="脚本参数"
class="full-width"
:model-value="currentCommand.scptarg"
@update:model-value="updateModelValue('scptarg', $event)"
>
<q-select
dense
outlined
stack-label
clearable
class="full-width"
:label="['脚本编码', '输出编码'][index]"
:model-value="modelValue.charset[item]"
@update:model-value="(val) => updateCharset(item, val)"
:options="['GBK', 'utf8', 'Big5']"
type="text"
>
<template v-slot:prepend>
<q-icon :name="['format_size', 'output'][index]" />
</template>
</q-select>
</q-item>
</q-list>
</q-btn-dropdown>
<q-separator vertical inset />
<q-btn
class="action-btn run-btn"
dense
flat
color="primary"
icon="play_arrow"
label="运行"
@click="$emit('run')"
></q-btn>
<q-btn
class="action-btn save-btn"
flat
dense
v-if="!isRunCodePage"
:disable="!canCommandSave"
:color="canCommandSave ? 'primary' : 'grey'"
icon="save"
label="保存"
@click="$emit('save')"
></q-btn>
</q-btn-group>
</div>
<template v-slot:prepend>
<q-icon name="input" />
</template>
</q-input>
</q-item>
<!-- 编码设置 -->
<q-item
v-for="(item, index) in Object.keys(currentCommand.charset)"
:key="index"
v-show="currentCommand.program !== 'quickcommand'"
>
<q-select
dense
outlined
stack-label
clearable
class="full-width"
:label="['脚本编码', '输出编码'][index]"
:model-value="currentCommand.charset[item]"
@update:model-value="
(val) => updateModelValue(`charset.${item}`, val)
"
:options="['GBK', 'utf8', 'Big5']"
type="text"
>
<template v-slot:prepend>
<q-icon :name="['format_size', 'output'][index]" />
</template>
</q-select>
</q-item>
</q-list>
</q-btn-dropdown>
<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
class="action-btn run-btn"
dense
flat
color="primary"
icon="play_arrow"
label="运行"
@click="$emit('action', 'run')"
></q-btn>
<q-btn
v-if="!isRunCodePage"
:disable="!canCommandSave"
:color="canCommandSave ? 'primary' : 'grey'"
class="action-btn save-btn"
flat
dense
icon="save"
label="保存"
@click="$emit('action', 'save')"
></q-btn>
</q-btn-group>
<q-dialog v-model="showUserData">
<UserData
@insertText="
insertSpecialVar($event);
showUserData = false;
"
:showInsertBtn="true"
/>
</q-dialog>
</div>
</template>
<script>
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 {
name: "CommandLanguageBar",
@ -171,10 +234,6 @@ export default {
type: Object,
required: true,
},
height: {
type: Number,
default: 40,
},
canCommandSave: {
type: Boolean,
default: true,
@ -184,149 +243,121 @@ export default {
default: false,
},
},
emits: [
"update:modelValue",
"program-changed",
"run",
"save",
"show-composer",
],
emits: ["update:modelValue", "action"],
components: {
UserData,
},
data() {
return {
programs,
specialVars,
commandTypes,
isSettingsVisible: false,
showUserData: false,
};
},
computed: {
programLanguages() {
return Object.keys(this.programs);
currentCommand() {
return this.modelValue;
},
},
methods: {
updateProgram(value) {
this.$emit("update:modelValue", {
...this.modelValue,
program: value,
});
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();
handleProgramChange(newProgram) {
const newCommand = { ...this.currentCommand };
newCommand.program = newProgram;
if (newProgram === "custom") {
this.isSettingsVisible = true;
}
},
matchLanguage() {
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);
if (newProgram === "html") {
newCommand.output = "html";
}
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) {
const actions = [
() => this.showHelp(),
() => this.$emit("show-composer"),
() => this.$emit("action", "show-composer"),
];
actions[index]();
},
showHelp() {
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>
<style scoped>
.button-group {
.action-btn {
padding: 0 10px;
}
padding: 0 5px;
}
.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) {
display: inline-block;
animation: slideRight 1.5s infinite;
animation: leftRight 1.5s infinite;
}
/* 保存按钮动画 */
.save-btn:not([disabled]):hover :deep(.q-icon) {
display: inline-block;
animation: saveAnimation 1.2s infinite;
animation: upDown 1.2s infinite;
}
/* 设置按钮动画 */
.settings-btn :deep(.q-icon:first-child) {
display: inline-block;
transform: scale(1);
transition: transform 0.5s ease;
.command-language-bar {
background-color: #fffffe;
height: 30px;
margin-bottom: 2px;
display: flex;
align-items: center;
justify-content: space-between;
}
.settings-btn:hover :deep(.q-icon:first-child) {
transform: scale(1.05);
.body--dark .command-language-bar {
background-color: #1e1e1e;
}
@keyframes slideRight {
0% {
transform: translateX(-2px);
opacity: 0.7;
}
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;
.command-language-bar :deep(.q-field__control),
.command-language-bar :deep(.q-field__control > *),
.command-language-bar :deep(.q-field__native) {
max-height: 30px;
min-height: 30px;
}
</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"
@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>
</template>
@ -36,12 +22,8 @@ export default {
type: String,
default: "temp",
},
isFullscreen: {
type: Boolean,
default: false,
},
},
emits: ["restore", "toggle-fullscreen"],
emits: ["restore"],
methods: {
showHistory() {
this.$refs.history.open();
@ -58,46 +40,10 @@ export default {
position: fixed;
right: 24px;
bottom: 24px;
z-index: 1000;
z-index: 500;
display: flex;
flex-direction: column;
gap: 12px;
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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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