topfans/frontend/pages/discover/generation-result.vue
2026-04-07 23:08:49 +08:00

1291 lines
31 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">
<image
class="card-image"
:src="image"
mode="aspectFill"
/>
<!-- 选中标记 -->
<view v-if="selectedIndex === index" class="selected-mark">
<text class="check-icon">✓</text>
</view>
</view>
</view>
</view>
<!-- 礼盒 -->
<view class="gift-box" :class="{ 'gift-opened': isGiftOpened }">
<view class="gift-container">
<view class="gift-lid">
<view class="gift-bow"></view>
<view class="gift-lid-top">
<text class="gift-text">TOPFANS</text>
</view>
</view>
<view class="gift-body">
<view class="gift-window">
<text class="question-mark">?</text>
</view>
</view>
</view>
</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`
};
};
// 获取卡片样式
const getCardStyle = (index) => {
const positions = [
{ left: '8%', top: '18%', rotate: '-12deg', scale: 1 },
{ left: '52%', top: '15%', rotate: '8deg', scale: 0.92 },
{ left: '5%', top: '42%', rotate: '-6deg', scale: 0.95 },
{ left: '50%', top: '45%', rotate: '10deg', scale: 0.93 }
];
const pos = positions[index] || positions[0];
// 如果是选中的卡片,添加高亮效果
if (selectedIndex.value === index) {
return {
left: pos.left,
top: pos.top,
transform: `rotate(${pos.rotate}) scale(${pos.scale * 1.05})`,
filter: 'brightness(1.2)',
zIndex: 100
};
}
return {
left: pos.left,
top: pos.top,
transform: `rotate(${pos.rotate}) scale(${pos.scale})`
};
};
// 选择卡片
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
};
// 调用创建铸造订单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.redirectTo({
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: 280rpx;
height: 348rpx;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
filter: drop-shadow(0 10rpx 30rpx rgba(0, 0, 0, 0.4));
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,
cardFloat 4s ease-in-out infinite 0.8s;
opacity: 1;
}
@keyframes cardAppear {
0% {
transform: scale(0) translateY(100rpx);
opacity: 0;
}
100% {
transform: scale(1) translateY(0);
opacity: 1;
}
}
.card-item:active {
transform: scale(0.95) !important;
}
.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;
}
@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,
#FFE5F0 0%,
#E8D4FF 25%,
#D4E8FF 50%,
#E8FFE5 75%,
#FFE5F0 100%);
background-size: 200% 200%;
animation: holographicShift 3s ease-in-out infinite;
border-radius: 20rpx;
padding: 12rpx;
box-shadow:
0 0 30rpx rgba(255, 107, 157, 0.4),
0 0 60rpx rgba(168, 107, 255, 0.3),
inset 0 0 20rpx rgba(255, 255, 255, 0.3);
overflow: hidden;
border: 4rpx solid transparent;
background-clip: padding-box;
}
.card-frame::before {
content: '';
position: absolute;
top: -4rpx;
left: -4rpx;
right: -4rpx;
bottom: -4rpx;
background: linear-gradient(45deg,
#FF6B9D,
#A86BFF,
#6BDDFF,
#6BFF9D,
#FFD66B,
#FF6B9D);
background-size: 300% 300%;
border-radius: 20rpx;
z-index: -1;
animation: rainbowBorder 4s linear infinite;
}
@keyframes holographicShift {
0%, 100% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
}
@keyframes rainbowBorder {
0% {
background-position: 0% 50%;
}
100% {
background-position: 300% 50%;
}
}
.card-image {
width: 100%;
height: 100%;
border-radius: 12rpx;
display: block;
}
.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: rgba(255, 107, 157, 0.9);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 20rpx rgba(255, 107, 157, 0.6);
animation: checkPop 0.3s ease-out;
}
@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: 60rpx;
color: #FFFFFF;
font-weight: bold;
}
.card-selected {
z-index: 100 !important;
}
.gift-box {
position: absolute;
bottom: 20%;
left: 50%;
transform: translateX(-50%);
width: 420rpx;
height: 450rpx;
z-index: 3;
animation: giftFloat 3s ease-in-out infinite;
transition: all 0.8s cubic-bezier(0.4, 0, 0.2, 1);
perspective: 1000rpx;
}
.gift-box.gift-opened {
bottom: 5%;
animation: none;
}
@keyframes giftFloat {
0%, 100% {
transform: translateX(-50%) translateY(0);
}
50% {
transform: translateX(-50%) translateY(-20rpx);
}
}
.gift-container {
position: relative;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
}
.gift-lid {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 340rpx;
height: 140rpx;
z-index: 3;
transition: all 1s cubic-bezier(0.4, 0, 0.2, 1);
transform-style: preserve-3d;
transform-origin: bottom left;
}
.gift-opened .gift-lid {
transform: translateX(-70%) translateY(140rpx) rotateZ(-45deg);
opacity: 0.9;
}
.gift-bow {
position: absolute;
top: -30rpx;
left: 50%;
transform: translateX(-50%);
width: 200rpx;
height: 80rpx;
z-index: 4;
}
.gift-bow::before {
content: '';
position: absolute;
top: 20rpx;
left: 50%;
transform: translateX(-50%);
width: 40rpx;
height: 60rpx;
background: linear-gradient(135deg, #FFB6D9 0%, #FF9DC5 100%);
border-radius: 8rpx;
box-shadow: 0 4rpx 15rpx rgba(255, 107, 157, 0.4);
}
.gift-bow::after {
content: '';
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 180rpx;
height: 50rpx;
background: linear-gradient(135deg, #FFB6D9 0%, #FF9DC5 50%, #FFB6D9 100%);
border-radius: 30rpx;
box-shadow: 0 6rpx 20rpx rgba(255, 107, 157, 0.5);
}
.gift-lid-top {
position: relative;
width: 340rpx;
height: 100rpx;
background: linear-gradient(180deg,
#FFF0F5 0%,
#FFE5F0 30%,
#FFD4E8 100%);
border-radius: 20rpx 20rpx 8rpx 8rpx;
box-shadow:
0 10rpx 40rpx rgba(255, 107, 157, 0.4),
inset 0 -5rpx 15rpx rgba(255, 182, 217, 0.3),
inset 0 5rpx 15rpx rgba(255, 255, 255, 0.5);
display: flex;
align-items: center;
justify-content: center;
overflow: visible;
border: 3rpx solid rgba(255, 255, 255, 0.6);
}
.gift-lid-top::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 280rpx;
height: 60rpx;
background: linear-gradient(90deg,
transparent 0%,
rgba(255, 182, 217, 0.4) 50%,
transparent 100%);
border-radius: 30rpx;
}
.gift-text {
position: relative;
font-size: 36rpx;
font-weight: bold;
color: #FF6B9D;
letter-spacing: 4rpx;
text-shadow:
0 2rpx 8rpx rgba(255, 107, 157, 0.5),
0 0 20rpx rgba(255, 182, 217, 0.3);
z-index: 2;
}
.gift-body {
position: relative;
width: 340rpx;
height: 280rpx;
background: linear-gradient(180deg,
#FFE5F0 0%,
#FFD4E8 50%,
#FFC9E3 100%);
border-radius: 8rpx 8rpx 20rpx 20rpx;
box-shadow:
0 15rpx 50rpx rgba(255, 107, 157, 0.4),
inset 0 5rpx 20rpx rgba(255, 255, 255, 0.3),
inset 0 -5rpx 20rpx rgba(255, 182, 217, 0.3);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
border: 3rpx solid rgba(255, 255, 255, 0.5);
border-top: none;
}
.gift-body::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 8rpx;
background: linear-gradient(90deg,
rgba(255, 107, 157, 0.3) 0%,
rgba(255, 182, 217, 0.5) 50%,
rgba(255, 107, 157, 0.3) 100%);
}
.gift-body::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 90%;
height: 90%;
background: radial-gradient(circle at center,
rgba(255, 255, 255, 0.2) 0%,
transparent 70%);
pointer-events: none;
}
.gift-window {
position: relative;
width: 180rpx;
height: 180rpx;
background: linear-gradient(135deg,
#FFF5FA 0%,
#FFEBF3 50%,
#FFE0ED 100%);
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow:
inset 0 4rpx 15rpx rgba(255, 107, 157, 0.2),
0 4rpx 20rpx rgba(255, 182, 217, 0.3);
z-index: 2;
border: 2rpx solid rgba(255, 182, 217, 0.4);
}
.question-mark {
font-size: 120rpx;
font-weight: bold;
color: #FFB6D9;
text-shadow:
0 4rpx 15rpx rgba(255, 107, 157, 0.4),
0 0 30rpx rgba(255, 182, 217, 0.3);
}
.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;
}
.action-button:active {
transform: scale(0.95);
opacity: 0.9;
}
.button-text {
font-size: 32rpx;
color: #FFFFFF;
font-weight: bold;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.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>