将编辑器历史按钮移至顶栏

This commit is contained in:
fofolee 2025-02-18 17:51:20 +08:00
parent 9e00a08253
commit 2217685072
5 changed files with 280 additions and 304 deletions

View File

@ -1,11 +1,20 @@
<template> <template>
<div class="command-editor"> <div class="command-editor">
<!-- 编程语言栏 --> <div class="command-editor-header">
<CommandLanguageBar <!-- 语言选择器及相关配置 -->
v-model="commandManager.state.currentCommand" <CommandLanguage
:canCommandSave="canCommandSave" v-model="commandManager.state.currentCommand"
@action="handleAction" @action="handleAction"
/> />
<!-- 操作按钮 -->
<CommandActions
ref="editorActions"
:command-code="currentCommand.features?.code || 'temp'"
:can-command-save="canCommandSave"
@action="handleAction"
/>
</div>
<!-- 命令设置栏 --> <!-- 命令设置栏 -->
<CommandConfig <CommandConfig
@ -30,14 +39,6 @@
/> />
</div> </div>
<!-- 编辑器工具按钮组 -->
<EditorTools
ref="editorTools"
v-show="!isConfigExpanded"
:commandCode="currentCommand.features?.code || 'temp'"
@restore="restoreHistory"
/>
<!-- 可视化编排 --> <!-- 可视化编排 -->
<q-dialog v-model="showComposer" maximized> <q-dialog v-model="showComposer" maximized>
<CommandComposer <CommandComposer
@ -53,12 +54,12 @@
</template> </template>
<script> <script>
import { defineAsyncComponent, ref, computed } from "vue"; import { defineAsyncComponent, computed } from "vue";
import CommandConfig from "components/editor/CommandConfig.vue"; import CommandConfig from "components/editor/CommandConfig.vue";
import CommandLanguageBar from "components/editor/CommandLanguageBar";
import EditorTools from "components/editor/EditorTools";
import CommandRunResult from "components/CommandRunResult"; import CommandRunResult from "components/CommandRunResult";
import CommandComposer from "components/composer/CommandComposer.vue"; import CommandComposer from "components/composer/CommandComposer.vue";
import CommandLanguage from "components/editor/CommandLanguage";
import CommandActions from "components/editor/CommandActions";
import programs from "js/options/programs.js"; import programs from "js/options/programs.js";
import { useCommandManager } from "js/commandManager.js"; import { useCommandManager } from "js/commandManager.js";
@ -86,9 +87,9 @@ export default {
CodeEditor, CodeEditor,
CommandConfig, CommandConfig,
CommandRunResult, CommandRunResult,
CommandLanguageBar, CommandLanguage,
CommandActions,
CommandComposer, CommandComposer,
EditorTools,
}, },
emits: ["editorEvent"], emits: ["editorEvent"],
data() { data() {
@ -187,12 +188,15 @@ export default {
case "insert-text": case "insert-text":
this.$refs.editor.repacleEditorSelection(data); this.$refs.editor.repacleEditorSelection(data);
break; break;
case "restore":
this.restoreHistory(data);
break;
default: default:
break; break;
} }
}, },
saveToHistory() { saveToHistory() {
this.$refs.editorTools.tryToSave( this.$refs.editorActions.tryToSave(
this.currentCommand.cmd, this.currentCommand.cmd,
this.currentCommand.program this.currentCommand.program
); );
@ -254,6 +258,34 @@ export default {
background-color: #1e1e1e; background-color: #1e1e1e;
} }
.command-editor-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 30px;
}
.command-editor-header :deep(.q-btn) {
padding: 0 5px;
}
.command-editor-header :deep(.q-btn__content) {
font-size: 12px;
}
.command-editor-header :deep(.q-btn-dropdown__arrow) {
margin-left: 0;
}
.command-editor-header :deep(.q-focus-helper) {
display: none;
}
.command-editor-header :deep(.q-btn:hover) {
filter: brightness(1.2);
transition: all 0.2s ease;
}
.codeEditor { .codeEditor {
flex: 1; flex: 1;
min-height: 0; min-height: 0;

View File

@ -0,0 +1,207 @@
<template>
<div class="command-actions">
<q-btn
label="历史"
class="action-btn"
dense
flat
stretch
color="primary"
:icon="isSaving ? 'check_circle' : 'history'"
:class="{ 'saving-animation': isSaving }"
@click="$refs.history.open()"
></q-btn>
<q-btn-dropdown
class="special-var-btn"
dense
flat
stretch
label="变量"
color="primary"
icon="data_object"
>
<q-list>
<q-item
v-for="(item, index) in Object.values(specialVars)"
:key="index"
clickable
v-close-popup
@click="handleSpecialVarClick(item)"
>
<q-item-section>
<q-item-label class="row items-center justify-between">
<div v-text="item.label" />
<div v-if="item.onlyCmdTypes" class="row">
<q-badge color="grey-9" class="q-ml-xs"> </q-badge>
<q-badge
v-for="type in item.onlyCmdTypes"
:key="type"
class="q-ml-xs"
v-text="commandTypes[type].label"
color="grey-9"
/>
</div>
</q-item-label>
<q-tooltip v-if="item.tooltip">
{{ item.tooltip }}
</q-tooltip>
<q-item-label caption>{{ item.desc }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
<q-separator vertical class="q-my-xs" />
<q-btn
v-if="!isRunCodePage"
class="action-btn run-btn"
dense
flat
stretch
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="`运行(${ctrlKey}B)`"
@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="`保存(${ctrlKey}S)`"
@click="$emit('action', 'save')"
></q-btn>
<q-dialog v-model="showUserData">
<UserData
@insertText="
insertSpecialVar($event);
showUserData = false;
"
:showInsertBtn="true"
/>
</q-dialog>
<!-- 历史记录组件 -->
<EditorHistory
ref="history"
:command-code="commandCode"
@restore="$emit('action', 'restore', $event)"
/>
</div>
</template>
<script>
import UserData from "components/popup/UserData.vue";
import commandTypes from "js/options/commandTypes.js";
import specialVars from "js/options/specialVars.js";
import EditorHistory from "components/popup/EditorHistory.vue";
export default {
name: "CommandActions",
components: {
UserData,
EditorHistory,
},
props: {
canCommandSave: {
type: Boolean,
default: true,
},
commandCode: {
type: String,
default: "temp",
},
},
emits: ["action"],
data() {
return {
ctrlKey: window.utools.isMacOS() ? "⌘" : "⌃",
commandTypes,
specialVars,
showUserData: false,
isSaving: false,
};
},
computed: {
isRunCodePage() {
return this.$route.name === "code";
},
},
methods: {
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}"`);
},
tryToSave(content, program) {
const saved = this.$refs.history.tryToSave(content, program);
if (saved) this.showSaveAnimation();
},
showSaveAnimation() {
this.isSaving = true;
setTimeout(() => {
this.isSaving = false;
}, 2000);
},
},
};
</script>
<style scoped>
.command-actions {
display: flex;
align-items: center;
}
/* 保存动画 */
@keyframes saving {
0% {
transform: scale(1);
opacity: 1;
}
30% {
transform: scale(1.4);
opacity: 0.8;
}
60% {
transform: scale(0.8);
opacity: 0.6;
}
100% {
transform: scale(1);
opacity: 1;
}
}
.saving-animation :deep(.q-icon) {
animation: saving 2s cubic-bezier(0.4, 0, 0.2, 1);
will-change: transform, opacity;
}
/* 运行按钮动画 */
.run-btn:hover :deep(.q-icon) {
display: inline-block;
animation: leftRight 1.5s infinite;
}
/* 保存按钮动画 */
.save-btn:not([disabled]):hover :deep(.q-icon) {
display: inline-block;
animation: upDown 1.2s infinite;
}
</style>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="command-language-bar"> <div class="command-language">
<q-select <q-select
class="q-pl-xs" class="q-pl-xs"
dense dense
@ -39,46 +39,10 @@
</q-item> </q-item>
</template> </template>
</q-select> </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"
clickable
v-close-popup
@click="handleSpecialVarClick(item)"
>
<q-item-section>
<q-item-label class="row items-center justify-between">
<div v-text="item.label" />
<div v-if="item.onlyCmdTypes" class="row">
<q-badge color="grey-9" class="q-ml-xs"> </q-badge>
<q-badge
v-for="type in item.onlyCmdTypes"
:key="type"
class="q-ml-xs"
v-text="commandTypes[type].label"
color="grey-9"
/>
</div>
</q-item-label>
<q-tooltip v-if="item.tooltip">
{{ item.tooltip }}
</q-tooltip>
<q-item-label caption>{{ item.desc }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
<q-space />
<q-btn-group unelevated class="button-group">
<template v-if="currentCommand.program === 'quickcommand'"> <template v-if="currentCommand.program === 'quickcommand'">
<q-btn <q-btn
v-for="(item, index) in ['help_center', 'view_timeline']" v-for="(item, index) in ['help_center', 'view_timeline']"
@ -177,80 +141,26 @@
</q-item> </q-item>
</q-list> </q-list>
</q-btn-dropdown> </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="`运行(${ctrlKey}B)`"
@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="`保存(${ctrlKey}S)`"
@click="$emit('action', 'save')"
></q-btn>
</q-btn-group> </q-btn-group>
<q-dialog v-model="showUserData">
<UserData
@insertText="
insertSpecialVar($event);
showUserData = false;
"
:showInsertBtn="true"
/>
</q-dialog>
</div> </div>
</template> </template>
<script> <script>
import programs from "js/options/programs.js"; import programs from "js/options/programs.js";
import specialVars from "js/options/specialVars.js";
import commandTypes from "js/options/commandTypes.js";
import UserData from "components/popup/UserData.vue";
export default { export default {
name: "CommandLanguageBar", name: "CommandLanguage",
props: { props: {
modelValue: { modelValue: {
type: Object, type: Object,
required: true, required: true,
}, },
canCommandSave: {
type: Boolean,
default: true,
},
}, },
emits: ["update:modelValue", "action"], emits: ["update:modelValue", "action"],
components: {
UserData,
},
data() { data() {
return { return {
programs, programs,
specialVars,
commandTypes,
isSettingsVisible: false, isSettingsVisible: false,
showUserData: false,
ctrlKey: window.utools.isMacOS() ? "⌘" : "⌃",
}; };
}, },
computed: { computed: {
@ -269,10 +179,8 @@ export default {
this.isSettingsVisible = true; this.isSettingsVisible = true;
} }
if (newProgram === "html") { if (newProgram === "html") {
// htmloutputhtml
newCommand.output = "html"; newCommand.output = "html";
} else if (this.isRunCodePage) { } else if (this.isRunCodePage) {
// outputtext
newCommand.output = "text"; newCommand.output = "text";
} }
const featuresIcon = this.currentCommand.features.icon || ""; const featuresIcon = this.currentCommand.features.icon || "";
@ -299,69 +207,24 @@ export default {
showHelp() { showHelp() {
window.showUb.docs(); window.showUb.docs();
}, },
handleSpecialVarClick(item) {
if (item.label === "{{usr:}}") this.showUserData = true;
else this.insertSpecialVar(item.label);
},
insertSpecialVar(text) {
if (!text) return;
this.$emit("action", "insert-text", `"${text}"`);
},
}, },
}; };
</script> </script>
<style scoped> <style scoped>
.button-group { .command-language {
padding: 0 5px; flex: 1;
} }
.button-group :deep(.q-focus-helper) { .command-language :deep(.q-field__control),
display: none; .command-language :deep(.q-field__control > *),
} .command-language :deep(.q-field__native) {
.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: leftRight 1.5s infinite;
}
/* 保存按钮动画 */
.save-btn:not([disabled]):hover :deep(.q-icon) {
display: inline-block;
animation: upDown 1.2s infinite;
}
.command-language-bar {
background-color: #fffffe;
height: 30px;
margin-bottom: 2px;
display: flex;
align-items: center;
justify-content: space-between;
}
.body--dark .command-language-bar {
background-color: #1e1e1e;
}
.command-language-bar :deep(.q-field__control),
.command-language-bar :deep(.q-field__control > *),
.command-language-bar :deep(.q-field__native) {
max-height: 30px; max-height: 30px;
min-height: 30px; min-height: 30px;
} }
.command-language {
display: flex;
align-items: center;
}
</style> </style>

View File

@ -1,49 +0,0 @@
<template>
<div class="editor-tools">
<!-- 历史记录组件 -->
<EditorHistory
ref="history"
:commandCode="commandCode"
@restore="$emit('restore', $event)"
/>
</div>
</template>
<script>
import EditorHistory from "components/popup/EditorHistory.vue";
export default {
name: "EditorTools",
components: {
EditorHistory,
},
props: {
commandCode: {
type: String,
default: "temp",
},
},
emits: ["restore"],
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: 500;
display: flex;
flex-direction: column;
gap: 12px;
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
</style>

View File

@ -1,20 +1,5 @@
<template> <template>
<div class="editor-history-container"> <div class="editor-history-container">
<!-- 渲染默认插槽内容(历史按钮) -->
<q-btn
round
dense
class="history-btn"
:class="{ saving: isSaving }"
icon="history"
@click="showHistory"
>
<div class="save-overlay">
<q-icon name="check" />
</div>
<q-tooltip>历史记录</q-tooltip>
</q-btn>
<q-dialog <q-dialog
v-model="show" v-model="show"
position="right" position="right"
@ -36,7 +21,11 @@
enter-active-class="fade-in" enter-active-class="fade-in"
leave-active-class="fade-out" leave-active-class="fade-out"
> >
<div v-if="selectedIndex !== null" class="preview-content" :key="selectedIndex"> <div
v-if="selectedIndex !== null"
class="preview-content"
:key="selectedIndex"
>
<pre>{{ historyList[selectedIndex]?.content || "" }}</pre> <pre>{{ historyList[selectedIndex]?.content || "" }}</pre>
</div> </div>
<div v-else class="preview-placeholder" key="placeholder"> <div v-else class="preview-placeholder" key="placeholder">
@ -142,8 +131,6 @@ export default {
historyList: [], historyList: [],
maxHistoryItems: 50, maxHistoryItems: 50,
storagePrefix: "editor_history_", storagePrefix: "editor_history_",
isSaving: false,
saveTimer: null,
selectedIndex: null, selectedIndex: null,
showClearConfirm: false, showClearConfirm: false,
}; };
@ -175,21 +162,7 @@ export default {
return false; return false;
} }
const saved = this.saveHistory(content, program); return this.saveHistory(content, program);
if (saved) {
this.showSaveAnimation();
}
return true;
},
showSaveAnimation() {
if (this.saveTimer) {
clearTimeout(this.saveTimer);
}
this.isSaving = true;
this.saveTimer = setTimeout(() => {
this.isSaving = false;
}, 1500);
}, },
showHistory() { showHistory() {
@ -497,56 +470,6 @@ export default {
opacity: 0.3; opacity: 0.3;
} }
.history-btn {
color: #666;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 3px 3px rgba(0, 0, 0, 0.2);
position: relative;
overflow: hidden;
}
.history-btn:hover {
transform: translateY(-1px);
}
.save-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: var(--q-positive);
transform: translateY(100%);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.save-overlay i {
color: white;
font-size: 1.2em;
}
.history-btn.saving {
pointer-events: none;
}
.history-btn.saving .save-overlay {
transform: translateY(0);
}
/* 暗色模式适配 */
.body--dark .history-btn {
color: #bbb;
background: rgba(255, 255, 255, 0.1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.body--dark .history-btn:hover {
background: #505050;
}
/* 确认对话框样式 */ /* 确认对话框样式 */
.confirm-dialog { .confirm-dialog {
min-width: 300px; min-width: 300px;