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

482 lines
11 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="discover-container">
<!-- 左上角返回按钮 -->
<view class="back-button" @click="handleBack">
<image class="back-icon" src="/static/icon/back.png" mode="aspectFit" />
</view>
<!-- 内容区域 -->
<view class="content-wrapper">
<!-- 模版铸造页 -->
<scroll-view v-show="activeTab === 0" class="tab-content" scroll-y>
<DiscoverFeed />
</scroll-view>
<!-- 自主化铸造页 -->
<view v-show="activeTab === 1" class="tab-content">
<CreateFeed />
</view>
</view>
<!-- 底部操作栏 -->
<view class="bottom-actions">
<!-- 切换按钮组 -->
<view class="toggle-buttons">
<view class="toggle-capsule">
<view class="toggle-btn" :class="{ active: activeTab === 0 }" @click="switchTab(0)">
<text class="toggle-text">自主化铸造</text>
</view>
<view class="toggle-btn" :class="{ active: activeTab === 1 }" @click="switchTab(1)">
<text class="toggle-text">模版铸造</text>
</view>
</view>
<view class="skip-btn" @click="handleSkip">
<text class="skip-text">跳过</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import DiscoverFeed from './discover-feed.vue';
import CreateFeed from './create-feed.vue';
// 导入API
import { getOssSignatureApi, createMintOrderApi } from '@/utils/api.js';
const activeTab = ref(0);
const formData = ref(null);
// 切换标签
const switchTab = (index) => {
activeTab.value = index;
};
// 返回按钮
const handleBack = () => {
uni.navigateBack();
};
// 将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, ossData) => {
const timestamp = Date.now();
const randomStr = Math.random().toString(36).substring(2, 8);
const fileName = `skip_upload_${timestamp}_${randomStr}.png`;
let imageUrl;
// #ifdef H5
// H5环境使用FormData + Blob
const blob = base64ToBlob(base64Image);
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);
const response = await fetch(ossData.host, {
method: 'POST',
body: formData
});
if (response.ok || response.status === 204) {
imageUrl = `${ossData.host}/${ossData.dir}${fileName}`;
} else {
throw new Error(`上传失败,状态码: ${response.status}`);
}
// #endif
// #ifdef APP-PLUS
// App环境使用plus.nativeObj.Bitmap
const tempFilePath = await new Promise((resolve, reject) => {
const bitmap = new plus.nativeObj.Bitmap('temp_skip');
bitmap.loadBase64Data(base64Image, () => {
const tempPath = '_doc/' + fileName;
bitmap.save(tempPath, {}, () => {
bitmap.clear();
resolve(tempPath);
}, (error) => {
bitmap.clear();
reject(new Error('图片保存失败'));
});
}, (error) => {
bitmap.clear();
reject(new Error('加载base64失败'));
});
});
// 上传到OSS
imageUrl = await new Promise((resolve, reject) => {
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) => {
if (uploadRes.statusCode === 200 || uploadRes.statusCode === 204) {
const url = `${ossData.host}/${ossData.dir}${fileName}`;
resolve(url);
} else {
reject(new Error(`上传失败,状态码: ${uploadRes.statusCode}`));
}
},
fail: (error) => {
reject(error);
}
});
});
// #endif
return imageUrl;
};
// 跳过按钮
const handleSkip = async () => {
try {
// 从storage获取表单数据
const formDataStr = uni.getStorageSync('castlove_form_data');
if (!formDataStr) {
uni.showToast({
title: '未找到表单数据',
icon: 'none'
});
return;
}
const orderValue = JSON.parse(formDataStr);
// 检查是否有图片
if (!orderValue.imageBase64) {
uni.showToast({
title: '未找到图片数据',
icon: 'none'
});
return;
}
// 显示加载提示
// uni.showLoading({ title: '上传图片中...', mask: true });
// 获取OSS签名
const signRes = await getOssSignatureApi('asset');
if (signRes.code !== 200) {
throw new Error(signRes.message || '获取签名失败');
}
const orderId = signRes.data.order_id || '';
// 上传原图到OSS
const imageUrl = await uploadImageToOss(orderValue.imageBase64, signRes.data);
// 更新加载提示
uni.showLoading({ title: '创建订单中...', mask: true });
// 构建订单数据
const orderData = {
name: orderValue.name,
event: orderValue.event,
description: orderValue.remark || '',
material_type: orderValue.materialType || 'image',
material_url: imageUrl,
rarity: 0,
tags: [],
order_id: orderId
};
// 调用创建铸造订单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,
name: orderValue.name,
event: orderValue.event,
description: orderValue.remark || '',
material_type: orderValue.materialType || 'image',
order_id: orderId
};
// 存储到storage
uni.setStorageSync('temp_nft_data', JSON.stringify(nftData));
// 跳转到成功页面
uni.redirectTo({
url: '/pages/castlove/success'
});
} catch (error) {
console.error('[Discover] 跳过失败:', error);
uni.hideLoading();
uni.showToast({
title: error.message || '操作失败,请重试',
icon: 'none'
});
}
};
// 底部操作
const handleHome = () => {
uni.navigateBack();
};
const handleMore = () => {
uni.showToast({
title: '更多功能',
icon: 'none'
});
};
// 监听子组件切换事件
const handleSwitchTab = (index) => {
switchTab(index);
};
onLoad(() => {
// 尝试从存储中读取表单数据
try {
const storedData = uni.getStorageSync('castlove_form_data');
if (storedData) {
// 默认显示自主化铸造
activeTab.value = 1;
}
} catch (e) {
console.error('[Discover] 读取表单数据失败:', e);
}
});
onMounted(() => {
uni.$on('discover:switchTab', handleSwitchTab);
console.log('[Discover] 页面加载完成');
});
onUnmounted(() => {
uni.$off('discover:switchTab', handleSwitchTab);
});
</script>
<style scoped>
.discover-container {
position: relative;
width: 100vw;
height: 100vh;
overflow: hidden;
background-image: url('/static/background/starbook.jpg');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
/* 左上角返回按钮 */
.back-button {
position: fixed;
top: calc(env(safe-area-inset-top) + 80rpx);
left: 32rpx;
width: 80rpx;
height: 80rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
z-index: 101;
}
.back-icon {
width: 64rpx;
height: 64rpx;
}
.content-wrapper {
width: 100%;
height: 100%;
padding-bottom: 240rpx;
}
.tab-content {
width: 100%;
height: 100%;
}
/* 底部操作栏 */
.bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 24rpx;
padding: 32rpx 48rpx;
padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
z-index: 100;
}
/* 开启创作按钮 */
.create-button {
width: 100%;
height: 96rpx;
background: rgba(255, 255, 255, 0.95);
border-radius: 48rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.12);
backdrop-filter: blur(10rpx);
}
.home-icon {
width: 40rpx;
height: 40rpx;
}
.create-text {
font-size: 32rpx;
color: #333;
font-weight: 600;
}
/* 切换按钮组 */
.toggle-buttons {
width: 100%;
display: flex;
gap: 24rpx;
align-items: center;
justify-content: space-between;
}
/* 胶囊容器 */
.toggle-capsule {
width: 420rpx;
height: 80rpx;
background: rgba(255, 255, 255, 0.6);
border-radius: 40rpx;
backdrop-filter: blur(10rpx);
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
display: flex;
padding: 4rpx;
gap: 4rpx;
}
.toggle-btn {
flex: 1;
height: 80rpx;
border-radius: 36rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
background: transparent;
}
.toggle-btn.active {
background: linear-gradient(to bottom right,
#F0E4B1 0%, /* 左:浅橙粉 */
#F08399 50%,
#B94E73 100% /* 右:柔粉红 */
);
box-shadow:
/* 外层投影 - 让按钮浮起 */
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
0 2rpx 6rpx rgba(255, 143, 158, 0.15),
/* 内阴影 - 模拟顶部受光 + 底部凹陷 */
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4), /* 顶部高光 */
inset 0 -2rpx 4rpx rgba(0, 0, 0, 0.05); /* 底部暗部 */
}
.toggle-text {
font-size: 28rpx;
color: #666;
font-weight: 500;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
}
.toggle-btn.active .toggle-text {
color: #ffffff;
font-weight: 600;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
}
.toggle-btn:active {
transform: scale(0.98);
}
/* 跳过按钮 */
.skip-btn {
width: 120rpx;
height: 80rpx;
background: linear-gradient(to bottom right,
#F0E4B1 0%, /* 左:浅橙粉 */
#F08399 50%,
#B94E73 100% /* 右:柔粉红 */
);
box-shadow:
/* 外层投影 - 让按钮浮起 */
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
0 2rpx 6rpx rgba(255, 143, 158, 0.15),
/* 内阴影 - 模拟顶部受光 + 底部凹陷 */
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4), /* 顶部高光 */
inset 0 -2rpx 4rpx rgba(0, 0, 0, 0.05); /* 底部暗部 */
border-radius: 40rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.skip-text {
font-size: 28rpx;
color: #ffffff;
font-weight: 600;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
}
.skip-btn:active {
transform: scale(0.98);
}
</style>