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 根据asset_id查询展示状态从asset_registry表
GetDisplayStatusByAssetID(assetID int64) (int32, error) GetDisplayStatusByAssetID(assetID int64) (int32, error)
// GetGradeByAssetID 根据asset_id查询藏品等级从asset_registry表
GetGradeByAssetID(assetID int64) (int32, error)
// GetByOwner 查询用户的资产列表 // GetByOwner 查询用户的资产列表
GetByOwner(ownerUID, starID int64, limit, offset int) ([]*models.Asset, error) 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 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 批量查询资产 // GetByIDs 批量查询资产
func (r *assetRepository) GetByIDs(assetIDs []int64) ([]*models.Asset, error) { func (r *assetRepository) GetByIDs(assetIDs []int64) ([]*models.Asset, error) {
if len(assetIDs) == 0 { if len(assetIDs) == 0 {

View File

@ -42,6 +42,16 @@ func (r *MaterialRepository) FindByHash(hash string, starID int64) (*models.Mate
return &m, nil 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 软删除素材 // SoftDelete 软删除素材
func (r *MaterialRepository) SoftDelete(materialID int64) error { func (r *MaterialRepository) SoftDelete(materialID int64) error {
now := time.Now().UnixMilli() 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) 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. 构建响应 // 7. 构建响应
response := &pb.GetAssetResponse{ response := &pb.GetAssetResponse{
Base: &pbCommon.BaseResponse{ Base: &pbCommon.BaseResponse{
@ -497,7 +507,7 @@ func (s *assetService) GetAsset(req *pb.GetAssetRequest, userID, starID int64) (
Message: "", Message: "",
Timestamp: time.Now().UnixMilli(), 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", logger.Logger.Debug("Get asset successful",
@ -646,7 +656,7 @@ func ModelToProtoAsset(asset *models.Asset) *pb.AssetListItem {
} }
// ModelToProtoAssetDetail 将数据库模型转换为Proto格式Asset详情 // 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 { if asset == nil {
return nil return nil
} }
@ -659,7 +669,7 @@ func ModelToProtoAssetDetail(asset *models.Asset, ownerNickname string, isLiked
CoverUrl: asset.CoverURL, CoverUrl: asset.CoverURL,
MaterialUrl: getStringValue(asset.MaterialURL), MaterialUrl: getStringValue(asset.MaterialURL),
Description: getStringValue(asset.Description), Description: getStringValue(asset.Description),
Grade: getInt32Value(asset.Grade), Grade: grade,
Tags: []string(asset.Tags), Tags: []string(asset.Tags),
Visibility: asset.Visibility, Visibility: asset.Visibility,
Status: asset.Status, 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) { 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 { if err == nil && existing != nil {
return existing, nil return existing, nil
} }
@ -32,6 +33,22 @@ func (s *MaterialService) UploadMaterial(m *models.Material) (*models.Material,
return nil, err 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 { if err := s.materialRepo.Create(m); err != nil {
return nil, err return nil, err
} }

View File

@ -464,7 +464,7 @@ func (s *mintService) CreateMintOrder(req *pb.CreateMintOrderRequest, userID, st
Timestamp: time.Now().UnixMilli(), Timestamp: time.Now().UnixMilli(),
}, },
Order: ModelToProtoMintOrder(mintOrder), 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, CostCrystal: capturedCostCrystal,
BalanceAfter: newBalance, BalanceAfter: newBalance,
} }
@ -569,7 +569,7 @@ func (s *mintService) GetMintOrder(orderID string, userID, starID int64) (*pb.Ge
// 由于是查询自己的订单is_liked 设为 false简化处理 // 由于是查询自己的订单is_liked 设为 false简化处理
// 获取 display_status // 获取 display_status
displayStatus, _ := s.assetRepo.GetDisplayStatusByAssetID(asset.ID) 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 // 如果 cover_url 存在,生成预签名 URL
if assetProto.CoverUrl != "" { if assetProto.CoverUrl != "" {

View File

@ -14,8 +14,11 @@ DECLARE
asset_rec record; asset_rec record;
i int := 0; i int := 0;
BEGIN BEGIN
-- 收集star_id=87的所有可用slot_id -- 收集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; 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资产创建展示记录 -- 为每个mock资产创建展示记录
FOR asset_rec IN SELECT id, owner_uid FROM assets WHERE id >= 10000 ORDER BY id LOOP 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 x = tiltVisualX.value
const rotateY = (x / 120) * 12 const rotateY = (x / 120) * 12
return { 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 angle = 135 + (x / 120) * 60
const a = Math.max(0, Math.min(0.35, Number(props.shimmerMidOpacity) || 0.1)) const a = Math.max(0, Math.min(0.35, Number(props.shimmerMidOpacity) || 0.1))
return { 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 baseOpacity = t ? t.opacity : layer.opacity
const x = t ? t.x : 0 const x = t ? t.x : 0
// mix-blend-mode / WebView normal // mix-blend-mode / WebView normal
// return { return {
// opacity: baseOpacity, opacity: baseOpacity,
// transform: `translate3d(${x}px, 0, 0)`, transform: `translate3d(${x}px, 0, 0)`,
// background: layer.background || 'transparent', background: layer.background || 'transparent',
// } }
} }
function getDotStyle(dot) { function getDotStyle(dot) {

View File

@ -27,7 +27,20 @@
"Push" : {} "Push" : {}
}, },
"nativePlugins" : { "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" : { "distribute" : {
@ -51,7 +64,9 @@
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>", "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>",
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>", "<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
"<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>", "<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" ] "abiFilters" : [ "armeabi-v7a", "arm64-v8a" ]
}, },
@ -80,33 +95,33 @@
}, },
"icons" : { "icons" : {
"android" : { "android" : {
"hdpi" : "static/app-icons/72x72.png", "hdpi" : "unpackage/res/icons/72x72.png",
"xhdpi" : "static/app-icons/96x96.png", "xhdpi" : "unpackage/res/icons/96x96.png",
"xxhdpi" : "static/app-icons/144x144.png", "xxhdpi" : "unpackage/res/icons/144x144.png",
"xxxhdpi" : "static/app-icons/192x192.png" "xxxhdpi" : "unpackage/res/icons/192x192.png"
}, },
"ios" : { "ios" : {
"appstore" : "static/app-icons/1024x1024.png", "appstore" : "unpackage/res/icons/1024x1024.png",
"ipad" : { "ipad" : {
"app" : "static/app-icons/76x76.png", "app" : "unpackage/res/icons/76x76.png",
"app@2x" : "static/app-icons/152x152.png", "app@2x" : "unpackage/res/icons/152x152.png",
"notification" : "static/app-icons/20x20.png", "notification" : "unpackage/res/icons/20x20.png",
"notification@2x" : "static/app-icons/40x40.png", "notification@2x" : "unpackage/res/icons/40x40.png",
"proapp@2x" : "static/app-icons/167x167.png", "proapp@2x" : "unpackage/res/icons/167x167.png",
"settings" : "static/app-icons/29x29.png", "settings" : "unpackage/res/icons/29x29.png",
"settings@2x" : "static/app-icons/58x58.png", "settings@2x" : "unpackage/res/icons/58x58.png",
"spotlight" : "static/app-icons/40x40.png", "spotlight" : "unpackage/res/icons/40x40.png",
"spotlight@2x" : "static/app-icons/80x80.png" "spotlight@2x" : "unpackage/res/icons/80x80.png"
}, },
"iphone" : { "iphone" : {
"app@2x" : "static/app-icons/120x120.png", "app@2x" : "unpackage/res/icons/120x120.png",
"app@3x" : "static/app-icons/180x180.png", "app@3x" : "unpackage/res/icons/180x180.png",
"notification@2x" : "static/app-icons/40x40.png", "notification@2x" : "unpackage/res/icons/40x40.png",
"notification@3x" : "static/app-icons/60x60.png", "notification@3x" : "unpackage/res/icons/60x60.png",
"settings@2x" : "static/app-icons/58x58.png", "settings@2x" : "unpackage/res/icons/58x58.png",
"settings@3x" : "static/app-icons/87x87.png", "settings@3x" : "unpackage/res/icons/87x87.png",
"spotlight@2x" : "static/app-icons/80x80.png", "spotlight@2x" : "unpackage/res/icons/80x80.png",
"spotlight@3x" : "static/app-icons/120x120.png" "spotlight@3x" : "unpackage/res/icons/120x120.png"
} }
} }
} }

View File

@ -10,104 +10,10 @@
</view> </view>
</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-outer">
<view class="loading-ring"></view> <view class="loading-ring"></view>
@ -137,44 +43,28 @@
<image class="card-frame" src="/static/square/gerenzhongxincangpinkuang.png" mode="aspectFit"> <image class="card-frame" src="/static/square/gerenzhongxincangpinkuang.png" mode="aspectFit">
</image> </image>
<view v-if="isLenticularAsset" class="detail-lenticular-slot"> <view v-if="isLenticularAsset" class="detail-lenticular-slot">
<LenticularCard <LenticularCard class="detail-lenticular-card" :layers="lenticularLayers"
class="detail-lenticular-card" :transforms="layerTransforms" :gyro-source="gyroSourceLabel" :skip-built-in-touch="true"
:layers="lenticularLayers" tilt-hint-text="倾斜手机查看光栅效果" :shimmer-mid-opacity="0.16" @simulate="simulate" />
:transforms="layerTransforms"
:gyro-source="gyroSourceLabel"
:skip-built-in-touch="true"
tilt-hint-text="倾斜手机查看光栅效果"
:shimmer-mid-opacity="0.16"
@simulate="simulate"
/>
</view> </view>
<image v-else class="card-image" :src="coverUrl" mode="aspectFill"></image> <image v-else class="card-image" :src="coverUrl" mode="aspectFill"></image>
<image class="card-badge" :src="gradeBadgeUrl" mode="aspectFit"></image>
<!-- 贴纸叠加层 --> <!-- 贴纸叠加层 -->
<image <image v-for="sticker in activeStickers" :key="sticker.id" class="card-sticker"
v-for="sticker in activeStickers" :src="sticker.src" mode="aspectFit" :style="getStickerStyle(sticker)" />
:key="sticker.id"
class="card-sticker"
:src="sticker.src"
mode="aspectFit"
:style="getStickerStyle(sticker)"
/>
</view> </view>
<!-- 贴纸合成导出隐藏画布 --> <!-- 贴纸合成导出隐藏画布 -->
<canvas <canvas v-if="activeStickers.length" canvas-id="stickerCompositCanvas" class="export-canvas"
v-if="activeStickers.length" :style="{ position: 'fixed', left: '-9999px', top: '-9999px', width: '450px', height: '600px' }" />
canvas-id="stickerCompositCanvas"
class="export-canvas"
:style="{ position: 'fixed', left: '-9999px', top: '-9999px', width: '450px', height: '600px' }"
/>
<!-- 点赞 + 收益 + 倒计时行 --> <!-- 点赞 + 收益 + 倒计时行 -->
<view class="card-meta-row"> <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'" <image :src="isLiked ? '/static/icon/like-after.png' : '/static/icon/like-before.png'"
class="like-icon" mode="aspectFit"></image> class="like-icon" mode="aspectFit"></image>
<text class="like-num">{{ likeCount }}</text> <text class="like-num">{{ likeCount || 0 }}</text>
</view> </view>
<!-- 收益 --> <!-- 收益 -->
@ -204,7 +94,8 @@
<view class="info-item"> <view class="info-item">
<text class="info-label">创作者</text> <text class="info-label">创作者</text>
<view class="info-nickname"> <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> <text class="info-value">{{ assetData.owner_nickname || '未知' }}</text>
</view> </view>
</view> </view>
@ -219,8 +110,8 @@
<!-- 创作者信息模块 --> <!-- 创作者信息模块 -->
<!-- <view v-if="likeCount > 0" class="creator-section" @tap="showLikeUsersModal = true"> --> <!-- <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="{ <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})`, 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) zIndex: index < 2 ? index + 1 : (index < 4 ? index + 3 : index + 1)
@ -239,17 +130,17 @@
<!-- </view> --> <!-- </view> -->
<!-- 完整链上哈希显示遮罩层 --> <!-- 完整链上哈希显示遮罩层 -->
<view v-if="showTxHash" class="txhash-mask" @tap="showTxHash = false"> <view v-if="showTxHash" class="txhash-mask" @tap="showTxHash = false">
<view class="txhash-popup" @tap.stop> <view class="txhash-popup" @tap.stop>
<view class="txhash-popup-header"> <view class="txhash-popup-header">
<text class="txhash-popup-title">链上哈希</text> <text class="txhash-popup-title">链上哈希</text>
<view class="txhash-popup-close" @tap="showTxHash = false"> <view class="txhash-popup-close" @tap="showTxHash = false">
<image class="close-icon" src="/static/icon/hide.png" mode="aspectFit"></image> <image class="close-icon" src="/static/icon/hide.png" mode="aspectFit"></image>
</view>
</view> </view>
<text class="txhash-popup-content" selectable @longpress="copyHash">{{ displayTxHash }}</text>
</view> </view>
<text class="txhash-popup-content" selectable @longpress="copyHash">{{ displayTxHash }}</text>
</view> </view>
</view>
<!-- 链上数据 --> <!-- 链上数据 -->
<view class="chain-section"> <view class="chain-section">
@ -273,7 +164,9 @@
<text class="chain-value">{{ showBlockNumber ? (assetData.block_number || '未知') : <text class="chain-value">{{ showBlockNumber ? (assetData.block_number || '未知') :
hiddenBlockNumber }}</text> hiddenBlockNumber }}</text>
<view class="toggle-btn" @tap="showBlockNumber = !showBlockNumber"> <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> </view>
</view> </view>
@ -283,7 +176,9 @@
<text class="chain-value chain-hash" @longpress="copyHash">{{ showTxHash ? displayTxHash <text class="chain-value chain-hash" @longpress="copyHash">{{ showTxHash ? displayTxHash
: hiddenTxHash }}</text> : hiddenTxHash }}</text>
<view class="toggle-btn" @tap="showTxHash = !showTxHash"> <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> </view>
</view> </view>
@ -585,6 +480,19 @@ const hiddenBlockNumber = computed(() => {
return `${str.substring(0, 6)}******`; 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 copyHash = () => {
const hash = assetData.value.tx_hash; const hash = assetData.value.tx_hash;
@ -864,14 +772,14 @@ onUnmounted(() => {
.header-bar { .header-bar {
position: fixed; position: fixed;
/* top: 80rpx; */ /* top: 80rpx; */
/* #ifdef APP-PLUS */ /* #ifdef APP-PLUS */
/* App 通用(兜底) */ /* App 通用(兜底) */
top: 96rpx; top: 96rpx;
/* #endif */ /* #endif */
/* #ifdef H5 */ /* #ifdef H5 */
/* H5 使用普通高度 */ /* H5 使用普通高度 */
top: 16rpx; top: 16rpx;
/* #endif */ /* #endif */
left: 32rpx; left: 32rpx;
z-index: 100; z-index: 100;
} }
@ -1092,6 +1000,16 @@ onUnmounted(() => {
transform: rotate(-10deg); transform: rotate(-10deg);
} }
.card-badge {
position: absolute;
top: -8rpx;
left: -56rpx;
width: 120rpx;
height: 120rpx;
z-index: 4;
transform: rotate(-10deg);
}
.card-frame { .card-frame {
position: absolute; position: absolute;
top: 0; top: 0;
@ -1427,7 +1345,7 @@ onUnmounted(() => {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 0 64rpx; padding: 0 64rpx;
} }
.txhash-popup { .txhash-popup {
@ -1538,9 +1456,9 @@ onUnmounted(() => {
margin-bottom: 8rpx; margin-bottom: 8rpx;
/* transform: scale(1.2); */ /* transform: scale(1.2); */
} }
.visit-text{
.visit-text {
color: #FFFFFF; color: #FFFFFF;
font-size: 16rpx; font-size: 16rpx;
} }
</style> </style>

View File

@ -70,6 +70,7 @@ import NftCard from '../components/NftCard.vue';
import LenticularCard from '@/components/lenticular/LenticularCard.vue'; import LenticularCard from '@/components/lenticular/LenticularCard.vue';
import { useLenticularCraftTiltPreview } from '@/composables/useLenticularCraftTiltPreview.js'; import { useLenticularCraftTiltPreview } from '@/composables/useLenticularCraftTiltPreview.js';
import { buildLenticularLayersTwo } from '@/utils/castloveMintForm.js'; import { buildLenticularLayersTwo } from '@/utils/castloveMintForm.js';
import { getAssetDetailApi, getAssetMaterialsApi } from '@/utils/api.js';
// //
const nftData = ref({ const nftData = ref({
@ -103,26 +104,70 @@ const nftCardStyle = {
}; };
// //
onMounted(() => { onMounted(async () => {
// //
try { try {
const tempNftData = uni.getStorageSync('temp_nft_data'); const tempNftData = uni.getStorageSync('temp_nft_data');
if (tempNftData) { if (tempNftData) {
const data = JSON.parse(tempNftData); const data = JSON.parse(tempNftData);
nftData.value = {
image: data.image || '', // 使 API
name: data.name || '未命名藏品', if (data.asset_id) {
event: data.event || '', try {
remark: data.remark || '', const res = await getAssetDetailApi(data.asset_id);
materialType: data.materialType || '', console.log('[success] asset detail API 返回:', res);
is_lenticular: data.is_lenticular || false, if (res.code === 200 && res.data?.asset) {
bg_image: data.bg_image || '', const asset = res.data.asset;
}; console.log('[success] asset:', asset);
//
if (data.is_lenticular && data.image && data.bg_image) { // materials
isLenticular.value = true let imageUrl = asset.image || data.image || '';
lenticularLayers.value = buildLenticularLayersTwo(data.bg_image, data.image) let bgImageUrl = asset.bg_image || data.bg_image || '';
scheduleTiltStart()
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) { } 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(() => { onUnload(() => {
stopTiltPreview() stopTiltPreview()
try { try {

View File

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