介绍
按照元素高度自适应排列,形成参差不齐的多列布局。
引入
import Waterfall from 'sard-uniapp/components/waterfall/waterfall.vue'代码演示
基础使用
瀑布流布局是使用绝对定位实现的,每一项内容需放置在 WaterfallItem 组件里面以便控制其位置;可放置任意内容,并不限定为具体模板。
如果里面包含图片或其他使其在挂载后仍然不确定高度的组件时,需在加载完后调用瀑布流项的 load 插槽中的 onLoad 回调,以便计算其渲染后的高度并进行准确定位。
瀑布流会按照渲染的顺序逐一将加载好的瀑布流项渲染出来;如果后面比前面要先加载完,也得等前面加载完才能渲染。因此,如果前面有一张很大的图片需要很久才能加载完,则会堵塞后面的渲染。
因此,一次性不要加载太多图片,图片的尺寸也要有所限制。
瀑布流组件的 load 事件会在所有项都加载完后触发,可以用来处理加载状态。
<template>
<page-meta :page-style="isLocked ? 'overflow: hidden' : ''"></page-meta>
<doc-page title="基础使用">
<sar-waterfall class="mx-32" @load="onLoad">
<sar-waterfall-item v-for="(item, index) in list" :key="index">
<template #default="{ onLoad }">
<SimulatedImage :meta="item.img" @load="onLoad" />
<view class="mt-10">{{ item.title }}</view>
</template>
</sar-waterfall-item>
</sar-waterfall>
</doc-page>
</template>
<script setup lang="ts">
import { useCurrentPageLock, usePageTopPopup } from 'sard-uniapp'
import { onBackPress } from '@dcloudio/uni-app'
import { random, toast } from 'sard-uniapp'
import { nextTick, onMounted, ref } from 'vue'
import SimulatedImage from './SimulatedImage.vue'
import { text } from '../../read-more/demo/data'
interface ListItem {
title: string
img: {
width: number
height: number
}
}
const list = ref<ListItem[]>([])
const getData = () => {
return new Promise<ListItem[]>((resolve) => {
const data = Array(20)
.fill(0)
.map(() => {
const min = 20
const max = 50
const startIndex = random(0, text.length - max)
const length = random(min, max)
return {
title: text.slice(startIndex, startIndex + length),
img: {
width: random(100, 500),
height: random(100, 500),
},
}
})
resolve(data)
})
}
const onLoad = () => {
toast.hide()
}
onMounted(async () => {
nextTick(() => {
toast.loading('加载中')
})
list.value.push(...(await getData()))
})
const { isLocked } = useCurrentPageLock()
const { shouldStopBack, hidePopup } = usePageTopPopup()
onBackPress(() => {
if (shouldStopBack.value) {
hidePopup()
return true
}
})
</script><template>
<page-meta :page-style="isLocked ? 'overflow: hidden' : ''"></page-meta>
<doc-page title="基础使用">
<sar-waterfall class="mx-32" @load="onLoad">
<sar-waterfall-item v-for="(item, index) in list" :key="index">
<template #default="{ onLoad }">
<SimulatedImage :meta="item.img" @load="onLoad" />
<view class="mt-10">{{ item.title }}</view>
</template>
</sar-waterfall-item>
</sar-waterfall>
</doc-page>
</template>
<script setup lang="js">
import { useCurrentPageLock, usePageTopPopup } from "sard-uniapp";
import { onBackPress } from "@dcloudio/uni-app";
import { random, toast } from "sard-uniapp";
import { nextTick, onMounted, ref } from "vue";
import { text } from "../../read-more/demo/data";
const list = ref([]);
const getData = () => {
return new Promise((resolve) => {
const data = Array(20).fill(0).map(() => {
const min = 20;
const max = 50;
const startIndex = random(0, text.length - max);
const length = random(min, max);
return {
title: text.slice(startIndex, startIndex + length),
img: {
width: random(100, 500),
height: random(100, 500)
}
};
});
resolve(data);
});
};
const onLoad = () => {
toast.hide();
};
onMounted(async () => {
nextTick(() => {
toast.loading("\u52A0\u8F7D\u4E2D");
});
list.value.push(...await getData());
});
const { isLocked } = useCurrentPageLock();
const { shouldStopBack, hidePopup } = usePageTopPopup();
onBackPress(() => {
if (shouldStopBack.value) {
hidePopup();
return true;
}
});
</script>SimulatedImage.vue
<template>
<view class="relative box-border" :style="{ paddingTop }">
<view class="absolute inset-0 flex justify-center items-center sbg-fourth">
<text>{{ meta.width }}</text>
<text>x</text>
<text>{{ meta.height }}</text>
</view>
</view>
</template>
<script setup lang="ts">
import { random, useTimeout } from 'sard-uniapp'
import { computed, onMounted, ref } from 'vue'
const props = defineProps<{
meta: {
width: number
height: number
}
}>()
const emit = defineEmits<{
(
e: 'load',
event: {
detail: {
width: number
height: number
}
},
): void
}>()
const internalWidth = ref(320)
const internalHeight = ref(240)
const currWidth = computed(() => internalWidth.value)
const currHeight = computed(() => internalHeight.value)
const paddingTop = computed(
() => (currHeight.value / currWidth.value) * 100 + '%',
)
const { start } = useTimeout(
() => {
internalWidth.value = props.meta.width
internalHeight.value = props.meta.height
emit('load', {
detail: {
width: props.meta.width,
height: props.meta.height,
},
})
},
random(150, 1500),
)
onMounted(() => {
start()
})
</script><template>
<view class="relative box-border" :style="{ paddingTop }">
<view class="absolute inset-0 flex justify-center items-center sbg-fourth">
<text>{{ meta.width }}</text>
<text>x</text>
<text>{{ meta.height }}</text>
</view>
</view>
</template>
<script setup lang="js">
import { random, useTimeout } from "sard-uniapp";
import { computed, onMounted, ref } from "vue";
const props = defineProps();
const emit = defineEmits();
const internalWidth = ref(320);
const internalHeight = ref(240);
const currWidth = computed(() => internalWidth.value);
const currHeight = computed(() => internalHeight.value);
const paddingTop = computed(
() => currHeight.value / currWidth.value * 100 + "%"
);
const { start } = useTimeout(
() => {
internalWidth.value = props.meta.width;
internalHeight.value = props.meta.height;
emit("load", {
detail: {
width: props.meta.width,
height: props.meta.height
}
});
},
random(150, 1500)
);
onMounted(() => {
start();
});
</script>已知宽高
如果接口已将图片宽高信息返回,可以使用 WaterfallLoad 组件处理 onLoad 回调,会在挂载时调用,无需等待图片加载完即可渲染,可极大提高用户体验。
WaterfallLoad 会根据 width、height 属性计算占用的空间。
<template>
<page-meta :page-style="isLocked ? 'overflow: hidden' : ''"></page-meta>
<doc-page title="已知宽高">
<sar-waterfall class="mx-32">
<sar-waterfall-item v-for="(item, index) in list" :key="index">
<template #default="{ onLoad }">
<sar-waterfall-load
:width="item.img.width"
:height="item.img.height"
@load="onLoad"
>
<SimulatedImage class="w-full h-full pt-0" :meta="item.img" />
</sar-waterfall-load>
<view class="mt-10">{{ item.title }}</view>
</template>
</sar-waterfall-item>
</sar-waterfall>
</doc-page>
</template>
<script setup lang="ts">
import { useCurrentPageLock, usePageTopPopup } from 'sard-uniapp'
import { onBackPress } from '@dcloudio/uni-app'
import { random } from 'sard-uniapp'
import { onMounted, ref } from 'vue'
import SimulatedImage from './SimulatedImage.vue'
import { text } from '../../read-more/demo/data'
interface ListItem {
title: string
img: {
width: number
height: number
}
}
const list = ref<ListItem[]>([])
const getData = () => {
return new Promise<ListItem[]>((resolve) => {
const data = Array(20)
.fill(0)
.map(() => {
const min = 20
const max = 50
const startIndex = random(0, text.length - max)
const length = random(min, max)
return {
title: text.slice(startIndex, startIndex + length),
img: {
width: random(100, 500),
height: random(100, 500),
},
}
})
resolve(data)
})
}
onMounted(async () => {
list.value.push(...(await getData()))
})
const { isLocked } = useCurrentPageLock()
const { shouldStopBack, hidePopup } = usePageTopPopup()
onBackPress(() => {
if (shouldStopBack.value) {
hidePopup()
return true
}
})
</script><template>
<page-meta :page-style="isLocked ? 'overflow: hidden' : ''"></page-meta>
<doc-page title="已知宽高">
<sar-waterfall class="mx-32">
<sar-waterfall-item v-for="(item, index) in list" :key="index">
<template #default="{ onLoad }">
<sar-waterfall-load
:width="item.img.width"
:height="item.img.height"
@load="onLoad"
>
<SimulatedImage class="w-full h-full pt-0" :meta="item.img" />
</sar-waterfall-load>
<view class="mt-10">{{ item.title }}</view>
</template>
</sar-waterfall-item>
</sar-waterfall>
</doc-page>
</template>
<script setup lang="js">
import { useCurrentPageLock, usePageTopPopup } from "sard-uniapp";
import { onBackPress } from "@dcloudio/uni-app";
import { random } from "sard-uniapp";
import { onMounted, ref } from "vue";
import { text } from "../../read-more/demo/data";
const list = ref([]);
const getData = () => {
return new Promise((resolve) => {
const data = Array(20).fill(0).map(() => {
const min = 20;
const max = 50;
const startIndex = random(0, text.length - max);
const length = random(min, max);
return {
title: text.slice(startIndex, startIndex + length),
img: {
width: random(100, 500),
height: random(100, 500)
}
};
});
resolve(data);
});
};
onMounted(async () => {
list.value.push(...await getData());
});
const { isLocked } = useCurrentPageLock();
const { shouldStopBack, hidePopup } = usePageTopPopup();
onBackPress(() => {
if (shouldStopBack.value) {
hidePopup();
return true;
}
});
</script>真实案例
需同时监听 image 组件的 load 和 error 事件,确保 onLoad 回调函数能调用,无论图片加载成功或失败。
<template>
<page-meta :page-style="isLocked ? 'overflow: hidden' : ''"></page-meta>
<doc-page title="真实案例">
<sar-waterfall class="mx-32" @load="onLoad">
<sar-waterfall-item v-for="(item, index) in list" :key="index">
<template #default="{ onLoad }">
<image
mode="widthFix"
class="flex w-full"
:src="item.url"
@load="onLoad"
@error="onLoad"
/>
<view class="mt-10 text-base">{{ item.title }}</view>
</template>
</sar-waterfall-item>
</sar-waterfall>
</doc-page>
</template>
<script setup lang="ts">
import { useCurrentPageLock, usePageTopPopup } from 'sard-uniapp'
import { onBackPress } from '@dcloudio/uni-app'
import { random, shuffle, toast } from 'sard-uniapp'
import { nextTick, onMounted, ref } from 'vue'
import { text } from '../../read-more/demo/data'
interface ListItem {
title: string
url: string
}
const list = ref<ListItem[]>([])
const getData = () => {
return new Promise<ListItem[]>((resolve) => {
const data = Array(20)
.fill(0)
.map((_, i) => {
const min = 20
const max = 50
const startIndex = random(0, text.length - max)
const length = random(min, max)
return {
title: text.slice(startIndex, startIndex + length),
url: `https://fastly.jsdelivr.net/npm/@sard/assets/images/cat${(i % 12) + 1}.jpg`,
}
})
resolve(data)
})
}
const onLoad = () => {
toast.hide()
}
onMounted(async () => {
nextTick(() => {
toast.loading('加载中')
})
list.value.push(...shuffle(await getData()))
})
const { isLocked } = useCurrentPageLock()
const { shouldStopBack, hidePopup } = usePageTopPopup()
onBackPress(() => {
if (shouldStopBack.value) {
hidePopup()
return true
}
})
</script><template>
<page-meta :page-style="isLocked ? 'overflow: hidden' : ''"></page-meta>
<doc-page title="真实案例">
<sar-waterfall class="mx-32" @load="onLoad">
<sar-waterfall-item v-for="(item, index) in list" :key="index">
<template #default="{ onLoad }">
<image
mode="widthFix"
class="flex w-full"
:src="item.url"
@load="onLoad"
@error="onLoad"
/>
<view class="mt-10 text-base">{{ item.title }}</view>
</template>
</sar-waterfall-item>
</sar-waterfall>
</doc-page>
</template>
<script setup lang="js">
import { useCurrentPageLock, usePageTopPopup } from "sard-uniapp";
import { onBackPress } from "@dcloudio/uni-app";
import { random, shuffle, toast } from "sard-uniapp";
import { nextTick, onMounted, ref } from "vue";
import { text } from "../../read-more/demo/data";
const list = ref([]);
const getData = () => {
return new Promise((resolve) => {
const data = Array(20).fill(0).map((_, i) => {
const min = 20;
const max = 50;
const startIndex = random(0, text.length - max);
const length = random(min, max);
return {
title: text.slice(startIndex, startIndex + length),
url: `https://fastly.jsdelivr.net/npm/@sard/assets/images/cat${i % 12 + 1}.jpg`
};
});
resolve(data);
});
};
const onLoad = () => {
toast.hide();
};
onMounted(async () => {
nextTick(() => {
toast.loading("\u52A0\u8F7D\u4E2D");
});
list.value.push(...shuffle(await getData()));
});
const { isLocked } = useCurrentPageLock();
const { shouldStopBack, hidePopup } = usePageTopPopup();
onBackPress(() => {
if (shouldStopBack.value) {
hidePopup();
return true;
}
});
</script>最大等待时间
为了避免因图片太大导致渲染阻塞,WaterfallLoad 组件提供了 max-wait 属性,在超过等待时间后取自定义的宽高进行渲染,无需等待图片加载完;加载时间小于等待时间时,则取图片实际的宽高进行渲染。
宽高只用于计算比例来进行等比缩放,而不是最终的展示尺寸。
<template>
<page-meta :page-style="isLocked ? 'overflow: hidden' : ''"></page-meta>
<doc-page title="最大等待时间">
<sar-waterfall class="mx-32" @load="onLoad">
<sar-waterfall-item v-for="(item, index) in list" :key="index">
<template #default="{ onLoad }">
<sar-waterfall-load
:width="100"
:height="100"
:max-wait="300"
@load="onLoad"
>
<template #default="{ onLoad, overtime }">
<image
mode="aspectFill"
class="flex w-full h-full"
:src="item.url"
@load="onLoad"
@error="onLoad"
/>
<sar-tag
v-if="overtime"
theme="danger"
mark="right"
class="absolute top-0 left-0"
>
超时
</sar-tag>
</template>
</sar-waterfall-load>
<view class="mt-10 text-base">{{ item.title }}</view>
</template>
</sar-waterfall-item>
</sar-waterfall>
</doc-page>
</template>
<script setup lang="ts">
import { useCurrentPageLock, usePageTopPopup } from 'sard-uniapp'
import { onBackPress } from '@dcloudio/uni-app'
import { random, shuffle, toast } from 'sard-uniapp'
import { nextTick, onMounted, ref } from 'vue'
import { text } from '../../read-more/demo/data'
interface ListItem {
title: string
url: string
}
const list = ref<ListItem[]>([])
const getData = () => {
return new Promise<ListItem[]>((resolve) => {
const data = Array(20)
.fill(0)
.map((_, i) => {
const min = 20
const max = 50
const startIndex = random(0, text.length - max)
const length = random(min, max)
return {
title: text.slice(startIndex, startIndex + length),
url: `https://fastly.jsdelivr.net/npm/@sard/assets/images/cat${(i % 12) + 1}.jpg`,
}
})
resolve(data)
})
}
const onLoad = () => {
toast.hide()
}
onMounted(async () => {
nextTick(() => {
toast.loading('加载中')
})
list.value.push(...shuffle(await getData()))
})
const { isLocked } = useCurrentPageLock()
const { shouldStopBack, hidePopup } = usePageTopPopup()
onBackPress(() => {
if (shouldStopBack.value) {
hidePopup()
return true
}
})
</script><template>
<page-meta :page-style="isLocked ? 'overflow: hidden' : ''"></page-meta>
<doc-page title="最大等待时间">
<sar-waterfall class="mx-32" @load="onLoad">
<sar-waterfall-item v-for="(item, index) in list" :key="index">
<template #default="{ onLoad }">
<sar-waterfall-load
:width="100"
:height="100"
:max-wait="300"
@load="onLoad"
>
<template #default="{ onLoad, overtime }">
<image
mode="aspectFill"
class="flex w-full h-full"
:src="item.url"
@load="onLoad"
@error="onLoad"
/>
<sar-tag
v-if="overtime"
theme="danger"
mark="right"
class="absolute top-0 left-0"
>
超时
</sar-tag>
</template>
</sar-waterfall-load>
<view class="mt-10 text-base">{{ item.title }}</view>
</template>
</sar-waterfall-item>
</sar-waterfall>
</doc-page>
</template>
<script setup lang="js">
import { useCurrentPageLock, usePageTopPopup } from "sard-uniapp";
import { onBackPress } from "@dcloudio/uni-app";
import { random, shuffle, toast } from "sard-uniapp";
import { nextTick, onMounted, ref } from "vue";
import { text } from "../../read-more/demo/data";
const list = ref([]);
const getData = () => {
return new Promise((resolve) => {
const data = Array(20).fill(0).map((_, i) => {
const min = 20;
const max = 50;
const startIndex = random(0, text.length - max);
const length = random(min, max);
return {
title: text.slice(startIndex, startIndex + length),
url: `https://fastly.jsdelivr.net/npm/@sard/assets/images/cat${i % 12 + 1}.jpg`
};
});
resolve(data);
});
};
const onLoad = () => {
toast.hide();
};
onMounted(async () => {
nextTick(() => {
toast.loading("\u52A0\u8F7D\u4E2D");
});
list.value.push(...shuffle(await getData()));
});
const { isLocked } = useCurrentPageLock();
const { shouldStopBack, hidePopup } = usePageTopPopup();
onBackPress(() => {
if (shouldStopBack.value) {
hidePopup();
return true;
}
});
</script>大图
可使用 column-gap 和 row-gap 设置瀑布流项的间隔大小,单位为 px。
<template>
<page-meta :page-style="isLocked ? 'overflow: hidden' : ''"></page-meta>
<doc-page title="大图">
<sar-waterfall class="mx-16" :column-gap="4" :row-gap="4" @load="onLoad">
<sar-waterfall-item v-for="(item, index) in list" :key="index">
<template #default="{ onLoad }">
<image
mode="widthFix"
class="flex w-full"
:src="item.url"
@load="onLoad"
@error="onLoad"
/>
</template>
</sar-waterfall-item>
</sar-waterfall>
</doc-page>
</template>
<script setup lang="ts">
import { useCurrentPageLock, usePageTopPopup } from 'sard-uniapp'
import { onBackPress } from '@dcloudio/uni-app'
import { shuffle, toast } from 'sard-uniapp'
import { nextTick, onMounted, ref } from 'vue'
interface ListItem {
url: string
}
const list = ref<ListItem[]>([])
const getData = () => {
return new Promise<ListItem[]>((resolve) => {
const data = Array(19)
.fill(0)
.map((_, i) => {
return {
url: `https://fastly.jsdelivr.net/npm/@sard/assets/images/tiger${(i % 12) + 1}.jpg`,
}
})
resolve(data)
})
}
const onLoad = () => {
toast.hide()
}
onMounted(async () => {
nextTick(() => {
toast.loading('加载中')
})
list.value.push(...shuffle(await getData()))
})
const { isLocked } = useCurrentPageLock()
const { shouldStopBack, hidePopup } = usePageTopPopup()
onBackPress(() => {
if (shouldStopBack.value) {
hidePopup()
return true
}
})
</script><template>
<page-meta :page-style="isLocked ? 'overflow: hidden' : ''"></page-meta>
<doc-page title="大图">
<sar-waterfall class="mx-16" :column-gap="4" :row-gap="4" @load="onLoad">
<sar-waterfall-item v-for="(item, index) in list" :key="index">
<template #default="{ onLoad }">
<image
mode="widthFix"
class="flex w-full"
:src="item.url"
@load="onLoad"
@error="onLoad"
/>
</template>
</sar-waterfall-item>
</sar-waterfall>
</doc-page>
</template>
<script setup lang="js">
import { useCurrentPageLock, usePageTopPopup } from "sard-uniapp";
import { onBackPress } from "@dcloudio/uni-app";
import { shuffle, toast } from "sard-uniapp";
import { nextTick, onMounted, ref } from "vue";
const list = ref([]);
const getData = () => {
return new Promise((resolve) => {
const data = Array(19).fill(0).map((_, i) => {
return {
url: `https://fastly.jsdelivr.net/npm/@sard/assets/images/tiger${i % 12 + 1}.jpg`
};
});
resolve(data);
});
};
const onLoad = () => {
toast.hide();
};
onMounted(async () => {
nextTick(() => {
toast.loading("\u52A0\u8F7D\u4E2D");
});
list.value.push(...shuffle(await getData()));
});
const { isLocked } = useCurrentPageLock();
const { shouldStopBack, hidePopup } = usePageTopPopup();
onBackPress(() => {
if (shouldStopBack.value) {
hidePopup();
return true;
}
});
</script>自定义列数
可使用 columns 属性设置任意的列数。
<template>
<page-meta :page-style="isLocked ? 'overflow: hidden' : ''"></page-meta>
<doc-page title="自定义列数">
<sar-slider v-model="columns" class="m-30" show-scale :min="1" :max="8" />
<sar-waterfall
class="mx-16"
:columns="columns"
:column-gap="4"
:row-gap="4"
@load="onLoad"
>
<sar-waterfall-item v-for="(item, index) in list" :key="index">
<template #default="{ onLoad }">
<image
mode="widthFix"
class="flex w-full"
:src="item.url"
@load="onLoad"
@error="onLoad"
/>
</template>
</sar-waterfall-item>
</sar-waterfall>
</doc-page>
</template>
<script setup lang="ts">
import { useCurrentPageLock, usePageTopPopup } from 'sard-uniapp'
import { onBackPress } from '@dcloudio/uni-app'
import { shuffle, toast } from 'sard-uniapp'
import { nextTick, onMounted, ref } from 'vue'
const columns = ref(3)
interface ListItem {
url: string
}
const list = ref<ListItem[]>([])
const getData = () => {
return new Promise<ListItem[]>((resolve) => {
const data = Array(30)
.fill(0)
.map((_, i) => {
return {
url: `https://fastly.jsdelivr.net/npm/@sard/assets/images/tiger${(i % 12) + 1}.jpg`,
}
})
resolve(data)
})
}
const onLoad = () => {
toast.hide()
}
onMounted(async () => {
nextTick(() => {
toast.loading('加载中')
})
list.value.push(...shuffle(await getData()))
})
const { isLocked } = useCurrentPageLock()
const { shouldStopBack, hidePopup } = usePageTopPopup()
onBackPress(() => {
if (shouldStopBack.value) {
hidePopup()
return true
}
})
</script><template>
<page-meta :page-style="isLocked ? 'overflow: hidden' : ''"></page-meta>
<doc-page title="自定义列数">
<sar-slider v-model="columns" class="m-30" show-scale :min="1" :max="8" />
<sar-waterfall
class="mx-16"
:columns="columns"
:column-gap="4"
:row-gap="4"
@load="onLoad"
>
<sar-waterfall-item v-for="(item, index) in list" :key="index">
<template #default="{ onLoad }">
<image
mode="widthFix"
class="flex w-full"
:src="item.url"
@load="onLoad"
@error="onLoad"
/>
</template>
</sar-waterfall-item>
</sar-waterfall>
</doc-page>
</template>
<script setup lang="js">
import { useCurrentPageLock, usePageTopPopup } from "sard-uniapp";
import { onBackPress } from "@dcloudio/uni-app";
import { shuffle, toast } from "sard-uniapp";
import { nextTick, onMounted, ref } from "vue";
const columns = ref(3);
const list = ref([]);
const getData = () => {
return new Promise((resolve) => {
const data = Array(30).fill(0).map((_, i) => {
return {
url: `https://fastly.jsdelivr.net/npm/@sard/assets/images/tiger${i % 12 + 1}.jpg`
};
});
resolve(data);
});
};
const onLoad = () => {
toast.hide();
};
onMounted(async () => {
nextTick(() => {
toast.loading("\u52A0\u8F7D\u4E2D");
});
list.value.push(...shuffle(await getData()));
});
const { isLocked } = useCurrentPageLock();
const { shouldStopBack, hidePopup } = usePageTopPopup();
onBackPress(() => {
if (shouldStopBack.value) {
hidePopup();
return true;
}
});
</script>结合下拉刷新与触底加载
可配合 LoadMore 和 PullDownRefresh 组件实现数据动态增减。
因瀑布流在数据加载完后仍需等待图片加载,因此提供了 onLoad 方法,传递到此方法中的函数会在图片加载完后调用,可用于取消加载状态。
<template>
<page-meta :page-style="isLocked ? 'overflow: hidden' : ''"></page-meta>
<doc-page title="结合下拉刷新与触底加载">
<sar-pull-down-refresh
ref="pullDownRefresh"
:loading="refreshing"
:disabled="loadMoreStatus === 'loading'"
@refresh="onRefresh"
>
<sar-waterfall
ref="waterfallRef"
class="mx-16"
:column-gap="4"
:row-gap="4"
>
<sar-waterfall-item v-for="item in list" :key="item.id">
<template #default="{ onLoad }">
<image
mode="widthFix"
class="flex w-full"
:src="item.url"
@load="onLoad"
@error="onLoad"
/>
<sar-button
class="absolute top-10 right-10"
size="small"
theme="danger"
@click="onDelete(item)"
>
删除
</sar-button>
</template>
</sar-waterfall-item>
</sar-waterfall>
<sar-load-more
:status="loadMoreStatus"
@load-more="onLoadMore"
@reload="onReload"
/>
</sar-pull-down-refresh>
</doc-page>
</template>
<script setup lang="ts">
import { useCurrentPageLock, usePageTopPopup } from 'sard-uniapp'
import { onBackPress } from '@dcloudio/uni-app'
import { onPageScroll, onReachBottom } from '@dcloudio/uni-app'
import {
shuffle,
sleep,
toast,
WaterfallExpose,
type LoadMoreStatus,
} from 'sard-uniapp'
import { onMounted, ref } from 'vue'
// api
interface ListItem {
id: number
url: string
}
const currentPage = ref(1)
const list = ref<ListItem[]>([])
let uuid = 1
const fetchApi = async (page: number) => {
await sleep(300)
const getList = () =>
Array(12)
.fill(0)
.map((_, i) => {
return {
id: uuid++,
url: `https://fastly.jsdelivr.net/npm/@sard/assets/images/tiger${(i % 12) + 1}.jpg`,
}
})
return {
page,
total: 3,
list: page > 3 ? [] : shuffle(getList()),
}
}
// waterfall
const waterfallRef = ref<WaterfallExpose>()
// 下拉刷新
const refreshing = ref(false)
const pullDownRefresh = ref()
onPageScroll(({ scrollTop }) => {
pullDownRefresh.value?.enableToRefresh(scrollTop === 0)
})
const onRefresh = () => {
refreshing.value = true
fetchApi(1)
.then((res) => {
currentPage.value = 1
list.value = []
setTimeout(() => {
list.value = res.list
loadMoreStatus.value = 'loading'
waterfallRef.value?.onLoad(() => {
setTimeout(() => {
loadMoreStatus.value =
res.page < res.total ? 'incomplete' : 'complete'
}, 300)
})
toast('刷新成功')
})
})
.catch(() => {
toast('刷新失败')
})
.finally(() => {
refreshing.value = false
})
}
// 加载更多
const loadMoreStatus = ref<LoadMoreStatus>('incomplete')
const loadMoreFetch = (page: number) => {
loadMoreStatus.value = 'loading'
fetchApi(page)
.then((res) => {
list.value = [...list.value, ...res.list]
waterfallRef.value?.onLoad(() => {
setTimeout(() => {
loadMoreStatus.value =
res.page < res.total ? 'incomplete' : 'complete'
}, 300)
})
})
.catch(() => {
loadMoreStatus.value = 'error'
})
}
const onLoadMore = () => {
if (!refreshing.value) {
loadMoreFetch(++currentPage.value)
}
}
const onReload = () => {
if (!refreshing.value) {
loadMoreFetch(currentPage.value)
}
}
onReachBottom(() => {
if (!refreshing.value && loadMoreStatus.value === 'incomplete') {
loadMoreFetch(++currentPage.value)
}
})
onMounted(() => {
loadMoreFetch(currentPage.value)
})
// 删除
const onDelete = (item: ListItem) => {
list.value.splice(list.value.indexOf(item), 1)
}
const { isLocked } = useCurrentPageLock()
const { shouldStopBack, hidePopup } = usePageTopPopup()
onBackPress(() => {
if (shouldStopBack.value) {
hidePopup()
return true
}
})
</script><template>
<page-meta :page-style="isLocked ? 'overflow: hidden' : ''"></page-meta>
<doc-page title="结合下拉刷新与触底加载">
<sar-pull-down-refresh
ref="pullDownRefresh"
:loading="refreshing"
:disabled="loadMoreStatus === 'loading'"
@refresh="onRefresh"
>
<sar-waterfall
ref="waterfallRef"
class="mx-16"
:column-gap="4"
:row-gap="4"
>
<sar-waterfall-item v-for="item in list" :key="item.id">
<template #default="{ onLoad }">
<image
mode="widthFix"
class="flex w-full"
:src="item.url"
@load="onLoad"
@error="onLoad"
/>
<sar-button
class="absolute top-10 right-10"
size="small"
theme="danger"
@click="onDelete(item)"
>
删除
</sar-button>
</template>
</sar-waterfall-item>
</sar-waterfall>
<sar-load-more
:status="loadMoreStatus"
@load-more="onLoadMore"
@reload="onReload"
/>
</sar-pull-down-refresh>
</doc-page>
</template>
<script setup lang="js">
import { useCurrentPageLock, usePageTopPopup } from "sard-uniapp";
import { onBackPress } from "@dcloudio/uni-app";
import { onPageScroll, onReachBottom } from "@dcloudio/uni-app";
import {
shuffle,
sleep,
toast
} from "sard-uniapp";
import { onMounted, ref } from "vue";
const currentPage = ref(1);
const list = ref([]);
let uuid = 1;
const fetchApi = async (page) => {
await sleep(300);
const getList = () => Array(12).fill(0).map((_, i) => {
return {
id: uuid++,
url: `https://fastly.jsdelivr.net/npm/@sard/assets/images/tiger${i % 12 + 1}.jpg`
};
});
return {
page,
total: 3,
list: page > 3 ? [] : shuffle(getList())
};
};
const waterfallRef = ref();
const refreshing = ref(false);
const pullDownRefresh = ref();
onPageScroll(({ scrollTop }) => {
pullDownRefresh.value?.enableToRefresh(scrollTop === 0);
});
const onRefresh = () => {
refreshing.value = true;
fetchApi(1).then((res) => {
currentPage.value = 1;
list.value = [];
setTimeout(() => {
list.value = res.list;
loadMoreStatus.value = "loading";
waterfallRef.value?.onLoad(() => {
setTimeout(() => {
loadMoreStatus.value = res.page < res.total ? "incomplete" : "complete";
}, 300);
});
toast("\u5237\u65B0\u6210\u529F");
});
}).catch(() => {
toast("\u5237\u65B0\u5931\u8D25");
}).finally(() => {
refreshing.value = false;
});
};
const loadMoreStatus = ref("incomplete");
const loadMoreFetch = (page) => {
loadMoreStatus.value = "loading";
fetchApi(page).then((res) => {
list.value = [...list.value, ...res.list];
waterfallRef.value?.onLoad(() => {
setTimeout(() => {
loadMoreStatus.value = res.page < res.total ? "incomplete" : "complete";
}, 300);
});
}).catch(() => {
loadMoreStatus.value = "error";
});
};
const onLoadMore = () => {
if (!refreshing.value) {
loadMoreFetch(++currentPage.value);
}
};
const onReload = () => {
if (!refreshing.value) {
loadMoreFetch(currentPage.value);
}
};
onReachBottom(() => {
if (!refreshing.value && loadMoreStatus.value === "incomplete") {
loadMoreFetch(++currentPage.value);
}
});
onMounted(() => {
loadMoreFetch(currentPage.value);
});
const onDelete = (item) => {
list.value.splice(list.value.indexOf(item), 1);
};
const { isLocked } = useCurrentPageLock();
const { shouldStopBack, hidePopup } = usePageTopPopup();
onBackPress(() => {
if (shouldStopBack.value) {
hidePopup();
return true;
}
});
</script>API
WaterfallProps
| 属性 | 描述 | 类型 | 默认值 |
|---|---|---|---|
| root-class | 组件根元素类名 | string | - |
| root-style | 组件根元素样式 | StyleValue | - |
| columns | 自定义列数 | number | 2 |
| column-gap | 列间距,单位px | number | 16 |
| row-gap | 行间距,单位px | number | 16 |
WaterfallSlots
| 插槽 | 描述 | 属性 |
|---|---|---|
| default | 自定义默认内容 | - |
WaterfallEmits
| 事件 | 描述 | 类型 |
|---|---|---|
| load | 所有瀑布流项加载完时触发 | () => void |
| loadstart | 瀑布流项项开始加载时触发 | () => void |
WaterfallExpose
| 属性 | 描述 | 类型 |
|---|---|---|
| reflow | 重新排版 | () => void |
| onLoad | 添加回调,会在所有项加载完时调用 | (handler: () => void) => void |
WaterfallItemProps
| 属性 | 描述 | 类型 | 默认值 |
|---|---|---|---|
| root-class | 组件根元素类名 | string | - |
| root-style | 组件根元素样式 | StyleValue | - |
WaterfallItemSlots
| 插槽 | 描述 | 属性 |
|---|---|---|
| default | 自定义默认内容 | { onLoad: () => void; columnWidth: number } |
default 插槽属性说明:
onLoad: 图片加载完后(无论成功或失败)需要调用此方法。columnWidth: 列宽。
WaterfallLoadProps
| 属性 | 描述 | 类型 | 默认值 |
|---|---|---|---|
| root-class | 组件根元素类名 | string | - |
| root-style | 组件根元素样式 | StyleValue | - |
| max-wait | 最大等待时间,单位ms | number | 0 |
| width | 自定义宽度 | number | 320 |
| height | 自定义高度 | number | 240 |
WaterfallLoadSlots
| 插槽 | 描述 | 属性 |
|---|---|---|
| default | 自定义默认内容 | { onLoad: (event: any) => void; overtime: boolean } |
default 插槽属性说明:
onLoad: 图片加载完后(成功和失败)可以调用此方法,加载完时间比max-wait要大则超时。overtime: 是否超时。
WaterfallEmits
| 事件 | 描述 | 类型 |
|---|---|---|
| load | 加载完时触发,无论是正常加载完,还是超时 | () => void |
主题定制
CSS 变量
page,
.sar-portal {
--sar-waterfall-duration: var(--sar-duration);
}