Explorar o código

增加ai审核

liuxiaole hai 1 semana
pai
achega
267c1f348c

+ 3 - 1
.env.development

@@ -24,7 +24,9 @@ VITE_API_URL_PLATFORM = /api/alienStorePlatform
 
 # 开发环境跨域代理,支持配置多个
 # VITE_PROXY = [["/api","https://api.ailien.shop"]] #生产环境
-VITE_PROXY = [["/api","http://120.26.186.130:8000"]] # 邹建宇
+# /ai-upload、/ai-moderate 供 Tus 上传与 AI 审核(见 src/utils/config.ts、src/api/modules/aiImageUpload.ts)
+# 图片上传默认走 AI 审核;若需恢复仅 OSS/uploadMore,设置 VITE_UPLOAD_IMAGE_AI_MODERATE=false
+VITE_PROXY = [["/api","http://120.26.186.130:8000"],["/ai-upload","http://upload.ailien.shop:8088"],["/ai-moderate","http://183.252.196.135:8892"]] # 邹建宇
 
 
 # WebSocket 基础地址(分享等能力,与商家端一致)

+ 426 - 0
src/api/modules/aiImageUpload.ts

@@ -0,0 +1,426 @@
+/**
+ * Web 端:Tus 分片上传 + AI 图片审核(与 Apifox / uni 版协议对齐)
+ */
+import { useUserStore } from "@/stores/modules/user";
+import { ResultEnum } from "@/enums/httpEnum";
+import { AI_UPLOAD_FILES_PUBLIC_BASE, BASE_AI_MODERATE_URL, BASE_AI_URL } from "@/utils/config";
+
+const TUS_VERSION = "1.0.0";
+const TUS_CHUNK_SIZE = 1024 * 1024;
+
+const DEFAULT_MODERATE_USER_TIP = "图片未通过内容审核,请更换后重试";
+
+function authHeader(): string {
+  try {
+    return useUserStore().token || "";
+  } catch {
+    return "";
+  }
+}
+
+function mapModerateReasonToUserMessage(raw?: string | null): string {
+  const s = raw == null ? "" : String(raw).trim();
+  if (!s) return DEFAULT_MODERATE_USER_TIP;
+
+  if (/赌|赌博|casino|gambl/i.test(s)) return "图片涉及赌博等违规内容,请更换后重试";
+  if (/色情|淫秽|porn|sex/i.test(s)) return "图片涉及色情等违规内容,请更换后重试";
+  if (/暴力|血腥|violence|gore/i.test(s)) return "图片涉及暴力等违规内容,请更换后重试";
+  if (/辱骂|谩骂|人身攻击|abuse|insult/i.test(s)) return "图片涉及不文明内容,请更换后重试";
+  if (/政治|谣言/i.test(s)) return "图片涉及违规信息,请更换后重试";
+  if (/广告|spam|营销/i.test(s)) return "图片涉及违规推广内容,请更换后重试";
+  if (/违法|违禁|illegal/i.test(s)) return "图片含违法违规内容,请更换后重试";
+  if (/黄赌毒/i.test(s)) return "请勿上传涉黄、涉赌、涉毒等违规内容";
+  if (/审核|moderat|vlm|violation|flagged/i.test(s)) return DEFAULT_MODERATE_USER_TIP;
+
+  return DEFAULT_MODERATE_USER_TIP;
+}
+
+function pickPayload(res: unknown): unknown {
+  if (res == null) return null;
+  if (typeof res === "object" && !Array.isArray(res) && (res as any).data !== undefined) {
+    return (res as any).data;
+  }
+  return res;
+}
+
+function headerGet(headers: Headers, name: string): string {
+  const v = headers.get(name) || headers.get(name.toLowerCase());
+  return v ? String(v).trim() : "";
+}
+
+function pickUploadIdFromLocation(loc: string): string {
+  if (!loc) return "";
+  const m = String(loc).match(/\/files\/([^/?#]+)/);
+  return m ? decodeURIComponent(m[1]) : "";
+}
+
+export function pickUploadIdFromResponse(res: Response, body?: unknown): string {
+  const h = res.headers;
+  const idHdr = headerGet(h, "upload-id") || headerGet(h, "x-upload-id");
+  if (idHdr) return idHdr;
+  const fromLoc = pickUploadIdFromLocation(headerGet(h, "location"));
+  if (fromLoc) return fromLoc;
+
+  const d = pickPayload(body ?? null);
+  const inner = d && typeof d === "object" && (d as any).data !== undefined ? (d as any).data : d;
+  const x = inner && typeof inner === "object" ? (inner as Record<string, unknown>) : {};
+  const id =
+    (x.upload_id as string) || (x.uploadId as string) || (x.id as string) || (typeof inner === "string" ? inner : "") || "";
+  return id || "";
+}
+
+function pickFileUrlFromBody(body: unknown): string {
+  const fromObj = (o: unknown): string => {
+    if (!o || typeof o !== "object" || Array.isArray(o)) return "";
+    const r = o as Record<string, string>;
+    return r.url || r.file_url || r.fileUrl || r.access_url || r.accessUrl || r.ossUrl || r.cdnUrl || "";
+  };
+  const d = pickPayload(body);
+  let inner = d && typeof d === "object" && (d as any).data !== undefined ? (d as any).data : d;
+  let u = fromObj(inner);
+  if (u) return u;
+  if (inner && typeof inner === "object" && (inner as any).data !== undefined) {
+    u = fromObj((inner as any).data);
+  }
+  if (u) return u;
+  if (typeof inner === "string" && inner.startsWith("http")) return inner;
+  return typeof d === "string" && d.startsWith("http") ? d : "";
+}
+
+/** 创建上传会话 POST /upload */
+export async function createUploadSession(data: {
+  filename: string;
+  size: number;
+}): Promise<{ response: Response; body: unknown }> {
+  const res = await fetch(`${BASE_AI_URL}/upload`, {
+    method: "POST",
+    headers: {
+      "Content-Type": "application/json",
+      "Tus-Resumable": TUS_VERSION,
+      Authorization: authHeader()
+    },
+    body: JSON.stringify(data)
+  });
+  let body: unknown = null;
+  const ct = res.headers.get("content-type") || "";
+  if (ct.includes("application/json")) {
+    try {
+      body = await res.json();
+    } catch {
+      body = null;
+    }
+  } else {
+    const t = await res.text();
+    body = t || null;
+  }
+  if (res.status < 200 || res.status >= 300) {
+    const msg =
+      body && typeof body === "object" && (body as any).message
+        ? String((body as any).message)
+        : `创建上传会话失败(${res.status})`;
+    throw new Error(msg);
+  }
+  if (body && typeof body === "object" && (body as any).code !== undefined) {
+    const c = (body as any).code;
+    if (c !== 200 && c !== 0) {
+      throw new Error((body as any).msg || (body as any).message || "创建上传会话失败");
+    }
+  }
+  return { response: res, body };
+}
+
+/** PATCH 二进制分片 */
+export async function patchBinaryToUpload(uploadId: string, arrayBuffer: ArrayBuffer, uploadOffset = 0): Promise<Response> {
+  const url = `${BASE_AI_URL}/files/${encodeURIComponent(uploadId)}`;
+  const res = await fetch(url, {
+    method: "PATCH",
+    headers: {
+      Authorization: authHeader(),
+      "Tus-Resumable": TUS_VERSION,
+      "Content-Type": "application/offset+octet-stream",
+      "Upload-Offset": String(uploadOffset)
+    },
+    body: arrayBuffer
+  });
+  if (res.status >= 200 && res.status < 300) {
+    return res;
+  }
+  const t = await res.text().catch(() => "");
+  throw new Error(`上传失败(${res.status})${t ? ` ${t.slice(0, 200)}` : ""}`);
+}
+
+export async function patchBinaryToUploadChunked(
+  uploadId: string,
+  arrayBuffer: ArrayBuffer,
+  onProgress?: (ratio: number) => void
+): Promise<void> {
+  const total = arrayBuffer.byteLength;
+  if (total === 0) {
+    await patchBinaryToUpload(uploadId, new ArrayBuffer(0), 0);
+    onProgress?.(1);
+    return;
+  }
+  let offset = 0;
+  while (offset < total) {
+    const end = Math.min(offset + TUS_CHUNK_SIZE, total);
+    const chunk = arrayBuffer.slice(offset, end);
+    await patchBinaryToUpload(uploadId, chunk, offset);
+    offset = end;
+    onProgress?.(offset / total);
+  }
+}
+
+/** HEAD /files/{id} */
+export async function getUploadProgress(uploadId: string): Promise<Response> {
+  return fetch(`${BASE_AI_URL}/files/${encodeURIComponent(uploadId)}`, {
+    method: "HEAD",
+    headers: {
+      Authorization: authHeader(),
+      "Tus-Resumable": TUS_VERSION,
+      "Content-Type": "application/octet-stream"
+    }
+  });
+}
+
+/** DELETE /files/{id} */
+export async function deleteUploadSession(uploadId: string): Promise<void> {
+  await fetch(`${BASE_AI_URL}/files/${encodeURIComponent(uploadId)}`, {
+    method: "DELETE",
+    headers: {
+      Authorization: authHeader(),
+      "Tus-Resumable": TUS_VERSION
+    }
+  });
+}
+
+/** POST /upload/{id}/finalize */
+export async function finalizeUploadSession(uploadId: string, data: Record<string, unknown> = {}): Promise<unknown> {
+  const res = await fetch(`${BASE_AI_URL}/upload/${encodeURIComponent(uploadId)}/finalize`, {
+    method: "POST",
+    headers: {
+      "Content-Type": "application/json",
+      "Tus-Resumable": TUS_VERSION,
+      Authorization: authHeader()
+    },
+    body: JSON.stringify(data)
+  });
+  let body: unknown = null;
+  const ct = res.headers.get("content-type") || "";
+  if (ct.includes("application/json")) {
+    try {
+      body = await res.json();
+    } catch {
+      body = null;
+    }
+  } else {
+    body = await res.text();
+  }
+  if (res.status < 200 || res.status >= 300) {
+    throw new Error(`上传完成确认失败(${res.status})`);
+  }
+  if (body && typeof body === "object" && (body as any).code !== undefined) {
+    const c = (body as any).code;
+    if (c !== 200 && c !== 0) {
+      throw new Error((body as any).msg || (body as any).message || "上传完成确认失败");
+    }
+  }
+  return body;
+}
+
+export interface ModerateImageParams {
+  text?: string;
+  image_urls?: string | string[];
+  file?: File | null;
+}
+
+/** POST multipart /api/v1/moderate,文件字段名 files */
+export async function moderateImage(params: ModerateImageParams = {}): Promise<any> {
+  const urlsStr = Array.isArray(params.image_urls)
+    ? params.image_urls.filter(Boolean).join(",")
+    : String(params.image_urls ?? "");
+
+  const fd = new FormData();
+  fd.append("text", String(params.text ?? ""));
+  fd.append("image_urls", urlsStr);
+  if (params.file) {
+    fd.append("files", params.file, params.file.name);
+  }
+
+  const res = await fetch(`${BASE_AI_MODERATE_URL}/api/v1/moderate`, {
+    method: "POST",
+    headers: {
+      Authorization: authHeader()
+    },
+    body: fd
+  });
+
+  let body: any = null;
+  const ct = res.headers.get("content-type") || "";
+  if (ct.includes("application/json")) {
+    try {
+      body = await res.json();
+    } catch {
+      body = null;
+    }
+  } else {
+    const t = await res.text();
+    try {
+      body = t ? JSON.parse(t) : null;
+    } catch {
+      body = { raw: t };
+    }
+  }
+
+  if (res.status < 200 || res.status >= 300) {
+    throw new Error(`审核请求失败(${res.status})`);
+  }
+  if (body && typeof body === "object" && body.code !== undefined && body.code !== 200 && body.code !== 0) {
+    throw new Error(body.msg || body.message || "审核请求失败");
+  }
+  return body;
+}
+
+export interface UploadFileViaAiOptions {
+  /** 0~100 */
+  onProgress?: (progress: number) => void;
+  /** 为 true 时跳过审核(仅上传) */
+  skipModerate?: boolean;
+}
+
+/**
+ * 浏览器 File 走 Tus 上传 → finalize →(可选)AI 审核,返回可访问的文件 URL
+ */
+export async function uploadFileViaAi(file: File, options: UploadFileViaAiOptions = {}): Promise<string> {
+  const { onProgress, skipModerate } = options;
+
+  const report = (p: number) => {
+    onProgress?.(Math.min(100, Math.max(0, Math.round(p))));
+  };
+
+  let uploadId = "";
+  try {
+    report(5);
+    const buf = await file.arrayBuffer();
+    const size = buf.byteLength;
+    report(18);
+
+    const { response: sessionRes, body: sessionBody } = await createUploadSession({
+      filename: file.name || `img_${Date.now()}.jpg`,
+      size
+    });
+    uploadId = pickUploadIdFromResponse(sessionRes, sessionBody);
+    if (!uploadId) {
+      throw new Error("创建上传会话失败:未返回 upload_id");
+    }
+
+    const publicUrl = `${AI_UPLOAD_FILES_PUBLIC_BASE}/${uploadId}`;
+
+    report(35);
+    await patchBinaryToUploadChunked(uploadId, buf, ratio => {
+      report(35 + ratio * (82 - 35));
+    });
+    report(82);
+
+    const finalizeBody = await finalizeUploadSession(uploadId, {});
+    report(90);
+
+    const urlFromApi = pickFileUrlFromBody(finalizeBody);
+    const fileUrl = urlFromApi || publicUrl;
+    if (!fileUrl) {
+      throw new Error("上传完成确认失败:未返回文件地址");
+    }
+
+    if (!skipModerate) {
+      const moderateRes = await moderateImage({
+        text: "",
+        image_urls: fileUrl,
+        file
+      });
+      const first = moderateRes?.results?.[0];
+      if (first?.flagged) {
+        if (first.reason) console.warn("[AI审核]", first.reason);
+        throw new Error(mapModerateReasonToUserMessage(first.reason));
+      }
+    }
+
+    report(100);
+    return fileUrl;
+  } catch (e) {
+    throw e;
+  }
+}
+
+/** 与 /file/uploadMore 成功态兼容,供 Upload 组件、http 封装统一消费 */
+export type UploadMoreLikeResponse = {
+  code: number;
+  msg: string;
+  data: { fileUrl: string };
+  /** 部分组件直接读顶层 fileUrl */
+  fileUrl: string;
+};
+
+export function isAiImageModerationEnabled(): boolean {
+  return import.meta.env.VITE_UPLOAD_IMAGE_AI_MODERATE !== "false";
+}
+
+/** 图片走 Tus + 审核,返回与 uploadMore 相近结构 */
+export async function uploadImageAsUploadMoreResponse(file: File): Promise<UploadMoreLikeResponse> {
+  const fileUrl = await uploadFileViaAi(file);
+  return {
+    code: ResultEnum.SUCCESS,
+    msg: "success",
+    data: { fileUrl },
+    fileUrl
+  };
+}
+
+/**
+ * FormData 内含字段 `file` 且为图片时走 AI 上传+审核,否则走原接口
+ */
+export function runUploadMoreWithOptionalAi<T = UploadMoreLikeResponse>(
+  params: FormData,
+  fallback: () => Promise<T>
+): Promise<T> {
+  if (!isAiImageModerationEnabled()) {
+    return fallback();
+  }
+  const file = params.get("file");
+  if (file instanceof File && typeof file.type === "string" && file.type.startsWith("image/")) {
+    return uploadImageAsUploadMoreResponse(file) as Promise<T>;
+  }
+  return fallback();
+}
+
+/** 多文件顺序上传,整体进度 0~100 */
+export async function uploadFilesViaAi(files: File[], options: UploadFileViaAiOptions = {}): Promise<string[]> {
+  const list = files.filter(f => f instanceof File);
+  if (!list.length) return [];
+
+  const n = list.length;
+  const urls: string[] = [];
+  for (let i = 0; i < n; i++) {
+    const u = await uploadFileViaAi(list[i], {
+      ...options,
+      onProgress: p => {
+        const overall = (i / n) * 100 + (p / 100) * (100 / n);
+        options.onProgress?.(overall);
+      }
+    });
+    urls.push(u);
+  }
+  return urls;
+}
+
+/** 与 uni 版 aiApi 命名对齐的聚合对象,便于按需解构 */
+export const aiImageApi = {
+  createUploadSession: (data: { filename: string; size: number }) => createUploadSession(data),
+  patchBinaryToUpload,
+  getUploadProgress,
+  deleteUploadSession,
+  finalizeUploadSession,
+  moderateImage,
+  uploadFileViaAi,
+  uploadFilesViaAi
+};
+
+export default aiImageApi;

+ 5 - 2
src/api/modules/newLoginApi.ts

@@ -1,6 +1,7 @@
 import type { Login } from "@/api/interface";
 import httpLogin from "@/api/indexApi";
 import { Upload } from "@/api/interface/index";
+import { runUploadMoreWithOptionalAi } from "@/api/modules/aiImageUpload";
 // 获取图片验证码
 export const getImgCode = () => {
   return httpLogin.get(
@@ -61,9 +62,11 @@ export const getInputPrompt = params => {
 export const getDistrict = params => {
   return httpLogin.get(`/alienStore/gaode/getDistrict`, params);
 };
-//文件上传
+//文件上传(图片默认走 Tus + AI 审核,可设 VITE_UPLOAD_IMAGE_AI_MODERATE=false 关闭)
 export const uploadImg = (params: FormData) => {
-  return httpLogin.post<Upload.ResFileUrl>(`/alienStore/file/uploadMore`, params, { cancel: false });
+  return runUploadMoreWithOptionalAi(params, () =>
+    httpLogin.post<Upload.ResFileUrl>(`/alienStore/file/uploadMore`, params, { cancel: false })
+  );
 };
 // 发布/更新动态(新接口)
 export const addOrUpdateDynamic = (params: {

+ 7 - 2
src/api/modules/storeDecoration.ts

@@ -3,6 +3,7 @@ import { PORT_NONE } from "@/api/config/servicePort";
 import http from "@/api";
 import httpApi from "@/api/indexApi";
 import { Upload } from "@/api/interface/index";
+import { runUploadMoreWithOptionalAi } from "@/api/modules/aiImageUpload";
 
 /**
  * @name 商铺用户模块
@@ -64,12 +65,16 @@ export const saveOrUpdateDecoration = (params: any) => {
 
 // 上传房屋图纸 - 使用 /alienStore/file/uploadMore 接口
 export const uploadDecorationImage = (params: FormData) => {
-  return httpApi.post<Upload.ResFileUrl>(`/alienStore/file/uploadMore`, params, { cancel: false });
+  return runUploadMoreWithOptionalAi(params, () =>
+    httpApi.post<Upload.ResFileUrl>(`/alienStore/file/uploadMore`, params, { cancel: false })
+  );
 };
 
 // 聊天图片/视频上传(与商家端一致,使用同一接口)
 export const uploadChatFile = (params: FormData) => {
-  return httpApi.post<Upload.ResFileUrl>(`/alienStore/file/uploadMore`, params, { cancel: false });
+  return runUploadMoreWithOptionalAi(params, () =>
+    httpApi.post<Upload.ResFileUrl>(`/alienStore/file/uploadMore`, params, { cancel: false })
+  );
 };
 
 // 删除装修需求

+ 7 - 2
src/api/modules/upload.ts

@@ -1,4 +1,5 @@
 import { Upload } from "@/api/interface/index";
+import { runUploadMoreWithOptionalAi } from "@/api/modules/aiImageUpload";
 import { PORT1 } from "@/api/config/servicePort";
 import { PORT_NONE } from "@/api/config/servicePort";
 import httpStore from "@/api/indexStore";
@@ -48,12 +49,16 @@ httpStoreAlienStore.interceptors.response.use(
  */
 // 图片上传(默认使用 alienStorePlatform)
 export const uploadImg = (params: FormData) => {
-  return httpStore.post<Upload.ResFileUrl>(PORT_NONE + `/file/uploadMore`, params, { cancel: false });
+  return runUploadMoreWithOptionalAi(params, () =>
+    httpStore.post<Upload.ResFileUrl>(PORT_NONE + `/file/uploadMore`, params, { cancel: false })
+  );
 };
 
 // 图片上传(使用 alienStore 前缀,用于价目表等页面)
 export const uploadImgStore = (params: FormData) => {
-  return httpStoreAlienStore.post<Upload.ResFileUrl>(PORT_NONE + `/file/uploadMore`, params, { cancel: false });
+  return runUploadMoreWithOptionalAi(params, () =>
+    httpStoreAlienStore.post<Upload.ResFileUrl>(PORT_NONE + `/file/uploadMore`, params, { cancel: false })
+  );
 };
 
 // 视频上传

+ 14 - 1
src/api/upload.js

@@ -1,5 +1,6 @@
 import { useUserStore } from "@/stores/modules/user";
 import { ElMessage } from "element-plus";
+import { isAiImageModerationEnabled, uploadFileViaAi } from "@/api/modules/aiImageUpload";
 
 /**
  * 门店/文件相关接口基址(与 src/api/modules/upload.ts 中 httpStoreAlienStore 一致)
@@ -133,11 +134,23 @@ export async function uploadFilesToOss(files, fileType, options = {}) {
   }
 
   try {
-    const signRes = await fetchOssSignature();
+    let signRes = null;
     const uploadedUrls = [];
+    const useAiImage = isAiImageModerationEnabled();
 
     for (let i = 0; i < fileArr.length; i++) {
       const file = fileArr[i];
+      const isImage = fileType === "image" || (!fileType && file.type && String(file.type).startsWith("image/"));
+
+      if (useAiImage && isImage) {
+        const url = await uploadFileViaAi(file);
+        uploadedUrls.push(url);
+        continue;
+      }
+
+      if (!signRes) {
+        signRes = await fetchOssSignature();
+      }
       const filePath = file.name || "";
       const ext = getFileExtension(filePath, fileType);
       const rawBase = filePath.split("/").pop() || "";

+ 8 - 0
src/typings/global.d.ts

@@ -62,6 +62,14 @@ declare interface ViteEnv {
   VITE_CRYPTO_KEY: string;
   VITE_CRYPTO_IV: string;
   VITE_WS_BASE?: string;
+  /** Tus 上传服务根(可选,不配则开发走 /ai-upload 代理、生产走 upload.ailien.shop) */
+  VITE_AI_UPLOAD_BASE?: string;
+  /** AI 审核服务根(可选,不配则开发走 /ai-moderate 代理) */
+  VITE_AI_MODERATE_BASE?: string;
+  /** 上传完成后对外访问 URL 前缀,默认 http://upload.ailien.shop:8088/files */
+  VITE_AI_FILES_PUBLIC_BASE?: string;
+  /** 设为 false 时图片仍走原 OSS / uploadMore,不经过 Tus + AI 审核 */
+  VITE_UPLOAD_IMAGE_AI_MODERATE?: string;
 }
 
 interface ImportMetaEnv extends ViteEnv {

+ 20 - 0
src/utils/config.ts

@@ -0,0 +1,20 @@
+/**
+ * AI Tus 上传与内容审核服务基址(与 uni 端逻辑对齐,适配 Web)
+ * 开发环境默认走 Vite 代理前缀,需在 .env.development 的 VITE_PROXY 中配置 /ai-upload、/ai-moderate
+ */
+const trimSlash = (s: string) => s.replace(/\/$/, "");
+
+export const BASE_AI_URL = trimSlash(
+  String(import.meta.env.VITE_AI_UPLOAD_BASE || "").trim() ||
+    (import.meta.env.DEV ? "/ai-upload" : "http://upload.ailien.shop:8088")
+);
+
+export const BASE_AI_MODERATE_URL = trimSlash(
+  String(import.meta.env.VITE_AI_MODERATE_BASE || "").trim() ||
+    (import.meta.env.DEV ? "/ai-moderate" : "http://183.252.196.135:8892")
+);
+
+/** 上传完成后对外可访问的文件 URL:`${AI_UPLOAD_FILES_PUBLIC_BASE}/${uploadId}` */
+export const AI_UPLOAD_FILES_PUBLIC_BASE = trimSlash(
+  String(import.meta.env.VITE_AI_FILES_PUBLIC_BASE || "").trim() || "http://upload.ailien.shop:8088/files"
+);

+ 5 - 5
src/views/dynamicManagement/index.vue

@@ -156,7 +156,7 @@
                 </div>
               </div>
             </div>
-            <div style=" padding-bottom: 10px;color: #ffffff">
+            <div style="padding-bottom: 10px; color: #ffffff">
               {{ currentDetail.title }}
             </div>
             <div class="detail-description">
@@ -1423,11 +1423,11 @@ const handleReportUpload = async (options: any) => {
 
     const response: any = await uploadImg(uploadFormData);
 
-    // 处理返回格式:{ code, success, data: string[], msg }
-    if (response && response.code === 200 && response.data && Array.isArray(response.data) && response.data.length > 0) {
-      // 上传成功,返回图片URL(取数组第一个元素)
+    const url =
+      response?.data && (Array.isArray(response.data) && response.data.length > 0 ? response.data[0] : response.data.fileUrl);
+    if (response && response.code === 200 && url) {
       onSuccess({
-        url: response.data[0]
+        url
       });
     } else {
       ElMessage.error(response?.msg || "图片上传失败");

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

@@ -1490,9 +1490,11 @@ const handleReportUpload = async (options: any) => {
 
     const response: any = await uploadImg(uploadFormData);
 
-    if (response && response.code === 200 && response.data && Array.isArray(response.data) && response.data.length > 0) {
+    const url =
+      response?.data && (Array.isArray(response.data) && response.data.length > 0 ? response.data[0] : response.data.fileUrl);
+    if (response && response.code === 200 && url) {
       onSuccess({
-        url: response.data[0]
+        url
       });
     } else {
       ElMessage.error(response?.msg || "图片上传失败");