feat:在个人展示页显示光栅卡
This commit is contained in:
parent
5f4a443a93
commit
3e6ff3c898
@ -616,6 +616,7 @@ func (ctrl *GalleryController) GetMyExhibitedAssets(c *gin.Context) {
|
||||
ExpireAt: item.ExpireAt,
|
||||
Earnings: item.Earnings,
|
||||
SlotIndex: item.SlotIndex,
|
||||
IsLenticular: item.IsLenticular,
|
||||
})
|
||||
}
|
||||
|
||||
@ -805,6 +806,7 @@ func (ctrl *GalleryController) GetUserExhibitedAssets(c *gin.Context) {
|
||||
ExpireAt: item.ExpireAt,
|
||||
Earnings: item.Earnings,
|
||||
SlotIndex: item.SlotIndex,
|
||||
IsLenticular: item.IsLenticular,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -81,6 +81,7 @@ type ExhibitedAssetItemDTO struct {
|
||||
ExpireAt int64 `json:"expire_at"` // 展出过期时间(毫秒时间戳)
|
||||
Earnings int64 `json:"earnings"` // 当前可领取收益
|
||||
SlotIndex int32 `json:"slot_index"` // 展位序号
|
||||
IsLenticular bool `json:"is_lenticular"` // 是否为光栅卡
|
||||
}
|
||||
|
||||
// GetMyExhibitedAssetsResponseDTO 获取我展出的作品列表响应
|
||||
|
||||
@ -1223,6 +1223,7 @@ type ExhibitedAssetItem struct {
|
||||
ExpireAt int64 `protobuf:"varint,6,opt,name=expire_at,json=expireAt,proto3" json:"expire_at,omitempty"` // 展出过期时间(毫秒时间戳)
|
||||
Earnings int64 `protobuf:"varint,7,opt,name=earnings,proto3" json:"earnings,omitempty"` // 当前可领取收益
|
||||
SlotIndex int32 `protobuf:"varint,8,opt,name=slot_index,json=slotIndex,proto3" json:"slot_index,omitempty"` // 展位序号
|
||||
IsLenticular bool `protobuf:"varint,9,opt,name=is_lenticular,json=isLenticular,proto3" json:"is_lenticular,omitempty"` // 是否为光栅卡(根据 tags 判断)
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@ -1313,6 +1314,13 @@ func (x *ExhibitedAssetItem) GetSlotIndex() int32 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ExhibitedAssetItem) GetIsLenticular() bool {
|
||||
if x != nil {
|
||||
return x.IsLenticular
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 获取灵感瀑布藏品列表请求
|
||||
type GetInspirationFlowRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
@ -1809,7 +1817,7 @@ const file_gallery_proto_rawDesc = "" +
|
||||
"\x04page\x18\x02 \x01(\x05R\x04page\x12\x1b\n" +
|
||||
"\tpage_size\x18\x03 \x01(\x05R\bpageSize\x12\x14\n" +
|
||||
"\x05total\x18\x04 \x01(\x03R\x05total\x12\x19\n" +
|
||||
"\bhas_more\x18\x05 \x01(\bR\ahasMore\"\xfa\x01\n" +
|
||||
"\bhas_more\x18\x05 \x01(\bR\ahasMore\"\x9f\x02\n" +
|
||||
"\x12ExhibitedAssetItem\x12\x19\n" +
|
||||
"\basset_id\x18\x01 \x01(\x03R\aassetId\x12\x12\n" +
|
||||
"\x04name\x18\x02 \x01(\tR\x04name\x12\x1b\n" +
|
||||
@ -1820,7 +1828,8 @@ const file_gallery_proto_rawDesc = "" +
|
||||
"\texpire_at\x18\x06 \x01(\x03R\bexpireAt\x12\x1a\n" +
|
||||
"\bearnings\x18\a \x01(\x03R\bearnings\x12\x1d\n" +
|
||||
"\n" +
|
||||
"slot_index\x18\b \x01(\x05R\tslotIndex\"\x9a\x01\n" +
|
||||
"slot_index\x18\b \x01(\x05R\tslotIndex\x12#\n" +
|
||||
"\ris_lenticular\x18\t \x01(\bR\fisLenticular\"\x9a\x01\n" +
|
||||
"\x19GetInspirationFlowRequest\x12\x16\n" +
|
||||
"\x06cursor\x18\x01 \x01(\tR\x06cursor\x12\x1c\n" +
|
||||
"\tdirection\x18\x02 \x01(\tR\tdirection\x12\x14\n" +
|
||||
|
||||
@ -208,6 +208,7 @@ message ExhibitedAssetItem {
|
||||
int64 expire_at = 6; // 展出过期时间(毫秒时间戳)
|
||||
int64 earnings = 7; // 当前可领取收益
|
||||
int32 slot_index = 8; // 展位序号
|
||||
bool is_lenticular = 9; // 是否为光栅卡(根据 tags 判断)
|
||||
}
|
||||
|
||||
// ==================== 灵感瀑布相关消息 ====================
|
||||
|
||||
@ -100,6 +100,7 @@ type ExhibitedAssetInfo struct {
|
||||
ExpireAt int64
|
||||
Earnings int64
|
||||
SlotIndex int32
|
||||
IsLenticular bool
|
||||
}
|
||||
|
||||
// galleryRepository Repository实现
|
||||
@ -418,7 +419,8 @@ func (r *galleryRepository) GetMyExhibitedAssets(userID, starID int64, page, pag
|
||||
err = r.db.Model(&models.Exhibition{}).
|
||||
Raw(`
|
||||
SELECT exhibitions.asset_id, a.name, a.cover_url, a.like_count,
|
||||
exhibitions.start_time as exhibited_at, exhibitions.expire_at, bs.slot_index
|
||||
exhibitions.start_time as exhibited_at, exhibitions.expire_at, bs.slot_index,
|
||||
(a.tags @> '["craft:lenticular"]') as is_lenticular
|
||||
FROM exhibitions
|
||||
JOIN assets a ON a.id = exhibitions.asset_id
|
||||
JOIN booth_slots bs ON bs.slot_id = exhibitions.slot_id
|
||||
@ -462,7 +464,8 @@ func (r *galleryRepository) GetUserExhibitedAssets(userID, starID int64, page, p
|
||||
err = r.db.Model(&models.Exhibition{}).
|
||||
Raw(`
|
||||
SELECT exhibitions.asset_id, a.name, a.cover_url, a.like_count,
|
||||
exhibitions.start_time as exhibited_at, exhibitions.expire_at, bs.slot_index
|
||||
exhibitions.start_time as exhibited_at, exhibitions.expire_at, bs.slot_index,
|
||||
(a.tags @> '["craft:lenticular"]') as is_lenticular
|
||||
FROM exhibitions
|
||||
JOIN assets a ON a.id = exhibitions.asset_id
|
||||
JOIN booth_slots bs ON bs.slot_id = exhibitions.slot_id
|
||||
|
||||
@ -217,6 +217,7 @@ func (s *exhibitionService) GetMyExhibitedAssets(ctx context.Context, userID, st
|
||||
ExpireAt: item.ExpireAt,
|
||||
Earnings: item.Earnings,
|
||||
SlotIndex: item.SlotIndex,
|
||||
IsLenticular: item.IsLenticular,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -39,11 +39,11 @@
|
||||
<view class="vignette" />
|
||||
</view>
|
||||
|
||||
<view v-if="showHint && tiltHintText" class="tilt-hint">
|
||||
<!-- <view v-if="showHint && tiltHintText" class="tilt-hint">
|
||||
<text class="tilt-hint-icon">↻</text>
|
||||
<text class="tilt-hint-text">{{ tiltHintText }}</text>
|
||||
<text v-if="approximatePreview" class="tilt-hint-sub">叠化近似预览(倾斜或拖动体验光栅效果)</text>
|
||||
</view>
|
||||
</view> -->
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@ -28,7 +28,17 @@
|
||||
<view v-for="(item, index) in exhibitionWorks" :key="item.id" class="exhibition-card"
|
||||
:class="index % 2 === 0 ? 'card-tilt-left' : 'card-tilt-right'"
|
||||
@tap="handleExhibitionCardTap(item, index)">
|
||||
<image class="card-image" :src="item.cover_url || '/static/nft/placeholder.png'"
|
||||
<LenticularCard
|
||||
v-if="item.is_lenticular"
|
||||
class="card-lenticular"
|
||||
:layers="getLenticularLayers(item.id)"
|
||||
:transforms="getLenticularTransforms(item.id)"
|
||||
:gyro-source="gyroSourceLabel"
|
||||
:skip-built-in-touch="false"
|
||||
:shimmer-mid-opacity="0.16"
|
||||
@simulate="(x, y) => onLenticularSimulate(item.id, x, y)"
|
||||
/>
|
||||
<image v-else class="card-image" :src="item.cover_url || '/static/nft/placeholder.png'"
|
||||
mode="aspectFill"></image>
|
||||
<!-- 领取收益按钮 -->
|
||||
<view class="claim-reward-btn" v-if="isRewardClaimable(item.id)">
|
||||
@ -167,12 +177,15 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { getMyExhibitedAssetsApi, getMyLikedAssetsApi, getMyTodayLikedAssetsApi, getMyWeekLikedAssetsApi, getMyGalleriesApi, placeAssetToGalleryApi } from '@/utils/api.js';
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
||||
import { getMyExhibitedAssetsApi, getMyLikedAssetsApi, getMyTodayLikedAssetsApi, getMyWeekLikedAssetsApi, getMyGalleriesApi, placeAssetToGalleryApi, getAssetMaterialsApi } from '@/utils/api.js';
|
||||
import { getExhibitionRevenue, claimExhibitionRevenue } from '@/utils/task-api.js';
|
||||
import AssetSelector from '../components/AssetSelector.vue';
|
||||
import { onShow } from '@dcloudio/uni-app';
|
||||
import { doubleTapLike } from '@/utils/likeHelper.js';
|
||||
import LenticularCard from '@/components/lenticular/LenticularCard.vue';
|
||||
import { useLenticularCraftTiltPreview } from '@/composables/useLenticularCraftTiltPreview.js';
|
||||
import { buildLenticularLayers } from '@/utils/castloveMintForm.js';
|
||||
|
||||
const goBack = () => {
|
||||
// 获取页面栈
|
||||
@ -496,6 +509,157 @@ const likedWorks = ref([]);
|
||||
// 点赞标签状态: current-当前, today-今日, week-本周
|
||||
const likedTab = ref('current');
|
||||
|
||||
// 光栅卡预览相关
|
||||
// 每张光栅卡独立的 transforms,通过 asset id 索引
|
||||
const lenticularTransformsMap = ref({});
|
||||
const lenticularLayersByAsset = ref({});
|
||||
const activeLenticularId = ref(null);
|
||||
const gyroSourceLabel = ref('device');
|
||||
|
||||
// 创建单一引擎供模拟倾斜使用
|
||||
const lenticularPhysics = ref(null);
|
||||
const lenticularEngine = ref(null);
|
||||
let lenticularRafId = null;
|
||||
|
||||
// 使用 useLenticularPreview 来管理光栅效果
|
||||
const lenticularLayersRef = ref([]);
|
||||
|
||||
function getLenticularLayers(assetId) {
|
||||
return lenticularLayersByAsset.value[assetId] || [];
|
||||
}
|
||||
|
||||
function getLenticularTransforms(assetId) {
|
||||
return lenticularTransformsMap.value[assetId] || {};
|
||||
}
|
||||
|
||||
async function loadLenticularLayersForAsset(assetId) {
|
||||
// 需要获取 bg + subject 素材构建 layers
|
||||
// 目前使用 buildLenticularLayers(coverUrl) 作为占位
|
||||
const item = exhibitionWorks.value.find(w => w.id === assetId);
|
||||
if (!item) return;
|
||||
|
||||
// 异步获取素材列表,参考 asset-detail.vue 的实现
|
||||
try {
|
||||
const materialsRes = await getAssetMaterialsApi(assetId);
|
||||
if (materialsRes.code === 200 && materialsRes.data) {
|
||||
const materialsList = Array.isArray(materialsRes.data) ? materialsRes.data : materialsRes.data.materials || [];
|
||||
let subjectUrl = item.cover_url;
|
||||
let bgUrl = '';
|
||||
for (const mat of materialsList) {
|
||||
if (mat.material_type === 'main' && mat.material_url_signed) {
|
||||
subjectUrl = mat.material_url_signed;
|
||||
}
|
||||
if (mat.material_type === 'bg' && mat.material_url_signed) {
|
||||
bgUrl = mat.material_url_signed;
|
||||
}
|
||||
}
|
||||
// 使用 buildLenticularLayersTwo 或 buildLenticularLayers
|
||||
if (bgUrl) {
|
||||
const { buildLenticularLayersTwo } = await import('@/utils/castloveMintForm.js');
|
||||
lenticularLayersByAsset.value[assetId] = buildLenticularLayersTwo(bgUrl, subjectUrl);
|
||||
} else {
|
||||
lenticularLayersByAsset.value[assetId] = buildLenticularLayers(subjectUrl);
|
||||
}
|
||||
// 初始化 transforms
|
||||
initTransformsForAsset(assetId);
|
||||
} else {
|
||||
lenticularLayersByAsset.value[assetId] = buildLenticularLayers(item.cover_url);
|
||||
initTransformsForAsset(assetId);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[myWorks] 获取素材列表失败:', e);
|
||||
lenticularLayersByAsset.value[assetId] = buildLenticularLayers(item.cover_url);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理光栅卡触摸模拟
|
||||
function onLenticularSimulate(assetId, x, y) {
|
||||
simulateLenticularTilt(assetId, x, y);
|
||||
}
|
||||
|
||||
// 处理光栅卡触摸模拟
|
||||
function simulateLenticularTilt(assetId, x, y) {
|
||||
if (!lenticularEngine.value) return;
|
||||
const layers = lenticularLayersByAsset.value[assetId];
|
||||
if (!layers || layers.length === 0) return;
|
||||
|
||||
// 更新引擎的层
|
||||
lenticularEngine.value.setLayers(layers);
|
||||
|
||||
// 计算渲染状态
|
||||
const renderState = lenticularEngine.value.feedSimulatedTilt(x, y ? y : 0);
|
||||
|
||||
const transforms = {};
|
||||
for (const l of layers) {
|
||||
const rs = renderState.layerOffsets.get(l.id);
|
||||
const op = renderState.layerOpacities.get(l.id);
|
||||
transforms[l.id] = {
|
||||
x: rs ? rs.x : 0,
|
||||
y: rs ? rs.y : 0,
|
||||
opacity: op != null ? op : l.opacity,
|
||||
};
|
||||
}
|
||||
// 创建新对象以触发 Vue 响应式更新
|
||||
lenticularTransformsMap.value = { ...lenticularTransformsMap.value, [assetId]: transforms };
|
||||
}
|
||||
|
||||
// 初始化光栅引擎
|
||||
function initLenticularEngine() {
|
||||
if (lenticularEngine.value) return;
|
||||
import('@/utils/lenticular-engine.js').then(({ LenticularEngine, DEFAULT_PHYSICS }) => {
|
||||
const physics = { ...DEFAULT_PHYSICS, parallaxDepth: 18 };
|
||||
physics.gyroSimEnabled = false;
|
||||
lenticularPhysics.value = physics;
|
||||
lenticularEngine.value = new LenticularEngine(physics);
|
||||
});
|
||||
}
|
||||
|
||||
// 为每个资产初始化 transforms 为空(居中状态)
|
||||
function initTransformsForAsset(assetId) {
|
||||
const layers = lenticularLayersByAsset.value[assetId];
|
||||
if (!layers || layers.length === 0) return;
|
||||
const transforms = {};
|
||||
for (const l of layers) {
|
||||
transforms[l.id] = { x: 0, y: 0, opacity: l.opacity || 1 };
|
||||
}
|
||||
lenticularTransformsMap.value = { ...lenticularTransformsMap.value, [assetId]: transforms };
|
||||
}
|
||||
|
||||
// 开始光栅渲染循环(为所有卡片更新 transforms)
|
||||
function startLenticularRenderLoop() {
|
||||
if (lenticularRafId !== null) return;
|
||||
const tick = () => {
|
||||
// 遍历所有有层数据的卡片,更新 transforms
|
||||
for (const assetId of Object.keys(lenticularLayersByAsset.value)) {
|
||||
const layers = lenticularLayersByAsset.value[assetId];
|
||||
if (!layers || layers.length === 0) continue;
|
||||
// 使用当前的 sensorData 或默认的 gamma=0 来渲染
|
||||
lenticularEngine.value.setLayers(layers);
|
||||
const renderState = lenticularEngine.value.feedSimulatedTilt(0, 0);
|
||||
const transforms = {};
|
||||
for (const l of layers) {
|
||||
const rs = renderState.layerOffsets.get(l.id);
|
||||
const op = renderState.layerOpacities.get(l.id);
|
||||
transforms[l.id] = {
|
||||
x: rs ? rs.x : 0,
|
||||
y: rs ? rs.y : 0,
|
||||
opacity: op != null ? op : l.opacity,
|
||||
};
|
||||
}
|
||||
lenticularTransformsMap.value = { ...lenticularTransformsMap.value, [assetId]: transforms };
|
||||
}
|
||||
lenticularRafId = requestAnimationFrame(tick);
|
||||
};
|
||||
lenticularRafId = requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
function stopLenticularRenderLoop() {
|
||||
if (lenticularRafId !== null) {
|
||||
cancelAnimationFrame(lenticularRafId);
|
||||
lenticularRafId = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 切换点赞标签
|
||||
const switchLikedTab = async (tab) => {
|
||||
if (likedTab.value === tab) return;
|
||||
@ -519,8 +683,17 @@ const loadExhibitedAssets = async () => {
|
||||
expire_at: item.expire_at,
|
||||
name: item.name,
|
||||
slot_index: item.slot_index ?? 0,
|
||||
is_lenticular: item.is_lenticular ?? false,
|
||||
}))
|
||||
.sort((a, b) => (a.slot_index ?? 0) - (b.slot_index ?? 0));
|
||||
|
||||
// 为每个光栅卡加载层级数据
|
||||
for (const item of exhibitionWorks.value) {
|
||||
if (item.is_lenticular) {
|
||||
loadLenticularLayersForAsset(item.id);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('展出作品:', exhibitionWorks.value);
|
||||
}
|
||||
} catch (err) {
|
||||
@ -562,6 +735,8 @@ const loadLikedAssets = async () => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initLenticularEngine();
|
||||
startLenticularRenderLoop();
|
||||
loadExhibitedAssets();
|
||||
loadLikedAssets();
|
||||
|
||||
@ -583,6 +758,7 @@ onUnmounted(() => {
|
||||
if (countdownTimer) {
|
||||
clearInterval(countdownTimer);
|
||||
}
|
||||
stopLenticularRenderLoop();
|
||||
uni.$off('userInfoUpdated');
|
||||
uni.$off('assetLiked');
|
||||
});
|
||||
@ -792,6 +968,18 @@ onShow(() => {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.card-lenticular {
|
||||
width: 88%;
|
||||
height: 93%;
|
||||
left: 5%;
|
||||
top: 4%;
|
||||
border-radius: 24rpx;
|
||||
transform-origin: center center;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 领取收益按钮 */
|
||||
.claim-reward-btn {
|
||||
position: absolute;
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
// 不需要手动注释!
|
||||
|
||||
// #ifdef H5
|
||||
const baseURL = 'http://192.168.110.60:8080' // H5 开发用本机
|
||||
// const baseURL = 'http://101.132.250.62:8080' // H5 开发用本机
|
||||
// const baseURL = 'http://192.168.110.60:8080' // H5 开发用本机
|
||||
const baseURL = 'http://101.132.250.62:8080' // H5 开发用本机
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
|
||||
Loading…
Reference in New Issue
Block a user