feat: 修改的铸造界面
This commit is contained in:
parent
de207465f4
commit
45e0ad0227
@ -1,2 +1 @@
|
|||||||
# TopFans
|
# TopFans
|
||||||
背景有什么想法?边框想要什么样式的?有没有特别想加的装饰元素?整体材质的效果想要什么样的?主色调是应援色吗,还是别的颜色?
|
|
||||||
@ -276,7 +276,7 @@ func (ctrl *AssetController) CreateMintOrder(c *gin.Context) {
|
|||||||
Rarity: req.Rarity,
|
Rarity: req.Rarity,
|
||||||
Tags: req.Tags,
|
Tags: req.Tags,
|
||||||
MaterialType: req.MaterialType,
|
MaterialType: req.MaterialType,
|
||||||
Event: req.Event,
|
Info: req.Info,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -83,6 +83,7 @@ func ConvertAsset(pbAsset *pbAsset.Asset) AssetDTO {
|
|||||||
CreatedAt: pbAsset.CreatedAt,
|
CreatedAt: pbAsset.CreatedAt,
|
||||||
UpdatedAt: pbAsset.UpdatedAt,
|
UpdatedAt: pbAsset.UpdatedAt,
|
||||||
IsLiked: pbAsset.IsLiked,
|
IsLiked: pbAsset.IsLiked,
|
||||||
|
Info: pbAsset.Info,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 可选字段
|
// 可选字段
|
||||||
|
|||||||
@ -4,14 +4,14 @@ package dto
|
|||||||
|
|
||||||
// CreateMintOrderRequestDTO 创建铸造订单请求
|
// CreateMintOrderRequestDTO 创建铸造订单请求
|
||||||
type CreateMintOrderRequestDTO struct {
|
type CreateMintOrderRequestDTO struct {
|
||||||
OrderID string `json:"order_id" binding:"required"` // 阶段一生成的订单ID(必填)
|
OrderID string `json:"order_id" binding:"required"` // 阶段一生成的订单ID(必填)
|
||||||
Name string `json:"name" binding:"required"` // 藏品名称(必填)
|
Name string `json:"name"` // 藏品名称(可选)
|
||||||
MaterialURL string `json:"material_url" binding:"required"` // 用户上传的素材URL(必填,前端已上传到OSS)
|
MaterialURL string `json:"material_url" binding:"required"` // 用户上传的素材URL(必填,前端已上传到OSS)
|
||||||
Description string `json:"description"` // 藏品描述(可选)
|
Description string `json:"description"` // 藏品描述(可选)
|
||||||
Rarity int32 `json:"rarity"` // 稀有度(可选)
|
Rarity int32 `json:"rarity"` // 稀有度(可选)
|
||||||
Tags []string `json:"tags"` // 标签列表(可选)
|
Tags []string `json:"tags"` // 标签列表(可选)
|
||||||
MaterialType string `json:"material_type"` // 素材类型(可选)
|
MaterialType string `json:"material_type"` // 素材类型(可选)
|
||||||
Event string `json:"event"` // 藏品事件(可选)
|
Info string `json:"info" binding:"required"` // 藏品信息(必填)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PreCreateMintOrderRequestDTO 阶段一:预创建铸造订单(返回 order_id)
|
// PreCreateMintOrderRequestDTO 阶段一:预创建铸造订单(返回 order_id)
|
||||||
@ -79,6 +79,7 @@ type AssetDTO struct {
|
|||||||
MintedAt int64 `json:"minted_at,omitempty"` // 上链成功时间(毫秒时间戳,可选)
|
MintedAt int64 `json:"minted_at,omitempty"` // 上链成功时间(毫秒时间戳,可选)
|
||||||
Owner *OwnerInfoDTO `json:"owner,omitempty"` // 持有者信息(可选,保留用于兼容性)
|
Owner *OwnerInfoDTO `json:"owner,omitempty"` // 持有者信息(可选,保留用于兼容性)
|
||||||
IsLiked bool `json:"is_liked"` // 当前用户是否已点赞
|
IsLiked bool `json:"is_liked"` // 当前用户是否已点赞
|
||||||
|
Info string `json:"info"` // 藏品信息
|
||||||
}
|
}
|
||||||
|
|
||||||
// OwnerInfoDTO 持有者信息
|
// OwnerInfoDTO 持有者信息
|
||||||
|
|||||||
@ -22,6 +22,7 @@ type Asset struct {
|
|||||||
Rarity *int32 `gorm:"column:rarity"` // 稀有度(预留)
|
Rarity *int32 `gorm:"column:rarity"` // 稀有度(预留)
|
||||||
Tags StringArray `gorm:"type:jsonb;column:tags"` // 标签(预留,JSON数组)
|
Tags StringArray `gorm:"type:jsonb;column:tags"` // 标签(预留,JSON数组)
|
||||||
Visibility string `gorm:"type:varchar(20);default:'private';column:visibility"` // 可见性:private, friends, public(预留)
|
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
|
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"` // 藏品描述(阶段一写入,可选)
|
Description *string `gorm:"type:text;column:description"` // 藏品描述(阶段一写入,可选)
|
||||||
MaterialType *string `gorm:"type:varchar(50);column:material_type"` // 素材类型(可选)
|
MaterialType *string `gorm:"type:varchar(50);column:material_type"` // 素材类型(可选)
|
||||||
Event *string `gorm:"type:varchar(100);column:event"` // 藏品事件(可选)
|
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"`
|
CreatedAt int64 `gorm:"not null;index:idx_mint_orders_created_at,sort:desc;column:created_at"`
|
||||||
UpdatedAt int64 `gorm:"not null;column:updated_at"`
|
UpdatedAt int64 `gorm:"not null;column:updated_at"`
|
||||||
MintedAt *int64 `gorm:"column:minted_at"` // 上链成功时间(用于 Task Service 事件)
|
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"` // 持有者信息(保留用于兼容性)
|
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下的昵称)
|
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"` // 当前用户是否已点赞
|
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
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@ -221,6 +222,13 @@ func (x *Asset) GetIsLiked() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *Asset) GetInfo() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Info
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// 持有者信息
|
// 持有者信息
|
||||||
type OwnerInfo struct {
|
type OwnerInfo struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
@ -548,13 +556,13 @@ func (x *InitMintOrderResponse) GetOrder() *MintOrder {
|
|||||||
type CreateMintOrderRequest struct {
|
type CreateMintOrderRequest struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
OrderId string `protobuf:"bytes,2,opt,name=order_id,json=orderId,proto3" json:"order_id,omitempty"` // 阶段一生成的订单ID(必填)
|
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)
|
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"` // 藏品描述(可选)
|
Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` // 藏品描述(可选)
|
||||||
Rarity int32 `protobuf:"varint,5,opt,name=rarity,proto3" json:"rarity,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"` // 标签列表(可选)
|
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"` // 素材类型(可选)
|
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
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@ -638,9 +646,9 @@ func (x *CreateMintOrderRequest) GetMaterialType() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *CreateMintOrderRequest) GetEvent() string {
|
func (x *CreateMintOrderRequest) GetInfo() string {
|
||||||
if x != nil {
|
if x != nil {
|
||||||
return x.Event
|
return x.Info
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,11 +28,12 @@ message Asset {
|
|||||||
int64 created_at = 15; // 创建时间(毫秒时间戳)
|
int64 created_at = 15; // 创建时间(毫秒时间戳)
|
||||||
int64 updated_at = 16; // 更新时间(毫秒时间戳)
|
int64 updated_at = 16; // 更新时间(毫秒时间戳)
|
||||||
int64 minted_at = 17; // 上链成功时间(毫秒时间戳,可选)
|
int64 minted_at = 17; // 上链成功时间(毫秒时间戳,可选)
|
||||||
|
|
||||||
// 扩展字段:持有者信息(用于详情展示)
|
// 扩展字段:持有者信息(用于详情展示)
|
||||||
OwnerInfo owner = 18; // 持有者信息(保留用于兼容性)
|
OwnerInfo owner = 18; // 持有者信息(保留用于兼容性)
|
||||||
string owner_nickname = 19; // 持有者昵称(在该star下的昵称)
|
string owner_nickname = 19; // 持有者昵称(在该star下的昵称)
|
||||||
bool is_liked = 20; // 当前用户是否已点赞
|
bool is_liked = 20; // 当前用户是否已点赞
|
||||||
|
string info = 21; // 藏品信息
|
||||||
}
|
}
|
||||||
|
|
||||||
// 持有者信息
|
// 持有者信息
|
||||||
@ -77,13 +78,13 @@ message InitMintOrderResponse {
|
|||||||
// 创建铸造订单请求
|
// 创建铸造订单请求
|
||||||
message CreateMintOrderRequest {
|
message CreateMintOrderRequest {
|
||||||
string order_id = 2; // 阶段一生成的订单ID(必填)
|
string order_id = 2; // 阶段一生成的订单ID(必填)
|
||||||
string name = 1; // 藏品名称(必填,可覆盖阶段一保存值)
|
string name = 1; // 藏品名称(可选)
|
||||||
string material_url = 3; // 用户上传的素材URL(必填,前端已上传到OSS)
|
string material_url = 3; // 用户上传的素材URL(必填,前端已上传到OSS)
|
||||||
string description = 4; // 藏品描述(可选)
|
string description = 4; // 藏品描述(可选)
|
||||||
int32 rarity = 5; // 稀有度(可选)
|
int32 rarity = 5; // 稀有度(可选)
|
||||||
repeated string tags = 6; // 标签列表(可选)
|
repeated string tags = 6; // 标签列表(可选)
|
||||||
string material_type = 7; // 素材类型(可选)
|
string material_type = 7; // 素材类型(可选)
|
||||||
string event = 8; // 藏品事件(可选)
|
string info = 8; // 藏品信息(必填)
|
||||||
// cover_url 不再需要前端传入,由后端 AI 处理完成后自动生成
|
// 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",
|
"path": "pages/login/login",
|
||||||
"style": {
|
"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",
|
"path": "pages/castlove/success",
|
||||||
"style": {
|
"style": {
|
||||||
|
|||||||
@ -114,15 +114,15 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onUnmounted } from 'vue';
|
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 NftCard from '../components/NftCard.vue';
|
||||||
import { getAssetDetailApi, getMintOrderDetailApi, likeAssetApi, unlikeAssetApi } from '@/utils/api.js';
|
import { getAssetDetailApi, getMintOrderDetailApi, likeAssetApi, unlikeAssetApi } from '@/utils/api.js';
|
||||||
import { getAssetCoverRealUrl } from '@/utils/assetImageHelper.js';
|
import { getAssetCoverRealUrl } from '@/utils/assetImageHelper.js';
|
||||||
|
|
||||||
// 页面参数
|
// 页面参数
|
||||||
const assetIdParam = ref('');
|
const assetIdParam = ref('');
|
||||||
const orderIdParam = ref('');
|
const orderIdParam = ref('');
|
||||||
const fromParam = ref('');
|
const fromParam = ref('');
|
||||||
|
const lastLoadKey = ref(''); // 记录上次加载的key,避免重复加载
|
||||||
|
|
||||||
// 数据状态
|
// 数据状态
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
@ -188,16 +188,21 @@ const copyHash = () => {
|
|||||||
|
|
||||||
// 加载数据
|
// 加载数据
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
|
console.log('loadData 开始执行');
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
loadError.value = '';
|
loadError.value = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let assetId = assetIdParam.value;
|
let assetId = assetIdParam.value;
|
||||||
|
console.log('assetIdParam:', assetIdParam.value, 'orderIdParam:', orderIdParam.value);
|
||||||
|
|
||||||
if (!assetId && orderIdParam.value) {
|
if (!assetId && orderIdParam.value) {
|
||||||
|
console.log('通过 order_id 获取资产信息');
|
||||||
const mintRes = await getMintOrderDetailApi(orderIdParam.value);
|
const mintRes = await getMintOrderDetailApi(orderIdParam.value);
|
||||||
|
console.log('getMintOrderDetailApi 响应:', mintRes);
|
||||||
if (mintRes.code === 200 && mintRes.data?.asset?.asset_id) {
|
if (mintRes.code === 200 && mintRes.data?.asset?.asset_id) {
|
||||||
assetId = mintRes.data.asset.asset_id;
|
assetId = mintRes.data.asset.asset_id;
|
||||||
|
console.log('获取到 assetId:', assetId);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('获取铸造订单详情失败');
|
throw new Error('获取铸造订单详情失败');
|
||||||
}
|
}
|
||||||
@ -205,7 +210,9 @@ const loadData = async () => {
|
|||||||
|
|
||||||
if (!assetId) throw new Error('藏品信息不完整');
|
if (!assetId) throw new Error('藏品信息不完整');
|
||||||
|
|
||||||
|
console.log('调用 getAssetDetailApi,assetId:', assetId);
|
||||||
const res = await getAssetDetailApi(assetId);
|
const res = await getAssetDetailApi(assetId);
|
||||||
|
console.log('getAssetDetailApi 响应:', res);
|
||||||
if (res.code === 200 && res.data?.asset) {
|
if (res.code === 200 && res.data?.asset) {
|
||||||
const asset = res.data.asset;
|
const asset = res.data.asset;
|
||||||
assetData.value = asset;
|
assetData.value = asset;
|
||||||
@ -216,15 +223,26 @@ const loadData = async () => {
|
|||||||
remainSeconds.value = asset.remain_time;
|
remainSeconds.value = asset.remain_time;
|
||||||
startCountdown();
|
startCountdown();
|
||||||
}
|
}
|
||||||
|
console.log(res.data)
|
||||||
coverUrl.value = await getAssetCoverRealUrl(asset.cover_url);
|
// 异步加载封面图片,不阻塞页面渲染
|
||||||
|
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 {
|
} else {
|
||||||
throw new Error(res.message || '获取藏品详情失败');
|
throw new Error(res.message || '获取藏品详情失败');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error('loadData 出错:', err);
|
||||||
loadError.value = err.message || '加载失败,请重试';
|
loadError.value = err.message || '加载失败,请重试';
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
console.log('loadData 执行完成,loading:', loading.value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -277,10 +295,22 @@ const handleBack = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onLoad((options) => {
|
onLoad((options) => {
|
||||||
|
console.log('onLoad 触发,参数:', options);
|
||||||
assetIdParam.value = options?.asset_id || '';
|
assetIdParam.value = options?.asset_id || '';
|
||||||
orderIdParam.value = options?.order_id || '';
|
orderIdParam.value = options?.order_id || '';
|
||||||
fromParam.value = options?.from || '';
|
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(() => {
|
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>
|
</view>
|
||||||
|
|
||||||
<!-- Cabin 图标层(与背景同步移动) -->
|
<!-- 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"
|
<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 }"
|
: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' }"
|
:style="{ left: cabin.x + 'px', top: cabin.y + 'px', width: cabin.w + 'px' }"
|
||||||
@ -27,8 +27,8 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 翻页箭头按钮(仅广场 Tab 显示) -->
|
<!-- 翻页箭头按钮 -->
|
||||||
<view v-show="activeTab === 0" class="nav-arrows">
|
<view class="nav-arrows">
|
||||||
<view class="arrow-btn arrow-left" @click="scrollPage(-1)">
|
<view class="arrow-btn arrow-left" @click="scrollPage(-1)">
|
||||||
<text class="arrow-text">‹</text>
|
<text class="arrow-text">‹</text>
|
||||||
</view>
|
</view>
|
||||||
@ -38,10 +38,10 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- Header组件 -->
|
<!-- 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"
|
<swiper class="banner-swiper" :autoplay="true" :interval="4000" :duration="400" :circular="true"
|
||||||
:indicator-dots="false">
|
:indicator-dots="false">
|
||||||
<swiper-item @click.stop="showRankingModal = true">
|
<swiper-item @click.stop="showRankingModal = true">
|
||||||
@ -54,11 +54,7 @@
|
|||||||
</swiper>
|
</swiper>
|
||||||
</view>
|
</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>
|
<view v-if="navExpanded" class="nav-mask" @click="navExpanded = false"></view>
|
||||||
@ -68,7 +64,7 @@
|
|||||||
@update:visible="handleRankingModalClose" @visit="handleRankingVisit" />
|
@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" />
|
@update:isExpanded="navExpanded = $event" />
|
||||||
|
|
||||||
<!-- 全局引导遮罩 -->
|
<!-- 全局引导遮罩 -->
|
||||||
@ -100,10 +96,6 @@ import {
|
|||||||
} from "vuex";
|
} from "vuex";
|
||||||
import Header from "../components/Header.vue";
|
import Header from "../components/Header.vue";
|
||||||
import BottomNav from "../components/BottomNav.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 GuideStartModal from "@/components/GuideStartModal.vue";
|
||||||
import GuideOverlay from "@/components/GuideOverlay.vue";
|
import GuideOverlay from "@/components/GuideOverlay.vue";
|
||||||
import {
|
import {
|
||||||
@ -840,7 +832,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
// 从其他路由页面返回时重置
|
// 从其他路由页面返回时重置
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
if (activeTab.value === 0) resetSquare();
|
resetSquare();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 页面加载完成后初始化引导
|
// 页面加载完成后初始化引导
|
||||||
@ -889,7 +881,6 @@ const getBannerBottom = () => (screenWidth.value / 750) * 496;
|
|||||||
let touchInBanner = false;
|
let touchInBanner = false;
|
||||||
|
|
||||||
const onBgTouchStart = (e) => {
|
const onBgTouchStart = (e) => {
|
||||||
if (activeTab.value !== 0) return;
|
|
||||||
const touchY = e.touches[0].clientY;
|
const touchY = e.touches[0].clientY;
|
||||||
touchInBanner = touchY < getBannerBottom();
|
touchInBanner = touchY < getBannerBottom();
|
||||||
if (touchInBanner) return;
|
if (touchInBanner) return;
|
||||||
@ -902,11 +893,9 @@ const onBgTouchStart = (e) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onBgTouchMove = (e) => {
|
const onBgTouchMove = (e) => {
|
||||||
// 非 tab 0 时不阻止,让 scroll-view 正常滚动
|
// 如果触摸在 banner 区域也不处理,让 banner 自己处理
|
||||||
if (activeTab.value !== 0) return;
|
|
||||||
// tab 0 时如果触摸在 banner 区域也不处理,让 banner 自己处理
|
|
||||||
if (touchInBanner) return;
|
if (touchInBanner) return;
|
||||||
// tab 0 且不在 banner 区域时,阻止默认行为以实现横向背景滑动
|
// 不在 banner 区域时,阻止默认行为以实现横向背景滑动
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const currentX = e.touches[0].clientX;
|
const currentX = e.touches[0].clientX;
|
||||||
@ -920,7 +909,7 @@ const onBgTouchMove = (e) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onBgTouchEnd = () => {
|
const onBgTouchEnd = () => {
|
||||||
if (activeTab.value !== 0 || touchInBanner) {
|
if (touchInBanner) {
|
||||||
touchInBanner = false;
|
touchInBanner = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -963,14 +952,25 @@ const handleCabinClick = (cabin) => {
|
|||||||
|
|
||||||
// --- Tab 切换 ---
|
// --- Tab 切换 ---
|
||||||
const handleTabChange = (newTab) => {
|
const handleTabChange = (newTab) => {
|
||||||
const prevTab = activeTab.value;
|
if (newTab === 0) {
|
||||||
activeTab.value = newTab;
|
// 已经在广场页面,不需要跳转
|
||||||
navExpanded.value = false;
|
navExpanded.value = false;
|
||||||
if (prevTab !== 0 && newTab === 0) resetSquare();
|
return;
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleCastloveBack = () => {
|
const routes = [
|
||||||
handleTabChange(0);
|
'/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) => {
|
onLoad((options) => {
|
||||||
@ -988,13 +988,6 @@ onLoad((options) => {
|
|||||||
console.log('[Guide] 调试模式已关闭')
|
console.log('[Guide] 调试模式已关闭')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options && options.tab) {
|
|
||||||
const tabIndex = parseInt(options.tab);
|
|
||||||
if (!isNaN(tabIndex) && tabIndex >= 0 && tabIndex <= 4) {
|
|
||||||
activeTab.value = tabIndex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</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 基础配置
|
// API 基础配置
|
||||||
const baseURL = 'http://101.132.250.62:8080'
|
// const baseURL = 'http://101.132.250.62:8080'
|
||||||
// const baseURL = 'http://192.168.110.60:8080'
|
const baseURL = 'http://192.168.110.60:8080'
|
||||||
// const baseURL = 'http://localhost:8080'
|
// const baseURL = 'http://localhost:8080'
|
||||||
|
|
||||||
// 是否使用模拟数据(开发调试时设为 true,后端API准备好后改为 false)
|
// 是否使用模拟数据(开发调试时设为 true,后端API准备好后改为 false)
|
||||||
|
|||||||
@ -72,13 +72,13 @@ export function extractOssObjectPath(url) {
|
|||||||
*/
|
*/
|
||||||
export async function getAssetCoverRealUrl(coverUrl) {
|
export async function getAssetCoverRealUrl(coverUrl) {
|
||||||
// 默认图片路径
|
// 默认图片路径
|
||||||
const DEFAULT_IMAGE = '/static/nft/collection.png';
|
const DEFAULT_IMAGE = '';
|
||||||
|
|
||||||
// 如果是本地静态资源,直接返回
|
// 如果是本地静态资源,直接返回
|
||||||
if (!coverUrl || coverUrl.startsWith('/static/')) {
|
if (!coverUrl || coverUrl.startsWith('/static/')) {
|
||||||
return coverUrl || DEFAULT_IMAGE;
|
return coverUrl || DEFAULT_IMAGE;
|
||||||
}
|
}
|
||||||
|
console.log(coverUrl)
|
||||||
try {
|
try {
|
||||||
// 提取完整OSS对象路径(保留 asset/13/87/filename.jpg 这样的前缀)
|
// 提取完整OSS对象路径(保留 asset/13/87/filename.jpg 这样的前缀)
|
||||||
const objectPath = extractOssObjectPath(coverUrl);
|
const objectPath = extractOssObjectPath(coverUrl);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user