Compare commits

...
Author SHA1 Message Date
guoyuqi 7cdc4f476a feat: add container catalog batch upload 2026-10-20 10:26:18 +08:00
3 changed files with 74 additions and 62 deletions
+4 -2
View File
@@ -1,4 +1,5 @@
import http from '@/api'; import http from '@/api';
import type { AxiosRequestConfig } from 'axios';
import { ResPage, SearchWithPage } from '../interface'; import { ResPage, SearchWithPage } from '../interface';
import { Container } from '../interface/container'; import { Container } from '../interface/container';
import { TimeoutEnum } from '@/enums/http-enum'; import { TimeoutEnum } from '@/enums/http-enum';
@@ -23,10 +24,11 @@ export const loadContainerUsers = (name: string) => {
export const listContainerFiles = (params: Container.ContainerFileReq) => { export const listContainerFiles = (params: Container.ContainerFileReq) => {
return http.post<Array<Container.ContainerFileInfo>>(`/containers/files/search`, params, TimeoutEnum.T_40S); return http.post<Array<Container.ContainerFileInfo>>(`/containers/files/search`, params, TimeoutEnum.T_40S);
}; };
export const uploadContainerFile = (params: FormData) => { export const uploadContainerFile = (params: FormData, config?: AxiosRequestConfig & { skipErrorMessage?: boolean }) => {
return http.upload(`/containers/files/upload`, params, { return http.upload(`/containers/files/upload`, params, {
headers: { 'Content-Type': 'multipart/form-data' },
timeout: TimeoutEnum.T_5M, timeout: TimeoutEnum.T_5M,
...config,
headers: { 'Content-Type': 'multipart/form-data', ...config?.headers },
}); });
}; };
export const getContainerFileContent = (params: Container.ContainerFileReq) => { export const getContainerFileContent = (params: Container.ContainerFileReq) => {
@@ -32,11 +32,9 @@
@blur="cancelPathEditing" @blur="cancelPathEditing"
@keyup.enter.prevent="submitPathInput" @keyup.enter.prevent="submitPathInput"
/> />
<el-upload ref="uploadRef" :auto-upload="false" :show-file-list="false" :on-change="onUploadChange"> <el-button class="mt-2" type="primary" plain @click="onUpload">
<el-button class="mt-2" :loading="uploading" type="primary" plain> {{ $t('commons.button.upload') }}
{{ $t('commons.button.upload') }} </el-button>
</el-button>
</el-upload>
<el-button class="mt-2 ml-3" plain :disabled="selectedRows.length === 0" @click="onBatchDelete"> <el-button class="mt-2 ml-3" plain :disabled="selectedRows.length === 0" @click="onBatchDelete">
{{ $t('commons.button.delete') }} {{ $t('commons.button.delete') }}
</el-button> </el-button>
@@ -97,6 +95,7 @@
</el-form> </el-form>
</template> </template>
</DrawerPro> </DrawerPro>
<FileUpload ref="uploadRef" @close="loadContainerFiles" />
<DialogPro v-model="previewVisible" :title="previewTitle" size="w-70" :close-on-click-modal="true"> <DialogPro v-model="previewVisible" :title="previewTitle" size="w-70" :close-on-click-modal="true">
<el-alert <el-alert
v-if="previewTruncated" v-if="previewTruncated"
@@ -118,11 +117,11 @@ import {
getContainerFileContent, getContainerFileContent,
getContainerFileSize, getContainerFileSize,
listContainerFiles, listContainerFiles,
uploadContainerFile,
} from '@/api/modules/container'; } from '@/api/modules/container';
import { MsgError, MsgSuccess, MsgWarning } from '@/utils/message'; import { MsgError, MsgSuccess, MsgWarning } from '@/utils/message';
import i18n from '@/lang'; import i18n from '@/lang';
import { ElInput, ElMessageBox, UploadFile, UploadInstance } from 'element-plus'; import { ElInput, ElMessageBox } from 'element-plus';
import FileUpload from '@/views/host/file-management/upload/index.vue';
import { Document, FolderOpened, HomeFilled } from '@element-plus/icons-vue'; import { Document, FolderOpened, HomeFilled } from '@element-plus/icons-vue';
import { computeSize2 } from '@/utils/size'; import { computeSize2 } from '@/utils/size';
const visible = ref(false); const visible = ref(false);
@@ -130,9 +129,8 @@ const title = ref('');
const containerID = ref(''); const containerID = ref('');
const filePath = ref('/'); const filePath = ref('/');
const containerFiles = ref<any[]>([]); const containerFiles = ref<any[]>([]);
const uploadRef = ref<UploadInstance>(); const uploadRef = ref<InstanceType<typeof FileUpload>>();
const pathInputRef = ref<InstanceType<typeof ElInput>>(); const pathInputRef = ref<InstanceType<typeof ElInput>>();
const uploading = ref(false);
const previewVisible = ref(false); const previewVisible = ref(false);
const previewTitle = ref(''); const previewTitle = ref('');
const previewContent = ref(''); const previewContent = ref('');
@@ -253,27 +251,8 @@ const submitPathInput = async () => {
} }
}; };
const onUploadChange = async (uploadFile: UploadFile) => { const onUpload = () => {
if (!uploadFile.raw) { uploadRef.value?.acceptParams({ path: filePath.value, containerID: containerID.value });
return;
}
const formData = new FormData();
formData.append('containerID', containerID.value);
formData.append('path', filePath.value);
formData.append('file', uploadFile.raw);
uploading.value = true;
await uploadContainerFile(formData)
.then(async () => {
MsgSuccess(i18n.global.t('file.uploadSuccess'));
await loadContainerFiles();
})
.catch((err) => {
MsgError(err?.message || i18n.global.t('commons.msg.operationFailed'));
})
.finally(() => {
uploading.value = false;
uploadRef.value?.clearFiles();
});
}; };
const onOpenFile = async (path: string) => { const onOpenFile = async (path: string) => {
@@ -119,8 +119,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, reactive, ref } from 'vue'; import { computed, onMounted, onBeforeUnmount, ref } from 'vue';
import { UploadFile, UploadFiles, UploadInstance, UploadProps, UploadRawFile } from 'element-plus'; import { UploadFile, UploadFiles, UploadInstance, UploadProps, UploadRawFile, genFileId } from 'element-plus';
import { uploadContainerFile } from '@/api/modules/container';
import { batchCheckFiles, stopChunkUpload, uploadFileData } from '@/api/modules/files'; import { batchCheckFiles, stopChunkUpload, uploadFileData } from '@/api/modules/files';
import { CHUNK_SIZE, uploadChunkWithRetry } from '@/utils/chunk-upload'; import { CHUNK_SIZE, uploadChunkWithRetry } from '@/utils/chunk-upload';
import i18n from '@/lang'; import i18n from '@/lang';
@@ -134,6 +135,7 @@ import { useGlobalStore } from '@/composables/useGlobalStore';
interface UploadFileProps { interface UploadFileProps {
path: string; path: string;
isAppendOnly?: boolean; isAppendOnly?: boolean;
containerID?: string;
} }
const uploadRef = ref<UploadInstance>(); const uploadRef = ref<UploadInstance>();
@@ -154,6 +156,8 @@ const uploadOverallPercent = computed(() => {
const open = ref(false); const open = ref(false);
const isAppendOnly = ref(false); const isAppendOnly = ref(false);
const path = ref(); const path = ref();
const containerID = ref('');
const targetNode = ref('');
let uploadHelper = ref(''); let uploadHelper = ref('');
const dialogExistFileRef = ref(); const dialogExistFileRef = ref();
const abortController = ref<AbortController | null>(null); const abortController = ref<AbortController | null>(null);
@@ -213,9 +217,6 @@ const closePage = () => {
clearFiles(); clearFiles();
em('close', false); em('close', false);
}; };
const state = reactive({
uploadEle: null,
});
const uploaderFiles = ref<UploadFiles>([]); const uploaderFiles = ref<UploadFiles>([]);
const hoverIndex = ref<number | null>(null); const hoverIndex = ref<number | null>(null);
const tmpFiles = ref<UploadFiles>([]); const tmpFiles = ref<UploadFiles>([]);
@@ -225,9 +226,11 @@ const upload = (command: string) => {
if (uploadLocked.value) { if (uploadLocked.value) {
return; return;
} }
state.uploadEle.webkitdirectory = command == 'dir'; const input = uploadRef.value?.$el.querySelector('input[type=file]') as HTMLInputElement | null;
uploadRef.value.$el.querySelector('input').value = ''; if (!input) return;
uploadRef.value.$el.querySelector('input').click(); input.webkitdirectory = command === 'dir';
input.value = '';
input.click();
}; };
const removeFile = (index: number) => { const removeFile = (index: number) => {
@@ -267,7 +270,7 @@ const handleDrop = async (event: DragEvent) => {
}; };
const convertFileToUploadFile = (file: File, path: string): UploadFile => { const convertFileToUploadFile = (file: File, path: string): UploadFile => {
const uid = Date.now(); const uid = genFileId();
const uploadRawFile: UploadRawFile = new File([file], file.name, { const uploadRawFile: UploadRawFile = new File([file], file.name, {
type: file.type, type: file.type,
@@ -294,12 +297,15 @@ const traverseFileTree = async (item: any, path = '') => {
return; return;
} }
if (item.isFile) { if (item.isFile) {
if (tmpFiles.value.length > 1000) { if (uploaderFiles.value.length + tmpFiles.value.length >= 1000) {
breakFlag.value = true; breakFlag.value = true;
return; return;
} }
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
item.file((file: File) => { item.file((file: File) => {
if (uploaderFiles.value.length + tmpFiles.value.length >= 1000) {
breakFlag.value = true;
}
if (!breakFlag.value) { if (!breakFlag.value) {
tmpFiles.value.push(convertFileToUploadFile(file, path)); tmpFiles.value.push(convertFileToUploadFile(file, path));
} }
@@ -365,7 +371,14 @@ const handleSuccess: UploadProps['onSuccess'] = (res, file) => {
}; };
const submit = async () => { const submit = async () => {
const files = uploaderFiles.value.slice(); if (uploadLocked.value) return;
const files = containerID.value
? uploaderFiles.value.filter((file) => file.status !== 'success')
: uploaderFiles.value.slice();
if (containerID.value) {
await uploadFile(files, false);
return;
}
const fileNamesWithPath = Array.from( const fileNamesWithPath = Array.from(
new Set(files.map((file) => `${path.value}/${file.raw.webkitRelativePath || file.name}`)), new Set(files.map((file) => `${path.value}/${file.raw.webkitRelativePath || file.name}`)),
); );
@@ -410,7 +423,8 @@ const uploadFile = async (files: any[], overwrite: boolean) => {
upLoading.value = true; upLoading.value = true;
uploadTotalCount.value = files.length; uploadTotalCount.value = files.length;
const controller = new AbortController(); const controller = new AbortController();
const uploadNode = currentNode.value; const uploadNode = containerID.value ? targetNode.value : currentNode.value;
const uploadContainerID = containerID.value;
abortController.value = controller; abortController.value = controller;
let successCount = 0; let successCount = 0;
try { try {
@@ -426,10 +440,27 @@ const uploadFile = async (files: any[], overwrite: boolean) => {
let isSuccess = false; let isSuccess = false;
try { try {
isSuccess = if (uploadContainerID) {
Number(file.size) === 0 const data = new FormData();
? await uploadEmptyFile(file, controller, uploadNode, overwrite) data.append('containerID', uploadContainerID);
: await uploadChunkedFile(file, controller, uploadNode, overwrite); data.append('path', getUploadPath(file));
data.append('file', file.raw, getFilenameFromPath(file.raw.webkitRelativePath || file.name));
await uploadContainerFile(data, {
headers: { CurrentNode: uploadNode },
signal: controller.signal,
skipErrorMessage: true,
timeout: 0,
onUploadProgress: ({ loaded, total }) => {
uploadPercent.value = total ? Math.round((loaded / total) * 100) : 0;
},
});
isSuccess = true;
} else {
isSuccess =
Number(file.size) === 0
? await uploadEmptyFile(file, controller, uploadNode, overwrite)
: await uploadChunkedFile(file, controller, uploadNode, overwrite);
}
} catch (error) { } catch (error) {
if (!controller.signal.aborted) { if (!controller.signal.aborted) {
MsgError(getErrorMessage(error)); MsgError(getErrorMessage(error));
@@ -573,6 +604,9 @@ const getFilenameFromPath = (path: string) => {
}; };
const acceptParams = (props: UploadFileProps) => { const acceptParams = (props: UploadFileProps) => {
if (uploadLocked.value) return;
containerID.value = props.containerID || '';
targetNode.value = currentNode.value;
path.value = props.path; path.value = props.path;
isAppendOnly.value = Boolean(props.isAppendOnly); isAppendOnly.value = Boolean(props.isAppendOnly);
open.value = true; open.value = true;
@@ -581,22 +615,19 @@ const acceptParams = (props: UploadFileProps) => {
uploadCurrentIndex.value = 0; uploadCurrentIndex.value = 0;
uploadHelper.value = ''; uploadHelper.value = '';
canceling.value = false; canceling.value = false;
nextTick(() => {
state.uploadEle = document.querySelector('.el-upload__input');
});
}; };
onMounted(() => { const beforeUnload = (event: BeforeUnloadEvent) => {
window.addEventListener('beforeunload', (e) => { if (upLoading.value) {
if (upLoading.value) { event.preventDefault();
if (upLoading.value) { event.returnValue = i18n.global.t('file.cancelUploadHelper');
e.preventDefault(); }
e.returnValue = i18n.global.t('file.cancelUploadHelper'); };
return i18n.global.t('file.cancelUploadHelper'); onMounted(() => window.addEventListener('beforeunload', beforeUnload));
} onBeforeUnmount(() => {
} window.removeEventListener('beforeunload', beforeUnload);
}); abortController.value?.abort();
void cleanupActiveChunkUpload();
}); });
defineExpose({ acceptParams, handleDrop, open }); defineExpose({ acceptParams, handleDrop, open });