1662 lines
44 KiB
Vue
1662 lines
44 KiB
Vue
<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%;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.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>
|