feat(projects): 添加了全局上传组件,支持文件夹和文件上传

This commit is contained in:
黄磊 2025-03-10 22:47:27 -04:00
parent 9c92fc6252
commit 76815bcf19
10 changed files with 314 additions and 35 deletions

12
.vscode/settings.json vendored
View File

@ -15,5 +15,15 @@
"prettier.enable": false, "prettier.enable": false,
"typescript.tsdk": "node_modules/typescript/lib", "typescript.tsdk": "node_modules/typescript/lib",
"unocss.root": ["./"], "unocss.root": ["./"],
"vue.server.hybridMode": true "vue.server.hybridMode": true,
"marscode.chatLanguage": "cn",
"marscode.codeCompletionPro": {
"enableCodeCompletionPro": true
},
"marscode.enableInlineCommand": true,
"marscode.enableCodelens": {
"enableInlineDocumentation": true,
"enableInlineUnitTest": true,
"enableInlineExplain": true
}
} }

View File

@ -1,13 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ComponentPublicInstance } from 'vue'; import type { ComponentPublicInstance } from 'vue';
import { nextTick, onMounted, ref } from 'vue'; import { h, nextTick, onMounted, ref } from 'vue';
import { simpleUploadURL } from '@/service/api/pan'; import type { UploadFile } from 'vue-simple-uploader';
import { NButton } from 'naive-ui';
import { fetchCheckFile, fetchUploadFolder, simpleUploadURL } from '@/service/api/pan';
import { localStg } from '@/utils/storage'; import { localStg } from '@/utils/storage';
import { usePanStore } from '@/store/modules/pan';
defineOptions({ defineOptions({
name: 'GlobalUploader' name: 'GlobalUploader'
}); });
const panStore = usePanStore();
/** 数据定义 */ /** 数据定义 */
const uploaderRef = ref(); const uploaderRef = ref();
const showUploader = ref(true); const showUploader = ref(true);
@ -85,6 +90,157 @@ const handleFolderUpload = () => {
} }
}; };
//
const handleUploadBefore = async (files: UploadFile[], uploadParams?: any) => {
if (!files || files.length === 0) {
window.$message?.warning('没有选择要上传的文件');
return;
}
//
fileListLength.value = window.$uploader?.fileList.length || 0;
//
const filePaths = window.$uploader?.filePaths || {};
const paths = Object.keys(filePaths);
if (paths.length > 0) {
try {
//
const folderStructure = paths.map(path => {
const folder = filePaths[path];
return {
path: folder.parent.path,
name: folder.name,
fullPath: folder.parent.path ? `${folder.parent.path}${folder.name}` : folder.name
};
});
//
await fetchUploadFolder({
isFolder: true,
currentDirectory: panStore.currentPath as string,
folderStructure //
});
//
if (uploadParams) {
//
if (uploadParams.mergeMode === 'smart') {
window.$message?.success('使用智能合并模式上传文件');
} else if (uploadParams.renameMode === 'auto') {
window.$message?.success('使用自动重命名模式上传文件');
}
}
} catch (error) {
window.$message?.error(`上传文件夹时出现错误:${error}`);
}
}
//
files.forEach(_file => {
Object.assign(window.$uploader?.opts, {
query: {
isFolder: false,
currentPath: panStore.currentPath,
...(uploadParams || {})
}
});
});
// Dom
nextTick(() => {
window.$uploader?.resume();
});
};
//
const hanldeUploadCancel = () => {
window.$uploader?.cancel();
window.$message?.info('还没实现取消上传的方法');
};
//
const handleSmartMerge = (files: UploadFile[], conflicts: any[]) => {
//
//
handleUploadBefore(files, { mergeMode: 'smart', conflicts });
};
//
const handleRenameAndUpload = (files: UploadFile[], conflicts: any[]) => {
//
// "(1)", "(2)"
handleUploadBefore(files, { renameMode: 'auto', conflicts });
};
//
const handleFilesAdded = async (files: UploadFile[]) => {
if (files.length === 0) return;
try {
// files
const fileInfo = files.map(file => {
// 使
const pathParts = file.relativePath.split(/[/\\]/);
//
const isFolderUpload = pathParts.length > 1;
return {
name: file.name,
//
fullPath: file.relativePath,
// undefined
rootPath: isFolderUpload ? pathParts[0] : undefined,
//
pathParts,
isFolder: isFolderUpload
};
});
const query = {
fileInfo,
currentPath: panStore.currentPath
};
//
const { data: conflictData, error } = await fetchCheckFile(query);
if (!error) {
if (conflictData.exist) {
window.$dialog?.warning({
title: '提示',
content: `文件已存在,是否覆盖?`,
// 使action
action: () => {
return h('div', { style: 'display: flex; justify-content: center; gap: 8px;' }, [
h(
NButton,
{
size: 'small',
onClick: () => handleSmartMerge(files, conflictData.conflicts || [])
},
{ default: () => '智能合并' }
),
h(
NButton,
{
size: 'small',
onClick: () => handleRenameAndUpload(files, conflictData.conflicts || [])
},
{ default: () => '重命名上传' }
),
h(
NButton,
{
size: 'small',
onClick: () => {
hanldeUploadCancel();
}
},
{ default: () => '取消' }
)
]);
}
});
} else {
handleUploadBefore(files);
}
}
} catch (error) {
window.$message?.error(`校验文件是否存在时出现错误:${error}`);
}
};
// //
const closePanel = () => { const closePanel = () => {
if (process.value === -10 || process.value === 100 || fileListLength.value === 0) { if (process.value === -10 || process.value === 100 || fileListLength.value === 0) {
@ -133,6 +289,7 @@ onMounted(() => {
:auto-start="false" :auto-start="false"
:file-status-text="statusText" :file-status-text="statusText"
class="w-720px" class="w-720px"
@files-added="handleFilesAdded"
> >
<uploader-unsupport>您的浏览器不支持上传组件</uploader-unsupport> <uploader-unsupport>您的浏览器不支持上传组件</uploader-unsupport>

View File

@ -16,3 +16,21 @@ export const fetchDeleteFile = (id: string) => {
method: 'delete' method: 'delete'
}); });
}; };
/** 检查文件是否存在 */
export const fetchCheckFile = (query: any) => {
return request({
url: '/pan/file/check/',
method: 'post',
data: query
});
};
/** 上传文件夹 */
export const fetchUploadFolder = (data: any) => {
return request({
url: '/pan/folder',
method: 'post',
data
});
};

View File

@ -1,12 +1,11 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { ref } from 'vue'; import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { SetupStoreId } from '@/enum'; import { SetupStoreId } from '@/enum';
import { localStg } from '@/utils/storage'; import { localStg } from '@/utils/storage';
export const usePanStore = defineStore(SetupStoreId.Pan, () => { export const usePanStore = defineStore(SetupStoreId.Pan, () => {
const fileShowMode = ref<UnionKey.FileListMode>(localStg.get('fileShowMode') || 'grid'); const fileShowMode = ref<UnionKey.FileListMode>(localStg.get('fileShowMode') || 'grid');
const currentPath = ref<string[]>([]); const currentPath = ref<string>();
// 切换文件列表视图的方法 // 切换文件列表视图的方法
const toggleFileShowMode = (mode: UnionKey.FileListMode) => { const toggleFileShowMode = (mode: UnionKey.FileListMode) => {
@ -17,41 +16,22 @@ export const usePanStore = defineStore(SetupStoreId.Pan, () => {
// 初始化路径 // 初始化路径
const initPathFromRoute = (pathStr?: string) => { const initPathFromRoute = (pathStr?: string) => {
if (!pathStr) { if (!pathStr) {
currentPath.value = []; /** 从缓存中拿路径 */
const cachePath = localStg.get('currentPath');
if (cachePath) {
currentPath.value = cachePath;
return;
}
currentPath.value = undefined;
return; return;
} }
const pathArr = pathStr.split('/'); currentPath.value = pathStr;
pathArr.shift(); localStg.set('currentPath', pathStr);
currentPath.value = pathArr;
};
// 带路由更新的导航方法
const navigateTo = async (path: string[]) => {
const router = useRouter();
currentPath.value = path;
// 更新路由(使用 replace 避免产生多余历史记录)
await router.replace({ query: { path: path.length ? `/${path.join('/')}` : undefined } });
};
// 进入文件夹方法
const enterFolder = async (folderName: string) => {
await navigateTo([...currentPath.value, folderName]);
};
// 返回上级方法
const backToParent = async () => {
if (currentPath.value.length > 0) {
await navigateTo(currentPath.value.slice(0, -1));
}
}; };
return { return {
currentPath, currentPath,
initPathFromRoute, initPathFromRoute,
navigateTo,
enterFolder,
backToParent,
fileShowMode, fileShowMode,
toggleFileShowMode toggleFileShowMode
}; };

View File

@ -92,4 +92,6 @@ declare namespace Api {
home: import('@elegant-router/types').LastLevelRouteKey; home: import('@elegant-router/types').LastLevelRouteKey;
} }
} }
namespace Pan {}
} }

View File

@ -37,8 +37,11 @@ declare namespace StorageType {
layout: UnionKey.ThemeLayoutMode; layout: UnionKey.ThemeLayoutMode;
siderCollapse: boolean; siderCollapse: boolean;
}; };
/** 文件展示模式 */
fileShowMode: UnionKey.FileListMode; fileShowMode: UnionKey.FileListMode;
/** uploader chunk size */ /** uploader chunk size */
uploaderChunkSize: number; uploaderChunkSize: number;
/** 当前的路径 */
currentPath: string;
} }
} }

