mcwl-pc/app/components/fileUpload/index.vue

555 lines
15 KiB
Vue

<template>
<div class="upload-container">
<div class="fileUpload">
<div class="item-upload">
<!-- <span v-if="required" class="required">*</span> -->
<!-- <n-button type="success" @click="triggerFileSelect"> 文件选择 </n-button> -->
<input
type="file"
ref="fileInputRef"
class="file2"
style="display: none"
@change="selectFile"
/>
</div>
<!-- <div v-if="showFileName" class="item-info">
{{ fileName }}
</div>
<div v-else class="item-info">支持{{ getAcceptType() }}文件格式</div>
<div v-if="progress > 0" class="item-cancel">
<n-button type="success" style="margin-top: 5px" @click="cancelUploadInfo">
X
</n-button>
</div> -->
</div>
<div class="item-process" v-if="progress > 0">
<n-progress
type="line"
:percentage="progress"
indicator-placement="inside"
processing
/>
</div>
<!-- <div v-if="requiredValid" class="item-valid">请上传文件</div> -->
<!-- <div class="resume-upload">
<n-button type="primary" @click="resumeUpload">
继续上传
</n-button>
</div> -->
</div>
</template>
<script setup lang="ts">
import { MessageReactive, useMessage } from "naive-ui";
import SparkMD5 from 'spark-md5';
import { onMounted, ref } from "vue";
const messageReactive: MessageReactive | null = null;
// const { calculateHash } = useFileHash();
// const { calculateHash, isCalculating } = useFileHash()
// debugger
// Props
const props = defineProps({
accept: {
type: Array as PropType<string[]>,
default: () => ["doc", "docx", "pdf", "safetensors", "ckpt"],
},
required: {
type: Boolean,
default: false,
},
fileSize: {
type: Number,
default: 2000,
},
verifyHash: {
type: Boolean,
default: false,
},
verifyName: {
type: Boolean,
default: false,
},
type: {
type: String,
default: "model",
},
});
// 响应式状态
const fileInputRef = ref<HTMLInputElement | null>(null);
const file = ref<File | null>(null);
const fileName = ref("");
const fileSize = ref(0);
const progress = ref(0);
const objectKey = ref("");
const chunkSize = ref(5 * 1024 * 1024);
const totalChunks = ref(0);
const currentChunk = ref(1);
const uploadId = ref("");
const partArr = ref([]);
const showFileName = ref(false);
const bucketName = ref("");
const objectKeyName = ref("");
const requiredValid = ref(false);
const hashCode = ref("");
const message = useMessage();
const currentFileSize = ref(0)
// 添加新的状态来跟踪上传状态
const uploadStatus = ref<'idle' | 'uploading' | 'paused' | 'completed'>('idle');
const uploadedChunks = ref<Set<number>>(new Set());
const retryCount = ref<number>(0);
const maxRetries = 5; // 最大重试次数
// 触发文件选择
const triggerFileSelect = () => {
fileInputRef.value?.click();
};
// 获取接受的文件类型
const getAcceptType = () => {
return props.accept.join(",");
};
// 添加计算hash的函数
const calculateFileHash = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = (event) => {
try {
const buffer = event.target?.result as ArrayBuffer;
const spark = new SparkMD5.ArrayBuffer();
spark.append(buffer);
const hash = spark.end();
resolve(hash);
} catch (error) {
reject(error);
}
};
reader.onerror = () => {
reject(new Error('Failed to read file'));
};
});
};
// const calculateFileHash = (file: File): Promise<string> => {
// return new Promise((resolve, reject) => {
// const reader = new FileReader();
// reader.readAsArrayBuffer(file);
// reader.onload = async (event) => {
// try {
// const buffer = event.target?.result as ArrayBuffer;
// const hashBuffer = await window.crypto.subtle.digest("SHA-256", buffer);
// const hashArray = Array.from(new Uint8Array(hashBuffer));
// const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
// resolve(hashHex);
// } catch (error) {
// reject(error);
// }
// };
// reader.onerror = () => {
// reject(new Error("Failed to read file"));
// };
// });
// };
// 生成唯一ID
// const guid = () => {
// return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
// const r = (Math.random() * 16) | 0;
// const v = c === "x" ? r : (r & 0x3) | 0x8;
// return v.toString(16);
// });
// };
// 初始化参数
const initParam = () => {
file.value = null;
objectKey.value = "";
totalChunks.value = 0;
currentChunk.value = 1;
uploadId.value = "";
partArr.value = [];
requiredValid.value = false;
uploadStatus.value = 'idle';
uploadedChunks.value.clear();
retryCount.value = 0;
};
function getOssDefaultPath(name:any) {
const timestamp = Date.now();
const now = new Date();
const year = now.getFullYear(); // 获取当前年份
const month = String(now.getMonth() + 1).padStart(2, "0"); // 获取当前月份,确保两位
const day = String(now.getDate()).padStart(2, "0"); // 获取当前日期,确保两位
return `${year}/${month}/${day}/${timestamp}/${name}`; // 拼接路径
}
// 选择文件处理
const selectFile = async (event: Event) => {
const target = event.target as HTMLInputElement;
if (!target.files?.length) return;
file.value = target.files[0];
target.value = "";
if (file.value?.size) {
currentFileSize.value = file.value?.size
// 文件大小检查
const calculatedFileSize = Math.round((file.value.size / 1024 / 1024) * 100) / 100;
if (calculatedFileSize > props.fileSize) {
message.warning(`文件大小超过上限${props.fileSize}MB`);
return;
}
fileSize.value = calculatedFileSize;
// 文件类型检查
const selectedFileName = file.value.name;
const fileType = selectedFileName.split(".").pop()?.toLowerCase();
if (!fileType || !props.accept.includes(fileType)) {
message.warning("文件类型不支持");
return;
}
try {
// 如果存在未完成的上传,询问是否继续
if (uploadStatus.value === 'paused' && file.value) {
const shouldResume = window.confirm('检测到未完成的上传,是否继续上传?');
if (shouldResume) {
await resumeUpload();
return;
} else {
// 用户选择重新上传,清除之前的上传状态
initParam();
}
}
// 继续上传流程
progress.value = 0;
fileName.value = file.value.name;
showFileName.value = true;
// 计算文件hash
if (props.verifyHash) {
message.info("正在校验文件hash...");
try {
hashCode.value = await calculateFileHash(file.value);
} catch (err) {
message.warning(err)
hashCode.value = ''
}finally{
// messageReactive.destroy();
// messageReactive = null;
}
// hashCode.value = await calculateHash(file.value)
// hashCode.value = await calculateFileHash(file.value);
// 检查文件是否已存在
try {
const hashRes = await request.get(
`/file/selectHash?hashCode=${hashCode.value}&type=${
props.type === "model" ? 0 : 1
}`
);
if (hashRes.data !== 1) {
message.warning("文件已存在");
return;
}
} finally {
if (messageReactive) {
// messageReactive.destroy();
// messageReactive = null;
}
}
}
// 校验名称
if (props.verifyName) {
message.info("正在校验文件名...");
// 检查文件是否已存在
// 上传文件前先校验是否存在 存在为0 不存在为1
try {
// const res = await request.get(
// `/file/selectFile?type=${props.type}&name=${fileName.value}`
// );
const res = await request.post(
`/file/selectFile`,
{
type:props.type,
name:fileName.value
}
);
if (res.data !== 1) {
message.warning("文件名已存在");
return;
}
} finally {
if (messageReactive) {
// messageReactive.destroy();
// messageReactive = null;
}
}
}
totalChunks.value = Math.ceil(file.value.size / chunkSize.value);
objectKey.value = `${getOssDefaultPath(fileName.value)}`;
// 获取上传ID
const res = await request.get(`/file/getUploadId?objectKey=${objectKey.value}`);
uploadId.value = res.data;
progress.value = 1;
uploadFile();
} catch (error) {
console.error("文件处理失败:", error);
message.error("文件处理失败");
initParam();
}
}
// const target = event.target as HTMLInputElement;
// if (!target.files?.length) return;
// file.value = target.files[0];
// target.value = "";
// if (file.value?.size) {
// const calculatedFileSize = Math.round((file.value.size / 1024 / 1024) * 100) / 100;
// if (calculatedFileSize > props.fileSize) {
// message.warning(`文件大小超过上限${props.fileSize}MB`);
// return;
// }
// fileSize.value = calculatedFileSize;
// const selectedFileName = file.value.name;
// const fileType = selectedFileName.split(".").pop()?.toLowerCase();
// if (!fileType || !props.accept.includes(fileType)) {
// message.warning("文件类型不支持");
// return;
// }
// progress.value = 0;
// fileName.value = file.value.name;
// showFileName.value = true;
// totalChunks.value = Math.ceil(file.value.size / chunkSize.value);
// objectKey.value = `${getOssDefaultPath(fileName.value, guid())}`;
// try {
// const res = await request.get(`/file/getUploadId?objectKey=${objectKey.value}`);
// uploadId.value = res.data;
// progress.value = 1;
// uploadFile();
// } catch (error) {
// message.error("获取上传ID失败");
// }
// }
};
// 文件上传
const uploadFile = async () => {
if (!file.value) return;
while (currentChunk.value <= totalChunks.value) {
// 如果这个分片已经上传成功,跳过
if (uploadedChunks.value.has(currentChunk.value)) {
currentChunk.value++;
continue;
}
const index = currentChunk.value - 1;
const start = index * chunkSize.value;
const end = Math.min((index + 1) * chunkSize.value, file.value.size);
const formData = new FormData();
formData.append("file", file.value.slice(start, end));
formData.append("chunk", String(currentChunk.value));
formData.append("objectKey", objectKey.value);
formData.append("uploadId", uploadId.value);
try {
uploadStatus.value = 'uploading';
const res = await request.post("/file/chunk", formData, {
headers: { "Content-Type": "multipart/form-data" },
// 添加上传进度监听
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round(
((currentChunk.value - 1) / totalChunks.value) * 100 +
(progressEvent.loaded / progressEvent.total!) * (100 / totalChunks.value)
);
progress.value = percentCompleted;
}
});
// 记录成功上传的分片
uploadedChunks.value.add(currentChunk.value);
partArr.value.push(res.data);
currentChunk.value++;
retryCount.value = 0; // 重置重试计数
progress.value = Math.floor((currentChunk.value / totalChunks.value) * 100);
} catch (error) {
console.error("切片上传失败:", error);
// 重试逻辑
if (retryCount.value < maxRetries) {
retryCount.value++;
message.warning(`${currentChunk.value} 个分片上传失败,正在进行第 ${retryCount.value} 次重试...`);
await new Promise(resolve => setTimeout(resolve, 1000)); // 等待1秒后重试
continue; // 重试当前分片
}
// 超过最大重试次数
uploadStatus.value = 'paused';
message.error(`上传失败,已暂停。您可以稍后继续上传。`);
return;
}
}
// 所有分片上传完成
if (currentChunk.value > totalChunks.value) {
progress.value = 99;
uploadStatus.value = 'completed';
await complete();
}
};
// 添加继续上传方法
const resumeUpload = async () => {
if (uploadStatus.value === 'paused' && file.value) {
retryCount.value = 0; // 重置重试计数
message.info('正在继续上传...');
await uploadFile();
}
};
// 完成上传
const complete = async () => {
try {
const res = await request.post(
`/file/completeUpload?objectKey=${objectKey.value}&uploadId=${uploadId.value}`,
partArr.value
);
bucketName.value = res.data.bucketName;
objectKeyName.value = res.data.objectKey;
progress.value = 100;
// 发送上传成功事件
emit("upload-success", {
objectKey: objectKeyName.value,
objectUrl: res.data.objectUrl, // 假设后端返回了文件路径
fileName: fileName.value,
currentFileSize: currentFileSize.value,
hashCode: hashCode.value,
});
initParam();
} catch (error) {
console.error("文件上传失败:", error);
message.warning("文件上传失败");
}
};
// 定义上传完成后发送的事件
const emit = defineEmits<{
"upload-success": [
{
objectKey: string;
objectUrl: string;
fileName: string;
currentFileSize: number;
hashCode: number;
}
];
}>();
// 取消上传
const cancelUploadInfo = async () => {
if (uploadStatus.value !== 'completed') {
try {
await request.post("/file/cancelUpload", {
objectKey: objectKey.value,
uploadId: uploadId.value,
});
uploadStatus.value = 'idle';
uploadedChunks.value.clear();
progress.value = 0;
showFileName.value = false;
initParam();
} catch (error) {
message.error("取消上传失败");
}
}
};
// 校验是否已上传
const validRequired = () => {
if (bucketName.value && objectKeyName.value) {
requiredValid.value = false;
return true;
}
requiredValid.value = true;
return false;
};
// 获取上传信息
const getUploadInfo = () => {
return {
bucketName: bucketName.value,
fileName: objectKeyName.value,
fileSize: fileSize.value,
};
};
// 初始化
onMounted(() => {
progress.value = 0;
});
// 明确暴露方法
defineExpose({
triggerFileSelect,
validRequired,
getUploadInfo,
});
</script>
<style scoped>
.upload-container {
.fileUpload {
display: flex;
flex-direction: row;
padding: 0px 8px;
.item-upload {
width: 100px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
.required {
color: red;
padding: 0px 5px;
}
}
.item-info {
flex: 1;
padding-top: 12px;
font-size: 13px;
}
.item-cancel {
width: 30px;
padding-right: 20px;
}
}
.item-process {
margin-top: 10px;
/* padding: 0px 23px; */
box-sizing: border-box;
}
.item-valid {
color: red;
padding: 0px 10px;
font-size: 13px;
}
}
</style>