From 1a4b320dc57affe6f38273f586d5d5563b4e6fa2 Mon Sep 17 00:00:00 2001 From: duanshuwen Date: Fri, 17 Jul 2026 21:36:08 +0800 Subject: [PATCH] feat: add aigc regenerate flow and improve task polling Add reusable RegenerateConfirmPopup component. Implement full regeneration flow in AIGC detail page with balance checks, insufficient point prompt, task creation and progress page navigation. Refine progress page polling logic with lifecycle handling, duplicate request prevention and timer cleanup. --- .../RegenerateConfirmPopup/index.vue | 92 +++++++++++++++++++ .../RegenerateConfirmPopup/styles/index.scss | 87 ++++++++++++++++++ src/pages-aigc/detail/detail.vue | 79 +++++++++++++++- src/pages-aigc/progress/progress.vue | 89 +++++++++++++++--- src/request/base/config.js | 2 +- 5 files changed, 332 insertions(+), 17 deletions(-) create mode 100644 src/pages-aigc/detail/components/RegenerateConfirmPopup/index.vue create mode 100644 src/pages-aigc/detail/components/RegenerateConfirmPopup/styles/index.scss diff --git a/src/pages-aigc/detail/components/RegenerateConfirmPopup/index.vue b/src/pages-aigc/detail/components/RegenerateConfirmPopup/index.vue new file mode 100644 index 0000000..e05ed22 --- /dev/null +++ b/src/pages-aigc/detail/components/RegenerateConfirmPopup/index.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/src/pages-aigc/detail/components/RegenerateConfirmPopup/styles/index.scss b/src/pages-aigc/detail/components/RegenerateConfirmPopup/styles/index.scss new file mode 100644 index 0000000..104e7d1 --- /dev/null +++ b/src/pages-aigc/detail/components/RegenerateConfirmPopup/styles/index.scss @@ -0,0 +1,87 @@ +.regenerate-dialog { + width: 318px; + max-width: calc(100vw - 56px); + padding: 30px 26px 24px; + border-radius: 24px; + background: #ffffff; + box-shadow: 0 18px 48px rgba(0, 0, 0, 0.26); + box-sizing: border-box; +} + +.regenerate-dialog-title { + display: block; + color: #111827; + font-size: 20px; + line-height: 28px; + font-weight: 900; + text-align: center; +} + +.regenerate-dialog-desc { + display: block; + margin-top: 14px; + color: #667386; + font-size: 14px; + line-height: 22px; + font-weight: 800; + text-align: center; +} + +.regenerate-dialog-balance { + min-height: 36px; + display: flex; + align-items: center; + justify-content: center; + margin-top: 14px; + padding: 8px 12px; + border-radius: 14px; + background: #effaf5; + color: #0fa567; + font-size: 12px; + line-height: 18px; + font-weight: 900; + text-align: center; + box-sizing: border-box; +} + +.regenerate-dialog-actions { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 18px; + margin-top: 28px; +} + +.regenerate-dialog-button { + min-width: 0; + height: 54px; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + border-radius: 999px; + font-size: 16px; + line-height: 22px; + font-weight: 900; + text-overflow: ellipsis; + white-space: nowrap; + box-sizing: border-box; +} + +.regenerate-dialog-button.is-cancel { + border: 1px solid #e2e6ea; + background: #ffffff; + color: #172033; +} + +.regenerate-dialog-button.is-primary { + background: #061e34; + color: #ffffff; +} + +.regenerate-dialog-button.is-pressed { + opacity: 0.86; +} + +.regenerate-dialog-button.is-disabled { + opacity: 0.6; +} diff --git a/src/pages-aigc/detail/detail.vue b/src/pages-aigc/detail/detail.vue index e2e12fa..d32fa13 100644 --- a/src/pages-aigc/detail/detail.vue +++ b/src/pages-aigc/detail/detail.vue @@ -19,6 +19,10 @@ + + @@ -29,9 +33,12 @@ import Privacy from "@/components/Privacy/index.vue"; import AigcTopBar from "@/pages-aigc/components/AigcTopBar/index.vue"; import { useCurrentCredit } from "@/pages-aigc/hooks/useCurrentCredit.js"; import { + createAigcGeneratorTask, createAigcGeneratorTaskShare, getAigcGeneratorTaskDetail, } from "@/request/api/AigcApi.js"; +import PointInsufficientDialog from "@/pages-aigc/useTemplate/components/PointInsufficientDialog/index.vue"; +import RegenerateConfirmPopup from "./components/RegenerateConfirmPopup/index.vue"; import ResultActions from "./components/ResultActions/index.vue"; import ResultMeta from "./components/ResultMeta/index.vue"; import ResultPreview from "./components/ResultPreview/index.vue"; @@ -45,6 +52,9 @@ const shareKey = ref(""); const privacyVisible = ref(false); const privacyContractName = ref("隐私保护指引"); const pendingSaveAfterPrivacy = ref(false); +const regeneratePopupRef = ref(null); +const isRegenerating = ref(false); +const pointDialogVisible = ref(false); const GENERATOR_TYPE_TEXT = { 0: "图片版", @@ -124,6 +134,7 @@ const metaItems = computed(() => { }); const sharePreviewImage = computed(() => taskDetail.value?.imageResultUrl || ""); +const regenerateCost = computed(() => Math.max(0, Number(taskDetail.value?.generatorCost) || 0)); const showPlaceholderToast = (title) => { uni.showToast({ @@ -425,8 +436,72 @@ const handlePrivacyDisagree = () => { pendingSaveAfterPrivacy.value = false; }; -const handleRegenerate = () => { - showPlaceholderToast("再生成一版"); +const handleRegenerate = async () => { + await fetchCurrentCredit(); + regeneratePopupRef.value?.open(); +}; + +const handleConfirmRegenerate = async () => { + if (isRegenerating.value) return; + + isRegenerating.value = true; + await fetchCurrentCredit(); + + const balance = Number(pointBalance.value) || 0; + if (balance < regenerateCost.value) { + isRegenerating.value = false; + regeneratePopupRef.value?.close(); + pointDialogVisible.value = true; + return; + } + + const templateItemId = String(taskDetail.value?.templateItemId || "").trim(); + const imageResultUrl = String(taskDetail.value?.imageResultUrl || "").trim(); + if (!templateItemId || !imageResultUrl) { + isRegenerating.value = false; + showPlaceholderToast(!templateItemId ? "缺少模板生成项" : "缺少可复用素材"); + return; + } + + uni.showLoading({ + title: "创建中", + mask: true, + }); + + try { + const res = await createAigcGeneratorTask({ + templateItemId, + imageUrlList: [imageResultUrl], + }); + const nextTaskId = String(res?.data || "").trim(); + + if (res?.code === 0 && nextTaskId) { + regeneratePopupRef.value?.close(); + fetchCurrentCredit(); + uni.redirectTo({ + url: `/pages-aigc/progress/progress?taskId=${encodeURIComponent(nextTaskId)}`, + fail: () => showPlaceholderToast("打开任务进度失败"), + }); + return; + } + + showPlaceholderToast(res?.msg || "创建任务失败"); + } catch (error) { + console.error("重新创建AIGC生成任务失败", error); + showPlaceholderToast("创建任务失败"); + } finally { + uni.hideLoading(); + isRegenerating.value = false; + } +}; + +const handleClosePointDialog = () => { + pointDialogVisible.value = false; +}; + +const handlePointRecharge = () => { + pointDialogVisible.value = false; + handleRecharge(); }; const handleChangeTemplate = () => { diff --git a/src/pages-aigc/progress/progress.vue b/src/pages-aigc/progress/progress.vue index 13b7718..6ed472b 100644 --- a/src/pages-aigc/progress/progress.vue +++ b/src/pages-aigc/progress/progress.vue @@ -15,7 +15,7 @@