View File

@ -5,5 +5,38 @@ declare module 'vue-simple-uploader' {
export interface Uploader { export interface Uploader {
/** 文件列表 */ /** 文件列表 */
fileList: File[]; fileList: File[];
/** 文件路径 */
filePaths: { [key: string]: UploadFile };
/** 上传选项 */
opts: any;
/** 上传器退出 */
cancel: () => void;
/** 上传暂停 */
resume: () => void;
}
type UploaderOptions = {
/** 上传地址 */
target: string;
/** 上传方法 */
uploadMethod?: string;
/** 上传参数 */
query?: { [key: string]: any };
};
export interface UploadFile {
/** 文件ID */
id: number;
/** 文件名称 */
name: string;
/** 真实的文件 */
file: File;
/** 文件路径 */
relativePath: string;
/** 文件大小 */
size: number;
path: string;
/** 父节点信息 */
parent: UploadFile;
} }
} }

64
src/utils/hasher.ts Normal file
View File

@ -0,0 +1,64 @@
/**
* 使spark-md5 MD5(线) 使Web Worker MD5() 使VueUse的Web Worker
* MD5() 使hash-wasm MD5() URL: https://juejin.cn/post/7340636105765437492
*/
// import { useWebWorkerFn } from '@vueuse/core';
// type WorkerParams = { file: File; chunkSize: number };
// type WorkerResult = { hash?: string; error?: string };
// 定义一个可序列化的函数
// const computedMd5InWorker = async (params: WorkerParams): Promise<WorkerResult> => {
// try {
// const { file, chunkSize } = params;
// const md5 = await createMD5();
// md5.init();
// const totalChunks = Math.ceil(file.size / chunkSize);
// for (let i = 0; i < totalChunks; i += 1) {
// const start = i * chunkSize;
// const end = Math.min(start + chunkSize, file.size);
// const chunk = file.slice(start, end);
// // eslint-disable-next-line no-await-in-loop
// const buffer = await chunk.arrayBuffer();
// md5.update(new Uint8Array(buffer));
// }
// return { hash: md5.digest() };
// } catch (errot) {
// return { error: (errot as Error).message };
// }
// };
// export const useFileHasher = () => {
// // 使用 VueUse 的 Web Worker 封装
// const { workerFn } = useWebWorkerFn(
// async (params: WorkerParams): Promise<WorkerResult> => {
// try {
// const { file, chunkSize } = params;
// } catch (errot) {
// return { error: (errot as Error).message };
// }
// },
// {
// timeout: 50000,
// dependencies: ['https://cdn.jsdelivr.net/npm/hash-wasm@4.12.0/dist/md5.umd.min.js']
// }
// );
// // 带进度反馈的计算方法
// const computedMD5 = async (file: File, chunkSize = 4 * 1024 * 1024) => {
// if (!file) return { error: 'No file provided' };
// // 调用 Web Worker 计算
// try {
// return await workerFn({ file, chunkSize });
// } catch (error) {
// return { error: (error as Error).message };
// }
// };
// return {
// computedMD5
// };
// };

View File

@ -1,7 +1,19 @@
<script setup lang="ts"></script> <script setup lang="ts">
// import { useFileHasher } from '@/utils/hasher';
// const { computedMD5 } = useFileHasher();
// const handleChange = async (e: any) => {
// const file = e.target.files[0];
// if (!file) return;
// const res = await computedMD5(file);
// console.log(res);
// };
</script>
<template> <template>
<div>网盘收藏夹</div> <div>网盘收藏夹</div>
<input type="file" />
</template> </template>
<style scoped></style> <style scoped></style>

View File

@ -34,7 +34,7 @@ export default defineConfig(configEnv => {
server: { server: {
host: '0.0.0.0', host: '0.0.0.0',
port: 9527, port: 9527,
open: true, open: false,
proxy: createViteProxy(viteEnv, enableProxy) proxy: createViteProxy(viteEnv, enableProxy)
}, },
preview: { preview: {