topfans/frontend/pages/profile/myWorks.vue
2026-05-06 10:51:07 +08:00

808 lines
17 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/beijingban.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>
<view class="scroll-content">
<!-- 我的在展作品 -->
<view class="section-block section-1">
<view class="section-label">
<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)"
>
<image class="card-image" :src="item.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">{{ item.like_count || 0 }}</text>
</view>
</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="section-label">
<image class="section-label-bg" src="/static/nft/dingbutubiao_liang.png" mode="aspectFill"></image>
<text class="section-label-text">今日点赞作品</text>
</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-img"
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="/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 } from 'vue';
import { getMyExhibitedAssetsApi, getMyLikedAssetsApi, getMyGalleriesApi, placeAssetToGalleryApi } from '@/utils/api.js';
import AssetSelector from '../components/AssetSelector.vue';
import { onShow } from '@dcloudio/uni-app';
import { doubleTapLike } from '@/utils/likeHelper.js';
const goBack = () => {
uni.navigateBack();
};
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 = {};
const handleExhibitionCardTap = (item, index) => {
if (cardTapTimers[item.id]) {
// 第二次点击,双击点赞
clearTimeout(cardTapTimers[item.id]);
delete cardTapTimers[item.id];
doubleTapLike(item.id, (success) => {
if (success) {
// 更新在展作品的点赞数
exhibitionWorks.value[index].like_count = (exhibitionWorks.value[index].like_count || 0) + 1;
// 将作品添加到今日点赞列表
const likedItem = {
id: item.id,
cover_url: item.cover_url,
like_count: exhibitionWorks.value[index].like_count,
earnings: item.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/rank/rank-icon1.png',
'/static/rank/rank-icon2.png',
'/static/rank/rank-icon3.png',
];
const formatScore = (score) => {
if (!score && score !== 0) return '0';
return Number(score).toLocaleString();
};
// 在展作品列表
const exhibitionWorks = ref([]);
// 今日点赞作品列表
const likedWorks = ref([]);
// 加载我的展出作品
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,
}));
}
} catch (err) {
console.error('加载展出作品失败:', err);
}
};
// 加载我的点赞作品
const loadLikedAssets = async () => {
try {
const 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(() => {
loadExhibitedAssets();
loadLikedAssets();
});
onShow(() => {
loadLikedAssets();
});
</script>
<style scoped>
.page-container {
min-height: 100vh;
position: relative;
}
/* 背景图片 */
.bg-image {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
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;
}
/* 内容区域 */
.scroll-content {
position: relative;
z-index: 1;
/* padding: 0 32rpx; */
}
/* 区块 */
.section-1 {
margin-bottom: 8rpx;
background: rgb(249 159 192 / 45%);
border-radius: 48rpx;
padding: 16rpx;
}
/* 区块标签 */
.section-label {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
margin-bottom: 16rpx;
height: 80rpx;
min-width: 232rpx;
}
.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;
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;
}
.card-tilt-right {
transform: rotate(4deg) translateY(10rpx);
}
.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-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;
bottom: 16rpx;
left: 40%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 6rpx;
padding: 6rpx 16rpx;
z-index: 9;
}
/* 图片下方收益 */
.card-income-row {
position: absolute;
bottom: -52rpx;
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 {
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 12rpx;
box-shadow:
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4);
}
.card-rate-text {
font-size: 22rpx;
color: #fff;
font-weight: 700;
}
/* 空状态 */
.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: 32rpx;
padding: 16rpx 20rpx;
gap: 16rpx;
overflow: hidden;
box-sizing: border-box;
width: 80%;
padding-left: 10%;
}
.liked-item-first {
padding: 28rpx 20rpx;
width: 90%;
padding-left: 20%;
}
/* 排名徽章 */
.rank-icon-img {
width: 56rpx;
height: 68rpx;
flex-shrink: 0;
margin-right: 8rpx;
}
.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;
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;
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: #c060e0;
font-weight: 700;
}
/* 空状态 */
.empty-liked {
padding: 60rpx 0;
display: flex;
align-items: center;
justify-content: center;
}
</style>