Files
schisandra-cloud-album-front/src/components/MyUI/Waterfall/Waterfall.vue
landaiqing 33d76461f1 refine MyUI
2024-11-07 21:39:16 +08:00

183 lines
5.8 KiB
Vue
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.

<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue';
import Spin from '../Spin/Spin.vue';
import { useResizeObserver } from '../Utils';
/*
宽度固定图片等比例缩放使用JS获取每张图片宽度和高度结合 `relative` 和 `absolute` 定位
计算每个图片的位置 `top``left`,保证每张新的图片都追加在当前高度最小的那列末尾
*/
interface Image {
name?: string // 图片名称
src: string // 图片地址
}
interface Props {
images?: Image[] // 图片数组
columnCount?: number // 要划分的列数
columnGap?: number // 各列之间的间隙,单位 px
width?: string | number // 瀑布流区域的总宽度,单位 px
borderRadius?: number // 瀑布流区域和图片圆角,单位 px
backgroundColor?: string // 瀑布流区域背景填充色
spinProps?: object // Spin 组件属性配置,参考 Spin Props用于配置图片加载中样式
}
const props = withDefaults(defineProps<Props>(), {
images: () => [],
columnCount: 3,
columnGap: 20,
width: '100%',
borderRadius: 8,
backgroundColor: '#F2F4F8',
spinProps: () => ({})
});
const waterfallRef = ref();
const waterfallWidth = ref<number>();
const loaded = ref(Array(props.images.length).fill(false)); // 图片是否加载完成
const imageWidth = ref<number>();
const imagesProperty = ref<{ width: number; height: number; top: number; left: number }[]>([]);
const preColumnHeight = ref<number[]>(Array(props.columnCount).fill(0)); // 每列的高度
const flag = ref(0);
const totalWidth = computed(() => {
if (typeof props.width === 'number') {
return `${props.width}px`;
} else {
return props.width;
}
});
const height = computed(() => {
return Math.max(...preColumnHeight.value) + props.columnGap;
});
const len = computed(() => {
return props.images.length;
});
watch(
() => [props.images, props.columnCount, props.columnGap, props.width],
() => {
waterfallWidth.value = waterfallRef.value.offsetWidth;
preColumnHeight.value = Array(props.columnCount).fill(0);
flag.value++;
preloadImages(flag.value);
},
{
deep: true, // 强制转成深层侦听器
flush: 'post' // 在侦听器回调中访问被 Vue 更新之后的 DOM
}
);
onMounted(() => {
waterfallWidth.value = waterfallRef.value.offsetWidth;
preloadImages(flag.value);
});
function updateWatefall() {
const currentWidth = waterfallRef.value.offsetWidth;
// 窗口宽度改变时重新计算瀑布流布局
if (props.images.length && currentWidth !== waterfallWidth.value) {
waterfallWidth.value = currentWidth;
flag.value++;
preloadImages(flag.value);
}
}
useResizeObserver(waterfallRef, updateWatefall);
async function preloadImages(symbol: number) {
// 计算图片宽高和位置topleft
// 计算每列的图片宽度
imageWidth.value = ((waterfallWidth.value as number) - (props.columnCount + 1) * props.columnGap) / props.columnCount;
imagesProperty.value.splice(0);
for (let i = 0; i < len.value; i++) {
if (symbol === flag.value) {
await loadImage(props.images[i].src, i);
} else {
return false;
}
}
}
function loadImage(url: string, n: number) {
return new Promise((resolve) => {
const image = new Image();
image.src = url;
image.onload = function () {
// 图片加载完成时执行此时可通过image.width和image.height获取到图片原始宽高
const height = image.height / (image.width / (imageWidth.value as number));
imagesProperty.value[n] = {
// 存储图片宽高和位置信息
width: imageWidth.value as number,
height: height,
...getPosition(n, height)
};
resolve('load');
};
});
}
function getPosition(i: number, height: number) {
// 获取图片位置信息topleft
if (i < props.columnCount) {
preColumnHeight.value[i] = props.columnGap + height;
return {
top: props.columnGap,
left: ((imageWidth.value as number) + props.columnGap) * i + props.columnGap
};
} else {
const top = Math.min(...preColumnHeight.value);
let index = 0;
for (let n = 0; n < props.columnCount; n++) {
if (preColumnHeight.value[n] === top) {
index = n;
break;
}
}
preColumnHeight.value[index] = top + props.columnGap + height;
return {
top: top + props.columnGap,
left: ((imageWidth.value as number) + props.columnGap) * index + props.columnGap
};
}
}
function onLoaded(index: number) {
loaded.value[index] = true;
}
function getImageName(image: Image) {
// 从图像地址src中获取图像名称
if (image) {
if (image.name) {
return image.name;
} else {
const res = image.src.split('?')[0].split('/');
return res[res.length - 1];
}
}
}
</script>
<template>
<div
ref="waterfallRef"
class="m-waterfall"
:style="`--border-radius: ${borderRadius}px; background-color: ${backgroundColor}; width: ${totalWidth}; height: ${height}px;`"
>
<Spin
class="waterfall-image"
:style="`width: ${property.width}px; height: ${property.height}px; top: ${property && property.top}px; left: ${property && property.left}px;`"
:spinning="!loaded[index]"
size="small"
indicator="dynamic-circle"
v-bind="spinProps"
v-for="(property, index) in imagesProperty"
:key="index"
>
<img class="u-image" :src="images[index].src" :alt="getImageName(images[index])" @load="onLoaded(index)" />
</Spin>
</div>
</template>
<style lang="less" scoped>
.m-waterfall {
position: relative;
border-radius: var(--border-radius);
.waterfall-image {
position: absolute;
.u-image {
width: 100%;
height: 100%;
border-radius: var(--border-radius);
display: inline-block;
vertical-align: bottom;
}
}
}
</style>