diff --git a/src/pages-aigc/detail/detail.vue b/src/pages-aigc/detail/detail.vue
index d32fa13..3c7834e 100644
--- a/src/pages-aigc/detail/detail.vue
+++ b/src/pages-aigc/detail/detail.vue
@@ -246,24 +246,54 @@ const handleRecharge = () => {
});
};
-const downloadMedia = (url) =>
+const getMediaFileExtension = (url, mediaType) => {
+ const pathname = String(url || "").split(/[?#]/)[0];
+ const matchedExtension = pathname.match(/\.([a-z0-9]{2,5})$/i)?.[1]?.toLowerCase();
+ const supportedExtensions =
+ mediaType === "video" ? ["mp4", "mov", "m4v", "3gp"] : ["jpg", "jpeg", "png", "webp", "gif"];
+
+ if (matchedExtension && supportedExtensions.includes(matchedExtension)) {
+ return `.${matchedExtension}`;
+ }
+ return mediaType === "video" ? ".mp4" : ".jpg";
+};
+
+const createMediaError = (stage, error = {}) => ({
+ stage,
+ errMsg: String(error?.errMsg || error?.message || ""),
+ statusCode: error?.statusCode,
+});
+
+const downloadMedia = (url, mediaType) =>
new Promise((resolve, reject) => {
if (!/^https?:\/\//i.test(url)) {
- resolve(url);
+ resolve({ filePath: url, shouldCleanup: false });
return;
}
- uni.downloadFile({
+ const downloadOptions = {
url,
success: (res) => {
- if (res.statusCode === 200 && res.tempFilePath) {
- resolve(res.tempFilePath);
+ const filePath = res.filePath || res.tempFilePath;
+ if (res.statusCode === 200 && filePath) {
+ resolve({ filePath, shouldCleanup: Boolean(downloadOptions.filePath) });
return;
}
- reject(res);
+ reject(createMediaError("download", res));
},
- fail: reject,
- });
+ fail: (error) => reject(createMediaError("download", error)),
+ };
+
+ // 微信端显式保留扩展名,避免部分系统无法识别下载后的媒体格式。
+ // #ifdef MP-WEIXIN
+ if (typeof wx !== "undefined" && wx.env?.USER_DATA_PATH) {
+ const extension = getMediaFileExtension(url, mediaType);
+ const uniqueName = `${Date.now()}-${Math.random().toString(16).slice(2)}`;
+ downloadOptions.filePath = `${wx.env.USER_DATA_PATH}/aigc-${uniqueName}${extension}`;
+ }
+ // #endif
+
+ uni.downloadFile(downloadOptions);
});
const saveImageToAlbum = (filePath) =>
@@ -324,6 +354,26 @@ const isAlbumPermissionDenied = (error) => {
);
};
+const isDownloadDomainError = (error) => {
+ const errorMessage = String(error?.errMsg || "").toLowerCase();
+ return error?.stage === "download" && (
+ errorMessage.includes("domain list") ||
+ errorMessage.includes("url domain") ||
+ errorMessage.includes("合法域名")
+ );
+};
+
+const removeDownloadedMedia = (filePath, shouldCleanup) => {
+ if (!filePath || !shouldCleanup) return;
+
+ // #ifdef MP-WEIXIN
+ wx.getFileSystemManager().unlink({
+ filePath,
+ fail: () => {},
+ });
+ // #endif
+};
+
const promptAlbumPermission = (mediaType) => {
const mediaName = mediaType === "video" ? "视频" : "图片";
uni.showModal({
@@ -354,23 +404,42 @@ const saveResultMedia = async () => {
let saveError = null;
let loadingVisible = false;
+ let downloadedMedia = null;
try {
- await ensureAlbumPermission();
+ try {
+ await ensureAlbumPermission();
+ } catch (error) {
+ throw createMediaError("permission", error);
+ }
+
uni.showLoading({
title: "保存中",
mask: true,
});
loadingVisible = true;
- const filePath = await downloadMedia(mediaUrl);
- if (isVideo) {
- await saveVideoToAlbum(filePath);
- } else {
- await saveImageToAlbum(filePath);
+ downloadedMedia = await downloadMedia(mediaUrl, mediaType);
+ try {
+ if (isVideo) {
+ await saveVideoToAlbum(downloadedMedia.filePath);
+ } else {
+ await saveImageToAlbum(downloadedMedia.filePath);
+ }
+ } catch (error) {
+ throw createMediaError("save", error);
}
} catch (error) {
saveError = error;
+ const mediaHost = String(mediaUrl).match(/^https?:\/\/([^/?#]+)/i)?.[1] || "local";
+ console.error("保存AIGC媒体失败", {
+ stage: error?.stage,
+ errMsg: error?.errMsg,
+ statusCode: error?.statusCode,
+ mediaType,
+ mediaHost,
+ });
} finally {
+ removeDownloadedMedia(downloadedMedia?.filePath, downloadedMedia?.shouldCleanup);
if (loadingVisible) {
uni.hideLoading();
}
@@ -387,6 +456,10 @@ const saveResultMedia = async () => {
if (isAlbumPermissionDenied(saveError)) {
promptAlbumPermission(mediaType);
+ } else if (isDownloadDomainError(saveError)) {
+ showPlaceholderToast("资源下载域名未配置");
+ } else if (saveError?.stage === "download") {
+ showPlaceholderToast("资源下载失败,请稍后重试");
} else {
showPlaceholderToast(`保存${mediaName}失败`);
}
diff --git a/src/pages-aigc/progress/components/GeneratingProgressPanel/index.vue b/src/pages-aigc/progress/components/GeneratingProgressPanel/index.vue
index 00113a3..4ff23c6 100644
--- a/src/pages-aigc/progress/components/GeneratingProgressPanel/index.vue
+++ b/src/pages-aigc/progress/components/GeneratingProgressPanel/index.vue
@@ -70,7 +70,7 @@ const props = defineProps({
},
});
-const emit = defineEmits(["continue", "view"]);
+const emit = defineEmits(["continue", "view", "complete"]);
const TASK_STATUS_PROGRESS = {
0: 23,
@@ -99,10 +99,19 @@ const clearProgressTimer = () => {
}
};
+const emitComplete = () => {
+ if (Number(props.taskStatus) === 2 && normalizedProgress.value === 100) {
+ emit("complete");
+ }
+};
+
const startProgressAnimation = () => {
clearProgressTimer();
const target = targetProgress.value;
- if (normalizedProgress.value === target) return;
+ if (normalizedProgress.value === target) {
+ emitComplete();
+ return;
+ }
progressTimer = setInterval(() => {
const current = normalizedProgress.value;
@@ -114,6 +123,7 @@ const startProgressAnimation = () => {
displayProgress.value = nextProgress;
if (nextProgress === target) {
clearProgressTimer();
+ emitComplete();
}
}, 60);
};
diff --git a/src/pages-aigc/progress/progress.vue b/src/pages-aigc/progress/progress.vue
index 6ed472b..73d7113 100644
--- a/src/pages-aigc/progress/progress.vue
+++ b/src/pages-aigc/progress/progress.vue
@@ -7,7 +7,8 @@
+ :task-status="taskDetail.taskStatus" @continue="handleContinueTemplates" @view="handleViewTasks"
+ @complete="handleProgressComplete" />
@@ -28,6 +29,7 @@ const DETAIL_POLL_INTERVAL = 3000;
let detailPollTimer = null;
let isFetchingTaskDetail = false;
let isPageVisible = false;
+let isNavigatingToDetail = false;
const showToast = (title) => {
uni.showToast({
@@ -178,6 +180,20 @@ const handleRecharge = () => {
fail: () => showToast("打开积分充值失败"),
});
};
+
+const handleProgressComplete = () => {
+ if (isNavigatingToDetail || !taskId.value || !isTaskCompleted()) return;
+
+ isNavigatingToDetail = true;
+ stopTaskDetailPolling();
+ uni.redirectTo({
+ url: `/pages-aigc/detail/detail?taskId=${encodeURIComponent(taskId.value)}`,
+ fail: () => {
+ isNavigatingToDetail = false;
+ showToast("打开生成详情失败");
+ },
+ });
+};