first commit

This commit is contained in:
2026-02-13 17:36:42 +08:00
commit f067e1bb78
155 changed files with 46676 additions and 0 deletions

View 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>