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,
|
"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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
});
|
||||||
|
};
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
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;
|
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;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 {
|
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
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>
|
<template>
|
||||||
<div>网盘收藏夹</div>
|
<div>网盘收藏夹</div>
|
||||||
|
<input type="file" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
@ -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: {
|
||||||
|
Loading…
Reference in New Issue
Block a user