254 lines
5.2 KiB
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>
|