416 lines
16 KiB
Vue
416 lines
16 KiB
Vue
<template>
|
||
<view class="u-waterfall">
|
||
<!-- 新增支持多列布局 -->
|
||
<view
|
||
v-for="(column, index) in columnList"
|
||
:key="index"
|
||
:ref="`u-column-${index}`"
|
||
:id="`u-column-${index}`"
|
||
class="u-column"
|
||
>
|
||
<slot name="column"
|
||
:colIndex="index"
|
||
:colList="column">
|
||
</slot>
|
||
<slot name="left"
|
||
:colIndex="index"
|
||
:leftList="column">
|
||
</slot>
|
||
<template v-if="!$slots['left'] && !$slots['column']" v-for="(item, itemIndex) in column" :key="itemIndex">
|
||
<slot :item="item" :itemIndex="itemIndex"></slot>
|
||
</template>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
/**
|
||
* waterfall 瀴布流
|
||
* @description 这是一个瀑布流形式的组件,对原组件进行升级已经支持自定义列数模式,便于适配不同屏幕。搭配loadMore 加载更多组件,让您开箱即用,眼前一亮。
|
||
* @tutorial https://uview-plus.jiangruyi.com/components/waterfall.html
|
||
* @property {Array} flow-list 用于渲染的数据
|
||
* @property {String Number} add-time 单条数据添加到队列的时间间隔,单位ms,见上方注意事项说明(默认200)
|
||
* @property {String Number} columns 瀑布流列数,默认为2,设置为auto时自动根据屏幕宽度调整列数
|
||
* @example <u-waterfall :flowList="flowList"></u-waterfall>
|
||
*/
|
||
import { mpMixin } from '../../libs/mixin/mpMixin';
|
||
import { mixin } from '../../libs/mixin/mixin';
|
||
import { sleep } from '../../libs/function/index';
|
||
export default {
|
||
name: "u-waterfall",
|
||
props: {
|
||
// #ifdef VUE2
|
||
value: {
|
||
// 瀑布流数据
|
||
type: Array,
|
||
required: true,
|
||
default: function() {
|
||
return [];
|
||
}
|
||
},
|
||
// #endif
|
||
// #ifdef VUE3
|
||
modelValue: {
|
||
// 瀑布流数据
|
||
type: Array,
|
||
required: true,
|
||
default: function() {
|
||
return [];
|
||
}
|
||
},
|
||
// #endif
|
||
// 每次向结构插入数据的时间间隔,单位ms
|
||
// 单位ms
|
||
addTime: {
|
||
type: [Number, String],
|
||
default: 200
|
||
},
|
||
// id值,用于清除某一条数据时,根据此idKey名称找到并移除,如数据为{idx: 22, name: 'lisa'}
|
||
// 那么该把idKey设置为idx
|
||
idKey: {
|
||
type: String,
|
||
default: 'id'
|
||
},
|
||
// 瀑布流列数
|
||
columns: {
|
||
type: [Number, String],
|
||
default: 2
|
||
},
|
||
// 瀑布流最小列数
|
||
columnsMin: {
|
||
type: [Number, String],
|
||
default: 2
|
||
},
|
||
// 最小列宽
|
||
minColumnWidth: {
|
||
type: Number,
|
||
default: 230
|
||
}
|
||
},
|
||
mixins: [mpMixin, mixin],
|
||
data() {
|
||
return {
|
||
columnList: [[]], // 存储每列的数据
|
||
children: [],
|
||
// 用于标记是否已经初始化
|
||
initialized: false,
|
||
windowWidth: 375,
|
||
windowHeight: 0
|
||
}
|
||
},
|
||
watch: {
|
||
copyFlowList: {
|
||
handler(nVal, oVal) {
|
||
if (!nVal || nVal.length == 0) {
|
||
this.clear(false);
|
||
} else {
|
||
if (this.columnList.length == 1) {
|
||
this.initColumnList()
|
||
}
|
||
// 取差值,即这一次数组变化新增的部分
|
||
let startIndex = Array.isArray(oVal) && oVal.length > 0 ? oVal.length : 0;
|
||
// 直接处理数据,不再使用tempList和splitData
|
||
this.handleData(nVal.slice(startIndex));
|
||
}
|
||
},
|
||
immediate: true
|
||
},
|
||
columns: {
|
||
handler() {
|
||
this.initColumnList();
|
||
// 重新分配数据
|
||
if (this.copyFlowList.length > 0) {
|
||
this.redistributeData();
|
||
}
|
||
},
|
||
immediate: false
|
||
}
|
||
},
|
||
created() {
|
||
this.initColumnList();
|
||
},
|
||
mounted() {
|
||
this.initialized = true;
|
||
// 添加窗口大小变化监听
|
||
// #ifdef H5
|
||
if (this.columns === 'auto') {
|
||
uni.onWindowResize(this.handleWindowResize);
|
||
}
|
||
// #endif
|
||
},
|
||
// 添加beforeUnmount生命周期清理事件监听
|
||
// #ifdef VUE3
|
||
beforeUnmount() {
|
||
// #ifdef H5
|
||
if (this.columns === 'auto') {
|
||
uni.offWindowResize(this.handleWindowResize);
|
||
}
|
||
// #endif
|
||
},
|
||
// #endif
|
||
// #ifdef VUE2
|
||
beforeDestroy() {
|
||
// #ifdef H5
|
||
if (this.columns === 'auto') {
|
||
window.removeEventListener('resize', this.handleWindowResize);
|
||
}
|
||
// #endif
|
||
},
|
||
// #endif
|
||
computed: {
|
||
// 破坏flowList变量的引用,否则watch的结果新旧值是一样的
|
||
copyFlowList() {
|
||
// #ifdef VUE3
|
||
if (!this.modelValue || this.modelValue.length == 0) {
|
||
// console.log('clear');
|
||
return [];
|
||
} else {
|
||
return this.cloneData(this.modelValue);
|
||
}
|
||
// #endif
|
||
// #ifdef VUE2
|
||
return this.cloneData(this.value);
|
||
// #endif
|
||
}
|
||
},
|
||
emits: ['update:modelValue', 'after-add-one', 'after-add-all'],
|
||
methods: {
|
||
// 初始化列数据数组
|
||
initColumnList() {
|
||
this.windowWidth = uni.getSystemInfoSync().windowWidth;
|
||
const cols = this.getColumnsCount();
|
||
// console.log(cols)
|
||
this.columnList = Array.from({ length: cols }, () => []);
|
||
},
|
||
|
||
// 获取列数,支持auto模式
|
||
getColumnsCount() {
|
||
if (this.columns === 'auto') {
|
||
// 列间距为10rpx(约等于7px)
|
||
const columnGap = 7;
|
||
// 计算可容纳的列数
|
||
let columnCount = Math.max(1, Math.floor(this.windowWidth / (this.minColumnWidth + columnGap)));
|
||
if (columnCount < this.columnsMin) {
|
||
columnCount = this.columnsMin
|
||
}
|
||
return columnCount;
|
||
}
|
||
return parseInt(this.columns) || 2;
|
||
},
|
||
|
||
// 窗口大小变化处理函数
|
||
handleWindowResize(res) {
|
||
this.windowWidth = res.size.windowWidth
|
||
this.windowHeight = res.size.windowHeight
|
||
// 防抖处理,避免频繁触发
|
||
if (this.resizeTimer) {
|
||
clearTimeout(this.resizeTimer);
|
||
}
|
||
this.resizeTimer = setTimeout(() => {
|
||
const newColumnsCount = this.getColumnsCount();
|
||
const oldColumnsCount = this.columnList.length;
|
||
|
||
// 只有列数发生变化时才重新分配数据
|
||
if (newColumnsCount !== oldColumnsCount) {
|
||
this.redistributeData();
|
||
}
|
||
}, 300);
|
||
},
|
||
|
||
// 重新分配所有数据
|
||
async redistributeData() {
|
||
// 清空所有列
|
||
this.initColumnList();
|
||
// 保存所有数据
|
||
const allData = this.cloneData(this.copyFlowList);
|
||
// 重新分配数据
|
||
this.handleData(allData);
|
||
},
|
||
|
||
// 处理新增数据
|
||
async handleData(newData) {
|
||
if (!newData || newData.length === 0) return;
|
||
|
||
// 初始化列高度数组
|
||
const columnHeights = new Array(this.columnList.length).fill(0);
|
||
|
||
// 获取各列当前高度
|
||
for (let i = 0; i < this.columnList.length; i++) {
|
||
try {
|
||
const rect = await this.$uGetRect(`#u-column-${i}`);
|
||
// console.log(`#u-column-${i}`, rect.height)
|
||
columnHeights[i] = rect.height || 0;
|
||
} catch (e) {
|
||
columnHeights[i] = 0;
|
||
}
|
||
}
|
||
|
||
// 分配新数据到最短的列
|
||
for (let item of newData) {
|
||
const minHeightIndex = columnHeights.indexOf(Math.min(...columnHeights));
|
||
// console.log('this.columnList', this.columnList)
|
||
this.columnList[minHeightIndex].push(item);
|
||
|
||
// 获取实际渲染后的元素高度而不是估算
|
||
await sleep(this.addTime)
|
||
await this.$nextTick(async () => {
|
||
try {
|
||
const rect = await this.$uGetRect(`#u-column-${minHeightIndex}`);
|
||
// console.log(`#u-column-${minHeightIndex}`, rect.height)
|
||
if (rect.height) {
|
||
columnHeights[minHeightIndex] = rect.height;
|
||
// 加载一个后置事件
|
||
this.$emit('after-add-one', {
|
||
...item,
|
||
height: rect.height
|
||
});
|
||
}
|
||
} catch (e) {
|
||
// console.log(e)
|
||
// columnHeights[i] = 0;
|
||
}
|
||
});
|
||
// this.$nextTick(async () => {
|
||
// try {
|
||
// // 等待DOM更新后获取实际高度
|
||
// const lastIndex = this.columnList[minHeightIndex].length - 1;
|
||
// const el = this.$refs[`u-column-${minHeightIndex}`][0].children[lastIndex];
|
||
// if (el) {
|
||
// const rect = await this.$uGetRect(el);
|
||
// const actualHeight = rect.height || 100;
|
||
// columnHeights[minHeightIndex] += actualHeight;
|
||
// } else {
|
||
// // 备用方案:如果无法获取实际高度,则使用默认值
|
||
// columnHeights[minHeightIndex] += 100;
|
||
// }
|
||
// } catch (e) {
|
||
// // 出错时使用默认高度
|
||
// console.log(e)
|
||
// columnHeights[minHeightIndex] += 100;
|
||
// }
|
||
// });
|
||
}
|
||
// 加载所有后置事件
|
||
this.$emit('after-add-all', {
|
||
columnHeights: columnHeights,
|
||
newData: newData
|
||
});
|
||
},
|
||
|
||
// 复制而不是引用对象和数组
|
||
cloneData(data) {
|
||
return JSON.parse(JSON.stringify(data));
|
||
},
|
||
|
||
// 清空数据列表
|
||
clear(bak = true) {
|
||
this.initColumnList();
|
||
// 同时清除父组件列表中的数据
|
||
if (bak) {
|
||
// #ifdef VUE2
|
||
this.$emit('input', []);
|
||
// #endif
|
||
// #ifdef VUE3
|
||
this.$emit('update:modelValue', []);
|
||
// #endif
|
||
}
|
||
},
|
||
|
||
// 清除某一条指定的数据,根据id实现
|
||
remove(id) {
|
||
// 遍历所有列查找并删除数据
|
||
for (let i = 0; i < this.columnList.length; i++) {
|
||
const index = this.columnList[i].findIndex(val => val[this.idKey] == id);
|
||
if (index !== -1) {
|
||
this.columnList[i].splice(index, 1);
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 同时清除父组件的数据中的对应id的条目
|
||
// #ifdef VUE2
|
||
const valueIndex = this.value.findIndex(val => val[this.idKey] == id);
|
||
if (valueIndex !== -1) {
|
||
const newValue = this.cloneData(this.value);
|
||
newValue.splice(valueIndex, 1);
|
||
this.$emit('input', newValue);
|
||
}
|
||
// #endif
|
||
// #ifdef VUE3
|
||
const modelValueIndex = this.modelValue.findIndex(val => val[this.idKey] == id);
|
||
if (modelValueIndex !== -1) {
|
||
const newModelValue = this.cloneData(this.modelValue);
|
||
newModelValue.splice(modelValueIndex, 1);
|
||
this.$emit('update:modelValue', newModelValue);
|
||
}
|
||
// #endif
|
||
},
|
||
|
||
// 修改某条数据的某个属性
|
||
modify(id, key, value) {
|
||
let found = false;
|
||
let targetItem = null;
|
||
|
||
// 在所有列中查找数据
|
||
for (let i = 0; i < this.columnList.length; i++) {
|
||
const index = this.columnList[i].findIndex(val => val[this.idKey] == id);
|
||
if (index !== -1) {
|
||
// 修改对应key的值
|
||
this.columnList[i][index][key] = value;
|
||
targetItem = this.columnList[i][index];
|
||
found = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (found && targetItem) {
|
||
// 修改父组件的数据中的对应id的条目
|
||
// #ifdef VUE2
|
||
const valueIndex = this.value.findIndex(val => val[this.idKey] == id);
|
||
if (valueIndex !== -1) {
|
||
let data = this.cloneData(this.value);
|
||
data[valueIndex][key] = value;
|
||
this.$emit('input', data);
|
||
}
|
||
// #endif
|
||
// #ifdef VUE3
|
||
const modelValueIndex = this.modelValue.findIndex(val => val[this.idKey] == id);
|
||
if (modelValueIndex !== -1) {
|
||
let data = this.cloneData(this.modelValue);
|
||
data[modelValueIndex][key] = value;
|
||
this.$emit('update:modelValue', data);
|
||
}
|
||
// #endif
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.u-waterfall {
|
||
@include flex;
|
||
flex-direction: row;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.u-column {
|
||
@include flex;
|
||
flex: 1;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
/* #ifndef APP-NVUE */
|
||
height: 100%;
|
||
/* #endif */
|
||
// 添加列之间的间距
|
||
&:not(:first-child) {
|
||
margin-left: 10rpx;
|
||
}
|
||
}
|
||
|
||
.u-image {
|
||
/* #ifndef APP-NVUE */
|
||
max-width: 100%;
|
||
/* #endif */
|
||
}
|
||
</style> |