|
|
@@ -37,23 +37,23 @@
|
|
|
<el-table-column label="状态/进度" width="180">
|
|
|
<template #default="{ row }">
|
|
|
<div v-if="uploadStates[row.uid]">
|
|
|
- <el-progress v-if="uploadStates[ossId]?.status === 'uploading'"
|
|
|
- :percentage="uploadStates[ossId]?.progress || 0" :stroke-width="8" striped striped-flow
|
|
|
+ <el-progress v-if="uploadStates[row.uid]?.status === 'uploading'"
|
|
|
+ :percentage="uploadStates[row.uid]?.progress || 0" :stroke-width="8" striped striped-flow
|
|
|
:duration="10" />
|
|
|
- <el-tag v-else-if="uploadStates[ossId]?.status === 'success'" type="success">上传成功</el-tag>
|
|
|
- <span v-else-if="uploadStates[ossId]?.status === 'error'">
|
|
|
+ <el-tag v-else-if="uploadStates[row.uid]?.status === 'success'" type="success">上传成功</el-tag>
|
|
|
+ <span v-else-if="uploadStates[row.uid]?.status === 'error'">
|
|
|
<el-tag type="danger">上传失败</el-tag>
|
|
|
- <el-tooltip v-if="uploadStates[ossId]?.errorMessage"
|
|
|
- :content="uploadStates[ossId]?.errorMessage || '未知错误'" placement="top">
|
|
|
+ <el-tooltip v-if="uploadStates[row.uid]?.errorMessage"
|
|
|
+ :content="uploadStates[row.uid]?.errorMessage || '未知错误'" placement="top">
|
|
|
<el-icon style="margin-left: 4px; vertical-align: middle; cursor: help">
|
|
|
<QuestionFilled />
|
|
|
</el-icon>
|
|
|
</el-tooltip>
|
|
|
</span>
|
|
|
- <el-tag v-else-if="uploadStates[ossId]?.status === 'merging'" type="warning"
|
|
|
+ <el-tag v-else-if="uploadStates[row.uid]?.status === 'merging'" type="warning"
|
|
|
effect="light">合并中...</el-tag>
|
|
|
- <el-tag v-else-if="uploadStates[ossId]?.status === 'pending'" type="info">待上传</el-tag>
|
|
|
- <el-tag v-else type="info" effect="plain">未知状态 ({{ uploadStates[ossId]?.status }})</el-tag>
|
|
|
+ <el-tag v-else-if="uploadStates[row.uid]?.status === 'pending'" type="info">待上传</el-tag>
|
|
|
+ <el-tag v-else type="info" effect="plain">未知状态 ({{ uploadStates[row.uid]?.status }})</el-tag>
|
|
|
</div>
|
|
|
<el-tag v-else type="info">待上传</el-tag>
|
|
|
</template>
|
|
|
@@ -61,7 +61,7 @@
|
|
|
<el-table-column fixed="right" label="操作" width="120">
|
|
|
<template #default="{ row }">
|
|
|
<el-button type="danger" link @click="handleManualRemove(row)"
|
|
|
- :disabled="uploadStates[ossId]?.status === 'uploading'">删除</el-button>
|
|
|
+ :disabled="uploadStates[row.uid]?.status === 'uploading'">删除</el-button>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
</el-table>
|
|
|
@@ -111,12 +111,19 @@ interface UploadState {
|
|
|
status: string;
|
|
|
progress?: number;
|
|
|
errorMessage?: string;
|
|
|
+ uploadId?: string;
|
|
|
+ totalChunks?: number;
|
|
|
+ uploadedChunks?: Set<number>;
|
|
|
+ finalUrl?: string | null;
|
|
|
+ historySaved?: boolean;
|
|
|
}
|
|
|
interface UploadHistoryEntry {
|
|
|
name: string;
|
|
|
size: number;
|
|
|
status: string;
|
|
|
uploadTime: string;
|
|
|
+ url?: string | null;
|
|
|
+ uploadId?: string;
|
|
|
}
|
|
|
|
|
|
// ================= props, emits, 变量声明 =================
|
|
|
@@ -149,6 +156,12 @@ const uploadStates = reactive<Record<string | number, UploadState>>({});
|
|
|
const uploadHistory = ref<UploadHistoryEntry[]>([]);
|
|
|
const MAX_HISTORY_ITEMS = 10;
|
|
|
|
|
|
+// 分片上传相关常量
|
|
|
+// const API_ENDPOINT = "http://localhost:8087/api/upload"; // TODO: 替换为实际后端API
|
|
|
+const CHUNK_SIZE = 1 * 1024 * 1024; // 1MB
|
|
|
+import { ElMessage, ElMessageBox } from "element-plus";
|
|
|
+import { uploadChunk } from '@/api/smsb/source/minioData';
|
|
|
+
|
|
|
const isUploading = computed(() => {
|
|
|
return Object.values(uploadStates).some((state) => state.status === 'uploading');
|
|
|
});
|
|
|
@@ -183,12 +196,14 @@ function saveHistory() {
|
|
|
localStorage.setItem('smsb-upload-history', JSON.stringify(uploadHistory.value.slice(0, MAX_HISTORY_ITEMS)));
|
|
|
}
|
|
|
// TODO: 可扩展:支持多文件上传/批量操作时的历史记录批量追加
|
|
|
-function addHistoryEntry(file: any, status: string) {
|
|
|
+function addHistoryEntry(file: any, status: string, url?: string | null, uploadId?: string) {
|
|
|
uploadHistory.value.unshift({
|
|
|
name: file.name,
|
|
|
size: file.size,
|
|
|
status,
|
|
|
- uploadTime: new Date().toLocaleString()
|
|
|
+ uploadTime: new Date().toLocaleString(),
|
|
|
+ url: url || null,
|
|
|
+ uploadId: uploadId || undefined
|
|
|
});
|
|
|
uploadHistory.value = uploadHistory.value.slice(0, MAX_HISTORY_ITEMS);
|
|
|
saveHistory();
|
|
|
@@ -236,19 +251,192 @@ function handleManualRemove(fileToRemove: any) {
|
|
|
delete uploadStates[fileToRemove.uid];
|
|
|
}
|
|
|
|
|
|
-// TODO: 对接实际上传API,完善错误处理与用户提示,支持多文件上传/批量操作
|
|
|
+// ========== 分片上传核心逻辑 ==========
|
|
|
+async function sendChunkRequest(formData: FormData, fileUid: string | number, chunkIndex: number, file: any): Promise<void> {
|
|
|
+ try {
|
|
|
+ const response = await uploadChunk(formData);
|
|
|
+ if (response.status === 200 && response.data?.success) {
|
|
|
+ const state = uploadStates[fileUid];
|
|
|
+ if (!state) return;
|
|
|
+ state.uploadedChunks?.add(chunkIndex);
|
|
|
+ state.progress = Math.round((state.uploadedChunks!.size / (state.totalChunks || 1)) * 100);
|
|
|
+ const isLastChunk = chunkIndex === (state.totalChunks || 1) - 1;
|
|
|
+ const fileUrl = response.data.data?.fileUrl;
|
|
|
+ if (isLastChunk && fileUrl) {
|
|
|
+ if (state.status !== "success" && !state.historySaved) {
|
|
|
+ state.finalUrl = fileUrl;
|
|
|
+ state.status = "success";
|
|
|
+ state.progress = 100;
|
|
|
+ addHistoryEntry(file, "success", state.finalUrl, state.uploadId);
|
|
|
+ state.historySaved = true;
|
|
|
+ }
|
|
|
+ } else if (isLastChunk && !fileUrl) {
|
|
|
+ if (
|
|
|
+ state.uploadedChunks!.size === state.totalChunks &&
|
|
|
+ state.status === "uploading"
|
|
|
+ ) {
|
|
|
+ state.status = "merging";
|
|
|
+ state.progress = 100;
|
|
|
+ setTimeout(() => {
|
|
|
+ const currentState = uploadStates[fileUid];
|
|
|
+ if (currentState?.status === "merging" && !currentState.historySaved) {
|
|
|
+ currentState.status = "success";
|
|
|
+ addHistoryEntry(file, "success", currentState.finalUrl, currentState.uploadId);
|
|
|
+ currentState.historySaved = true;
|
|
|
+ ElMessage.success(`${file.name} 上传并合并成功!`);
|
|
|
+ }
|
|
|
+ }, 5000);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ throw new Error(response.data?.message || `Chunk ${chunkIndex + 1} upload failed with status ${response.status}`);
|
|
|
+ }
|
|
|
+ } catch (error: any) {
|
|
|
+ const state = uploadStates[fileUid];
|
|
|
+ if (state && state.status !== "error") {
|
|
|
+ state.status = "error";
|
|
|
+ state.errorMessage = error.response?.data?.message || error.message || "上传分片时发生网络或服务器错误";
|
|
|
+ }
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function uploadFileChunks(file: any) {
|
|
|
+ const rawFile = file.raw;
|
|
|
+ if (!rawFile) {
|
|
|
+ uploadStates[file.uid].status = "error";
|
|
|
+ uploadStates[file.uid].errorMessage = "文件数据无效";
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const fileUid = file.uid;
|
|
|
+ const totalChunks = Math.ceil(rawFile.size / CHUNK_SIZE);
|
|
|
+ // 使用 ossId 作为 uploadId
|
|
|
+ const uploadId = ossId.value || fileUid;
|
|
|
+ uploadStates[fileUid] = {
|
|
|
+ ...uploadStates[fileUid],
|
|
|
+ status: "uploading",
|
|
|
+ uploadId,
|
|
|
+ totalChunks,
|
|
|
+ progress: 0,
|
|
|
+ errorMessage: null,
|
|
|
+ uploadedChunks: new Set(),
|
|
|
+ finalUrl: null,
|
|
|
+ historySaved: false,
|
|
|
+ };
|
|
|
+ const chunkPromises: Promise<void>[] = [];
|
|
|
+ for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
|
|
|
+ const start = chunkIndex * CHUNK_SIZE;
|
|
|
+ const end = Math.min(start + CHUNK_SIZE, rawFile.size);
|
|
|
+ const chunk = rawFile.slice(start, end);
|
|
|
+ const formData = new FormData();
|
|
|
+ formData.append("file", chunk, `${file.name}.chunk${chunkIndex}`);
|
|
|
+ formData.append("filename", file.name);
|
|
|
+ formData.append("chunkIndex", String(chunkIndex));
|
|
|
+ formData.append("totalChunks", String(totalChunks));
|
|
|
+ formData.append("fileSize", String(rawFile.size));
|
|
|
+ formData.append("fileType", rawFile.type || "application/octet-stream");
|
|
|
+ formData.append("uploadId", uploadId);
|
|
|
+ // 打印每个分片的参数
|
|
|
+ console.log('[分片上传] chunk params', {
|
|
|
+ uploadId,
|
|
|
+ filename: file.name,
|
|
|
+ chunkIndex,
|
|
|
+ totalChunks,
|
|
|
+ fileSize: rawFile.size,
|
|
|
+ fileType: rawFile.type || "application/octet-stream"
|
|
|
+ });
|
|
|
+ chunkPromises.push(sendChunkRequest(formData, fileUid, chunkIndex, file));
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const results = await Promise.allSettled(chunkPromises);
|
|
|
+ const failedChunk = results.find((result) => result.status === "rejected");
|
|
|
+ if (failedChunk) {
|
|
|
+ uploadStates[fileUid].status = "error";
|
|
|
+ uploadStates[fileUid].errorMessage = uploadStates[fileUid].errorMessage || "一个或多个分片上传失败";
|
|
|
+ ElMessage.error(`${file.name} 上传失败: ${uploadStates[fileUid]?.errorMessage}`);
|
|
|
+ } else {
|
|
|
+ const finalState = uploadStates[fileUid];
|
|
|
+ if (finalState && finalState.status === "uploading") {
|
|
|
+ if (finalState.uploadedChunks!.size === totalChunks) {
|
|
|
+ if (!finalState.historySaved) {
|
|
|
+ finalState.status = "success";
|
|
|
+ finalState.progress = 100;
|
|
|
+ addHistoryEntry(file, "success", finalState.finalUrl, finalState.uploadId);
|
|
|
+ finalState.historySaved = true;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ finalState.status = "error";
|
|
|
+ finalState.errorMessage = "内部状态不一致";
|
|
|
+ }
|
|
|
+ } else if (finalState && (finalState.status === "success" || finalState.status === "merging")) {
|
|
|
+ // 已完成
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ uploadStates[fileUid].status = "error";
|
|
|
+ uploadStates[fileUid].errorMessage = "上传过程中发生意外错误";
|
|
|
+ ElMessage.error(`${file.name} 上传失败: ${uploadStates[fileUid]?.errorMessage || "未知错误"}`);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 多文件上传入口,弹窗确认
|
|
|
async function handleUpload() {
|
|
|
- for (const file of fileList.value) {
|
|
|
- uploadStates[file.uid] = { status: 'uploading', progress: 0 };
|
|
|
- try {
|
|
|
- // 这里只是模拟上传,实际应替换为后端API
|
|
|
- await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
|
- uploadStates[file.uid] = { status: 'success', progress: 100 };
|
|
|
- addHistoryEntry(file, 'success');
|
|
|
- } catch (e: any) {
|
|
|
- uploadStates[file.uid] = { status: 'error', errorMessage: e?.message || '上传失败' };
|
|
|
- addHistoryEntry(file, 'error');
|
|
|
+ const filesToUpload = fileList.value.filter(
|
|
|
+ (file) =>
|
|
|
+ file.uid !== undefined &&
|
|
|
+ (!uploadStates[file.uid] ||
|
|
|
+ uploadStates[file.uid]?.status === "pending" ||
|
|
|
+ uploadStates[file.uid]?.status === "error")
|
|
|
+ );
|
|
|
+ if (filesToUpload.length === 0) {
|
|
|
+ ElMessage.info("没有待上传或上传失败的文件。");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm(
|
|
|
+ `即将上传 ${filesToUpload.length} 个文件。确定吗?`,
|
|
|
+ "开始上传",
|
|
|
+ { confirmButtonText: "确定", cancelButtonText: "取消", type: "info" }
|
|
|
+ );
|
|
|
+ filesToUpload.forEach((file) => {
|
|
|
+ if (
|
|
|
+ file.uid !== undefined &&
|
|
|
+ uploadStates[file.uid]?.status === "error"
|
|
|
+ ) {
|
|
|
+ const existingState = uploadStates[file.uid];
|
|
|
+ uploadStates[file.uid] = {
|
|
|
+ status: "pending",
|
|
|
+ progress: 0,
|
|
|
+ uploadId: existingState?.uploadId || null,
|
|
|
+ totalChunks: 0,
|
|
|
+ errorMessage: null,
|
|
|
+ uploadedChunks: new Set(),
|
|
|
+ finalUrl: null,
|
|
|
+ historySaved: false,
|
|
|
+ };
|
|
|
+ }
|
|
|
+ });
|
|
|
+ const allUploadPromises = filesToUpload.map((file) => uploadFileChunks(file));
|
|
|
+ await Promise.allSettled(allUploadPromises);
|
|
|
+ const successfulUploads = fileList.value.filter(
|
|
|
+ (f) => f.uid !== undefined && uploadStates[f.uid]?.status === "success"
|
|
|
+ ).length;
|
|
|
+ const failedUploads = fileList.value.filter(
|
|
|
+ (f) => f.uid !== undefined && uploadStates[f.uid]?.status === "error"
|
|
|
+ ).length;
|
|
|
+ const stillProcessing = fileList.value.filter(
|
|
|
+ (f) =>
|
|
|
+ f.uid !== undefined &&
|
|
|
+ (uploadStates[f.uid]?.status === "uploading" ||
|
|
|
+ uploadStates[f.uid]?.status === "merging")
|
|
|
+ ).length;
|
|
|
+ let summaryMessage = `上传任务处理完成: ${successfulUploads} 个成功, ${failedUploads} 个失败。`;
|
|
|
+ if (stillProcessing > 0) {
|
|
|
+ summaryMessage += ` ${stillProcessing} 个仍在处理中。`;
|
|
|
}
|
|
|
+ ElMessage.info(summaryMessage);
|
|
|
+ } catch {
|
|
|
+ ElMessage.info("上传已取消");
|
|
|
}
|
|
|
}
|
|
|
</script>
|