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

@@ -1,175 +0,0 @@
<template>
<view class="generating-task-panel">
<view class="generating-task-status">
<text class="generating-task-badge">{{ generatorTypeText }}</text>
<text class="generating-task-title">接口处理中可稍后查看</text>
<text class="generating-task-desc">{{ displayItemTitle }} · 任务已进入后台队列</text>
<text class="generating-task-hint">生成完成后可在最近任务查看结果</text>
</view>
<view class="generating-progress-row">
<view class="generating-progress-track">
<view class="generating-progress-fill" :style="{ width: progressWidth }" />
</view>
<text class="generating-progress-text">{{ normalizedProgress }}%</text>
</view>
<view class="generating-step-list">
<view v-for="step in displaySteps" :key="step.label" class="generating-step-item"
:class="{ 'is-complete': step.complete }">
<view class="generating-step-dot" />
<text class="generating-step-label">{{ step.label }}</text>
</view>
</view>
<view class="generating-background-note">
<text class="generating-note-title">不用停留等待</text>
<text class="generating-note-desc">
系统会继续处理当前任务你可以先去制作其他模板完成后会进入最近任务
</text>
<view class="generating-actions">
<view class="generating-action is-primary" hover-class="is-pressed" @tap="emit('continue')">
继续生成其他模板
</view>
<view class="generating-action is-secondary" hover-class="is-pressed" @tap="emit('view')">
查看任务
</view>
</view>
<text class="generating-freeze-text">
已冻结 {{ displayCost }} 积分生成成功后扣除失败自动退回
</text>
</view>
</view>
</template>
<script setup>
import { computed, onUnmounted, ref, watch } from "vue";
const props = defineProps({
progress: {
type: [Number, String],
default: 8,
},
cost: {
type: [Number, String],
default: 100,
},
generatorType: {
type: [Number, String],
default: 0,
},
itemTitle: {
type: String,
default: "",
},
taskId: {
type: String,
default: "",
},
taskStatus: {
type: [Number, String],
default: null,
},
});
const emit = defineEmits(["continue", "view"]);
const normalizeProgress = (progress) => {
const value = Number(progress);
if (!Number.isFinite(value)) return 0;
return Math.max(0, Math.min(99, Math.round(value)));
};
const displayProgress = ref(normalizeProgress(props.progress));
let progressTimer = null;
const normalizedProgress = computed(() => normalizeProgress(displayProgress.value));
const progressWidth = computed(() => `${normalizedProgress.value}%`);
const isAnimatingStatus = (status) => {
if (status === null || status === undefined || status === "") return false;
return [0, 1, 4].includes(Number(status));
};
const clearProgressTimer = () => {
if (progressTimer) {
clearInterval(progressTimer);
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 = () => {
clearProgressTimer();
if (!isAnimatingStatus(props.taskStatus) || normalizedProgress.value >= 99) return;
progressTimer = setInterval(() => {
displayProgress.value = getNextProgress(normalizedProgress.value);
if (normalizedProgress.value >= 99) {
clearProgressTimer();
}
}, 320);
};
const generatorTypeText = computed(() => {
if (props.generatorType === "" || props.generatorType === null || props.generatorType === undefined) {
return "生成任务";
}
if (Number(props.generatorType) === 0) return "图片版";
if (Number(props.generatorType) === 1) return "视频版";
return `类型${props.generatorType}`;
});
const displayItemTitle = computed(() => props.itemTitle || props.taskId || "生成任务");
const displayCost = computed(() => Number(props.cost) || 0);
const displaySteps = computed(() => {
const steps =
Number(props.generatorType) === 1
? ["素材质检", "生成图片底图", "升级动效视频", "导出视频"]
: ["素材质检", "图片优化", "套入模板", "导出图片"];
const thresholds = [20, 46, 72, 96];
return steps.map((label, index) => ({
label,
complete: normalizedProgress.value >= thresholds[index],
}));
});
watch(
() => props.progress,
(progress) => {
const initialProgress = normalizeProgress(progress);
if (!isAnimatingStatus(props.taskStatus) || displayProgress.value < initialProgress) {
displayProgress.value = initialProgress;
}
}
);
watch(
() => props.taskStatus,
(status) => {
clearProgressTimer();
if (isAnimatingStatus(status)) {
startProgressAnimation();
}
},
{ immediate: true }
);
onUnmounted(() => {
clearProgressTimer();
});
</script>
<style scoped lang="scss">
@import "./styles/index.scss";
</style>

View File

@@ -1,227 +0,0 @@
.generating-task-panel {
width: 100%;
min-width: 0;
box-sizing: border-box;
}
.generating-task-status {
position: relative;
min-height: 134px;
overflow: hidden;
display: block;
padding: 18px 18px 16px;
border-radius: 22px;
background:
radial-gradient(96% 120% at 96% 0%, rgba(32, 207, 117, 0.1), rgba(32, 207, 117, 0) 52%),
rgba(255, 255, 255, 0.84);
box-shadow: 0 12px 28px rgba(15, 29, 37, 0.08);
box-sizing: border-box;
}
.generating-task-badge {
height: 24px;
display: inline-flex;
align-items: center;
padding: 0 10px;
border-radius: 999px;
background: rgba(32, 207, 117, 0.13);
color: #0e9f61;
font-size: 11px;
line-height: 24px;
font-weight: 900;
}
.generating-task-title,
.generating-task-desc,
.generating-task-hint {
display: block;
}
.generating-task-title {
margin-top: 16px;
color: #172033;
font-size: 24px;
line-height: 30px;
font-weight: 900;
}
.generating-task-desc {
margin-top: 5px;
overflow: hidden;
color: #526073;
font-size: 12px;
line-height: 18px;
font-weight: 800;
text-overflow: ellipsis;
white-space: nowrap;
}
.generating-task-hint {
margin-top: 10px;
color: #91a0b1;
font-size: 11px;
line-height: 15px;
font-weight: 800;
}
.generating-progress-row {
height: 52px;
display: grid;
grid-template-columns: minmax(0, 1fr) 42px;
align-items: center;
gap: 10px;
margin-top: 18px;
}
.generating-progress-track {
width: 100%;
height: 12px;
overflow: hidden;
border-radius: 999px;
background: rgba(255, 255, 255, 0.8);
}
.generating-progress-fill {
height: 100%;
min-width: 24px;
border-radius: 999px;
background: linear-gradient(90deg, #20cf75 0%, #f5bd32 100%);
transition: width 280ms ease;
}
.generating-progress-text {
color: #172033;
font-size: 15px;
line-height: 20px;
font-weight: 900;
text-align: right;
white-space: nowrap;
}
.generating-step-list {
display: grid;
gap: 9px;
margin-top: 12px;
}
.generating-step-item {
height: 42px;
display: flex;
align-items: center;
gap: 10px;
padding: 0 12px;
border-radius: 14px;
background: rgba(255, 255, 255, 0.72);
box-sizing: border-box;
}
.generating-step-dot {
flex: 0 0 18px;
width: 18px;
height: 18px;
border: 2px solid #c7d2df;
border-radius: 50%;
box-sizing: border-box;
}
.generating-step-item.is-complete .generating-step-dot {
border-color: #20cf75;
background: #20cf75;
box-shadow: inset 0 0 0 4px #ffffff;
}
.generating-step-label {
min-width: 0;
overflow: hidden;
color: #7d899a;
font-size: 13px;
line-height: 18px;
font-weight: 900;
text-overflow: ellipsis;
white-space: nowrap;
}
.generating-step-item.is-complete .generating-step-label {
color: #172033;
}
.generating-background-note {
margin-top: 16px;
padding: 14px;
border-radius: 18px;
background:
linear-gradient(135deg, rgba(32, 207, 117, 0.11), rgba(255, 255, 255, 0.9) 54%),
rgba(255, 255, 255, 0.84);
color: #172033;
box-shadow: 0 10px 22px rgba(18, 48, 56, 0.07);
box-sizing: border-box;
}
.generating-note-title,
.generating-note-desc,
.generating-freeze-text {
display: block;
}
.generating-note-title {
font-size: 14px;
line-height: 19px;
font-weight: 900;
}
.generating-note-desc {
margin-top: 4px;
color: #526073;
font-size: 11px;
line-height: 16px;
font-weight: 800;
}
.generating-actions {
display: grid;
grid-template-columns: 1.2fr 1fr;
gap: 10px;
margin-top: 12px;
}
.generating-action {
min-width: 0;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
border-radius: 999px;
font-size: 15px;
line-height: 20px;
font-weight: 900;
text-overflow: ellipsis;
white-space: nowrap;
}
.generating-action.is-primary {
background: #20cf75;
color: #ffffff;
box-shadow: 0 8px 16px rgba(32, 207, 117, 0.18);
}
.generating-action.is-secondary {
background: rgba(255, 255, 255, 0.78);
color: #526073;
}
.generating-action.is-pressed {
opacity: 0.82;
}
.generating-freeze-text {
overflow: hidden;
margin-top: 8px;
color: #8c98a8;
font-size: 10px;
line-height: 14px;
font-weight: 800;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}

View File

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

View File

@@ -4,26 +4,19 @@
@recharge="handleRecharge" />
<view class="aigc-use-template-body">
<view class="aigc-use-template-content" :class="{ 'is-generating': currentStep === 'generating' }">
<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" />
<view class="aigc-use-template-content">
<TemplateVersionHero :template="currentTemplate" />
<GenerateConfirmPanel v-if="currentStep === 'generateConfirm'" :cost="currentCost"
:balance="pointBalance" @generate="handleGenerate" @recharge="handleRecharge" />
<template v-else>
<TemplateVersionHero :template="currentTemplate" />
<VersionOptionList :options="templateItems" :selected-value="selectedTemplateItemId"
@select="handleSelectTemplateItem" />
<GenerateConfirmPanel v-if="currentStep === 'generateConfirm'" :cost="currentCost"
:balance="pointBalance" @generate="handleGenerate" @recharge="handleRecharge" />
<template v-else>
<VersionOptionList :options="templateItems" :selected-value="selectedTemplateItemId"
@select="handleSelectTemplateItem" />
<text class="aigc-use-template-note">
动效视频版会先生图再升级为 5 秒内动效视频
</text>
</template>
<text class="aigc-use-template-note">
动效视频版会先生图再升级为 5 秒内动效视频
</text>
</template>
</view>
</view>
@@ -55,14 +48,12 @@ import AigcTopBar from "@/pages-aigc/components/AigcTopBar/index.vue";
import { useCurrentCredit } from "@/pages-aigc/hooks/useCurrentCredit.js";
import {
createAigcGeneratorTask,
getAigcGeneratorTaskDetail,
getAigcTemplateItemList,
getAigcTemplateList,
} from "@/request/api/AigcApi.js";
import { updateImageFile } from "@/request/api/UpdateFile.js";
import guideAvatar from "./assets/xiaoqi-avatar.png";
import GenerateConfirmPanel from "./components/GenerateConfirmPanel/index.vue";
import GeneratingProgressPanel from "./components/GeneratingProgressPanel/index.vue";
import PhotoConfirmDrawer from "./components/PhotoConfirmDrawer/index.vue";
import PhotoGuidePanel from "./components/PhotoGuidePanel/index.vue";
import PhotoPickDrawer from "./components/PhotoPickDrawer/index.vue";
@@ -78,8 +69,6 @@ const templateItems = ref([]);
const selectedTemplateItemId = ref("");
const selectedImageLocalPath = ref("");
const uploadedImageUrl = ref("");
const createdTaskId = ref("");
const taskStatus = ref(null);
const currentStep = ref("version");
const pointDialogVisible = ref(false);
const isUploading = ref(false);
@@ -338,19 +327,6 @@ const handleConfirmPhoto = () => {
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 () => {
if (isCreatingTask.value) return;
@@ -375,9 +351,10 @@ const handleGenerate = async () => {
}
isCreatingTask.value = true;
createdTaskId.value = "";
taskStatus.value = null;
currentStep.value = "generating";
uni.showLoading({
title: "创建中",
mask: true,
});
try {
const res = await createAigcGeneratorTask({
@@ -385,37 +362,25 @@ const handleGenerate = async () => {
imageUrlList: [uploadedImageUrl.value],
});
if (res?.code === 0 && res.data) {
createdTaskId.value = res.data;
const taskId = String(res?.data || "").trim();
if (res?.code === 0 && taskId) {
fetchCurrentCredit();
await fetchCreatedTaskStatus();
uni.redirectTo({
url: `/pages-aigc/progress/progress?taskId=${encodeURIComponent(taskId)}`,
fail: () => showPlaceholderToast("打开任务进度失败"),
});
} else {
currentStep.value = "generateConfirm";
showPlaceholderToast(res?.msg || "创建任务失败");
}
} catch (error) {
console.warn("创建AIGC生成任务失败", error);
currentStep.value = "generateConfirm";
showPlaceholderToast("创建任务失败");
} finally {
uni.hideLoading();
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 = () => {
pointDialogVisible.value = false;
};