|
@ -1,37 +1,56 @@ |
|
|
<template> |
|
|
<template> |
|
|
<view class="waterfall-layout"> |
|
|
<view class="waterfall-layout"> |
|
|
<!-- 瀑布流容器 --> |
|
|
<view class="waterfall-container"> |
|
|
|
|
|
<!-- 左列 --> |
|
|
|
|
|
<view class="column"> |
|
|
<view |
|
|
<view |
|
|
class="waterfall-container" |
|
|
v-for="(item, index) in leftItems" |
|
|
:style="{ height: containerHeight + 'rpx' }" |
|
|
:key="item.id || index" |
|
|
|
|
|
class="waterfall-item" |
|
|
|
|
|
@click="handleItemClick(item)" |
|
|
> |
|
|
> |
|
|
|
|
|
<image |
|
|
|
|
|
v-if="item.image" |
|
|
|
|
|
:src="item.image" |
|
|
|
|
|
class="item-image" |
|
|
|
|
|
mode="aspectFill" |
|
|
|
|
|
/> |
|
|
|
|
|
<view class="item-content"> |
|
|
|
|
|
<text v-if="item.title" class="item-title">{{ item.title }}</text> |
|
|
|
|
|
<view v-if="item.user" class="item-footer"> |
|
|
|
|
|
<view class="user-info"> |
|
|
|
|
|
<image |
|
|
|
|
|
:src="item.user.avatar" |
|
|
|
|
|
class="user-avatar" |
|
|
|
|
|
mode="aspectFill" |
|
|
|
|
|
/> |
|
|
|
|
|
<text class="username">{{ item.user.name }}</text> |
|
|
|
|
|
</view> |
|
|
|
|
|
<view class="like-info"> |
|
|
|
|
|
<text class="like-icon">♥</text> |
|
|
|
|
|
<text class="like-count">{{ item.likes }}</text> |
|
|
|
|
|
</view> |
|
|
|
|
|
</view> |
|
|
|
|
|
</view> |
|
|
|
|
|
</view> |
|
|
|
|
|
</view> |
|
|
|
|
|
|
|
|
|
|
|
<!-- 右列 --> |
|
|
|
|
|
<view class="column"> |
|
|
<view |
|
|
<view |
|
|
|
|
|
v-for="(item, index) in rightItems" |
|
|
|
|
|
:key="item.id || index" |
|
|
class="waterfall-item" |
|
|
class="waterfall-item" |
|
|
v-for="(item, index) in positionedItems" |
|
|
|
|
|
:key="item.id" |
|
|
|
|
|
:style="{ |
|
|
|
|
|
position: 'absolute', |
|
|
|
|
|
left: item.left + 'rpx', |
|
|
|
|
|
top: item.top + 'rpx', |
|
|
|
|
|
width: item.width + 'rpx', |
|
|
|
|
|
}" |
|
|
|
|
|
@click="handleItemClick(item)" |
|
|
@click="handleItemClick(item)" |
|
|
> |
|
|
> |
|
|
<!-- 图片 --> |
|
|
|
|
|
<image |
|
|
<image |
|
|
v-if="item.image" |
|
|
v-if="item.image" |
|
|
:src="item.image" |
|
|
:src="item.image" |
|
|
class="item-image" |
|
|
class="item-image" |
|
|
mode="aspectFill" |
|
|
mode="aspectFill" |
|
|
/> |
|
|
/> |
|
|
<!-- 内容区域 --> |
|
|
|
|
|
<view class="item-content"> |
|
|
<view class="item-content"> |
|
|
<text v-if="item.title" class="item-title">{{ item.title }}</text> |
|
|
<text v-if="item.title" class="item-title">{{ item.title }}</text> |
|
|
<!-- <text v-if="item.description" class="item-desc">{{ item.description }}</text> --> |
|
|
|
|
|
<!-- <view v-if="item.tags && item.tags.length" class="item-tags"> |
|
|
|
|
|
<text class="tag" v-for="tag in item.tags" :key="tag">{{ tag }}</text> |
|
|
|
|
|
</view> --> |
|
|
|
|
|
<!-- 用户信息和点赞 --> |
|
|
|
|
|
<view v-if="item.user" class="item-footer"> |
|
|
<view v-if="item.user" class="item-footer"> |
|
|
<view class="user-info"> |
|
|
<view class="user-info"> |
|
|
<image |
|
|
<image |
|
@ -50,6 +69,7 @@ |
|
|
</view> |
|
|
</view> |
|
|
</view> |
|
|
</view> |
|
|
</view> |
|
|
</view> |
|
|
|
|
|
</view> |
|
|
</template> |
|
|
</template> |
|
|
|
|
|
|
|
|
<script> |
|
|
<script> |
|
@ -61,7 +81,7 @@ export default { |
|
|
type: Array, |
|
|
type: Array, |
|
|
default: () => [], |
|
|
default: () => [], |
|
|
}, |
|
|
}, |
|
|
// 列数 |
|
|
// 列数(固定为2列) |
|
|
columnCount: { |
|
|
columnCount: { |
|
|
type: Number, |
|
|
type: Number, |
|
|
default: 2, |
|
|
default: 2, |
|
@ -79,128 +99,66 @@ export default { |
|
|
}, |
|
|
}, |
|
|
data() { |
|
|
data() { |
|
|
return { |
|
|
return { |
|
|
positionedItems: [], |
|
|
leftItems: [], |
|
|
containerHeight: 0, |
|
|
rightItems: [], |
|
|
columnHeights: [], |
|
|
|
|
|
}; |
|
|
}; |
|
|
}, |
|
|
}, |
|
|
watch: { |
|
|
watch: { |
|
|
items: { |
|
|
items: { |
|
|
handler() { |
|
|
handler(newItems) { |
|
|
this.$nextTick(() => { |
|
|
this.calculateLayout(newItems); |
|
|
this.calculateLayout(); |
|
|
|
|
|
}); |
|
|
|
|
|
}, |
|
|
}, |
|
|
immediate: true, |
|
|
immediate: true, |
|
|
deep: true, |
|
|
deep: true, |
|
|
}, |
|
|
}, |
|
|
}, |
|
|
}, |
|
|
mounted() { |
|
|
mounted() { |
|
|
this.calculateLayout(); |
|
|
this.calculateLayout(this.items); |
|
|
}, |
|
|
}, |
|
|
methods: { |
|
|
methods: { |
|
|
// 计算布局 |
|
|
// 获取列的实际高度(通过DOM查询) |
|
|
calculateLayout() { |
|
|
getColumnHeight(columnRef) { |
|
|
if (!this.items.length) { |
|
|
if (!columnRef) return 0; |
|
|
this.positionedItems = []; |
|
|
const query = uni.createSelectorQuery().in(this); |
|
|
this.containerHeight = 0; |
|
|
return new Promise((resolve) => { |
|
|
return; |
|
|
query.select(columnRef).boundingClientRect((data) => { |
|
|
} |
|
|
resolve(data ? data.height : 0); |
|
|
|
|
|
}).exec(); |
|
|
// 初始化列高度 |
|
|
|
|
|
this.columnHeights = new Array(this.columnCount).fill(0); |
|
|
|
|
|
this.positionedItems = []; |
|
|
|
|
|
|
|
|
|
|
|
// 逐个处理项目 |
|
|
|
|
|
this.items.forEach((item, index) => { |
|
|
|
|
|
this.calculateItemPosition(item, index); |
|
|
|
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
// 设置容器高度 |
|
|
|
|
|
this.containerHeight = Math.max(...this.columnHeights) + this.itemGap; |
|
|
|
|
|
}, |
|
|
}, |
|
|
|
|
|
|
|
|
// 计算单个项目位置 |
|
|
// 计算布局 |
|
|
calculateItemPosition(item, index) { |
|
|
calculateLayout(items) { |
|
|
// 找到最短的列 |
|
|
if (!items || !items.length) { |
|
|
const shortestColumnIndex = this.columnHeights.indexOf(Math.min(...this.columnHeights)) |
|
|
this.leftItems = []; |
|
|
|
|
|
this.rightItems = []; |
|
|
// 协调的间距计算:左右边距20rpx,列间距16rpx |
|
|
return; |
|
|
const sideMargin = 20 // 左右边距 |
|
|
|
|
|
const columnGap = 16 // 列间距 |
|
|
|
|
|
// 确保左右边距都是20rpx的计算方式 |
|
|
|
|
|
const availableWidth = 750 - 2 * sideMargin // 可用宽度 = 750 - 40 = 710rpx |
|
|
|
|
|
const itemWidth = Math.floor((availableWidth - (this.columnCount - 1) * columnGap) / this.columnCount) |
|
|
|
|
|
|
|
|
|
|
|
// 计算位置 - 确保左右边距都是20rpx |
|
|
|
|
|
const left = sideMargin + shortestColumnIndex * (itemWidth + columnGap) |
|
|
|
|
|
// 修正top计算:第一行距离顶部itemGap,后续项目距离上一个项目itemGap |
|
|
|
|
|
const top = this.columnHeights[shortestColumnIndex] === 0 ? this.itemGap : this.columnHeights[shortestColumnIndex] + this.itemGap; |
|
|
|
|
|
|
|
|
|
|
|
// 估算项目高度 |
|
|
|
|
|
const estimatedHeight = this.estimateItemHeight(item); |
|
|
|
|
|
|
|
|
|
|
|
// 添加到定位项目数组 |
|
|
|
|
|
this.positionedItems.push({ |
|
|
|
|
|
...item, |
|
|
|
|
|
left, |
|
|
|
|
|
top, |
|
|
|
|
|
width: itemWidth, |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// 更新列高度 |
|
|
|
|
|
this.columnHeights[shortestColumnIndex] = top + estimatedHeight; |
|
|
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
// 估算项目高度 |
|
|
|
|
|
estimateItemHeight(item) { |
|
|
|
|
|
let height = 0; |
|
|
|
|
|
|
|
|
|
|
|
// 图片高度 |
|
|
|
|
|
if (item.image) { |
|
|
|
|
|
height += 476; // 图片固定高度 |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 内容区域padding |
|
|
// 清空现有数据 |
|
|
height += 32; // 上下padding各16rpx |
|
|
this.leftItems = []; |
|
|
|
|
|
this.rightItems = []; |
|
|
|
|
|
|
|
|
// 标题高度 - 更精确的计算 |
|
|
// 逐个添加项目 |
|
|
if (item.title) { |
|
|
for (let i = 0; i < items.length; i++) { |
|
|
// 基于字符数和换行估算,考虑中英文混合 |
|
|
this.addItem(items[i]); |
|
|
const titleLength = item.title.length; |
|
|
|
|
|
const charsPerLine = 12; // 每行大约12个字符(考虑中文字符较宽) |
|
|
|
|
|
const titleLines = Math.min(Math.ceil(titleLength / charsPerLine), 2); |
|
|
|
|
|
height += titleLines * 40 + 12; // 行高40rpx(稍微增加) + 底部间距 |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
|
|
|
// 描述高度 - 更精确的计算 |
|
|
// 添加单个项目到合适的列 |
|
|
// if (item.description) { |
|
|
addItem(item) { |
|
|
// const descLength = item.description.length; |
|
|
// 简单的交替分配逻辑:比较两列的项目数量 |
|
|
// const charsPerLine = 15; // 描述文字稍小,每行约15个字符 |
|
|
if (this.leftItems.length <= this.rightItems.length) { |
|
|
// const descLines = Math.min(Math.ceil(descLength / charsPerLine), 2); |
|
|
this.leftItems.push(item); |
|
|
// height += descLines * 36 + 16; // 行高36rpx + 底部间距 |
|
|
} else { |
|
|
// } |
|
|
this.rightItems.push(item); |
|
|
|
|
|
|
|
|
// 标签高度 - 考虑多行标签 |
|
|
|
|
|
// if (item.tags && item.tags.length) { |
|
|
|
|
|
// // 估算标签可能占用的行数 |
|
|
|
|
|
// const tagRows = Math.ceil(item.tags.length / 3); // 假设每行最多3个标签 |
|
|
|
|
|
// height += tagRows * 36 + 16; // 每行标签高度36rpx + 底部间距 |
|
|
|
|
|
// } |
|
|
|
|
|
|
|
|
|
|
|
// 用户信息区域 |
|
|
|
|
|
if (item.user) { |
|
|
|
|
|
height += 48 + 16; // 用户信息高度 + 底部间距 |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
return height; |
|
|
|
|
|
}, |
|
|
}, |
|
|
|
|
|
|
|
|
// 清空所有项目 |
|
|
// 清空所有项目 |
|
|
clearItems() { |
|
|
clearItems() { |
|
|
this.positionedItems = []; |
|
|
this.leftItems = []; |
|
|
this.containerHeight = 0; |
|
|
this.rightItems = []; |
|
|
this.columnHeights = new Array(this.columnCount).fill(0); |
|
|
|
|
|
this.$emit("items-cleared"); |
|
|
this.$emit("items-cleared"); |
|
|
}, |
|
|
}, |
|
|
|
|
|
|
|
@ -211,19 +169,25 @@ export default { |
|
|
|
|
|
|
|
|
// 获取所有项目 |
|
|
// 获取所有项目 |
|
|
getAllItems() { |
|
|
getAllItems() { |
|
|
return this.positionedItems; |
|
|
return [...this.leftItems, ...this.rightItems]; |
|
|
}, |
|
|
}, |
|
|
|
|
|
|
|
|
// 移除项目 |
|
|
// 移除项目 |
|
|
removeItem(itemId) { |
|
|
removeItem(itemId) { |
|
|
const index = this.positionedItems.findIndex( |
|
|
// 从左列移除 |
|
|
(item) => item.id === itemId |
|
|
let index = this.leftItems.findIndex(item => item.id === itemId); |
|
|
); |
|
|
|
|
|
if (index !== -1) { |
|
|
if (index !== -1) { |
|
|
this.positionedItems.splice(index, 1); |
|
|
this.leftItems.splice(index, 1); |
|
|
this.calculateLayout(); // 重新计算布局 |
|
|
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); |
|
|
this.$emit("item-removed", itemId); |
|
|
|
|
|
} |
|
|
}, |
|
|
}, |
|
|
}, |
|
|
}, |
|
|
}; |
|
|
}; |
|
@ -236,12 +200,19 @@ export default { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.waterfall-container { |
|
|
.waterfall-container { |
|
|
position: relative; |
|
|
display: flex; |
|
|
width: 100%; |
|
|
gap: 16rpx; |
|
|
padding: 0 20rpx; |
|
|
padding: 0 20rpx; |
|
|
box-sizing: border-box; |
|
|
box-sizing: border-box; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.column { |
|
|
|
|
|
flex: 1; |
|
|
|
|
|
display: flex; |
|
|
|
|
|
flex-direction: column; |
|
|
|
|
|
gap: 16rpx; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
.waterfall-item { |
|
|
.waterfall-item { |
|
|
box-sizing: border-box; |
|
|
box-sizing: border-box; |
|
|
border-radius: 12rpx; |
|
|
border-radius: 12rpx; |
|
|