🔨 执行命令.

This commit is contained in:
lijiahangmax
2024-03-16 23:04:10 +08:00
parent 03c334a507
commit 2abf5bcf08
7 changed files with 353 additions and 244 deletions

View File

@@ -4,14 +4,16 @@
`2024-03-` `release` `2024-03-` `release`
🐞 修复 SFTP 加载失败后一直 loading * 🐞 修复 SFTP 加载失败后一直 loading
🐞 修复 SSH 配置未启用还可以连接 * 🐞 修复 SSH 配置未启用还可以连接
🐞 修复 主机配置保存后无法修改状态 * 🐞 修复 主机配置保存后无法修改状态
🐞 修复 添加快捷命令时编辑器无代码提示 * 🐞 修复 添加快捷命令时编辑器无代码提示
🔨 修改 菜单路由命名逻辑修改 * 🔨 修改 菜单路由命名逻辑修改
🔨 优化 前端组件命名规范化 * 🔨 优化 前端组件命名规范化
🌈 新增 双击终端会话 Tab 快速复制 * 🌈 新增 双击终端会话 Tab 快速复制
🌈 新增 执行模板功能 * 🌈 新增 批量执行命令
* 🌈 新增 命令执行日志
* 🌈 新增 执行模板功能
[如何升级](/about/update.md?id=_v102) [如何升级](/about/update.md?id=_v102)
@@ -19,14 +21,14 @@
`2024-03-06` `release` `2024-03-06` `release`
🐞 修复 用户操作日志条件重置后类型框数据不正常的问题 * 🐞 修复 用户操作日志条件重置后类型框数据不正常的问题
🩰 修改 主机连接日志 UI * 🩰 修改 主机连接日志 UI
🌈 新增 SFTP 使用日志列表 * 🌈 新增 SFTP 使用日志列表
🌈 新增 主机连接日志强制下线会话 * 🌈 新增 主机连接日志强制下线会话
🌈 新增 主机连接日志删除/清理 * 🌈 新增 主机连接日志删除/清理
🌈 新增 用户操作日志日志删除/清理 * 🌈 新增 用户操作日志日志删除/清理
🌈 新增 用户操作日志日志删除/清理 * 🌈 新增 用户操作日志日志删除/清理
🔨 优化 用户锁定次数/时间可配置 * 🔨 优化 用户锁定次数/时间可配置
[如何升级](/about/update.md?id=_v101) [如何升级](/about/update.md?id=_v101)
@@ -34,8 +36,8 @@
`2024-03-01` `release` `2024-03-01` `release`
🌈 新增 用户自定义终端标签颜色 * 🌈 新增 用户自定义终端标签颜色
🔨 拓展数据模块添加缓存 * 🔨 拓展数据模块添加缓存
[如何升级](/about/update.md?id=_v100) [如何升级](/about/update.md?id=_v100)
@@ -43,15 +45,15 @@
`2024-02-28` `preview` `2024-02-28` `preview`
🌈 主机管理 * 🌈 主机管理
🌈 主机秘钥 * 🌈 主机秘钥
🌈 主机身份 * 🌈 主机身份
🌈 资产授权 * 🌈 资产授权
🌈 主机终端 * 🌈 主机终端
🌈 连接日志 * 🌈 连接日志
🌈 角色管理 * 🌈 角色管理
🌈 用户管理 * 🌈 用户管理
🌈 操作日志 * 🌈 操作日志
🌈 系统菜单 * 🌈 系统菜单
🌈 数据字典项 * 🌈 数据字典项
🌈 数据字典值 * 🌈 数据字典值

View File

@@ -24,6 +24,7 @@ export interface ExecLogQueryResponse extends TableData, ExecLogQueryExtraRespon
username: string; username: string;
description: string; description: string;
command: string; command: string;
parameterSchema: string;
timeout: number; timeout: number;
status: string; status: string;
startTime: number; startTime: number;

View File

