362 lines
8.4 KiB
Vue
362 lines
8.4 KiB
Vue
<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>
|