topfans/frontend/pages/discover/generation-result.vue
2026-04-24 18:04:55 +08:00

1181 lines
30 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="cards-container" :class="{ 'cards-visible': isGiftOpened }">
<view
v-for="(image, index) in generatedImages"
:key="index"
class="card-item"
:class="{ 'card-selected': selectedIndex === index }"
:style="getCardStyle(index)"
@click="selectCard(index)"
>
<view class="card-frame">
<!-- TOPFANS Logo标签 -->
<!-- <view class="topfans-label">
<text class="topfans-text">TOPFANS</text>
</view> -->
<image
class="card-image"
:src="image"
mode="aspectFill"
/>
</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">
<view class="action-button" @click="selectAsset" @tap="selectAsset">
<text class="button-text">选择您的藏品</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { getOssSignatureApi, createMintOrderApi } from '@/utils/api.js';
const generatedImages = ref([]);
const selectedIndex = ref(-1);
const isUploading = ref(false);
const currentOrderId = ref(''); // 保存当前订单ID
const isGiftOpened = ref(false); // 礼盒是否已打开
// 获取星星样式
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`
};
};
// 获取卡片样式 - 金字塔布局5张图
const getCardStyle = (index) => {
// 金字塔布局:
// 顶部1张大卡片居中往下移- z-index: 30
// 中间2张中等卡片居中对称- z-index: 20
// 底部2张小卡片左右对称边距更小- z-index: 10
// 使用rpx单位屏幕宽度750rpx卡片宽度200rpx
// 定义5个位置对应图片0,1,2,3,4的初始位置
const allPositions = [
// 位置0图片0底部左
{ left: 40, top: 624, rotate: '-5deg', scale: 0.72, zIndex: 10 },
// 位置1图片1中间左
{ left: 130, top: 580, rotate: '-8deg', scale: 0.82, zIndex: 20 },
// 位置2图片2顶部中心
{ left: 275, top: 500, rotate: '0deg', scale: 1.15, zIndex: 30 },
// 位置3图片3中间右
{ left: 420, top: 580, rotate: '8deg', scale: 0.82, zIndex: 20 },
// 位置4图片4底部右
{ left: 510, top: 624, rotate: '5deg', scale: 0.72, zIndex: 10 }
];
// 计算当前图片应该在哪个位置
let posIndex;
if (selectedIndex.value === -1) {
// 没有选中图片index对应位置index
posIndex = index;
} else {
// 有选中的卡片,重新计算位置
if (index === selectedIndex.value) {
// 选中的卡片移到位置2顶部中心
posIndex = 2;
} else {
// 其他卡片按顺序填充到 0,1,3,4 位置
// 计算相对位置
const relativePos = (index - selectedIndex.value + 5) % 5;
// 映射到实际位置跳过位置2
// relativePos: 1,2,3,4 对应位置: 3,4,0,1
if (relativePos === 1) {
posIndex = 3; // 下一张 → 中间右
} else if (relativePos === 2) {
posIndex = 4; // 下下张 → 底部右
} else if (relativePos === 3) {
posIndex = 0; // 下下下张 → 底部左
} else if (relativePos === 4) {
posIndex = 1; // 上一张 → 中间左
} else {
// relativePos === 0这是选中的卡片不应该到这里
console.error('逻辑错误relativePos为0但不是选中的卡片');
posIndex = 2;
}
}
}
const pos = allPositions[posIndex];
// 如果是选中的卡片放大1.5倍并设置最高层级
if (selectedIndex.value === index) {
return {
left: `${pos.left}rpx`,
top: `${pos.top}rpx`,
transform: `scale(${pos.scale * 1.15})!important`,
filter: 'brightness(1.15) drop-shadow(0 0 40rpx rgba(100, 200, 255, 0.9))',
zIndex: 100 // 选中时层级最高
};
}
return {
left: `${pos.left}rpx`,
top: `${pos.top}rpx`,
transform: `scale(${pos.scale})`,
zIndex: pos.zIndex
};
};
// 选择卡片
const selectCard = (index) => {
selectedIndex.value = index;
// 可以添加选中效果
// uni.showToast({
// title: `已选择第${index + 1}张卡片`,
// icon: 'none',
// duration: 1000
// });
};
// 将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(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 selectAsset = async () => {
if (selectedIndex.value === -1) {
uni.showToast({
title: '请先选择一张卡片',
icon: 'none'
});
return;
}
if (isUploading.value) {
uni.showToast({
title: '上传中,请稍候',
icon: 'none'
});
return;
}
try {
// 获取选中的图片base64格式
const selectedImage = generatedImages.value[selectedIndex.value];
// 上传到OSS会自动设置currentOrderId
const imageUrl = await uploadImageToOss(selectedImage);
// 创建铸造订单
uni.showLoading({ title: '创建订单中...', mask: true });
// 从storage获取表单数据
const formDataStr = uni.getStorageSync('castlove_form_data');
const orderValue = formDataStr ? JSON.parse(formDataStr) : {};
// 构建订单数据
const orderData = {
name: orderValue.name,
event: orderValue.event,
description: orderValue.description,
material_type: orderValue.material_type,
material_url: imageUrl,
rarity: 0,
tags: [],
order_id: currentOrderId.value,
info: orderValue.info || '' // 添加info字段
};
// 调用创建铸造订单API
const response = await createMintOrderApi(orderData);
uni.hideLoading();
if (response.code !== 200) {
throw new Error(response.message || '创建订单失败');
}
uni.removeStorageSync('castlove_form_data');
// 构建藏品数据存储到temp_nft_data
const nftData = {
image: imageUrl, // 使用OSS URL
name: orderValue.name,
event: orderValue.event,
description: orderValue.description,
material_type: orderValue.material_type,
order_id: currentOrderId.value // 使用ref保存的order_id
};
// 存储到storage
uni.setStorageSync('temp_nft_data', JSON.stringify(nftData));
// 跳转到成功页面
uni.navigateTo({
url: '/pages/castlove/success'
});
} catch (error) {
console.error('[GenerationResult] 选择藏品失败:', error);
uni.showToast({
title: error.message || '操作失败,请重试',
icon: 'none'
});
}
};
onMounted(() => {
// 从存储中获取生成的图片
try {
const imagesData = uni.getStorageSync('generated_images');
if (imagesData) {
generatedImages.value = JSON.parse(imagesData);
console.log('[GenerationResult] 接收到生成的图片:', generatedImages.value.length);
// 清除存储
uni.removeStorageSync('generated_images');
// 延迟打开礼盒动画
setTimeout(() => {
isGiftOpened.value = true;
}, 800);
} else {
uni.showToast({
title: '未找到生成的图片',
icon: 'none'
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
}
} catch (e) {
console.error('[GenerationResult] 读取图片数据失败:', e);
uni.showToast({
title: '数据错误',
icon: 'none'
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
}
});
</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%;
background: linear-gradient(135deg,
rgba(180, 220, 255, 0.95) 0%,
rgba(200, 230, 255, 0.95) 50%,
rgba(220, 240, 255, 0.95) 100%);
border-radius: 20rpx;
padding: 10rpx;
box-shadow:
0 0 40rpx rgba(100, 200, 255, 0.5),
0 20rpx 60rpx rgba(0, 0, 0, 0.4),
inset 0 2rpx 10rpx rgba(255, 255, 255, 0.6);
overflow: visible;
border: 5rpx solid rgba(255, 255, 255, 0.9);
}
.card-frame::before {
content: '';
position: absolute;
top: -8rpx;
left: -8rpx;
right: -8rpx;
bottom: -8rpx;
background: linear-gradient(135deg,
rgba(100, 200, 255, 0.6) 0%,
rgba(150, 220, 255, 0.6) 50%,
rgba(200, 240, 255, 0.6) 100%);
border-radius: 22rpx;
z-index: -1;
filter: blur(10rpx);
}
.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;
}
}
.bottom-action {
position: absolute;
bottom: 80rpx;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
/* gap: 16rpx; */
z-index: 11;
}
.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.2);
opacity: 0.9;
}
.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>