@@ -1,5 +1,4 @@
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'; import type { TableColumnData } from '@arco-design/web-vue/es/table/interface';
import { dateFormat } from '@/utils';
const columns = [ const columns = [
{ {
@@ -14,7 +13,7 @@ const columns = [
dataIndex: 'name', dataIndex: 'name',
slotName: 'name', slotName: 'name',
align: 'left', align: 'left',
width: 200, width: 250,
ellipsis: true, ellipsis: true,
}, { }, {
title: '模板命令', title: '模板命令',
@@ -22,15 +21,6 @@ const columns = [
slotName: 'command', slotName: 'command',
align: 'left', align: 'left',
ellipsis: true, ellipsis: true,
}, {
title: '修改时间',
dataIndex: 'updateTime',
slotName: 'updateTime',
align: 'center',
width: 180,
render: ({ record }) => {
return dateFormat(new Date(record.updateTime));
},
}, { }, {
title: '操作', title: '操作',
slotName: 'handle', slotName: 'handle',

View File

@@ -0,0 +1,60 @@
<template>
<div class="container">
<!-- 表头 -->
<div class="exec-header">
<h3>执行命令</h3>
<span class="span-blue usn pointer" @click="openTemplate">从模板中选择</span>
</div>
<!-- 命令编辑器 -->
<div class="editor-wrapper">
<slot />
</div>
<!-- 命名提示信息 -->
<div v-pre class="editor-help">
使用 @{{ xxx }} 来替换参数, 输入_可以获取全部变量
</div>
<!-- 命令模板模态框 -->
<exec-template-modal ref="templateModal"
@selected="s => emits('selected', s)" />
</div>
</template>
<script lang="ts">
export default {
name: 'execPanelEditor'
};
</script>
<script lang="ts" setup>
import { ref } from 'vue';
import ExecTemplateModal from '@/components/exec/template/modal/index.vue';
const emits = defineEmits(['selected']);
const templateModal = ref<any>();
// 打开模板
const openTemplate = () => {
templateModal.value.open();
};
</script>
<style lang="less" scoped>
.editor-wrapper {
width: 100%;
height: calc(100% - 66px);
position: relative;
}
.editor-help {
user-select: none;
display: flex;
margin-top: 8px;
height: 18px;
color: var(--color-text-3);
overflow: hidden;
white-space: nowrap;
}
</style>

View File

@@ -0,0 +1,41 @@
<template>
<div class="exec-form-container">
<!-- 表头 -->
<div class="exec-header">
<h3>执行参数</h3>
<!-- 操作 -->
<a-button-group size="small">
<a-button @click="emits('reset')">重置</a-button>
<a-button type="primary" @click="emits('exec')">执行</a-button>
</a-button-group>
</div>
<!-- 命令表单 -->
<slot name="form" />
<a-divider v-if="schemaCount"
orientation="center"
style="margin: 12px 0 26px 0;">
命令参数
</a-divider>
<!-- 参数表单 -->
<slot v-if="schemaCount" name="params" />
</div>
</template>
<script lang="ts">
export default {
name: 'execPanelForm'
};
</script>
<script lang="ts" setup>
const emits = defineEmits(['reset', 'exec']);
defineProps<{
schemaCount: number
}>();
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,104 @@
<template>
<div class="container">
<!-- 表头 -->
<div class="exec-header">
<h3>执行历史</h3>
</div>
<div v-if="!historyLogs.length" class="flex-center mt16">
<a-empty description="无执行记录" />
</div>
<!-- 执行记录 -->
<div v-else class="exec-history-rows">
<div v-for="record in historyLogs"
:key="record.id"
class="exec-history"
@click="emits('selected', record)">
<!-- 机器数量 -->
<span class="exec-history-count">
{{ record.hostIdList?.length || 0 }}
</span>
<!-- 执行描述 -->
<span class="exec-history-desc">
{{ record.description }}
</span>
</div>
</div>
</div>
</template>
<script lang="ts">
export default {
name: 'execPanelHistory'
};
</script>
<script lang="ts" setup>
import type { ExecLogQueryResponse } from '@/api/exec/exec-log';
import { onMounted, ref } from 'vue';
import { getExecLogHistory } from '@/api/exec/exec-log';
import { historyCount } from '../types/const';
const emits = defineEmits(['selected']);
const historyLogs = ref<Array<ExecLogQueryResponse>>([]);
// 加载执行记录
const fetchExecHistory = async () => {
const { data } = await getExecLogHistory(historyCount);
historyLogs.value = data;
};
// 加载执行记录
onMounted(fetchExecHistory);
</script>
<style lang="less" scoped>
.exec-history-rows {
position: absolute;
width: calc(100% - 32px);
height: calc(100% - 60px);
overflow: auto;
&::-webkit-scrollbar-track {
display: none;
}
}
.exec-history {
padding: 8px;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
background: var(--color-fill-2);
transition: all .2s;
user-select: none;
cursor: pointer;
&:hover {
background: var(--color-fill-3);
}
&-count {
width: 24px;
height: 24px;
border-radius: 2px;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-bg-2);
background: rgb(var(--arcoblue-6));
}
&-desc {
color: var(--color-text-2);
width: calc(100% - 36px);
overflow: hidden;
white-space: nowrap;
text-align: end;
text-overflow: ellipsis;
}
}
</style>

View File

@@ -2,125 +2,85 @@
<!-- 命令执行 --> <!-- 命令执行 -->
<a-spin class="exec-container" :loading="loading"> <a-spin class="exec-container" :loading="loading">
<!-- 执行参数 --> <!-- 执行参数 -->
<div class="exec-form-container"> <exec-panel-form class="exec-form-container"
<!-- 表头 --> :schema-count="parameterSchema.length"
<div class="exec-form-header"> @exec="execCommand"
<h3>执行参数</h3> @reset="resetForm">
<!-- 操作 -->
<a-button-group size="small">
<a-button @click="reset">重置</a-button>
<a-button type="primary" @click="exec">执行</a-button>
</a-button-group>
</div>
<!-- 命令表单 --> <!-- 命令表单 -->
<a-form :model="formModel" <template #form>
ref="formRef" <a-form :model="formModel"
label-align="right" ref="formRef"
:rules="formRules"> label-align="right"
<!-- 执行主机 --> :rules="formRules">
<a-form-item field="hostIdList" <!-- 执行主机 -->
label="执行主机" <a-form-item field="hostIdList"
label-col-flex="72px"> label="执行主机"
<div class="selected-host"> label-col-flex="72px">
<!-- 已选择数量 --> <div class="selected-host">
<span class="usn" v-if="formModel.hostIdList?.length"> <!-- 已选择数量 -->
已选择<span class="selected-host-count span-blue">{{ formModel.hostIdList?.length }}</span>台主机 <span class="usn" v-if="formModel.hostIdList?.length">
</span> 已选择<span class="selected-host-count span-blue">{{ formModel.hostIdList?.length }}</span>台主机
<span class="usn pointer span-blue" @click="openSelectHost"> </span>
{{ formModel.hostIdList?.length ? '重新选择' : '选择主机' }} <span class="usn pointer span-blue" @click="openSelectHost">
</span> {{ formModel.hostIdList?.length ? '重新选择' : '选择主机' }}
</div> </span>
</a-form-item> </div>
<!-- 执行描述 --> </a-form-item>
<a-form-item field="description"
label="执行描述"
label-col-flex="72px">
<a-input v-model="formModel.description"
placeholder="请输入执行描述"
allow-clear />
</a-form-item>
<!-- 超时时间 -->
<a-form-item field="timeout"
label="超时时间"
label-col-flex="72px">
<a-input-number v-model="formModel.timeout"
placeholder="为0则不超时"
:min="0"
:max="100000"
hide-button>
<template #suffix>
</template>
</a-input-number>
</a-form-item>
</a-form>
<!-- 命令参数 -->
<a-divider v-if="parameterSchema.length"
orientation="center"
style="margin: 12px 0 26px 0;">
命令参数
</a-divider>
<!-- 参数表单 -->
<a-form v-if="parameterSchema.length"
:model="parameterFormModel"
ref="parameterFormRef"
label-align="right">
<a-form-item v-for="item in parameterSchema"
:key="item.name"
:field="item.name as string"
:label="item.name"
label-col-flex="72px"
required>
<a-input v-model="parameterFormModel[item.name as string]"
:placeholder="item.desc"
allow-clear />
</a-form-item>
</a-form>
</div>
<!-- 执行命令 -->
<div class="exec-command-container">
<!-- 表头 -->
<div class="exec-form-header">
<h3>执行命令</h3>
<span class="span-blue usn pointer" @click="openTemplate">从模板中选择</span>
</div>
<!-- 命令编辑器 -->
<div class="command-editor-wrapper">
<exec-editor v-model="formModel.command"
theme="vs-dark"
:parameter="parameterSchema" />
</div>
<!-- 命名提示信息 -->
<div v-pre class="command-editor-help">
使用 @{{ xxx }} 来替换参数, 输入_可以获取全部变量
</div>
</div>
<!-- 执行历史 -->
<div class="exec-history-container">
<div v-if="!historyLogs.length" class="flex-center mt16">
<a-empty description="无执行记录" />
</div>
<div v-else class="exec-history-rows">
<div v-for="record in historyLogs"
:key="record.id"
class="exec-history">
<!-- 机器数量 -->
<span class="exec-history-count">
{{ record.hostIdList?.length || 0 }}
</span>
<!-- 执行描述 --> <!-- 执行描述 -->
<span class="exec-history-desc"> <a-form-item field="description"
{{ record.description }} label="执行描述"
</span> label-col-flex="72px">
</div> <a-input v-model="formModel.description"
</div> placeholder="请输入执行描述"
</div> allow-clear />
</a-form-item>
<!-- 超时时间 -->
<a-form-item field="timeout"
label="超时时间"
label-col-flex="72px">
<a-input-number v-model="formModel.timeout"
placeholder="为0则不超时"
:min="0"
:max="100000"
hide-button>
<template #suffix>
</template>
</a-input-number>
</a-form-item>
</a-form>
</template>
<!-- 参数表单 -->
<template #params>
<a-form :model="parameterFormModel"
ref="parameterFormRef"
label-align="right">
<a-form-item v-for="item in parameterSchema"
:key="item.name"
:field="item.name as string"
:label="item.name"
label-col-flex="72px"
required>
<a-input v-model="parameterFormModel[item.name as string]"
:placeholder="item.desc"
allow-clear />
</a-form-item>
</a-form>
</template>
</exec-panel-form>
<!-- 执行命令 -->
<exec-panel-editor class="exec-command-container"
@selected="setWithTemplate">
<exec-editor v-model="formModel.command"
theme="vs-dark"
:parameter="parameterSchema" />
</exec-panel-editor>
<!-- 执行历史 -->
<exec-panel-history class="exec-history-container"
@selected="setWithExecLog" />
<!-- 主机模态框 --> <!-- 主机模态框 -->
<authorized-host-modal ref="hostModal" <authorized-host-modal ref="hostModal"
@selected="setSelectedHost" /> @selected="setSelectedHost" />
<!-- 命令模板模态框 -->
<exec-template-modal ref="templateModal"
@selected="setWithTemplate" />
</a-spin> </a-spin>
</template> </template>
@@ -135,50 +95,38 @@
import type { TemplateParam } from '@/components/view/exec-editor/const'; import type { TemplateParam } from '@/components/view/exec-editor/const';
import type { ExecTemplateQueryResponse } from '@/api/exec/exec-template'; import type { ExecTemplateQueryResponse } from '@/api/exec/exec-template';
import type { ExecLogQueryResponse } from '@/api/exec/exec-log'; import type { ExecLogQueryResponse } from '@/api/exec/exec-log';
import { onMounted, ref } from 'vue'; import { ref } from 'vue';
import formRules from '../types/form.rules'; import formRules from '../types/form.rules';
import useLoading from '@/hooks/loading'; import useLoading from '@/hooks/loading';
import { batchExecCommand } from '@/api/exec/exec'; import { batchExecCommand } from '@/api/exec/exec';
import { historyCount } from '../types/const';
import { getExecLogHistory } from '@/api/exec/exec-log';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import ExecEditor from '@/components/view/exec-editor/index.vue'; import ExecEditor from '@/components/view/exec-editor/index.vue';
import AuthorizedHostModal from '@/components/asset/host/authorized-host-modal/index.vue'; import AuthorizedHostModal from '@/components/asset/host/authorized-host-modal/index.vue';
import ExecTemplateModal from '@/components/exec/template/modal/index.vue'; import ExecPanelForm from './exec-panel-form.vue';
import ExecPanelHistory from './exec-panel-history.vue';
import ExecPanelEditor from './exec-panel-editor.vue';
const defaultForm = (): ExecCommandRequest => { const defaultForm = (): ExecCommandRequest => {
return { return {
timeout: 0 timeout: 0,
command: ''
}; };
}; };
const { loading, setLoading } = useLoading(); const { loading, setLoading } = useLoading();
const hostModal = ref<any>(); const hostModal = ref<any>();
const templateModal = ref<any>();
const formRef = ref<any>(); const formRef = ref<any>();
const parameterFormRef = ref<any>(); const parameterFormRef = ref<any>();
const formModel = ref<ExecCommandRequest>({ ...defaultForm() }); const formModel = ref<ExecCommandRequest>({ ...defaultForm() });
const parameterFormModel = ref<Record<string, any>>({}); const parameterFormModel = ref<Record<string, any>>({});
const parameterSchema = ref<Array<TemplateParam>>([]); const parameterSchema = ref<Array<TemplateParam>>([]);
const historyLogs = ref<Array<ExecLogQueryResponse>>([]);
// 加载执行记录
const fetchExecHistory = async () => {
const { data } = await getExecLogHistory(historyCount);
historyLogs.value = data;
};
// 打开选择主机 // 打开选择主机
const openSelectHost = () => { const openSelectHost = () => {
hostModal.value.open(formModel.value.hostIdList); hostModal.value.open(formModel.value.hostIdList);
}; };
// 打开模板
const openTemplate = () => {
templateModal.value.open();
};
// 设置选中主机 // 设置选中主机
const setSelectedHost = (hosts: Array<number>) => { const setSelectedHost = (hosts: Array<number>) => {
formModel.value.hostIdList = hosts; formModel.value.hostIdList = hosts;
@@ -197,12 +145,27 @@
}; };
// 从执行日志设置 // 从执行日志设置
const setWithExecLog = () => { const setWithExecLog = (record: ExecLogQueryResponse) => {
// TODO formModel.value = {
...formModel.value,
command: record.command,
description: record.description,
timeout: record.timeout,
hostIdList: record.hostIdList
};
parameterSchema.value = record.parameterSchema ? JSON.parse(record.parameterSchema) : [];
if (parameterSchema.value.length) {
parameterFormModel.value = parameterSchema.value.reduce((acc, cur) => ({
...acc,
[cur.name as string]: cur.value
}), {});
} else {
parameterFormModel.value = {};
}
}; };
// 执行 // 执行
const exec = async () => { const execCommand = async () => {
setLoading(true); setLoading(true);
try { try {
// 验证参数 // 验证参数
@@ -236,16 +199,12 @@
}; };
// 重置 // 重置
const reset = () => { const resetForm = () => {
formModel.value = Object.assign({}, { ...defaultForm() }); formModel.value = Object.assign({}, { ...defaultForm() });
parameterFormModel.value = {}; parameterFormModel.value = {};
parameterSchema.value = []; parameterSchema.value = [];
}; };
// 加载执行记录
onMounted(fetchExecHistory);
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@@ -278,17 +237,23 @@
padding: 16px; padding: 16px;
position: relative; position: relative;
} }
}
.exec-form-header { :deep(.exec-header) {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: flex-start; align-items: flex-start;
height: 28px; height: 28px;
margin-bottom: 12px; margin-bottom: 4px;
h3 { h3, > span {
margin: 0; margin: 0;
} overflow: hidden;
white-space: nowrap;
}
h3 {
color: var(--color-text-1);
} }
} }
@@ -308,58 +273,4 @@
} }
} }
.exec-command-container {
background: red;
.command-editor-wrapper {
width: 100%;
height: calc(100% - 66px);
position: relative;
}
.command-editor-help {
user-select: none;
display: flex;
margin-top: 8px;
height: 18px;
color: var(--color-text-3);
}
}
.exec-history-container {
.exec-history-rows {
overflow-y: auto;
}
.exec-history {
padding: 6px 8px;
display: flex;
justify-content: space-between;
margin-bottom: 8px;
background: var(--color-fill-2);
transition: all .2s;
user-select: none;
&:hover {
background: var(--color-fill-3);
}
&-count {
width: 24px;
height: 24px;
border-radius: 2px;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-bg-2);
background: rgb(var(--arcoblue-6));
}
&-desc {
}
}
}
</style> </style>