first commit
This commit is contained in:
317
src/components/WorkList/index.vue
Normal file
317
src/components/WorkList/index.vue
Normal file
@@ -0,0 +1,317 @@
|
||||
<template>
|
||||
<view class="work-list">
|
||||
<!-- 骨架屏 -->
|
||||
<view v-if="isLoading && workList.length === 0" class="skeleton-wrapper">
|
||||
<view class="waterfall">
|
||||
<view class="column">
|
||||
<view v-for="i in 3" :key="'l'+i" class="skeleton-card">
|
||||
<view class="skeleton-image"></view>
|
||||
<view class="skeleton-info">
|
||||
<view class="skeleton-title"></view>
|
||||
<view class="skeleton-bottom">
|
||||
<view class="skeleton-avatar"></view>
|
||||
<view class="skeleton-name"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="column">
|
||||
<view v-for="i in 3" :key="'r'+i" class="skeleton-card" :class="{'skeleton-short': i === 1}">
|
||||
<view class="skeleton-image"></view>
|
||||
<view class="skeleton-info">
|
||||
<view class="skeleton-title"></view>
|
||||
<view class="skeleton-bottom">
|
||||
<view class="skeleton-avatar"></view>
|
||||
<view class="skeleton-name"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 瀑布流双列布局 -->
|
||||
<view v-else class="waterfall" hover-class="none">
|
||||
<view class="column" hover-class="none">
|
||||
<WorkCard
|
||||
v-for="item in leftList"
|
||||
:key="item.id"
|
||||
:work="item"
|
||||
:page-visible="pageVisible"
|
||||
@click="handleWorkClick"
|
||||
@like-change="handleLikeChange"
|
||||
/>
|
||||
</view>
|
||||
<view class="column" hover-class="none">
|
||||
<WorkCard
|
||||
v-for="item in rightList"
|
||||
:key="item.id"
|
||||
:work="item"
|
||||
:page-visible="pageVisible"
|
||||
@click="handleWorkClick"
|
||||
@like-change="handleLikeChange"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view class="load-status">
|
||||
<view v-if="isLoading && workList.length > 0" class="loading">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="isFinished && workList.length > 0" class="no-more">
|
||||
<text class="no-more-text">没有更多了</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { getWorkList } from '@/api/work'
|
||||
import WorkCard from '@/components/WorkCard/index.vue'
|
||||
|
||||
const props = defineProps({
|
||||
// 自动加载模式的参数
|
||||
sortType: { type: String, default: 'hot' },
|
||||
categoryId: { type: [Number, String], default: null },
|
||||
// 外部数据模式的参数
|
||||
works: { type: Array, default: null },
|
||||
loading: { type: Boolean, default: false },
|
||||
finished: { type: Boolean, default: false },
|
||||
// 页面可见性,控制WorkCard中video组件的挂载/卸载
|
||||
pageVisible: { type: Boolean, default: true }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['click', 'item-click', 'like-change', 'load-more'])
|
||||
|
||||
// 内部数据(自动加载模式)
|
||||
const internalList = ref([])
|
||||
const internalLoading = ref(false)
|
||||
const internalHasMore = ref(true)
|
||||
const pageNum = ref(1)
|
||||
const pageSize = 10
|
||||
const MAX_LIST_SIZE = 100 // 列表最大条数,超出后裁剪头部释放内存
|
||||
|
||||
// 判断是否使用外部数据模式
|
||||
const isExternalMode = computed(() => props.works !== null)
|
||||
|
||||
// 统一的数据源
|
||||
const workList = computed(() => isExternalMode.value ? props.works : internalList.value)
|
||||
const isLoading = computed(() => isExternalMode.value ? props.loading : internalLoading.value)
|
||||
const isFinished = computed(() => isExternalMode.value ? props.finished : !internalHasMore.value)
|
||||
|
||||
// 瀑布流分列
|
||||
const leftList = computed(() => workList.value.filter((_, i) => i % 2 === 0))
|
||||
const rightList = computed(() => workList.value.filter((_, i) => i % 2 === 1))
|
||||
|
||||
// 加载数据(自动加载模式)
|
||||
const loadData = async (isRefresh = false) => {
|
||||
if (isExternalMode.value) return
|
||||
if (internalLoading.value) return
|
||||
if (!isRefresh && !internalHasMore.value) return
|
||||
|
||||
internalLoading.value = true
|
||||
|
||||
try {
|
||||
const params = {
|
||||
sortType: props.sortType || 'hot',
|
||||
pageNum: isRefresh ? 1 : pageNum.value,
|
||||
pageSize
|
||||
}
|
||||
|
||||
// 只有当 categoryId 存在且不为 null/undefined 时才添加
|
||||
if (props.categoryId != null && props.categoryId !== '' && props.categoryId !== 'null' && props.categoryId !== 'undefined') {
|
||||
params.categoryId = props.categoryId
|
||||
}
|
||||
|
||||
const res = await getWorkList(params)
|
||||
|
||||
if (isRefresh) {
|
||||
internalList.value = res?.list || []
|
||||
pageNum.value = 1
|
||||
} else {
|
||||
internalList.value = [...internalList.value, ...(res?.list || [])]
|
||||
}
|
||||
|
||||
// 裁剪超出上限的头部数据,防止内存溢出
|
||||
if (internalList.value.length > MAX_LIST_SIZE) {
|
||||
internalList.value = internalList.value.slice(-MAX_LIST_SIZE)
|
||||
}
|
||||
|
||||
internalHasMore.value = res?.hasNext ?? false
|
||||
pageNum.value++
|
||||
} catch (e) {
|
||||
console.error('加载作品失败', e)
|
||||
} finally {
|
||||
internalLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const refresh = () => {
|
||||
if (isExternalMode.value) return
|
||||
internalHasMore.value = true
|
||||
loadData(true)
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
const loadMore = () => {
|
||||
if (isExternalMode.value) {
|
||||
emit('load-more')
|
||||
} else if (!internalLoading.value && internalHasMore.value) {
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
|
||||
// 监听筛选条件变化(自动加载模式)
|
||||
watch(() => [props.sortType, props.categoryId], () => {
|
||||
if (!isExternalMode.value) {
|
||||
refresh()
|
||||
}
|
||||
}, { immediate: false, deep: true })
|
||||
|
||||
// 处理点击
|
||||
const handleWorkClick = (work) => {
|
||||
emit('click', work)
|
||||
emit('item-click', work)
|
||||
}
|
||||
|
||||
// 处理点赞变化
|
||||
const handleLikeChange = (data) => {
|
||||
emit('like-change', data)
|
||||
// 自动加载模式下更新内部数据
|
||||
if (!isExternalMode.value) {
|
||||
const item = internalList.value.find(w => w.id === data.id)
|
||||
if (item) {
|
||||
item.liked = data.liked
|
||||
item.likeCount = Math.max(0, data.likeCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 监听页面滚动到底部
|
||||
const onReachBottom = () => {
|
||||
loadMore()
|
||||
}
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({ refresh, loadMore })
|
||||
|
||||
onMounted(() => {
|
||||
if (!isExternalMode.value) {
|
||||
loadData(true)
|
||||
}
|
||||
uni.$on('reachBottom', onReachBottom)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
uni.$off('reachBottom', onReachBottom)
|
||||
// 释放列表数据内存
|
||||
internalList.value = []
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.work-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.waterfall {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
-webkit-transform: translateZ(0);
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.column {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
-webkit-transform: translateZ(0);
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
/* 骨架屏样式 */
|
||||
.skeleton-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.skeleton-card {
|
||||
background: #18181b;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.skeleton-image {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
background: linear-gradient(90deg, #27272a 25%, #3f3f46 50%, #27272a 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
.skeleton-short .skeleton-image {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.skeleton-info {
|
||||
padding: 10px 12px 12px;
|
||||
}
|
||||
|
||||
.skeleton-title {
|
||||
height: 16px;
|
||||
width: 80%;
|
||||
background: linear-gradient(90deg, #27272a 25%, #3f3f46 50%, #27272a 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.skeleton-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.skeleton-avatar {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(90deg, #27272a 25%, #3f3f46 50%, #27272a 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
.skeleton-name {
|
||||
height: 12px;
|
||||
width: 60px;
|
||||
margin-left: 6px;
|
||||
background: linear-gradient(90deg, #27272a 25%, #3f3f46 50%, #27272a 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.load-status {
|
||||
padding: 20px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-text,
|
||||
.no-more-text {
|
||||
font-size: 13px;
|
||||
color: #71717a;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user