feat: 增加的接口
This commit is contained in:
parent
e1a61c4519
commit
844402e673
@ -397,7 +397,7 @@ func (p *SocialProvider) GetMyLikedAssets(ctx context.Context, req *pb.GetMyLike
|
||||
zap.Int32("page_size", req.PageSize),
|
||||
)
|
||||
|
||||
return p.assetLikeService.GetMyLikedAssets(ctx, req)
|
||||
return p.assetLikeService.GetMyLikedAssets(ctx, req, userID, starID)
|
||||
}
|
||||
|
||||
// extractUserInfo 从 Dubbo attachments 中提取用户信息
|
||||
|
||||
554
frontend/pages/components/AssetSelector.vue
Normal file
554
frontend/pages/components/AssetSelector.vue
Normal file
@ -0,0 +1,554 @@
|
||||
<template>
|
||||
<view v-if="visible" class="asset-selector-mask" @tap="closeModal">
|
||||
<view class="asset-selector-modal" @tap.stop :class="{ 'show': animated }">
|
||||
<!-- 背景图片 -->
|
||||
<image class="modal-background" src="/static/background/starbook.jpg" mode="aspectFill"></image>
|
||||
|
||||
<!-- 内容包装器 -->
|
||||
<view class="modal-content">
|
||||
<!-- 顶部拖动区域 -->
|
||||
<view
|
||||
class="modal-drag-area"
|
||||
@touchstart="handleTouchStart"
|
||||
@touchmove="handleTouchMove"
|
||||
@touchend="handleTouchEnd"
|
||||
>
|
||||
<view class="modal-handle"></view>
|
||||
<text class="modal-title">选择要展出的藏品</text>
|
||||
</view>
|
||||
|
||||
<!-- 类型Tab -->
|
||||
<view class="modal-tabs">
|
||||
<view
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="modal-tab-item"
|
||||
:class="{ active: currentType === tab.key }"
|
||||
@tap="switchType(tab.key)"
|
||||
>
|
||||
<text>{{ tab.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载中 -->
|
||||
<view v-if="loading" class="modal-loading">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-else-if="!hasData" class="modal-empty">
|
||||
<text class="empty-text">暂无藏品</text>
|
||||
</view>
|
||||
|
||||
<!-- 藏品列表 -->
|
||||
<scroll-view v-else class="modal-scroll" scroll-y :show-scrollbar="false">
|
||||
<!-- 原创藏品:按 grade 分组 -->
|
||||
<template v-if="currentType === 'regular'">
|
||||
<view v-for="gradeItem in regularGrades" :key="gradeItem.grade" class="grade-section">
|
||||
<view class="grade-header">
|
||||
<text class="grade-title">{{ formatGrade(gradeItem.grade) }}</text>
|
||||
</view>
|
||||
<scroll-view class="asset-row" scroll-x :show-scrollbar="false" :enable-flex="true">
|
||||
<view class="asset-row-content">
|
||||
<view
|
||||
v-for="item in gradeItem.items"
|
||||
:key="item.asset_id"
|
||||
class="asset-item"
|
||||
@tap="selectAsset(item)"
|
||||
>
|
||||
<image
|
||||
class="asset-image"
|
||||
:src="item.coverUrl || '/static/nft/collection.png'"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="status-badge" :class="item.display_status === 1 ? 'badge-active' : 'badge-pending'">
|
||||
<text class="status-text">{{ item.display_status === 1 ? '已展示' : '待展示' }}</text>
|
||||
</view>
|
||||
<view class="asset-info">
|
||||
<text class="asset-name">{{ item.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 典藏/活动藏品:直接列表 -->
|
||||
<template v-else>
|
||||
<view class="grade-section">
|
||||
<scroll-view class="asset-row" scroll-x :show-scrollbar="false" :enable-flex="true">
|
||||
<view class="asset-row-content">
|
||||
<view
|
||||
v-for="item in currentItems"
|
||||
:key="item.asset_id"
|
||||
class="asset-item"
|
||||
@tap="selectAsset(item)"
|
||||
>
|
||||
<image
|
||||
class="asset-image"
|
||||
:src="item.coverUrl || '/static/nft/collection.png'"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="status-badge" :class="item.display_status === 1 ? 'badge-active' : 'badge-pending'">
|
||||
<text class="status-text">{{ item.display_status === 1 ? '已展示' : '待展示' }}</text>
|
||||
</view>
|
||||
<view class="asset-info">
|
||||
<text class="asset-name">{{ item.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { getMyAssetsApi } from '@/utils/api.js';
|
||||
import { getAssetCoverRealUrl } from '@/utils/assetImageHelper.js';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 替换模式:传入要被替换的藏品信息
|
||||
replaceAsset: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close', 'select']);
|
||||
|
||||
// Tab配置
|
||||
const tabs = [
|
||||
{ key: 'regular', label: '原创' },
|
||||
{ key: 'collection', label: '典藏' },
|
||||
{ key: 'activity', label: '活动' }
|
||||
];
|
||||
|
||||
const currentType = ref('regular');
|
||||
const loading = ref(false);
|
||||
const assetsGroups = ref([]);
|
||||
const animated = ref(false);
|
||||
|
||||
// 监听visible变化,控制动画
|
||||
watch(() => props.visible, (newVal) => {
|
||||
if (newVal) {
|
||||
setTimeout(() => {
|
||||
animated.value = true;
|
||||
}, 50);
|
||||
if (assetsGroups.value.length === 0) {
|
||||
loadAssets();
|
||||
}
|
||||
} else {
|
||||
animated.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
// 判断当前类型是否有数据
|
||||
const hasData = computed(() => {
|
||||
const group = assetsGroups.value.find(g => g.type === currentType.value);
|
||||
if (!group) return false;
|
||||
if (currentType.value === 'regular') {
|
||||
return group.grades && group.grades.some(g => g.items && g.items.length > 0);
|
||||
}
|
||||
return group.items && group.items.length > 0;
|
||||
});
|
||||
|
||||
// 获取原创藏品的等级分组
|
||||
const regularGrades = computed(() => {
|
||||
const group = assetsGroups.value.find(g => g.type === 'regular');
|
||||
return group && group.grades ? group.grades : [];
|
||||
});
|
||||
|
||||
// 获取典藏藏品列表
|
||||
const collectionItems = computed(() => {
|
||||
const group = assetsGroups.value.find(g => g.type === 'collection');
|
||||
return group && group.items ? group.items : [];
|
||||
});
|
||||
|
||||
// 获取活动藏品列表
|
||||
const activityItems = computed(() => {
|
||||
const group = assetsGroups.value.find(g => g.type === 'activity');
|
||||
return group && group.items ? group.items : [];
|
||||
});
|
||||
|
||||
// 当前类型的藏品列表
|
||||
const currentItems = computed(() => {
|
||||
if (currentType.value === 'collection') return collectionItems.value;
|
||||
if (currentType.value === 'activity') return activityItems.value;
|
||||
return [];
|
||||
});
|
||||
|
||||
// grade 中文转换
|
||||
const gradeMap = { 1: '一', 2: '二', 3: '三', 4: '四', 5: '五' };
|
||||
const formatGrade = (grade) => `等级${gradeMap[grade] || grade}`;
|
||||
|
||||
// 切换类型
|
||||
const switchType = (type) => {
|
||||
currentType.value = type;
|
||||
};
|
||||
|
||||
// 加载藏品列表
|
||||
const loadAssets = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await getMyAssetsApi(1, 20);
|
||||
if (response.code === 200 && response.data && response.data.data.groups) {
|
||||
// 收集所有需要处理的藏品项
|
||||
const allItems = [];
|
||||
const itemRefs = [];
|
||||
|
||||
// 处理分组数据
|
||||
const processedGroups = [];
|
||||
for (const group of response.data.data.groups) {
|
||||
const processedGroup = {
|
||||
type: group.type,
|
||||
category: group.category,
|
||||
category_name: group.category_name,
|
||||
total_count: group.total_count,
|
||||
has_more: group.has_more,
|
||||
grades: [],
|
||||
items: []
|
||||
};
|
||||
|
||||
// 处理 grades
|
||||
if (group.grades) {
|
||||
for (const grade of group.grades) {
|
||||
const processedGrade = {
|
||||
grade: grade.grade,
|
||||
total_count: grade.total_count,
|
||||
has_more: grade.has_more,
|
||||
items: []
|
||||
};
|
||||
for (const item of grade.items || []) {
|
||||
allItems.push(item);
|
||||
itemRefs.push({ type: 'grade', parent: processedGrade, grade: item.grade, category: null });
|
||||
}
|
||||
processedGroup.grades.push(processedGrade);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 items
|
||||
if (group.items) {
|
||||
for (const item of group.items) {
|
||||
allItems.push(item);
|
||||
itemRefs.push({ type: 'item', parent: processedGroup, grade: null, category: item.category });
|
||||
}
|
||||
}
|
||||
|
||||
processedGroups.push(processedGroup);
|
||||
}
|
||||
|
||||
// 并行处理所有封面URL
|
||||
const coverUrlPromises = allItems.map(item => getAssetCoverRealUrl(item.cover_url_signed));
|
||||
const coverUrls = await Promise.all(coverUrlPromises);
|
||||
|
||||
// 将处理好的封面URL填回数据结构
|
||||
for (let i = 0; i < allItems.length; i++) {
|
||||
const item = allItems[i];
|
||||
const ref = itemRefs[i];
|
||||
const processedItem = {
|
||||
asset_id: item.asset_id,
|
||||
name: item.name,
|
||||
coverUrl: coverUrls[i],
|
||||
display_status: item.display_status || 0,
|
||||
like_count: item.like_count,
|
||||
grade: ref.grade,
|
||||
category: ref.category
|
||||
};
|
||||
ref.parent.items.push(processedItem);
|
||||
}
|
||||
|
||||
assetsGroups.value = processedGroups;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取藏品列表失败:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 选择藏品
|
||||
const selectAsset = (asset) => {
|
||||
emit('select', {
|
||||
asset,
|
||||
isReplace: !!props.replaceAsset,
|
||||
oldAsset: props.replaceAsset
|
||||
});
|
||||
closeModal();
|
||||
};
|
||||
|
||||
// 关闭弹窗
|
||||
const closeModal = () => {
|
||||
animated.value = false;
|
||||
setTimeout(() => {
|
||||
emit('close');
|
||||
}, 300);
|
||||
};
|
||||
|
||||
// 弹窗滑动关闭
|
||||
const touchStartY = ref(0);
|
||||
const touchStartTime = ref(0);
|
||||
|
||||
const handleTouchStart = (e) => {
|
||||
touchStartY.value = e.touches[0].clientY;
|
||||
touchStartTime.value = Date.now();
|
||||
};
|
||||
|
||||
const handleTouchMove = (e) => {
|
||||
const currentY = e.touches[0].clientY;
|
||||
const deltaY = currentY - touchStartY.value;
|
||||
if (deltaY > 0) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const handleTouchEnd = (e) => {
|
||||
const currentY = e.changedTouches[0].clientY;
|
||||
const deltaY = currentY - touchStartY.value;
|
||||
const deltaTime = Date.now() - touchStartTime.value;
|
||||
|
||||
if (deltaY > 0) {
|
||||
const velocity = deltaY / deltaTime;
|
||||
if (deltaY > 100 || velocity > 0.5) {
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.asset-selector-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 9998;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.asset-selector-modal {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 80vh;
|
||||
border-top-left-radius: 40rpx;
|
||||
border-top-right-radius: 40rpx;
|
||||
overflow: hidden;
|
||||
transform: translateY(100%);
|
||||
transition: transform 0.3s ease-out;
|
||||
background: #0d0820;
|
||||
}
|
||||
|
||||
.asset-selector-modal.show {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.modal-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20rpx 30rpx 30rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.modal-drag-area {
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.modal-drag-area:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.modal-handle {
|
||||
width: 80rpx;
|
||||
height: 8rpx;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 4rpx;
|
||||
margin: 0 auto 20rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #e6e6e6;
|
||||
text-align: center;
|
||||
margin-bottom: 30rpx;
|
||||
margin-top: 30rpx;
|
||||
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modal-tabs {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 40rpx;
|
||||
padding: 20rpx 30rpx;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.modal-tab-item {
|
||||
padding: 12rpx 30rpx;
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
border-bottom: 4rpx solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-tab-item.active {
|
||||
color: #ffffff;
|
||||
border-bottom-color: #ffffff;
|
||||
}
|
||||
|
||||
.modal-loading,
|
||||
.modal-empty {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding-top: 200rpx;
|
||||
}
|
||||
|
||||
.loading-text,
|
||||
.empty-text {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.modal-scroll {
|
||||
height: calc(80vh - 200rpx);
|
||||
}
|
||||
|
||||
.modal-scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal-scroll {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.grade-section {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-radius: 16rpx;
|
||||
padding: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.grade-header {
|
||||
margin-bottom: 16rpx;
|
||||
padding-bottom: 10rpx;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.grade-title {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.asset-row {
|
||||
width: 100%;
|
||||
height: 288rpx;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.asset-row::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.asset-row {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.asset-row-content {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
padding-left: 24rpx;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.asset-item {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-right: 32rpx;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.asset-image {
|
||||
width: 192rpx;
|
||||
height: 224rpx;
|
||||
border-radius: 16rpx;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
position: absolute;
|
||||
top: 8rpx;
|
||||
right: 8rpx;
|
||||
border-radius: 8rpx;
|
||||
padding: 4rpx 8rpx;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.badge-active {
|
||||
background: linear-gradient(135deg, #FFD700, #FFA500);
|
||||
box-shadow: 0 0 12rpx rgba(255, 215, 0, 0.6);
|
||||
}
|
||||
|
||||
.badge-pending {
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 18rpx;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.asset-info {
|
||||
padding: 12rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.asset-name {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@ -27,7 +27,7 @@
|
||||
:key="item.id"
|
||||
class="exhibition-card"
|
||||
:class="index % 2 === 0 ? 'card-tilt-left' : 'card-tilt-right'"
|
||||
@tap="goToAssetDetail(item.id)"
|
||||
@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>
|
||||
@ -42,14 +42,29 @@
|
||||
<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.rate || '0' }}/H</text>
|
||||
<text class="card-income-text">{{ item.earnings || 0 }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态占位 -->
|
||||
<view v-if="exhibitionWorks.length === 0" class="empty-exhibition">
|
||||
<text class="empty-text">暂无在展作品</text>
|
||||
<!-- 空状态占位:显示剩余空展位卡片 -->
|
||||
<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>
|
||||
@ -110,19 +125,140 @@
|
||||
|
||||
<!-- <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?id=${id}` });
|
||||
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 = [
|
||||
@ -142,24 +278,56 @@ const exhibitionWorks = ref([]);
|
||||
// 今日点赞作品列表
|
||||
const likedWorks = ref([]);
|
||||
|
||||
// 模拟数据(实际接入API时替换)
|
||||
const loadMockData = () => {
|
||||
exhibitionWorks.value = [
|
||||
{ id: '1', cover_url: '/static/sucai/image-08.png', owner_name: 'u585', like_count: 1234, rate: '0.7' },
|
||||
{ id: '2', cover_url: '/static/sucai/image-11.png', owner_name: 'u585', like_count: 856, rate: '0.6' },
|
||||
];
|
||||
// 加载我的展出作品
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
likedWorks.value = [
|
||||
{ id: '1', cover_url: '/static/sucai/image-03.png', status_text: '排名破100', score: 1354321, reward: 20 },
|
||||
{ id: '2', cover_url: '/static/sucai/image-04.png', status_text: '排名破300', score: 354321, reward: 17 },
|
||||
{ id: '3', cover_url: '/static/sucai/image-05.png', status_text: '热度飙升中', score: 14321, reward: 15 },
|
||||
{ id: '4', cover_url: '/static/sucai/image-06.png', status_text: '潜力待挖中', score: 321, reward: 8 },
|
||||
{ id: '5', cover_url: '/static/sucai/image-07.png', status_text: '潜力待挖中', score: 89, reward: 3 },
|
||||
];
|
||||
// 加载我的点赞作品
|
||||
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(() => {
|
||||
loadMockData();
|
||||
loadExhibitedAssets();
|
||||
loadLikedAssets();
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
loadLikedAssets();
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -413,11 +581,61 @@ onMounted(() => {
|
||||
|
||||
/* 空状态 */
|
||||
.empty-exhibition {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60rpx 0;
|
||||
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 {
|
||||
|
||||
@ -30,6 +30,19 @@
|
||||
:style="{ width: card.w + 'px', height: card.h + 'px', borderRadius: card.radius + 'px' }"
|
||||
/>
|
||||
|
||||
<!-- 光波动画层 - 外层 -->
|
||||
<view
|
||||
class="wf-like-wave wf-like-wave-outer"
|
||||
:class="{ 'wf-like-wave-active': likingMap[card.id] }"
|
||||
:style="{ width: card.w + 'px', height: card.h + 'px', borderRadius: card.radius + 'px' }"
|
||||
/>
|
||||
<!-- 光波动画层 - 内层 -->
|
||||
<view
|
||||
class="wf-like-wave wf-like-wave-inner"
|
||||
:class="{ 'wf-like-wave-active': likingMap[card.id] }"
|
||||
:style="{ width: card.w + 'px', height: card.h + 'px', borderRadius: card.radius + 'px' }"
|
||||
/>
|
||||
|
||||
<!-- 底部点赞数 -->
|
||||
<view class="wf-card-footer">
|
||||
<image class="wf-heart" src="/static/icon/heart-icon.png" mode="aspectFit" />
|
||||
@ -45,6 +58,7 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { getRandomUsersApi, getOssPresignedUrlApi } from '@/utils/api.js'
|
||||
import { doubleTapLike } from '@/utils/likeHelper.js'
|
||||
|
||||
const props = defineProps({
|
||||
screenWidth: { type: Number, default: 375 },
|
||||
@ -69,6 +83,7 @@ const cards = ref([])
|
||||
const allUsers = ref([])
|
||||
const totalWidth = ref(0)
|
||||
const scrollLeft = ref(0)
|
||||
const likingMap = ref({}) // 记录正在播放点赞动画的卡片ID
|
||||
let currentScrollLeft = 0
|
||||
let idCounter = 0
|
||||
|
||||
@ -133,7 +148,7 @@ const scrollStyle = computed(() => ({
|
||||
width: props.screenWidth + 'px',
|
||||
height: (props.screenHeight - props.bannerBottom) + 'px',
|
||||
zIndex: 2,
|
||||
overflow: 'hidden',
|
||||
// overflow: 'hidden',
|
||||
}))
|
||||
|
||||
// ========== 布局引擎 ==========
|
||||
@ -267,7 +282,7 @@ const cardStyle = (card) => ({
|
||||
width: card.w + 'px',
|
||||
height: card.h + 'px',
|
||||
borderRadius: card.radius + 'px',
|
||||
overflow: 'hidden',
|
||||
// overflow: 'hidden',
|
||||
background: card.coverUrl ? 'transparent' : PALETTES[Math.abs(card.id) % PALETTES.length],
|
||||
})
|
||||
|
||||
@ -355,10 +370,37 @@ const appendMore = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 点击处理 ==========
|
||||
// ========== 点击处理(双击点赞,单击跳转) ==========
|
||||
const cardTapTimers = {};
|
||||
|
||||
const handleCardClick = (card) => {
|
||||
if (userInteracting) return
|
||||
emit('cardClick', card)
|
||||
|
||||
// if (cardTapTimers[card.id]) {
|
||||
// 第二次点击,双击点赞
|
||||
clearTimeout(cardTapTimers[card.id]);
|
||||
delete cardTapTimers[card.id];
|
||||
console.log('双击,触发动画');
|
||||
|
||||
// 触发动画
|
||||
likingMap.value = { ...likingMap.value, [card.id]: true };
|
||||
setTimeout(() => {
|
||||
likingMap.value = { ...likingMap.value, [card.id]: false };
|
||||
}, 600);
|
||||
|
||||
doubleTapLike(card.id, (success) => {
|
||||
if (success) {
|
||||
card.likes = (card.likes || 0) + 1;
|
||||
uni.showToast({ title: '点赞成功', icon: 'success' });
|
||||
}
|
||||
});
|
||||
// } else {
|
||||
// // 第一次点击,单击跳转
|
||||
// console.log('单击,等待跳转');
|
||||
// cardTapTimers[card.id] = setTimeout(() => {
|
||||
// delete cardTapTimers[card.id];
|
||||
// uni.navigateTo({ url: `/pages/asset-detail/asset-detail?asset_id=${card.id}` });
|
||||
// }, 300);
|
||||
// }
|
||||
}
|
||||
|
||||
// ========== 初始化 ==========
|
||||
@ -399,6 +441,8 @@ watch(() => [props.screenHeight, props.bannerBottom], () => {
|
||||
cursor: pointer;
|
||||
will-change: transform;
|
||||
transform-origin: top center;
|
||||
overflow: visible;
|
||||
pointer-events: visible;
|
||||
}
|
||||
|
||||
.wf-card:active {
|
||||
@ -454,4 +498,44 @@ watch(() => [props.screenHeight, props.bannerBottom], () => {
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* 光波动画 - 粉色发光边框扩散 */
|
||||
.wf-like-wave {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
border: 6rpx solid #ff6b9d;
|
||||
box-shadow: 0 0 16rpx 4rpx #ff6b9d, inset 0 0 16rpx 4rpx rgba(255, 107, 157, 0.6);
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
.wf-like-wave-outer {
|
||||
top: -20rpx;
|
||||
left: -20rpx;
|
||||
border-width: 12rpx;
|
||||
}
|
||||
|
||||
.wf-like-wave-inner {
|
||||
top: -4rpx;
|
||||
left: -4rpx;
|
||||
border-width: 6rpx;
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.wf-like-wave-active {
|
||||
animation: likeWave 1s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes likeWave {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1.25);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@ -95,10 +95,7 @@ const bannerBottomPx = computed(() => Math.round(screenWidth.value / 750 * 716))
|
||||
|
||||
// ========== Handlers ==========
|
||||
const handleCardClick = (card) => {
|
||||
if (!card.userId) return
|
||||
uni.navigateTo({
|
||||
url: `/pages/exhibition/exhibition?target_uid=${card.userId}`,
|
||||
})
|
||||
// WaterfallGrid 组件内部已处理单击跳转和双击点赞
|
||||
}
|
||||
|
||||
const handleActivityClick = (item) => {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 764 KiB After Width: | Height: | Size: 450 KiB |
BIN
frontend/static/sucai/image-17.png
Normal file
BIN
frontend/static/sucai/image-17.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/static/sucai/image-18.png
Normal file
BIN
frontend/static/sucai/image-18.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 572 KiB |
BIN
frontend/static/sucai/image-19.png
Normal file
BIN
frontend/static/sucai/image-19.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 198 KiB |
@ -588,4 +588,22 @@ export function getUserExhibitedAssetsApi(userId, page = 1, pageSize = 20) {
|
||||
// url: `/api/v1/users/${userId}/exhibited-assets?page=${page}&page_size=${pageSize}`,
|
||||
// method: 'GET'
|
||||
// })
|
||||
}
|
||||
|
||||
// ==================== 我的作品统计接口 ====================
|
||||
|
||||
// 获取我展出的作品列表
|
||||
export function getMyExhibitedAssetsApi(page = 1, pageSize = 20) {
|
||||
return request({
|
||||
url: `/api/v1/me/exhibited-assets?page=${page}&page_size=${pageSize}`,
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取我点赞的作品列表
|
||||
export function getMyLikedAssetsApi(page = 1, pageSize = 20) {
|
||||
return request({
|
||||
url: `/api/v1/me/liked-assets?page=${page}&page_size=${pageSize}`,
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
88
frontend/utils/likeHelper.js
Normal file
88
frontend/utils/likeHelper.js
Normal file
@ -0,0 +1,88 @@
|
||||
// 双击点赞工具函数
|
||||
|
||||
import { likeAssetApi } from './api.js';
|
||||
|
||||
// 存储已点赞的作品,key: assetId, value: 点赞日期 (YYYY-MM-DD)
|
||||
const LIKE_STORAGE_KEY = 'liked_assets_daily';
|
||||
|
||||
/**
|
||||
* 获取今天日期字符串
|
||||
*/
|
||||
function getTodayStr() {
|
||||
const now = new Date();
|
||||
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查作品今天是否已点赞
|
||||
*/
|
||||
function hasLikedToday(assetId) {
|
||||
try {
|
||||
const storage = uni.getStorageSync(LIKE_STORAGE_KEY) || {};
|
||||
const today = getTodayStr();
|
||||
return storage[assetId] === today;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录作品今天已点赞
|
||||
*/
|
||||
function markLikedToday(assetId) {
|
||||
try {
|
||||
const storage = uni.getStorageSync(LIKE_STORAGE_KEY) || {};
|
||||
storage[assetId] = getTodayStr();
|
||||
uni.setStorageSync(LIKE_STORAGE_KEY, storage);
|
||||
} catch (e) {
|
||||
console.error('存储点赞记录失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除过期的点赞记录(昨天及之前)
|
||||
*/
|
||||
function cleanExpiredLikes() {
|
||||
try {
|
||||
const storage = uni.getStorageSync(LIKE_STORAGE_KEY) || {};
|
||||
const today = getTodayStr();
|
||||
const keys = Object.keys(storage);
|
||||
keys.forEach(key => {
|
||||
if (storage[key] !== today) {
|
||||
delete storage[key];
|
||||
}
|
||||
});
|
||||
uni.setStorageSync(LIKE_STORAGE_KEY, storage);
|
||||
} catch (e) {
|
||||
console.error('清除过期点赞记录失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 双击点赞处理(每天每个作品只能点赞一次)
|
||||
* @param {string|number} assetId - 藏品ID
|
||||
* @param {Function} callback - 回调函数,参数为是否成功
|
||||
*/
|
||||
export function doubleTapLike(assetId, callback) {
|
||||
// 清理过期记录
|
||||
cleanExpiredLikes();
|
||||
|
||||
// 检查今天是否已点赞
|
||||
if (hasLikedToday(assetId)) {
|
||||
uni.showToast({ title: '今日已点赞', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
likeAssetApi(assetId).then(res => {
|
||||
console.log('点赞成功', res);
|
||||
markLikedToday(assetId);
|
||||
if (callback) callback(true);
|
||||
}).catch(err => {
|
||||
console.error('点赞失败:', err);
|
||||
// 如果是"已点赞"错误,也更新本地记录
|
||||
if (err.message && err.message.includes('already liked')) {
|
||||
markLikedToday(assetId);
|
||||
}
|
||||
if (callback) callback(false);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user