sunshibo 2 недель назад
Родитель
Сommit
cd405df1cd

+ 73 - 4
src/api/upload.js

@@ -710,6 +710,19 @@ function buildOssObjectPublicUrl(bucket, endpointHost, objectKey) {
  * @param {{ signal?: AbortSignal }} [fetchOptions]
  * @returns {Promise<{ fileUrl: string; downloadUrl: string }>}
  */
+function reportOssUploadProgress(progressRatio, fetchOptions = {}) {
+  const p = Number(progressRatio);
+  if (!Number.isFinite(p)) return;
+  const overlay = useSimpleUploadOverlayStore();
+  if (!overlay.show) return;
+  const batch = fetchOptions.batchProgress;
+  if (batch && typeof batch.index === "number" && typeof batch.total === "number") {
+    overlay.setMultiFileUploadProgress(batch.index, batch.total, p);
+    return;
+  }
+  overlay.setUploadProgress(p, { skipAudit: Boolean(fetchOptions.skipAuditProgress) });
+}
+
 async function putFileToOssWithSts(file, fetchOptions = {}) {
   const sts = await fetchOssStsToken(fetchOptions);
   const endpoint = resolveOssUploadEndpoint(sts.endpoint);
@@ -729,7 +742,10 @@ async function putFileToOssWithSts(file, fetchOptions = {}) {
   try {
     result = await client.put(objectKey, file, {
       mime: file.type || undefined,
-      timeout: putTimeoutMs
+      timeout: putTimeoutMs,
+      progress: p => {
+        reportOssUploadProgress(p, fetchOptions);
+      }
     });
   } catch (err) {
     if (isUploadUserCancelledError(err)) {
@@ -908,15 +924,59 @@ export async function requestOssFinalizeWithOcr(file, objectKey, ocrType, fetchO
  * @returns {Promise<{ fileUrl: string; downloadUrl: string }>}
  */
 async function putFileToOssWithStsAndFinalize(file, fetchOptions = {}) {
+  const overlay = useSimpleUploadOverlayStore();
+  const trackOverlay = overlay.show;
+  const batch = fetchOptions.batchProgress;
+
   const uploaded = await putFileToOssWithSts(file, fetchOptions);
+  if (trackOverlay) {
+    if (batch && typeof batch.index === "number" && typeof batch.total === "number") {
+      overlay.setMultiFileUploadProgress(batch.index, batch.total, 1);
+      overlay.beginAuditPhaseForBatch(batch.index, batch.total);
+    } else {
+      overlay.setUploadProgress(1);
+      overlay.beginAuditPhase();
+    }
+  }
+
   const audit = await requestOssFinalizeAudit(file, uploaded.objectKey, fetchOptions);
+
+  if (trackOverlay) {
+    if (batch && typeof batch.index === "number" && typeof batch.total === "number") {
+      overlay.setMultiFileAuditProgress(batch.index, batch.total, 1);
+      if (batch.index === batch.total - 1) {
+        overlay.completeSuccess();
+      }
+    } else {
+      overlay.completeSuccess();
+    }
+  }
+
   const downloadUrl = audit.downloadUrl?.trim() || uploaded.downloadUrl;
   return { fileUrl: uploaded.fileUrl, downloadUrl };
 }
 
 /** 仅 STS 直传 OSS,不走 /upload/oss/finalize(证照等由页面单独 OCR 审核) */
 async function putFileToOssWithStsOnly(file, fetchOptions = {}) {
-  const uploaded = await putFileToOssWithSts(file, fetchOptions);
+  const overlay = useSimpleUploadOverlayStore();
+  const trackOverlay = overlay.show;
+  const uploaded = await putFileToOssWithSts(file, {
+    ...fetchOptions,
+    skipAuditProgress: true
+  });
+  if (trackOverlay) {
+    const batch = fetchOptions.batchProgress;
+    if (batch && typeof batch.index === "number" && typeof batch.total === "number") {
+      overlay.setMultiFileUploadProgress(batch.index, batch.total, 1);
+      if (batch.index === batch.total - 1) {
+        overlay.setUploadProgress(1, { skipAudit: true });
+        overlay.completeSuccess();
+      }
+    } else {
+      overlay.setUploadProgress(1, { skipAudit: true });
+      overlay.completeSuccess();
+    }
+  }
   return {
     fileUrl: uploaded.fileUrl,
     downloadUrl: uploaded.downloadUrl,
@@ -1155,13 +1215,22 @@ export async function uploadFilesToOss(files, _fileType, options = {}) {
     assertWebUploadFilesWithinLimit(fileArr, _fileType, { maxVideoMb });
     const runUpload = async signal => {
       const uploadedUrls = [];
-      const fetchOpts = signal ? { signal } : {};
-      for (const file of fileArr) {
+      const total = fileArr.length;
+      for (let i = 0; i < total; i++) {
+        const file = fileArr[i];
+        const fetchOpts = {
+          ...(signal ? { signal } : {}),
+          batchProgress: total > 1 ? { index: i, total } : undefined
+        };
         const { fileUrl, downloadUrl } = skipFinalize
           ? await putFileToOssWithStsOnly(file, fetchOpts)
           : await putFileToOssWithStsAndFinalize(file, fetchOpts);
         uploadedUrls.push(downloadUrl || fileUrl);
       }
+      const overlay = useSimpleUploadOverlayStore();
+      if (overlay.show && skipFinalize) {
+        overlay.completeSuccess();
+      }
       return uploadedUrls;
     };
 

+ 4 - 2
src/components/Upload/Imgs.vue

@@ -176,8 +176,10 @@ async function finishBatchUploadOverlay() {
   if (batchHadUploadError) {
     overlay.dismiss();
   } else {
-    overlay.bumpToComplete();
-    await sleep(280);
+    if (overlay.show && overlay.percent < 100) {
+      overlay.completeSuccess();
+    }
+    await sleep(420);
     overlay.dismiss();
     if (props.showSuccessNotification) {
       ElMessage.success("上传成功");

+ 9 - 4
src/components/popupLoading/index.vue

@@ -34,10 +34,12 @@
           {{ title }}
         </p>
       </div>
-      <div class="pl-divider" />
-      <button type="button" class="pl-cancel" @click="onCancel">
-        <span class="pl-cancel__text">{{ cancelText }}</span>
-      </button>
+      <template v-if="showCancel">
+        <div class="pl-divider" />
+        <button type="button" class="pl-cancel" @click="onCancel">
+          <span class="pl-cancel__text">{{ cancelText }}</span>
+        </button>
+      </template>
     </div>
   </div>
 </template>
@@ -75,6 +77,9 @@ const displayPercent = computed(() => {
   return Math.round(Math.min(100, Math.max(0, n)));
 });
 
+/** 进度到 100% 后不再允许取消(上传/审核已结束) */
+const showCancel = computed(() => displayPercent.value < 100);
+
 /** SVG 环形:与 Element 主题色对齐 */
 const vb = 100;
 const cx = vb / 2;

+ 149 - 15
src/stores/modules/simpleUploadOverlay.ts

@@ -2,16 +2,54 @@ import { defineStore } from "pinia";
 import { ref } from "vue";
 import { ElMessage } from "element-plus";
 
-let progressTimer: ReturnType<typeof setInterval> | null = null;
+/** 上传占 90%,内容审核占 10% */
+const UPLOAD_PERCENT_MAX = 90;
+const AUDIT_PERCENT_MAX = 100;
+
+const AUDIT_ROTATE_MESSAGES = [
+  "检测违规内容中...",
+  "审核内容合规性...",
+  "筛查敏感信息中...",
+  "识别不良信息中...",
+  "智能风控核验中..."
+];
+
+const AUDIT_TITLE_DONE = "上传完毕,处理中...";
+const AUDIT_TITLE_SUCCESS = "上传成功";
+
+let uploadCreepTimer: ReturnType<typeof setInterval> | null = null;
+let auditMessageTimer: ReturnType<typeof setInterval> | null = null;
+let auditProgressTimer: ReturnType<typeof setInterval> | null = null;
+let auditMessageIndex = 0;
 let activeController: AbortController | null = null;
 
-function clearProgressTimer() {
-  if (progressTimer) {
-    clearInterval(progressTimer);
-    progressTimer = null;
+function clearUploadCreepTimer() {
+  if (uploadCreepTimer) {
+    clearInterval(uploadCreepTimer);
+    uploadCreepTimer = null;
+  }
+}
+
+function clearAuditTimers() {
+  if (auditMessageTimer) {
+    clearInterval(auditMessageTimer);
+    auditMessageTimer = null;
+  }
+  if (auditProgressTimer) {
+    clearInterval(auditProgressTimer);
+    auditProgressTimer = null;
   }
 }
 
+function clearAllTimers() {
+  clearUploadCreepTimer();
+  clearAuditTimers();
+}
+
+function clampPercent(n: number) {
+  return Math.min(AUDIT_PERCENT_MAX, Math.max(0, Math.round(n)));
+}
+
 export const useSimpleUploadOverlayStore = defineStore("simple-upload-overlay", () => {
   const show = ref(false);
   const percent = ref(0);
@@ -20,28 +58,119 @@ export const useSimpleUploadOverlayStore = defineStore("simple-upload-overlay",
 
   function beginUpload(opts?: { title?: string }) {
     activeController?.abort(new DOMException("已开始新的上传", "AbortError"));
-    clearProgressTimer();
+    clearAllTimers();
     activeController = new AbortController();
     title.value = opts?.title ?? "上传中";
-    percent.value = 3;
+    percent.value = 0;
     show.value = true;
-    progressTimer = setInterval(() => {
-      if (percent.value < 88) {
-        percent.value = Math.min(88, percent.value + 2 + Math.random() * 6);
+    auditMessageIndex = 0;
+    /** OSS 未回调 progress 时缓慢推进,上限留到 85% 等待真实进度 */
+    uploadCreepTimer = setInterval(() => {
+      if (percent.value < 85) {
+        percent.value = Math.min(85, percent.value + 1);
       }
-    }, 260);
+    }, 220);
     return activeController.signal;
   }
 
+  /** 单文件 OSS 上传进度 ratio 0~1 → 0~90% */
+  function setUploadProgress(ratio: number, opts?: { skipAudit?: boolean }) {
+    const r = Math.min(1, Math.max(0, Number(ratio) || 0));
+    const max = opts?.skipAudit ? AUDIT_PERCENT_MAX : UPLOAD_PERCENT_MAX;
+    const next = clampPercent(r * max);
+    if (next > percent.value) {
+      percent.value = next;
+    }
+    if (next >= UPLOAD_PERCENT_MAX - 1) {
+      clearUploadCreepTimer();
+    }
+  }
+
+  /** 多文件:第 index 个文件、共 total 个,单文件内 ratio 0~1 */
+  function setMultiFileUploadProgress(fileIndex: number, totalFiles: number, ratio: number) {
+    const total = Math.max(1, totalFiles);
+    const idx = Math.min(Math.max(0, fileIndex), total - 1);
+    const r = Math.min(1, Math.max(0, Number(ratio) || 0));
+    const base = (idx / total) * UPLOAD_PERCENT_MAX;
+    const span = UPLOAD_PERCENT_MAX / total;
+    const next = clampPercent(base + r * span);
+    if (next > percent.value) {
+      percent.value = next;
+    }
+    if (r >= 1 && idx === total - 1) {
+      clearUploadCreepTimer();
+      percent.value = UPLOAD_PERCENT_MAX;
+    }
+  }
+
+  /** 多文件审核阶段进度 */
+  function setMultiFileAuditProgress(fileIndex: number, totalFiles: number, ratio: number) {
+    const total = Math.max(1, totalFiles);
+    const idx = Math.min(Math.max(0, fileIndex), total - 1);
+    const r = Math.min(1, Math.max(0, Number(ratio) || 0));
+    const base = UPLOAD_PERCENT_MAX + (idx / total) * (AUDIT_PERCENT_MAX - UPLOAD_PERCENT_MAX);
+    const span = (AUDIT_PERCENT_MAX - UPLOAD_PERCENT_MAX) / total;
+    percent.value = clampPercent(base + r * span);
+  }
+
+  function startAuditMessageRotation() {
+    clearAuditTimers();
+    auditMessageIndex = 0;
+    title.value = AUDIT_TITLE_DONE;
+    auditMessageTimer = setInterval(() => {
+      title.value = AUDIT_ROTATE_MESSAGES[auditMessageIndex % AUDIT_ROTATE_MESSAGES.length];
+      auditMessageIndex += 1;
+    }, 1200);
+    let auditP = percent.value < UPLOAD_PERCENT_MAX ? UPLOAD_PERCENT_MAX : percent.value;
+    percent.value = UPLOAD_PERCENT_MAX;
+    auditProgressTimer = setInterval(() => {
+      if (auditP < 99) {
+        auditP = Math.min(99, auditP + 0.35);
+        percent.value = clampPercent(auditP);
+      }
+    }, 280);
+  }
+
+  /** 进入审核阶段:固定 90%,先显示「上传完毕,处理中...」再轮播审核文案 */
+  function beginAuditPhase() {
+    clearUploadCreepTimer();
+    percent.value = UPLOAD_PERCENT_MAX;
+    title.value = AUDIT_TITLE_DONE;
+    setTimeout(() => {
+      if (!show.value) return;
+      startAuditMessageRotation();
+    }, 600);
+  }
+
+  function beginAuditPhaseForBatch(fileIndex: number, totalFiles: number) {
+    clearUploadCreepTimer();
+    const total = Math.max(1, totalFiles);
+    const idx = Math.min(Math.max(0, fileIndex), total - 1);
+    const auditBase = UPLOAD_PERCENT_MAX + (idx / total) * (AUDIT_PERCENT_MAX - UPLOAD_PERCENT_MAX);
+    percent.value = clampPercent(Math.max(UPLOAD_PERCENT_MAX, auditBase));
+    title.value = AUDIT_TITLE_DONE;
+    setTimeout(() => {
+      if (!show.value) return;
+      startAuditMessageRotation();
+    }, 600);
+  }
+
+  function completeSuccess() {
+    clearAllTimers();
+    percent.value = AUDIT_PERCENT_MAX;
+    title.value = AUDIT_TITLE_SUCCESS;
+  }
+
+  /** @deprecated 请用 completeSuccess */
   function bumpToComplete() {
-    clearProgressTimer();
-    percent.value = 100;
+    completeSuccess();
   }
 
   function dismiss() {
-    clearProgressTimer();
+    clearAllTimers();
     show.value = false;
     percent.value = 0;
+    title.value = "上传中";
     activeController = null;
   }
 
@@ -51,7 +180,6 @@ export const useSimpleUploadOverlayStore = defineStore("simple-upload-overlay",
     ElMessage.info("取消上传");
   }
 
-  /** 供 skipSimpleUploadOverlay 的上传与 beginUpload 配套,把 fetch 绑到同一 AbortSignal */
   function getActiveAbortSignal(): AbortSignal | undefined {
     return activeController?.signal;
   }
@@ -62,6 +190,12 @@ export const useSimpleUploadOverlayStore = defineStore("simple-upload-overlay",
     title,
     cancelText,
     beginUpload,
+    setUploadProgress,
+    setMultiFileUploadProgress,
+    setMultiFileAuditProgress,
+    beginAuditPhase,
+    beginAuditPhaseForBatch,
+    completeSuccess,
     bumpToComplete,
     dismiss,
     userCancel,

+ 7 - 5
src/utils/withSimpleUploadOverlay.ts

@@ -6,9 +6,9 @@ function sleep(ms: number) {
 }
 
 /**
- * 使用全局 PopupLoading 包裹 OSS 上传;成功后提示「上传成功」
- * 取消(AbortError)不弹成功提示;失败时关闭弹层,由调用方决定是否 ElMessage.error
- * `successMessage === null` 时不弹成功提示(用于上传后还要继续审核等场景)。
+ * 使用全局 PopupLoading 包裹 OSS 上传 + 审核
+ * 进度:上传 0~90%,审核 90~100%;100% 且文案为「上传成功」后关闭
+ * `successMessage === null` 时不额外 ElMessage(弹层内已展示成功文案)。
  */
 export async function withSimpleUploadOverlay<T>(
   task: (signal: AbortSignal) => Promise<T>,
@@ -18,8 +18,10 @@ export async function withSimpleUploadOverlay<T>(
   const signal = overlay.beginUpload({ title: options?.title });
   try {
     const result = await task(signal);
-    overlay.bumpToComplete();
-    await sleep(280);
+    if (overlay.show && overlay.percent < 100) {
+      overlay.completeSuccess();
+    }
+    await sleep(420);
     overlay.dismiss();
     if (options?.successMessage !== null) {
       ElMessage.success(options?.successMessage ?? "上传成功");

+ 4 - 2
src/views/dynamicManagement/publishDynamic.vue

@@ -162,8 +162,10 @@ async function finishPublishSimpleOverlay() {
   if (publishSimpleOverlayHadError) {
     overlay.dismiss();
   } else {
-    overlay.bumpToComplete();
-    await publishOverlaySleep(280);
+    if (overlay.show && overlay.percent < 100) {
+      overlay.completeSuccess();
+    }
+    await publishOverlaySleep(420);
     overlay.dismiss();
   }
 }

+ 4 - 2
src/views/storeDecoration/add.vue

@@ -635,8 +635,10 @@ async function finishBatchUploadOverlay() {
   if (batchHadUploadError) {
     overlay.dismiss();
   } else {
-    overlay.bumpToComplete();
-    await overlaySleep(280);
+    if (overlay.show && overlay.percent < 100) {
+      overlay.completeSuccess();
+    }
+    await overlaySleep(420);
     overlay.dismiss();
   }
 }