- 添加了 itemDetail 组件,用于展示商品详细信息 - 在 home 页面中集成了瀑布流列表组件 - 实现了商品列表的加载更多和下拉刷新功能 - 添加了商品详情弹窗组件
209 lines
6.2 KiB
Vue
209 lines
6.2 KiB
Vue
<script setup>
|
||
import { ref, computed } from 'vue'
|
||
import { goodStore } from "@/stores/goods"
|
||
import DetailPopup from '../DetailPopup/index.vue'
|
||
import WaterfallFlow from '@/components/waterfallFlow/index.vue'
|
||
const {
|
||
itemList,
|
||
pageRef,
|
||
currentItem,
|
||
loading: storeLoading,
|
||
getArtworkList,
|
||
} = goodStore()
|
||
|
||
const localState = ref({
|
||
finished: false,
|
||
refreshing: false,
|
||
showDetail: false,
|
||
showHeight: ''
|
||
})
|
||
|
||
// 记录每个图片的加载状态
|
||
const imageLoadingStatus = ref({})
|
||
|
||
// 生成骨架屏列表数据
|
||
const skeletonList = computed(() => {
|
||
// 如果有实际数据,返回空数组
|
||
if (itemList.value && itemList.value.length > 0) return []
|
||
// 否则返回6个骨架屏项目
|
||
return Array(6).fill().map((_, index) => ({
|
||
id: `skeleton-${index}`,
|
||
isSkeletonItem: true
|
||
}))
|
||
})
|
||
|
||
// 合并实际列表和骨架屏列表
|
||
const displayList = computed(() => {
|
||
if (itemList.value && itemList.value.length > 0) return itemList.value
|
||
return skeletonList.value
|
||
})
|
||
|
||
// 图片加载完成处理函数
|
||
const handleImageLoad = (itemId) => {
|
||
imageLoadingStatus.value[itemId] = true
|
||
}
|
||
|
||
// 图片加载错误处理函数
|
||
const handleImageError = (itemId) => {
|
||
imageLoadingStatus.value[itemId] = true // 也标记为加载完成,避免一直显示骨架屏
|
||
}
|
||
|
||
// 加载更多
|
||
const loadMore = async () => {
|
||
pageRef.value.page++
|
||
const { finished } = await getArtworkList()
|
||
localState.value.finished = finished
|
||
}
|
||
|
||
// 刷新
|
||
const onRefresh = async () => {
|
||
try {
|
||
localState.value.refreshing = true
|
||
localState.value.finished = false
|
||
// 重置图片加载状态
|
||
imageLoadingStatus.value = {}
|
||
const { finished } = await getArtworkList(true)
|
||
localState.value.finished = finished
|
||
} finally {
|
||
localState.value.refreshing = false
|
||
}
|
||
}
|
||
// 打开详情
|
||
const openShow = async (item) => {
|
||
if (item.isSkeletonItem) return
|
||
localState.value.showDetail = true
|
||
currentItem.value = item
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="px-[16px] pt-[16px]">
|
||
<van-pull-refresh
|
||
v-model="localState.refreshing"
|
||
:success-duration="700"
|
||
@refresh="onRefresh"
|
||
>
|
||
<template #success>
|
||
<van-icon name="success" /> <span>{{ $t('home.refresh_show') }}</span>
|
||
</template>
|
||
<van-list
|
||
v-model:loading="storeLoading"
|
||
:finished="localState.finished"
|
||
:finished-text="$t('home.finished_text')"
|
||
@load="loadMore"
|
||
>
|
||
<div class="w-full flex gap-[16px]">
|
||
<WaterfallFlow :items="displayList" :column-count="2">
|
||
<template #default="{ item }">
|
||
<div
|
||
@click="openShow(item)"
|
||
class="w-full"
|
||
>
|
||
<div class="relative w-full">
|
||
<!-- 自定义骨架屏 -->
|
||
<div
|
||
v-if="item.isSkeletonItem || !imageLoadingStatus[item.id]"
|
||
class="custom-skeleton rounded-4px overflow-hidden"
|
||
>
|
||
<div class="skeleton-image"></div>
|
||
<div class="skeleton-content">
|
||
<div class="skeleton-title"></div>
|
||
<div class="skeleton-text"></div>
|
||
<div class="skeleton-text"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<template v-if="!item.isSkeletonItem">
|
||
<img
|
||
:src="item.artwork?.hdPic"
|
||
class="w-full object-cover rounded-4px"
|
||
:class="{'hidden': !imageLoadingStatus[item.id]}"
|
||
@load="handleImageLoad(item.id)"
|
||
@error="handleImageError(item.id)"
|
||
/>
|
||
<div
|
||
class="absolute rounded-2px overflow-hidden line-height-12px left-[8px] top-[8px] h-[17px] w-[60px] flex items-center justify-center bg-[#2b53ac] text-[12px] text-[#fff]"
|
||
:class="{'z-10': !imageLoadingStatus[item.id]}"
|
||
>
|
||
Lot{{item.index+25000 }}
|
||
</div>
|
||
</template>
|
||
</div>
|
||
<div v-if="!item.isSkeletonItem" class="pt-[8px]" :class="{'hidden': !imageLoadingStatus[item.id]}">
|
||
<div class="text-[14px] text-[#000000] leading-[20px]">
|
||
{{ item?.artwork?.name }} | {{item?.artwork?.artistName}}
|
||
</div>
|
||
<div class="mt-[4px] text-[12px] text-[#575757]">
|
||
{{ $t('home.start_price') }}:{{ item?.startPrice??0 }}
|
||
</div>
|
||
<div
|
||
v-if="item.soldPrice"
|
||
class="mt-[4px] text-[12px] text-[#b58047]"
|
||
>
|
||
{{ $t('home.close_price') }}:{{ item?.soldPrice??0 }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</WaterfallFlow>
|
||
</div>
|
||
</van-list>
|
||
</van-pull-refresh>
|
||
<DetailPopup v-model:show="localState.showDetail" :detailInfo="currentItem"></DetailPopup>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.content {
|
||
overflow-y: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
}
|
||
|
||
/* 自定义骨架屏样式 */
|
||
.custom-skeleton {
|
||
width: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background-color: #fff;
|
||
}
|
||
|
||
.skeleton-image {
|
||
width: 100%;
|
||
padding-bottom: 100%; /* 保持1:1的宽高比 */
|
||
background: linear-gradient(90deg, #f2f2f2 25%, #e6e6e6 37%, #f2f2f2 63%);
|
||
background-size: 400% 100%;
|
||
animation: skeleton-loading 1.4s ease infinite;
|
||
}
|
||
|
||
.skeleton-content {
|
||
padding: 8px 0;
|
||
}
|
||
|
||
.skeleton-title {
|
||
height: 16px;
|
||
margin-bottom: 8px;
|
||
background: linear-gradient(90deg, #f2f2f2 25%, #e6e6e6 37%, #f2f2f2 63%);
|
||
background-size: 400% 100%;
|
||
animation: skeleton-loading 1.4s ease infinite;
|
||
border-radius: 2px;
|
||
}
|
||
|
||
.skeleton-text {
|
||
height: 12px;
|
||
margin-top: 4px;
|
||
background: linear-gradient(90deg, #f2f2f2 25%, #e6e6e6 37%, #f2f2f2 63%);
|
||
background-size: 400% 100%;
|
||
animation: skeleton-loading 1.4s ease infinite;
|
||
border-radius: 2px;
|
||
width: 60%;
|
||
}
|
||
|
||
@keyframes skeleton-loading {
|
||
0% {
|
||
background-position: 100% 50%;
|
||
}
|
||
100% {
|
||
background-position: 0 50%;
|
||
}
|
||
}
|
||
</style> |