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("打开生成详情失败"); + }, + }); +};