🔨 批量上传.

This commit is contained in:
lijiahang
2024-05-10 18:58:48 +08:00
parent 564e40a31d
commit 0a43e5db45
18 changed files with 557 additions and 135 deletions

View File

@@ -40,7 +40,7 @@ import java.util.List;
@SuppressWarnings({"ELValidationInJSP", "SpringElInspection"}) @SuppressWarnings({"ELValidationInJSP", "SpringElInspection"})
public class UploadTaskController { public class UploadTaskController {
// TODO 测试空文件上传 0B 取消怎么那么慢 是不是删除也慢 异步cancel cancel 需要设置子元素为 cancel // TODO 前端日志 测试删除慢吗
@Resource @Resource
private UploadTaskService uploadTaskService; private UploadTaskService uploadTaskService;

View File

@@ -7,7 +7,6 @@ import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import java.io.Serializable; import java.io.Serializable;
import java.util.List;
/** /**
* 上传任务 视图响应对象 * 上传任务 视图响应对象
@@ -31,7 +30,4 @@ public class UploadTaskCreateVO implements Serializable {
@Schema(description = "上传 token") @Schema(description = "上传 token")
private String token; private String token;
@Schema(description = "主机")
private List<HostBaseVO> hosts;
} }

View File

@@ -43,6 +43,9 @@ public class UploadTaskFileVO implements Serializable {
@Schema(description = "文件大小") @Schema(description = "文件大小")
private Long fileSize; private Long fileSize;
@Schema(description = "额外信息")
private String extraInfo;
@Schema(description = "状态") @Schema(description = "状态")
private String status; private String status;

View File

@@ -0,0 +1,43 @@
package com.orion.ops.module.asset.entity.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
/**
* 上传任务主机 视图响应对象
*
* @author Jiahang Li
* @version 1.0.7
* @since 2024-5-8 10:31
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(name = "UploadTaskHostVO", description = "上传任务主机 视图响应对象")
public class UploadTaskHostVO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "id")
private Long id;
@Schema(description = "主机名称")
private String name;
@Schema(description = "主机编码")
private String code;
@Schema(description = "主机地址")
private String address;
@Schema(description = "上传文件")
private List<UploadTaskFileVO> files;
}

View File

@@ -56,7 +56,7 @@ public class UploadTaskVO implements Serializable {
@Schema(description = "创建时间") @Schema(description = "创建时间")
private Date createTime; private Date createTime;
@Schema(description = "上传文件") @Schema(description = "上传主机及文件")
private List<UploadTaskFileVO> files; private List<UploadTaskHostVO> hosts;
} }

View File

@@ -128,7 +128,6 @@ public class SftpSession extends TerminalSession implements ISftpSession {
} catch (Exception e) { } catch (Exception e) {
throw Exceptions.ioRuntime(e); throw Exceptions.ioRuntime(e);
} finally { } finally {
// TODO Test
// 关闭 inputStream 可能会被阻塞 ???...??? 只能关闭 executor // 关闭 inputStream 可能会被阻塞 ???...??? 只能关闭 executor
Streams.close(this.executor); Streams.close(this.executor);
this.connect(); this.connect();

View File

@@ -141,6 +141,9 @@ public class FileUploadTask implements IFileUploadTask {
.current(0L) .current(0L)
.build()) .build())
.collect(Collectors.toList()); .collect(Collectors.toList());
if (files.isEmpty()) {
return;
}
// 添加到上传器 // 添加到上传器
uploaderList.add(new FileUploader(id, k, files)); uploaderList.add(new FileUploader(id, k, files));
}); });
@@ -150,6 +153,10 @@ public class FileUploadTask implements IFileUploadTask {
* 执行上传 * 执行上传
*/ */
private void runUpload() throws Exception { private void runUpload() throws Exception {
if (uploaderList.isEmpty()) {
return;
}
// 执行
if (uploaderList.size() == 1) { if (uploaderList.size() == 1) {
// 单个主机直接执行 // 单个主机直接执行
IFileUploader handler = uploaderList.get(0); IFileUploader handler = uploaderList.get(0);

View File

@@ -141,7 +141,6 @@ public class FileUploader implements IFileUploader {
int read; int read;
while ((read = inputStream.read(buffer)) != -1) { while ((read = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, read); outputStream.write(buffer, 0, read);
// todo test
file.setCurrent(file.getCurrent() + read); file.setCurrent(file.getCurrent() + read);
} }
outputStream.flush(); outputStream.flush();

View File

@@ -32,10 +32,7 @@ import com.orion.ops.module.asset.entity.request.upload.UploadTaskCreateRequest;
import com.orion.ops.module.asset.entity.request.upload.UploadTaskFileRequest; import com.orion.ops.module.asset.entity.request.upload.UploadTaskFileRequest;
import com.orion.ops.module.asset.entity.request.upload.UploadTaskQueryRequest; import com.orion.ops.module.asset.entity.request.upload.UploadTaskQueryRequest;
import com.orion.ops.module.asset.entity.request.upload.UploadTaskRequest; import com.orion.ops.module.asset.entity.request.upload.UploadTaskRequest;
import com.orion.ops.module.asset.entity.vo.HostBaseVO; import com.orion.ops.module.asset.entity.vo.*;
import com.orion.ops.module.asset.entity.vo.UploadTaskCreateVO;
import com.orion.ops.module.asset.entity.vo.UploadTaskFileVO;
import com.orion.ops.module.asset.entity.vo.UploadTaskVO;
import com.orion.ops.module.asset.enums.HostConfigTypeEnum; import com.orion.ops.module.asset.enums.HostConfigTypeEnum;
import com.orion.ops.module.asset.enums.UploadTaskFileStatusEnum; import com.orion.ops.module.asset.enums.UploadTaskFileStatusEnum;
import com.orion.ops.module.asset.enums.UploadTaskStatusEnum; import com.orion.ops.module.asset.enums.UploadTaskStatusEnum;
@@ -147,7 +144,6 @@ public class UploadTaskServiceImpl implements UploadTaskService {
return UploadTaskCreateVO.builder() return UploadTaskCreateVO.builder()
.id(id) .id(id)
.token(token) .token(token)
.hosts(hosts)
.build(); .build();
} }
@@ -158,11 +154,12 @@ public class UploadTaskServiceImpl implements UploadTaskService {
Valid.notNull(record, ErrorMessage.DATA_ABSENT); Valid.notNull(record, ErrorMessage.DATA_ABSENT);
// 查询任务文件 // 查询任务文件
List<UploadTaskFileVO> files = uploadTaskFileService.getFileByTaskId(id); List<UploadTaskFileVO> files = uploadTaskFileService.getFileByTaskId(id);
// 计算传输进度
this.computeUploadProgress(id, files);
// 返回 // 返回
UploadTaskVO uploadTask = UploadTaskConvert.MAPPER.to(record); UploadTaskVO uploadTask = UploadTaskConvert.MAPPER.to(record);
uploadTask.setFiles(files); // 计算传输进度
this.computeUploadProgress(id, files);
// 设置任务文件
this.setTaskFiles(uploadTask, files);
return uploadTask; return uploadTask;
} }
@@ -208,8 +205,9 @@ public class UploadTaskServiceImpl implements UploadTaskService {
} else { } else {
// 计算进度 // 计算进度
this.computeUploadProgress(id, files); this.computeUploadProgress(id, files);
// 设置任务文件
} }
task.setFiles(files); this.setTaskFiles(task, files);
} }
return tasks; return tasks;
} }
@@ -299,8 +297,6 @@ public class UploadTaskServiceImpl implements UploadTaskService {
.map(localFileClient::getReturnPath) .map(localFileClient::getReturnPath)
.map(localFileClient::getAbsolutePath) .map(localFileClient::getAbsolutePath)
.collect(Collectors.toList()); .collect(Collectors.toList());
// TODO test
paths.forEach(System.out::println);
// 删除文件 // 删除文件
paths.forEach(Files1::delete); paths.forEach(Files1::delete);
} }
@@ -393,7 +389,7 @@ public class UploadTaskServiceImpl implements UploadTaskService {
uploadFile.setStatus(UploadTaskFileStatusEnum.CANCELED.name()); uploadFile.setStatus(UploadTaskFileStatusEnum.CANCELED.name());
uploadFile.setEndTime(new Date()); uploadFile.setEndTime(new Date());
LambdaQueryWrapper<UploadTaskFileDO> updateFileQuery = uploadTaskFileDAO.wrapper() LambdaQueryWrapper<UploadTaskFileDO> updateFileQuery = uploadTaskFileDAO.wrapper()
.in(UploadTaskFileDO::getId, updateIdList) .in(UploadTaskFileDO::getTaskId, updateIdList)
.in(UploadTaskFileDO::getStatus, .in(UploadTaskFileDO::getStatus,
UploadTaskFileStatusEnum.WAITING.name(), UploadTaskFileStatusEnum.WAITING.name(),
UploadTaskFileStatusEnum.UPLOADING.name()); UploadTaskFileStatusEnum.UPLOADING.name());
@@ -446,11 +442,34 @@ public class UploadTaskServiceImpl implements UploadTaskService {
} else if (UploadTaskFileStatusEnum.FINISHED.name().equals(status)) { } else if (UploadTaskFileStatusEnum.FINISHED.name().equals(status)) {
file.setCurrent(file.getFileSize()); file.setCurrent(file.getFileSize());
} else if (UploadTaskFileStatusEnum.FAILED.name().equals(status)) { } else if (UploadTaskFileStatusEnum.FAILED.name().equals(status)) {
file.setCurrent(0L); file.setCurrent(file.getFileSize());
} else if (UploadTaskFileStatusEnum.CANCELED.name().equals(status)) { } else if (UploadTaskFileStatusEnum.CANCELED.name().equals(status)) {
file.setCurrent(0L); file.setCurrent(file.getFileSize());
} }
} }
} }
/**
* 设置任务文件
*
* @param task task
* @param files files
*/
private void setTaskFiles(UploadTaskVO task, List<UploadTaskFileVO> files) {
Map<Long, List<UploadTaskFileVO>> hostFiles = files.stream()
.collect(Collectors.groupingBy(UploadTaskFileVO::getHostId));
List<UploadTaskHostVO> hosts = JSON.parseObject(task.getExtraInfo(), UploadTaskExtraDTO.class)
.getHosts()
.stream()
.map(s -> UploadTaskHostVO.builder()
.id(s.getId())
.code(s.getCode())
.name(s.getName())
.address(s.getAddress())
.files(hostFiles.get(s.getId()))
.build())
.collect(Collectors.toList());
task.setHosts(hosts);
}
} }

