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:
@@ -0,0 +1,161 @@
|
||||
<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({
|
||||
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 TASK_STATUS_PROGRESS = {
|
||||
0: 23,
|
||||
1: 56,
|
||||
2: 100,
|
||||
4: 74,
|
||||
};
|
||||
|
||||
const normalizeProgress = (progress) => {
|
||||
const value = Number(progress);
|
||||
if (!Number.isFinite(value)) return 0;
|
||||
return Math.max(0, Math.min(100, Math.round(value)));
|
||||
};
|
||||
|
||||
const displayProgress = ref(0);
|
||||
let progressTimer = null;
|
||||
|
||||
const normalizedProgress = computed(() => normalizeProgress(displayProgress.value));
|
||||
const progressWidth = computed(() => `${normalizedProgress.value}%`);
|
||||
const targetProgress = computed(() => TASK_STATUS_PROGRESS[Number(props.taskStatus)] ?? 0);
|
||||
|
||||
const clearProgressTimer = () => {
|
||||
if (progressTimer !== null) {
|
||||
clearInterval(progressTimer);
|
||||
progressTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
const startProgressAnimation = () => {
|
||||
clearProgressTimer();
|
||||
const target = targetProgress.value;
|
||||
if (normalizedProgress.value === target) return;
|
||||
|
||||
progressTimer = setInterval(() => {
|
||||
const current = normalizedProgress.value;
|
||||
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();
|
||||
}
|
||||
}, 60);
|
||||
};
|
||||
|
||||
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.taskStatus,
|
||||
() => {
|
||||
startProgressAnimation();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
clearProgressTimer();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./styles/index.scss";
|
||||
</style>
|
||||
@@ -0,0 +1,227 @@
|
||||
.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;
|
||||
}
|
||||
124
src/pages-aigc/progress/progress.vue
Normal file
124
src/pages-aigc/progress/progress.vue
Normal 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>
|
||||
28
src/pages-aigc/progress/styles/index.scss
Normal file
28
src/pages-aigc/progress/styles/index.scss
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user