|
|
@@ -175,6 +175,40 @@ function deepFindHttpUrl(val, depth = 0, maxDepth = 8) {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
+ * 递归查找上传接口返回的 `download_url` / `downloadUrl`(后台保存相册等常以该字段为 imgUrl)
|
|
|
+ * @param {unknown} val
|
|
|
+ * @param {number} depth
|
|
|
+ * @param {number} maxDepth
|
|
|
+ * @returns {string}
|
|
|
+ */
|
|
|
+function preferDownloadUrlFromBody(val, depth = 0, maxDepth = 10) {
|
|
|
+ if (depth > maxDepth || val == null) return "";
|
|
|
+ if (Array.isArray(val)) {
|
|
|
+ for (const item of val) {
|
|
|
+ const u = preferDownloadUrlFromBody(item, depth + 1, maxDepth);
|
|
|
+ if (u) return u;
|
|
|
+ }
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ if (typeof val !== "object") return "";
|
|
|
+ const o = /** @type {Record<string, unknown>} */ (val);
|
|
|
+ for (const k of ["download_url", "downloadUrl"]) {
|
|
|
+ const v = o[k];
|
|
|
+ if (typeof v === "string" && v.trim()) {
|
|
|
+ const t = v.trim();
|
|
|
+ const abs = normalizeToFileUrl(t);
|
|
|
+ if (abs) return abs;
|
|
|
+ if (/^https?:\/\//i.test(t)) return t;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ for (const v of Object.values(o)) {
|
|
|
+ const u = preferDownloadUrlFromBody(v, depth + 1, maxDepth);
|
|
|
+ if (u) return u;
|
|
|
+ }
|
|
|
+ return "";
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
* 从 JSON 体中解析文件访问地址(兼容多种后端约定)
|
|
|
* @param {unknown} body
|
|
|
* @returns {string}
|
|
|
@@ -186,6 +220,17 @@ function pickUrlFromJsonBody(body) {
|
|
|
return normalizeToFileUrl(body) || (body.trim().startsWith("http") ? body.trim() : "");
|
|
|
}
|
|
|
|
|
|
+ if (typeof body === "object" && !Array.isArray(body)) {
|
|
|
+ const dUrl = preferDownloadUrlFromBody(body);
|
|
|
+ if (dUrl) return dUrl;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (Array.isArray(body)) {
|
|
|
+ const dUrl = preferDownloadUrlFromBody(body);
|
|
|
+ if (dUrl) return dUrl;
|
|
|
+ return deepFindHttpUrl(body);
|
|
|
+ }
|
|
|
+
|
|
|
if (typeof body !== "object" || Array.isArray(body)) {
|
|
|
return deepFindHttpUrl(body);
|
|
|
}
|
|
|
@@ -338,12 +383,12 @@ function assertSimpleUploadBusinessOk(parsed) {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * POST multipart:字段 file;可选 filename(此处不传则服务端用原名)
|
|
|
+ * POST multipart:字段 file;解析展示用地址与业务保存用的 download_url
|
|
|
* @param {File} file
|
|
|
* @param {{ signal?: AbortSignal }} [fetchOptions]
|
|
|
- * @returns {Promise<string>} 文件访问 URL
|
|
|
+ * @returns {Promise<{ url: string; downloadUrl: string; parsed: unknown }>}
|
|
|
*/
|
|
|
-async function postFileToSimpleUpload(file, fetchOptions = {}) {
|
|
|
+async function postFileToSimpleUploadDetailed(file, fetchOptions = {}) {
|
|
|
const base = String(BASE_AI_URL || "").replace(/\/$/, "");
|
|
|
if (!base) {
|
|
|
throw new Error("未配置上传服务地址(VITE_AI_UPLOAD_BASE 或默认 /ai-upload)");
|
|
|
@@ -416,6 +461,8 @@ async function postFileToSimpleUpload(file, fetchOptions = {}) {
|
|
|
throw new Error(moderationUserTip);
|
|
|
}
|
|
|
|
|
|
+ const downloadUrl = (preferDownloadUrlFromBody(parsed) || "").trim();
|
|
|
+
|
|
|
let url = pickUrlFromJsonBody(parsed);
|
|
|
if (!url && trimmed.startsWith("http")) {
|
|
|
url = trimmed;
|
|
|
@@ -442,6 +489,17 @@ async function postFileToSimpleUpload(file, fetchOptions = {}) {
|
|
|
console.error("[upload/simple] 无法解析文件地址,响应片段:", trimmed.slice(0, 800));
|
|
|
throw new Error("上传失败,未返回文件地址");
|
|
|
}
|
|
|
+ return { url, downloadUrl, parsed };
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * POST multipart:字段 file;可选 filename(此处不传则服务端用原名)
|
|
|
+ * @param {File} file
|
|
|
+ * @param {{ signal?: AbortSignal }} [fetchOptions]
|
|
|
+ * @returns {Promise<string>} 文件访问 URL
|
|
|
+ */
|
|
|
+async function postFileToSimpleUpload(file, fetchOptions = {}) {
|
|
|
+ const { url } = await postFileToSimpleUploadDetailed(file, fetchOptions);
|
|
|
return url;
|
|
|
}
|
|
|
|
|
|
@@ -516,6 +574,66 @@ export async function uploadFilesToOss(files, _fileType, options = {}) {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
+ * 单文件上传:返回列表展示用地址与业务保存用的 `download_url`(响应中无则为空字符串)
|
|
|
+ * @param {File} file
|
|
|
+ * @param {{ showLoading?: boolean; skipSimpleUploadOverlay?: boolean; uploadSuccessMessage?: string | null; uploadOverlayTitle?: string }} [options]
|
|
|
+ * @returns {Promise<{ fileUrl: string; downloadUrl: string }>}
|
|
|
+ */
|
|
|
+export async function uploadFileToOssWithDownloadMeta(file, options = {}) {
|
|
|
+ const { showLoading = false, skipSimpleUploadOverlay = false, uploadSuccessMessage, uploadOverlayTitle } = options;
|
|
|
+ if (!(file instanceof File)) {
|
|
|
+ throw new Error("请选择要上传的文件");
|
|
|
+ }
|
|
|
+
|
|
|
+ let closeLoading = () => {};
|
|
|
+ if (showLoading && skipSimpleUploadOverlay) {
|
|
|
+ const loading = ElMessage({ message: "上传中...", type: "info", duration: 0, showClose: false });
|
|
|
+ closeLoading = () => loading.close();
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const runUpload = async signal => {
|
|
|
+ const { url, downloadUrl } = await postFileToSimpleUploadDetailed(file, signal ? { signal } : {});
|
|
|
+ return { fileUrl: url, downloadUrl };
|
|
|
+ };
|
|
|
+
|
|
|
+ let result;
|
|
|
+ if (skipSimpleUploadOverlay) {
|
|
|
+ const overlaySignal = useSimpleUploadOverlayStore().getActiveAbortSignal?.();
|
|
|
+ result = await runUpload(overlaySignal ?? null);
|
|
|
+ } else {
|
|
|
+ const overlayOpts =
|
|
|
+ uploadSuccessMessage !== undefined || uploadOverlayTitle
|
|
|
+ ? {
|
|
|
+ title: uploadOverlayTitle,
|
|
|
+ successMessage: uploadSuccessMessage
|
|
|
+ }
|
|
|
+ : undefined;
|
|
|
+ result = await withSimpleUploadOverlay(signal => runUpload(signal), overlayOpts);
|
|
|
+ }
|
|
|
+
|
|
|
+ closeLoading();
|
|
|
+ return result;
|
|
|
+ } catch (e) {
|
|
|
+ closeLoading();
|
|
|
+ console.error("上传失败", e);
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
* 上传单个文件,返回 URL 字符串
|
|
|
* @param {File} file
|
|
|
* @param {string} [fileType]
|