View File

@@ -1,5 +1,4 @@
import type { DataGrid, Pagination } from '@/types/global'; import type { DataGrid, Pagination } from '@/types/global';
import type { HostQueryResponse } from '@/api/asset/host';
import type { TableData } from '@arco-design/web-vue/es/table/interface'; import type { TableData } from '@arco-design/web-vue/es/table/interface';
import axios from 'axios'; import axios from 'axios';
import qs from 'query-string'; import qs from 'query-string';
@@ -29,7 +28,6 @@ export interface UploadTaskFileCreateRequest {
export interface UploadTaskCreateResponse { export interface UploadTaskCreateResponse {
id: number; id: number;
token: string; token: string;
hosts: Array<HostQueryResponse>;
} }
/** /**
@@ -58,13 +56,24 @@ export interface UploadTaskQueryResponse extends TableData {
startTime: number; startTime: number;
endTime: number; endTime: number;
createTime: number; createTime: number;
files: Array<UploadTaskFileQueryResponse>; hosts: Array<UploadTaskHost>;
} }
/** /**
* 上传任务文件查询响应 * 上传任务主机响应
*/ */
export interface UploadTaskFileQueryResponse { export interface UploadTaskHost {
id: number;
code: string;
name: string;
address: string;
files: Array<UploadTaskFile>;
}
/**
* 上传任务文件响应
*/
export interface UploadTaskFile {
id: number; id: number;
taskId: number; taskId: number;
hostId: number; hostId: number;

View File

@@ -4,10 +4,10 @@
<div class="panel-header"> <div class="panel-header">
<h3>文件列表</h3> <h3>文件列表</h3>
<!-- 操作 --> <!-- 操作 -->
<a-button-group size="small" :disabled="startStatus"> <a-button-group size="mini" :disabled="startStatus">
<a-button @click="clear">清空</a-button> <a-button @click="clear">清空</a-button>
<!-- 选择文件 --> <!-- 选择文件 -->
<a-upload v-model:file-list="fileList" <a-upload v-model:file-list="files"
:auto-upload="false" :auto-upload="false"
:show-file-list="false" :show-file-list="false"
:multiple="true"> :multiple="true">
@@ -16,7 +16,7 @@
</template> </template>
</a-upload> </a-upload>
<!-- 选择文件夹 --> <!-- 选择文件夹 -->
<a-upload v-model:file-list="fileList" <a-upload v-model:file-list="files"
:auto-upload="false" :auto-upload="false"
:show-file-list="false" :show-file-list="false"
:directory="true"> :directory="true">
@@ -27,10 +27,11 @@
</a-button-group> </a-button-group>
</div> </div>
<!-- 文件列表 --> <!-- 文件列表 -->
<div v-if="fileList.length" class="files-container"> <div v-if="files.length" class="files-container">
<a-scrollbar style="overflow-y: auto; height: 100%;">
<a-upload class="files-wrapper" <a-upload class="files-wrapper"
:class="[ startStatus ? 'uploading-files-wrapper' : 'waiting-files-wrapper' ]" :class="[ startStatus ? 'uploading-files-wrapper' : 'waiting-files-wrapper' ]"
v-model:file-list="fileList" v-model:file-list="files"
:auto-upload="false" :auto-upload="false"
:show-cancel-button="false" :show-cancel-button="false"
:show-retry-button="false" :show-retry-button="false"
@@ -55,6 +56,7 @@
</div> </div>
</template> </template>
</a-upload> </a-upload>
</a-scrollbar>
</div> </div>
<!-- 未选择文件 --> <!-- 未选择文件 -->
<a-result v-else <a-result v-else
@@ -72,38 +74,35 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { FileItem } from '@arco-design/web-vue'; import type { FileItem } from '@arco-design/web-vue';
import type { UploadTaskFileCreateRequest } from '@/api/exec/upload-task';
import type { IFileUploader } from '@/components/system/uploader/const'; import type { IFileUploader } from '@/components/system/uploader/const';
import { onUnmounted, ref } from 'vue'; import { computed, onUnmounted, ref } from 'vue';
import { getFileSize } from '@/utils/file'; import { getFileSize } from '@/utils/file';
import FileUploader from '@/components/system/uploader/file-uploader'; import FileUploader from '@/components/system/uploader/file-uploader';
const emits = defineEmits(['end', 'error']); const emits = defineEmits(['update:fileList', 'end', 'error', 'clearFile']);
const props = defineProps<{
fileList: Array<FileItem>;
}>();
const startStatus = ref(false); const startStatus = ref(false);
const fileList = ref<FileItem[]>([]);
const uploader = ref<IFileUploader>(); const uploader = ref<IFileUploader>();
const files = computed<Array<FileItem>>({
// 获取上传的文件 get() {
const getFiles = (): Array<UploadTaskFileCreateRequest> => { return props.fileList;
return fileList.value },
.map(s => { set(e) {
return { emits('update:fileList', e);
fileId: s.uid, }
filePath: s.file?.webkitRelativePath || s.file?.name,
fileSize: s.file?.size,
};
}); });
};
// 开始上传 // 开始上传
const startUpload = async (token: string) => { const startUpload = async (token: string) => {
// 修改状态 // 修改状态
startStatus.value = true; startStatus.value = true;
fileList.value.forEach(s => s.status = 'uploading'); props.fileList.forEach(s => s.status = 'uploading');
// 开始上传 // 开始上传
try { try {
uploader.value = new FileUploader(token, fileList.value); uploader.value = new FileUploader(token, props.fileList);
uploader.value?.setHook(() => { uploader.value?.setHook(() => {
emits('end'); emits('end');
}); });
@@ -115,8 +114,8 @@
// 清空 // 清空
const clear = () => { const clear = () => {
fileList.value = [];
startStatus.value = false; startStatus.value = false;
emits('clearFile');
}; };
// 关闭 // 关闭
@@ -125,7 +124,7 @@
uploader.value?.close(); uploader.value?.close();
}; };
defineExpose({ getFiles, startUpload, close }); defineExpose({ startUpload, close });
// 卸载时关闭 // 卸载时关闭
onUnmounted(() => { onUnmounted(() => {
@@ -171,7 +170,6 @@
:deep(.arco-upload-wrapper) { :deep(.arco-upload-wrapper) {
position: absolute; position: absolute;
height: 100%; height: 100%;
overflow-y: auto;
} }
:deep(.arco-upload) { :deep(.arco-upload) {
@@ -181,8 +179,6 @@
:deep(.arco-upload-list) { :deep(.arco-upload-list) {
padding: 0; padding: 0;
max-height: 100%; max-height: 100%;
overflow-x: hidden;
overflow-y: auto;
} }
:deep(.arco-upload-list-item-error) { :deep(.arco-upload-list-item-error) {
@@ -222,4 +218,10 @@
} }
} }
:deep(.arco-scrollbar) {
position: absolute;
height: 100%;
width: 100%;
}
</style> </style>

View File

@@ -4,21 +4,21 @@
<div class="panel-header"> <div class="panel-header">
<h3>批量上传</h3> <h3>批量上传</h3>
<!-- 操作 --> <!-- 操作 -->
<a-button-group size="small"> <a-button-group size="mini">
<!-- 重置 --> <!-- 重置 -->
<a-button v-if="status.value !== UploadTaskStatus.REQUESTING.value" <a-button v-if="status.value === UploadTaskStatus.WAITING.value"
@click="emits('clear')"> @click="emits('clear')">
重置 重置
</a-button> </a-button>
<!-- 取消上传 --> <!-- 取消上传 -->
<a-button v-if="status.value === UploadTaskStatus.REQUESTING.value <a-button v-if="status.value === UploadTaskStatus.REQUESTING.value"
|| status.value === UploadTaskStatus.UPLOADING.value" type="primary"
@click="emits('cancel')"> status="warning"
@click="emits('abort')">
取消上传 取消上传
</a-button> </a-button>
<!-- 开始上传 --> <!-- 开始上传 -->
<a-button v-if="status.value !== UploadTaskStatus.REQUESTING.value <a-button v-if="status.value === UploadTaskStatus.WAITING.value"
&& status.value !== UploadTaskStatus.UPLOADING.value"
type="primary" type="primary"
@click="submit"> @click="submit">
开始上传 开始上传
@@ -73,7 +73,7 @@
import formRules from '../types/form.rules'; import formRules from '../types/form.rules';
import { UploadTaskStatus } from '../types/const'; import { UploadTaskStatus } from '../types/const';
const emits = defineEmits(['upload', 'openHost', 'cancel', 'clear']); const emits = defineEmits(['upload', 'openHost', 'abort', 'clear']);
const props = defineProps<{ const props = defineProps<{
status: UploadTaskStatusType; status: UploadTaskStatusType;
formModel: UploadTaskCreateRequest; formModel: UploadTaskCreateRequest;

View File

@@ -0,0 +1,174 @@
<template>
<div class="container">
<!-- 表头 -->
<div class="panel-header">
<h3>上传主机</h3>
<!-- 操作 -->
<a-button-group size="mini">
<!-- 返回 -->
<a-button @click="emits('back')">返回</a-button>
<!-- 取消上传 -->
<a-button 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">
<!-- 未完成 -->
<a-tag class="host-item-status-tag" color="#52C41A">
{{ host.files.length - getFinishCount(host.files) }}
<template #icon>
<icon-clock-circle class="host-item-status-icon" />
</template>
</a-tag>
<!-- 已完成 -->
<a-tag class="host-item-status-tag" color="#1890FF">
{{ getFinishCount(host.files) }}
<template #icon>
<icon-check-circle class="host-item-status-icon" />
</template>
</a-tag>
</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 { UploadTaskFileStatus } from '../types/const';
const emits = defineEmits(['update:selectedHost', 'back', 'cancel']);
const props = defineProps<{
selectedHost: number;
task: UploadTaskQueryResponse;
}>();
// 修改选中的主机
const changeSelectedHost = (id: number) => {
emits('update:selectedHost', id);
};
// 获取已完成数量
const getFinishCount = (files: Array<UploadTaskFile>) => {
return files.filter(s => s.status === UploadTaskFileStatus.FINISHED
|| s.status === UploadTaskFileStatus.CANCELED
|| s.status === UploadTaskFileStatus.FAILED).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% - 64px);
display: flex;
align-items: flex-start;
flex-direction: column;
justify-content: center;
}
&-status {
user-select: none;
&-tag {
max-width: 64px;
}
&-icon {
color: #FFFFFF;
}
}
&-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>

View File

@@ -0,0 +1,105 @@
<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-progress">
<a-progress type="circle"
size="mini"
:status="getDictValue(fileStatusKey, file.status, 'status') as any"
:percent="file.current / file.fileSize" />
</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 { useDictStore } from '@/store';
const emits = defineEmits(['update:selectedHost']);
const props = defineProps<{
files: Array<UploadTaskFile>;
}>();
const { getDictValue } = useDictStore();
</script>
<style lang="less" scoped>
@icon-width: 24px;
@progress-width: 24px;
.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 {
width: calc(100% - @icon-width - @progress-width);
font-size: 14px;
color: var(--color-text-1);
}
&-progress {
width: @progress-width;
display: flex;
justify-content: flex-end;
}
}
}
:deep(.arco-scrollbar) {
position: absolute;
height: 100%;
width: 100%;
}
</style>

View File

@@ -1,21 +1,40 @@
<template> <template>
<a-spin class="panel-container full" :loading="loading"> <a-spin class="panel-container full" :loading="loading">
<!-- 上传步骤 --> <!-- 上传步骤 -->
<batch-upload-step class="panel-item step-panel-container" <batch-upload-step class="panel-item first-panel-container"
:status="status" /> :status="status" />
<!-- 上传表单 --> <!-- 上传表单 -->
<batch-upload-form class="panel-item form-panel-container" <batch-upload-form v-if="status.formPanel"
class="panel-item center-panel-container"
:form-model="formModel" :form-model="formModel"
:status="status" :status="status"
@upload="doCreateUploadTask" @upload="doCreateUploadTask"
@cancel="doCancelUploadTask" @abort="abortUploadRequest"
@open-host="openHostModal" @open-host="openHostModal"
@clear="clear" /> @clear="clearForm" />
<!-- 上传文件 --> <!-- 上传主机 -->
<batch-upload-files class="panel-item files-panel-container" <batch-upload-hosts v-else
class="panel-item center-panel-container"
v-model:selected-host="selectedHost"
:task="task"
@back="backFormPanel"
@cancel="doCancelUploadTask" />
<!-- 文件列表 -->
<batch-upload-files v-if="status.formPanel"
v-model:file-list="fileList"
class="panel-item last-panel-container"
ref="filesRef" ref="filesRef"
@end="uploadRequestEnd" @end="uploadRequestEnd"
@error="uploadRequestError" /> @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" <authorized-host-modal ref="hostModal"
@selected="setSelectedHost" /> @selected="setSelectedHost" />
@@ -24,21 +43,24 @@
<script lang="ts"> <script lang="ts">
export default { export default {
name: 'batchUploadPanel' name: 'uploadPanel'
}; };
</script> </script>
<script lang="ts" setup> <script lang="ts" setup>
import type { UploadTaskCreateRequest } from '@/api/exec/upload-task'; import type { FileItem } from '@arco-design/web-vue';
import type { UploadTaskCreateRequest, UploadTaskQueryResponse } from '@/api/exec/upload-task';
import type { UploadTaskStatusType } from '../types/const'; import type { UploadTaskStatusType } from '../types/const';
import { ref } from 'vue'; import { ref } from 'vue';
import { UploadTaskStatus } from '../types/const'; import { UploadTaskStatus } from '../types/const';
import { cancelUploadTask, createUploadTask, startUploadTask } from '@/api/exec/upload-task'; import { cancelUploadTask, createUploadTask, startUploadTask, getUploadTask } from '@/api/exec/upload-task';
import useLoading from '@/hooks/loading'; import useLoading from '@/hooks/loading';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import BatchUploadStep from './batch-upload-step.vue'; import BatchUploadStep from './batch-upload-step.vue';
import BatchUploadForm from './batch-upload-form.vue'; import BatchUploadForm from './batch-upload-form.vue';
import BatchUploadFiles from './batch-upload-files.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'; import AuthorizedHostModal from '@/components/asset/host/authorized-host-modal/index.vue';
const defaultForm = (): UploadTaskCreateRequest => { const defaultForm = (): UploadTaskCreateRequest => {
@@ -53,14 +75,15 @@
const { loading, setLoading } = useLoading(); const { loading, setLoading } = useLoading();
const taskId = ref(); const taskId = ref();
const task = ref<UploadTaskQueryResponse>({} as UploadTaskQueryResponse);
const selectedHost = ref();
const formModel = ref<UploadTaskCreateRequest>({ ...defaultForm() }); const formModel = ref<UploadTaskCreateRequest>({ ...defaultForm() });
const fileList = ref<Array<FileItem>>([]);
const status = ref<UploadTaskStatusType>(UploadTaskStatus.WAITING); const status = ref<UploadTaskStatusType>(UploadTaskStatus.WAITING);
const filesRef = ref(); const filesRef = ref();
const hostModal = ref<any>(); const hostModal = ref<any>();
// TODO pullstatus // TODO pullstatus
// host tab
// status tab
// //
const setSelectedHost = (hosts: Array<number>) => { const setSelectedHost = (hosts: Array<number>) => {
@@ -70,7 +93,13 @@
// //
const doCreateUploadTask = async () => { const doCreateUploadTask = async () => {
// //
const files = filesRef.value?.getFiles(); 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) { if (!files || !files.length) {
Message.error('请先选择需要上传的文件'); Message.error('请先选择需要上传的文件');
return; return;
@@ -95,28 +124,35 @@
// //
const doCancelUploadTask = async () => { const doCancelUploadTask = async () => {
setLoading(true); setLoading(true);
filesRef.value?.close();
try { try {
// //
await cancelUploadTask(taskId.value, false); await cancelUploadTask(taskId.value, false);
status.value = UploadTaskStatus.CANCELED; status.value = UploadTaskStatus.WAITING;
Message.success('已取消');
} catch (e) { } catch (e) {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
//
const abortUploadRequest = () => {
status.value = UploadTaskStatus.WAITING;
filesRef.value?.close();
};
// //
const uploadRequestEnd = async () => { const uploadRequestEnd = async () => {
if (status.value.value !== UploadTaskStatus.REQUESTING.value) { if (status.value.value === UploadTaskStatus.REQUESTING.value) {
//
return;
}
// //
setLoading(true); setLoading(true);
try { try {
// //
await startUploadTask(taskId.value); await startUploadTask(taskId.value);
//
const { data } = await getUploadTask(taskId.value);
task.value = data;
selectedHost.value = data.hosts[0].id;
status.value = UploadTaskStatus.UPLOADING; status.value = UploadTaskStatus.UPLOADING;
} catch (e) { } catch (e) {
// //
@@ -124,6 +160,10 @@
} finally { } finally {
setLoading(false); setLoading(false);
} }
} else {
//
await doCancelUploadTask();
}
}; };
// //
@@ -144,11 +184,22 @@
hostModal.value.open(formModel.value.hostIdList); hostModal.value.open(formModel.value.hostIdList);
}; };
// //
const clear = () => { const backFormPanel = () => {
status.value = UploadTaskStatus.WAITING; status.value = UploadTaskStatus.WAITING;
taskId.value = undefined;
task.value = undefined as any;
selectedHost.value = undefined as any;
};
//
const clearForm = () => {
formModel.value = { ...defaultForm() }; formModel.value = { ...defaultForm() };
filesRef.value?.close(); };
//
const clearFile = () => {
fileList.value = [];
}; };
</script> </script>
@@ -156,7 +207,7 @@
<style lang="less" scoped> <style lang="less" scoped>
@step-width: 258px; @step-width: 258px;
@center-width: 398px; @center-width: 398px;
@files-width: calc(100% - @step-width - 16px - @center-width - 16px); @last-width: calc(100% - @step-width - 16px - @center-width - 16px);
.panel-container { .panel-container {
height: 100%; height: 100%;
@@ -168,20 +219,21 @@
padding: 16px; padding: 16px;
border-radius: 4px; border-radius: 4px;
margin-right: 16px; margin-right: 16px;
position: relative;
background: var(--color-bg-2); background: var(--color-bg-2);
} }
.step-panel-container { .first-panel-container {
width: @step-width; width: @step-width;
} }
.form-panel-container { .center-panel-container {
width: @center-width; width: @center-width;
} }
.files-panel-container { .last-panel-container {
margin-right: 0; margin-right: 0;
width: @files-width; width: @last-width;
} }
} }

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="layout-container upload-container"> <div class="layout-container upload-container">
<!-- 上传面板 --> <!-- 上传面板 -->
<batch-upload-panel /> <upload-panel />
</div> </div>
</template> </template>
@@ -15,7 +15,7 @@
import { onMounted } from 'vue'; import { onMounted } from 'vue';
import { useDictStore } from '@/store'; import { useDictStore } from '@/store';
import { dictKeys } from './types/const'; import { dictKeys } from './types/const';
import BatchUploadPanel from './components/batch-upload-panel.vue'; import UploadPanel from './components/upload-panel.vue';
// 加载字典值 // 加载字典值
onMounted(async () => { onMounted(async () => {

View File

@@ -1,8 +1,9 @@
// 上传任务状态定义 // 上传任务状态定义
export interface UploadTaskStatusType { export interface UploadTaskStatusType {
value: string, value: string;
step: number, step: number;
status: string, status: string;
formPanel: boolean;
} }
// 上传任务状态 // 上传任务状态
@@ -12,37 +13,50 @@ export const UploadTaskStatus = {
value: 'WAITING', value: 'WAITING',
step: 1, step: 1,
status: 'process', status: 'process',
formPanel: true,
}, },
// 请求中 // 请求中
REQUESTING: { REQUESTING: {
value: 'REQUESTING', value: 'REQUESTING',
step: 2, step: 2,
status: 'process', status: 'process',
formPanel: true,
}, },
// 上传中 // 上传中
UPLOADING: { UPLOADING: {
value: 'UPLOADING', value: 'UPLOADING',
step: 3, step: 3,
status: 'process', status: 'process',
formPanel: false,
}, },
// 已完成 // 已完成
FINISHED: { FINISHED: {
value: 'FINISHED', value: 'FINISHED',
step: 4, step: 4,
status: 'finish', status: 'finish',
formPanel: false,
}, },
// 已失败 // 已失败
FAILED: { FAILED: {
value: 'FAILED', value: 'FAILED',
step: 4, step: 4,
status: 'error', status: 'error',
formPanel: false,
}, },
};
// 上传任务文件状态
export const UploadTaskFileStatus = {
// 等待中
WAITING: 'WAITING',
// 上传中
UPLOADING: 'UPLOADING',
// 已完成
FINISHED: 'FINISHED',
// 已完成
FAILED: 'FAILED',
// 已取消 // 已取消
CANCELED: { CANCELED: 'CANCELED',
value: 'CANCELED',
step: 4,
status: 'error',
},
}; };
// 上传任务状态 字典项 // 上传任务状态 字典项

View File

@@ -4,7 +4,7 @@
<div class="panel-header"> <div class="panel-header">
<h3>执行参数</h3> <h3>执行参数</h3>
<!-- 操作 --> <!-- 操作 -->
<a-button-group size="small"> <a-button-group size="mini">
<a-button @click="emits('reset')">重置</a-button> <a-button @click="emits('reset')">重置</a-button>
<a-button type="primary" @click="emits('exec')">执行</a-button> <a-button type="primary" @click="emits('exec')">执行</a-button>
</a-button-group> </a-button-group>