🔨 批量上传.

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

@@ -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;

View File

@@ -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>

View File

@@ -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;

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>
<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;
}
}

View File

@@ -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 () => {

View File

@@ -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',
};
// 上传任务状态 字典项

View File

@@ -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>