feat: improve terminal quick command workflow (#12331)

This commit is contained in:
ssongliu
2026-03-25 08:56:18 +00:00
committed by GitHub
parent 40249b527d
commit bb642cd8a8
12 changed files with 133 additions and 159 deletions
+2
View File
@@ -1371,6 +1371,8 @@ const message = {
quickCommand: 'Quick command | Quick commands',
noSuchCommand: 'No quick command data found in the imported CSV file, check and try again!',
quickCommandHelper: 'You can use the quick commands at the bottom of the "Terminals -> Terminals".',
quickCommandBatchHelper:
'One quick command per line, using the Name|Command format, for example: Check disk|df -h',
groupDeleteHelper:
'After the group is removed, all connections in the group will be migrated to the default group. Continue?',
command: 'Command',
+2
View File
@@ -1412,6 +1412,8 @@ const message = {
quickCommandHelper: 'Puede usar comandos rápidos en la parte inferior de "Terminales -> Terminales".',
groupDeleteHelper:
'Después de eliminar el grupo, todas las conexiones pasarán al grupo predeterminado. ¿Desea continuar?',
quickCommandBatchHelper:
'Un comando rápido por línea, usando el formato Nombre|Comando, por ejemplo: Ver disco|df -h',
command: 'Comando',
quickCmd: 'Comando rápido',
addHost: 'Agregar',
+2
View File
@@ -1383,6 +1383,8 @@ const message = {
quickCommandHelper: '端末 - >端子の下部にあるクイックコマンドを使用できます',
groupDeleteHelper:
'グループが削除された後グループ内のすべての接続がデフォルトグループに移行されます続けたいですか',
quickCommandBatchHelper:
'1 行に 1 つのクイックコマンドを名前|コマンド の形式で入力します: ディスク確認|df -h',
command: '指示',
quickCmd: 'クイックコマンド',
addHost: '追加',
+1
View File
@@ -1357,6 +1357,7 @@ const message = {
noSuchCommand: '가져온 CSV 파일에서 빠른 명령어 데이터를 찾을 없습니다. 확인 다시 시도하세요!',
quickCommandHelper: '"터미널 -> 터미널" 하단에서 빠른 명령을 사용할 있습니다.',
groupDeleteHelper: '그룹을 제거하면 해당 그룹의 모든 연결이 기본 그룹으로 이동됩니다. 계속하시겠습니까?',
quickCommandBatchHelper: ' 줄에 하나의 빠른 명령을 이름|명령 형식으로 입력하세요. : 디스크 확인|df -h',
command: '명령',
quickCmd: '빠른 명령',
addHost: '추가',
+2
View File
@@ -1397,6 +1397,8 @@ const message = {
quickCommandHelper: 'Anda boleh menggunakan arahan pantas di bahagian bawah "Terminal -> Terminal".',
groupDeleteHelper:
'Selepas kumpulan dikeluarkan, semua sambungan dalam kumpulan akan dipindahkan ke kumpulan lalai. Adakah anda mahu meneruskan?',
quickCommandBatchHelper:
'Satu arahan pantas setiap baris, menggunakan format Nama|Arahan, contohnya: Semak cakera|df -h',
command: 'Arahan',
quickCmd: 'Arahan pantas',
addHost: 'Tambah',
+2
View File
@@ -1405,6 +1405,8 @@ const message = {
quickCommandHelper: 'Você pode usar os comandos rápidos na parte inferior de "Terminais -> Terminais".',
groupDeleteHelper:
'Após o grupo ser removido, todas as conexões no grupo serão migradas para o grupo padrão. Você deseja continuar?',
quickCommandBatchHelper:
'Um comando rápido por linha, usando o formato Nome|Comando, por exemplo: Ver disco|df -h',
command: 'Comando',
quickCmd: 'Comando rápido',
addHost: 'Adicionar',
+1
View File
@@ -1390,6 +1390,7 @@ const message = {
quickCommandHelper: 'Вы можете использовать быстрые команды внизу страницы "Терминалы -> Терминалы".',
groupDeleteHelper:
'После удаления группы все подключения в группе будут перемещены в группу по умолчанию. Хотите продолжить?',
quickCommandBatchHelper: 'Одна быстрая команда на строку в формате Имя|Команда, например: Проверить диск|df -h',
command: 'Команда',
quickCmd: 'Быстрая команда',
addHost: 'Добавить',
+2
View File
@@ -1394,6 +1394,8 @@ const message = {
quickCommandHelper: '"Terminaller -> Terminaller" altındaki hızlı komutları kullanabilirsiniz.',
groupDeleteHelper:
'Grup kaldırıldıktan sonra, gruptaki tüm bağlantılar varsayılan gruba taşınacaktır. Devam etmek istiyor musunuz?',
quickCommandBatchHelper:
'Her satıra bir hızlı komut, Ad|Komut formatında girin, örneğin: Diski kontrol et|df -h',
command: 'Komut',
quickCmd: 'Hızlı komut',
addHost: 'Ekle',
+1
View File
@@ -1296,6 +1296,7 @@ const message = {
noSuchCommand: '匯入的CSV檔案中未能發現快速指令資料請檢查後重試',
quickCommandHelper: '常用命令列表用於在終端介面底部快速選擇',
groupDeleteHelper: '移除組後組內所有連接將遷移到 default 組內是否繼續',
quickCommandBatchHelper: '每行一條快速指令 名稱|命令 格式例如查看磁碟|df -h',
command: '指令',
addHost: '新增主機',
localhost: '本機伺服器',
+1
View File
@@ -1305,6 +1305,7 @@ const message = {
quickCommand: '快速命令',
noSuchCommand: '导入的 csv 文件中未能发现快速命令数据请检查后重试',
quickCommandHelper: '常用命令列表用于在终端界面底部快速选择',
quickCommandBatchHelper: '每行一条快速命令 名称|命令 格式例如查看磁盘|df -h',
command: '命令',
addHost: '添加主机',
localhost: '本地服务器',
@@ -14,7 +14,10 @@
:model="dialogData.rowData"
:rules="rules"
>
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-form-item v-if="dialogData.title === 'create'" :label="$t('terminal.batchInput')">
<el-switch v-model="batchMode" />
</el-form-item>
<el-form-item v-if="!batchMode" :label="$t('commons.table.name')" prop="name">
<el-input clearable v-model="dialogData.rowData!.name" />
</el-form-item>
<el-form-item :label="$t('commons.table.group')" prop="groupID">
@@ -29,9 +32,20 @@
</div>
</el-select>
</el-form-item>
<el-form-item :label="$t('terminal.command')" prop="command">
<el-form-item v-if="!batchMode" :label="$t('terminal.command')" prop="command">
<el-input type="textarea" clearable v-model="dialogData.rowData!.command" />
</el-form-item>
<el-form-item v-else :label="$t('terminal.command')">
<el-input
v-model="batchCommandText"
type="textarea"
clearable
:autosize="{ minRows: 8, maxRows: 16 }"
/>
<div class="input-help mt-1 text-[var(--el-text-color-secondary)] text-xs leading-5">
{{ $t('terminal.quickCommandBatchHelper') }}
</div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
@@ -49,14 +63,16 @@ import { reactive, ref } from 'vue';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { ElForm } from 'element-plus';
import { MsgSuccess } from '@/utils/message';
import { MsgError, MsgSuccess } from '@/utils/message';
import { Command } from '@/api/interface/command';
import { addCommand, editCommand } from '@/api/modules/command';
import { addCommand, editCommand, importCommands } from '@/api/modules/command';
import { getGroupList } from '@/api/modules/group';
const loading = ref();
const open = ref();
const groupList = ref();
const batchMode = ref(false);
const batchCommandText = ref('');
const rules = reactive({
name: [Rules.requiredInput],
@@ -79,6 +95,8 @@ const dialogData = ref<DialogProps>({
const acceptParams = (params: DialogProps): void => {
dialogData.value = params;
title.value = i18n.global.t('commons.button.' + dialogData.value.title) + i18n.global.t('terminal.quickCommand');
batchMode.value = false;
batchCommandText.value = '';
loadGroups();
open.value = true;
};
@@ -88,8 +106,45 @@ const handleClose = () => {
const emit = defineEmits<{ (e: 'search'): void }>();
const buildBatchCommands = (): Array<Command.CommandOperate> => {
const lines = batchCommandText.value
.split('\n')
.map((line) => line.trim())
.filter(Boolean);
return lines.map((line) => {
const separatorIndex = line.indexOf('|');
const hasCustomName = separatorIndex > 0;
const name = hasCustomName ? line.slice(0, separatorIndex).trim() : line;
const command = hasCustomName ? line.slice(separatorIndex + 1).trim() : line;
return {
id: 0,
type: 'command',
groupID: dialogData.value.rowData!.groupID,
name,
command,
};
});
};
const onSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
if (batchMode.value) {
if (!dialogData.value.rowData?.groupID) {
MsgError(i18n.global.t('commons.rule.requiredSelect'));
return;
}
const items = buildBatchCommands().filter((item) => item.name && item.command);
if (items.length === 0) {
MsgError(i18n.global.t('commons.rule.requiredInput'));
return;
}
loading.value = true;
await importCommands(items);
open.value = false;
emit('search');
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
return;
}
formEl.validate(async (valid) => {
if (!valid) return;
loading.value = true;
+58 -155
View File
@@ -45,79 +45,44 @@
</template>
<Terminal
:style="{
height: cmdPanelVisible
? `calc(100vh - ${loadHeightWithPanel()})`
: `calc(100vh - ${loadHeight()})`,
height: `calc(100vh - ${loadHeight()})`,
'background-color': `var(--panel-logs-bg-color)`,
}"
:ref="'t-' + item.index"
:key="item.Refresh"
></Terminal>
<transition name="el-fade-in">
<div
v-show="cmdPanelVisible"
class="mb-2 border-b border-[var(--el-border-color)] pb-2 w-full bg-[var(--el-bg-color)]"
>
<el-tabs v-model="activeGroupTab" type="card" class="command-tabs">
<el-tab-pane
v-for="group in commandTree"
:key="group.value"
:label="''"
:name="group.value"
>
<template #label>
<span class="group-tab-label">
<span v-if="group.label.length <= 6">{{ group.label }}</span>
<el-tooltip v-else :content="group.label" placement="top">
<span>{{ group.label.substring(0, 6) }}...</span>
</el-tooltip>
</span>
</template>
<div class="grid grid-cols-[repeat(auto-fill,minmax(160px,1fr))] gap-2 p-0">
<el-tag
v-for="cmd in group.children"
:key="cmd.value"
class="command-tag"
@click="executeCommand(cmd.value)"
type="info"
effect="plain"
>
<div class="flex items-center justify-between w-full gap-1.5">
<span class="command-tag-name" :title="cmd.label">
{{
cmd.label.length > 8 ? cmd.label.substring(0, 8) + '...' : cmd.label
}}
</span>
<el-popover placement="top" :width="320" trigger="hover">
<template #reference>
<el-icon class="command-preview-icon">
<InfoFilled />
</el-icon>
</template>
<div class="command-preview">
<div class="command-preview-name">
<strong>{{ cmd.label }}</strong>
</div>
<div class="command-preview-value">{{ cmd.value }}</div>
</div>
</el-popover>
</div>
</el-tag>
</div>
</el-tab-pane>
</el-tabs>
</div>
</transition>
<div class="flex items-center gap-2 w-full py-2 flex-wrap">
<AiSetting v-if="!mobile" class="shrink-0" />
<el-button @click="cmdPanelVisible = !cmdPanelVisible" class="min-w-[120px] max-w-[150px] shrink-0">
{{ $t('terminal.quickCommand') }}
<el-icon class="ml-1">
<component :is="cmdPanelVisible ? 'ArrowUp' : 'ArrowDown'" />
</el-icon>
</el-button>
<el-cascader
v-model="quickCmd"
:options="commandTree"
:props="quickCommandProps"
:show-all-levels="false"
filterable
clearable
class="quick-command-cascader min-w-[180px] max-w-[260px] shrink-0"
:placeholder="$t('terminal.quickCommand')"
@change="handleQuickCommandChange"
>
<template #default="{ data }">
<el-tooltip
v-if="!data.children?.length"
placement="right"
popper-class="command-detail-tooltip"
>
<template #content>
<div class="command-detail-content">{{ data.value }}</div>
</template>
<div class="cascader-option">
<span class="cascader-option-label">{{ data.label }}</span>
</div>
</el-tooltip>
<div v-else class="cascader-option">
<span class="cascader-option-label">{{ data.label }}</span>
</div>
</template>
</el-cascader>
<el-input
v-model="batchVal"
@keydown.enter.exact.prevent="batchInput"
@@ -304,8 +269,9 @@ const terminalTabs = ref([]) as any;
let tabIndex = 0;
const commandTree = ref();
const cmdPanelVisible = ref(false);
const activeGroupTab = ref('');
const quickCommandProps = {
expandTrigger: 'hover' as const,
};
let quickCmd = ref();
let batchVal = ref();
let isBatch = ref<boolean>(false);
@@ -360,9 +326,6 @@ const cleanTimer = () => {
const loadHeight = () => {
return globalStore.openMenuTabs ? '250px' : '210px';
};
const loadHeightWithPanel = () => {
return globalStore.openMenuTabs ? '490px' : '450px';
};
const loadEmptyHeight = () => {
return globalStore.openMenuTabs ? '201px' : '156px';
};
@@ -412,9 +375,6 @@ const loadCommandTree = async () => {
item.label = i18n.global.t('commons.table.default');
}
}
if (commandTree.value.length > 0) {
activeGroupTab.value = commandTree.value[0].value;
}
};
const executeCommand = (command: string) => {
@@ -430,6 +390,14 @@ const executeCommand = (command: string) => {
}
};
const handleQuickCommandChange = (val: Array<string>) => {
if (!val?.length) {
return;
}
executeCommand(val[val.length - 1]);
quickCmd.value = '';
};
function batchInput() {
if (batchVal.value === '' || !ctx) {
return;
@@ -649,97 +617,32 @@ onMounted(() => {
transition: height 0.2s ease;
}
.command-tabs {
:deep(.el-tabs__header) {
margin-bottom: 0;
background-color: var(--el-bg-color);
}
:deep(.el-tabs__content) {
height: 180px;
overflow-y: auto;
overflow-x: hidden;
background-color: var(--el-bg-color);
}
:deep(.el-tabs__item) {
min-width: 80px;
max-width: 110px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
padding: 0 8px;
}
}
.group-tab-label {
width: 90px;
display: inline-block;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 14px;
}
.command-tag {
cursor: pointer;
height: auto;
padding: 8px 12px;
transition: all 0.3s;
border-radius: 4px;
white-space: nowrap;
border: 1px solid transparent;
&:hover {
border-color: var(--el-color-primary);
.quick-command-cascader {
:deep(.el-input__wrapper) {
border-radius: 6px;
}
}
.command-tag-name {
font-weight: 500;
font-size: 13px;
flex: 1;
text-align: left;
}
.command-preview-icon {
font-size: 14px;
opacity: 0.6;
transition: opacity 0.3s;
cursor: help;
flex-shrink: 0;
.cascader-option {
width: 100%;
display: flex;
align-items: center;
&:hover {
opacity: 1;
}
min-width: 0;
}
.command-preview {
.command-preview-name {
font-size: 13px;
margin-bottom: 6px;
color: var(--el-text-color-primary);
word-break: break-word;
}
.command-preview-value {
font-size: 12px;
font-family: monospace;
padding: 8px;
background-color: var(--el-fill-color-light);
border-radius: 4px;
color: var(--el-text-color-regular);
word-break: break-all;
white-space: pre-wrap;
}
}
.command-tag-name {
.cascader-option-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: inline-block;
}
:deep(.command-detail-tooltip) {
max-width: 420px;
}
.command-detail-content {
white-space: pre-wrap;
word-break: break-all;
line-height: 1.5;
}
</style>