feat(aigc): refine generation UI and add task status tracking

Add real-time task status polling for active generation jobs.
Overhaul GeneratingProgressPanel with dynamic step indicators, action buttons and updated styling.
Update PointInsufficientDialog to show detailed credit balance and usage summary.
Fix RecordCard media display and status logic for generating and completed tasks.
Restyle PhotoPickDrawer and update its instructional copy.
Add overflow styling for generating template content container.
This commit is contained in:
duanshuwen
2026-07-14 22:09:37 +08:00
parent 1ebdf92159
commit 9bdef279b7
10 changed files with 474 additions and 206 deletions

View File

@@ -3,7 +3,9 @@
<video v-if="isVideoRecord" class="record-card-image" :src="mediaUrl" :controls="false" :show-play-btn="false"
:show-center-play-btn="false" object-fit="cover" />
<image v-else-if="mediaUrl" class="record-card-image" :src="mediaUrl" mode="aspectFill" />
<view v-else class="record-card-image record-card-media-placeholder" />
<view v-else class="record-card-image record-card-media-placeholder">
<text v-if="isGenerating" class="record-card-media-text">生成中</text>
</view>
<view class="record-card-copy">
<text class="record-card-title ellipsis-1">{{ title }}</text>
@@ -11,7 +13,7 @@
<text class="record-card-status ellipsis-1">{{ statusText }}</text>
</view>
<view v-if="record.taskStatus === 2" class="record-card-action" @tap="emit('view', record)">
<view v-if="isCompleted" class="record-card-action" @tap="emit('view', record)">
查看
</view>
</view>
@@ -44,12 +46,17 @@ const TASK_STATUS_TEXT = {
const title = computed(() => props.record.itemTitle || props.record.taskId || "");
const normalizedTaskStatus = computed(() => Number(props.record.taskStatus));
const isCompleted = computed(() => normalizedTaskStatus.value === 2);
const isGenerating = computed(() => [0, 1].includes(normalizedTaskStatus.value));
const mediaUrl = computed(() => {
if (!isCompleted.value) return "";
return props.record.imageResultUrl || props.record.videoResultUrl || "";
});
const isVideoRecord = computed(() => {
return !props.record.imageResultUrl && Boolean(props.record.videoResultUrl);
return isCompleted.value && !props.record.imageResultUrl && Boolean(props.record.videoResultUrl);
});
const statusText = computed(() => {

View File

@@ -26,9 +26,19 @@
}
.record-card-media-placeholder {
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #edf5f1 0%, #f6faf8 100%);
}
.record-card-media-text {
color: #263f52;
font-size: 11px;
line-height: 16px;
font-weight: 800;
}
.record-card-copy {
min-width: 0;
}

View File

@@ -1,5 +1,12 @@
<template>
<view class="generating-progress-panel">
<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 }" />
@@ -8,25 +15,37 @@
</view>
<view class="generating-step-list">
<view
v-for="(step, index) in steps"
:key="`${step}-${index}`"
class="generating-step-item"
:class="getStepClass(index)"
>
<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 }}</text>
<text class="generating-step-label">{{ step.label }}</text>
</view>
</view>
<text class="generating-freeze-text">
已冻结 {{ cost }} 积分生成成功后扣除失败自动退回
</text>
<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, onMounted, onUnmounted, ref, watch } from "vue";
import { computed, onUnmounted, ref, watch } from "vue";
const props = defineProps({
progress: {
@@ -37,37 +56,42 @@ const props = defineProps({
type: [Number, String],
default: 100,
},
steps: {
type: Array,
default: () => ["素材质检", "图片优化", "套入模板", "导出图片"],
generatorType: {
type: [Number, String],
default: 0,
},
itemTitle: {
type: String,
default: "",
},
taskId: {
type: String,
default: "",
},
taskStatus: {
type: [Number, String],
default: null,
},
});
const emit = defineEmits(["complete"]);
const displayProgress = ref(0);
let progressTimer = null;
let hasCompleted = false;
const emit = defineEmits(["continue", "view"]);
const normalizeProgress = (progress) => {
const value = Number(progress);
if (!Number.isFinite(value)) return 0;
return Math.max(0, Math.min(100, Math.round(value)));
return Math.max(0, Math.min(99, Math.round(value)));
};
const normalizedProgress = computed(() => {
return normalizeProgress(displayProgress.value);
});
const displayProgress = ref(normalizeProgress(props.progress));
let progressTimer = null;
const normalizedProgress = computed(() => normalizeProgress(displayProgress.value));
const progressWidth = computed(() => `${normalizedProgress.value}%`);
const currentStepIndex = computed(() => {
const progress = normalizedProgress.value;
if (progress <= 24) return 0;
if (progress <= 49) return 1;
if (progress <= 79) return 2;
return 3;
});
const isAnimatingStatus = (status) => {
if (status === null || status === undefined || status === "") return false;
return [0, 1, 4].includes(Number(status));
};
const clearProgressTimer = () => {
if (progressTimer) {
@@ -80,56 +104,66 @@ 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);
if (progress < 99) return Math.min(progress + 1, 99);
return 100;
return Math.min(progress + 1, 99);
};
const completeProgress = () => {
const startProgressAnimation = () => {
clearProgressTimer();
if (hasCompleted) return;
hasCompleted = true;
emit("complete");
};
const startProgressTimer = () => {
clearProgressTimer();
if (normalizedProgress.value >= 100) {
completeProgress();
return;
}
if (!isAnimatingStatus(props.taskStatus) || normalizedProgress.value >= 99) return;
progressTimer = setInterval(() => {
displayProgress.value = getNextProgress(normalizedProgress.value);
if (normalizedProgress.value >= 100) {
completeProgress();
if (normalizedProgress.value >= 99) {
clearProgressTimer();
}
}, 320);
};
const resetProgress = () => {
hasCompleted = false;
displayProgress.value = normalizeProgress(props.progress);
startProgressTimer();
};
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 getStepClass = (index) => ({
"is-complete": normalizedProgress.value >= 100 || index < currentStepIndex.value,
"is-active": normalizedProgress.value < 100 && index === currentStepIndex.value,
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,
() => {
resetProgress();
(progress) => {
const initialProgress = normalizeProgress(progress);
if (!isAnimatingStatus(props.taskStatus) || displayProgress.value < initialProgress) {
displayProgress.value = initialProgress;
}
}
);
onMounted(() => {
resetProgress();
});
watch(
() => props.taskStatus,
(status) => {
clearProgressTimer();
if (isAnimatingStatus(status)) {
startProgressAnimation();
}
},
{ immediate: true }
);
onUnmounted(() => {
clearProgressTimer();

View File

@@ -1,13 +1,76 @@
.generating-progress-panel {
margin-top: 20px;
.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: 18px;
height: 52px;
display: grid;
grid-template-columns: minmax(0, 1fr) 34px;
grid-template-columns: minmax(0, 1fr) 42px;
align-items: center;
gap: 10px;
margin-top: 18px;
}
.generating-progress-track {
@@ -15,35 +78,34 @@
height: 12px;
overflow: hidden;
border-radius: 999px;
background: rgba(255, 255, 255, 0.78);
background: rgba(255, 255, 255, 0.8);
}
.generating-progress-fill {
height: 100%;
min-width: 24px;
border-radius: 999px;
background: linear-gradient(90deg, #20cf75 0%, #f2d646 100%);
transition: width 0.28s ease;
background: linear-gradient(90deg, #20cf75 0%, #f5bd32 100%);
transition: width 280ms ease;
}
.generating-progress-text {
color: #061e34;
color: #172033;
font-size: 15px;
line-height: 18px;
line-height: 20px;
font-weight: 900;
text-align: right;
white-space: nowrap;
}
.generating-step-list {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 16px;
display: grid;
gap: 9px;
margin-top: 12px;
}
.generating-step-item {
height: 40px;
height: 42px;
display: flex;
align-items: center;
gap: 10px;
@@ -53,39 +115,25 @@
box-sizing: border-box;
}
.generating-step-item.is-active {
background: rgba(255, 255, 255, 0.86);
}
.generating-step-item.is-complete {
background: rgba(236, 255, 244, 0.72);
}
.generating-step-dot {
flex: 0 0 18px;
width: 18px;
height: 18px;
border: 2px solid #d6e8f4;
border: 2px solid #c7d2df;
border-radius: 50%;
background: rgba(255, 255, 255, 0.7);
box-sizing: border-box;
}
.generating-step-item.is-active .generating-step-dot {
.generating-step-item.is-complete .generating-step-dot {
border-color: #20cf75;
background: #20cf75;
box-shadow: 0 0 0 4px rgba(32, 207, 117, 0.12);
}
.generating-step-item.is-complete .generating-step-dot {
border-color: rgba(32, 207, 117, 0.42);
background: rgba(32, 207, 117, 0.22);
box-shadow: inset 0 0 0 4px #ffffff;
}
.generating-step-label {
min-width: 0;
overflow: hidden;
color: #6c7e96;
color: #7d899a;
font-size: 13px;
line-height: 18px;
font-weight: 900;
@@ -93,18 +141,85 @@
white-space: nowrap;
}
.generating-step-item.is-active .generating-step-label,
.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;
margin: 18px 14px 0;
color: #7b8da4;
font-size: 12px;
line-height: 18px;
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;

View File

@@ -1,51 +1,34 @@
<template>
<uni-popup
ref="popupRef"
type="bottom"
background-color="transparent"
mask-background-color="rgba(0, 0, 0, 0)"
:safe-area="false"
:is-mask-click="false"
>
<uni-popup ref="popupRef" type="bottom" background-color="transparent" mask-background-color="rgba(0, 0, 0, 0)"
:safe-area="false" :is-mask-click="false">
<view class="photo-pick-drawer">
<view
class="photo-pick-close"
:class="{ 'is-disabled': loading }"
@tap="handleClose"
>
<view class="photo-pick-close" :class="{ 'is-disabled': loading }" @tap="handleClose">
<uni-icons type="closeempty" size="30" color="#8fa2ba" />
</view>
<text class="photo-pick-title">选择照片</text>
<view class="photo-pick-card">
<view class="photo-pick-plus">+</view>
<text class="photo-pick-card-title">上传照片素材</text>
<text class="photo-pick-card-desc">
选择后会自动进行清晰度人脸数量和遮挡检测
</text>
<view class="photo-pick-card-title">上传素材要求</view>
<view class="photo-pick-card-desc">
请使用清晰半身照避免墨镜口罩强逆光选择后系统自动进行清晰度人脸数量和遮挡检测
</view>
</view>
<view class="photo-pick-actions">
<view
class="photo-pick-action"
:class="{ 'is-disabled': loading }"
:hover-class="loading ? 'none' : 'is-pressed'"
@tap="handleCamera"
>
拍照
<view class="photo-pick-action is-primary" :class="{ 'is-disabled': loading }"
:hover-class="loading ? 'none' : 'is-pressed'" @tap="handleCamera">
<text class="photo-pick-action-title">拍照</text>
<text class="photo-pick-action-desc">现场拍摄素材</text>
</view>
<view
class="photo-pick-action"
:class="{ 'is-disabled': loading }"
:hover-class="loading ? 'none' : 'is-pressed'"
@tap="handleAlbum"
>
相册
<view class="photo-pick-action" :class="{ 'is-disabled': loading }"
:hover-class="loading ? 'none' : 'is-pressed'" @tap="handleAlbum">
<text class="photo-pick-action-title">相册</text>
<text class="photo-pick-action-desc">从相册选择</text>
</view>
</view>
<text class="photo-pick-hint">建议使用半身照避免墨镜口罩强逆光</text>
<text class="photo-pick-hint">入口在这里选择后进入照片质检与生成确认</text>
</view>
</uni-popup>
</template>

View File

@@ -39,34 +39,14 @@
.photo-pick-card {
width: 100%;
min-height: 126px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
padding: 14px 18px;
border: 1.5px dashed rgba(32, 207, 117, 0.46);
border-radius: 20px;
background: #f2fbf6;
text-align: center;
box-sizing: border-box;
}
.photo-pick-plus {
width: 42px;
height: 42px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: #20cf75;
color: #ffffff;
font-size: 25px;
line-height: 42px;
font-weight: 500;
}
.photo-pick-card-title,
.photo-pick-card-desc {
display: block;
@@ -75,16 +55,15 @@
}
.photo-pick-card-title {
max-width: 230px;
color: #172033;
font-size: 16px;
line-height: 22px;
font-weight: 900;
font-weight: 700;
white-space: nowrap;
margin-bottom: 12px;
}
.photo-pick-card-desc {
max-width: 230px;
color: #667386;
font-size: 12px;
line-height: 18px;
@@ -95,22 +74,58 @@
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
margin-top: 10px;
margin-top: 14px;
}
.photo-pick-action {
height: 42px;
min-width: 0;
min-height: 66px;
padding: 8px 10px;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 16px;
background: #f5f8f7;
gap: 3px;
border-radius: 18px;
background: #ffffff;
color: #172033;
font-size: 14px;
box-shadow: 0 8px 18px rgba(18, 48, 56, 0.08);
transition: opacity 180ms ease;
}
.photo-pick-action.is-primary {
background: #20cf75;
color: #ffffff;
box-shadow: 0 12px 22px rgba(32, 207, 117, 0.22);
}
.photo-pick-action-title,
.photo-pick-action-desc {
display: block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.photo-pick-action-title {
font-size: 15px;
line-height: 20px;
font-weight: 900;
}
.photo-pick-action-desc {
color: #7a8596;
font-size: 10px;
line-height: 14px;
font-weight: 800;
}
.photo-pick-action.is-primary .photo-pick-action-desc {
color: rgba(255, 255, 255, 0.82);
}
.photo-pick-action.is-pressed {
opacity: 0.82;
}

View File

@@ -7,12 +7,28 @@
:safe-area="false"
:is-mask-click="false"
>
<view class="point-dialog">
<text class="point-dialog-title">积分不足</text>
<view class="point-dialog point-recharge-dialog">
<text class="point-dialog-badge">充值提醒</text>
<text class="point-dialog-title">当前积分不足</text>
<text class="point-dialog-desc">
本次 {{ normalizedCost }} 积分当前仅 {{ normalizedBalance }} 积分还差 {{ shortage }} 积分
本次生成需要先冻结对应积分充值后可继续当前素材生成
</text>
<view class="point-dialog-summary">
<view class="point-summary-item">
<text class="point-summary-label">当前积分</text>
<text class="point-summary-value">{{ normalizedBalance }}</text>
</view>
<view class="point-summary-item">
<text class="point-summary-label">本次需要</text>
<text class="point-summary-value">{{ normalizedCost }}</text>
</view>
<view class="point-summary-item is-shortage">
<text class="point-summary-label">需补足</text>
<text class="point-summary-value">{{ shortage }}</text>
</view>
</view>
<view class="point-dialog-actions">
<view
class="point-dialog-button is-cancel"

View File

@@ -1,36 +1,96 @@
.point-dialog {
width: 318px;
max-width: calc(100vw - 56px);
min-height: 220px;
padding: 29px 24px 22px;
border: 1px solid rgba(20, 143, 255, 0.8);
border-radius: 20px;
background: #dceeff;
box-shadow: 0 18px 38px rgba(8, 28, 44, 0.22);
padding: 30px 26px 24px;
border-radius: 24px;
background: #ffffff;
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.26);
box-sizing: border-box;
}
.point-recharge-dialog {
padding-top: 24px;
}
.point-dialog-badge {
width: fit-content;
height: 26px;
display: flex;
align-items: center;
margin: 0 auto 12px;
padding: 0 12px;
border-radius: 999px;
background: #e9f9f1;
color: #0fa567;
font-size: 12px;
line-height: 26px;
font-weight: 900;
}
.point-dialog-title {
display: block;
color: #102942;
font-size: 22px;
line-height: 29px;
margin-bottom: 14px;
color: #111827;
font-size: 20px;
line-height: 28px;
font-weight: 900;
text-align: center;
}
.point-dialog-desc {
display: block;
overflow: hidden;
margin: 15px auto 0;
max-width: 228px;
color: #506985;
margin: 0;
color: #667386;
font-size: 14px;
line-height: 22px;
font-weight: 800;
text-align: center;
}
.point-dialog-summary {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
margin-top: 18px;
}
.point-summary-item {
min-width: 0;
padding: 11px 6px 10px;
border-radius: 15px;
background: #f3fbf6;
color: #172033;
text-align: center;
box-sizing: border-box;
}
.point-summary-label,
.point-summary-value {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.point-summary-label {
color: #7b8798;
font-size: 10px;
line-height: 13px;
font-weight: 800;
}
.point-summary-value {
margin-top: 3px;
color: #172033;
font-size: 18px;
line-height: 24px;
font-weight: 900;
}
.point-summary-item.is-shortage .point-summary-value {
color: #0fa567;
}
.point-dialog-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -55,9 +115,9 @@
}
.point-dialog-button.is-cancel {
border: 1px solid rgba(124, 168, 207, 0.28);
background: rgba(255, 255, 255, 0.14);
color: #102942;
border: 1px solid #e2e6ea;
background: #ffffff;
color: #172033;
}
.point-dialog-button.is-primary {

View File

@@ -31,6 +31,11 @@
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,21 +4,26 @@
@recharge="handleRecharge" />
<view class="aigc-use-template-body">
<view class="aigc-use-template-content">
<TemplateVersionHero :template="currentTemplate" />
<GenerateConfirmPanel v-if="currentStep === 'generateConfirm'" :cost="currentCost" :balance="pointBalance"
@generate="handleGenerate" @recharge="handleRecharge" />
<GeneratingProgressPanel v-else-if="currentStep === 'generating'" :cost="currentCost" :progress="8"
@complete="handleGenerateComplete" />
<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" />
<template v-else>
<VersionOptionList :options="templateItems" :selected-value="selectedTemplateItemId"
@select="handleSelectTemplateItem" />
<TemplateVersionHero :template="currentTemplate" />
<text class="aigc-use-template-note">
动效视频版会先生图再升级为 5 秒内动效视频
</text>
<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>
</template>
</view>
</view>
@@ -50,6 +55,7 @@ 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";
@@ -73,6 +79,7 @@ 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);
@@ -331,6 +338,19 @@ 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;
@@ -355,10 +375,9 @@ const handleGenerate = async () => {
}
isCreatingTask.value = true;
uni.showLoading({
title: "创建中",
mask: true,
});
createdTaskId.value = "";
taskStatus.value = null;
currentStep.value = "generating";
try {
const res = await createAigcGeneratorTask({
@@ -368,28 +387,32 @@ const handleGenerate = async () => {
if (res?.code === 0 && res.data) {
createdTaskId.value = res.data;
currentStep.value = "generating";
fetchCurrentCredit();
await fetchCreatedTaskStatus();
} else {
currentStep.value = "generateConfirm";
showPlaceholderToast(res?.msg || "创建任务失败");
}
} catch (error) {
console.warn("创建AIGC生成任务失败", error);
currentStep.value = "generateConfirm";
showPlaceholderToast("创建任务失败");
} finally {
isCreatingTask.value = false;
uni.hideLoading();
}
};
const handleGenerateComplete = () => {
if (!createdTaskId.value) {
showPlaceholderToast("任务创建失败");
const handleContinueTemplates = () => {
const pages = getCurrentPages();
const previousPage = pages[pages.length - 2];
if (previousPage?.route === "pages-aigc/home/home") {
uni.navigateBack();
return;
}
uni.navigateTo({
url: `/pages-aigc/detail/detail?taskId=${encodeURIComponent(createdTaskId.value)}`,
uni.redirectTo({
url: "/pages-aigc/home/home",
fail: () => showPlaceholderToast("继续生成其他模板"),
});
};