topfans/frontend/pages/profile/hisWorks.vue

1128 lines
29 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">{{ nickname }}的作品</text>
<view class="nav-placeholder"></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="goToAssetDetail(exhibitionAtSlot[0].id)">
<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>
<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" :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].earnings || 0 }}/时</text>
</view>
</view>
</view>
<view v-else class="empty-card empty-card-left">
<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>
<!-- 右边展位 (slot_index=2) -->
<view v-if="exhibitionAtSlot[1]" class="exhibition-card card-tilt-right"
@tap="goToAssetDetail(exhibitionAtSlot[1].id)">
<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>
<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" :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].earnings || 0 }}/时</text>
</view>
</view>
</view>
<view v-else class="empty-card empty-card-right">
<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>
</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>
<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">
<image class="reward-token-icon"
:src="item.earnings > 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>
</view>
</template>
<script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { getUserGalleriesApi, getUserLikedAssetsApi, getAssetMaterialsApi } from '@/utils/api.js';
import LenticularCard from '@/components/lenticular/LenticularCard.vue';
import { useLenticularCraftTiltPreview } from '@/composables/useLenticularCraftTiltPreview.js';
import { buildLenticularLayers, buildLenticularLayersTwo } from '@/utils/castloveMintForm.js';
import { LenticularEngine, DEFAULT_PHYSICS } from '@/utils/lenticular-engine.js';
const userId = ref('');
const nickname = ref('');
onLoad((options) => {
if (options.userId) userId.value = options.userId;
if (options.nickname) nickname.value = decodeURIComponent(options.nickname);
});
const goBack = () => {
const pages = getCurrentPages();
if (pages.length > 1) {
uni.navigateBack();
} else {
uni.reLaunch({
url: '/pages/square/square'
});
}
};
const goToAssetDetail = (id) => {
if (!id) return;
uni.navigateTo({ url: `/pages/asset-detail/asset-detail?asset_id=${id}` });
};
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 getCountdownBackgroundStyle = () => {
return {
position: 'absolute',
bottom: '20rpx',
right: '40%',
transform: 'translateX(50%)',
width: '140rpx',
height: '36rpx',
zIndex: 9
};
};
const rankIcons = [
'/static/square/icon1.png',
'/static/square/icon2.png',
'/static/square/icon3.png',
];
// 在展作品列表
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 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('[hisWorks] 获取点赞作品素材列表失败:', 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) {
const item = exhibitionWorks.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) {
lenticularLayersByAsset.value[assetId] = buildLenticularLayersTwo(bgUrl, subjectUrl);
} else {
lenticularLayersByAsset.value[assetId] = buildLenticularLayers(subjectUrl);
}
initTransformsForAsset(assetId);
} else {
lenticularLayersByAsset.value[assetId] = buildLenticularLayers(item.cover_url);
initTransformsForAsset(assetId);
}
} catch (e) {
console.error('[hisWorks] 获取素材列表失败:', 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;
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;
}
}
// 将作品映射到正确的左右位置slot 1=左边, slot 2=右边)
const exhibitionAtSlot = computed(() => {
const slots = [null, null];
for (const item of exhibitionWorks.value) {
const pos = (item.slot_index ?? 0) - 1;
if (pos >= 0 && pos < 2) {
slots[pos] = item;
}
}
return slots;
});
// 切换点赞标签
const switchLikedTab = async (tab) => {
if (likedTab.value === tab) return;
likedTab.value = tab;
likedWorks.value = [];
// 清理点赞作品光栅卡数据
likedLenticularLayersByAsset.value = {};
likedLenticularTransformsMap.value = {};
await loadLikedAssets();
};
// 加载他人的展出作品
const loadExhibitedAssets = async () => {
if (!userId.value) return;
try {
const res = await getUserGalleriesApi(userId.value);
if (res.data && res.data.slots) {
exhibitionWorks.value = res.data.slots
.filter(slot => slot.status === 'OCCUPIED' && slot.asset)
.map(slot => ({
id: slot.asset.asset_id,
cover_url: slot.asset.cover_url,
like_count: slot.asset.like_count,
earnings: slot.asset.earnings || 0,
name: slot.asset.name,
slot_index: slot.slot_index ?? 0,
is_lenticular: slot.asset.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);
}
}
}
} catch (err) {
console.error('加载展出作品失败:', err);
}
};
// 加载他人的点赞作品
const loadLikedAssets = async () => {
if (!userId.value) return;
try {
let res;
switch (likedTab.value) {
case 'today':
// 后端暂无今日/本周接口,暂时复用全部接口
res = await getUserLikedAssetsApi(userId.value, 1, 20);
break;
case 'week':
res = await getUserLikedAssetsApi(userId.value, 1, 20);
break;
default:
res = await getUserLikedAssetsApi(userId.value, 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,
name: item.name,
is_lenticular: item.is_lenticular ?? false,
status_text: index < 3 ? '排名进榜' : '潜力待挖',
score: item.like_count,
reward: Math.floor(item.earnings || 0),
}));
// 为每个光栅卡加载层级数据
for (const item of likedWorks.value) {
if (item.is_lenticular) {
loadLikedLenticularLayersForAsset(item.id);
}
}
}
} catch (err) {
console.error('加载点赞作品失败:', err);
}
};
onMounted(() => {
initLenticularEngine();
startLenticularRenderLoop();
loadExhibitedAssets();
loadLikedAssets();
// 启动倒计时定时器
countdownTimer = setInterval(() => {
updateCountdowns();
}, 1000);
});
let countdownTimer = null;
onUnmounted(() => {
if (countdownTimer) {
clearInterval(countdownTimer);
}
stopLenticularRenderLoop();
});
</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;
}
.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;
}
/* 内容区域 */
.scroll-content {
position: relative;
z-index: 1;
}
.section-block {
padding: 16rpx;
}
/* 区块 */
.section-1 {
margin-bottom: 8rpx;
}
/* 点赞标签容器 */
.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;
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;
}
.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;
}
.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 {
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-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-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;
}
/* 作品封面 */
.liked-cover-wrap {
width: 88rpx;
height: 88rpx;
flex-shrink: 0;
margin-left: -18rpx;
position: relative;
}
.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;
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;
text-overflow: ellipsis;
margin-bottom: 16rpx;
}
.liked-score-row {
display: flex;
align-items: center;
}
.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;
}
.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);
}
/* 空状态 */
.empty-liked {
padding: 60rpx 0;
display: flex;
align-items: center;
justify-content: center;
}
</style>