topfans/frontend/pages/profile/myWorks.vue
2026-05-18 19:45:34 +08:00

1402 lines
34 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">
<image class="nav-back-icon" src="/static/icon/back.png" mode="aspectFit"></image>
</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">
<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)">
<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)">
<image class="claim-crystal-icon" src="/static/square/shuijingtubiao.png" mode="aspectFit">
</image>
<view @tap.stop="handleClaimReward(item, index)" 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">{{ item.like_count || 0 }}</text>
</view>
</view>
<!-- 倒计时背景 -->
<view class="countdown-background" v-if="!isRewardClaimable(item.id)"
:style="getCountdownBackgroundStyle(index)">
<!-- 倒计时文字 -->
<text class="countdown-text">
{{ formatCountdown(item.id) }}
</text>
</view>
<!-- 图片下方收益 -->
<view class="card-income-row"
:class="index % 2 === 0 ? 'income-tilt-right' : '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">{{ item.earnings || 0 }}/时</text>
</view>
</view>
</view>
<!-- 空状态占位:显示剩余空展位卡片 -->
<view v-if="exhibitionWorks.length < 2" class="empty-exhibition">
<!-- 根据已展出数量决定显示几个空卡片 -->
<view v-if="exhibitionWorks.length === 0" class="empty-card empty-card-left"
@tap="openAssetSelector(0)">
<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 class="empty-card empty-card-right" @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>
</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' : ''">
<image 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">
<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>
</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';
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);
const openAssetSelector = (slotIndex = 0) => {
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);
// 过滤出可操作的槽位can_operate: true
const operatableSlots = slots.filter(s => s.can_operate);
console.log('可操作槽位:', operatableSlots);
if (operatableSlots.length === 0 || !ownerId) {
uni.showToast({ title: '暂无可用展馆', icon: 'none' });
return;
}
let targetSlotId = null;
if (isReplace && oldAsset) {
const slot = slots.find(s => s.asset_id === oldAsset.asset_id);
targetSlotId = slot?.slot_id;
} else {
// 使用 currentSlotIndex 对应可操作槽位列表中的槽位
const targetSlot = operatableSlots[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) => {
console.log('领取收益:', item);
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);
if (!revenueRecord) {
uni.showToast({ title: '一分钟延迟领取', icon: 'none' });
return;
}
// 调用领取接口
const claimRes = await claimExhibitionRevenue(revenueRecord.id, starId);
uni.showToast({ title: '收益已领取', icon: 'success' });
// 更新全局余额
if (claimRes?.data?.total_balance !== undefined) {
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 })
}
}
// 刷新列表
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, 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();
}
// 将作品添加到当前点赞列表
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: 0,
};
likedWorks.value.unshift(likedItem);
uni.showToast({ title: '点赞成功', icon: 'success' });
}
});
} 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);
}
});
};
// 格式化倒计时显示
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 exhibitionWorks = ref([]);
// 倒计时状态
const countdowns = ref({});
// 当前点赞作品列表
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;
likedTab.value = tab;
likedWorks.value = [];
await loadLikedAssets();
};
// 加载我的展出作品
const loadExhibitedAssets = async () => {
try {
const res = await getMyExhibitedAssetsApi(1, 20);
if (res.data && res.data.items) {
exhibitionWorks.value = res.data.items
.map(item => ({
id: item.asset_id,
cover_url: item.cover_url,
like_count: item.like_count,
earnings: item.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));
// 为每个光栅卡加载层级数据
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,
name: item.name,
// 暂时用排名模拟状态文字
status_text: index < 3 ? '排名进榜' : '潜力待挖',
score: item.like_count,
reward: Math.floor(item.earnings || 0),
}));
}
} catch (err) {
console.error('加载点赞作品失败:', err);
}
};
onMounted(() => {
initLenticularEngine();
startLenticularRenderLoop();
loadExhibitedAssets();
loadLikedAssets();
// 启动倒计时定时器
countdownTimer = setInterval(() => {
updateCountdowns();
}, 1000);
// 监听身份切换事件,切换后刷新数据
uni.$on('userInfoUpdated', () => {
loadExhibitedAssets();
loadLikedAssets();
});
});
let countdownTimer = null;
onUnmounted(() => {
if (countdownTimer) {
clearInterval(countdownTimer);
}
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: 80rpx 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 {
width: 80rpx;
height: 80rpx;
}
.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);
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;
}
.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;
gap: 16rpx;
overflow: hidden;
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-cover-frame {
position: absolute;
top: 0;
left: 0;
width: 110%;
height: 110%;
z-index: 2;
transform: rotate(-22deg);
transform-origin: center center;
}
/* 作品信息 */
.liked-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
/* gap: 8rpx; */
overflow: hidden;
}
.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 {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 8rpx;
flex-shrink: 0;
}
.reward-token-icon {
width: 56rpx;
height: 56rpx;
}
.reward-amount {
font-size: 28rpx;
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>