update:分片上传

This commit is contained in:
2025-11-25 15:29:26 +08:00
parent 7aa7bed593
commit 31e7c09b5a
6 changed files with 147 additions and 67 deletions

View File

@@ -22,6 +22,7 @@
"format": "prettier --write src/"
},
"dependencies": {
"@antv/g6": "^4.8.24",
"@antv/x6": "^2.18.1",
"@antv/x6-plugin-clipboard": "^2.1.6",
"@antv/x6-plugin-dnd": "^2.1.1",
@@ -34,12 +35,15 @@
"@antv/x6-vue-shape": "^2.1.2",
"@element-plus/icons-vue": "^2.3.2",
"axios": "^1.11.0",
"chinese-workday": "^1.10.0",
"dayjs": "^1.11.18",
"dhtmlx-gantt": "^8.0.6",
"echarts": "^6.0.0",
"element-plus": "^2.11.7",
"html2canvas": "^1.4.1",
"jspdf": "^3.0.3",
"lodash-es": "^4.17.21",
"mitt": "^3.0.1",
"monaco-editor": "^0.54.0",
"pinia": "^3.0.3",
"sass-embedded": "^1.90.0",
@@ -51,10 +55,7 @@
"vuedraggable": "^4.1.0",
"vxe-pc-ui": "^4.9.13",
"vxe-table": "^4.16.0",
"xlsx": "^0.18.5",
"@antv/g6": "^4.8.24",
"dhtmlx-gantt": "^8.0.6",
"chinese-workday": "^1.10.0"
"xlsx": "^0.18.5"
},
"devDependencies": {
"@tsconfig/node22": "^22.0.2",
@@ -74,4 +75,4 @@
"vite-plugin-vue-devtools": "^8.0.0",
"vue-tsc": "^3.0.4"
}
}
}

View File

