topfans/frontend/pages/discover/generation-loading.vue
2026-05-16 02:42:32 +08:00

362 lines
8.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="generation-loading">
<image class="background-image" src="/static/background/exhibitionSuccess.png" mode="aspectFill" />
<view class="gift-box">
<image class="gift-image" src="/static/nft/lihe.png" mode="aspectFit" />
</view>
<view class="progress-container">
<view class="progress-bar-wrapper">
<view class="progress-bar">
<view class="progress-fill" :style="{ width: progress + '%' }"></view>
</view>
<view class="progress-icon" :style="{ left: progress + '%' }">
<view class="icon-circle"></view>
</view>
</view>
<text class="progress-text">{{ Math.round(progress) }}%</text>
<text class="loading-text">Thinking</text>
</view>
<!-- 镭射批量导出离屏 -->
<canvas
canvas-id="laserBatchCanvas"
class="laser-batch-canvas"
:style="{ width: laserCanvasW + 'px', height: laserCanvasH + 'px' }"
/>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { imageGenerationApi } from '@/utils/api.js';
import {
consumeGenerationFlowPayload,
FLOW_MODE_PREFILLED,
FLOW_MODE_LENTICULAR,
FLOW_MODE_LASER,
GENERATED_IMAGES_KEY,
GENERATION_REQUEST_KEY,
CASTLOVE_FORM_KEY,
persistLenticularPreviewMeta,
persistLaserPreviewImages,
} from '@/utils/castloveGenerationFlow.js';
import {
generateLaserVariantBatch,
LASER_BATCH_CANVAS_SIZE,
} from '@/utils/laser-card/laserBatchExport.js';
const progress = ref(0);
const laserCanvasW = LASER_BATCH_CANVAS_SIZE.w;
const laserCanvasH = LASER_BATCH_CANVAS_SIZE.h;
let progressTimer = null;
let generationData = null;
let flowStartedAt = 0;
// 模拟进度增长
const simulateProgress = () => {
progressTimer = setInterval(() => {
if (progress.value < 90) {
// 进度到90%前缓慢增长
const increment = Math.random() * 5 + 2;
progress.value = Math.min(90, progress.value + increment);
}
}, 500);
};
// 停止进度模拟
const stopProgress = () => {
if (progressTimer) {
clearInterval(progressTimer);
progressTimer = null;
}
};
// 完成进度
const completeProgress = () => {
stopProgress();
const finalInterval = setInterval(() => {
if (progress.value < 100) {
progress.value = Math.min(100, progress.value + 5);
} else {
clearInterval(finalInterval);
// 进度完成后延迟跳转
setTimeout(() => {
handleSuccess();
}, 500);
}
}, 50);
};
// 回退进度
const revertProgress = () => {
stopProgress();
const revertInterval = setInterval(() => {
if (progress.value > 0) {
progress.value = Math.max(0, progress.value - 10);
} else {
clearInterval(revertInterval);
// 回退完成后返回上一页
setTimeout(() => {
uni.navigateBack();
}, 500);
}
}, 100);
};
const waitMinDuration = async (minMs) => {
if (!minMs || minMs <= 0) return;
const elapsed = Date.now() - flowStartedAt;
if (elapsed < minMs) {
await new Promise((r) => setTimeout(r, minMs - elapsed));
}
};
const handleSuccess = () => {
uni.showToast({ title: '生成成功', icon: 'success', duration: 1200 });
setTimeout(() => {
uni.navigateTo({ url: '/pages/discover/generation-result' });
}, 1200);
};
const runPrefilledFlow = async (flow) => {
try {
const images = flow.images || [];
if (!images.length) throw new Error('无候选图');
await waitMinDuration(flow.minDurationMs ?? 1400);
uni.setStorageSync(GENERATED_IMAGES_KEY, JSON.stringify(images));
completeProgress();
} catch (err) {
console.error('[GenerationLoading] prefilled', err);
uni.showToast({ title: err.message || '加载失败', icon: 'none' });
revertProgress();
}
};
const runLenticularFlow = async (flow) => {
try {
const formStr = uni.getStorageSync(CASTLOVE_FORM_KEY);
if (!formStr) throw new Error('缺少表单数据');
const formData = JSON.parse(formStr);
await waitMinDuration(flow.minDurationMs ?? 1600);
persistLenticularPreviewMeta(formData);
completeProgress();
} catch (err) {
console.error('[GenerationLoading] lenticular', err);
uni.showToast({ title: err.message || '光栅预览失败', icon: 'none' });
revertProgress();
}
};
const runLaserFlow = async (flow) => {
try {
const formStr = uni.getStorageSync(CASTLOVE_FORM_KEY);
if (!formStr) throw new Error('缺少表单数据');
const formData = JSON.parse(formStr);
const imagePath = formData.image || formData.uploadedImage || '';
if (!imagePath) throw new Error('缺少上传图片');
const paths = await generateLaserVariantBatch(imagePath, 'laserBatchCanvas');
await waitMinDuration(flow.minDurationMs ?? 1600);
persistLaserPreviewImages(paths);
completeProgress();
} catch (err) {
console.error('[GenerationLoading] laser', err);
uni.showToast({ title: err.message || '镭射生成失败', icon: 'none' });
revertProgress();
}
};
const callImageGeneration = async () => {
try {
const res = await imageGenerationApi(generationData);
if (res.data?.images?.length > 0) {
uni.setStorageSync(GENERATED_IMAGES_KEY, JSON.stringify(res.data.images));
completeProgress();
} else {
uni.showToast({ title: '未生成图片', icon: 'none' });
revertProgress();
}
} catch (err) {
console.error('[GenerationLoading] API', err);
uni.showToast({ title: err.message || '生成失败', icon: 'none' });
revertProgress();
}
};
onMounted(() => {
flowStartedAt = Date.now();
simulateProgress();
try {
const flow = consumeGenerationFlowPayload();
if (flow?.mode === FLOW_MODE_LENTICULAR) {
runLenticularFlow(flow);
return;
}
if (flow?.mode === FLOW_MODE_LASER) {
runLaserFlow(flow);
return;
}
if (flow?.mode === FLOW_MODE_PREFILLED) {
runPrefilledFlow(flow);
return;
}
const requestDataStr = uni.getStorageSync(GENERATION_REQUEST_KEY);
if (!requestDataStr) {
uni.showToast({ title: '数据错误', icon: 'none' });
setTimeout(() => uni.navigateBack(), 1500);
return;
}
generationData = JSON.parse(requestDataStr);
const castloveDataStr = uni.getStorageSync(CASTLOVE_FORM_KEY);
if (castloveDataStr) {
const castloveData = JSON.parse(castloveDataStr);
if (generationData.subject_reference?.[0]) {
generationData.subject_reference[0].image_file =
castloveData.imageBase64 ||
castloveData.lenticularSubjectBase64 ||
'';
}
}
uni.removeStorageSync(GENERATION_REQUEST_KEY);
callImageGeneration();
} catch (e) {
console.error('[GenerationLoading] mount', e);
uni.showToast({ title: '数据错误', icon: 'none' });
setTimeout(() => uni.navigateBack(), 1500);
}
});
</script>
<style scoped>
.generation-loading {
position: relative;
width: 100%;
height: 100vh;
overflow: hidden;
background: #000;
}
.laser-batch-canvas {
position: fixed;
left: -9999px;
top: -9999px;
opacity: 0;
pointer-events: none;
}
.background-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
.gift-box {
position: absolute;
top: 25%;
left: 50%;
transform: translateX(-50%);
width: 480rpx;
height: 480rpx;
z-index: 2;
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%,
100% {
transform: translateX(-50%) translateY(0) rotate(0deg);
}
50% {
transform: translateX(-50%) translateY(-20rpx) rotate(2deg);
}
}
.gift-image {
width: 100%;
height: 100%;
}
.progress-container {
position: absolute;
bottom: 25%;
left: 50%;
transform: translateX(-50%);
width: 600rpx;
display: flex;
flex-direction: column;
align-items: center;
z-index: 3;
}
.progress-bar-wrapper {
position: relative;
width: 100%;
height: 40rpx;
margin-bottom: 20rpx;
}
.progress-bar {
width: 100%;
height: 40rpx;
background: rgba(255, 255, 255, 0.3);
border-radius: 20rpx;
overflow: hidden;
border: 2rpx solid rgba(255, 255, 255, 0.5);
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #ff6b9d 0%, #ffa8c5 50%, #ffb6d9 100%);
border-radius: 20rpx;
transition: width 0.3s ease;
}
.progress-icon {
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
width: 60rpx;
height: 60rpx;
transition: left 0.3s ease;
}
.icon-circle {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #ffb6d9 0%, #ff6b9d 100%);
border-radius: 50%;
border: 4rpx solid #ffffff;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
.progress-text {
font-size: 56rpx;
font-weight: bold;
color: #ffffff;
margin-bottom: 10rpx;
}
.loading-text {
font-size: 36rpx;
color: #ffffff;
letter-spacing: 6rpx;
font-weight: 500;
}
</style>