243 lines
5.1 KiB
Vue
243 lines
5.1 KiB
Vue
<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: relative;
|
||
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;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.nft-cover {
|
||
/* 确保图片完全填满容器,无论原始比例如何 */
|
||
width: 100%;
|
||
height: 100%;
|
||
display: block;
|
||
/* 图片圆角处理,与容器保持一致 */
|
||
border-radius: 16rpx;
|
||
z-index: 2;
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* 添加按钮容器 */
|
||
.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;
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* 锁图标 */
|
||
.lock-icon {
|
||
width: 40%;
|
||
height: 40%;
|
||
display: block;
|
||
object-fit: contain;
|
||
}
|
||
</style>
|
||
|