@@ -217,3 +217,13 @@ export const getDirectorySizeByUserIdApi = (params: any) => {
export const chunkUploadToMinioApi = (params: any) => {
return upload(`${PREFIX}data/chunkUploadToMinio`, params);
};
// 知识库上传
export const batchAddFileInfoApi = (params: any) => {
return post(`${PREFIX}data/batchAddFileInfo`, params);
};
// 知识库上传后处理
export const callBackknowledgeFileApi = (params: any) => {
return post(`${PREFIX}data/callBackknowledgeFile`, params);
};

View File

@@ -1,6 +1,6 @@
<template>
<div class="comp-upload-list">
<!-- <div class="btn" @click="listVisible = true"><el-icon :size="22"><Upload /></el-icon></div> -->
<div class="btn" @click="listVisible = true"><el-icon :size="22"><Upload /></el-icon></div>
<el-drawer
title="上传列表"
v-model="listVisible"
@@ -10,11 +10,23 @@
>
<div class="content">
<div v-if="listData.length > 0" class="list">
<div v-for="(item) in listData" :key="item.file.name" class="item">
<div v-for="(item, index) in listData" :key="item.file.name" class="item">
<div class="main">
<div class="name">{{ item.file.name }}</div>
<div class="toper">
<div class="name">
{{ item.file.name }}
</div>
<div class="status">
{{ UPLOAD_FILE_STATUS[item.data.status] }}
</div>
</div>
<div class="progress">
<el-progress :show-text="false" :percentage="Number(item.data.current / item.data.total * 100) || 0" :stroke-width="5" />
<el-progress
:show-text="false"
:percentage="Number(item.data.current / item.data.total * 100) || 0"
:status="item.data.status === '-1' ? 'exception' : ''"
:stroke-width="5"
/>
</div>
<div class="info">
<div class="speed">{{ item.data.speed || '--' }}/s</div>
@@ -23,7 +35,7 @@
</div>
</div>
</div>
<div class="options"><el-icon :size="16"><Delete /></el-icon></div>
<div class="options"><el-icon v-if="item.data.status !== '1'" :size="16" @click="removeFun(index)"><Delete /></el-icon></div>
</div>
</div>
<div v-else class="no-task">暂无上传任务</div>
@@ -33,63 +45,122 @@
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import {
// Upload,
Delete } from '@element-plus/icons-vue';
import { ref } from 'vue';
import { Upload, Delete } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import { UploadStore } from '@/stores/upload';
import { formatFileSize } from '@/utils/file';
import { chunkUploadToMinioApi, callBackknowledgeFileApi } from '@/api/data/data';
import emitter from '@/utils/eventBus';
const uploadStore = UploadStore();
const taskStatusObj: any = {}; // 更具uploadTaskId和businessId记录所以任务文件的上传状态
const listVisible = ref(false);
const listData = ref<any>([]);
const chunkSize = 1024 * 1024; // 每片1MB
const chunkSize = 1024 * 1024 * 5; // 每片5MB
const UPLOAD_FILE_STATUS: any = { // TODO
'-1': '上传失败',
'0': '待上传',
'1': '上传中',
'2': '上传完成',
};
watch(() => uploadStore.addFile, (data: any) => {
initFun(data);
emitter.on('ADD_UPLOAD_FILE', (addData: any) => {
const data = addData.data;
data.status = '0'; // 默认状态
if (!taskStatusObj[data.uploadTaskId]) {
taskStatusObj[data.uploadTaskId] = {};
}
taskStatusObj[data.uploadTaskId][data.businessId] = data.status;
initFun(addData);
});
const initFun = (data: any) => {
listData.value.push(data);
if (listData.value.length === 1) {
sliceFileFun(data);
sliceFileFun(0);
}
};
const sliceFileFun = async(data: any) => {
const file = data.file;
// 分片并上传
const sliceFileFun = async(fileIndex: number) => {
const fileObj = listData.value[fileIndex];
const file = fileObj.file;
const fileData = fileObj.data;
const totalChunks = Math.ceil(file.size / chunkSize);
for (let index = 0; index < totalChunks; index++) {
const start = index * chunkSize;
let fileTempPath = '';
ElMessage.success(`${file.name} 开始上传`);
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
const start = chunkIndex * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
const formData: any = new FormData();
formData.append('chunk', chunk);
formData.append('chunkIndex', index);
formData.append('totalChunks', totalChunks);
formData.append('filename', file.name);
const res: any = await uploadFun({ total: totalChunks, current: index });
data.data.total = res.total;
data.data.current = res.current;
data.data.speed = res.speed;
const chunkFile = file.slice(start, end);
const params: any = {
uploadTaskId: fileData.uploadTaskId,
businessId: fileData.businessId,
objectKey: fileData.objectKey,
sourceFileName: fileData.sourceFileName,
chunk: chunkIndex + 1,
chunkTotal: totalChunks,
file: chunkFile,
};
if (fileTempPath) {
params.fileTempPath = fileTempPath;
}
const res: any = await uploadFun(params);
if (res.result) {
fileTempPath = res.fileTempPath;
fileObj.data.total = totalChunks;
fileObj.data.current = chunkIndex + 1;
fileObj.data.speed = res.speed;
fileObj.data.status = (chunkIndex + 1 === totalChunks) ? '2' : '1';
} else {
fileObj.data.status = '-1';
}
taskStatusObj[fileObj.data.uploadTaskId][fileObj.data.businessId] = fileObj.data.status;
if (fileObj.data.status === '2') {
ElMessage.success(`${file.name} 上传成功`);
}
callBackFun(fileData);
}
listData.value.shift();
ElMessage.success(`${file.name}上传成功`);
if (listData.value.length >= 1) {
sliceFileFun(listData.value[0]);
setTimeout(() => {
fileTempPath = '';
const index = listData.value.findIndex((item: any) => item.data.status === '0');
if (listData.value[index]) {
sliceFileFun(index);
}
}, 2000); // 2s后自动开启下次上传没有具体作用
};
// 后处理回调校验
const callBackFun = (data: any) => {
const { uploadTaskId, isApprove, taskType } = data;
const totalTaskNum = Object.keys(taskStatusObj[uploadTaskId]).length;
const finishTaskNum = Object.values(taskStatusObj[uploadTaskId]).filter(val => val === '2' || val === '-1').length;
const successData = Object.entries(taskStatusObj[uploadTaskId]).filter(([, value]) => value === '2').map(([key]) => key);
const failData = Object.entries(taskStatusObj[uploadTaskId]).filter(([, value]) => value === '-1').map(([key]) => key);
if (finishTaskNum === totalTaskNum) {
const params = {
uploadTaskId: uploadTaskId,
succBusinessIds: successData,
failBusinessIds: failData,
isApprove,
};
const apiObj: any = {
2: callBackknowledgeFileApi, // 知识库后处理
};
apiObj[taskType](params);
}
};
const uploadFun = (data: any) => {
const uploadFun = async(params: any) => {
const starTime = new Date().getTime();
return new Promise((resolve) => {
setTimeout(() => {
const endTime = new Date().getTime();
data.speed = formatFileSize((endTime - starTime) / 1000 * chunkSize);
resolve(data);
}, 1000);
});
const res: any = await chunkUploadToMinioApi(params);
const data = res.data || {};
const endTime = new Date().getTime();
data.speed = formatFileSize(chunkSize / ((endTime - starTime) / 1000)); // 根据每次分片上传时间大致算一个速度
return data;
};
const removeFun = (index: any) => {
listData.value.splice(index, 1);
};
</script>
@@ -124,10 +195,20 @@ const uploadFun = (data: any) => {
.main {
padding: 0 10px;
flex: 1;
.name {
font-size: 12px;
line-height: 14px;
.toper {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 4px;
line-height: 14px;
font-size: 12px;
.name {
flex: 1;
color: var(--el-text-color-primary);
}
.status {
color: var(--el-text-color-secondary);
}
}
.progress {
height: 5px;
@@ -142,6 +223,7 @@ const uploadFun = (data: any) => {
}
}
.options {
width: 20px;
display: flex;
align-items: center;
justify-content: center;

View File

@@ -1,17 +0,0 @@
import { defineStore } from 'pinia';
export const UploadStore = defineStore('upload', {
state: () => {
return {
addFile: {} as any,
};
},
actions: {
uploadFile(file: any, data: any) {
this.addFile = {
file,
data,
};
},
},
});

4
src/utils/eventBus.ts Normal file
View File

@@ -0,0 +1,4 @@
import mitt from 'mitt';
const emitter = mitt();
export default emitter;

View File

@@ -2934,7 +2934,7 @@ minimatch@^9.0.4:
mitt@^3.0.1:
version "3.0.1"
resolved "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz"
resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1"
integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==
ml-array-max@^1.2.4: