🔖 项目重命名.
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- 表头 -->
|
||||
<div class="panel-header">
|
||||
<h3>文件列表</h3>
|
||||
<!-- 操作 -->
|
||||
<a-button-group size="mini" :disabled="startStatus">
|
||||
<a-button @click="clear">清空</a-button>
|
||||
<!-- 选择文件 -->
|
||||
<a-upload v-model:file-list="files"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:multiple="true">
|
||||
<template #upload-button>
|
||||
<a-button type="primary">选择文件</a-button>
|
||||
</template>
|
||||
</a-upload>
|
||||
<!-- 选择文件夹 -->
|
||||
<a-upload v-model:file-list="files"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:directory="true">
|
||||
<template #upload-button>
|
||||
<a-button type="primary">选择文件夹</a-button>
|
||||
</template>
|
||||
</a-upload>
|
||||
</a-button-group>
|
||||
</div>
|
||||
<!-- 文件列表 -->
|
||||
<div v-if="files.length" class="files-container">
|
||||
<a-scrollbar style="overflow-y: auto; height: 100%;">
|
||||
<a-upload class="files-wrapper"
|
||||
:class="[ startStatus ? 'uploading-files-wrapper' : 'waiting-files-wrapper' ]"
|
||||
v-model:file-list="files"
|
||||
:auto-upload="false"
|
||||
:show-cancel-button="false"
|
||||
:show-retry-button="false"
|
||||
:show-remove-button="!startStatus"
|
||||
:show-file-list="true">
|
||||
<template #upload-button />
|
||||
<template #file-name="{ fileItem }">
|
||||
<div class="file-name-wrapper">
|
||||
<!-- 文件名称 -->
|
||||
<span class="file-name text-ellipsis"
|
||||
:title="fileItem.file.webkitRelativePath || fileItem.file.name">
|
||||
{{ fileItem.file.webkitRelativePath || fileItem.file.name }}
|
||||
</span>
|
||||
<!-- 文件大小 -->
|
||||
<span class="file-size span-blue">
|
||||
{{ getFileSize(fileItem.file.size) }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</a-upload>
|
||||
</a-scrollbar>
|
||||
</div>
|
||||
<!-- 未选择文件 -->
|
||||
<a-result v-else
|
||||
class="usn"
|
||||
status="404"
|
||||
subtitle="请先点击上方按钮选择文件" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'batchUploadFiles'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FileItem } from '@arco-design/web-vue';
|
||||
import type { IFileUploader } from '@/components/system/uploader/const';
|
||||
import { computed, onUnmounted, ref } from 'vue';
|
||||
import { getFileSize } from '@/utils/file';
|
||||
import FileUploader from '@/components/system/uploader/file-uploader';
|
||||
|
||||
const emits = defineEmits(['update:fileList', 'end', 'error', 'clearFile']);
|
||||
const props = defineProps<{
|
||||
fileList: Array<FileItem>;
|
||||
}>();
|
||||
|
||||
const startStatus = ref(false);
|
||||
const uploader = ref<IFileUploader>();
|
||||
const files = computed<Array<FileItem>>({
|
||||
get() {
|
||||
return props.fileList;
|
||||
},
|
||||
set(e) {
|
||||
emits('update:fileList', e);
|
||||
}
|
||||
});
|
||||
|
||||
// 开始上传
|
||||
const startUpload = async (token: string) => {
|
||||
// 修改状态
|
||||
startStatus.value = true;
|
||||
props.fileList.forEach(s => s.status = 'uploading');
|
||||
// 开始上传
|
||||
try {
|
||||
uploader.value = new FileUploader(token, props.fileList);
|
||||
uploader.value?.setHook(() => {
|
||||
emits('end');
|
||||
});
|
||||
await uploader.value?.start();
|
||||
} catch (e) {
|
||||
emits('error');
|
||||
}
|
||||
};
|
||||
|
||||
// 清空
|
||||
const clear = () => {
|
||||
startStatus.value = false;
|
||||
emits('clearFile');
|
||||
};
|
||||
|
||||
// 关闭
|
||||
const close = () => {
|
||||
startStatus.value = false;
|
||||
uploader.value?.close();
|
||||
};
|
||||
|
||||
defineExpose({ startUpload, close });
|
||||
|
||||
// 卸载时关闭
|
||||
onUnmounted(() => {
|
||||
uploader.value?.close();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@file-size-width: 82px;
|
||||
|
||||
.files-container {
|
||||
width: 100%;
|
||||
height: calc(100% - 36px);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:deep(.waiting-files-wrapper) {
|
||||
.arco-upload-list {
|
||||
padding: 0 12px 0 0 !important;
|
||||
}
|
||||
|
||||
.arco-upload-list-item-name {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.arco-upload-list-item .arco-upload-progress {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.uploading-files-wrapper) {
|
||||
.arco-upload-list {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.arco-upload-list-item-name {
|
||||
margin-right: 10px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.files-wrapper {
|
||||
:deep(.arco-upload-wrapper) {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.arco-upload) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.arco-upload-list) {
|
||||
padding: 0;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
:deep(.arco-upload-list-item-error) {
|
||||
.arco-upload-list-item-name {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.arco-upload-progress {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.arco-upload-list-item-name-text) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.arco-upload-list-item:first-of-type) {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.file-name-wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.file-name {
|
||||
width: calc(100% - @file-size-width);
|
||||
padding: 2px 0;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.file-size {
|
||||
width: @file-size-width;
|
||||
display: inline-flex;
|
||||
font-size: 13px;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.arco-scrollbar) {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- 表头 -->
|
||||
<div class="panel-header">
|
||||
<h3>批量上传</h3>
|
||||
<!-- 操作 -->
|
||||
<a-button-group size="mini">
|
||||
<!-- 重置 -->
|
||||
<a-button v-if="status.value === UploadTaskStepStatus.WAITING.value"
|
||||
@click="emits('clear')">
|
||||
重置
|
||||
</a-button>
|
||||
<!-- 取消上传 -->
|
||||
<a-button v-if="status.value === UploadTaskStepStatus.REQUESTING.value"
|
||||
type="primary"
|
||||
status="warning"
|
||||
@click="emits('abort')">
|
||||
取消上传
|
||||
</a-button>
|
||||
<!-- 开始上传 -->
|
||||
<a-button v-if="status.value === UploadTaskStepStatus.WAITING.value"
|
||||
type="primary"
|
||||
@click="submit">
|
||||
开始上传
|
||||
</a-button>
|
||||
</a-button-group>
|
||||
</div>
|
||||
<!-- 表单 -->
|
||||
<a-form :model="formModel"
|
||||
ref="formRef"
|
||||
class="form-wrapper"
|
||||
label-align="right"
|
||||
:auto-label-width="true"
|
||||
:rules="formRules">
|
||||
<!-- 上传描述 -->
|
||||
<a-form-item field="description" label="上传描述">
|
||||
<a-input v-model="formModel.description"
|
||||
placeholder="请输入上传描述"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 上传路径 -->
|
||||
<a-form-item field="remotePath"
|
||||
style="margin-bottom: 4px;"
|
||||
label="上传路径"
|
||||
help="${username} 用户名 ${home} 用户家目录">
|
||||
<a-input v-model="formModel.remotePath"
|
||||
placeholder="请输入上传路径"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 上传主机 -->
|
||||
<a-form-item field="hostIdList" label="上传主机">
|
||||
<div class="selected-host">
|
||||
<!-- 已选择数量 -->
|
||||
<span class="usn" v-if="formModel.hostIdList?.length">
|
||||
已选择<span class="selected-host-count span-blue">{{ formModel.hostIdList?.length }}</span>台主机
|
||||
</span>
|
||||
<span class="usn pointer span-blue" @click="emits('openHost')">
|
||||
{{ formModel.hostIdList?.length ? '重新选择' : '选择主机' }}
|
||||
</span>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'batchUploadForm'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { UploadTaskCreateRequest } from '@/api/exec/upload-task';
|
||||
import type { UploadTaskStatusType } from '../types/const';
|
||||
import { ref } from 'vue';
|
||||
import formRules from '../types/form.rules';
|
||||
import { UploadTaskStepStatus } from '../types/const';
|
||||
|
||||
const emits = defineEmits(['upload', 'openHost', 'abort', 'clear']);
|
||||
const props = defineProps<{
|
||||
status: UploadTaskStatusType;
|
||||
formModel: UploadTaskCreateRequest;
|
||||
}>();
|
||||
|
||||
const formRef = ref<any>();
|
||||
|
||||
// 提交表单
|
||||
const submit = async () => {
|
||||
// 验证参数
|
||||
let error = await formRef.value.validate();
|
||||
if (error) {
|
||||
return false;
|
||||
}
|
||||
emits('upload');
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.selected-host {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: var(--color-text-2);
|
||||
background: var(--color-fill-2);
|
||||
transition: all 0.3s;
|
||||
|
||||
&-count {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
margin: 0 6px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--color-fill-3);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- 表头 -->
|
||||
<div class="panel-header">
|
||||
<h3>上传主机</h3>
|
||||
<!-- 操作 -->
|
||||
<a-button-group size="mini">
|
||||
<!-- 返回 -->
|
||||
<a-button @click="emits('back')">返回</a-button>
|
||||
<!-- 取消上传 -->
|
||||
<a-button v-if="status.value === UploadTaskStepStatus.UPLOADING.value"
|
||||
type="primary"
|
||||
status="warning"
|
||||
@click="emits('cancel')">
|
||||
取消上传
|
||||
</a-button>
|
||||
</a-button-group>
|
||||
</div>
|
||||
<!-- 主机列表 -->
|
||||
<div class="wrapper">
|
||||
<a-scrollbar style="overflow-y: auto; height: 100%;">
|
||||
<!-- 主机 -->
|
||||
<div v-for="host in task.hosts"
|
||||
class="host-item"
|
||||
:class="[ selectedHost === host.id ? 'host-item-active' : '']"
|
||||
@click="changeSelectedHost(host.id)">
|
||||
<!-- 主机信息 -->
|
||||
<div class="host-item-host">
|
||||
<!-- 主机名称 -->
|
||||
<div class="host-item-name text-ellipsis" :title="host.name">
|
||||
{{ host.name }}
|
||||
</div>
|
||||
<!-- 主机地址 -->
|
||||
<div class="host-item-address text-ellipsis" :title="host.address">
|
||||
{{ host.address }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- 主机状态 -->
|
||||
<a-space class="host-item-status"
|
||||
direction="vertical"
|
||||
size="mini">
|
||||
<!-- 传输中 -->
|
||||
<span class="host-item-status-text span-green" title="传输中">
|
||||
{{ getStatusCount(host.files, [UploadTaskFileStatus.WAITING, UploadTaskFileStatus.UPLOADING]) }}
|
||||
</span>
|
||||
<!-- 已完成 -->
|
||||
<span class="host-item-status-text span-blue" title="已完成">
|
||||
{{ getStatusCount(host.files, [UploadTaskFileStatus.FINISHED, UploadTaskFileStatus.CANCELED]) }}
|
||||
</span>
|
||||
<!-- 已失败 -->
|
||||
<span class="host-item-status-text span-red" title="已失败">
|
||||
{{ getStatusCount(host.files, [UploadTaskFileStatus.FAILED]) }}
|
||||
</span>
|
||||
</a-space>
|
||||
</div>
|
||||
</a-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'batchUploadHosts'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { UploadTaskQueryResponse } from '@/api/exec/upload-task';
|
||||
import type { UploadTaskFile } from '@/api/exec/upload-task';
|
||||
import type { UploadTaskStatusType } from '../types/const';
|
||||
import { UploadTaskStepStatus } from '../types/const';
|
||||
import { UploadTaskFileStatus } from '@/views/exec/upload-task/types/const';
|
||||
|
||||
const emits = defineEmits(['update:selectedHost', 'back', 'cancel']);
|
||||
const props = defineProps<{
|
||||
status: UploadTaskStatusType;
|
||||
selectedHost: number;
|
||||
task: UploadTaskQueryResponse;
|
||||
}>();
|
||||
|
||||
// 修改选中的主机
|
||||
const changeSelectedHost = (id: number) => {
|
||||
emits('update:selectedHost', id);
|
||||
};
|
||||
|
||||
// 获取已完成数量
|
||||
const getStatusCount = (files: Array<UploadTaskFile>, status: Array<string>) => {
|
||||
return files.filter(s => status.includes(s.status)).length;
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
height: calc(100% - 36px);
|
||||
position: relative;
|
||||
|
||||
.host-item {
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
background: var(--color-fill-1);
|
||||
cursor: pointer;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--color-fill-2);
|
||||
}
|
||||
|
||||
&-host {
|
||||
width: calc(100% - 48px);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&-status {
|
||||
user-select: none;
|
||||
|
||||
&-text {
|
||||
width: 100%;
|
||||
max-width: 48px;
|
||||
display: inline-flex;
|
||||
justify-content: flex-end;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
&-name {
|
||||
width: 100%;
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
&-address {
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.host-item-active {
|
||||
background: var(--color-fill-2) !important;
|
||||
|
||||
&::after {
|
||||
width: 3px;
|
||||
height: 100%;
|
||||
border-radius: 4px 6px 6px 4px;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 1px;
|
||||
background: rgb(var(--arcoblue-6));
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.arco-scrollbar) {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- 表头 -->
|
||||
<div class="panel-header">
|
||||
<h3>传输列表</h3>
|
||||
</div>
|
||||
<div class="wrapper">
|
||||
<a-scrollbar style="overflow-y: auto; height: 100%;">
|
||||
<!-- 主机 -->
|
||||
<div v-for="file in files" class="file-item">
|
||||
<!-- 图标 -->
|
||||
<div class="file-item-icon span-blue">
|
||||
<icon-file />
|
||||
</div>
|
||||
<!-- 文件路径 -->
|
||||
<div class="file-item-path text-ellipsis" :title="file.filePath">
|
||||
{{ file.filePath }}
|
||||
</div>
|
||||
<!-- 状态 -->
|
||||
<div class="file-item-status">
|
||||
<!-- 文件大小 -->
|
||||
<div class="file-item-size span-blue">
|
||||
<!-- 当前大小 -->
|
||||
<template v-if="file.status === UploadTaskFileStatus.WAITING || file.status === UploadTaskFileStatus.UPLOADING">
|
||||
{{ getFileSize(file.current || 0) }}
|
||||
</template>
|
||||
<!-- 总大小 -->
|
||||
<template v-else>
|
||||
{{ getFileSize(file.fileSize) }}
|
||||
</template>
|
||||
</div>
|
||||
<!-- 进度 -->
|
||||
<a-tooltip position="left"
|
||||
:content="((file.current || 0) / file.fileSize * 100).toFixed(2) + '%'"
|
||||
mini>
|
||||
<a-progress type="circle"
|
||||
size="mini"
|
||||
:status="getDictValue(fileStatusKey, file.status, 'status') as any"
|
||||
:percent="(file.current || 0) / file.fileSize" />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</a-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'batchUploadProgress'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { UploadTaskFile } from '@/api/exec/upload-task';
|
||||
import { fileStatusKey } from '../types/const';
|
||||
import { UploadTaskFileStatus } from '@/views/exec/upload-task/types/const';
|
||||
import { useDictStore } from '@/store';
|
||||
import { getFileSize } from '@/utils/file';
|
||||
|
||||
const emits = defineEmits(['update:selectedHost']);
|
||||
const props = defineProps<{
|
||||
files: Array<UploadTaskFile>;
|
||||
}>();
|
||||
|
||||
const { getDictValue } = useDictStore();
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@icon-width: 24px;
|
||||
@status-width: 102px;
|
||||
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
height: calc(100% - 36px);
|
||||
position: relative;
|
||||
|
||||
.file-item {
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
background: var(--color-fill-1);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--color-fill-2);
|
||||
}
|
||||
|
||||
&-icon {
|
||||
width: @icon-width;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
&-path {
|
||||
padding: 2px 0;
|
||||
width: calc(100% - @icon-width - @status-width);
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
&-size {
|
||||
font-size: 12px;
|
||||
margin-right: 12px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&-status {
|
||||
width: @status-width;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.arco-scrollbar) {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<a-steps :current="status.step"
|
||||
:status="status.status as any"
|
||||
direction="vertical">
|
||||
<!-- 创建任务 -->
|
||||
<a-step description="创建上传任务">创建任务</a-step>
|
||||
<!-- 上传文件 -->
|
||||
<a-step>
|
||||
上传文件
|
||||
<template #icon v-if="status.step === 2">
|
||||
<icon-loading />
|
||||
</template>
|
||||
<template #description>
|
||||
<span>将文件上传到临时分区</span><br>
|
||||
<span v-if="status.step === 2" class="span-red desc-tips">
|
||||
在此期间请不要关闭页面
|
||||
</span>
|
||||
</template>
|
||||
</a-step>
|
||||
<!-- 分发文件 -->
|
||||
<a-step>
|
||||
分发文件
|
||||
<template #icon v-if="status.step === 3">
|
||||
<icon-loading />
|
||||
</template>
|
||||
<template #description>
|
||||
<span>将文件分发到目标服务器</span><br>
|
||||
<span v-if="status.step === 3" class="span-blue desc-tips">
|
||||
在此期间可以关闭页面
|
||||
</span>
|
||||
</template>
|
||||
</a-step>
|
||||
<!-- 上传完成 -->
|
||||
<a-step description="释放资源">上传完成</a-step>
|
||||
</a-steps>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'batchUploadStep'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { UploadTaskStatusType } from '../types/const';
|
||||
|
||||
defineProps<{
|
||||
status: UploadTaskStatusType;
|
||||
}>();
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.container {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.desc-tips {
|
||||
display: inline-block;
|
||||
margin-top: 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,331 @@
|
||||
<template>
|
||||
<a-spin class="panel-container full" :loading="loading">
|
||||
<!-- 上传步骤 -->
|
||||
<batch-upload-step class="panel-item first-panel-container"
|
||||
:status="taskStatus" />
|
||||
<!-- 上传表单 -->
|
||||
<batch-upload-form v-if="taskStatus.formPanel"
|
||||
class="panel-item center-panel-container"
|
||||
:form-model="formModel"
|
||||
:status="taskStatus"
|
||||
@upload="doCreateUploadTask"
|
||||
@abort="abortUploadRequest"
|
||||
@open-host="openHostModal"
|
||||
@clear="clearForm" />
|
||||
<!-- 上传主机 -->
|
||||
<batch-upload-hosts v-else
|
||||
class="panel-item center-panel-container"
|
||||
v-model:selected-host="selectedHost"
|
||||
:status="taskStatus"
|
||||
:task="task"
|
||||
@back="backFormPanel"
|
||||
@cancel="doCancelUploadTask" />
|
||||
<!-- 文件列表 -->
|
||||
<batch-upload-files v-if="taskStatus.formPanel"
|
||||
v-model:file-list="fileList"
|
||||
class="panel-item last-panel-container"
|
||||
ref="filesRef"
|
||||
@end="uploadRequestEnd"
|
||||
@error="uploadRequestError"
|
||||
@clear-file="clearFile" />
|
||||
<!-- 传输进度 -->
|
||||
<template v-else>
|
||||
<template v-for="host in task.hosts">
|
||||
<batch-upload-progress v-if="host.id === selectedHost"
|
||||
class="panel-item last-panel-container"
|
||||
:files="host.files" />
|
||||
</template>
|
||||
</template>
|
||||
<!-- 主机模态框 -->
|
||||
<authorized-host-modal ref="hostModal"
|
||||
@selected="setSelectedHost" />
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'uploadPanel'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FileItem } from '@arco-design/web-vue';
|
||||
import type { UploadTaskCreateRequest, UploadTaskQueryResponse } from '@/api/exec/upload-task';
|
||||
import type { UploadTaskStatusType } from '../types/const';
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
import { UploadTaskStepStatus } from '../types/const';
|
||||
import { UploadTaskStatus } from '@/views/exec/upload-task/types/const';
|
||||
import { cancelUploadTask, createUploadTask, startUploadTask, getUploadTask, getUploadTaskStatus } from '@/api/exec/upload-task';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import BatchUploadStep from './batch-upload-step.vue';
|
||||
import BatchUploadForm from './batch-upload-form.vue';
|
||||
import BatchUploadFiles from './batch-upload-files.vue';
|
||||
import BatchUploadHosts from './batch-upload-hosts.vue';
|
||||
import BatchUploadProgress from './batch-upload-progress.vue';
|
||||
import AuthorizedHostModal from '@/components/asset/host/authorized-host-modal/index.vue';
|
||||
|
||||
const defaultForm = (): UploadTaskCreateRequest => {
|
||||
return {
|
||||
description: '',
|
||||
remotePath: '',
|
||||
hostIdList: [],
|
||||
files: []
|
||||
};
|
||||
};
|
||||
|
||||
const { loading, setLoading } = useLoading();
|
||||
|
||||
const pullIntervalId = ref();
|
||||
const taskId = ref();
|
||||
const task = ref<UploadTaskQueryResponse>({} as UploadTaskQueryResponse);
|
||||
const selectedHost = ref();
|
||||
const formModel = ref<UploadTaskCreateRequest>({ ...defaultForm() });
|
||||
const fileList = ref<Array<FileItem>>([]);
|
||||
const taskStatus = ref<UploadTaskStatusType>(UploadTaskStepStatus.WAITING);
|
||||
const filesRef = ref();
|
||||
const hostModal = ref();
|
||||
|
||||
// 打开日志
|
||||
const openLog = async (id: number) => {
|
||||
setLoading(true);
|
||||
taskId.value = id;
|
||||
taskStatus.value = UploadTaskStepStatus.WAITING;
|
||||
try {
|
||||
// 查询任务
|
||||
const { data } = await getUploadTask(id);
|
||||
task.value = data;
|
||||
selectedHost.value = data.hosts[0].id;
|
||||
// 设置任务状态
|
||||
if (data.status === UploadTaskStatus.FINISHED
|
||||
|| data.status === UploadTaskStatus.CANCELED) {
|
||||
taskStatus.value = UploadTaskStepStatus.FINISHED;
|
||||
} else if (data.status === UploadTaskStatus.FAILED) {
|
||||
taskStatus.value = UploadTaskStepStatus.FAILED;
|
||||
} else {
|
||||
taskStatus.value = UploadTaskStepStatus.UPLOADING;
|
||||
}
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({ openLog });
|
||||
|
||||
// 设置选中主机
|
||||
const setSelectedHost = (hosts: Array<number>) => {
|
||||
formModel.value.hostIdList = hosts;
|
||||
};
|
||||
|
||||
// 创建上传任务
|
||||
const doCreateUploadTask = async () => {
|
||||
// 获取文件
|
||||
const files = fileList.value.map(s => {
|
||||
return {
|
||||
fileId: s.uid,
|
||||
filePath: s.file?.webkitRelativePath || s.file?.name,
|
||||
fileSize: s.file?.size,
|
||||
};
|
||||
});
|
||||
if (!files || !files.length) {
|
||||
Message.error('请先选择需要上传的文件');
|
||||
return;
|
||||
}
|
||||
// 创建任务
|
||||
setLoading(true);
|
||||
taskStatus.value = UploadTaskStepStatus.WAITING;
|
||||
try {
|
||||
formModel.value.files = files;
|
||||
const { data } = await createUploadTask(formModel.value);
|
||||
taskId.value = data.id;
|
||||
taskStatus.value = UploadTaskStepStatus.REQUESTING;
|
||||
// 上传文件
|
||||
await filesRef.value.startUpload(data.token);
|
||||
} catch (e) {
|
||||
taskStatus.value = UploadTaskStepStatus.FAILED;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 取消上传任务
|
||||
const doCancelUploadTask = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 取消上传
|
||||
await cancelUploadTask(taskId.value, false);
|
||||
taskStatus.value = UploadTaskStepStatus.WAITING;
|
||||
Message.success('已取消');
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 中断上传请求
|
||||
const abortUploadRequest = () => {
|
||||
taskStatus.value = UploadTaskStepStatus.WAITING;
|
||||
filesRef.value?.close();
|
||||
};
|
||||
|
||||
// 上传请求结束
|
||||
const uploadRequestEnd = async () => {
|
||||
if (taskStatus.value.value === UploadTaskStepStatus.REQUESTING.value) {
|
||||
// 如果结束后还是请求中则代表请求完毕
|
||||
setLoading(true);
|
||||
try {
|
||||
// 开始上传
|
||||
await startUploadTask(taskId.value);
|
||||
// 查询任务
|
||||
const { data } = await getUploadTask(taskId.value);
|
||||
task.value = data;
|
||||
selectedHost.value = data.hosts[0].id;
|
||||
taskStatus.value = UploadTaskStepStatus.UPLOADING;
|
||||
} catch (e) {
|
||||
// 设置失败
|
||||
await uploadRequestError();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} else {
|
||||
// 手动停止或者其他原因则修改为取消上传
|
||||
await doCancelUploadTask();
|
||||
}
|
||||
};
|
||||
|
||||
// 上传请求失败
|
||||
const uploadRequestError = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 开始上传
|
||||
await cancelUploadTask(taskId.value, true);
|
||||
taskStatus.value = UploadTaskStepStatus.FAILED;
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载轮询状态
|
||||
const pullTaskStatus = async () => {
|
||||
if (!taskId.value || !task.value) {
|
||||
return;
|
||||
}
|
||||
// 非上传中则不查询
|
||||
if (taskStatus.value.value !== UploadTaskStepStatus.UPLOADING.value) {
|
||||
return;
|
||||
}
|
||||
// 查询状态
|
||||
const { data } = await getUploadTaskStatus([taskId.value], true);
|
||||
if (!data.length) {
|
||||
return;
|
||||
}
|
||||
const taskStatusData = data[0];
|
||||
// 设置任务状态
|
||||
if (taskStatusData.status === UploadTaskStatus.FINISHED
|
||||
|| taskStatusData.status === UploadTaskStatus.CANCELED) {
|
||||
taskStatus.value = UploadTaskStepStatus.FINISHED;
|
||||
} else if (taskStatusData.status === UploadTaskStatus.FAILED) {
|
||||
taskStatus.value = UploadTaskStepStatus.FAILED;
|
||||
}
|
||||
// 设置文件进度
|
||||
for (let host of task.value.hosts) {
|
||||
for (let file of host.files) {
|
||||
const fileStatus = taskStatusData.files.find(s => s.id === file.id);
|
||||
if (fileStatus) {
|
||||
file.status = fileStatus.status;
|
||||
file.current = fileStatus.current;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 打开主机模态框
|
||||
const openHostModal = () => {
|
||||
hostModal.value.open(formModel.value.hostIdList);
|
||||
};
|
||||
|
||||
// 返回表单页面
|
||||
const backFormPanel = () => {
|
||||
taskStatus.value = UploadTaskStepStatus.WAITING;
|
||||
taskId.value = undefined;
|
||||
task.value = undefined as any;
|
||||
selectedHost.value = undefined as any;
|
||||
};
|
||||
|
||||
// 清空表单
|
||||
const clearForm = () => {
|
||||
formModel.value = { ...defaultForm() };
|
||||
};
|
||||
|
||||
// 清空文件
|
||||
const clearFile = () => {
|
||||
fileList.value = [];
|
||||
};
|
||||
|
||||
// 设置轮询状态
|
||||
onMounted(() => {
|
||||
pullIntervalId.value = setInterval(pullTaskStatus, 5000);
|
||||
});
|
||||
|
||||
// 卸载状态查询
|
||||
onUnmounted(() => {
|
||||
clearInterval(pullIntervalId.value);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@step-width: 258px;
|
||||
@center-width: 398px;
|
||||
@last-width: calc(100% - @step-width - 16px - @center-width - 16px);
|
||||
|
||||
.panel-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
.panel-item {
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
margin-right: 16px;
|
||||
position: relative;
|
||||
background: var(--color-bg-2);
|
||||
}
|
||||
|
||||
.first-panel-container {
|
||||
width: @step-width;
|
||||
}
|
||||
|
||||
.center-panel-container {
|
||||
width: @center-width;
|
||||
}
|
||||
|
||||
.last-panel-container {
|
||||
margin-right: 0;
|
||||
width: @last-width;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.panel-header) {
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
|
||||
h3, > span {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
53
orion-visor-ui/src/views/exec/batch-upload/index.vue
Normal file
53
orion-visor-ui/src/views/exec/batch-upload/index.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div class="layout-container upload-container">
|
||||
<!-- 上传面板 -->
|
||||
<upload-panel ref="panel" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'batchUpload'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useDictStore } from '@/store';
|
||||
import { dictKeys } from './types/const';
|
||||
import { useRoute } from 'vue-router';
|
||||
import UploadPanel from './components/upload-panel.vue';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const panel = ref();
|
||||
|
||||
// 加载字典值
|
||||
onMounted(async () => {
|
||||
const dictStore = useDictStore();
|
||||
await dictStore.loadKeys(dictKeys);
|
||||
});
|
||||
|
||||
// 跳转日志
|
||||
onMounted(async () => {
|
||||
const idParam = route.query.id;
|
||||
const keyParam = route.query.key;
|
||||
if (idParam) {
|
||||
await panel.value?.openLog(Number.parseInt(idParam as string));
|
||||
} else if (keyParam) {
|
||||
await panel.value?.openLog(Number.parseInt(keyParam as string));
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.upload-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
</style>
|
||||
55
orion-visor-ui/src/views/exec/batch-upload/types/const.ts
Normal file
55
orion-visor-ui/src/views/exec/batch-upload/types/const.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
// 上传任务状态定义
|
||||
export interface UploadTaskStatusType {
|
||||
value: string;
|
||||
step: number;
|
||||
status: string;
|
||||
formPanel: boolean;
|
||||
}
|
||||
|
||||
// 上传任务状态
|
||||
export const UploadTaskStepStatus = {
|
||||
// 等待中
|
||||
WAITING: {
|
||||
value: 'WAITING',
|
||||
step: 1,
|
||||
status: 'process',
|
||||
formPanel: true,
|
||||
},
|
||||
// 请求中
|
||||
REQUESTING: {
|
||||
value: 'REQUESTING',
|
||||
step: 2,
|
||||
status: 'process',
|
||||
formPanel: true,
|
||||
},
|
||||
// 上传中
|
||||
UPLOADING: {
|
||||
value: 'UPLOADING',
|
||||
step: 3,
|
||||
status: 'process',
|
||||
formPanel: false,
|
||||
},
|
||||
// 已完成
|
||||
FINISHED: {
|
||||
value: 'FINISHED',
|
||||
step: 4,
|
||||
status: 'finish',
|
||||
formPanel: false,
|
||||
},
|
||||
// 已失败
|
||||
FAILED: {
|
||||
value: 'FAILED',
|
||||
step: 4,
|
||||
status: 'error',
|
||||
formPanel: false,
|
||||
},
|
||||
};
|
||||
|
||||
// 上传任务状态 字典项
|
||||
export const taskStatusKey = 'uploadTaskStatus';
|
||||
|
||||
// 上传任务文件状态 字典项
|
||||
export const fileStatusKey = 'uploadTaskFileStatus';
|
||||
|
||||
// 加载的字典值
|
||||
export const dictKeys = [taskStatusKey, fileStatusKey];
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { FieldRule } from '@arco-design/web-vue';
|
||||
|
||||
export const description = [{
|
||||
maxLength: 128,
|
||||
message: '描述长度不能大于128位'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const hostIdList = [{
|
||||
required: true,
|
||||
message: '请选择上传主机'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const remotePath = [{
|
||||
required: true,
|
||||
message: '请输入上传路径'
|
||||
}, {
|
||||
maxLength: 1024,
|
||||
message: '上传路径长度不能大于1024位'
|
||||
}] as FieldRule[];
|
||||
|
||||
export default {
|
||||
description,
|
||||
hostIdList,
|
||||
remotePath,
|
||||
} as Record<string, FieldRule | FieldRule[]>;
|
||||
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<!-- table -->
|
||||
<a-table row-key="id"
|
||||
ref="tableRef"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:data="row.hosts"
|
||||
:expandable="expandable"
|
||||
:scroll="{ y: '100%' }"
|
||||
:pagination="false"
|
||||
:bordered="false">
|
||||
<!-- 执行主机 -->
|
||||
<template #hostName="{ record }">
|
||||
<span class="table-cell-value span-blue">
|
||||
{{ record.hostName }}
|
||||
</span>
|
||||
<br>
|
||||
<span class="table-cell-sub-value usn text-copy"
|
||||
style="font-size: 12px;"
|
||||
@click="copy(record.hostAddress)">
|
||||
{{ record.hostAddress }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- 错误信息 -->
|
||||
<template #errorMessage="{ record }">
|
||||
<span class="span-red">
|
||||
{{ record.errorMessage }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- 执行状态 -->
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="getDictValue(execHostStatusKey, record.status, 'color')">
|
||||
{{ getDictValue(execHostStatusKey, record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<!-- 执行时间 -->
|
||||
<template #startTime="{ record }">
|
||||
<span class="table-cell-value">
|
||||
{{ (record.startTime && dateFormat(new Date(record.startTime))) || '-' }}
|
||||
</span>
|
||||
<br>
|
||||
<span class="table-cell-sub-value usn" style="font-size: 12px;">
|
||||
持续时间: {{ formatDuration(record.startTime, record.finishTime) || '-' }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- 操作 -->
|
||||
<template #handle="{ record }">
|
||||
<div class="table-handle-wrapper">
|
||||
<!-- 命令 -->
|
||||
<a-button type="text"
|
||||
size="mini"
|
||||
@click="emits('viewCommand', record.command)">
|
||||
命令
|
||||
</a-button>
|
||||
<!-- 参数 -->
|
||||
<a-button type="text"
|
||||
size="mini"
|
||||
@click="emits('viewParams', record.parameter)">
|
||||
参数
|
||||
</a-button>
|
||||
<!-- 下载 -->
|
||||
<a-button type="text"
|
||||
size="mini"
|
||||
@click="downloadLogFile(record.id)">
|
||||
下载
|
||||
</a-button>
|
||||
<!-- 中断 -->
|
||||
<a-popconfirm content="确认要中断命令吗, 删除后会中断执行?"
|
||||
position="left"
|
||||
type="warning"
|
||||
@ok="interruptedHost(record)">
|
||||
<a-button v-permission="['asset:exec-command-log:interrupt']"
|
||||
type="text"
|
||||
size="mini"
|
||||
status="danger"
|
||||
:disabled="record.status !== execHostStatus.WAITING && record.status !== execHostStatus.RUNNING">
|
||||
中断
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
<!-- 删除 -->
|
||||
<a-popconfirm content="确认删除这条记录吗?"
|
||||
position="left"
|
||||
type="warning"
|
||||
@ok="deleteRow(record)">
|
||||
<a-button v-permission="['asset:exec-command-log:delete']"
|
||||
type="text"
|
||||
size="mini"
|
||||
status="danger">
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
</template>
|
||||
</a-table>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'execCommandHostLogTable'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ExecLogQueryResponse, ExecHostLogQueryResponse } from '@/api/exec/exec-log';
|
||||
import { deleteExecCommandHostLog } from '@/api/exec/exec-command-log';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import columns from '../types/host-table.columns';
|
||||
import { execHostStatusKey, execHostStatus } from '@/components/exec/log/const';
|
||||
import { useDictStore } from '@/store';
|
||||
import { useExpandable } from '@/types/table';
|
||||
import { dateFormat, formatDuration } from '@/utils';
|
||||
import { downloadExecCommandLogFile, interruptHostExecCommand } from '@/api/exec/exec-command-log';
|
||||
import { copy } from '@/hooks/copy';
|
||||
import { downloadFile } from '@/utils/file';
|
||||
|
||||
const props = defineProps<{
|
||||
row: ExecLogQueryResponse;
|
||||
}>();
|
||||
|
||||
const emits = defineEmits(['viewCommand', 'viewParams', 'refreshHost']);
|
||||
|
||||
const expandable = useExpandable({ width: 90 });
|
||||
const { loading, setLoading } = useLoading();
|
||||
const { toOptions, getDictValue } = useDictStore();
|
||||
|
||||
// 下载文件
|
||||
const downloadLogFile = async (id: number) => {
|
||||
const data = await downloadExecCommandLogFile(id);
|
||||
downloadFile(data);
|
||||
};
|
||||
|
||||
// 中断执行
|
||||
const interruptedHost = async (record: ExecHostLogQueryResponse) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// 调用中断接口
|
||||
await interruptHostExecCommand({
|
||||
hostLogId: record.id
|
||||
});
|
||||
Message.success('已中断');
|
||||
record.status = execHostStatus.INTERRUPTED;
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除当前行
|
||||
const deleteRow = async ({ id, logId }: {
|
||||
id: number,
|
||||
logId: number
|
||||
}) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// 调用删除接口
|
||||
await deleteExecCommandHostLog(id);
|
||||
Message.success('删除成功');
|
||||
emits('refreshHost', logId);
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<a-modal v-model:visible="visible"
|
||||
body-class="modal-form-large"
|
||||
title-align="start"
|
||||
title="清空批量执行日志"
|
||||
:align-center="false"
|
||||
:draggable="true"
|
||||
:mask-closable="false"
|
||||
:unmount-on-close="true"
|
||||
ok-text="清空"
|
||||
:ok-button-props="{ disabled: loading }"
|
||||
:cancel-button-props="{ disabled: loading }"
|
||||
:on-before-ok="handlerOk"
|
||||
@close="handleClose">
|
||||
<a-spin class="full" :loading="loading">
|
||||
<a-form :model="formModel"
|
||||
ref="formRef"
|
||||
label-align="right"
|
||||
:auto-label-width="true">
|
||||
<!-- 执行时间 -->
|
||||
<a-form-item field="startTimeRange" label="执行时间">
|
||||
<a-range-picker v-model="formModel.startTimeRange"
|
||||
:time-picker-props="{ defaultValue: ['00:00:00', '23:59:59'] }"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss" />
|
||||
</a-form-item>
|
||||
<!-- 执行用户 -->
|
||||
<a-form-item field="userId" label="执行用户">
|
||||
<user-selector v-model="formModel.userId"
|
||||
placeholder="请选择执行用户"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 执行描述 -->
|
||||
<a-form-item field="description" label="执行描述">
|
||||
<a-input v-model="formModel.description"
|
||||
placeholder="请输入执行描述"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 执行命令 -->
|
||||
<a-form-item field="command" label="执行命令">
|
||||
<a-input v-model="formModel.command"
|
||||
placeholder="请输入执行命令"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 执行状态 -->
|
||||
<a-form-item field="status" label="执行状态">
|
||||
<a-select v-model="formModel.status"
|
||||
:options="toOptions(execStatusKey)"
|
||||
placeholder="请选择执行状态" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'execCommandLogClearModal'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ExecLogQueryRequest } from '@/api/exec/exec-log';
|
||||
import { ref } from 'vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import useVisible from '@/hooks/visible';
|
||||
import { execStatusKey } from '@/components/exec/log/const';
|
||||
import { getExecCommandLogCount, clearExecCommandLog } from '@/api/exec/exec-command-log';
|
||||
import { Message, Modal } from '@arco-design/web-vue';
|
||||
import { useDictStore } from '@/store';
|
||||
import UserSelector from '@/components/user/user/selector/index.vue';
|
||||
|
||||
const emits = defineEmits(['clear']);
|
||||
|
||||
const { visible, setVisible } = useVisible();
|
||||
const { loading, setLoading } = useLoading();
|
||||
const { toOptions } = useDictStore();
|
||||
|
||||
const formRef = ref<any>();
|
||||
const formModel = ref<ExecLogQueryRequest>({});
|
||||
|
||||
const defaultForm = (): ExecLogQueryRequest => {
|
||||
return {
|
||||
id: undefined,
|
||||
userId: undefined,
|
||||
description: undefined,
|
||||
command: undefined,
|
||||
status: undefined,
|
||||
startTimeRange: undefined
|
||||
};
|
||||
};
|
||||
|
||||
// 打开
|
||||
const open = (record: any) => {
|
||||
renderForm({ ...defaultForm(), ...record });
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
// 渲染表单
|
||||
const renderForm = (record: any) => {
|
||||
formModel.value = Object.assign({}, record);
|
||||
};
|
||||
|
||||
defineExpose({ open });
|
||||
|
||||
// 确定
|
||||
const handlerOk = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 获取总数量
|
||||
const { data } = await getExecCommandLogCount(formModel.value);
|
||||
if (data) {
|
||||
// 清空
|
||||
doClear(data);
|
||||
} else {
|
||||
// 无数据
|
||||
Message.warning('当前条件未查询到数据');
|
||||
}
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// 执行删除
|
||||
const doClear = (count: number) => {
|
||||
Modal.confirm({
|
||||
title: '删除清空',
|
||||
content: `确定要删除 ${count} 条数据吗? 确定后将立即删除且无法恢复!`,
|
||||
onOk: async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 调用删除
|
||||
await clearExecCommandLog(formModel.value);
|
||||
emits('clear');
|
||||
// 清空
|
||||
setVisible(false);
|
||||
handlerClear();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 关闭
|
||||
const handleClose = () => {
|
||||
handlerClear();
|
||||
};
|
||||
|
||||
// 清空
|
||||
const handlerClear = () => {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,427 @@
|
||||
<template>
|
||||
<!-- 搜索 -->
|
||||
<a-card class="general-card table-search-card">
|
||||
<query-header :model="formModel"
|
||||
label-align="left"
|
||||
:itemOptions="{ 5: { span: 2 } }"
|
||||
@submit="fetchTableData"
|
||||
@reset="fetchTableData"
|
||||
@keyup.enter="() => fetchTableData()">
|
||||
<!-- 执行描述 -->
|
||||
<a-form-item field="description" label="执行描述">
|
||||
<a-input v-model="formModel.description"
|
||||
placeholder="请输入执行描述"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 执行状态 -->
|
||||
<a-form-item field="status" label="执行状态">
|
||||
<a-select v-model="formModel.status"
|
||||
:options="toOptions(execStatusKey)"
|
||||
placeholder="请选择执行状态"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 执行用户 -->
|
||||
<a-form-item field="userId" label="执行用户">
|
||||
<user-selector v-model="formModel.userId"
|
||||
placeholder="请选择执行用户"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 执行命令 -->
|
||||
<a-form-item field="command" label="执行命令">
|
||||
<a-input v-model="formModel.command"
|
||||
placeholder="请输入执行命令"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- id -->
|
||||
<a-form-item field="id" label="id">
|
||||
<a-input-number v-model="formModel.id"
|
||||
placeholder="请输入id"
|
||||
allow-clear
|
||||
hide-button />
|
||||
</a-form-item>
|
||||
<!-- 执行时间 -->
|
||||
<a-form-item field="startTimeRange" label="执行时间">
|
||||
<a-range-picker v-model="formModel.startTimeRange"
|
||||
:time-picker-props="{ defaultValue: ['00:00:00', '23:59:59'] }"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss" />
|
||||
</a-form-item>
|
||||
</query-header>
|
||||
</a-card>
|
||||
<!-- 表格 -->
|
||||
<a-card class="general-card table-card">
|
||||
<template #title>
|
||||
<!-- 左侧操作 -->
|
||||
<div class="table-left-bar-handle">
|
||||
<!-- 标题 -->
|
||||
<div class="table-title">
|
||||
批量执行日志列表
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧操作 -->
|
||||
<div class="table-right-bar-handle">
|
||||
<a-space>
|
||||
<!-- 执行命令 -->
|
||||
<a-button v-permission="['asset:exec-command:exec']"
|
||||
type="primary"
|
||||
@click="$router.push({ name: 'execCommand' })">
|
||||
执行命令
|
||||
<template #icon>
|
||||
<icon-thunderbolt />
|
||||
</template>
|
||||
</a-button>
|
||||
<!-- 清空 -->
|
||||
<a-button v-permission="['asset:exec-command-log:management:clear']"
|
||||
status="danger"
|
||||
@click="openClear">
|
||||
清空
|
||||
<template #icon>
|
||||
<icon-close />
|
||||
</template>
|
||||
</a-button>
|
||||
<!-- 删除 -->
|
||||
<a-popconfirm :content="`确认删除选中的 ${selectedKeys.length} 条记录吗? 删除后会中断执行!`"
|
||||
position="br"
|
||||
type="warning"
|
||||
@ok="deleteSelectRows">
|
||||
<a-button v-permission="['asset:exec-command-log:delete']"
|
||||
type="secondary"
|
||||
status="danger"
|
||||
:disabled="selectedKeys.length === 0">
|
||||
删除
|
||||
<template #icon>
|
||||
<icon-delete />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
<!-- table -->
|
||||
<a-table row-key="id"
|
||||
ref="tableRef"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
v-model:selected-keys="selectedKeys"
|
||||
:row-selection="rowSelection"
|
||||
:expandable="expandable"
|
||||
:data="tableRenderData"
|
||||
:pagination="pagination"
|
||||
:bordered="false"
|
||||
@page-change="(page) => fetchTableData(page, pagination.pageSize)"
|
||||
@page-size-change="(size) => fetchTableData(1, size)"
|
||||
@expand="loadExecHost">
|
||||
<!-- 展开表格 -->
|
||||
<template #expand-row="{ record }">
|
||||
<exec-command-host-log-table :row="record"
|
||||
@view-command="s => emits('viewCommand', s)"
|
||||
@view-params="s => emits('viewParams', s)"
|
||||
@refresh-host="refreshExecHost" />
|
||||
</template>
|
||||
<!-- 执行命令 -->
|
||||
<template #command="{ record }">
|
||||
<span :title="record.command">
|
||||
{{ record.command }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- 执行状态 -->
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="getDictValue(execStatusKey, record.status, 'color')">
|
||||
{{ getDictValue(execStatusKey, record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<!-- 执行时间 -->
|
||||
<template #startTime="{ record }">
|
||||
<span class="table-cell-value">
|
||||
{{ (record.startTime && dateFormat(new Date(record.startTime))) || '-' }}
|
||||
</span>
|
||||
<br>
|
||||
<span class="table-cell-sub-value usn" style="font-size: 12px;">
|
||||
持续时间: {{ formatDuration(record.startTime, record.finishTime) || '-' }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- 操作 -->
|
||||
<template #handle="{ record }">
|
||||
<div class="table-handle-wrapper">
|
||||
<!-- 重新执行 -->
|
||||
<a-popconfirm content="确定要重新执行吗?"
|
||||
position="left"
|
||||
type="warning"
|
||||
@ok="doReExecCommand(record)">
|
||||
<a-button v-permission="['asset:exec-command:exec']"
|
||||
type="text"
|
||||
size="mini">
|
||||
重新执行
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
<!-- 命令 -->
|
||||
<a-button type="text"
|
||||
size="mini"
|
||||
@click="emits('viewCommand', record.command)">
|
||||
命令
|
||||
</a-button>
|
||||
<!-- 日志 -->
|
||||
<a-button v-permission="['asset:exec-command-log:query', 'asset:exec-command:exec']"
|
||||
type="text"
|
||||
size="mini"
|
||||
title="ctrl + 左键新页面打开"
|
||||
@click="(e) => emits('viewLog', record.id, e.ctrlKey)">
|
||||
日志
|
||||
</a-button>
|
||||
<!-- 中断 -->
|
||||
<a-popconfirm content="确定要中断执行吗?"
|
||||
position="left"
|
||||
type="warning"
|
||||
@ok="doInterruptExecCommand(record)">
|
||||
<a-button v-permission="['asset:exec-command-log:interrupt']"
|
||||
type="text"
|
||||
size="mini"
|
||||
status="danger"
|
||||
:disabled="record.status !== execStatus.WAITING && record.status !== execStatus.RUNNING">
|
||||
中断
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
<!-- 删除 -->
|
||||
<a-popconfirm content="确认删除这条记录吗, 删除后会中断执行?"
|
||||
position="left"
|
||||
type="warning"
|
||||
@ok="deleteRow(record)">
|
||||
<a-button v-permission="['asset:exec-command-log:delete']"
|
||||
type="text"
|
||||
size="mini"
|
||||
status="danger">
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'execCommandLogTable'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TableData } from '@arco-design/web-vue/es/table/interface';
|
||||
import type { ExecLogQueryResponse, ExecLogQueryRequest } from '@/api/exec/exec-log';
|
||||
import { reactive, ref, onMounted, onUnmounted } from 'vue';
|
||||
import {
|
||||
batchDeleteExecCommandLog,
|
||||
deleteExecCommandLog,
|
||||
getExecCommandHostLogList,
|
||||
getExecCommandLogPage,
|
||||
getExecCommandLogStatus
|
||||
} from '@/api/exec/exec-command-log';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import columns from '../types/table.columns';
|
||||
import { execStatus, execStatusKey } from '@/components/exec/log/const';
|
||||
import { useExpandable, usePagination, useRowSelection } from '@/types/table';
|
||||
import { useDictStore } from '@/store';
|
||||
import { dateFormat, formatDuration } from '@/utils';
|
||||
import { reExecCommand } from '@/api/exec/exec-command';
|
||||
import { interruptExecCommand } from '@/api/exec/exec-command-log';
|
||||
import UserSelector from '@/components/user/user/selector/index.vue';
|
||||
import ExecCommandHostLogTable from './exec-command-host-log-table.vue';
|
||||
|
||||
const emits = defineEmits(['viewCommand', 'viewParams', 'viewLog', 'openClear']);
|
||||
|
||||
const pagination = usePagination();
|
||||
const rowSelection = useRowSelection();
|
||||
const expandable = useExpandable();
|
||||
const { loading, setLoading } = useLoading();
|
||||
const { toOptions, getDictValue } = useDictStore();
|
||||
|
||||
const pullIntervalId = ref();
|
||||
const tableRef = ref();
|
||||
const selectedKeys = ref<number[]>([]);
|
||||
const tableRenderData = ref<ExecLogQueryResponse[]>([]);
|
||||
const formModel = reactive<ExecLogQueryRequest>({
|
||||
id: undefined,
|
||||
userId: undefined,
|
||||
description: undefined,
|
||||
command: undefined,
|
||||
status: undefined,
|
||||
startTimeRange: undefined,
|
||||
});
|
||||
|
||||
// 打开清理
|
||||
const openClear = () => {
|
||||
emits('openClear', { ...formModel, id: undefined });
|
||||
};
|
||||
|
||||
// 删除选中行
|
||||
const deleteSelectRows = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// 调用删除接口
|
||||
await batchDeleteExecCommandLog(selectedKeys.value);
|
||||
Message.success(`成功删除 ${selectedKeys.value.length} 条数据`);
|
||||
selectedKeys.value = [];
|
||||
// 重新加载数据
|
||||
fetchTableData();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除当前行
|
||||
const deleteRow = async ({ id }: {
|
||||
id: number
|
||||
}) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// 调用删除接口
|
||||
await deleteExecCommandLog(id);
|
||||
Message.success('删除成功');
|
||||
// 重新加载数据
|
||||
fetchTableData();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 重新执行命令
|
||||
const doReExecCommand = async (record: ExecLogQueryResponse) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// 调用中断接口
|
||||
await reExecCommand({
|
||||
logId: record.id
|
||||
});
|
||||
Message.success('已重新执行');
|
||||
fetchTableData();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 中断执行
|
||||
const doInterruptExecCommand = async (record: ExecLogQueryResponse) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// 调用中断接口
|
||||
await interruptExecCommand({
|
||||
logId: record.id
|
||||
});
|
||||
Message.success('已中断');
|
||||
record.status = execStatus.COMPLETED;
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 刷新执行主机
|
||||
const refreshExecHost = (id: number) => {
|
||||
// 获取到执行主机
|
||||
const exec = tableRenderData.value.find(s => s.id === id);
|
||||
if (!exec) {
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
getExecCommandHostLogList(id).then(s => {
|
||||
exec.hosts = s.data;
|
||||
});
|
||||
};
|
||||
|
||||
// 加载主机数据
|
||||
const loadExecHost = async (key: number | string, record: TableData) => {
|
||||
if (record.hosts?.length) {
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const { data } = await getExecCommandHostLogList(record.id);
|
||||
record.hosts = data;
|
||||
};
|
||||
|
||||
// 加载状态
|
||||
const pullExecStatus = async () => {
|
||||
const unCompleteIdList = tableRenderData.value
|
||||
.filter(s => s.status === execStatus.WAITING || s.status === execStatus.RUNNING)
|
||||
.map(s => s.id);
|
||||
if (!unCompleteIdList.length) {
|
||||
return;
|
||||
}
|
||||
// 加载未完成的状态
|
||||
const { data: { logList, hostList } } = await getExecCommandLogStatus(unCompleteIdList);
|
||||
// 设置任务状态
|
||||
logList.forEach(s => {
|
||||
const tableRow = tableRenderData.value.find(r => r.id === s.id);
|
||||
if (!tableRow) {
|
||||
return;
|
||||
}
|
||||
tableRow.status = s.status;
|
||||
tableRow.startTime = s.startTime;
|
||||
tableRow.finishTime = s.finishTime;
|
||||
});
|
||||
// 设置主机状态
|
||||
hostList.forEach(s => {
|
||||
const host = tableRenderData.value
|
||||
.find(r => r.id === s.logId)
|
||||
?.hosts
|
||||
?.find(r => r.id === s.id);
|
||||
if (!host) {
|
||||
return;
|
||||
}
|
||||
host.status = s.status;
|
||||
host.startTime = s.startTime;
|
||||
host.finishTime = s.finishTime;
|
||||
host.exitCode = s.exitCode;
|
||||
host.errorMessage = s.errorMessage;
|
||||
});
|
||||
};
|
||||
|
||||
// 加载数据
|
||||
const doFetchTableData = async (request: ExecLogQueryRequest) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data } = await getExecCommandLogPage(request);
|
||||
tableRenderData.value = data.rows;
|
||||
pagination.total = data.total;
|
||||
pagination.current = request.page;
|
||||
pagination.pageSize = request.limit;
|
||||
selectedKeys.value = [];
|
||||
tableRef.value.expandAll(false);
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 切换页码
|
||||
const fetchTableData = (page = 1, limit = pagination.pageSize, form = formModel) => {
|
||||
doFetchTableData({ page, limit, ...form });
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
fetchTableData
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 加载数据
|
||||
fetchTableData();
|
||||
// 注册状态轮询
|
||||
pullIntervalId.value = setInterval(pullExecStatus, 10000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// 卸载状态轮询
|
||||
clearInterval(pullIntervalId.value);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.arco-table-cell-fixed-expand) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
97
orion-visor-ui/src/views/exec/exec-command-log/index.vue
Normal file
97
orion-visor-ui/src/views/exec/exec-command-log/index.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<div class="layout-container" v-if="render">
|
||||
<!-- 列表-表格 -->
|
||||
<exec-command-log-table ref="tableRef"
|
||||
@view-command="viewCommand"
|
||||
@view-params="viewParams"
|
||||
@view-log="viewLog"
|
||||
@open-clear="openClearModal" />
|
||||
<!-- 清理模态框 -->
|
||||
<exec-command-log-clear-modal ref="clearModal"
|
||||
@clear="clearCallback" />
|
||||
<!-- 执行日志模态框 -->
|
||||
<exec-log-panel-modal ref="logModal"
|
||||
type="BATCH" />
|
||||
<!-- json 模态框 -->
|
||||
<json-editor-modal ref="jsonModal"
|
||||
:esc-to-close="true" />
|
||||
<!-- shell 模态框 -->
|
||||
<shell-editor-modal ref="shellModal"
|
||||
:footer="false"
|
||||
:esc-to-close="true" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'execCommandLog'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onBeforeMount } from 'vue';
|
||||
import { useDictStore } from '@/store';
|
||||
import { dictKeys } from '@/components/exec/log/const';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { openNewRoute } from '@/router';
|
||||
import ExecCommandLogTable from './components/exec-command-log-table.vue';
|
||||
import ExecCommandLogClearModal from './components/exec-command-log-clear-modal.vue';
|
||||
import JsonEditorModal from '@/components/view/json-editor/modal/index.vue';
|
||||
import ShellEditorModal from '@/components/view/shell-editor/modal/index.vue';
|
||||
import ExecLogPanelModal from '@/components/exec/log/panel-modal/index.vue';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const render = ref(false);
|
||||
const tableRef = ref();
|
||||
const logModal = ref();
|
||||
const clearModal = ref();
|
||||
const jsonModal = ref();
|
||||
const shellModal = ref();
|
||||
|
||||
// 打开清理模态框
|
||||
const openClearModal = (e: any) => {
|
||||
clearModal.value.open(e);
|
||||
};
|
||||
|
||||
// 查看命令
|
||||
const viewCommand = (data: string) => {
|
||||
shellModal.value.open(data, '命令');
|
||||
};
|
||||
|
||||
// 查看参数
|
||||
const viewParams = (data: string) => {
|
||||
jsonModal.value.open(JSON.parse(data));
|
||||
};
|
||||
|
||||
// 查看日志
|
||||
const viewLog = (id: number, newWindow: boolean) => {
|
||||
if (newWindow) {
|
||||
// 跳转新页面
|
||||
openNewRoute({
|
||||
name: 'execCommand',
|
||||
query: {
|
||||
id
|
||||
}
|
||||
});
|
||||
} else {
|
||||
logModal.value.open(id);
|
||||
}
|
||||
};
|
||||
|
||||
// 清理回调
|
||||
const clearCallback = () => {
|
||||
tableRef.value.fetchTableData();
|
||||
};
|
||||
|
||||
onBeforeMount(async () => {
|
||||
const dictStore = useDictStore();
|
||||
await dictStore.loadKeys(dictKeys);
|
||||
render.value = true;
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface';
|
||||
import { isNumber } from '@/utils/is';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'id',
|
||||
dataIndex: 'id',
|
||||
slotName: 'id',
|
||||
width: 70,
|
||||
align: 'left',
|
||||
fixed: 'left',
|
||||
}, {
|
||||
title: '执行主机',
|
||||
dataIndex: 'hostName',
|
||||
slotName: 'hostName',
|
||||
align: 'left',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
}, {
|
||||
title: '退出码',
|
||||
dataIndex: 'exitCode',
|
||||
slotName: 'exitCode',
|
||||
align: 'left',
|
||||
width: 118,
|
||||
render: ({ record }) => {
|
||||
return isNumber(record.exitCode) ? record.exitCode : '-';
|
||||
},
|
||||
}, {
|
||||
title: '执行状态',
|
||||
dataIndex: 'status',
|
||||
slotName: 'status',
|
||||
align: 'left',
|
||||
width: 118,
|
||||
}, {
|
||||
title: '错误信息',
|
||||
dataIndex: 'errorMessage',
|
||||
slotName: 'errorMessage',
|
||||
align: 'left',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
width: 168,
|
||||
}, {
|
||||
title: '执行时间',
|
||||
dataIndex: 'startTime',
|
||||
slotName: 'startTime',
|
||||
align: 'left',
|
||||
width: 190,
|
||||
}, {
|
||||
title: '操作',
|
||||
slotName: 'handle',
|
||||
width: 258,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
},
|
||||
] as TableColumnData[];
|
||||
|
||||
export default columns;
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'id',
|
||||
dataIndex: 'id',
|
||||
slotName: 'id',
|
||||
width: 70,
|
||||
align: 'left',
|
||||
fixed: 'left',
|
||||
}, {
|
||||
title: '执行描述',
|
||||
dataIndex: 'description',
|
||||
slotName: 'description',
|
||||
align: 'left',
|
||||
width: 168,
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
}, {
|
||||
title: '执行命令',
|
||||
dataIndex: 'command',
|
||||
slotName: 'command',
|
||||
align: 'left',
|
||||
ellipsis: true,
|
||||
}, {
|
||||
title: '执行用户',
|
||||
dataIndex: 'username',
|
||||
slotName: 'username',
|
||||
align: 'left',
|
||||
width: 118,
|
||||
ellipsis: true,
|
||||
}, {
|
||||
title: '执行状态',
|
||||
dataIndex: 'status',
|
||||
slotName: 'status',
|
||||
align: 'left',
|
||||
width: 118,
|
||||
}, {
|
||||
title: '执行时间',
|
||||
dataIndex: 'startTime',
|
||||
slotName: 'startTime',
|
||||
align: 'left',
|
||||
width: 190,
|
||||
}, {
|
||||
title: '操作',
|
||||
slotName: 'handle',
|
||||
width: 288,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
},
|
||||
] as TableColumnData[];
|
||||
|
||||
export default columns;
|
||||
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- 表头 -->
|
||||
<div class="panel-header">
|
||||
<h3>执行命令</h3>
|
||||
<span v-permission="['asset:exec-template:query']"
|
||||
class="span-blue usn pointer"
|
||||
@click="emits('openTemplate')">
|
||||
从模板中选择
|
||||
</span>
|
||||
</div>
|
||||
<!-- 命令编辑器 -->
|
||||
<div class="editor-wrapper">
|
||||
<slot />
|
||||
</div>
|
||||
<!-- 命名提示信息 -->
|
||||
<div v-pre class="editor-help">
|
||||
使用 @{{ xxx }} 来替换参数, 输入_可以获取全部变量
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'execCommandPanelEditor'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
const emits = defineEmits(['openTemplate']);
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.editor-wrapper {
|
||||
width: 100%;
|
||||
height: calc(100% - 56px);
|
||||
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>
|
||||
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- 表头 -->
|
||||
<div class="panel-header">
|
||||
<h3>执行参数</h3>
|
||||
<!-- 操作 -->
|
||||
<a-button-group size="mini">
|
||||
<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: 'execCommandPanelForm'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const emits = defineEmits(['reset', 'exec']);
|
||||
|
||||
defineProps<{
|
||||
schemaCount: number
|
||||
}>();
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- 表头 -->
|
||||
<div class="panel-header">
|
||||
<h3>执行历史</h3>
|
||||
<span class="history-help">
|
||||
展示最近 {{ historyCount }} 条执行记录
|
||||
</span>
|
||||
</div>
|
||||
<!-- 加载中 -->
|
||||
<a-skeleton v-if="loading" :animation="true">
|
||||
<a-skeleton-line :rows="4"
|
||||
:line-height="40"
|
||||
:line-spacing="8" />
|
||||
</a-skeleton>
|
||||
<!-- 无数据 -->
|
||||
<div v-else-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: 'execCommandPanelHistory'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ExecLogQueryResponse } from '@/api/exec/exec-log';
|
||||
import type { ExecCommandRequest } from '@/api/exec/exec-command';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { getExecCommandLogHistory } from '@/api/exec/exec-command-log';
|
||||
import { historyCount } from '../types/const';
|
||||
import useLoading from '@/hooks/loading';
|
||||
|
||||
const emits = defineEmits(['selected']);
|
||||
|
||||
const { loading, setLoading } = useLoading(true);
|
||||
|
||||
const historyLogs = ref<Array<ExecLogQueryResponse>>([]);
|
||||
|
||||
// 添加
|
||||
const add = (record: ExecCommandRequest) => {
|
||||
const index = historyLogs.value.findIndex(s => s.description === record.description);
|
||||
if (index === -1) {
|
||||
// 不存在
|
||||
historyLogs.value.unshift({
|
||||
description: record.description,
|
||||
command: record.command,
|
||||
parameterSchema: record.parameterSchema,
|
||||
timeout: record.timeout,
|
||||
hostIdList: record.hostIdList
|
||||
} as ExecLogQueryResponse);
|
||||
} else {
|
||||
// 存在
|
||||
const his = historyLogs.value[index];
|
||||
historyLogs.value.splice(index, 1);
|
||||
historyLogs.value.unshift({
|
||||
...his,
|
||||
command: record.command,
|
||||
parameterSchema: record.parameterSchema,
|
||||
timeout: record.timeout,
|
||||
hostIdList: record.hostIdList
|
||||
} as ExecLogQueryResponse);
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({ add });
|
||||
|
||||
// 加载批量执行日志
|
||||
const fetchExecHistory = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data } = await getExecCommandLogHistory(historyCount);
|
||||
historyLogs.value = data;
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载批量执行日志
|
||||
onMounted(fetchExecHistory);
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.exec-history-rows {
|
||||
position: absolute;
|
||||
width: calc(100% - 32px);
|
||||
height: calc(100% - 64px);
|
||||
overflow: auto;
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.exec-history {
|
||||
padding: 8px;
|
||||
border-radius: 2px;
|
||||
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;
|
||||
margin-right: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #FFF;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.history-help {
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,354 @@
|
||||
<template>
|
||||
<!-- 命令执行 -->
|
||||
<a-spin class="exec-container" :loading="loading">
|
||||
<!-- 执行参数 -->
|
||||
<exec-command-panel-form class="exec-form-container"
|
||||
:schema-count="parameterSchema.length"
|
||||
@exec="execCommand"
|
||||
@reset="resetForm">
|
||||
<!-- 命令表单 -->
|
||||
<template #form>
|
||||
<a-form :model="formModel"
|
||||
ref="formRef"
|
||||
class="form-wrapper"
|
||||
label-align="right"
|
||||
:auto-label-width="true"
|
||||
:rules="formRules">
|
||||
<a-row :gutter="16">
|
||||
<!-- 执行主机 -->
|
||||
<a-col :span="24">
|
||||
<a-form-item field="hostIdList" label="执行主机">
|
||||
<div class="selected-host">
|
||||
<!-- 已选择数量 -->
|
||||
<span class="usn" v-if="formModel.hostIdList?.length">
|
||||
已选择<span class="selected-host-count span-blue">{{ formModel.hostIdList?.length }}</span>台主机
|
||||
</span>
|
||||
<span class="usn pointer span-blue" @click="openSelectHost">
|
||||
{{ formModel.hostIdList?.length ? '重新选择' : '选择主机' }}
|
||||
</span>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<!-- 执行描述 -->
|
||||
<a-col :span="24">
|
||||
<a-form-item field="description" label="执行描述">
|
||||
<a-input v-model="formModel.description"
|
||||
placeholder="请输入执行描述"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<!-- 超时时间 -->
|
||||
<a-col :span="14">
|
||||
<a-form-item field="timeout"
|
||||
label="超时时间"
|
||||
:hide-asterisk="true">
|
||||
<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-col>
|
||||
<!-- 脚本执行 -->
|
||||
<a-col :span="10">
|
||||
<a-form-item field="scriptExec"
|
||||
label="脚本执行"
|
||||
:hide-asterisk="true">
|
||||
<div class="flex-center">
|
||||
<a-switch v-model="formModel.scriptExec"
|
||||
type="round"
|
||||
:checked-value="EnabledStatus.ENABLED"
|
||||
:unchecked-value="EnabledStatus.DISABLED" />
|
||||
<div class="question-right ml8">
|
||||
<a-tooltip content="启用后会将命令写入脚本文件 传输到主机后执行">
|
||||
<icon-question-circle />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</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"
|
||||
required>
|
||||
<a-input v-model="parameterFormModel[item.name as string]"
|
||||
:placeholder="item.desc"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
</exec-command-panel-form>
|
||||
<!-- 执行命令 -->
|
||||
<exec-command-panel-editor class="exec-command-container"
|
||||
@open-template="() => templateModal.open()">
|
||||
<exec-editor v-model="formModel.command"
|
||||
theme="vs-dark"
|
||||
:parameter="parameterSchema" />
|
||||
</exec-command-panel-editor>
|
||||
<!-- 执行历史 -->
|
||||
<exec-command-panel-history class="exec-history-container"
|
||||
ref="historyRef"
|
||||
@selected="setWithExecLog" />
|
||||
<!-- 命令模板模态框 -->
|
||||
<exec-template-modal ref="templateModal"
|
||||
@selected="setWithTemplate" />
|
||||
<!-- 主机模态框 -->
|
||||
<authorized-host-modal ref="hostModal"
|
||||
@selected="setSelectedHost" />
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'execPanel'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ExecTemplateQueryResponse } from '@/api/exec/exec-template';
|
||||
import type { ExecLogQueryResponse } from '@/api/exec/exec-log';
|
||||
import type { ExecCommandRequest } from '@/api/exec/exec-command';
|
||||
import type { TemplateParam } from '@/components/view/exec-editor/const';
|
||||
import { ref } from 'vue';
|
||||
import formRules from '../types/form.rules';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { batchExecCommand } from '@/api/exec/exec-command';
|
||||
import { getExecTemplateWithAuthorized } from '@/api/exec/exec-template';
|
||||
import { EnabledStatus } from '@/types/const';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import ExecEditor from '@/components/view/exec-editor/index.vue';
|
||||
import AuthorizedHostModal from '@/components/asset/host/authorized-host-modal/index.vue';
|
||||
import ExecCommandPanelForm from './exec-command-panel-form.vue';
|
||||
import ExecCommandPanelHistory from './exec-command-panel-history.vue';
|
||||
import ExecCommandPanelEditor from './exec-command-panel-editor.vue';
|
||||
import ExecTemplateModal from '@/components/exec/template/modal/index.vue';
|
||||
|
||||
const emits = defineEmits(['submit']);
|
||||
|
||||
const defaultForm = (): ExecCommandRequest => {
|
||||
return {
|
||||
description: '',
|
||||
command: '',
|
||||
timeout: 0,
|
||||
scriptExec: EnabledStatus.DISABLED,
|
||||
parameterSchema: '[]',
|
||||
hostIdList: [],
|
||||
};
|
||||
};
|
||||
|
||||
const { loading, setLoading } = useLoading();
|
||||
|
||||
const hostModal = ref<any>();
|
||||
const historyRef = ref<any>();
|
||||
const formRef = ref<any>();
|
||||
const templateModal = ref<any>();
|
||||
const parameterFormRef = ref<any>();
|
||||
const formModel = ref<ExecCommandRequest>({ ...defaultForm() });
|
||||
const parameterFormModel = ref<Record<string, any>>({});
|
||||
const parameterSchema = ref<Array<TemplateParam>>([]);
|
||||
|
||||
// 打开选择主机
|
||||
const openSelectHost = () => {
|
||||
hostModal.value.open(formModel.value.hostIdList);
|
||||
};
|
||||
|
||||
// 设置选中主机
|
||||
const setSelectedHost = (hosts: Array<number>) => {
|
||||
formModel.value.hostIdList = hosts;
|
||||
};
|
||||
|
||||
// 从执行模板设置
|
||||
const setWithTemplate = async ({ id }: ExecTemplateQueryResponse) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 查询模板信息
|
||||
const { data } = await getExecTemplateWithAuthorized(id);
|
||||
formModel.value = {
|
||||
...formModel.value,
|
||||
description: data.name,
|
||||
command: data.command,
|
||||
timeout: data.timeout,
|
||||
scriptExec: data.scriptExec,
|
||||
hostIdList: data.hostIdList,
|
||||
};
|
||||
parameterSchema.value = data.parameterSchema ? JSON.parse(data.parameterSchema) : [];
|
||||
if (parameterSchema.value.length) {
|
||||
parameterFormModel.value = parameterSchema.value.reduce((acc, cur) => ({
|
||||
...acc,
|
||||
[cur.name as string]: cur.value
|
||||
}), {});
|
||||
} else {
|
||||
parameterFormModel.value = {};
|
||||
}
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 从执行日志设置
|
||||
const setWithExecLog = (record: ExecLogQueryResponse) => {
|
||||
formModel.value = {
|
||||
...formModel.value,
|
||||
command: record.command,
|
||||
description: record.description,
|
||||
timeout: record.timeout,
|
||||
scriptExec: record.scriptExec,
|
||||
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 execCommand = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 验证参数
|
||||
let error = await formRef.value.validate();
|
||||
if (error) {
|
||||
return false;
|
||||
}
|
||||
error = await parameterFormRef.value?.validate();
|
||||
if (error) {
|
||||
return false;
|
||||
}
|
||||
if (!formModel.value.command) {
|
||||
Message.error('请输入命令');
|
||||
return false;
|
||||
}
|
||||
// 设置 schema
|
||||
for (let ps of parameterSchema.value) {
|
||||
ps.value = parameterFormModel.value[ps.name as string];
|
||||
}
|
||||
// 执行命令
|
||||
const request = {
|
||||
...formModel.value,
|
||||
parameterSchema: JSON.stringify(parameterSchema.value),
|
||||
};
|
||||
const { data } = await batchExecCommand(request);
|
||||
// 设置历史命令
|
||||
historyRef.value.add(request);
|
||||
Message.success('已开始执行');
|
||||
emits('submit', data);
|
||||
} catch (e) {
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 重置
|
||||
const resetForm = () => {
|
||||
formModel.value = Object.assign({}, { ...defaultForm() });
|
||||
parameterFormModel.value = {};
|
||||
parameterSchema.value = [];
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@form-width: 420px;
|
||||
@history-width: 320px;
|
||||
@command-gap: @form-width + @history-width + 32px;
|
||||
|
||||
.exec-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
position: absolute;
|
||||
|
||||
.exec-form-container {
|
||||
width: @form-width;
|
||||
}
|
||||
|
||||
.exec-command-container {
|
||||
width: calc(100% - @command-gap);
|
||||
margin: 0 16px;
|
||||
}
|
||||
|
||||
.exec-history-container {
|
||||
width: @history-width;
|
||||
}
|
||||
|
||||
.exec-form-container, .exec-command-container, .exec-history-container {
|
||||
background: var(--color-bg-2);
|
||||
border-radius: 4px;
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.exec-form-container {
|
||||
|
||||
.form-wrapper {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.selected-host {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
padding: 0 8px;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: var(--color-text-2);
|
||||
background: var(--color-fill-2);
|
||||
transition: all 0.3s;
|
||||
|
||||
&-count {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
margin: 0 6px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--color-fill-3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.panel-header) {
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
margin-bottom: 4px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
|
||||
h3, > span {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
81
orion-visor-ui/src/views/exec/exec-command/index.vue
Normal file
81
orion-visor-ui/src/views/exec/exec-command/index.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<div class="layout-container full">
|
||||
<!-- 执行面板 -->
|
||||
<div v-show="!logVisible" class="panel-wrapper">
|
||||
<exec-command-panel @submit="openLog" />
|
||||
</div>
|
||||
<!-- 执行日志 -->
|
||||
<div v-if="logVisible" class="panel-wrapper">
|
||||
<exec-log-panel ref="log"
|
||||
type="BATCH"
|
||||
:visibleBack="true"
|
||||
@back="setLogVisible(false)" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'execCommand'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ExecLogQueryResponse } from '@/api/exec/exec-log';
|
||||
import { nextTick, onMounted, ref } from 'vue';
|
||||
import useVisible from '@/hooks/visible';
|
||||
import { useDictStore } from '@/store';
|
||||
import { dictKeys } from '@/components/exec/log/const';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { getExecCommandLog } from '@/api/exec/exec-command-log';
|
||||
import ExecCommandPanel from './components/exec-command-panel.vue';
|
||||
import ExecLogPanel from '@/components/exec/log/panel/index.vue';
|
||||
|
||||
const { visible: logVisible, setVisible: setLogVisible } = useVisible();
|
||||
const route = useRoute();
|
||||
|
||||
const log = ref();
|
||||
|
||||
// 打开日志
|
||||
const openLog = (record: ExecLogQueryResponse) => {
|
||||
setLogVisible(true);
|
||||
nextTick(() => {
|
||||
log.value.open(record);
|
||||
});
|
||||
};
|
||||
|
||||
// 打开日志
|
||||
const openLogWithId = async (id: number) => {
|
||||
setLogVisible(true);
|
||||
// 查询日志
|
||||
const { data } = await getExecCommandLog(id);
|
||||
openLog(data);
|
||||
};
|
||||
|
||||
// 加载字典值
|
||||
onMounted(async () => {
|
||||
const dictStore = useDictStore();
|
||||
await dictStore.loadKeys(dictKeys);
|
||||
});
|
||||
|
||||
// 跳转日志
|
||||
onMounted(async () => {
|
||||
const idParam = route.query.id;
|
||||
const keyParam = route.query.key;
|
||||
if (idParam) {
|
||||
await openLogWithId(Number.parseInt(idParam as string));
|
||||
} else if (keyParam) {
|
||||
await openLogWithId(Number.parseInt(keyParam as string));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.panel-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,2 @@
|
||||
// 执行
|
||||
export const historyCount = 20;
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { FieldRule } from '@arco-design/web-vue';
|
||||
|
||||
export const description = [{
|
||||
maxLength: 128,
|
||||
message: '执行描述长度不能大于128位'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const hostIdList = [{
|
||||
required: true,
|
||||
message: '请选择执行主机'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const command = [{
|
||||
required: true,
|
||||
message: '请输入执行命令'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const timeout = [{
|
||||
required: true,
|
||||
message: '请输入超时时间'
|
||||
}, {
|
||||
type: 'number',
|
||||
min: 0,
|
||||
max: 100000,
|
||||
message: '超时时间需要在 0 - 100000 之间'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const scriptExec = [{
|
||||
required: true,
|
||||
message: '请选择是否使用脚本执行'
|
||||
}] as FieldRule[];
|
||||
|
||||
export default {
|
||||
description,
|
||||
hostIdList,
|
||||
command,
|
||||
timeout,
|
||||
scriptExec,
|
||||
} as Record<string, FieldRule | FieldRule[]>;
|
||||
@@ -0,0 +1,281 @@
|
||||
<template>
|
||||
<a-drawer v-model:visible="visible"
|
||||
title="执行命令"
|
||||
width="66%"
|
||||
:esc-to-close="false"
|
||||
:mask-closable="false"
|
||||
:unmount-on-close="true"
|
||||
:ok-button-props="{ disabled: loading }"
|
||||
:cancel-button-props="{ disabled: loading }"
|
||||
:on-before-ok="handlerOk"
|
||||
@cancel="handleClose">
|
||||
<a-spin class="full modal-form-small" :loading="loading">
|
||||
<!-- 命令表单 -->
|
||||
<a-form :model="formModel"
|
||||
ref="formRef"
|
||||
label-align="right"
|
||||
:auto-label-width="true"
|
||||
:rules="formRules">
|
||||
<a-row :gutter="16">
|
||||
<!-- 执行描述 -->
|
||||
<a-col :span="16">
|
||||
<a-form-item field="description"
|
||||
label="执行描述"
|
||||
:hide-asterisk="true">
|
||||
<a-input v-model="formModel.description"
|
||||
placeholder="请输入执行描述"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<!-- 超时时间 -->
|
||||
<a-col :span="8">
|
||||
<a-form-item field="timeout"
|
||||
label="超时时间"
|
||||
:hide-asterisk="true">
|
||||
<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-col>
|
||||
<!-- 执行主机 -->
|
||||
<a-col :span="16">
|
||||
<a-form-item field="hostIdList"
|
||||
label="执行主机"
|
||||
:hide-asterisk="true">
|
||||
<div class="selected-host">
|
||||
<!-- 已选择数量 -->
|
||||
<span class="usn" v-if="formModel.hostIdList?.length">
|
||||
已选择<span class="selected-host-count span-blue">{{ formModel.hostIdList?.length }}</span>台主机
|
||||
</span>
|
||||
<span class="usn pointer span-blue" @click="openSelectHost">
|
||||
{{ formModel.hostIdList?.length ? '重新选择' : '选择主机' }}
|
||||
</span>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<!-- 脚本执行 -->
|
||||
<a-col :span="8">
|
||||
<a-form-item field="scriptExec"
|
||||
label="脚本执行"
|
||||
:hide-asterisk="true">
|
||||
<div class="flex-center">
|
||||
<a-switch v-model="formModel.scriptExec"
|
||||
type="round"
|
||||
:checked-value="EnabledStatus.ENABLED"
|
||||
:unchecked-value="EnabledStatus.DISABLED" />
|
||||
<div class="question-right ml8">
|
||||
<a-tooltip position="tr" content="启用后会将命令写入脚本文件 传输到主机后执行">
|
||||
<icon-question-circle />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<!-- 执行命令 -->
|
||||
<a-col :span="24">
|
||||
<a-form-item field="command"
|
||||
label="执行命令"
|
||||
:hide-label="true"
|
||||
:help="'使用 @{{ xxx }} 来替换参数, 输入_可以获取全部变量'">
|
||||
<exec-editor v-model="formModel.command"
|
||||
container-class="command-editor"
|
||||
theme="vs-dark"
|
||||
:parameter="parameterSchema" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</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="left"
|
||||
:auto-label-width="true">
|
||||
<a-row :gutter="16">
|
||||
<a-col v-for="item in parameterSchema"
|
||||
:key="item.name"
|
||||
:span="12">
|
||||
<a-form-item :field="item.name as string"
|
||||
:label="item.name"
|
||||
required>
|
||||
<a-input v-model="parameterFormModel[item.name as string]"
|
||||
:placeholder="item.desc"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-spin>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'execTemplateExecDrawer'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TemplateParam } from '@/components/view/exec-editor/const';
|
||||
import type { ExecTemplateQueryResponse } from '@/api/exec/exec-template';
|
||||
import type { ExecCommandRequest } from '@/api/exec/exec-command';
|
||||
import { onUnmounted, ref } from 'vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import useVisible from '@/hooks/visible';
|
||||
import formRules from '../../exec-command/types/form.rules';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { EnabledStatus } from '@/types/const';
|
||||
import { batchExecCommand } from '@/api/exec/exec-command';
|
||||
import { getExecTemplateWithAuthorized } from '@/api/exec/exec-template';
|
||||
import ExecEditor from '@/components/view/exec-editor/index.vue';
|
||||
|
||||
const emits = defineEmits(['openHost']);
|
||||
|
||||
const { visible, setVisible } = useVisible();
|
||||
const { loading, setLoading } = useLoading();
|
||||
|
||||
const formRef = ref<any>();
|
||||
const parameterFormRef = ref<any>();
|
||||
const formModel = ref<ExecCommandRequest>({});
|
||||
const parameterFormModel = ref<Record<string, any>>({});
|
||||
const parameterSchema = ref<Array<TemplateParam>>([]);
|
||||
|
||||
// 打开
|
||||
const open = async (id: number) => {
|
||||
renderForm({} as ExecTemplateQueryResponse);
|
||||
setVisible(true);
|
||||
setLoading(true);
|
||||
try {
|
||||
// 查询模板信息
|
||||
const { data } = await getExecTemplateWithAuthorized(id);
|
||||
renderForm(data);
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 渲染表单
|
||||
const renderForm = (record: ExecTemplateQueryResponse) => {
|
||||
formModel.value = {
|
||||
description: record.name,
|
||||
timeout: record.timeout,
|
||||
scriptExec: record.scriptExec,
|
||||
command: record.command,
|
||||
hostIdList: record.hostIdList || [],
|
||||
};
|
||||
if (record.parameterSchema) {
|
||||
parameterSchema.value = JSON.parse(record.parameterSchema);
|
||||
const params = {} as any;
|
||||
for (let param of parameterSchema.value) {
|
||||
params[param.name as keyof any] = param.defaultValue;
|
||||
}
|
||||
parameterFormModel.value = params;
|
||||
} else {
|
||||
parameterSchema.value = [];
|
||||
parameterFormModel.value = {};
|
||||
}
|
||||
};
|
||||
|
||||
// 设置选中主机
|
||||
const setSelectedHost = (hosts: Array<number>) => {
|
||||
formModel.value.hostIdList = hosts;
|
||||
};
|
||||
|
||||
defineExpose({ open, setSelectedHost });
|
||||
|
||||
// 打开选择主机
|
||||
const openSelectHost = () => {
|
||||
emits('openHost', formModel.value.hostIdList);
|
||||
};
|
||||
|
||||
// 确定
|
||||
const handlerOk = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 验证参数
|
||||
let error = await formRef.value.validate();
|
||||
if (error) {
|
||||
return false;
|
||||
}
|
||||
error = await parameterFormRef.value?.validate();
|
||||
if (error) {
|
||||
return false;
|
||||
}
|
||||
// 设置 schema
|
||||
for (let ps of parameterSchema.value) {
|
||||
ps.value = parameterFormModel.value[ps.name as string];
|
||||
}
|
||||
// 执行命令
|
||||
await batchExecCommand({
|
||||
...formModel.value,
|
||||
parameterSchema: JSON.stringify(parameterSchema.value),
|
||||
});
|
||||
Message.success('已开始执行');
|
||||
// 清空
|
||||
handlerClear();
|
||||
} catch (e) {
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭
|
||||
const handleClose = () => {
|
||||
handlerClear();
|
||||
};
|
||||
|
||||
// 清空
|
||||
const handlerClear = () => {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
// 卸载关闭
|
||||
onUnmounted(handlerClear);
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.selected-host {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
padding: 0 8px;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: var(--color-fill-2);
|
||||
transition: all 0.3s;
|
||||
|
||||
&-count {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
margin: 0 6px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--color-fill-3);
|
||||
}
|
||||
}
|
||||
|
||||
.command-editor {
|
||||
width: 100%;
|
||||
height: 62vh;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,383 @@
|
||||
<template>
|
||||
<a-drawer v-model:visible="visible"
|
||||
width="66%"
|
||||
:title="title"
|
||||
:esc-to-close="false"
|
||||
:mask-closable="false"
|
||||
:unmount-on-close="true"
|
||||
:ok-button-props="{ disabled: loading }"
|
||||
:cancel-button-props="{ disabled: loading }"
|
||||
:on-before-ok="handlerOk"
|
||||
@cancel="handleClose">
|
||||
<a-spin class="full modal-form-small" :loading="loading">
|
||||
<a-form :model="formModel"
|
||||
ref="formRef"
|
||||
label-align="right"
|
||||
:auto-label-width="true"
|
||||
:rules="formRules">
|
||||
<a-row :gutter="16">
|
||||
<!-- 模板名称 -->
|
||||
<a-col :span="16">
|
||||
<a-form-item field="name"
|
||||
label="模板名称"
|
||||
:hide-asterisk="true">
|
||||
<a-input v-model="formModel.name"
|
||||
placeholder="请输入模板名称"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<!-- 超时时间 -->
|
||||
<a-col :span="8">
|
||||
<a-form-item field="timeout"
|
||||
label="超时时间"
|
||||
:hide-asterisk="true">
|
||||
<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-col>
|
||||
<!-- 默认主机 -->
|
||||
<a-col :span="16">
|
||||
<a-form-item field="hostIdList"
|
||||
label="默认主机"
|
||||
:hide-asterisk="true">
|
||||
<div class="selected-host">
|
||||
<!-- 已选择数量 -->
|
||||
<span class="usn" v-if="formModel.hostIdList?.length">
|
||||
已选择<span class="selected-host-count span-blue">{{ formModel.hostIdList?.length }}</span>台主机
|
||||
</span>
|
||||
<span class="usn pointer span-blue" @click="openSelectHost">
|
||||
{{ formModel.hostIdList?.length ? '重新选择' : '选择主机' }}
|
||||
</span>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<!-- 脚本执行 -->
|
||||
<a-col :span="8">
|
||||
<a-form-item field="scriptExec"
|
||||
label="脚本执行"
|
||||
:hide-asterisk="true">
|
||||
<div class="flex-center">
|
||||
<a-switch v-model="formModel.scriptExec"
|
||||
type="round"
|
||||
:checked-value="EnabledStatus.ENABLED"
|
||||
:unchecked-value="EnabledStatus.DISABLED" />
|
||||
<div class="question-right ml8">
|
||||
<a-tooltip position="tr" content="启用后会将命令写入脚本文件 传输到主机后执行">
|
||||
<icon-question-circle />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<!-- 模板命令 -->
|
||||
<a-col :span="24">
|
||||
<a-form-item field="command"
|
||||
label="模板命令"
|
||||
:hide-label="true"
|
||||
:help="'使用 @{{ xxx }} 来替换参数, 输入_可以获取全部变量'">
|
||||
<exec-editor v-model="formModel.command"
|
||||
container-class="command-editor"
|
||||
theme="vs-dark"
|
||||
:parameter="parameter" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<!-- 命令参数 -->
|
||||
<a-col :span="24">
|
||||
<a-form-item field="parameter"
|
||||
class="parameter-form-item"
|
||||
label="命令参数">
|
||||
<!-- label -->
|
||||
<template #label>
|
||||
<span class="span-blue pointer" @click="addParameter">添加参数</span>
|
||||
</template>
|
||||
<!-- 参数 -->
|
||||
<template v-if="parameter.length">
|
||||
<a-input-group v-for="(item, i) in parameter"
|
||||
:key="i"
|
||||
class="parameter-item"
|
||||
:class="[ i === parameter.length - 1 ? 'parameter-item-last' : '' ]">
|
||||
<a-input class="parameter-item-name"
|
||||
v-model="item.name"
|
||||
placeholder="参数名称 (必填)"
|
||||
allow-clear />
|
||||
<a-input class="parameter-item-default"
|
||||
v-model="item.defaultValue"
|
||||
placeholder="默认值 (非必填)"
|
||||
allow-clear />
|
||||
<a-input class="parameter-item-description"
|
||||
v-model="item.desc"
|
||||
placeholder="描述 (非必填)"
|
||||
allow-clear />
|
||||
<span class="parameter-item-close click-icon-wrapper"
|
||||
title="移除"
|
||||
@click="removeParameter(i)">
|
||||
<icon-close />
|
||||
</span>
|
||||
</a-input-group>
|
||||
</template>
|
||||
<!-- 无参数 -->
|
||||
<template v-else>
|
||||
<span class="no-parameter">
|
||||
<icon-empty class="mr4" />无参数
|
||||
</span>
|
||||
</template>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-spin>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'execTemplateFormDrawer'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TemplateParam } from '@/components/view/exec-editor/const';
|
||||
import type { ExecTemplateUpdateRequest } from '@/api/exec/exec-template';
|
||||
import { onUnmounted, ref } from 'vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import useVisible from '@/hooks/visible';
|
||||
import formRules from '../types/form.rules';
|
||||
import { createExecTemplate, getExecTemplateWithAuthorized, updateExecTemplate } from '@/api/exec/exec-template';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { EnabledStatus } from '@/types/const';
|
||||
import ExecEditor from '@/components/view/exec-editor/index.vue';
|
||||
|
||||
const { visible, setVisible } = useVisible();
|
||||
const { loading, setLoading } = useLoading();
|
||||
|
||||
const title = ref<string>();
|
||||
const isAddHandle = ref<boolean>(true);
|
||||
|
||||
const defaultForm = (): ExecTemplateUpdateRequest => {
|
||||
return {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
command: undefined,
|
||||
timeout: 0,
|
||||
scriptExec: EnabledStatus.DISABLED,
|
||||
parameterSchema: undefined,
|
||||
hostIdList: [],
|
||||
};
|
||||
};
|
||||
|
||||
const formRef = ref<any>();
|
||||
const formModel = ref<ExecTemplateUpdateRequest>({});
|
||||
const parameter = ref<Array<TemplateParam>>([]);
|
||||
|
||||
const emits = defineEmits(['added', 'updated', 'openHost']);
|
||||
|
||||
// 打开新增
|
||||
const openAdd = () => {
|
||||
title.value = '添加执行模板';
|
||||
isAddHandle.value = true;
|
||||
renderForm({ ...defaultForm() });
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
// 打开修改
|
||||
const openUpdate = async (id: number) => {
|
||||
title.value = '修改执行模板';
|
||||
isAddHandle.value = false;
|
||||
renderForm({ ...defaultForm() });
|
||||
setVisible(true);
|
||||
setLoading(true);
|
||||
try {
|
||||
// 查询模板信息
|
||||
const { data } = await getExecTemplateWithAuthorized(id);
|
||||
renderForm({ ...defaultForm(), ...data });
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 渲染表单
|
||||
const renderForm = (record: any) => {
|
||||
formModel.value = Object.assign({}, record);
|
||||
if (record.parameterSchema) {
|
||||
parameter.value = JSON.parse(record.parameterSchema);
|
||||
} else {
|
||||
parameter.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
// 设置选中主机
|
||||
const setSelectedHost = (hosts: Array<number>) => {
|
||||
formModel.value.hostIdList = hosts;
|
||||
};
|
||||
|
||||
defineExpose({ openAdd, openUpdate, setSelectedHost });
|
||||
|
||||
// 打开选择主机
|
||||
const openSelectHost = () => {
|
||||
emits('openHost', formModel.value.hostIdList || []);
|
||||
};
|
||||
|
||||
// 添加参数
|
||||
const addParameter = () => {
|
||||
parameter.value.push({});
|
||||
};
|
||||
|
||||
// 移除参数
|
||||
const removeParameter = (index: number) => {
|
||||
parameter.value.splice(index, 1);
|
||||
};
|
||||
|
||||
// 确定
|
||||
const handlerOk = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 验证参数
|
||||
const error = await formRef.value.validate();
|
||||
if (error) {
|
||||
return false;
|
||||
}
|
||||
// 验证并设置命令参数
|
||||
for (const p of parameter.value) {
|
||||
if (!p.name) {
|
||||
Message.warning('请补全命令参数');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
formModel.value.parameterSchema = JSON.stringify(parameter.value);
|
||||
if (isAddHandle.value) {
|
||||
// 新增
|
||||
await createExecTemplate(formModel.value);
|
||||
Message.success('创建成功');
|
||||
emits('added');
|
||||
} else {
|
||||
// 修改
|
||||
await updateExecTemplate(formModel.value);
|
||||
Message.success('修改成功');
|
||||
emits('updated');
|
||||
}
|
||||
// 清空
|
||||
handlerClear();
|
||||
} catch (e) {
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭
|
||||
const handleClose = () => {
|
||||
handlerClear();
|
||||
};
|
||||
|
||||
// 清空
|
||||
const handlerClear = () => {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
// 卸载关闭
|
||||
onUnmounted(handlerClear);
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.selected-host {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
padding: 0 8px;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: var(--color-fill-2);
|
||||
transition: all 0.3s;
|
||||
|
||||
&-count {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
margin: 0 6px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--color-fill-3);
|
||||
}
|
||||
}
|
||||
|
||||
.parameter-form-item {
|
||||
user-select: none;
|
||||
margin-top: 4px;
|
||||
|
||||
:deep(.arco-form-item-content) {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.parameter-item-last {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.parameter-item {
|
||||
width: 100%;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
& > span {
|
||||
border-radius: 2px;
|
||||
border-right-color: transparent;
|
||||
}
|
||||
|
||||
&-name {
|
||||
width: 29%;
|
||||
}
|
||||
|
||||
&-default {
|
||||
width: 29%;
|
||||
}
|
||||
|
||||
&-description {
|
||||
width: calc(39% - 44px);
|
||||
}
|
||||
|
||||
&-close {
|
||||
cursor: pointer;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 16px;
|
||||
background: var(--color-fill-2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-fill-3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-parameter {
|
||||
background: var(--color-fill-2);
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
}
|
||||
|
||||
.command-editor {
|
||||
width: 100%;
|
||||
height: 55vh;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<!-- 搜索 -->
|
||||
<a-card class="general-card table-search-card">
|
||||
<query-header :model="formModel"
|
||||
label-align="left"
|
||||
@submit="fetchTableData"
|
||||
@reset="fetchTableData"
|
||||
@keyup.enter="() => fetchTableData()">
|
||||
<!-- id -->
|
||||
<a-form-item field="id" label="id">
|
||||
<a-input-number v-model="formModel.id"
|
||||
placeholder="请输入id"
|
||||
allow-clear
|
||||
hide-button />
|
||||
</a-form-item>
|
||||
<!-- 模板名称 -->
|
||||
<a-form-item field="name" label="模板名称">
|
||||
<a-input v-model="formModel.name"
|
||||
placeholder="请输入模板名称"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 模板命令 -->
|
||||
<a-form-item field="command" label="模板命令">
|
||||
<a-input v-model="formModel.command"
|
||||
placeholder="请输入模板命令"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
</query-header>
|
||||
</a-card>
|
||||
<!-- 表格 -->
|
||||
<a-card class="general-card table-card">
|
||||
<template #title>
|
||||
<!-- 左侧操作 -->
|
||||
<div class="table-left-bar-handle">
|
||||
<!-- 标题 -->
|
||||
<div class="table-title">
|
||||
执行模板列表
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧操作 -->
|
||||
<div class="table-right-bar-handle">
|
||||
<a-space>
|
||||
<!-- 新增 -->
|
||||
<a-button v-permission="['asset:exec-template:create']"
|
||||
type="primary"
|
||||
@click="emits('openAdd')">
|
||||
新增
|
||||
<template #icon>
|
||||
<icon-plus />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
<!-- table -->
|
||||
<a-table row-key="id"
|
||||
ref="tableRef"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:data="tableRenderData"
|
||||
:pagination="pagination"
|
||||
@page-change="(page) => fetchTableData(page, pagination.pageSize)"
|
||||
@page-size-change="(size) => fetchTableData(1, size)"
|
||||
:bordered="false">
|
||||
<!-- 模板命令 -->
|
||||
<template #command="{ record }">
|
||||
<span class="copy-left" @click="copy(record.command, '已复制')">
|
||||
<icon-copy />
|
||||
</span>
|
||||
<span :title="record.command">{{ record.command }}</span>
|
||||
</template>
|
||||
<!-- 操作 -->
|
||||
<template #handle="{ record }">
|
||||
<div class="table-handle-wrapper">
|
||||
<a-button v-permission="['asset:exec-command:exec']"
|
||||
type="text"
|
||||
size="mini"
|
||||
@click="emits('openExec', record.id)">
|
||||
执行
|
||||
</a-button>
|
||||
<!-- 修改 -->
|
||||
<a-button v-permission="['asset:exec-template:update']"
|
||||
type="text"
|
||||
size="mini"
|
||||
@click="emits('openUpdate', record.id)">
|
||||
修改
|
||||
</a-button>
|
||||
<!-- 删除 -->
|
||||
<a-popconfirm content="确认删除这条记录吗?"
|
||||
position="left"
|
||||
type="warning"
|
||||
@ok="deleteRow(record)">
|
||||
<a-button v-permission="['asset:exec-template:delete']"
|
||||
type="text"
|
||||
size="mini"
|
||||
status="danger">
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'execTemplateTable'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ExecTemplateQueryRequest, ExecTemplateQueryResponse } from '@/api/exec/exec-template';
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import { deleteExecTemplate, getExecTemplatePage } from '@/api/exec/exec-template';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import columns from '../types/table.columns';
|
||||
import { usePagination } from '@/types/table';
|
||||
import { copy } from '@/hooks/copy';
|
||||
|
||||
const emits = defineEmits(['openAdd', 'openUpdate', 'openExec']);
|
||||
|
||||
const pagination = usePagination();
|
||||
const { loading, setLoading } = useLoading();
|
||||
|
||||
const tableRenderData = ref<ExecTemplateQueryResponse[]>([]);
|
||||
const formModel = reactive<ExecTemplateQueryRequest>({
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
command: undefined,
|
||||
});
|
||||
|
||||
// 删除当前行
|
||||
const deleteRow = async ({ id }: {
|
||||
id: number
|
||||
}) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// 调用删除接口
|
||||
await deleteExecTemplate(id);
|
||||
Message.success('删除成功');
|
||||
// 重新加载数据
|
||||
fetchTableData();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 添加后回调
|
||||
const addedCallback = () => {
|
||||
fetchTableData();
|
||||
};
|
||||
|
||||
// 更新后回调
|
||||
const updatedCallback = () => {
|
||||
fetchTableData();
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
addedCallback, updatedCallback
|
||||
});
|
||||
|
||||
// 加载数据
|
||||
const doFetchTableData = async (request: ExecTemplateQueryRequest) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data } = await getExecTemplatePage(request);
|
||||
tableRenderData.value = data.rows;
|
||||
pagination.total = data.total;
|
||||
pagination.current = request.page;
|
||||
pagination.pageSize = request.limit;
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 切换页码
|
||||
const fetchTableData = (page = 1, limit = pagination.pageSize, form = formModel) => {
|
||||
doFetchTableData({ page, limit, ...form });
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchTableData();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
77
orion-visor-ui/src/views/exec/exec-template/index.vue
Normal file
77
orion-visor-ui/src/views/exec/exec-template/index.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div class="layout-container" v-if="render">
|
||||
<!-- 列表-表格 -->
|
||||
<exec-template-table ref="table"
|
||||
@open-add="() => drawer.openAdd()"
|
||||
@open-update="(e) => drawer.openUpdate(e)"
|
||||
@open-exec="(e) => execModal.open(e)" />
|
||||
<!-- 添加修改模态框 -->
|
||||
<exec-template-form-drawer ref="drawer"
|
||||
@added="modalAddCallback"
|
||||
@updated="modalUpdateCallback"
|
||||
@open-host="(e) => openHostModal('drawer', e)" />
|
||||
<!-- 执行模态框 -->
|
||||
<exec-template-exec-drawer ref="execModal"
|
||||
@open-host="(e) => openHostModal('exec', e)" />
|
||||
<!-- 主机模态框 -->
|
||||
<authorized-host-modal ref="hostModal"
|
||||
@selected="hostSelected" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'execTemplate'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onBeforeMount } from 'vue';
|
||||
import ExecTemplateTable from './components/exec-template-table.vue';
|
||||
import ExecTemplateFormDrawer from './components/exec-template-form-drawer.vue';
|
||||
import ExecTemplateExecDrawer from './components/exec-template-exec-drawer.vue';
|
||||
import AuthorizedHostModal from '@/components/asset/host/authorized-host-modal/index.vue';
|
||||
|
||||
const render = ref(false);
|
||||
const table = ref();
|
||||
const drawer = ref();
|
||||
const execModal = ref();
|
||||
const hostModal = ref();
|
||||
const lastOpenHostRef = ref();
|
||||
|
||||
// 添加回调
|
||||
const modalAddCallback = () => {
|
||||
table.value.addedCallback();
|
||||
};
|
||||
|
||||
// 修改回调
|
||||
const modalUpdateCallback = () => {
|
||||
table.value.updatedCallback();
|
||||
};
|
||||
|
||||
// 打开主机模态框
|
||||
const openHostModal = (openRef: string, data: any) => {
|
||||
lastOpenHostRef.value = openRef;
|
||||
hostModal.value.open(data);
|
||||
};
|
||||
|
||||
// 选中主机
|
||||
const hostSelected = (data: any) => {
|
||||
if (lastOpenHostRef.value === 'drawer') {
|
||||
// 设置选中的主机
|
||||
drawer.value.setSelectedHost(data);
|
||||
} else if (lastOpenHostRef.value === 'exec') {
|
||||
// 设置选中的主机
|
||||
execModal.value.setSelectedHost(data);
|
||||
}
|
||||
};
|
||||
|
||||
onBeforeMount(async () => {
|
||||
render.value = true;
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { FieldRule } from '@arco-design/web-vue';
|
||||
|
||||
export const name = [{
|
||||
required: true,
|
||||
message: '请输入模板名称'
|
||||
}, {
|
||||
maxLength: 64,
|
||||
message: '模板名称长度不能大于64位'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const command = [{
|
||||
required: true,
|
||||
message: '请输入模板命令'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const timeout = [{
|
||||
required: true,
|
||||
message: '请输入超时时间'
|
||||
}, {
|
||||
type: 'number',
|
||||
min: 0,
|
||||
max: 100000,
|
||||
message: '超时时间需要在 0 - 100000 之间'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const scriptExec = [{
|
||||
required: true,
|
||||
message: '请选择是否使用脚本执行'
|
||||
}] as FieldRule[];
|
||||
|
||||
export default {
|
||||
name,
|
||||
command,
|
||||
timeout,
|
||||
scriptExec,
|
||||
} as Record<string, FieldRule | FieldRule[]>;
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface';
|
||||
import { dateFormat } from '@/utils';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'id',
|
||||
dataIndex: 'id',
|
||||
slotName: 'id',
|
||||
width: 70,
|
||||
align: 'left',
|
||||
fixed: 'left',
|
||||
}, {
|
||||
title: '模板名称',
|
||||
dataIndex: 'name',
|
||||
slotName: 'name',
|
||||
align: 'left',
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
}, {
|
||||
title: '模板命令',
|
||||
dataIndex: 'command',
|
||||
slotName: 'command',
|
||||
align: 'left',
|
||||
ellipsis: true,
|
||||
}, {
|
||||
title: '修改时间',
|
||||
dataIndex: 'updateTime',
|
||||
slotName: 'updateTime',
|
||||
align: 'center',
|
||||
width: 180,
|
||||
render: ({ record }) => {
|
||||
return dateFormat(new Date(record.updateTime));
|
||||
},
|
||||
}, {
|
||||
title: '操作',
|
||||
slotName: 'handle',
|
||||
width: 180,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
},
|
||||
] as TableColumnData[];
|
||||
|
||||
export default columns;
|
||||
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<a-modal v-model:visible="visible"
|
||||
body-class="modal-form-large"
|
||||
title-align="start"
|
||||
title="清理上传任务"
|
||||
:top="80"
|
||||
:align-center="false"
|
||||
:draggable="true"
|
||||
:mask-closable="false"
|
||||
:unmount-on-close="true"
|
||||
:ok-button-props="{ disabled: loading }"
|
||||
:cancel-button-props="{ disabled: loading }"
|
||||
:on-before-ok="handlerOk"
|
||||
@close="handleClose">
|
||||
<a-spin class="full" :loading="loading">
|
||||
<a-form :model="formModel"
|
||||
ref="formRef"
|
||||
label-align="right"
|
||||
:auto-label-width="true">
|
||||
<!-- 上传时间 -->
|
||||
<a-form-item field="createTimeRange" label="上传时间">
|
||||
<a-range-picker v-model="formModel.createTimeRange"
|
||||
:time-picker-props="{ defaultValue: ['00:00:00', '23:59:59'] }"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss" />
|
||||
</a-form-item>
|
||||
<!-- 上传用户 -->
|
||||
<a-form-item field="userId" label="上传用户">
|
||||
<user-selector v-model="formModel.userId"
|
||||
placeholder="请选择上传用户"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 远程路径 -->
|
||||
<a-form-item field="remotePath" label="远程路径">
|
||||
<a-input v-model="formModel.remotePath"
|
||||
placeholder="请输入远程路径"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 上传描述 -->
|
||||
<a-form-item field="description" label="上传描述">
|
||||
<a-input v-model="formModel.description"
|
||||
placeholder="请输入上传描述"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 上传状态 -->
|
||||
<a-form-item field="status" label="上传状态">
|
||||
<a-select v-model="formModel.status"
|
||||
:options="toOptions(uploadTaskStatusKey)"
|
||||
placeholder="请选择状态"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'uploadTaskFormModal'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { UploadTaskQueryRequest } from '@/api/exec/upload-task';
|
||||
import { ref } from 'vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import useVisible from '@/hooks/visible';
|
||||
import { uploadTaskStatusKey } from '../types/const';
|
||||
import { getUploadTaskCount, clearUploadTask } from '@/api/exec/upload-task';
|
||||
import { Message, Modal } from '@arco-design/web-vue';
|
||||
import { useDictStore } from '@/store';
|
||||
import UserSelector from '@/components/user/user/selector/index.vue';
|
||||
|
||||
const emits = defineEmits(['clear']);
|
||||
|
||||
const { visible, setVisible } = useVisible();
|
||||
const { loading, setLoading } = useLoading();
|
||||
const { toOptions } = useDictStore();
|
||||
|
||||
const formRef = ref<any>();
|
||||
const formModel = ref<UploadTaskQueryRequest>({});
|
||||
|
||||
const defaultForm = (): UploadTaskQueryRequest => {
|
||||
return {
|
||||
userId: undefined,
|
||||
remotePath: undefined,
|
||||
description: undefined,
|
||||
status: undefined,
|
||||
createTimeRange: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
// 打开
|
||||
const open = (record: any) => {
|
||||
renderForm({ ...defaultForm(), ...record });
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
// 渲染表单
|
||||
const renderForm = (record: any) => {
|
||||
formModel.value = Object.assign({}, record);
|
||||
};
|
||||
|
||||
defineExpose({ open });
|
||||
|
||||
// 确定
|
||||
const handlerOk = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 获取总数量
|
||||
const { data } = await getUploadTaskCount(formModel.value);
|
||||
if (data) {
|
||||
// 清空
|
||||
doClear(data);
|
||||
} else {
|
||||
// 无数据
|
||||
Message.warning('当前条件未查询到数据');
|
||||
}
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// 执行删除
|
||||
const doClear = (count: number) => {
|
||||
Modal.confirm({
|
||||
title: '删除清空',
|
||||
content: `确定要删除 ${count} 条数据吗? 确定后将立即删除且无法恢复!`,
|
||||
onOk: async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 调用删除
|
||||
await clearUploadTask(formModel.value);
|
||||
emits('clear');
|
||||
// 清空
|
||||
setVisible(false);
|
||||
handlerClear();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 关闭
|
||||
const handleClose = () => {
|
||||
handlerClear();
|
||||
};
|
||||
|
||||
// 清空
|
||||
const handlerClear = () => {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,325 @@
|
||||
<template>
|
||||
<!-- 搜索 -->
|
||||
<a-card class="general-card table-search-card">
|
||||
<query-header :model="formModel"
|
||||
label-align="left"
|
||||
:itemOptions="{ 5: { span: 2 } }"
|
||||
@submit="fetchTableData"
|
||||
@reset="fetchTableData"
|
||||
@keyup.enter="() => fetchTableData()">
|
||||
<!-- id -->
|
||||
<a-form-item field="id" label="id">
|
||||
<a-input-number v-model="formModel.id"
|
||||
placeholder="请输入id"
|
||||
allow-clear
|
||||
hide-button />
|
||||
</a-form-item>
|
||||
<!-- 上传用户 -->
|
||||
<a-form-item field="userId" label="上传用户">
|
||||
<user-selector v-model="formModel.userId"
|
||||
placeholder="请选择上传用户"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 远程路径 -->
|
||||
<a-form-item field="remotePath" label="远程路径">
|
||||
<a-input v-model="formModel.remotePath"
|
||||
placeholder="请输入远程路径"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 上传描述 -->
|
||||
<a-form-item field="description" label="上传描述">
|
||||
<a-input v-model="formModel.description"
|
||||
placeholder="请输入上传描述"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 上传状态 -->
|
||||
<a-form-item field="status" label="上传状态">
|
||||
<a-select v-model="formModel.status"
|
||||
:options="toOptions(uploadTaskStatusKey)"
|
||||
placeholder="请选择状态"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 上传时间 -->
|
||||
<a-form-item field="createTimeRange" label="上传时间">
|
||||
<a-range-picker v-model="formModel.createTimeRange"
|
||||
:time-picker-props="{ defaultValue: ['00:00:00', '23:59:59'] }"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss" />
|
||||
</a-form-item>
|
||||
</query-header>
|
||||
</a-card>
|
||||
<!-- 表格 -->
|
||||
<a-card class="general-card table-card">
|
||||
<template #title>
|
||||
<!-- 左侧操作 -->
|
||||
<div class="table-left-bar-handle">
|
||||
<!-- 标题 -->
|
||||
<div class="table-title">
|
||||
上传任务列表
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧操作 -->
|
||||
<div class="table-right-bar-handle">
|
||||
<a-space>
|
||||
<!-- 上传 -->
|
||||
<a-button v-permission="['asset:upload-task:upload']"
|
||||
type="primary"
|
||||
@click="$router.push({ name: 'batchUpload' })">
|
||||
上传
|
||||
<template #icon>
|
||||
<icon-upload />
|
||||
</template>
|
||||
</a-button>
|
||||
<!-- 清空 -->
|
||||
<a-button v-permission="['asset:upload-task:management:clear']"
|
||||
status="danger"
|
||||
@click="openClear">
|
||||
清空
|
||||
<template #icon>
|
||||
<icon-close />
|
||||
</template>
|
||||
</a-button>
|
||||
<!-- 删除 -->
|
||||
<a-popconfirm :content="`确认删除选中的 ${selectedKeys.length} 条记录吗?`"
|
||||
position="br"
|
||||
type="warning"
|
||||
@ok="deleteSelectRows">
|
||||
<a-button v-permission="['asset:upload-task:delete']"
|
||||
type="secondary"
|
||||
status="danger"
|
||||
:disabled="selectedKeys.length === 0">
|
||||
删除
|
||||
<template #icon>
|
||||
<icon-delete />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
<!-- table -->
|
||||
<a-table row-key="id"
|
||||
ref="tableRef"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
v-model:selected-keys="selectedKeys"
|
||||
:row-selection="rowSelection"
|
||||
:data="tableRenderData"
|
||||
:pagination="pagination"
|
||||
:bordered="false"
|
||||
@page-change="(page) => fetchTableData(page, pagination.pageSize)"
|
||||
@page-size-change="(size) => fetchTableData(1, size)">
|
||||
<!-- 上传路径 -->
|
||||
<template #remotePath="{ record }">
|
||||
<span class="text-copy span-blue" @click="copy(record.remotePath)">
|
||||
{{ record.remotePath }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- 文件数量 -->
|
||||
<template #fileCount="{ record }">
|
||||
<span class="span-blue">
|
||||
{{ record.fileCount }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- 主机数量 -->
|
||||
<template #hostCount="{ record }">
|
||||
<span class="span-blue">
|
||||
{{ record.hostCount }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- 上传状态 -->
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="getDictValue(uploadTaskStatusKey, record.status, 'color')">
|
||||
{{ getDictValue(uploadTaskStatusKey, record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<!-- 操作 -->
|
||||
<template #handle="{ record }">
|
||||
<div class="table-handle-wrapper">
|
||||
<!-- 详情 -->
|
||||
<a-button v-permission="['asset:upload-task:query']"
|
||||
type="text"
|
||||
size="mini"
|
||||
@click="$router.push({ name: 'batchUpload', query: { id: record.id } })">
|
||||
详情
|
||||
</a-button>
|
||||
<!-- 取消 -->
|
||||
<a-popconfirm v-if="record.status === UploadTaskStatus.WAITING || record.status === UploadTaskStatus.UPLOADING"
|
||||
content="确定要取消上传吗?"
|
||||
position="left"
|
||||
type="warning"
|
||||
@ok="doCancel(record)">
|
||||
<a-button v-permission="['asset:upload-task:upload']"
|
||||
type="text"
|
||||
size="mini">
|
||||
取消
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
<!-- 删除 -->
|
||||
<a-popconfirm content="确认删除这条记录吗?"
|
||||
position="left"
|
||||
type="warning"
|
||||
@ok="deleteRow(record)">
|
||||
<a-button v-permission="['asset:upload-task:delete']"
|
||||
type="text"
|
||||
size="mini"
|
||||
status="danger">
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'uploadTaskTable'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { UploadTaskQueryRequest, UploadTaskQueryResponse } from '@/api/exec/upload-task';
|
||||
import { reactive, ref, onMounted, onUnmounted } from 'vue';
|
||||
import { batchDeleteUploadTask, deleteUploadTask, getUploadTaskPage, cancelUploadTask, getUploadTaskStatus } from '@/api/exec/upload-task';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import columns from '../types/table.columns';
|
||||
import { UploadTaskStatus, uploadTaskStatusKey } from '../types/const';
|
||||
import { usePagination, useRowSelection } from '@/types/table';
|
||||
import { useDictStore } from '@/store';
|
||||
import { copy } from '@/hooks/copy';
|
||||
import UserSelector from '@/components/user/user/selector/index.vue';
|
||||
|
||||
const emits = defineEmits(['openClear']);
|
||||
|
||||
const pagination = usePagination();
|
||||
const rowSelection = useRowSelection();
|
||||
const { loading, setLoading } = useLoading();
|
||||
const { toOptions, getDictValue } = useDictStore();
|
||||
|
||||
const pullIntervalId = ref();
|
||||
const selectedKeys = ref<number[]>([]);
|
||||
const tableRenderData = ref<UploadTaskQueryResponse[]>([]);
|
||||
const formModel = reactive<UploadTaskQueryRequest>({
|
||||
id: undefined,
|
||||
userId: undefined,
|
||||
remotePath: undefined,
|
||||
description: undefined,
|
||||
status: undefined,
|
||||
createTimeRange: undefined,
|
||||
});
|
||||
|
||||
// 打开清理
|
||||
const openClear = () => {
|
||||
emits('openClear', { ...formModel, id: undefined });
|
||||
};
|
||||
|
||||
// 执行取消
|
||||
const doCancel = async (record: UploadTaskQueryResponse) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// 取消
|
||||
await cancelUploadTask(record.id, false);
|
||||
// 设置状态
|
||||
record.status = UploadTaskStatus.CANCELED;
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除选中行
|
||||
const deleteSelectRows = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// 调用删除接口
|
||||
await batchDeleteUploadTask(selectedKeys.value);
|
||||
Message.success(`成功删除 ${selectedKeys.value.length} 条数据`);
|
||||
selectedKeys.value = [];
|
||||
// 重新加载数据
|
||||
fetchTableData();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除当前行
|
||||
const deleteRow = async ({ id }: {
|
||||
id: number
|
||||
}) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// 调用删除接口
|
||||
await deleteUploadTask(id);
|
||||
Message.success('删除成功');
|
||||
// 重新加载数据
|
||||
fetchTableData();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载状态
|
||||
const pullTaskStatus = async () => {
|
||||
const unCompleteIdList = tableRenderData.value
|
||||
.filter(s => s.status === UploadTaskStatus.WAITING || s.status === UploadTaskStatus.UPLOADING)
|
||||
.map(s => s.id);
|
||||
if (!unCompleteIdList.length) {
|
||||
return;
|
||||
}
|
||||
// 加载未完成的状态
|
||||
const { data } = await getUploadTaskStatus(unCompleteIdList, false);
|
||||
data.forEach(s => {
|
||||
const tableRow = tableRenderData.value.find(r => r.id === s.id);
|
||||
if (!tableRow) {
|
||||
return;
|
||||
}
|
||||
tableRow.status = s.status;
|
||||
});
|
||||
};
|
||||
|
||||
// 加载数据
|
||||
const doFetchTableData = async (request: UploadTaskQueryRequest) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data } = await getUploadTaskPage(request);
|
||||
tableRenderData.value = data.rows;
|
||||
pagination.total = data.total;
|
||||
pagination.current = request.page;
|
||||
pagination.pageSize = request.limit;
|
||||
selectedKeys.value = [];
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 切换页码
|
||||
const fetchTableData = (page = 1, limit = pagination.pageSize, form = formModel) => {
|
||||
doFetchTableData({ page, limit, ...form });
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
fetchTableData
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 加载数据
|
||||
fetchTableData();
|
||||
// 注册状态轮询
|
||||
pullIntervalId.value = setInterval(pullTaskStatus, 10000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// 卸载状态轮询
|
||||
clearInterval(pullIntervalId.value);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
||||
45
orion-visor-ui/src/views/exec/upload-task/index.vue
Normal file
45
orion-visor-ui/src/views/exec/upload-task/index.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div class="layout-container" v-if="render">
|
||||
<!-- 列表-表格 -->
|
||||
<upload-task-table ref="table"
|
||||
@open-clear="(e) => clear.open(e)" />
|
||||
<!-- 清理模态框 -->
|
||||
<upload-task-clear-modal ref="clear"
|
||||
@clear="clearCallback" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'uploadTask'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onBeforeMount } from 'vue';
|
||||
import { useDictStore } from '@/store';
|
||||
import { dictKeys } from './types/const';
|
||||
import UploadTaskTable from './components/upload-task-table.vue';
|
||||
import UploadTaskClearModal from './components/upload-task-clear-modal.vue';
|
||||
|
||||
const render = ref(false);
|
||||
const table = ref();
|
||||
const clear = ref();
|
||||
|
||||
// 清理回调
|
||||
const clearCallback = () => {
|
||||
table.value.fetchTableData();
|
||||
};
|
||||
|
||||
// 加载字典值
|
||||
onBeforeMount(async () => {
|
||||
const dictStore = useDictStore();
|
||||
await dictStore.loadKeys(dictKeys);
|
||||
render.value = true;
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
33
orion-visor-ui/src/views/exec/upload-task/types/const.ts
Normal file
33
orion-visor-ui/src/views/exec/upload-task/types/const.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
// 上传任务状态
|
||||
export const UploadTaskStatus = {
|
||||
// 等待中
|
||||
WAITING: 'WAITING',
|
||||
// 上传中
|
||||
UPLOADING: 'UPLOADING',
|
||||
// 已完成
|
||||
FINISHED: 'FINISHED',
|
||||
// 已完成
|
||||
FAILED: 'FAILED',
|
||||
// 已取消
|
||||
CANCELED: 'CANCELED',
|
||||
};
|
||||
|
||||
// 上传任务文件状态
|
||||
export const UploadTaskFileStatus = {
|
||||
// 等待中
|
||||
WAITING: 'WAITING',
|
||||
// 上传中
|
||||
UPLOADING: 'UPLOADING',
|
||||
// 已完成
|
||||
FINISHED: 'FINISHED',
|
||||
// 已完成
|
||||
FAILED: 'FAILED',
|
||||
// 已取消
|
||||
CANCELED: 'CANCELED',
|
||||
};
|
||||
|
||||
// 上传任务状态 字典项
|
||||
export const uploadTaskStatusKey = 'uploadTaskStatus';
|
||||
|
||||
// 加载的字典值
|
||||
export const dictKeys = [uploadTaskStatusKey];
|
||||
@@ -0,0 +1,70 @@
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface';
|
||||
import { dateFormat } from '@/utils';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'id',
|
||||
dataIndex: 'id',
|
||||
slotName: 'id',
|
||||
width: 70,
|
||||
align: 'left',
|
||||
fixed: 'left',
|
||||
}, {
|
||||
title: '上传用户',
|
||||
dataIndex: 'username',
|
||||
slotName: 'username',
|
||||
align: 'left',
|
||||
width: 118,
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
}, {
|
||||
title: '上传描述',
|
||||
dataIndex: 'description',
|
||||
slotName: 'description',
|
||||
align: 'left',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
}, {
|
||||
title: '远程路径',
|
||||
dataIndex: 'remotePath',
|
||||
slotName: 'remotePath',
|
||||
align: 'left',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
}, {
|
||||
title: '上传状态',
|
||||
dataIndex: 'status',
|
||||
slotName: 'status',
|
||||
align: 'center',
|
||||
width: 138,
|
||||
}, {
|
||||
title: '文件数量',
|
||||
dataIndex: 'fileCount',
|
||||
slotName: 'fileCount',
|
||||
width: 98,
|
||||
align: 'center',
|
||||
}, {
|
||||
title: '主机数量',
|
||||
dataIndex: 'hostCount',
|
||||
slotName: 'hostCount',
|
||||
width: 98,
|
||||
align: 'center',
|
||||
}, {
|
||||
title: '上传时间',
|
||||
dataIndex: 'createTime',
|
||||
slotName: 'createTime',
|
||||
align: 'center',
|
||||
width: 180,
|
||||
render: ({ record }) => {
|
||||
return dateFormat(new Date(record.createTime));
|
||||
},
|
||||
}, {
|
||||
title: '操作',
|
||||
slotName: 'handle',
|
||||
width: 180,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
},
|
||||
] as TableColumnData[];
|
||||
|
||||
export default columns;
|
||||
Reference in New Issue
Block a user