编辑器语言栏组件化

This commit is contained in:
fofolee 2024-12-20 13:33:27 +08:00
parent 16f907567e
commit b191cca77e
4 changed files with 405 additions and 329 deletions

View File

@ -1,5 +1,5 @@
<template>
<div class="absolute-full container" style="overflow: hidden">
<div class="absolute-full command-editor-container">
<!-- 命令设置栏 -->
<CommandSideBar
ref="sidebar"
@ -17,7 +17,7 @@
></CommandSideBar>
<!-- 编程语言栏 -->
<div
<CommandLanguageBar
class="absolute-top"
:style="{
left: showSidebar ? sideBarWidth + 'px' : 65,
@ -26,183 +26,15 @@
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>
v-model="quickcommandInfo"
:height="languageBarHeight"
:canCommandSave="canCommandSave"
:isRunCodePage="isRunCodePage"
@program-changed="programChanged"
@run="runCurrentCommand"
@save="saveCurrentCommand"
@add-action="insertText"
/>
<!-- 编辑器 -->
<MonacoEditor
@ -226,28 +58,13 @@
/>
<!-- 编辑器工具按钮组 -->
<div class="editor-tools">
<!-- 历史记录组件 -->
<EditorHistory
ref="history"
:commandCode="quickcommandInfo?.features?.code || 'temp'"
@restore="restoreHistory"
/>
<!-- 全屏按钮 -->
<q-btn
round
dense
:icon="isFullscreen ? 'fullscreen_exit' : 'fullscreen'"
@click="toggleFullscreen"
class="fullscreen-btn"
:class="{ 'btn-fullscreen': isFullscreen }"
>
<q-tooltip>{{
isFullscreen ? "退出全屏 (F11)" : "全屏编辑 (F11)"
}}</q-tooltip>
</q-btn>
</div>
<EditorTools
ref="editorTools"
:commandCode="quickcommandInfo?.features?.code || 'temp'"
:isFullscreen="isFullscreen"
@restore="restoreHistory"
@toggle-fullscreen="toggleFullscreen"
/>
<!-- 运行结果 -->
<CommandRunResult :action="action" ref="result"></CommandRunResult>
@ -258,34 +75,27 @@
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";
import CommandLanguageBar from "components/CommandLanguageBar";
import EditorTools from "components/EditorTools";
// Performance Scripting > 500ms
const MonacoEditor = defineAsyncComponent(() =>
import("components/MonacoEditor")
);
const defaultSideBarWidth = 200;
const defaultlanguageBarHeight = 40;
export default {
components: {
MonacoEditor,
CommandSideBar,
CommandRunResult,
QuickAction,
KeyRecorder,
EditorHistory,
CommandLanguageBar,
EditorTools,
},
data() {
return {
programLanguages: Object.keys(this.$root.programs),
sideBarWidth: defaultSideBarWidth,
languageBarHeight: defaultlanguageBarHeight,
sideBarWidth: 200,
languageBarHeight: 40,
canCommandSave: this.action.type === "code" ? false : true,
showActions: false,
showRecorder: false,
quickcommandInfo: {
program: "quickcommand",
cmd: "",
@ -382,14 +192,6 @@ export default {
insertText(text) {
this.$refs.editor.repacleEditorSelection(text);
},
//
showHelp() {
window.showUb.docs();
},
//
toggleSideBarWidth() {
this.sideBarWidth = !!this.sideBarWidth ? 0 : defaultSideBarWidth;
},
//
saveCurrentCommand(message = "保存成功") {
let updatedData = this.$refs.sidebar?.SaveMenuData();
@ -447,17 +249,6 @@ export 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;
@ -466,11 +257,8 @@ export default {
this.$refs.editor.resizeEditor();
}, 300);
},
showHistory() {
this.$refs.history.open();
},
saveToHistory() {
this.$refs.history.tryToSave(
this.$refs.editorTools.tryToSave(
this.$refs.editor.getEditorValue(),
this.quickcommandInfo.program
);
@ -487,100 +275,27 @@ export default {
</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;
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);
}
/* 统一过渡效果 */
.sidebar-transition,
.language-bar-transition,
.editor-transition {
.language-bar-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);
.command-editor-container {
color: black;
background: white;
overflow: hidden
}
.isFullscreen .editor-tools {
right: 32px;
bottom: 32px;
.body--dark .command-editor-container {
color: white;
background: var(--q-dark-page);
}
</style>

View File

@ -0,0 +1,268 @@
<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"
label="环境"
>
<template v-slot:append>
<q-avatar size="lg" square>
<img :src="$root.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="$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="modelValue.program !== 'html'"
style="padding: 0 10px"
dense
flat
ref="settings"
color="primary"
:icon="modelValue.program === 'quickcommand' ? 'insights' : 'settings'"
>
<q-list>
<!-- quickcommand系列按键 -->
<q-item
clickable
v-for="(item, index) in ['keyboard', 'ads_click', 'help']"
:key="index"
@click="handleQuickCommandAction(index)"
v-show="modelValue.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(modelValue.customOptions)"
:key="index"
v-show="modelValue.program === 'custom'"
>
<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'"
>
<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
style="padding: 0 10px"
dense
flat
color="primary"
icon="play_arrow"
label="运行"
@click="$emit('run')"
></q-btn>
<q-btn
flat
style="padding: 0 10px"
dense
v-if="!isRunCodePage"
:disable="!canCommandSave"
:color="canCommandSave ? 'primary' : 'grey'"
icon="save"
label="保存"
@click="$emit('save')"
></q-btn>
</q-btn-group>
</div>
<!-- 移动对话框到这里 -->
<q-dialog v-model="showActions">
<QuickAction @addAction="addAction" />
</q-dialog>
<q-dialog v-model="showRecorder" position="bottom">
<KeyRecorder @sendKeys="addAction" />
</q-dialog>
</div>
</template>
<script>
import QuickAction from "components/popup/QuickAction";
import KeyRecorder from "components/popup/KeyRecorder";
export default {
name: 'CommandLanguageBar',
components: {
QuickAction,
KeyRecorder,
},
props: {
modelValue: {
type: Object,
required: true
},
height: {
type: Number,
default: 40
},
canCommandSave: {
type: Boolean,
default: true
},
isRunCodePage: {
type: Boolean,
default: false
}
},
data() {
return {
showActions: false,
showRecorder: false,
}
},
emits: ['update:modelValue', 'program-changed', 'run', 'save', 'show-recorder', 'show-actions', 'show-help', 'add-action'],
computed: {
programLanguages() {
return Object.keys(this.$root.programs)
}
},
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()
}
},
matchLanguage() {
if (!this.modelValue.customOptions.ext) return
let language = Object.values(this.$root.programs).filter(
program => program.ext === this.modelValue.customOptions.ext
)
if (language.length) {
this.$emit('program-changed', language[0].name)
}
},
handleQuickCommandAction(index) {
const actions = [
() => this.showRecorder = true,
() => this.showActions = true,
() => this.showHelp()
]
actions[index]()
},
addAction(text) {
this.$emit('add-action', text)
},
showHelp() {
window.showUb.docs()
}
}
}
</script>

View File

@ -0,0 +1,103 @@
<template>
<div class="editor-tools">
<!-- 历史记录组件 -->
<EditorHistory
ref="history"
: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>
<script>
import EditorHistory from "components/popup/EditorHistory.vue";
export default {
name: 'EditorTools',
components: {
EditorHistory
},
props: {
commandCode: {
type: String,
default: 'temp'
},
isFullscreen: {
type: Boolean,
default: false
}
},
emits: ['restore', 'toggle-fullscreen'],
methods: {
showHistory() {
this.$refs.history.open();
},
tryToSave(content, program) {
this.$refs.history.tryToSave(content, program);
}
}
}
</script>
<style scoped>
.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);
}
.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

@ -35,21 +35,11 @@
background: var(--q-dark-page);
}
.container {
color: black;
background: white;
}
body {
background: #f4f4f4;
color: #333;
}
.body--dark .container {
color: white;
background: var(--q-dark-page);
}
.q-tooltip {
font-size: 11px;
}