Przeglądaj źródła

Merge remote-tracking branch 'origin/master'

lihao16 6 miesięcy temu
rodzic
commit
9a7d87f1b1

+ 50 - 124
smsb-plus-ui/src/components/SmsbFileUpload/SmsbFileUploader.vue

@@ -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>