浏览代码

fix: 修改上传的提示语等小bug

sgc 1 天之前
父节点
当前提交
1ae9e30b45

+ 54 - 10
src/api/upload.js

@@ -4,9 +4,36 @@ import { ElMessage } from "element-plus";
 import { AI_UPLOAD_FILES_PUBLIC_BASE, BASE_AI_URL } from "@/utils/config";
 import { withSimpleUploadOverlay } from "@/utils/withSimpleUploadOverlay";
 
-/** 非 TUS 简单上传接口路径(与 Apifox「上传文件-非TUS」一致;开发环境经 VITE_PROXY /ai-upload 转发) */
+/** 简单上传的路径(不含 base) */
 const SIMPLE_UPLOAD_PATH = "/upload/simple";
 
+/**
+ * 已由 uploadFilesToOss 弹出过 ElMessage,调用方勿重复 error
+ * @param {unknown} err
+ */
+export function isUploadApiErrorAlreadyMessaged(err) {
+  return Boolean(err && typeof err === "object" && /** @type {{ __uploadMessageShown?: boolean }} */ (err).__uploadMessageShown);
+}
+
+/**
+ * 用户取消上传(关闭弹层、AbortSignal)或请求被顶替:不向用户弹出技术性英文(如 signal is aborted without reason)
+ * @param {unknown} err
+ * @returns {boolean}
+ */
+export function isUploadUserCancelledError(err) {
+  if (err == null || typeof err !== "object") return false;
+  const o = /** @type {{ name?: unknown; message?: unknown }} */ (err);
+  if (String(o.name ?? "") === "AbortError") return true;
+  const msg = String(o.message ?? "").toLowerCase();
+  if (!msg) return false;
+  return (
+    msg.includes("signal is aborted") ||
+    msg.includes("the user aborted") ||
+    msg.includes("aborted a request") ||
+    msg.includes("body stream was aborted")
+  );
+}
+
 /** 从路径或 fileType 解析文件后缀(如 jpg、mp4),用于生成带格式的文件名 */
 export function getFileExtension(filePath, fileType) {
   const pathPart = (filePath || "").split("?")[0];
@@ -334,14 +361,24 @@ async function postFileToSimpleUpload(file, fetchOptions = {}) {
 
   const { signal } = fetchOptions;
 
-  const res = await fetch(`${base}${SIMPLE_UPLOAD_PATH}`, {
-    method: "POST",
-    headers,
-    /** 不带跨域 Cookie,减轻上传服务 CORS 要求(鉴权仅用 Authorization) */
-    credentials: "omit",
-    body: formData,
-    signal: signal ?? undefined
-  });
+  const uploadUrl = `${base}${SIMPLE_UPLOAD_PATH}`;
+  let res;
+  try {
+    res = await fetch(uploadUrl, {
+      method: "POST",
+      headers,
+      /** 不带跨域 Cookie,减轻上传服务 CORS 要求(鉴权仅用 Authorization) */
+      credentials: "omit",
+      body: formData,
+      signal: signal ?? undefined
+    });
+  } catch (err) {
+    if (isUploadUserCancelledError(err)) {
+      throw err instanceof Error ? err : new Error(String(err ?? "AbortError"));
+    }
+    console.error("[upload/simple] 上传请求失败:", uploadUrl, err);
+    throw new Error("上传失败");
+  }
 
   const rawText = await res.text();
   let parsed = null;
@@ -462,11 +499,18 @@ export async function uploadFilesToOss(files, _fileType, options = {}) {
   } catch (e) {
     closeLoading();
     console.error("上传失败", e);
-    if (e?.name === "AbortError") {
+    if (isUploadUserCancelledError(e)) {
       throw e;
     }
     const msg = e?.message || "上传失败";
     ElMessage.error(msg);
+    try {
+      if (e && typeof e === "object") {
+        Object.defineProperty(e, "__uploadMessageShown", { value: true, enumerable: false, configurable: true });
+      }
+    } catch (_) {
+      /* ignore */
+    }
     throw e;
   }
 }

+ 2 - 2
src/stores/modules/simpleUploadOverlay.ts

@@ -19,7 +19,7 @@ export const useSimpleUploadOverlayStore = defineStore("simple-upload-overlay",
   const cancelText = ref("取消上传");
 
   function beginUpload(opts?: { title?: string }) {
-    activeController?.abort();
+    activeController?.abort(new DOMException("已开始新的上传", "AbortError"));
     clearProgressTimer();
     activeController = new AbortController();
     title.value = opts?.title ?? "上传中";
@@ -46,7 +46,7 @@ export const useSimpleUploadOverlayStore = defineStore("simple-upload-overlay",
   }
 
   function userCancel() {
-    activeController?.abort();
+    activeController?.abort(new DOMException("用户已取消上传", "AbortError"));
     dismiss();
     ElMessage.info("取消上传");
   }

+ 5 - 3
src/views/dynamicManagement/publishDynamic.vue

@@ -133,7 +133,7 @@ import type { FormInstance, FormRules, UploadUserFile, UploadFile } from "elemen
 // import { publishDynamic, saveDraft, uploadDynamicImage } from "@/api/modules/dynamicManagement";
 // import { addOrUpdateDynamic } from "@/api/modules/dynamicManagement";
 import { addOrUpdateDynamic } from "@/api/modules/newLoginApi";
-import { uploadFilesToOss } from "@/api/upload.js";
+import { uploadFilesToOss, isUploadUserCancelledError } from "@/api/upload.js";
 import { useSimpleUploadOverlayStore } from "@/stores/modules/simpleUploadOverlay";
 import { useUserStore } from "@/stores/modules/user";
 import { getUserDraftDynamics, getInputPrompt } from "@/api/modules/newLoginApi";
@@ -428,8 +428,10 @@ const uploadSingleFile = async (file: UploadFile) => {
       formData.images.push(fileUrl);
     }
   } catch (error: any) {
-    publishSimpleOverlayHadError = true;
-    console.error("上传失败:", error);
+    if (!isUploadUserCancelledError(error)) {
+      publishSimpleOverlayHadError = true;
+      console.error("上传失败:", error);
+    }
     file.status = "fail";
     if (file.url && file.url.startsWith("blob:")) {
       URL.revokeObjectURL(file.url);

+ 18 - 12
src/views/storeDecoration/storeCoverMap/index.vue

@@ -148,7 +148,7 @@ import { ElLoading, ElMessage } from "element-plus";
 import { getStoreHeadImg, saveStoreHeadImg } from "@/api/modules/storeDecoration";
 import { getDetail } from "@/api/modules/homeEntry";
 import { localGet } from "@/utils";
-import { uploadFilesToOss } from "@/api/upload.js";
+import { uploadFilesToOss, isUploadUserCancelledError, isUploadApiErrorAlreadyMessaged } from "@/api/upload.js";
 import { getCoverAuditData } from "@/api/modules/coverAudit";
 import previewDemoImg from "@/assets/images/uDianShiImg4.svg";
 
@@ -335,9 +335,9 @@ function onCoverVideoPreviewClosed() {
 
 /**
  * 与 App `checkImageCompliance` 中封面分支一致:`getCoverAuditData({ url })`,
- * code 成功且非 `overall_match === false` 视为通过。
+ * code 成功且非 `overall_match === false` 视为通过。仅在页面层弹一次 toast,此处不调用 ElMessage。
  */
-async function validateUploadedCover(url: string): Promise<boolean> {
+async function runCoverComplianceAudit(url: string): Promise<{ ok: true } | { ok: false; message: string }> {
   try {
     const res = (await getCoverAuditData({ url })) as {
       code?: number | string;
@@ -348,22 +348,19 @@ async function validateUploadedCover(url: string): Promise<boolean> {
     const c = res.code;
     const okCode = c === 200 || c === 0 || c === "200" || c === "0";
     if (!okCode) {
-      ElMessage.error(String(res.msg || res.message || "素材合规性检查失败"));
-      return false;
+      return { ok: false, message: String(res.msg || res.message || "素材合规性检查失败") };
     }
     const d = res.data;
     if (d && d.overall_match === false) {
-      ElMessage.error(String(d.match_reason || "素材不符合规范,请重新上传"));
-      return false;
+      return { ok: false, message: String(d.match_reason || "素材不符合规范,请重新上传") };
     }
-    return true;
+    return { ok: true };
   } catch (e: unknown) {
     const msg =
       typeof e === "object" && e && "message" in e && typeof (e as { message?: unknown }).message === "string"
         ? String((e as Error).message)
         : "素材合规性检查失败,请重试";
-    ElMessage.error(msg);
-    return false;
+    return { ok: false, message: msg };
   }
 }
 
@@ -403,8 +400,11 @@ async function ingestPickedFile(file: File): Promise<boolean> {
       background: "rgba(0, 0, 0, 0.35)"
     });
 
-    const pass = await validateUploadedCover(url);
-    if (!pass) return false;
+    const audit = await runCoverComplianceAudit(url);
+    if (!audit.ok) {
+      ElMessage.error(audit.message);
+      return false;
+    }
 
     let posterUrl = "";
     if (isVideo) {
@@ -420,6 +420,12 @@ async function ingestPickedFile(file: File): Promise<boolean> {
     ElMessage.success("已上传并通过合规校验,请点击保存写入门店");
     return true;
   } catch (e: unknown) {
+    if (isUploadUserCancelledError(e)) {
+      return false;
+    }
+    if (isUploadApiErrorAlreadyMessaged(e)) {
+      return false;
+    }
     const msg =
       typeof e === "object" && e && "message" in e && typeof (e as { message?: unknown }).message === "string"
         ? String((e as Error).message)