Files
uTools-quickcommand/src/components/CommandEditor.vue
2024-12-19 18:35:14 +08:00

653 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="absolute-full container" style="overflow: hidden">
<!-- 命令设置栏 -->
<CommandSideBar
ref="sidebar"
:canCommandSave="canCommandSave"
:quickcommandInfo="quickcommandInfo"
class="absolute-left shadow-10"
:style="{
width: sideBarWidth + 'px',
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"
></CommandSideBar>
<!-- 编程语言栏 -->
<div
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)',
}"
>
<div class="row" v-show="!!languageBarHeight">
<!-- 去掉收起侧栏功能处理侧栏宽度变化时Monaco调整大小导致ResizeObserver loop limit exceeded错误 -->
<!-- <div class="col-auto flex">
<q-btn v-if="!isRunCodePage" flat dense color="primary" class="menuBtn" icon="menu"
@click="toggleSideBarWidth"><q-tooltip>{{ !!sideBarWidth ? "收起" : "展开" }}侧栏</q-tooltip></q-btn>
</div> -->
<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="programChanged"
v-model="quickcommandInfo.program"
:options="programLanguages"
label="环境"
>
<template v-slot:append>
<q-avatar size="lg" square>
<img :src="$root.programs[quickcommandInfo.program].icon" />
</q-avatar>
</template>
<template v-slot:option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<img width="32" :src="$root.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>
<q-btn-dropdown
v-show="quickcommandInfo.program !== 'html'"
style="padding: 0 10px"
dense
flat
ref="settings"
color="primary"
:icon="
quickcommandInfo.program === 'quickcommand'
? 'insights'
: 'settings'
"
>
<q-list>
<!-- quickcommand系列按键 -->
<q-item
clickable
v-for="(item, index) in ['keyboard', 'ads_click', 'help']"
:key="index"
@click="
[
() => (showRecorder = true),
() => (showActions = true),
showHelp,
][index]
"
v-show="quickcommandInfo.program === 'quickcommand'"
>
<q-item-section avatar>
<q-icon :name="item" />
</q-item-section>
<q-item-section>{{
["录制按键", "快捷动作", "查看文档"][index]
}}</q-item-section>
</q-item>
<!-- 自定义解释器 -->
<q-item
v-for="(item, index) in Object.keys(
quickcommandInfo.customOptions
)"
:key="index"
v-show="quickcommandInfo.program === 'custom'"
>
<q-input
stack-label
autofocus
dense
outlined
class="full-width"
@blur="matchLanguage"
:label="
[
'解释器路径,如:/opt/python',
'运行参数,如:-u',
'脚本后缀不含点py',
][index]
"
v-model="quickcommandInfo.customOptions[item]"
>
<template v-slot:prepend>
<q-icon name="code" />
</template>
</q-input>
</q-item>
<!-- 脚本参数 -->
<q-item v-show="quickcommandInfo.program !== 'quickcommand'">
<q-input
dense
stack-label
outlined
label="脚本参数"
class="full-width"
v-model="quickcommandInfo.scptarg"
>
<template v-slot:prepend>
<q-icon name="input" />
</template>
</q-input>
</q-item>
<!-- 编码设置 -->
<q-item
v-for="(item, index) in Object.keys(quickcommandInfo.charset)"
:key="index"
v-show="quickcommandInfo.program !== 'quickcommand'"
>
<q-select
dense
outlined
stack-label
clearable
class="full-width"
:label="['脚本编码', '输出编码'][index]"
v-model="quickcommandInfo.charset[item]"
: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
style="padding: 0 10px"
dense
flat
color="primary"
icon="play_arrow"
label="运行"
@click="runCurrentCommand()"
></q-btn>
<q-btn
flat
style="padding: 0 10px"
dense
v-if="!isRunCodePage"
:disable="!canCommandSave"
:color="canCommandSave ? 'primary' : 'grey'"
icon="save"
label="保存"
@click="saveCurrentCommand()"
></q-btn>
</q-btn-group>
</div>
<q-dialog v-model="showActions">
<QuickAction @addAction="insertText" />
</q-dialog>
<q-dialog v-model="showRecorder" position="bottom">
<KeyRecorder @sendKeys="insertText" />
</q-dialog>
</div>
</div>
<!-- 编辑器 -->
<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,
}"
/>
<!-- 编辑器工具按钮组 -->
<div class="editor-tools">
<!-- 历史按钮 -->
<q-btn
class="history-btn"
round
flat
icon="history"
@click="showHistory"
>
<q-tooltip>历史记录</q-tooltip>
</q-btn>
<!-- 全屏按钮 -->
<q-btn
class="fullscreen-btn"
round
flat
:icon="isFullscreen ? 'fullscreen_exit' : 'fullscreen'"
@click="toggleFullscreen"
:class="{ 'btn-fullscreen': isFullscreen }"
>
<q-tooltip>{{
isFullscreen ? "退出全屏 (F11)" : "全屏编辑 (F11)"
}}</q-tooltip>
</q-btn>
</div>
<!-- 运行结果 -->
<CommandRunResult :action="action" ref="result"></CommandRunResult>
<!-- 历史记录组件 -->
<EditorHistory
ref="history"
@restore="restoreHistory"
:commandCode="quickcommandInfo.features?.code || 'temp'"
/>
</div>
</template>
<script>
import { defineAsyncComponent } from "vue";
import CommandSideBar from "components/CommandSideBar";
import CommandRunResult from "components/CommandRunResult";
import QuickAction from "components/popup/QuickAction";
import KeyRecorder from "components/popup/KeyRecorder";
import EditorHistory from 'components/popup/EditorHistory.vue'
// Performance Scripting > 500ms
const MonacoEditor = defineAsyncComponent(() =>
import("components/MonacoEditor")
);
const defaultSideBarWidth = 200;
const defaultlanguageBarHeight = 40;
export default {
components: {
MonacoEditor,
CommandSideBar,
CommandRunResult,
QuickAction,
KeyRecorder,
EditorHistory
},
data() {
return {
programLanguages: Object.keys(this.$root.programs),
sideBarWidth: defaultSideBarWidth,
languageBarHeight: defaultlanguageBarHeight,
canCommandSave: this.action.type === "code" ? false : true,
showActions: false,
showRecorder: false,
quickcommandInfo: {
program: "quickcommand",
cmd: "",
scptarg: "",
charset: {
scriptCode: "",
outputCode: "",
},
customOptions: {
bin: "",
argv: "",
ext: "",
},
},
resultMaxLength: 10000,
showSidebar: this.action.type !== "run",
isRunCodePage: this.action.type === "run",
listener: null,
isFullscreen: false,
};
},
props: {
action: Object,
},
created() {
this.commandInit();
},
mounted() {
this.sidebarInit();
},
computed: {
configurationPage() {
return this.$root.$refs.view;
},
allQuickCommandTags() {
return _.without(
this.configurationPage.allQuickCommandTags,
"默认",
"未分类",
"搜索结果"
// "来自分享"
);
},
},
methods: {
// 命令初始化
commandInit() {
let quickCommandInfo =
this.action.type === "run"
? this.$root.utools.getDB("cfg_codeHistory")
: this.action.data;
quickCommandInfo?.program &&
Object.assign(this.quickcommandInfo, _.cloneDeep(quickCommandInfo));
// 默认命令编辑
if (this.quickcommandInfo.tags?.includes("默认") && !utools.isDev()) {
this.canCommandSave = false;
}
},
// 侧边栏初始化
sidebarInit() {
this.$refs.sidebar?.init();
},
// Monaco编辑器初始化Monaco异步加载完执行
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(this.$root.programs).filter(
(program) => program.ext === this.quickcommandInfo.customOptions.ext
);
if (language.length) {
this.setLanguage(language[0].name);
}
},
// 设置编程语言
setLanguage(language) {
let highlight = this.$root.programs[language].highlight;
this.$refs.editor.setEditorLanguage(highlight ? highlight : language);
},
insertText(text) {
this.$refs.editor.repacleEditorSelection(text);
},
// 打开文档
showHelp() {
window.showUb.docs();
},
// 展开收起栏
toggleSideBarWidth() {
this.sideBarWidth = !!this.sideBarWidth ? 0 : defaultSideBarWidth;
},
// 保存
saveCurrentCommand(config = { silent: false }) {
let updatedData = this.$refs.sidebar?.SaveMenuData();
if (!updatedData) return;
Object.assign(this.quickcommandInfo, _.cloneDeep(updatedData));
let newQuickcommandInfo = _.cloneDeep(this.quickcommandInfo);
this.$root.utools.putDB(
newQuickcommandInfo,
"qc_" + this.quickcommandInfo.features.code
);
this.$emit("editorEvent", {
type: "save",
data: newQuickcommandInfo,
});
if (!config.silent) {
this.saveToHistory(); // 保存时记录历史
}
},
// 运行
runCurrentCommand(cmd) {
this.saveToHistory('运行时自动保存'); // 运行时保存
let command = _.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;
this.$refs.result.runCurrentCommand(command);
},
saveCodeHistory() {
if (this.action.type !== "run") return;
let command = _.cloneDeep(this.quickcommandInfo);
command.cursorPosition = this.$refs.editor.getCursorPosition();
this.$root.utools.putDB(command, "cfg_codeHistory");
},
monacoTyping(val) {
this.quickcommandInfo.cmd = val;
},
monacoKeyStroke(event, data) {
switch (event) {
case "run":
this.runCurrentCommand();
break;
case "save":
this.saveCurrentCommand();
break;
case "log":
if (this.quickcommandInfo.program !== "quickcommand") return;
this.runCurrentCommand(`console.log(${data})`);
break;
case "fullscreen":
this.toggleFullscreen();
break;
default:
break;
}
},
getFullscreenScale() {
const currentWidth = window.innerWidth - this.sideBarWidth;
const currentHeight = window.innerHeight - this.languageBarHeight;
const fullWidth = window.innerWidth;
const fullHeight = window.innerHeight;
const scaleX = fullWidth / currentWidth;
const scaleY = fullHeight / currentHeight;
return Math.max(scaleX, scaleY);
},
toggleFullscreen() {
this.isFullscreen = !this.isFullscreen;
// 重新布局编辑器
setTimeout(() => {
this.$refs.editor.resizeEditor();
}, 300);
},
showHistory() {
this.$refs.history.open();
},
saveToHistory(message = '已保存') {
// 确保编辑器已经初始化
if (!this.$refs.editor) {
return;
}
const content = this.$refs.editor.getEditorValue();
// 检查内容是否为空或未定义
if (!content || !content.trim()) {
return;
}
this.$refs.history.saveHistory(
content,
this.quickcommandInfo.program
);
// 显示提示
quickcommand.showMessageBox(
message,
'success',
1000,
'bottom-right'
);
},
restoreHistory(item) {
// 保存当前内容
this.saveToHistory('已保存当前内容');
// 恢复历史内容
this.$refs.editor.setEditorValue(item.content);
quickcommand.showMessageBox(
'已恢复历史内容',
'success',
1000,
'bottom-right'
);
},
},
};
</script>
<style scoped>
.menuBtn {
background: rgba(0, 0, 0, 0.05);
border-radius: 0;
}
.body--dark .menuBtn {
background: rgba(255, 255, 255, 0.07);
}
.fullscreen-btn {
z-index: 1000;
background: rgba(var(--q-primary-rgb), 0.08);
color: var(--q-primary);
transform-origin: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(4px);
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.fullscreen-btn:hover {
background: rgba(var(--q-primary-rgb), 0.15);
transform: scale(1.1) translateY(-2px);
box-shadow: 0 4px 12px rgba(var(--q-primary-rgb), 0.2);
}
.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) translateY(-2px);
}
.body--dark .fullscreen-btn {
background: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.9);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.body--dark .fullscreen-btn:hover {
background: rgba(255, 255, 255, 0.15);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
/* 统一过渡效果 */
.sidebar-transition,
.language-bar-transition,
.editor-transition {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
will-change: transform, left, top, opacity;
}
.editor-container {
position: relative;
overflow: hidden;
}
.editor-wrapper {
position: absolute;
right: 0;
bottom: 0;
height: auto;
}
.monaco-editor {
width: 100%;
height: 100%;
}
.editor-fullscreen {
left: 0 !important;
top: 0 !important;
z-index: 2;
}
.editor-transition {
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
will-change: transform, left, top, opacity;
}
.editor-tools {
position: fixed;
right: 24px;
bottom: 24px;
z-index: 1000;
display: flex;
flex-direction: column;
gap: 12px;
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.isFullscreen .editor-tools {
right: 32px;
bottom: 32px;
}
.history-btn {
background: rgba(var(--q-primary-rgb), 0.08);
color: var(--q-primary);
transform-origin: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(4px);
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.history-btn:hover {
background: rgba(var(--q-primary-rgb), 0.15);
transform: scale(1.1) translateY(-2px);
box-shadow: 0 4px 12px rgba(var(--q-primary-rgb), 0.2);
}
.body--dark .history-btn {
background: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.9);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.body--dark .history-btn:hover {
background: rgba(255, 255, 255, 0.15);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
</style>