|
|
@@ -2,12 +2,15 @@
|
|
|
<div class="file-uploader">
|
|
|
<!-- 文件拖拽区域 -->
|
|
|
<el-upload class="upload-area" drag action="#" :auto-upload="false" :on-change="handleFileChange"
|
|
|
- :on-remove="handleFileRemove" :file-list="displayFileList" multiple>
|
|
|
+ :on-remove="handleFileRemove" :file-list="displayFileList" multiple :show-file-list="false">
|
|
|
<el-icon class="upload-icon"><upload-filled /></el-icon>
|
|
|
<div class="upload-text">
|
|
|
<em>拖拽文件到此处或</em>
|
|
|
<br />
|
|
|
<el-button type="primary" class="select-button"> 点击选择文件 </el-button>
|
|
|
+ <div class="file-restrictions">
|
|
|
+ 支持文件类型: 图片(jpg/jpeg, png), 视频(mp4, avi)
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</el-upload>
|
|
|
|
|
|
@@ -63,42 +66,9 @@
|
|
|
</el-scrollbar>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 上传按钮和历史记录 -->
|
|
|
+ <!-- 上传按钮 -->
|
|
|
<div class="upload-actions">
|
|
|
<el-button type="primary" :disabled="isUploading || fileList.length === 0" @click="handleUpload">上传</el-button>
|
|
|
- <el-button @click="clearHistory" type="info" plain v-if="uploadHistory.length > 0">清空历史</el-button>
|
|
|
- </div>
|
|
|
- <div v-if="uploadHistory.length > 0" class="history-list">
|
|
|
- <h3>上传历史记录 (最近 {{ MAX_HISTORY_ITEMS }} 条):</h3>
|
|
|
- <el-scrollbar height="250px">
|
|
|
- <el-table :data="uploadHistory" style="width: 100%" :cell-style="{ textAlign: 'center' }">
|
|
|
- <el-table-column prop="name" label="文件名" header-align="center">
|
|
|
- <template #default="{ row }">
|
|
|
- <el-tooltip :content="row.name" placement="top">
|
|
|
- <span class="history-filename">{{ row.name }}</span>
|
|
|
- </el-tooltip>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column prop="size" label="大小" width="120" header-align="center">
|
|
|
- <template #default="{ row }">
|
|
|
- {{ formatFileSize(row.size || 0) }}
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column prop="status" label="状态" width="100" header-align="center">
|
|
|
- <template #default="{ row }">
|
|
|
- <el-tag v-if="row.status === 'success'" type="success">成功</el-tag>
|
|
|
- <el-tag v-else-if="row.status === 'error'" type="danger">失败</el-tag>
|
|
|
- <el-tag v-else type="info">{{ row.status }}</el-tag>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <!-- <el-table-column prop="uploadTime" label="上传时间" width="180" /> -->
|
|
|
- <el-table-column label="操作" width="60" header-align="center">
|
|
|
- <template #default="{ $index }">
|
|
|
- <el-button type="danger" link @click="removeHistoryItem($index)">删除</el-button>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- </el-table>
|
|
|
- </el-scrollbar>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
@@ -107,6 +77,21 @@
|
|
|
import { ref, computed, reactive, onMounted, watch, defineProps, defineEmits } from 'vue';
|
|
|
import { UploadFilled } from '@element-plus/icons-vue';
|
|
|
|
|
|
+// 允许的文件类型白名单
|
|
|
+const ALLOWED_EXTENSIONS = {
|
|
|
+ image: ['jpg', 'jpeg', 'png'],
|
|
|
+ // audio: ['mp3', 'wav', 'ogg', 'aac', 'flac'],
|
|
|
+ video: ['mp4', 'avi']
|
|
|
+};
|
|
|
+
|
|
|
+// 检查文件类型是否允许
|
|
|
+const isFileTypeAllowed = (fileName: string) => {
|
|
|
+ const extension = fileName.split('.').pop()?.toLowerCase();
|
|
|
+ if (!extension) return false;
|
|
|
+
|
|
|
+ return Object.values(ALLOWED_EXTENSIONS).flat().includes(extension);
|
|
|
+};
|
|
|
+
|
|
|
// ================= 类型声明 =================
|
|
|
interface UploadState {
|
|
|
status: string;
|
|
|
@@ -116,15 +101,6 @@ interface UploadState {
|
|
|
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, 变量声明 =================
|
|
|
@@ -136,14 +112,6 @@ const props = defineProps({
|
|
|
}); // only declared once
|
|
|
const emit = defineEmits(['update:modelValue']); // only declared once
|
|
|
|
|
|
-const fileTypes = ref([
|
|
|
- { label: '图片', value: 'image' },
|
|
|
- { label: '视频', value: 'video' },
|
|
|
- { label: '文档', value: 'document' },
|
|
|
- { label: '其它', value: 'other' }
|
|
|
-]);
|
|
|
-const selectedType = ref('');
|
|
|
-
|
|
|
const fileList = ref<any[]>([]);
|
|
|
const displayFileList = computed(() => fileList.value);
|
|
|
|
|
|
@@ -154,11 +122,8 @@ const ossId = computed({
|
|
|
});
|
|
|
|
|
|
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';
|
|
|
@@ -182,46 +147,6 @@ function formatFileSize(bytes: number): string {
|
|
|
return (bytes / 1024 / 1024 / 1024).toFixed(1) + ' GB';
|
|
|
}
|
|
|
|
|
|
-// TODO: 上传历史的持久化与回显逻辑可根据业务扩展
|
|
|
-function loadHistory() {
|
|
|
- const raw = localStorage.getItem('smsb-upload-history');
|
|
|
- if (raw) {
|
|
|
- try {
|
|
|
- uploadHistory.value = JSON.parse(raw);
|
|
|
- } catch {
|
|
|
- uploadHistory.value = [];
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-function saveHistory() {
|
|
|
- localStorage.setItem('smsb-upload-history', JSON.stringify(uploadHistory.value.slice(0, MAX_HISTORY_ITEMS)));
|
|
|
-}
|
|
|
-// TODO: 可扩展:支持多文件上传/批量操作时的历史记录批量追加
|
|
|
-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(),
|
|
|
- url: url || null,
|
|
|
- uploadId: uploadId || undefined
|
|
|
- });
|
|
|
- uploadHistory.value = uploadHistory.value.slice(0, MAX_HISTORY_ITEMS);
|
|
|
- saveHistory();
|
|
|
-}
|
|
|
-function clearHistory() {
|
|
|
- uploadHistory.value = [];
|
|
|
- saveHistory();
|
|
|
-}
|
|
|
-function removeHistoryItem(index: number) {
|
|
|
- uploadHistory.value.splice(index, 1);
|
|
|
- saveHistory();
|
|
|
-}
|
|
|
-
|
|
|
-onMounted(() => {
|
|
|
- loadHistory();
|
|
|
-});
|
|
|
-
|
|
|
// 上传成功逻辑示例(请根据实际上传API修改)
|
|
|
// TODO: 对接实际上传API,处理后端返回、进度、错误、合并等流程
|
|
|
function handleUploadSuccess(response: any) {
|
|
|
@@ -233,18 +158,19 @@ function handleUploadSuccess(response: any) {
|
|
|
if (response?.data?.ossId) {
|
|
|
ossId.value = response.data.ossId;
|
|
|
uploadStates[fileUid] = { status: 'success', progress: 100 };
|
|
|
- addHistoryEntry({ ...fileList.value[fileIndex], ossId: response.data.ossId }, 'success');
|
|
|
}
|
|
|
// Remove successfully uploaded file
|
|
|
fileList.value.splice(fileIndex, 1);
|
|
|
- delete uploadStates[fileUid];
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function handleFileChange(uploadFile: any, uploadFiles: any[]) {
|
|
|
- // TODO: 文件类型、大小等前端校验
|
|
|
-
|
|
|
+ if (!isFileTypeAllowed(uploadFile.name)) {
|
|
|
+ ElMessage.error(`不支持的文件类型: ${uploadFile.name},仅支持图片(jpg,png等)、音频(mp3,wav等)和视频(mp4,mov等)文件`);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
// 避免重复添加
|
|
|
if (!fileList.value.some((f) => f.uid === uploadFile.uid)) {
|
|
|
fileList.value.push(uploadFile);
|
|
|
@@ -272,18 +198,11 @@ async function sendChunkRequest(formData: FormData, fileUid: string | number, ch
|
|
|
const isLastChunk = chunkIndex === (state.totalChunks || 1) - 1;
|
|
|
const fileUrl = response.data.data?.fileUrl;
|
|
|
if (isLastChunk && fileUrl) {
|
|
|
- if (state.status !== "success" && !state.historySaved) {
|
|
|
+ if (state.status !== "success") {
|
|
|
state.finalUrl = fileUrl;
|
|
|
state.status = "success";
|
|
|
state.progress = 100;
|
|
|
- addHistoryEntry(file, "success", state.finalUrl, state.uploadId);
|
|
|
- state.historySaved = true;
|
|
|
- // Remove successfully uploaded file
|
|
|
- const fileIndex = fileList.value.findIndex(f => f.uid === fileUid);
|
|
|
- if (fileIndex !== -1) {
|
|
|
- fileList.value.splice(fileIndex, 1);
|
|
|
- delete uploadStates[fileUid];
|
|
|
- }
|
|
|
+ // delete uploadStates[fileUid];
|
|
|
}
|
|
|
} else if (isLastChunk && !fileUrl) {
|
|
|
if (
|
|
|
@@ -294,10 +213,8 @@ async function sendChunkRequest(formData: FormData, fileUid: string | number, ch
|
|
|
state.progress = 100;
|
|
|
setTimeout(() => {
|
|
|
const currentState = uploadStates[fileUid];
|
|
|
- if (currentState?.status === "merging" && !currentState.historySaved) {
|
|
|
+ if (currentState?.status === "merging") {
|
|
|
currentState.status = "success";
|
|
|
- addHistoryEntry(file, "success", currentState.finalUrl, currentState.uploadId);
|
|
|
- currentState.historySaved = true;
|
|
|
ElMessage.success(`${file.name} 上传并合并成功!`);
|
|
|
}
|
|
|
}, 5000);
|
|
|
@@ -313,6 +230,8 @@ async function sendChunkRequest(formData: FormData, fileUid: string | number, ch
|
|
|
state.errorMessage = error.response?.data?.message || error.message || "上传分片时发生网络或服务器错误";
|
|
|
}
|
|
|
throw error;
|
|
|
+ } finally {
|
|
|
+ // Add finally block to ensure cleanup or other necessary actions
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -336,7 +255,6 @@ async function uploadFileChunks(file: any) {
|
|
|
errorMessage: null,
|
|
|
uploadedChunks: new Set(),
|
|
|
finalUrl: null,
|
|
|
- historySaved: false,
|
|
|
};
|
|
|
const chunkPromises: Promise<void>[] = [];
|
|
|
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
|
|
|
@@ -373,18 +291,19 @@ async function uploadFileChunks(file: any) {
|
|
|
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;
|
|
|
- }
|
|
|
+ finalState.status = "success";
|
|
|
+ finalState.progress = 100;
|
|
|
+ // 上传成功后移除 fileList 中的该文件,防止再次被上传
|
|
|
+ const fileIndex = fileList.value.findIndex(f => f.uid === fileUid);
|
|
|
+ if (fileIndex !== -1) fileList.value.splice(fileIndex, 1);
|
|
|
} else {
|
|
|
finalState.status = "error";
|
|
|
finalState.errorMessage = "内部状态不一致";
|
|
|
}
|
|
|
} else if (finalState && (finalState.status === "success" || finalState.status === "merging")) {
|
|
|
- // 已完成
|
|
|
+ // 已完成,但确保移除 fileList 中的该文件
|
|
|
+ const fileIndex = fileList.value.findIndex(f => f.uid === fileUid);
|
|
|
+ if (fileIndex !== -1) fileList.value.splice(fileIndex, 1);
|
|
|
}
|
|
|
}
|
|
|
} catch (error) {
|
|
|
@@ -413,6 +332,8 @@ async function handleUpload() {
|
|
|
"开始上传",
|
|
|
{ confirmButtonText: "确定", cancelButtonText: "取消", type: "info" }
|
|
|
);
|
|
|
+ // 缓存本次上传的文件uid数组
|
|
|
+ const fileUidsToUpload = filesToUpload.map(f => f.uid);
|
|
|
filesToUpload.forEach((file) => {
|
|
|
if (
|
|
|
file.uid !== undefined &&
|
|
|
@@ -427,17 +348,16 @@ async function handleUpload() {
|
|
|
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"
|
|
|
+ const successfulUploads = fileUidsToUpload.filter(
|
|
|
+ uid => uploadStates[uid]?.status === "success"
|
|
|
).length;
|
|
|
- const failedUploads = fileList.value.filter(
|
|
|
- (f) => f.uid !== undefined && uploadStates[f.uid]?.status === "error"
|
|
|
+ const failedUploads = fileUidsToUpload.filter(
|
|
|
+ uid => uploadStates[uid]?.status === "error"
|
|
|
).length;
|
|
|
const stillProcessing = fileList.value.filter(
|
|
|
(f) =>
|
|
|
@@ -552,4 +472,10 @@ async function handleUpload() {
|
|
|
white-space: nowrap;
|
|
|
text-overflow: ellipsis;
|
|
|
}
|
|
|
+
|
|
|
+.file-restrictions {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+ margin-top: 8px;
|
|
|
+}
|
|
|
</style>
|