🔨 批量上传.
This commit is contained in:
@@ -40,7 +40,7 @@ import java.util.List;
|
||||
@SuppressWarnings({"ELValidationInJSP", "SpringElInspection"})
|
||||
public class UploadTaskController {
|
||||
|
||||
// TODO 测试空文件上传 0B 取消怎么那么慢 是不是删除也慢 异步cancel cancel 需要设置子元素为 cancel
|
||||
// TODO 前端日志 测试删除慢吗
|
||||
|
||||
@Resource
|
||||
private UploadTaskService uploadTaskService;
|
||||
|
||||
@@ -7,7 +7,6 @@ import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 上传任务 视图响应对象
|
||||
@@ -31,7 +30,4 @@ public class UploadTaskCreateVO implements Serializable {
|
||||
@Schema(description = "上传 token")
|
||||
private String token;
|
||||
|
||||
@Schema(description = "主机")
|
||||
private List<HostBaseVO> hosts;
|
||||
|
||||
}
|
||||
|
||||
@@ -43,6 +43,9 @@ public class UploadTaskFileVO implements Serializable {
|
||||
@Schema(description = "文件大小")
|
||||
private Long fileSize;
|
||||
|
||||
@Schema(description = "额外信息")
|
||||
private String extraInfo;
|
||||
|
||||
@Schema(description = "状态")
|
||||
private String status;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -56,7 +56,7 @@ public class UploadTaskVO implements Serializable {
|
||||
@Schema(description = "创建时间")
|
||||
private Date createTime;
|
||||
|
||||
@Schema(description = "上传文件")
|
||||
private List<UploadTaskFileVO> files;
|
||||
@Schema(description = "上传主机及文件")
|
||||
private List<UploadTaskHostVO> hosts;
|
||||
|
||||
}
|
||||
|
||||
@@ -128,7 +128,6 @@ public class SftpSession extends TerminalSession implements ISftpSession {
|
||||
} catch (Exception e) {
|
||||
throw Exceptions.ioRuntime(e);
|
||||
} finally {
|
||||
// TODO Test
|
||||
// 关闭 inputStream 可能会被阻塞 ???...??? 只能关闭 executor
|
||||
Streams.close(this.executor);
|
||||
this.connect();
|
||||
|
||||
@@ -141,6 +141,9 @@ public class FileUploadTask implements IFileUploadTask {
|
||||
.current(0L)
|
||||
.build())
|
||||
.collect(Collectors.toList());
|
||||
if (files.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
// 添加到上传器
|
||||
uploaderList.add(new FileUploader(id, k, files));
|
||||
});
|
||||
@@ -150,6 +153,10 @@ public class FileUploadTask implements IFileUploadTask {
|
||||
* 执行上传
|
||||
*/
|
||||
private void runUpload() throws Exception {
|
||||
if (uploaderList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
// 执行
|
||||
if (uploaderList.size() == 1) {
|
||||
// 单个主机直接执行
|
||||
IFileUploader handler = uploaderList.get(0);
|
||||
|
||||
@@ -141,7 +141,6 @@ public class FileUploader implements IFileUploader {
|
||||
int read;
|
||||
while ((read = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, read);
|
||||
// todo test
|
||||
file.setCurrent(file.getCurrent() + read);
|
||||
}
|
||||
outputStream.flush();
|
||||
|
||||
@@ -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.UploadTaskQueryRequest;
|
||||
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.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.entity.vo.*;
|
||||
import com.orion.ops.module.asset.enums.HostConfigTypeEnum;
|
||||
import com.orion.ops.module.asset.enums.UploadTaskFileStatusEnum;
|
||||
import com.orion.ops.module.asset.enums.UploadTaskStatusEnum;
|
||||
@@ -147,7 +144,6 @@ public class UploadTaskServiceImpl implements UploadTaskService {
|
||||
return UploadTaskCreateVO.builder()
|
||||
.id(id)
|
||||
.token(token)
|
||||
.hosts(hosts)
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -158,11 +154,12 @@ public class UploadTaskServiceImpl implements UploadTaskService {
|
||||
Valid.notNull(record, ErrorMessage.DATA_ABSENT);
|
||||
// 查询任务文件
|
||||
List<UploadTaskFileVO> files = uploadTaskFileService.getFileByTaskId(id);
|
||||
// 计算传输进度
|
||||
this.computeUploadProgress(id, files);
|
||||
// 返回
|
||||
UploadTaskVO uploadTask = UploadTaskConvert.MAPPER.to(record);
|
||||
uploadTask.setFiles(files);
|
||||
// 计算传输进度
|
||||
this.computeUploadProgress(id, files);
|
||||
// 设置任务文件
|
||||
this.setTaskFiles(uploadTask, files);
|
||||
return uploadTask;
|
||||
}
|
||||
|
||||
@@ -208,8 +205,9 @@ public class UploadTaskServiceImpl implements UploadTaskService {
|
||||
} else {
|
||||
// 计算进度
|
||||
this.computeUploadProgress(id, files);
|
||||
// 设置任务文件
|
||||
}
|
||||
task.setFiles(files);
|
||||
this.setTaskFiles(task, files);
|
||||
}
|
||||
return tasks;
|
||||
}
|
||||
@@ -299,8 +297,6 @@ public class UploadTaskServiceImpl implements UploadTaskService {
|
||||
.map(localFileClient::getReturnPath)
|
||||
.map(localFileClient::getAbsolutePath)
|
||||
.collect(Collectors.toList());
|
||||
// TODO test
|
||||
paths.forEach(System.out::println);
|
||||
// 删除文件
|
||||
paths.forEach(Files1::delete);
|
||||
}
|
||||
@@ -393,7 +389,7 @@ public class UploadTaskServiceImpl implements UploadTaskService {
|
||||
uploadFile.setStatus(UploadTaskFileStatusEnum.CANCELED.name());
|
||||
uploadFile.setEndTime(new Date());
|
||||
LambdaQueryWrapper<UploadTaskFileDO> updateFileQuery = uploadTaskFileDAO.wrapper()
|
||||
.in(UploadTaskFileDO::getId, updateIdList)
|
||||
.in(UploadTaskFileDO::getTaskId, updateIdList)
|
||||
.in(UploadTaskFileDO::getStatus,
|
||||
UploadTaskFileStatusEnum.WAITING.name(),
|
||||
UploadTaskFileStatusEnum.UPLOADING.name());
|
||||
@@ -446,11 +442,34 @@ public class UploadTaskServiceImpl implements UploadTaskService {
|
||||
} else if (UploadTaskFileStatusEnum.FINISHED.name().equals(status)) {
|
||||
file.setCurrent(file.getFileSize());
|
||||
} else if (UploadTaskFileStatusEnum.FAILED.name().equals(status)) {
|
||||
file.setCurrent(0L);
|
||||
file.setCurrent(file.getFileSize());
|
||||
} 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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 axios from 'axios';
|
||||
import qs from 'query-string';
|
||||
@@ -29,7 +28,6 @@ export interface UploadTaskFileCreateRequest {
|
||||
export interface UploadTaskCreateResponse {
|
||||
id: number;
|
||||
token: string;
|
||||
hosts: Array<HostQueryResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,13 +56,24 @@ export interface UploadTaskQueryResponse extends TableData {
|
||||
startTime: number;
|
||||
endTime: 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;
|
||||
taskId: number;
|
||||
hostId: number;
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
<div class="panel-header">
|
||||
<h3>文件列表</h3>
|
||||
<!-- 操作 -->
|
||||
<a-button-group size="small" :disabled="startStatus">
|
||||
<a-button-group size="mini" :disabled="startStatus">
|
||||
<a-button @click="clear">清空</a-button>
|
||||
<!-- 选择文件 -->
|
||||
<a-upload v-model:file-list="fileList"
|
||||
<a-upload v-model:file-list="files"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:multiple="true">
|
||||
@@ -16,7 +16,7 @@
|
||||
</template>
|
||||
</a-upload>
|
||||
<!-- 选择文件夹 -->
|
||||
<a-upload v-model:file-list="fileList"
|
||||
<a-upload v-model:file-list="files"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:directory="true">
|
||||
@@ -27,34 +27,36 @@
|
||||
</a-button-group>
|
||||
</div>
|
||||
<!-- 文件列表 -->
|
||||
<div v-if="fileList.length" class="files-container">
|
||||
<a-upload class="files-wrapper"
|
||||
:class="[ startStatus ? 'uploading-files-wrapper' : 'waiting-files-wrapper' ]"
|
||||
v-model:file-list="fileList"
|
||||
: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">
|
||||
<!-- 文件名称 -->
|
||||
<a-tooltip position="left"
|
||||
:mini="true"
|
||||
:content="fileItem.file.webkitRelativePath || fileItem.file.name">
|
||||
<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">
|
||||
<a-tooltip position="left"
|
||||
:mini="true"
|
||||
:content="fileItem.file.webkitRelativePath || fileItem.file.name">
|
||||
<!-- 文件名称 -->
|
||||
<span class="file-name text-ellipsis">
|
||||
{{ fileItem.file.webkitRelativePath || fileItem.file.name }}
|
||||
</span>
|
||||
</a-tooltip>
|
||||
<!-- 文件大小 -->
|
||||
<span class="file-size span-blue">
|
||||
</a-tooltip>
|
||||
<!-- 文件大小 -->
|
||||
<span class="file-size span-blue">
|
||||
{{ getFileSize(fileItem.file.size) }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</a-upload>
|
||||
</div>
|
||||
</template>
|
||||
</a-upload>
|
||||
</a-scrollbar>
|
||||
</div>
|
||||
<!-- 未选择文件 -->
|
||||
<a-result v-else
|
||||
@@ -72,38 +74,35 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
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 { onUnmounted, ref } from 'vue';
|
||||
import { computed, onUnmounted, ref } from 'vue';
|
||||
import { getFileSize } from '@/utils/file';
|
||||
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 fileList = ref<FileItem[]>([]);
|
||||
const uploader = ref<IFileUploader>();
|
||||
|
||||
// 获取上传的文件
|
||||
const getFiles = (): Array<UploadTaskFileCreateRequest> => {
|
||||
return fileList.value
|
||||
.map(s => {
|
||||
return {
|
||||
fileId: s.uid,
|
||||
filePath: s.file?.webkitRelativePath || s.file?.name,
|
||||
fileSize: s.file?.size,
|
||||
};
|
||||
});
|
||||
};
|
||||
const files = computed<Array<FileItem>>({
|
||||
get() {
|
||||
return props.fileList;
|
||||
},
|
||||
set(e) {
|
||||
emits('update:fileList', e);
|
||||
}
|
||||
});
|
||||
|
||||
// 开始上传
|
||||
const startUpload = async (token: string) => {
|
||||
// 修改状态
|
||||
startStatus.value = true;
|
||||
fileList.value.forEach(s => s.status = 'uploading');
|
||||
props.fileList.forEach(s => s.status = 'uploading');
|
||||
// 开始上传
|
||||
try {
|
||||
uploader.value = new FileUploader(token, fileList.value);
|
||||
uploader.value = new FileUploader(token, props.fileList);
|
||||
uploader.value?.setHook(() => {
|
||||
emits('end');
|
||||
});
|
||||
@@ -115,8 +114,8 @@
|
||||
|
||||
// 清空
|
||||
const clear = () => {
|
||||
fileList.value = [];
|
||||
startStatus.value = false;
|
||||
emits('clearFile');
|
||||
};
|
||||
|
||||
// 关闭
|
||||
@@ -125,7 +124,7 @@
|
||||
uploader.value?.close();
|
||||
};
|
||||
|
||||
defineExpose({ getFiles, startUpload, close });
|
||||
defineExpose({ startUpload, close });
|
||||
|
||||
// 卸载时关闭
|
||||
onUnmounted(() => {
|
||||
@@ -171,7 +170,6 @@
|
||||
:deep(.arco-upload-wrapper) {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
:deep(.arco-upload) {
|
||||
@@ -181,8 +179,6 @@
|
||||
:deep(.arco-upload-list) {
|
||||
padding: 0;
|
||||
max-height: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
:deep(.arco-upload-list-item-error) {
|
||||
@@ -222,4 +218,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.arco-scrollbar) {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -4,21 +4,21 @@
|
||||
<div class="panel-header">
|
||||
<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')">
|
||||
重置
|
||||
</a-button>
|
||||
<!-- 取消上传 -->
|
||||
<a-button v-if="status.value === UploadTaskStatus.REQUESTING.value
|
||||
|| status.value === UploadTaskStatus.UPLOADING.value"
|
||||
@click="emits('cancel')">
|
||||
<a-button v-if="status.value === UploadTaskStatus.REQUESTING.value"
|
||||
type="primary"
|
||||
status="warning"
|
||||
@click="emits('abort')">
|
||||
取消上传
|
||||
</a-button>
|
||||
<!-- 开始上传 -->
|
||||
<a-button v-if="status.value !== UploadTaskStatus.REQUESTING.value
|
||||
&& status.value !== UploadTaskStatus.UPLOADING.value"
|
||||
<a-button v-if="status.value === UploadTaskStatus.WAITING.value"
|
||||
type="primary"
|
||||
@click="submit">
|
||||
开始上传
|
||||
@@ -73,7 +73,7 @@
|
||||
import formRules from '../types/form.rules';
|
||||
import { UploadTaskStatus } from '../types/const';
|
||||
|
||||
const emits = defineEmits(['upload', 'openHost', 'cancel', 'clear']);
|
||||
const emits = defineEmits(['upload', 'openHost', 'abort', 'clear']);
|
||||
const props = defineProps<{
|
||||
status: UploadTaskStatusType;
|
||||
formModel: UploadTaskCreateRequest;
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,21 +1,40 @@
|
||||
<template>
|
||||
<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" />
|
||||
<!-- 上传表单 -->
|
||||
<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"
|
||||
:status="status"
|
||||
@upload="doCreateUploadTask"
|
||||
@cancel="doCancelUploadTask"
|
||||
@abort="abortUploadRequest"
|
||||
@open-host="openHostModal"
|
||||
@clear="clear" />
|
||||
<!-- 上传文件 -->
|
||||
<batch-upload-files class="panel-item files-panel-container"
|
||||
@clear="clearForm" />
|
||||
<!-- 上传主机 -->
|
||||
<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"
|
||||
@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"
|
||||
@selected="setSelectedHost" />
|
||||
@@ -24,21 +43,24 @@
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'batchUploadPanel'
|
||||
name: 'uploadPanel'
|
||||
};
|
||||
</script>
|
||||
|
||||
<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 { ref } from 'vue';
|
||||
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 { 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 => {
|
||||
@@ -53,14 +75,15 @@
|
||||
const { loading, setLoading } = useLoading();
|
||||
|
||||
const taskId = ref();
|
||||
const task = ref<UploadTaskQueryResponse>({} as UploadTaskQueryResponse);
|
||||
const selectedHost = ref();
|
||||
const formModel = ref<UploadTaskCreateRequest>({ ...defaultForm() });
|
||||
const fileList = ref<Array<FileItem>>([]);
|
||||
const status = ref<UploadTaskStatusType>(UploadTaskStatus.WAITING);
|
||||
const filesRef = ref();
|
||||
const hostModal = ref<any>();
|
||||
|
||||
// TODO pullstatus 按钮显示就可以去掉了吧
|
||||
// host tab
|
||||
// status tab
|
||||
// TODO pullstatus
|
||||
|
||||
// 设置选中主机
|
||||
const setSelectedHost = (hosts: Array<number>) => {
|
||||
@@ -70,7 +93,13 @@
|
||||
// 创建上传任务
|
||||
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) {
|
||||
Message.error('请先选择需要上传的文件');
|
||||
return;
|
||||
@@ -95,34 +124,45 @@
|
||||
// 取消上传任务
|
||||
const doCancelUploadTask = async () => {
|
||||
setLoading(true);
|
||||
filesRef.value?.close();
|
||||
try {
|
||||
// 取消上传
|
||||
await cancelUploadTask(taskId.value, false);
|
||||
status.value = UploadTaskStatus.CANCELED;
|
||||
status.value = UploadTaskStatus.WAITING;
|
||||
Message.success('已取消');
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 中断上传请求
|
||||
const abortUploadRequest = () => {
|
||||
status.value = UploadTaskStatus.WAITING;
|
||||
filesRef.value?.close();
|
||||
};
|
||||
|
||||
// 上传请求结束
|
||||
const uploadRequestEnd = async () => {
|
||||
if (status.value.value !== UploadTaskStatus.REQUESTING.value) {
|
||||
// 手动停止或者其他原因
|
||||
return;
|
||||
}
|
||||
// 如果结束后还是请求中则代表请求完毕
|
||||
setLoading(true);
|
||||
try {
|
||||
// 开始上传
|
||||
await startUploadTask(taskId.value);
|
||||
status.value = UploadTaskStatus.UPLOADING;
|
||||
} catch (e) {
|
||||
// 设置失败
|
||||
await uploadRequestError();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (status.value.value === UploadTaskStatus.REQUESTING.value) {
|
||||
// 如果结束后还是请求中则代表请求完毕
|
||||
setLoading(true);
|
||||
try {
|
||||
// 开始上传
|
||||
await startUploadTask(taskId.value);
|
||||
// 查询任务
|
||||
const { data } = await getUploadTask(taskId.value);
|
||||
task.value = data;
|
||||
selectedHost.value = data.hosts[0].id;
|
||||
status.value = UploadTaskStatus.UPLOADING;
|
||||
} catch (e) {
|
||||
// 设置失败
|
||||
await uploadRequestError();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} else {
|
||||
// 手动停止或者其他原因则修改为取消上传
|
||||
await doCancelUploadTask();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -144,11 +184,22 @@
|
||||
hostModal.value.open(formModel.value.hostIdList);
|
||||
};
|
||||
|
||||
// 清空
|
||||
const clear = () => {
|
||||
// 返回表单页面
|
||||
const backFormPanel = () => {
|
||||
status.value = UploadTaskStatus.WAITING;
|
||||
taskId.value = undefined;
|
||||
task.value = undefined as any;
|
||||
selectedHost.value = undefined as any;
|
||||
};
|
||||
|
||||
// 清空表单
|
||||
const clearForm = () => {
|
||||
formModel.value = { ...defaultForm() };
|
||||
filesRef.value?.close();
|
||||
};
|
||||
|
||||
// 清空文件
|
||||
const clearFile = () => {
|
||||
fileList.value = [];
|
||||
};
|
||||
|
||||
</script>
|
||||
@@ -156,7 +207,7 @@
|
||||
<style lang="less" scoped>
|
||||
@step-width: 258px;
|
||||
@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 {
|
||||
height: 100%;
|
||||
@@ -168,20 +219,21 @@
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
margin-right: 16px;
|
||||
position: relative;
|
||||
background: var(--color-bg-2);
|
||||
}
|
||||
|
||||
.step-panel-container {
|
||||
.first-panel-container {
|
||||
width: @step-width;
|
||||
}
|
||||
|
||||
.form-panel-container {
|
||||
.center-panel-container {
|
||||
width: @center-width;
|
||||
}
|
||||
|
||||
.files-panel-container {
|
||||
.last-panel-container {
|
||||
margin-right: 0;
|
||||
width: @files-width;
|
||||
width: @last-width;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="layout-container upload-container">
|
||||
<!-- 上传面板 -->
|
||||
<batch-upload-panel />
|
||||
<upload-panel />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
import { onMounted } from 'vue';
|
||||
import { useDictStore } from '@/store';
|
||||
import { dictKeys } from './types/const';
|
||||
import BatchUploadPanel from './components/batch-upload-panel.vue';
|
||||
import UploadPanel from './components/upload-panel.vue';
|
||||
|
||||
// 加载字典值
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// 上传任务状态定义
|
||||
export interface UploadTaskStatusType {
|
||||
value: string,
|
||||
step: number,
|
||||
status: string,
|
||||
value: string;
|
||||
step: number;
|
||||
status: string;
|
||||
formPanel: boolean;
|
||||
}
|
||||
|
||||
// 上传任务状态
|
||||
@@ -12,37 +13,50 @@ export const UploadTaskStatus = {
|
||||
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 UploadTaskFileStatus = {
|
||||
// 等待中
|
||||
WAITING: 'WAITING',
|
||||
// 上传中
|
||||
UPLOADING: 'UPLOADING',
|
||||
// 已完成
|
||||
FINISHED: 'FINISHED',
|
||||
// 已完成
|
||||
FAILED: 'FAILED',
|
||||
// 已取消
|
||||
CANCELED: {
|
||||
value: 'CANCELED',
|
||||
step: 4,
|
||||
status: 'error',
|
||||
},
|
||||
CANCELED: 'CANCELED',
|
||||
};
|
||||
|
||||
// 上传任务状态 字典项
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="panel-header">
|
||||
<h3>执行参数</h3>
|
||||
<!-- 操作 -->
|
||||
<a-button-group size="small">
|
||||
<a-button-group size="mini">
|
||||
<a-button @click="emits('reset')">重置</a-button>
|
||||
<a-button type="primary" @click="emits('exec')">执行</a-button>
|
||||
</a-button-group>
|
||||
|
||||
Reference in New Issue
Block a user