feat(aigc): add auto-redirect when generation completes
Add complete event to GeneratingProgressPanel that emits when task status is completed and progress reaches 100%. Add handler in the parent progress page to automatically navigate to task detail page when the complete event is triggered, and stop ongoing task detail polling. Also enhance media download handling in the detail page with proper WeChat mini-program file path handling, error wrapping, temporary file cleanup, and improved error reporting for download failures.
This commit is contained in:
@@ -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}失败`);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
<view class="aigc-progress-content">
|
||||
<GeneratingProgressPanel v-if="taskDetail" :cost="taskDetail.generatorCost"
|
||||
:generator-type="taskDetail.generatorType" :item-title="taskDetail.itemTitle" :task-id="taskId"
|
||||
:task-status="taskDetail.taskStatus" @continue="handleContinueTemplates" @view="handleViewTasks" />
|
||||
:task-status="taskDetail.taskStatus" @continue="handleContinueTemplates" @view="handleViewTasks"
|
||||
@complete="handleProgressComplete" />
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
@@ -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("打开生成详情失败");
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
Reference in New Issue
Block a user