mirror of
https://github.com/soybeanjs/soybean-admin.git
synced 2025-09-27 05:36:43 +08:00
feat(projects): 添加了全局上传组件,支持文件夹和文件上传
This commit is contained in:
parent
9c92fc6252
commit
76815bcf19
12
.vscode/settings.json
vendored
12
.vscode/settings.json
vendored
@ -15,5 +15,15 @@
|
||||
"prettier.enable": false,
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"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
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import type { ComponentPublicInstance } from 'vue';
|
||||
import { nextTick, onMounted, ref } from 'vue';
|
||||
import { simpleUploadURL } from '@/service/api/pan';
|
||||
import { h, nextTick, onMounted, ref } from 'vue';
|
||||
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 { usePanStore } from '@/store/modules/pan';
|
||||
|
||||
defineOptions({
|
||||
name: 'GlobalUploader'
|
||||
});
|
||||
|
||||
const panStore = usePanStore();
|
||||
|
||||
/** 数据定义 */
|
||||
const uploaderRef = ref();
|
||||
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 = () => {
|
||||
if (process.value === -10 || process.value === 100 || fileListLength.value === 0) {
|
||||
@ -133,6 +289,7 @@ onMounted(() => {
|
||||
:auto-start="false"
|
||||
:file-status-text="statusText"
|
||||
class="w-720px"
|
||||
@files-added="handleFilesAdded"
|
||||
>
|
||||
<uploader-unsupport>您的浏览器不支持上传组件</uploader-unsupport>
|
||||
|
||||
|
@ -16,3 +16,21 @@ export const fetchDeleteFile = (id: string) => {
|
||||
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
|
||||
});
|
||||
};
|
||||
|
@ -1,12 +1,11 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { SetupStoreId } from '@/enum';
|
||||
import { localStg } from '@/utils/storage';
|
||||
|
||||
export const usePanStore = defineStore(SetupStoreId.Pan, () => {
|
||||
const fileShowMode = ref<UnionKey.FileListMode>(localStg.get('fileShowMode') || 'grid');
|
||||
const currentPath = ref<string[]>([]);
|
||||
const currentPath = ref<string>();
|
||||
|
||||
// 切换文件列表视图的方法
|
||||
const toggleFileShowMode = (mode: UnionKey.FileListMode) => {
|
||||
@ -17,41 +16,22 @@ export const usePanStore = defineStore(SetupStoreId.Pan, () => {
|
||||
// 初始化路径
|
||||
const initPathFromRoute = (pathStr?: string) => {
|
||||
if (!pathStr) {
|
||||
currentPath.value = [];
|
||||
/** 从缓存中拿路径 */
|
||||
const cachePath = localStg.get('currentPath');
|
||||
if (cachePath) {
|
||||
currentPath.value = cachePath;
|
||||
return;
|
||||
}
|
||||
const pathArr = pathStr.split('/');
|
||||
pathArr.shift();
|
||||
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));
|
||||
currentPath.value = undefined;
|
||||
return;
|
||||
}
|
||||
currentPath.value = pathStr;
|
||||
localStg.set('currentPath', pathStr);
|
||||
};
|
||||
|
||||
return {
|
||||
currentPath,
|
||||
initPathFromRoute,
|
||||
navigateTo,
|
||||
enterFolder,
|
||||
backToParent,
|
||||
fileShowMode,
|
||||
toggleFileShowMode
|
||||
};
|
||||
|
2
src/typings/api.d.ts
vendored
2
src/typings/api.d.ts
vendored
@ -92,4 +92,6 @@ declare namespace Api {
|
||||
home: import('@elegant-router/types').LastLevelRouteKey;
|
||||
}
|
||||
}
|
||||
|
||||
namespace Pan {}
|
||||
}
|
||||
|
3
src/typings/storage.d.ts
vendored
3
src/typings/storage.d.ts
vendored
@ -37,8 +37,11 @@ declare namespace StorageType {
|
||||
layout: UnionKey.ThemeLayoutMode;
|
||||
siderCollapse: boolean;
|
||||
};
|
||||
/** 文件展示模式 */
|
||||
fileShowMode: UnionKey.FileListMode;
|
||||
/** uploader chunk size */
|
||||
uploaderChunkSize: number;
|
||||
/** 当前的路径 */
|
||||
currentPath: string;
|
||||
}
|
||||
}
|
||||
|
33
src/typings/uploader.d.ts
vendored
33
src/typings/uploader.d.ts
vendored
@ -5,5 +5,38 @@ declare module 'vue-simple-uploader' {
|
||||
export interface Uploader {
|
||||
/** 文件列表 */
|
||||
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
64
src/utils/hasher.ts
Normal 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
|
||||
// };
|
||||
// };
|
@ -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>
|
||||
<div>网盘收藏夹</div>
|
||||
<input type="file" />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
@ -34,7 +34,7 @@ export default defineConfig(configEnv => {
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 9527,
|
||||
open: true,
|
||||
open: false,
|
||||
proxy: createViteProxy(viteEnv, enableProxy)
|
||||
},
|
||||
preview: {
|
||||
|
Loading…
Reference in New Issue
Block a user