You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

508 lines
12 KiB

<template>
<view class="waterfall-layout">
<!-- 空状态 -->
<view v-if="!leftItems.length && !rightItems.length" class="empty-state">
<text class="empty-title">暂无内容</text>
<text class="empty-desc">快来发布第一篇笔记吧~</text>
</view>
<!-- 瀑布流内容 -->
<view v-else class="waterfall-container">
<!-- 左列 -->
<view class="column">
<view
v-for="(item, index) in leftItems"
class="waterfall-item"
@click="handleItemClick(index, leftItems)"
>
<view class="image-container">
<image
v-if="item.coverImage"
:src="item.coverImage && item.coverImage.split(',')[0]"
class="item-image"
mode="aspectFill"
/>
<!-- 状态蒙层 -->
<view
v-if="item.status === 0 || item.status === -1"
class="status-overlay"
>
<text class="status-text">{{
item.status === 0 ? "待审核" : "审核不通过"
}}</text>
</view>
</view>
<view class="item-content">
<text v-if="item.title" class="item-title">{{ item.title }}</text>
<view class="item-footer">
<view class="user-info">
<image
:src="item.headImg"
class="user-avatar"
mode="aspectFill"
/>
<text class="username">{{ item.nickname }}</text>
</view>
<view
v-if="item.status !== 0 && item.status !== -1"
class="like-info"
@click.stop="handleLikeClick(item)"
>
<image
v-if="!item.userLiked"
src="https://epic.js-dyyj.com/uploads/20250728/2f3ae212c01fa3b67be81abc5723cf5c.png"
style="height: 30rpx; width: 35rpx"
></image>
<image
v-else
src="https://epic.js-dyyj.com/uploads/20250728/dd7ed269b24e84a2dd141da6ab980fd6.png"
style="height: 30rpx; width: 35rpx"
></image>
<text class="like-count">{{ item.likeCount || 0 }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 右列 -->
<view class="column">
<view
v-for="(item, index) in rightItems"
class="waterfall-item"
@click="handleItemClick(index, rightItems)"
>
<view class="image-container">
<image
v-if="item.coverImage"
:src="item.coverImage && item.coverImage.split(',')[0]"
class="item-image"
mode="aspectFill"
/>
<!-- 状态蒙层 -->
<view
v-if="item.status === 0 || item.status === -1"
class="status-overlay"
>
<text class="status-text">{{
item.status === 0 ? "待审核" : "审核不通过"
}}</text>
</view>
</view>
<view class="item-content">
<text v-if="item.title" class="item-title">{{ item.title }}</text>
<view class="item-footer">
<view class="user-info">
<image
:src="item.headImg"
class="user-avatar"
mode="aspectFill"
/>
<text class="username"
>{{ item.nickname }}{{ item.nickname
}}{{ item.nickname }}</text
>
</view>
<view
v-if="item.status !== 0 && item.status !== -1"
class="like-info"
@click.stop="handleLikeClick(item)"
>
<image
v-if="!item.userLiked"
src="https://epic.js-dyyj.com/uploads/20250728/2f3ae212c01fa3b67be81abc5723cf5c.png"
style="height: 30rpx; width: 35rpx"
></image>
<image
v-else
src="https://epic.js-dyyj.com/uploads/20250728/dd7ed269b24e84a2dd141da6ab980fd6.png"
style="height: 30rpx; width: 35rpx"
></image>
<text class="like-count">{{ item.likeCount || 0 }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
name: "WaterfallLayout",
props: {
// 数据源
items: {
type: Array,
default: () => [],
},
// 列数(固定为2列)
columnCount: {
type: Number,
default: 2,
},
// 列间距(rpx)
columnGap: {
type: Number,
default: 16,
},
// 项目间距(rpx)
itemGap: {
type: Number,
default: 16,
},
},
data() {
return {
leftItems: [],
rightItems: [],
};
},
watch: {
items: {
handler(newItems) {
this.calculateLayout(newItems);
},
immediate: true,
deep: true,
},
},
mounted() {
this.calculateLayout(this.items);
},
methods: {
// 处理点赞/取消点赞
handleLikeClick(item) {
if (!item || !item.id) {
uni.showToast({
title: "操作失败,笔记ID不存在",
icon: "none",
});
return;
}
// 乐观更新UI
const isLiked = !item.userLiked;
item.userLiked = isLiked;
item.likeCount = isLiked
? (item.likeCount || 0) + 1
: Math.max((item.likeCount || 0) - 1, 0);
// 调用API
const url = isLiked
? `/framework/noteLike/add/${item.id}`
: `/framework/noteLike/cancel/${item.id}`;
this.Post({}, url, "DES")
.then((res) => {
if (res.code !== 200) {
// 如果请求失败,回滚UI更新
item.userLiked = !isLiked;
item.likeCount = !isLiked
? (item.likeCount || 0) + 1
: Math.max((item.likeCount || 0) - 1, 0);
uni.showToast({
title: res.msg || "操作失败,请稍后重试",
icon: "none",
});
} else {
// 请求成功,发出事件通知父组件
this.$emit("like-change", {
noteId: item.id,
isLiked: isLiked,
likeCount: item.likeCount,
});
// 同时发送全局事件,通知其他页面更新
uni.$emit("note-like-change", {
noteId: item.id,
isLiked: isLiked,
likeCount: item.likeCount,
});
}
})
.catch((err) => {
// 请求失败,回滚UI更新
item.userLiked = !isLiked;
item.likeCount = !isLiked
? (item.likeCount || 0) + 1
: Math.max((item.likeCount || 0) - 1, 0);
uni.showToast({
title: err.msg || "网络异常,请稍后重试",
icon: "none",
});
});
},
// 获取列的实际高度(通过DOM查询)
getColumnHeight(columnRef) {
if (!columnRef) return 0;
const query = uni.createSelectorQuery().in(this);
return new Promise((resolve) => {
query
.select(columnRef)
.boundingClientRect((data) => {
resolve(data ? data.height : 0);
})
.exec();
});
},
// 计算布局
calculateLayout(items) {
if (!items || !items.length) {
this.leftItems = [];
this.rightItems = [];
return;
}
// 清空现有数据
this.leftItems = [];
this.rightItems = [];
// 逐个添加项目
for (let i = 0; i < items.length; i++) {
this.addItem(items[i]);
}
},
// 添加单个项目到合适的列
addItem(item) {
// 简单的交替分配逻辑:比较两列的项目数量
if (this.leftItems.length <= this.rightItems.length) {
this.leftItems.push(item);
} else {
this.rightItems.push(item);
}
},
// 清空所有项目
clearItems() {
this.leftItems = [];
this.rightItems = [];
this.$emit("items-cleared");
},
// 处理项目点击
handleItemClick(index, list) {
this.$emit("item-click", list[index]);
},
// 获取所有项目
getAllItems() {
return [...this.leftItems, ...this.rightItems];
},
// 移除项目
removeItem(itemId) {
// 从左列移除
let index = this.leftItems.findIndex((item) => item.id === itemId);
if (index !== -1) {
this.leftItems.splice(index, 1);
this.$emit("item-removed", itemId);
return;
}
// 从右列移除
index = this.rightItems.findIndex((item) => item.id === itemId);
if (index !== -1) {
this.rightItems.splice(index, 1);
this.$emit("item-removed", itemId);
}
},
},
};
</script>
<style scoped>
.waterfall-layout {
width: 100%;
box-sizing: border-box;
}
/* 空状态样式 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 160rpx 40rpx;
text-align: center;
}
.empty-icon {
width: 240rpx;
height: 240rpx;
margin-bottom: 40rpx;
opacity: 0.6;
}
.empty-title {
font-size: 32rpx;
color: #666;
margin-bottom: 16rpx;
font-weight: 500;
}
.empty-desc {
font-size: 28rpx;
color: #999;
line-height: 1.4;
}
.waterfall-container {
display: flex;
gap: 16rpx;
padding: 0 20rpx;
box-sizing: border-box;
}
.column {
flex: 1;
display: flex;
flex-direction: column;
gap: 16rpx;
}
.waterfall-item {
box-sizing: border-box;
border-radius: 12rpx;
background: #fff;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
overflow: hidden;
transition: transform 0.2s ease;
position: relative;
}
.waterfall-item:active {
transform: scale(0.98);
}
.item-image {
width: 100%;
height: 476rpx;
object-fit: cover;
}
.item-content {
padding: 16rpx;
}
.item-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
line-height: 1.3;
margin-bottom: 12rpx;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
}
.item-desc {
font-size: 24rpx;
color: #666;
line-height: 1.4;
margin-bottom: 16rpx;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
}
.item-tags {
display: flex;
flex-wrap: wrap;
gap: 8rpx;
margin-bottom: 16rpx;
}
.tag {
padding: 4rpx 12rpx;
background: #f5f5f5;
color: #666;
font-size: 20rpx;
border-radius: 12rpx;
white-space: nowrap;
}
.item-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 16rpx;
}
.user-info {
display: flex;
align-items: center;
gap: 12rpx;
}
.user-avatar {
width: 32rpx;
height: 32rpx;
border-radius: 50%;
}
.username {
font-size: 22rpx;
color: #666;
width: 160rpx;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.like-info {
display: flex;
align-items: center;
gap: 6rpx;
padding: 4rpx 8rpx;
}
.like-icon {
font-size: 24rpx;
color: #ff6b6b;
}
.like-count {
font-size: 22rpx;
color: #666;
}
.image-container {
position: relative;
width: 100%;
overflow: hidden;
height: 476rpx;
}
/* 状态蒙层样式 */
.status-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
justify-content: center;
align-items: center;
z-index: 10;
}
.status-text {
color: #ffffff;
font-size: 28rpx;
font-weight: 500;
padding: 10rpx 20rpx;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 8rpx;
}
</style>