feat: 修改的铸造界面
This commit is contained in:
parent
de207465f4
commit
45e0ad0227
@ -1,2 +1 @@
|
||||
# TopFans
|
||||
背景有什么想法?边框想要什么样式的?有没有特别想加的装饰元素?整体材质的效果想要什么样的?主色调是应援色吗,还是别的颜色?
|
||||
@ -276,7 +276,7 @@ func (ctrl *AssetController) CreateMintOrder(c *gin.Context) {
|
||||
Rarity: req.Rarity,
|
||||
Tags: req.Tags,
|
||||
MaterialType: req.MaterialType,
|
||||
Event: req.Event,
|
||||
Info: req.Info,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
|
||||
@ -83,6 +83,7 @@ func ConvertAsset(pbAsset *pbAsset.Asset) AssetDTO {
|
||||
CreatedAt: pbAsset.CreatedAt,
|
||||
UpdatedAt: pbAsset.UpdatedAt,
|
||||
IsLiked: pbAsset.IsLiked,
|
||||
Info: pbAsset.Info,
|
||||
}
|
||||
|
||||
// 可选字段
|
||||
|
||||
@ -4,14 +4,14 @@ package dto
|
||||
|
||||
// CreateMintOrderRequestDTO 创建铸造订单请求
|
||||
type CreateMintOrderRequestDTO struct {
|
||||
OrderID string `json:"order_id" binding:"required"` // 阶段一生成的订单ID(必填)
|
||||
Name string `json:"name" binding:"required"` // 藏品名称(必填)
|
||||
MaterialURL string `json:"material_url" binding:"required"` // 用户上传的素材URL(必填,前端已上传到OSS)
|
||||
Description string `json:"description"` // 藏品描述(可选)
|
||||
Rarity int32 `json:"rarity"` // 稀有度(可选)
|
||||
Tags []string `json:"tags"` // 标签列表(可选)
|
||||
MaterialType string `json:"material_type"` // 素材类型(可选)
|
||||
Event string `json:"event"` // 藏品事件(可选)
|
||||
OrderID string `json:"order_id" binding:"required"` // 阶段一生成的订单ID(必填)
|
||||
Name string `json:"name"` // 藏品名称(可选)
|
||||
MaterialURL string `json:"material_url" binding:"required"` // 用户上传的素材URL(必填,前端已上传到OSS)
|
||||
Description string `json:"description"` // 藏品描述(可选)
|
||||
Rarity int32 `json:"rarity"` // 稀有度(可选)
|
||||
Tags []string `json:"tags"` // 标签列表(可选)
|
||||
MaterialType string `json:"material_type"` // 素材类型(可选)
|
||||
Info string `json:"info" binding:"required"` // 藏品信息(必填)
|
||||
}
|
||||
|
||||
// PreCreateMintOrderRequestDTO 阶段一:预创建铸造订单(返回 order_id)
|
||||
@ -79,6 +79,7 @@ type AssetDTO struct {
|
||||
MintedAt int64 `json:"minted_at,omitempty"` // 上链成功时间(毫秒时间戳,可选)
|
||||
Owner *OwnerInfoDTO `json:"owner,omitempty"` // 持有者信息(可选,保留用于兼容性)
|
||||
IsLiked bool `json:"is_liked"` // 当前用户是否已点赞
|
||||
Info string `json:"info"` // 藏品信息
|
||||
}
|
||||
|
||||
// OwnerInfoDTO 持有者信息
|
||||
|
||||
@ -22,6 +22,7 @@ type Asset struct {
|
||||
Rarity *int32 `gorm:"column:rarity"` // 稀有度(预留)
|
||||
Tags StringArray `gorm:"type:jsonb;column:tags"` // 标签(预留,JSON数组)
|
||||
Visibility string `gorm:"type:varchar(20);default:'private';column:visibility"` // 可见性:private, friends, public(预留)
|
||||
Info string `gorm:"type:text;column:info"` // 藏品信息(必填)
|
||||
|
||||
// 状态字段
|
||||
Status int32 `gorm:"not null;default:0;index:idx_assets_status;column:status"` // 0:Pending, 1:Active
|
||||
@ -122,6 +123,7 @@ type MintOrder struct {
|
||||
Description *string `gorm:"type:text;column:description"` // 藏品描述(阶段一写入,可选)
|
||||
MaterialType *string `gorm:"type:varchar(50);column:material_type"` // 素材类型(可选)
|
||||
Event *string `gorm:"type:varchar(100);column:event"` // 藏品事件(可选)
|
||||
Info *string `gorm:"type:text;column:info"` // 藏品信息(必填)
|
||||
CreatedAt int64 `gorm:"not null;index:idx_mint_orders_created_at,sort:desc;column:created_at"`
|
||||
UpdatedAt int64 `gorm:"not null;column:updated_at"`
|
||||
MintedAt *int64 `gorm:"column:minted_at"` // 上链成功时间(用于 Task Service 事件)
|
||||
|
||||
@ -47,6 +47,7 @@ type Asset struct {
|
||||
Owner *OwnerInfo `protobuf:"bytes,18,opt,name=owner,proto3" json:"owner,omitempty"` // 持有者信息(保留用于兼容性)
|
||||
OwnerNickname string `protobuf:"bytes,19,opt,name=owner_nickname,json=ownerNickname,proto3" json:"owner_nickname,omitempty"` // 持有者昵称(在该star下的昵称)
|
||||
IsLiked bool `protobuf:"varint,20,opt,name=is_liked,json=isLiked,proto3" json:"is_liked,omitempty"` // 当前用户是否已点赞
|
||||
Info string `protobuf:"bytes,21,opt,name=info,proto3" json:"info,omitempty"` // 藏品信息
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@ -221,6 +222,13 @@ func (x *Asset) GetIsLiked() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *Asset) GetInfo() string {
|
||||
if x != nil {
|
||||
return x.Info
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// 持有者信息
|
||||
type OwnerInfo struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
@ -548,13 +556,13 @@ func (x *InitMintOrderResponse) GetOrder() *MintOrder {
|
||||
type CreateMintOrderRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
OrderId string `protobuf:"bytes,2,opt,name=order_id,json=orderId,proto3" json:"order_id,omitempty"` // 阶段一生成的订单ID(必填)
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // 藏品名称(必填,可覆盖阶段一保存值)
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // 藏品名称(可选)
|
||||
MaterialUrl string `protobuf:"bytes,3,opt,name=material_url,json=materialUrl,proto3" json:"material_url,omitempty"` // 用户上传的素材URL(必填,前端已上传到OSS)
|
||||
Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` // 藏品描述(可选)
|
||||
Rarity int32 `protobuf:"varint,5,opt,name=rarity,proto3" json:"rarity,omitempty"` // 稀有度(可选)
|
||||
Tags []string `protobuf:"bytes,6,rep,name=tags,proto3" json:"tags,omitempty"` // 标签列表(可选)
|
||||
MaterialType string `protobuf:"bytes,7,opt,name=material_type,json=materialType,proto3" json:"material_type,omitempty"` // 素材类型(可选)
|
||||
Event string `protobuf:"bytes,8,opt,name=event,proto3" json:"event,omitempty"` // 藏品事件(可选)
|
||||
Info string `protobuf:"bytes,8,opt,name=info,proto3" json:"info,omitempty"` // 藏品信息(必填)
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@ -638,9 +646,9 @@ func (x *CreateMintOrderRequest) GetMaterialType() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *CreateMintOrderRequest) GetEvent() string {
|
||||
func (x *CreateMintOrderRequest) GetInfo() string {
|
||||
if x != nil {
|
||||
return x.Event
|
||||
return x.Info
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@ -28,11 +28,12 @@ message Asset {
|
||||
int64 created_at = 15; // 创建时间(毫秒时间戳)
|
||||
int64 updated_at = 16; // 更新时间(毫秒时间戳)
|
||||
int64 minted_at = 17; // 上链成功时间(毫秒时间戳,可选)
|
||||
|
||||
|
||||
// 扩展字段:持有者信息(用于详情展示)
|
||||
OwnerInfo owner = 18; // 持有者信息(保留用于兼容性)
|
||||
string owner_nickname = 19; // 持有者昵称(在该star下的昵称)
|
||||
bool is_liked = 20; // 当前用户是否已点赞
|
||||
string info = 21; // 藏品信息
|
||||
}
|
||||
|
||||
// 持有者信息
|
||||
@ -77,13 +78,13 @@ message InitMintOrderResponse {
|
||||
// 创建铸造订单请求
|
||||
message CreateMintOrderRequest {
|
||||
string order_id = 2; // 阶段一生成的订单ID(必填)
|
||||
string name = 1; // 藏品名称(必填,可覆盖阶段一保存值)
|
||||
string name = 1; // 藏品名称(可选)
|
||||
string material_url = 3; // 用户上传的素材URL(必填,前端已上传到OSS)
|
||||
string description = 4; // 藏品描述(可选)
|
||||
int32 rarity = 5; // 稀有度(可选)
|
||||
repeated string tags = 6; // 标签列表(可选)
|
||||
string material_type = 7; // 素材类型(可选)
|
||||
string event = 8; // 藏品事件(可选)
|
||||
string info = 8; // 藏品信息(必填)
|
||||
// cover_url 不再需要前端传入,由后端 AI 处理完成后自动生成
|
||||
}
|
||||
|
||||
|
||||
@ -8,6 +8,33 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/starbook/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"app-plus": {
|
||||
"bounce": "none"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/starcity/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"app-plus": {
|
||||
"bounce": "none"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/friends/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"app-plus": {
|
||||
"bounce": "none"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/login/login",
|
||||
"style": {
|
||||
@ -56,6 +83,30 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/castlove/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/castlove/create",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"app-plus": {
|
||||
"bounce": "none"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/castlove/mall",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"app-plus": {
|
||||
"bounce": "none"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/castlove/success",
|
||||
"style": {
|
||||
|
||||
@ -114,15 +114,15 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onUnmounted } from 'vue';
|
||||
import { onLoad } from '@dcloudio/uni-app';
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app';
|
||||
import NftCard from '../components/NftCard.vue';
|
||||
import { getAssetDetailApi, getMintOrderDetailApi, likeAssetApi, unlikeAssetApi } from '@/utils/api.js';
|
||||
import { getAssetCoverRealUrl } from '@/utils/assetImageHelper.js';
|
||||
|
||||
// 页面参数
|
||||
const assetIdParam = ref('');
|
||||
const orderIdParam = ref('');
|
||||
const fromParam = ref('');
|
||||
const lastLoadKey = ref(''); // 记录上次加载的key,避免重复加载
|
||||
|
||||
// 数据状态
|
||||
const loading = ref(true);
|
||||
@ -188,16 +188,21 @@ const copyHash = () => {
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
console.log('loadData 开始执行');
|
||||
loading.value = true;
|
||||
loadError.value = '';
|
||||
|
||||
try {
|
||||
let assetId = assetIdParam.value;
|
||||
console.log('assetIdParam:', assetIdParam.value, 'orderIdParam:', orderIdParam.value);
|
||||
|
||||
if (!assetId && orderIdParam.value) {
|
||||
console.log('通过 order_id 获取资产信息');
|
||||
const mintRes = await getMintOrderDetailApi(orderIdParam.value);
|
||||
console.log('getMintOrderDetailApi 响应:', mintRes);
|
||||
if (mintRes.code === 200 && mintRes.data?.asset?.asset_id) {
|
||||
assetId = mintRes.data.asset.asset_id;
|
||||
console.log('获取到 assetId:', assetId);
|
||||
} else {
|
||||
throw new Error('获取铸造订单详情失败');
|
||||
}
|
||||
@ -205,7 +210,9 @@ const loadData = async () => {
|
||||
|
||||
if (!assetId) throw new Error('藏品信息不完整');
|
||||
|
||||
console.log('调用 getAssetDetailApi,assetId:', assetId);
|
||||
const res = await getAssetDetailApi(assetId);
|
||||
console.log('getAssetDetailApi 响应:', res);
|
||||
if (res.code === 200 && res.data?.asset) {
|
||||
const asset = res.data.asset;
|
||||
assetData.value = asset;
|
||||
@ -216,15 +223,26 @@ const loadData = async () => {
|
||||
remainSeconds.value = asset.remain_time;
|
||||
startCountdown();
|
||||
}
|
||||
|
||||
coverUrl.value = await getAssetCoverRealUrl(asset.cover_url);
|
||||
console.log(res.data)
|
||||
// 异步加载封面图片,不阻塞页面渲染
|
||||
console.log('开始加载封面图片:', asset.cover_url);
|
||||
getAssetCoverRealUrl(asset.cover_url).then(url => {
|
||||
console.log('封面图片加载成功:', url);
|
||||
coverUrl.value = url;
|
||||
}).catch(err => {
|
||||
console.error('加载封面图片失败:', err);
|
||||
coverUrl.value = ''; // 失败时不显示默认图片
|
||||
});
|
||||
console.log('loadData 执行成功');
|
||||
} else {
|
||||
throw new Error(res.message || '获取藏品详情失败');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('loadData 出错:', err);
|
||||
loadError.value = err.message || '加载失败,请重试';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
console.log('loadData 执行完成,loading:', loading.value);
|
||||
}
|
||||
};
|
||||
|
||||
@ -277,10 +295,22 @@ const handleBack = () => {
|
||||
};
|
||||
|
||||
onLoad((options) => {
|
||||
console.log('onLoad 触发,参数:', options);
|
||||
assetIdParam.value = options?.asset_id || '';
|
||||
orderIdParam.value = options?.order_id || '';
|
||||
fromParam.value = options?.from || '';
|
||||
loadData();
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
console.log('onShow 触发');
|
||||
const currentKey = `${assetIdParam.value}_${orderIdParam.value}`;
|
||||
console.log('当前key:', currentKey, '上次key:', lastLoadKey.value);
|
||||
|
||||
// 如果参数变化了,或者是首次加载,则重新加载数据
|
||||
if ((assetIdParam.value || orderIdParam.value) && currentKey !== lastLoadKey.value) {
|
||||
lastLoadKey.value = currentKey;
|
||||
loadData();
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
|
||||
1118
frontend/pages/castlove/create.vue
Normal file
1118
frontend/pages/castlove/create.vue
Normal file
File diff suppressed because it is too large
Load Diff
946
frontend/pages/castlove/index.vue
Normal file
946
frontend/pages/castlove/index.vue
Normal file
@ -0,0 +1,946 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<Header :showBack="true" backIconColor="#e6e6e6" />
|
||||
|
||||
<view class="castlove-content">
|
||||
<!-- 背景图片 -->
|
||||
<image class="background-image" src="/static/background/profile-bg.png" mode="aspectFill"></image>
|
||||
|
||||
<!-- 蒙层 -->
|
||||
<view class="background-overlay"></view>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<view class="content-wrapper">
|
||||
<scroll-view class="content-wrapper" scroll-y="true" :show-scrollbar="false" :enhanced="true">
|
||||
<!-- 图片上传区域 -->
|
||||
<view class="upload-section">
|
||||
<view class="upload-box" @click="chooseImage">
|
||||
<image v-if="uploadedImage" class="uploaded-image" :src="uploadedImage" mode="aspectFit">
|
||||
</image>
|
||||
<view v-else class="upload-placeholder">
|
||||
<image class="upload-icon" src="/static/icon/add.png" mode="aspectFit"></image>
|
||||
<text class="upload-text">点击上传藏品图片</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="upload-hint">支持JPG、PNG格式,大小不超过5MB</view>
|
||||
</view>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view class="form-section">
|
||||
<!-- 素材类型选择器 -->
|
||||
<view class="form-item form-item-picker">
|
||||
<text class="form-label">素材类型</text>
|
||||
<view class="custom-picker">
|
||||
<view class="picker-display" @click="toggleMaterialTypePicker">
|
||||
<text class="picker-text">{{ materialTypes[materialTypeIndex] }}</text>
|
||||
<text class="picker-arrow"
|
||||
:class="{ 'picker-arrow-up': showMaterialTypePicker }">›</text>
|
||||
</view>
|
||||
<!-- 自定义下拉选项列表 -->
|
||||
<view v-if="showMaterialTypePicker" class="picker-options">
|
||||
<view v-for="(type, index) in materialTypes" :key="index" class="picker-option"
|
||||
:class="{ 'picker-option-active': materialTypeIndex === index }"
|
||||
@click.stop="selectMaterialType(index)">
|
||||
<text class="picker-option-text">{{ type }}</text>
|
||||
<text v-if="materialTypeIndex === index" class="picker-option-check">✓</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 藏品名称输入框 -->
|
||||
<view class="form-item">
|
||||
<text class="form-label">藏品名称</text>
|
||||
<input class="form-input" v-model="nftName" placeholder="请输入藏品名称"
|
||||
placeholder-class="input-placeholder" />
|
||||
</view>
|
||||
|
||||
<!-- 藏品事件输入框 -->
|
||||
<view class="form-item">
|
||||
<text class="form-label">藏品事件</text>
|
||||
<input class="form-input" v-model="nftEvent" placeholder="请输入藏品事件"
|
||||
placeholder-class="input-placeholder" />
|
||||
</view>
|
||||
|
||||
<!-- 备注输入框 -->
|
||||
<view class="form-item">
|
||||
<text class="form-label">备注</text>
|
||||
<textarea class="form-textarea" v-model="nftRemark" placeholder="请输入备注信息"
|
||||
placeholder-class="textarea-placeholder" maxlength="200" auto-height
|
||||
:show-confirm-bar="false" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部按钮区域 -->
|
||||
<view class="button-section">
|
||||
<button class="btn-secondary" @click="handleBack">返回</button>
|
||||
<button class="btn-primary" @click="handleConfirm">开始铸造</button>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 蒙层 - 导航栏展开时显示 -->
|
||||
<view v-if="navExpanded" class="nav-mask" @click="navExpanded = false"></view>
|
||||
|
||||
<BottomNav :activeTab="2" @update:activeTab="handleTabChange" :isExpanded="navExpanded" @update:isExpanded="navExpanded = $event" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import Header from "../components/Header.vue";
|
||||
import BottomNav from "../components/BottomNav.vue";
|
||||
import { getOssSignatureApi, deleteMintOrderApi } from '@/utils/api.js';
|
||||
|
||||
const navExpanded = ref(false);
|
||||
|
||||
// 表单数据
|
||||
const uploadedImage = ref(''); // 本地预览路径
|
||||
const uploadedImageUrl = ref(''); // OSS完整URL
|
||||
const uploadedImageBase64 = ref(''); // Base64格式
|
||||
const originalFileName = ref(''); // 原始文件名
|
||||
const currentOrderId = ref(''); // 当前订单ID
|
||||
const isUploading = ref(false); // 上传中状态
|
||||
const materialTypes = ['粉丝自制', '热爱痕迹', '其他'];
|
||||
const materialTypeIndex = ref(0);
|
||||
const showMaterialTypePicker = ref(false);
|
||||
const nftName = ref('');
|
||||
const nftEvent = ref('');
|
||||
const nftRemark = ref('');
|
||||
|
||||
// 选择图片
|
||||
const chooseImage = () => {
|
||||
// 如果正在上传,禁止选择新图片
|
||||
if (isUploading.value) {
|
||||
uni.showToast({
|
||||
title: '图片上传中,请稍候',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sourceType: ['album', 'camera'],
|
||||
success: (res) => {
|
||||
const filePath = res.tempFilePaths[0];
|
||||
const tempFile = res.tempFiles && res.tempFiles[0];
|
||||
|
||||
// 获取文件信息进行验证
|
||||
uni.getFileInfo({
|
||||
filePath: filePath,
|
||||
success: (fileInfo) => {
|
||||
// 验证文件大小(5MB = 5 * 1024 * 1024 bytes)
|
||||
const maxSize = 5 * 1024 * 1024;
|
||||
if (fileInfo.size > maxSize) {
|
||||
uni.showToast({
|
||||
title: '图片大小不能超过5MB',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证文件格式
|
||||
const mimeType = (tempFile && tempFile.type) ? tempFile.type.toLowerCase() : '';
|
||||
const pathLower = filePath.toLowerCase();
|
||||
const validByMime = mimeType === 'image/jpeg' || mimeType === 'image/png';
|
||||
const validByExt = pathLower.endsWith('.jpg') || pathLower.endsWith('.jpeg') || pathLower.endsWith('.png');
|
||||
if (!validByMime && !validByExt) {
|
||||
uni.showToast({
|
||||
title: '只支持JPG和PNG格式',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 提取原始文件名
|
||||
const rawName = (tempFile && tempFile.name)
|
||||
? tempFile.name
|
||||
: filePath.split('/').pop();
|
||||
originalFileName.value = rawName;
|
||||
|
||||
// 验证通过,转换为 base64
|
||||
convertImageToBase64(filePath, originalFileName.value);
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error('获取文件信息失败:', error);
|
||||
uni.showToast({
|
||||
title: '获取文件信息失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('选择图片失败:', err);
|
||||
uni.showToast({
|
||||
title: '选择图片失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 将图片转换为 base64(兼容 H5、App、小程序)
|
||||
const convertImageToBase64 = (filePath, fileName) => {
|
||||
isUploading.value = true;
|
||||
uni.showLoading({ title: '处理中...', mask: true });
|
||||
|
||||
// 判断运行环境
|
||||
// #ifdef H5
|
||||
// H5 环境:使用 FileReader
|
||||
convertImageToBase64H5(filePath, fileName);
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
// App/小程序环境:使用 FileSystemManager
|
||||
convertImageToBase64Native(filePath, fileName);
|
||||
// #endif
|
||||
};
|
||||
|
||||
// H5 环境的 base64 转换
|
||||
const convertImageToBase64H5 = (filePath, fileName) => {
|
||||
// H5 环境下,filePath 是 blob URL
|
||||
// 需要通过 fetch 获取 blob,然后用 FileReader 转换
|
||||
fetch(filePath)
|
||||
.then(res => res.blob())
|
||||
.then(blob => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
// e.target.result 已经是完整的 data:image/xxx;base64,xxx 格式
|
||||
uploadedImageBase64.value = e.target.result;
|
||||
uploadedImage.value = filePath;
|
||||
|
||||
console.log('[CastloveContent] Base64转换成功 (H5)');
|
||||
console.log('[CastloveContent] Base64长度:', uploadedImageBase64.value.length);
|
||||
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: '图片加载成功',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
});
|
||||
|
||||
isUploading.value = false;
|
||||
};
|
||||
reader.onerror = (error) => {
|
||||
console.error('Base64转换失败 (H5):', error);
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: '图片处理失败',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
isUploading.value = false;
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('获取图片失败 (H5):', error);
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: '图片处理失败',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
isUploading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
// App/小程序环境的 base64 转换
|
||||
const convertImageToBase64Native = (filePath, fileName) => {
|
||||
// #ifdef MP-WEIXIN || MP-ALIPAY || MP-BAIDU || MP-TOUTIAO || MP-QQ
|
||||
// 小程序环境
|
||||
const fs = uni.getFileSystemManager();
|
||||
fs.readFile({
|
||||
filePath: filePath,
|
||||
encoding: 'base64',
|
||||
success: (res) => {
|
||||
const ext = fileName.toLowerCase().split('.').pop();
|
||||
let mimeType = 'image/jpeg';
|
||||
if (ext === 'png') {
|
||||
mimeType = 'image/png';
|
||||
}
|
||||
|
||||
uploadedImageBase64.value = `data:${mimeType};base64,${res.data}`;
|
||||
uploadedImage.value = filePath;
|
||||
|
||||
console.log('[CastloveContent] Base64转换成功 (小程序)');
|
||||
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: '图片加载成功',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
});
|
||||
|
||||
isUploading.value = false;
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error('[CastloveContent] Base64转换失败:', error);
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: '图片处理失败',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
isUploading.value = false;
|
||||
}
|
||||
});
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
// App环境:使用plus.io API
|
||||
plus.io.resolveLocalFileSystemURL(filePath, (entry) => {
|
||||
entry.file((file) => {
|
||||
const reader = new plus.io.FileReader();
|
||||
reader.onloadend = (e) => {
|
||||
uploadedImageBase64.value = e.target.result;
|
||||
uploadedImage.value = filePath;
|
||||
|
||||
console.log('[CastloveContent] Base64转换成功 (App)');
|
||||
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: '图片加载成功',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
});
|
||||
|
||||
isUploading.value = false;
|
||||
};
|
||||
reader.onerror = (error) => {
|
||||
console.error('[CastloveContent] Base64转换失败:', error);
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: '图片处理失败',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
isUploading.value = false;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}, (error) => {
|
||||
console.error('[CastloveContent] 读取文件失败:', error);
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: '图片处理失败',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
isUploading.value = false;
|
||||
});
|
||||
// #endif
|
||||
};
|
||||
|
||||
// 上传图片到OSS
|
||||
const uploadImageToOss = async (filePath, fileName) => {
|
||||
try {
|
||||
isUploading.value = true;
|
||||
uni.showLoading({ title: '上传中...', mask: true });
|
||||
|
||||
// 1. 获取OSS签名(type='asset'),后端返回order_id
|
||||
const signRes = await getOssSignatureApi('asset');
|
||||
if (signRes.code !== 200) {
|
||||
throw new Error(signRes.message || '获取签名失败');
|
||||
}
|
||||
|
||||
// 保存order_id
|
||||
currentOrderId.value = signRes.data.order_id;
|
||||
|
||||
// 2. 构建FormData并上传到OSS
|
||||
uni.uploadFile({
|
||||
url: signRes.data.host,
|
||||
filePath: filePath,
|
||||
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) => {
|
||||
try {
|
||||
if (uploadRes.statusCode === 200 || uploadRes.statusCode === 204) {
|
||||
// 3. 拼接完整URL
|
||||
uploadedImageUrl.value = `${signRes.data.host}/${signRes.data.dir}${fileName}`;
|
||||
|
||||
// 4. 显示预览图(使用本地路径)
|
||||
uploadedImage.value = filePath;
|
||||
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: '上传成功',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
});
|
||||
} else {
|
||||
throw new Error(`上传失败,状态码: ${uploadRes.statusCode}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('处理上传结果失败:', error);
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: error.message || '上传失败',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
} finally {
|
||||
isUploading.value = false;
|
||||
}
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error('OSS上传失败:', error);
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: '上传失败',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
isUploading.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('上传图片失败:', error);
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: error.message || '上传失败',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
isUploading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 切换素材类型选择器显示
|
||||
const toggleMaterialTypePicker = () => {
|
||||
showMaterialTypePicker.value = !showMaterialTypePicker.value;
|
||||
};
|
||||
|
||||
// 选择素材类型
|
||||
const selectMaterialType = (index) => {
|
||||
materialTypeIndex.value = index;
|
||||
showMaterialTypePicker.value = false;
|
||||
};
|
||||
|
||||
// 返回按钮
|
||||
const handleBack = async () => {
|
||||
// 如果有未保存的数据,提示用户
|
||||
if (uploadedImage.value || nftName.value || nftEvent.value || nftRemark.value) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要返回吗?未保存的数据将会丢失',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
// 如果有订单ID,需要删除订单
|
||||
if (currentOrderId.value) {
|
||||
try {
|
||||
await deleteMintOrderApi(currentOrderId.value);
|
||||
} catch (error) {
|
||||
console.error('删除订单失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 清空表单数据
|
||||
resetForm();
|
||||
// 返回广场页面
|
||||
uni.redirectTo({
|
||||
url: '/pages/square/square'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 没有未保存数据,直接返回
|
||||
uni.redirectTo({
|
||||
url: '/pages/square/square'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 底部导航切换
|
||||
const handleTabChange = (newTab) => {
|
||||
const routes = [
|
||||
'/pages/square/square',
|
||||
'/pages/starbook/index',
|
||||
'/pages/castlove/mall',
|
||||
'/pages/starcity/index',
|
||||
'/pages/friends/index'
|
||||
];
|
||||
|
||||
if (newTab >= 0 && newTab < routes.length) {
|
||||
uni.redirectTo({
|
||||
url: routes[newTab]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 确认铸造按钮
|
||||
const handleConfirm = async () => {
|
||||
// 表单验证
|
||||
if (!uploadedImage.value) {
|
||||
uni.showToast({
|
||||
title: '请上传藏品图片',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!uploadedImageBase64.value) {
|
||||
uni.showToast({
|
||||
title: '图片尚未处理完成',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!nftName.value.trim()) {
|
||||
uni.showToast({
|
||||
title: '请输入藏品名称',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!nftEvent.value.trim()) {
|
||||
uni.showToast({
|
||||
title: '请输入藏品事件',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存表单数据到全局存储
|
||||
const formData = {
|
||||
image: uploadedImage.value,
|
||||
imageBase64: uploadedImageBase64.value, // Base64格式
|
||||
name: nftName.value.trim(),
|
||||
event: nftEvent.value.trim(),
|
||||
remark: nftRemark.value.trim(),
|
||||
materialType: materialTypes[materialTypeIndex.value]
|
||||
};
|
||||
|
||||
try {
|
||||
uni.setStorageSync('castlove_form_data', JSON.stringify(formData));
|
||||
console.log('[CastloveContent] 表单数据已保存,Base64长度:', uploadedImageBase64.value.length);
|
||||
} catch (e) {
|
||||
console.error('保存表单数据失败:', e);
|
||||
uni.showToast({
|
||||
title: '保存数据失败',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 清空表单
|
||||
resetForm();
|
||||
|
||||
// 跳转到发现页面
|
||||
uni.navigateTo({
|
||||
url: '/pages/discover/discover'
|
||||
});
|
||||
};
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
uploadedImage.value = '';
|
||||
uploadedImageUrl.value = '';
|
||||
uploadedImageBase64.value = '';
|
||||
originalFileName.value = '';
|
||||
currentOrderId.value = '';
|
||||
isUploading.value = false;
|
||||
materialTypeIndex.value = 0;
|
||||
nftName.value = '';
|
||||
nftEvent.value = '';
|
||||
nftRemark.value = '';
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-container {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.castlove-content {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.background-image {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
object-fit: cover;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.background-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
padding: 100rpx 40rpx 40rpx 32rpx;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 图片上传区域 */
|
||||
.upload-section {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.upload-box {
|
||||
width: 500rpx;
|
||||
height: 500rpx;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
backdrop-filter: blur(10rpx);
|
||||
border-radius: 30rpx;
|
||||
border: 4rpx dashed rgba(255, 255, 255, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.upload-hint {
|
||||
margin-top: 20rpx;
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.uploaded-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.upload-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 表单区域 */
|
||||
.form-section {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 30rpx;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.form-item-picker {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 36rpx;
|
||||
color: #e6e6e6;
|
||||
font-weight: 500;
|
||||
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||||
padding: 18rpx 18rpx;
|
||||
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
|
||||
border-radius: 44rpx;
|
||||
display: inline-block;
|
||||
align-self: flex-start;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
|
||||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
/* 自定义选择器 */
|
||||
.custom-picker {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.picker-display {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
backdrop-filter: blur(10rpx);
|
||||
border-radius: 20rpx;
|
||||
padding: 0 30rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
text-shadow: 0 4rpx 4rpx rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.picker-display:active {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.picker-text {
|
||||
font-size: 32rpx;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
text-shadow: 0 4rpx 4rpx rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.picker-arrow {
|
||||
font-size: 48rpx;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-weight: bold;
|
||||
transform: rotate(90deg);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.picker-arrow-up {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
/* 下拉选项列表 */
|
||||
.picker-options {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
margin-top: 10rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(15rpx);
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.2);
|
||||
z-index: 10;
|
||||
animation: slideDown 0.3s ease-out;
|
||||
text-shadow: 0 4rpx 4rpx rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10rpx);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.picker-option {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
padding: 0 30rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
transition: background 0.2s ease;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.picker-option:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.picker-option:active {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.picker-option-active {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.picker-option-text {
|
||||
font-size: 32rpx;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
|
||||
.picker-option-check {
|
||||
font-size: 36rpx;
|
||||
color: #e6e6e6;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
backdrop-filter: blur(10rpx);
|
||||
border-radius: 20rpx;
|
||||
padding: 0 30rpx;
|
||||
font-size: 32rpx;
|
||||
color: #e6e6e6;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.input-placeholder {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-size: 32rpx;
|
||||
text-shadow: 0 4rpx 4rpx rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
min-height: 88rpx;
|
||||
max-height: 400rpx;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
backdrop-filter: blur(10rpx);
|
||||
border-radius: 20rpx;
|
||||
padding: 20rpx 30rpx;
|
||||
font-size: 32rpx;
|
||||
color: #e6e6e6;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.textarea-placeholder {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-size: 32rpx;
|
||||
text-shadow: 0 4rpx 4rpx rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* 底部按钮区域 */
|
||||
.button-section {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-top: auto;
|
||||
padding-top: 40rpx;
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.btn-secondary,
|
||||
.btn-primary {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
border-radius: 44rpx;
|
||||
font-size: 36rpx;
|
||||
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
|
||||
text-shadow: 0 4rpx 4rpx rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(10rpx);
|
||||
color: #e6e6e6;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
|
||||
color: #e6e6e6;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.btn-primary::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-secondary:active {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.nav-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
z-index: 999;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
66
frontend/pages/castlove/mall.vue
Normal file
66
frontend/pages/castlove/mall.vue
Normal file
@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<CastloveContent />
|
||||
|
||||
<!-- 蒙层 - 导航栏展开时显示 -->
|
||||
<view v-if="navExpanded" class="nav-mask" @click="navExpanded = false"></view>
|
||||
|
||||
<BottomNav :activeTab="2" :isExpanded="navExpanded" @update:activeTab="handleTabChange" @update:isExpanded="navExpanded = $event" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import BottomNav from "../components/BottomNav.vue";
|
||||
import CastloveContent from "../components/CastloveContent.vue";
|
||||
|
||||
const navExpanded = ref(false);
|
||||
|
||||
const handleTabChange = (newTab) => {
|
||||
const routes = [
|
||||
'/pages/square/square',
|
||||
'/pages/starbook/index',
|
||||
'/pages/castlove/mall',
|
||||
'/pages/starcity/index',
|
||||
'/pages/friends/index'
|
||||
];
|
||||
|
||||
if (newTab >= 0 && newTab < routes.length) {
|
||||
uni.redirectTo({
|
||||
url: routes[newTab]
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-container {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nav-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
z-index: 999;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
69
frontend/pages/friends/index.vue
Normal file
69
frontend/pages/friends/index.vue
Normal file
@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<Header :showBack="true" backIconColor="#e6e6e6" />
|
||||
<FriendsContent />
|
||||
|
||||
<!-- 蒙层 - 导航栏展开时显示 -->
|
||||
<view v-if="navExpanded" class="nav-mask" @click="navExpanded = false"></view>
|
||||
|
||||
<BottomNav :activeTab="4" :isExpanded="navExpanded" @update:activeTab="handleTabChange" @update:isExpanded="navExpanded = $event" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import Header from "../components/Header.vue";
|
||||
import BottomNav from "../components/BottomNav.vue";
|
||||
import FriendsContent from "../components/FriendsContent.vue";
|
||||
|
||||
const navExpanded = ref(false);
|
||||
|
||||
const handleTabChange = (newTab) => {
|
||||
const routes = [
|
||||
'/pages/square/square',
|
||||
'/pages/starbook/index',
|
||||
'/pages/castlove/mall',
|
||||
'/pages/starcity/index',
|
||||
'/pages/friends/index'
|
||||
];
|
||||
|
||||
if (newTab >= 0 && newTab < routes.length) {
|
||||
uni.redirectTo({
|
||||
url: routes[newTab]
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-container {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);
|
||||
}
|
||||
|
||||
.nav-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
z-index: 999;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -9,7 +9,7 @@
|
||||
</view>
|
||||
|
||||
<!-- Cabin 图标层(与背景同步移动) -->
|
||||
<view class="cabin-layer" :style="cabinLayerStyle" v-show="activeTab === 0">
|
||||
<view class="cabin-layer" :style="cabinLayerStyle">
|
||||
<view v-for="cabin in visibleCabins" :key="cabin.key" :id="'cabin-' + cabin.key" class="cabin-wrapper"
|
||||
:class="{ 'cabin-nickname-mine': cabin.isMine || cabin.nickname === currentUserNickname, 'cabin-slots-zero': cabin.sharedBoothSlotsRemaining === 0 }"
|
||||
:style="{ left: cabin.x + 'px', top: cabin.y + 'px', width: cabin.w + 'px' }"
|
||||
@ -27,8 +27,8 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 翻页箭头按钮(仅广场 Tab 显示) -->
|
||||
<view v-show="activeTab === 0" class="nav-arrows">
|
||||
<!-- 翻页箭头按钮 -->
|
||||
<view class="nav-arrows">
|
||||
<view class="arrow-btn arrow-left" @click="scrollPage(-1)">
|
||||
<text class="arrow-text">‹</text>
|
||||
</view>
|
||||
@ -38,10 +38,10 @@
|
||||
</view>
|
||||
|
||||
<!-- Header组件 -->
|
||||
<Header :showBack="activeTab !== 0" :showGuideIcon="activeTab === 0" :showTaskIcon="activeTab === 0" :showStarActivityIcon="activeTab === 0" backIconColor="#e6e6e6" />
|
||||
<Header :showGuideIcon="true" :showTaskIcon="true" :showStarActivityIcon="true" backIconColor="#e6e6e6" />
|
||||
|
||||
<!-- 轮播图 + 应援活动列表(仅广场 Tab 显示,swiper 切换) -->
|
||||
<view v-show="activeTab === 0" class="banner-carousel" @click.stop>
|
||||
<!-- 轮播图 + 应援活动列表 -->
|
||||
<view class="banner-carousel" @click.stop>
|
||||
<swiper class="banner-swiper" :autoplay="true" :interval="4000" :duration="400" :circular="true"
|
||||
:indicator-dots="false">
|
||||
<swiper-item @click.stop="showRankingModal = true">
|
||||
@ -54,11 +54,7 @@
|
||||
</swiper>
|
||||
</view>
|
||||
|
||||
<!-- 页面内容组件 - 使用 v-show 切换 -->
|
||||
<StarbookContent v-show="activeTab === 1" :isActive="activeTab === 1" />
|
||||
<CastloveContent v-show="activeTab === 2" @back="handleCastloveBack" />
|
||||
<StarCityContent v-show="activeTab === 3" />
|
||||
<FriendsContent v-show="activeTab === 4" />
|
||||
|
||||
|
||||
<!-- 蒙层 - 导航栏展开时显示 -->
|
||||
<view v-if="navExpanded" class="nav-mask" @click="navExpanded = false"></view>
|
||||
@ -68,7 +64,7 @@
|
||||
@update:visible="handleRankingModalClose" @visit="handleRankingVisit" />
|
||||
|
||||
<!-- 底部导航栏 -->
|
||||
<BottomNav :activeTab="activeTab" :isExpanded="navExpanded" @update:activeTab="handleTabChange"
|
||||
<BottomNav :activeTab="0" :isExpanded="navExpanded" @update:activeTab="handleTabChange"
|
||||
@update:isExpanded="navExpanded = $event" />
|
||||
|
||||
<!-- 全局引导遮罩 -->
|
||||
@ -100,10 +96,6 @@ import {
|
||||
} from "vuex";
|
||||
import Header from "../components/Header.vue";
|
||||
import BottomNav from "../components/BottomNav.vue";
|
||||
import CastloveContent from "../components/CastloveContent.vue";
|
||||
import StarbookContent from "../components/StarbookContent.vue";
|
||||
import StarCityContent from "../components/StarCityContent.vue";
|
||||
import FriendsContent from "../components/FriendsContent.vue";
|
||||
import GuideStartModal from "@/components/GuideStartModal.vue";
|
||||
import GuideOverlay from "@/components/GuideOverlay.vue";
|
||||
import {
|
||||
@ -840,7 +832,7 @@ onMounted(async () => {
|
||||
|
||||
// 从其他路由页面返回时重置
|
||||
onShow(() => {
|
||||
if (activeTab.value === 0) resetSquare();
|
||||
resetSquare();
|
||||
});
|
||||
|
||||
// 页面加载完成后初始化引导
|
||||
@ -889,7 +881,6 @@ const getBannerBottom = () => (screenWidth.value / 750) * 496;
|
||||
let touchInBanner = false;
|
||||
|
||||
const onBgTouchStart = (e) => {
|
||||
if (activeTab.value !== 0) return;
|
||||
const touchY = e.touches[0].clientY;
|
||||
touchInBanner = touchY < getBannerBottom();
|
||||
if (touchInBanner) return;
|
||||
@ -902,11 +893,9 @@ const onBgTouchStart = (e) => {
|
||||
};
|
||||
|
||||
const onBgTouchMove = (e) => {
|
||||
// 非 tab 0 时不阻止,让 scroll-view 正常滚动
|
||||
if (activeTab.value !== 0) return;
|
||||
// tab 0 时如果触摸在 banner 区域也不处理,让 banner 自己处理
|
||||
// 如果触摸在 banner 区域也不处理,让 banner 自己处理
|
||||
if (touchInBanner) return;
|
||||
// tab 0 且不在 banner 区域时,阻止默认行为以实现横向背景滑动
|
||||
// 不在 banner 区域时,阻止默认行为以实现横向背景滑动
|
||||
e.preventDefault();
|
||||
|
||||
const currentX = e.touches[0].clientX;
|
||||
@ -920,7 +909,7 @@ const onBgTouchMove = (e) => {
|
||||
};
|
||||
|
||||
const onBgTouchEnd = () => {
|
||||
if (activeTab.value !== 0 || touchInBanner) {
|
||||
if (touchInBanner) {
|
||||
touchInBanner = false;
|
||||
return;
|
||||
}
|
||||
@ -963,14 +952,25 @@ const handleCabinClick = (cabin) => {
|
||||
|
||||
// --- Tab 切换 ---
|
||||
const handleTabChange = (newTab) => {
|
||||
const prevTab = activeTab.value;
|
||||
activeTab.value = newTab;
|
||||
navExpanded.value = false;
|
||||
if (prevTab !== 0 && newTab === 0) resetSquare();
|
||||
};
|
||||
|
||||
const handleCastloveBack = () => {
|
||||
handleTabChange(0);
|
||||
if (newTab === 0) {
|
||||
// 已经在广场页面,不需要跳转
|
||||
navExpanded.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const routes = [
|
||||
'/pages/square/square',
|
||||
'/pages/starbook/index',
|
||||
'/pages/castlove/mall',
|
||||
'/pages/starcity/index',
|
||||
'/pages/friends/index'
|
||||
];
|
||||
|
||||
if (newTab >= 0 && newTab < routes.length) {
|
||||
uni.redirectTo({
|
||||
url: routes[newTab]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onLoad((options) => {
|
||||
@ -988,13 +988,6 @@ onLoad((options) => {
|
||||
console.log('[Guide] 调试模式已关闭')
|
||||
}
|
||||
}
|
||||
|
||||
if (options && options.tab) {
|
||||
const tabIndex = parseInt(options.tab);
|
||||
if (!isNaN(tabIndex) && tabIndex >= 0 && tabIndex <= 4) {
|
||||
activeTab.value = tabIndex;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
69
frontend/pages/starbook/index.vue
Normal file
69
frontend/pages/starbook/index.vue
Normal file
@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<Header :showBack="true" backIconColor="#e6e6e6" />
|
||||
<StarbookContent :isActive="true" />
|
||||
|
||||
<!-- 蒙层 - 导航栏展开时显示 -->
|
||||
<view v-if="navExpanded" class="nav-mask" @click="navExpanded = false"></view>
|
||||
|
||||
<BottomNav :activeTab="1" :isExpanded="navExpanded" @update:activeTab="handleTabChange" @update:isExpanded="navExpanded = $event" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import Header from "../components/Header.vue";
|
||||
import BottomNav from "../components/BottomNav.vue";
|
||||
import StarbookContent from "../components/StarbookContent.vue";
|
||||
|
||||
const navExpanded = ref(false);
|
||||
|
||||
const handleTabChange = (newTab) => {
|
||||
const routes = [
|
||||
'/pages/square/square',
|
||||
'/pages/starbook/index',
|
||||
'/pages/castlove/mall',
|
||||
'/pages/starcity/index',
|
||||
'/pages/friends/index'
|
||||
];
|
||||
|
||||
if (newTab >= 0 && newTab < routes.length) {
|
||||
uni.redirectTo({
|
||||
url: routes[newTab]
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-container {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);
|
||||
}
|
||||
|
||||
.nav-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
z-index: 999;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
69
frontend/pages/starcity/index.vue
Normal file
69
frontend/pages/starcity/index.vue
Normal file
@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<Header :showBack="true" backIconColor="#e6e6e6" />
|
||||
<StarCityContent />
|
||||
|
||||
<!-- 蒙层 - 导航栏展开时显示 -->
|
||||
<view v-if="navExpanded" class="nav-mask" @click="navExpanded = false"></view>
|
||||
|
||||
<BottomNav :activeTab="3" :isExpanded="navExpanded" @update:activeTab="handleTabChange" @update:isExpanded="navExpanded = $event" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import Header from "../components/Header.vue";
|
||||
import BottomNav from "../components/BottomNav.vue";
|
||||
import StarCityContent from "../components/StarCityContent.vue";
|
||||
|
||||
const navExpanded = ref(false);
|
||||
|
||||
const handleTabChange = (newTab) => {
|
||||
const routes = [
|
||||
'/pages/square/square',
|
||||
'/pages/starbook/index',
|
||||
'/pages/castlove/mall',
|
||||
'/pages/starcity/index',
|
||||
'/pages/friends/index'
|
||||
];
|
||||
|
||||
if (newTab >= 0 && newTab < routes.length) {
|
||||
uni.redirectTo({
|
||||
url: routes[newTab]
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-container {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);
|
||||
}
|
||||
|
||||
.nav-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
z-index: 999;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,6 +1,6 @@
|
||||
// API 基础配置
|
||||
const baseURL = 'http://101.132.250.62:8080'
|
||||
// const baseURL = 'http://192.168.110.60:8080'
|
||||
// const baseURL = 'http://101.132.250.62:8080'
|
||||
const baseURL = 'http://192.168.110.60:8080'
|
||||
// const baseURL = 'http://localhost:8080'
|
||||
|
||||
// 是否使用模拟数据(开发调试时设为 true,后端API准备好后改为 false)
|
||||
|
||||
@ -72,13 +72,13 @@ export function extractOssObjectPath(url) {
|
||||
*/
|
||||
export async function getAssetCoverRealUrl(coverUrl) {
|
||||
// 默认图片路径
|
||||
const DEFAULT_IMAGE = '/static/nft/collection.png';
|
||||
const DEFAULT_IMAGE = '';
|
||||
|
||||
// 如果是本地静态资源,直接返回
|
||||
if (!coverUrl || coverUrl.startsWith('/static/')) {
|
||||
return coverUrl || DEFAULT_IMAGE;
|
||||
}
|
||||
|
||||
console.log(coverUrl)
|
||||
try {
|
||||
// 提取完整OSS对象路径(保留 asset/13/87/filename.jpg 这样的前缀)
|
||||
const objectPath = extractOssObjectPath(coverUrl);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user