topfans/frontend/pages/discover/generation-loading.vue

351 lines
8.2 KiB
Vue
Raw Permalink 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) {
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>