feat: 修改的铸造界面

This commit is contained in:
zerosaturation 2026-04-10 21:05:19 +08:00
parent de207465f4
commit 45e0ad0227
19 changed files with 3000 additions and 826 deletions

View File

@ -1,2 +1 @@
# TopFans
背景有什么想法?边框想要什么样式的?有没有特别想加的装饰元素?整体材质的效果想要什么样的?主色调是应援色吗,还是别的颜色?

View File

@ -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 {

View File

@ -83,6 +83,7 @@ func ConvertAsset(pbAsset *pbAsset.Asset) AssetDTO {
CreatedAt: pbAsset.CreatedAt,
UpdatedAt: pbAsset.UpdatedAt,
IsLiked: pbAsset.IsLiked,
Info: pbAsset.Info,
}
// 可选字段

View File

@ -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 持有者信息

View File

@ -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 事件)

View File

@ -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 ""
}

View File

@ -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; // URLOSS
string description = 4; //
int32 rarity = 5; //
repeated string tags = 6; //
string material_type = 7; //
string event = 8; //
string info = 8; //
// cover_url AI
}

View File

@ -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": {

View File

@ -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('调用 getAssetDetailApiassetId:', 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(() => {

File diff suppressed because it is too large Load Diff

View 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">支持JPGPNG格式大小不超过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(''); // OSSURL
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 H5App
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. OSStype='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. FormDataOSS
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>

View 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

View 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>

View File

@ -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>

View 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>

View 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>

View File

@ -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

View File

@ -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);