topfans/frontend/pages/castlove/lenticular/lenticular-result.vue

1281 lines
32 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-result">
<!-- 背景图 -->
<image class="background-image" src="/static/background/exhibitionSuccess.png" mode="aspectFill" />
<!-- 星星装饰 -->
<view class="stars-container">
<view v-for="i in 20" :key="i" class="star" :style="getStarStyle(i)"></view>
</view>
<!-- 顶部奖励提示 -->
<!-- <view class="reward-tips">
<view class="reward-item">
<text class="reward-text">恭喜您获得水晶</text>
<image class="reward-icon" src="/static/icon/crystal.png" mode="aspectFit" />
<text class="reward-text">X 32</text>
</view>
<view class="reward-item">
<text class="reward-text">恭喜您获小卡碎片</text>
<image class="reward-icon" src="/static/starcity/xiaoka.png" mode="aspectFit" />
<text class="reward-text">X 2</text>
</view>
</view> -->
<!-- 光栅卡:单张工作台预览(陀螺仪) -->
<view class="lenticular-result-wrap" :class="{ 'cards-visible': isGiftOpened }">
<view class="lenticular-result-card">
<view class="card-wrapper craft-card-wrapper">
<image class="card-frame" src="/static/square/gerenzhongxincangpinkuang.png" mode="aspectFit" />
<view class="craft-lenticular-slot">
<LenticularCard v-if="lenticularLayers.length > 0" class="craft-lenticular-card"
:layers="lenticularLayers" :transforms="layerTransforms" :gyro-source="gyroSourceLabel"
:skip-built-in-touch="true" tilt-hint-text="晃动查看" :shimmer-mid-opacity="0.16"
@simulate="simulate" />
</view>
</view>
</view>
</view>
<!-- 礼盒 -->
<view class="gift-box" :class="{ 'gift-opened': isGiftOpened }">
<image v-if="!isGiftOpened" class="gift-image" src="/static/nft/lihe.png" mode="aspectFit" />
<image v-else class="gift-image gift-image-opened" src="/static/nft/lihe_kaiqi.png" mode="aspectFit" />
</view>
<!-- 底部按钮 -->
<view class="bottom-action" :class="{ 'bottom-action--row': isCraftDetailFlow }">
<view v-if="isCraftDetailFlow" class="action-button action-button--secondary" @tap="handleRegenerate">
<text class="button-text">重新生成</text>
</view>
<view class="action-button" @tap="selectAsset">
<text class="button-text">开始铸造</text>
</view>
</view>
<!-- 铸造确认弹窗 -->
<ConfirmModal v-if="showConfirmModal" :visible="showConfirmModal" :cost-crystal="confirmCostInfo.costCrystal"
:current-balance="confirmCostInfo.currentBalance" :mint-count="confirmCostInfo.mintCount"
:next-tier-cost="confirmCostInfo.nextTierCost" @confirm="handleConfirmMint" @cancel="handleCancelMint" />
</view>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { onShow, onHide } from '@dcloudio/uni-app';
import { getOssSignatureApi, createMintOrderApi, estimateMintCostApi } from '@/utils/api.js';
import { resolveH5OssPostUrl } from '@/utils/h5OssPostUrl.js';
import LenticularCard from '@/components/lenticular/LenticularCard.vue';
import ConfirmModal from '@/components/ConfirmModal.vue';
import { useLenticularCraftTiltPreview } from '@/composables/useLenticularCraftTiltPreview.js';
import {
buildLenticularLayersTwo,
LENTICULAR_STUDIO_STORAGE_KEY,
} from '@/utils/castloveMintForm.js';
import {
CASTLOVE_FORM_KEY,
GENERATED_IMAGES_KEY,
GENERATION_RESULT_META_KEY,
completeSelectionAndOpenDetail,
isDetailAfterSelect,
STUDIO_LENTICULAR,
} from '@/utils/castloveGenerationFlow.js';
import { submitCraftMintFromPath } from '@/utils/craftMintSubmit.js';
const generatedImages = ref([]);
const selectedIndex = ref(-1);
const isUploading = ref(false);
const currentOrderId = ref('');
const isGiftOpened = ref(false);
const craftFormData = ref(null);
const resultMeta = ref('lenticular');
// 确认铸造弹窗
const showConfirmModal = ref(false);
const confirmCostInfo = ref({
costCrystal: 0,
currentBalance: 0,
mintCount: 0,
nextTierCost: 0,
});
const lenticularLayers = ref([]);
const {
layerTransforms,
simulate,
gyroSourceLabel,
scheduleTiltStart,
stopTiltPreview,
} = useLenticularCraftTiltPreview(lenticularLayers);
// 光栅卡详情确认流程
const isCraftDetailFlow = computed(() => isDetailAfterSelect(craftFormData.value));
// 光栅展示模式:结果显示为光栅 或 表单数据为光栅类型
const isLenticularDisplay = computed(
() => resultMeta.value?.displayMode === STUDIO_LENTICULAR || craftFormData.value?.studio_kind === STUDIO_LENTICULAR
);
// const primaryActionLabel = computed(() =>
// isCraftDetailFlow.value ? '选择作品' : '选择您的藏品'
// );
const handleRegenerate = () => {
uni.navigateBack({ delta: 2 });
};
// 更新本地存储的余额
function updateLocalBalance(newBalance) {
try {
const userStr = uni.getStorageSync('user')
if (userStr) {
const user = typeof userStr === 'string' ? JSON.parse(userStr) : { ...userStr }
user.crystal_balance = Number(newBalance) || 0
uni.setStorageSync('user', JSON.stringify(user))
uni.$emit('balanceUpdated', { crystal_balance: user.crystal_balance })
}
} catch (e) {
console.warn('[lenticular-result] 更新本地余额失败:', e)
}
}
// 获取星星样式
const getStarStyle = (index) => {
const left = Math.random() * 100;
const top = Math.random() * 100;
const size = Math.random() * 3 + 1;
const delay = Math.random() * 3;
return {
left: `${left}%`,
top: `${top}%`,
width: `${size}rpx`,
height: `${size}rpx`,
animationDelay: `${delay}s`
};
};
// 将base64转换为Blob
const base64ToBlob = (base64Data) => {
const parts = base64Data.split(';base64,');
const contentType = parts[0].split(':')[1];
const raw = atob(parts[1]);
const rawLength = raw.length;
const uInt8Array = new Uint8Array(rawLength);
for (let i = 0; i < rawLength; i++) {
uInt8Array[i] = raw.charCodeAt(i);
}
return new Blob([uInt8Array], { type: contentType });
};
// 上传图片到OSS
const uploadImageToOss = async (base64Image) => {
console.log('[GenerationResult] uploadImageToOss 开始');
isUploading.value = true;
try {
uni.showLoading({ title: '上传中...', mask: true });
// 1. 获取OSS签名
const signRes = await getOssSignatureApi('asset');
console.log('[GenerationResult] 获取签名结果:', signRes);
if (signRes.code !== 200) {
throw new Error(signRes.message || '获取签名失败');
}
// 保存order_id
currentOrderId.value = signRes.data.order_id || '';
console.log('[GenerationResult] order_id:', currentOrderId.value);
// 2. 生成文件名
const timestamp = Date.now();
const randomStr = Math.random().toString(36).substring(2, 8);
const fileName = `ai_generated_${timestamp}_${randomStr}.png`;
let imageUrl;
// #ifdef H5
// H5环境使用FormData + Blob
console.log('[GenerationResult] H5环境使用Blob上传');
const blob = base64ToBlob(base64Image);
console.log('[GenerationResult] Blob创建成功大小:', blob.size);
const formData = new FormData();
formData.append('key', signRes.data.dir + fileName);
formData.append('policy', signRes.data.policy);
formData.append('success_action_status', '200');
formData.append('x-oss-credential', signRes.data.x_oss_credential);
formData.append('x-oss-date', signRes.data.x_oss_date);
formData.append('x-oss-security-token', signRes.data.security_token);
formData.append('x-oss-signature', signRes.data.signature);
formData.append('x-oss-signature-version', signRes.data.x_oss_signature_version);
formData.append('file', blob, fileName);
const response = await fetch(signRes.data.host, {
method: 'POST',
body: formData
});
console.log('[GenerationResult] OSS响应状态:', response.status);
if (response.ok || response.status === 204) {
imageUrl = `${signRes.data.host}/${signRes.data.dir}${fileName}`;
} else {
throw new Error(`上传失败,状态码: ${response.status}`);
}
// #endif
// #ifndef H5
// App/小程序环境使用uni.uploadFile
console.log('[GenerationResult] 非H5环境使用uni.uploadFile');
let tempFilePath; // 声明临时文件路径变量
// #ifdef APP-PLUS
// App环境直接使用base64上传通过临时文件
console.log('[GenerationResult] App环境使用base64临时文件');
// 使用uni.saveFile保存base64为临时文件
const base64Data = base64Image.replace(/^data:image\/\w+;base64,/, '');
// 先转换为临时文件路径
tempFilePath = await new Promise((resolve, reject) => {
// 使用uni的base64ToTempFilePath如果可用
if (uni.base64ToTempFilePath) {
uni.base64ToTempFilePath({
base64Data: base64Image,
success: (res) => {
console.log('[GenerationResult] base64转临时文件成功:', res.tempFilePath);
resolve(res.tempFilePath);
},
fail: (error) => {
console.error('[GenerationResult] base64转临时文件失败:', error);
reject(new Error('base64转临时文件失败'));
}
});
} else {
// 降级方案使用plus.io
console.log('[GenerationResult] 使用plus.io方案');
const bitmap = new plus.nativeObj.Bitmap('temp');
bitmap.loadBase64Data(base64Image, () => {
const tempPath = '_doc/' + fileName;
bitmap.save(tempPath, {}, () => {
console.log('[GenerationResult] 图片保存成功:', tempPath);
bitmap.clear();
resolve(tempPath);
}, (error) => {
console.error('[GenerationResult] 图片保存失败:', error);
bitmap.clear();
reject(new Error('图片保存失败'));
});
}, (error) => {
console.error('[GenerationResult] 加载base64失败:', error);
bitmap.clear();
reject(new Error('加载base64失败'));
});
}
});
// #endif
// #ifdef MP-WEIXIN || MP-ALIPAY || MP-BAIDU || MP-TOUTIAO || MP-QQ
// 小程序环境使用FileSystemManager
console.log('[GenerationResult] 小程序环境');
const base64Data = base64Image.replace(/^data:image\/\w+;base64,/, '');
// #ifdef MP-WEIXIN
tempFilePath = `${wx.env.USER_DATA_PATH}/${fileName}`;
// #endif
// #ifdef MP-ALIPAY || MP-BAIDU || MP-TOUTIAO || MP-QQ
tempFilePath = `${uni.env.USER_DATA_PATH}/${fileName}`;
// #endif
console.log('[GenerationResult] 临时文件路径:', tempFilePath);
const fs = uni.getFileSystemManager();
await new Promise((resolve, reject) => {
fs.writeFile({
filePath: tempFilePath,
data: base64Data,
encoding: 'base64',
success: () => {
console.log('[GenerationResult] 文件写入成功');
resolve();
},
fail: (error) => {
console.error('[GenerationResult] 文件写入失败:', error);
reject(new Error('保存临时文件失败'));
}
});
});
// #endif
console.log('[GenerationResult] 开始上传到OSS文件路径:', tempFilePath);
// 上传到OSS
imageUrl = await new Promise((resolve, reject) => {
uni.uploadFile({
url: signRes.data.host,
filePath: tempFilePath,
name: 'file',
formData: {
key: signRes.data.dir + fileName,
policy: signRes.data.policy,
success_action_status: '200',
'x-oss-credential': signRes.data.x_oss_credential,
'x-oss-date': signRes.data.x_oss_date,
'x-oss-security-token': signRes.data.security_token,
'x-oss-signature': signRes.data.signature,
'x-oss-signature-version': signRes.data.x_oss_signature_version
},
success: (uploadRes) => {
console.log('[GenerationResult] OSS上传响应:', uploadRes.statusCode);
console.log('[GenerationResult] OSS上传完整响应:', uploadRes);
if (uploadRes.statusCode === 200 || uploadRes.statusCode === 204) {
const url = `${signRes.data.host}/${signRes.data.dir}${fileName}`;
console.log('[GenerationResult] 上传成功URL:', url);
resolve(url);
} else {
reject(new Error(`上传失败,状态码: ${uploadRes.statusCode}`));
}
},
fail: (error) => {
console.error('[GenerationResult] OSS上传失败:', error);
reject(error);
}
});
});
// #endif
console.log('[GenerationResult] 上传成功URL:', imageUrl);
uni.hideLoading();
uni.showToast({
title: '上传成功',
icon: 'success',
duration: 1500
});
isUploading.value = false;
return imageUrl;
} catch (error) {
console.error('[GenerationResult] 上传失败:', error);
uni.hideLoading();
uni.showToast({
title: error.message || '上传失败',
icon: 'none',
duration: 2000
});
isUploading.value = false;
throw error;
}
};
// H5环境上传
const uploadToOssH5 = async (base64Image, fileName, ossData) => {
return new Promise((resolve, reject) => {
try {
// 将base64转换为Blob
const blob = base64ToBlob(base64Image);
// 构建FormData
const formData = new FormData();
formData.append('key', ossData.dir + fileName);
formData.append('policy', ossData.policy);
formData.append('success_action_status', '200');
formData.append('x-oss-credential', ossData.x_oss_credential);
formData.append('x-oss-date', ossData.x_oss_date);
formData.append('x-oss-security-token', ossData.security_token);
formData.append('x-oss-signature', ossData.signature);
formData.append('x-oss-signature-version', ossData.x_oss_signature_version);
formData.append('file', blob, fileName);
// 使用fetch上传
fetch(resolveH5OssPostUrl(ossData.host), {
method: 'POST',
body: formData
})
.then(response => {
if (response.ok) {
const imageUrl = `${ossData.host}/${ossData.dir}${fileName}`;
uni.hideLoading();
uni.showToast({
title: '上传成功',
icon: 'success',
duration: 1500
});
isUploading.value = false;
resolve(imageUrl); // 只返回URL
} else {
throw new Error(`上传失败,状态码: ${response.status}`);
}
})
.catch(error => {
uni.hideLoading();
isUploading.value = false;
reject(error);
});
} catch (error) {
uni.hideLoading();
isUploading.value = false;
reject(error);
}
});
};
// App/小程序环境上传
const uploadToOssNative = async (base64Image, fileName, ossData) => {
console.log('[GenerationResult] uploadToOssNative 开始');
console.log('[GenerationResult] fileName:', fileName);
return new Promise((resolve, reject) => {
try {
// 去掉base64前缀
const base64Data = base64Image.replace(/^data:image\/\w+;base64,/, '');
console.log('[GenerationResult] base64数据长度:', base64Data.length);
// #ifdef MP-WEIXIN
// 微信小程序环境
console.log('[GenerationResult] 使用微信小程序环境');
const fs = uni.getFileSystemManager();
const tempFilePath = `${wx.env.USER_DATA_PATH}/${fileName}`;
console.log('[GenerationResult] 临时文件路径:', tempFilePath);
fs.writeFile({
filePath: tempFilePath,
data: base64Data,
encoding: 'base64',
success: () => {
console.log('[GenerationResult] 文件写入成功');
uploadFileToOss(tempFilePath, fileName, ossData, resolve, reject, fs);
},
fail: (error) => {
console.error('[GenerationResult] 保存临时文件失败:', error);
uni.hideLoading();
isUploading.value = false;
reject(new Error('保存临时文件失败'));
}
});
// #endif
// #ifdef APP-PLUS
// App环境
console.log('[GenerationResult] 使用App环境');
const fs = plus.io.getFileSystemManager ? uni.getFileSystemManager() : null;
console.log('[GenerationResult] FileSystemManager:', fs ? '可用' : '不可用');
if (!fs) {
// 使用plus.io API
console.log('[GenerationResult] 使用plus.io API');
const tempFilePath = `_doc/${fileName}`;
console.log('[GenerationResult] 临时文件路径:', tempFilePath);
plus.io.resolveLocalFileSystemURL('_doc', (entry) => {
console.log('[GenerationResult] 获取文件系统成功');
entry.getFile(fileName, { create: true }, (fileEntry) => {
console.log('[GenerationResult] 创建文件成功');
fileEntry.createWriter((writer) => {
writer.onwrite = () => {
console.log('[GenerationResult] 文件写入成功');
uploadFileToOss(fileEntry.toLocalURL(), fileName, ossData, resolve, reject, null);
};
writer.onerror = (error) => {
console.error('[GenerationResult] 写入文件失败:', error);
uni.hideLoading();
isUploading.value = false;
reject(new Error('写入文件失败'));
};
// 将base64转换为Blob
console.log('[GenerationResult] 开始转换base64为Blob');
try {
const blob = base64ToBlob(base64Image);
console.log('[GenerationResult] Blob创建成功大小:', blob.size);
writer.write(blob);
} catch (blobError) {
console.error('[GenerationResult] Blob转换失败:', blobError);
uni.hideLoading();
isUploading.value = false;
reject(new Error('Blob转换失败'));
}
});
}, (fileError) => {
console.error('[GenerationResult] 创建文件失败:', fileError);
uni.hideLoading();
isUploading.value = false;
reject(new Error('创建文件失败'));
});
}, (error) => {
console.error('[GenerationResult] 获取文件系统失败:', error);
uni.hideLoading();
isUploading.value = false;
reject(new Error('获取文件系统失败'));
});
} else {
// 使用FileSystemManager API
console.log('[GenerationResult] 使用FileSystemManager API');
const tempFilePath = `${plus.io.getStorageRootPath()}${fileName}`;
console.log('[GenerationResult] 临时文件路径:', tempFilePath);
fs.writeFile({
filePath: tempFilePath,
data: base64Data,
encoding: 'base64',
success: () => {
console.log('[GenerationResult] 文件写入成功');
uploadFileToOss(tempFilePath, fileName, ossData, resolve, reject, fs);
},
fail: (error) => {
console.error('[GenerationResult] 保存临时文件失败:', error);
uni.hideLoading();
isUploading.value = false;
reject(new Error('保存临时文件失败'));
}
});
}
// #endif
} catch (error) {
console.error('[GenerationResult] uploadToOssNative异常:', error);
uni.hideLoading();
isUploading.value = false;
reject(error);
}
});
};
// 统一的文件上传逻辑
const uploadFileToOss = (tempFilePath, fileName, ossData, resolve, reject, fs) => {
console.log('[GenerationResult] uploadFileToOss 开始');
console.log('[GenerationResult] tempFilePath:', tempFilePath);
console.log('[GenerationResult] OSS host:', ossData.host);
uni.uploadFile({
url: ossData.host,
filePath: tempFilePath,
name: 'file',
formData: {
key: ossData.dir + fileName,
policy: ossData.policy,
success_action_status: '200',
'x-oss-credential': ossData.x_oss_credential,
'x-oss-date': ossData.x_oss_date,
'x-oss-security-token': ossData.security_token,
'x-oss-signature': ossData.signature,
'x-oss-signature-version': ossData.x_oss_signature_version
},
success: (uploadRes) => {
console.log('[GenerationResult] OSS上传响应:', uploadRes);
console.log('[GenerationResult] 状态码:', uploadRes.statusCode);
if (uploadRes.statusCode === 200 || uploadRes.statusCode === 204) {
const imageUrl = `${ossData.host}/${ossData.dir}${fileName}`;
console.log('[GenerationResult] 上传成功图片URL:', imageUrl);
// 删除临时文件
if (fs) {
fs.unlink({
filePath: tempFilePath,
success: () => console.log('[GenerationResult] 临时文件已删除'),
fail: (err) => console.error('[GenerationResult] 删除临时文件失败:', err)
});
}
resolve(imageUrl);
} else {
console.error('[GenerationResult] 上传失败,状态码:', uploadRes.statusCode);
reject(new Error(`上传失败,状态码: ${uploadRes.statusCode}`));
}
},
fail: (error) => {
console.error('[GenerationResult] OSS上传失败:', error);
reject(error);
}
});
};
const getSelectedImageUrl = () => {
if (isLenticularDisplay.value) {
const mid = lenticularLayers.value.find((l) => l.id === 'mid');
return mid?.src || craftFormData.value?.lenticularSubjectImage || '';
}
if (selectedIndex.value < 0) return '';
return generatedImages.value[selectedIndex.value] || '';
};
// 选择藏品
const selectAsset = async () => {
const imagePath =
isLenticularDisplay.value
? lenticularLayers.value.find((l) => l.id === 'mid')?.src || ''
: '';
const bgImagePath = isLenticularDisplay.value
? lenticularLayers.value.find((l) => l.id === 'base')?.src || ''
: undefined;
console.log('[selectAsset] start', {
isLenticularDisplay: isLenticularDisplay.value,
imagePath: imagePath ? 'has' : 'empty',
bgImagePath: bgImagePath ? 'has' : 'empty',
lenticularLayers: lenticularLayers.value.map(l => ({ id: l.id, src: l.src ? l.src.substring(0, 50) : '' })),
})
if (!imagePath) {
uni.showToast({ title: '缺少作品图', icon: 'none' });
return;
}
// 获取铸造费用估算
try {
uni.showLoading({ title: '加载中…', mask: true });
const costRes = await estimateMintCostApi();
uni.hideLoading();
if (costRes.code === 200 && costRes.data) {
confirmCostInfo.value = {
costCrystal: costRes.data.cost_crystal || 0,
currentBalance: costRes.data.current_balance || 0,
mintCount: costRes.data.mint_count || 0,
nextTierCost: costRes.data.next_tier_cost || '',
};
} else {
confirmCostInfo.value = {
costCrystal: 100,
currentBalance: 0,
mintCount: 0,
nextTierCost: 0,
};
}
} catch (e) {
uni.hideLoading();
console.error('[selectAsset] 获取铸造费用失败:', e);
confirmCostInfo.value = {
costCrystal: 100,
currentBalance: 0,
mintCount: 0,
nextTierCost: 0,
};
}
// 显示确认弹窗
showConfirmModal.value = true;
};
// 确认铸造
const handleConfirmMint = async () => {
showConfirmModal.value = false;
// 检查余额是否充足
if (confirmCostInfo.value.currentBalance < confirmCostInfo.value.costCrystal) {
uni.showToast({ title: '水晶余额不足,无法铸造', icon: 'none' });
return;
}
const imagePath =
isLenticularDisplay.value
? lenticularLayers.value.find((l) => l.id === 'mid')?.src || ''
: '';
const bgImagePath = isLenticularDisplay.value
? lenticularLayers.value.find((l) => l.id === 'base')?.src || ''
: undefined;
uni.showLoading({ title: '铸造中…', mask: true });
try {
await submitCraftMintFromPath({ imagePath, bgImagePath, formData: craftFormData.value });
// 只有铸造成功后才扣除本地余额
updateLocalBalance(confirmCostInfo.value.balanceAfter);
uni.hideLoading();
uni.navigateTo({ url: '/pages/castlove/success' });
} catch (e) {
uni.hideLoading();
uni.showToast({ title: e.message || '铸造失败', icon: 'none' });
}
};
// 取消铸造
const handleCancelMint = () => {
showConfirmModal.value = false;
};
const initLenticularPreview = () => {
try {
const raw = uni.getStorageSync(LENTICULAR_STUDIO_STORAGE_KEY);
if (!raw) {
return;
}
const p = typeof raw === 'string' ? JSON.parse(raw) : raw;
const bgPath = p.bgPath || '';
const subjectPath = p.subjectPath || '';
lenticularLayers.value = buildLenticularLayersTwo(bgPath, subjectPath);
selectedIndex.value = 0;
scheduleTiltStart();
} catch (e) {
console.error('[GenerationResult] lenticular init', e);
}
};
onMounted(() => {
try {
const formDataStr = uni.getStorageSync(CASTLOVE_FORM_KEY);
if (formDataStr) {
craftFormData.value = JSON.parse(formDataStr);
}
const metaStr = uni.getStorageSync(GENERATION_RESULT_META_KEY);
if (metaStr) {
resultMeta.value = JSON.parse(metaStr);
uni.removeStorageSync(GENERATION_RESULT_META_KEY);
}
} catch (e) {
console.error('[GenerationResult] 读取表单失败:', e);
}
try {
const imagesData = uni.getStorageSync(GENERATED_IMAGES_KEY);
if (!imagesData) {
uni.showToast({ title: '未找到生成的图片', icon: 'none' });
setTimeout(() => uni.navigateTo({
url: '/pages/castlove/lenticular/lenticular-create'
}), 1500);
return;
}
const parsed = JSON.parse(imagesData);
uni.removeStorageSync(GENERATED_IMAGES_KEY);
if (isLenticularDisplay.value || (parsed[0] && parsed[0].type === 'lenticular')) {
initLenticularPreview();
} else {
generatedImages.value = parsed;
if (generatedImages.value.length > 0) {
selectedIndex.value = 0;
}
}
setTimeout(() => {
isGiftOpened.value = true;
}, 800);
} catch (e) {
console.error('[GenerationResult] 读取图片数据失败:', e);
uni.showToast({ title: '数据错误', icon: 'none' });
setTimeout(() => uni.navigateTo({
url: '/pages/castlove/lenticular/lenticular-create'
}), 1500);
}
});
onShow(() => {
if (isLenticularDisplay.value && lenticularLayers.value.length) {
scheduleTiltStart();
}
});
onHide(() => {
stopTiltPreview();
});
onUnmounted(() => {
stopTiltPreview();
});
</script>
<style scoped>
.generation-result {
position: relative;
width: 100%;
height: 100vh;
overflow: hidden;
background: #000;
}
.background-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
.stars-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 2;
pointer-events: none;
}
.star {
position: absolute;
background: #FFFFFF;
border-radius: 50%;
animation: starTwinkle 2s ease-in-out infinite;
box-shadow: 0 0 10rpx rgba(255, 255, 255, 0.8);
}
@keyframes starTwinkle {
0%,
100% {
opacity: 0.3;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.2);
}
}
.reward-tips {
position: absolute;
top: 80rpx;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 16rpx;
z-index: 11;
padding: 0 40rpx;
}
.reward-item {
background: transparent;
white-space: nowrap;
display: flex;
align-items: center;
gap: 12rpx;
}
.reward-icon {
width: 40rpx;
height: 40rpx;
flex-shrink: 0;
}
.reward-text {
font-size: 28rpx;
color: #FFFFFF;
font-weight: 600;
text-shadow:
0 2rpx 8rpx rgba(0, 0, 0, 0.6),
0 0 20rpx rgba(255, 107, 157, 0.5);
white-space: nowrap;
}
.cards-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10;
opacity: 0;
pointer-events: none;
transition: opacity 0.5s ease-in-out;
}
.cards-container.cards-visible {
opacity: 1;
pointer-events: auto;
}
.card-item {
position: absolute;
width: 200rpx;
height: 260rpx;
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
filter: drop-shadow(0 15rpx 40rpx rgba(0, 0, 0, 0.5));
transform: scale(0) translateY(100rpx);
opacity: 0;
}
.cards-visible .card-item {
animation: cardAppear 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
opacity: 1;
}
@keyframes cardAppear {
0% {
transform: scale(0) translateY(100rpx);
opacity: 0;
}
100% {
transform: scale(1) translateY(0);
opacity: 1;
}
}
.card-item:active:not(.card-selected) {
opacity: 0.8;
}
.cards-visible .card-item:nth-child(1) {
animation-delay: 0s, 0.8s;
}
.cards-visible .card-item:nth-child(2) {
animation-delay: 0.15s, 0.95s;
}
.cards-visible .card-item:nth-child(3) {
animation-delay: 0.3s, 1.1s;
}
.cards-visible .card-item:nth-child(4) {
animation-delay: 0.45s, 1.25s;
}
.cards-visible .card-item:nth-child(5) {
animation-delay: 0.6s, 1.4s;
}
@keyframes cardFloat {
0%,
100% {
transform: translateY(0) rotate(var(--rotate, 0deg));
}
50% {
transform: translateY(-20rpx) rotate(var(--rotate, 0deg));
}
}
.card-frame {
position: relative;
width: 100%;
height: 100%;
overflow: visible;
}
.topfans-label {
position: absolute;
top: 15rpx;
left: 15rpx;
background: linear-gradient(135deg, #A855FF 0%, #D946EF 100%);
padding: 8rpx 20rpx;
border-radius: 30rpx;
z-index: 10;
box-shadow: 0 4rpx 15rpx rgba(168, 85, 255, 0.5);
}
.topfans-text {
font-size: 22rpx;
font-weight: bold;
color: #FFFFFF;
letter-spacing: 1rpx;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.3);
}
.card-image {
width: 100%;
height: 100%;
border-radius: 14rpx;
display: block;
position: relative;
z-index: 1;
object-fit: cover;
}
.card-shine {
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg,
transparent 0%,
rgba(255, 255, 255, 0.5) 50%,
transparent 100%);
animation: shine 3s ease-in-out infinite;
}
@keyframes shine {
0% {
left: -100%;
}
50%,
100% {
left: 100%;
}
}
.topfans-badge {
position: absolute;
top: 20rpx;
left: 20rpx;
width: 80rpx;
height: 80rpx;
filter: drop-shadow(0 2rpx 8rpx rgba(255, 107, 157, 0.5));
}
.card-label {
position: absolute;
top: 24rpx;
right: 20rpx;
padding: 8rpx 16rpx;
background: rgba(255, 107, 157, 0.9);
border-radius: 20rpx;
}
.label-text {
font-size: 20rpx;
color: #FFFFFF;
font-weight: bold;
}
.card-title {
position: absolute;
bottom: 20rpx;
left: 20rpx;
right: 20rpx;
padding: 12rpx;
background: rgba(0, 0, 0, 0.6);
border-radius: 12rpx;
backdrop-filter: blur(10rpx);
}
.title-text {
font-size: 22rpx;
color: #FFFFFF;
font-weight: 600;
text-align: center;
display: block;
}
.heart-icon {
position: absolute;
top: 20rpx;
right: 20rpx;
width: 40rpx;
height: 40rpx;
}
.selected-mark {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100rpx;
height: 100rpx;
background: linear-gradient(135deg, #00D4FF 0%, #0099FF 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow:
0 0 30rpx rgba(0, 212, 255, 0.8),
0 8rpx 30rpx rgba(0, 0, 0, 0.4);
animation: checkPop 0.3s ease-out;
border: 4rpx solid rgba(255, 255, 255, 0.9);
z-index: 10;
}
@keyframes checkPop {
0% {
transform: translate(-50%, -50%) scale(0);
}
50% {
transform: translate(-50%, -50%) scale(1.2);
}
100% {
transform: translate(-50%, -50%) scale(1);
}
}
.check-icon {
font-size: 70rpx;
color: #FFFFFF;
font-weight: bold;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
}
.card-selected {
z-index: 100 !important;
}
.gift-box {
position: absolute;
bottom: 20%;
left: 50%;
transform: translateX(-50%);
width: 480rpx;
height: 480rpx;
z-index: 3;
animation: giftFloat 3s ease-in-out infinite;
transition: all 0.8s cubic-bezier(0.4, 0, 0.2, 1);
}
.gift-box.gift-opened {
width: 17rem;
bottom: 15%;
animation: none;
}
@keyframes giftFloat {
0%,
100% {
transform: translateX(-50%) translateY(0);
}
50% {
transform: translateX(-50%) translateY(-20rpx);
}
}
.gift-image {
width: 100%;
height: 100%;
transition: opacity 0.5s ease-in-out;
}
.gift-image-opened {
width: 17rem;
animation: giftOpenPop 0.5s ease-out;
}
@keyframes giftOpenPop {
0% {
transform: scale(0.8);
opacity: 0;
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
opacity: 1;
}
}
.lenticular-result-wrap {
position: absolute;
top: 200rpx;
left: 0;
right: 0;
display: flex;
justify-content: center;
z-index: 8;
opacity: 0;
transform: translateY(40rpx);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.lenticular-result-wrap.cards-visible {
opacity: 1;
transform: translateY(0);
}
.lenticular-result-card {
display: flex;
flex-direction: column;
align-items: center;
}
.lenticular-result-card .card-wrapper {
position: relative;
width: 352rpx;
height: 520rpx;
}
.lenticular-result-card .craft-card-wrapper {
margin-bottom: 32rpx;
}
.lenticular-result-card .card-frame {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 2;
transform: rotate(-10deg);
}
.lenticular-result-card .craft-lenticular-slot {
position: absolute;
left: 50%;
top: 50%;
width: 87%;
height: 96%;
border-radius: 48rpx;
transform: translate(-50%, -50%) rotate(-10deg);
z-index: 2;
overflow: hidden;
}
.lenticular-result-card .craft-lenticular-card {
width: 100%;
height: 100%;
}
.bottom-action {
position: absolute;
bottom: 80rpx;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
z-index: 11;
}
.bottom-action--row {
flex-direction: row;
gap: 24rpx;
width: 90%;
justify-content: center;
}
.action-button {
padding: 24rpx 80rpx;
background: linear-gradient(90deg, #FF6B9D 0%, #FFA8C5 100%);
border-radius: 50rpx;
box-shadow: 0 8rpx 24rpx rgba(255, 107, 157, 0.5);
cursor: pointer;
user-select: none;
-webkit-user-select: none;
-webkit-tap-highlight-color: transparent;
display: flex;
align-items: center;
justify-content: center;
}
.action-button:active {
transform: scale(1.05);
opacity: 0.9;
}
.action-button--secondary {
background: linear-gradient(90deg, #8e9eab 0%, #b8c6db 100%);
box-shadow: 0 8rpx 24rpx rgba(120, 130, 150, 0.45);
}
.button-text {
font-size: 32rpx;
color: #FFFFFF;
font-weight: bold;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2);
white-space: nowrap;
line-height: 1.2;
}
.hint-text {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.7);
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.3);
}
</style>