diff --git a/src/api/file.ts b/src/api/file.ts index 2b2f43c401cd38a7a84d12f6e77f0ab9d356f43a..f27d439abfb39bee316bae0e3fd7100c6c7902f2 100644 --- a/src/api/file.ts +++ b/src/api/file.ts @@ -4,6 +4,7 @@ import type { FileItem, FileRecycleItem, } from '@/types/modules/file'; +import { AxiosProgressEvent } from 'axios'; /** * 查询文件列表 @@ -41,8 +42,12 @@ export function getFolderPath(folderId: string) { /** * 上传文件 + * + * @param file 文件 + * @param parentId pid + * @param onProgress progress */ -export function uploadFile(file: File, parentId?: string) { +export function uploadFile(file: File, parentId?: string, onProgress?: (progressEvent: AxiosProgressEvent) => void) { const formData = new FormData(); formData.append('file', file); if (parentId) { @@ -52,6 +57,7 @@ export function uploadFile(file: File, parentId?: string) { headers: { 'Content-Type': 'multipart/form-data', }, + onUploadProgress: onProgress, }); } diff --git a/src/store/index.ts b/src/store/index.ts index 18aa19e767cdbc03b664dffb537f2eb8db4fcec4..3918c8f048b188f74d4628eec5413f9297b9355e 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -2,8 +2,9 @@ import { createPinia } from 'pinia'; import useAppStore from './modules/app'; import useUserStore from './modules/user'; import useStorageStore from './modules/storage'; +import useUploadTaskStore from './modules/upload'; const pinia = createPinia(); -export { useAppStore, useUserStore, useStorageStore }; +export { useAppStore, useUserStore, useStorageStore, useUploadTaskStore }; export default pinia; diff --git a/src/store/modules/upload/index.ts b/src/store/modules/upload/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..54b991ee7e44529eb8b8c34e1549e16bb9f94e18 --- /dev/null +++ b/src/store/modules/upload/index.ts @@ -0,0 +1,127 @@ +import { defineStore } from 'pinia'; +import { ref } from 'vue'; +import { Message } from '@arco-design/web-vue'; +import { uploadFile } from '@/api/file'; + +export interface UploadTask { + id: string | number; + file: File; + status: 'pending' | 'uploading' | 'success' | 'error'; + progress: number; + parentId: string | null; + errorMessage?: string; +} + +let taskIdCounter = 0; + +export const useUploadTaskStore = defineStore('uploadTask', () => { + // 正在进行和已完成的上传任务列表 + const taskList = ref([]); + // 面板是否展开 + const isExpanded = ref(false); + // 是否显示面板 + const showPanel = ref(false); + + /** + * 内部私有方法:执行单个文件上传 + */ + const doUpload = async (task: UploadTask) => { + try { + task.status = 'uploading'; + + const formData = new FormData(); + formData.append('file', task.file); + if (task.parentId) { + formData.append('parentId', task.parentId); + } + + await uploadFile(task.file, task.parentId ?? '', (progressEvent) => { + task.progress = Math.round( + (progressEvent.loaded * 100) / (progressEvent.total ?? 1) + ); + }); + + task.status = 'success'; + task.progress = 100; + } catch (error) { + task.status = 'error'; + task.errorMessage = (error as Error).message || '上传失败'; + Message.error(`${task.file.name} 上传失败`); + } + }; + + /** + * 添加新文件到上传队列 + * @param files File[] 文件列表 + * @param parentId 目标目录ID + */ + const addUploadTasks = (files: File[], parentId: string) => { + if (!showPanel.value) { + showPanel.value = true; + isExpanded.value = true; + } + + const existingFingerprints = new Set( + taskList.value.map((task) => { + const { file } = task; + return `${file.name}-${file.size}-${file.lastModified}`; + }) + ); + + const newFilesToUpload = files.filter((file) => { + const fingerprint = `${file.name}-${file.size}-${file.lastModified}`; + + if (existingFingerprints.has(fingerprint)) { + return false; + } + existingFingerprints.add(fingerprint); + return true; + }); + + if (newFilesToUpload.length === 0) { + Message.info('所选文件均已在上传列表中'); + return; + } + if (newFilesToUpload.length < files.length) { + Message.warning('已自动跳过列表中已存在的文件'); + } + + const newTasks: UploadTask[] = newFilesToUpload.map((file) => { + taskIdCounter += 1; + return { + id: `upload-task-${taskIdCounter}`, + file, + status: 'pending', + progress: 0, + parentId, + } + }); + taskList.value.push(...newTasks); + + taskList.value.forEach((task) => { + doUpload(task); + }); + }; + + // 展开/收起 + const toggleExpand = () => { + isExpanded.value = !isExpanded.value; + }; + + // 关闭/清空 + const closePanel = () => { + showPanel.value = false; + taskList.value = []; + }; + + return { + taskList, + isExpanded, + showPanel, + addUploadTasks, + toggleExpand, + closePanel, + }; +}); + +export default useUploadTaskStore; diff --git a/src/views/files/components/index.ts b/src/views/files/components/index.ts index 73f4713f7eccbb43a979ce0d01edbf7ea396145c..f674131367ac5de4d63020c7f1edfa4386e9f1e8 100644 --- a/src/views/files/components/index.ts +++ b/src/views/files/components/index.ts @@ -17,3 +17,5 @@ export { default as ShareModal } from './share-modal.vue'; export { default as DeleteConfirmModal } from './delete-confirm-modal.vue'; export { default as RecycleBinView } from './recycle-bin-view.vue'; export { default as MySharesView } from './my-shares-view.vue'; +export { default as UploadModalV2 } from './upload-modal-v2.vue'; +export { default as UploadPanel } from './upload-panel.vue'; \ No newline at end of file diff --git a/src/views/files/components/upload-modal-v2.vue b/src/views/files/components/upload-modal-v2.vue new file mode 100644 index 0000000000000000000000000000000000000000..a7ae6bf17932792ef49baa743a5cde91795f6844 --- /dev/null +++ b/src/views/files/components/upload-modal-v2.vue @@ -0,0 +1,196 @@ + + + + + diff --git a/src/views/files/components/upload-panel.vue b/src/views/files/components/upload-panel.vue new file mode 100644 index 0000000000000000000000000000000000000000..ef8a91df3ef80451acda352142b74c5a5f7df06b --- /dev/null +++ b/src/views/files/components/upload-panel.vue @@ -0,0 +1,185 @@ + + + + + diff --git a/src/views/files/index.vue b/src/views/files/index.vue index 665b25cbf26cbc186fb7a6af408cdf13f6d71458..f94db19f6b081a9ce353712ed3170a78e932914a 100644 --- a/src/views/files/index.vue +++ b/src/views/files/index.vue @@ -145,8 +145,21 @@ + + + + + - + + + @@ -207,6 +223,7 @@ IconFileVideo, IconMore, IconShareAlt, + IconUpload } from '@arco-design/web-vue/es/icon'; import type { FileItem } from '@/types/modules/file'; import { useFileList, useFileOperations } from './hooks'; @@ -223,6 +240,8 @@ DeleteConfirmModal, RecycleBinView, MySharesView, + UploadModalV2, + UploadPanel } from './components'; const route = useRoute(); @@ -595,4 +614,16 @@ overflow-y: auto; } } + + /** 上传按钮 */ + .custom-upload-fab { + height: 56px; + width: 56px; + position: fixed; + right: 40px; + bottom: 40px; + z-index: 99; + + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + }