Files
blog/src/lib/components/Waterfall.vue

254 lines
5.2 KiB
Vue

<template>
<div ref="waterfallWrapper" class="waterfall-list" :style="wrapperStyle">
<div
v-for="(item, index) in list"
:key="getKey(item, index)"
:style="getItemStyle(index)"
class="waterfall-item"
>
<div class="waterfall-card h-full">
<slot name="item" :item="item" :index="index" :url="getRenderURL(item)" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useDebounceFn } from "@vueuse/core";
import type { PropType } from "vue";
import { computed, provide, useTemplateRef, watch } from "vue";
import type { ViewCard } from "../types/waterfall";
import { useCalculateCols, useLayout } from "../use";
import { waterfallImageLoadedKey } from "../utils/keys";
import { getValue } from "../utils/util";
const props = defineProps({
list: {
type: Array as PropType<any[]>,
default: () => [],
},
rowKey: {
type: String,
default: "id",
},
imgSelector: {
type: String,
default: "src",
},
width: {
type: Number,
default: 200,
},
widthSelector: {
type: String,
default: "width",
},
heightSelector: {
type: String,
default: "height",
},
columns: {
type: Number,
default: 3,
},
gutter: {
type: Number,
default: 10,
},
hasAroundGutter: {
type: Boolean,
default: true,
},
animationPrefix: {
type: String,
default: "animate__animated",
},
animationEffect: {
type: String,
default: "fadeIn",
},
animationDuration: {
type: Number,
default: 1000,
},
animationDelay: {
type: Number,
default: 300,
},
backgroundColor: {
type: String,
default: "#fff",
},
lazyload: {
type: Boolean,
default: true,
},
loadProps: {
type: Object,
default: () => {},
},
crossOrigin: {
type: Boolean,
default: true,
},
delay: {
type: Number,
default: 300,
},
});
const waterfallWrapper = useTemplateRef<HTMLElement>("waterfallWrapper");
// 瀹瑰櫒鍧椾俊鎭?
const { wrapperWidth, colWidth, cols, offsetX } = useCalculateCols(
props,
waterfallWrapper
);
const getNumericValue = (item: ViewCard, selector: string): number | null => {
const value = getValue(item, selector)[0];
const resolved = Number(value);
if (!Number.isFinite(resolved) || resolved <= 0) return null;
return resolved;
};
const getItemHeightByIndex = (index: number): number | null => {
const item = props.list[index];
if (!item || colWidth.value <= 0) return null;
const width = getNumericValue(item, props.widthSelector);
const height = getNumericValue(item, props.heightSelector);
if (!width || !height) return null;
return (colWidth.value * height) / width;
};
const itemHeights = computed(() =>
props.list.map((_, index) => getItemHeightByIndex(index))
);
// 瀹瑰櫒楂樺害锛屽潡瀹氫綅
const { wrapperHeight, layoutHandle } = useLayout(
props,
colWidth,
cols,
offsetX,
waterfallWrapper,
(index, item) => getItemHeightByIndex(index) ?? item.offsetHeight
);
const wrapperStyle = computed(() => ({
height: `${wrapperHeight.value}px`,
backgroundColor: props.backgroundColor,
}));
const getItemStyle = (index: number) => {
const height = itemHeights.value[index];
if (!height) return undefined;
return {
height: `${height}px`,
};
};
// 1s鍐呮渶澶氭墽琛屼竴娆℃帓鐗堬紝鍑忓皯鎬ц兘寮€閿€
const renderer = useDebounceFn(() => {
layoutHandle();
}, props.delay);
watch(
[wrapperWidth, cols, itemHeights, () => props.list],
() => {
renderer();
},
{ immediate: true }
);
// 鍥剧墖鍔犺浇瀹屾垚
provide(waterfallImageLoadedKey, renderer);
// 鏍规嵁閫夋嫨鍣ㄨ幏鍙栧浘鐗囧湴鍧€
const getRenderURL = (item: ViewCard): string => {
return String(getValue(item, props.imgSelector)[0] ?? "");
};
// 鑾峰彇鍞竴鍊?
const getKey = (item: ViewCard, index: number): string | number => {
return item[props.rowKey] ?? index;
};
const clearAndReload = () => {
layoutHandle();
};
defineExpose({
waterfallWrapper,
wrapperHeight,
getRenderURL,
getKey,
list: props.list,
backgroundColor: props.backgroundColor,
renderer,
clearAndReload,
});
</script>
<style scoped>
.waterfall-list {
width: 100%;
position: relative;
overflow: hidden;
--waterfall-radius: 22px;
--waterfall-border: rgba(255, 255, 255, 0.72);
--waterfall-surface: linear-gradient(
180deg,
rgba(255, 255, 255, 0.9),
rgba(244, 250, 255, 0.72)
);
--waterfall-shadow: 0 16px 32px rgba(118, 144, 169, 0.12);
}
.waterfall-item {
position: absolute;
left: 0;
top: 0;
transform: translate3d(0, 3000px, 0);
visibility: hidden;
will-change: transform, opacity;
}
.waterfall-card {
height: 100%;
overflow: hidden;
border: 1px solid var(--waterfall-border);
border-radius: var(--waterfall-radius);
background: var(--waterfall-surface);
box-shadow: var(--waterfall-shadow);
backdrop-filter: blur(14px);
transform-origin: center top;
}
.waterfall-card :deep(img) {
display: block;
}
@keyframes fadeIn {
0% {
opacity: 0.01;
transform: translate3d(0, 18px, 0) scale(0.985);
}
100% {
opacity: 1;
transform: translate3d(0, 0, 0) scale(1);
}
}
.fadeIn {
animation-name: fadeIn;
}
</style>