mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-22 16:00:51 +00:00
feat: improve terminal quick command workflow (#12331)
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1383,6 +1383,8 @@ const message = {
|
||||
quickCommandHelper: '「端末 - >端子」の下部にあるクイックコマンドを使用できます。',
|
||||
groupDeleteHelper:
|
||||
'グループが削除された後、グループ内のすべての接続がデフォルトグループに移行されます。続けたいですか?',
|
||||
quickCommandBatchHelper:
|
||||
'1 行に 1 つのクイックコマンドを、名前|コマンド の形式で入力します。例: ディスク確認|df -h',
|
||||
command: '指示',
|
||||
quickCmd: 'クイックコマンド',
|
||||
addHost: '追加',
|
||||
|
||||
@@ -1357,6 +1357,7 @@ const message = {
|
||||
noSuchCommand: '가져온 CSV 파일에서 빠른 명령어 데이터를 찾을 수 없습니다. 확인 후 다시 시도하세요!',
|
||||
quickCommandHelper: '"터미널 -> 터미널" 하단에서 빠른 명령을 사용할 수 있습니다.',
|
||||
groupDeleteHelper: '그룹을 제거하면 해당 그룹의 모든 연결이 기본 그룹으로 이동됩니다. 계속하시겠습니까?',
|
||||
quickCommandBatchHelper: '한 줄에 하나의 빠른 명령을 이름|명령 형식으로 입력하세요. 예: 디스크 확인|df -h',
|
||||
command: '명령',
|
||||
quickCmd: '빠른 명령',
|
||||
addHost: '추가',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1390,6 +1390,7 @@ const message = {
|
||||
quickCommandHelper: 'Вы можете использовать быстрые команды внизу страницы "Терминалы -> Терминалы".',
|
||||
groupDeleteHelper:
|
||||
'После удаления группы все подключения в группе будут перемещены в группу по умолчанию. Хотите продолжить?',
|
||||
quickCommandBatchHelper: 'Одна быстрая команда на строку в формате Имя|Команда, например: Проверить диск|df -h',
|
||||
command: 'Команда',
|
||||
quickCmd: 'Быстрая команда',
|
||||
addHost: 'Добавить',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1296,6 +1296,7 @@ const message = {
|
||||
noSuchCommand: '匯入的CSV檔案中未能發現快速指令資料,請檢查後重試',
|
||||
quickCommandHelper: '常用命令列表,用於在終端介面底部快速選擇',
|
||||
groupDeleteHelper: '移除組後,組內所有連接將遷移到 default 組內,是否繼續?',
|
||||
quickCommandBatchHelper: '每行一條快速指令,以 名稱|命令 格式,例如:查看磁碟|df -h',
|
||||
command: '指令',
|
||||
addHost: '新增主機',
|
||||
localhost: '本機伺服器',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user