feat(aigc): add standalone AIGC task progress page

- Add dedicated progress page for viewing AIGC generation task status
- Extract reusable GeneratingProgressPanel component
- Update task record cards to link to progress page for pending tasks
- Rework useTemplate flow to redirect to progress page post task creation
- Remove unused inline generating code and styles from useTemplate
- Adjust quick access menu item order
This commit is contained in:
duanshuwen
2026-07-15 11:24:28 +08:00
parent d6c82204d4
commit c7785e55d7
10 changed files with 229 additions and 114 deletions

View File

@@ -48,10 +48,6 @@
import { computed, onUnmounted, ref, watch } from "vue"; import { computed, onUnmounted, ref, watch } from "vue";
const props = defineProps({ const props = defineProps({
progress: {
type: [Number, String],
default: 8,
},
cost: { cost: {
type: [Number, String], type: [Number, String],
default: 100, default: 100,
@@ -76,47 +72,50 @@ const props = defineProps({
const emit = defineEmits(["continue", "view"]); const emit = defineEmits(["continue", "view"]);
const TASK_STATUS_PROGRESS = {
0: 23,
1: 56,
2: 100,
4: 74,
};
const normalizeProgress = (progress) => { const normalizeProgress = (progress) => {
const value = Number(progress); const value = Number(progress);
if (!Number.isFinite(value)) return 0; if (!Number.isFinite(value)) return 0;
return Math.max(0, Math.min(99, Math.round(value))); return Math.max(0, Math.min(100, Math.round(value)));
}; };
const displayProgress = ref(normalizeProgress(props.progress)); const displayProgress = ref(0);
let progressTimer = null; let progressTimer = null;
const normalizedProgress = computed(() => normalizeProgress(displayProgress.value)); const normalizedProgress = computed(() => normalizeProgress(displayProgress.value));
const progressWidth = computed(() => `${normalizedProgress.value}%`); const progressWidth = computed(() => `${normalizedProgress.value}%`);
const targetProgress = computed(() => TASK_STATUS_PROGRESS[Number(props.taskStatus)] ?? 0);
const isAnimatingStatus = (status) => {
if (status === null || status === undefined || status === "") return false;
return [0, 1, 4].includes(Number(status));
};
const clearProgressTimer = () => { const clearProgressTimer = () => {
if (progressTimer) { if (progressTimer !== null) {
clearInterval(progressTimer); clearInterval(progressTimer);
progressTimer = null; progressTimer = null;
} }
}; };
const getNextProgress = (progress) => {
if (progress < 35) return Math.min(progress + 7, 35);
if (progress < 70) return Math.min(progress + 5, 70);
if (progress < 92) return Math.min(progress + 3, 92);
return Math.min(progress + 1, 99);
};
const startProgressAnimation = () => { const startProgressAnimation = () => {
clearProgressTimer(); clearProgressTimer();
if (!isAnimatingStatus(props.taskStatus) || normalizedProgress.value >= 99) return; const target = targetProgress.value;
if (normalizedProgress.value === target) return;
progressTimer = setInterval(() => { progressTimer = setInterval(() => {
displayProgress.value = getNextProgress(normalizedProgress.value); const current = normalizedProgress.value;
if (normalizedProgress.value >= 99) { const direction = current < target ? 1 : -1;
const distance = Math.abs(target - current);
const step = distance > 36 ? 4 : distance > 16 ? 2 : 1;
const nextProgress = current + direction * Math.min(step, distance);
displayProgress.value = nextProgress;
if (nextProgress === target) {
clearProgressTimer(); clearProgressTimer();
} }
}, 320); }, 60);
}; };
const generatorTypeText = computed(() => { const generatorTypeText = computed(() => {
@@ -144,23 +143,10 @@ const displaySteps = computed(() => {
})); }));
}); });
watch(
() => props.progress,
(progress) => {
const initialProgress = normalizeProgress(progress);
if (!isAnimatingStatus(props.taskStatus) || displayProgress.value < initialProgress) {
displayProgress.value = initialProgress;
}
}
);
watch( watch(
() => props.taskStatus, () => props.taskStatus,
(status) => { () => {
clearProgressTimer();
if (isAnimatingStatus(status)) {
startProgressAnimation(); startProgressAnimation();
}
}, },
{ immediate: true } { immediate: true }
); );

View File

@@ -0,0 +1,124 @@
<template>
<view class="aigc-progress-page">
<AigcTopBar :points="pointBalance" recharge-label="充值" @back="handleBack" @history="handleViewTasks"
@recharge="handleRecharge" />
<scroll-view class="aigc-progress-body" scroll-y>
<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" />
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import AigcTopBar from "@/pages-aigc/components/AigcTopBar/index.vue";
import { useCurrentCredit } from "@/pages-aigc/hooks/useCurrentCredit.js";
import { getAigcGeneratorTaskDetail } from "@/request/api/AigcApi.js";
import GeneratingProgressPanel from "./components/GeneratingProgressPanel/index.vue";
const { pointBalance, fetchCurrentCredit } = useCurrentCredit();
const taskId = ref("");
const taskDetail = ref(null);
const showToast = (title) => {
uni.showToast({
title,
icon: "none",
});
};
const getPreviousPageRoute = () => {
const pages = getCurrentPages();
return pages[pages.length - 2]?.route || "";
};
const fetchTaskDetail = async () => {
uni.showLoading({
title: "加载中",
mask: true,
});
try {
const res = await getAigcGeneratorTaskDetail({ taskId: taskId.value });
if (res?.code === 0 && res.data && typeof res.data === "object" && !Array.isArray(res.data)) {
taskDetail.value = res.data;
return;
}
taskDetail.value = null;
showToast(res?.msg || "获取任务进度失败");
} catch (error) {
console.error("获取AIGC生成任务进度失败", error);
taskDetail.value = null;
showToast("获取任务进度失败");
} finally {
uni.hideLoading();
}
};
onLoad((query = {}) => {
taskId.value = String(query.taskId || "").trim();
if (!taskId.value) {
showToast("缺少任务ID");
return;
}
fetchTaskDetail();
});
onShow(() => {
fetchCurrentCredit();
});
const handleBack = () => {
const pages = getCurrentPages();
if (pages.length > 1) {
uni.navigateBack();
return;
}
uni.redirectTo({
url: "/pages-aigc/home/home",
});
};
const handleContinueTemplates = () => {
if (getPreviousPageRoute() === "pages-aigc/home/home") {
uni.navigateBack();
return;
}
uni.redirectTo({
url: "/pages-aigc/home/home",
fail: () => showToast("打开模板列表失败"),
});
};
const handleViewTasks = () => {
if (getPreviousPageRoute() === "pages-aigc/record/record") {
uni.navigateBack();
return;
}
uni.navigateTo({
url: "/pages-aigc/record/record",
fail: () => showToast("打开最近任务失败"),
});
};
const handleRecharge = () => {
uni.navigateTo({
url: "/pages-aigc/recharge/recharge",
fail: () => showToast("打开积分充值失败"),
});
};
</script>
<style scoped lang="scss">
@import "./styles/index.scss";
</style>

View File

@@ -0,0 +1,28 @@
.aigc-progress-page {
display: flex;
flex-direction: column;
width: 100vw;
height: 100vh;
overflow: hidden;
color: #172033;
background:
radial-gradient(
130% 78% at 0% 0%,
rgba(255, 249, 226, 0.76),
rgba(255, 249, 226, 0) 42%
),
linear-gradient(180deg, #effaf5 0%, #f4fbf7 58%, #eef8ff 100%);
box-sizing: border-box;
}
.aigc-progress-body {
flex: 1 1 0;
min-height: 0;
overflow: hidden;
}
.aigc-progress-content {
min-height: 100%;
padding: 12px 12px 16px;
box-sizing: border-box;
}

View File

@@ -13,8 +13,8 @@
<text class="record-card-status ellipsis-1">{{ statusText }}</text> <text class="record-card-status ellipsis-1">{{ statusText }}</text>
</view> </view>
<view v-if="isCompleted" class="record-card-action" @tap="emit('view', record)"> <view v-if="actionText" class="record-card-action" @tap="emit('view', record)">
查看 {{ actionText }}
</view> </view>
</view> </view>
</template> </template>
@@ -48,7 +48,12 @@ const title = computed(() => props.record.itemTitle || props.record.taskId || ""
const normalizedTaskStatus = computed(() => Number(props.record.taskStatus)); const normalizedTaskStatus = computed(() => Number(props.record.taskStatus));
const isCompleted = computed(() => normalizedTaskStatus.value === 2); const isCompleted = computed(() => normalizedTaskStatus.value === 2);
const isGenerating = computed(() => [0, 1].includes(normalizedTaskStatus.value)); const isGenerating = computed(() => [0, 1, 4].includes(normalizedTaskStatus.value));
const actionText = computed(() => {
if (isCompleted.value) return "查看";
if (isGenerating.value) return "进度";
return "";
});
const mediaUrl = computed(() => { const mediaUrl = computed(() => {
if (!isCompleted.value) return ""; if (!isCompleted.value) return "";

View File

@@ -49,12 +49,21 @@ const handleBack = () => {
}; };
const handleViewRecord = (record) => { const handleViewRecord = (record) => {
if (record?.taskStatus === 2 && record.taskId) { if (!record?.taskId) return;
const taskStatus = Number(record.taskStatus);
if (taskStatus === 2) {
uni.navigateTo({ uni.navigateTo({
url: `/pages-aigc/detail/detail?taskId=${encodeURIComponent(record.taskId)}`, url: `/pages-aigc/detail/detail?taskId=${encodeURIComponent(record.taskId)}`,
}); });
return; return;
} }
if ([0, 1, 4].includes(taskStatus)) {
uni.navigateTo({
url: `/pages-aigc/progress/progress?taskId=${encodeURIComponent(record.taskId)}`,
});
}
}; };
</script> </script>

View File

@@ -31,11 +31,6 @@
box-sizing: border-box; box-sizing: border-box;
} }
.aigc-use-template-content.is-generating {
overflow-y: auto;
padding: 12px 12px 16px;
}
.aigc-use-template-popup-host { .aigc-use-template-popup-host {
position: fixed; position: fixed;
z-index: 30; z-index: 30;

View File

@@ -4,13 +4,7 @@
@recharge="handleRecharge" /> @recharge="handleRecharge" />
<view class="aigc-use-template-body"> <view class="aigc-use-template-body">
<view class="aigc-use-template-content" :class="{ 'is-generating': currentStep === 'generating' }"> <view class="aigc-use-template-content">
<GeneratingProgressPanel v-if="currentStep === 'generating'" :cost="currentCost" :progress="8"
:generator-type="currentTemplateItem.generatorType"
:item-title="currentTemplateItem.itemTitle || currentTemplate.templateTitle" :task-id="createdTaskId"
:task-status="taskStatus" @continue="handleContinueTemplates" @view="handleHistory" />
<template v-else>
<TemplateVersionHero :template="currentTemplate" /> <TemplateVersionHero :template="currentTemplate" />
<GenerateConfirmPanel v-if="currentStep === 'generateConfirm'" :cost="currentCost" <GenerateConfirmPanel v-if="currentStep === 'generateConfirm'" :cost="currentCost"
@@ -24,7 +18,6 @@
动效视频版会先生图再升级为 5 秒内动效视频 动效视频版会先生图再升级为 5 秒内动效视频
</text> </text>
</template> </template>
</template>
</view> </view>
</view> </view>
@@ -55,14 +48,12 @@ import AigcTopBar from "@/pages-aigc/components/AigcTopBar/index.vue";
import { useCurrentCredit } from "@/pages-aigc/hooks/useCurrentCredit.js"; import { useCurrentCredit } from "@/pages-aigc/hooks/useCurrentCredit.js";
import { import {
createAigcGeneratorTask, createAigcGeneratorTask,
getAigcGeneratorTaskDetail,
getAigcTemplateItemList, getAigcTemplateItemList,
getAigcTemplateList, getAigcTemplateList,
} from "@/request/api/AigcApi.js"; } from "@/request/api/AigcApi.js";
import { updateImageFile } from "@/request/api/UpdateFile.js"; import { updateImageFile } from "@/request/api/UpdateFile.js";
import guideAvatar from "./assets/xiaoqi-avatar.png"; import guideAvatar from "./assets/xiaoqi-avatar.png";
import GenerateConfirmPanel from "./components/GenerateConfirmPanel/index.vue"; import GenerateConfirmPanel from "./components/GenerateConfirmPanel/index.vue";
import GeneratingProgressPanel from "./components/GeneratingProgressPanel/index.vue";
import PhotoConfirmDrawer from "./components/PhotoConfirmDrawer/index.vue"; import PhotoConfirmDrawer from "./components/PhotoConfirmDrawer/index.vue";
import PhotoGuidePanel from "./components/PhotoGuidePanel/index.vue"; import PhotoGuidePanel from "./components/PhotoGuidePanel/index.vue";
import PhotoPickDrawer from "./components/PhotoPickDrawer/index.vue"; import PhotoPickDrawer from "./components/PhotoPickDrawer/index.vue";
@@ -78,8 +69,6 @@ const templateItems = ref([]);
const selectedTemplateItemId = ref(""); const selectedTemplateItemId = ref("");
const selectedImageLocalPath = ref(""); const selectedImageLocalPath = ref("");
const uploadedImageUrl = ref(""); const uploadedImageUrl = ref("");
const createdTaskId = ref("");
const taskStatus = ref(null);
const currentStep = ref("version"); const currentStep = ref("version");
const pointDialogVisible = ref(false); const pointDialogVisible = ref(false);
const isUploading = ref(false); const isUploading = ref(false);
@@ -338,19 +327,6 @@ const handleConfirmPhoto = () => {
currentStep.value = "generateConfirm"; currentStep.value = "generateConfirm";
}; };
const fetchCreatedTaskStatus = async () => {
if (!createdTaskId.value) return;
try {
const res = await getAigcGeneratorTaskDetail({ taskId: createdTaskId.value });
if (res?.code === 0 && res.data) {
taskStatus.value = res.data.taskStatus;
}
} catch (error) {
console.warn("获取AIGC生成任务状态失败", error);
}
};
const handleGenerate = async () => { const handleGenerate = async () => {
if (isCreatingTask.value) return; if (isCreatingTask.value) return;
@@ -375,9 +351,10 @@ const handleGenerate = async () => {
} }
isCreatingTask.value = true; isCreatingTask.value = true;
createdTaskId.value = ""; uni.showLoading({
taskStatus.value = null; title: "创建中",
currentStep.value = "generating"; mask: true,
});
try { try {
const res = await createAigcGeneratorTask({ const res = await createAigcGeneratorTask({
@@ -385,37 +362,25 @@ const handleGenerate = async () => {
imageUrlList: [uploadedImageUrl.value], imageUrlList: [uploadedImageUrl.value],
}); });
if (res?.code === 0 && res.data) { const taskId = String(res?.data || "").trim();
createdTaskId.value = res.data; if (res?.code === 0 && taskId) {
fetchCurrentCredit(); fetchCurrentCredit();
await fetchCreatedTaskStatus(); uni.redirectTo({
url: `/pages-aigc/progress/progress?taskId=${encodeURIComponent(taskId)}`,
fail: () => showPlaceholderToast("打开任务进度失败"),
});
} else { } else {
currentStep.value = "generateConfirm";
showPlaceholderToast(res?.msg || "创建任务失败"); showPlaceholderToast(res?.msg || "创建任务失败");
} }
} catch (error) { } catch (error) {
console.warn("创建AIGC生成任务失败", error); console.warn("创建AIGC生成任务失败", error);
currentStep.value = "generateConfirm";
showPlaceholderToast("创建任务失败"); showPlaceholderToast("创建任务失败");
} finally { } finally {
uni.hideLoading();
isCreatingTask.value = false; isCreatingTask.value = false;
} }
}; };
const handleContinueTemplates = () => {
const pages = getCurrentPages();
const previousPage = pages[pages.length - 2];
if (previousPage?.route === "pages-aigc/home/home") {
uni.navigateBack();
return;
}
uni.redirectTo({
url: "/pages-aigc/home/home",
fail: () => showPlaceholderToast("继续生成其他模板"),
});
};
const handleClosePointDialog = () => { const handleClosePointDialog = () => {
pointDialogVisible.value = false; pointDialogVisible.value = false;
}; };

View File

@@ -170,6 +170,12 @@
"navigationStyle": "custom" "navigationStyle": "custom"
} }
}, },
{
"path": "progress/progress",
"style": {
"navigationStyle": "custom"
}
},
{ {
"path": "detail/detail", "path": "detail/detail",
"style": { "style": {

View File

@@ -12,10 +12,7 @@
</view> </view>
</view> </view>
<AigcConsentDialog <AigcConsentDialog v-model:visible="aigcConsentVisible" @agree="handleAigcConsentAgree" />
v-model:visible="aigcConsentVisible"
@agree="handleAigcConsentAgree"
/>
</view> </view>
</template> </template>
@@ -36,16 +33,16 @@ const itemList = ref([
title: "快速预定", title: "快速预定",
type: Command.quickBooking, type: Command.quickBooking,
}, },
// {
// icon: "",
// title: "旅行记录",
// type: Command.travelAIGC,
// },
{ {
icon: "", icon: "",
title: "我的订单", title: "旅行记录",
type: Command.myOrder, type: Command.travelAIGC,
}, },
// {
// icon: "",
// title: "我的订单",
// type: Command.myOrder,
// },
{ {
icon: "", icon: "",
title: "呼叫服务", title: "呼叫服务",