feat: 修改各种小bug

This commit is contained in:
zerosaturation 2026-05-17 23:33:45 +08:00
parent 78c70e14df
commit 2450fc1368
17 changed files with 367 additions and 269 deletions

View File

@ -27,6 +27,9 @@ type AssetRepository interface {
// GetDisplayStatusByAssetID 根据asset_id查询展示状态从asset_registry表
GetDisplayStatusByAssetID(assetID int64) (int32, error)
// GetGradeByAssetID 根据asset_id查询藏品等级从asset_registry表
GetGradeByAssetID(assetID int64) (int32, error)
// GetByOwner 查询用户的资产列表
GetByOwner(ownerUID, starID int64, limit, offset int) ([]*models.Asset, error)
@ -127,6 +130,27 @@ func (r *assetRepository) GetDisplayStatusByAssetID(assetID int64) (int32, error
return registry.DisplayStatus, nil
}
// GetGradeByAssetID 根据asset_id查询藏品等级从asset_registry表
func (r *assetRepository) GetGradeByAssetID(assetID int64) (int32, error) {
if assetID <= 0 {
return 0, errors.New("asset_id must be greater than 0")
}
var registry models.AssetRegistry
// 直接使用表名查询 public.asset_registry
if err := r.db.Table("public.asset_registry").Where("asset_id = ?", assetID).First(&registry).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return 0, nil // 没找到返回0不报错
}
return 0, err
}
if registry.Grade != nil {
return *registry.Grade, nil
}
return 0, nil
}
// GetByIDs 批量查询资产
func (r *assetRepository) GetByIDs(assetIDs []int64) ([]*models.Asset, error) {
if len(assetIDs) == 0 {

View File

@ -42,6 +42,16 @@ func (r *MaterialRepository) FindByHash(hash string, starID int64) (*models.Mate
return &m, nil
}
// FindByHashAndOssKey 根据 hash 和 oss_key 查找(双重去重)
func (r *MaterialRepository) FindByHashAndOssKey(hash string, ossKey string, starID int64) (*models.Material, error) {
var m models.Material
err := r.db.Where("hash = ? AND oss_key = ? AND star_id = ? AND deleted_at IS NULL", hash, ossKey, starID).First(&m).Error
if err != nil {
return nil, err
}
return &m, nil
}
// SoftDelete 软删除素材
func (r *MaterialRepository) SoftDelete(materialID int64) error {
now := time.Now().UnixMilli()

View File

@ -490,6 +490,16 @@ func (s *assetService) GetAsset(req *pb.GetAssetRequest, userID, starID int64) (
exhibitionExpireAt, _ = s.assetRepo.GetExhibitionExpireTime(asset.ID)
}
// 6.5 从 asset_registry 表获取 grade
grade, err := s.assetRepo.GetGradeByAssetID(asset.ID)
if err != nil {
logger.Logger.Warn("Failed to get grade, will return 0",
zap.Int64("asset_id", asset.ID),
zap.Error(err),
)
grade = 0
}
// 7. 构建响应
response := &pb.GetAssetResponse{
Base: &pbCommon.BaseResponse{
@ -497,7 +507,7 @@ func (s *assetService) GetAsset(req *pb.GetAssetRequest, userID, starID int64) (
Message: "",
Timestamp: time.Now().UnixMilli(),
},
Asset: ModelToProtoAssetDetail(asset, ownerNickname, isLiked, displayStatus, earnings, exhibitionExpireAt),
Asset: ModelToProtoAssetDetail(asset, ownerNickname, isLiked, displayStatus, earnings, exhibitionExpireAt, grade),
}
logger.Logger.Debug("Get asset successful",
@ -646,7 +656,7 @@ func ModelToProtoAsset(asset *models.Asset) *pb.AssetListItem {
}
// ModelToProtoAssetDetail 将数据库模型转换为Proto格式Asset详情
func ModelToProtoAssetDetail(asset *models.Asset, ownerNickname string, isLiked bool, displayStatus int32, earnings int64, exhibitionExpireAt int64) *pb.Asset {
func ModelToProtoAssetDetail(asset *models.Asset, ownerNickname string, isLiked bool, displayStatus int32, earnings int64, exhibitionExpireAt int64, grade int32) *pb.Asset {
if asset == nil {
return nil
}
@ -659,7 +669,7 @@ func ModelToProtoAssetDetail(asset *models.Asset, ownerNickname string, isLiked
CoverUrl: asset.CoverURL,
MaterialUrl: getStringValue(asset.MaterialURL),
Description: getStringValue(asset.Description),
Grade: getInt32Value(asset.Grade),
Grade: grade,
Tags: []string(asset.Tags),
Visibility: asset.Visibility,
Status: asset.Status,

View File

@ -22,9 +22,10 @@ func NewMaterialService(materialRepo *repository.MaterialRepository, relationRep
}
}
// UploadMaterial 上传素材(含 hash 去重)
// UploadMaterial 上传素材(含 hash + oss_key 双重去重)
func (s *MaterialService) UploadMaterial(m *models.Material) (*models.Material, error) {
existing, err := s.materialRepo.FindByHash(m.Hash, m.StarID)
// 先按 hash + oss_key 双重判断,避免同一张图被错误复用
existing, err := s.materialRepo.FindByHashAndOssKey(m.Hash, m.OssKey, m.StarID)
if err == nil && existing != nil {
return existing, nil
}
@ -32,6 +33,22 @@ func (s *MaterialService) UploadMaterial(m *models.Material) (*models.Material,
return nil, err
}
// 如果没找到,再按单纯 hash 查找(兼容旧数据)
existing, err = s.materialRepo.FindByHash(m.Hash, m.StarID)
if err == nil && existing != nil {
// 找到旧记录但 oss_key 不同,说明是不同文件但 hash 巧合相同,需要创建新记录
if existing.OssKey != m.OssKey {
if err := s.materialRepo.Create(m); err != nil {
return nil, err
}
return m, nil
}
return existing, nil
}
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
if err := s.materialRepo.Create(m); err != nil {
return nil, err
}

View File

@ -464,7 +464,7 @@ func (s *mintService) CreateMintOrder(req *pb.CreateMintOrderRequest, userID, st
Timestamp: time.Now().UnixMilli(),
},
Order: ModelToProtoMintOrder(mintOrder),
Asset: ModelToProtoAssetDetail(asset, ownerNickname, false, 0, 0, 0), // 新创建的资产is_liked 为 falsedisplay_status 默认为 0earnings 和 exhibitionExpireAt 为 0
Asset: ModelToProtoAssetDetail(asset, ownerNickname, false, 0, 0, 0, getInt32Value(asset.Grade)), // 新创建的资产is_liked 为 falsedisplay_status 默认为 0earnings 和 exhibitionExpireAt 为 0grade 从 asset.Grade 获取
CostCrystal: capturedCostCrystal,
BalanceAfter: newBalance,
}
@ -569,7 +569,7 @@ func (s *mintService) GetMintOrder(orderID string, userID, starID int64) (*pb.Ge
// 由于是查询自己的订单is_liked 设为 false简化处理
// 获取 display_status
displayStatus, _ := s.assetRepo.GetDisplayStatusByAssetID(asset.ID)
assetProto = ModelToProtoAssetDetail(asset, ownerNickname, false, displayStatus, 0, 0) // 新创建的资产earnings 和 exhibitionExpireAt 为 0
assetProto = ModelToProtoAssetDetail(asset, ownerNickname, false, displayStatus, 0, 0, getInt32Value(asset.Grade)) // 新创建的资产earnings 和 exhibitionExpireAt 为 0
// 如果 cover_url 存在,生成预签名 URL
if assetProto.CoverUrl != "" {

View File

@ -14,8 +14,11 @@ DECLARE
asset_rec record;
i int := 0;
BEGIN
-- 收集star_id=87的所有可用slot_id
SELECT array_agg(slot_id ORDER BY slot_id) INTO slot_ids FROM booth_slots WHERE star_id = 87;
-- 收集star_id=87的所有可用slot_id排除已被用户1占用的
SELECT array_agg(slot_id ORDER BY slot_id) INTO slot_ids
FROM booth_slots
WHERE star_id = 87
AND slot_id NOT IN (SELECT slot_id FROM exhibitions WHERE occupier_uid = 1 AND deleted_at IS NULL);
-- 为每个mock资产创建展示记录
FOR asset_rec IN SELECT id, owner_uid FROM assets WHERE id >= 10000 ORDER BY id LOOP

View File

@ -109,7 +109,7 @@ const cardRotateStyle = computed(() => {
const x = tiltVisualX.value
const rotateY = (x / 120) * 12
return {
// transform: `perspective(1000px) rotateY(${rotateY}deg) rotateX(0deg)`,
transform: `perspective(1000px) rotateY(${rotateY}deg) rotateX(0deg)`,
}
})
@ -118,7 +118,7 @@ const shimmerStyle = computed(() => {
const angle = 135 + (x / 120) * 60
const a = Math.max(0, Math.min(0.35, Number(props.shimmerMidOpacity) || 0.1))
return {
// background: `linear-gradient(${angle}deg, transparent 30%, rgba(221,183,255,${a}) 50%, transparent 70%)`,
background: `linear-gradient(${angle}deg, transparent 30%, rgba(221,183,255,${a}) 50%, transparent 70%)`,
}
})
@ -127,11 +127,11 @@ function getLayerStyle(layer) {
const baseOpacity = t ? t.opacity : layer.opacity
const x = t ? t.x : 0
// mix-blend-mode / WebView normal
// return {
// opacity: baseOpacity,
// transform: `translate3d(${x}px, 0, 0)`,
// background: layer.background || 'transparent',
// }
return {
opacity: baseOpacity,
transform: `translate3d(${x}px, 0, 0)`,
background: layer.background || 'transparent',
}
}
function getDotStyle(dot) {

View File

@ -27,7 +27,20 @@
"Push" : {}
},
"nativePlugins" : {
"imengyu-UniAndroidGyro" : {}
"imengyu-UniAndroidGyro" : {
"__plugin_info__" : {
"name" : "imengyu-UniAndroidGyro",
"description" : "APP端陀螺仪数据采集",
"platforms" : "Android,iOS",
"url" : "",
"android_package_name" : "",
"ios_bundle_id" : "",
"isCloud" : false,
"bought" : -1,
"pid" : "",
"parameters" : {}
}
}
},
/* */
"distribute" : {
@ -51,7 +64,9 @@
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>",
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
"<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>",
"<uses-permission android:name=\"android.permission.INTERNET\"/>"
"<uses-permission android:name=\"android.permission.INTERNET\"/>",
"<uses-permission android:name=\"android.permission.BODY_SENSORS\"/>",
"<uses-feature android:name=\"android.hardware.sensor.gyroscope\"/>"
],
"abiFilters" : [ "armeabi-v7a", "arm64-v8a" ]
},
@ -80,33 +95,33 @@
},
"icons" : {
"android" : {
"hdpi" : "static/app-icons/72x72.png",
"xhdpi" : "static/app-icons/96x96.png",
"xxhdpi" : "static/app-icons/144x144.png",
"xxxhdpi" : "static/app-icons/192x192.png"
"hdpi" : "unpackage/res/icons/72x72.png",
"xhdpi" : "unpackage/res/icons/96x96.png",
"xxhdpi" : "unpackage/res/icons/144x144.png",
"xxxhdpi" : "unpackage/res/icons/192x192.png"
},
"ios" : {
"appstore" : "static/app-icons/1024x1024.png",
"appstore" : "unpackage/res/icons/1024x1024.png",
"ipad" : {
"app" : "static/app-icons/76x76.png",
"app@2x" : "static/app-icons/152x152.png",
"notification" : "static/app-icons/20x20.png",
"notification@2x" : "static/app-icons/40x40.png",
"proapp@2x" : "static/app-icons/167x167.png",
"settings" : "static/app-icons/29x29.png",
"settings@2x" : "static/app-icons/58x58.png",
"spotlight" : "static/app-icons/40x40.png",
"spotlight@2x" : "static/app-icons/80x80.png"
"app" : "unpackage/res/icons/76x76.png",
"app@2x" : "unpackage/res/icons/152x152.png",
"notification" : "unpackage/res/icons/20x20.png",
"notification@2x" : "unpackage/res/icons/40x40.png",
"proapp@2x" : "unpackage/res/icons/167x167.png",
"settings" : "unpackage/res/icons/29x29.png",
"settings@2x" : "unpackage/res/icons/58x58.png",
"spotlight" : "unpackage/res/icons/40x40.png",
"spotlight@2x" : "unpackage/res/icons/80x80.png"
},
"iphone" : {
"app@2x" : "static/app-icons/120x120.png",
"app@3x" : "static/app-icons/180x180.png",
"notification@2x" : "static/app-icons/40x40.png",
"notification@3x" : "static/app-icons/60x60.png",
"settings@2x" : "static/app-icons/58x58.png",
"settings@3x" : "static/app-icons/87x87.png",
"spotlight@2x" : "static/app-icons/80x80.png",
"spotlight@3x" : "static/app-icons/120x120.png"
"app@2x" : "unpackage/res/icons/120x120.png",
"app@3x" : "unpackage/res/icons/180x180.png",
"notification@2x" : "unpackage/res/icons/40x40.png",
"notification@3x" : "unpackage/res/icons/60x60.png",
"settings@2x" : "unpackage/res/icons/58x58.png",
"settings@3x" : "unpackage/res/icons/87x87.png",
"spotlight@2x" : "unpackage/res/icons/80x80.png",
"spotlight@3x" : "unpackage/res/icons/120x120.png"
}
}
}

View File

@ -10,104 +10,10 @@
</view>
</view>
<!-- 铸爱选图后确认铸造光栅 / 镭射 -->
<scroll-view v-if="craftConfirmMode" scroll-y class="content-scroll craft-confirm-scroll">
<view class="content-wrapper craft-confirm-body">
<view class="card-section craft-card-section">
<view class="card-wrapper craft-card-wrapper">
<image
class="card-frame"
src="/static/square/gerenzhongxincangpinkuang.png"
mode="aspectFit"
/>
<view v-if="isCraftLenticular" class="craft-lenticular-slot">
<LenticularCard
class="craft-lenticular-card"
:layers="lenticularLayers"
:transforms="layerTransforms"
:gyro-source="gyroSourceLabel"
:skip-built-in-touch="true"
tilt-hint-text="晃动查看"
:shimmer-mid-opacity="0.16"
@simulate="simulate"
/>
</view>
<image
v-else
class="card-image craft-card-image"
:src="craftCoverUrl"
mode="aspectFill"
/>
</view>
<view class="card-meta-row">
<view class="earnings-area">
<image class="crystal-icon" src="/static/icon/crystal.png" mode="aspectFit" />
<text class="earnings-text">{{ craftEarningsHint }}</text>
</view>
</view>
</view>
<view class="info-row craft-info-row">
<view class="info-col">
<view class="info-item">
<text class="info-label">分类</text>
<text class="info-value">{{ craftCategoryLabel }}</text>
</view>
</view>
<view class="info-col">
<view class="info-item">
<text class="info-label">创作者</text>
<text class="info-value">{{ craftCreatorName }}</text>
</view>
</view>
<view class="info-col">
<view class="info-item">
<text class="info-label">铸爱时间</text>
<text class="info-value">{{ craftMintDate }}</text>
</view>
</view>
</view>
<view class="chain-section">
<image class="chain-logo" src="/static/logo/APPLOGO.png" mode="aspectFit" />
<view class="chain-left">
<view class="chain-row">
<text class="chain-label">数根名称</text>
<view class="chain-value-wrap">
<text class="chain-value">{{ craftAssetName }}</text>
</view>
</view>
<view class="chain-row">
<text class="chain-label">数根发行方</text>
<view class="chain-value-wrap">
<text class="chain-value">TOPFANS</text>
</view>
</view>
<view class="chain-row">
<text class="chain-label">区块链编号</text>
<view class="chain-value-wrap">
<text class="chain-value">{{ craftBlockPlaceholder }}</text>
</view>
</view>
<view class="chain-row">
<text class="chain-label">交易哈希</text>
<view class="chain-value-wrap">
<text class="chain-value chain-hash">{{ craftHashPlaceholder }}</text>
</view>
</view>
</view>
</view>
<view class="craft-mint-bar">
<button class="craft-mint-btn" :disabled="craftMinting" @tap="handleCraftMint">
{{ craftMinting ? '提交中…' : '确认铸造' }}
</button>
</view>
</view>
</scroll-view>
<!-- 加载中 -->
<view v-else-if="loading" class="loading-wrapper">
<view v-if="loading" class="loading-wrapper">
<!-- 旋转光环 -->
<view class="loading-ring-outer">
<view class="loading-ring"></view>
@ -137,44 +43,28 @@
<image class="card-frame" src="/static/square/gerenzhongxincangpinkuang.png" mode="aspectFit">
</image>
<view v-if="isLenticularAsset" class="detail-lenticular-slot">
<LenticularCard
class="detail-lenticular-card"
:layers="lenticularLayers"
:transforms="layerTransforms"
:gyro-source="gyroSourceLabel"
:skip-built-in-touch="true"
tilt-hint-text="倾斜手机查看光栅效果"
:shimmer-mid-opacity="0.16"
@simulate="simulate"
/>
<LenticularCard class="detail-lenticular-card" :layers="lenticularLayers"
:transforms="layerTransforms" :gyro-source="gyroSourceLabel" :skip-built-in-touch="true"
tilt-hint-text="倾斜手机查看光栅效果" :shimmer-mid-opacity="0.16" @simulate="simulate" />
</view>
<image v-else class="card-image" :src="coverUrl" mode="aspectFill"></image>
<image class="card-badge" :src="gradeBadgeUrl" mode="aspectFit"></image>
<!-- 贴纸叠加层 -->
<image
v-for="sticker in activeStickers"
:key="sticker.id"
class="card-sticker"
:src="sticker.src"
mode="aspectFit"
:style="getStickerStyle(sticker)"
/>
<image v-for="sticker in activeStickers" :key="sticker.id" class="card-sticker"
:src="sticker.src" mode="aspectFit" :style="getStickerStyle(sticker)" />
</view>
<!-- 贴纸合成导出隐藏画布 -->
<canvas
v-if="activeStickers.length"
canvas-id="stickerCompositCanvas"
class="export-canvas"
:style="{ position: 'fixed', left: '-9999px', top: '-9999px', width: '450px', height: '600px' }"
/>
<canvas v-if="activeStickers.length" canvas-id="stickerCompositCanvas" class="export-canvas"
:style="{ position: 'fixed', left: '-9999px', top: '-9999px', width: '450px', height: '600px' }" />
<!-- 点赞 + 收益 + 倒计时行 -->
<view class="card-meta-row">
<!-- 点赞 -->
<view v-if="assetData.display_status === 1" class="like-area" @tap="handleLike">
<view class="like-area" @tap="handleLike">
<image :src="isLiked ? '/static/icon/like-after.png' : '/static/icon/like-before.png'"
class="like-icon" mode="aspectFit"></image>
<text class="like-num">{{ likeCount }}</text>
<text class="like-num">{{ likeCount || 0 }}</text>
</view>
<!-- 收益 -->
@ -204,7 +94,8 @@
<view class="info-item">
<text class="info-label">创作者</text>
<view class="info-nickname">
<image v-if="userAvatarUrl" class="info-avatar" :src="userAvatarUrl" mode="aspectFill"></image>
<image v-if="userAvatarUrl" class="info-avatar" :src="userAvatarUrl" mode="aspectFill">
</image>
<text class="info-value">{{ assetData.owner_nickname || '未知' }}</text>
</view>
</view>
@ -219,8 +110,8 @@
<!-- 创作者信息模块 -->
<!-- <view v-if="likeCount > 0" class="creator-section" @tap="showLikeUsersModal = true"> -->
<!-- 点赞用户头像区域 -->
<!-- <view class="liked-users-area">
<!-- 点赞用户头像区域 -->
<!-- <view class="liked-users-area">
<view v-for="(user, index) in likedUsers" :key="index" class="liked-user-avatar" :style="{
transform: `translate(${user.ellipseX || 0}rpx, ${user.ellipseY || 0}rpx) scale(${user.size || 1})`,
zIndex: index < 2 ? index + 1 : (index < 4 ? index + 3 : index + 1)
@ -239,17 +130,17 @@
<!-- </view> -->
<!-- 完整链上哈希显示遮罩层 -->
<view v-if="showTxHash" class="txhash-mask" @tap="showTxHash = false">
<view class="txhash-popup" @tap.stop>
<view class="txhash-popup-header">
<text class="txhash-popup-title">链上哈希</text>
<view class="txhash-popup-close" @tap="showTxHash = false">
<image class="close-icon" src="/static/icon/hide.png" mode="aspectFit"></image>
<view v-if="showTxHash" class="txhash-mask" @tap="showTxHash = false">
<view class="txhash-popup" @tap.stop>
<view class="txhash-popup-header">
<text class="txhash-popup-title">链上哈希</text>
<view class="txhash-popup-close" @tap="showTxHash = false">
<image class="close-icon" src="/static/icon/hide.png" mode="aspectFit"></image>
</view>
</view>
<text class="txhash-popup-content" selectable @longpress="copyHash">{{ displayTxHash }}</text>
</view>
<text class="txhash-popup-content" selectable @longpress="copyHash">{{ displayTxHash }}</text>
</view>
</view>
<!-- 链上数据 -->
<view class="chain-section">
@ -273,7 +164,9 @@
<text class="chain-value">{{ showBlockNumber ? (assetData.block_number || '未知') :
hiddenBlockNumber }}</text>
<view class="toggle-btn" @tap="showBlockNumber = !showBlockNumber">
<image class="toggle-icon" :src="showBlockNumber ? '/static/icon/show.png' : '/static/icon/hide.png'" mode="aspectFit"></image>
<image class="toggle-icon"
:src="showBlockNumber ? '/static/icon/show.png' : '/static/icon/hide.png'"
mode="aspectFit"></image>
</view>
</view>
</view>
@ -283,7 +176,9 @@
<text class="chain-value chain-hash" @longpress="copyHash">{{ showTxHash ? displayTxHash
: hiddenTxHash }}</text>
<view class="toggle-btn" @tap="showTxHash = !showTxHash">
<image class="toggle-icon" :src="showTxHash ? '/static/icon/show.png' : '/static/icon/hide.png'" mode="aspectFit"></image>
<image class="toggle-icon"
:src="showTxHash ? '/static/icon/show.png' : '/static/icon/hide.png'"
mode="aspectFit"></image>
</view>
</view>
</view>
@ -585,6 +480,19 @@ const hiddenBlockNumber = computed(() => {
return `${str.substring(0, 6)}******`;
});
//
const gradeBadgeUrl = computed(() => {
const grade = assetData.value.grade;
const gradeMap = {
1: '/static/starbookcontent/grade/Ndengji.png',
2: '/static/starbookcontent/grade/Rdengji.png',
3: '/static/starbookcontent/grade/SRdengji.png',
4: '/static/starbookcontent/grade/SSRdengji.png',
5: '/static/starbookcontent/grade/URengji.png',
};
return gradeMap[grade] || '/static/starbookcontent/grade/Ndengji.png';
});
//
const copyHash = () => {
const hash = assetData.value.tx_hash;
@ -864,14 +772,14 @@ onUnmounted(() => {
.header-bar {
position: fixed;
/* top: 80rpx; */
/* #ifdef APP-PLUS */
/* App 通用(兜底) */
top: 96rpx;
/* #endif */
/* #ifdef H5 */
/* H5 使用普通高度 */
top: 16rpx;
/* #endif */
/* #ifdef APP-PLUS */
/* App 通用(兜底) */
top: 96rpx;
/* #endif */
/* #ifdef H5 */
/* H5 使用普通高度 */
top: 16rpx;
/* #endif */
left: 32rpx;
z-index: 100;
}
@ -1092,6 +1000,16 @@ onUnmounted(() => {
transform: rotate(-10deg);
}
.card-badge {
position: absolute;
top: -8rpx;
left: -56rpx;
width: 120rpx;
height: 120rpx;
z-index: 4;
transform: rotate(-10deg);
}
.card-frame {
position: absolute;
top: 0;
@ -1427,7 +1345,7 @@ onUnmounted(() => {
align-items: center;
justify-content: center;
padding: 0 64rpx;
}
.txhash-popup {
@ -1538,9 +1456,9 @@ onUnmounted(() => {
margin-bottom: 8rpx;
/* transform: scale(1.2); */
}
.visit-text{
.visit-text {
color: #FFFFFF;
font-size: 16rpx;
}
</style>

View File

@ -70,6 +70,7 @@ import NftCard from '../components/NftCard.vue';
import LenticularCard from '@/components/lenticular/LenticularCard.vue';
import { useLenticularCraftTiltPreview } from '@/composables/useLenticularCraftTiltPreview.js';
import { buildLenticularLayersTwo } from '@/utils/castloveMintForm.js';
import { getAssetDetailApi, getAssetMaterialsApi } from '@/utils/api.js';
//
const nftData = ref({
@ -103,26 +104,70 @@ const nftCardStyle = {
};
//
onMounted(() => {
onMounted(async () => {
//
try {
const tempNftData = uni.getStorageSync('temp_nft_data');
if (tempNftData) {
const data = JSON.parse(tempNftData);
nftData.value = {
image: data.image || '',
name: data.name || '未命名藏品',
event: data.event || '',
remark: data.remark || '',
materialType: data.materialType || '',
is_lenticular: data.is_lenticular || false,
bg_image: data.bg_image || '',
};
//
if (data.is_lenticular && data.image && data.bg_image) {
isLenticular.value = true
lenticularLayers.value = buildLenticularLayersTwo(data.bg_image, data.image)
scheduleTiltStart()
// 使 API
if (data.asset_id) {
try {
const res = await getAssetDetailApi(data.asset_id);
console.log('[success] asset detail API 返回:', res);
if (res.code === 200 && res.data?.asset) {
const asset = res.data.asset;
console.log('[success] asset:', asset);
// materials
let imageUrl = asset.image || data.image || '';
let bgImageUrl = asset.bg_image || data.bg_image || '';
try {
const materialsRes = await getAssetMaterialsApi(data.asset_id);
console.log('[success] materials API 返回:', materialsRes);
if (materialsRes.code === 200 && materialsRes.data) {
const materials = Array.isArray(materialsRes.data) ? materialsRes.data : materialsRes.data.materials || [];
console.log('[success] materials 数组:', materials);
// type
const mainMat = materials.find(m => m.material_type === 'main');
const bgMat = materials.find(m => m.material_type === 'bg');
console.log('[success] mainMat:', mainMat, 'bgMat:', bgMat);
if (mainMat?.material_url_signed) imageUrl = mainMat.material_url_signed;
if (bgMat?.material_url_signed) bgImageUrl = bgMat.material_url_signed;
console.log('[success] imageUrl:', imageUrl, 'bgImageUrl:', bgImageUrl);
}
} catch (e) {
console.error('获取素材详情失败:', e);
}
nftData.value = {
image: imageUrl,
name: asset.name || data.name || '未命名藏品',
event: asset.event || data.event || '',
remark: asset.remark || data.remark || '',
materialType: asset.material_type || data.materialType || '',
is_lenticular: asset.is_lenticular || data.is_lenticular || false,
bg_image: bgImageUrl,
};
console.log('[success] nftData 设置后:', nftData.value);
//
if ((asset.is_lenticular || data.is_lenticular) && bgImageUrl && imageUrl) {
isLenticular.value = true
lenticularLayers.value = buildLenticularLayersTwo(bgImageUrl, imageUrl)
scheduleTiltStart()
}
} else {
// API 使
applyLocalNftData(data);
}
} catch (e) {
console.error('获取藏品详情失败,使用本地数据:', e);
applyLocalNftData(data);
}
} else {
applyLocalNftData(data);
}
}
} catch (e) {
@ -134,6 +179,25 @@ onMounted(() => {
}
});
//
function applyLocalNftData(data) {
nftData.value = {
image: data.image || '',
name: data.name || '未命名藏品',
event: data.event || '',
remark: data.remark || '',
materialType: data.materialType || '',
is_lenticular: data.is_lenticular || false,
bg_image: data.bg_image || '',
};
//
if (data.is_lenticular && data.image && data.bg_image) {
isLenticular.value = true
lenticularLayers.value = buildLenticularLayersTwo(data.bg_image, data.image)
scheduleTiltStart()
}
}
onUnload(() => {
stopTiltPreview()
try {

View File

@ -319,7 +319,7 @@ const handleClaimReward = async (item, _index) => {
const revenueRecord = records.find(r => r.asset_id === item.id);
if (!revenueRecord) {
uni.showToast({ title: '暂无可领取收益', icon: 'none' });
uni.showToast({ title: '一分钟延迟领取', icon: 'none' });
return;
}
@ -330,7 +330,23 @@ const handleClaimReward = async (item, _index) => {
//
if (claimRes?.data?.total_balance !== undefined) {
uni.$emit('balanceUpdated', { crystal_balance: claimRes.data.total_balance });
const userStr = uni.getStorageSync('user')
if (userStr) {
//
let user
if (typeof userStr === 'string') {
user = JSON.parse(userStr)
} else {
//
user = { ...userStr }
}
//
user.crystal_balance = Number(claimRes.data.total_balance) || 0
//
uni.setStorageSync('user', JSON.stringify(user))
uni.$emit('balanceUpdated', { crystal_balance: user.crystal_balance })
}
}
//

View File

@ -1,7 +1,8 @@
<template>
<view v-if="visible" class="modal-wrapper" @touchmove.stop.prevent="handlePreventMove" @click.stop>
<transition name="fade">
<view v-if="visible" class="modal-mask" @touchstart.stop="handleMaskTouchStart" @touchmove.stop.prevent="handlePreventMove" @click.stop>
<view v-if="visible" class="modal-mask" @touchstart.stop="handleMaskTouchStart"
@touchmove.stop.prevent="handlePreventMove" @click.stop>
</view>
</transition>
@ -18,10 +19,11 @@
<!-- 顶部区域返回按钮和Tab -->
<view class="top-bar">
<!-- 返回按钮 -->
<view class="back-button" @touchstart.stop="handleCloseTouchStart" @touchend.stop="handleCloseTouchEnd" @click="handleCloseClick">
<view class="back-button" @touchstart.stop="handleCloseTouchStart"
@touchend.stop="handleCloseTouchEnd" @click="handleCloseClick">
<image class="back-icon" src="/static/icon/back.png" mode="aspectFit" />
</view>
<!-- Tab 切换 -->
<view class="tab-bar">
<view class="tab-item active">
@ -56,43 +58,38 @@
<view v-for="task in sortedTasks" :key="task.task_key" class="task-item">
<view class="task-info">
<text class="task-name">{{ task.name }}</text>
<text class="task-progress" v-if="task.current_count !== undefined">{{ task.current_count }}/{{ task.target_count }}</text>
<text class="task-progress" v-if="task.current_count !== undefined">{{
task.current_count }}/{{ task.target_count }}</text>
</view>
<view class="task-right">
<!-- 礼盒图标 - 点击展开/收起 -->
<view class="reward-gift" @click.stop="toggleReward(task.task_key)">
<image
class="gift-icon"
:class="{ 'gift-open': expandedTaskKey === task.task_key }"
:src="expandedTaskKey === task.task_key ? '/static/nft/lihe_kaiqi.png' : '/static/nft/lihe.png'"
mode="aspectFit"
></image>
<image class="gift-icon" :class="{ 'gift-open': expandedTaskKey === task.task_key }"
:src="expandedTaskKey === task.task_key ? '/static/nft/lihe_kaiqi.png' : '/static/nft/lihe.png'"
mode="aspectFit"></image>
<!-- 奖励详情弹出 -->
<view v-if="expandedTaskKey === task.task_key" class="reward-popup">
<view class="reward-item">
<image class="reward-icon-img" src="/static/icon/crystal.png" mode="aspectFit"></image>
<image class="reward-icon-img" src="/static/icon/crystal.png"
mode="aspectFit"></image>
<text class="reward-value">{{ task.crystal_reward }}</text>
</view>
<view class="reward-item">
<!-- <view class="reward-item">
<image class="reward-icon-img" src="/static/nft/jingyanzhi.png" mode="aspectFit"></image>
<text class="reward-value">{{ task.exp_reward }}</text>
</view>
<view class="reward-item">
<image class="reward-icon-img" src="/static/nft/huoyuezhitubiao.png" mode="aspectFit"></image>
<text class="reward-value">{{ task.activity_reward || 10 }}</text>
</view>
</view> -->
</view>
</view>
<!-- 状态按钮/标签 -->
<button
class="claim-btn"
:class="getStatusClass(task.status)"
:loading="claimingTask === task.task_key"
:disabled="!task.can_claim"
@click="task.can_claim && handleClaim(task)"
>
<button class="claim-btn" :class="getStatusClass(task.status)"
:loading="claimingTask === task.task_key" :disabled="!task.can_claim"
@click="task.can_claim && handleClaim(task)">
{{ task.can_claim ? '待领取' : getStatusText(task.status) }}
</button>
</view>
@ -104,25 +101,15 @@
<!-- 进度条 -->
<view class="progress-section">
<view class="progress-bar">
<view
v-for="(milestone, index) in milestones"
:key="index"
class="progress-node"
:class="{ 'active': index < completedCount }"
>
<view v-for="(milestone, index) in milestones" :key="index" class="progress-node"
:class="{ 'active': index < completedCount }">
<!-- 奖励图标 -->
<image
class="milestone-icon"
:src="[20, 40, 80].includes(milestone.value) ? '/static/icon/crystal.png' : '/static/nft/lihe.png'"
mode="aspectFit"
></image>
<image class="milestone-icon"
:src="[20, 40, 80].includes(milestone.value) ? '/static/icon/crystal.png' : '/static/nft/lihe.png'"
mode="aspectFit"></image>
<!-- 进度节点 -->
<image
class="node-circle"
:class="{ 'node-inactive': index >= completedCount }"
src="/static/nft/huoyuezhi_jiedian.png"
mode="aspectFit"
></image>
<image class="node-circle" :class="{ 'node-inactive': index >= completedCount }"
src="/static/nft/huoyuezhi_jiedian.png" mode="aspectFit"></image>
<text class="node-label">{{ milestone.value }}</text>
</view>
</view>
@ -130,13 +117,8 @@
<!-- 一键领取按钮 -->
<view class="claim-all-bar">
<button
class="claim-all-btn"
:class="{ 'disabled': !hasClaimableTasks }"
:loading="claimingAll"
:disabled="!hasClaimableTasks"
@click="handleClaimAll"
>
<button class="claim-all-btn" :class="{ 'disabled': !hasClaimableTasks }"
:loading="claimingAll" :disabled="!hasClaimableTasks" @click="handleClaimAll">
一键领取 ({{ claimableCount }})
</button>
</view>
@ -223,7 +205,7 @@ function lockBodyScroll() {
scrollTop = res.scrollTop || 0
}
}).exec()
// #ifdef H5
// H5 body
const body = document.body
@ -233,7 +215,7 @@ function lockBodyScroll() {
body.style.top = `-${scrollTop}px`
body.style.width = '100%'
// #endif
// #ifdef MP
//
uni.pageScrollTo({
@ -241,7 +223,7 @@ function lockBodyScroll() {
duration: 0
})
// #endif
// #ifdef APP-PLUS
// APP
const currentWebview = plus.webview.currentWebview()
@ -273,7 +255,7 @@ function unlockBodyScroll() {
//
window.scrollTo(0, scrollTop)
// #endif
// #ifdef MP
//
uni.pageScrollTo({
@ -281,7 +263,7 @@ function unlockBodyScroll() {
duration: 0
})
// #endif
// #ifdef APP-PLUS
// APP
const currentWebview = plus.webview.currentWebview()
@ -359,9 +341,24 @@ async function handleClaim(task) {
tasks.value[index].status = 'claimed'
tasks.value[index].can_claim = false
}
const userStr = uni.getStorageSync('user')
if (userStr) {
//
let user
if (typeof userStr === 'string') {
user = JSON.parse(userStr)
} else {
//
user = { ...userStr }
}
emit('updated')
//
user.crystal_balance = Number(res.data?.crystal_balance) || 0
emit('updated')
uni.$emit('balanceUpdated', { crystal_balance: res.data?.crystal_balance, experience: res.data?.experience })
//
uni.setStorageSync('user', JSON.stringify(user))
uni.$emit('balanceUpdated', { crystal_balance: user.crystal_balance })
}
uni.showToast({ title: '领取成功', icon: 'success' })
} catch (err) {
console.error('handleClaim error:', err)
@ -386,7 +383,24 @@ async function handleClaimAll() {
})
emit('updated')
uni.$emit('balanceUpdated', { crystal_balance: res.data?.crystal_balance, experience: res.data?.experience })
const userStr = uni.getStorageSync('user')
if (userStr) {
//
let user
if (typeof userStr === 'string') {
user = JSON.parse(userStr)
} else {
//
user = { ...userStr }
}
emit('updated')
//
user.crystal_balance = Number(res.data?.crystal_balance) || 0
//
uni.setStorageSync('user', JSON.stringify(user))
uni.$emit('balanceUpdated', { crystal_balance: user.crystal_balance })
}
uni.showToast({ title: '领取成功', icon: 'success' })
} catch (err) {
console.error('handleClaimAll error:', err)
@ -617,7 +631,9 @@ const handleCloseClick = (e) => {
}
@keyframes spin {
to { transform: rotate(360deg); }
to {
transform: rotate(360deg);
}
}
.loading-text,
@ -747,6 +763,7 @@ const handleCloseClick = (e) => {
opacity: 0;
transform: translateY(-50%) scale(0.8);
}
to {
opacity: 1;
transform: translateY(-50%) scale(1);
@ -811,21 +828,25 @@ const handleCloseClick = (e) => {
height: 50rpx;
border-radius: 25rpx;
/* 渐变:左浅橙粉 → 右柔粉红 */
background: linear-gradient(to bottom right,
#F0E4B1 0%, /* 左:浅橙粉 */
#F08399 50%,
#B94E73 100% /* 右:柔粉红 */
);
background: linear-gradient(to bottom right,
#F0E4B1 0%,
/* 左:浅橙粉 */
#F08399 50%,
#B94E73 100%
/* 右:柔粉红 */
);
/* 立体感核心:多层阴影 + 内阴影模拟凸起 */
box-shadow:
/* 外层投影 - 让按钮浮起 */
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
0 2rpx 6rpx rgba(255, 143, 158, 0.15),
/* 立体感核心:多层阴影 + 内阴影模拟凸起 */
box-shadow:
/* 外层投影 - 让按钮浮起 */
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
0 2rpx 6rpx rgba(255, 143, 158, 0.15),
/* 内阴影 - 模拟顶部受光 + 底部凹陷 */
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4), /* 顶部高光 */
inset 0 -2rpx 4rpx rgba(0, 0, 0, 0.05); /* 底部暗部 */
/* 内阴影 - 模拟顶部受光 + 底部凹陷 */
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4),
/* 顶部高光 */
inset 0 -2rpx 4rpx rgba(0, 0, 0, 0.05);
/* 底部暗部 */
border: 2rpx solid rgba(255, 255, 255, 0.5);
color: #fff;
font-size: 24rpx;

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB