topfans/frontend/pages/components/NftCard.vue
2026-04-07 23:08:49 +08:00

240 lines
5.1 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="nft-card"
:class="{ 'clickable': coverImage && !locked }"
:style="cardStyle"
@click="handleCardClick"
>
<view class="nft-border-wrapper">
<image
class="nft-border"
:class="{ 'nft-border-empty': !coverImage }"
src="/static/nft/border.png"
mode="aspectFit"
></image>
<view class="nft-cover-wrapper" v-if="coverImage">
<image class="nft-cover" :src="currentImage" mode="aspectFill" @error="onImageError"></image>
</view>
<!-- 展馆页面中没有封面且可添加时显示添加按钮 -->
<view class="add-button-wrapper" v-if="!coverImage && showAddButton && !locked && operation === 'place'" @click="handleAddClick">
<image class="add-button-icon" src="/static/icon/add.png" mode="aspectFit"></image>
</view>
<!-- 锁定状态蒙层 -->
<view class="locked-overlay" v-if="locked">
<image class="lock-icon" src="/static/icon/lock.png" mode="aspectFit"></image>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, watch } from 'vue';
// 定义 props
const props = defineProps({
// 藏品封面图片路径
coverImage: {
type: String,
default: '/static/nft/collection.png'
},
// 藏品卡片宽度单位px
width: {
type: [Number, String],
default: 100
},
// 藏品卡片高度单位px
height: {
type: [Number, String],
default: 100
},
// 自定义样式对象
customStyle: {
type: Object,
default: () => ({})
},
// 是否显示添加按钮(用于展馆页面)
showAddButton: {
type: Boolean,
default: false
},
// 槽位可见性public-公开 | private-私有
visibility: {
type: String,
default: 'public'
},
// 操作类型place-可添加 | remove-可移除 | none-不可操作
operation: {
type: String,
default: 'none'
},
// 是否锁定(用于星册页面)
locked: {
type: Boolean,
default: false
}
});
// 定义 emit
const emit = defineEmits(['click', 'imageError', 'add']);
// 默认图片路径
const DEFAULT_IMAGE = '/static/nft/collection.png';
// 当前显示的图片路径
const currentImage = ref(props.coverImage || DEFAULT_IMAGE);
// 监听 coverImage 变化
watch(() => props.coverImage, (newValue) => {
currentImage.value = newValue || DEFAULT_IMAGE;
});
// 计算卡片样式
const cardStyle = computed(() => {
const baseStyle = {
width: typeof props.width === 'number' ? `${props.width}px` : props.width,
height: typeof props.height === 'number' ? `${props.height}px` : props.height
};
return {
...baseStyle,
...props.customStyle
};
});
// 图片加载失败处理
const onImageError = (e) => {
console.error('藏品图片加载失败,使用默认图片');
// 如果不是默认图片加载失败则fallback到默认图片
if (currentImage.value !== DEFAULT_IMAGE) {
currentImage.value = DEFAULT_IMAGE;
}
emit('imageError', e);
};
// 卡片点击处理
const handleCardClick = () => {
if (props.coverImage && !props.locked) {
emit('click');
}
};
// 添加按钮点击处理
const handleAddClick = () => {
emit('add');
};
</script>
<style scoped>
.nft-card {
position: absolute;
display: block;
pointer-events: none;
transition: transform 0.2s ease;
}
.nft-card.clickable {
pointer-events: auto;
cursor: pointer;
}
.nft-card.clickable:active {
transform: scale(0.95);
}
.nft-border-wrapper {
position: relative;
width: 100%;
height: 100%;
}
.nft-border {
width: 100%;
height: 100%;
display: block;
z-index: 5;
position: relative;
}
/* 封面为空时边框的阴影效果 */
.nft-border-empty {
filter: drop-shadow(0 8rpx 8rpx rgba(0, 0, 0, 0.85));
}
.nft-cover-wrapper {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
/* 根据边框的3:4比例计算封面区域的实际尺寸 */
/* 使用 65% 让显示区域比边框空白区域略小,确保图片不会超出边框 */
width: 70%;
/* 高度根据3:4比例计算宽度的 4/3 倍 */
height: 100%; /* 65% * 4/3 = 86.67% */
overflow: hidden;
/* 容器圆角处理 */
border-radius: 16rpx;
z-index: 1;
/* 确保容器本身不会超出 */
box-sizing: border-box;
}
.nft-cover {
/* 确保图片完全填满容器,无论原始比例如何 */
width: 100%;
height: 100%;
display: block;
/* 图片圆角处理,与容器保持一致 */
border-radius: 16rpx;
z-index: 2;
}
/* 添加按钮容器 */
.add-button-wrapper {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 40%;
height: 40%;
display: flex;
align-items: center;
justify-content: center;
z-index: 3;
pointer-events: auto;
}
/* 添加按钮图标 */
.add-button-icon {
width: 100%;
height: 100%;
display: block;
object-fit: contain;
filter: drop-shadow(0 8rpx 8rpx rgba(0, 0, 0, 0.85));
}
/* 锁定状态蒙层 */
.locked-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 70%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
z-index: 4;
}
/* 锁图标 */
.lock-icon {
width: 40%;
height: 40%;
display: block;
object-fit: contain;
}
</style>