Files
WAI_Project_UNIX/uni_modules/cool-ui/components/cl-waterfall/cl-waterfall.uvue
2025-07-21 16:47:04 +08:00

237 lines
5.1 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view
class="cl-waterfall"
:class="[pt.classNames]"
:style="{
padding: `0 ${props.gutter / 2}rpx`
}"
>
<view
class="cl-waterfall__column"
v-for="(column, columnIndex) in columns"
:key="columnIndex"
:style="{
margin: `0 ${props.gutter / 2}rpx`
}"
>
<view class="cl-waterfall__column-inner">
<view
v-for="(item, index) in column"
:key="`${columnIndex}-${index}-${item[props.nodeKey]}`"
class="cl-waterfall__item"
:class="{
'is-virtual': item.isVirtual
}"
>
<slot name="item" :item="item" :index="index"></slot>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { assign } from "@/cool";
import { computed, getCurrentInstance, nextTick, onMounted, ref, watch } from "vue";
import { parsePt } from "@/cool";
defineOptions({
name: "cl-waterfall"
});
// 定义插槽类型item插槽接收item和index参数
defineSlots<{
item(props: { item: UTSJSONObject; index: number }): any;
}>();
// 组件属性定义
const props = defineProps({
// 透传属性
pt: {
type: Object,
default: () => ({})
},
// 瀑布流列数默认为2列
column: {
type: Number,
default: 2
},
// 列间距单位为rpx默认为12
gutter: {
type: Number,
default: 12
},
// 数据项的唯一标识字段名,默认为"id"
nodeKey: {
type: String,
default: "id"
}
});
// 获取当前组件实例的代理对象
const { proxy } = getCurrentInstance()!;
type PassThrough = {
classNames?: string;
};
const pt = computed(() => parsePt<PassThrough>(props.pt));
// 存储每列的当前高度,用于计算最短列
const heights = ref<number[]>([]);
// 存储瀑布流数据,二维数组,每个子数组代表一列
const columns = ref<UTSJSONObject[][]>([]);
/**
* 获取各列的当前高度
* 通过uni.createSelectorQuery查询DOM元素的实际高度
* @returns Promise<number> 返回Promise对象
*/
async function getHeight(): Promise<number> {
// 等待DOM更新完成
await nextTick();
return new Promise((resolve) => {
// 创建选择器查询,获取所有列容器的边界信息
uni.createSelectorQuery()
.in(proxy)
.selectAll(".cl-waterfall__column-inner")
.boundingClientRect()
.exec((rect) => {
// 提取每列的高度信息如果获取失败则默认为0
heights.value = (rect[0] as NodeInfo[]).map((e) => e.height ?? 0);
resolve(1);
});
});
}
/**
* 向瀑布流添加新数据
* 使用虚拟定位技术计算每个项目的高度,然后分配到最短的列
* @param data 要添加的数据数组
*/
async function append(data: UTSJSONObject[]) {
// 首先获取当前各列高度
await getHeight();
// 将新数据作为虚拟项目添加到第一列,用于计算高度
columns.value[0].push(
...data.map((e) => {
return {
...e,
isVirtual: true // 标记为虚拟项目会在CSS中隐藏
} as UTSJSONObject;
})
);
// 等待DOM更新
await nextTick();
// 延迟300ms后计算虚拟项目的高度并重新分配
setTimeout(() => {
uni.createSelectorQuery()
.in(proxy)
.selectAll(".is-virtual")
.boundingClientRect()
.exec((rect) => {
// 遍历每个虚拟项目
(rect[0] as NodeInfo[]).forEach((e, i) => {
// 找到当前高度最小的列
const min = Math.min(...heights.value);
const index = heights.value.indexOf(min);
// 将实际数据添加到最短列
columns.value[index].push(data[i]);
// 更新该列的高度
heights.value[index] += e.height ?? 0;
// 清除第一列中的虚拟项目(临时用于计算高度的项目)
columns.value[0] = columns.value[0].filter((e) => e.isVirtual != true);
});
});
}, 300);
}
/**
* 根据ID移除指定项目
* @param id 要移除的项目ID
*/
function remove(id: string | number) {
columns.value.forEach((column, columnIndex) => {
// 过滤掉指定ID的项目
columns.value[columnIndex] = column.filter((e) => e[props.nodeKey] != id);
});
}
/**
* 根据ID更新指定项目的数据
* @param id 要更新的项目ID
* @param data 新的数据对象
*/
function update(id: string | number, data: UTSJSONObject) {
columns.value.forEach((column) => {
column.forEach((e) => {
// 找到指定ID的项目并更新数据
if (e[props.nodeKey] == id) {
assign(e, data);
}
});
});
}
/**
* 清空瀑布流数据
* 重新初始化列数组
*/
function clear() {
columns.value = [];
// 根据列数创建空的列数组
for (let i = 0; i < props.column; i++) {
columns.value.push([]);
}
}
// 组件挂载时的初始化逻辑
onMounted(() => {
// 监听列数变化,当列数改变时重新初始化
watch(
computed(() => props.column),
() => {
clear(); // 清空现有数据
getHeight(); // 重新获取高度
},
{
immediate: true // 立即执行一次
}
);
});
defineExpose({
append,
remove,
update,
clear
});
</script>
<style lang="scss" scoped>
.cl-waterfall {
@apply flex flex-row w-full relative;
&__column {
flex: 1;
}
&__item {
&.is-virtual {
@apply absolute top-0 w-full;
left: -100%;
opacity: 0;
}
}
}
</style>