topfans/frontend/pages/profile/myWorks.vue
2026-05-31 20:48:26 +08:00

1661 lines
44 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="page-container">
<!-- 背景图片 -->
<image class="bg-image" src="/static/square/squearbj.png" mode="aspectFill"></image>
<!-- 顶部导航 -->
<view class="nav-bar">
<view class="nav-back" @tap="goBack">
<text class="nav-back-icon"></text>
</view>
<!-- <text class="nav-title">我的作品</text> -->
<view class="nav-placeholder"></view>
<view class="nav-settings" @tap="goToSettings">
<image class="nav-settings-icon" src="/static/icon/settings.png" mode="aspectFit"></image>
</view>
</view>
<view class="scroll-content">
<!-- 我的在展作品 -->
<view class="section-block section-1">
<view class="section-label section-label-1">
<image class="section-label-bg" src="/static/nft/dingbutubiao_liang.png" mode="aspectFill"></image>
<text class="section-label-text">我的在展作品</text>
</view>
<view class="exhibition-grid">
<!-- 左边展位 (slot_index=1) -->
<view v-if="exhibitionAtSlot[0]" class="exhibition-card card-tilt-left"
@tap="handleExhibitionCardTap(exhibitionAtSlot[0], 0)">
<LenticularCard v-if="exhibitionAtSlot[0].is_lenticular" class="card-lenticular"
:layers="getLenticularLayers(exhibitionAtSlot[0].id)"
:transforms="getLenticularTransforms(exhibitionAtSlot[0].id)" :gyro-source="gyroSourceLabel"
:skip-built-in-touch="true" :shimmer-mid-opacity="0.16"
@simulate="(x, y) => onLenticularSimulate(exhibitionAtSlot[0].id, x, y)" />
<image v-else class="card-image"
:src="exhibitionAtSlot[0].cover_url || '/static/nft/placeholder.png'" mode="aspectFill">
</image>
<!-- 领取收益按钮 -->
<view class="claim-reward-btn" v-if="isRewardClaimable(exhibitionAtSlot[0].id)">
<image class="claim-crystal-icon" src="/static/square/shuijingtubiao.png" mode="aspectFit">
</image>
<view @tap.stop="handleClaimReward(exhibitionAtSlot[0], 0)" class="claim-btn-text">领取收益
</view>
</view>
<image class="card-frame" src="/static/square/gerenzhongxincangpinkuang.png" mode="aspectFill">
</image>
<!-- 点赞数 -->
<view class="card-rate-badge">
<image class="heart-icon" src="/static/icon/heart-icon.png" mode="aspectFit"></image>
<view class="card-rate-text-wrap">
<text class="card-rate-text">{{ exhibitionAtSlot[0].like_count || 0 }}</text>
</view>
</view>
<view class="countdown-background" v-if="!isRewardClaimable(exhibitionAtSlot[0].id)"
:style="getCountdownBackgroundStyle()">
<text class="countdown-text">{{ formatCountdown(exhibitionAtSlot[0].id) }}</text>
</view>
<view class="card-income-row income-tilt-right">
<image class="topfans-icon" src="/static/icon/crystal.png" mode="aspectFit"></image>
<view class="card-income-text-wrap">
<text class="card-income-text">{{ exhibitionAtSlot[0].hourly_earnings || 0 }}/时</text>
</view>
</view>
</view>
<view v-else class="empty-card empty-card-left" @tap="openAssetSelector(1)">
<image class="empty-cover" src="/static/nft/placeholder.png" mode="aspectFill"></image>
<image class="card-frame" src="/static/square/gerenzhongxincangpinkuang.png" mode="aspectFill">
</image>
<view class="empty-add-btn"><text class="empty-add-icon">+</text></view>
</view>
<!-- 右边展位 (slot_index=2) -->
<view v-if="exhibitionAtSlot[1]" class="exhibition-card card-tilt-right"
@tap="handleExhibitionCardTap(exhibitionAtSlot[1], 1)">
<LenticularCard v-if="exhibitionAtSlot[1].is_lenticular" class="card-lenticular"
:layers="getLenticularLayers(exhibitionAtSlot[1].id)"
:transforms="getLenticularTransforms(exhibitionAtSlot[1].id)" :gyro-source="gyroSourceLabel"
:skip-built-in-touch="true" :shimmer-mid-opacity="0.16"
@simulate="(x, y) => onLenticularSimulate(exhibitionAtSlot[1].id, x, y)" />
<image v-else class="card-image"
:src="exhibitionAtSlot[1].cover_url || '/static/nft/placeholder.png'" mode="aspectFill">
</image>
<!-- 领取收益按钮 -->
<view class="claim-reward-btn" v-if="isRewardClaimable(exhibitionAtSlot[1].id)">
<image class="claim-crystal-icon" src="/static/square/shuijingtubiao.png" mode="aspectFit">
</image>
<view @tap.stop="handleClaimReward(exhibitionAtSlot[1], 1)" class="claim-btn-text">领取收益
</view>
</view>
<image class="card-frame" src="/static/square/gerenzhongxincangpinkuang.png" mode="aspectFill">
</image>
<view class="card-rate-badge">
<image class="heart-icon" src="/static/icon/heart-icon.png" mode="aspectFit"></image>
<view class="card-rate-text-wrap">
<text class="card-rate-text">{{ exhibitionAtSlot[1].like_count || 0 }}</text>
</view>
</view>
<view class="countdown-background" v-if="!isRewardClaimable(exhibitionAtSlot[1].id)"
:style="getCountdownBackgroundStyle()">
<text class="countdown-text">{{ formatCountdown(exhibitionAtSlot[1].id) }}</text>
</view>
<view class="card-income-row income-tilt-left">
<image class="topfans-icon" src="/static/icon/crystal.png" mode="aspectFit"></image>
<view class="card-income-text-wrap">
<text class="card-income-text">{{ exhibitionAtSlot[1].hourly_earnings || 0 }}/时</text>
</view>
</view>
</view>
<view v-else class="empty-card empty-card-right" @tap="openAssetSelector(2)">
<image class="empty-cover" src="/static/nft/placeholder.png" mode="aspectFill"></image>
<image class="card-frame" src="/static/square/gerenzhongxincangpinkuang.png" mode="aspectFill">
</image>
<view class="empty-add-btn"><text class="empty-add-icon">+</text></view>
</view>
</view>
</view>
<!-- 当前点赞作品 -->
<view class="section-block">
<view class="liked-tabs">
<view class="section-label" :class="{ 'tab-active': likedTab === 'current' }"
@tap="switchLikedTab('current')">
<image class="section-label-bg" src="/static/nft/dingbutubiao_liang.png" mode="aspectFill">
</image>
<text class="section-label-text">当前点赞作品</text>
</view>
<!-- <view class="section-label" :class="{ 'tab-active': likedTab === 'today' }"
@tap="switchLikedTab('today')">
<image class="section-label-bg" src="/static/nft/dingbutubiao_liang.png" mode="aspectFill">
</image>
<text class="section-label-text">今日点赞作品</text>
</view>
<view class="section-label" :class="{ 'tab-active': likedTab === 'week' }"
@tap="switchLikedTab('week')">
<image class="section-label-bg" src="/static/nft/dingbutubiao_liang.png" mode="aspectFill">
</image>
<text class="section-label-text">本周点赞作品</text>
</view> -->
</view>
<scroll-view class="liked-list" scroll-y="true" :show-scrollbar="false">
<view v-for="(item, index) in likedWorks" :key="item.id" class="liked-row"
@tap="goToAssetDetail(item.id)">
<!-- 排名图标,绝对定位在卡片左侧 -->
<image v-if="index < 3" :src="rankIcons[index]" :class="'rank-icon rank-icon-' + (index + 1)"
mode="aspectFit"></image>
<!-- 卡片主体 -->
<view class="liked-item" :class="index === 0 ? 'liked-item-first' : ''">
<!-- 作品封面 -->
<view class="liked-cover-wrap" :class="index === 0 ? 'liked-cover-wrap-first' : ''">
<LenticularCard v-if="item.is_lenticular" class="liked-lenticular"
:layers="getLikedLenticularLayers(item.id)"
:transforms="getLikedLenticularTransforms(item.id)" :gyro-source="gyroSourceLabel"
:skip-built-in-touch="true" :shimmer-mid-opacity="0.16"
@simulate="(x, y) => onLikedLenticularSimulate(item.id, x, y)" />
<image v-else class="liked-cover" :src="item.cover_url || '/static/nft/placeholder.png'"
mode="aspectFill"></image>
<image class="liked-cover-frame" src="/static/square/cangpinkuang1.png"
mode="aspectFill"></image>
</view>
<!-- 作品信息 -->
<view class="liked-info">
<text class="liked-status">{{ item.status_text }}</text>
<view class="liked-score-row">
<text class="liked-score">{{ formatScore(item.score) }}</text>
<image class="fire-icon" src="/static/square/rementubiao.png" mode="aspectFit">
</image>
</view>
</view>
<!-- 右侧奖励 -->
<view class="liked-reward" :class="{ 'reward-claimable': likedCountdowns[item.id]?.expired }" v-if="likedCountdowns[item.id]?.expired || (likedCountdowns[item.id] && showCountdownMode)">
<view class="liked-reward-box">
<image class="reward-token-icon"
:src="item.reward > 10 ? '/static/square/shuijingtubiao.png' : '/static/icon/crystal.png'"
mode="aspectFit">
</image>
<text class="reward-amount">{{ item.reward }}</text>
</view>
<text v-if="likedCountdowns[item.id]?.expired" class="reward-text" @tap.stop="handleClaimReward(item)">领取收益</text>
</view>
<view class="liked-reward" v-else>
<image class="reward-token-icon" mode="aspectFit" src="/static/assetDetail/time.png">
</image>
<text class="liked-countdown">{{ formatLikedCountdown(item.id) }}</text>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="likedWorks.length === 0" class="empty-liked">
<text class="empty-text">当前暂无点赞作品</text>
</view>
</scroll-view>
</view>
<!-- <view style="height: 60rpx;"></view> -->
</view>
<!-- 藏品选择器组件 -->
<AssetSelector :visible="showAssetSelector" :replace-asset="assetToReplace" @close="closeAssetSelector"
@select="handleAssetSelect" />
</view>
</template>
<script setup>
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';
import { LenticularEngine, DEFAULT_PHYSICS } from '@/utils/lenticular-engine.js';
const goBack = () => {
// 获取页面栈
const pages = getCurrentPages();
if (pages.length > 1) {
// 有上一页,执行返回
uni.navigateBack();
} else {
// 没有上一页跳转到square页面
uni.reLaunch({
url: '/pages/square/square'
});
}
};
const goToSettings = () => {
uni.navigateTo({
url: '/pages/profile/profile'
});
};
const goToCastlove = () => {
uni.navigateTo({
url: '/pages/castlove/mall'
});
};
// 藏品选择器相关
const showAssetSelector = ref(false);
const assetToReplace = ref(null);
const currentSlotIndex = ref(0); // 现在存储的是 slot_index (1 或 2)
// 我的展馆槽位信息(用于确定空位)
const mySlots = ref([]);
const loadGalleryInfo = async () => {
try {
const galleriesRes = await getMyGalleriesApi();
console.log('[DEBUG] 展馆API返回:', galleriesRes);
// 只取前2个可操作槽位按 slot_index 排序
mySlots.value = galleriesRes.data?.slots
.filter(s => s.can_operate)
.sort((a, b) => (a.slot_index ?? 0) - (b.slot_index ?? 0))
.slice(0, 2) || [];
console.log('[DEBUG] 加载展馆槽位 mySlots:', mySlots.value);
} catch (err) {
console.error('加载展馆信息失败:', err);
}
};
// 计算空位:哪些 slot_index 没有展出中
const emptySlotIndices = computed(() => {
const occupiedSlots = exhibitionWorks.value.map(w => w.slot_index).filter(idx => idx > 0);
return [1, 2].filter(idx => !occupiedSlots.includes(idx));
});
// 将作品映射到正确的左右位置slot 1=左边, slot 2=右边)
const exhibitionAtSlot = computed(() => {
// 创建一个长度为 2 的数组index 0=左边, index 1=右边
const slots = [null, null];
for (const item of exhibitionWorks.value) {
const pos = (item.slot_index ?? 0) - 1; // slot_index 1→0, 2→1
if (pos >= 0 && pos < 2) {
slots[pos] = item;
}
}
return slots;
});
const openAssetSelector = (slotIndex = 0) => {
// slotIndex 现在是 slot_index 值1 或 2
currentSlotIndex.value = slotIndex;
showAssetSelector.value = true;
};
const closeAssetSelector = () => {
showAssetSelector.value = false;
assetToReplace.value = null;
};
const handleAssetSelect = async ({ asset, isReplace, oldAsset }) => {
console.log('选中藏品:', asset, '替换模式:', isReplace, '槽位:', currentSlotIndex.value);
uni.showLoading({ title: '加载中...' });
try {
const galleriesRes = await getMyGalleriesApi();
console.log('展馆API返回:', galleriesRes);
const slots = galleriesRes.data?.slots || [];
const ownerId = galleriesRes.data?.gallery_owner_id;
console.log('槽位列表:', slots, 'ownerId:', ownerId);
if (slots.length === 0 || !ownerId) {
uni.showToast({ title: '暂无可用展馆', icon: 'none' });
return;
}
let targetSlotId = null;
if (isReplace && oldAsset) {
// 替换模式:根据旧藏品找到 slot_id
const slot = slots.find(s => s.asset_id === oldAsset.asset_id);
targetSlotId = slot?.slot_id;
} else {
// 新放置模式:用 currentSlotIndex就是 slot_index直接找
const targetSlot = slots.find(s => s.slot_index === currentSlotIndex.value);
targetSlotId = targetSlot?.slot_id;
}
if (!targetSlotId) {
uni.showToast({ title: '展馆已满', icon: 'none' });
return;
}
console.log('调用展出接口: asset_id=', asset.asset_id, 'ownerId=', ownerId, 'slotId=', targetSlotId);
await placeAssetToGalleryApi(asset.asset_id, ownerId, targetSlotId);
uni.showToast({ title: '展出成功', icon: 'success' });
await loadExhibitedAssets();
} catch (err) {
console.error('展出失败:', err);
uni.showToast({ title: err.message || '展出失败', icon: 'none' });
} finally {
uni.hideLoading();
}
};
const goToAssetDetail = (id) => {
if (!id) return;
uni.navigateTo({ url: `/pages/asset-detail/asset-detail?asset_id=${id}` });
};
// 双击点赞处理
const cardTapTimers = {};
// 监听点赞成功事件,更新收益显示
uni.$on('assetLiked', ({ asset_id, data }) => {
// 查找对应的在展作品
const index = exhibitionWorks.value.findIndex(item => item.id === asset_id);
if (index !== -1) {
// 如果返回了收益数据,更新显示
if (data?.earnings !== undefined) {
exhibitionWorks.value[index].earnings = data.earnings;
}
}
});
// 监听从详情页返回时的点赞更新
uni.$on('assetLikeChanged', ({ asset_id, like_count, earnings }) => {
const index = exhibitionWorks.value.findIndex(item => item.id === asset_id);
if (index !== -1) {
if (like_count !== undefined) {
exhibitionWorks.value[index].like_count = like_count;
}
if (earnings !== undefined) {
exhibitionWorks.value[index].earnings = earnings;
}
}
});
// 领取收益处理
const handleClaimReward = async (item, _index) => {
uni.showLoading({ title: '领取中...' });
try {
// 从缓存获取 star_id
const starId = uni.getStorageSync('star_id') || 1;
// 查询该藏品的可领取收益记录
const res = await getExhibitionRevenue(starId, 'claimable', 1, 100);
const records = res.data?.items || [];
// 找到对应资产ID的收益记录
const revenueRecord = records.find(r => r.asset_id === item.id);
console.log(item.id,records[1])
if (!revenueRecord) {
uni.showToast({ title: '一分钟延迟领取', icon: 'none' });
return;
}
// 调用领取接口
const claimRes = await claimExhibitionRevenue(revenueRecord.id, starId);
uni.showToast({ title: '收益已领取', icon: 'success' });
// 刷新列表
await loadExhibitedAssets();
await loadLikedAssets();
} catch (err) {
console.error('领取收益失败:', err);
uni.showToast({ title: err.message || '领取失败', icon: 'none' });
} finally {
uni.hideLoading();
}
};
const handleExhibitionCardTap = (item, index) => {
// 如果收益可领取,不触发点赞,直接返回
if (isRewardClaimable(item.id)) {
return;
}
if (cardTapTimers[item.id]) {
// 第二次点击,双击点赞
clearTimeout(cardTapTimers[item.id]);
delete cardTapTimers[item.id];
doubleTapLike(item.id, item.exhibition_id, async (success, data) => {
if (success) {
// 更新在展作品的点赞数
// exhibitionWorks.value[index].like_count = (exhibitionWorks.value[index].like_count || 0) + 1;
// // 如果返回了收益数据,更新显示
// if (data?.earnings !== undefined) {
// exhibitionWorks.value[index].earnings = data.earnings;
// } else {
// 如果没有返回收益数据,刷新列表获取最新收益
await loadExhibitedAssets();
await loadLikedAssets();
// }
uni.showToast({ title: '点赞成功', icon: 'success' });
// 将作品添加到当前点赞列表
// const likedItem = {
// id: item.id,
// cover_url: item.cover_url,
// like_count: exhibitionWorks.value[index].like_count,
// earnings: exhibitionWorks.value[index].earnings,
// name: item.name,
// status_text: '潜力待挖',
// score: exhibitionWorks.value[index].like_count,
// reward: item.earnings,
// };
// likedWorks.value.unshift(likedItem);
}
});
} else {
// 第一次点击,单击跳转
cardTapTimers[item.id] = setTimeout(() => {
delete cardTapTimers[item.id];
goToAssetDetail(item.id);
}, 300);
}
};
const rankIcons = [
'/static/square/icon1.png',
'/static/square/icon2.png',
'/static/square/icon3.png',
];
const formatScore = (score) => {
if (!score && score !== 0) return '0';
return Number(score).toLocaleString();
};
// 计算剩余时间
const calculateRemainingTime = (item) => {
if (item.remainSeconds !== undefined) {
if (item.remainSeconds <= 0) {
return { hours: 0, minutes: 0, seconds: 0, expired: true };
}
const hours = Math.floor(item.remainSeconds / 3600);
const minutes = Math.floor((item.remainSeconds % 3600) / 60);
const seconds = Math.floor(item.remainSeconds % 60);
return { hours, minutes, seconds, expired: false };
}
if (item.expire_at) {
const now = Date.now();
const expireTime = new Date(item.expire_at).getTime();
const remaining = expireTime - now;
if (remaining <= 0) {
return { hours: 0, minutes: 0, seconds: 0, expired: true };
}
const hours = Math.floor(remaining / (60 * 60 * 1000));
const minutes = Math.floor((remaining % (60 * 60 * 1000)) / (60 * 1000));
const seconds = Math.floor((remaining % (60 * 1000)) / 1000);
return { hours, minutes, seconds, expired: false };
}
return { hours: 0, minutes: 0, seconds: 0, expired: true };
};
// 更新所有倒计时
const updateCountdowns = () => {
exhibitionWorks.value.forEach(item => {
if (item && item.id) {
if (item.remainSeconds !== undefined && item.remainSeconds > 0) {
item.remainSeconds--;
}
countdowns.value[item.id] = calculateRemainingTime(item);
}
});
// 更新点赞作品倒计时
likedWorks.value.forEach(item => {
if (item && item.id) {
likedCountdowns.value[item.id] = calculateRemainingTime(item);
}
});
};
// 格式化倒计时显示
const formatCountdown = (itemId) => {
const countdown = countdowns.value[itemId];
if (!countdown || countdown.expired) return '00:00:00';
const h = String(countdown.hours).padStart(2, '0');
const m = String(countdown.minutes).padStart(2, '0');
const s = String(countdown.seconds).padStart(2, '0');
return `${h}:${m}:${s}`;
};
// 判断收益是否可领取(倒计时已到期)
const isRewardClaimable = (itemId) => {
const countdown = countdowns.value[itemId];
return countdown && countdown.expired;
};
// 获取倒计时背景样式
const getCountdownBackgroundStyle = () => {
return {
position: 'absolute',
bottom: '20rpx',
right: '40%',
transform: 'translateX(50%)',
width: '140rpx',
height: '36rpx',
zIndex: 9
};
};
// 更新点赞作品倒计时
const updateLikedCountdowns = () => {
likedWorks.value.forEach(item => {
if (item && item.id) {
likedCountdowns.value[item.id] = calculateRemainingTime(item);
}
});
};
// 格式化点赞作品倒计时显示
const formatLikedCountdown = (itemId) => {
const countdown = likedCountdowns.value[itemId];
if (!countdown || countdown.expired) return '';
const h = String(countdown.hours).padStart(2, '0');
const m = String(countdown.minutes).padStart(2, '0');
const s = String(countdown.seconds).padStart(2, '0');
return `${h}:${m}:${s}`;
};
// 在展作品列表
const exhibitionWorks = ref([]);
// 倒计时状态
const countdowns = ref({});
// 点赞作品倒计时状态
const likedCountdowns = ref({});
// 定时切换显示模式(奖励/倒计时)
const showCountdownMode = ref(false);
let displayModeTimer = null;
// 当前点赞作品列表
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 likedLenticularLayersByAsset = ref({});
const likedLenticularTransformsMap = ref({});
// 创建单一引擎供模拟倾斜使用
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] || {};
}
// 点赞作品光栅卡相关函数
function getLikedLenticularLayers(assetId) {
return likedLenticularLayersByAsset.value[assetId] || [];
}
function getLikedLenticularTransforms(assetId) {
return likedLenticularTransformsMap.value[assetId] || {};
}
function onLikedLenticularSimulate(assetId, x, y) {
simulateLikedLenticularTilt(assetId, x, y);
}
function simulateLikedLenticularTilt(assetId, x, y) {
if (!lenticularEngine.value) return;
const layers = likedLenticularLayersByAsset.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,
};
}
likedLenticularTransformsMap.value = { ...likedLenticularTransformsMap.value, [assetId]: transforms };
}
async function loadLikedLenticularLayersForAsset(assetId) {
const item = likedWorks.value.find(w => w.id === assetId);
if (!item) return;
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;
}
}
if (bgUrl) {
likedLenticularLayersByAsset.value[assetId] = buildLenticularLayersTwo(bgUrl, subjectUrl);
} else {
likedLenticularLayersByAsset.value[assetId] = buildLenticularLayers(subjectUrl);
}
initLikedTransformsForAsset(assetId);
} else {
likedLenticularLayersByAsset.value[assetId] = buildLenticularLayers(item.cover_url);
initLikedTransformsForAsset(assetId);
}
} catch (e) {
console.error('[myWorks] 获取点赞作品素材列表失败:', e);
likedLenticularLayersByAsset.value[assetId] = buildLenticularLayers(item.cover_url);
initLikedTransformsForAsset(assetId);
}
}
function initLikedTransformsForAsset(assetId) {
const layers = likedLenticularLayersByAsset.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 };
}
likedLenticularTransformsMap.value = { ...likedLenticularTransformsMap.value, [assetId]: transforms };
}
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) {
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;
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 };
}
// 遍历点赞作品的光栅卡
for (const assetId of Object.keys(likedLenticularLayersByAsset.value)) {
const layers = likedLenticularLayersByAsset.value[assetId];
if (!layers || layers.length === 0) continue;
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,
};
}
likedLenticularTransformsMap.value = { ...likedLenticularTransformsMap.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;
likedTab.value = tab;
likedWorks.value = [];
// 清理点赞作品光栅卡数据
likedLenticularLayersByAsset.value = {};
likedLenticularTransformsMap.value = {};
// 清理点赞作品倒计时
likedCountdowns.value = {};
await loadLikedAssets();
};
// 加载我的展出作品
const loadExhibitedAssets = async () => {
try {
const res = await getMyExhibitedAssetsApi(1, 20);
console.log('[DEBUG] 展出作品API返回:', res);
if (res.data && res.data.items) {
exhibitionWorks.value = res.data.items
.map(item => ({
id: item.asset_id,
exhibition_id: item.exhibition_id,
cover_url: item.cover_url,
like_count: item.like_count,
earnings: item.earnings,
hourly_earnings: item.hourly_earnings,
exhibited_at: item.exhibited_at,
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));
console.log('[DEBUG] 整理后的 exhibitionWorks:', exhibitionWorks.value);
// 为每个光栅卡加载层级数据
for (const item of exhibitionWorks.value) {
if (item.is_lenticular) {
loadLenticularLayersForAsset(item.id);
}
}
console.log('展出作品:', exhibitionWorks.value);
}
} catch (err) {
console.error('加载展出作品失败:', err);
}
};
// 加载我的点赞作品
const loadLikedAssets = async () => {
try {
let res;
switch (likedTab.value) {
case 'today':
res = await getMyTodayLikedAssetsApi(1, 20);
break;
case 'week':
res = await getMyWeekLikedAssetsApi(1, 20);
break;
default:
res = await getMyLikedAssetsApi(1, 20);
}
if (res.data && res.data.items) {
likedWorks.value = res.data.items.map((item, index) => ({
id: item.asset_id,
cover_url: item.cover_url,
like_count: item.like_count,
earnings: item.earnings,
liked_at: item.liked_at,
expire_at: item.expire_at,
name: item.name,
is_lenticular: item.is_lenticular ?? false,
status_text: item.status_text || '潜力待挖',
score: item.like_count,
reward: Math.floor(item.earnings || 0),
}));
// 为每个光栅卡加载层级数据
for (const item of likedWorks.value) {
if (item.is_lenticular) {
loadLikedLenticularLayersForAsset(item.id);
}
}
// 初始化点赞作品倒计时
updateLikedCountdowns();
}
} catch (err) {
console.error('加载点赞作品失败:', err);
}
};
onMounted(() => {
initLenticularEngine();
startLenticularRenderLoop();
loadGalleryInfo();
loadExhibitedAssets();
loadLikedAssets();
// 启动倒计时定时器
countdownTimer = setInterval(() => {
updateCountdowns();
}, 1000);
// 启动显示模式切换定时器30秒切换一次
displayModeTimer = setInterval(() => {
showCountdownMode.value = !showCountdownMode.value;
}, 30000);
// 监听身份切换事件,切换后刷新数据
uni.$on('userInfoUpdated', () => {
loadExhibitedAssets();
loadLikedAssets();
});
});
let countdownTimer = null;
onUnmounted(() => {
if (countdownTimer) {
clearInterval(countdownTimer);
}
if (displayModeTimer) {
clearInterval(displayModeTimer);
}
stopLenticularRenderLoop();
uni.$off('userInfoUpdated');
uni.$off('assetLiked');
});
onShow(() => {
// loadLikedAssets();
});
</script>
<style scoped>
.page-container {
min-height: 100vh;
position: relative;
}
/* 背景图片 */
.bg-image {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 120%;
z-index: 0;
}
/* 导航栏 */
.nav-bar {
position: relative;
z-index: 10;
display: flex;
align-items: center;
justify-content: space-between;
padding: 96rpx 32rpx 16rpx;
}
.nav-back {
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
/* background: rgba(255,255,255,0.5);
border-radius: 50%; */
}
.nav-back-icon {
font-size: 48rpx;
font-weight: bold;
color: #fff;
}
.nav-title {
font-size: 36rpx;
font-weight: 700;
color: #5a2d82;
letter-spacing: 2rpx;
}
.nav-placeholder {
width: 64rpx;
}
.nav-settings {
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(to bottom right,
#F0E4B1 0%,
#F08399 50%,
#B94E73 100%);
border-radius: 24rpx;
padding: 8rpx 20rpx 8rpx 20rpx;
box-shadow:
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4);
}
.nav-settings-icon {
width: 48rpx;
height: 48rpx;
}
/* 内容区域 */
.scroll-content {
position: relative;
z-index: 1;
/* padding: 0 32rpx; */
}
.section-block {
/* background: rgb(249 159 192 / 45%);
border-radius: 48rpx; */
padding: 16rpx;
}
/* 区块 */
.section-1 {
margin-bottom: 8rpx;
/* border-radius: 48rpx;
padding: 16rpx; */
}
/* 点赞标签容器 */
.liked-tabs {
display: flex;
gap: 16rpx;
margin-bottom: 16rpx;
}
/* 区块标签 */
.section-label {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
height: 80rpx;
min-width: 180rpx;
opacity: 0.6;
transition: opacity 0.2s;
}
.section-label.section-label-1 {
opacity: 1;
}
.section-label.tab-active {
opacity: 1;
}
.section-label-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.section-label-text {
position: relative;
z-index: 1;
font-size: 26rpx;
color: #fff;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.9);
font-weight: 600;
padding: 0 28rpx;
}
/* 在展作品网格 */
.exhibition-grid {
display: flex;
flex-direction: row;
/* gap: 24rpx; */
padding: 10rpx 10rpx 80rpx;
justify-content: center;
}
.exhibition-card {
width: 248rpx;
height: 380rpx;
border-radius: 20rpx;
overflow: visible;
position: relative;
}
.card-tilt-left {
transform: rotate(-4deg) translateY(10rpx);
margin-right: 32rpx;
border-radius: 32rpx;
box-shadow: -16rpx 16rpx 16rpx rgba(229, 76, 93, 0.9);
}
.card-tilt-right {
transform: rotate(4deg) translateY(10rpx);
margin-left: 32rpx;
border-radius: 32rpx;
box-shadow: 16rpx 16rpx 16rpx rgba(229, 76, 93, 0.9);
}
.card-income-row.income-tilt-right {
transform: translateX(-50%) rotate(4deg);
}
.card-income-row.income-tilt-left {
transform: translateX(-50%) rotate(-4deg);
}
.card-image {
width: 88%;
height: 92%;
border-radius: 80rpx;
transform-origin: center center;
position: relative;
z-index: 3;
padding: 16rpx;
overflow: hidden;
}
.card-frame {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
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;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 88%;
height: 92%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
z-index: 10;
background: rgba(0, 0, 0, 0.6);
border-radius: 16rpx;
gap: 8rpx;
}
.claim-crystal-icon {
width: 50%;
height: 50%;
position: absolute;
top: 50%;
transform: translateY(-50%)
}
.claim-btn-text {
background: linear-gradient(to bottom right,
#F0E4B1 0%,
#F08399 50%,
#B94E73 100%);
border-radius: 24rpx;
padding: 8rpx 20rpx;
box-shadow:
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4);
font-size: 22rpx;
color: #fff;
font-weight: 600;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.3);
position: absolute;
bottom: 24rpx;
}
.card-user-tag {
position: absolute;
bottom: 56rpx;
left: 0;
right: 0;
display: flex;
justify-content: center;
}
.card-user-text {
font-size: 20rpx;
color: #fff;
background: rgba(0, 0, 0, 0.45);
padding: 4rpx 14rpx;
border-radius: 20rpx;
}
.card-rate-badge {
position: absolute;
top: 16rpx;
left: 25%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 6rpx;
padding: 6rpx 16rpx;
z-index: 9;
}
/* 图片下方收益 */
.card-income-row {
position: absolute;
bottom: -88rpx;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
white-space: nowrap;
overflow: visible;
}
.topfans-icon {
width: 52rpx;
height: 52rpx;
position: relative;
z-index: 2;
margin-right: -16rpx;
left: 20rpx;
/* top: 8rpx */
}
.card-income-text-wrap {
width: 64rpx;
background: linear-gradient(to bottom right,
#F0E4B1 0%,
#F08399 50%,
#B94E73 100%);
border-radius: 999rpx;
padding: 8rpx 20rpx 8rpx 40rpx;
box-shadow:
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4);
display: flex;
align-items: center;
justify-content: center;
}
.card-income-text {
font-size: 16rpx;
color: #fff;
font-weight: 700;
text-align: center;
}
.heart-icon {
width: 28rpx;
height: 28rpx;
}
.card-rate-text-wrap {
background: linear-gradient(to bottom right,
#F0E4B1 0%,
#F08399 50%,
#B94E73 100%);
border-radius: 999rpx;
padding: 2rpx 16rpx;
box-shadow:
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4);
display: flex;
}
.card-rate-text {
font-size: 22rpx;
color: #fff;
font-weight: 700;
}
/* 倒计时背景 */
.countdown-background {
/* position: absolute; */
/* top: 20rpx;
left: 30%; */
/* transform: translateX(-50%); */
width: 140rpx;
height: 36rpx;
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
border-radius: 999rpx;
z-index: 9;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.4);
opacity: 0.95;
}
/* 倒计时文字 */
.countdown-text {
font-size: 22rpx;
font-weight: bold;
color: #fff;
font-family: 'yt', sans-serif;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.8);
z-index: 10;
font-variant-numeric: tabular-nums;
font-feature-settings: "tnum";
display: flex;
justify-content: center;
}
/* 空状态 */
.empty-exhibition {
display: flex;
align-items: center;
justify-content: center;
/* padding: 80rpx 0; */
gap: 24rpx;
}
.empty-card {
width: 248rpx;
height: 380rpx;
border-radius: 20rpx;
overflow: visible;
position: relative;
margin: 0 32rpx;
}
.empty-card-left {
transform: rotate(-4deg) translateY(10rpx);
}
.empty-card-right {
transform: rotate(4deg) translateY(10rpx);
}
.empty-cover {
width: 88%;
height: 92%;
border-radius: 80rpx;
position: relative;
z-index: 3;
padding: 16rpx;
opacity: 0.5;
}
/* 卡片内的添加按钮 */
.empty-add-btn {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80rpx;
height: 80rpx;
background: linear-gradient(135deg, #F0E4B1 0%, #F08399 50%, #B94E73 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
box-shadow: 0 4rpx 16rpx rgba(185, 78, 115, 0.4);
}
.empty-add-icon {
font-size: 48rpx;
color: #fff;
font-weight: 700;
line-height: 1;
}
.empty-text {
font-size: 28rpx;
color: #b09cc0;
}
/* 当前点赞列表 */
.liked-list {
max-height: 732rpx;
}
.liked-row {
display: flex;
align-items: center;
justify-content: flex-end;
margin-bottom: 16rpx;
}
.liked-item {
display: flex;
align-items: center;
background: #ffffff50;
border-radius: 48rpx;
padding: 24rpx 20rpx;
box-sizing: border-box;
width: 80%;
padding-left: 13%;
}
.liked-item-first {
padding: 28rpx 20rpx;
width: 90%;
padding-left: 20%;
background-image: url(/static/square/diyi.png);
background-size: 102%;
background-position: center;
background-repeat: no-repeat;
}
/* 排名图标 - 排名越靠前越大 */
.rank-icon {
flex-shrink: 0;
position: relative;
left: 32rpx;
}
.rank-icon-1 {
width: 96rpx;
height: 128rpx;
}
.rank-icon-2 {
width: 72rpx;
height: 104rpx;
}
.rank-icon-3 {
width: 64rpx;
height: 88rpx;
}
.rank-number-badge {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
background: rgba(180, 140, 220, 0.3);
display: flex;
align-items: center;
justify-content: center;
}
.rank-number-text {
font-size: 24rpx;
color: #fff;
font-weight: 700;
}
/* 作品封面 */
.liked-cover-wrap {
width: 88rpx;
height: 88rpx;
flex-shrink: 0;
margin-left: -18rpx;
/* margin-right: 48rpx; */
position: relative;
}
/* .liked-cover-wrap-first {
width: 88rpx;
height: 110rpx;
} */
.liked-cover {
width: 90%;
height: 90%;
border-radius: 24rpx;
transform: rotate(-22deg);
transform-origin: center center;
position: relative;
z-index: 3;
padding: 0.25rem;
}
.liked-lenticular {
width: 90%;
height: 90%;
border-radius: 24rpx;
transform: rotate(-22deg);
transform-origin: center center;
position: relative;
z-index: 3;
overflow: hidden;
}
.liked-cover-frame {
position: absolute;
top: 0;
left: 0;
width: 110%;
height: 110%;
z-index: 2;
transform: rotate(-22deg);
transform-origin: center center;
}
/* 作品信息 */
.liked-info {
display: flex;
flex-direction: column;
/* gap: 8rpx; */
/* overflow: hidden; */
margin: 0 16rpx 0 32rpx;
}
.liked-status {
font-size: 28rpx;
color: #fff;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.7);
font-weight: 600;
white-space: nowrap;
/* overflow: hidden; */
text-overflow: ellipsis;
margin-bottom: 16rpx;
}
.liked-score-row {
display: flex;
align-items: center;
/* gap: 6rpx; */
}
.liked-score {
font-size: 26rpx;
color: #fff;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.7);
font-weight: 700;
margin-right: 8rpx;
}
.fire-icon {
width: 32rpx;
height: 32rpx;
align-self: flex-end;
margin-top: 4rpx;
}
/* 右侧奖励 */
.liked-reward {
min-width: 136rpx;
padding: 8rpx 16rpx;
background: rgba(255, 255, 255, 0.3);
border-radius: 999rpx;
display: flex;
flex-direction: row;
align-items: center;
/* gap: 8rpx; */
}
.liked-reward.reward-claimable {
flex-direction: column;
background: none;
}
.liked-reward.reward-claimable .liked-reward-box{
justify-content: center;
}
.liked-reward.reward-claimable .liked-reward-box .reward-amount {
color: #ff9500;
}
.liked-reward-box{
width: 100%;
display:flex;
align-items: center;
}
.reward-token-icon {
width: 56rpx;
height: 56rpx;
margin-right: 8rpx;
}
.reward-amount {
font-size: 28rpx;
color: #fff;
font-weight: 600;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.7);
}
.reward-text{
background:rgba(255, 255, 255, 0.3) ;
border-radius: 24rpx;
padding: 8rpx 20rpx;
box-shadow:
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4);
font-size: 22rpx;
color: #fff;
}
.liked-countdown {
font-size: 22rpx;
color: #fff;
font-weight: 600;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.7);
}
/* 空状态 */
.empty-liked {
padding: 60rpx 0;
display: flex;
align-items: center;
justify-content: center;
}
</style>